mirror of
https://github.com/brianc/node-postgres.git
synced 2025-12-08 20:16:25 +00:00
Merge remote branch 'upstream/master'
Conflicts: lib/query.js test/unit/client/typed-query-results-tests.js
This commit is contained in:
commit
207b7dbb2b
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,3 +1,6 @@
|
||||
/.emacs-project
|
||||
*.swp
|
||||
*.log
|
||||
.lock-wscript
|
||||
build/
|
||||
/todo.org
|
||||
|
||||
2
.npmignore
Normal file
2
.npmignore
Normal file
@ -0,0 +1,2 @@
|
||||
.lock-wscript
|
||||
build/
|
||||
13
Makefile
13
Makefile
@ -11,19 +11,28 @@ params := -u $(user) --password $(password) -p $(port) -d $(database) -h $(host)
|
||||
|
||||
node-command := xargs -n 1 -I file node file $(params)
|
||||
|
||||
.PHONY : test test-connection test-integration bench
|
||||
.PHONY : test test-connection test-integration bench test-native build/default/binding.node
|
||||
test: test-unit
|
||||
|
||||
test-all: test-unit test-integration
|
||||
test-all: test-unit test-integration test-native
|
||||
|
||||
bench:
|
||||
@find benchmark -name "*-bench.js" | $(node-command)
|
||||
|
||||
build/default/binding.node:
|
||||
@node-waf configure build
|
||||
|
||||
test-unit:
|
||||
@find test/unit -name "*-tests.js" | $(node-command)
|
||||
|
||||
test-connection:
|
||||
@node script/test-connection.js $(params)
|
||||
|
||||
test-native: build/default/binding.node
|
||||
@echo "***Testing native bindings***"
|
||||
@find test/native -name "*-tests.js" | $(node-command)
|
||||
@find test/integration -name "*-tests.js" | $(node-command) --native true
|
||||
|
||||
test-integration: test-connection
|
||||
@echo "***Testing Pure Javascript***"
|
||||
@find test/integration -name "*-tests.js" | $(node-command)
|
||||
|
||||
178
README.md
178
README.md
@ -1,80 +1,102 @@
|
||||
#node-postgres
|
||||
|
||||
Non-blocking (async) pure JavaScript PostgreSQL client for node.js written
|
||||
with love and TDD.
|
||||
Non-blocking PostgreSQL client for node.js. Pure JavaScript and native libpq bindings.
|
||||
|
||||
## Installation
|
||||
|
||||
npm install pg
|
||||
|
||||
## Example
|
||||
## Examples
|
||||
|
||||
var pg = require('pg');
|
||||
var connectionString = "pg://user:password@host:port/database";
|
||||
pg.connect(connectionString, function(err, client) {
|
||||
if(err) {
|
||||
//handle connection error
|
||||
}
|
||||
else {
|
||||
//queries are queued and executed in order
|
||||
client.query("CREATE TEMP TABLE user(name varchar(50), birthday timestamptz)");
|
||||
client.query("INSERT INTO user(name, birthday) VALUES('brianc', '1982-01-01T10:21:11')");
|
||||
|
||||
//parameterized queries with transparent type coercion
|
||||
client.query("INSERT INTO user(name, birthday) VALUES($1, $2)", ['santa', new Date()]);
|
||||
|
||||
//nested queries with callbacks
|
||||
client.query("SELECT * FROM user ORDER BY name", function(err, result) {
|
||||
if(err) {
|
||||
//handle query error
|
||||
}
|
||||
else {
|
||||
client.query("SELECT birthday FROM user WHERE name = $1", [result.rows[0].name], function(err, result) {
|
||||
//typed parameters and results
|
||||
assert.ok(result.rows[0].birthday.getYear() === 1982)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
All examples will work with the pure javascript bindings (currently default) or the libpq native (c/c++) bindings (currently in beta)
|
||||
|
||||
## Philosophy
|
||||
To use native libpq bindings replace `require('pg')` with `require('pg').native`.
|
||||
|
||||
* well tested
|
||||
* no monkey patching
|
||||
* no dependencies (...besides PostgreSQL)
|
||||
* [in-depth documentation](http://github.com/brianc/node-postgres/wiki) (work in progress)
|
||||
The two share the same interface so __no other code changes should be required__. If you find yourself having to change code other than the require statement when switching from `pg` to `pg.native`, please report an issue.
|
||||
|
||||
## features
|
||||
node-postgres supports both an 'event emitter' style API and a 'callback' style. The callback style is more concise and generally preferred, but the evented API can come in handy. They can be mixed and matched. The only events which do __not__ fire when callbacks are supplied are the `error` events, as they are to be handled by the callback function.
|
||||
|
||||
- prepared statement support
|
||||
- parameters
|
||||
- query caching
|
||||
- type coercion
|
||||
- date <-> timestamptz
|
||||
- integer <-> integer, smallint, bigint
|
||||
- float <-> double, numeric
|
||||
- boolean <-> boolean
|
||||
- notification message support
|
||||
- connection pooling
|
||||
- mucho testing
|
||||
~250 tests executed on
|
||||
- ubuntu
|
||||
- node v0.2.2, v0.2.3, v0.2.4, v0.2.5, v0.2.6, v0.3.0, v0.3.1, v0.3.2, v0.3.3, v0.3.4, v0.3.5, v0.3.6, v0.3.7, v0.3.8
|
||||
- postgres 8.4.4
|
||||
- osx
|
||||
- node v0.2.2, v0.2.3, v0.2.4, v0.2.5, v0.2.6, v0.3.0, v0.3.1, v0.3.2, v0.3.3, v0.3.4, v0.3.5, v0.3.6, v0.3.7, v0.3.8
|
||||
- postgres v8.4.4, v9.0.1 installed both locally and on networked Windows 7
|
||||
### Simple, using built-in client pool
|
||||
|
||||
## Contributing
|
||||
var pg = require('pg');
|
||||
//or native libpq bindings
|
||||
//var pg = require('pg').native
|
||||
|
||||
clone the repo:
|
||||
var conString = "tcp://postgres:1234@localhost/postgres";
|
||||
|
||||
git clone git://github.com/brianc/node-postgres
|
||||
cd node-postgres
|
||||
make test
|
||||
//error handling omitted
|
||||
pg.connect(conString, function(err, client) {
|
||||
client.query("SELECT NOW() as when", function(err, result) {
|
||||
console.log("Row count: %d",result.rows.length); // 1
|
||||
console.log("Current year: %d", result.rows[0].when.getYear());
|
||||
});
|
||||
});
|
||||
|
||||
And just like magic, you're ready to contribute! <3
|
||||
### Evented api
|
||||
|
||||
var pg = require('pg'); //native libpq bindings = `var pg = require('pg').native`
|
||||
var conString = "tcp://postgres:1234@localhost/postgres";
|
||||
|
||||
var client = new pg.Client(conString);
|
||||
client.connect();
|
||||
|
||||
//queries are queued and executed one after another once the connection becomes available
|
||||
client.query("CREATE TEMP TABLE beatles(name varchar(10), height integer, birthday timestamptz)");
|
||||
client.query("INSERT INTO beatles(name, height, birthday) values($1, $2, $3)", ['Ringo', 67, new Date(1945, 11, 2)]);
|
||||
client.query("INSERT INTO beatles(name, height, birthday) values($1, $2, $3)", ['John', 68, new Date(1944, 10, 13)]);
|
||||
|
||||
//queries can be executed either via text/parameter values passed as individual arguments
|
||||
//or by passing an options object containing text, (optional) parameter values, and (optional) query name
|
||||
client.query({
|
||||
name: 'insert beatle',
|
||||
text: "INSERT INTO beatles(name, height, birthday) values($1, $2, $3)",
|
||||
values: ['George', 70, new Date(1946, 02, 14)]
|
||||
});
|
||||
|
||||
//subsequent queries with the same name will be executed without re-parsing the query plan by postgres
|
||||
client.query({
|
||||
name: 'insert beatle',
|
||||
values: ['Paul', 63, new Date(1945, 04, 03)]
|
||||
});
|
||||
var query = client.query("SELECT * FROM beatles WHERE name = $1", ['John']);
|
||||
|
||||
//can stream row results back 1 at a time
|
||||
query.on('row', function(row) {
|
||||
console.log(row);
|
||||
console.log("Beatle name: %s", row.name); //Beatle name: John
|
||||
console.log("Beatle birth year: %d", row.birthday.getYear()); //dates are returned as javascript dates
|
||||
console.log("Beatle height: %d' %d\"", Math.floor(row.height/12), row.height%12); //integers are returned as javascript ints
|
||||
});
|
||||
|
||||
//fired after last row is emitted
|
||||
query.on('end', function() {
|
||||
client.end();
|
||||
});
|
||||
|
||||
### Info
|
||||
|
||||
* a pure javascript client and native libpq bindings with _the same api_
|
||||
* _heavily_ tested
|
||||
* the same suite of 200+ integration tests passed by both javascript & libpq bindings
|
||||
* benchmark & long-running memory leak tests performed before releases
|
||||
* tested with with
|
||||
* postgres 8.x, 9.x
|
||||
* Linux, OS X
|
||||
* node 2.x & 4.x
|
||||
* row-by-row result streaming
|
||||
* optional, built-in connection pooling
|
||||
* responsive project maintainer
|
||||
* supported PostgreSQL features
|
||||
* parameterized queries
|
||||
* named statements with query plan caching
|
||||
* async notifications
|
||||
* extensible js<->postgresql data-type coercion
|
||||
* query queue
|
||||
* active development
|
||||
* fast
|
||||
* No dependencies (other than PostgreSQL)
|
||||
* No monkey patching
|
||||
* Tried to mirror the node-mysql api as much as possible for future multi-database-supported ORM implementation ease
|
||||
|
||||
### Contributors
|
||||
|
||||
@ -85,36 +107,26 @@ Many thanks to the following:
|
||||
* [pshc](https://github.com/pshc)
|
||||
* [pjornblomqvist](https://github.com/bjornblomqvist)
|
||||
* [JulianBirch](https://github.com/JulianBirch)
|
||||
* [ef4](https://github.com/ef4)
|
||||
* [napa3um](https://github.com/napa3um)
|
||||
|
||||
## More info please
|
||||
## Documentation
|
||||
|
||||
### [Documentation](node-postgres/wiki)
|
||||
Still a work in progress, I am trying to flesh out the wiki...
|
||||
|
||||
### [Documentation](https://github.com/brianc/node-postgres/wiki)
|
||||
|
||||
### __PLEASE__ check out the WIKI
|
||||
|
||||
## Production Use
|
||||
* [bayt.com](http://bayt.com)
|
||||
|
||||
_if you use node-postgres in production and would like your site listed here, fork & add it_
|
||||
|
||||
## Help
|
||||
|
||||
If you need help or run into _any_ issues getting node-postgres to work on your system please report a bug or contact me directly.
|
||||
If you need help or run into _any_ issues getting node-postgres to work on your system please report a bug or contact me directly. I am usually available via google-talk at my github account public email address.
|
||||
|
||||
### Working?
|
||||
|
||||
[this page](http://www.explodemy.com) is running the worlds worst (but fully functional) PostgreSQL backed, Node.js powered website.
|
||||
|
||||
### Why did you write this?
|
||||
|
||||
As soon as I saw node.js for the first time I knew I had found something lovely and simple and _just what I always wanted!_. So...I poked around for a while. I was excited. I still am!
|
||||
|
||||
I drew major inspiration from [postgres-js](http://github.com/creationix/postgres-js).
|
||||
|
||||
I also drew some major inspirrado from
|
||||
[node-mysql](http://github.com/felixge/node-mysql) and liked what I
|
||||
saw there.
|
||||
|
||||
### Plans for the future?
|
||||
|
||||
- transparent prepared statement caching
|
||||
- more testings of error scenarios
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) 2010 Brian Carlson (brian.m.carlson@gmail.com)
|
||||
|
||||
68
benchmark/js-versus-native-bench.js
Normal file
68
benchmark/js-versus-native-bench.js
Normal 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();
|
||||
});
|
||||
});
|
||||
@ -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");
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@ -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
93
lib/client-pool.js
Normal 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
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
143
lib/index.js
143
lib/index.js
@ -1,144 +1,17 @@
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var sys = require('sys');
|
||||
var net = require('net');
|
||||
var Pool = require(__dirname + '/utils').Pool;
|
||||
var Client = require(__dirname+'/client');
|
||||
var defaults = require(__dirname + '/defaults');
|
||||
|
||||
//wrap up common connection management boilerplate
|
||||
var connect = function(config, callback) {
|
||||
if(poolEnabled()) {
|
||||
return getPooledClient(config, callback)
|
||||
}
|
||||
|
||||
var client = new Client(config);
|
||||
client.connect();
|
||||
|
||||
var onError = function(error) {
|
||||
client.connection.removeListener('readyForQuery', onReady);
|
||||
callback(error);
|
||||
}
|
||||
|
||||
var onReady = function() {
|
||||
client.removeListener('error', onError);
|
||||
callback(null, client);
|
||||
client.on('drain', client.end.bind(client));
|
||||
}
|
||||
|
||||
client.once('error', onError);
|
||||
|
||||
//TODO refactor
|
||||
//i don't like reaching into the client's connection for attaching
|
||||
//to specific events here
|
||||
client.connection.once('readyForQuery', onReady);
|
||||
}
|
||||
|
||||
|
||||
//connection pool global cache
|
||||
var clientPools = {
|
||||
}
|
||||
|
||||
var poolEnabled = function() {
|
||||
return defaults.poolSize;
|
||||
}
|
||||
|
||||
var log = function() {
|
||||
//do nothing
|
||||
}
|
||||
|
||||
//for testing
|
||||
// var log = function() {
|
||||
// console.log.apply(console, arguments);
|
||||
// }
|
||||
|
||||
var getPooledClient = function(config, callback) {
|
||||
//lookup pool using config as key
|
||||
//TODO this don't work so hot w/ object configs
|
||||
var pool = clientPools[config];
|
||||
|
||||
//create pool if doesn't exist
|
||||
if(!pool) {
|
||||
//log("creating pool %s", config)
|
||||
pool = clientPools[config] = new Pool(defaults.poolSize, function() {
|
||||
//log("creating new client in pool %s", config)
|
||||
var client = new Client(config);
|
||||
client.connected = false;
|
||||
return client;
|
||||
})
|
||||
}
|
||||
|
||||
pool.checkOut(function(err, client) {
|
||||
|
||||
//if client already connected just
|
||||
//pass it along to the callback and return
|
||||
if(client.connected) {
|
||||
callback(null, client);
|
||||
return;
|
||||
}
|
||||
|
||||
var onError = function(error) {
|
||||
client.connection.removeListener('readyForQuery', onReady);
|
||||
callback(error);
|
||||
pool.checkIn(client);
|
||||
}
|
||||
|
||||
var onReady = function() {
|
||||
client.removeListener('error', onError);
|
||||
client.connected = true;
|
||||
callback(null, client);
|
||||
client.on('drain', function() {
|
||||
pool.checkIn(client);
|
||||
});
|
||||
}
|
||||
|
||||
client.once('error', onError);
|
||||
|
||||
//TODO refactor
|
||||
//i don't like reaching into the client's connection for attaching
|
||||
//to specific events here
|
||||
client.connection.once('readyForQuery', onReady);
|
||||
|
||||
client.connect();
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
//destroys the world
|
||||
//or optionally only a single pool
|
||||
//mostly used for testing or
|
||||
//a hard shutdown
|
||||
var end = function(name) {
|
||||
if(!name) {
|
||||
for(var poolName in clientPools) {
|
||||
end(poolName)
|
||||
return
|
||||
}
|
||||
}
|
||||
var pool = clientPools[name];
|
||||
//log("destroying pool %s", name);
|
||||
pool.waits.forEach(function(wait) {
|
||||
wait(new Error("Client is being destroyed"))
|
||||
})
|
||||
pool.waits = [];
|
||||
pool.items.forEach(function(item) {
|
||||
var client = item.ref;
|
||||
if(client.activeQuery) {
|
||||
//log("client is still active, waiting for it to complete");
|
||||
client.on('drain', client.end.bind(client))
|
||||
} else {
|
||||
client.end();
|
||||
}
|
||||
})
|
||||
//remove reference to pool lookup
|
||||
clientPools[name] = null;
|
||||
delete(clientPools[name])
|
||||
}
|
||||
|
||||
var pool = require(__dirname + "/client-pool").init(Client);
|
||||
|
||||
module.exports = {
|
||||
Client: Client,
|
||||
Connection: require(__dirname + '/connection'),
|
||||
connect: connect,
|
||||
end: end,
|
||||
connect: pool.connect,
|
||||
end: pool.end,
|
||||
defaults: defaults
|
||||
}
|
||||
|
||||
//lazy require native module...the c++ may not have been compiled
|
||||
module.exports.__defineGetter__("native", function() {
|
||||
return require(__dirname + '/native');
|
||||
})
|
||||
|
||||
186
lib/native.js
Normal file
186
lib/native.js
Normal 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')
|
||||
};
|
||||
75
lib/query.js
75
lib/query.js
@ -1,8 +1,7 @@
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var sys = require('sys');var sys = require('sys');
|
||||
var Result = require(__dirname + "/result");
|
||||
var TextParser = require(__dirname + "/textParser");
|
||||
var BinaryParser = require(__dirname + "/binaryParser");
|
||||
var types = require(__dirname + "/types");
|
||||
|
||||
var Query = function(config) {
|
||||
this.text = config.text;
|
||||
@ -11,9 +10,6 @@ var Query = function(config) {
|
||||
this.types = config.types;
|
||||
this.name = config.name;
|
||||
this.binary = config.binary;
|
||||
//for code clarity purposes we'll declare this here though it's not
|
||||
//set or used until a rowDescription message comes in
|
||||
this.rowDescription = null;
|
||||
this.callback = config.callback;
|
||||
this._fieldNames = [];
|
||||
this._fieldConverters = [];
|
||||
@ -40,65 +36,13 @@ var noParse = function(val) {
|
||||
p.handleRowDescription = function(msg) {
|
||||
this._fieldNames = [];
|
||||
this._fieldConverters = [];
|
||||
|
||||
var parsers = {
|
||||
text: new TextParser(),
|
||||
binary: new BinaryParser()
|
||||
};
|
||||
|
||||
var len = msg.fields.length;
|
||||
for(var i = 0; i < len; i++) {
|
||||
var field = msg.fields[i];
|
||||
var dataTypeId = field.dataTypeID;
|
||||
var format = field.format;
|
||||
this._fieldNames[i] = field.name;
|
||||
switch(dataTypeId) {
|
||||
case 16:
|
||||
this._fieldConverters[i] = parsers[format].parseBool;
|
||||
break;
|
||||
case 20:
|
||||
this._fieldConverters[i] = parsers[format].parseInt64;
|
||||
break;
|
||||
case 21:
|
||||
this._fieldConverters[i] = parsers[format].parseInt16;
|
||||
break;
|
||||
case 23:
|
||||
this._fieldConverters[i] = parsers[format].parseInt32;
|
||||
break;
|
||||
case 25:
|
||||
this._fieldConverters[i] = parsers[format].parseText;
|
||||
break;
|
||||
case 26:
|
||||
this._fieldConverters[i] = parsers[format].parseInt64;
|
||||
break;
|
||||
case 700:
|
||||
this._fieldConverters[i] = parsers[format].parseFloat32;
|
||||
break;
|
||||
case 701:
|
||||
this._fieldConverters[i] = parsers[format].parseFloat64;
|
||||
break;
|
||||
case 1700:
|
||||
this._fieldConverters[i] = parsers[format].parseNumeric;
|
||||
break;
|
||||
case 16:
|
||||
this._fieldConverters[i] = parsers[format].parseBool;
|
||||
break;
|
||||
case 1114:
|
||||
case 1184:
|
||||
this._fieldConverters[i] = parsers[format].parseDate;
|
||||
break;
|
||||
case 1008:
|
||||
case 1009:
|
||||
this._fieldConverters[i] = parsers[format].parseStringArray;
|
||||
break;
|
||||
case 1007:
|
||||
case 1016:
|
||||
this._fieldConverters[i] = parsers[format].parseIntArray;
|
||||
break;
|
||||
default:
|
||||
this._fieldConverters[i] = dataTypeParsers[dataTypeId] || noParse;
|
||||
break;
|
||||
}
|
||||
this._fieldConverters[i] = types.getTypeParser(field.dataTypeID, format);
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
@ -162,7 +106,7 @@ p.getRows = function(connection) {
|
||||
connection.execute({
|
||||
portal: this.name,
|
||||
rows: this.rows
|
||||
});
|
||||
}, true);
|
||||
connection.flush();
|
||||
};
|
||||
|
||||
@ -177,11 +121,11 @@ p.prepare = function(connection) {
|
||||
text: self.text,
|
||||
name: self.name,
|
||||
types: self.types
|
||||
});
|
||||
}, true);
|
||||
connection.parsedStatements[this.name] = true;
|
||||
}
|
||||
|
||||
//TODO is there some btter way to prepare values for the database?
|
||||
//TODO is there some better way to prepare values for the database?
|
||||
if(self.values) {
|
||||
self.values = self.values.map(function(val) {
|
||||
return (val instanceof Date) ? JSON.stringify(val) : val;
|
||||
@ -194,17 +138,14 @@ p.prepare = function(connection) {
|
||||
statement: self.name,
|
||||
values: self.values,
|
||||
binary: self.binary
|
||||
});
|
||||
}, true);
|
||||
|
||||
connection.describe({
|
||||
type: 'P',
|
||||
name: self.name || ""
|
||||
});
|
||||
}, true);
|
||||
|
||||
this.getRows(connection);
|
||||
};
|
||||
|
||||
var dataTypeParsers = {
|
||||
};
|
||||
|
||||
module.exports = Query;
|
||||
|
||||
@ -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
123
lib/textParsers.js
Normal 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
35
lib/types.js
Normal 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,
|
||||
}
|
||||
107
lib/utils.js
107
lib/utils.js
@ -1,4 +1,5 @@
|
||||
var url = require('url');
|
||||
var defaults = require(__dirname + "/defaults");
|
||||
var events = require('events');
|
||||
var sys = require('sys');
|
||||
|
||||
@ -11,6 +12,7 @@ if(typeof events.EventEmitter.prototype.once !== 'function') {
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
var Pool = function(maxSize, createFn) {
|
||||
events.EventEmitter.call(this);
|
||||
this.maxSize = maxSize;
|
||||
@ -18,10 +20,15 @@ var Pool = function(maxSize, createFn) {
|
||||
this.items = [];
|
||||
this.waits = [];
|
||||
}
|
||||
|
||||
sys.inherits(Pool, events.EventEmitter);
|
||||
|
||||
var p = Pool.prototype;
|
||||
|
||||
p.checkOut = function(callback) {
|
||||
if(!this.maxSize) {
|
||||
return callback(null, this.createFn());
|
||||
}
|
||||
var len = 0;
|
||||
for(var i = 0, len = this.items.length; i < len; i++) {
|
||||
var item = this.items[i];
|
||||
@ -32,13 +39,7 @@ p.checkOut = function(callback) {
|
||||
//check if we can create a new item
|
||||
if(this.items.length < this.maxSize && this.createFn) {
|
||||
var result = this.createFn();
|
||||
var item = result;
|
||||
//create function can return item conforming to interface
|
||||
//of stored items to allow for create function to create
|
||||
//checked out items
|
||||
if(typeof item.checkedIn == "undefined") {
|
||||
var item = {ref: result, checkedIn: true}
|
||||
}
|
||||
var item = {ref: result, checkedIn: true}
|
||||
this.items.push(item);
|
||||
if(item.checkedIn) {
|
||||
return this._pulse(item, callback)
|
||||
@ -73,17 +74,91 @@ p._pulse = function(item, cb) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
var parseConnectionString = function(str) {
|
||||
//unix socket
|
||||
if(str.charAt(0) === '/') {
|
||||
return { host: str };
|
||||
}
|
||||
var result = url.parse(str);
|
||||
var config = {};
|
||||
config.host = result.hostname;
|
||||
config.database = result.pathname ? result.pathname.slice(1) : null
|
||||
var auth = (result.auth || ':').split(':');
|
||||
config.user = auth[0];
|
||||
config.password = auth[1];
|
||||
config.port = result.port;
|
||||
return config;
|
||||
};
|
||||
|
||||
//allows passing false as property to remove it from config
|
||||
var norm = function(config, propName) {
|
||||
config[propName] = (config[propName] || (config[propName] === false ? undefined : defaults[propName]))
|
||||
};
|
||||
|
||||
//normalizes connection info
|
||||
//which can be in the form of an object
|
||||
//or a connection string
|
||||
var normalizeConnectionInfo = function(config) {
|
||||
switch(typeof config) {
|
||||
case 'object':
|
||||
norm(config, 'user');
|
||||
norm(config, 'password');
|
||||
norm(config, 'host');
|
||||
norm(config, 'port');
|
||||
norm(config, 'database');
|
||||
return config;
|
||||
case 'string':
|
||||
return normalizeConnectionInfo(parseConnectionString(config));
|
||||
default:
|
||||
throw new Error("Unrecognized connection config parameter: " + config);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var add = function(params, config, paramName) {
|
||||
var value = config[paramName];
|
||||
if(value) {
|
||||
params.push(paramName+"='"+value+"'");
|
||||
}
|
||||
}
|
||||
|
||||
//builds libpq specific connection string
|
||||
//from a supplied config object
|
||||
//the config object conforms to the interface of the config object
|
||||
//accepted by the pure javascript client
|
||||
var getLibpgConString = function(config, callback) {
|
||||
if(typeof config == 'object') {
|
||||
var params = []
|
||||
add(params, config, 'user');
|
||||
add(params, config, 'password');
|
||||
add(params, config, 'port');
|
||||
if(config.database) {
|
||||
params.push("dbname='" + config.database + "'");
|
||||
}
|
||||
if(config.host) {
|
||||
if(config.host != 'localhost' && config.host != '127.0.0.1') {
|
||||
//do dns lookup
|
||||
return require('dns').lookup(config.host, 4, function(err, address) {
|
||||
if(err) return callback(err, null);
|
||||
params.push("hostaddr="+address)
|
||||
callback(null, params.join(" "))
|
||||
})
|
||||
}
|
||||
params.push("hostaddr=127.0.0.1 ");
|
||||
}
|
||||
callback(null, params.join(" "));
|
||||
} else {
|
||||
throw new Error("Unrecognized config type for connection");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Pool: Pool,
|
||||
parseConnectionString: function(str) {
|
||||
var result = url.parse(str);
|
||||
result.host = result.hostname;
|
||||
result.database = result.pathname ? result.pathname.slice(1) : null
|
||||
var auth = (result.auth || ':').split(':');
|
||||
result.user = auth[0];
|
||||
result.password = auth[1];
|
||||
return result;
|
||||
}
|
||||
normalizeConnectionInfo: normalizeConnectionInfo,
|
||||
//only exported here to make testing of this method possible
|
||||
//since it contains quite a bit of logic and testing for
|
||||
//each connection scenario in an integration test is impractical
|
||||
buildLibpqConnectionString: getLibpgConString
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
13
package.json
13
package.json
@ -1,14 +1,17 @@
|
||||
{ "name": "pg",
|
||||
"version": "0.2.7",
|
||||
"description": "Pure JavaScript PostgreSQL client",
|
||||
"version": "0.5.0",
|
||||
"description": "PostgreSQL client - pure javascript & libpq with the same API",
|
||||
"keywords" : ["postgres", "pg", "libpq", "postgre", "database", "rdbms"],
|
||||
"homepage": "http://github.com/brianc/node-postgres",
|
||||
"repository" : {
|
||||
"type" : "git",
|
||||
"url" : "git://github.com/brianc/node-postgres.git"
|
||||
},
|
||||
"author" : "Brian Carlson <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
632
src/binding.cc
Normal 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, ¬ice);
|
||||
}
|
||||
|
||||
//called to process io_events from libev
|
||||
void HandleIOEvent(int revents)
|
||||
{
|
||||
if(revents & EV_ERROR) {
|
||||
LOG("Connection error.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(connecting_) {
|
||||
TRACE("Processing connecting_ io");
|
||||
HandleConnectionIO();
|
||||
return;
|
||||
}
|
||||
|
||||
if(revents & EV_READ) {
|
||||
TRACE("revents & EV_READ");
|
||||
if(PQconsumeInput(connection_) == 0) {
|
||||
LOG("Something happened, consume input is 0");
|
||||
return;
|
||||
}
|
||||
|
||||
//declare handlescope as this method is entered via a libev callback
|
||||
//and not part of the public v8 interface
|
||||
HandleScope scope;
|
||||
|
||||
if (PQisBusy(connection_) == 0) {
|
||||
PGresult *result;
|
||||
bool didHandleResult = false;
|
||||
while ((result = PQgetResult(connection_))) {
|
||||
HandleResult(result);
|
||||
didHandleResult = true;
|
||||
PQclear(result);
|
||||
}
|
||||
//might have fired from notification
|
||||
if(didHandleResult) {
|
||||
Emit(ready_symbol, 0, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
PGnotify *notify;
|
||||
while ((notify = PQnotifies(connection_))) {
|
||||
Local<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);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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())
|
||||
}))
|
||||
}))
|
||||
})
|
||||
|
||||
79
test/integration/client/big-simple-query-tests.js
Normal file
79
test/integration/client/big-simple-query-tests.js
Normal file
File diff suppressed because one or more lines are too long
@ -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'
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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();
|
||||
})
|
||||
|
||||
@ -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));
|
||||
|
||||
|
||||
});
|
||||
|
||||
42
test/integration/client/notice-tests.js
Normal file
42
test/integration/client/notice-tests.js
Normal 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")
|
||||
}
|
||||
});
|
||||
}));
|
||||
}));
|
||||
})
|
||||
|
||||
@ -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;
|
||||
}))
|
||||
}))
|
||||
}))
|
||||
}))
|
||||
})
|
||||
|
||||
@ -16,5 +16,6 @@ module.exports = {
|
||||
},
|
||||
connectionString: helper.connectionString,
|
||||
Sink: helper.Sink,
|
||||
pg: helper.pg
|
||||
pg: helper.pg,
|
||||
args: helper.args
|
||||
};
|
||||
|
||||
@ -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();
|
||||
})
|
||||
}))
|
||||
|
||||
32
test/integration/connection-pool/unique-name-tests.js
Normal file
32
test/integration/connection-pool/unique-name-tests.js
Normal 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();
|
||||
}))
|
||||
@ -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() {
|
||||
|
||||
@ -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) {
|
||||
}
|
||||
|
||||
17
test/native/callback-api-tests.js
Normal file
17
test/native/callback-api-tests.js
Normal 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();
|
||||
}))
|
||||
}));
|
||||
})
|
||||
22
test/native/connection-tests.js
Normal file
22
test/native/connection-tests.js
Normal 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();
|
||||
})
|
||||
})
|
||||
})
|
||||
61
test/native/error-tests.js
Normal file
61
test/native/error-tests.js
Normal 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();
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
115
test/native/evented-api-tests.js
Normal file
115
test/native/evented-api-tests.js
Normal 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();
|
||||
})
|
||||
})
|
||||
})
|
||||
49
test/native/stress-tests.js
Normal file
49
test/native/stress-tests.js
Normal 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();
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
|
||||
9
test/unit/client/notification-tests.js
Normal file
9
test/unit/client/notification-tests.js
Normal 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!!");
|
||||
})
|
||||
|
||||
@ -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});
|
||||
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
require(__dirname+'/../test-helper');
|
||||
|
||||
var Connection = require('connection');
|
||||
var makeClient = function() {
|
||||
var connection = new Connection({stream: "no"});
|
||||
connection.startup = function() {};
|
||||
|
||||
@ -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})
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
};
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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});
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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)
|
||||
}))
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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
32
wscript
Normal 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")
|
||||
Loading…
x
Reference in New Issue
Block a user