diff --git a/.gitignore b/.gitignore index d2554e7..9ba7be7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ .bzr .bzrignore -test.js \ No newline at end of file +test.js diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..eccf303 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2010 Tim Caswell , + (c) 2010 Aurynn Shaw + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.markdown b/README.markdown deleted file mode 100644 index d219a55..0000000 --- a/README.markdown +++ /dev/null @@ -1,8 +0,0 @@ -# PostgreSQL for Javascript - -This library is a implementation of the PostgreSQL backend/frontend protocol in javascript. -It uses the node.js tcp and event libraries. A javascript md5 library is included for servers that require md5 password hashing (this is default). - -This is using the api from node-persistence and is used by sousaball. - -See test.js for sample usage. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..684683d --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# PostgreSQL for Javascript + +This library is a implementation of the PostgreSQL backend/frontend protocol in javascript. +It uses the node.js tcp and event libraries. A javascript md5 library is included for servers that require md5 password hashing (this is default). + +This library allows for the correct handling of server-side prepared queries. + +Nested DB calls will be executed in the order of definition. + +All code is available under the terms of the MIT license + +(c) 2010, Tim Caswell, Aurynn Shaw. + +Bugs can be reported @ https://public.commandprompt.com/projects/postgresjs + +## Example use + + var sys = require("sys"); + var pg = require("postgres-pure"); + + var db = new pg.connect("pgsql://test:12345@localhost:5432/template1"); + db.query("SELECT * FROM sometable", function (data) { + console.log(data); + }); + db.close(); + +## Example use of Parameterized Queries + + var sys = require("sys"); + var pg = require("postgres-pure"); + + var db = new pg.connect("pgsql://test:12345@localhost:5432/template1"); + db.query("SELECT * FROM yourtable WHERE id = ?", 1, function (data) { + console.log(data); + }); + db.close(); + +## Example use of Prepared Queries + + var sys = require("sys"); + var pg = require("postgres"); + + var db = new pg.connect("pgsql://test:12345@localhost:5432/template1"); + db.prepare("SELECT * FROM yourtable WHERE id = ?", function (stmt) { + stmt.execute(1, function (rs) { + console.log(rs[0]); + }); + }); + db.close(); \ No newline at end of file diff --git a/benchmark.js b/benchmark.js new file mode 100644 index 0000000..337bcd7 --- /dev/null +++ b/benchmark.js @@ -0,0 +1,26 @@ +var sys = require("sys"); + +var iterations = process.argv[2]; + +var pg = require("./lib/postgres-pure"); +var db = new pg.connect("pgsql://test:12345@localhost:5432/insert_test"); +db.transaction(function (tx) { + tx.query("CREATE TABLE insert_test (id serial not null, val text)", function (err, rs) {}); + tx.begin(); + tx.prepare("INSERT INTO insert_test (val) VALUES (?::text) RETURNING id, val", function (sth) { + var i = 0; + for (i = 0; i <= iterations; i++) { + sth.execute(""+i, function (err, rs) { + // This is the point where we have executed. + console.log(rs[0]); + }) + } + }); + tx.query("SELECT COUNT(*) FROM insert_test", function (err, rs) { + console.log(sys.inspect(rs)); + }); + tx.rollback(); + tx.query("DROP TABLE insert_test", function () {}); +}); +console.log("Done"); +db.close(); \ No newline at end of file diff --git a/demo.js b/demo.js new file mode 100644 index 0000000..36e2c69 --- /dev/null +++ b/demo.js @@ -0,0 +1,92 @@ +var sys = require("sys"); +var pg = require("./lib/postgres-pure"); +// pg.DEBUG=4; + +var db = new pg.connect("pgsql://test:12345@localhost:5432/insert_test"); + +// db.query("SELECT 1::errortest;", function (error, rs, tx) { +// if (error) { +// console.log("Error!"); +// console.log(error); +// console.log(error.code); +// } +// else { +// console.log(sys.inspect(rs)); +// } +// }); + +// db.query("SELECT 1::querytest;", function (error, rs, tx) { +// if (error) { +// console.log("Error!"); +// console.log(error); +// console.log(error.code); +// } +// else { +// console.log(sys.inspect(rs)); +// } +// +// tx.query("SELECT 2::int as querytest2", function (err, rs) { +// console.log(sys.inspect(rs)); +// }); +// tx.query("SELECT 3::qt" ,function (err, rs) { +// console.log(sys.inspect(err)); +// }) +// }); + +// db.prepare("INSERT INTO returning_test (val) VALUES (?) RETURNING id, val", function (sth) { +// sth.execute("text value", function(e, rs) { +// if (rs === undefined) { +// console.log("No data."); +// } +// else { +// console.log(sys.inspect(rs)); +// } +// }); +// }); + +db.prepare("SELECT ?::int AS preparetest", function (sth, tx) { + sth.execute(1, function (err, rs) { + console.log(sys.inspect(rs)); + }); + sth.execute(2, function (err, rs) { + console.log(sys.inspect(rs)); + + }); + tx.prepare("SELECT ?::int AS preparetest2", function (sth) { + sth.execute(3, function (err, rs) { + console.log(sys.inspect(rs)); + }) ; + }); + tx.prepare("SELECT ?::int AS preparetest2", function (sth) { + sth.execute(4, function (err, rs) { + console.log(sys.inspect(rs)); + }) ; + }); +}); + +// db.transaction(function (tx) { +// tx.begin(); +// // tx.query("INSERT INTO insert_test (val) VALUES (?) RETURNING id", "test value", function (err,rs) { +// // console.log(sys.inspect(rs)); +// // }); +// // tx.prepare("INSERT INTO insert_test (val) VALUES (?) RETURNING id", function (sth) { +// // sth.execute("twooooo", function (err, rs) { +// // console.log(sys.inspect(rs)); +// // }); +// // }); +// tx.query("SELECT 1::barrrrrr", function (err, rs) { +// console.log(sys.inspect(err)); +// }); +// tx.query("SELECT 1::int as foobar", function (err, rs) { +// console.log(sys.inspect(err)); +// }); +// tx.rollback(); +// tx.query("SELECT 1::int as foobarbaz", function (err, rs) { +// console.log(sys.inspect(rs)); +// }); +// }); +db.close(); + +// db.prepare(query, function (sth, tx) { +// sth.execute(args, callback, errback); +// }) \ No newline at end of file diff --git a/lib/buffer_extras.js b/lib/buffer_extras.js index ca4d372..8105b72 100644 --- a/lib/buffer_extras.js +++ b/lib/buffer_extras.js @@ -87,22 +87,28 @@ Buffer.makeWriter = function makeWriter() { return writer; }, int16: function pushInt16(number) { - var b = new Buffer(2); - b.int16Write(number); - data.push(b); - return writer; + var b = new Buffer(2); + b.int16Write(number); + data.push(b); + return writer; }, string: function pushString(string) { - data.push(Buffer.fromString(string)); - return writer; + data.push(Buffer.fromString(string)); + return writer; + }, + byte1: function byte1(string) { + var b = new Buffer(1); // one octet. + b.write(string, 'utf8', 0); + data.push(b); + return writer; }, cstring: function pushCstring(string) { - data.push(Buffer.fromString(string + "\0")); - return writer; + data.push(Buffer.fromString(string + "\0")); + return writer; }, multicstring: function pushMulticstring(fields) { - data.push(Buffer.fromString(fields.join("\0") + "\0\0")); - return writer; + data.push(Buffer.fromString(fields.join("\0") + "\0\0")); + return writer; }, hash: function pushHash(hash) { var keys = Object.keys(hash); diff --git a/lib/md5.js b/lib/md5.js deleted file mode 100644 index 1e8f3b6..0000000 --- a/lib/md5.js +++ /dev/null @@ -1,381 +0,0 @@ -/* - * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message - * Digest Algorithm, as defined in RFC 1321. - * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 - * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet - * Distributed under the BSD License - * See http://pajhome.org.uk/crypt/md5 for more info. - */ - -/* - * Configurable variables. You may need to tweak these to be compatible with - * the server-side, but the defaults work in most cases. - */ -var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ -var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ - -/* - * These are the functions you'll usually want to call - * They take string arguments and return either hex or base-64 encoded strings - */ -function hex_md5(s) { return rstr2hex(rstr_md5(str2rstr_utf8(s))); } -function b64_md5(s) { return rstr2b64(rstr_md5(str2rstr_utf8(s))); } -function any_md5(s, e) { return rstr2any(rstr_md5(str2rstr_utf8(s)), e); } -function hex_hmac_md5(k, d) - { return rstr2hex(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); } -function b64_hmac_md5(k, d) - { return rstr2b64(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); } -function any_hmac_md5(k, d, e) - { return rstr2any(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)), e); } - -module.exports = function md5(s) { return rstr2hex(rstr_md5(s)); } - -/* - * Perform a simple self-test to see if the VM is working - */ -function md5_vm_test() -{ - return hex_md5("abc").toLowerCase() == "900150983cd24fb0d6963f7d28e17f72"; -} - -/* - * Calculate the MD5 of a raw string - */ -function rstr_md5(s) -{ - return binl2rstr(binl_md5(rstr2binl(s), s.length * 8)); -} - -/* - * Calculate the HMAC-MD5, of a key and some data (raw strings) - */ -function rstr_hmac_md5(key, data) -{ - var bkey = rstr2binl(key); - if(bkey.length > 16) bkey = binl_md5(bkey, key.length * 8); - - var ipad = Array(16), opad = Array(16); - for(var i = 0; i < 16; i++) - { - ipad[i] = bkey[i] ^ 0x36363636; - opad[i] = bkey[i] ^ 0x5C5C5C5C; - } - - var hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8); - return binl2rstr(binl_md5(opad.concat(hash), 512 + 128)); -} - -/* - * Convert a raw string to a hex string - */ -function rstr2hex(input) -{ - try { hexcase } catch(e) { hexcase=0; } - var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; - var output = ""; - var x; - for(var i = 0; i < input.length; i++) - { - x = input.charCodeAt(i); - output += hex_tab.charAt((x >>> 4) & 0x0F) - + hex_tab.charAt( x & 0x0F); - } - return output; -} - -/* - * Convert a raw string to a base-64 string - */ -function rstr2b64(input) -{ - try { b64pad } catch(e) { b64pad=''; } - var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - var output = ""; - var len = input.length; - for(var i = 0; i < len; i += 3) - { - var triplet = (input.charCodeAt(i) << 16) - | (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0) - | (i + 2 < len ? input.charCodeAt(i+2) : 0); - for(var j = 0; j < 4; j++) - { - if(i * 8 + j * 6 > input.length * 8) output += b64pad; - else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F); - } - } - return output; -} - -/* - * Convert a raw string to an arbitrary string encoding - */ -function rstr2any(input, encoding) -{ - var divisor = encoding.length; - var i, j, q, x, quotient; - - /* Convert to an array of 16-bit big-endian values, forming the dividend */ - var dividend = Array(Math.ceil(input.length / 2)); - for(i = 0; i < dividend.length; i++) - { - dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1); - } - - /* - * Repeatedly perform a long division. The binary array forms the dividend, - * the length of the encoding is the divisor. Once computed, the quotient - * forms the dividend for the next step. All remainders are stored for later - * use. - */ - var full_length = Math.ceil(input.length * 8 / - (Math.log(encoding.length) / Math.log(2))); - var remainders = Array(full_length); - for(j = 0; j < full_length; j++) - { - quotient = Array(); - x = 0; - for(i = 0; i < dividend.length; i++) - { - x = (x << 16) + dividend[i]; - q = Math.floor(x / divisor); - x -= q * divisor; - if(quotient.length > 0 || q > 0) - quotient[quotient.length] = q; - } - remainders[j] = x; - dividend = quotient; - } - - /* Convert the remainders to the output string */ - var output = ""; - for(i = remainders.length - 1; i >= 0; i--) - output += encoding.charAt(remainders[i]); - - return output; -} - -/* - * Encode a string as utf-8. - * For efficiency, this assumes the input is valid utf-16. - */ -function str2rstr_utf8(input) -{ - var output = ""; - var i = -1; - var x, y; - - while(++i < input.length) - { - /* Decode utf-16 surrogate pairs */ - x = input.charCodeAt(i); - y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0; - if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF) - { - x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF); - i++; - } - - /* Encode output as utf-8 */ - if(x <= 0x7F) - output += String.fromCharCode(x); - else if(x <= 0x7FF) - output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F), - 0x80 | ( x & 0x3F)); - else if(x <= 0xFFFF) - output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F), - 0x80 | ((x >>> 6 ) & 0x3F), - 0x80 | ( x & 0x3F)); - else if(x <= 0x1FFFFF) - output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07), - 0x80 | ((x >>> 12) & 0x3F), - 0x80 | ((x >>> 6 ) & 0x3F), - 0x80 | ( x & 0x3F)); - } - return output; -} - -/* - * Encode a string as utf-16 - */ -function str2rstr_utf16le(input) -{ - var output = ""; - for(var i = 0; i < input.length; i++) - output += String.fromCharCode( input.charCodeAt(i) & 0xFF, - (input.charCodeAt(i) >>> 8) & 0xFF); - return output; -} - -function str2rstr_utf16be(input) -{ - var output = ""; - for(var i = 0; i < input.length; i++) - output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF, - input.charCodeAt(i) & 0xFF); - return output; -} - -/* - * Convert a raw string to an array of little-endian words - * Characters >255 have their high-byte silently ignored. - */ -function rstr2binl(input) -{ - var output = Array(input.length >> 2); - for(var i = 0; i < output.length; i++) - output[i] = 0; - for(var i = 0; i < input.length * 8; i += 8) - output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (i%32); - return output; -} - -/* - * Convert an array of little-endian words to a string - */ -function binl2rstr(input) -{ - var output = ""; - for(var i = 0; i < input.length * 32; i += 8) - output += String.fromCharCode((input[i>>5] >>> (i % 32)) & 0xFF); - return output; -} - -/* - * Calculate the MD5 of an array of little-endian words, and a bit length. - */ -function binl_md5(x, len) -{ - /* append padding */ - x[len >> 5] |= 0x80 << ((len) % 32); - x[(((len + 64) >>> 9) << 4) + 14] = len; - - var a = 1732584193; - var b = -271733879; - var c = -1732584194; - var d = 271733878; - - for(var i = 0; i < x.length; i += 16) - { - var olda = a; - var oldb = b; - var oldc = c; - var oldd = d; - - a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936); - d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586); - c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819); - b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330); - a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897); - d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426); - c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341); - b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983); - a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416); - d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417); - c = md5_ff(c, d, a, b, x[i+10], 17, -42063); - b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162); - a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682); - d = md5_ff(d, a, b, c, x[i+13], 12, -40341101); - c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290); - b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329); - - a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510); - d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632); - c = md5_gg(c, d, a, b, x[i+11], 14, 643717713); - b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302); - a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691); - d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083); - c = md5_gg(c, d, a, b, x[i+15], 14, -660478335); - b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848); - a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438); - d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690); - c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961); - b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501); - a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467); - d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784); - c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473); - b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734); - - a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558); - d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463); - c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562); - b = md5_hh(b, c, d, a, x[i+14], 23, -35309556); - a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060); - d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353); - c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632); - b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640); - a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174); - d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222); - c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979); - b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189); - a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487); - d = md5_hh(d, a, b, c, x[i+12], 11, -421815835); - c = md5_hh(c, d, a, b, x[i+15], 16, 530742520); - b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651); - - a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844); - d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415); - c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905); - b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055); - a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571); - d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606); - c = md5_ii(c, d, a, b, x[i+10], 15, -1051523); - b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799); - a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359); - d = md5_ii(d, a, b, c, x[i+15], 10, -30611744); - c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380); - b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649); - a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070); - d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379); - c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259); - b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551); - - a = safe_add(a, olda); - b = safe_add(b, oldb); - c = safe_add(c, oldc); - d = safe_add(d, oldd); - } - return Array(a, b, c, d); -} - -/* - * These functions implement the four basic operations the algorithm uses. - */ -function md5_cmn(q, a, b, x, s, t) -{ - return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b); -} -function md5_ff(a, b, c, d, x, s, t) -{ - return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); -} -function md5_gg(a, b, c, d, x, s, t) -{ - return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); -} -function md5_hh(a, b, c, d, x, s, t) -{ - return md5_cmn(b ^ c ^ d, a, b, x, s, t); -} -function md5_ii(a, b, c, d, x, s, t) -{ - return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); -} - -/* - * Add integers, wrapping at 2^32. This uses 16-bit operations internally - * to work around bugs in some JS interpreters. - */ -function safe_add(x, y) -{ - var lsw = (x & 0xFFFF) + (y & 0xFFFF); - var msw = (x >> 16) + (y >> 16) + (lsw >> 16); - return (msw << 16) | (lsw & 0xFFFF); -} - -/* - * Bitwise rotate a 32-bit number to the left. - */ -function bit_rol(num, cnt) -{ - return (num << cnt) | (num >>> (32 - cnt)); -} diff --git a/lib/parsers.js b/lib/parsers.js new file mode 100644 index 0000000..e5acadf --- /dev/null +++ b/lib/parsers.js @@ -0,0 +1,89 @@ +process.mixin(GLOBAL, require("sys")); +var oids = require('./type-oids'); +exports.DEBUG = 0; +// DATE parsing + +// parse a string like YYYY-MM-DD into +var date_regex = { + "ISO": /(\d{4})-(\d{2})-(\d{2}).*/, + "ISOr": "$2/$3/$1" +} +var time_regex = { + "ISO": /.*(\d{2}):(\d{2}):(\d{2}).*/, + "ISOr": "$1:$2:$3" +} +var tz_regex = { + "ISO": /.*:\d{2}-(\d{2})$/, + "ISOr": "GMT-$100" +} + +exports.parseDateFromPostgres = function (str,datestyle,OID) { + if (exports.DEBUG > 0) { + debug("parsing string: "+ str + " with datestyle: " + datestyle + " with OID: " + OID); + } + + + var style = datestyle.split(','); + var order = style[1]; style = style[0].replace(/^\s+|\s+$/,''); + + if (!(style in date_regex) && (exports.DEBUG > 0)) { + + sys.debug("Error datestyle not implemented: " + style); + } + + var date='',time='',tz=''; + switch(OID) { + case oids.TIMESTAMPTZ: + tz = str.replace(tz_regex[style],tz_regex[style+'r']); + if (exports.DEBUG > 0) { + debug("Timezone: " + tz); + } + if (tz == str) tz = ''; + case oids.TIMESTAMP: + case oids.DATE: + date = str.replace(date_regex[style],date_regex[style+'r']); + if (exports.DEBUG > 0) { + debug("date: " + date); + } + if (date == str) date = ''; + if (OID==oids.DATE) break; + case oids.TIME: + time = ' ' + str.replace(time_regex[style],time_regex[style+'r']); + if (exports.DEBUG > 0) { + debug("time: " + time); + } + if (time == str) time = ''; + } + + date = ((date=='')?'January 1, 1970':date) + ((time=='')?'':' ') + time + ((tz=='')?'':' ') + tz; + + if (exports.DEBUG > 0) { + debug("created date: " + date); + } + + var d = new Date(); + d.setTime(Date.parse(date)); + return d; +}; + +// turn a number 5 into a string 05 +function pad (p,num) { + num = ''+num; + return ((num.length==1)?p:'') + num; +} + +exports.formatDateForPostgres = function(d, OID) { + var date='',time='',tz=''; + switch(OID) { + case oids.TIMESTAMPTZ: + tz = '-' + d.getTimezoneOffset()/60; + case oids.TIMESTAMP: + case oids.DATE: + date = [d.getFullYear(),pad('0',d.getMonth()),pad('0',d.getDate())].join(''); + if (OID==oids.DATE) break; + case oids.TIME: + time = [pad('0',d.getHours()),pad('0',d.getMinutes()),pad('0',d.getSeconds())].join(':'); + } + + return date + ((time=='')?'':' ') + time + ((tz=='')?'':' ') + tz; +} diff --git a/lib/postgres-pure.js b/lib/postgres-pure.js index 678f288..5edba31 100644 --- a/lib/postgres-pure.js +++ b/lib/postgres-pure.js @@ -1,5 +1,6 @@ /* -Copyright (c) 2010 Tim Caswell +Copyright (c) 2010 Tim Caswell , + (c) 2010 Aurynn Shaw Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -21,10 +22,9 @@ THE SOFTWARE. */ -var md5 = require('./md5'); +var crypto = require("crypto"); var net = require("net"); var sys = require("sys"); -var sqllib = require('./sql'); var url = require('url'); var Buffer = require('./buffer_extras'); @@ -49,17 +49,85 @@ function encoder(header) { return w; } -// http://www.postgresql.org/docs/8.3/static/protocol-message-formats.html +// http://www.postgresql.org/docs/8.4/static/protocol-message-formats.html var formatter = { + Bind: function (portal, prepared_name, params, args) { + var builder = (encoder('B')) + .push.cstring(portal) + .push.cstring(prepared_name) + // .push.int16(args.length); // declare our format codes as expected. + .push.int16(0); // Text + /* + for (var i = 0; i < args.length; i++) { + switch (typeof args[i]) { + case "number": + case "boolean": + case "object": + if (args[i] instanceof Date) { + builder.push.int16(0); // Dates are Strings. + } + else { + builder.push.int16(1); // binary. This will need to be + // careful, as Date will be an object. + } + break; + case "string": // Any other type. + builder.push.int16(0); + break; + } + } + */ + + builder.push.int16(args.length); + for (var i = 0; i < args.length; i++) { + switch (typeof args[i]) { + case "number": + builder.push.int32(args[i].toString().length) // 4 bytes. int32. + .push.string(args[i].toString()); + break; + case "string": + builder.push.int32(args[i].length) + .push.string(args[i]); // Not a cstring. Don't \0 + break; + case "boolean": + builder.push.int32(1) // One byte. + .push.string(args[i] ? 1 : 0); + break; + case "object": + if (args[i] instanceof Date) { + + // This uses the UTC string, and tells PG to be in the UTC + // timezone, for ease of use. + // And it kind of requires that all fields in PG be timestamptz... + var l = args[i].toLocaleString(); + var a = l.split(" "); + var tz = a[a.length-2]; + a.length = a.length - 2; // Truncate the array + var v = a.join(" ") + "-" + tz.split("-")[1].slice(0, 2); + builder.push.int32(v.length).push.string(v); + break; + } + if (args[i] === null || args[i] === undefined) { + // this isn't right... + builder.push.int32(-1); + break; + } + }; + } + builder.push.int16(0); // They should all use text. Don't declare return + // types, as we already have the types from the + // ParameterDescription + return builder; + }, CopyData: function () { // TODO: implement }, CopyDone: function () { // TODO: implement }, - Describe: function (name, type) { + Describe: function (type, name) { return (encoder('D')) - .push.string(type) + .push.byte1(type) // Byte string aka ascii. .push.cstring(name); }, Execute: function (name, max_rows) { @@ -78,9 +146,12 @@ var formatter = { .push.cstring(name) .push.cstring(query) .push.int16(var_types.length); - var_types.each(function (var_type) { - builder.push.int32(var_type); - }); + for (var i = 0; i < var_types.length; i++) { + builder.push.int32(var_types[i]); + } + // var_types.each(function (var_type) { + // builder.push.int32(var_type); + // }); return builder; }, PasswordMessage: function (password) { @@ -111,361 +182,1256 @@ var formatter = { // Parse response streams from the server function parse_response(code, buffer) { - var input, type, args, num_fields, data, size, i; - reader = buffer.toReader(); - args = []; - switch (code) { - case 'R': - switch (reader.int32()) { - case 0: - type = "AuthenticationOk"; - break; - case 2: - type = "AuthenticationKerberosV5"; - break; - case 3: - type = "AuthenticationCleartextPassword"; - break; - case 4: - type = "AuthenticationCryptPassword"; - args = [reader.string(2)]; - break; - case 5: - type = "AuthenticationMD5Password"; - args = [reader.buffer(4)]; - break; - case 6: - type = "AuthenticationSCMCredential"; - break; - case 7: - type = "AuthenticationGSS"; - break; - case 8: - // TODO: add in AuthenticationGSSContinue - type = "AuthenticationSSPI"; - break; - default: - - break; - } - break; - case 'E': - type = "ErrorResponse"; - args = [{}]; - reader.multicstring().forEach(function (field) { - args[0][field[0]] = field.substr(1); - }); - break; - case 'S': - type = "ParameterStatus"; - args = [reader.cstring(), reader.cstring()]; - break; - case 'K': - type = "BackendKeyData"; - args = [reader.int32(), reader.int32()]; - break; - case 'Z': - type = "ReadyForQuery"; - args = [reader.string(1)]; - break; - case 'T': - type = "RowDescription"; - num_fields = reader.int16(); - data = []; - for (i = 0; i < num_fields; i += 1) { - data.push({ - field: reader.cstring(), - table_id: reader.int32(), - column_id: reader.int16(), - type_id: reader.int32(), - type_size: reader.int16(), - type_modifier: reader.int32(), - format_code: reader.int16() - }); - } - args = [data]; - break; - case 'D': - type = "DataRow"; - data = []; - num_fields = reader.int16(); - for (i = 0; i < num_fields; i += 1) { - size = reader.int32(); - if (size === -1) { - data.push(null); - } else { - data.push(reader.string(size)); - } + var input, type, args, num_fields, data, size, i; + reader = buffer.toReader(); + args = []; + switch (code) { + case 'R': + switch (reader.int32()) { + case 0: + type = "AuthenticationOk"; + break; + case 2: + type = "AuthenticationKerberosV5"; + break; + case 3: + type = "AuthenticationCleartextPassword"; + break; + case 4: + type = "AuthenticationCryptPassword"; + args = [reader.string(2)]; + break; + case 5: + type = "AuthenticationMD5Password"; + args = [reader.buffer(4)]; + break; + case 6: + type = "AuthenticationSCMCredential"; + break; + case 7: + type = "AuthenticationGSS"; + break; + case 8: + // TODO: add in AuthenticationGSSContinue + type = "AuthenticationSSPI"; + break; + default: + break; + } + break; + case 'E': + type = "ErrorResponse"; + var err = {}; + // args = [{}]; + reader.multicstring().forEach(function (field) { + err[field[0]] = field.substr(1); + }); + // Now, convert it to a more useful object. + var obj = new Error(); + obj.code = err['C']; + obj.message = err['M']; + obj.severity = err['S']; + + args = [obj]; + break; + case 't': + type = "ParameterDescription", + num_fields = reader.int16(); + data = []; + for (var i = 0; i < num_fields; i++) { + data.push(reader.int32()); + } + args = [data]; + break; + case 'S': + type = "ParameterStatus"; + args = [reader.cstring(), reader.cstring()]; + break; + case 'K': + type = "BackendKeyData"; + args = [reader.int32(), reader.int32()]; + break; + case 'Z': + type = "ReadyForQuery"; + args = [reader.string(1)]; + break; + case 'T': + type = "RowDescription"; + num_fields = reader.int16(); + data = []; + for (var i = 0; i < num_fields; i += 1) { + data.push({ + field: reader.cstring(), + table_id: reader.int32(), + column_id: reader.int16(), + type_id: reader.int32(), + type_size: reader.int16(), + type_modifier: reader.int32(), + format_code: reader.int16() + }); + } + args = [data]; + break; + case 'D': + type = "DataRow"; + data = []; + num_fields = reader.int16(); + for (i = 0; i < num_fields; i += 1) { + size = reader.int32(); + if (size === -1) { + data.push(null); + } else { + data.push(reader.string(size)); + } + } + args = [data]; + break; + case 'C': + type = "CommandComplete"; + args = [reader.cstring()]; + break; + case 'N': + type = "NoticeResponse"; + args = [{}]; + reader.multicstring().forEach(function (field) { + args[0][field[0]] = field.substr(1); + }); + break; + case '1': + type = 'ParseComplete'; + args = [{}]; + break; + case 'n': + type = 'NoData'; + args = []; + break; + case '2': + type = "BindComplete"; + args = [{}]; + break; + case 'A': + type = "Notify"; + args = [{'pid': reader.int32(), 'name': reader.cstring(), 'payload': reader.cstring()}]; + break; } - args = [data]; - break; - case 'C': - type = "CommandComplete"; - args = [reader.cstring()]; - break; - case 'N': - type = "NoticeResponse"; - args = [{}]; - reader.multicstring().forEach(function (field) { - args[0][field[0]] = field.substr(1); - }); - break; - } - if (!type) { - sys.debug("Unknown response " + code); - } - return {type: type, args: args}; + if (!type) { + sys.debug("Unknown response " + code); + } + return {type: type, args: args}; } +function Error () { + this.severity = ""; + this.code = ""; + this.message = ""; + this.toString = function () { + return "[postgres.js] " + this.severity + ": " + this.message; + } +} -function Connection(args) { - if (typeof args === 'string') { - args = url.parse(args); - args.database = args.pathname.substr(1); - args.auth = args.auth.split(":"); - args.username = args.auth[0]; - args.password = args.auth[1]; - } - var started, conn, connection, events, query_queue, row_description, query_callback, results, readyState, closeState; - - // Default to port 5432 - args.port = args.port || 5432; +function message () { + var self = this; + this.messages = []; // public instance array. + var pos = 0; + this._position = 0; // instance variable + this.setMessages = function (msg) { + if (msg instanceof Array) { + this.messages = msg; + } + this.length = this.messages.length; + } + this.addMessage = function (msg) { + if (msg != null && msg != undefined) { + this.messages.push(msg); + this.length = this.messages.length; + } + } + this.next = function() { + if (exports.DEBUG > 3) { + sys.debug("message pos: " + this._position); + sys.debug("messages:" + sys.inspect(this.messages)); + } + if (this.messages[this._position] !== null && this.messages[this._position] !== undefined) { + this._position = this._position + 1; + if (exports.DEBUG > 3){ + sys.debug("Returning: " + sys.inspect(this.messages[this._position-1])); + } + return this.messages[this._position-1]; + } + if (exports.DEBUG > 3) { + sys.debug("pos " + this._position + " exceeds internal message buffer."); + sys.debug("Returning null"); + } + return null; + } + this.empty = function () { + if (exports.DEBUG > 3){ + sys.debug("message pos " + this._position + " exceeds length "+ this.messages.length +": " + (this._position >= this.messages.length)); + } + if (this._position >= this.messages.length) { + return true; + } + return false; + } +} - // Default to host 127.0.0.1 - args.hostname = args.hostname || "127.0.0.1"; +message.prototype = new process.EventEmitter; +// message.prototype.constructor = message; +function Query(sql, callback) { + var self = this; + message.call(this); + this.sql = sql; + this.results = []; + var pos = 0; + this.callback = callback; + /* + Returns the next query object in this object buffer. + This can be null. + + Should there be a Sync message here? + */ + this.setMessages([ + { + type: 'Query', + args: [this.sql] + }, + // { + // type: 'Flush', + // args: [] + // } + ]); + this.on("newRow", function (row) { + // sys.debug("Got row: " + sys.inspect(row)); + // sys.debug(sys.inspect(self.results)); + self.results.push(row); + }); + this.on("Complete", function (data) { + if (exports.DEBUG > 2) { + sys.debug("Callback: " + self.callback); + } + + self.callback(null, self.results); + }); + this.toString = function () { return "Query: " + this.length}; + this.on("RowDescription", function (desc) { + self.row_description = desc; + if (exports.DEBUG > 2) { + sys.debug("Caught RowDescription message."); + } + }); + this.on("ErrorResponse", function (e) { + self.callback(e); + }); + // sys.debug(this.listeners("newRow")); +} - connection = net.createConnection(args.port, args.hostname); - events = new process.EventEmitter(); - query_queue = []; - readyState = false; - closeState = false; - started = false; - conn = this; - - // Disable the idle timeout on the connection - connection.setTimeout(0); +Query.prototype = new message; +Query.prototype.constructor = Query; - // Sends a message to the postgres server - function sendMessage(type, args) { - var buffer = (formatter[type].apply(this, args)).frame(); - if (exports.DEBUG > 0) { - sys.debug("Sending " + type + ": " + JSON.stringify(args)); - if (exports.DEBUG > 2) { - sys.debug("->" + buffer.inspect().replace('<', '[')); - } +function Prepared(sql, conn /*, use_named */) { + // Per #postgresql on freenode, use the unnamed prepared statement to + // increase performance for queries. + + // var prepared_name = md5(sql); // Use the md5 hash. This is easily selectable later. + + var self = this; + message.call(this); + self.row_description = null; + self.parameters = null; + self.noData = false; + self.sql = sql; + + var parseComplete = null; + var readyToExec = false; + var conn = conn; + var name = ''; + var pos = 0; + var callback = callback; + var arr = [ + { + type: "Parse", + // Prepared name, the query, + // and a zero-length array to declare no types. + args: ['', self.sql, []], + }, + { + type: "Describe", + args: ["S", ''], // The unnamed prepare. + }, + // { + // type: "Flush", + // args: [], + // }, + ]; // This describes a (nearly) complete lifecycle of a prepared statement. + + self.setMessages(arr); + + self.on("ParseComplete", function () { + // Execute can now be run successfully. + // Until this point, we can't assume that there's a matching query. + // Anyway, we now run a DESCRIBE operation, and store the row + // description in our object. + // Later optimization might hold on to these objects as hashes in the + // connection object. + // conn.next(); + parseComplete = true; + }); + + self.on("RowDescription", function (desc) { + // this should be called second. + self.row_description = desc; + }); + + var execute = []; + + // Describes what parameters we should be sending. + self.on("ParameterDescription", function (desc) { + self.parameters = desc; + }); + + self.on("newRow", function (row) { + currExec.emit("newRow", row); + }); + + /* + Executing the function tests whether or not + we've been passed an argument list. + If we have been, then we need to issue a BIND on the wire. + If we haven't been, we can move straight to EXECUTE. + */ + var eB = []; + var currExec = null; + var cPos = 0; + self.on("Complete", function (data) { + if (currExec != null) { + currExec.emit("Complete", data); // Fires the callback. + currExec = eB[cPos++]; // So we don't lose the reference to the + // execute command. + if (currExec != null && currExec != undefined) { + currExec.args.forEach(function (i) { + if (exports.DEBUG > 2) { + sys.debug("Arg is: " + i.type); + } + self.addMessage(i); + }); + } + } + }); + + self.on("NoData", function () { + if (currExec !== null) { + currExec.noData = true; + } + }); + + self.on("BindComplete", function () { + if (currExec != null) { + currExec.emit("BindComplete"); + } + }); + + self.on("ErrorResponse", function (err) { + if (currExec != null) { + currExec.emit("ErrorResponse", err); + } + else { + self.error = e; + } + }); + + // self.empty = function () { + // return self.__proto__.empty.call(self); + // } + + self.next = function () { + // Override. + var msg = this.__proto__.next.call(this); + if (exports.DEBUG > 3) { + sys.debug("currexec " + currExec); + sys.debug("msg" + msg); + } + if (msg !== null) { + return msg; + } + else if (currExec === null) { + + if (exports.DEBUG > 0) { + sys.debug("eB is " + eB); + } + if (eB[cPos] !== null && eB[cPos] !== undefined) { + currExec = eB[cPos++]; // So we don't lose the reference to the + // execute command. + if (exports.DEBUG > 3) { + sys.debug(sys.inspect(currExec.args.slice(1))); + } + currExec.args.forEach(function (i) { + if (exports.DEBUG > 2) { + sys.debug("Adding Arg: " + i.type); + } + self.addMessage.call(self, i); + }); + if (exports.DEBUG > 2) { + sys.debug("Calling next from prototype."); + } + return self.__proto__.next.call(this); + } + } + if (exports.DEBUG > 0) { + sys.debug("currExec isn't null, returning null from next"); + } + + return null; // Patience.. } - connection.write(buffer); - } - - var queue = []; - function checkInput() { - if (queue.length === 0) { return; } - var first = queue[0]; - var code = String.fromCharCode(first[0]); - var length = first.int32Read(1) - 4; - - // Make sure we have a whole message, TCP comes in chunks - if (first.length < length + 5) { - if (queue.length > 1) { - // Merge the first two buffers - queue.shift(); - var b = new Buffer(first.length + queue[0].length); - first.copy(b); - queue[0].copy(b, first.length); - queue[0] = b; - return checkInput(); - } else { - return; - } + + self.execute = function () { + // If the first argument is an array, then we use that as our bind + // parameters. Otherwise, arguments[0] should be a function. + if (exports.DEBUG > 0) { + sys.debug("Execute got called."); + } + var args = Array.prototype.slice.call(arguments, 0); + var callback = null; + + if (typeof(args[args.length -1]) == 'function' ) { + callback = args.pop(); + } + else { + // No callback? That's an error. + self.emit("error", "Cannot execute without callback."); + return; + } + // The rest of args is now the arguments to pass to the bound + // query, if any. + + readyToExec = true; + var eP; + eP = new process.EventEmitter(); + //eP.portal = md5(name + args.join("|")); // Clearly need a generated portal name here. + eP.portal = ''; + eP.bound = false; + eP.results = []; + eP.args = [ + { + type: "Execute", + args: [eP.portal, 0], + }, + { + type: "Flush", + args: [] + }, + { + type: "Sync", + args: [] + } + ]; + if (args.length > 0) { + eP.args.unshift({ + type: "Bind", + args:[eP.portal, name, self.parameters, args] + }); + } + eP.on("Complete", function () { + if (exports.DEBUG > 0) { + sys.debug("Results length " + eP.results.length); + sys.debug("noData is: "+eP.noData); + } + if (eP.results.length > 0 && eP.bound) { + if (exports.DEBUG > 0) { + sys.debug("Execute Complete: Calling with results"); + } + // Callback gets wrapped at the tx layer, + // so we know when this gets tripped. + callback(null, eP.results); + } + else { + + console.dir(sys.inspect(callback)); + callback(null, []); + } + }); + eP.on("BindComplete", function () { + eP.bound = true; + }); + eP.on("NoData", function () { + + eP.noData = true; + }); + eP.on("newRow", function (row) { + eP.results.push(row); + }); + eP.on("ErrorResponse", function (e) { + callback(e); + }); + if(exports.DEBUG > 0) { + sys.debug("Pushing execute message to eB in Prepared."); + } + eB.push(eP); } - var message = first.slice(5, 5 + length); - if (first.length === 5 + length) { - queue.shift(); - } else { - queue[0] = first.slice(length + 5, first.length); +} +Prepared.prototype = new message(); +Prepared.prototype.constructor = Prepared; + + +/* Initializes a connection to the database. +DB connections are of the form: + +pgsql://user:password@hostname:port/databasename + +*/ + +function Connection(args) { + if (typeof args === 'string') { + args = url.parse(args); + args.database = args.pathname.substr(1); + args.auth = args.auth.split(":"); + args.username = args.auth[0]; + args.password = args.auth[1]; } + var started, conn, connection, + events, query_queue, current_query, + results, readyState, closeState, + tx_queue, current_tx, queryEmpty; + + // Default to port 5432 + args.port = args.port || 5432; + + // Default to host 127.0.0.1 + args.hostname = args.hostname || "127.0.0.1"; + + connection = net.createConnection(args.port, args.hostname); + events = new process.EventEmitter(); + readyState = false; + closeState = false; + started = false; + conn = this; + current_query = null; + current_tx = null + tx_queue = []; + results = []; + queryEmpty = true; + var wait = false; + var txState = null; + + var notifications = new process.EventEmitter(); + + // Disable the idle timeout on the connection + connection.setTimeout(0); - if (exports.DEBUG > 1) { - sys.debug("stream: " + code + " " + message.inspect()); + // Sends a message to the postgres server + function sendMessage(type, args) { + if (exports.DEBUG > 0 ) { + sys.debug("Got type of "+type) + } + var buffer = (formatter[type].apply(this, args)).frame(); + if (exports.DEBUG > 0) { + sys.debug("Sending " + type + ": " + JSON.stringify(args)); + if (exports.DEBUG > 2) { + sys.debug("->" + buffer.inspect().replace('<', '[')); + } + } + connection.write(buffer); + events.emit("nextMessage"); + // if (current_query) { + // conn.next(current_query); // We don't always expect to get a response message. + // // And if we do, the message object can sort it out. + // } } - command = parse_response(code, message); - if (command.type) { - if (exports.DEBUG > 0) { - sys.debug("Received " + command.type + ": " + JSON.stringify(command.args)); - } - command.args.unshift(command.type); - events.emit.apply(events, command.args); + + var queue = []; + /* Parses a message from the PG server */ + function checkInput() { + if (queue.length === 0) { return; } + var first = queue[0]; + var code = String.fromCharCode(first[0]); + var length = first.int32Read(1) - 4; + + // Make sure we have a whole message, TCP comes in chunks + if (first.length < length + 5 || isNaN(length)) { + if (queue.length > 1) { + // Merge the first two buffers + queue.shift(); + var b = new Buffer(first.length + queue[0].length); + first.copy(b); + queue[0].copy(b, first.length); + queue[0] = b; + return checkInput(); + } else { + return; + } + } + // What does this do? -AS + var message = first.slice(5, 5 + length); + if (first.length === 5 + length) { + queue.shift(); + } else { + queue[0] = first.slice(length + 5, first.length); + } + + if (exports.DEBUG > 1) { + sys.debug("stream: " + code + " " + message.inspect()); + } + // This shouldn't block. + // TODO: Rewrite into a callback. + command = parse_response(code, message); + if (command.type) { + if (exports.DEBUG > 0) { + sys.debug("Received " + command.type + ": " + JSON.stringify(command.args)); + } + command.args.unshift(command.type); + // Uses a selective emitter. + // First, tests whether or not the executing query listens to the event. + // This permits a given query to take over selected aspects of the + // If not, fires on the primary (connection) event loop. + if (exports.DEBUG > 0) { + sys.debug("current_query is null: "+ current_query !== null); + if (current_query !== null) { + sys.debug("current_query listeners: " + current_query.listeners(command.type).length); + } + } + if (current_query !== null && current_query.listeners(command.type).length >= 1) { + if (exports.DEBUG > 0) { + sys.debug("Sending "+command.type+" to current_query"); + } + current_query.emit.apply(current_query, command.args); + } + else { + if (exports.DEBUG > 0) { + sys.debug("Sending "+command.type+" to local handler"); + } + events.emit.apply(events, command.args); + } + } + checkInput(); } - checkInput(); - } - // Set up tcp client - connection.addListener("connect", function () { - sendMessage('StartupMessage', [{user: args.username, database: args.database}]); - }); - connection.addListener("data", function (data) { - if (exports.DEBUG > 2) { - sys.debug("<-" + data.inspect()); - } - queue.push(data); - checkInput(); - }); - connection.addListener("end", function (data) { - connection.end(); - }); - connection.addListener("disconnect", function (had_error) { - if (had_error) { - sys.debug("CONNECTION DIED WITH ERROR"); - } - }); - - // Set up callbacks to automatically do the login and other logic - events.addListener('AuthenticationMD5Password', function (salt) { - var result = "md5" + md5(md5(args.password + args.username) + salt.toString("binary")); - sendMessage('PasswordMessage', [result]); - }); - events.addListener('AuthenticationCleartextPassword', function () { - sendMessage('PasswordMessage', [args.password]); - }); - events.addListener('ErrorResponse', function (e) { - conn.emit('error', e.S + ": " + e.M); - if (e.S === 'FATAL') { - connection.end(); - } - }); - events.addListener('ReadyForQuery', function () { - if (!started) { - started = true; - conn.emit('connection'); - } - if (query_queue.length > 0) { - var query = query_queue.shift(); - query_callback = query.callback; - row_callback = query.row_callback; - sendMessage('Query', [query.sql]); - readyState = false; - } else { - if (closeState) { + // Set up tcp client + connection.on("connect", function () { + sendMessage('StartupMessage', [{user: args.username, database: args.database}]); + }); + connection.on("data", function (data) { + if (exports.DEBUG > 2) { + sys.debug("<-" + data.inspect()); + } + queue.push(data); + checkInput(); + }); + connection.on("end", function (data) { connection.end(); - } else { - readyState = true; - } + }); + connection.on("disconnect", function (had_error) { + if (had_error) { + sys.debug("CONNECTION DIED WITH ERROR"); + } + }); + + // Set up callbacks to automatically do the login and other logic + events.on('AuthenticationMD5Password', function (salt) { + var result = "md5" + crypto.createHash("md5").update( + crypto.createHash("md5").update(args.password + args.username).digest("hex") + salt.toString("binary") + ).digest("hex"); + sendMessage('PasswordMessage', [result]); + }); + events.on('AuthenticationCleartextPassword', function () { + sendMessage('PasswordMessage', [args.password]); + }); + + /* + Errors shuld be handled at the TX layer. + This allows for a given TX set (which contains 1..n message sets) to + catch and gracefully recover from an error state, and not attempt to + continue to slam messages onto the wire when there isn't an ability to + do so. + */ + + events.on('ErrorResponse', function (e) { + conn.emit('error', e.S + ": " + e.M); + if (e.S === 'FATAL') { + connection.end(); + } + }); + + // Should this be handled at the tx level? + events.on('ReadyForQuery', function (state) { + if (exports.DEBUG > 0) { + sys.debug("In RFQ"); + } + txState = state; + if (!started) { + started = true; + conn.emit('connection'); + } + + // We can cycle to the next? + + if (exports.DEBUG > 3) { + sys.debug("Readystate is: " + readyState); + sys.debug("TX Queue length: "+tx_queue.length); + sys.debug("TX is "+ current_tx); + } + readyState = true; + if (current_tx === null ) { + events.emit("nextTx"); + } + else if ( current_query === null || current_query === undefined || current_query.empty() === true ) { + if (exports.DEBUG > 3) { + sys.debug("3: Current query is empty. Moving to next query."); + } + events.emit("nextQuery"); + } + else { + // See if we can move forwards. + events.emit("nextMessage"); + } + }); + + events.on("canClose?", function () { + if ((tx_queue.length === 0 + && ( current_tx === null || current_tx.can_release() ) + && ( current_query === null || current_query.empty() ) ) // this must all be true. + && (closeState === true && readyState === true) + ) { + if (exports.DEBUG > 1) { + sys.debug("Closing connection."); + } + closeState = true; + connection.end(); + } + }); + + // Pumps the current_query queue via .next() + // and runs whatever we get back. + events.on("nextMessage", function () { + if (exports.DEBUG > 0) { + sys.debug("got nextMessage"); + } + + // pulls the next message from the current query. + // If it's null, it calls nextQuery. + + if (current_query === null) { + if (exports.DEBUG >1 ) { + sys.debug("nextMessage: Getting next message block"); + } + + events.emit("nextQuery"); + } + else /* if ( current_query.empty() === false ) */ { + var msg = current_query.next(); + if (exports.DEBUG > 3) { + sys.debug(msg); + } + + if (msg && msg.type && msg.args) { + // We have what we need to perform this query. + if (exports.DEBUG > 0) { + sys.debug("Sending message: " + msg.type + ": " + msg.args); + } + readyState = false; + sendMessage.apply(conn, [msg.type, msg.args]); + } + if (exports.DEBUG > 2){ + sys.debug("waiting for RFQ"); + } + } + }); + + events.on("nextQuery", function () { + if (exports.DEBUG > 0) { + sys.debug("got nextQuery"); + } + // if (readyState) { + if ( current_tx !== null && current_tx !== undefined) { + current_query = current_tx.next(); + if (current_query !== null && current_query !== undefined) { + events.emit("nextMessage"); + } + else { + if (exports.DEBUG > 0) { + sys.debug("next query is null."); + } + events.emit("nextTx"); + } + } + else if (tx_queue.length > 0) { + events.emit("nextTx"); + } + else { + // if (exports.DEBUG > 0) { + // sys.debug("calling canClose? from nextQuery"); + // } + // events.emit("canClose?"); + } + // } + }); + + events.on("nextTx", function () { + if (exports.DEBUG > 0) { + sys.debug("got nextTx"); + } + if (txState == 'I') { + if (readyState) { + if (tx_queue.length > 0) { + if (exports.DEBUG > 1) { + sys.debug("nextTx: tx_queue length is: " + tx_queue.length); + } + current_tx = tx_queue.shift(); + if (current_tx !== null && current_tx !== undefined) { + if (exports.DEBUG>2) { + sys.debug("nextTx: current_tx is " + sys.inspect(current_tx)); + } + events.emit("nextQuery"); + } + else { + if (exports.DEBUG > 0) { + sys.debug("next TX is null."); + } + } + } + else { + if (exports.DEBUG > 0) { + sys.debug("calling canClose? from nextTx"); + } + events.emit("canClose?"); + } + } + } + }); + + + // This should always be caught by the current query. + events.on("RowDescription", function (data) { + row_description = data; + results = []; + }); + + + // Data row is handled by the connection for the time + // being, even though we should be looking at handling it in the + // query object, where the RowDescription lives. + events.on("DataRow", function (data) { + var row, i, l, description, value; + row = {}; + l = data.length; + for (i = 0; i < l; i += 1) { + description = current_query.row_description[i]; + value = data[i]; + if (value !== null) { + // Type OIDs are stable, more or less. They're unlikely to change. + // see pg_type.h for the defined values. + switch (description.type_id) { + case 16: // bool + value = value === 't'; + break; + case 20: // int8 + case 21: // int2 + case 23: // int4 + value = parseInt(value, 10); + break; + case 1082: // Date + value = new Date(value); + break; + case 1114: // Timestamp, no timezone + value = new Date(value); + break; + case 1184: // Timestamp, with timezone + // Initial value: + // "2011-02-01 21:00:52.353444-07" + // Needs to become: + // "2011-02-01 21:00:52.353 GMT-0700" + if (value[value.length-1].toLowerCase() == 'z') { + // It's in UTC time + // So, we add the appropriate modifiers. + var tz = value.slice(0, value.length-1); + tz = tz.slice(0, tz.length-4); + value = new Date(tz + " GMT+0000" ); + } + else { + var tz = value.slice(value.length-3, value.length); // last three. + var orig = value; + orig = orig.slice(0, orig.length-7); + value = new Date(orig + " GMT"+tz+"00"); + } + } + } + row[description.field] = value; + } + if (exports.DEBUG > 0) { + sys.debug(current_query.listeners("newRow").length); + sys.debug(current_query); + } + if (current_query.listeners("newRow").length > 0) { + current_query.emit("newRow", row); + } + else { + results.push(row); + } + }); + + + events.on('CommandComplete', function (data, results) { + if (results != null && results.length > 0) { + // To allow for insert..returning + current_query.emit("Complete", results, data); + results = []; // blank the current result buffer. + } + else { + // Send the typing information. + current_query.emit("Complete", data); + } + // query_callback.call(this, results); + //readyState = true; + }); + + events.on("Notify", function (args) { + /* Name is the name of the notification + Payload is the string as sent from postgres. + */ + notifications.emit(args['name'], args['payload']); // Bubble through. + }); + + conn.query = function query(/*query, args, callback */) { + + // Not sure I like this. + // I think this should be wrapped in a tx object. + var tx = conn.tx(); + var args = Array.prototype.slice.call(arguments); + + var callback = args.pop(); + + if (typeof(callback) === 'function') { + args.push(function (err, rs) { + // do the implicit sync/commit here? + // tx.commit(); + callback(err, rs, tx); // So they have access to the transaction? + }); + } + else { + // No callback. + args.push(callback); // re-add it. + args.push(function (rs) { + /* + This becomes the implied callback. + For the moment, do nothing. + */ + }); + } + tx.query.apply(tx, args); + events.emit("queryAdded"); + }; + + conn.prepare = function prepare(query, callback) { + // Sets up a prepared query, and drops it onto the queue. + + var tx = conn.tx(); // automatically pushes onto the stack. + var cb = null; + if (typeof(callback) === 'function') { + cb = function () { + // do the implicit sync/commit here? + callback.apply(callback, arguments); + } + if (exports.DEBUG > 3) { + sys.debug("Transaction is: " + tx); + sys.debug("Query is: " + query); + } + tx.prepare.call(tx, query, cb); + events.emit("queryAdded"); + } + else { + // They didn't give us a prepared callback? That's.. odd. + conn.emit("error", "Cannot prepare query without callback"); + } } - }); - events.addListener("RowDescription", function (data) { - row_description = data; - results = []; - }); - events.addListener("DataRow", function (data) { - var row, i, l, description, value; - row = {}; - l = data.length; - for (i = 0; i < l; i += 1) { - description = row_description[i]; - value = data[i]; - if (value !== null) { - // TODO: investigate to see if these numbers are stable across databases or - // if we need to dynamically pull them from the pg_types table - switch (description.type_id) { - case 16: // bool - value = value === 't'; - break; - case 20: // int8 - case 21: // int2 - case 23: // int4 - value = parseInt(value, 10); - break; + + // Sets up a new transaction object/block. + // Unsure if this should do an implicit BEGIN. + // Maybe a setting? + this.transaction = function (callback) { + var tx = new Transaction(this); + tx.on("ranCallback", function () { + if (exports.DEBUG >0 ) { + sys.debug("Received notification of callback execution."); + } + }); + if (callback !== null && callback !== undefined) { + callback(tx); } - } - row[description.field] = value; + conn.push(tx); + return tx; } - if (row_callback) { - row_callback(row); - } else { - results.push(row); + // Alias function. + this.tx = function () { + return conn.transaction(); } - }); - events.addListener('CommandComplete', function (data) { - query_callback.call(this, results); - }); - - conn.execute = function (sql/*, *parameters*/) { - var parameters = Array.prototype.slice.call(arguments, 1); - var callback = parameters.pop(); - - // Merge the parameters in with the sql if needed. - sql = sqllib.merge(sql, parameters); - // TODO: somehow give the query_queue a hint that this isn't query and it - // can optimize. - query_queue.push({sql: sql, callback: function () { - callback(); - }}); + this.close = function () { + closeState = true; + + // Close the connection right away if there are no pending queries + if (readyState) { + connection.end(); + } + }; + events.on("queryAdded", function () { + if (readyState) { + events.emit("nextMessage"); + } + }); + + + // Pushes a new transaction into the transaction pool, assuming + // that the connection hasn't (yet) been closed. + // As transactions use internal buffers for query messages, this won't + // immediately interfere with attempts to add messages. + conn.push = function (tx) { + if (!closeState) { + tx_queue.push(tx); + } + else { + conn.emit("error", "Cannot add commands post-closure."); + } + } - if (readyState) { - events.emit('ReadyForQuery'); + conn.next = function (query) { + if (query === current_query) { + events.emit("nextMessage"); + } + // If it's not, why are you doing this? } - }; + /* + Keeps watch for the addition of listeners on our connection. + This allows for monitoring the driver for notification requests, which, + in turn, allows us to catch that the user wants this particular + notification from the DB watched for. + + Ergo, we set up a DB listener with the same name, and fire our emitter + when it's triggered. + + Easy, and very nifty. + */ + // conn.on('newListener', function (e, listener) { + // if (e === 'String') { + // // It's a string. + // if (!(e in ['newListener'])) { + // conn.notify(e, listener); + // } + // } + // }); + conn.notify = function (name, callback) { + // Sets up a listener for the NOTIFY event from the database. + // Works by queuing up a LISTEN event in the DB (if it's not already + // been registered on this connection), and adding the event (with + // callback) to the event. + + this.query("LISTEN "+ name, function (err,rs) { + /* Doesn't need to do anything */ + if (err !== null) { + callback(err); + } + }); + + notifications.on( name, function (payload) { callback(null, payload) }); + events.emit("queryAdded"); + } +} +Connection.prototype = new process.EventEmitter(); - conn.query = function query(sql/*, *parameters, row_callback*/) { - var row_callback, parameters, callback; +/* Block object +A barebones object to represent multiple queries on the wire. +These are handed to the connectionManager to handle concurrency. To whit, +a "block" is a grouping of queries associated with some greater context. - // Grab the variable length parameters and the row_callback is there is one. - parameters = Array.prototype.slice.call(arguments, 1); - callback = parameters.pop(); - if (typeof parameters[parameters.length - 1] === 'function') { - row_callback = parameters.pop(); +*/ +function Transaction (connection /*, params */) { + var conn = connection; // Associated connection. + var thisp = this; + var current_statement = null; + var messages = []; + // Error watcher, for wire errors. + + var message_length = 0; + + // Whether or not I can add more stuff to this transaction + // Marked true by the Connection. + var synced = false; + var closed = false; + + this.errors = new process.EventEmitter(); + + events = new process.EventEmitter(); + + events.on("queryAdded", function () { + message_length += 1; + }); + + // This acts effectively as a decorator from Python + var wrap = function (func) { + return (function () { + if (exports.DEBUG > 3) { + sys.debug("Wrapping function: " + func); + } + func.apply(func, arguments); + }); } - - // Merge the parameters in with the sql if needed. - if (parameters.length > 0) { - sql = sqllib.merge(sql, parameters); + + // The basic Query declaration. + // This, by default, acts as a simple query when run without arguments + // (allowing for certain queries to be handled faster), + // and forcibly using the parameterized style in the event that arguments + // are passed, using PG's normal argument processing and + // escaping rules. + // This should act to reduce most instances of issue with people trying + // to write their own SQL escaping. + + /* sql, some_args, callback */ + this.query = function () { + var args = Array.prototype.slice.call(arguments); + if (exports.DEBUG > 3) { + sys.debug("Args are: " + args); + } + var sql = args.shift(); + var callback = args.pop(); + if (args.length >0) { + // We now have a prepared query. + thisp.prepare(sql, function (sth) { + // Add the callback to the args list, so + args.push(wrap(callback)); + // we can use apply properly. + sth.execute.apply(sth, args); + }); + } + else { + // We have an otherwise normal query. + // This does not require a normal Sync message + // or any other such magic-ery. + + var q = new Query(sql, function (err, rs) { + if (exports.DEBUG > 3) { + sys.debug("RS is:" + sys.inspect(rs)); + } + callback(err, rs); + // callbackRun(); + }); + if (exports.DEBUG > 0) { + sys.debug("New plain query created: "+q); + } + this.push(q); + } } - - if (row_callback) { - query_queue.push({sql: sql, row_callback: row_callback, callback: function () { - callback(); - }}); - } else { - query_queue.push({sql: sql, callback: function (data) { - callback(data); - }}); + + // Standard prepared query. + + this.prepare = function (sql, callback) { + + // Sets up a prepared query, and drops it onto the queue. + if (sql.match(/\?/)) { + var i = 1; + var fsql = sql.replace(/\?/g, function () { return "$" + i++; }); + } + var p = new Prepared(fsql, thisp); + thisp.push(p); + events.emit("queryAdded"); + wrap(callback)(p, thisp); + if (exports.DEBUG == 4) { + sys.debug("Prepared messages: " + sys.inspect(messages, 4)); + } + // conn.emit.call(conn, "queryAdded"); } - if (readyState) { - events.emit('ReadyForQuery'); + this.can_release = function () { + // returns boolean + if (messages.length > 0) { + return false; + } + return true; } - }; - - this.end = function () { - closeState = true; - - // Close the connection right away if there are no pending queries - if (readyState) { - connection.end(); + this.close = function () { + closed = true; + thisp.sync(); + } + + var tx_control_error = function (err, rs) { + if (null !== err) { + // if blue is not the sky, + thisp.sync(); // We need to clear the error. + // What else do we do as an error handler? + sys.debug("DB error: " + err); + } + // sys.debug("Callback called!"); + // do nothing else. We're good. + } + + this.begin = function () { + // Begins the transaction. We now lock the transaction to the wire. + thisp.query("BEGIN", tx_control_error); + } + this.rollback = function () { + // Rolls back the request, and does the connection release. + // this.sync(); + this.query("ROLLBACK", tx_control_error); + + } + this.commit = function () { + // Commits this block of stuff that's happened, via the SYNC message. + // This will also cause a rollback if there's been errors. + // this.sync(); + this.query("COMMIT", tx_control_error); + } + + this.sync = function () { + thisp.push(new Sync()); + } + + this.push = function (msg) { + if (!closed) { + messages.push(msg); + this.emit("queryAdded"); + if (exports.DEBUG > 0) { + sys.debug("Added message of " + sys.inspect(msg) + " to TX"); + } + } + else { + thisp.emit("error", "Transaction no longer valid!"); + } } - }; + this.next = function () { + if (messages.length > 0) { + if (messages[0] !== null && messages[0] !== undefined) { + if (exports.DEBUG == 4) { + sys.debug("TX Returning: " + sys.inspect(messages[0])); + } + return messages.shift(); // Front of the array, there. + } + } + if (exports.DEBUG > 0){ + sys.debug("tx: Returning null from tx.next()"); + } + return null; + } + this.on("Error", function (e) { + /* Global transaction error response. + This should push a SYNC message into the buffer immediately, + as well as a ROLLBACK command. + This will free up the wire from whatever the last message set was, + and allow for a given piece of code to recover gracefully. + */ + }); +} + +Transaction.prototype = new process.EventEmitter(); + +// This will eventually be the return object +/* +Should expose the same API as the Connection object, just hold multiple copies +of connections open for better performance. +Woo. +Notifications get written across all possible connections (just in case) +*/ +function connectionManager (dsn /*, connections=1 */) { + // var conn = new Connection(dsn); + } -Connection.prototype = new process.EventEmitter(); -Connection.prototype.get_store = function (name, columns) { - return new sqllib.Store(this, name, columns, { - do_insert: function (data, keys, values, callback) { - this.conn.query("INSERT INTO " + - this.name + "(" + keys.join(", ") + ")" + - " VALUES (" + values.join(", ") + ")" + - " RETURNING _id", - function (result) { - data._id = parseInt(result[0]._id, 10); - callback(data._id); - } - ); - }, - index_col: '_id', - types: ["_id SERIAL"] - }); -}; -exports.Connection = Connection; +exports.connect = Connection; \ No newline at end of file diff --git a/lib/sql.js b/lib/sql.js deleted file mode 100644 index d2b338c..0000000 --- a/lib/sql.js +++ /dev/null @@ -1,244 +0,0 @@ -/* -Copyright (c) 2010 Tim Caswell - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ - -// Escape of values from native to SQL string. -function sql_escape(value) { - if (value === null) { - return "NULL"; - } - if (value === true) { - return "TRUE"; - } - if (value === false) { - return "FALSE"; - } - if (value.constructor.name === 'String') { - return "'" + value.replace("'", "''") + "'"; - } - return value.toString(); -} - -// Fill in the placeholders with native values -function merge(sql, parameters) { - if (parameters.length === 0) { - return sql; - } - if (parameters.length === 1 && parameters[0].constructor.name === 'Object') { - parameters = parameters[0]; - // Named parameters - for (var key in parameters) { - if (parameters.hasOwnProperty(key)) { - sql = sql.replace(":" + key, sql_escape(parameters[key])); - } - } - } else { - if (parameters.length === 1 && parameters[0].constructor.name === 'Array') { - parameters = parameters[0]; - } - // ordered parameters - parameters.forEach(function (param) { - sql = sql.replace("?", sql_escape(param)); - }); - } - return sql; -} - -// Converter between JS types and SQL data types -function js_to_sql(class) { - if (class === String) { - return 'text'; - } - if (class === Number) { - return 'integer'; - } - if (class === Boolean) { - return 'bool'; - } - throw "Unknown type " + class; -} - -// Convert a condition hash/array to a proper SQL where clause. -function condition_to_sql(condition, value) { - var operator, - p = condition.indexOf(' '); - if (p === -1) { - return condition + " = " + sql_escape(value); - } - operator = condition.substr(p + 1); - condition = condition.substr(0, p); - if (['<', '>', '=', '<=', '>=', '!=', '<>'].indexOf(operator) >= 0) { - return condition + " " + operator + " " + sql_escape(value); - } - if (operator === '%') { - return condition + " LIKE " + sql_escape(value); - } - sys.debug(operator); - throw "Invalid operator " + operator; -} - -// overrides needs to contain at least the following -// index_col: the name of the special index column rowid in sqlite and oid in postgres -// do_insert: function (data, keys, values, callback) -// do_update: function (data, pairs, callback) - -function Store(conn, name, columns, overrides) { - var key, - types = []; - - this.name = name; - this.conn = conn; - - if (overrides.types) { - types = overrides.types; - delete overrides.types; - } - - if (columns) { - for (key in columns) { - if (columns.hasOwnProperty(key)) { - types.push(key + " " + js_to_sql(columns[key])); - } - } - - conn.execute("CREATE TABLE " + name + "(" + types.join(", ") +")", function () {}); - } - - if (overrides) { - var self = this; - Object.keys(overrides).forEach(function (key) { - self[key] = overrides[key]; - }); - } - - -} -Store.prototype = { - - get: function (id, callback) { - this.conn.query( - "SELECT " + this.index_col + " AS _id, * FROM " + this.name + " WHERE " + this.index_col + " = ?", id, - function (data) { - callback(data[0]); - } - ); - }, - - find: function (conditions, row_callback, callback) { - // row_callback is optional - if (typeof callback === 'undefined') { - callback = row_callback; - row_callback = false; - } - var sql; - // Shortcut if there are no conditions. - if (conditions === undefined || conditions.length === 0) { - return this.all(callback); - } - - if (conditions.constructor.name !== 'Array') { - conditions = [conditions]; - } - - sql = "SELECT " + this.index_col + " AS _id, * FROM " + this.name + " WHERE " + - conditions.map(function (group) { - var ands = [], key; - for (key in group) { - if (group.hasOwnProperty(key)) { - ands.push(condition_to_sql(key, group[key])); - } - } - return "(" + ands.join(" AND ") + ")"; - }).join(" OR "); - - if (row_callback) { - this.conn.query(sql, row_callback, callback); - } - this.conn.query(sql, callback); - }, - - each: function (row_callback, callback) { - return this.conn.query("SELECT " + this.index_col + " AS _id, * FROM " + this.name, row_callback, callback); - }, - - all: function (callback) { - return this.conn.query("SELECT " + this.index_col + " AS _id, * FROM " + this.name, callback); - }, - - do_update: function (data, pairs, callback) { - this.conn.execute("UPDATE " + this.name + - " SET " + pairs.join(", ") + - " WHERE " + this.index_col + " = " + sql_escape(data._id), - function () { - callback(); - } - ); - }, - - // Save a data object to the database. If it already has an _id do an update. - save: function (data, callback) { - var keys = [], - values = [], - pairs = [], - key; - - if (data._id) { - for (key in data) { - if (data.hasOwnProperty(key) && key !== '_id') { - pairs.push(key + " = " + sql_escape(data[key])); - } - } - this.do_update(data, pairs, callback); - } else { - for (key in data) { - if (data.hasOwnProperty(key)) { - keys.push(key); - values.push(sql_escape(data[key])); - } - } - this.do_insert(data, keys, values, callback); - } - }, - - // Remove an entry from the database and remove the _id from the data object. - remove: function (data, callback) { - if (typeof data === 'number') { - data = {_id: data}; - } - this.conn.execute("DELETE FROM " + this.name + - " WHERE " + this.index_col + " = " + sql_escape(data._id), - function () { - delete data._id; - callback() - } - ); - }, - - nuke: function (callback) { - this.conn.query("DELETE FROM " + this.name, callback); - } - -}; - -exports.merge = merge; -exports.Store = Store; - - diff --git a/lib/type-oids.js b/lib/type-oids.js new file mode 100644 index 0000000..390031e --- /dev/null +++ b/lib/type-oids.js @@ -0,0 +1,67 @@ +// taken from ry's node_postgres module; +; +exports.BOOL = 16; +exports.BYTEA = 17; +exports.CHAR = 18; +exports.NAME = 19; +exports.INT8 = 20; +exports.INT2 = 21; +exports.INT2VECTOR = 22; +exports.INT4 = 23; +exports.REGPROC = 24; +exports.TEXT = 25; +exports.OID = 26; +exports.TID = 27; +exports.XID = 28; +exports.CID = 29; +exports.VECTOROID = 30; +exports.PG_TYPE_RELTYPE_ = 71; +exports.PG_ATTRIBUTE_RELTYPE_ = 75; +exports.PG_PROC_RELTYPE_ = 81; +exports.PG_CLASS_RELTYPE_ = 83; +exports.POINT = 600; +exports.LSEG = 601; +exports.PATH = 602; +exports.BOX = 603; +exports.POLYGON = 604; +exports.LINE = 628; +exports.FLOAT4 = 700; +exports.FLOAT8 = 701; +exports.ABSTIME = 702; +exports.RELTIME = 703; +exports.TINTERVAL = 704; +exports.UNKNOWN = 705; +exports.CIRCLE = 718; +exports.CASH = 790; +exports.MACADDR = 829; +exports.INET = 869; +exports.CIDR = 650; +exports.INT4ARRAY = 1007; +exports.ACLITEM = 1033; +exports.BPCHAR = 1042; +exports.VARCHAR = 1043; +exports.DATE = 1082; +exports.TIME = 1083; +exports.TIMESTAMP = 1114; +exports.TIMESTAMPTZ = 1184; +exports.INTERVAL = 1186; +exports.TIMETZ = 1266; +exports.BIT = 1560; +exports.VARBIT = 1562; +exports.NUMERIC = 1700; +exports.REFCURSOR = 1790; +exports.REGPROCEDURE = 2202; +exports.REGOPER = 2203; +exports.REGOPERATOR = 2204; +exports.REGCLASS = 2205; +exports.REGTYPE = 2206; +exports.RECORD = 2249; +exports.CSTRING = 2275; +exports.ANY = 2276; +exports.ANYARRAY = 2277; +exports.VOID = 2278; +exports.TRIGGER = 2279; +exports.LANGUAGE_HANDLER = 2280; +exports.INTERNAL = 2281; +exports.OPAQUE = 2282; +exports.ANYELEMENT = 2283; diff --git a/package.json b/package.json new file mode 100644 index 0000000..ee1f816 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{"name": "postgres-js", + "version": "0.1.1", + "description": "Pure-JavaScript implementation of the Postgres protocol", + "homepage": "https://public.commandprompt.com/projects/postgres-js/", + "author": "Tim Caswell ", + "contributors": [ + { "name": "Aurynn Shaw","email": "aurynn@gmail.com" }, + { "name": "Anthony Sekatski","email": "anthony@8protons.com" }, + ], + "main": "lib/postgres-pure", + "dependencies" : + { "strtok" : ">=0.1.1" + } +} \ No newline at end of file diff --git a/t/complex_prepared.js b/t/complex_prepared.js new file mode 100644 index 0000000..e9066d0 --- /dev/null +++ b/t/complex_prepared.js @@ -0,0 +1,33 @@ +var sys = require("sys"); +var pg = require("../lib/postgres-pure"); +pg.DEBUG=0; + +var db = new pg.connect("pgsql://test:12345@localhost:5432/template1"); + +db.prepare("SELECT ?::int AS foobar", function (sth, tx) { + sth.execute(1, function (err, rs) { + console.log(eq(rs[0]['foobar'], 1)); + //console.log(sys.inspect(rs)); + }); + sth.execute(2, function (err, rs) { + console.log(eq(rs[0]['foobar'], 2)); + // console.log(sys.inspect(rs)); + + }); + tx.prepare("SELECT ?::int AS cheese", function (sth) { + sth.execute(3, function (err, rs) { + console.log(eq(rs[0]['cheese'], 3)); + // console.log(sys.inspect(rs)); + }); + }); + // db.close(); +}); +db.close(); + + +function eq (l, r) { + if (l === r) { + return "ok" + } + return "not ok\n " + l + " != " + r; +} \ No newline at end of file diff --git a/t/error.js b/t/error.js new file mode 100644 index 0000000..3aeabc8 --- /dev/null +++ b/t/error.js @@ -0,0 +1,26 @@ +/* Tests whether the error object is being correctly passed to the query +callback function */ +var sys = require("sys"); +var pg = require("../lib/postgres-pure"); +pg.DEBUG=0; + +var db = new pg.connect("pgsql://test:12345@localhost:5432/template1"); +db.query("SELECT 1::foobar;", function (err, rs, tx) { + console.log("error not null: " + not_eq(null, err)); + console.log("error code is 42704 (no such type foobar): " + eq("42704", err.code)); +}); +db.close(); + +function not_eq (l, r) { + if (l !== r) { + return "ok" + } + return "not ok\n " + l + " != " + r; +} + +function eq (l, r) { + if (l === r) { + return "ok" + } + return "not ok\n " + l + " != " + r; +} \ No newline at end of file diff --git a/t/insert.js b/t/insert.js new file mode 100644 index 0000000..2f4864d --- /dev/null +++ b/t/insert.js @@ -0,0 +1,26 @@ +var sys = require("sys"); +var pg = require("../lib/postgres-pure"); +pg.DEBUG=3; + +var db = new pg.connect("pgsql://test:12345@localhost:5432/postgresjs"); +var tx = db.transaction(); +tx.begin(); +tx.query("CREATE TABLE testcase (id int)", function (err, rs) { + tx.query("INSERT INTO testcase VALUES (?)", 1, function(err, rs) { + console.log("Got to tx.") + }); + tx.query("DROP TABLE testcase;", function (err, rs) { + // nothing + }); + tx.rollback(); +}); +db.close(); + + + +function eq (l, r) { + if (l === r) { + return "ok" + } + return "not ok\n " + l + " != " + r; +} \ No newline at end of file diff --git a/t/notify.js b/t/notify.js new file mode 100644 index 0000000..747526c --- /dev/null +++ b/t/notify.js @@ -0,0 +1,32 @@ +/* Tests whether the error object is being correctly passed to the query +callback function */ +var sys = require("sys"); +var pg = require("../lib/postgres-pure"); +pg.DEBUG=0; + +var db = new pg.connect("pgsql://test:12345@localhost:5432/template1"); +var db2 = new pg.connect("pgsql://test:12345@localhost:5432/template1"); + +db.notify("test", function (err, payload) { + console.log(eq(err, null)); +}); +db2.on("connection", function () { + db2.query("NOTIFY test", function (err,rs) {}); // Null + db2.close(); +}); +db.close(); + + +function not_eq (l, r) { + if (l !== r) { + return "ok" + } + return "not ok " + l + " != " + r; +} + +function eq (l, r) { + if (l === r) { + return "ok" + } + return "not ok " + l + " != " + r; +} \ No newline at end of file diff --git a/t/post_connect.js b/t/post_connect.js new file mode 100644 index 0000000..c448988 --- /dev/null +++ b/t/post_connect.js @@ -0,0 +1,31 @@ +/* Tests whether the error object is being correctly passed to the query +callback function */ +var sys = require("sys"); +var pg = require("../lib/postgres-pure"); +pg.DEBUG=0; + +var db = new pg.connect("pgsql://test:12345@localhost:5432/template1"); +db.on("connection", function () { + db.query("SELECT 1::int", function (err, rs, tx) { + // verify that the error is null, and everything is okay. + console.log(eq(err, null)); + // Test if the returned value is what we think it is. + console.log(eq(rs[0]['int4'], 1)); + }); + db.close(); +}); + + +function not_eq (l, r) { + if (l !== r) { + return "ok" + } + return "not ok " + l + " != " + r; +} + +function eq (l, r) { + if (l === r) { + return "ok" + } + return "not ok " + l + " != " + r; +} \ No newline at end of file diff --git a/t/query.js b/t/query.js new file mode 100644 index 0000000..9d20578 --- /dev/null +++ b/t/query.js @@ -0,0 +1,12 @@ +var sys = require("sys"); +var pg = require("../lib/postgres-pure"); +pg.DEBUG=0; + +var db = new pg.connect("pgsql://test:12345@localhost:5432/template1"); +db.query("SELECT 1::int as foobar;", function (err, rs, tx) { + console.log(sys.inspect(rs)); + tx.query("SELECT 2::int as foobartwo", function (err, rs) { + console.log(sys.inspect(rs)); + }); +}); +db.close(); \ No newline at end of file diff --git a/t/simple_prepared.js b/t/simple_prepared.js new file mode 100644 index 0000000..fec67ab --- /dev/null +++ b/t/simple_prepared.js @@ -0,0 +1,15 @@ +var sys = require("sys"); +var pg = require("../lib/postgres-pure"); +pg.DEBUG=0; + +var db = new pg.connect("pgsql://test:12345@localhost:5432/template1"); +db.prepare("SELECT ?::int AS foobar", function (sth, tx) { + sth.execute(1, function (err, rs) { + console.log(sys.inspect(rs)); + }); + sth.execute(2, function (err, rs) { + console.log(sys.inspect(rs)); + + }); +}); +db.close(); diff --git a/t/transaction.js b/t/transaction.js new file mode 100644 index 0000000..8dd8541 --- /dev/null +++ b/t/transaction.js @@ -0,0 +1,25 @@ +var sys = require("sys"); +var pg = require("../lib/postgres-pure"); +pg.DEBUG=0; + +var db = new pg.connect("pgsql://test:12345@localhost:5432/template1"); +db.transaction(function (tx) { + tx.begin(); + tx.query("CREATE TABLE insert_test (id serial not null, val text)", function (err, rs) { + // Null query. + }); + tx.query("INSERT INTO insert_test (val) VALUES (?) RETURNING id", "test value", function (err,rs) { + console.log(sys.inspect(rs)); + }); + tx.prepare("INSERT INTO insert_test (val) VALUES (?) RETURNING id", function (sth) { + sth.execute("twooooo", function (err, rs) { + console.log(sys.inspect(rs)); + }); + }); + tx.rollback(); + tx.query("SELECT * FROM insert_test", function (err, rs) { + // Should error + console.log(sys.inspect(err)); + }); +}); +db.close(); \ No newline at end of file