diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f67e405 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +.PHONY: publish-patch test + +test: + npm test + +publish-patch: test + npm version patch -m "Bump version" + git push origin master --tags + npm publish diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/bench/index.js b/bench/index.js new file mode 100644 index 0000000..297ebef --- /dev/null +++ b/bench/index.js @@ -0,0 +1,54 @@ +var pg = require('pg').native +var Native = require('../') + +var warmup = function(fn, cb) { + var count = 0; + var max = 10; + var run = function(err) { + if(err) return cb(err); + + if(max >= count++) { + return fn(run) + } + + cb() + } + run(); +} + +var native = Native(); +native.connectSync(); + +var queryText = 'SELECT generate_series(0, 1000)'; +var client = new pg.Client(); +client.connect(function() { + var pure = function(cb) { + client.query(queryText, function(err) { + if(err) throw err; + cb(err); + }); + } + var nativeQuery = function(cb) { + native.query(queryText, function(err) { + if(err) throw err; + cb(err); + }); + } + + var run = function() { + var start = Date.now() + warmup(pure, function() { + console.log('pure done', Date.now() - start) + start = Date.now() + warmup(nativeQuery, function() { + console.log('native done', Date.now() - start) + }) + }) + } + + setInterval(function() { + run() + }, 500) + +}); + diff --git a/index.js b/index.js new file mode 100644 index 0000000..273a9cc --- /dev/null +++ b/index.js @@ -0,0 +1,124 @@ +var Libpq = require('libpq'); + +var Client = module.exports = function(types) { + if(!types) { + var pgTypes = require('pg-types') + types = pgTypes.getTypeParser.bind(pgTypes) + } + if(!(this instanceof Client)) { + return new Client(types); + } + this.pq = new Libpq(); + this.types = types; +}; + +Client.prototype.connect = function(params, cb) { + this.pq.connect(params, cb); +}; + +Client.prototype.connectSync = function(params) { + this.pq.connectSync(params); +}; + +Client.prototype.end = function(cb) { + this.pq.finish(); + if(cb) setImmediate(cb); +}; + +var mapResults = function(pq, types) { + var rows = []; + var rowCount = pq.ntuples(); + var colCount = pq.nfields(); + for(var i = 0; i < rowCount; i++) { + var row = {}; + rows.push(row); + for(var j = 0; j < colCount; j++) { + var rawValue = pq.getvalue(i, j); + var value = rawValue; + if(rawValue == '') { + if(pq.getisnull()) { + value = null; + } + } else { + value = types(pq.ftype(j))(rawValue); + } + row[pq.fname(j)] = value; + } + } + return rows; +}; + +var dispatchQuery = function(pq, fn, cb) { + setImmediate(function() { + var sent = fn(); + if(!sent) return cb(new Error(pq.errorMessage())); + cb(); + }); +}; + +var consumeResults = function(pq, cb) { + var cleanup = function() { + pq.removeListener('readable', onReadable); + pq.stopReader(); + } + + var readError = function(message) { + cleanup(); + return cb(new Error(message || pq.errorMessage)); + }; + + var onReadable = function() { + //read waiting data from the socket + //e.g. clear the pending 'select' + if(!pq.consumeInput()) { + return readError(); + } + //check if there is still outstanding data + //if so, wait for it all to come in + if(pq.isBusy()) { + return; + } + //load our result object + pq.getResult(); + + //"read until results return null" + //or in our case ensure we only have one result + if(pq.getResult()) { + return readError('Only one result at a time is accepted'); + } + cleanup(); + return cb(null); + }; + pq.on('readable', onReadable); + pq.startReader(); +}; + +Client.prototype.query = function(text, values, cb) { + var queryFn; + var pq = this.pq + var types = this.types + if(typeof values == 'function') { + cb = values; + queryFn = pq.sendQuery.bind(pq, text); + } else { + queryFn = pq.sendQueryParams.bind(pq, text, values); + } + + dispatchQuery(pq, queryFn, function(err) { + if(err) return cb(err); + consumeResults(pq, function(err) { + return cb(err, err ? null : mapResults(pq, types)); + }); + }); +}; + +Client.prototype.querySync = function(text, values) { + var queryFn; + var pq = this.pq; + pq[values ? 'execParams' : 'exec'].call(pq, text, values); + var success = !pq.errorMessage(); + if(!success) { + throw new Error(pq.resultErrorMessage()); + } + return mapResults(pq, this.types); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..33de6bf --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "pg-native", + "version": "0.0.0", + "description": "A slightly nicer interface to Postgres over node-libpq", + "main": "index.js", + "scripts": { + "test": "mocha" + }, + "repository": { + "type": "git", + "url": "git://github.com/brianc/node-pg-native.git" + }, + "keywords": [ + "postgres", + "pg", + "libpq" + ], + "author": "Brian M. Carlson", + "license": "MIT", + "bugs": { + "url": "https://github.com/brianc/node-pg-native/issues" + }, + "homepage": "https://github.com/brianc/node-pg-native", + "dependencies": { + "libpq": "^0.2.0", + "pg-types": "^1.4.0" + }, + "devDependencies": { + "mocha": "^1.21.4", + "async": "^0.9.0" + } +} diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..de4c76c --- /dev/null +++ b/test/index.js @@ -0,0 +1,93 @@ +var Client = require('../') +var assert = require('assert') +var async = require('async') + +describe('connection', function() { + it('works', function(done) { + Client().connect(done); + }); + + it('connects with args', function(done) { + Client().connect('host=localhost', done); + }); + + it('errors out with bad connection args', function(done) { + Client().connect('host=asldkfjasdf', function(err) { + assert(err, 'should raise an error for bad host'); + done(); + }); + }); +}); + +describe('connectSync', function() { + it('works without args', function() { + Client().connectSync(); + }); + + it('works with args', function() { + Client().connectSync('host=localhost'); + }); + + it('throws if bad host', function() { + assert.throws(function() { + Client().connectSync('host=laksdjfdsf'); + }); + }); +}); + +describe('async query', function() { + before(function(done) { + this.client = Client(); + this.client.connect(function(err) { + if(err) return done(err); + return done(); + }); + }); + + after(function(done) { + this.client.end(done); + }); + + it('simple query works', function(done) { + var runQuery = function(n, done) { + this.client.query('SELECT NOW() AS the_time', function(err, rows) { + if(err) return done(err); + assert.equal(rows[0].the_time.getFullYear(), new Date().getFullYear()); + return done(); + }); + }.bind(this); + async.timesSeries(3, runQuery, done) + }); + + it('parameters work', function(done) { + var runQuery = function(n, done) { + this.client.query('SELECT $1::text AS name', ['Brian'], done); + }.bind(this); + async.timesSeries(3, runQuery, done) + }); + + it('prepared, named statements work'); +}); + +describe('query sync', function(done) { + before(function() { + this.client = Client(); + this.client.connectSync(); + }); + + after(function(done) { + this.client.end(done); + }); + + it('simple query works', function() { + var rows = this.client.querySync('SELECT NOW() AS the_time'); + assert.equal(rows.length, 1); + assert.equal(rows[0].the_time.getFullYear(), new Date().getFullYear()); + }); + + it('parameterized query works', function() { + var rows = this.client.querySync('SELECT $1::text AS name', ['Brian']); + assert.equal(rows.length, 1); + assert.equal(rows[0].name, 'Brian'); + }); +}); diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..25fe946 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,2 @@ +--bail +--no-exit