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/README.markdown b/README.markdown deleted file mode 100644 index a404f29..0000000 --- a/README.markdown +++ /dev/null @@ -1,19 +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). - -## Example use - - var sys = require("sys"); - var Postgres = require('postgres.js'); - - function onLoad() { - var db = new Postgres.Connection("database", "username", "password"); - db.query("SELECT * FROM sometable", function (data) { - sys.p(data); - }); - db.close(); - } - - diff --git a/README.md b/README.md new file mode 100644 index 0000000..56bfe4a --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# 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 also allows for the handling of prepared queries and parameterized queries. + +## Example use + + var sys = require("sys"); + var Postgres = require("./postgres"); + + Postgres.DEBUG= 1; + + var db = new Postgres.Connection("database", "username", "password"); + + var promise = db.query("SELECT * FROM test"); + + promise.addCallback(function (data) { + sys.p(data); + }); + + promise.addErrback(function (error_message) { + sys.debug(error_message); + }); + + db.close(); + +## Example use of Parameterized Queries + + var sys = require("sys"); + var pg = require("postgres.js"); + + var db = new pg.Connection("database", "username", "password"); + db.query("SELECT * FROM yourtable WHERE id = ?", [1]).addCallback(function (data) { + + sys.p(data); + }); + db.close(); + +## Example use of Prepared Queries + + var sys = require("sys"); + var pg = require("postgres.js"); + + var db = new pg.Connection("database", "username", "password"); + + db.prepare("SELECT * FROM yourtable WHERE id = ?").addCallback(function (query) { + + sys.p(query); + query.execute(["1"]).addCallback(function (d) { + sys.p(d); + }); + /* More queries here. */ + }); + db.close(); diff --git a/demo.js b/demo.js new file mode 100644 index 0000000..e1a604a --- /dev/null +++ b/demo.js @@ -0,0 +1,11 @@ +var sys = require("sys"); +var Postgres = require("./postgres"); + +Postgres.DEBUG= 1; + +var db = new Postgres.Connection("database", "username", "password"); +db.query("SELECT * FROM test"); +db.query("SELECT * FROM test").addCallback(function (data) { + sys.p(data); +}); +db.close(); diff --git a/postgres-js/bits.js b/lib/bits.js similarity index 99% rename from postgres-js/bits.js rename to lib/bits.js index fa13416..4914bdd 100644 --- a/postgres-js/bits.js +++ b/lib/bits.js @@ -143,5 +143,3 @@ }()); - - diff --git a/postgres-js/md5.js b/lib/md5.js similarity index 100% rename from postgres-js/md5.js rename to lib/md5.js diff --git a/lib/parsers.js b/lib/parsers.js new file mode 100644 index 0000000..ef029bf --- /dev/null +++ b/lib/parsers.js @@ -0,0 +1,71 @@ +var oids = require('./type-oids'), + sys = require('sys'); + +// 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) { + var style = datestyle.split(','); + var order = style[1]; style = style[0].replace(/^\s+|\s+$/,''); + + if (!(style in date_regex)) { + 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 (tz == str) tz = ''; + case oids.TIMESTAMP: + case oids.DATE: + date = str.replace(date_regex[style],date_regex[style+'r']); + if (date == str) date = ''; + if (OID==oids.DATE) break; + case oids.TIME: + time = ' ' + str.replace(time_regex[style],time_regex[style+'r']); + if (time == str) time = ''; + } + + date = ((date=='')?'January 1, 1970':date) + ((time=='')?'':' ') + time + ((tz=='')?'':' ') + tz; + + 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()+1),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/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/postgres.js b/postgres.js index f6dfca6..9efa8f3 100644 --- a/postgres.js +++ b/postgres.js @@ -1,13 +1,17 @@ /*jslint bitwise: true, eqeqeq: true, immed: true, newcap: true, nomen: true, onevar: true, plusplus: true, regexp: true, undef: true, white: true, indent: 2 */ /*globals include md5 node exports */ +process.mixin(require('./lib/md5')); -process.mixin(require('./postgres-js/md5')); -var bits = require('./postgres-js/bits'); +var bits = require('./lib/bits'); +var oid = require("./lib/type-oids"); +var parsers = require("./lib/parsers"); var tcp = require("tcp"); var sys = require("sys"); exports.DEBUG = 0; +var postgres_parameters = {}; + // http://www.postgresql.org/docs/8.3/static/protocol-message-formats.html var formatter = { CopyData: function () { @@ -167,19 +171,30 @@ function parse_response(code, stream) { } -exports.Connection = function (database, username, password, port) { - var connection, events, query_queue, row_description, query_callback, results, readyState, closeState; - +exports.Connection = function (database, username, password, port, host) { + var connection, events, query_queue, row_description, results, readyState, closeState; + var query_callback, query_promise; + // Default to port 5432 if (port === undefined) { port = 5432; } + + t_host = host; + if (t_host === undefined) { + t_host = "localhost"; + } - connection = tcp.createConnection(port); + connection = tcp.createConnection(port, host=t_host); events = new process.EventEmitter(); query_queue = []; readyState = false; closeState = false; + timedout = false; + + function reconnect() { + connection.connect(port, host=t_host); + } // Sends a message to the postgres server function sendMessage(type, args) { @@ -225,11 +240,18 @@ exports.Connection = function (database, username, password, port) { connection.addListener("eof", function (data) { connection.close(); }); - connection.addListener("disconnect", function (had_error) { + connection.addListener("close", function (had_error) { if (had_error) { sys.debug("CONNECTION DIED WITH ERROR"); + } else if (timedout) { + reconnect(); + timedout = false; } }); + connection.addListener("timeout", function () { + info("server timed out... reconnecting"); + timedout = true; + }); // Set up callbacks to automatically do the login events.addListener('AuthenticationMD5Password', function (salt) { @@ -244,18 +266,27 @@ exports.Connection = function (database, username, password, port) { sys.debug(e.S + ": " + e.M); connection.close(); } + if(query_promise != null) query_promise.emitError(e.M); + }); + events.addListener('ParameterStatus', function(key, value) { + postgres_parameters[key] = value; }); events.addListener('ReadyForQuery', function () { + if(connection.readyState == "closed") { + reconnect(); + return; + } if (query_queue.length > 0) { var query = query_queue.shift(); - query_callback = query.callback; + query_callback = query.callback || null; + query_promise = query.promise || null; sendMessage('Query', [query.sql]); readyState = false; } else { if (closeState) { connection.close(); } else { - readyState = true; + readyState = true; } } }); @@ -274,14 +305,24 @@ exports.Connection = function (database, username, password, port) { // 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 + case oid.BOOL: value = value === 't'; break; - case 20: // int8 - case 21: // int2 - case 23: // int4 + case oid.INT8: + case oid.INT2: + case oid.INT4: value = parseInt(value, 10); break; + case oid.DATE: + case oid.TIME: + case oid.TIMESTAMP: + case oid.TIMESTAMPTZ: + value = parsers.parseDateFromPostgres( + value, + postgres_parameters['DateStyle'], + description.type_id + ); + break; } } row[description.field] = value; @@ -289,18 +330,113 @@ exports.Connection = function (database, username, password, port) { results.push(row); }); events.addListener('CommandComplete', function (data) { - query_callback.call(this, results); + if(query_callback) query_callback(results); + else if(query_promise) query_promise.emitSuccess(results); }); - this.query = function (sql, callback) { - query_queue.push({sql: sql, callback: callback}); - if (readyState) { - events.emit('ReadyForQuery'); + this.query = function (sql, args) { + var promise = new process.Promise(); + + if (args == null) { + + // This has no parameters to manipulate. + + query_queue.push({sql: sql, promise: promise}); + if (readyState) { + events.emit('ReadyForQuery'); + } + } + else { + // We have an args list. + // This means, we have to map our ?'s and test for a variety of + // edge cases. + if(exports.DEBUG > 0) sys.puts("Got args."); + var i = 0; + var slice = md5(md5(sql)); + //sys.p(slice); + var offset = Math.floor(Math.random() * 10); + cont = "$" + slice.replace(/\d/g, "").slice(offset,4+offset) + "$"; + var treated = sql; + if(exports.DEBUG > 0) sys.p(cont); + if (sql.match(/\?/)) { + treated = sql.replace(/\?/g, function (str, offset, s) { + if (!args[i]) { + // raise an error + throw new Error("Argument "+i+" does not exist!"); + } + return cont+args[i++]+cont; + } ); + } + if(exports.DEBUG > 0) sys.p(treated); + query_queue.push({sql: treated, promise: promise}); + if (readyState) { + events.emit('ReadyForQuery'); + } + } + + return promise; + }; + + this.prepare = function (query) { + + var r = new process.Promise(); + var name = md5(md5(query)); + var offset = Math.floor(Math.random() * 10); + name = name.replace(/\d/g, "").slice(offset,4+offset); + + var treated = query; + var i = 0; + if (query.match(/\?/)) { + + treated = treated.replace(/\?/g, function (str, p1, offset, s) { + i = i + 1; + return "$"+i; + }); } + + stmt = "PREPARE " + name + " AS " + treated; + + var conn = this; + + query_queue.push({sql: stmt, callback: function (c) { + var q = new Stmt(name, i, conn ); + r.emitSuccess(q); + } + }); + return r; }; + this.close = function () { - closeState = true; + closeState = true; }; + }; +function Stmt (name, len, conn) { + var stmt = "EXECUTE "+name+" ( "; + var que = []; + for (var i = 1; i<=len; i++) { + que.push("?"); + } + stmt = stmt + que.join(",") + " )"; + + if(exports.DEBUG > 0) sys.puts(stmt); + + this.execute = function (args) { + if (args.length > len) { + throw new Error("Cannot execute: Too many arguments"); + } + else if (args.length < len) { + // Pad out the length with nulls. + for (var i = args.length; i<= len; i++) { + args.push(null); + } + } + else { + // Nothing to see here. + ; + } + return conn.query(stmt, args); + }; +}