Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[CONJS-62] Support named timezones and daylight savings time
This permit to forces use of the indicated timezone, rather than the
current Node.js timezone. This has to be set when database timezone
differ from Node.js timezone.

Possible values are [IANA time zone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) (ex: 'America/New_York')

Implementation rely on moment-timezone.js
  • Loading branch information
rusher committed Apr 25, 2019
1 parent 49e8881 commit d24c8ee
Show file tree
Hide file tree
Showing 12 changed files with 188 additions and 126 deletions.
3 changes: 1 addition & 2 deletions documentation/connection-options.md
Expand Up @@ -340,7 +340,7 @@ mariadb.createConnection({
| **multipleStatements** | Allows you to issue several SQL statements in a single `quer()` call. (That is, `INSERT INTO a VALUES('b'); INSERT INTO c VALUES('d');`). <br/><br/>This may be a **security risk** as it allows for SQL Injection attacks. |*boolean* |false|
| **namedPlaceholders** | Allows the use of named placeholders. |*boolean* |false|
| **permitLocalInfile** | Allows the use of `LOAD DATA INFILE` statements.<br/><br/>Loading data from a file from the client may be a security issue, as a man-in-the-middle proxy server can change the actual file the server loads. Being able to execute a query on the client gives you access to files on the client. |*boolean* |false|
| **timezone** | Forces use of the indicated timezone, rather than the current Node.js timezone. Possible values are `Z` for UTC, `local` or `±HH:MM` format |*string* |
| **timezone** | Forces use of the indicated timezone, rather than the current Node.js timezone. This has to be set when database timezone differ from Node.js timezone. Possible values are [IANA time zone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) (ex: 'America/New_York') |*string* |
| **nestTables** | Presents result-sets by table to avoid results with colliding fields. See the `query()` description for more information. |*boolean* |false|
| **pipelining** | Sends queries one by one without waiting on the results of the previous entry. For more information, see [Pipelining](/documentation/pipelining.md) |*boolean* |true|
| **trace** | Adds the stack trace at the time of query creation to the error stack trace, making it easier to identify the part of the code that issued the query. Note: This feature is disabled by default due to the performance cost of stack creation. Only turn it on when you need to debug issues. |*boolean* |false|
Expand All @@ -353,7 +353,6 @@ mariadb.createConnection({
| **bulk** | disabled bulk command in batch|*boolean* |



## F.A.Q.

#### error Hostname/IP doesn't match certificate's altnames
Expand Down
17 changes: 4 additions & 13 deletions lib/cmd/common-text-cmd.js
Expand Up @@ -12,8 +12,7 @@ class CommonText extends ResultSet {
this.configAssign(connOpts, cmdOpts);
this.sql = sql;
this.initialValues = values;
this.getDateQuote =
this.opts.timezone === 'local' ? getLocalDate : getTimezoneDate;
this.getDateQuote = this.opts.tz ? getTimezoneDate : getLocalDate;
}

/**
Expand Down Expand Up @@ -428,18 +427,10 @@ function getLocalDate(date, opts) {
}

function getTimezoneDate(date, opts) {
if (opts.timezoneMillisOffset) {
date.setTime(date.getTime() + opts.timezoneMillisOffset);
if (date.getMilliseconds() != 0) {
return opts.tz(date).format("'YYYY-MM-DD HH:mm:ss.SSS'");
}

const year = date.getUTCFullYear();
const mon = date.getUTCMonth() + 1;
const day = date.getUTCDate();
const hour = date.getUTCHours();
const min = date.getUTCMinutes();
const sec = date.getUTCSeconds();
const ms = date.getUTCMilliseconds();
return getDatePartQuote(year, mon, day, hour, min, sec, ms);
return opts.tz(date).format("'YYYY-MM-DD HH:mm:ss'");
}

module.exports = CommonText;
3 changes: 1 addition & 2 deletions lib/cmd/resultset.js
Expand Up @@ -107,8 +107,7 @@ class ResultSet extends Command {
cmdOpts.dateStrings != undefined
? cmdOpts.dateStrings
: connOpts.dateStrings,
timezone: connOpts.timezone,
timezoneMillisOffset: connOpts.timezoneMillisOffset,
tz: cmdOpts.tz != undefined ? cmdOpts.tz : connOpts.tz,
namedPlaceholders:
cmdOpts.namedPlaceholders != undefined
? cmdOpts.namedPlaceholders
Expand Down
50 changes: 37 additions & 13 deletions lib/config/connection-options.js
Expand Up @@ -2,6 +2,8 @@

const Collations = require('../const/collations.js');
const urlFormat = /mariadb:\/\/(([^/@:]+)?(:([^/]+))?@)?(([^/:]+)(:([0-9]+))?)\/([^?]+)(\?(.*))?$/;
const moment = require('moment-timezone');
const Errors = require('../misc/errors');

/**
* Default option similar to mysql driver.
Expand Down Expand Up @@ -84,24 +86,46 @@ class ConnectionOptions {
this.timezone = opts.timezone || 'local';
if (this.timezone !== 'local') {
if (this.timezone === 'Z') {
this.timezoneMillisOffset = 0;
this.tz = moment.tz.setDefault('Etc/UTC');
} else {
const matched = this.timezone.match(/([+\-\s])(\d\d):?(\d\d)?/);
if (!matched) {
throw new RangeError(
"timezone format error. must be 'local'/'Z' or ±HH:MM. was '" +
this.timezone +
"'"
if (matched) {
const hour =
(matched[1] === '-' ? 1 : -1) * Number.parseInt(matched[2], 10);
const minutes =
matched.length > 2 && matched[3]
? Number.parseInt(matched[3], 10)
: 0;
if (minutes > 0) {
throw new RangeError(
"timezone format incompatible with IANA standard timezone format was '" +
this.timezone +
"'"
);
}
this.tz = moment.tz.setDefault(
'Etc/GMT' + (matched[1] === '-' ? '+' : '') + hour
);
console.log(
"warning: please use IANA standard timezone format ('Etc/GMT" +
(matched[1] === '-' ? '+' : '') +
hour +
"')"
);
} else {
this.tz = moment.tz.setDefault(this.timezone);
}
if (!this.tz.defaultZone) {
throw Errors.createError(
"Unknown IANA timezone '" + this.timezone + "'.",
true,
null,
'08S01',
Errors.ER_WRONG_IANA_TIMEZONE
);
}
const hour =
(matched[1] === '-' ? -1 : 1) * Number.parseInt(matched[2], 10);
const minutes =
matched.length > 2 && matched[3]
? Number.parseInt(matched[3], 10)
: 0;
this.timezoneMillisOffset = hour * 3600000 + minutes * 60000;
}
this.localTz = moment.tz.guess();
}
this.trace = opts.trace || false;
this.typeCast = opts.typeCast;
Expand Down
11 changes: 5 additions & 6 deletions lib/connection.js
Expand Up @@ -832,12 +832,11 @@ function Connection(options) {

_executeSessionVariableQuery()
.then(() => {
_executeInitQuery()
.then(() => {
_status = Status.CONNECTED;
process.nextTick(resolve, this);
})
.catch(errorInitialQueries);
return _executeInitQuery();
})
.then(() => {
_status = Status.CONNECTED;
process.nextTick(resolve, this);
})
.catch(errorInitialQueries);
};
Expand Down
30 changes: 13 additions & 17 deletions lib/io/bulk-packet.js
@@ -1,7 +1,6 @@
'use strict';

const Iconv = require('iconv-lite');

const SMALL_BUFFER_SIZE = 1024;
const MEDIUM_BUFFER_SIZE = 16384; //16k
const LARGE_BUFFER_SIZE = 131072; //128k
Expand All @@ -27,10 +26,9 @@ class BulkPacket {
this.waitingResponseNo = 1;
this.singleQuery = false;
this.haveErrorResponse = false;
this.writeBinaryDate =
opts.timezone === 'local'
? this.writeBinaryLocalDate
: this.writeBinaryTimezoneDate;
this.writeBinaryDate = opts.tz
? this.writeBinaryTimezoneDate
: this.writeBinaryLocalDate;
if (this.encoding === 'utf8') {
this.writeLengthEncodedString = this.writeDefaultLengthEncodedString;
} else if (Buffer.isEncoding(this.encoding)) {
Expand Down Expand Up @@ -382,7 +380,6 @@ class BulkPacket {

_writeBinaryDate(year, mon, day, hour, min, sec, ms) {
let len = ms === 0 ? 7 : 11;

//not enough space remaining
if (len + 1 > this.buf.length - this.pos) {
let tmpBuf = Buffer.allocUnsafe(len + 1);
Expand Down Expand Up @@ -427,17 +424,16 @@ class BulkPacket {
}

writeBinaryTimezoneDate(date, opts) {
if (opts.timezoneMillisOffset) {
date.setTime(date.getTime() + opts.timezoneMillisOffset);
}

const year = date.getUTCFullYear();
const mon = date.getUTCMonth() + 1;
const day = date.getUTCDate();
const hour = date.getUTCHours();
const min = date.getUTCMinutes();
const sec = date.getUTCSeconds();
const ms = date.getUTCMilliseconds();
const formated = opts.tz(date).format('YYYY-MM-DD HH:mm:ss.SSSSSS');
const dateZoned = new Date(formated + 'Z');

const year = dateZoned.getUTCFullYear();
const mon = dateZoned.getUTCMonth() + 1;
const day = dateZoned.getUTCDate();
const hour = dateZoned.getUTCHours();
const min = dateZoned.getUTCMinutes();
const sec = dateZoned.getUTCSeconds();
const ms = dateZoned.getUTCMilliseconds();
return this._writeBinaryDate(year, mon, day, hour, min, sec, ms);
}

Expand Down
13 changes: 10 additions & 3 deletions lib/io/packet.js
Expand Up @@ -359,10 +359,17 @@ class Packet {
this.pos += len;
const str = this.buf.toString('ascii', this.pos - len, this.pos);
if (str.startsWith('0000-00-00 00:00:00')) return null;
if (opts.timezone === 'local') {
return new Date(str);

if (opts.tz) {
return new Date(
opts
.tz(str)
.clone()
.tz(opts.localTz)
.format('YYYY-MM-DD HH:mm:ss.SSSSSS')
);
}
return new Date(str + opts.timezone);
return new Date(str);
}

readIntLengthEncoded() {
Expand Down
3 changes: 2 additions & 1 deletion lib/misc/errors.js
Expand Up @@ -6,7 +6,7 @@ class SQLError extends Error {
super(
(addHeader === undefined || addHeader
? '(conn=' +
(info.threadId ? info.threadId : -1) +
(info ? (info.threadId ? info.threadId : -1) : -1) +
', no: ' +
(errno ? errno : -1) +
', SQLState: ' +
Expand Down Expand Up @@ -99,6 +99,7 @@ module.exports.ER_SETTING_SESSION_ERROR = 45029;
module.exports.ER_INITIAL_SQL_ERROR = 45030;
module.exports.ER_BATCH_WITH_NO_VALUES = 45031;
module.exports.ER_RESET_BAD_PACKET = 45032;
module.exports.ER_WRONG_IANA_TIMEZONE = 45033;

const keys = Object.keys(module.exports);
const errByNo = {};
Expand Down
8 changes: 5 additions & 3 deletions package.json
Expand Up @@ -15,7 +15,7 @@
"test:lint": "eslint \"{lib,test}/**/*.js\" ",
"test:types": "eslint \"types/*.ts\" ",
"test:types-prettier": "prettier --write \"types/*.ts\"",
"test:prettier": "prettier --write \"{lib,test,benchmarks}/**/*.js\"",
"test:prettier": "prettier --write \"{tools,lib,test,benchmarks}/**/*.js\"",
"coverage": "npm run coverage:test && npm run coverage:report",
"coverage:test": "nyc mocha \"test/**/*.js\"",
"coverage:report": "nyc report --reporter=text-lcov > coverage.lcov && codecov",
Expand Down Expand Up @@ -47,7 +47,8 @@
"dependencies": {
"denque": "^1.4.0",
"iconv-lite": "^0.4.24",
"long": "^4.0.0"
"long": "^4.0.0",
"moment-timezone": "^0.5.25"
},
"devDependencies": {
"@types/geojson": "^7946.0.7",
Expand All @@ -67,7 +68,8 @@
"mocha-lcov-reporter": "^1.3.0",
"nyc": "^13.3.0",
"prettier": "^1.15.3",
"typescript": "^3.3.4000"
"typescript": "^3.3.4000",
"dom-parser": "^0.1.6"
},
"bugs": {
"url": "https://jira.mariadb.org/projects/CONJS/"
Expand Down
44 changes: 15 additions & 29 deletions test/integration/test-batch.js
Expand Up @@ -14,21 +14,7 @@ describe('batch', () => {
let maxAllowedSize, bigBuf, timezoneParam;

before(function(done) {
const hourOffset = Math.round((-1 * new Date().getTimezoneOffset()) / 60);

if (hourOffset < 0) {
if (hourOffset <= -10) {
timezoneParam = hourOffset + ':00';
} else {
timezoneParam = '-0' + Math.abs(hourOffset) + ':00';
}
} else {
if (hourOffset >= 10) {
timezoneParam = '+' + Math.abs(hourOffset) + ':00';
} else {
timezoneParam = '+0' + Math.abs(hourOffset) + ':00';
}
}
timezoneParam = 'America/New_York';

shareConn
.query('SELECT @@max_allowed_packet as t')
Expand Down Expand Up @@ -93,8 +79,8 @@ describe('batch', () => {
[
true,
'john😎🌶\\\\',
new Date('2001-12-31 23:59:58'),
new Date('2018-01-01 12:30:20.456789'),
new Date('2001-12-31 23:59:58+3'),
new Date('2018-01-01 12:30:20.456789+3'),
{
type: 'Point',
coordinates: [10, 10]
Expand All @@ -103,8 +89,8 @@ describe('batch', () => {
[
true,
f,
new Date('2001-12-31 23:59:58'),
new Date('2018-01-01 12:30:20.456789'),
new Date('2001-12-31 23:59:58+3'),
new Date('2018-01-01 12:30:20.456789+3'),
{
type: 'Point',
coordinates: [10, 10]
Expand All @@ -114,7 +100,7 @@ describe('batch', () => {
false,
{ name: 'jackमस्', val: 'tt' },
null,
new Date('2018-01-21 11:30:20.123456'),
new Date('2018-01-21 11:30:20.123456+3'),
{
type: 'Point',
coordinates: [10, 20]
Expand All @@ -123,8 +109,8 @@ describe('batch', () => {
[
0,
null,
new Date('2020-12-31 23:59:59'),
new Date('2018-01-21 11:30:20.123456'),
new Date('2020-12-31 23:59:59+3'),
new Date('2018-01-21 11:30:20.123456+3'),
{
type: 'Point',
coordinates: [20, 20]
Expand All @@ -142,8 +128,8 @@ describe('batch', () => {
id2: 1,
id3: 2,
t: 'john😎🌶\\\\',
d: new Date('2001-12-31 23:59:58'),
d2: new Date('2018-01-01 12:30:20.456789'),
d: new Date('2001-12-31 23:59:58+3'),
d2: new Date('2018-01-01 12:30:20.456789+3'),
g: {
type: 'Point',
coordinates: [10, 10]
Expand All @@ -155,8 +141,8 @@ describe('batch', () => {
id2: 1,
id3: 2,
t: 'blabla',
d: new Date('2001-12-31 23:59:58'),
d2: new Date('2018-01-01 12:30:20.456789'),
d: new Date('2001-12-31 23:59:58+3'),
d2: new Date('2018-01-01 12:30:20.456789+3'),
g: {
type: 'Point',
coordinates: [10, 10]
Expand All @@ -169,7 +155,7 @@ describe('batch', () => {
id3: 2,
t: '{"name":"jackमस्","val":"tt"}',
d: null,
d2: new Date('2018-01-21 11:30:20.123456'),
d2: new Date('2018-01-21 11:30:20.123456+3'),
g: {
type: 'Point',
coordinates: [10, 20]
Expand All @@ -181,8 +167,8 @@ describe('batch', () => {
id2: 0,
id3: 2,
t: null,
d: new Date('2020-12-31 23:59:59'),
d2: new Date('2018-01-21 11:30:20.123456'),
d: new Date('2020-12-31 23:59:59+3'),
d2: new Date('2018-01-21 11:30:20.123456+3'),
g: {
type: 'Point',
coordinates: [20, 20]
Expand Down

0 comments on commit d24c8ee

Please sign in to comment.