Skip to content

Commit 9c6775f

Browse files
committed
Adds timezone support
This timezone is the timezone you want the dates to be written in the database. Local dates are converted to the timezone you want and then written. Reading will do the oposite. Without this timezone setting, dates will be written in UTC (default timezone = "Z"). There's also a tweak on the typeCasting, not finished yet, that enables people to make a custom typeCast function and use the default as fallback.
1 parent 8d7d814 commit 9c6775f

File tree

8 files changed

+80
-54
lines changed

8 files changed

+80
-54
lines changed

lib/Connection.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ function Connection(options) {
1313
this.config = options.config;
1414

1515
this._socket = options.socket;
16-
this._timezone = options.config.timezone || "Z";
16+
this._timezone = options.config.timezone;
1717
this._protocol = new Protocol({config: this.config});
1818
this._connectCalled = false;
1919
}
@@ -86,6 +86,15 @@ Connection.prototype.query = function(sql, values, cb) {
8686

8787
if (!('typeCast' in options)) {
8888
options.typeCast = this.config.typeCast;
89+
} else {
90+
var userTypeCast = options.typeCast;
91+
var defaultTypeCast = this.config.typeCast;
92+
93+
options.typeCast = function (field, parser, timeZone) {
94+
userTypeCast(field, parser, timeZone, function () {
95+
return defaultTypeCast(field, parser, timeZone);
96+
});
97+
};
8998
}
9099

91100
return this._protocol.query(options, cb);
@@ -113,11 +122,11 @@ Connection.prototype.resume = function() {
113122
};
114123

115124
Connection.prototype.escape = function(value) {
116-
return SqlString.escape(value);
125+
return SqlString.escape(value, true, this._timezone);
117126
};
118127

119128
Connection.prototype.format = function(sql, values) {
120-
return SqlString.format(sql, values);
129+
return SqlString.format(sql, values, this._timezone);
121130
};
122131

123132
Connection.prototype._handleNetworkError = function(err) {

lib/ConnectionConfig.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,16 @@ function ConnectionConfig(options) {
1616
this.database = options.database;
1717
this.insecureAuth = options.insecureAuth || false;
1818
this.debug = options.debug;
19-
this.timezone = options.timezone;
19+
this.timezone = options.timezone || 'Z';
2020
this.typeCast = (options.typeCast === undefined)
2121
? true
2222
: options.typeCast;
23+
if (this.timezone[0] == " ") {
24+
// "+" is a url encoded char for space so it
25+
// gets translated to space when giving a
26+
// connection string..
27+
this.timezone = "+" + this.timezone.substr(1);
28+
}
2329

2430
this.maxPacketSize = 0;
2531
this.charsetNumber = (options.charset)

lib/protocol/SqlString.js

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
var SqlString = exports;
22

3-
SqlString.escape = function(val, stringifyObjects) {
3+
SqlString.escape = function(val, stringifyObjects, timeZone) {
44
if (val === undefined || val === null) {
55
return 'NULL';
66
}
@@ -11,22 +11,22 @@ SqlString.escape = function(val, stringifyObjects) {
1111
}
1212

1313
if (val instanceof Date) {
14-
val = SqlString.dateToString(val);
14+
val = SqlString.dateToString(val, timeZone || "Z");
1515
}
1616

1717
if (Buffer.isBuffer(val)) {
1818
return SqlString.bufferToString(val);
1919
}
2020

2121
if (Array.isArray(val)) {
22-
return SqlString.arrayToList(val);
22+
return SqlString.arrayToList(val, timeZone);
2323
}
2424

2525
if (typeof val === 'object') {
2626
if (stringifyObjects) {
2727
val = val.toString();
2828
} else {
29-
return SqlString.objectToValues(val);
29+
return SqlString.objectToValues(val, timeZone);
3030
}
3131
}
3232

@@ -44,38 +44,39 @@ SqlString.escape = function(val, stringifyObjects) {
4444
return "'"+val+"'";
4545
};
4646

47-
SqlString.arrayToList = function(array) {
47+
SqlString.arrayToList = function(array, timeZone) {
4848
return array.map(function(v) {
4949
if (Array.isArray(v)) return '(' + SqlString.arrayToList(v) + ')';
50-
return SqlString.escape(v, true);
50+
return SqlString.escape(v, true, timeZone);
5151
}).join(', ');
5252
};
5353

54-
function zeroPad(number) {
55-
return (number < 10)
56-
? '0' + number
57-
: number;
58-
}
59-
60-
SqlString.format = function(sql, values) {
54+
SqlString.format = function(sql, values, timeZone) {
6155
values = [].concat(values);
6256

6357
return sql.replace(/\?/g, function(match) {
6458
if (!values.length) {
6559
return match;
6660
}
6761

68-
return SqlString.escape(values.shift());
62+
return SqlString.escape(values.shift(), false, timeZone);
6963
});
7064
};
7165

72-
SqlString.dateToString = function(date) {
73-
var year = date.getFullYear();
74-
var month = zeroPad(date.getMonth() + 1);
75-
var day = zeroPad(date.getDate());
76-
var hour = zeroPad(date.getHours());
77-
var minute = zeroPad(date.getMinutes());
78-
var second = zeroPad(date.getSeconds());
66+
SqlString.dateToString = function(date, timeZone) {
67+
var dt = new Date(), tz = convertTimezone(timeZone);
68+
69+
dt.setTime(date.getTime() + (date.getTimezoneOffset() * 60000));
70+
if (tz !== false) {
71+
dt.setTime(dt.getTime() + (tz * 60000));
72+
}
73+
74+
var year = dt.getFullYear();
75+
var month = zeroPad(dt.getMonth() + 1);
76+
var day = zeroPad(dt.getDate());
77+
var hour = zeroPad(dt.getHours());
78+
var minute = zeroPad(dt.getMinutes());
79+
var second = zeroPad(dt.getSeconds());
7980

8081
return year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second;
8182
};
@@ -95,16 +96,30 @@ SqlString.bufferToString = function(buffer) {
9596
return "X'" + hex+ "'";
9697
};
9798

98-
SqlString.objectToValues = function(object) {
99+
SqlString.objectToValues = function(object, timeZone) {
99100
var values = [];
100101
for (var key in object) {
101102
var value = object[key];
102103
if(typeof value === 'function') {
103104
continue;
104105
}
105106

106-
values.push('`' + key + '` = ' + SqlString.escape(value, true));
107+
values.push('`' + key + '` = ' + SqlString.escape(value, true, timeZone));
107108
}
108109

109110
return values.join(', ');
110111
};
112+
113+
function zeroPad(number) {
114+
return (number < 10) ? '0' + number : number;
115+
}
116+
117+
function convertTimezone(tz) {
118+
if (tz == "Z") return 0;
119+
120+
var m = tz.match(/([\+\-\s])(\d\d):?(\d\d)?/);
121+
if (m) {
122+
return (m[1] == '-' ? -1 : 1) * (parseInt(m[2], 10) + ((m[3] ? parseInt(m[3], 10) : 0) / 60)) * 60;
123+
}
124+
return false;
125+
}

lib/protocol/packets/RowDataPacket.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ RowDataPacket.prototype._typeCast = function(field, parser, timeZone) {
4242
// objects and strings as if they were in local time.
4343
if (field.type === Types.DATE) {
4444
dateString += ' 00:00:00';
45+
} else {
46+
// no timezone for date columns, there's no time.. so there's no time..zone
47+
dateString += timeZone;
4548
}
4649

47-
dateString += timeZone;
48-
4950
return new Date(dateString);
5051
case Types.TINY:
5152
case Types.SHORT:

test/common.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,16 @@ common.createConnection = function(config) {
2828
if (common.isTravis()) {
2929
// see: http://about.travis-ci.org/docs/user/database-setup/
3030
config = _.extend({
31-
user: 'root',
31+
user: 'root'
3232
}, config);
3333
} else {
3434
config = _.extend({
3535
host : process.env.MYSQL_HOST,
3636
port : process.env.MYSQL_PORT,
3737
user : process.env.MYSQL_USER,
3838
password : process.env.MYSQL_PASSWORD,
39-
}, config)
39+
timezone : "+05:00"
40+
}, config);
4041
}
4142

4243
return Mysql.createConnection(config);
Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
var common = require('../../common');
22
var connection = common.createConnection({typeCast: true});
33
var assert = require('assert');
4+
var util = require('util');
45

56
connection.connect();
67

78
var options = {
89
sql : "SELECT NOW() as date, POINT(1.2,-3.4) as point",
9-
typeCast : false,
10+
typeCast : false
1011
};
1112

12-
var rows = undefined;
13+
var rows;
1314
var query = connection.query(options, function(err, _rows) {
1415
if (err) throw err;
1516

@@ -20,17 +21,9 @@ connection.end();
2021

2122
process.on('exit', function() {
2223
assert.strictEqual(typeof rows[0].date, 'object');
23-
assert.equal(Buffer.isBuffer(rows[0].date), true);
24+
assert.equal(util.isDate(rows[0].date), true);
2425

25-
var point = rows[0].point
26-
assert.strictEqual(typeof point, 'object');
27-
assert.equal(Buffer.isBuffer(point), true);
28-
assert.equal(point.readUInt32LE(0), 0); // unknown
29-
var byteOrder = point.readUInt8(4);
30-
var wkbType = byteOrder? point.readUInt32LE(5) : point.readUInt32BE(5);
31-
assert.equal(wkbType, 1); // WKBPoint
32-
var x = byteOrder? point.readDoubleLE(9) : point.readDoubleBE(9);
33-
var y = byteOrder? point.readDoubleLE(17) : point.readDoubleBE(17);
34-
assert.equal(x, 1.2);
35-
assert.equal(y, -3.4);
26+
assert.strictEqual(typeof rows[0].point, 'object');
27+
assert.strictEqual(rows[0].point.x, 1.2);
28+
assert.strictEqual(rows[0].point.y, -3.4);
3629
});

test/integration/connection/test-type-casting.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ var tests = [
4949
{type: 'multipoint', insertRaw: "GeomFromText('MULTIPOINT(0 0, 20 20, 60 60)')", expect: [{x:0, y:0}, {x:20, y:20}, {x:60, y:60}], deep: true},
5050
{type: 'multilinestring', insertRaw: "GeomFromText('MULTILINESTRING((10 10, 20 20), (15 15, 30 15))')", expect: [[{x:10,y:10},{x:20,y:20}],[{x:15,y:15},{x:30,y:15}]], deep: true},
5151
{type: 'multipolygon', insertRaw: "GeomFromText('MULTIPOLYGON(((0 0,10 0,10 10,0 10,0 0)),((5 5,7 5,7 7,5 7, 5 5)))')", expect: [[[{x:0,y:0},{x:10,y:0},{x:10,y:10},{x:0,y:10},{x:0,y:0}]],[[{x:5,y:5},{x:7,y:5},{x:7,y:7},{x:5,y:7},{x:5,y:5}]]], deep: true},
52-
{type: 'geometrycollection', insertRaw: "GeomFromText('GEOMETRYCOLLECTION(POINT(10 10), POINT(30 30), LINESTRING(15 15, 20 20))')", expect: [{x:10,y:10},{x:30,y:30},[{x:15,y:15},{x:20,y:20}]], deep: true},
52+
{type: 'geometrycollection', insertRaw: "GeomFromText('GEOMETRYCOLLECTION(POINT(10 10), POINT(30 30), LINESTRING(15 15, 20 20))')", expect: [{x:10,y:10},{x:30,y:30},[{x:15,y:15},{x:20,y:20}]], deep: true}
5353
];
5454

5555
var table = 'type_casting';
@@ -68,7 +68,7 @@ tests.forEach(function(test, index) {
6868

6969
var createTable = [
7070
'CREATE TEMPORARY TABLE `' + table + '` (',
71-
'`id` int(11) unsigned NOT NULL AUTO_INCREMENT,',
71+
'`id` int(11) unsigned NOT NULL AUTO_INCREMENT,'
7272
].concat(schema).concat([
7373
'PRIMARY KEY (`id`)',
7474
') ENGINE=InnoDB DEFAULT CHARSET=utf8'
@@ -91,6 +91,7 @@ process.on('exit', function() {
9191
tests.forEach(function(test) {
9292
var expected = test.expect || test.insert;
9393
var got = row[test.columnName];
94+
var message;
9495

9596
if (expected instanceof Date) {
9697
assert.equal(got instanceof Date, true, test.type);
@@ -105,12 +106,12 @@ process.on('exit', function() {
105106
}
106107

107108
if (test.deep) {
108-
var message =
109-
'got: "' + JSON.stringify(got) + '" expected: "' + JSON.stringify(expected) + '" test: ' + test.type + '';
109+
message = 'got: "' + JSON.stringify(got) + '" expected: "' + JSON.stringify(expected) +
110+
'" test: ' + test.type + '';
110111
assert.deepEqual(expected, got, message);
111112
} else {
112-
var message =
113-
'got: "' + got + '" expected: "' + expected + '" test: ' + test.type + '';
113+
message = 'got: "' + got + '" expected: "' + expected +
114+
'" test: ' + test.type + '';
114115
assert.strictEqual(expected, got, message);
115116
}
116117
});

test/unit/protocol/test-SqlString.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ test('SqlString.escape', {
3636
'arrays are turned into lists': function() {
3737
assert.equal(SqlString.escape([1, 2, 'c']), "1, 2, 'c'");
3838
},
39-
39+
4040
'nested arrays are turned into grouped lists': function() {
4141
assert.equal(SqlString.escape([[1,2,3], [4,5,6], ['a', 'b', {nested: true}]]), "(1, 2, 3), (4, 5, 6), ('a', 'b', '[object Object]')");
4242
},
@@ -87,7 +87,7 @@ test('SqlString.escape', {
8787

8888
'dates are converted to YYYY-MM-DD HH:II:SS': function() {
8989
var expected = '2012-05-07 11:42:03';
90-
var date = new Date(expected);
90+
var date = new Date(Date.UTC(2012, 4, 7, 11, 42, 3));
9191
var string = SqlString.escape(date);
9292

9393
assert.strictEqual(string, "'" + expected + "'");
@@ -106,7 +106,7 @@ test('SqlString.escape', {
106106

107107
'Infinity -> Infinity': function() {
108108
assert.equal(SqlString.escape(Infinity), 'Infinity');
109-
},
109+
}
110110
});
111111

112112
test('SqlString.format', {

0 commit comments

Comments
 (0)