不要怂,就是干,撸起袖子干!

Commit 4b4c8c3d by Jan Aagaard Meier

Merge pull request #2024 from sequelize/timezone

Allow the user to set a custom time zone
2 parents 88e751d3 03fb9a0f
......@@ -12,6 +12,7 @@ Notice: All 1.7.x changes are present in 2.0.x aswell
- [FEATURE] Extract CLI into [separate projects](https://github.com/sequelize/cli).
- [FEATURE] Sqlite now inserts dates with millisecond precision
- [FEATURE] Sequelize.VIRTUAL datatype which provides regular attribute functionality (set, get, etc) but never persists to database.
- [FEATURE] Added to option of setting a timezone offset in the sequelize constructor (`timezone` option). This timezone is used when initializing a connection (using `SET TIME ZONE` or equivalent), and when converting a timestamp string from the DB to a JS date with mysql (postgres stores the timezone, so for postgres we rely on what's in the DB).
- [BUG] An error is now thrown if an association would create a naming conflict between the association and the foreign key when doing eager loading. Closes [#1272](https://github.com/sequelize/sequelize/issues/1272)
- [BUG] Fix logging options for sequelize.sync
- [BUG] find no longer applies limit: 1 if querying on a primary key, should fix a lot of subquery issues.
......
......@@ -555,7 +555,7 @@ module.exports = (function() {
if (value && value._isSequelizeMethod) {
return value.toString(this);
} else {
return SqlString.escape(value, false, null, this.dialect, field);
return SqlString.escape(value, false, this.options.timezone, this.dialect, field);
}
},
......
......@@ -51,7 +51,7 @@ ConnectionManager.prototype.connect = function(config) {
});
}).tap(function (connection) {
connection.query("SET time_zone = '+0:00'");
connection.query("SET time_zone = '" + self.sequelize.options.timezone + "'");
});
};
ConnectionManager.prototype.disconnect = function(connection) {
......@@ -64,4 +64,4 @@ ConnectionManager.prototype.validate = function(connection) {
return connection && connection.state !== 'disconnected';
};
module.exports = ConnectionManager;
\ No newline at end of file
module.exports = ConnectionManager;
......@@ -66,7 +66,7 @@ module.exports = (function() {
case 'DATE':
case 'TIMESTAMP':
case 'DATETIME':
row[prop] = new Date(row[prop] + 'Z');
row[prop] = new Date(row[prop] + self.sequelize.options.timezone);
break;
case 'BIT':
case 'BLOB':
......
......@@ -27,7 +27,7 @@ ConnectionManager.prototype.connect = function(config) {
user: config.username,
password: config.password,
database: config.database,
timezone: 'Z'
timezone: self.sequelize.options.timezone
};
if (config.dialectOptions) {
......@@ -62,7 +62,7 @@ ConnectionManager.prototype.connect = function(config) {
});
}).tap(function (connection) {
connection.query("SET time_zone = '+0:00'");
connection.query("SET time_zone = '" + self.sequelize.options.timezone + "'");
});
};
ConnectionManager.prototype.disconnect = function(connection) {
......@@ -77,4 +77,4 @@ ConnectionManager.prototype.validate = function(connection) {
return connection && connection.state !== 'disconnected';
};
module.exports = ConnectionManager;
\ No newline at end of file
module.exports = ConnectionManager;
......@@ -60,7 +60,7 @@ ConnectionManager.prototype.connect = function(config) {
}).tap(function (connection) {
if (self.sequelize.config.keepDefaultTimezone) return;
return new Promise(function (resolve, reject) {
connection.query("SET TIME ZONE 'UTC'").on('error', function (err) {
connection.query("SET TIME ZONE INTERVAL '" + self.sequelize.options.timezone + "' HOUR TO MINUTE").on('error', function (err) {
reject(err);
}).on('end', function () {
resolve();
......@@ -75,4 +75,4 @@ ConnectionManager.prototype.disconnect = function(connection) {
});
};
module.exports = ConnectionManager;
\ No newline at end of file
module.exports = ConnectionManager;
......@@ -829,7 +829,7 @@ module.exports = (function() {
value = hstore.stringify(value);
}
return SqlString.escape(value, false, null, this.dialect, field);
return SqlString.escape(value, false, this.options.timezone, this.dialect, field);
},
/**
......
......@@ -21,6 +21,7 @@ var parseHstoreFields = function(model, row) {
});
};
module.exports = (function() {
var Query = function(client, sequelize, callee, options) {
this.client = client;
......
......@@ -75,9 +75,9 @@ module.exports = (function() {
var val = result[name];
if (val !== null) {
if (val.indexOf('+') === -1) {
if (val.indexOf('+') === -1) {
// For backwards compat. Dates inserted by sequelize < 2.0dev12 will not have a timestamp set
result[name] = new Date(val + 'Z'); // Z means UTC
result[name] = new Date(val + self.sequelize.options.timezone);
} else {
result[name] = new Date(val); // We already have a timezone stored in the string
}
......
......@@ -576,7 +576,8 @@ module.exports = (function() {
QueryInterface.prototype.rawSelect = function(tableName, options, attributeSelector, Model) {
var sql = this.QueryGenerator.selectQuery(tableName, options, Model)
, queryOptions = Utils._.extend({ transaction: options.transaction }, { plain: true, raw: true, type: QueryTypes.SELECT });
, queryOptions = Utils._.extend({ transaction: options.transaction }, { plain: true, raw: true, type: QueryTypes.SELECT })
, self = this;
if (attributeSelector === undefined) {
throw new Error('Please pass an attribute selector!');
......@@ -593,7 +594,7 @@ module.exports = (function() {
} else if (dataType === DataTypes.INTEGER || dataType instanceof DataTypes.BIGINT) {
result = parseInt(result, 10);
} else if (dataType === DataTypes.DATE) {
result = new Date(result + 'Z');
result = new Date(result + self.sequelize.options.timezone);
} else if (dataType === DataTypes.STRING) {
// Nothing to do, result is already a string.
}
......
......@@ -63,17 +63,18 @@ module.exports = (function() {
* @param {Object} [options.define={}] Default options for model definitions. See sequelize.define for options
* @param {Object} [options.query={}] Default options for sequelize.query
* @param {Object} [options.sync={}] Default options for sequelize.sync
* @param {String} [options.timezone='+00:00'] The timezone used when converting a date from the database into a javascript date. The timezone is also used to SET TIMEZONE when connecting to the server, to ensure that the result of NOW, CURRENT_TIMESTAMP and other time related functions have in the right timezone. For best cross platform performance use the format +/-HH:MM.
* @param {Function} [options.logging=console.log] A function that gets executed everytime Sequelize would log something.
* @param {Boolean} [options.omitNull=false] A flag that defines if null values should be passed to SQL queries or not.
* @param {Boolean} [options.queue=true] Queue queries, so that only maxConcurrentQueries number of queries are executing at once. If false, all queries will be executed immediately.
* @param {int} [options.maxConcurrentQueries=50] The maximum number of queries that should be executed at once if queue is true.
* @param {Integer} [options.maxConcurrentQueries=50] The maximum number of queries that should be executed at once if queue is true.
* @param {Boolean} [options.native=false] A flag that defines if native library shall be used or not. Currently only has an effect for postgres
* @param {Boolean} [options.replication=false] Use read / write replication. To enable replication, pass an object, with two properties, read and write. Write should be an object (a single server for handling writes), and read an array of object (several servers to handle reads). Each read/write server can have the following properties: `host`, `port`, `username`, `password`, `database`
* @param {Object} [options.pool={}] Should sequelize use a connection pool. Default is true
* @param {int} [options.pool.maxConnections]
* @param {int} [options.pool.minConnections]
* @param {int} [options.pool.maxIdleTime] The maximum time, in milliseconds, that a connection can be idle before being released
* @param {function} [options.pool.validateConnection] A function that validates a connection. Called with client. The default function checks that client is an object, and that its state is not disconnected
* @param {Integer} [options.pool.maxConnections]
* @param {Integer} [options.pool.minConnections]
* @param {Integer} [options.pool.maxIdleTime] The maximum time, in milliseconds, that a connection can be idle before being released
* @param {Function} [options.pool.validateConnection] A function that validates a connection. Called with client. The default function checks that client is an object, and that its state is not disconnected
* @param {Boolean} [options.quoteIdentifiers=true] Set to `false` to make table names and attributes case-insensitive on Postgres and skip double quoting of them.
*/
......@@ -120,6 +121,7 @@ module.exports = (function() {
define: {},
query: {},
sync: {},
timezone:'+00:00',
logging: console.log,
omitNull: false,
native: false,
......@@ -134,6 +136,10 @@ module.exports = (function() {
this.options.dialect = 'postgres';
}
if (this.options.dialect === 'sqlite' && this.options.timezone !== '+00:00') {
throw new Error('Setting a custom timezone is not supported by SQLite, dates are always returned as UTC. Please remove the custom timezone parameter.');
}
if (this.options.logging === true) {
console.log('DEPRECATION WARNING: The logging-option should be either a function or false. Default: console.log');
this.options.logging = console.log;
......
......@@ -159,36 +159,18 @@ SqlString.formatNamedParameters = function(sql, values, timeZone, dialect) {
};
SqlString.dateToString = function(date, timeZone, dialect) {
var dt = new Date(date);
date = moment(date).zone(timeZone);
// TODO: Ideally all dialects would work a bit more like this
if (dialect === 'postgres' || dialect === 'sqlite') {
return moment(dt).zone('+00:00').format('YYYY-MM-DD HH:mm:ss.SSS Z');
}
if (timeZone !== 'local') {
var tz = convertTimezone(timeZone);
dt.setTime(dt.getTime() + (dt.getTimezoneOffset() * 60000));
if (tz !== false) {
dt.setTime(dt.getTime() + (tz * 60000));
}
if (dialect === 'mysql' || dialect === 'mariadb') {
return date.format('YYYY-MM-DD HH:mm:ss');
} else {
// ZZ here means current timezone, _not_ UTC
return date.format('YYYY-MM-DD HH:mm:ss.SSS Z');
}
return moment(dt).format('YYYY-MM-DD HH:mm:ss');
};
SqlString.bufferToString = function(buffer, dialect) {
var hex = '';
try {
hex = buffer.toString('hex');
} catch (err) {
// node v0.4.x does not support hex / throws unknown encoding error
for (var i = 0; i < buffer.length; i++) {
var byte = buffer[i];
hex += zeroPad(byte.toString(16));
}
}
var hex = buffer.toString('hex');
if (dialect === 'postgres') {
// bytea hex format http://www.postgresql.org/docs/current/static/datatype-binary.html
......@@ -214,15 +196,3 @@ SqlString.objectToValues = function(object, timeZone) {
function zeroPad(number) {
return (number < 10) ? '0' + number : number;
}
function convertTimezone(tz) {
if (tz === 'Z') {
return 0;
}
var m = tz.match(/([\+\-\s])(\d\d):?(\d\d)?/);
if (m) {
return (m[1] === '-' ? -1 : 1) * (parseInt(m[2], 10) + ((m[3] ? parseInt(m[3], 10) : 0) / 60)) * 60;
}
return false;
}
......@@ -119,7 +119,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () {
return User.create({}).then(function (user) {
// Create the user first to set the proper default values. PG does not support column references in insert,
// so we must create a record with the right value for always_false, then reference it in an update
var now = dialect === 'sqlite' ? self.sequelize.fn('', self.sequelize.fn('date', 'now')) : self.sequelize.fn('NOW')
var now = dialect === 'sqlite' ? self.sequelize.fn('', self.sequelize.fn('datetime', 'now')) : self.sequelize.fn('NOW')
user.set({
d: now,
......@@ -292,7 +292,7 @@ describe(Support.getTestDialectTeaser("DAO"), function () {
var Contact = this.sequelize.define('Contact', {
first: { type: Sequelize.STRING },
last: { type: Sequelize.STRING },
tags: {
tags: {
type: Sequelize.STRING,
get: function(field) {
var val = this.getDataValue(field);
......@@ -402,10 +402,10 @@ describe(Support.getTestDialectTeaser("DAO"), function () {
name: 'Jan Meier'
})
user.set('name', 'Mick Hansen')
expect(user.previous('name')).to.equal('Jan Meier')
expect(user.get('name')).to.equal('Mick Hansen')
})
})
})
})
\ No newline at end of file
})
......@@ -978,4 +978,4 @@ if (dialect.match(/^postgres/)) {
})
})
})
}
\ No newline at end of file
}
"use strict";
var chai = require('chai')
, expect = chai.expect
, Support = require(__dirname + '/support')
, dialect = Support.getTestDialect()
, Transaction = require(__dirname + '/../lib/transaction')
, Sequelize = require(__dirname + '/../index')
, Promise = Sequelize.Promise
, sinon = require('sinon');
if (dialect !== 'sqlite') {
// Sqlite does not support setting timezone
describe(Support.getTestDialectTeaser('Timezone'), function () {
beforeEach(function () {
this.sequelizeWithTimezone = Support.createSequelizeInstance({
timezone: '+07:00',
dialect: dialect
});
});
it('returns the same value for current timestamp', function () {
var query = "SELECT now() as now";
return Promise.all([
this.sequelize.query(query),
this.sequelizeWithTimezone.query(query)
]).spread(function (now1, now2) {
expect(now1[0].now.getTime()).to.be.closeTo(now2[0].now.getTime(), 50);
});
});
if (Support.dialectIsMySQL()) {
it('handles existing timestamps', function () {
var NormalUser = this.sequelize.define('user', {})
, TimezonedUser = this.sequelizeWithTimezone.define('user', {});
return this.sequelize.sync({ force: true }).bind(this).then(function () {
return TimezonedUser.create({});
}).then(function (timezonedUser) {
this.timezonedUser = timezonedUser;
return NormalUser.find(timezonedUser.id);
}).then(function (normalUser) {
// Expect 7 hours difference, in milliseconds.
// This difference is expected since two instances, configured for each their timezone is trying to read the same timestamp
// this test does not apply to PG, since it stores the timezone along with the timestamp.
expect(normalUser.createdAt.getTime() - this.timezonedUser.createdAt.getTime()).to.be.closeTo(60 * 60 * 7 * 1000, 50);
});
});
}
});
}
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!