diff --git a/lib/protocol/SqlString.js b/lib/protocol/SqlString.js index 5cdec451b..74f764922 100644 --- a/lib/protocol/SqlString.js +++ b/lib/protocol/SqlString.js @@ -84,24 +84,34 @@ SqlString.format = function(sql, values, stringifyObjects, timeZone) { SqlString.dateToString = function(date, timeZone) { var dt = new Date(date); - if (timeZone != 'local') { + var year; + var month; + var day; + var hour; + var minute; + var second = dt.getUTCSeconds(); + var millisecond = dt.getUTCMilliseconds(); + + if (timeZone === 'local') { + year = dt.getFullYear(); + month = dt.getMonth() + 1; + day = dt.getDate(); + hour = dt.getHours(); + minute = dt.getMinutes(); + } else { var tz = convertTimezone(timeZone); - - dt.setTime(dt.getTime() + (dt.getTimezoneOffset() * 60000)); if (tz !== false) { dt.setTime(dt.getTime() + (tz * 60000)); } + year = dt.getUTCFullYear(); + month = dt.getUTCMonth() + 1; + day = dt.getUTCDate(); + hour = dt.getUTCHours(); + minute = dt.getUTCMinutes(); } - var year = dt.getFullYear(); - var month = zeroPad(dt.getMonth() + 1, 2); - var day = zeroPad(dt.getDate(), 2); - var hour = zeroPad(dt.getHours(), 2); - var minute = zeroPad(dt.getMinutes(), 2); - var second = zeroPad(dt.getSeconds(), 2); - var millisecond = zeroPad(dt.getMilliseconds(), 3); - - return year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second + '.' + millisecond; + return year + '-' + zeroPad(month, 2) + '-' + zeroPad(day, 2) + ' ' + + zeroPad(hour, 2) + ':' + zeroPad(minute, 2) + ':' + zeroPad(second, 2) + '.' + zeroPad(millisecond, 3); }; SqlString.bufferToString = function bufferToString(buffer) { diff --git a/package.json b/package.json index 0f8b405a1..5d7302dc6 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "istanbul": "0.3.9", "rimraf": "2.2.8", "require-all": "~1.1.0", + "timezone-mock": "~0.0.0", "mkdirp": "0.5.1", "urun": "0.0.8", "utest": "0.0.8" diff --git a/test/integration/connection/test-timezones.js b/test/integration/connection/test-timezones.js index f4fe53bcf..71af15794 100644 --- a/test/integration/connection/test-timezones.js +++ b/test/integration/connection/test-timezones.js @@ -1,8 +1,25 @@ +var timezone_mock = require('timezone-mock'); + var assert = require('assert'); var common = require('../../common'); +function registerMock() { + timezone_mock.register('US/Pacific'); + assert.ok(new Date().getTimezoneOffset() === 420 || new Date().getTimezoneOffset() === 480); +} + var table = 'timezone_test'; -var tests = [0, 1, 5, 12, 26, -1, -5, -20, 'Z', 'local']; +var pre_statements = ['', 'SET TIME_ZONE="+00:00"', 'SET TIME_ZONE="SYSTEM"', registerMock]; +var pre_idx = 0; +var test_days = ['01-01', '03-07', '03-08', '03-09', '12-31'].map(function (day) { + // Choosing this because 2015-03-08 02:30 Pacific does not exist (due to DST), + // so if anything is using a local date object it will fail (at least if the + // test system is in Pacific Time). + return '2015-' + day + 'T02:32:11.000Z'; +}); +var day_idx = 0; +var test_timezones = ['Z', 'local', 0, 1, 5, 12, 23, -1, -5, -20]; +var tz_idx = 0; common.getTestConnection(function (err, connection) { assert.ifError(err); @@ -11,61 +28,108 @@ common.getTestConnection(function (err, connection) { connection.query([ 'CREATE TEMPORARY TABLE ?? (', - '`offset` varchar(10),', + '`day` varchar(24),', + '`timezone` varchar(10),', + '`pre_idx` int,', '`dt` datetime', ') ENGINE=InnoDB DEFAULT CHARSET=utf8' ].join('\n'), [table], assert.ifError); + if (pre_statements[pre_idx]) { + connection.query(pre_statements[pre_idx], assert.ifError); + } testNextDate(connection); }); function testNextDate(connection) { - if (tests.length === 0) { - connection.end(assert.ifError); - return; + if (tz_idx === test_timezones.length || day_idx === test_days.length) { + ++pre_idx; + if (pre_idx === pre_statements.length) { + connection.end(assert.ifError); + return; + } else { + if (typeof pre_statements[pre_idx] === 'function') { + pre_statements[pre_idx](); + } else { + connection.query(pre_statements[pre_idx], assert.ifError); + } + day_idx = tz_idx = 0; + } } - var dt = new Date(); - var offset = tests.pop(); + var day = test_days[day_idx]; + var offset = test_timezones[tz_idx]; - // datetime will round fractional seconds up, which causes this test to fail - // depending on when it is executed. MySQL 5.6.4 and up supports datetime(6) - // which would not require this change. - // http://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html - dt.setSeconds(0); - dt.setMilliseconds(0); + ++day_idx; + if (day_idx === test_days.length) { + day_idx = 0; + ++tz_idx; + } - if (offset === 'Z' || offset === 'local') { - connection.config.timezone = offset; + var timezone; + if (offset === 'Z') { + timezone = offset; + offset = 0; + } else if (offset === 'local') { + timezone = offset; } else { - connection.config.timezone = (offset < 0 ? "-" : "+") + pad2(Math.abs(offset)) + ":00"; + timezone = (offset < 0 ? "-" : "+") + pad2(Math.abs(offset)) + ":00"; } - connection.query('INSERT INTO ?? SET ?', [table, {offset: offset, dt: dt}], assert.ifError); + var dt = new Date(day); + assert.strictEqual(day, dt.toISOString()); - if (offset === 'Z') { - dt.setTime(dt.getTime() + (dt.getTimezoneOffset() * 60000)); - } else if (offset !== 'local') { - dt.setTime(dt.getTime() + (dt.getTimezoneOffset() * 60000) + (offset * 3600000)); + var expected_date_string; + if (offset === 'local') { + // If using a local timezone, it should be the same day/hour/etc as the Javascript date formatter + // Beware Daylight Saving Time though, using a "local" timezone is never a good idea, it maps + // multiple unique UTC dates to the same string. + expected_date_string = dt.getFullYear() + '-' + pad2(dt.getMonth() + 1) + '-' + pad2(dt.getDate()) + ' ' + + pad2(dt.getHours()) + ':' + pad2(dt.getMinutes()) + ':' + pad2(dt.getSeconds()); + } else { + // If using a specific timezone, it should be a simple offset from the UTC date + var expected_dt = new Date(dt.getTime() + offset * 60*60*1000); + expected_date_string = expected_dt.getUTCFullYear() + '-' + + pad2(expected_dt.getUTCMonth() + 1) + '-' + + pad2(expected_dt.getUTCDate()) + ' ' + + pad2(expected_dt.getUTCHours()) + ':' + + pad2(expected_dt.getUTCMinutes()) + ':' + + pad2(expected_dt.getUTCSeconds()); } - dt.setSeconds(0); - dt.setMilliseconds(0); + connection.config.timezone = timezone; + connection.query('INSERT INTO ?? SET ?', [table, {day: day, timezone: timezone, dt: dt, pre_idx: pre_idx}], assert.ifError); var options = { - sql: 'SELECT * FROM ?? WHERE offset = ?', + sql: 'SELECT * FROM ?? WHERE timezone = ? AND day = ? AND pre_idx = ?', + values: [table, timezone, day, pre_idx], typeCast: function (field, next) { - if (field.type != 'DATETIME') return next(); - - return new Date(field.string()); + if (field.type !== 'DATETIME') { + return next(); + } + return field.string(); }, - values: [table, offset] }; - connection.query(options, function (err, rows) { + connection.query(options, function (err, rows_raw) { assert.ifError(err); - assert.strictEqual(dt.toString(), rows[0].dt.toString()); - testNextDate(connection); + assert.equal(rows_raw.length, 1); + delete options.typeCast; + connection.query(options, function (err, rows) { + assert.ifError(err); + assert.equal(rows.length, 1); + if (dt.getTime() !== rows[0].dt.getTime() || expected_date_string !== rows_raw[0].dt) { + console.log('Failure while testing date: ' + day + ', Timezone: ' + timezone); + console.log('Pre-statement: ' + pre_statements[pre_idx]); + console.log('Expected raw string: ' + expected_date_string); + console.log('Received raw string: ' + rows_raw[0].dt); + console.log('Expected date object: ' + dt.toISOString() + ' (' + dt.getTime() + ', ' + dt.toLocaleString() + ')'); + console.log('Received date object: ' + rows[0].dt.toISOString() + ' (' + rows[0].dt.getTime() + ', ' + rows[0].dt.toLocaleString() + ')'); + assert.strictEqual(expected_date_string, rows_raw[0].dt); + assert.strictEqual(dt.toISOString(), rows[0].dt.toISOString()); + } + testNextDate(connection); + }); }); }