diff --git a/benchmark/prepare-values.js b/benchmark/prepare-values.js new file mode 100644 index 000000000..49e973e7a --- /dev/null +++ b/benchmark/prepare-values.js @@ -0,0 +1,46 @@ +var utils = require("../lib/utils"); + +var numArr = []; +for (var i = 0; i < 1000; i++) numArr[i] = i; +console.time("prepare-number-array"); +for (var i = 0; i < 100; i++) { + utils.prepareValue(numArr); +} +console.timeEnd("prepare-number-array"); + + +var strArr = new Array(10000); +console.time("prepare-string-array"); +for (var i = 0; i < 100; i++) { + utils.prepareValue(strArr); +} +console.timeEnd("prepare-string-array"); + + +var objArr = []; +for (var i = 0; i < 1000; i++) objArr[i] = { x: { y: 42 }}; +console.time("prepare-object-array"); +for (var i = 0; i < 100; i++) { + utils.prepareValue(objArr); +} +console.timeEnd("prepare-object-array"); + + +var obj = { x: { y: 42 }}; +console.time("prepare-object"); +for (var i = 0; i < 100000; i++) { + utils.prepareValue(obj); +} +console.timeEnd("prepare-object"); + + +var customType = { + toPostgres: function () { + return { toPostgres: function () { return new Date(); } }; + } +}; +console.time("prepare-custom-type"); +for (var i = 0; i < 100000; i++) { + utils.prepareValue(customType); +} +console.timeEnd("prepare-custom-type"); diff --git a/lib/utils.js b/lib/utils.js index fb1f56ebe..2c20a77da 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -17,53 +17,62 @@ if(typeof events.EventEmitter.prototype.once !== 'function') { // uses comma separator so won't work for types like box that use // a different array separator. function arrayString(val) { - var result = '{'; - for (var i = 0 ; i < val.length; i++) { - if(i > 0) { - result = result + ','; - } - if(val[i] instanceof Date) { - result = result + JSON.stringify(val[i]); - } - else if(typeof val[i] === 'undefined') { - result = result + 'NULL'; - } - else if(Array.isArray(val[i])) { - result = result + arrayString(val[i]); - } - else - { - result = result + - (val[i] === null ? 'NULL' : JSON.stringify(val[i])); - } - } - result = result + '}'; - return result; + var result = '{'; + for (var i = 0 ; i < val.length; i++) { + if(i > 0) { + result = result + ','; + } + if(val[i] === null || typeof val[i] === 'undefined') { + result = result + 'NULL'; + } + else if(Array.isArray(val[i])) { + result = result + arrayString(val[i]); + } + else + { + result = result + JSON.stringify(prepareValue(val[i])); + } + } + result = result + '}'; + return result; } //converts values from javascript types //to their 'raw' counterparts for use as a postgres parameter //note: you can override this function to provide your own conversion mechanism //for complex types, etc... -var prepareValue = function(val) { +var prepareValue = function(val, seen) { if (val instanceof Buffer) { return val; } if(val instanceof Date) { return dateToString(val); } - if(typeof val === 'undefined') { - return null; - } if(Array.isArray(val)) { return arrayString(val); } - if(!val || typeof val !== 'object') { - return val === null ? null : val.toString(); + if(val === null || typeof val === 'undefined') { + return null; + } + if(typeof val === 'object') { + return prepareObject(val, seen); } - return JSON.stringify(val); + return val.toString(); }; +function prepareObject(val, seen) { + if(val.toPostgres && typeof val.toPostgres === 'function') { + seen = seen || []; + if (seen.indexOf(val) !== -1) { + throw new Error('circular reference detected while preparing "' + val + '" for query'); + } + seen.push(val); + + return prepareValue(val.toPostgres(prepareValue), seen); + } + return JSON.stringify(val); +} + function dateToString(date) { function pad(number, digits) { number = ""+number; diff --git a/test/test-helper.js b/test/test-helper.js index 8d854b819..757b200b4 100644 --- a/test/test-helper.js +++ b/test/test-helper.js @@ -222,6 +222,15 @@ var Sink = function(expected, timeout, callback) { } } +var getTimezoneOffset = Date.prototype.getTimezoneOffset; + +var setTimezoneOffset = function(minutesOffset) { + Date.prototype.getTimezoneOffset = function () { return minutesOffset; }; +} + +var resetTimezoneOffset = function() { + Date.prototype.getTimezoneOffset = getTimezoneOffset; +} module.exports = { Sink: Sink, @@ -229,7 +238,9 @@ module.exports = { args: args, config: args, sys: sys, - Client: Client + Client: Client, + setTimezoneOffset: setTimezoneOffset, + resetTimezoneOffset: resetTimezoneOffset }; diff --git a/test/unit/utils-tests.js b/test/unit/utils-tests.js index 56c81dc52..116dfdd1e 100644 --- a/test/unit/utils-tests.js +++ b/test/unit/utils-tests.js @@ -1,4 +1,4 @@ -require(__dirname + '/test-helper'); +var helper = require(__dirname + '/test-helper'); var utils = require(__dirname + "/../../lib/utils"); var defaults = require(__dirname + "/../../lib").defaults; @@ -48,3 +48,126 @@ test('normalizing query configs', function() { config = utils.normalizeQueryConfig({text: 'TEXT', values: [10]}, callback) assert.deepEqual(config, {text: 'TEXT', values: [10], callback: callback}) }) + +test('prepareValues: buffer prepared properly', function() { + var buf = new Buffer("quack"); + var out = utils.prepareValue(buf); + assert.strictEqual(buf, out); +}); + +test('prepareValues: date prepared properly', function() { + helper.setTimezoneOffset(-330); + + var date = new Date(2014, 1, 1, 11, 11, 1, 7); + var out = utils.prepareValue(date); + assert.strictEqual(out, "2014-02-01T11:11:01.007+05:30"); + + helper.resetTimezoneOffset(); +}); + +test('prepareValues: undefined prepared properly', function() { + var out = utils.prepareValue(void 0); + assert.strictEqual(out, null); +}); + +test('prepareValue: null prepared properly', function() { + var out = utils.prepareValue(null); + assert.strictEqual(out, null); +}); + +test('prepareValue: true prepared properly', function() { + var out = utils.prepareValue(true); + assert.strictEqual(out, 'true'); +}); + +test('prepareValue: false prepared properly', function() { + var out = utils.prepareValue(false); + assert.strictEqual(out, 'false'); +}); + +test('prepareValue: number prepared properly', function () { + var out = utils.prepareValue(3.042); + assert.strictEqual(out, '3.042'); +}); + +test('prepareValue: string prepared properly', function() { + var out = utils.prepareValue('big bad wolf'); + assert.strictEqual(out, 'big bad wolf'); +}); + +test('prepareValue: simple array prepared properly', function() { + var out = utils.prepareValue([1, null, 3, undefined, [5, 6, "squ,awk"]]); + assert.strictEqual(out, '{"1",NULL,"3",NULL,{"5","6","squ,awk"}}'); +}); + +test('prepareValue: complex array prepared properly', function() { + var out = utils.prepareValue([{ x: 42 }, { y: 84 }]); + assert.strictEqual(out, '{"{\\"x\\":42}","{\\"y\\":84}"}'); +}); + +test('prepareValue: date array prepared properly', function() { + helper.setTimezoneOffset(-330); + + var date = new Date(2014, 1, 1, 11, 11, 1, 7); + var out = utils.prepareValue([date]); + assert.strictEqual(out, '{"2014-02-01T11:11:01.007+05:30"}'); + + helper.resetTimezoneOffset(); +}); + +test('prepareValue: arbitrary objects prepared properly', function() { + var out = utils.prepareValue({ x: 42 }); + assert.strictEqual(out, '{"x":42}'); +}); + +test('prepareValue: objects with simple toPostgres prepared properly', function() { + var customType = { + toPostgres: function() { + return "zomgcustom!"; + } + }; + var out = utils.prepareValue(customType); + assert.strictEqual(out, "zomgcustom!"); +}); + +test('prepareValue: objects with complex toPostgres prepared properly', function() { + var buf = new Buffer("zomgcustom!"); + var customType = { + toPostgres: function() { + return [1, 2]; + } + }; + var out = utils.prepareValue(customType); + assert.strictEqual(out, '{"1","2"}'); +}); + +test('prepareValue: objects with toPostgres receive prepareValue', function() { + var customRange = { + lower: { toPostgres: function() { return 5; } }, + upper: { toPostgres: function() { return 10; } }, + toPostgres: function(prepare) { + return "[" + prepare(this.lower) + "," + prepare(this.upper) + "]"; + } + }; + var out = utils.prepareValue(customRange); + assert.strictEqual(out, "[5,10]"); +}); + +test('prepareValue: objects with circular toPostgres rejected', function() { + var buf = new Buffer("zomgcustom!"); + var customType = { + toPostgres: function() { + return { toPostgres: function () { return customType; } }; + } + }; + + //can't use `assert.throws` since we need to distinguish circular reference + //errors from call stack exceeded errors + try { + utils.prepareValue(customType); + } catch (e) { + assert.ok(e.message.match(/circular/), "Expected circular reference error but got " + e); + return; + } + throw new Error("Expected prepareValue to throw exception"); +});