diff --git a/.gitignore b/.gitignore index a86636e7..5eb08587 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /.emacs-project +*.swp +*.log diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..e9c4cc0e --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +SHELL := /bin/bash + +user=postgres +password=1234 +host=localhost +port=5432 +database=postgres +verbose=false + +params := -u $(user) --password $(password) -p $(port) -d $(database) -h $(host) --verbose $(verbose) + +node-command := xargs -n 1 -I file node file $(params) + +.PHONY : test test-connection test-integration bench +test: test-unit + +test-all: test-unit test-integration + +bench: + @find benchmark -name "*-bench.js" | $(node-command) + +test-unit: + @find test/unit -name "*-tests.js" | $(node-command) + +test-connection: + @node script/test-connection.js $(params) + +test-integration: test-connection + @find test/integration -name "*-tests.js" | $(node-command) diff --git a/README.md b/README.md index ea911d76..c7ade355 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,15 @@ Non-blocking (async) pure JavaScript PostgreSQL client for node.js written with love and TDD. -## alpha version - -### Installation +## Installation npm install pg -### Example +## Example var pg = require('pg'); - - pg.connect("pg://user:password@host:port/database", function(err, client) { + var connectionString = "pg://user:password@host:port/database"; + pg.connect(connectionString, function(err, client) { if(err) { //handle connection error } @@ -40,14 +38,14 @@ with love and TDD. } } -### Philosophy +## Philosophy * well tested * no monkey patching * no dependencies (...besides PostgreSQL) * [in-depth documentation](http://github.com/brianc/node-postgres/wiki) (work in progress) -### features +## features - prepared statement support - parameters @@ -58,8 +56,9 @@ with love and TDD. - float <-> double, numeric - boolean <-> boolean - notification message support -- tested like a Toyota - ~1000 assertions executed on +- connection pooling +- mucho testing + ~250 tests executed on - ubuntu - node v0.2.2, v0.2.3, v0.2.4, v0.2.5, v0.3.0, v0.3.1 - postgres 8.4.4 @@ -67,21 +66,31 @@ with love and TDD. - node v0.2.2, v0.2.3, v0.2.4, v0.2.5, v0.3.0, v0.3.1 - postgres v8.4.4, v9.0.1 installed both locally and on networked Windows 7 -### Contributing +## Contributing clone the repo: git clone git://github.com/brianc/node-postgres cd node-postgres - node test/run.js + make test And just like magic, you're ready to contribute! <3 +### Contributors + +Many thanks to the following: + +* [creationix](https://github.com/creationix) +* [felixge](https://github.com/felixge) +* [pshc](https://github.com/pshc) +* [pjornblomqvist](https://github.com/bjornblomqvist) +* [JulianBirch](https://github.com/JulianBirch) + ## More info please -### Documentation +### [Documentation](node-postgres/wiki) -__PLEASE__ check out the [WIKI](node-postgres/wiki). __MUCH__ more information there. +### __PLEASE__ check out the WIKI ### Working? @@ -89,9 +98,7 @@ __PLEASE__ check out the [WIKI](node-postgres/wiki). __MUCH__ more information ### 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! +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). @@ -102,9 +109,7 @@ saw there. ### Plans for the future? - transparent prepared statement caching -- connection pooling - more testings of error scenarios -- streamline writing of buffers ## License diff --git a/benchmark/simple-query-bench.js b/benchmark/simple-query-bench.js new file mode 100644 index 00000000..46601589 --- /dev/null +++ b/benchmark/simple-query-bench.js @@ -0,0 +1,58 @@ +var pg = require(__dirname + '/../lib') +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: 'query compare', + repeat: 1000, + actions: [{ + name: 'simple query', + run: function(next) { + var query = client.query('SELECT name FROM person WHERE age > 10'); + query.on('end', function() { + next(); + }); + } + },{ + name: 'unnamed prepared statement', + run: function(next) { + var query = client.query('SELECT name FROM person WHERE age > $1', [10]); + query.on('end', function() { + next(); + }); + } + },{ + name: 'named prepared statement', + run: function(next) { + var config = { + name: 'get peeps', + text: 'SELECT name FROM person WHERE age > $1', + values: [10] + } + client.query(config).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(); + }) +} + + + +var client = new pg.Client(conString); +client.connect(); +client.connection.once('readyForQuery', doBenchmark) diff --git a/lib/client.js b/lib/client.js index 3093888f..5f18b638 100644 --- a/lib/client.js +++ b/lib/client.js @@ -12,7 +12,7 @@ var Connection = require(__dirname + '/connection'); var parseConnectionString = function(str) { var result = url.parse(str); result.host = result.hostname; - result.database = result.pathname.slice(1); + result.database = result.pathname ? result.pathname.slice(1) : null var auth = (result.auth || ':').split(':'); result.user = auth[0]; result.password = auth[1]; diff --git a/lib/connection.js b/lib/connection.js index 46be8636..b2adbd87 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -16,6 +16,7 @@ var Connection = function(config) { this.offset = null; this.encoding = 'utf8'; this.parsedStatements = {}; + this.writer = new Writer(); }; sys.inherits(Connection, EventEmitter); @@ -53,14 +54,14 @@ p.connect = function(port, host) { }; p.startup = function(config) { - var bodyBuffer = new Writer() + var bodyBuffer = this.writer .addInt16(3) .addInt16(0) .addCString('user') .addCString(config.user) .addCString('database') .addCString(config.database) - .addCString('').join(); + .addCString('').flush(); //this message is sent without a code var length = bodyBuffer.length + 4; @@ -74,7 +75,7 @@ p.startup = function(config) { p.password = function(password) { //0x70 = 'p' - this.send(0x70, new Writer().addCString(password).join()); + this.send(0x70, this.writer.addCString(password).flush()); }; p.send = function(code, bodyBuffer) { @@ -97,7 +98,7 @@ p.end = function() { p.query = function(text) { //0x51 = Q - this.send(0x51, new Writer().addCString(text).join()); + this.send(0x51, this.writer.addCString(text).flush()); }; p.parse = function(query) { @@ -111,7 +112,7 @@ p.parse = function(query) { //normalize null type array query.types = query.types || []; var len = query.types.length; - var buffer = new Writer() + var buffer = this.writer .addCString(query.name) //name of query .addCString(query.text) //actual query text .addInt16(len); @@ -120,7 +121,7 @@ p.parse = function(query) { } //0x50 = 'P' - this.send(0x50, buffer.join()); + this.send(0x50, buffer.flush()); return this; }; @@ -132,7 +133,7 @@ p.bind = function(config) { config.statement = config.statement || ''; var values = config.values || []; var len = values.length; - var buffer = new Writer() + var buffer = this.writer .addCString(config.portal) .addCString(config.statement) .addInt16(0) //always use default text format @@ -144,22 +145,22 @@ p.bind = function(config) { } else { val = val.toString(); buffer.addInt32(Buffer.byteLength(val)); - buffer.add(Buffer(val,this.encoding)); + buffer.addString(val); } } buffer.addInt16(0); //no format codes, use text //0x42 = 'B' - this.send(0x42, buffer.join()); + this.send(0x42, buffer.flush()); }; p.execute = function(config) { config = config || {}; config.portal = config.portal || ''; config.rows = config.rows || ''; - var buffer = new Writer() + var buffer = this.writer .addCString(config.portal) .addInt32(config.rows) - .join(); + .flush(); //0x45 = 'E' this.send(0x45, buffer); @@ -181,7 +182,7 @@ p.end = function() { }; p.describe = function(msg) { - this.send(0x44, new Writer().addCString(msg.type + (msg.name || '')).join()); + this.send(0x44, this.writer.addCString(msg.type + (msg.name || '')).flush()); }; //parsing methods diff --git a/lib/index.js b/lib/index.js index da3be82f..4fe24fe5 100644 --- a/lib/index.js +++ b/lib/index.js @@ -4,6 +4,36 @@ 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 = { } @@ -13,12 +43,13 @@ var poolEnabled = function() { } var log = function() { - + //do nothing } -var log = function() { - console.log.apply(console, arguments); -} +//for testing +// var log = function() { +// console.log.apply(console, arguments); +// } var getPooledClient = function(config, callback) { //lookup pool using config as key @@ -27,16 +58,17 @@ var getPooledClient = function(config, callback) { //create pool if doesn't exist if(!pool) { - log("creating pool %s", config) + //log("creating pool %s", config) pool = clientPools[config] = new Pool(defaults.poolSize, function() { - log("creating new client in pool %s", config) + //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) { @@ -72,56 +104,36 @@ var getPooledClient = function(config, callback) { } //destroys the world -var end = function(callback) { - for(var name in clientPools) { - 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]) +//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]) } -//wrap up common connection management boilerplate -var connect = function(config, callback) { - if(poolEnabled()) { - return getPooledClient(config, callback) - } - throw new Error("FUCK") - 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); -} module.exports = { Client: Client, diff --git a/lib/query.js b/lib/query.js index f0ef80b2..de64c416 100644 --- a/lib/query.js +++ b/lib/query.js @@ -1,6 +1,5 @@ var EventEmitter = require('events').EventEmitter; var sys = require('sys');var sys = require('sys'); -var Row = require(__dirname + '/row'); var Query = function(config) { this.text = config.text; @@ -44,7 +43,7 @@ p.submit = function(connection) { }; }; var handleDatarow = function(msg) { - var result = new Row(); + var result = {}; for(var i = 0; i < msg.fields.length; i++) { var rawValue = msg.fields[i]; result[names[i]] = rawValue === null ? null : converters[i](rawValue); @@ -75,8 +74,9 @@ p.submit = function(connection) { if(self.callback) { self.callback(err); connection.removeListener('commandComplete', onCommandComplete); + } else { + self.emit('error', err); } - self.emit('error', err); self.emit('end'); }; @@ -180,7 +180,7 @@ var dateParser = function(isoDate) { var seconds = /(\d{2})/.exec(end); seconds = (seconds ? seconds[1] : 0); seconds = parseInt(seconds,10); - var mili = /\.(\d{1,})/.exec(end+"000"); + var mili = /\.(\d{1,})/.exec(end+"000"); mili = mili ? mili[1].slice(0,3) : 0; var tZone = /([Z|+\-])(\d{2})?(\d{2})?/.exec(end); //minutes to adjust for timezone diff --git a/lib/row.js b/lib/row.js deleted file mode 100644 index 565c0ae1..00000000 --- a/lib/row.js +++ /dev/null @@ -1,8 +0,0 @@ -var sys = require('sys'); -var Row = function() { - -}; - -var p = Row.prototype; - -module.exports = Row; diff --git a/lib/writer.js b/lib/writer.js index cd728079..03e35524 100644 --- a/lib/writer.js +++ b/lib/writer.js @@ -1,60 +1,93 @@ -var Writer = function() { - this.buffers = []; +var Writer = function(size) { + this.size = size || 1024; + this.buffer = new Buffer(this.size); + this.offset = 0; }; var p = Writer.prototype; -p.add = function(buffer) { - this.buffers.push(buffer); +p._remaining = function() { + return this.buffer.length - this.offset; +} + +p._resize = function() { + var oldBuffer = this.buffer; + this.buffer = Buffer(oldBuffer.length + this.size); + oldBuffer.copy(this.buffer); +} + +//resizes internal buffer if not enough size left +p._ensure = function(size) { + if(this._remaining() < size) { + this._resize() + } +} + +p.addInt32 = function(num) { + this._ensure(4) + this.buffer[this.offset++] = (num >>> 24 & 0xFF) + this.buffer[this.offset++] = (num >>> 16 & 0xFF) + this.buffer[this.offset++] = (num >>> 8 & 0xFF) + this.buffer[this.offset++] = (num >>> 0 & 0xFF) return this; -}; +} -p.addInt16 = function(val, front) { - return this.add(Buffer([ - (val >>> 8), - (val >>> 0) - ])); -}; +p.addInt16 = function(num) { + this._ensure(2) + this.buffer[this.offset++] = (num >>> 8 & 0xFF) + this.buffer[this.offset++] = (num >>> 0 & 0xFF) + return this; +} -p.getByteLength = function(initial) { - return this.buffers.reduce(function(previous, current){ - return previous + current.length; - },initial || 0); -}; +p.addCString = function(string) { + var string = string || ""; + var len = Buffer.byteLength(string) + 1; + this._ensure(len); + this.buffer.write(string, this.offset); + this.offset += len; + this.buffer[this.offset] = 0; //add null terminator + return this; +} -p.addInt32 = function(val, first) { - return this.add(Buffer([ - (val >>> 24 & 0xFF), - (val >>> 16 & 0xFF), - (val >>> 8 & 0xFF), - (val >>> 0 & 0xFF) - ])); -}; - -p.addCString = function(val) { - var len = Buffer.byteLength(val); - var buffer = new Buffer(len+1); - buffer.write(val); - buffer[len] = 0; - return this.add(buffer); -}; - -p.addChar = function(char, first) { - return this.add(Buffer(char,'utf8'), first); -}; +p.addChar = function(char) { + this._ensure(1); + this.buffer.write(char, this.offset); + this.offset++; + return this; +} p.join = function() { - var result = Buffer(this.getByteLength()); - var index = 0; - var buffers = this.buffers; - var length = this.buffers.length; - for(var i = 0; i < length; i ++) { - var buffer = buffers[i]; - buffer.copy(result, index, 0); - index += buffer.length; - } + return this.buffer.slice(0, this.offset); +} + +p.addString = function(string) { + var string = string || ""; + var len = Buffer.byteLength(string); + this._ensure(len); + this.buffer.write(string, this.offset); + this.offset += len; + return this; +} + +p.getByteLength = function() { + return this.offset; +} + +p.add = function(otherBuffer) { + this._ensure(otherBuffer.length); + otherBuffer.copy(this.buffer, this.offset); + this.offset += otherBuffer.length; + return this; +} + +p.clear = function() { + this.offset=0; +} + +p.flush = function() { + var result = this.join(); + this.clear(); return result; -}; +} module.exports = Writer; - diff --git a/package.json b/package.json index c48f8d8f..bd5f2c7b 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { "name": "pg", - "version": "0.1.0", + "version": "0.2.2", "description": "Pure JavaScript PostgreSQL client", "homepage": "http://github.com/brianc/node-postgres", "repository" : { diff --git a/script/test-connection.js b/script/test-connection.js new file mode 100644 index 00000000..3099a4dc --- /dev/null +++ b/script/test-connection.js @@ -0,0 +1,23 @@ +var helper = require(__dirname + '/../test/test-helper'); +var connectionString = helper.connectionString(); +console.log(); +console.log("testing ability to connect to '%s'", connectionString); +var pg = require(__dirname + '/../lib'); +pg.connect(connectionString, function(err, client) { + if(err !== null) { + console.error("Recieved connection error when attempting to contact PostgreSQL:"); + console.error(err); + process.exit(255); + } + console.log("Checking for existance of required test table 'person'") + client.query("SELECT COUNT(name) FROM person", function(err, callback) { + if(err != null) { + console.error("Recieved error when executing query 'SELECT COUNT(name) FROM person'") + console.error("It is possible you have not yet run the table create script under script/create-test-tables") + console.error("Consult the postgres-node wiki under the 'Testing' section for more information") + console.error(err); + process.exit(255); + } + pg.end(); + }) +}) diff --git a/test/cli.js b/test/cli.js index 78e8c412..99490b16 100644 --- a/test/cli.js +++ b/test/cli.js @@ -17,6 +17,9 @@ for(var i = 0; i < args.length; i++) { case '--password': config.password = args[++i]; break; + case '--verbose': + config.verbose = (args[++i] == "true"); + break; case '-d': case '--database': config.database = args[++i]; @@ -45,5 +48,5 @@ var log = function(keys) { console.log(key + ": '" + config[key] + "'"); }); } -//log(['user','password','database','port','host']) + module.exports = config; diff --git a/test/integration/client/api-tests.js b/test/integration/client/api-tests.js index 4478b78b..bdffbee5 100644 --- a/test/integration/client/api-tests.js +++ b/test/integration/client/api-tests.js @@ -1,9 +1,20 @@ var helper = require(__dirname + '/../test-helper'); var pg = require(__dirname + '/../../../lib'); +var connectionString = helper.connectionString(__filename); + +var log = function() { + //console.log.apply(console, arguments); +} + +var sink = new helper.Sink(4, 10000, function() { + log("ending connection pool: %s", connectionString); + pg.end(connectionString); +}); test('api', function() { - pg.connect(helper.args, assert.calls(function(err, client) { - assert.equal(err, null, "Failed to connect"); + log("connecting to %s", connectionString) + pg.connect(connectionString, assert.calls(function(err, client) { + assert.equal(err, null, "Failed to connect: " + sys.inspect(err)); client.query('CREATE TEMP TABLE band(name varchar(100))'); @@ -13,23 +24,30 @@ test('api', function() { test('simple query execution',assert.calls( function() { - client.query("SELECT * FROM band WHERE name = 'the beach boys'", function(err, result) { + log("executing simple query") + client.query("SELECT * FROM band WHERE name = 'the beach boys'", assert.calls(function(err, result) { assert.length(result.rows, 1) assert.equal(result.rows.pop().name, 'the beach boys') - }); + log("simple query executed") + })); })) test('prepared statement execution',assert.calls( function() { + log("executing prepared statement 1") client.query('SELECT * FROM band WHERE name = $1', ['dead black hearts'],assert.calls( function(err, result) { + log("Prepared statement 1 finished") assert.length(result.rows, 1); assert.equal(result.rows.pop().name, 'dead black hearts'); })) + log("executing prepared statement two") client.query('SELECT * FROM band WHERE name LIKE $1 ORDER BY name', ['the %'], assert.calls(function(err, result) { + log("prepared statement two finished") assert.length(result.rows, 2); assert.equal(result.rows.pop().name, 'the flaming lips'); assert.equal(result.rows.pop().name, 'the beach boys'); + sink.add(); })) })) @@ -37,12 +55,16 @@ test('api', function() { }) test('executing nested queries', function() { - pg.connect(helper.args, assert.calls(function(err, client) { + pg.connect(connectionString, assert.calls(function(err, client) { + assert.isNull(err); + log("connected for nested queriese") client.query('select now as now from NOW()', assert.calls(function(err, result) { assert.equal(new Date().getYear(), result.rows[0].now.getYear()) client.query('select now as now_again FROM NOW()', assert.calls(function() { client.query('select * FROM NOW()', assert.calls(function() { + log('all nested queries recieved') assert.ok('all queries hit') + sink.add(); })) })) })) @@ -50,10 +72,23 @@ test('executing nested queries', function() { }) test('raises error if cannot connect', function() { - var connectionString = "pg://asdf@seoiasfd:4884/ieieie"; + var connectionString = "pg://sfalsdkf:asdf@localhost/ieieie"; + log("trying to connect to invalid place for error") pg.connect(connectionString, assert.calls(function(err, client) { assert.ok(err, 'should have raised an error') + log("invalid connection supplied error to callback") + sink.add(); })) }) -pg.end(); +test("query errors are handled and do not bubble if callback is provded", function() { + pg.connect(connectionString, assert.calls(function(err, client) { + assert.isNull(err) + log("checking for query error") + client.query("SELECT OISDJF FROM LEIWLISEJLSE", assert.calls(function(err, result) { + assert.ok(err); + log("query error supplied error to callback") + sink.add(); + })) + })) +}) diff --git a/test/integration/client/simple-query-tests.js b/test/integration/client/simple-query-tests.js index 99b07478..a0459094 100644 --- a/test/integration/client/simple-query-tests.js +++ b/test/integration/client/simple-query-tests.js @@ -4,7 +4,7 @@ test("simple query interface", function() { var client = helper.client(); - var query = client.query("select name from person"); + var query = client.query("select name from person order by name"); client.on('drain', client.end.bind(client)); @@ -12,6 +12,17 @@ test("simple query interface", function() { query.on('row', function(row) { rows.push(row['name']) }); + query.once('row', function(row) { + test('Can iterate through columns', function () { + var columnCount = 0; + for (column in row) { + columnCount++; + }; + if ('length' in row) { + assert.length(row, columnCount, 'Iterating through the columns gives a different length from calling .length.'); + } + }); + }); assert.emits(query, 'end', function() { test("returned right number of rows", function() { @@ -51,4 +62,3 @@ test("multiple select statements", function() { }); client.on('drain', client.end.bind(client)); }); - diff --git a/test/integration/client/test-helper.js b/test/integration/client/test-helper.js index 3984d63a..6cc8146d 100644 --- a/test/integration/client/test-helper.js +++ b/test/integration/client/test-helper.js @@ -10,7 +10,11 @@ module.exports = { host: helper.args.host, port: helper.args.port }); + client.connect(); return client; - } + }, + connectionString: helper.connectionString, + Sink: helper.Sink, + pg: helper.pg }; diff --git a/test/integration/client/transaction-tests.js b/test/integration/client/transaction-tests.js new file mode 100644 index 00000000..aaaaf5b1 --- /dev/null +++ b/test/integration/client/transaction-tests.js @@ -0,0 +1,48 @@ +var helper = require(__dirname + '/test-helper'); + +test('a single connection transaction', function() { + var connectionString = helper.connectionString(); + var sink = new helper.Sink(1, function() { + helper.pg.end(); + }); + + helper.pg.connect(connectionString, assert.calls(function(err, client) { + assert.isNull(err); + + client.query('begin'); + + var getZed = { + text: 'SELECT * FROM person WHERE name = $1', + values: ['Zed'] + }; + + test('Zed should not exist in the database', function() { + client.query(getZed, assert.calls(function(err, result) { + assert.isNull(err); + assert.empty(result.rows); + })) + }) + + client.query("INSERT INTO person(name, age) VALUES($1, $2)", ['Zed', 270], assert.calls(function(err, result) { + assert.isNull(err) + })); + + test('Zed should exist in the database', function() { + client.query(getZed, assert.calls(function(err, result) { + assert.isNull(err); + assert.equal(result.rows[0].name, 'Zed'); + })) + }) + + client.query('rollback'); + + test('Zed should not exist in the database', function() { + client.query(getZed, assert.calls(function(err, result) { + assert.isNull(err); + assert.empty(result.rows); + sink.add(); + })) + }) + + })) +}) diff --git a/test/integration/client/type-coercion-tests.js b/test/integration/client/type-coercion-tests.js index 09def7f6..53a2c9dc 100644 --- a/test/integration/client/type-coercion-tests.js +++ b/test/integration/client/type-coercion-tests.js @@ -1,37 +1,39 @@ var helper = require(__dirname + '/test-helper'); - -var client = helper.client(); -client.on('drain', client.end.bind(client)); - +var sink; +var connectionString = helper.connectionString(); var testForTypeCoercion = function(type){ - client.query("create temp table test_type(col " + type.name + ")"); + helper.pg.connect(connectionString, function(err, client) { + assert.isNull(err) + client.query("create temp table test_type(col " + type.name + ")"); - test("Coerces " + type.name, function() { - type.values.forEach(function(val) { + test("Coerces " + type.name, function() { + type.values.forEach(function(val) { - var insertQuery = client.query({ - name: 'insert type test ' + type.name, - text: 'insert into test_type(col) VALUES($1)', - values: [val] + 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' + }); + + 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(); }); - var query = client.query({ - name: 'get type ' + type.name , - text: 'select col from test_type' - }); - - 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' - }); + client.query('drop table test_type'); }); - - client.query('drop table test_type'); - }); + }) }; var types = [{ @@ -76,9 +78,18 @@ var types = [{ values: ['13:12:12.321', null] }]; +var valueCount = 0; +types.forEach(function(type) { + valueCount += type.values.length; +}) +sink = new helper.Sink(valueCount, function() { + helper.pg.end(); +}) + types.forEach(testForTypeCoercion); test("timestampz round trip", function() { + var now = new Date(); var client = helper.client(); client.on('error', function(err) { @@ -112,6 +123,7 @@ test("timestampz round trip", function() { }); }); + client.on('drain', client.end.bind(client)); }); diff --git a/test/integration/connection-pool/double-connection-tests.js b/test/integration/connection-pool/double-connection-tests.js index e69de29b..ae7eb316 100644 --- a/test/integration/connection-pool/double-connection-tests.js +++ b/test/integration/connection-pool/double-connection-tests.js @@ -0,0 +1,2 @@ +var helper = require(__dirname + "/test-helper") +helper.testPoolSize(2); diff --git a/test/integration/connection-pool/ending-pool-tests.js b/test/integration/connection-pool/ending-pool-tests.js new file mode 100644 index 00000000..c2690421 --- /dev/null +++ b/test/integration/connection-pool/ending-pool-tests.js @@ -0,0 +1,27 @@ +var helper = require(__dirname + '/test-helper') +var conString1 = helper.connectionString(); +var conString2 = helper.connectionString(); +var conString3 = helper.connectionString(); +var conString4 = helper.connectionString(); + +var called = false; +test('disconnects', function() { + var sink = new helper.Sink(4, function() { + called = true; + //this should exit the process, killing each connection pool + helper.pg.end(); + }); + [conString1, conString2, conString3, conString4].forEach(function() { + helper.pg.connect(conString1, function(err, client) { + assert.isNull(err); + client.query("SELECT * FROM NOW()", function(err, result) { + process.nextTick(function() { + assert.equal(called, false, "Should not have disconnected yet") + sink.add(); + }) + }) + }) + }) +}) + + diff --git a/test/integration/connection-pool/max-connection-tests.js b/test/integration/connection-pool/max-connection-tests.js index e69de29b..61755a0b 100644 --- a/test/integration/connection-pool/max-connection-tests.js +++ b/test/integration/connection-pool/max-connection-tests.js @@ -0,0 +1,3 @@ +var helper = require(__dirname + "/test-helper") +helper.testPoolSize(10); +helper.testPoolSize(11); diff --git a/test/integration/connection-pool/single-connection-tests.js b/test/integration/connection-pool/single-connection-tests.js index ad565e60..5ca0a888 100644 --- a/test/integration/connection-pool/single-connection-tests.js +++ b/test/integration/connection-pool/single-connection-tests.js @@ -1,32 +1,2 @@ var helper = require(__dirname + "/test-helper") - -setTimeout(function() { - helper.pg.defaults.poolSize = 10; - test('executes a single pooled connection/query', function() { - var args = helper.args; - var conString = "pg://"+args.user+":"+args.password+"@"+args.host+":"+args.port+"/"+args.database; - var queryCount = 0; - helper.pg.connect(conString, assert.calls(function(err, client) { - assert.isNull(err); - client.query("select * from NOW()", assert.calls(function(err, result) { - assert.isNull(err); - queryCount++; - })) - })) - var id = setTimeout(function() { - assert.equal(queryCount, 1) - }, 1000) - var check = function() { - setTimeout(function() { - if(queryCount == 1) { - clearTimeout(id) - helper.pg.end(); - } else { - check(); - } - }, 50) - } - check(); - }) -}, 1000) - +helper.testPoolSize(1); diff --git a/test/integration/connection-pool/test-helper.js b/test/integration/connection-pool/test-helper.js index 0e6a6ac8..41a45fc2 100644 --- a/test/integration/connection-pool/test-helper.js +++ b/test/integration/connection-pool/test-helper.js @@ -1,4 +1,40 @@ -module.exports = { - args: require(__dirname + "/../test-helper").args, - pg: require(__dirname + "/../../../lib") +var helper = require(__dirname + "/../test-helper"); +var pg = require(__dirname + "/../../../lib"); +helper.pg = pg; + +var testPoolSize = function(max) { + var conString = helper.connectionString(); + var sink = new helper.Sink(max, function() { + helper.pg.end(conString); + }); + + test("can pool " + max + " times", function() { + for(var i = 0; i < max; i++) { + helper.pg.poolSize = 10; + test("connection #" + i + " executes", function() { + helper.pg.connect(conString, function(err, client) { + assert.isNull(err); + client.query("select * from person", function(err, result) { + assert.length(result.rows, 26) + }) + client.query("select count(*) as c from person", function(err, result) { + assert.equal(result.rows[0].c, 26) + }) + var query = client.query("SELECT * FROM NOW()") + query.on('end',function() { + sink.add() + }) + }) + }) + } + }) } + +module.exports = { + args: helper.args, + pg: helper.pg, + connectionString: helper.connectionString, + Sink: helper.Sink, + testPoolSize: testPoolSize +} + diff --git a/test/integration/connection-pool/waiting-connection-tests.js b/test/integration/connection-pool/waiting-connection-tests.js index e69de29b..f2519ec5 100644 --- a/test/integration/connection-pool/waiting-connection-tests.js +++ b/test/integration/connection-pool/waiting-connection-tests.js @@ -0,0 +1,2 @@ +var helper = require(__dirname + "/test-helper") +helper.testPoolSize(200); diff --git a/test/integration/test-helper.js b/test/integration/test-helper.js index 89e1ab43..510a5855 100644 --- a/test/integration/test-helper.js +++ b/test/integration/test-helper.js @@ -1,3 +1,6 @@ var helper = require(__dirname + '/../test-helper'); //export parent helper stuffs -module.exports = { args: helper.args }; +module.exports = helper; + +if(helper.args.verbose) { +} diff --git a/test/run.js b/test/run.js deleted file mode 100755 index aa609f8e..00000000 --- a/test/run.js +++ /dev/null @@ -1,25 +0,0 @@ - -//executes all the unit tests -var fs = require('fs'); - -var args = require(__dirname + '/cli'); - -var runDir = function(dir) { - fs.readdirSync(dir).forEach(function(file) { - if(file.indexOf(".js") < 0) { - return runDir(fs.realpathSync(dir + file) + "/"); - } - require(dir + file.split('.js') [0]); - }); -}; - -var arg = args.test; - -if(arg == 'all') { - runDir(__dirname+'/unit/'); - runDir(__dirname+'/integration/'); -} -else { - runDir(__dirname+'/' + arg + '/'); -} - diff --git a/test/test-helper.js b/test/test-helper.js index 66f925b6..fbeaa4d2 100644 --- a/test/test-helper.js +++ b/test/test-helper.js @@ -1,4 +1,3 @@ - require.paths.unshift(__dirname + '/../lib/'); Client = require('client'); @@ -11,6 +10,15 @@ buffers = require(__dirname + '/test-buffers'); Connection = require('connection'); var args = require(__dirname + '/cli'); +process.on('uncaughtException', function(d) { + if ('stack' in d && 'message' in d) { + console.log("Message: " + d.message); + console.log(d.stack); + } else { + console.log(d); + } +}); + assert.same = function(actual, expected) { for(var key in expected) { assert.equal(actual[key], expected[key]); @@ -85,7 +93,7 @@ assert.length = function(actual, expectedLength) { var expect = function(callback, timeout) { var executed = false; var id = setTimeout(function() { - assert.ok(executed, "Expected execution of " + callback + " fired"); + assert.ok(executed, "Expected execution of funtion to be fired"); }, timeout || 2000) return function(err, queryResult) { @@ -101,46 +109,93 @@ assert.isNull = function(item, message) { assert.ok(item === null, message); }; -['equal', 'length', 'empty', 'strictEqual', 'emits', 'equalBuffers', 'same', 'calls', 'ok'].forEach(function(name) { - var old = assert[name]; - assert[name] = function() { - test.assertCount++ - return old.apply(this, arguments); - }; -}); - test = function(name, action) { test.testCount ++; + if(args.verbose) { + console.log(name); + } var result = action(); if(result === false) { test.ignored.push(name); - process.stdout.write('?'); + if(!args.verbose) { + process.stdout.write('?'); + } }else{ - process.stdout.write('.'); + if(!args.verbose) { + process.stdout.write('.'); + } } }; -test.assertCount = test.assertCount || 0; +//print out the filename +process.stdout.write(require('path').basename(process.argv[1])); +//print a new line since we'll be printing test names +if(args.verbose) { + console.log(); +} test.testCount = test.testCount || 0; test.ignored = test.ignored || []; test.errors = test.errors || []; -test.start = test.start || new Date(); process.on('exit', function() { console.log(''); - var duration = ((new Date() - test.start)/1000); - console.log(test.testCount + ' tests ' + test.assertCount + ' assertions in ' + duration + ' seconds'); - test.ignored.forEach(function(name) { - console.log("Ignored: " + name); - }); - test.errors.forEach(function(error) { - console.log("Error: " + error.name); - }); + if(test.ignored.length || test.errors.length) { + test.ignored.forEach(function(name) { + console.log("Ignored: " + name); + }); + test.errors.forEach(function(error) { + console.log("Error: " + error.name); + }); + console.log(''); + } test.errors.forEach(function(error) { throw error.e; }); }); +process.on('uncaughtException', function(err) { + console.error("\n %s", err.stack || err.toString()) + //causes xargs to abort right away + process.exit(255); +}); + +var count = 0; + +var Sink = function(expected, timeout, callback) { + var defaultTimeout = 1000; + if(typeof timeout == 'function') { + callback = timeout; + timeout = defaultTimeout; + } + timeout = timeout || defaultTimeout; + var internalCount = 0; + var kill = function() { + assert.ok(false, "Did not reach expected " + expected + " with an idle timeout of " + timeout); + } + var killTimeout = setTimeout(kill, timeout); + return { + add: function(count) { + count = count || 1; + internalCount += count; + clearTimeout(killTimeout) + if(internalCount < expected) { + killTimeout = setTimeout(kill, timeout) + } + else { + assert.equal(internalCount, expected); + callback(); + } + } + } +} + module.exports = { - args: args + args: args, + Sink: Sink, + pg: require('index'), + connectionString: function() { + return "pg"+(count++)+"://"+args.user+":"+args.password+"@"+args.host+":"+args.port+"/"+args.database; + } }; + + diff --git a/test/unit/client/configuration-tests.js b/test/unit/client/configuration-tests.js index 7c7a8263..4a908114 100644 --- a/test/unit/client/configuration-tests.js +++ b/test/unit/client/configuration-tests.js @@ -19,7 +19,7 @@ test('client settings', function() { port: 321, password: password }); - + assert.equal(client.user, user); assert.equal(client.database, database); assert.equal(client.port, 321); @@ -27,3 +27,26 @@ test('client settings', function() { }); }); + +test('initializing from a config string', function() { + + test('uses the correct values from the config string', function() { + var client = new Client("pg://brian:pass@host1:333/databasename") + assert.equal(client.user, 'brian') + assert.equal(client.password, "pass") + assert.equal(client.host, "host1") + assert.equal(client.port, 333) + assert.equal(client.database, "databasename") + }) + + 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.host, "host1") + assert.equal(client.port, 5432) + assert.equal(client.database, "") + }) + + +}) diff --git a/test/unit/row-tests.js b/test/unit/row-tests.js deleted file mode 100644 index a7f79af6..00000000 --- a/test/unit/row-tests.js +++ /dev/null @@ -1,4 +0,0 @@ -//mostly just testing simple row api -require(__dirname + "/test-helper"); -var Row = require('row'); - diff --git a/test/unit/utils-tests.js b/test/unit/utils-tests.js index 88350cca..1fa733c9 100644 --- a/test/unit/utils-tests.js +++ b/test/unit/utils-tests.js @@ -1,5 +1,5 @@ require(__dirname + '/test-helper'); -var Pool = require("utils").Pool; +var Pool = require(__dirname + "/../../lib/utils").Pool; //this tests the monkey patching //to ensure comptability with older diff --git a/test/unit/writer-tests.js b/test/unit/writer-tests.js index a469ecc6..32972de0 100644 --- a/test/unit/writer-tests.js +++ b/test/unit/writer-tests.js @@ -1,57 +1,153 @@ require(__dirname + "/test-helper"); +var Writer = require(__dirname + "/../../lib/writer"); + +test('adding int32', function() { + var testAddingInt32 = function(int, expectedBuffer) { + test('writes ' + int, function() { + var subject = new Writer(); + var result = subject.addInt32(int).join(); + assert.equalBuffers(result, expectedBuffer); + }) + } + + testAddingInt32(0, [0, 0, 0, 0]); + testAddingInt32(1, [0, 0, 0, 1]); + testAddingInt32(256, [0, 0, 1, 0]); + test('writes largest int32', function() { + //todo need to find largest int32 when I have internet access + return false; + }) + + test('writing multiple int32s', function() { + var subject = new Writer(); + var result = subject.addInt32(1).addInt32(10).addInt32(0).join(); + assert.equalBuffers(result, [0, 0, 0, 1, 0, 0, 0, 0x0a, 0, 0, 0, 0]); + }) + + test('having to resize the buffer', function() { + test('after resize correct result returned', function() { + var subject = new Writer(10); + subject.addInt32(1).addInt32(1).addInt32(1) + assert.equalBuffers(subject.join(), [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1]) + }) + }) +}) + +test('int16', function() { + test('writes 0', function() { + var subject = new Writer(); + var result = subject.addInt16(0).join(); + assert.equalBuffers(result, [0,0]); + }) + + test('writes 400', function() { + var subject = new Writer(); + var result = subject.addInt16(400).join(); + assert.equalBuffers(result, [1, 0x90]) + }) + + test('writes many', function() { + var subject = new Writer(); + var result = subject.addInt16(0).addInt16(1).addInt16(2).join(); + assert.equalBuffers(result, [0, 0, 0, 1, 0, 2]) + }) + + test('resizes if internal buffer fills up', function() { + var subject = new Writer(3); + var result = subject.addInt16(2).addInt16(3).join(); + assert.equalBuffers(result, [0, 2, 0, 3]) + }) + +}) + +test('cString', function() { + test('writes empty cstring', function() { + var subject = new Writer(); + var result = subject.addCString().join(); + assert.equalBuffers(result, [0]) + }) + + test('writes non-empty cstring', function() { + var subject = new Writer(); + var result = subject.addCString("!!!").join(); + assert.equalBuffers(result, [33, 33, 33, 0]); + }) + + test('resizes if reached end', function() { + var subject = new Writer(3); + var result = subject.addCString("!!!").join(); + assert.equalBuffers(result, [33, 33, 33, 0]); + }) + + test('writes multiple cstrings', function() { + var subject = new Writer(); + var result = subject.addCString("!").addCString("!").join(); + assert.equalBuffers(result, [33, 0, 33, 0]); + }) + +}) + +test('writes char', function() { + var subject = new Writer(2); + var result = subject.addChar('a').addChar('b').addChar('c').join(); + assert.equalBuffers(result, [0x61, 0x62, 0x63]) +}) + +test('gets correct byte length', function() { + var subject = new Writer(5); + assert.equal(subject.getByteLength(), 0) + subject.addInt32(0) + assert.equal(subject.getByteLength(), 4) + subject.addCString("!") + assert.equal(subject.getByteLength(), 6) +}) + +test('can add arbitrary buffer to the end', function() { + var subject = new Writer(4); + subject.addCString("!!!") + var result = subject.add(Buffer("@@@")).join(); + assert.equalBuffers(result, [33, 33, 33, 0, 0x40, 0x40, 0x40]); +}) + +test('can write normal string', function() { + var subject = new Writer(4); + var result = subject.addString("!").join(); + assert.equalBuffers(result, [33]); + test('can write cString too', function() { + var result = subject.addCString("!").join(); + assert.equalBuffers(result, [33, 33, 0]); + test('can resize', function() { + var result = subject.addString("!!").join(); + assert.equalBuffers(result, [33, 33, 0, 33, 33]); + }) + + }) + +}) -BufferList.prototype.compare = function(expected) { - var buf = this.join(); - assert.equalBuffers(buf, expected); -}; +test('clearing', function() { + var subject = new Writer(); + subject.addCString("@!!#!#"); + subject.addInt32(10401); + subject.clear(); + assert.equalBuffers(subject.join(), []); + test('can keep writing', function() { + var joinedResult = subject.addCString("!").addInt32(9).addInt16(2).join(); + assert.equalBuffers(joinedResult, [33, 0, 0, 0, 0, 9, 0, 2]); + test('flush', function() { + var flushedResult = subject.flush(); + test('returns result', function() { + assert.equalBuffers(flushedResult, [33, 0, 0, 0, 0, 9, 0, 2]) + }) + test('clears the writer', function() { + assert.equalBuffers(subject.join(), []) + assert.equalBuffers(subject.flush(), []) + }) + }) + }) -test('adds int16', function() { - new BufferList().addInt16(5).compare([0, 5]); -}); +}) -test('adds two int16s', function() { - new BufferList().addInt16(5).addInt16(3).compare([0,5,0,3]); -}); -test('adds int32', function() { - new BufferList().addInt32(1).compare([0,0,0,1]); - new BufferList().addInt32(1).addInt32(3).compare([0,0,0,1,0,0,0,3]); -}); -test('adds CStrings', function() { - new BufferList().addCString('').compare([0]); - new BufferList().addCString('!!').compare([33,33,0]); - new BufferList().addCString('!').addCString('!').compare([33,0,33,0]); -}); - -test('computes length', function() { - var buf = new BufferList().join(true); - assert.equalBuffers(buf, [0,0,0,4]); -}); - -test('appends character', function() { - var buf = new BufferList().join(false,'!'); - assert.equalBuffers(buf,[33]); -}); - -test('appends char and length', function() { - var buf = new BufferList().join(true,'!'); - assert.equalBuffers(buf,[33,0,0,0,4]); -}); - -test('does complicated buffer', function() { - var buf = new BufferList() - .addInt32(1) - .addInt16(2) - .addCString('!') - .join(true,'!'); - assert.equalBuffers(buf, [33, 0, 0, 0, 0x0c, 0, 0, 0, 1, 0, 2, 33, 0]); -}); - -test('concats', function() { - var buf1 = new BufferList().addInt32(8).join(false,'!'); - var buf2 = new BufferList().addInt16(1).join(); - var result = BufferList.concat(buf1, buf2); - assert.equalBuffers(result, [33, 0, 0, 0, 8, 0, 1]); -});