Skip to content

Commit 8248692

Browse files
committed
Merge pull request #555 from benesch/custom-type-coercion
Supercharge `prepareValue`
2 parents 7dbc4c9 + 619ba46 commit 8248692

File tree

4 files changed

+220
-31
lines changed

4 files changed

+220
-31
lines changed

Diff for: benchmark/prepare-values.js

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
var utils = require("../lib/utils");
2+
3+
var numArr = [];
4+
for (var i = 0; i < 1000; i++) numArr[i] = i;
5+
console.time("prepare-number-array");
6+
for (var i = 0; i < 100; i++) {
7+
utils.prepareValue(numArr);
8+
}
9+
console.timeEnd("prepare-number-array");
10+
11+
12+
var strArr = new Array(10000);
13+
console.time("prepare-string-array");
14+
for (var i = 0; i < 100; i++) {
15+
utils.prepareValue(strArr);
16+
}
17+
console.timeEnd("prepare-string-array");
18+
19+
20+
var objArr = [];
21+
for (var i = 0; i < 1000; i++) objArr[i] = { x: { y: 42 }};
22+
console.time("prepare-object-array");
23+
for (var i = 0; i < 100; i++) {
24+
utils.prepareValue(objArr);
25+
}
26+
console.timeEnd("prepare-object-array");
27+
28+
29+
var obj = { x: { y: 42 }};
30+
console.time("prepare-object");
31+
for (var i = 0; i < 100000; i++) {
32+
utils.prepareValue(obj);
33+
}
34+
console.timeEnd("prepare-object");
35+
36+
37+
var customType = {
38+
toPostgres: function () {
39+
return { toPostgres: function () { return new Date(); } };
40+
}
41+
};
42+
console.time("prepare-custom-type");
43+
for (var i = 0; i < 100000; i++) {
44+
utils.prepareValue(customType);
45+
}
46+
console.timeEnd("prepare-custom-type");

Diff for: lib/utils.js

+38-29
Original file line numberDiff line numberDiff line change
@@ -17,53 +17,62 @@ if(typeof events.EventEmitter.prototype.once !== 'function') {
1717
// uses comma separator so won't work for types like box that use
1818
// a different array separator.
1919
function arrayString(val) {
20-
var result = '{';
21-
for (var i = 0 ; i < val.length; i++) {
22-
if(i > 0) {
23-
result = result + ',';
24-
}
25-
if(val[i] instanceof Date) {
26-
result = result + JSON.stringify(val[i]);
27-
}
28-
else if(typeof val[i] === 'undefined') {
29-
result = result + 'NULL';
30-
}
31-
else if(Array.isArray(val[i])) {
32-
result = result + arrayString(val[i]);
33-
}
34-
else
35-
{
36-
result = result +
37-
(val[i] === null ? 'NULL' : JSON.stringify(val[i]));
38-
}
39-
}
40-
result = result + '}';
41-
return result;
20+
var result = '{';
21+
for (var i = 0 ; i < val.length; i++) {
22+
if(i > 0) {
23+
result = result + ',';
24+
}
25+
if(val[i] === null || typeof val[i] === 'undefined') {
26+
result = result + 'NULL';
27+
}
28+
else if(Array.isArray(val[i])) {
29+
result = result + arrayString(val[i]);
30+
}
31+
else
32+
{
33+
result = result + JSON.stringify(prepareValue(val[i]));
34+
}
35+
}
36+
result = result + '}';
37+
return result;
4238
}
4339

4440
//converts values from javascript types
4541
//to their 'raw' counterparts for use as a postgres parameter
4642
//note: you can override this function to provide your own conversion mechanism
4743
//for complex types, etc...
48-
var prepareValue = function(val) {
44+
var prepareValue = function(val, seen) {
4945
if (val instanceof Buffer) {
5046
return val;
5147
}
5248
if(val instanceof Date) {
5349
return dateToString(val);
5450
}
55-
if(typeof val === 'undefined') {
56-
return null;
57-
}
5851
if(Array.isArray(val)) {
5952
return arrayString(val);
6053
}
61-
if(!val || typeof val !== 'object') {
62-
return val === null ? null : val.toString();
54+
if(val === null || typeof val === 'undefined') {
55+
return null;
56+
}
57+
if(typeof val === 'object') {
58+
return prepareObject(val, seen);
6359
}
64-
return JSON.stringify(val);
60+
return val.toString();
6561
};
6662

63+
function prepareObject(val, seen) {
64+
if(val.toPostgres && typeof val.toPostgres === 'function') {
65+
seen = seen || [];
66+
if (seen.indexOf(val) !== -1) {
67+
throw new Error('circular reference detected while preparing "' + val + '" for query');
68+
}
69+
seen.push(val);
70+
71+
return prepareValue(val.toPostgres(prepareValue), seen);
72+
}
73+
return JSON.stringify(val);
74+
}
75+
6776
function dateToString(date) {
6877
function pad(number, digits) {
6978
number = ""+number;

Diff for: test/test-helper.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -222,14 +222,25 @@ var Sink = function(expected, timeout, callback) {
222222
}
223223
}
224224

225+
var getTimezoneOffset = Date.prototype.getTimezoneOffset;
226+
227+
var setTimezoneOffset = function(minutesOffset) {
228+
Date.prototype.getTimezoneOffset = function () { return minutesOffset; };
229+
}
230+
231+
var resetTimezoneOffset = function() {
232+
Date.prototype.getTimezoneOffset = getTimezoneOffset;
233+
}
225234

226235
module.exports = {
227236
Sink: Sink,
228237
pg: require(__dirname + '/../lib/'),
229238
args: args,
230239
config: args,
231240
sys: sys,
232-
Client: Client
241+
Client: Client,
242+
setTimezoneOffset: setTimezoneOffset,
243+
resetTimezoneOffset: resetTimezoneOffset
233244
};
234245

235246

Diff for: test/unit/utils-tests.js

+124-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
require(__dirname + '/test-helper');
1+
var helper = require(__dirname + '/test-helper');
22
var utils = require(__dirname + "/../../lib/utils");
33
var defaults = require(__dirname + "/../../lib").defaults;
44

@@ -48,3 +48,126 @@ test('normalizing query configs', function() {
4848
config = utils.normalizeQueryConfig({text: 'TEXT', values: [10]}, callback)
4949
assert.deepEqual(config, {text: 'TEXT', values: [10], callback: callback})
5050
})
51+
52+
test('prepareValues: buffer prepared properly', function() {
53+
var buf = new Buffer("quack");
54+
var out = utils.prepareValue(buf);
55+
assert.strictEqual(buf, out);
56+
});
57+
58+
test('prepareValues: date prepared properly', function() {
59+
helper.setTimezoneOffset(-330);
60+
61+
var date = new Date(2014, 1, 1, 11, 11, 1, 7);
62+
var out = utils.prepareValue(date);
63+
assert.strictEqual(out, "2014-02-01T11:11:01.007+05:30");
64+
65+
helper.resetTimezoneOffset();
66+
});
67+
68+
test('prepareValues: undefined prepared properly', function() {
69+
var out = utils.prepareValue(void 0);
70+
assert.strictEqual(out, null);
71+
});
72+
73+
test('prepareValue: null prepared properly', function() {
74+
var out = utils.prepareValue(null);
75+
assert.strictEqual(out, null);
76+
});
77+
78+
test('prepareValue: true prepared properly', function() {
79+
var out = utils.prepareValue(true);
80+
assert.strictEqual(out, 'true');
81+
});
82+
83+
test('prepareValue: false prepared properly', function() {
84+
var out = utils.prepareValue(false);
85+
assert.strictEqual(out, 'false');
86+
});
87+
88+
test('prepareValue: number prepared properly', function () {
89+
var out = utils.prepareValue(3.042);
90+
assert.strictEqual(out, '3.042');
91+
});
92+
93+
test('prepareValue: string prepared properly', function() {
94+
var out = utils.prepareValue('big bad wolf');
95+
assert.strictEqual(out, 'big bad wolf');
96+
});
97+
98+
test('prepareValue: simple array prepared properly', function() {
99+
var out = utils.prepareValue([1, null, 3, undefined, [5, 6, "squ,awk"]]);
100+
assert.strictEqual(out, '{"1",NULL,"3",NULL,{"5","6","squ,awk"}}');
101+
});
102+
103+
test('prepareValue: complex array prepared properly', function() {
104+
var out = utils.prepareValue([{ x: 42 }, { y: 84 }]);
105+
assert.strictEqual(out, '{"{\\"x\\":42}","{\\"y\\":84}"}');
106+
});
107+
108+
test('prepareValue: date array prepared properly', function() {
109+
helper.setTimezoneOffset(-330);
110+
111+
var date = new Date(2014, 1, 1, 11, 11, 1, 7);
112+
var out = utils.prepareValue([date]);
113+
assert.strictEqual(out, '{"2014-02-01T11:11:01.007+05:30"}');
114+
115+
helper.resetTimezoneOffset();
116+
});
117+
118+
test('prepareValue: arbitrary objects prepared properly', function() {
119+
var out = utils.prepareValue({ x: 42 });
120+
assert.strictEqual(out, '{"x":42}');
121+
});
122+
123+
test('prepareValue: objects with simple toPostgres prepared properly', function() {
124+
var customType = {
125+
toPostgres: function() {
126+
return "zomgcustom!";
127+
}
128+
};
129+
var out = utils.prepareValue(customType);
130+
assert.strictEqual(out, "zomgcustom!");
131+
});
132+
133+
test('prepareValue: objects with complex toPostgres prepared properly', function() {
134+
var buf = new Buffer("zomgcustom!");
135+
var customType = {
136+
toPostgres: function() {
137+
return [1, 2];
138+
}
139+
};
140+
var out = utils.prepareValue(customType);
141+
assert.strictEqual(out, '{"1","2"}');
142+
});
143+
144+
test('prepareValue: objects with toPostgres receive prepareValue', function() {
145+
var customRange = {
146+
lower: { toPostgres: function() { return 5; } },
147+
upper: { toPostgres: function() { return 10; } },
148+
toPostgres: function(prepare) {
149+
return "[" + prepare(this.lower) + "," + prepare(this.upper) + "]";
150+
}
151+
};
152+
var out = utils.prepareValue(customRange);
153+
assert.strictEqual(out, "[5,10]");
154+
});
155+
156+
test('prepareValue: objects with circular toPostgres rejected', function() {
157+
var buf = new Buffer("zomgcustom!");
158+
var customType = {
159+
toPostgres: function() {
160+
return { toPostgres: function () { return customType; } };
161+
}
162+
};
163+
164+
//can't use `assert.throws` since we need to distinguish circular reference
165+
//errors from call stack exceeded errors
166+
try {
167+
utils.prepareValue(customType);
168+
} catch (e) {
169+
assert.ok(e.message.match(/circular/), "Expected circular reference error but got " + e);
170+
return;
171+
}
172+
throw new Error("Expected prepareValue to throw exception");
173+
});

0 commit comments

Comments
 (0)