Merge remote branch 'upstream/master'

Conflicts:
	lib/query.js
	test/unit/client/typed-query-results-tests.js
This commit is contained in:
Alexander Sulfrian 2011-06-21 21:37:48 +02:00
commit 207b7dbb2b
55 changed files with 2402 additions and 612 deletions

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
/.emacs-project
*.swp
*.log
.lock-wscript
build/
/todo.org

2
.npmignore Normal file
View File

@ -0,0 +1,2 @@
.lock-wscript
build/

View File

@ -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)

178
README.md
View File

@ -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)

View File

@ -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();
});
});

View File

@ -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");
})
})
});
});

View File

@ -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,
};

93
lib/client-pool.js Normal file
View File

@ -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
}
}
};

View File

@ -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() {

View File

@ -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

View File

@ -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

View File

@ -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');
})

186
lib/native.js Normal file
View File

@ -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')
};

View File

@ -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;

View File

@ -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;

123
lib/textParsers.js Normal file
View File

@ -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,
};

35
lib/types.js Normal file
View File

@ -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,
}

View File

@ -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
}

View File

@ -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);
}

View File

@ -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 <brian.m.carlson@gmail.com>",
"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" }
}

632
src/binding.cc Normal file
View File

@ -0,0 +1,632 @@
#include <libpq-fe.h>
#include <node.h>
#include <node_events.h>
#include <string.h>
#include <assert.h>
#include <stdlib.h>
#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<String> connect_symbol;
static Persistent<String> error_symbol;
static Persistent<String> ready_symbol;
static Persistent<String> row_symbol;
static Persistent<String> notice_symbol;
static Persistent<String> severity_symbol;
static Persistent<String> code_symbol;
static Persistent<String> message_symbol;
static Persistent<String> detail_symbol;
static Persistent<String> hint_symbol;
static Persistent<String> position_symbol;
static Persistent<String> internalPosition_symbol;
static Persistent<String> internalQuery_symbol;
static Persistent<String> where_symbol;
static Persistent<String> file_symbol;
static Persistent<String> line_symbol;
static Persistent<String> routine_symbol;
static Persistent<String> name_symbol;
static Persistent<String> value_symbol;
static Persistent<String> type_symbol;
static Persistent<String> channel_symbol;
static Persistent<String> payload_symbol;
class Connection : public EventEmitter {
public:
//creates the V8 objects & attaches them to the module (target)
static void
Init (Handle<Object> target)
{
HandleScope scope;
Local<FunctionTemplate> 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<Connection*>(w->data);
connection->HandleIOEvent(revents);
}
//v8 entry point into Connection#connect
static Handle<Value>
Connect(const Arguments& args)
{
HandleScope scope;
Connection *self = ObjectWrap::Unwrap<Connection>(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<Value>
SendQuery(const Arguments& args)
{
HandleScope scope;
Connection *self = ObjectWrap::Unwrap<Connection>(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<Value>
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<Value>
SendPrepare(const Arguments& args)
{
HandleScope scope;
Connection *self = ObjectWrap::Unwrap<Connection>(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<Value>
SendQueryPrepared(const Arguments& args)
{
HandleScope scope;
//dispatch prepared parameterized query
return DispatchParameterizedQuery(args, true);
}
static Handle<Value>
DispatchParameterizedQuery(const Arguments& args, bool isPrepared)
{
HandleScope scope;
Connection *self = ObjectWrap::Unwrap<Connection>(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<Value> params = args[1];
Local<Array> jsParams = Local<Array>::Cast(args[1]);
int len = jsParams->Length();
char** paramValues = ArgToCStringArray(jsParams);
if(!paramValues) {
THROW("Unable to allocate char **paramValues from Local<Array> 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<Value>
End(const Arguments& args)
{
HandleScope scope;
Connection *self = ObjectWrap::Unwrap<Connection>(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<Value>
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<Value> notice = String::New(message);
Emit(notice_symbol, 1, &notice);
}
//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<Object> result = Object::New();
result->Set(channel_symbol, String::New(notify->relname));
result->Set(payload_symbol, String::New(notify->extra));
Handle<Value> res = (Handle<Value>)result;
Emit((Handle<String>)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<Array> row = Array::New();
int fieldCount = PQnfields(result);
for(int fieldNumber = 0; fieldNumber < fieldCount; fieldNumber++) {
Local<Object> 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<Value> e = (Handle<Value>)row;
Emit(row_symbol, 1, &e);
}
}
void HandleErrorResult(const PGresult* result)
{
HandleScope scope;
Local<Object> 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<Value> m = msg;
Emit(error_symbol, 1, &m);
}
void AttachErrorField(const PGresult *result, const Local<Object> msg, const Persistent<String> 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<Value> 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<Array> params)
{
int len = params->Length();
char** paramValues = new char*[len];
for(int i = 0; i < len; i++) {
Handle<Value> 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<Value> 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<Object> target)
{
HandleScope scope;
Connection::Init(target);
}

View File

@ -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;
}

View File

@ -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())
}))
}))
})

File diff suppressed because one or more lines are too long

View File

@ -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'
})
})
})
})
}

View File

@ -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() {

View File

@ -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();
})

View File

@ -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));
});

View File

@ -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")
}
});
}));
}));
})

View File

@ -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;
}))
}))
}))
}))
})

View File

@ -16,5 +16,6 @@ module.exports = {
},
connectionString: helper.connectionString,
Sink: helper.Sink,
pg: helper.pg
pg: helper.pg,
args: helper.args
};

View File

@ -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();
})
}))

View File

@ -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();
}))

View File

@ -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() {

View File

@ -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) {
}

View File

@ -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();
}))
}));
})

View File

@ -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();
})
})
})

View File

@ -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();
})
})
})

View File

@ -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();
})
})
})

View File

@ -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();
})
})
})

View File

@ -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
};

View File

@ -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)
})

View File

@ -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!!");
})

View File

@ -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});

View File

@ -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());

View File

@ -1,5 +1,5 @@
require(__dirname+'/../test-helper');
var Connection = require('connection');
var makeClient = function() {
var connection = new Connection({stream: "no"});
connection.startup = function() {};

View File

@ -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})
}
},

View File

@ -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) {

View File

@ -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();
};

View File

@ -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);
});
});

View File

@ -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});

View File

@ -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;

View File

@ -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)
}))
})
})

View File

@ -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 ]);
})
})

32
wscript Normal file
View File

@ -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")