From 6ffcb8605a403f96952e87be491a3c5b0ce01a2f Mon Sep 17 00:00:00 2001 From: Trevor Robinson Date: Fri, 9 Aug 2019 18:31:08 -0500 Subject: [PATCH 1/3] Add support for timezone --- documentation/Readme.md | 13 -- lib/commands/execute.js | 3 +- lib/connection_config.js | 8 + lib/packets/execute.js | 12 +- lib/packets/packet.js | 129 ++++++++---- lib/parsers/binary_parser.js | 20 +- lib/parsers/parser_cache.js | 21 +- lib/parsers/text_parser.js | 18 +- test/common.js | 7 +- .../connection/test-custom-date-parameter.js | 2 +- .../connection/test-date-parameter.js | 2 +- test/integration/connection/test-datetime.js | 196 ++++++++++++++++-- test/integration/regressions/test-#629.js | 13 +- test/run.js | 3 - test/unit/packets/test-datetime.js | 4 +- 15 files changed, 333 insertions(+), 118 deletions(-) diff --git a/documentation/Readme.md b/documentation/Readme.md index 3234f8e131..a8b2717563 100644 --- a/documentation/Readme.md +++ b/documentation/Readme.md @@ -36,19 +36,6 @@ You need to check corresponding field's zeroFill flag and convert to string manu ``` **Note :** *This option could lose precision on the number as Javascript Number is a Float!* -- `timezone` connection option is not supported by `Node-MySQL2`. You can emulate this by using `typeCast` option instead: -```javascript -const config = { - //... - typeCast: function (field, next) { - if (field.type === 'DATETIME') { - return new Date(`${field.string()}Z`) // can be 'Z' for UTC or an offset in the form '+HH:MM' or '-HH:MM' - } - return next(); - } -} -``` - ## Other Resources - [Wire protocol documentation](http://dev.mysql.com/doc/internals/en/client-server-protocol.html) diff --git a/lib/commands/execute.js b/lib/commands/execute.js index d75a739c7c..9bc37843dd 100644 --- a/lib/commands/execute.js +++ b/lib/commands/execute.js @@ -38,7 +38,8 @@ class Execute extends Command { const executePacket = new Packets.Execute( this.statement.id, this.parameters, - connection.config.charsetNumber + connection.config.charsetNumber, + connection.config.timezone ); //For reasons why this try-catch is here, please see // https://github.com/sidorares/node-mysql2/pull/689 diff --git a/lib/connection_config.js b/lib/connection_config.js index 16f9ac7e40..22ab03e3ec 100644 --- a/lib/connection_config.js +++ b/lib/connection_config.js @@ -91,6 +91,14 @@ class ConnectionConfig { this.debug = options.debug; this.trace = options.trace !== false; this.stringifyObjects = options.stringifyObjects || false; + if ( + options.timezone && + !/^(?:local|Z|[ +-]\d\d:\d\d)$/.test(options.timezone) + ) { + // strictly supports timezones specified by mysqljs/mysql: + // https://github.com/mysqljs/mysql#user-content-connection-options + throw new Error(`Invalid timezone: ${options.timezone}`); + } this.timezone = options.timezone || 'local'; this.queryFormat = options.queryFormat; this.pool = options.pool || undefined; diff --git a/lib/packets/execute.js b/lib/packets/execute.js index 6a39a978a8..c612f66e12 100644 --- a/lib/packets/execute.js +++ b/lib/packets/execute.js @@ -18,7 +18,7 @@ function isJSON(value) { * Converts a value to an object describing type, String/Buffer representation and length * @param {*} value */ -function toParameter(value, encoding) { +function toParameter(value, encoding, timezone) { let type = Types.VAR_STRING; let length; let writer = function(value) { @@ -47,7 +47,10 @@ function toParameter(value, encoding) { if (Object.prototype.toString.call(value) === '[object Date]') { type = Types.DATETIME; length = 12; - writer = Packet.prototype.writeDate; + writer = function(value) { + // eslint-disable-next-line no-invalid-this + return Packet.prototype.writeDate.call(this, value, timezone); + }; } else if (isJSON(value)) { value = JSON.stringify(value); type = Types.JSON; @@ -71,10 +74,11 @@ function toParameter(value, encoding) { } class Execute { - constructor(id, parameters, charsetNumber) { + constructor(id, parameters, charsetNumber, timezone) { this.id = id; this.parameters = parameters; this.encoding = CharsetToEncoding[charsetNumber]; + this.timezone = timezone; } toPacket() { @@ -92,7 +96,7 @@ class Execute { length += 1; // new-params-bound-flag length += 2 * this.parameters.length; // type byte for each parameter if new-params-bound-flag is set parameters = this.parameters.map(value => - toParameter(value, this.encoding) + toParameter(value, this.encoding, this.timezone) ); length += parameters.reduce( (accumulator, parameter) => accumulator + parameter.length, diff --git a/lib/packets/packet.js b/lib/packets/packet.js index 90b5699af4..595b8707a0 100644 --- a/lib/packets/packet.js +++ b/lib/packets/packet.js @@ -248,38 +248,48 @@ class Packet { } // DATE, DATETIME and TIMESTAMP - readDateTime() { - const length = this.readInt8(); - if (length === 0xfb) { - return null; - } - let y = 0; - let m = 0; - let d = 0; - let H = 0; - let M = 0; - let S = 0; - let ms = 0; - if (length > 3) { - y = this.readInt16(); - m = this.readInt8(); - d = this.readInt8(); - } - if (length > 6) { - H = this.readInt8(); - M = this.readInt8(); - S = this.readInt8(); - } - if (length > 10) { - ms = this.readInt32() / 1000; + readDateTime(timezone) { + if (!timezone || timezone === 'Z' || timezone === 'local') { + const length = this.readInt8(); + if (length === 0xfb) { + return null; + } + let y = 0; + let m = 0; + let d = 0; + let H = 0; + let M = 0; + let S = 0; + let ms = 0; + if (length > 3) { + y = this.readInt16(); + m = this.readInt8(); + d = this.readInt8(); + } + if (length > 6) { + H = this.readInt8(); + M = this.readInt8(); + S = this.readInt8(); + } + if (length > 10) { + ms = this.readInt32() / 1000; + } + if (y + m + d + H + M + S + ms === 0) { + return INVALID_DATE; + } + if (timezone === 'Z') { + return new Date(Date.UTC(y, m - 1, d, H, M, S, ms)); + } + return new Date(y, m - 1, d, H, M, S, ms); } - if (y + m + d + H + M + S + ms === 0) { - return INVALID_DATE; + let str = this.readDateTimeString(6, 'T'); + if (str.length === 10) { + str += 'T00:00:00'; } - return new Date(y, m - 1, d, H, M, S, ms); + return new Date(str + timezone); } - readDateTimeString(decimals) { + readDateTimeString(decimals, timeSep = ' ') { const length = this.readInt8(); let y = 0; let m = 0; @@ -299,7 +309,9 @@ class Packet { H = this.readInt8(); M = this.readInt8(); S = this.readInt8(); - str += ` ${[leftPad(2, H), leftPad(2, M), leftPad(2, S)].join(':')}`; + str += `${timeSep}${[leftPad(2, H), leftPad(2, M), leftPad(2, S)].join( + ':' + )}`; } if (length > 10) { ms = this.readInt32(); @@ -427,8 +439,8 @@ class Packet { return sign * result; } return sign === -1 ? `-${str}` : str; - - } if (numDigits > 16) { + } + if (numDigits > 16) { str = this.readString(end - this.offset); return sign === -1 ? `-${str}` : str; } @@ -450,7 +462,6 @@ class Packet { return num; } return str; - } // note that if value of inputNumberAsString is bigger than MAX_SAFE_INTEGER @@ -575,7 +586,7 @@ class Packet { return parseGeometry(); } - parseDate() { + parseDate(timezone) { const strLen = this.readLengthCodedNumber(); if (strLen === null) { return null; @@ -590,15 +601,26 @@ class Packet { const m = this.parseInt(2); this.offset++; // - const d = this.parseInt(2); - return new Date(y, m - 1, d); + if (!timezone || timezone === 'local') { + return new Date(y, m - 1, d); + } + if (timezone === 'Z') { + return new Date(Date.UTC(y, m - 1, d)); + } + return new Date( + `${leftPad(4, y)}-${leftPad(2, m)}-${leftPad(2, d)}T00:00:00${timezone}` + ); } - parseDateTime() { + parseDateTime(timezone) { const str = this.readLengthCodedString('binary'); if (str === null) { return null; } - return new Date(str); + if (!timezone || timezone === 'local') { + return new Date(str); + } + return new Date(`${str}${timezone}`); } parseFloat(len) { @@ -785,15 +807,34 @@ class Packet { return this.offset; } - writeDate(d) { + writeDate(d, timezone) { this.buffer.writeUInt8(11, this.offset); - this.buffer.writeUInt16LE(d.getFullYear(), this.offset + 1); - this.buffer.writeUInt8(d.getMonth() + 1, this.offset + 3); - this.buffer.writeUInt8(d.getDate(), this.offset + 4); - this.buffer.writeUInt8(d.getHours(), this.offset + 5); - this.buffer.writeUInt8(d.getMinutes(), this.offset + 6); - this.buffer.writeUInt8(d.getSeconds(), this.offset + 7); - this.buffer.writeUInt32LE(d.getMilliseconds() * 1000, this.offset + 8); + if (!timezone || timezone === 'local') { + this.buffer.writeUInt16LE(d.getFullYear(), this.offset + 1); + this.buffer.writeUInt8(d.getMonth() + 1, this.offset + 3); + this.buffer.writeUInt8(d.getDate(), this.offset + 4); + this.buffer.writeUInt8(d.getHours(), this.offset + 5); + this.buffer.writeUInt8(d.getMinutes(), this.offset + 6); + this.buffer.writeUInt8(d.getSeconds(), this.offset + 7); + this.buffer.writeUInt32LE(d.getMilliseconds() * 1000, this.offset + 8); + } else { + if (timezone !== 'Z') { + const offset = + (timezone[0] === '-' ? -1 : 1) * + (parseInt(timezone.substring(1, 3), 10) * 60 + + parseInt(timezone.substring(4), 10)); + if (offset !== 0) { + d = new Date(d.getTime() + 60000 * offset); + } + } + this.buffer.writeUInt16LE(d.getUTCFullYear(), this.offset + 1); + this.buffer.writeUInt8(d.getUTCMonth() + 1, this.offset + 3); + this.buffer.writeUInt8(d.getUTCDate(), this.offset + 4); + this.buffer.writeUInt8(d.getUTCHours(), this.offset + 5); + this.buffer.writeUInt8(d.getUTCMinutes(), this.offset + 6); + this.buffer.writeUInt8(d.getUTCSeconds(), this.offset + 7); + this.buffer.writeUInt32LE(d.getUTCMilliseconds() * 1000, this.offset + 8); + } this.offset += 12; } diff --git a/lib/parsers/binary_parser.js b/lib/parsers/binary_parser.js index 96f662fb82..be6787fac3 100644 --- a/lib/parsers/binary_parser.js +++ b/lib/parsers/binary_parser.js @@ -15,6 +15,7 @@ function readCodeFor(field, config, options, fieldNum) { const supportBigNumbers = options.supportBigNumbers || config.supportBigNumbers; const bigNumberStrings = options.bigNumberStrings || config.bigNumberStrings; + const timezone = options.timezone || config.timezone; const unsigned = field.flags & FieldFlags.UNSIGNED; switch (field.columnType) { case Types.TINY: @@ -39,7 +40,7 @@ function readCodeFor(field, config, options, fieldNum) { if (config.dateStrings) { return `packet.readDateTimeString(${field.decimals});`; } - return 'packet.readDateTime();'; + return `packet.readDateTime('${timezone}');`; case Types.TIME: return 'packet.readTimeString()'; case Types.DECIMAL: @@ -68,15 +69,11 @@ function readCodeFor(field, config, options, fieldNum) { } return unsigned ? 'packet.readInt64();' : 'packet.readSInt64();'; - default: if (field.characterSet === Charsets.BINARY) { return 'packet.readLengthCodedBuffer();'; } - return ( - `packet.readLengthCodedString(CharsetToEncoding[fields[${fieldNum}].characterSet])` - ); - + return `packet.readLengthCodedString(CharsetToEncoding[fields[${fieldNum}].characterSet])`; } } @@ -127,10 +124,9 @@ function compile(fields, options, config) { if (typeof options.nestTables === 'string') { tableName = helpers.srcEscape(fields[i].table); - lvalue = - `this[${helpers.srcEscape( - fields[i].table + options.nestTables + fields[i].name - )}]`; + lvalue = `this[${helpers.srcEscape( + fields[i].table + options.nestTables + fields[i].name + )}]`; } else if (options.nestTables === true) { tableName = helpers.srcEscape(fields[i].table); lvalue = `this[${tableName}][${fieldName}]`; @@ -149,9 +145,7 @@ function compile(fields, options, config) { // } else if (fields[i].columnType == Types.NULL) { // result.push(lvalue + ' = null;'); // } else { - parserFn( - `if (nullBitmaskByte${nullByteIndex} & ${currentFieldNullBit})` - ); + parserFn(`if (nullBitmaskByte${nullByteIndex} & ${currentFieldNullBit})`); parserFn(`${lvalue} = null;`); parserFn('else'); parserFn(`${lvalue} = ${readCodeFor(fields[i], config, options, i)}`); diff --git a/lib/parsers/parser_cache.js b/lib/parsers/parser_cache.js index 8476d4fd1f..6b4a9507c9 100644 --- a/lib/parsers/parser_cache.js +++ b/lib/parsers/parser_cache.js @@ -6,18 +6,29 @@ const parserCache = new LRU({ max: 15000 }); -function keyFromFields(type, fields, options) { +function keyFromFields(type, fields, options, config) { let res = - `${type}/${typeof options.nestTables}/${options.nestTables}/${options.rowsAsArray}${options.supportBigNumbers}/${options.bigNumberStrings}/${typeof options.typeCast}`; + `${type}` + + `/${typeof options.nestTables}` + + `/${options.nestTables}` + + `/${options.rowsAsArray}` + + `/${options.supportBigNumbers || config.supportBigNumbers}` + + `/${options.bigNumberStrings || config.bigNumberStrings}` + + `/${typeof options.typeCast}` + + `/${options.timezone || config.timezone}` + + `/${options.decimalNumbers}` + + `/${options.dateStrings}`; for (let i = 0; i < fields.length; ++i) { - res += - `/${fields[i].name}:${fields[i].columnType}:${fields[i].flags}`; + const field = fields[i]; + res += `/${field.name}:${field.columnType}:${field.flags}:${ + field.characterSet + }`; } return res; } function getParser(type, fields, options, config, compiler) { - const key = keyFromFields(type, fields, options); + const key = keyFromFields(type, fields, options, config); let parser = parserCache.get(key); if (parser) { diff --git a/lib/parsers/text_parser.js b/lib/parsers/text_parser.js index 4c1208ee1d..334fd87547 100644 --- a/lib/parsers/text_parser.js +++ b/lib/parsers/text_parser.js @@ -15,6 +15,7 @@ function readCodeFor(type, charset, encodingExpr, config, options) { const supportBigNumbers = options.supportBigNumbers || config.supportBigNumbers; const bigNumberStrings = options.bigNumberStrings || config.bigNumberStrings; + const timezone = options.timezone || config.timezone; switch (type) { case Types.TINY: @@ -43,13 +44,13 @@ function readCodeFor(type, charset, encodingExpr, config, options) { if (config.dateStrings) { return 'packet.readLengthCodedString("ascii")'; } - return 'packet.parseDate()'; + return `packet.parseDate('${timezone}')`; case Types.DATETIME: case Types.TIMESTAMP: if (config.dateStrings) { return 'packet.readLengthCodedString("ascii")'; } - return 'packet.parseDateTime()'; + return `packet.parseDateTime('${timezone}')`; case Types.TIME: return 'packet.readLengthCodedString("ascii")'; case Types.GEOMETRY: @@ -64,7 +65,6 @@ function readCodeFor(type, charset, encodingExpr, config, options) { return 'packet.readLengthCodedBuffer()'; } return `packet.readLengthCodedString(${encodingExpr})`; - } } @@ -135,11 +135,11 @@ function compile(fields, options, config) { fieldName = helpers.srcEscape(fields[i].name); parserFn(`// ${fieldName}: ${typeNames[fields[i].columnType]}`); if (typeof options.nestTables === 'string') { - lvalue = - `this[${helpers.srcEscape(fields[i].table + options.nestTables + fields[i].name)}]`; + lvalue = `this[${helpers.srcEscape( + fields[i].table + options.nestTables + fields[i].name + )}]`; } else if (options.nestTables === true) { - lvalue = - `this[${helpers.srcEscape(fields[i].table)}][${fieldName}]`; + lvalue = `this[${helpers.srcEscape(fields[i].table)}][${fieldName}]`; } else if (options.rowsAsArray) { lvalue = `result[${i.toString(10)}]`; } else { @@ -155,7 +155,9 @@ function compile(fields, options, config) { ); if (typeof options.typeCast === 'function') { parserFn( - `${lvalue} = options.typeCast(wrap(fields[${i}], ${helpers.srcEscape(typeNames[fields[i].columnType])}, packet, ${encodingExpr}), function() { return ${readCode};})` + `${lvalue} = options.typeCast(wrap(fields[${i}], ${helpers.srcEscape( + typeNames[fields[i].columnType] + )}, packet, ${encodingExpr}), function() { return ${readCode};})` ); } else if (options.typeCast === false) { parserFn(`${lvalue} = packet.readLengthCodedBuffer();`); diff --git a/test/common.js b/test/common.js index 1f1c7bd631..1510de6180 100644 --- a/test/common.js +++ b/test/common.js @@ -9,8 +9,9 @@ const config = { port: process.env.MYSQL_PORT || 3306 }; -const configURI = - `mysql://${config.user}:${config.password}@${config.host}:${config.port}/${config.database}`; +const configURI = `mysql://${config.user}:${config.password}@${config.host}:${ + config.port +}/${config.database}`; exports.SqlString = require('sqlstring'); exports.config = config; @@ -107,6 +108,7 @@ exports.createConnection = function(args) { compress: (args && args.compress) || config.compress, decimalNumbers: args && args.decimalNumbers, charset: args && args.charset, + timezone: args && args.timezone, dateStrings: args && args.dateStrings, authSwitchHandler: args && args.authSwitchHandler, typeCast: args && args.typeCast @@ -146,6 +148,7 @@ exports.getConfig = function(input) { compress: (args && args.compress) || config.compress, decimalNumbers: args && args.decimalNumbers, charset: args && args.charset, + timezone: args && args.timezone, dateStrings: args && args.dateStrings, authSwitchHandler: args && args.authSwitchHandler, typeCast: args && args.typeCast diff --git a/test/integration/connection/test-custom-date-parameter.js b/test/integration/connection/test-custom-date-parameter.js index b3a828a569..4b356ce4c0 100644 --- a/test/integration/connection/test-custom-date-parameter.js +++ b/test/integration/connection/test-custom-date-parameter.js @@ -1,7 +1,7 @@ 'use strict'; const common = require('../../common'); -const connection = common.createConnection(); +const connection = common.createConnection({ timezone: 'Z' }); const assert = require('assert'); let rows = undefined; diff --git a/test/integration/connection/test-date-parameter.js b/test/integration/connection/test-date-parameter.js index 48bb08a8f3..03808aafcc 100644 --- a/test/integration/connection/test-date-parameter.js +++ b/test/integration/connection/test-date-parameter.js @@ -1,7 +1,7 @@ 'use strict'; const common = require('../../common'); -const connection = common.createConnection(); +const connection = common.createConnection({ timezone: 'Z' }); const assert = require('assert'); let rows = undefined; diff --git a/test/integration/connection/test-datetime.js b/test/integration/connection/test-datetime.js index a7051554b3..27bf31b90c 100644 --- a/test/integration/connection/test-datetime.js +++ b/test/integration/connection/test-datetime.js @@ -3,9 +3,22 @@ const common = require('../../common'); const connection = common.createConnection(); const connection1 = common.createConnection({ dateStrings: true }); +const connectionZ = common.createConnection({ timezone: 'Z' }); +const connection0930 = common.createConnection({ timezone: '+09:30' }); const assert = require('assert'); -let rows, rows1, rows2, rows3, rows4, rows5, rows6; +let rows, + rowsZ, + rows0930, + rows1, + rows1Z, + rows10930, + rows2, + rows3, + rows4, + rows5, + rows6, + rows7; const date = new Date('1990-01-01 08:15:11 UTC'); const datetime = new Date('2010-12-10 14:12:09.019473'); @@ -16,10 +29,29 @@ const date3 = null; const date4 = '2010-12-10 14:12:09.123456'; const date5 = '2010-12-10 14:12:09.019'; +function adjustTZ(d, offset = d.getTimezoneOffset()) { + return new Date(d.getTime() - offset * 60000); +} + +function toMidnight(d, offset = d.getTimezoneOffset()) { + const t = d.getTime(); + return new Date(t - (t % (24 * 60 * 60 * 1000)) + offset * 60000); +} + +function formatUTCDate(d) { + return d.toISOString().substring(0, 10); +} + +function formatUTCDateTime(d, precision = 0) { + const raw = d.toISOString().replace('T', ' '); + return precision <= 3 + ? raw.substring(0, 19 + (precision && 1) + precision) + : raw.substring(0, 23) + '0'.repeat(precision - 3); +} + connection.query( 'CREATE TEMPORARY TABLE t (d1 DATE, d2 DATETIME(3), d3 DATETIME(6))' ); -connection.query("set time_zone = '+00:00'"); connection.query('INSERT INTO t set d1=?, d2=?, d3=?', [ date, datetime, @@ -38,14 +70,34 @@ connection1.query('INSERT INTO t set d1=?, d2=?, d3=?, d4=?, d5=?, d6=?', [ date5 ]); +connectionZ.query( + 'CREATE TEMPORARY TABLE t (d1 DATE, d2 DATETIME(3), d3 DATETIME(6))' +); +connectionZ.query("set time_zone = '+00:00'"); +connectionZ.query('INSERT INTO t set d1=?, d2=?, d3=?', [ + date, + datetime, + datetime +]); + +connection0930.query( + 'CREATE TEMPORARY TABLE t (d1 DATE, d2 DATETIME(3), d3 DATETIME(6))' +); +connection0930.query("set time_zone = '+09:30'"); +connection0930.query('INSERT INTO t set d1=?, d2=?, d3=?', [ + date, + datetime, + datetime +]); + const dateAsStringExpected = [ { - d1: '1990-01-01', - d2: '2000-03-03 08:15:11', - d3: '2010-12-10 14:12:09', - d4: null, - d5: '2010-12-10 14:12:09.123456', - d6: '2010-12-10 14:12:09.019' + d1: formatUTCDate(adjustTZ(date)), + d2: formatUTCDateTime(adjustTZ(date1)), + d3: date2.substring(0, 19), + d4: date3, + d5: date4, + d6: date5 } ]; @@ -60,6 +112,28 @@ connection.execute( } ); +connectionZ.execute( + 'select from_unixtime(?) t', + [(+date).valueOf() / 1000], + (err, _rows) => { + if (err) { + throw err; + } + rowsZ = _rows; + } +); + +connection0930.execute( + 'select from_unixtime(?) t', + [(+date).valueOf() / 1000], + (err, _rows) => { + if (err) { + throw err; + } + rows0930 = _rows; + } +); + connection.query('select from_unixtime(631152000) t', (err, _rows) => { if (err) { throw err; @@ -67,21 +141,42 @@ connection.query('select from_unixtime(631152000) t', (err, _rows) => { rows1 = _rows; }); -connection.query('select * from t', (err, _rows) => { +connectionZ.query('select from_unixtime(631152000) t', (err, _rows) => { if (err) { throw err; } - rows2 = _rows; + rows1Z = _rows; }); -connection.execute('select * from t', (err, _rows) => { +connection0930.query('select from_unixtime(631152000) t', (err, _rows) => { if (err) { throw err; } - rows3 = _rows; - connection.end(); + rows10930 = _rows; }); +connection.query( + 'select *, cast(d1 as char) as d4, cast(d2 as char) as d5, cast(d3 as char) as d6 from t', + (err, _rows) => { + if (err) { + throw err; + } + rows2 = _rows; + connection.end(); + } +); + +connectionZ.execute( + 'select *, cast(d1 as char) as d4, cast(d2 as char) as d5, cast(d3 as char) as d6 from t', + (err, _rows) => { + if (err) { + throw err; + } + rows3 = _rows; + connectionZ.end(); + } +); + connection1.query('select * from t', (err, _rows) => { if (err) { throw err; @@ -108,29 +203,94 @@ connection1.execute( } ); +connection0930.execute( + 'select *, cast(d1 as char) as d4, cast(d2 as char) as d5, cast(d3 as char) as d6 from t', + (err, _rows) => { + if (err) { + throw err; + } + rows7 = _rows; + connection0930.end(); + } +); + process.on('exit', () => { + try { + common.createConnection({ timezone: 'utc' }); + assert.fail('Expected throw'); + } catch (err) { + assert.equal(err.message, 'Invalid timezone: utc'); + } + + // local TZ assert.equal(rows[0].t.constructor, Date); assert.equal(rows[0].t.getDate(), date.getDate()); assert.equal(rows[0].t.getHours(), date.getHours()); assert.equal(rows[0].t.getMinutes(), date.getMinutes()); assert.equal(rows[0].t.getSeconds(), date.getSeconds()); + // UTC + assert.equal(rowsZ[0].t.constructor, Date); + assert.equal(rowsZ[0].t.getDate(), date.getDate()); + assert.equal(rowsZ[0].t.getHours(), date.getHours()); + assert.equal(rowsZ[0].t.getMinutes(), date.getMinutes()); + assert.equal(rowsZ[0].t.getSeconds(), date.getSeconds()); + + // +09:30 + assert.equal(rows0930[0].t.constructor, Date); + assert.equal(rows0930[0].t.getDate(), date.getDate()); + assert.equal(rows0930[0].t.getHours(), date.getHours()); + assert.equal(rows0930[0].t.getMinutes(), date.getMinutes()); + assert.equal(rows0930[0].t.getSeconds(), date.getSeconds()); + + // local TZ assert.equal(rows1[0].t.constructor, Date); assert.equal( - rows1[0].t - new Date('Mon Jan 01 1990 11:00:00 GMT+1100 (EST)'), - 0 + rows1[0].t.getTime(), + new Date('Mon Jan 01 1990 00:00:00 UTC').getTime() ); - assert.equal(rows2[0].d1.getDate(), date.getDate()); + // UTC + assert.equal(rows1Z[0].t.constructor, Date); + assert.equal( + rows1Z[0].t.getTime(), + new Date('Mon Jan 01 1990 00:00:00 UTC').getTime() + ); + + // +09:30 + assert.equal(rows10930[0].t.constructor, Date); + assert.equal( + rows10930[0].t.getTime(), + new Date('Mon Jan 01 1990 00:00:00 UTC').getTime() + ); + + // local TZ + assert.equal(rows2[0].d1.getTime(), toMidnight(date).getTime()); assert.equal(rows2[0].d2.getTime(), datetime.getTime()); assert.equal(rows2[0].d3.getTime(), datetime.getTime()); + assert.equal(rows2[0].d4, formatUTCDate(adjustTZ(date))); + assert.equal(rows2[0].d5, formatUTCDateTime(adjustTZ(datetime), 3)); + assert.equal(rows2[0].d6, formatUTCDateTime(adjustTZ(datetime), 6)); - assert.equal(rows3[0].d1.getDate(), date.getDate()); + // UTC + assert.equal(rows3[0].d1.getTime(), toMidnight(date, 0).getTime()); assert.equal(rows3[0].d2.getTime(), datetime.getTime()); assert.equal(rows3[0].d3.getTime(), datetime.getTime()); + assert.equal(rows3[0].d4, formatUTCDate(date)); + assert.equal(rows3[0].d5, formatUTCDateTime(datetime, 3)); + assert.equal(rows3[0].d6, formatUTCDateTime(datetime, 6)); + // dateStrings assert.deepEqual(rows4, dateAsStringExpected); assert.deepEqual(rows5, dateAsStringExpected); - assert.equal(rows6.length, 1); + + // +09:30 + const tzOffset = -570; + assert.equal(rows7[0].d1.getTime(), toMidnight(date, tzOffset).getTime()); + assert.equal(rows7[0].d2.getTime(), datetime.getTime()); + assert.equal(rows7[0].d3.getTime(), datetime.getTime()); + assert.equal(rows7[0].d4, formatUTCDate(adjustTZ(date, tzOffset))); + assert.equal(rows7[0].d5, formatUTCDateTime(adjustTZ(datetime, tzOffset), 3)); + assert.equal(rows7[0].d6, formatUTCDateTime(adjustTZ(datetime, tzOffset), 6)); }); diff --git a/test/integration/regressions/test-#629.js b/test/integration/regressions/test-#629.js index 0a29000ff9..569448a102 100644 --- a/test/integration/regressions/test-#629.js +++ b/test/integration/regressions/test-#629.js @@ -1,7 +1,10 @@ 'use strict'; const common = require('../../common'); -const connection = common.createConnection({ dateStrings: false }); +const connection = common.createConnection({ + dateStrings: false, + timezone: 'Z' +}); const assert = require('assert'); const tableName = 'dates'; @@ -50,8 +53,12 @@ connection.query( connection.query( [ `INSERT INTO \`${tableName}\` VALUES`, - `(${testRows[0][0]},"${testRows[0][1]}", "${testRows[0][2]}", "${testRows[0][3]}"),`, - `(${testRows[1][0]},"${testRows[1][1]}", "${testRows[1][2]}", "${testRows[1][3]}")` + `(${testRows[0][0]},"${testRows[0][1]}", "${testRows[0][2]}", "${ + testRows[0][3] + }"),`, + `(${testRows[1][0]},"${testRows[1][1]}", "${testRows[1][2]}", "${ + testRows[1][3] + }")` ].join(' '), executeTest ); diff --git a/test/run.js b/test/run.js index 242645bf0e..3f32c8ca91 100755 --- a/test/run.js +++ b/test/run.js @@ -10,9 +10,6 @@ if (process.env.FILTER) { options.include = new RegExp(`${process.env.FILTER}.*\\.js$`); } -// set timezone to UTC -process.env.TZ = 'UTC'; - require('urun')(__dirname, options); process.on('exit', code => { diff --git a/test/unit/packets/test-datetime.js b/test/unit/packets/test-datetime.js index 147b91f889..26f27f5571 100644 --- a/test/unit/packets/test-datetime.js +++ b/test/unit/packets/test-datetime.js @@ -7,7 +7,7 @@ let buf = Buffer.from('0a000004000007dd070116010203', 'hex'); let packet = new packets.Packet(4, buf, 0, buf.length); packet.readInt16(); // unused -let d = packet.readDateTime(); +let d = packet.readDateTime('Z'); assert.equal(+d, 1358816523000); @@ -20,7 +20,7 @@ packet = new packets.Packet(6, buf, 0, buf.length); packet.readInt16(); // ignore const s = packet.readLengthCodedString('cesu8'); assert.equal(s, 'foo1'); -d = packet.readDateTime(); +d = packet.readDateTime('Z'); assert.equal(+d, 1455030069425); const s1 = packet.readLengthCodedString('cesu8'); From d0bd15c32fdf45e2a44a914d6fb21c92202358d5 Mon Sep 17 00:00:00 2001 From: Trevor Robinson Date: Sat, 10 Aug 2019 20:50:43 -0500 Subject: [PATCH 2/3] Console error instead of exception for invalid timezone --- lib/connection_config.js | 14 +++++++++++--- test/integration/connection/test-datetime.js | 9 +++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/connection_config.js b/lib/connection_config.js index 22ab03e3ec..7cd0141392 100644 --- a/lib/connection_config.js +++ b/lib/connection_config.js @@ -67,7 +67,7 @@ class ConnectionConfig { // REVIEW: Should this be emitted somehow? // eslint-disable-next-line no-console console.error( - `Ignoring invalid configuration option passed to Connection: ${key}. This is currently a warning, but in future versions of MySQL2, an error will be thrown if you pass an invalid configuration options to a Connection` + `Ignoring invalid configuration option passed to Connection: ${key}. This is currently a warning, but in future versions of MySQL2, an error will be thrown if you pass an invalid configuration option to a Connection` ); } } @@ -97,9 +97,17 @@ class ConnectionConfig { ) { // strictly supports timezones specified by mysqljs/mysql: // https://github.com/mysqljs/mysql#user-content-connection-options - throw new Error(`Invalid timezone: ${options.timezone}`); + // eslint-disable-next-line no-console + console.error( + `Ignoring invalid timezone passed to Connection: ${ + options.timezone + }. This is currently a warning, but in future versions of MySQL2, an error will be thrown if you pass an invalid configuration option to a Connection` + ); + // SqlStrings falls back to UTC on invalid timezone + this.timezone = 'Z'; + } else { + this.timezone = options.timezone || 'local'; } - this.timezone = options.timezone || 'local'; this.queryFormat = options.queryFormat; this.pool = options.pool || undefined; this.ssl = diff --git a/test/integration/connection/test-datetime.js b/test/integration/connection/test-datetime.js index 27bf31b90c..c089a25af0 100644 --- a/test/integration/connection/test-datetime.js +++ b/test/integration/connection/test-datetime.js @@ -215,12 +215,9 @@ connection0930.execute( ); process.on('exit', () => { - try { - common.createConnection({ timezone: 'utc' }); - assert.fail('Expected throw'); - } catch (err) { - assert.equal(err.message, 'Invalid timezone: utc'); - } + const connBadTz = common.createConnection({ timezone: 'utc' }); + assert.equal(connBadTz.config.timezone, 'Z'); + connBadTz.end(); // local TZ assert.equal(rows[0].t.constructor, Date); From 3fcc1f3c55d4380ff708ffc2e40396a0c09ab8a1 Mon Sep 17 00:00:00 2001 From: Trevor Robinson Date: Sat, 10 Aug 2019 21:35:02 -0500 Subject: [PATCH 3/3] Remove default arguments to support node 4 --- lib/packets/packet.js | 10 ++++++---- test/integration/connection/test-datetime.js | 15 ++++++++++++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/lib/packets/packet.js b/lib/packets/packet.js index 595b8707a0..9edd1b6b43 100644 --- a/lib/packets/packet.js +++ b/lib/packets/packet.js @@ -289,7 +289,7 @@ class Packet { return new Date(str + timezone); } - readDateTimeString(decimals, timeSep = ' ') { + readDateTimeString(decimals, timeSep) { const length = this.readInt8(); let y = 0; let m = 0; @@ -309,9 +309,11 @@ class Packet { H = this.readInt8(); M = this.readInt8(); S = this.readInt8(); - str += `${timeSep}${[leftPad(2, H), leftPad(2, M), leftPad(2, S)].join( - ':' - )}`; + str += `${timeSep || ' '}${[ + leftPad(2, H), + leftPad(2, M), + leftPad(2, S) + ].join(':')}`; } if (length > 10) { ms = this.readInt32(); diff --git a/test/integration/connection/test-datetime.js b/test/integration/connection/test-datetime.js index c089a25af0..8e50216e3f 100644 --- a/test/integration/connection/test-datetime.js +++ b/test/integration/connection/test-datetime.js @@ -29,12 +29,18 @@ const date3 = null; const date4 = '2010-12-10 14:12:09.123456'; const date5 = '2010-12-10 14:12:09.019'; -function adjustTZ(d, offset = d.getTimezoneOffset()) { +function adjustTZ(d, offset) { + if (offset === undefined) { + offset = d.getTimezoneOffset(); + } return new Date(d.getTime() - offset * 60000); } -function toMidnight(d, offset = d.getTimezoneOffset()) { +function toMidnight(d, offset) { const t = d.getTime(); + if (offset === undefined) { + offset = d.getTimezoneOffset(); + } return new Date(t - (t % (24 * 60 * 60 * 1000)) + offset * 60000); } @@ -42,8 +48,11 @@ function formatUTCDate(d) { return d.toISOString().substring(0, 10); } -function formatUTCDateTime(d, precision = 0) { +function formatUTCDateTime(d, precision) { const raw = d.toISOString().replace('T', ' '); + if (precision === undefined) { + precision = 0; + } return precision <= 3 ? raw.substring(0, 19 + (precision && 1) + precision) : raw.substring(0, 23) + '0'.repeat(precision - 3);