Skip to content

Commit

Permalink
Adds timezone support
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dresende committed Oct 29, 2012
1 parent 8d7d814 commit 9c6775f
Show file tree
Hide file tree
Showing 8 changed files with 80 additions and 54 deletions.
15 changes: 12 additions & 3 deletions lib/Connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function Connection(options) {
this.config = options.config;

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

if (!('typeCast' in options)) {
options.typeCast = this.config.typeCast;
} else {
var userTypeCast = options.typeCast;
var defaultTypeCast = this.config.typeCast;

options.typeCast = function (field, parser, timeZone) {
userTypeCast(field, parser, timeZone, function () {
return defaultTypeCast(field, parser, timeZone);
});
};
}

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

Connection.prototype.escape = function(value) {
return SqlString.escape(value);
return SqlString.escape(value, true, this._timezone);
};

Connection.prototype.format = function(sql, values) {
return SqlString.format(sql, values);
return SqlString.format(sql, values, this._timezone);
};

Connection.prototype._handleNetworkError = function(err) {
Expand Down
8 changes: 7 additions & 1 deletion lib/ConnectionConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@ function ConnectionConfig(options) {
this.database = options.database;
this.insecureAuth = options.insecureAuth || false;
this.debug = options.debug;
this.timezone = options.timezone;
this.timezone = options.timezone || 'Z';
this.typeCast = (options.typeCast === undefined)
? true
: options.typeCast;
if (this.timezone[0] == " ") {
// "+" is a url encoded char for space so it
// gets translated to space when giving a
// connection string..
this.timezone = "+" + this.timezone.substr(1);
}

this.maxPacketSize = 0;
this.charsetNumber = (options.charset)
Expand Down
61 changes: 38 additions & 23 deletions lib/protocol/SqlString.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
var SqlString = exports;

SqlString.escape = function(val, stringifyObjects) {
SqlString.escape = function(val, stringifyObjects, timeZone) {
if (val === undefined || val === null) {
return 'NULL';
}
Expand All @@ -11,22 +11,22 @@ SqlString.escape = function(val, stringifyObjects) {
}

if (val instanceof Date) {
val = SqlString.dateToString(val);
val = SqlString.dateToString(val, timeZone || "Z");
}

if (Buffer.isBuffer(val)) {
return SqlString.bufferToString(val);
}

if (Array.isArray(val)) {
return SqlString.arrayToList(val);
return SqlString.arrayToList(val, timeZone);
}

if (typeof val === 'object') {
if (stringifyObjects) {
val = val.toString();
} else {
return SqlString.objectToValues(val);
return SqlString.objectToValues(val, timeZone);
}
}

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

SqlString.arrayToList = function(array) {
SqlString.arrayToList = function(array, timeZone) {
return array.map(function(v) {
if (Array.isArray(v)) return '(' + SqlString.arrayToList(v) + ')';
return SqlString.escape(v, true);
return SqlString.escape(v, true, timeZone);
}).join(', ');
};

function zeroPad(number) {
return (number < 10)
? '0' + number
: number;
}

SqlString.format = function(sql, values) {
SqlString.format = function(sql, values, timeZone) {
values = [].concat(values);

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

return SqlString.escape(values.shift());
return SqlString.escape(values.shift(), false, timeZone);
});
};

SqlString.dateToString = function(date) {
var year = date.getFullYear();
var month = zeroPad(date.getMonth() + 1);
var day = zeroPad(date.getDate());
var hour = zeroPad(date.getHours());
var minute = zeroPad(date.getMinutes());
var second = zeroPad(date.getSeconds());
SqlString.dateToString = function(date, timeZone) {
var dt = new Date(), tz = convertTimezone(timeZone);

dt.setTime(date.getTime() + (date.getTimezoneOffset() * 60000));
if (tz !== false) {
dt.setTime(dt.getTime() + (tz * 60000));
}

var year = dt.getFullYear();
var month = zeroPad(dt.getMonth() + 1);
var day = zeroPad(dt.getDate());
var hour = zeroPad(dt.getHours());
var minute = zeroPad(dt.getMinutes());
var second = zeroPad(dt.getSeconds());

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

SqlString.objectToValues = function(object) {
SqlString.objectToValues = function(object, timeZone) {
var values = [];
for (var key in object) {
var value = object[key];
if(typeof value === 'function') {
continue;
}

values.push('`' + key + '` = ' + SqlString.escape(value, true));
values.push('`' + key + '` = ' + SqlString.escape(value, true, timeZone));
}

return values.join(', ');
};

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;
}
5 changes: 3 additions & 2 deletions lib/protocol/packets/RowDataPacket.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@ RowDataPacket.prototype._typeCast = function(field, parser, timeZone) {
// objects and strings as if they were in local time.
if (field.type === Types.DATE) {
dateString += ' 00:00:00';
} else {
// no timezone for date columns, there's no time.. so there's no time..zone
dateString += timeZone;
}

dateString += timeZone;

return new Date(dateString);
case Types.TINY:
case Types.SHORT:
Expand Down
5 changes: 3 additions & 2 deletions test/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,16 @@ common.createConnection = function(config) {
if (common.isTravis()) {
// see: http://about.travis-ci.org/docs/user/database-setup/
config = _.extend({
user: 'root',
user: 'root'
}, config);
} else {
config = _.extend({
host : process.env.MYSQL_HOST,
port : process.env.MYSQL_PORT,
user : process.env.MYSQL_USER,
password : process.env.MYSQL_PASSWORD,
}, config)
timezone : "+05:00"
}, config);
}

return Mysql.createConnection(config);
Expand Down
21 changes: 7 additions & 14 deletions test/integration/connection/test-type-cast-query.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
var common = require('../../common');
var connection = common.createConnection({typeCast: true});
var assert = require('assert');
var util = require('util');

connection.connect();

var options = {
sql : "SELECT NOW() as date, POINT(1.2,-3.4) as point",
typeCast : false,
typeCast : false
};

var rows = undefined;
var rows;
var query = connection.query(options, function(err, _rows) {
if (err) throw err;

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

process.on('exit', function() {
assert.strictEqual(typeof rows[0].date, 'object');
assert.equal(Buffer.isBuffer(rows[0].date), true);
assert.equal(util.isDate(rows[0].date), true);

var point = rows[0].point
assert.strictEqual(typeof point, 'object');
assert.equal(Buffer.isBuffer(point), true);
assert.equal(point.readUInt32LE(0), 0); // unknown
var byteOrder = point.readUInt8(4);
var wkbType = byteOrder? point.readUInt32LE(5) : point.readUInt32BE(5);
assert.equal(wkbType, 1); // WKBPoint
var x = byteOrder? point.readDoubleLE(9) : point.readDoubleBE(9);
var y = byteOrder? point.readDoubleLE(17) : point.readDoubleBE(17);
assert.equal(x, 1.2);
assert.equal(y, -3.4);
assert.strictEqual(typeof rows[0].point, 'object');
assert.strictEqual(rows[0].point.x, 1.2);
assert.strictEqual(rows[0].point.y, -3.4);
});
13 changes: 7 additions & 6 deletions test/integration/connection/test-type-casting.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ var tests = [
{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},
{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},
{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},
{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},
{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}
];

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

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

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

if (test.deep) {
var message =
'got: "' + JSON.stringify(got) + '" expected: "' + JSON.stringify(expected) + '" test: ' + test.type + '';
message = 'got: "' + JSON.stringify(got) + '" expected: "' + JSON.stringify(expected) +
'" test: ' + test.type + '';
assert.deepEqual(expected, got, message);
} else {
var message =
'got: "' + got + '" expected: "' + expected + '" test: ' + test.type + '';
message = 'got: "' + got + '" expected: "' + expected +
'" test: ' + test.type + '';
assert.strictEqual(expected, got, message);
}
});
Expand Down
6 changes: 3 additions & 3 deletions test/unit/protocol/test-SqlString.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ test('SqlString.escape', {
'arrays are turned into lists': function() {
assert.equal(SqlString.escape([1, 2, 'c']), "1, 2, 'c'");
},

'nested arrays are turned into grouped lists': function() {
assert.equal(SqlString.escape([[1,2,3], [4,5,6], ['a', 'b', {nested: true}]]), "(1, 2, 3), (4, 5, 6), ('a', 'b', '[object Object]')");
},
Expand Down Expand Up @@ -87,7 +87,7 @@ test('SqlString.escape', {

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

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

'Infinity -> Infinity': function() {
assert.equal(SqlString.escape(Infinity), 'Infinity');
},
}
});

test('SqlString.format', {
Expand Down

0 comments on commit 9c6775f

Please sign in to comment.