diff --git a/.gitignore b/.gitignore index 5eb08587..8e220410 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ /.emacs-project *.swp *.log +.lock-wscript +build/ +/todo.org diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..2bd9cb3f --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +.lock-wscript +build/ diff --git a/Makefile b/Makefile index e9c4cc0e..b4885d14 100644 --- a/Makefile +++ b/Makefile @@ -11,19 +11,28 @@ params := -u $(user) --password $(password) -p $(port) -d $(database) -h $(host) node-command := xargs -n 1 -I file node file $(params) -.PHONY : test test-connection test-integration bench +.PHONY : test test-connection test-integration bench test-native build/default/binding.node test: test-unit -test-all: test-unit test-integration +test-all: test-unit test-integration test-native bench: @find benchmark -name "*-bench.js" | $(node-command) +build/default/binding.node: + @node-waf configure build + test-unit: @find test/unit -name "*-tests.js" | $(node-command) test-connection: @node script/test-connection.js $(params) +test-native: build/default/binding.node + @echo "***Testing native bindings***" + @find test/native -name "*-tests.js" | $(node-command) + @find test/integration -name "*-tests.js" | $(node-command) --native true + test-integration: test-connection + @echo "***Testing Pure Javascript***" @find test/integration -name "*-tests.js" | $(node-command) diff --git a/README.md b/README.md index d3a53498..60115bd1 100644 --- a/README.md +++ b/README.md @@ -1,80 +1,102 @@ #node-postgres -Non-blocking (async) pure JavaScript PostgreSQL client for node.js written -with love and TDD. +Non-blocking PostgreSQL client for node.js. Pure JavaScript and native libpq bindings. ## Installation npm install pg -## Example +## Examples - var pg = require('pg'); - var connectionString = "pg://user:password@host:port/database"; - pg.connect(connectionString, function(err, client) { - if(err) { - //handle connection error - } - else { - //queries are queued and executed in order - client.query("CREATE TEMP TABLE user(name varchar(50), birthday timestamptz)"); - client.query("INSERT INTO user(name, birthday) VALUES('brianc', '1982-01-01T10:21:11')"); - - //parameterized queries with transparent type coercion - client.query("INSERT INTO user(name, birthday) VALUES($1, $2)", ['santa', new Date()]); - - //nested queries with callbacks - client.query("SELECT * FROM user ORDER BY name", function(err, result) { - if(err) { - //handle query error - } - else { - client.query("SELECT birthday FROM user WHERE name = $1", [result.rows[0].name], function(err, result) { - //typed parameters and results - assert.ok(result.rows[0].birthday.getYear() === 1982) - }) - } - }) - } - } +All examples will work with the pure javascript bindings (currently default) or the libpq native (c/c++) bindings (currently in beta) -## Philosophy +To use native libpq bindings replace `require('pg')` with `require('pg').native`. -* well tested -* no monkey patching -* no dependencies (...besides PostgreSQL) -* [in-depth documentation](http://github.com/brianc/node-postgres/wiki) (work in progress) +The two share the same interface so __no other code changes should be required__. If you find yourself having to change code other than the require statement when switching from `pg` to `pg.native`, please report an issue. -## features +node-postgres supports both an 'event emitter' style API and a 'callback' style. The callback style is more concise and generally preferred, but the evented API can come in handy. They can be mixed and matched. The only events which do __not__ fire when callbacks are supplied are the `error` events, as they are to be handled by the callback function. -- prepared statement support - - parameters - - query caching -- type coercion - - date <-> timestamptz - - integer <-> integer, smallint, bigint - - float <-> double, numeric - - boolean <-> boolean -- notification message support -- connection pooling -- mucho testing - ~250 tests executed on - - ubuntu - - node v0.2.2, v0.2.3, v0.2.4, v0.2.5, v0.2.6, v0.3.0, v0.3.1, v0.3.2, v0.3.3, v0.3.4, v0.3.5, v0.3.6, v0.3.7, v0.3.8 - - postgres 8.4.4 - - osx - - node v0.2.2, v0.2.3, v0.2.4, v0.2.5, v0.2.6, v0.3.0, v0.3.1, v0.3.2, v0.3.3, v0.3.4, v0.3.5, v0.3.6, v0.3.7, v0.3.8 - - postgres v8.4.4, v9.0.1 installed both locally and on networked Windows 7 +### Simple, using built-in client pool -## Contributing + var pg = require('pg'); + //or native libpq bindings + //var pg = require('pg').native -clone the repo: + var conString = "tcp://postgres:1234@localhost/postgres"; - git clone git://github.com/brianc/node-postgres - cd node-postgres - make test + //error handling omitted + pg.connect(conString, function(err, client) { + client.query("SELECT NOW() as when", function(err, result) { + console.log("Row count: %d",result.rows.length); // 1 + console.log("Current year: %d", result.rows[0].when.getYear()); + }); + }); -And just like magic, you're ready to contribute! <3 +### Evented api + + var pg = require('pg'); //native libpq bindings = `var pg = require('pg').native` + var conString = "tcp://postgres:1234@localhost/postgres"; + + var client = new pg.Client(conString); + client.connect(); + + //queries are queued and executed one after another once the connection becomes available + client.query("CREATE TEMP TABLE beatles(name varchar(10), height integer, birthday timestamptz)"); + client.query("INSERT INTO beatles(name, height, birthday) values($1, $2, $3)", ['Ringo', 67, new Date(1945, 11, 2)]); + client.query("INSERT INTO beatles(name, height, birthday) values($1, $2, $3)", ['John', 68, new Date(1944, 10, 13)]); + + //queries can be executed either via text/parameter values passed as individual arguments + //or by passing an options object containing text, (optional) parameter values, and (optional) query name + client.query({ + name: 'insert beatle', + text: "INSERT INTO beatles(name, height, birthday) values($1, $2, $3)", + values: ['George', 70, new Date(1946, 02, 14)] + }); + + //subsequent queries with the same name will be executed without re-parsing the query plan by postgres + client.query({ + name: 'insert beatle', + values: ['Paul', 63, new Date(1945, 04, 03)] + }); + var query = client.query("SELECT * FROM beatles WHERE name = $1", ['John']); + + //can stream row results back 1 at a time + query.on('row', function(row) { + console.log(row); + console.log("Beatle name: %s", row.name); //Beatle name: John + console.log("Beatle birth year: %d", row.birthday.getYear()); //dates are returned as javascript dates + console.log("Beatle height: %d' %d\"", Math.floor(row.height/12), row.height%12); //integers are returned as javascript ints + }); + + //fired after last row is emitted + query.on('end', function() { + client.end(); + }); + +### Info + +* a pure javascript client and native libpq bindings with _the same api_ +* _heavily_ tested + * the same suite of 200+ integration tests passed by both javascript & libpq bindings + * benchmark & long-running memory leak tests performed before releases + * tested with with + * postgres 8.x, 9.x + * Linux, OS X + * node 2.x & 4.x +* row-by-row result streaming +* optional, built-in connection pooling +* responsive project maintainer +* supported PostgreSQL features + * parameterized queries + * named statements with query plan caching + * async notifications + * extensible js<->postgresql data-type coercion +* query queue +* active development +* fast +* No dependencies (other than PostgreSQL) +* No monkey patching +* Tried to mirror the node-mysql api as much as possible for future multi-database-supported ORM implementation ease ### Contributors @@ -85,36 +107,26 @@ Many thanks to the following: * [pshc](https://github.com/pshc) * [pjornblomqvist](https://github.com/bjornblomqvist) * [JulianBirch](https://github.com/JulianBirch) +* [ef4](https://github.com/ef4) +* [napa3um](https://github.com/napa3um) -## More info please +## Documentation -### [Documentation](node-postgres/wiki) +Still a work in progress, I am trying to flesh out the wiki... + +### [Documentation](https://github.com/brianc/node-postgres/wiki) ### __PLEASE__ check out the WIKI +## Production Use +* [bayt.com](http://bayt.com) + +_if you use node-postgres in production and would like your site listed here, fork & add it_ + ## Help -If you need help or run into _any_ issues getting node-postgres to work on your system please report a bug or contact me directly. +If you need help or run into _any_ issues getting node-postgres to work on your system please report a bug or contact me directly. I am usually available via google-talk at my github account public email address. -### Working? - -[this page](http://www.explodemy.com) is running the worlds worst (but fully functional) PostgreSQL backed, Node.js powered website. - -### Why did you write this? - -As soon as I saw node.js for the first time I knew I had found something lovely and simple and _just what I always wanted!_. So...I poked around for a while. I was excited. I still am! - -I drew major inspiration from [postgres-js](http://github.com/creationix/postgres-js). - -I also drew some major inspirrado from -[node-mysql](http://github.com/felixge/node-mysql) and liked what I -saw there. - -### Plans for the future? - -- transparent prepared statement caching -- more testings of error scenarios - ## License Copyright (c) 2010 Brian Carlson (brian.m.carlson@gmail.com) diff --git a/benchmark/js-versus-native-bench.js b/benchmark/js-versus-native-bench.js new file mode 100644 index 00000000..b65fb98c --- /dev/null +++ b/benchmark/js-versus-native-bench.js @@ -0,0 +1,68 @@ +var pg = require(__dirname + '/../lib') +var pgNative = require(__dirname + '/../lib/native'); +var bencher = require('bencher'); +var helper = require(__dirname + '/../test/test-helper') +var conString = helper.connectionString() + +var round = function(num) { + return Math.round((num*1000))/1000 +} + +var doBenchmark = function() { + var bench = bencher({ + name: 'js/native compare', + repeat: 1000, + actions: [{ + name: 'javascript client - simple query', + run: function(next) { + var query = client.query('SELECT name, age FROM person WHERE age > 10'); + query.on('end', function() { + next(); + }); + } + },{ + name: 'native client - simple query', + run: function(next) { + var query = nativeClient.query('SELECT name FROM person WHERE age > $1', [10]); + query.on('end', function() { + next(); + }); + } + }, { + name: 'javascript client - parameterized query', + run: function(next) { + var query = client.query('SELECT name, age FROM person WHERE age > $1', [10]); + query.on('end', function() { + next(); + }); + } + },{ + name: 'native client - parameterized query', + run: function(next) { + var query = nativeClient.query('SELECT name, age FROM person WHERE age > $1', [10]); + query.on('end', function() { + next(); + }); + } + }] + }); + bench(function(result) { + console.log(); + console.log("%s (%d repeats):", result.name, result.repeat) + result.actions.forEach(function(action) { + console.log(" %s: \n average: %d ms\n total: %d ms", action.name, round(action.meanTime), round(action.totalTime)); + }) + client.end(); + nativeClient.end(); + }) +} + +var client = new pg.Client(conString); +var nativeClient = new pgNative.Client(conString); +client.connect(); +client.on('connect', function() { + nativeClient.connect(); + nativeClient.on('connect', function() { + doBenchmark(); + }); +}); diff --git a/benchmark/large-datatset-bench.js b/benchmark/large-datatset-bench.js index 081bc082..a5e0346a 100644 --- a/benchmark/large-datatset-bench.js +++ b/benchmark/large-datatset-bench.js @@ -7,7 +7,7 @@ var round = function(num) { return Math.round((num*1000))/1000 } -var doBenchmark = function() { +var doBenchmark = function(cb) { var bench = bencher({ name: 'select large sets', repeat: 10, @@ -15,6 +15,10 @@ var doBenchmark = function() { name: 'selecting string', run: function(next) { var query = client.query('SELECT name FROM items'); + query.on('error', function(er) { + console.log(er);throw er; + }); + query.on('end', function() { next(); }); @@ -23,6 +27,10 @@ var doBenchmark = function() { name: 'selecting integer', run: function(next) { var query = client.query('SELECT count FROM items'); + query.on('error', function(er) { + console.log(er);throw er; + }); + query.on('end', function() { next(); }) @@ -31,6 +39,10 @@ var doBenchmark = function() { name: 'selecting date', run: function(next) { var query = client.query('SELECT created FROM items'); + query.on('error', function(er) { + console.log(er);throw er; + }); + query.on('end', function() { next(); }) @@ -44,7 +56,7 @@ var doBenchmark = function() { }) } }, { - name: 'loading all rows into memory', + name: 'loading all rows into memory', run: function(next) { var query = client.query('SELECT * FROM items', next); } @@ -57,6 +69,7 @@ var doBenchmark = function() { console.log(" %s: \n average: %d ms\n total: %d ms", action.name, round(action.meanTime), round(action.totalTime)); }) client.end(); + cb(); }) } @@ -78,6 +91,35 @@ for(var i = 0; i < count; i++) { } client.once('drain', function() { - console.log('done with insert. executing benchmark.'); - doBenchmark(); + console.log('done with insert. executing pure-javascript benchmark.'); + doBenchmark(function() { + var oldclient = client; + client = new pg.native.Client(conString); + client.on('error', function(err) { + console.log(err); + throw err; + }); + + client.connect(); + client.connect(); + console.log(); + console.log("creating temp table"); + client.query("CREATE TEMP TABLE items(name VARCHAR(10), created TIMESTAMPTZ, count INTEGER)"); + var count = 10000; + console.log("inserting %d rows", count); + for(var i = 0; i < count; i++) { + var query = { + name: 'insert', + text: "INSERT INTO items(name, created, count) VALUES($1, $2, $3)", + values: ["item"+i, new Date(2010, 01, 01, i, 0, 0), i] + }; + client.query(query); + } + client.once('drain', function() { + console.log("executing native benchmark"); + doBenchmark(function() { + console.log("all done"); + }) + }) + }); }); diff --git a/lib/binaryParser.js b/lib/binaryParsers.js similarity index 85% rename from lib/binaryParser.js rename to lib/binaryParsers.js index 2c3a8450..c1b5b6c2 100644 --- a/lib/binaryParser.js +++ b/lib/binaryParsers.js @@ -1,10 +1,3 @@ -var BinaryParser = function(config) { - config = config || {}; - this.encoding = config.encoding || 'utf8'; -}; - -var p = BinaryParser.prototype; - var parseBits = function(data, bits, offset, callback) { offset = offset || 0; callback = callback || function(lastValue, newValue, bits) { return (lastValue * Math.pow(2, bits)) + newValue; }; @@ -82,11 +75,11 @@ var parseFloat = function(data, precisionBits, exponentBits) { return ((sign == 0) ? 1 : -1) * Math.pow(2, exponent - bias) * mantissa; }; -p.parseBool = function(value) { +var parseBool = function(value) { return (parseBits(value, 8) == 1); } -p.parseInt16 = function(value) { +var parseInt16 = function(value) { if (parseBits(value, 1) == 1) { return -1 * (Math.pow(2, 15) - parseBits(value, 15, 1)); } @@ -94,7 +87,7 @@ p.parseInt16 = function(value) { return parseBits(value, 15, 1); } -p.parseInt32 = function(value) { +var parseInt32 = function(value) { if (parseBits(value, 1) == 1) { return -1 * (Math.pow(2, 31) - parseBits(value, 31, 1)); } @@ -102,7 +95,7 @@ p.parseInt32 = function(value) { return parseBits(value, 31, 1); } -p.parseInt64 = function(value) { +var parseInt64 = function(value) { if (parseBits(value, 1) == 1) { return -1 * (Math.pow(2, 63) - parseBits(value, 63, 1)); } @@ -110,15 +103,15 @@ p.parseInt64 = function(value) { return parseBits(value, 63, 1); } -p.parseFloat32 = function(value) { +var parseFloat32 = function(value) { return parseFloat(value, 23, 8); } -p.parseFloat64 = function(value) { +var parseFloat64 = function(value) { return parseFloat(value, 52, 11); } -p.parseNumeric = function(value) { +var parseNumeric = function(value) { var sign = parseBits(value, 16, 32); if (sign == 0xc000) { return NaN; @@ -138,7 +131,7 @@ p.parseNumeric = function(value) { return ((sign == 0) ? 1 : -1) * Math.round(result * scale) / scale; } -p.parseDate = function(value) { +var parseDate = function(value) { var sign = parseBits(value, 1); var rawValue = parseBits(value, 63, 1); @@ -160,7 +153,7 @@ p.parseDate = function(value) { return result; } -p.parseIntArray = p.parseStringArray = function(value) { +var parseArray = function(value) { var dim = parseBits(value, 32); var flags = parseBits(value, 32, 32); @@ -226,12 +219,32 @@ p.parseIntArray = p.parseStringArray = function(value) { return parse(dims, elementType); }; -p.parseText = function(value) { +var parseText = function(value) { return value.toString('utf8'); }; -p.parseBool = function(value) { +var parseBool = function(value) { return (parseBits(value, 8) > 0); }; -module.exports = BinaryParser; +var init = function(register) { + register(20, parseInt64); + register(21, parseInt16); + register(23, parseInt32); + register(26, parseInt64); + register(1700, parseNumeric); + register(700, parseFloat32); + register(701, parseFloat64); + register(16, parseBool); + register(1114, parseDate); + register(1184, parseDate); + register(1007, parseArray); + register(1016, parseArray); + register(1008, parseArray); + register(1009, parseArray); + register(25, parseText); +}; + +module.exports = { + init: init, +}; diff --git a/lib/client-pool.js b/lib/client-pool.js new file mode 100644 index 00000000..5a3d3f8a --- /dev/null +++ b/lib/client-pool.js @@ -0,0 +1,93 @@ +var Pool = require(__dirname + '/utils').Pool; +var defaults = require(__dirname + '/defaults'); + +module.exports = { + init: function(Client) { + + //connection pool global cache + var clientPools = { + } + + var connect = function(config, callback) { + //lookup pool using config as key + //TODO this don't work so hot w/ object configs + var pool = clientPools[config]; + + //create pool if doesn't exist + if(!pool) { + pool = clientPools[config] = new Pool(defaults.poolSize, function() { + var client = new Client(config); + client.connected = false; + return client; + }) + } + + pool.checkOut(function(err, client) { + //if client already connected just + //pass it along to the callback and return + if(client.connected) { + callback(null, client); + return; + } + + var onError = function(error) { + client.removeListener('connect', onReady); + callback(error); + pool.checkIn(client); + } + + var onReady = function() { + client.removeListener('error', onError); + client.connected = true; + callback(null, client); + client.on('drain', function() { + pool.checkIn(client); + }); + } + + client.once('error', onError); + + client.once('connect', onReady); + + client.connect(); + + }); + } + + //destroys the world + //or optionally only a single pool + //mostly used for testing or + //a hard shutdown + var end = function(name) { + if(!name) { + for(var poolName in clientPools) { + end(poolName) + return + } + } + var pool = clientPools[name]; + //log("destroying pool %s", name); + pool.waits.forEach(function(wait) { + wait(new Error("Client is being destroyed")) + }) + pool.waits = []; + pool.items.forEach(function(item) { + var client = item.ref; + if(client.activeQuery) { + //log("client is still active, waiting for it to complete"); + client.on('drain', client.end.bind(client)) + } else { + client.end(); + } + }) + //remove reference to pool lookup + clientPools[name] = null; + delete(clientPools[name]) + }; + //export functions with closures to client constructor + return { + connect: connect, + end: end + } + } +}; diff --git a/lib/client.js b/lib/client.js index 3af94fe0..8febca0e 100644 --- a/lib/client.js +++ b/lib/client.js @@ -10,7 +10,7 @@ var Connection = require(__dirname + '/connection'); var Client = function(config) { EventEmitter.call(this); if(typeof config === 'string') { - config = utils.parseConnectionString(config) + config = utils.normalizeConnectionInfo(config) } config = config || {}; this.user = config.user || defaults.user; @@ -22,6 +22,7 @@ var Client = function(config) { this.queryQueue = []; this.password = config.password || defaults.password; this.encoding = 'utf8'; + var self = this; }; sys.inherits(Client, EventEmitter); @@ -31,7 +32,12 @@ var p = Client.prototype; p.connect = function() { var self = this; var con = this.connection; - con.connect(this.port, this.host); + if(this.host && this.host.indexOf('/') === 0) { + con.connect(this.host + '/.s.PGSQL.' + this.port); + } else { + con.connect(this.port, this.host); + } + //once connection is established send startup message con.on('connect', function() { @@ -79,6 +85,12 @@ p.connect = function() { } }); + self.emit('connect'); + + con.on('notification', function(msg) { + self.emit('notification', msg); + }) + }); con.on('readyForQuery', function() { @@ -102,6 +114,11 @@ p.connect = function() { self.activeQuery = null; } }); + + con.on('notice', function(msg) { + self.emit('notice', msg); + }) + }; p._pulseQueryQueue = function() { diff --git a/lib/connection.js b/lib/connection.js index 2b5c0139..29cd28cc 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -78,8 +78,12 @@ p.password = function(password) { this._send(0x70, this.writer.addCString(password)); }; -p._send = function(code, writer) { - return this.stream.write(writer.flush(code)); +p._send = function(code, more) { + if(more === true) { + this.writer.addHeader(code); + } else { + return this.stream.write(this.writer.flush(code)); + } } var termBuffer = new Buffer([0x58, 0, 0, 0, 4]); @@ -92,7 +96,9 @@ p.query = function(text) { this.stream.write(this.writer.addCString(text).flush(0x51)); }; -p.parse = function(query) { +//send parse message +//"more" === true to buffer the message until flush() is called +p.parse = function(query, more) { //expect something like this: // { name: 'queryName', // text: 'select * from blah', @@ -111,13 +117,13 @@ p.parse = function(query) { buffer.addInt32(query.types[i]); } - //0x50 = 'P' - this._send(0x50, buffer); - - return this; + var code = 0x50; + this._send(code, more); }; -p.bind = function(config) { +//send bind message +//"more" === true to buffer the message until flush() is called +p.bind = function(config, more) { //normalize config config = config || {}; config.portal = config.portal || ''; @@ -149,11 +155,12 @@ p.bind = function(config) { buffer.addInt16(0); // format codes to use text } //0x42 = 'B' - - this._send(0x42, buffer); + this._send(0x42, more); }; -p.execute = function(config) { +//send execute message +//"more" === true to buffer the message until flush() is called +p.execute = function(config, more) { config = config || {}; config.portal = config.portal || ''; config.rows = config.rows || ''; @@ -162,28 +169,34 @@ p.execute = function(config) { .addInt32(config.rows); //0x45 = 'E' - this._send(0x45, buffer); + this._send(0x45, more); }; var emptyBuffer = Buffer(0); p.flush = function() { //0x48 = 'H' - this._send(0x48,this.writer.add(emptyBuffer)); + this.writer.add(emptyBuffer) + this._send(0x48); } p.sync = function() { - //0x53 = 'S' - this._send(0x53, this.writer.add(emptyBuffer)); + //clear out any pending data in the writer + this.writer.flush(0) + + this.writer.add(emptyBuffer); + this._send(0x53); }; p.end = function() { //0x58 = 'X' - this._send(0x58, this.writer.add(emptyBuffer)); + this.writer.add(emptyBuffer); + this._send(0x58); }; -p.describe = function(msg) { - this._send(0x44, this.writer.addCString(msg.type + (msg.name || ''))); +p.describe = function(msg, more) { + this.writer.addCString(msg.type + (msg.name || '')); + this._send(0x44, more); }; //parsing methods diff --git a/lib/defaults.js b/lib/defaults.js index 3bd8e794..52b30896 100644 --- a/lib/defaults.js +++ b/lib/defaults.js @@ -1,10 +1,10 @@ module.exports = { //database user's name - user: '', + user: process.env.USER, //name of database to connect - database: '', + database: process.env.USER, //database user's password - password: '', + password: null, //database port port: 5432, //number of rows to return at a time from a prepared statement's diff --git a/lib/index.js b/lib/index.js index 4fe24fe5..2f84f0a8 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,144 +1,17 @@ var EventEmitter = require('events').EventEmitter; -var sys = require('sys'); -var net = require('net'); -var Pool = require(__dirname + '/utils').Pool; var Client = require(__dirname+'/client'); var defaults = require(__dirname + '/defaults'); - -//wrap up common connection management boilerplate -var connect = function(config, callback) { - if(poolEnabled()) { - return getPooledClient(config, callback) - } - - var client = new Client(config); - client.connect(); - - var onError = function(error) { - client.connection.removeListener('readyForQuery', onReady); - callback(error); - } - - var onReady = function() { - client.removeListener('error', onError); - callback(null, client); - client.on('drain', client.end.bind(client)); - } - - client.once('error', onError); - - //TODO refactor - //i don't like reaching into the client's connection for attaching - //to specific events here - client.connection.once('readyForQuery', onReady); -} - - -//connection pool global cache -var clientPools = { -} - -var poolEnabled = function() { - return defaults.poolSize; -} - -var log = function() { - //do nothing -} - -//for testing -// var log = function() { -// console.log.apply(console, arguments); -// } - -var getPooledClient = function(config, callback) { - //lookup pool using config as key - //TODO this don't work so hot w/ object configs - var pool = clientPools[config]; - - //create pool if doesn't exist - if(!pool) { - //log("creating pool %s", config) - pool = clientPools[config] = new Pool(defaults.poolSize, function() { - //log("creating new client in pool %s", config) - var client = new Client(config); - client.connected = false; - return client; - }) - } - - pool.checkOut(function(err, client) { - - //if client already connected just - //pass it along to the callback and return - if(client.connected) { - callback(null, client); - return; - } - - var onError = function(error) { - client.connection.removeListener('readyForQuery', onReady); - callback(error); - pool.checkIn(client); - } - - var onReady = function() { - client.removeListener('error', onError); - client.connected = true; - callback(null, client); - client.on('drain', function() { - pool.checkIn(client); - }); - } - - client.once('error', onError); - - //TODO refactor - //i don't like reaching into the client's connection for attaching - //to specific events here - client.connection.once('readyForQuery', onReady); - - client.connect(); - - }); -} - -//destroys the world -//or optionally only a single pool -//mostly used for testing or -//a hard shutdown -var end = function(name) { - if(!name) { - for(var poolName in clientPools) { - end(poolName) - return - } - } - var pool = clientPools[name]; - //log("destroying pool %s", name); - pool.waits.forEach(function(wait) { - wait(new Error("Client is being destroyed")) - }) - pool.waits = []; - pool.items.forEach(function(item) { - var client = item.ref; - if(client.activeQuery) { - //log("client is still active, waiting for it to complete"); - client.on('drain', client.end.bind(client)) - } else { - client.end(); - } - }) - //remove reference to pool lookup - clientPools[name] = null; - delete(clientPools[name]) -} - +var pool = require(__dirname + "/client-pool").init(Client); module.exports = { Client: Client, Connection: require(__dirname + '/connection'), - connect: connect, - end: end, + connect: pool.connect, + end: pool.end, defaults: defaults } + +//lazy require native module...the c++ may not have been compiled +module.exports.__defineGetter__("native", function() { + return require(__dirname + '/native'); +}) diff --git a/lib/native.js b/lib/native.js new file mode 100644 index 00000000..00657d33 --- /dev/null +++ b/lib/native.js @@ -0,0 +1,186 @@ +//require the c++ bindings & export to javascript +var sys = require('sys'); +var EventEmitter = require('events').EventEmitter; + +var binding = require(__dirname + '/../build/default/binding'); +var utils = require(__dirname + "/utils"); +var types = require(__dirname + "/types"); +var Connection = binding.Connection; +var p = Connection.prototype; + +var nativeConnect = p.connect; + +p.connect = function() { + var self = this; + utils.buildLibpqConnectionString(this._config, function(err, conString) { + if(err) return self.emit('error', err); + nativeConnect.call(self, conString); + }) +} + +p.query = function(config, values, callback) { + var q = new NativeQuery(config, values, callback); + this._queryQueue.push(q); + this._pulseQueryQueue(); + return q; +} + +p._pulseQueryQueue = function() { + if(!this._connected) { + return; + } + if(this._activeQuery) { + return; + } + var query = this._queryQueue.shift(); + if(!query) { + this.emit('drain'); + return; + } + this._activeQuery = query; + if(query.name) { + if(this._namedQueries[query.name]) { + this._sendQueryPrepared(query.name, query.values||[]); + } else { + this._namedQuery = true; + this._namedQueries[query.name] = true; + this._sendPrepare(query.name, query.text, (query.values||[]).length); + } + } + else if(query.values) { + //call native function + this._sendQueryWithParams(query.text, query.values) + } else { + //call native function + this._sendQuery(query.text); + } +} + +var ctor = function(config) { + config = config || {}; + var connection = new Connection(); + connection._queryQueue = []; + connection._namedQueries = {}; + connection._activeQuery = null; + connection._config = utils.normalizeConnectionInfo(config); + connection.on('connect', function() { + connection._connected = true; + connection._pulseQueryQueue(); + }); + + //proxy some events to active query + connection.on('_row', function(row) { + connection._activeQuery.handleRow(row); + }) + connection.on('_error', function(err) { + //give up on trying to wait for named query prepare + this._namedQuery = false; + if(connection._activeQuery) { + connection._activeQuery.handleError(err); + } else { + connection.emit('error', err); + } + }) + connection.on('_readyForQuery', function() { + var q = this._activeQuery; + //a named query finished being prepared + if(this._namedQuery) { + this._namedQuery = false; + this._sendQueryPrepared(q.name, q.values||[]); + } else { + connection._activeQuery.handleReadyForQuery(); + connection._activeQuery = null; + connection._pulseQueryQueue(); + } + }); + return connection; +}; + +//event emitter proxy +var NativeQuery = function(text, values, callback) { + if(typeof text == 'object') { + this.text = text.text; + this.values = text.values; + this.name = text.name; + this.callback = values; + } else { + this.text = text; + this.values = values; + this.callback = callback; + if(typeof values == 'function') { + this.values = null; + this.callback = values; + } + } + if(this.callback) { + this.rows = []; + } + //normalize values + if(this.values) { + for(var i = 0, len = this.values.length; i < len; i++) { + var item = this.values[i]; + switch(typeof item) { + case 'undefined': + this.values[i] = null; + break; + case 'object': + this.values[i] = item === null ? null : JSON.stringify(item); + break; + case 'string': + //value already string + break; + default: + //numbers + this.values[i] = item.toString(); + } + } + } + + EventEmitter.call(this); +}; + +sys.inherits(NativeQuery, EventEmitter); +var p = NativeQuery.prototype; + +//maps from native rowdata into api compatible row object +var mapRowData = function(row) { + var result = {}; + for(var i = 0, len = row.length; i < len; i++) { + var item = row[i]; + result[item.name] = item.value == null ? null : types.getStringTypeParser(item.type)(item.value); + } + return result; +} + +p.handleRow = function(rowData) { + var row = mapRowData(rowData); + if(this.callback) { + this.rows.push(row); + } + this.emit('row', row); +}; + +p.handleError = function(error) { + if(this.callback) { + this.callback(error); + this.callback = null; + } else { + this.emit('error', error); + } +} + +p.handleReadyForQuery = function() { + if(this.callback) { + this.callback(null, { rows: this.rows }); + } + this.emit('end'); +}; + +var pool = require(__dirname + '/client-pool').init(ctor); + +module.exports = { + Client: ctor, + connect: pool.connect, + end: pool.end, + defaults: require(__dirname + '/defaults') +}; diff --git a/lib/query.js b/lib/query.js index 05c93872..39c91aef 100644 --- a/lib/query.js +++ b/lib/query.js @@ -1,8 +1,7 @@ var EventEmitter = require('events').EventEmitter; var sys = require('sys');var sys = require('sys'); var Result = require(__dirname + "/result"); -var TextParser = require(__dirname + "/textParser"); -var BinaryParser = require(__dirname + "/binaryParser"); +var types = require(__dirname + "/types"); var Query = function(config) { this.text = config.text; @@ -11,9 +10,6 @@ var Query = function(config) { this.types = config.types; this.name = config.name; this.binary = config.binary; - //for code clarity purposes we'll declare this here though it's not - //set or used until a rowDescription message comes in - this.rowDescription = null; this.callback = config.callback; this._fieldNames = []; this._fieldConverters = []; @@ -40,65 +36,13 @@ var noParse = function(val) { p.handleRowDescription = function(msg) { this._fieldNames = []; this._fieldConverters = []; - - var parsers = { - text: new TextParser(), - binary: new BinaryParser() - }; - var len = msg.fields.length; for(var i = 0; i < len; i++) { var field = msg.fields[i]; - var dataTypeId = field.dataTypeID; var format = field.format; this._fieldNames[i] = field.name; - switch(dataTypeId) { - case 16: - this._fieldConverters[i] = parsers[format].parseBool; - break; - case 20: - this._fieldConverters[i] = parsers[format].parseInt64; - break; - case 21: - this._fieldConverters[i] = parsers[format].parseInt16; - break; - case 23: - this._fieldConverters[i] = parsers[format].parseInt32; - break; - case 25: - this._fieldConverters[i] = parsers[format].parseText; - break; - case 26: - this._fieldConverters[i] = parsers[format].parseInt64; - break; - case 700: - this._fieldConverters[i] = parsers[format].parseFloat32; - break; - case 701: - this._fieldConverters[i] = parsers[format].parseFloat64; - break; - case 1700: - this._fieldConverters[i] = parsers[format].parseNumeric; - break; - case 16: - this._fieldConverters[i] = parsers[format].parseBool; - break; - case 1114: - case 1184: - this._fieldConverters[i] = parsers[format].parseDate; - break; - case 1008: - case 1009: - this._fieldConverters[i] = parsers[format].parseStringArray; - break; - case 1007: - case 1016: - this._fieldConverters[i] = parsers[format].parseIntArray; - break; - default: - this._fieldConverters[i] = dataTypeParsers[dataTypeId] || noParse; - break; - } + this._fieldConverters[i] = types.getTypeParser(field.dataTypeID, format); + }; }; @@ -162,7 +106,7 @@ p.getRows = function(connection) { connection.execute({ portal: this.name, rows: this.rows - }); + }, true); connection.flush(); }; @@ -177,11 +121,11 @@ p.prepare = function(connection) { text: self.text, name: self.name, types: self.types - }); + }, true); connection.parsedStatements[this.name] = true; } - //TODO is there some btter way to prepare values for the database? + //TODO is there some better way to prepare values for the database? if(self.values) { self.values = self.values.map(function(val) { return (val instanceof Date) ? JSON.stringify(val) : val; @@ -194,17 +138,14 @@ p.prepare = function(connection) { statement: self.name, values: self.values, binary: self.binary - }); + }, true); connection.describe({ type: 'P', name: self.name || "" - }); + }, true); this.getRows(connection); }; -var dataTypeParsers = { -}; - module.exports = Query; diff --git a/lib/textParser.js b/lib/textParser.js deleted file mode 100644 index 839f9488..00000000 --- a/lib/textParser.js +++ /dev/null @@ -1,91 +0,0 @@ -var TextParser = function(config) { - config = config || {}; -}; - -var p = TextParser.prototype; - -p.parseBool = function(value) { - return (value === 't'); -} - -p.parseInt64 = p.parseInt32 = p.parseInt16 = function(value) { - return parseInt(value); -} - -p.parseNumeric = p.parseFloat64 = p.parseFloat32 = function(value) { - return parseFloat(value); -} - -p.parseDate = function(value) { - //TODO this could do w/ a refactor - - var dateMatcher = /(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})(\.\d{1,})?/; - - var match = dateMatcher.exec(value); - var year = match[1]; - var month = parseInt(match[2],10)-1; - var day = match[3]; - var hour = parseInt(match[4],10); - var min = parseInt(match[5],10); - var seconds = parseInt(match[6], 10); - - var miliString = match[7]; - var mili = 0; - if(miliString) { - mili = 1000 * parseFloat(miliString); - } - - var tZone = /([Z|+\-])(\d{2})?(\d{2})?/.exec(value.split(' ')[1]); - //minutes to adjust for timezone - var tzAdjust = 0; - - if(tZone) { - var type = tZone[1]; - switch(type) { - case 'Z': break; - case '-': - tzAdjust = -(((parseInt(tZone[2],10)*60)+(parseInt(tZone[3]||0,10)))); - break; - case '+': - tzAdjust = (((parseInt(tZone[2],10)*60)+(parseInt(tZone[3]||0,10)))); - break; - default: - throw new Error("Unidentifed tZone part " + type); - } - } - - var utcOffset = Date.UTC(year, month, day, hour, min, seconds, mili); - - var date = new Date(utcOffset - (tzAdjust * 60* 1000)); - return date; -} - -p.parseIntArray = function(value) { - return JSON.parse(val.replace("{","[").replace("}","]")); -}; - -p.parseStringArray = function(value) { - if (!value) return null; - if (value[0] !== '{' || value[value.length-1] !== '}') - throw "Not postgresql array! (" + value + ")"; - - var x = value.substring(1, value.length - 1); - x = x.match(/(NULL|[^,]+|"((?:.|\n|\r)*?)(?!\\)"|\{((?:.|\n|\r)*?(?!\\)\}) (,|$))/mg); - if (x === null) throw "Not postgre array"; - return x.map(function (el) { - if (el === 'NULL') return null; - if (el[0] === '{') return arguments.callee(el); - if (el[0] === '\"') return el.substring(1, el.length - 1).replace('\\\"', '\"'); - return el; - }); -}; - -p.parseText = function(value) { - return value; -}; - -p.parseBool = function(value) { - return value == 't'; -}; - -module.exports = TextParser; diff --git a/lib/textParsers.js b/lib/textParsers.js new file mode 100644 index 00000000..110d18ce --- /dev/null +++ b/lib/textParsers.js @@ -0,0 +1,123 @@ +//parses PostgreSQL server formatted date strings into javascript date objects +var parseDate = function(isoDate) { + //TODO this could do w/ a refactor + var dateMatcher = /(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})(\.\d{1,})?/; + + var match = dateMatcher.exec(isoDate); + //could not parse date + if(!match) { + return null; + } + var year = match[1]; + var month = parseInt(match[2],10)-1; + var day = match[3]; + var hour = parseInt(match[4],10); + var min = parseInt(match[5],10); + var seconds = parseInt(match[6], 10); + + var miliString = match[7]; + var mili = 0; + if(miliString) { + mili = 1000 * parseFloat(miliString); + } + + var tZone = /([Z|+\-])(\d{2})?(\d{2})?/.exec(isoDate.split(' ')[1]); + //minutes to adjust for timezone + var tzAdjust = 0; + + if(tZone) { + var type = tZone[1]; + switch(type) { + case 'Z': break; + case '-': + tzAdjust = -(((parseInt(tZone[2],10)*60)+(parseInt(tZone[3]||0,10)))); + break; + case '+': + tzAdjust = (((parseInt(tZone[2],10)*60)+(parseInt(tZone[3]||0,10)))); + break; + default: + throw new Error("Unidentifed tZone part " + type); + } + } + + var utcOffset = Date.UTC(year, month, day, hour, min, seconds, mili); + + var date = new Date(utcOffset - (tzAdjust * 60* 1000)); + return date; +}; + +var parseBool = function(val) { + return val === 't'; +} + +var parseIntegerArray = function(val) { + return JSON.parse(val.replace("{","[").replace("}","]")); +}; + +var parseStringArray = function(val) { + if (!val) return null; + if (val[0] !== '{' || val[val.length-1] !== '}') + throw "Not postgresql array! (" + arrStr + ")"; + + var x = val.substring(1, val.length - 1); + x = x.match(/(NULL|[^,]+|"((?:.|\n|\r)*?)(?!\\)"|\{((?:.|\n|\r)*?(?!\\)\}) (,|$))/mg); + if (x === null) throw "Not postgre array"; + return x.map(function (el) { + if (el === 'NULL') return null; + if (el[0] === '{') return arguments.callee(el); + if (el[0] === '\"') return el.substring(1, el.length - 1).replace('\\\"', '\"'); + return el; + }); +}; + + +var NUM = '([+-]?\\d+)'; +var YEAR = NUM + '\\s+years?'; +var MON = NUM + '\\s+mons?'; +var DAY = NUM + '\\s+days?'; +var TIME = '([+-])?(\\d\\d):(\\d\\d):(\\d\\d)'; +var INTERVAL = [YEAR,MON,DAY,TIME].map(function(p){ return "("+p+")?" }).join('\\s*'); + +var parseInterval = function(val) { + if (!val) return {}; + var m = new RegExp(INTERVAL).exec(val); + var i = {}; + if (m[2]) i.years = parseInt(m[2]); + if (m[4]) i.months = parseInt(m[4]); + if (m[6]) i.days = parseInt(m[6]); + if (m[9]) i.hours = parseInt(m[9]); + if (m[10]) i.minutes = parseInt(m[10]); + if (m[11]) i.seconds = parseInt(m[11]); + if (m[8] == '-'){ + if (i.hours) i.hours *= -1; + if (i.minutes) i.minutes *= -1; + if (i.seconds) i.seconds *= -1; + } + for (field in i){ + if (i[field] == 0) + delete i[field]; + } + return i; +}; + +var init = function(register) { + register(20, parseInt); + register(21, parseInt); + register(23, parseInt); + register(26, parseInt); + register(1700, parseFloat); + register(700, parseFloat); + register(701, parseFloat); + register(16, parseBool); + register(1114, parseDate); + register(1184, parseDate); + register(1007, parseIntegerArray); + register(1016, parseIntegerArray); + register(1008, parseStringArray); + register(1009, parseStringArray); + register(1186, parseInterval); +}; + +module.exports = { + init: init, +}; diff --git a/lib/types.js b/lib/types.js new file mode 100644 index 00000000..2e62a68a --- /dev/null +++ b/lib/types.js @@ -0,0 +1,35 @@ +var textParsers = require(__dirname + "/textParsers"), +binaryParsers = require(__dirname + "/binaryParsers"); + +var typeParsers = { + text: {}, + binary: {} +}; + +//the empty parse function +var noParse = function(val) { + return val; +} + +//returns a function used to convert a specific type (specified by +//oid) into a result javascript type +var getTypeParser = function(oid, format) { + if (!typeParsers[format]) + return noParse; + + return typeParsers[format][oid] || noParse; +}; + + +textParsers.init(function(oid, converter) { + typeParsers.text[oid] = converter; +}); + +binaryParsers.init(function(oid, converter) { + typeParsers.binary[oid] = converter; +}); + + +module.exports = { + getTypeParser: getTypeParser, +} diff --git a/lib/utils.js b/lib/utils.js index 32726b8f..80483307 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,4 +1,5 @@ var url = require('url'); +var defaults = require(__dirname + "/defaults"); var events = require('events'); var sys = require('sys'); @@ -11,6 +12,7 @@ if(typeof events.EventEmitter.prototype.once !== 'function') { }); }; } + var Pool = function(maxSize, createFn) { events.EventEmitter.call(this); this.maxSize = maxSize; @@ -18,10 +20,15 @@ var Pool = function(maxSize, createFn) { this.items = []; this.waits = []; } + sys.inherits(Pool, events.EventEmitter); + var p = Pool.prototype; p.checkOut = function(callback) { + if(!this.maxSize) { + return callback(null, this.createFn()); + } var len = 0; for(var i = 0, len = this.items.length; i < len; i++) { var item = this.items[i]; @@ -32,13 +39,7 @@ p.checkOut = function(callback) { //check if we can create a new item if(this.items.length < this.maxSize && this.createFn) { var result = this.createFn(); - var item = result; - //create function can return item conforming to interface - //of stored items to allow for create function to create - //checked out items - if(typeof item.checkedIn == "undefined") { - var item = {ref: result, checkedIn: true} - } + var item = {ref: result, checkedIn: true} this.items.push(item); if(item.checkedIn) { return this._pulse(item, callback) @@ -73,17 +74,91 @@ p._pulse = function(item, cb) { return true; } return false; +}; + +var parseConnectionString = function(str) { + //unix socket + if(str.charAt(0) === '/') { + return { host: str }; + } + var result = url.parse(str); + var config = {}; + config.host = result.hostname; + config.database = result.pathname ? result.pathname.slice(1) : null + var auth = (result.auth || ':').split(':'); + config.user = auth[0]; + config.password = auth[1]; + config.port = result.port; + return config; +}; + +//allows passing false as property to remove it from config +var norm = function(config, propName) { + config[propName] = (config[propName] || (config[propName] === false ? undefined : defaults[propName])) +}; + +//normalizes connection info +//which can be in the form of an object +//or a connection string +var normalizeConnectionInfo = function(config) { + switch(typeof config) { + case 'object': + norm(config, 'user'); + norm(config, 'password'); + norm(config, 'host'); + norm(config, 'port'); + norm(config, 'database'); + return config; + case 'string': + return normalizeConnectionInfo(parseConnectionString(config)); + default: + throw new Error("Unrecognized connection config parameter: " + config); + } +}; + + +var add = function(params, config, paramName) { + var value = config[paramName]; + if(value) { + params.push(paramName+"='"+value+"'"); + } +} + +//builds libpq specific connection string +//from a supplied config object +//the config object conforms to the interface of the config object +//accepted by the pure javascript client +var getLibpgConString = function(config, callback) { + if(typeof config == 'object') { + var params = [] + add(params, config, 'user'); + add(params, config, 'password'); + add(params, config, 'port'); + if(config.database) { + params.push("dbname='" + config.database + "'"); + } + if(config.host) { + if(config.host != 'localhost' && config.host != '127.0.0.1') { + //do dns lookup + return require('dns').lookup(config.host, 4, function(err, address) { + if(err) return callback(err, null); + params.push("hostaddr="+address) + callback(null, params.join(" ")) + }) + } + params.push("hostaddr=127.0.0.1 "); + } + callback(null, params.join(" ")); + } else { + throw new Error("Unrecognized config type for connection"); + } } module.exports = { Pool: Pool, - parseConnectionString: function(str) { - var result = url.parse(str); - result.host = result.hostname; - result.database = result.pathname ? result.pathname.slice(1) : null - var auth = (result.auth || ':').split(':'); - result.user = auth[0]; - result.password = auth[1]; - return result; - } + normalizeConnectionInfo: normalizeConnectionInfo, + //only exported here to make testing of this method possible + //since it contains quite a bit of logic and testing for + //each connection scenario in an integration test is impractical + buildLibpqConnectionString: getLibpgConString } diff --git a/lib/writer.js b/lib/writer.js index f87ba29f..d3f34d28 100644 --- a/lib/writer.js +++ b/lib/writer.js @@ -1,7 +1,11 @@ +//binary data writer tuned for creating +//postgres message packets as effeciently as possible by reusing the +//same buffer to avoid memcpy and limit memory allocations var Writer = function(size) { this.size = size || 1024; this.buffer = new Buffer(this.size + 5); this.offset = 5; + this.headerPosition = 0; }; var p = Writer.prototype; @@ -70,18 +74,32 @@ p.add = function(otherBuffer) { } p.clear = function() { - this.offset=5; + this.offset = 5; + this.headerPosition = 0; + this.lastEnd = 0; +} + +//appends a header block to all the written data since the last +//subsequent header or to the beginning if there is only one data block +p.addHeader = function(code, last) { + var origOffset = this.offset; + this.offset = this.headerPosition; + this.buffer[this.offset++] = code; + //length is everything in this packet minus the code + this.addInt32(origOffset - (this.headerPosition+1)) + //set next header position + this.headerPosition = origOffset; + //make space for next header + this.offset = origOffset; + if(!last) { + this._ensure(5); + this.offset += 5; + } } p.join = function(code) { if(code) { - var end = this.offset; - this.offset = 0; - this.buffer[this.offset++] = code; - //write the length which is length of entire packet not including - //message type code byte - this.addInt32(end - 1); - this.offset = end; + this.addHeader(code, true); } return this.buffer.slice(code ? 0 : 5, this.offset); } diff --git a/package.json b/package.json index 07810497..ac30385b 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,17 @@ { "name": "pg", - "version": "0.2.7", - "description": "Pure JavaScript PostgreSQL client", + "version": "0.5.0", + "description": "PostgreSQL client - pure javascript & libpq with the same API", + "keywords" : ["postgres", "pg", "libpq", "postgre", "database", "rdbms"], "homepage": "http://github.com/brianc/node-postgres", "repository" : { "type" : "git", "url" : "git://github.com/brianc/node-postgres.git" }, "author" : "Brian Carlson ", - "main" : "./lib/index", - "directories" : { "lib" : "./lib" }, - "scripts" : { "test" : "make test" }, + "main" : "./lib", + "scripts" : { + "test" : "make test", + "install" : "node-waf configure build || true" + }, "engines" : { "node": ">= 0.2.2" } } diff --git a/src/binding.cc b/src/binding.cc new file mode 100644 index 00000000..0123021c --- /dev/null +++ b/src/binding.cc @@ -0,0 +1,632 @@ +#include +#include +#include +#include +#include +#include + +#define LOG(msg) printf("%s\n",msg) +#define TRACE(msg) //printf("%s\n", msg); + + +#define THROW(msg) return ThrowException(Exception::Error(String::New(msg))); + +using namespace v8; +using namespace node; + +static Persistent connect_symbol; +static Persistent error_symbol; +static Persistent ready_symbol; +static Persistent row_symbol; +static Persistent notice_symbol; +static Persistent severity_symbol; +static Persistent code_symbol; +static Persistent message_symbol; +static Persistent detail_symbol; +static Persistent hint_symbol; +static Persistent position_symbol; +static Persistent internalPosition_symbol; +static Persistent internalQuery_symbol; +static Persistent where_symbol; +static Persistent file_symbol; +static Persistent line_symbol; +static Persistent routine_symbol; +static Persistent name_symbol; +static Persistent value_symbol; +static Persistent type_symbol; +static Persistent channel_symbol; +static Persistent payload_symbol; + +class Connection : public EventEmitter { + +public: + + //creates the V8 objects & attaches them to the module (target) + static void + Init (Handle target) + { + HandleScope scope; + Local t = FunctionTemplate::New(New); + + t->Inherit(EventEmitter::constructor_template); + t->InstanceTemplate()->SetInternalFieldCount(1); + t->SetClassName(String::NewSymbol("Connection")); + + connect_symbol = NODE_PSYMBOL("connect"); + error_symbol = NODE_PSYMBOL("_error"); + ready_symbol = NODE_PSYMBOL("_readyForQuery"); + notice_symbol = NODE_PSYMBOL("notice"); + row_symbol = NODE_PSYMBOL("_row"); + severity_symbol = NODE_PSYMBOL("severity"); + code_symbol = NODE_PSYMBOL("code"); + message_symbol = NODE_PSYMBOL("message"); + detail_symbol = NODE_PSYMBOL("detail"); + hint_symbol = NODE_PSYMBOL("hint"); + position_symbol = NODE_PSYMBOL("position"); + internalPosition_symbol = NODE_PSYMBOL("internalPosition"); + internalQuery_symbol = NODE_PSYMBOL("internalQuery"); + where_symbol = NODE_PSYMBOL("where"); + file_symbol = NODE_PSYMBOL("file"); + line_symbol = NODE_PSYMBOL("line"); + routine_symbol = NODE_PSYMBOL("routine"); + name_symbol = NODE_PSYMBOL("name"); + value_symbol = NODE_PSYMBOL("value"); + type_symbol = NODE_PSYMBOL("type"); + channel_symbol = NODE_PSYMBOL("channel"); + payload_symbol = NODE_PSYMBOL("payload"); + + + NODE_SET_PROTOTYPE_METHOD(t, "connect", Connect); + NODE_SET_PROTOTYPE_METHOD(t, "_sendQuery", SendQuery); + NODE_SET_PROTOTYPE_METHOD(t, "_sendQueryWithParams", SendQueryWithParams); + NODE_SET_PROTOTYPE_METHOD(t, "_sendPrepare", SendPrepare); + NODE_SET_PROTOTYPE_METHOD(t, "_sendQueryPrepared", SendQueryPrepared); + NODE_SET_PROTOTYPE_METHOD(t, "end", End); + + target->Set(String::NewSymbol("Connection"), t->GetFunction()); + TRACE("created class"); + } + + //static function called by libev as callback entrypoint + static void + io_event(EV_P_ ev_io *w, int revents) + { + TRACE("Received IO event"); + Connection *connection = static_cast(w->data); + connection->HandleIOEvent(revents); + } + + //v8 entry point into Connection#connect + static Handle + Connect(const Arguments& args) + { + HandleScope scope; + Connection *self = ObjectWrap::Unwrap(args.This()); + if(args.Length() == 0 || !args[0]->IsString()) { + THROW("Must include connection string as only argument to connect"); + } + + String::Utf8Value conninfo(args[0]->ToString()); + bool success = self->Connect(*conninfo); + if(!success) { + self -> EmitLastError(); + self -> DestroyConnection(); + } + + return Undefined(); + } + + //v8 entry point into Connection#_sendQuery + static Handle + SendQuery(const Arguments& args) + { + HandleScope scope; + Connection *self = ObjectWrap::Unwrap(args.This()); + if(!args[0]->IsString()) { + THROW("First parameter must be a string query"); + } + + char* queryText = MallocCString(args[0]); + int result = self->Send(queryText); + free(queryText); + if(result == 0) { + THROW("PQsendQuery returned error code"); + } + //TODO should we flush before throw? + self->Flush(); + return Undefined(); + } + + //v8 entry point into Connection#_sendQueryWithParams + static Handle + SendQueryWithParams(const Arguments& args) + { + HandleScope scope; + //dispatch non-prepared parameterized query + return DispatchParameterizedQuery(args, false); + } + + //v8 entry point into Connection#_sendPrepare(string queryName, string queryText, int nParams) + static Handle + SendPrepare(const Arguments& args) + { + HandleScope scope; + + Connection *self = ObjectWrap::Unwrap(args.This()); + String::Utf8Value queryName(args[0]); + String::Utf8Value queryText(args[1]); + int length = args[2]->Int32Value(); + self->SendPrepare(*queryName, *queryText, length); + + return Undefined(); + } + + //v8 entry point into Connection#_sendQueryPrepared(string queryName, string[] paramValues) + static Handle + SendQueryPrepared(const Arguments& args) + { + HandleScope scope; + //dispatch prepared parameterized query + return DispatchParameterizedQuery(args, true); + } + + static Handle + DispatchParameterizedQuery(const Arguments& args, bool isPrepared) + { + HandleScope scope; + Connection *self = ObjectWrap::Unwrap(args.This()); + + String::Utf8Value queryName(args[0]); + //TODO this is much copy/pasta code + if(!args[0]->IsString()) { + THROW("First parameter must be a string"); + } + + if(!args[1]->IsArray()) { + THROW("Values must be an array"); + } + + Handle params = args[1]; + + Local jsParams = Local::Cast(args[1]); + int len = jsParams->Length(); + + + char** paramValues = ArgToCStringArray(jsParams); + if(!paramValues) { + THROW("Unable to allocate char **paramValues from Local of v8 params"); + } + + char* queryText = MallocCString(args[0]); + + int result = 0; + if(isPrepared) { + result = self->SendPreparedQuery(queryText, len, paramValues); + } else { + result = self->SendQueryParams(queryText, len, paramValues); + } + + free(queryText); + ReleaseCStringArray(paramValues, len); + if(result == 1) { + return Undefined(); + } + self->EmitLastError(); + THROW("Postgres returned non-1 result from query dispatch."); + } + + //v8 entry point into Connection#end + static Handle + End(const Arguments& args) + { + HandleScope scope; + + Connection *self = ObjectWrap::Unwrap(args.This()); + + self->End(); + return Undefined(); + } + + ev_io read_watcher_; + ev_io write_watcher_; + PGconn *connection_; + bool connecting_; + Connection () : EventEmitter () + { + connection_ = NULL; + connecting_ = false; + + TRACE("Initializing ev watchers"); + ev_init(&read_watcher_, io_event); + read_watcher_.data = this; + ev_init(&write_watcher_, io_event); + write_watcher_.data = this; + } + + ~Connection () + { + } + +protected: + //v8 entry point to constructor + static Handle + New (const Arguments& args) + { + HandleScope scope; + Connection *connection = new Connection(); + connection->Wrap(args.This()); + + return args.This(); + } + + int Send(const char *queryText) + { + return PQsendQuery(connection_, queryText); + } + + int SendQueryParams(const char *command, const int nParams, const char * const *paramValues) + { + return PQsendQueryParams(connection_, command, nParams, NULL, paramValues, NULL, NULL, 0); + } + + int SendPrepare(const char *name, const char *command, const int nParams) + { + return PQsendPrepare(connection_, name, command, nParams, NULL); + } + + int SendPreparedQuery(const char *name, int nParams, const char * const *paramValues) + { + return PQsendQueryPrepared(connection_, name, nParams, paramValues, NULL, NULL, 0); + } + + //flushes socket + void Flush() + { + if(PQflush(connection_) == 1) { + TRACE("Flushing"); + ev_io_start(EV_DEFAULT_ &write_watcher_); + } + } + + //safely destroys the connection at most 1 time + void DestroyConnection() + { + if(connection_ != NULL) { + PQfinish(connection_); + connection_ = NULL; + } + } + + //initializes initial async connection to postgres via libpq + //and hands off control to libev + bool Connect(const char* conninfo) + { + connection_ = PQconnectStart(conninfo); + + if (!connection_) { + LOG("Connection couldn't be created"); + } + + if (PQsetnonblocking(connection_, 1) == -1) { + LOG("Unable to set connection to non-blocking"); + return false; + } + + ConnStatusType status = PQstatus(connection_); + + if(CONNECTION_BAD == status) { + LOG("Bad connection status"); + return false; + } + + int fd = PQsocket(connection_); + if(fd < 0) { + LOG("socket fd was negative. error"); + return false; + } + + assert(PQisnonblocking(connection_)); + + PQsetNoticeProcessor(connection_, NoticeReceiver, this); + + TRACE("Setting watchers to socket"); + ev_io_set(&read_watcher_, fd, EV_READ); + ev_io_set(&write_watcher_, fd, EV_WRITE); + + connecting_ = true; + StartWrite(); + + Ref(); + return true; + } + + static void NoticeReceiver(void *arg, const char *message) + { + Connection *self = (Connection*)arg; + self->HandleNotice(message); + } + + void HandleNotice(const char *message) + { + HandleScope scope; + Handle notice = String::New(message); + Emit(notice_symbol, 1, ¬ice); + } + + //called to process io_events from libev + void HandleIOEvent(int revents) + { + if(revents & EV_ERROR) { + LOG("Connection error."); + return; + } + + if(connecting_) { + TRACE("Processing connecting_ io"); + HandleConnectionIO(); + return; + } + + if(revents & EV_READ) { + TRACE("revents & EV_READ"); + if(PQconsumeInput(connection_) == 0) { + LOG("Something happened, consume input is 0"); + return; + } + + //declare handlescope as this method is entered via a libev callback + //and not part of the public v8 interface + HandleScope scope; + + if (PQisBusy(connection_) == 0) { + PGresult *result; + bool didHandleResult = false; + while ((result = PQgetResult(connection_))) { + HandleResult(result); + didHandleResult = true; + PQclear(result); + } + //might have fired from notification + if(didHandleResult) { + Emit(ready_symbol, 0, NULL); + } + } + + PGnotify *notify; + while ((notify = PQnotifies(connection_))) { + Local result = Object::New(); + result->Set(channel_symbol, String::New(notify->relname)); + result->Set(payload_symbol, String::New(notify->extra)); + Handle res = (Handle)result; + Emit((Handle)String::New("notification"), 1, &res); + PQfreemem(notify); + } + + } + + if(revents & EV_WRITE) { + TRACE("revents & EV_WRITE"); + if (PQflush(connection_) == 0) { + StopWrite(); + } + } + } + + void HandleResult(const PGresult* result) + { + ExecStatusType status = PQresultStatus(result); + switch(status) { + case PGRES_TUPLES_OK: + HandleTuplesResult(result); + break; + case PGRES_FATAL_ERROR: + HandleErrorResult(result); + break; + case PGRES_COMMAND_OK: + case PGRES_EMPTY_QUERY: + //do nothing + break; + default: + printf("Unrecogized query status: %s\n", PQresStatus(status)); + break; + } + } + + //maps the postgres tuple results to v8 objects + //and emits row events + //TODO look at emitting fewer events because the back & forth between + //javascript & c++ might introduce overhead (requires benchmarking) + void HandleTuplesResult(const PGresult* result) + { + int rowCount = PQntuples(result); + for(int rowNumber = 0; rowNumber < rowCount; rowNumber++) { + //create result object for this row + Local row = Array::New(); + int fieldCount = PQnfields(result); + for(int fieldNumber = 0; fieldNumber < fieldCount; fieldNumber++) { + Local field = Object::New(); + //name of field + char* fieldName = PQfname(result, fieldNumber); + field->Set(name_symbol, String::New(fieldName)); + + //oid of type of field + int fieldType = PQftype(result, fieldNumber); + field->Set(type_symbol, Integer::New(fieldType)); + + //value of field + if(PQgetisnull(result, rowNumber, fieldNumber)) { + field->Set(value_symbol, Null()); + } else { + char* fieldValue = PQgetvalue(result, rowNumber, fieldNumber); + field->Set(value_symbol, String::New(fieldValue)); + } + + row->Set(Integer::New(fieldNumber), field); + } + + //not sure about what to dealloc or scope#Close here + Handle e = (Handle)row; + Emit(row_symbol, 1, &e); + } + } + + void HandleErrorResult(const PGresult* result) + { + HandleScope scope; + Local msg = Object::New(); + AttachErrorField(result, msg, severity_symbol, PG_DIAG_SEVERITY); + AttachErrorField(result, msg, code_symbol, PG_DIAG_SQLSTATE); + AttachErrorField(result, msg, message_symbol, PG_DIAG_MESSAGE_PRIMARY); + AttachErrorField(result, msg, detail_symbol, PG_DIAG_MESSAGE_DETAIL); + AttachErrorField(result, msg, hint_symbol, PG_DIAG_MESSAGE_HINT); + AttachErrorField(result, msg, position_symbol, PG_DIAG_STATEMENT_POSITION); + AttachErrorField(result, msg, internalPosition_symbol, PG_DIAG_INTERNAL_POSITION); + AttachErrorField(result, msg, internalQuery_symbol, PG_DIAG_INTERNAL_QUERY); + AttachErrorField(result, msg, where_symbol, PG_DIAG_CONTEXT); + AttachErrorField(result, msg, file_symbol, PG_DIAG_SOURCE_FILE); + AttachErrorField(result, msg, line_symbol, PG_DIAG_SOURCE_LINE); + AttachErrorField(result, msg, routine_symbol, PG_DIAG_SOURCE_FUNCTION); + Handle m = msg; + Emit(error_symbol, 1, &m); + } + + void AttachErrorField(const PGresult *result, const Local msg, const Persistent symbol, int fieldcode) + { + char *val = PQresultErrorField(result, fieldcode); + if(val) { + msg->Set(symbol, String::New(val)); + } + } + + void End() + { + StopRead(); + StopWrite(); + DestroyConnection(); + } + +private: + void HandleConnectionIO() + { + PostgresPollingStatusType status = PQconnectPoll(connection_); + switch(status) { + case PGRES_POLLING_READING: + TRACE("Polled: PGRES_POLLING_READING"); + StopWrite(); + StartRead(); + break; + case PGRES_POLLING_WRITING: + TRACE("Polled: PGRES_POLLING_WRITING"); + StopRead(); + StartWrite(); + break; + case PGRES_POLLING_FAILED: + StopRead(); + StopWrite(); + TRACE("Polled: PGRES_POLLING_FAILED"); + EmitLastError(); + break; + case PGRES_POLLING_OK: + TRACE("Polled: PGRES_POLLING_OK"); + connecting_ = false; + StartRead(); + Emit(connect_symbol, 0, NULL); + default: + //printf("Unknown polling status: %d\n", status); + break; + } + } + + void EmitError(const char *message) + { + Local exception = Exception::Error(String::New(message)); + Emit(error_symbol, 1, &exception); + } + + void EmitLastError() + { + EmitError(PQerrorMessage(connection_)); + } + + void StopWrite() + { + TRACE("Stoping write watcher"); + ev_io_stop(EV_DEFAULT_ &write_watcher_); + } + + void StartWrite() + { + TRACE("Starting write watcher"); + ev_io_start(EV_DEFAULT_ &write_watcher_); + } + + void StopRead() + { + TRACE("Stoping read watcher"); + ev_io_stop(EV_DEFAULT_ &read_watcher_); + } + + void StartRead() + { + TRACE("Starting read watcher"); + ev_io_start(EV_DEFAULT_ &read_watcher_); + } + //Converts a v8 array to an array of cstrings + //the result char** array must be free() when it is no longer needed + //if for any reason the array cannot be created, returns 0 + static char** ArgToCStringArray(Local params) + { + int len = params->Length(); + char** paramValues = new char*[len]; + for(int i = 0; i < len; i++) { + Handle val = params->Get(i); + if(val->IsString()) { + char* cString = MallocCString(val); + //will be 0 if could not malloc + if(!cString) { + LOG("ArgToCStringArray: OUT OF MEMORY OR SOMETHING BAD!"); + ReleaseCStringArray(paramValues, i-1); + return 0; + } + paramValues[i] = cString; + } else if(val->IsNull()) { + paramValues[i] = NULL; + } else { + //a paramter was not a string + LOG("Parameter not a string"); + ReleaseCStringArray(paramValues, i-1); + return 0; + } + } + return paramValues; + } + + //helper function to release cString arrays + static void ReleaseCStringArray(char **strArray, int len) + { + for(int i = 0; i < len; i++) { + free(strArray[i]); + } + delete [] strArray; + } + + //helper function to malloc new string from v8string + static char* MallocCString(v8::Handle v8String) + { + String::Utf8Value utf8String(v8String->ToString()); + char *cString = (char *) malloc(strlen(*utf8String) + 1); + if(!cString) { + return cString; + } + strcpy(cString, *utf8String); + return cString; + } +}; + + +extern "C" void +init (Handle target) +{ + HandleScope scope; + Connection::Init(target); +} diff --git a/test/cli.js b/test/cli.js index 99490b16..b781f5b6 100644 --- a/test/cli.js +++ b/test/cli.js @@ -38,6 +38,8 @@ for(var i = 0; i < args.length; i++) { case '-t': case '--test': config.test = args[++i]; + case '--native': + config.native = (args[++i] == "true"); default: break; } diff --git a/test/integration/client/api-tests.js b/test/integration/client/api-tests.js index 428b1b71..8788047f 100644 --- a/test/integration/client/api-tests.js +++ b/test/integration/client/api-tests.js @@ -1,5 +1,13 @@ var helper = require(__dirname + '/../test-helper'); var pg = require(__dirname + '/../../../lib'); + +if(helper.args.native) { + pg = require(__dirname + '/../../../lib/native') +} + +if(helper.args.libpq) { + pg = require(__dirname + "/../../../lib/binding"); +} var connectionString = helper.connectionString(__filename); var log = function() { @@ -14,7 +22,7 @@ var sink = new helper.Sink(5, 10000, function() { test('api', function() { log("connecting to %s", connectionString) pg.connect(connectionString, assert.calls(function(err, client) { - assert.equal(err, null, "Failed to connect: " + sys.inspect(err)); + assert.equal(err, null, "Failed to connect: " + helper.sys.inspect(err)); client.query('CREATE TEMP TABLE band(name varchar(100))'); @@ -88,8 +96,8 @@ test("query errors are handled and do not bubble if callback is provded", functi client.query("SELECT OISDJF FROM LEIWLISEJLSE", assert.calls(function(err, result) { assert.ok(err); log("query error supplied error to callback") - sink.add(); - })) + sink.add(); + })) })) }) @@ -108,3 +116,16 @@ test('callback is fired once and only once', function() { }) })) }) + +test('can provide callback and config object', function() { + pg.connect(connectionString, assert.calls(function(err, client) { + assert.isNull(err); + client.query({ + name: 'boom', + text: 'select NOW()' + }, assert.calls(function(err, result) { + assert.isNull(err); + assert.equal(result.rows[0].now.getYear(), new Date().getYear()) + })) + })) +}) diff --git a/test/integration/client/big-simple-query-tests.js b/test/integration/client/big-simple-query-tests.js new file mode 100644 index 00000000..30e2d9db --- /dev/null +++ b/test/integration/client/big-simple-query-tests.js @@ -0,0 +1,79 @@ +var helper = require(__dirname+"/test-helper"); + +/* + Test to trigger a bug. + + I really dont know what's wrong but it seams that its triggered by multable big queryies with supplied values. + The test bellow can trigger the bug. +*/ + +// Big query with a where clouse from supplied value +var big_query_rows_1 = []; +var big_query_rows_2 = []; +var big_query_rows_3 = []; + +// Works +test('big simple query 1',function() { + var client = helper.client(); + client.query("select 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' as bla from person where name = '' or 1 = 1") + .on('row', function(row) { big_query_rows_1.push(row); }) + .on('error', function(error) { console.log("big simple query 1 error"); console.log(error); }); + client.on('drain', client.end.bind(client)); +}); + +// Works +test('big simple query 2',function() { + var client = helper.client(); + client.query("select 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' as bla from person where name = $1 or 1 = 1",['']) + .on('row', function(row) { big_query_rows_2.push(row); }) + .on('error', function(error) { console.log("big simple query 2 error"); console.log(error); }); + client.on('drain', client.end.bind(client)); +}); + +// Fails most of the time with 'invalid byte sequence for encoding "UTF8": 0xb9' or 'insufficient data left in message' +// If test 1 and 2 are commented out it works +test('big simple query 3',function() { + var client = helper.client(); + client.query("select 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' as bla from person where name = $1 or 1 = 1",['']) + .on('row', function(row) { big_query_rows_3.push(row); }) + .on('error', function(error) { console.log("big simple query 3 error"); console.log(error); }); + client.on('drain', client.end.bind(client)); +}); + +process.on('exit', function() { + assert.equal(big_query_rows_1.length, 26,'big simple query 1 should return 26 rows'); + assert.equal(big_query_rows_2.length, 26,'big simple query 2 should return 26 rows'); + assert.equal(big_query_rows_3.length, 26,'big simple query 3 should return 26 rows'); +}); + + +var runBigQuery = function(client) { + var rows = []; + var q = client.query("select 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' as bla from person where name = $1 or 1 = 1",[''], function(err, result) { + if(err != null) { + console.log(err); + throw Err; + } + assert.length(result.rows, 26); + }); + q.on('row', function(row) { + rows.push(row); + }) + assert.emits(q, 'end', function() { + //query ended + assert.length(rows, 26); + }) +} + +test('many times', function() { + var client = helper.client(); + for(var i = 0; i < 20; i++) { + runBigQuery(client); + } + client.on('drain', function() { + client.end(); + setTimeout(function() { + //let client disconnect fully + }, 100) + }); +}) diff --git a/test/integration/client/configuration-tests.js b/test/integration/client/configuration-tests.js index d380f73b..ee8413ab 100644 --- a/test/integration/client/configuration-tests.js +++ b/test/integration/client/configuration-tests.js @@ -1,11 +1,11 @@ -require(__dirname + '/test-helper'); +var helper = require(__dirname + '/test-helper'); var pg = require("index"); test('default values', function() { assert.same(pg.defaults,{ - user: '', - database: '', - password: '', + user: process.env.USER, + database: process.env.USER, + password: null, port: 5432, rows: 0, poolSize: 10 @@ -13,31 +13,34 @@ test('default values', function() { test('are used in new clients', function() { var client = new pg.Client(); assert.same(client,{ - user: '', - database: '', - password: '', + user: process.env.USER, + database: process.env.USER, + password: null, port: 5432 }) }) }) -test('modified values', function() { - pg.defaults.user = 'boom' - pg.defaults.password = 'zap' - pg.defaults.database = 'pow' - pg.defaults.port = 1234 - pg.defaults.host = 'blam' - pg.defaults.rows = 10 - pg.defaults.poolSize = 0 +if(!helper.args.native) { + test('modified values', function() { + pg.defaults.user = 'boom' + pg.defaults.password = 'zap' + pg.defaults.database = 'pow' + pg.defaults.port = 1234 + pg.defaults.host = 'blam' + pg.defaults.rows = 10 + pg.defaults.poolSize = 0 - test('are passed into created clients', function() { - var client = new Client(); - assert.same(client,{ - user: 'boom', - password: 'zap', - database: 'pow', - port: 1234, - host: 'blam' + test('are passed into created clients', function() { + var client = new Client(); + assert.same(client,{ + user: 'boom', + password: 'zap', + database: 'pow', + port: 1234, + host: 'blam' + }) }) }) -}) +} + diff --git a/test/integration/client/empty-query-tests.js b/test/integration/client/empty-query-tests.js index 330d32b5..475acf79 100644 --- a/test/integration/client/empty-query-tests.js +++ b/test/integration/client/empty-query-tests.js @@ -2,9 +2,10 @@ var helper = require(__dirname+'/test-helper'); var client = helper.client(); test("empty query message handling", function() { - var query = client.query(""); - assert.emits(query, 'end'); - client.on('drain', client.end.bind(client)); + assert.emits(client, 'drain', function() { + client.end(); + }); + client.query(""); }); test('callback supported', assert.calls(function() { diff --git a/test/integration/client/error-handling-tests.js b/test/integration/client/error-handling-tests.js index c9211c23..52f4e9e1 100644 --- a/test/integration/client/error-handling-tests.js +++ b/test/integration/client/error-handling-tests.js @@ -18,7 +18,9 @@ test('error handling', function(){ var query = client.query("select omfg from yodas_soda where pixistix = 'zoiks!!!'"); assert.emits(query, 'error', function(error) { - assert.equal(error.severity, "ERROR"); + test('error is a psql error', function() { + assert.equal(error.severity, "ERROR"); + }) }); }); @@ -31,14 +33,14 @@ test('error handling', function(){ test("when query is parsing", function() { //this query wont parse since there ain't no table named bang - + var ensureFuture = function(testClient) { - test("client can issue more queries successfully", function() { - var goodQuery = testClient.query("select age from boom"); - assert.emits(goodQuery, 'row', function(row) { - assert.equal(row.age, 28); - }); + test("client can issue more queries successfully", function() { + var goodQuery = testClient.query("select age from boom"); + assert.emits(goodQuery, 'row', function(row) { + assert.equal(row.age, 28); }); + }); }; var query = client.query({ @@ -60,7 +62,10 @@ test('error handling', function(){ test("query emits the error", function() { assert.emits(query, 'error', function(err) { - assert.equal(err.severity, "ERROR"); + test('error has right severity', function() { + assert.equal(err.severity, "ERROR"); + }) + ensureFuture(client); }); }); @@ -80,3 +85,13 @@ test('error handling', function(){ }); }); + +test('when connecting to invalid host', function() { + var client = new Client({ + user: 'brian', + password: '1234', + host: 'asldkfjasdf!!#1308140.com' + }) + assert.emits(client, 'error'); + client.connect(); +}) diff --git a/test/integration/client/no-data-tests.js b/test/integration/client/no-data-tests.js index 47c5f96d..341d7287 100644 --- a/test/integration/client/no-data-tests.js +++ b/test/integration/client/no-data-tests.js @@ -4,22 +4,28 @@ test("noData message handling", function() { var client = helper.client(); - client.query({ + var q = client.query({ name: 'boom', text: 'create temp table boom(id serial, size integer)' }); - - client.query({ - name: 'insert', - text: 'insert into boom(size) values($1)', - values: [100] - }); client.query({ name: 'insert', + text: 'insert into boom(size) values($1)', + values: [100] + }, function(err, result) { + if(err) { + console.log(err); + throw err; + } + }); + + client.query({ + name: 'insert', + text: 'insert into boom(size) values($1)', values: [101] }); - + var query = client.query({ name: 'fetch', text: 'select size from boom where size < $1', @@ -31,5 +37,5 @@ test("noData message handling", function() { }); client.on('drain', client.end.bind(client)); - + }); diff --git a/test/integration/client/notice-tests.js b/test/integration/client/notice-tests.js new file mode 100644 index 00000000..db7b4822 --- /dev/null +++ b/test/integration/client/notice-tests.js @@ -0,0 +1,42 @@ +var helper = require(__dirname + '/test-helper'); +test('emits notice message', function() { + var client = helper.client(); + client.query('create temp table boom(id serial, size integer)'); + assert.emits(client, 'notice', function(notice) { + assert.ok(notice != null); + //TODO ending connection after notice generates weird errors + process.nextTick(function() { + client.end(); + }) + }); +}) + +test('emits notify message', function() { + var client = helper.client(); + client.query('LISTEN boom', assert.calls(function() { + var otherClient = helper.client(); + otherClient.query('LISTEN boom', assert.calls(function() { + assert.emits(client, 'notification', function(msg) { + //make sure PQfreemem doesn't invalidate string pointers + setTimeout(function() { + assert.equal(msg.channel, 'boom'); + assert.ok(msg.payload == 'omg!' /*9.x*/ || msg.payload == '' /*8.x*/, "expected blank payload or correct payload but got " + msg.message) + client.end() + }, 100) + + }); + assert.emits(otherClient, 'notification', function(msg) { + assert.equal(msg.channel, 'boom'); + otherClient.end(); + }); + + client.query("NOTIFY boom, 'omg!'", function(err, q) { + if(err) { + //notify not supported with payload on 8.x + client.query("NOTIFY boom") + } + }); + })); + })); +}) + diff --git a/test/integration/client/result-metadata-tests.js b/test/integration/client/result-metadata-tests.js index 2f466f6f..b69028f6 100644 --- a/test/integration/client/result-metadata-tests.js +++ b/test/integration/client/result-metadata-tests.js @@ -2,24 +2,27 @@ var helper = require(__dirname + "/test-helper"); var pg = helper.pg; var conString = helper.connectionString(); -pg.connect(conString, assert.calls(function(err, client) { - assert.isNull(err); - client.query("CREATE TEMP TABLE zugzug(name varchar(10))", assert.calls(function(err, result) { +test('should return insert metadata', function() { + return false; + pg.connect(conString, assert.calls(function(err, client) { assert.isNull(err); - //let's list this as ignored for now - // process.nextTick(function() { - // test('should identify "CREATE TABLE" message', function() { - // return false; - // assert.equal(result.command, "CREATE TABLE"); - // assert.equal(result.rowCount, 0); - // }) - // }) - assert.equal(result.oid, null); - client.query("INSERT INTO zugzug(name) VALUES('more work?')", assert.calls(function(err, result) { - assert.equal(result.command, "INSERT"); - assert.equal(result.rowCount, 1); - process.nextTick(client.end.bind(client)); - return false; + client.query("CREATE TEMP TABLE zugzug(name varchar(10))", assert.calls(function(err, result) { + assert.isNull(err); + //let's list this as ignored for now + // process.nextTick(function() { + // test('should identify "CREATE TABLE" message', function() { + // return false; + // assert.equal(result.command, "CREATE TABLE"); + // assert.equal(result.rowCount, 0); + // }) + // }) + assert.equal(result.oid, null); + client.query("INSERT INTO zugzug(name) VALUES('more work?')", assert.calls(function(err, result) { + assert.equal(result.command, "INSERT"); + assert.equal(result.rowCount, 1); + process.nextTick(client.end.bind(client)); + return false; + })) })) })) -})) +}) diff --git a/test/integration/client/test-helper.js b/test/integration/client/test-helper.js index 6cc8146d..b01235c9 100644 --- a/test/integration/client/test-helper.js +++ b/test/integration/client/test-helper.js @@ -16,5 +16,6 @@ module.exports = { }, connectionString: helper.connectionString, Sink: helper.Sink, - pg: helper.pg + pg: helper.pg, + args: helper.args }; diff --git a/test/integration/client/type-coercion-tests.js b/test/integration/client/type-coercion-tests.js index 6d99de64..1d6375df 100644 --- a/test/integration/client/type-coercion-tests.js +++ b/test/integration/client/type-coercion-tests.js @@ -4,35 +4,36 @@ var connectionString = helper.connectionString(); var testForTypeCoercion = function(type){ helper.pg.connect(connectionString, function(err, client) { assert.isNull(err) - client.query("create temp table test_type(col " + type.name + ")"); + client.query("create temp table test_type(col " + type.name + ")", assert.calls(function(err, result) { + assert.isNull(err); + test("Coerces " + type.name, function() { + type.values.forEach(function(val) { - test("Coerces " + type.name, function() { - type.values.forEach(function(val) { + var insertQuery = client.query('insert into test_type(col) VALUES($1)',[val],assert.calls(function(err, result) { + assert.isNull(err); + })); - var insertQuery = client.query({ - name: 'insert type test ' + type.name, - text: 'insert into test_type(col) VALUES($1)', - values: [val] + var query = client.query({ + name: 'get type ' + type.name , + text: 'select col from test_type' + }); + query.on('error', function(err) { + console.log(err); + throw err; + }); + + assert.emits(query, 'row', function(row) { + assert.strictEqual(row.col, val, "expected " + type.name + " of " + val + " but got " + row[0]); + }, "row should have been called for " + type.name + " of " + val); + + client.query('delete from test_type'); }); - var query = client.query({ - name: 'get type ' + type.name , - text: 'select col from test_type' + client.query('drop table test_type', function() { + sink.add(); }); - - assert.emits(query, 'row', function(row) { - assert.strictEqual(row.col, val, "expected " + type.name + " of " + val + " but got " + row[0]); - }); - - client.query({ - name: 'delete values', - text: 'delete from test_type' - }); - sink.add(); - }); - - client.query('drop table test_type'); - }); + }) + })); }) }; @@ -82,14 +83,15 @@ var valueCount = 0; types.forEach(function(type) { valueCount += type.values.length; }) -sink = new helper.Sink(valueCount, function() { +sink = new helper.Sink(types.length, function() { helper.pg.end(); }) -types.forEach(testForTypeCoercion); +types.forEach(function(type) { + testForTypeCoercion(type) +}); test("timestampz round trip", function() { - var now = new Date(); var client = helper.client(); client.on('error', function(err) { @@ -124,3 +126,15 @@ test("timestampz round trip", function() { client.on('drain', client.end.bind(client)); }); +helper.pg.connect(helper.connectionString(), assert.calls(function(err, client) { + assert.isNull(err); + client.query('select null as res;', assert.calls(function(err, res) { + assert.isNull(err); + assert.strictEqual(res.rows[0].res, null) + })) + client.query('select 7 <> $1 as res;',[null], function(err, res) { + assert.isNull(err); + assert.strictEqual(res.rows[0].res, null); + client.end(); + }) +})) diff --git a/test/integration/connection-pool/unique-name-tests.js b/test/integration/connection-pool/unique-name-tests.js new file mode 100644 index 00000000..5e613883 --- /dev/null +++ b/test/integration/connection-pool/unique-name-tests.js @@ -0,0 +1,32 @@ +var helper = require(__dirname + '/test-helper'); + +helper.pg.defaults.poolSize = 1; + +var args = { + user: helper.args.user, + password: helper.args.password, + database: helper.args.database, + port: helper.args.port, + host: helper.args.host +} + +helper.pg.connect(args, assert.calls(function(err, client) { + assert.isNull(err); + client.iGotAccessed = true; + client.query("SELECT NOW()") +})) + +var moreArgs = { + user: helper.args.user + "2", + host: helper.args.host, + password: helper.args.password, + database: helper.args.database, + port: helper.args.port, + zomg: true +} + +helper.pg.connect(moreArgs, assert.calls(function(err, client) { + assert.isNull(err); + assert.ok(client.iGotAccessed === true) + client.end(); +})) diff --git a/test/integration/connection/test-helper.js b/test/integration/connection/test-helper.js index 1fa68293..6390119b 100644 --- a/test/integration/connection/test-helper.js +++ b/test/integration/connection/test-helper.js @@ -1,6 +1,6 @@ var net = require('net'); var helper = require(__dirname+'/../test-helper'); - +var Connection = require('connection'); var connect = function(callback) { var username = helper.args.user; var database = helper.args.database; @@ -19,8 +19,10 @@ var connect = function(callback) { con.password(helper.args.password); }); con.once('authenticationMD5Password', function(msg){ - var inner = Client.md5(helper.args.password+helper.args.user); - var outer = Client.md5(inner + msg.salt.toString('binary')); + //need js client even if native client is included + var client = require(__dirname +"/../../../lib/client"); + var inner = client.md5(helper.args.password+helper.args.user); + var outer = client.md5(inner + msg.salt.toString('binary')); con.password("md5"+outer); }); con.once('readyForQuery', function() { diff --git a/test/integration/test-helper.js b/test/integration/test-helper.js index 510a5855..ac0b55c8 100644 --- a/test/integration/test-helper.js +++ b/test/integration/test-helper.js @@ -1,6 +1,10 @@ var helper = require(__dirname + '/../test-helper'); + +//TODO would this be better served set at ../test-helper? +if(helper.args.native) { + Client = require(__dirname + '/../../lib/native').Client; + helper.pg = helper.pg.native; +} //export parent helper stuffs module.exports = helper; -if(helper.args.verbose) { -} diff --git a/test/native/callback-api-tests.js b/test/native/callback-api-tests.js new file mode 100644 index 00000000..d9e41aeb --- /dev/null +++ b/test/native/callback-api-tests.js @@ -0,0 +1,17 @@ +var helper = require(__dirname + "/../test-helper"); +var Client = require(__dirname + "/../../lib/native").Client; +var conString = helper.connectionString(); + +test('fires callback with results', function() { + var client = new Client(conString); + client.connect(); + client.query('SELECT 1 as num', assert.calls(function(err, result) { + assert.isNull(err); + assert.equal(result.rows[0].num, 1); + client.query('SELECT * FROM person WHERE name = $1', ['Brian'], assert.calls(function(err, result) { + assert.isNull(err); + assert.equal(result.rows[0].name, 'Brian'); + client.end(); + })) + })); +}) diff --git a/test/native/connection-tests.js b/test/native/connection-tests.js new file mode 100644 index 00000000..d418b879 --- /dev/null +++ b/test/native/connection-tests.js @@ -0,0 +1,22 @@ +var helper = require(__dirname + "/../test-helper"); +var Client = require(__dirname + "/../../lib/native").Client; + +test('connecting with wrong parameters', function() { + var con = new Client("user=asldfkj hostaddr=127.0.0.1 port=5432 dbname=asldkfj"); + assert.emits(con, 'error', function(error) { + assert.ok(error != null, "error should not be null"); + con.end(); + }); + + con.connect(); +}); + +test('connects', function() { + var con = new Client(helper.connectionString()); + con.connect(); + assert.emits(con, 'connect', function() { + test('disconnects', function() { + con.end(); + }) + }) +}) diff --git a/test/native/error-tests.js b/test/native/error-tests.js new file mode 100644 index 00000000..9a95eef7 --- /dev/null +++ b/test/native/error-tests.js @@ -0,0 +1,61 @@ +var helper = require(__dirname + "/../test-helper"); +var Client = require(__dirname + "/../../lib/native").Client; +var conString = helper.connectionString(); + +test('query with non-text as first parameter throws error', function() { + var client = new Client(conString); + client.connect(); + assert.emits(client, 'connect', function() { + assert.throws(function() { + client.query({text:{fail: true}}); + }) + client.end(); + }) +}) + +test('parameterized query with non-text as first parameter throws error', function() { + var client = new Client(conString); + client.connect(); + assert.emits(client, 'connect', function() { + assert.throws(function() { + client.query({ + text: {fail: true}, + values: [1, 2] + }) + }) + client.end(); + }) +}) + +var connect = function(callback) { + var client = new Client(conString); + client.connect(); + assert.emits(client, 'connect', function() { + callback(client); + }) +} + +test('parameterized query with non-array for second value', function() { + test('inline', function() { + connect(function(client) { + assert.throws(function() { + client.query("SELECT *", "LKSDJF") + }) + client.end(); + }) + }) + + test('config', function() { + connect(function(client) { + assert.throws(function() { + client.query({ + text: "SELECT *", + values: "ALSDKFJ" + }) + }) + client.end(); + }) + }) +}) + + diff --git a/test/native/evented-api-tests.js b/test/native/evented-api-tests.js new file mode 100644 index 00000000..2cd3cc30 --- /dev/null +++ b/test/native/evented-api-tests.js @@ -0,0 +1,115 @@ +var helper = require(__dirname + "/../test-helper"); +var Client = require(__dirname + "/../../lib/native").Client; +var conString = helper.connectionString(); + +var setupClient = function() { + var client = new Client(conString); + client.connect(); + client.query("CREATE TEMP TABLE boom(name varchar(10), age integer)"); + client.query("INSERT INTO boom(name, age) VALUES('Aaron', 26)"); + client.query("INSERT INTO boom(name, age) VALUES('Brian', 28)"); + return client; +} + +test('connects', function() { + var client = new Client(conString); + client.connect(); + test('good query', function() { + var query = client.query("SELECT 1 as num, 'HELLO' as str"); + assert.emits(query, 'row', function(row) { + test('has integer data type', function() { + assert.strictEqual(row.num, 1); + }) + test('has string data type', function() { + assert.strictEqual(row.str, "HELLO") + }) + test('emits end AFTER row event', function() { + assert.emits(query, 'end'); + test('error query', function() { + var query = client.query("LSKDJF"); + assert.emits(query, 'error', function(err) { + assert.ok(err != null, "Should not have emitted null error"); + client.end(); + }) + }) + }) + }) + }) +}) + +test('multiple results', function() { + test('queued queries', function() { + var client = setupClient(); + var q = client.query("SELECT name FROM BOOM"); + assert.emits(q, 'row', function(row) { + assert.equal(row.name, 'Aaron'); + assert.emits(q, 'row', function(row) { + assert.equal(row.name, "Brian"); + }) + }) + assert.emits(q, 'end', function() { + test('query with config', function() { + var q = client.query({text:'SELECT 1 as num'}); + assert.emits(q, 'row', function(row) { + assert.strictEqual(row.num, 1); + assert.emits(q, 'end', function() { + client.end(); + }) + }) + }) + }) + }) +}) + +test('parameterized queries', function() { + test('with a single string param', function() { + var client = setupClient(); + var q = client.query("SELECT * FROM boom WHERE name = $1", ['Aaron']); + assert.emits(q, 'row', function(row) { + assert.equal(row.name, 'Aaron'); + }) + assert.emits(q, 'end', function() { + client.end(); + }); + }) + + test('with object config for query', function() { + var client = setupClient(); + var q = client.query({ + text: "SELECT name FROM boom WHERE name = $1", + values: ['Brian'] + }); + assert.emits(q, 'row', function(row) { + assert.equal(row.name, 'Brian'); + }) + assert.emits(q, 'end', function() { + client.end(); + }) + }) + + test('multiple parameters', function() { + var client = setupClient(); + var q = client.query('SELECT name FROM boom WHERE name = $1 or name = $2 ORDER BY name', ['Aaron', 'Brian']); + assert.emits(q, 'row', function(row) { + assert.equal(row.name, 'Aaron'); + assert.emits(q, 'row', function(row) { + assert.equal(row.name, 'Brian'); + assert.emits(q, 'end', function() { + client.end(); + }) + }) + }) + }) + + test('integer parameters', function() { + var client = setupClient(); + var q = client.query('SELECT * FROM boom WHERE age > $1', [27]); + assert.emits(q, 'row', function(row) { + assert.equal(row.name, 'Brian'); + assert.equal(row.age, 28); + }); + assert.emits(q, 'end', function() { + client.end(); + }) + }) +}) diff --git a/test/native/stress-tests.js b/test/native/stress-tests.js new file mode 100644 index 00000000..45dd1727 --- /dev/null +++ b/test/native/stress-tests.js @@ -0,0 +1,49 @@ +var helper = require(__dirname + "/../test-helper"); +var Client = require(__dirname + "/../../lib/native").Client; + +test('many rows', function() { + var client = new Client(helper.connectionString()); + client.connect(); + var q = client.query("SELECT * FROM person"); + var rows = []; + q.on('row', function(row) { + rows.push(row) + }); + assert.emits(q, 'end', function() { + client.end(); + assert.length(rows, 26); + }) +}); + +test('many queries', function() { + var client = new Client(helper.connectionString()); + client.connect(); + var count = 0; + var expected = 100; + for(var i = 0; i < expected; i++) { + var q = client.query("SELECT * FROM person"); + assert.emits(q, 'end', function() { + count++; + }) + } + assert.emits(client, 'drain', function() { + client.end(); + assert.equal(count, expected); + }) +}) + +test('many clients', function() { + var clients = []; + for(var i = 0; i < 10; i++) { + clients.push(new Client(helper.connectionString())); + } + clients.forEach(function(client) { + client.connect(); + for(var i = 0; i < 20; i++) { + client.query('SELECT * FROM person'); + } + assert.emits(client, 'drain', function() { + client.end(); + }) + }) +}) diff --git a/test/test-helper.js b/test/test-helper.js index 7f01fa12..830cbbcf 100644 --- a/test/test-helper.js +++ b/test/test-helper.js @@ -1,15 +1,16 @@ require.paths.unshift(__dirname + '/../lib/'); - -Client = require('client'); -EventEmitter = require('events').EventEmitter; - -sys = require('sys'); +//make assert a global... assert = require('assert'); -BufferList = require(__dirname+'/buffer-list') -buffers = require(__dirname + '/test-buffers'); -Connection = require('connection'); + +var EventEmitter = require('events').EventEmitter; +var sys = require('sys'); +var BufferList = require(__dirname+'/buffer-list') + +var Connection = require('connection'); var args = require(__dirname + '/cli'); +Client = require(__dirname + '/../lib').Client; + process.on('uncaughtException', function(d) { if ('stack' in d && 'message' in d) { console.log("Message: " + d.message); @@ -26,13 +27,13 @@ assert.same = function(actual, expected) { }; -assert.emits = function(item, eventName, callback) { +assert.emits = function(item, eventName, callback, message) { var called = false; var id = setTimeout(function() { test("Should have called " + eventName, function() { - assert.ok(called, "Expected '" + eventName + "' to be called.") + assert.ok(called, message || "Expected '" + eventName + "' to be called.") }); - },20000); + },2000); item.once(eventName, function() { called = true; @@ -95,6 +96,14 @@ assert.success = function(callback) { }) } +assert.throws = function(offender) { + try { + offender(); + } catch (e) { + return; + } + assert.ok(false, "Expected " + offender + " to throw exception"); +} assert.length = function(actual, expectedLength) { assert.equal(actual.length, expectedLength); @@ -205,7 +214,9 @@ module.exports = { pg: require('index'), connectionString: function() { return "pg"+(count++)+"://"+args.user+":"+args.password+"@"+args.host+":"+args.port+"/"+args.database; - } + }, + sys: sys, + Client: Client }; diff --git a/test/unit/client/configuration-tests.js b/test/unit/client/configuration-tests.js index 4a908114..cb60119b 100644 --- a/test/unit/client/configuration-tests.js +++ b/test/unit/client/configuration-tests.js @@ -4,8 +4,8 @@ test('client settings', function() { test('defaults', function() { var client = new Client(); - assert.equal(client.user, ''); - assert.equal(client.database, ''); + assert.equal(client.user, process.env.USER); + assert.equal(client.database, process.env.USER); assert.equal(client.port, 5432); }); @@ -41,12 +41,26 @@ test('initializing from a config string', function() { test('when not including all values the defaults are used', function() { var client = new Client("pg://host1") - assert.equal(client.user, "") - assert.equal(client.password, "") + assert.equal(client.user, process.env.USER) + assert.equal(client.password, null) assert.equal(client.host, "host1") assert.equal(client.port, 5432) - assert.equal(client.database, "") + assert.equal(client.database, process.env.USER) }) }) + +test('calls connect correctly on connection', function() { + var client = new Client("/tmp"); + var usedPort = ""; + var usedHost = ""; + client.connection.connect = function(port, host) { + usedPort = port; + usedHost = host; + }; + client.connect(); + assert.equal(usedPort, "/tmp/.s.PGSQL.5432"); + assert.strictEqual(usedHost, undefined) +}) + diff --git a/test/unit/client/notification-tests.js b/test/unit/client/notification-tests.js new file mode 100644 index 00000000..24d74604 --- /dev/null +++ b/test/unit/client/notification-tests.js @@ -0,0 +1,9 @@ +var helper = require(__dirname + "/test-helper"); +test('passes connection notification', function() { + var client = helper.client(); + assert.emits(client, 'notice', function(msg) { + assert.equal(msg, "HAY!!"); + }) + client.connection.emit('notice', "HAY!!"); +}) + diff --git a/test/unit/client/query-queue-tests.js b/test/unit/client/query-queue-tests.js index 8c231523..8323dac6 100644 --- a/test/unit/client/query-queue-tests.js +++ b/test/unit/client/query-queue-tests.js @@ -1,4 +1,5 @@ var helper = require(__dirname + '/test-helper'); +var Connection = require('connection'); var con = new Connection({stream: "NO"}); var client = new Client({connection:con}); diff --git a/test/unit/client/query-tests.js b/test/unit/client/query-tests.js index a024d189..19bbc21c 100644 --- a/test/unit/client/query-tests.js +++ b/test/unit/client/query-tests.js @@ -1,5 +1,6 @@ var helper = require(__dirname + '/test-helper'); -var q = require('query') +var q = {}; +q.dateParser = require(__dirname + "/../../../lib/types").getStringTypeParser(1114); test("testing dateParser", function() { assert.equal(q.dateParser("2010-12-11 09:09:04").toUTCString(),new Date("2010-12-11 09:09:04 GMT").toUTCString()); diff --git a/test/unit/client/test-helper.js b/test/unit/client/test-helper.js index f6ed43c6..b09bfe4e 100644 --- a/test/unit/client/test-helper.js +++ b/test/unit/client/test-helper.js @@ -1,5 +1,5 @@ require(__dirname+'/../test-helper'); - +var Connection = require('connection'); var makeClient = function() { var connection = new Connection({stream: "no"}); connection.startup = function() {}; diff --git a/test/unit/client/typed-query-results-tests.js b/test/unit/client/typed-query-results-tests.js index 5127d97a..00fbd963 100644 --- a/test/unit/client/typed-query-results-tests.js +++ b/test/unit/client/typed-query-results-tests.js @@ -114,6 +114,30 @@ test('typed results', function() { expected: function(val) { assert.UTCDate(val, 2010, 9, 31, 0, 0, 0, 0); } + },{ + name: 'interval time', + format: 'text', + dataTypeID: 1186, + actual: '01:02:03', + expected: function(val) { + assert.deepEqual(val, {'hours':1, 'minutes':2, 'seconds':3}) + } + },{ + name: 'interval long', + format: 'text', + dataTypeID: 1186, + actual: '1 year -32 days', + expected: function(val) { + assert.deepEqual(val, {'years':1, 'days':-32}) + } + },{ + name: 'interval combined negative', + format: 'text', + dataTypeID: 1186, + actual: '1 day -00:00:03', + expected: function(val) { + assert.deepEqual(val, {'days':1, 'seconds':-3}) + } }, diff --git a/test/unit/connection/error-tests.js b/test/unit/connection/error-tests.js index fb9f1b50..220320b4 100644 --- a/test/unit/connection/error-tests.js +++ b/test/unit/connection/error-tests.js @@ -1,5 +1,5 @@ var helper = require(__dirname + '/test-helper'); - +var Connection = require('connection'); var con = new Connection({stream: new MemoryStream()}); test("connection emits stream errors", function() { assert.emits(con, 'error', function(err) { diff --git a/test/unit/connection/inbound-parser-tests.js b/test/unit/connection/inbound-parser-tests.js index 440f8b46..970a7e21 100644 --- a/test/unit/connection/inbound-parser-tests.js +++ b/test/unit/connection/inbound-parser-tests.js @@ -1,5 +1,6 @@ require(__dirname+'/test-helper'); - +var Connection = require('connection'); +var buffers = require(__dirname + '/../../test-buffers'); var PARSE = function(buffer) { return new Parser(buffer).parse(); }; diff --git a/test/unit/connection/outbound-sending-tests.js b/test/unit/connection/outbound-sending-tests.js index 7451f297..6e38ccb9 100644 --- a/test/unit/connection/outbound-sending-tests.js +++ b/test/unit/connection/outbound-sending-tests.js @@ -1,10 +1,11 @@ require(__dirname + "/test-helper"); +var Connection = require('connection'); var stream = new MemoryStream(); var con = new Connection({ stream: stream }); -assert.recieved = function(stream, buffer) { +assert.received = function(stream, buffer) { assert.length(stream.packets, 1); var packet = stream.packets.pop(); assert.equalBuffers(packet, buffer); @@ -15,7 +16,7 @@ test("sends startup message", function() { user: 'brian', database: 'bang' }); - assert.recieved(stream, new BufferList() + assert.received(stream, new BufferList() .addInt16(3) .addInt16(0) .addCString('user') @@ -27,13 +28,13 @@ test("sends startup message", function() { test('sends password message', function() { con.password("!"); - assert.recieved(stream, new BufferList().addCString("!").join(true,'p')); + assert.received(stream, new BufferList().addCString("!").join(true,'p')); }); test('sends query message', function() { var txt = 'select * from boom'; con.query(txt); - assert.recieved(stream, new BufferList().addCString(txt).join(true,'Q')); + assert.received(stream, new BufferList().addCString(txt).join(true,'Q')); }); test('sends parse message', function() { @@ -42,7 +43,7 @@ test('sends parse message', function() { .addCString("") .addCString("!") .addInt16(0).join(true, 'P'); - assert.recieved(stream, expected); + assert.received(stream, expected); }); test('sends parse message with named query', function() { @@ -55,7 +56,7 @@ test('sends parse message with named query', function() { .addCString("boom") .addCString("select * from boom") .addInt16(0).join(true,'P'); - assert.recieved(stream, expected); + assert.received(stream, expected); test('with multiple parameters', function() { con.parse({ @@ -71,7 +72,7 @@ test('sends parse message with named query', function() { .addInt32(2) .addInt32(3) .addInt32(4).join(true,'P'); - assert.recieved(stream, expected); + assert.received(stream, expected); }); }); @@ -86,7 +87,7 @@ test('bind messages', function() { .addInt16(0) .addInt16(0) .join(true,"B"); - assert.recieved(stream, expectedBuffer); + assert.received(stream, expectedBuffer); }); test('with named statement, portal, and values', function() { @@ -109,7 +110,7 @@ test('bind messages', function() { .add(Buffer('zing')) .addInt16(0) .join(true, 'B'); - assert.recieved(stream, expectedBuffer); + assert.received(stream, expectedBuffer); }); }); @@ -122,7 +123,7 @@ test("sends execute message", function() { .addCString('') .addInt32(0) .join(true,'E'); - assert.recieved(stream, expectedBuffer); + assert.received(stream, expectedBuffer); }); test("for named portal with row limit", function() { @@ -134,38 +135,38 @@ test("sends execute message", function() { .addCString("my favorite portal") .addInt32(100) .join(true, 'E'); - assert.recieved(stream, expectedBuffer); + assert.received(stream, expectedBuffer); }); }); test('sends flush command', function() { con.flush(); var expected = new BufferList().join(true, 'H'); - assert.recieved(stream, expected); + assert.received(stream, expected); }); test('sends sync command', function() { con.sync(); var expected = new BufferList().join(true,'S'); - assert.recieved(stream, expected); + assert.received(stream, expected); }); test('sends end command', function() { con.end(); var expected = new Buffer([0x58, 0, 0, 0, 4]); - assert.recieved(stream, expected); + assert.received(stream, expected); }); test('sends describe command',function() { test('describe statement', function() { con.describe({type: 'S', name: 'bang'}); var expected = new BufferList().addChar('S').addCString('bang').join(true, 'D') - assert.recieved(stream, expected); + assert.received(stream, expected); }); test("describe unnamed portal", function() { con.describe({type: 'P'}); var expected = new BufferList().addChar('P').addCString("").join(true, "D"); - assert.recieved(stream, expected); + assert.received(stream, expected); }); }); diff --git a/test/unit/connection/startup-tests.js b/test/unit/connection/startup-tests.js index d1603418..abdfc590 100644 --- a/test/unit/connection/startup-tests.js +++ b/test/unit/connection/startup-tests.js @@ -1,5 +1,5 @@ require(__dirname+'/test-helper'); - +var Connection = require('connection'); test('connection can take existing stream', function() { var stream = new MemoryStream(); var con = new Connection({stream: stream}); diff --git a/test/unit/test-helper.js b/test/unit/test-helper.js index 1fa907ce..7999c540 100644 --- a/test/unit/test-helper.js +++ b/test/unit/test-helper.js @@ -1,10 +1,12 @@ -require(__dirname+'/../test-helper'); +var helper = require(__dirname+'/../test-helper'); +var EventEmitter = require('events').EventEmitter; +var Connection = require('connection'); MemoryStream = function() { EventEmitter.call(this); this.packets = []; }; -sys.inherits(MemoryStream, EventEmitter); +helper.sys.inherits(MemoryStream, EventEmitter); var p = MemoryStream.prototype; diff --git a/test/unit/utils-tests.js b/test/unit/utils-tests.js index 1fa733c9..da557b55 100644 --- a/test/unit/utils-tests.js +++ b/test/unit/utils-tests.js @@ -1,5 +1,7 @@ require(__dirname + '/test-helper'); -var Pool = require(__dirname + "/../../lib/utils").Pool; +var utils = require(__dirname + "/../../lib/utils"); +var Pool = utils.Pool; +var defaults = require(__dirname + "/../../lib").defaults; //this tests the monkey patching //to ensure comptability with older @@ -89,28 +91,152 @@ test('an empty pool', function() { }) }) -test('when creating async new pool members', function() { - var count = 0; - var pool = new Pool(3, function() { - var item = {ref: {name: ++count}, checkedIn: false}; - process.nextTick(function() { - pool.checkIn(item.ref) - }) - return item; +test('a pool with size of zero', function() { + var index = 0; + var pool = new Pool(0, function() { + return index++; }) - test('one request recieves member', function() { + test('checkin does nothing', function() { + index = 0; + pool.checkIn(301813); + assert.equal(pool.checkOut(assert.calls(function(err, item) { + assert.equal(item, 0); + }))); + }) + test('always creates a new item', function() { + index = 0; pool.checkOut(assert.calls(function(err, item) { - assert.equal(item.name, 1) - pool.checkOut(assert.calls(function(err, item) { - assert.equal(item.name, 2) - pool.checkOut(assert.calls(function(err, item) { - assert.equal(item.name, 3) - })) - })) + assert.equal(item, 0); + })) + pool.checkOut(assert.calls(function(err, item) { + assert.equal(item, 1); + })) + pool.checkOut(assert.calls(function(err, item) { + assert.equal(item, 2); + })) + }) +}) + +test('normalizing connection info', function() { + test('with objects', function() { + test('empty object uses defaults', function() { + var input = {}; + var output = utils.normalizeConnectionInfo(input); + assert.equal(output.user, defaults.user); + assert.equal(output.database, defaults.database); + assert.equal(output.port, defaults.port); + assert.equal(output.host, defaults.host); + assert.equal(output.password, defaults.password); + }); + + test('full object ignores defaults', function() { + var input = { + user: 'test1', + database: 'test2', + port: 'test3', + host: 'test4', + password: 'test5' + }; + assert.equal(utils.normalizeConnectionInfo(input), input); + }); + + test('connection string', function() { + test('non-unix socket', function() { + test('uses defaults', function() { + var input = ""; + var output = utils.normalizeConnectionInfo(input); + assert.equal(output.user, defaults.user); + assert.equal(output.database, defaults.database); + assert.equal(output.port, defaults.port); + assert.equal(output.host, defaults.host); + assert.equal(output.password, defaults.password); + }); + test('ignores defaults if string contains them all', function() { + var input = "tcp://user1:pass2@host3:3333/databaseName"; + var output = utils.normalizeConnectionInfo(input); + assert.equal(output.user, 'user1'); + assert.equal(output.database, 'databaseName'); + assert.equal(output.port, 3333); + assert.equal(output.host, 'host3'); + assert.equal(output.password, 'pass2'); + }) + }); + + test('unix socket', function() { + test('uses defaults', function() { + var input = "/var/run/postgresql"; + var output = utils.normalizeConnectionInfo(input); + assert.equal(output.user, process.env.USER); + assert.equal(output.host, '/var/run/postgresql'); + assert.equal(output.database, process.env.USER); + assert.equal(output.port, 5432); + }); + + test('uses overridden defaults', function() { + defaults.host = "/var/run/postgresql"; + defaults.user = "boom"; + defaults.password = "yeah"; + defaults.port = 1234; + var output = utils.normalizeConnectionInfo("asdf"); + assert.equal(output.user, "boom"); + assert.equal(output.password, "yeah"); + assert.equal(output.port, 1234); + assert.equal(output.host, "/var/run/postgresql"); + }) + }) + }) + }) +}) + +test('libpq connection string building', function() { + var checkForPart = function(array, part) { + assert.ok(array.indexOf(part) > -1, array.join(" ") + " did not contain " + part); + } + + test('builds simple string', function() { + var config = { + user: 'brian', + password: 'xyz', + port: 888, + host: 'localhost', + database: 'bam' + } + utils.buildLibpqConnectionString(config, assert.calls(function(err, constring) { + assert.isNull(err) + var parts = constring.split(" "); + checkForPart(parts, "user='brian'") + checkForPart(parts, "password='xyz'") + checkForPart(parts, "port='888'") + checkForPart(parts, "hostaddr=127.0.0.1") + checkForPart(parts, "dbname='bam'") + })) + }) + test('builds dns string', function() { + var config = { + user: 'brian', + password: 'asdf', + port: 5432, + host: 'example.com' + } + utils.buildLibpqConnectionString(config, assert.calls(function(err, constring) { + assert.isNull(err); + var parts = constring.split(" "); + checkForPart(parts, "user='brian'") + checkForPart(parts, "hostaddr=192.0.32.10") + })) + }) + + test('error when dns fails', function() { + var config = { + user: 'brian', + password: 'asf', + port: 5432, + host: 'asdlfkjasldfkksfd#!$!!!!..com' + } + utils.buildLibpqConnectionString(config, assert.calls(function(err, constring) { + assert.ok(err); + assert.isNull(constring) })) }) }) - - - diff --git a/test/unit/writer-tests.js b/test/unit/writer-tests.js index f2052bf4..be926302 100644 --- a/test/unit/writer-tests.js +++ b/test/unit/writer-tests.js @@ -156,7 +156,7 @@ test("resizing to much larger", function() { assert.equalBuffers(result, [33, 33, 33, 33, 33, 33, 33, 33, 0]) }) -test("header", function() { +test("flush", function() { test('added as a hex code to a full writer', function() { var subject = new Writer(2); var result = subject.addCString("!").flush(0x50) @@ -175,3 +175,15 @@ test("header", function() { assert.equalBuffers(result, [0x50, 0, 0, 0, 0x0D, 33, 33, 33, 33, 33, 33, 33, 33, 0]); }) }) + +test("header", function() { + test('adding two packets with headers', function() { + var subject = new Writer(10).addCString("!"); + subject.addHeader(0x50); + subject.addCString("!!"); + subject.addHeader(0x40); + subject.addCString("!"); + var result = subject.flush(0x10); + assert.equalBuffers(result, [0x50, 0, 0, 0, 6, 33, 0, 0x40, 0, 0, 0, 7, 33, 33, 0, 0x10, 0, 0, 0, 6, 33, 0 ]); + }) +}) diff --git a/wscript b/wscript new file mode 100644 index 00000000..2a340d5b --- /dev/null +++ b/wscript @@ -0,0 +1,32 @@ +import Options, Utils +from os import unlink, symlink, popen +from os.path import exists + +srcdir = '.' +blddir = 'build' +VERSION = '0.0.1' + +def set_options(opt): + opt.tool_options('compiler_cxx') + +def configure(conf): + conf.check_tool('compiler_cxx') + conf.check_tool('node_addon') + + pg_config = conf.find_program('pg_config', var='PG_CONFIG', mandatory=True) + pg_libdir = popen("%s --libdir" % pg_config).readline().strip() + conf.env.append_value("LIBPATH_PG", pg_libdir) + conf.env.append_value("LIB_PG", "pq") + pg_includedir = popen("%s --includedir" % pg_config).readline().strip() + conf.env.append_value("CPPPATH_PG", pg_includedir) + +def build(bld): + obj = bld.new_task_gen('cxx', 'shlib', 'node_addon') + obj.cxxflags = ["-g", "-D_LARGEFILE_SOURCE", "-Wall"] + obj.target = 'binding' + obj.source = "./src/binding.cc" + obj.uselib = "PG" + +def test(test): + Utils.exec_command("node test/native/connection-tests.js") + Utils.exec_command("node test/native/evented-api-tests.js")