diff --git a/.gitignore b/.gitignore index 81d95c881..44602419d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules/ *.log .lock-wscript build/ +*~ diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 000000000..c6c11efc5 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,5 @@ +{ + "trailing": true, + "indent": 2, + "evil": true +} diff --git a/.npmignore b/.npmignore index 2bd9cb3ff..b0d737dc3 100644 --- a/.npmignore +++ b/.npmignore @@ -1,2 +1,8 @@ +node_modules/ +*.swp +*.log .lock-wscript build/ +*~ +test/ +script/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index b13a0fe1c..74f1e4c0c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,44 @@ language: node_js -node_js: - - 0.8 +sudo: false +dist: trusty before_script: - node script/create-test-tables.js pg://postgres@127.0.0.1:5432/postgres +env: + - CC=clang CXX=clang++ npm_config_clang=1 PGUSER=postgres PGDATABASE=postgres + +node_js: "6" +addons: + postgresql: "9.6" + +matrix: + include: + - node_js: "0.10" + addons: + postgresql: "9.6" + env: [] + - node_js: "0.12" + addons: + postgresql: "9.6" + env: [] + - node_js: "4" + addons: + postgresql: "9.6" + - node_js: "5" + addons: + postgresql: "9.6" + - node_js: "6" + addons: + postgresql: "9.1" + dist: precise + - node_js: "6" + addons: + postgresql: "9.2" + - node_js: "6" + addons: + postgresql: "9.3" + - node_js: "6" + addons: + postgresql: "9.4" + - node_js: "6" + addons: + postgresql: "9.5" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..56b294133 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,236 @@ +All major and minor releases are briefly explained below. + +For richer information consult the commit log on github with referenced pull requests. + +We do not include break-fix version release in this file. + +### v6.2.0 + +- Add support for [parsing `replicationStart` messages](https://github.com/brianc/node-postgres/pull/1271/files). + +### v6.1.0 + +- Add optional callback parameter to the pure JavaScript `client.end` method. The native client already supported this. + +### v6.0.0 + +#### Breaking Changes +- Remove `pg.pools`. There is still a reference kept to the pools created & tracked by `pg.connect` but it has been renamed, is considered private, and should not be used. Accessing this API directly was uncommon and was _supposed_ to be private but was incorrectly documented on the wiki. Therefore, it is a breaking change of an (unintentionally) public interface to remove it by renaming it & making it private. Eventually `pg.connect` itself will be deprecated in favor of instantiating pools directly via `new pg.Pool()` so this property should become completely moot at some point. In the mean time...check out the new features... + +#### New features + +- Replace internal pooling code with [pg-pool](https://github.com/brianc/node-pg-pool). This is the first step in eventually deprecating and removing the singleton `pg.connect`. The pg-pool constructor is exported from node-postgres at `require('pg').Pool`. It provides a backwards compatible interface with `pg.connect` as well as a promise based interface & additional niceties. + +You can now create an instance of a pool and don't have to rely on the `pg` singleton for anything: + +``` +var pg = require('pg') + +var pool = new pg.Pool() + +// your friendly neighboorhood pool interface, without the singleton +pool.connect(function(err, client, done) { + // ... +}) +``` + +Promise support & other goodness lives now in [pg-pool](https://github.com/brianc/node-pg-pool). + +__Please__ read the readme at [pg-pool](https://github.com/brianc/node-pg-pool) for the full api. + +- Included support for tcp keep alive. Enable it as follows: + +```js +var client = new Client({ keepAlive: true }) +``` + +This should help with backends incorrectly considering idle clients to be dead and prematurely disconnecting them. + + +### v5.1.0 +- Make the query object returned from `client.query` implement the promise interface. This is the first step towards promisifying more of the node-postgres api. + +Example: +```js +var client = new Client() +client.connect() +client.query('SELECT $1::text as name', ['brianc']) + .then(function(res) { + console.log('hello from', res.rows[0]) + client.end() + }) +``` + +### v5.0.0 + +#### Breaking Changes +- `require('pg').native` now returns null if the native bindings cannot be found; previously, this threw an exception. + +#### New Features +- better error message when passing `undefined` as a query parameter +- support for `defaults.connectionString` +- support for `returnToHead` being passed to [generic pool](https://github.com/coopernurse/node-pool) + +### v4.5.0 +- Add option to parse JS date objects in query parameters as [UTC](https://github.com/brianc/node-postgres/pull/943) + +### v4.4.0 +- Warn to `stderr` if a named query exceeds 63 characters which is the max lenght supported by postgres. + +### v4.3.0 +- Unpin `pg-types` semver. Allow it to float against `pg-types@1.x`. + +### v4.2.0 +- Support for additional error fields in postgres >= 9.3 if available. + +### v4.1.0 +- Allow type parser overrides on a [per-client basis](https://github.com/brianc/node-postgres/pull/679) + +### v4.0.0 +- Make [native bindings](https://github.com/brianc/node-pg-native.git) an optional install with `npm install pg-native` +- No longer surround query result callback with `try/catch` block. +- Remove built in COPY IN / COPY OUT support - better implementations provided by [pg-copy-streams](https://github.com/brianc/node-pg-copy-streams.git) and [pg-native](https://github.com/brianc/node-pg-native.git) + +### v3.6.0 +- Include support for (parsing JSONB)[https://github.com/brianc/node-pg-types/pull/13] (supported in postgres 9.4) + +### v3.5.0 +- Include support for parsing boolean arrays + +### v3.4.0 +- Include port as connection parameter to [unix sockets](https://github.com/brianc/node-postgres/pull/604) +- Better support for odd [date parsing](https://github.com/brianc/node-pg-types/pull/8) + +### v3.2.0 + +- Add support for parsing [date arrays](https://github.com/brianc/node-pg-types/pull/3) +- Expose array parsers on [pg.types](https://github.com/brianc/node-pg-types/pull/2) +- Allow [pool](https://github.com/brianc/node-postgres/pull/591) to be configured + + +### v3.1.0 + +- Add [count of the number of times a client has been checked out from the pool](https://github.com/brianc/node-postgres/pull/556) +- Emit `end` from `pg` object [when a pool is drained](https://github.com/brianc/node-postgres/pull/571) + +### v3.0.0 + +#### Breaking changes +- [Parse the DATE PostgreSQL type as local time](https://github.com/brianc/node-postgres/pull/514) + +After [some discussion](https://github.com/brianc/node-postgres/issues/510) it was decided node-postgres was non-compliant in how it was handling DATE results. They were being converted to UTC, but the PostgreSQL documentation specifies they should be returned in the client timezone. This is a breaking change, and if you use the `date` type you might want to examine your code and make sure nothing is impacted. + +- [Fix possible numeric precision loss on numeric & int8 arrays](https://github.com/brianc/node-postgres/pull/501) + +pg@v2.0 included changes to not convert large integers into their JavaScript number representation because of possibility for numeric precision loss. The same types in arrays were not taken into account. This fix applies the same type of type-coercion rules to arrays of those types, so there will be no more possible numeric loss on an array of very large int8s for example. This is a breaking change because now a return type from a query of `int8[]` will contain _string_ representations +of the integers. Use your favorite JavaScript bignum module to represent them without precision loss, or punch over the type converter to return the old style arrays again. + +- [Fix to input array of dates being improperly converted to utc](https://github.com/benesch/node-postgres/commit/c41eedc3e01e5527a3d5c242fa1896f02ef0b261#diff-7172adb1fec2457a2700ed29008a8e0aR108) + +Single `date` parameters were properly sent to the PostgreSQL server properly in local time, but an input array of dates was being changed into utc dates. This is a violation of what PostgreSQL expects. Small breaking change, but none-the-less something you should check out if you are inserting an array of dates. + +- [Query no longer emits `end` event if it ends due to an error](https://github.com/brianc/node-postgres/commit/357b64d70431ec5ca721eb45a63b082c18e6ffa3) + +This is a small change to bring the semantics of query more in line with other EventEmitters. The tests all passed after this change, but I suppose it could still be a breaking change in certain use cases. If you are doing clever things with the `end` and `error` events of a query object you might want to check to make sure its still behaving normally, though it is most likely not an issue. + +#### New features +- [Supercharge `prepareValue`](https://github.com/brianc/node-postgres/pull/555) + +The long & short of it is now any object you supply in the list of query values will be inspected for a `.toPostgres` method. If the method is present it will be called and its result used as the raw text value sent to PostgreSQL for that value. This allows the same type of custom type coercion on query parameters as was previously afforded to query result values. + +- [Domain aware connection pool](https://github.com/brianc/node-postgres/pull/531) + +If domains are active node-postgres will honor them and do everything it can to ensure all callbacks are properly fired in the active domain. If you have tried to use domains with node-postgres (or many other modules which pool long lived event emitters) you may have run into an issue where the active domain changes before and after a callback. This has been a longstanding footgun within node-postgres and I am happy to get it fixed. + +- [Disconnected clients now removed from pool](https://github.com/brianc/node-postgres/pull/543) + +Avoids a scenario where your pool could fill up with disconnected & unusable clients. + +- [Break type parsing code into separate module](https://github.com/brianc/node-postgres/pull/541) + +To provide better documentation and a clearer explanation of how to override the query result parsing system we broke the type converters [into their own module](https://github.com/brianc/node-pg-types). There is still work around removing the 'global-ness' of the type converters so each query or connection can return types differently, but this is a good first step and allow a lot more obvious way to return int8 results as JavaScript numbers, for example + +### v2.11.0 +- Add support for [application_name](https://github.com/brianc/node-postgres/pull/497) + +### v2.10.0 +- Add support for [the password file](http://www.postgresql.org/docs/9.3/static/libpq-pgpass.html) + +### v2.9.0 +- Add better support for [unix domain socket](https://github.com/brianc/node-postgres/pull/487) connections + +### v2.8.0 +- Add support for parsing JSON[] and UUID[] result types + +### v2.7.0 +- Use single row mode in native bindings when available [@rpedela] + - reduces memory consumption when handling row values in 'row' event +- Automatically bind buffer type parameters as binary [@eugeneware] + +### v2.6.0 +- Respect PGSSLMODE environment variable + +### v2.5.0 +- Ability to opt-in to int8 parsing via `pg.defaults.parseInt8 = true` + +### v2.4.0 +- Use eval in the result set parser to increase performance + +### v2.3.0 +- Remove built-in support for binary Int64 parsing. +_Due to the low usage & required compiled dependency this will be pushed into a 3rd party add-on_ + +### v2.2.0 +- [Add support for excapeLiteral and escapeIdentifier in both JavaScript and the native bindings](https://github.com/brianc/node-postgres/pull/396) + +### v2.1.0 +- Add support for SSL connections in JavaScript driver + - this means you can connect to heroku postgres from your local machine without the native bindings! +- [Add field metadata to result object](https://github.com/brianc/node-postgres/blob/master/test/integration/client/row-description-on-results-tests.js) +- [Add ability for rows to be returned as arrays instead of objects](https://github.com/brianc/node-postgres/blob/master/test/integration/client/results-as-array-tests.js) + +### v2.0.0 + +- Properly handle various PostgreSQL to JavaScript type conversions to avoid data loss: + +``` +PostgreSQL | pg@v2.0 JavaScript | pg@v1.0 JavaScript +--------------------------------|---------------- +float4 | number (float) | string +float8 | number (float) | string +int8 | string | number (int) +numeric | string | number (float) +decimal | string | number (float) +``` + +For more information see https://github.com/brianc/node-postgres/pull/353 +If you are unhappy with these changes you can always [override the built in type parsing fairly easily](https://github.com/brianc/node-pg-parse-float). + +### v1.3.0 + +- Make client_encoding configurable and optional + +### v1.2.0 + +- return field metadata on result object: access via result.fields[i].name/dataTypeID + +### v1.1.0 + +- built in support for `JSON` data type for PostgreSQL Server @ v9.2.0 or greater + +### v1.0.0 + +- remove deprecated functionality + - Callback function passed to `pg.connect` now __requires__ 3 arguments + - Client#pauseDrain() / Client#resumeDrain removed + - numeric, decimal, and float data types no longer parsed into float before being returned. Will be returned from query results as `String` + +### v0.15.0 + +- client now emits `end` when disconnected from back-end server +- if client is disconnected in the middle of a query, query receives an error + +### v0.14.0 + +- add deprecation warnings in prep for v1.0 +- fix read/write failures in native module under node v0.9.x diff --git a/Makefile b/Makefile index c1e79ec1d..4eddd4f10 100644 --- a/Makefile +++ b/Makefile @@ -1,36 +1,48 @@ -SHELL := /bin/bash +SHELL := /bin/sh -connectionString=pg:// +connectionString=postgres:// params := $(connectionString) node-command := xargs -n 1 -I file node file $(params) -.PHONY : test test-connection test-integration bench test-native build/default/binding.node +.PHONY : test test-connection test-integration bench test-native \ + jshint publish test-missing-native update-npm + +all: + npm install help: - echo "make test-all connectionString=pg://" + @echo "make test-all [connectionString=postgres://]" + +test: test-unit -test: test-unit +test-all: jshint test-missing-native test-unit test-integration test-native test-binary -test-all: test-unit test-integration test-native test-binary + +update-npm: + @npm i npm --global bench: @find benchmark -name "*-bench.js" | $(node-command) -build/default/binding.node: - @node-gyp rebuild - test-unit: @find test/unit -name "*-tests.js" | $(node-command) test-connection: - @node script/test-connection.js $(params) + @echo "***Testing connection***" + @node script/create-test-tables.js $(params) -test-connection-binary: - @node script/test-connection.js $(params) binary +test-missing-native: + @echo "***Testing optional native install***" + @rm -rf node_modules/pg-native + @node test/native/missing-native.js + @rm -rf node_modules/pg-native -test-native: build/default/binding.node +node_modules/pg-native/index.js: + @npm i pg-native + +test-native: node_modules/pg-native/index.js test-connection @echo "***Testing native bindings***" @find test/native -name "*-tests.js" | $(node-command) @find test/integration -name "*-tests.js" | $(node-command) native @@ -39,6 +51,13 @@ test-integration: test-connection @echo "***Testing Pure Javascript***" @find test/integration -name "*-tests.js" | $(node-command) -test-binary: test-connection-binary +test-binary: test-connection @echo "***Testing Pure Javascript (binary)***" @find test/integration -name "*-tests.js" | $(node-command) binary + +test-pool: + @find test/integration/connection-pool -name "*.js" | $(node-command) binary + +jshint: + @echo "***Starting jshint***" + @./node_modules/.bin/jshint lib diff --git a/README.md b/README.md index f3123fa9e..2f9d11beb 100644 --- a/README.md +++ b/README.md @@ -1,158 +1,259 @@ -#node-postgres +# node-postgres -[![Build Status](https://secure.travis-ci.org/brianc/node-postgres.png)](http://travis-ci.org/brianc/node-postgres) +[![Build Status](https://secure.travis-ci.org/brianc/node-postgres.svg?branch=master)](http://travis-ci.org/brianc/node-postgres) +[![Dependency Status](https://david-dm.org/brianc/node-postgres.svg)](https://david-dm.org/brianc/node-postgres) +NPM version +NPM downloads -Non-blocking PostgreSQL client for node.js. Pure JavaScript and native libpq bindings. Active development, well tested, and production use. +Non-blocking PostgreSQL client for node.js. Pure JavaScript and optional native libpq bindings. -## Installation +## Install - npm install pg - -## Examples +```sh +$ npm install pg +``` -### Simple, using built-in client pool +## Intro & Examples -```javascript -var pg = require('pg'); -//or native libpq bindings -//var pg = require('pg').native +There are 3 ways of executing queries -var conString = "tcp://postgres:1234@localhost/postgres"; +1. Passing the query to a pool +2. Borrowing a client from a pool and executing the query with it +3. Obtaining an exclusive client and executing the query with it -//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()); - }); -}); -``` +It is recommended to pass the query to a pool as often as possible. If that isn't possible, because of long and complex transactions for example, borrow a client from a pool. Just remember to initialize the pool only once in your code so you maximize reusability of connections. + +### Why pooling? + +If you're working on something like a web application which makes frequent queries you'll want to access the PostgreSQL server through a pool of clients. Why? For one thing, there is ~20-30 millisecond delay (YMMV) when connecting a new client to the PostgreSQL server because of the startup handshake. Furthermore, PostgreSQL can support only a limited number of clients...it depends on the amount of ram on your database server, but generally more than 100 clients at a time is a __very bad thing__. :tm: Additionally, PostgreSQL can only execute 1 query at a time per connected client, so pipelining all queries for all requests through a single, long-lived client will likely introduce a bottleneck into your application if you need high concurrency. + +With that in mind we can imagine a situation where you have a web server which connects and disconnects a new client for every web request or every query (don't do this!). If you get only 1 request at a time everything will seem to work fine, though it will be a touch slower due to the connection overhead. Once you get >100 simultaneous requests your web server will attempt to open 100 connections to the PostgreSQL backend and :boom: you'll run out of memory on the PostgreSQL server, your database will become unresponsive, your app will seem to hang, and everything will break. Boooo! -### Evented api +__Good news__: node-postgres ships with built in client pooling. Client pooling allows your application to use a pool of already connected clients and reuse them for each request to your application. If your app needs to make more queries than there are available clients in the pool the queries will queue instead of overwhelming your database & causing a cascading failure. :thumbsup: + +node-postgres uses [pg-pool](https://github.com/brianc/node-pg-pool.git) to manage pooling. It bundles it and exports it for convenience. If you want, you can `require('pg-pool')` and use it directly - it's the same as the constructor exported at `pg.Pool`. + +It's __highly recommended__ you read the documentation for [pg-pool](https://github.com/brianc/node-pg-pool.git). + +[Here is an up & running quickly example](https://github.com/brianc/node-postgres/wiki/Example) + +For more information about `config.ssl` check [TLS (SSL) of nodejs](https://nodejs.org/dist/latest-v4.x/docs/api/tls.html) + +### Pooling example + +Let's create a pool in `./lib/db.js` which will be reused across the whole project ```javascript -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)] +const pg = require('pg'); + +// create a config to configure both pooling behavior +// and client options +// note: all config is optional and the environment variables +// will be read if the config is not present +var config = { + user: 'foo', //env var: PGUSER + database: 'my_db', //env var: PGDATABASE + password: 'secret', //env var: PGPASSWORD + host: 'localhost', // Server hosting the postgres database + port: 5432, //env var: PGPORT + max: 10, // max number of clients in the pool + idleTimeoutMillis: 30000, // how long a client is allowed to remain idle before being closed +}; + +//this initializes a connection pool +//it will keep idle connections open for 30 seconds +//and set a limit of maximum 10 idle clients +const pool = new pg.Pool(config); + +pool.on('error', function (err, client) { + // if an error is encountered by a client while it sits idle in the pool + // the pool itself will emit an error event with both the error and + // the client which emitted the original error + // this is a rare occurrence but can happen if there is a network partition + // between your application and the database, the database restarts, etc. + // and so you might want to handle it and at least log it out + console.error('idle client error', err.message, err.stack); }); -//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)] +//export the query method for passing queries to the pool +module.exports.query = function (text, values, callback) { + console.log('query:', text, values); + return pool.query(text, values, callback); +}; + +// the pool also supports checking out a client for +// multiple operations, such as a transaction +module.exports.connect = function (callback) { + return pool.connect(callback); +}; +``` + +Now if in `./foo.js` you want to pass a query to the pool + +```js +const pool = require('./lib/db'); + +//to run a query we just pass it to the pool +//after we're done nothing has to be taken care of +//we don't have to return any client to the pool or close a connection +pool.query('SELECT $1::int AS number', ['2'], function(err, res) { + if(err) { + return console.error('error running query', err); + } + + console.log('number:', res.rows[0].number); }); -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 +``` + +Or if in `./bar.js` you want borrow a client from the pool + +```js +const pool = require('./lib/db'); + +//ask for a client from the pool +pool.connect(function(err, client, done) { + if(err) { + return console.error('error fetching client from pool', err); + } + + //use the client for executing the query + client.query('SELECT $1::int AS number', ['1'], function(err, result) { + //call `done(err)` to release the client back to the pool (or destroy it if there is an error) + done(err); + + if(err) { + return console.error('error running query', err); + } + console.log(result.rows[0].number); + //output: 1 + }); }); +``` + +For more examples, including how to use a connection pool with promises and async/await see the [example](https://github.com/brianc/node-postgres/wiki/Example) page in the wiki. + +### Obtaining an exclusive client, example + +```js +var pg = require('pg'); + +// instantiate a new client +// the client will read connection information from +// the same environment variables used by postgres cli tools +var client = new pg.Client(); + +// connect to our database +client.connect(function (err) { + if (err) throw err; + + // execute a query on our database + client.query('SELECT $1::text as name', ['brianc'], function (err, result) { + if (err) throw err; + + // just print the result to the console + console.log(result.rows[0]); // outputs: { name: 'brianc' } -//fired after last row is emitted -query.on('end', function() { - client.end(); + // disconnect the client + client.end(function (err) { + if (err) throw err; + }); + }); }); + +``` + +## [More Documentation](https://github.com/brianc/node-postgres/wiki) + +## Native Bindings + +To install the [native bindings](https://github.com/brianc/node-pg-native.git): + +```sh +$ npm install pg pg-native ``` -### Example notes -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. +node-postgres contains a pure JavaScript protocol implementation which is quite fast, but you can optionally use [native](https://github.com/brianc/node-pg-native) [bindings](https://github.com/brianc/node-libpq) for a 20-30% increase in parsing speed (YMMV). Both versions are adequate for production workloads. I personally use the pure JavaScript implementation because I like knowing what's going on all the way down to the binary on the socket, and it allows for some fancier [use](https://github.com/brianc/node-pg-cursor) [cases](https://github.com/brianc/node-pg-query-stream) which are difficult to do with libpq. :smile: -All examples will work with the pure javascript bindings (currently default) or the libpq native (c/c++) bindings (currently in beta) +To use the native bindings, first install [pg-native](https://github.com/brianc/node-pg-native.git). Once pg-native is installed, simply replace `var pg = require('pg')` with `var pg = require('pg').native`. Make sure any exported constructors from `pg` are from the native instance. Example: -To use native libpq bindings replace `require('pg')` with `require('pg').native`. +```js +var pg = require('pg').native +var Pool = require('pg').Pool // bad! this is not bound to the native client +var Client = require('pg').Client // bad! this is the pure JavaScript client -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. +var pg = require('pg').native +var Pool = pg.Pool // good! a pool bound to the native client +var Client = pg.Client // good! this client uses libpq bindings +``` -### Info +#### API differences -* pure javascript client and native libpq bindings share _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 -* built-in (optional) connection pooling -* responsive project maintainer +node-postgres abstracts over the pg-native module to provide the same interface as the pure JavaScript version. Care has been taken to keep the number of api differences between the two modules to a minimum. +However, currently some differences remain, especially : +* the error object in pg-native is different : notably, the information about the postgres error code is not present in field `code` but in the field `sqlState` , and the name of a few other fields is different (see https://github.com/brianc/node-postgres/issues/938, https://github.com/brianc/node-postgres/issues/972). +So for example, if you rely on error.code in your application, your will have to adapt your code to work with native bindings. +* the notification object has a few less properties (see https://github.com/brianc/node-postgres/issues/1045) +* column objects have less properties (see https://github.com/brianc/node-postgres/issues/988) +* the modules https://github.com/brianc/node-pg-copy-streams and https://github.com/brianc/node-pg-query-stream do not work with native bindings (you will have to require 'pg' to use them). + +Thus, it is recommended you use either the pure JavaScript or native bindings in both development and production and don't mix & match them in the same process - it can get confusing! + +## Features + +* pure JavaScript client and native libpq bindings share _the same api_ +* connection pooling +* extensible js<->postgresql data-type coercion * supported PostgreSQL features * parameterized queries * named statements with query plan caching - * async notifications - * extensible js<->postgresql data-type coercion -* query queue -* active development -* fast -* close mirror of the node-mysql api for future multi-database-supported ORM implementation ease - -### Contributors - -Many thanks to the following: - -* [creationix](https://github.com/creationix) -* [felixge](https://github.com/felixge) -* [pshc](https://github.com/pshc) -* [pjornblomqvist](https://github.com/bjornblomqvist) -* [JulianBirch](https://github.com/JulianBirch) -* [ef4](https://github.com/ef4) -* [napa3um](https://github.com/napa3um) -* [drdaeman](https://github.com/drdaeman) -* [booo](https://github.com/booo) -* [neonstalwart](https://github.com/neonstalwart) -* [homme](https://github.com/homme) -* [bdunavant](https://github.com/bdunavant) -* [tokumine](https://github.com/tokumine) -* [shtylman](https://github.com/shtylman) -* [cricri](https://github.com/cricri) -* [AlexanderS](https://github.com/AlexanderS) -* [ahtih](https://github.com/ahtih) -* [chowey](https://github.com/chowey) -* [kennym](https://github.com/kennym) - -## Documentation - -Documentation is a work in progress primarily taking place on the github WIKI - -### [Documentation](https://github.com/brianc/node-postgres/wiki) - -### __PLEASE__ check out the WIKI - -If you have a question, post it to the FAQ section of the WIKI so everyone can read the answer - -## Production Use -* [yammer.com](http://www.yammer.com) -* [bayt.com](http://bayt.com) -* [bitfloor.com](https://bitfloor.com) -* [Vendly](http://www.vend.ly) - -_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. I am usually available via google-talk at my github account public email address. - + * async notifications with `LISTEN/NOTIFY` + * bulk import & export with `COPY TO/COPY FROM` + +## Extras + +node-postgres is by design pretty light on abstractions. These are some handy modules we've been using over the years to complete the picture. +Entire list can be found on [wiki](https://github.com/brianc/node-postgres/wiki/Extras) + +## Contributing + +__:heart: contributions!__ + +If you need help getting the tests running locally or have any questions about the code when working on a patch please feel free to email me or gchat me. + +I will __happily__ accept your pull request if it: +- __has tests__ +- looks reasonable +- does not break backwards compatibility + +Information about the testing processes is in the [wiki](https://github.com/brianc/node-postgres/wiki/Testing). + +Open source belongs to all of us, and we're all invited to participate! + +## Troubleshooting and FAQ + +The causes and solutions to common errors can be found among the [Frequently Asked Questions(FAQ)](https://github.com/brianc/node-postgres/wiki/FAQ) + +## Support + +If at all possible when you open an issue please provide +- version of node +- version of postgres +- smallest possible snippet of code to reproduce the problem + +Usually I'll pop the code into the repo as a test. Hopefully the test fails. Then I make the test pass. Then everyone's happy! + +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. Remember this is a labor of love, and though I try to get back to everything sometimes life takes priority, and I might take a while. It helps if you use nice code formatting in your issue, search for existing answers before posting, and come back and close out the issue if you figure out a solution. The easier you can make it for me, the quicker I'll try and respond to you! + +If you need deeper support, have application specific questions, would like to sponsor development, or want consulting around node & postgres please send me an email, I'm always happy to discuss! + +I usually tweet about any important status updates or changes to node-postgres on twitter. +Follow me [@briancarlson](https://twitter.com/briancarlson) to keep up to date. + + ## License -Copyright (c) 2010 Brian Carlson (brian.m.carlson@gmail.com) +Copyright (c) 2010-2016 Brian Carlson (brian.m.carlson@gmail.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/benchmark/js-versus-native-bench.js b/benchmark/js-versus-native-bench.js deleted file mode 100644 index b65fb98cc..000000000 --- a/benchmark/js-versus-native-bench.js +++ /dev/null @@ -1,68 +0,0 @@ -var pg = require(__dirname + '/../lib') -var pgNative = require(__dirname + '/../lib/native'); -var bencher = require('bencher'); -var helper = require(__dirname + '/../test/test-helper') -var conString = helper.connectionString() - -var round = function(num) { - return Math.round((num*1000))/1000 -} - -var doBenchmark = function() { - var bench = bencher({ - name: 'js/native compare', - repeat: 1000, - actions: [{ - name: 'javascript client - simple query', - run: function(next) { - var query = client.query('SELECT name, age FROM person WHERE age > 10'); - query.on('end', function() { - next(); - }); - } - },{ - name: 'native client - simple query', - run: function(next) { - var query = nativeClient.query('SELECT name FROM person WHERE age > $1', [10]); - query.on('end', function() { - next(); - }); - } - }, { - name: 'javascript client - parameterized query', - run: function(next) { - var query = client.query('SELECT name, age FROM person WHERE age > $1', [10]); - query.on('end', function() { - next(); - }); - } - },{ - name: 'native client - parameterized query', - run: function(next) { - var query = nativeClient.query('SELECT name, age FROM person WHERE age > $1', [10]); - query.on('end', function() { - next(); - }); - } - }] - }); - bench(function(result) { - console.log(); - console.log("%s (%d repeats):", result.name, result.repeat) - result.actions.forEach(function(action) { - console.log(" %s: \n average: %d ms\n total: %d ms", action.name, round(action.meanTime), round(action.totalTime)); - }) - client.end(); - nativeClient.end(); - }) -} - -var client = new pg.Client(conString); -var nativeClient = new pgNative.Client(conString); -client.connect(); -client.on('connect', function() { - nativeClient.connect(); - nativeClient.on('connect', function() { - doBenchmark(); - }); -}); diff --git a/benchmark/large-datatset-bench.js b/benchmark/large-datatset-bench.js deleted file mode 100644 index a5e0346aa..000000000 --- a/benchmark/large-datatset-bench.js +++ /dev/null @@ -1,125 +0,0 @@ -var pg = require(__dirname + '/../lib') -var bencher = require('bencher'); -var helper = require(__dirname + '/../test/test-helper') -var conString = helper.connectionString() - -var round = function(num) { - return Math.round((num*1000))/1000 -} - -var doBenchmark = function(cb) { - var bench = bencher({ - name: 'select large sets', - repeat: 10, - actions: [{ - 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(); - }); - } - }, { - 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(); - }) - } - }, { - 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(); - }) - } - }, { - name: 'selecting row', - run: function(next) { - var query = client.query('SELECT * FROM items'); - query.on('end', function() { - next(); - }) - } - }, { - name: 'loading all rows into memory', - run: function(next) { - var query = client.query('SELECT * FROM items', 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(); - cb(); - }) -} - - -var client = new pg.Client(conString); -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('done with insert. executing pure-javascript benchmark.'); - doBenchmark(function() { - var oldclient = client; - client = new pg.native.Client(conString); - client.on('error', function(err) { - console.log(err); - throw err; - }); - - client.connect(); - client.connect(); - console.log(); - console.log("creating temp table"); - client.query("CREATE TEMP TABLE items(name VARCHAR(10), created TIMESTAMPTZ, count INTEGER)"); - var count = 10000; - console.log("inserting %d rows", count); - for(var i = 0; i < count; i++) { - var query = { - name: 'insert', - text: "INSERT INTO items(name, created, count) VALUES($1, $2, $3)", - values: ["item"+i, new Date(2010, 01, 01, i, 0, 0), i] - }; - client.query(query); - } - client.once('drain', function() { - console.log("executing native benchmark"); - doBenchmark(function() { - console.log("all done"); - }) - }) - }); -}); diff --git a/benchmark/simple-query-bench.js b/benchmark/simple-query-bench.js deleted file mode 100644 index 466015897..000000000 --- a/benchmark/simple-query-bench.js +++ /dev/null @@ -1,58 +0,0 @@ -var pg = require(__dirname + '/../lib') -var bencher = require('bencher'); -var helper = require(__dirname + '/../test/test-helper') -var conString = helper.connectionString() - -var round = function(num) { - return Math.round((num*1000))/1000 -} - -var doBenchmark = function() { - var bench = bencher({ - name: 'query compare', - repeat: 1000, - actions: [{ - name: 'simple query', - run: function(next) { - var query = client.query('SELECT name FROM person WHERE age > 10'); - query.on('end', function() { - next(); - }); - } - },{ - name: 'unnamed prepared statement', - run: function(next) { - var query = client.query('SELECT name FROM person WHERE age > $1', [10]); - query.on('end', function() { - next(); - }); - } - },{ - name: 'named prepared statement', - run: function(next) { - var config = { - name: 'get peeps', - text: 'SELECT name FROM person WHERE age > $1', - values: [10] - } - client.query(config).on('end', function() { - next(); - }); - } - }] - }); - bench(function(result) { - console.log(); - console.log("%s (%d repeats):", result.name, result.repeat) - result.actions.forEach(function(action) { - console.log(" %s: \n average: %d ms\n total: %d ms", action.name, round(action.meanTime), round(action.totalTime)); - }) - client.end(); - }) -} - - - -var client = new pg.Client(conString); -client.connect(); -client.connection.once('readyForQuery', doBenchmark) diff --git a/binding.gyp b/binding.gyp deleted file mode 100644 index d89faaea0..000000000 --- a/binding.gyp +++ /dev/null @@ -1,31 +0,0 @@ -{ - 'targets': [ - { - 'target_name': 'binding', - 'sources': [ - 'src/binding.cc' - ], - 'conditions' : [ - ['OS=="mac"', { - 'include_dirs': ['= this.source.length; -}; -ArrayParser.prototype.nextChar = function() { - var c; - if ((c = this.source[this.pos++]) === "\\") { - return { - char: this.source[this.pos++], - escaped: true - }; - } else { - return { - char: c, - escaped: false - }; - } -}; -ArrayParser.prototype.record = function(char) { - return this.recorded.push(char); -}; -ArrayParser.prototype.newEntry = function(includeEmpty) { - var entry; - if (this.recorded.length > 0 || includeEmpty) { - entry = this.recorded.join(""); - if (entry === "NULL" && !includeEmpty) { - entry = null; - } - if (entry !== null) { - entry = this.converter(entry); - } - this.entries.push(entry); - this.recorded = []; - } -}; -ArrayParser.prototype.parse = function(nested) { - var c, p, quote; - if (nested == null) { - nested = false; - } - quote = false; - while (!this.eof()) { - c = this.nextChar(); - if (c.char === "{" && !quote) { - this.dimension++; - if (this.dimension > 1) { - p = new ArrayParser(this.source.substr(this.pos - 1), this.converter); - this.entries.push(p.parse(true)); - this.pos += p.pos - 2; - } - } else if (c.char === "}" && !quote) { - this.dimension--; - if (this.dimension === 0) { - this.newEntry(); - if (nested) { - return this.entries; - } - } - } else if (c.char === '"' && !c.escaped) { - if (quote) { - this.newEntry(true); - } - quote = !quote; - } else if (c.char === ',' && !quote) { - this.newEntry(); - } else { - this.record(c.char); - } - } - if (this.dimension !== 0) { - throw "array dimension not balanced"; - } - return this.entries; -}; - -module.exports = { - create: function(source, converter){ - return new ArrayParser(source, converter); - } -} diff --git a/lib/binaryParsers.js b/lib/binaryParsers.js deleted file mode 100644 index 9333a972e..000000000 --- a/lib/binaryParsers.js +++ /dev/null @@ -1,258 +0,0 @@ -var parseBits = function(data, bits, offset, invert, callback) { - offset = offset || 0; - invert = invert || false; - callback = callback || function(lastValue, newValue, bits) { return (lastValue * Math.pow(2, bits)) + newValue; }; - var offsetBytes = offset >> 3; - - var inv = function(value) { - if (invert) { - return ~value & 0xff; - } - - return value; - }; - - // read first (maybe partial) byte - var mask = 0xff; - var firstBits = 8 - (offset % 8); - if (bits < firstBits) { - mask = (0xff << (8 - bits)) & 0xff; - firstBits = bits; - } - - if (offset) { - mask = mask >> (offset % 8); - } - - var result = 0; - if ((offset % 8) + bits >= 8) { - result = callback(0, inv(data[offsetBytes]) & mask, firstBits); - } - - // read bytes - var bytes = (bits + offset) >> 3; - for (var i = offsetBytes + 1; i < bytes; i++) { - result = callback(result, inv(data[i]), 8); - } - - // bits to read, that are not a complete byte - var lastBits = (bits + offset) % 8; - if (lastBits > 0) { - result = callback(result, inv(data[bytes]) >> (8 - lastBits), lastBits); - } - - return result; -}; - -var parseFloatFromBits = function(data, precisionBits, exponentBits) { - var bias = Math.pow(2, exponentBits - 1) - 1; - var sign = parseBits(data, 1); - var exponent = parseBits(data, exponentBits, 1); - - if (exponent === 0) - return 0; - - // parse mantissa - var precisionBitsCounter = 1; - var parsePrecisionBits = function(lastValue, newValue, bits) { - if (lastValue === 0) { - lastValue = 1; - } - - for (var i = 1; i <= bits; i++) { - precisionBitsCounter /= 2; - if ((newValue & (0x1 << (bits - i))) > 0) { - lastValue += precisionBitsCounter; - } - } - - return lastValue; - }; - - var mantissa = parseBits(data, precisionBits, exponentBits + 1, false, parsePrecisionBits); - - // special cases - if (exponent == (Math.pow(2, exponentBits + 1) - 1)) { - if (mantissa === 0) { - return (sign === 0) ? Infinity : -Infinity; - } - - return NaN; - } - - // normale number - return ((sign === 0) ? 1 : -1) * Math.pow(2, exponent - bias) * mantissa; -}; - -var parseBool = function(value) { - return (parseBits(value, 8) == 1); -}; - -var parseInt16 = function(value) { - if (parseBits(value, 1) == 1) { - return -1 * (parseBits(value, 15, 1, true) + 1); - } - - return parseBits(value, 15, 1); -}; - -var parseInt32 = function(value) { - if (parseBits(value, 1) == 1) { - return -1 * (parseBits(value, 31, 1, true) + 1); - } - - return parseBits(value, 31, 1); -}; - -var parseInt64 = function(value) { - if (parseBits(value, 1) == 1) { - return -1 * (parseBits(value, 63, 1, true) + 1); - } - - return parseBits(value, 63, 1); -}; - -var parseFloat32 = function(value) { - return parseFloatFromBits(value, 23, 8); -}; - -var parseFloat64 = function(value) { - return parseFloatFromBits(value, 52, 11); -}; - -var parseNumeric = function(value) { - var sign = parseBits(value, 16, 32); - if (sign == 0xc000) { - return NaN; - } - - var weight = Math.pow(10000, parseBits(value, 16, 16)); - var result = 0; - - var digits = []; - var ndigits = parseBits(value, 16); - for (var i = 0; i < ndigits; i++) { - result += parseBits(value, 16, 64 + (16 * i)) * weight; - weight /= 10000; - } - - var scale = Math.pow(10, parseBits(value, 16, 48)); - return ((sign === 0) ? 1 : -1) * Math.round(result * scale) / scale; -}; - -var parseDate = function(value) { - var sign = parseBits(value, 1); - var rawValue = parseBits(value, 63, 1); - - // discard usecs and shift from 2000 to 1970 - var result = new Date((((sign === 0) ? 1 : -1) * rawValue / 1000) + 946684800000); - - // add microseconds to the date - result.usec = rawValue % 1000; - result.getMicroSeconds = function() { - return this.usec; - }; - result.setMicroSeconds = function(value) { - this.usec = value; - }; - result.getUTCMicroSeconds = function() { - return this.usec; - }; - - return result; -}; - -var parseArray = function(value) { - var dim = parseBits(value, 32); - - var flags = parseBits(value, 32, 32); - var elementType = parseBits(value, 32, 64); - - var offset = 96; - var dims = []; - for (var i = 0; i < dim; i++) { - // parse dimension - dims[i] = parseBits(value, 32, offset); - offset += 32; - - // ignore lower bounds - offset += 32; - } - - var parseElement = function(elementType) { - // parse content length - var length = parseBits(value, 32, offset); - offset += 32; - - // parse null values - if (length == 0xffffffff) { - return null; - } - - if ((elementType == 0x17) || (elementType == 0x14)) { - // int/bigint - var result = parseBits(value, length * 8, offset); - offset += length * 8; - return result; - } - else if (elementType == 0x19) { - // string - var result = value.toString(this.encoding, offset >> 3, (offset += (length << 3)) >> 3); - return result; - } - else { - console.log("ERROR: ElementType not implemented: " + elementType); - } - }; - - var parse = function(dimension, elementType) { - var array = []; - - if (dimension.length > 1) { - var count = dimension.shift(); - for (var i = 0; i < count; i++) { - array[i] = parse(dimension, elementType); - } - dimension.unshift(count); - } - else { - for (var i = 0; i < dimension[0]; i++) { - array[i] = parseElement(elementType); - } - } - - return array; - }; - - return parse(dims, elementType); -}; - -var parseText = function(value) { - return value.toString('utf8'); -}; - -var parseBool = function(value) { - return (parseBits(value, 8) > 0); -}; - -var init = function(register) { - register(20, parseInt64); - register(21, parseInt16); - register(23, parseInt32); - register(26, parseInt32); - register(1700, parseNumeric); - register(700, parseFloat32); - register(701, parseFloat64); - register(16, parseBool); - register(1114, parseDate); - register(1184, parseDate); - register(1007, parseArray); - register(1016, parseArray); - register(1008, parseArray); - register(1009, parseArray); - register(25, parseText); -}; - -module.exports = { - init: init -}; diff --git a/lib/client.js b/lib/client.js index fa93922e2..1bb27ea22 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1,42 +1,56 @@ +/** + * Copyright (c) 2010-2016 Brian Carlson (brian.m.carlson@gmail.com) + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * README.md file in the root directory of this source tree. + */ + var crypto = require('crypto'); var EventEmitter = require('events').EventEmitter; var util = require('util'); +var pgPass = require('pgpass'); +var TypeOverrides = require('./type-overrides'); -var Query = require(__dirname + '/query'); -var utils = require(__dirname + '/utils'); -var defaults = require(__dirname + '/defaults'); -var Connection = require(__dirname + '/connection'); +var ConnectionParameters = require('./connection-parameters'); +var Query = require('./query'); +var defaults = require('./defaults'); +var Connection = require('./connection'); var Client = function(config) { EventEmitter.call(this); - if(typeof config === 'string') { - config = utils.normalizeConnectionInfo(config) - } - config = config || {}; - this.user = config.user || defaults.user; - this.database = config.database || defaults.database; - this.port = config.port || defaults.port; - this.host = config.host || defaults.host; - this.connection = config.connection || new Connection({ - stream: config.stream, - ssl: config.ssl + + this.connectionParameters = new ConnectionParameters(config); + this.user = this.connectionParameters.user; + this.database = this.connectionParameters.database; + this.port = this.connectionParameters.port; + this.host = this.connectionParameters.host; + this.password = this.connectionParameters.password; + this.replication = this.connectionParameters.replication; + + var c = config || {}; + + this._types = new TypeOverrides(c.types); + + this.connection = c.connection || new Connection({ + stream: c.stream, + ssl: this.connectionParameters.ssl, + keepAlive: c.keepAlive || false }); this.queryQueue = []; - this.password = config.password || defaults.password; - this.binary = config.binary || defaults.binary; + this.binary = c.binary || defaults.binary; this.encoding = 'utf8'; this.processID = null; this.secretKey = null; - this.ssl = config.ssl || false; + this.ssl = this.connectionParameters.ssl || false; }; util.inherits(Client, EventEmitter); -var p = Client.prototype; - -p.connect = function(callback) { +Client.prototype.connect = function(callback) { var self = this; var con = this.connection; + if(this.host && this.host.indexOf('/') === 0) { con.connect(this.host + '/.s.PGSQL.' + this.port); } else { @@ -46,34 +60,44 @@ p.connect = function(callback) { //once connection is established send startup message con.on('connect', function() { - if (self.ssl) { + if(self.ssl) { con.requestSsl(); } else { - con.startup({ - user: self.user, - database: self.database - }); + con.startup(self.getStartupConf()); } }); + con.on('sslconnect', function() { - con.startup({ - user: self.user, - database: self.database - }); + con.startup(self.getStartupConf()); }); + function checkPgPass(cb) { + return function(msg) { + if (null !== self.password) { + cb(msg); + } else { + pgPass(self.connectionParameters, function(pass){ + if (undefined !== pass) { + self.connectionParameters.password = self.password = pass; + } + cb(msg); + }); + } + }; + } + //password request handling - con.on('authenticationCleartextPassword', function() { + con.on('authenticationCleartextPassword', checkPgPass(function() { con.password(self.password); - }); + })); //password request handling - con.on('authenticationMD5Password', function(msg) { + con.on('authenticationMD5Password', checkPgPass(function(msg) { var inner = Client.md5(self.password + self.user); - var outer = Client.md5(inner + msg.salt.toString('binary')); + var outer = Client.md5(Buffer.concat([new Buffer(inner), msg.salt])); var md5password = "md5" + outer; con.password(md5password); - }); + })); con.once('backendKeyData', function(msg) { self.processID = msg.processID; @@ -83,76 +107,131 @@ p.connect = function(callback) { //hook up query handling events to connection //after the connection initially becomes ready for queries con.once('readyForQuery', function() { - //delegate row descript to active query + + //delegate rowDescription to active query con.on('rowDescription', function(msg) { self.activeQuery.handleRowDescription(msg); }); - //delegate datarow to active query + + //delegate dataRow to active query con.on('dataRow', function(msg) { self.activeQuery.handleDataRow(msg); }); - //TODO should query gain access to connection? + + //delegate portalSuspended to active query con.on('portalSuspended', function(msg) { - self.activeQuery.getRows(con); + self.activeQuery.handlePortalSuspended(con); + }); + + //deletagate emptyQuery to active query + con.on('emptyQuery', function(msg) { + self.activeQuery.handleEmptyQuery(con); }); + //delegate commandComplete to active query con.on('commandComplete', function(msg) { - //delegate command complete to query - self.activeQuery.handleCommandComplete(msg); - //need to sync after each command complete of a prepared statement - if(self.activeQuery.isPreparedStatement) { - con.sync(); + self.activeQuery.handleCommandComplete(msg, con); + }); + + //if a prepared statement has a name and properly parses + //we track that its already been executed so we don't parse + //it again on the same client + con.on('parseComplete', function(msg) { + if(self.activeQuery.name) { + con.parsedStatements[self.activeQuery.name] = true; } }); - if (!callback) { - self.emit('connect'); - } else { - callback(null,self); - //remove callback for proper error handling after the connect event - callback = null; - } + con.on('copyInResponse', function(msg) { + self.activeQuery.handleCopyInResponse(self.connection); + }); + + con.on('copyData', function (msg) { + self.activeQuery.handleCopyData(msg, self.connection); + }); con.on('notification', function(msg) { self.emit('notification', msg); }); + //process possible callback argument to Client#connect + if (callback) { + callback(null, self); + //remove callback for proper error handling + //after the connect event + callback = null; + } + self.emit('connect'); }); con.on('readyForQuery', function() { - if(self.activeQuery) { - self.activeQuery.handleReadyForQuery(); - } + var activeQuery = self.activeQuery; self.activeQuery = null; self.readyForQuery = true; self._pulseQueryQueue(); + if(activeQuery) { + activeQuery.handleReadyForQuery(con); + } }); con.on('error', function(error) { - if(!self.activeQuery) { - if(!callback) { - self.emit('error', error); - } else { - callback(error); - } - } else { - //need to sync after error during a prepared statement - if(self.activeQuery.isPreparedStatement) { - con.sync(); - } - self.activeQuery.handleError(error); + if(self.activeQuery) { + var activeQuery = self.activeQuery; + self.activeQuery = null; + return activeQuery.handleError(error, con); + } + if(!callback) { + return self.emit('error', error); + } + con.end(); // make sure ECONNRESET errors don't cause error events + callback(error); + callback = null; + }); + + con.once('end', function() { + if ( callback ) { + // haven't received a connection message yet ! + var err = new Error('Connection terminated'); + callback(err); + callback = null; + return; + } + if(self.activeQuery) { + var disconnectError = new Error('Connection terminated'); + self.activeQuery.handleError(disconnectError, con); self.activeQuery = null; } + self.emit('end'); }); + con.on('notice', function(msg) { self.emit('notice', msg); }); }; -p.cancel = function(client, query) { - if (client.activeQuery == query) { +Client.prototype.getStartupConf = function() { + var params = this.connectionParameters; + + var data = { + user: params.user, + database: params.database + }; + + var appName = params.application_name || params.fallback_application_name; + if (appName) { + data.application_name = appName; + } + if (params.replication) { + data.replication = '' + params.replication; + } + + return data; +}; + +Client.prototype.cancel = function(client, query) { + if(client.activeQuery == query) { var con = this.connection; if(this.host && this.host.indexOf('/') === 0) { @@ -165,12 +244,66 @@ p.cancel = function(client, query) { con.on('connect', function() { con.cancel(client.processID, client.secretKey); }); - } - else if (client.queryQueue.indexOf(query) != -1) + } else if(client.queryQueue.indexOf(query) != -1) { client.queryQueue.splice(client.queryQueue.indexOf(query), 1); + } +}; + +Client.prototype.setTypeParser = function(oid, format, parseFn) { + return this._types.setTypeParser(oid, format, parseFn); +}; + +Client.prototype.getTypeParser = function(oid, format) { + return this._types.getTypeParser(oid, format); +}; + +// Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c +Client.prototype.escapeIdentifier = function(str) { + + var escaped = '"'; + + for(var i = 0; i < str.length; i++) { + var c = str[i]; + if(c === '"') { + escaped += c + c; + } else { + escaped += c; + } + } + + escaped += '"'; + + return escaped; +}; + +// Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c +Client.prototype.escapeLiteral = function(str) { + + var hasBackslash = false; + var escaped = '\''; + + for(var i = 0; i < str.length; i++) { + var c = str[i]; + if(c === '\'') { + escaped += c + c; + } else if (c === '\\') { + escaped += c + c; + hasBackslash = true; + } else { + escaped += c; + } + } + + escaped += '\''; + + if(hasBackslash === true) { + escaped = ' E' + escaped; + } + + return escaped; }; -p._pulseQueryQueue = function() { +Client.prototype._pulseQueryQueue = function() { if(this.readyForQuery===true) { this.activeQuery = this.queryQueue.shift(); if(this.activeQuery) { @@ -179,53 +312,47 @@ p._pulseQueryQueue = function() { this.activeQuery.submit(this.connection); } else if(this.hasExecuted) { this.activeQuery = null; - this._drainPaused > 0 ? this._drainPaused++ : this.emit('drain') + this.emit('drain'); } } }; -p.query = function(config, values, callback) { - //can take in strings or config objects - config = (typeof(config) == 'string') ? { text: config } : config; - if (this.binary && !('binary' in config)) { - config.binary = true; - } +Client.prototype.copyFrom = function (text) { + throw new Error("For PostgreSQL COPY TO/COPY FROM support npm install pg-copy-streams"); +}; - if(values) { - if(typeof values === 'function') { - callback = values; - } else { - config.values = values; - } - } +Client.prototype.copyTo = function (text) { + throw new Error("For PostgreSQL COPY TO/COPY FROM support npm install pg-copy-streams"); +}; - config.callback = callback; +Client.prototype.query = function(config, values, callback) { + //can take in strings, config object or query object + var query = (typeof config.submit == 'function') ? config : + new Query(config, values, callback); + if(this.binary && !query.binary) { + query.binary = true; + } + if(query._result) { + query._result._getTypeParser = this._types.getTypeParser.bind(this._types); + } - var query = new Query(config); this.queryQueue.push(query); this._pulseQueryQueue(); return query; }; -//prevents client from otherwise emitting 'drain' event until 'resumeDrain' is called -p.pauseDrain = function() { - this._drainPaused = 1; -}; - -//resume raising 'drain' event -p.resumeDrain = function() { - if(this._drainPaused > 1) { - this.emit('drain'); - } - this._drainPaused = 0; -}; - -p.end = function() { +Client.prototype.end = function(cb) { this.connection.end(); + if (cb) { + this.connection.once('end', cb); + } }; Client.md5 = function(string) { - return crypto.createHash('md5').update(string).digest('hex'); + return crypto.createHash('md5').update(string, 'utf-8').digest('hex'); }; +// expose a Query constructor +Client.Query = Query; + module.exports = Client; diff --git a/lib/connection-parameters.js b/lib/connection-parameters.js new file mode 100644 index 000000000..c1c535e98 --- /dev/null +++ b/lib/connection-parameters.js @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2010-2016 Brian Carlson (brian.m.carlson@gmail.com) + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * README.md file in the root directory of this source tree. + */ + +var url = require('url'); +var dns = require('dns'); + +var defaults = require('./defaults'); + +var val = function(key, config, envVar) { + if (envVar === undefined) { + envVar = process.env[ 'PG' + key.toUpperCase() ]; + } else if (envVar === false) { + // do nothing ... use false + } else { + envVar = process.env[ envVar ]; + } + + return config[key] || + envVar || + defaults[key]; +}; + +//parses a connection string +var parse = require('pg-connection-string').parse; + +var useSsl = function() { + switch(process.env.PGSSLMODE) { + case "disable": + return false; + case "prefer": + case "require": + case "verify-ca": + case "verify-full": + return true; + } + return defaults.ssl; +}; + +var ConnectionParameters = function(config) { + //if a string is passed, it is a raw connection string so we parse it into a config + config = typeof config == 'string' ? parse(config) : (config || {}); + //if the config has a connectionString defined, parse IT into the config we use + //this will override other default values with what is stored in connectionString + if(config.connectionString) { + config = parse(config.connectionString); + } + this.user = val('user', config); + this.database = val('database', config); + this.port = parseInt(val('port', config), 10); + this.host = val('host', config); + this.password = val('password', config); + this.binary = val('binary', config); + this.ssl = typeof config.ssl === 'undefined' ? useSsl() : config.ssl; + this.client_encoding = val("client_encoding", config); + this.replication = val("replication", config); + //a domain socket begins with '/' + this.isDomainSocket = (!(this.host||'').indexOf('/')); + + this.application_name = val('application_name', config, 'PGAPPNAME'); + this.fallback_application_name = val('fallback_application_name', config, false); +}; + +var add = function(params, config, paramName) { + var value = config[paramName]; + if(value) { + params.push(paramName+"='"+value+"'"); + } +}; + +ConnectionParameters.prototype.getLibpqConnectionString = function(cb) { + var params = []; + add(params, this, 'user'); + add(params, this, 'password'); + add(params, this, 'port'); + add(params, this, 'application_name'); + add(params, this, 'fallback_application_name'); + + var ssl = typeof this.ssl === 'object' ? this.ssl : {sslmode: this.ssl}; + add(params, ssl, 'sslmode'); + add(params, ssl, 'sslca'); + add(params, ssl, 'sslkey'); + add(params, ssl, 'sslcert'); + + if(this.database) { + params.push("dbname='" + this.database + "'"); + } + if(this.replication) { + params.push("replication='" + this.replication + "'"); + } + if(this.host) { + params.push("host=" + this.host); + } + if(this.isDomainSocket) { + return cb(null, params.join(' ')); + } + if(this.client_encoding) { + params.push("client_encoding='" + this.client_encoding + "'"); + } + dns.lookup(this.host, function(err, address) { + if(err) return cb(err, null); + params.push("hostaddr=" + address); + return cb(null, params.join(' ')); + }); +}; + +module.exports = ConnectionParameters; diff --git a/lib/connection.js b/lib/connection.js index 4a2a8575a..7318287c2 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -1,15 +1,40 @@ +/** + * Copyright (c) 2010-2016 Brian Carlson (brian.m.carlson@gmail.com) + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * README.md file in the root directory of this source tree. + */ + var net = require('net'); -var crypto = require('crypto'); var EventEmitter = require('events').EventEmitter; var util = require('util'); -var utils = require(__dirname + '/utils'); -var Writer = require(__dirname + '/writer'); +var Writer = require('buffer-writer'); +var Reader = require('packet-reader'); + +var indexOf = + 'indexOf' in Buffer.prototype ? + function indexOf(buffer, value, start) { + return buffer.indexOf(value, start); + } : + function indexOf(buffer, value, start) { + for (var i = start, len = buffer.length; i < len; i++) { + if (buffer[i] === value) { + return i; + } + } + + return -1; + }; +var TEXT_MODE = 0; +var BINARY_MODE = 1; var Connection = function(config) { EventEmitter.call(this); config = config || {}; this.stream = config.stream || new net.Stream(); + this._keepAlive = config.keepAlive; this.lastBuffer = false; this.lastOffset = 0; this.buffer = null; @@ -18,101 +43,135 @@ var Connection = function(config) { this.parsedStatements = {}; this.writer = new Writer(); this.ssl = config.ssl || false; + this._ending = false; + this._mode = TEXT_MODE; + this._emitMessage = false; + this._reader = new Reader({ + headerSize: 1, + lengthPadding: -4 + }); + var self = this; + this.on('newListener', function(eventName) { + if(eventName == 'message') { + self._emitMessage = true; + } + }); }; util.inherits(Connection, EventEmitter); -var p = Connection.prototype; - -p.connect = function(port, host) { +Connection.prototype.connect = function(port, host) { - if (this.stream.readyState === 'closed') { + if(this.stream.readyState === 'closed') { this.stream.connect(port, host); - } else if (this.stream.readyState == 'open') { + } else if(this.stream.readyState == 'open') { this.emit('connect'); } var self = this; this.stream.on('connect', function() { + if (self._keepAlive) { + self.stream.setKeepAlive(true); + } self.emit('connect'); }); - + this.stream.on('error', function(error) { + //don't raise ECONNRESET errors - they can & should be ignored + //during disconnect + if(self._ending && error.code == 'ECONNRESET') { + return; + } self.emit('error', error); }); - if(this.ssl) { - this.stream.once('data', function(buffer) { - self.setBuffer(buffer); - var msg = self.readSslResponse(); - self.emit('message', msg); - self.emit(msg.name, msg); - }); - this.once('sslresponse', function(msg) { - if (msg.text == 0x53) { - var tls = require('tls'); - self.stream.removeAllListeners(); - self.stream = tls.connect({ - socket: self.stream, - servername: host, - rejectUnauthorized: ssl.rejectUnauthorized, - ca: ssl.ca, - pfx: ssl.pfx, - key: ssl.key, - passphrase: ssl.passphrase, - cert: ssl.cert, - NPNProtocols: ssl.NPNProtocols - }); - self.attachListeners(self.stream); - self.emit('sslconnect'); - } else { - self.emit('error', new Error("The server doesn't support SSL/TLS connections.")); - } + this.stream.on('close', function() { + // NOTE: node-0.10 emits both 'end' and 'close' + // for streams closed by the peer, while + // node-0.8 only emits 'close' + self.emit('end'); }); - } else { - this.attachListeners(this.stream); + if(!this.ssl) { + return this.attachListeners(this.stream); } + + this.stream.once('data', function(buffer) { + var responseCode = buffer.toString('utf8'); + if(responseCode != 'S') { + return self.emit('error', new Error('The server does not support SSL connections')); + } + var tls = require('tls'); + self.stream = tls.connect({ + socket: self.stream, + servername: host, + rejectUnauthorized: self.ssl.rejectUnauthorized, + ca: self.ssl.ca, + pfx: self.ssl.pfx, + key: self.ssl.key, + passphrase: self.ssl.passphrase, + cert: self.ssl.cert, + NPNProtocols: self.ssl.NPNProtocols + }); + self.attachListeners(self.stream); + self.emit('sslconnect'); + + self.stream.on('error', function(error){ + self.emit('error', error); + }); + }); }; -p.attachListeners = function(stream) { +Connection.prototype.attachListeners = function(stream) { var self = this; - stream.on('data', function(buffer) { - self.setBuffer(buffer); - var msg; - while(msg = self.parseMessage()) { - self.emit('message', msg); + stream.on('data', function(buff) { + self._reader.addChunk(buff); + var packet = self._reader.read(); + while(packet) { + var msg = self.parseMessage(packet); + if(self._emitMessage) { + self.emit('message', msg); + } self.emit(msg.name, msg); + packet = self._reader.read(); } }); + stream.on('end', function() { + self.emit('end'); + }); }; -p.requestSsl = function(config) { +Connection.prototype.requestSsl = function() { this.checkSslResponse = true; - + var bodyBuffer = this.writer .addInt16(0x04D2) .addInt16(0x162F).flush(); - + var length = bodyBuffer.length + 4; - + var buffer = new Writer() .addInt32(length) .add(bodyBuffer) .join(); this.stream.write(buffer); -} +}; -p.startup = function(config) { - var bodyBuffer = this.writer +Connection.prototype.startup = function(config) { + var writer = this.writer .addInt16(3) .addInt16(0) - .addCString('user') - .addCString(config.user) - .addCString('database') - .addCString(config.database) - .addCString('').flush(); + ; + + Object.keys(config).forEach(function(key){ + var val = config[key]; + writer.addCString(key).addCString(val); + }); + + writer.addCString('client_encoding').addCString("'utf-8'"); + + var bodyBuffer = writer.addCString('').flush(); //this message is sent without a code var length = bodyBuffer.length + 4; @@ -124,13 +183,13 @@ p.startup = function(config) { this.stream.write(buffer); }; -p.cancel = function(processID, secretKey) { +Connection.prototype.cancel = function(processID, secretKey) { var bodyBuffer = this.writer .addInt16(1234) .addInt16(5678) .addInt32(processID) .addInt32(secretKey) - .addCString('').flush(); + .flush(); var length = bodyBuffer.length + 4; @@ -141,28 +200,28 @@ p.cancel = function(processID, secretKey) { this.stream.write(buffer); }; -p.password = function(password) { +Connection.prototype.password = function(password) { //0x70 = 'p' this._send(0x70, this.writer.addCString(password)); }; -p._send = function(code, more) { - if(!this.stream.writable) return false; +Connection.prototype._send = function(code, more) { + if(!this.stream.writable) { return false; } if(more === true) { this.writer.addHeader(code); } else { return this.stream.write(this.writer.flush(code)); } -} +}; -p.query = function(text) { +Connection.prototype.query = function(text) { //0x51 = Q this.stream.write(this.writer.addCString(text).flush(0x51)); }; //send parse message //"more" === true to buffer the message until flush() is called -p.parse = function(query, more) { +Connection.prototype.parse = function(query, more) { //expect something like this: // { name: 'queryName', // text: 'select * from blah', @@ -170,6 +229,11 @@ p.parse = function(query, more) { //normalize missing query names to allow for null query.name = query.name || ''; + if (query.name.length > 63) { + console.error('Warning! Postgres only supports 63 characters for query names.'); + console.error('You supplied', query.name, '(', query.name.length, ')'); + console.error('This can cause conflicts and silent errors executing queries'); + } //normalize null type array query.types = query.types || []; var len = query.types.length; @@ -187,7 +251,7 @@ p.parse = function(query, more) { //send bind message //"more" === true to buffer the message until flush() is called -p.bind = function(config, more) { +Connection.prototype.bind = function(config, more) { //normalize config config = config || {}; config.portal = config.portal || ''; @@ -195,22 +259,34 @@ p.bind = function(config, more) { config.binary = config.binary || false; var values = config.values || []; var len = values.length; + var useBinary = false; + for (var j = 0; j < len; j++) + useBinary |= values[j] instanceof Buffer; var buffer = this.writer .addCString(config.portal) - .addCString(config.statement) - .addInt16(0) //always use default text format - .addInt16(len); //number of parameters + .addCString(config.statement); + if (!useBinary) + buffer.addInt16(0); + else { + buffer.addInt16(len); + for (j = 0; j < len; j++) + buffer.addInt16(values[j] instanceof Buffer); + } + buffer.addInt16(len); for(var i = 0; i < len; i++) { var val = values[i]; if(val === null || typeof val === "undefined") { buffer.addInt32(-1); + } else if (val instanceof Buffer) { + buffer.addInt32(val.length); + buffer.add(val); } else { buffer.addInt32(Buffer.byteLength(val)); buffer.addString(val); } } - if (config.binary) { + if(config.binary) { buffer.addInt16(1); // format codes to use binary buffer.addInt16(1); } @@ -223,11 +299,11 @@ p.bind = function(config, more) { //send execute message //"more" === true to buffer the message until flush() is called -p.execute = function(config, more) { +Connection.prototype.execute = function(config, more) { config = config || {}; config.portal = config.portal || ''; config.rows = config.rows || ''; - var buffer = this.writer + this.writer .addCString(config.portal) .addInt32(config.rows); @@ -237,243 +313,266 @@ p.execute = function(config, more) { var emptyBuffer = Buffer(0); -p.flush = function() { +Connection.prototype.flush = function() { //0x48 = 'H' - this.writer.add(emptyBuffer) + this.writer.add(emptyBuffer); this._send(0x48); -} +}; -p.sync = function() { +Connection.prototype.sync = function() { //clear out any pending data in the writer - this.writer.flush(0) - + this.writer.flush(0); + this.writer.add(emptyBuffer); + this._ending = true; this._send(0x53); }; -p.end = function() { +Connection.prototype.end = function() { //0x58 = 'X' this.writer.add(emptyBuffer); + this._ending = true; this._send(0x58); }; -p.describe = function(msg, more) { +Connection.prototype.close = function(msg, more) { this.writer.addCString(msg.type + (msg.name || '')); - this._send(0x44, more); + this._send(0x43, more); }; -//parsing methods -p.setBuffer = function(buffer) { - if(this.lastBuffer) { //we have unfinished biznaz - //need to combine last two buffers - var remaining = this.lastBuffer.length - this.lastOffset; - var combinedBuffer = new Buffer(buffer.length + remaining); - this.lastBuffer.copy(combinedBuffer, 0, this.lastOffset); - buffer.copy(combinedBuffer, remaining, 0); - buffer = combinedBuffer; - } - this.buffer = buffer; - this.offset = 0; +Connection.prototype.describe = function(msg, more) { + this.writer.addCString(msg.type + (msg.name || '')); + this._send(0x44, more); }; -p.readSslResponse = function() { - var remaining = this.buffer.length - (this.offset); - if(remaining < 1) { - this.lastBuffer = this.buffer; - this.lastOffset = this.offset; - return false; - } - return { name: 'sslresponse', text: this.buffer[this.offset++] }; +Connection.prototype.sendCopyFromChunk = function (chunk) { + this.stream.write(this.writer.add(chunk).flush(0x64)); }; -p.parseMessage = function() { - var remaining = this.buffer.length - (this.offset); - if(remaining < 5) { - //cannot read id + length without at least 5 bytes - //just abort the read now - this.lastBuffer = this.buffer; - this.lastOffset = this.offset; - return false; - } +Connection.prototype.endCopyFrom = function () { + this.stream.write(this.writer.add(emptyBuffer).flush(0x63)); +}; - //read message id code - var id = this.buffer[this.offset++]; - //read message length - var length = this.parseInt32(); +Connection.prototype.sendCopyFail = function (msg) { + //this.stream.write(this.writer.add(emptyBuffer).flush(0x66)); + this.writer.addCString(msg); + this._send(0x66); +}; - if(remaining <= length) { - this.lastBuffer = this.buffer; - //rewind the last 5 bytes we read - this.lastOffset = this.offset-5; - return false; - } +var Message = function(name, length) { + this.name = name; + this.length = length; +}; - var msg = { - length: length - }; +Connection.prototype.parseMessage = function(buffer) { - switch(id) + this.offset = 0; + var length = buffer.length + 4; + switch(this._reader.header) { case 0x52: //R - msg.name = 'authenticationOk'; - return this.parseR(msg); + return this.parseR(buffer, length); case 0x53: //S - msg.name = 'parameterStatus'; - return this.parseS(msg); + return this.parseS(buffer, length); case 0x4b: //K - msg.name = 'backendKeyData'; - return this.parseK(msg); + return this.parseK(buffer, length); case 0x43: //C - msg.name = 'commandComplete'; - return this.parseC(msg); + return this.parseC(buffer, length); case 0x5a: //Z - msg.name = 'readyForQuery'; - return this.parseZ(msg); + return this.parseZ(buffer, length); case 0x54: //T - msg.name = 'rowDescription'; - return this.parseT(msg); + return this.parseT(buffer, length); case 0x44: //D - msg.name = 'dataRow'; - return this.parseD(msg); + return this.parseD(buffer, length); case 0x45: //E - msg.name = 'error'; - return this.parseE(msg); + return this.parseE(buffer, length); case 0x4e: //N - msg.name = 'notice'; - return this.parseN(msg); + return this.parseN(buffer, length); case 0x31: //1 - msg.name = 'parseComplete'; - return msg; + return new Message('parseComplete', length); case 0x32: //2 - msg.name = 'bindComplete'; - return msg; + return new Message('bindComplete', length); + + case 0x33: //3 + return new Message('closeComplete', length); case 0x41: //A - msg.name = 'notification'; - return this.parseA(msg); + return this.parseA(buffer, length); case 0x6e: //n - msg.name = 'noData'; - return msg; + return new Message('noData', length); case 0x49: //I - msg.name = 'emptyQuery'; - return msg; + return new Message('emptyQuery', length); case 0x73: //s - msg.name = 'portalSuspended'; - return msg; + return new Message('portalSuspended', length); + + case 0x47: //G + return this.parseG(buffer, length); + + case 0x48: //H + return this.parseH(buffer, length); - default: - throw new Error("Unrecognized message code " + id); + case 0x57: //W + return new Message('replicationStart', length); + + case 0x63: //c + return new Message('copyDone', length); + + case 0x64: //d + return this.parsed(buffer, length); } }; -p.parseR = function(msg) { +Connection.prototype.parseR = function(buffer, length) { var code = 0; + var msg = new Message('authenticationOk', length); if(msg.length === 8) { - code = this.parseInt32(); + code = this.parseInt32(buffer); if(code === 3) { msg.name = 'authenticationCleartextPassword'; } return msg; } if(msg.length === 12) { - code = this.parseInt32(); + code = this.parseInt32(buffer); if(code === 5) { //md5 required msg.name = 'authenticationMD5Password'; msg.salt = new Buffer(4); - this.buffer.copy(msg.salt, 0, this.offset, this.offset + 4); + buffer.copy(msg.salt, 0, this.offset, this.offset + 4); this.offset += 4; return msg; } } - throw new Error("Unknown authenticatinOk message type" + util.inspect(msg)); + throw new Error("Unknown authenticationOk message type" + util.inspect(msg)); }; -p.parseS = function(msg) { - msg.parameterName = this.parseCString(); - msg.parameterValue = this.parseCString(); +Connection.prototype.parseS = function(buffer, length) { + var msg = new Message('parameterStatus', length); + msg.parameterName = this.parseCString(buffer); + msg.parameterValue = this.parseCString(buffer); return msg; }; -p.parseK = function(msg) { - msg.processID = this.parseInt32(); - msg.secretKey = this.parseInt32(); +Connection.prototype.parseK = function(buffer, length) { + var msg = new Message('backendKeyData', length); + msg.processID = this.parseInt32(buffer); + msg.secretKey = this.parseInt32(buffer); return msg; }; -p.parseC = function(msg) { - msg.text = this.parseCString(); +Connection.prototype.parseC = function(buffer, length) { + var msg = new Message('commandComplete', length); + msg.text = this.parseCString(buffer); return msg; }; -p.parseZ = function(msg) { - msg.status = this.readChar(); +Connection.prototype.parseZ = function(buffer, length) { + var msg = new Message('readyForQuery', length); + msg.name = 'readyForQuery'; + msg.status = this.readString(buffer, 1); return msg; }; -p.parseT = function(msg) { - msg.fieldCount = this.parseInt16(); +var ROW_DESCRIPTION = 'rowDescription'; +Connection.prototype.parseT = function(buffer, length) { + var msg = new Message(ROW_DESCRIPTION, length); + msg.fieldCount = this.parseInt16(buffer); var fields = []; for(var i = 0; i < msg.fieldCount; i++){ - fields[i] = this.parseField(); + fields.push(this.parseField(buffer)); } msg.fields = fields; return msg; }; -p.parseField = function() { - var field = { - name: this.parseCString(), - tableID: this.parseInt32(), - columnID: this.parseInt16(), - dataTypeID: this.parseInt32(), - dataTypeSize: this.parseInt16(), - dataTypeModifier: this.parseInt32(), - format: this.parseInt16() === 0 ? 'text' : 'binary' - }; +var Field = function() { + this.name = null; + this.tableID = null; + this.columnID = null; + this.dataTypeID = null; + this.dataTypeSize = null; + this.dataTypeModifier = null; + this.format = null; +}; + +var FORMAT_TEXT = 'text'; +var FORMAT_BINARY = 'binary'; +Connection.prototype.parseField = function(buffer) { + var field = new Field(); + field.name = this.parseCString(buffer); + field.tableID = this.parseInt32(buffer); + field.columnID = this.parseInt16(buffer); + field.dataTypeID = this.parseInt32(buffer); + field.dataTypeSize = this.parseInt16(buffer); + field.dataTypeModifier = this.parseInt32(buffer); + if(this.parseInt16(buffer) === TEXT_MODE) { + this._mode = TEXT_MODE; + field.format = FORMAT_TEXT; + } else { + this._mode = BINARY_MODE; + field.format = FORMAT_BINARY; + } return field; }; -p.parseD = function(msg) { - var fieldCount = this.parseInt16(); - var fields = []; +var DATA_ROW = 'dataRow'; +var DataRowMessage = function(length, fieldCount) { + this.name = DATA_ROW; + this.length = length; + this.fieldCount = fieldCount; + this.fields = []; +}; + + +//extremely hot-path code +Connection.prototype.parseD = function(buffer, length) { + var fieldCount = this.parseInt16(buffer); + var msg = new DataRowMessage(length, fieldCount); for(var i = 0; i < fieldCount; i++) { - var length = this.parseInt32(); - fields[i] = (length === -1 ? null : this.readBytes(length)) - }; - msg.fieldCount = fieldCount; - msg.fields = fields; + msg.fields.push(this._readValue(buffer)); + } return msg; }; +//extremely hot-path code +Connection.prototype._readValue = function(buffer) { + var length = this.parseInt32(buffer); + if(length === -1) return null; + if(this._mode === TEXT_MODE) { + return this.readString(buffer, length); + } + return this.readBytes(buffer, length); +}; + //parses error -p.parseE = function(input) { +Connection.prototype.parseE = function(buffer, length) { var fields = {}; var msg, item; - var fieldType = this.readString(1); + var input = new Message('error', length); + var fieldType = this.readString(buffer, 1); while(fieldType != '\0') { - fields[fieldType] = this.parseCString(); - fieldType = this.readString(1); + fields[fieldType] = this.parseCString(buffer); + fieldType = this.readString(buffer, 1); } - if (input.name === 'error') { + if(input.name === 'error') { // the msg is an Error instance msg = new Error(fields.M); for (item in input) { // copy input properties to the error - if (input.hasOwnProperty(item)) { + if(input.hasOwnProperty(item)) { msg[item] = input[item]; } } @@ -490,6 +589,11 @@ p.parseE = function(input) { msg.internalPosition = fields.p; msg.internalQuery = fields.q; msg.where = fields.W; + msg.schema = fields.s; + msg.table = fields.t; + msg.column = fields.c; + msg.dataType = fields.d; + msg.constraint = fields.n; msg.file = fields.F; msg.line = fields.L; msg.routine = fields.R; @@ -497,52 +601,73 @@ p.parseE = function(input) { }; //same thing, different name -p.parseN = p.parseE; +Connection.prototype.parseN = function(buffer, length) { + var msg = this.parseE(buffer, length); + msg.name = 'notice'; + return msg; +}; -p.parseA = function(msg) { - msg.processId = this.parseInt32(); - msg.channel = this.parseCString(); - msg.payload = this.parseCString(); +Connection.prototype.parseA = function(buffer, length) { + var msg = new Message('notification', length); + msg.processId = this.parseInt32(buffer); + msg.channel = this.parseCString(buffer); + msg.payload = this.parseCString(buffer); return msg; }; -p.readChar = function() { - return Buffer([this.buffer[this.offset++]]).toString(this.encoding); +Connection.prototype.parseG = function (buffer, length) { + var msg = new Message('copyInResponse', length); + return this.parseGH(buffer, msg); }; -p.parseInt32 = function() { - var value = this.peekInt32(); - this.offset += 4; - return value; +Connection.prototype.parseH = function(buffer, length) { + var msg = new Message('copyOutResponse', length); + return this.parseGH(buffer, msg); +}; + +Connection.prototype.parseGH = function (buffer, msg) { + var isBinary = buffer[this.offset] !== 0; + this.offset++; + msg.binary = isBinary; + var columnCount = this.parseInt16(buffer); + msg.columnTypes = []; + for(var i = 0; i 1) { - this.emit('drain') - }; - this._drainPaused = 0; +//attempt to cancel an in-progress query +Client.prototype.cancel = function(query) { + if(this._activeQuery == query) { + this.native.cancel(function() {}); + } else if (this._queryQueue.indexOf(query) != -1) { + this._queryQueue.splice(this._queryQueue.indexOf(query), 1); + } }; -var clientBuilder = function(config) { - config = config || {}; - var connection = new Connection(); - connection._queryQueue = []; - connection._namedQueries = {}; - connection._activeQuery = null; - connection._config = utils.normalizeConnectionInfo(config); - //attach properties to normalize interface with pure js client - connection.user = connection._config.user; - connection.password = connection._config.password; - connection.database = connection._config.database; - connection.host = connection._config.host; - connection.port = connection._config.port; - connection.on('connect', function() { - connection._connected = true; - connection._pulseQueryQueue(true); - }); - - //proxy some events to active query - connection.on('_row', function(row) { - connection._activeQuery.handleRow(row); - }); - - connection.on('_cmdStatus', function(status) { - //set this here so we can pass it to the query - //when the query completes - connection._lastMeta = status; - }); - - //TODO: emit more native error properties (make it match js error) - connection.on('_error', function(err) { - //create Error object from object literal - var error = new Error(err.message || "Unknown native driver error"); - for(var key in err) { - error[key] = err[key]; - } - - //give up on trying to wait for named query prepare - this._namedQuery = false; - if(connection._activeQuery) { - connection._activeQuery.handleError(error); - } else { - connection.emit('error', error); - } - }); - - 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._lastMeta); - connection._activeQuery = null; - connection._pulseQueryQueue(); - } - }); - - return connection; +Client.prototype.setTypeParser = function(oid, format, parseFn) { + return this._types.setTypeParser(oid, format, parseFn); }; -module.exports = clientBuilder; +Client.prototype.getTypeParser = function(oid, format) { + return this._types.getTypeParser(oid, format); +}; diff --git a/lib/native/query.js b/lib/native/query.js index db94b29bd..af75d39b7 100644 --- a/lib/native/query.js +++ b/lib/native/query.js @@ -1,85 +1,138 @@ +/** + * Copyright (c) 2010-2016 Brian Carlson (brian.m.carlson@gmail.com) + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * README.md file in the root directory of this source tree. + */ + var EventEmitter = require('events').EventEmitter; var util = require('util'); +var utils = require('../utils'); +var NativeResult = require('./result'); -var types = require(__dirname + '/../types'); -var utils = require(__dirname + '/../utils'); -var Result = require(__dirname + '/../result'); - -//event emitter proxy -var NativeQuery = function(text, values, callback) { +var NativeQuery = module.exports = function(native) { EventEmitter.call(this); - + this.native = native; this.text = null; this.values = null; - this.callback = null; this.name = null; - - //allow 'config object' as first parameter - if(typeof text == 'object') { - this.text = text.text; - this.values = text.values; - this.name = text.name; - if(typeof values === 'function') { - this.callback = values; - } else if(values) { - this.values = values; - this.callback = callback; - } - } else { - this.text = text; - this.values = values; - this.callback = callback; - if(typeof values == 'function') { - this.values = null; - this.callback = values; - } - } - this.result = new Result(); - //normalize values - if(this.values) { - for(var i = 0, len = this.values.length; i < len; i++) { - this.values[i] = utils.prepareValue(this.values[i]); - } - } + this.callback = null; + this.state = 'new'; + this._arrayMode = false; + + //if the 'row' event is listened for + //then emit them as they come in + //without setting singleRowMode to true + //this has almost no meaning because libpq + //reads all rows into memory befor returning any + this._emitRowEvents = false; + this.on('newListener', function(event) { + if(event === 'row') this._emitRowEvents = true; + }.bind(this)); }; util.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.getTypeParser(item.type, 'text')(item.value); - } - return result; -} -p.handleRow = function(rowData) { - var row = mapRowData(rowData); - if(this.callback) { - this.result.addRow(row); - } - this.emit('row', row, this.result); +NativeQuery.prototype.then = function(onSuccess, onFailure) { + return this.promise().then(onSuccess, onFailure); +}; + +NativeQuery.prototype.catch = function(callback) { + return this.promise().catch(callback); +}; + +NativeQuery.prototype.promise = function() { + if (this._promise) return this._promise; + this._promise = new Promise(function(resolve, reject) { + this.once('end', resolve); + this.once('error', reject); + }.bind(this)); + return this._promise; }; -p.handleError = function(error) { - if(this.callback) { - this.callback(error); - this.callback = null; +NativeQuery.prototype.handleError = function(err) { + var self = this; + //copy pq error fields into the error object + var fields = self.native.pq.resultErrorFields(); + if(fields) { + for(var key in fields) { + err[key] = fields[key]; + } + } + if(self.callback) { + self.callback(err); } else { - this.emit('error', error); + self.emit('error', err); } -} + self.state = 'error'; +}; + +NativeQuery.prototype.submit = function(client) { + this.state = 'running'; + var self = this; + client.native.arrayMode = this._arrayMode; + + var after = function(err, rows) { + client.native.arrayMode = false; + setImmediate(function() { + self.emit('_done'); + }); + + //handle possible query error + if(err) { + return self.handleError(err); + } -p.handleReadyForQuery = function(meta) { - if(this.callback) { - this.result.command = meta.command.split(' ')[0]; - this.result.rowCount = parseInt(meta.value); - this.callback(null, this.result); + var result = new NativeResult(); + result.addCommandComplete(self.native.pq); + result.rows = rows; + + //emit row events for each row in the result + if(self._emitRowEvents) { + rows.forEach(function(row) { + self.emit('row', row, result); + }); + } + + + //handle successful result + self.state = 'end'; + self.emit('end', result); + if(self.callback) { + self.callback(null, result); + } + }; + + if(process.domain) { + after = process.domain.bind(after); } - this.emit('end'); -}; -module.exports = NativeQuery; + //named query + if(this.name) { + if (this.name.length > 63) { + console.error('Warning! Postgres only supports 63 characters for query names.'); + console.error('You supplied', this.name, '(', this.name.length, ')'); + console.error('This can cause conflicts and silent errors executing queries'); + } + var values = (this.values||[]).map(utils.prepareValue); + + //check if the client has already executed this named query + //if so...just execute it again - skip the planning phase + if(client.namedQueries[this.name]) { + return this.native.execute(this.name, values, after); + } + //plan the named query the first time, then execute it + return this.native.prepare(this.name, this.text, values.length, function(err) { + if(err) return after(err); + client.namedQueries[self.name] = true; + return self.native.execute(self.name, values, after); + }); + } + else if(this.values) { + var vals = this.values.map(utils.prepareValue); + this.native.query(this.text, vals, after); + } else { + this.native.query(this.text, after); + } +}; diff --git a/lib/native/result.js b/lib/native/result.js new file mode 100644 index 000000000..dc68f76c4 --- /dev/null +++ b/lib/native/result.js @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2016 Brian Carlson (brian.m.carlson@gmail.com) + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * README.md file in the root directory of this source tree. + */ + +var NativeResult = module.exports = function(pq) { + this.command = null; + this.rowCount = 0; + this.rows = null; + this.fields = null; +}; + +NativeResult.prototype.addCommandComplete = function(pq) { + this.command = pq.cmdStatus().split(' ')[0]; + this.rowCount = parseInt(pq.cmdTuples(), 10); + var nfields = pq.nfields(); + if(nfields < 1) return; + + this.fields = []; + for(var i = 0; i < nfields; i++) { + this.fields.push({ + name: pq.fname(i), + dataTypeID: pq.ftype(i) + }); + } +}; + +NativeResult.prototype.addRow = function(row) { + // This is empty to ensure pg code doesn't break when switching to pg-native + // pg-native loads all rows into the final result object by default. + // This is because libpg loads all rows into memory before passing the result + // to pg-native. +}; diff --git a/lib/pool-factory.js b/lib/pool-factory.js new file mode 100644 index 000000000..aa7bd0b19 --- /dev/null +++ b/lib/pool-factory.js @@ -0,0 +1,18 @@ +var Client = require('./client'); +var util = require('util'); +var Pool = require('pg-pool'); + +module.exports = function(Client) { + + var BoundPool = function(options) { + var config = { Client: Client }; + for (var key in options) { + config[key] = options[key]; + } + Pool.call(this, config); + }; + + util.inherits(BoundPool, Pool); + + return BoundPool; +}; diff --git a/lib/query.js b/lib/query.js index 3e0311a7b..f62786022 100644 --- a/lib/query.js +++ b/lib/query.js @@ -1,99 +1,149 @@ +/** + * Copyright (c) 2010-2016 Brian Carlson (brian.m.carlson@gmail.com) + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * README.md file in the root directory of this source tree. + */ + var EventEmitter = require('events').EventEmitter; var util = require('util'); -var Result = require(__dirname + '/result'); -var Types = require(__dirname + '/types'); -var utils = require(__dirname + '/utils'); +var Result = require('./result'); +var utils = require('./utils'); + +var Query = function(config, values, callback) { + // use of "new" optional + if(!(this instanceof Query)) { return new Query(config, values, callback); } + + config = utils.normalizeQueryConfig(config, values, callback); -var Query = function(config) { this.text = config.text; this.values = config.values; this.rows = config.rows; this.types = config.types; this.name = config.name; this.binary = config.binary; + this.stream = config.stream; //use unique portal name each time - this.portal = config.portal || "" + this.portal = config.portal || ""; this.callback = config.callback; - this._fieldNames = []; - this._fieldConverters = []; - this._result = new Result(); + if(process.domain && config.callback) { + this.callback = process.domain.bind(config.callback); + } + this._result = new Result(config.rowMode, config.types); this.isPreparedStatement = false; + this._canceledDueToError = false; + this._promise = null; EventEmitter.call(this); }; util.inherits(Query, EventEmitter); -var p = Query.prototype; -p.requiresPreparation = function() { - return (this.values || 0).length > 0 || this.name || this.rows || this.binary; +Query.prototype.then = function(onSuccess, onFailure) { + return this.promise().then(onSuccess, onFailure); }; +Query.prototype.catch = function(callback) { + return this.promise().catch(callback); +}; + +Query.prototype.promise = function() { + if (this._promise) return this._promise; + this._promise = new Promise(function(resolve, reject) { + this.once('end', resolve); + this.once('error', reject); + }.bind(this)); + return this._promise; +}; -var noParse = function(val) { - return val; +Query.prototype.requiresPreparation = function() { + //named queries must always be prepared + if(this.name) { return true; } + //always prepare if there are max number of rows expected per + //portal execution + if(this.rows) { return true; } + //don't prepare empty text queries + if(!this.text) { return false; } + //prepare if there are values + if(!this.values) { return false; } + return this.values.length > 0; }; + //associates row metadata from the supplied //message with this query object //metadata used when parsing row results -p.handleRowDescription = function(msg) { - this._fieldNames = []; - this._fieldConverters = []; - var len = msg.fields.length; - for(var i = 0; i < len; i++) { - var field = msg.fields[i]; - var format = field.format; - this._fieldNames[i] = field.name; - this._fieldConverters[i] = Types.getTypeParser(field.dataTypeID, format); - }; -}; - -p.handleDataRow = function(msg) { - var self = this; - var row = {}; - for(var i = 0; i < msg.fields.length; i++) { - var rawValue = msg.fields[i]; - if(rawValue === null) { - //leave null values alone - row[self._fieldNames[i]] = null; - } else { - //convert value to javascript - row[self._fieldNames[i]] = self._fieldConverters[i](rawValue); - } +Query.prototype.handleRowDescription = function(msg) { + this._result.addFields(msg.fields); + this._accumulateRows = this.callback || !this.listeners('row').length; +}; + +Query.prototype.handleDataRow = function(msg) { + var row; + + if (this._canceledDueToError) { + return; } - self.emit('row', row, self._result); - //if there is a callback collect rows - if(self.callback) { - self._result.addRow(row); + try { + row = this._result.parseRow(msg.fields); + } catch (err) { + this._canceledDueToError = err; + return; + } + + this.emit('row', row, this._result); + if (this._accumulateRows) { + this._result.addRow(row); } }; -p.handleCommandComplete = function(msg) { +Query.prototype.handleCommandComplete = function(msg, con) { this._result.addCommandComplete(msg); + //need to sync after each command complete of a prepared statement + if(this.isPreparedStatement) { + con.sync(); + } }; -p.handleReadyForQuery = function() { +//if a named prepared statement is created with empty query text +//the backend will send an emptyQuery message but *not* a command complete message +//execution on the connection will hang until the backend receives a sync message +Query.prototype.handleEmptyQuery = function(con) { + if (this.isPreparedStatement) { + con.sync(); + } +}; + +Query.prototype.handleReadyForQuery = function(con) { + if(this._canceledDueToError) { + return this.handleError(this._canceledDueToError, con); + } if(this.callback) { this.callback(null, this._result); } this.emit('end', this._result); }; -p.handleError = function(err) { +Query.prototype.handleError = function(err, connection) { + //need to sync after error during a prepared statement + if(this.isPreparedStatement) { + connection.sync(); + } + if(this._canceledDueToError) { + err = this._canceledDueToError; + this._canceledDueToError = false; + } //if callback supplied do not emit error event as uncaught error //events will bubble up to node process if(this.callback) { - this.callback(err) - } else { - this.emit('error', err); + return this.callback(err); } - this.emit('end'); + this.emit('error', err); }; -p.submit = function(connection) { - var self = this; +Query.prototype.submit = function(connection) { if(this.requiresPreparation()) { this.prepare(connection); } else { @@ -101,19 +151,23 @@ p.submit = function(connection) { } }; -p.hasBeenParsed = function(connection) { +Query.prototype.hasBeenParsed = function(connection) { return this.name && connection.parsedStatements[this.name]; }; -p.getRows = function(connection) { +Query.prototype.handlePortalSuspended = function(connection) { + this._getRows(connection, this.rows); +}; + +Query.prototype._getRows = function(connection, rows) { connection.execute({ portal: this.portalName, - rows: this.rows + rows: rows }, true); connection.flush(); }; -p.prepare = function(connection) { +Query.prototype.prepare = function(connection) { var self = this; //prepared statements need sync to be called after each command //complete or when an error is encountered @@ -125,14 +179,10 @@ p.prepare = function(connection) { name: self.name, types: self.types }, true); - connection.parsedStatements[this.name] = true; } - //TODO is there some better way to prepare values for the database? if(self.values) { - for(var i = 0, len = self.values.length; i < len; i++) { - self.values[i] = utils.prepareValue(self.values[i]); - } + self.values = self.values.map(utils.prepareValue); } //http://developer.postgresql.org/pgdocs/postgres/protocol-flow.html#PROTOCOL-FLOW-EXT-QUERY @@ -148,7 +198,21 @@ p.prepare = function(connection) { name: self.portalName || "" }, true); - this.getRows(connection); + this._getRows(connection, this.rows); }; +Query.prototype.handleCopyInResponse = function (connection) { + if(this.stream) this.stream.startStreamingToConnection(connection); + else connection.sendCopyFail('No source stream defined'); +}; + +Query.prototype.handleCopyData = function (msg, connection) { + var chunk = msg.chunk; + if(this.stream) { + this.stream.handleChunk(chunk); + } + //if there are no stream (for example when copy to query was sent by + //query method instead of copyTo) error will be handled + //on copyOutResponse event, so silently ignore this error here +}; module.exports = Query; diff --git a/lib/result.js b/lib/result.js index 3bc5cde54..463fbdbe6 100644 --- a/lib/result.js +++ b/lib/result.js @@ -1,34 +1,115 @@ +/** + * Copyright (c) 2010-2016 Brian Carlson (brian.m.carlson@gmail.com) + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * README.md file in the root directory of this source tree. + */ + +var types = require('pg-types'); + //result object returned from query //in the 'end' event and also //passed as second argument to provided callback -var Result = function() { +var Result = function(rowMode) { this.command = null; this.rowCount = null; this.oid = null; this.rows = []; + this.fields = []; + this._parsers = []; + this.RowCtor = null; + this.rowAsArray = rowMode == "array"; + if(this.rowAsArray) { + this.parseRow = this._parseRowAsArray; + } }; -var p = Result.prototype; - -var matchRegexp = /([A-Za-z]+) (\d+ )?(\d+)?/ +var matchRegexp = /([A-Za-z]+) ?(\d+ )?(\d+)?/; //adds a command complete message -p.addCommandComplete = function(msg) { - var match = matchRegexp.exec(msg.text); +Result.prototype.addCommandComplete = function(msg) { + var match; + if(msg.text) { + //pure javascript + match = matchRegexp.exec(msg.text); + } else { + //native bindings + match = matchRegexp.exec(msg.command); + } if(match) { this.command = match[1]; //match 3 will only be existing on insert commands if(match[3]) { - this.rowCount = parseInt(match[3]); - this.oid = parseInt(match[2]); + //msg.value is from native bindings + this.rowCount = parseInt(match[3] || msg.value, 10); + this.oid = parseInt(match[2], 10); + } else { + this.rowCount = parseInt(match[2], 10); + } + } +}; + +Result.prototype._parseRowAsArray = function(rowData) { + var row = []; + for(var i = 0, len = rowData.length; i < len; i++) { + var rawValue = rowData[i]; + if(rawValue !== null) { + row.push(this._parsers[i](rawValue)); } else { - this.rowCount = parseInt(match[2]); + row.push(null); } } + return row; }; -p.addRow = function(row) { +//rowData is an array of text or binary values +//this turns the row into a JavaScript object +Result.prototype.parseRow = function(rowData) { + return new this.RowCtor(this._parsers, rowData); +}; + +Result.prototype.addRow = function(row) { this.rows.push(row); }; +var inlineParser = function(fieldName, i) { + return "\nthis['" + + //fields containing single quotes will break + //the evaluated javascript unless they are escaped + //see https://github.com/brianc/node-postgres/issues/507 + //Addendum: However, we need to make sure to replace all + //occurences of apostrophes, not just the first one. + //See https://github.com/brianc/node-postgres/issues/934 + fieldName.replace(/'/g, "\\'") + + "'] = " + + "rowData[" + i + "] == null ? null : parsers[" + i + "](rowData[" + i + "]);"; +}; + +Result.prototype.addFields = function(fieldDescriptions) { + //clears field definitions + //multiple query statements in 1 action can result in multiple sets + //of rowDescriptions...eg: 'select NOW(); select 1::int;' + //you need to reset the fields + if(this.fields.length) { + this.fields = []; + this._parsers = []; + } + var ctorBody = ""; + for(var i = 0; i < fieldDescriptions.length; i++) { + var desc = fieldDescriptions[i]; + this.fields.push(desc); + var parser = this._getTypeParser(desc.dataTypeID, desc.format || 'text'); + this._parsers.push(parser); + //this is some craziness to compile the row result parsing + //results in ~60% speedup on large query result sets + ctorBody += inlineParser(desc.name, i); + } + if(!this.rowAsArray) { + this.RowCtor = Function("parsers", "rowData", ctorBody); + } +}; + +Result.prototype._getTypeParser = types.getTypeParser; + module.exports = Result; diff --git a/lib/textParsers.js b/lib/textParsers.js deleted file mode 100644 index c8811bec4..000000000 --- a/lib/textParsers.js +++ /dev/null @@ -1,187 +0,0 @@ -var arrayParser = require(__dirname + "/arrayParser.js"); - -//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) { - dateMatcher = /^(\d{4})-(\d{2})-(\d{2})$/; - match = dateMatcher.test(isoDate); - if(!match) { - return null; - } else { - //it is a date in YYYY-MM-DD format - return new Date(isoDate); - } - } - 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) { - if(!val) return null; - var p = arrayParser.create(val, function(entry){ - if(entry != null) - entry = parseInt(entry, 10); - return entry; - }); - - return p.parse(); -}; - -var parseFloatArray = function(val) { - if(!val) return null; - var p = arrayParser.create(val, function(entry){ - if(entry != null) - entry = parseFloat(entry, 10); - return entry; - }); - - return p.parse(); -}; - -var parseStringArray = function(val) { - if(!val) return null; - - var p = arrayParser.create(val); - return p.parse(); -}; - - -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], 10); - if (m[4]) i.months = parseInt(m[4], 10); - if (m[6]) i.days = parseInt(m[6], 10); - if (m[9]) i.hours = parseInt(m[9], 10); - if (m[10]) i.minutes = parseInt(m[10], 10); - if (m[11]) i.seconds = parseInt(m[11], 10); - 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 parseByteA = function(val) { - if(/^\\x/.test(val)){ - // new 'hex' style response (pg >9.0) - return new Buffer(val.substr(2), 'hex'); - }else{ - out = "" - i = 0 - while(i < val.length){ - if(val[i] != "\\"){ - out += val[i] - ++i - }else{ - if(val.substr(i+1,3).match(/[0-7]{3}/)){ - out += String.fromCharCode(parseInt(val.substr(i+1,3),8)) - i += 4 - }else{ - backslashes = 1 - while(i+backslashes < val.length && val[i+backslashes] == "\\") - backslashes++ - for(k=0; k maxLen) { - console.warn('WARNING: value %s is longer than max supported numeric value in javascript. Possible data loss', val) - } - return parseFloat(val); - }); - register(700, parseFloat); - register(701, parseFloat); - register(16, parseBool); - register(1082, parseDate); - register(1114, parseDate); - register(1184, parseDate); - register(1005, parseIntegerArray); // _int2 - register(1007, parseIntegerArray); // _int4 - register(1016, parseIntegerArray); // _int8 - register(1021, parseFloatArray); // _float4 - register(1022, parseFloatArray); // _float8 - register(1231, parseIntegerArray); // _numeric - register(1008, parseStringArray); - register(1009, parseStringArray); - register(1186, parseInterval); - register(17, parseByteA); -}; - -module.exports = { - init: init, -}; diff --git a/lib/type-overrides.js b/lib/type-overrides.js new file mode 100644 index 000000000..e0cf00e6a --- /dev/null +++ b/lib/type-overrides.js @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2010-2016 Brian Carlson (brian.m.carlson@gmail.com) + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * README.md file in the root directory of this source tree. + */ + +var types = require('pg-types'); + +function TypeOverrides(userTypes) { + this._types = userTypes || types; + this.text = {}; + this.binary = {}; +} + +TypeOverrides.prototype.getOverrides = function(format) { + switch(format) { + case 'text': return this.text; + case 'binary': return this.binary; + default: return {}; + } +}; + +TypeOverrides.prototype.setTypeParser = function(oid, format, parseFn) { + if(typeof format == 'function') { + parseFn = format; + format = 'text'; + } + this.getOverrides(format)[oid] = parseFn; +}; + +TypeOverrides.prototype.getTypeParser = function(oid, format) { + format = format || 'text'; + return this.getOverrides(format)[oid] || this._types.getTypeParser(oid, format); +}; + +module.exports = TypeOverrides; diff --git a/lib/types.js b/lib/types.js deleted file mode 100644 index cae3609bb..000000000 --- a/lib/types.js +++ /dev/null @@ -1,44 +0,0 @@ -var textParsers = require(__dirname + "/textParsers"), -binaryParsers = require(__dirname + "/binaryParsers"); - -var typeParsers = { - text: {}, - binary: {} -}; - -//the empty parse function -var noParse = function(val) { - return String(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; -}; - -var setTypeParser = function(oid, format, parseFn) { - if(typeof format == 'function') { - parseFn = format; - format = 'text'; - } - typeParsers[format][oid] = parseFn; -} - -textParsers.init(function(oid, converter) { - typeParsers.text[oid] = function(value) { - return converter(String(value)); - }; -}); - -binaryParsers.init(function(oid, converter) { - typeParsers.binary[oid] = converter; -}); - -module.exports = { - getTypeParser: getTypeParser, - setTypeParser: setTypeParser -} diff --git a/lib/utils.js b/lib/utils.js index 07a379289..861b7c5b6 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,116 +1,147 @@ -var url = require('url'); -var defaults = require(__dirname + "/defaults"); -var events = require('events'); - -//compatibility for old nodes -if(typeof events.EventEmitter.prototype.once !== 'function') { - events.EventEmitter.prototype.once = function (type, listener) { - var self = this; - self.on(type, function g () { - self.removeListener(type, g); - listener.apply(this, arguments); - }); - }; -} +/** + * Copyright (c) 2010-2016 Brian Carlson (brian.m.carlson@gmail.com) + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * README.md file in the root directory of this source tree. + */ -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; -}; +var defaults = require('./defaults'); -//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])) -}; +function escapeElement(elementRepresentation) { + var escaped = elementRepresentation + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"'); -//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+"'"); - } + return '"' + escaped + '"'; } -//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 + "'"); +// convert a JS array to a postgres array literal +// uses comma separator so won't work for types like box that use +// a different array separator. +function arrayString(val) { + var result = '{'; + for (var i = 0 ; i < val.length; i++) { + if(i > 0) { + result = result + ','; + } + if(val[i] === null || typeof val[i] === 'undefined') { + result = result + 'NULL'; } - 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 "); + else if(Array.isArray(val[i])) { + result = result + arrayString(val[i]); + } + else + { + result += escapeElement(prepareValue(val[i])); } - callback(null, params.join(" ")); - } else { - throw new Error("Unrecognized config type for connection"); } + result = result + '}'; + return result; } //converts values from javascript types //to their 'raw' counterparts for use as a postgres parameter //note: you can override this function to provide your own conversion mechanism //for complex types, etc... -var prepareValue = function(val) { +var prepareValue = function(val, seen) { + if (val instanceof Buffer) { + return val; + } if(val instanceof Date) { - return JSON.stringify(val); + if(defaults.parseInputDatesAsUTC) { + return dateToStringUTC(val); + } else { + return dateToString(val); + } } - if(typeof val === 'undefined') { + if(Array.isArray(val)) { + return arrayString(val); + } + if(val === null || typeof val === 'undefined') { return null; } - return val === null ? null : val.toString(); + if(typeof val === 'object') { + return prepareObject(val, seen); + } + return val.toString(); +}; + +function prepareObject(val, seen) { + if(val.toPostgres && typeof val.toPostgres === 'function') { + seen = seen || []; + if (seen.indexOf(val) !== -1) { + throw new Error('circular reference detected while preparing "' + val + '" for query'); + } + seen.push(val); + + return prepareValue(val.toPostgres(prepareValue), seen); + } + return JSON.stringify(val); } -module.exports = { - 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, - parseConnectionString: parseConnectionString, - prepareValue: prepareValue +function pad(number, digits) { + number = "" +number; + while(number.length < digits) + number = "0" + number; + return number; +} + +function dateToString(date) { + + var offset = -date.getTimezoneOffset(); + var ret = pad(date.getFullYear(), 4) + '-' + + pad(date.getMonth() + 1, 2) + '-' + + pad(date.getDate(), 2) + 'T' + + pad(date.getHours(), 2) + ':' + + pad(date.getMinutes(), 2) + ':' + + pad(date.getSeconds(), 2) + '.' + + pad(date.getMilliseconds(), 3); + + if(offset < 0) { + ret += "-"; + offset *= -1; + } + else + ret += "+"; + + return ret + pad(Math.floor(offset/60), 2) + ":" + pad(offset%60, 2); +} + +function dateToStringUTC(date) { + + var ret = pad(date.getUTCFullYear(), 4) + '-' + + pad(date.getUTCMonth() + 1, 2) + '-' + + pad(date.getUTCDate(), 2) + 'T' + + pad(date.getUTCHours(), 2) + ':' + + pad(date.getUTCMinutes(), 2) + ':' + + pad(date.getUTCSeconds(), 2) + '.' + + pad(date.getUTCMilliseconds(), 3); + + return ret + "+00:00"; +} + +function normalizeQueryConfig (config, values, callback) { + //can take in strings or config objects + config = (typeof(config) == 'string') ? { text: config } : config; + if(values) { + if(typeof values === 'function') { + config.callback = values; + } else { + config.values = values; + } + } + if(callback) { + config.callback = callback; + } + return config; } + +module.exports = { + prepareValue: function prepareValueWrapper (value) { + //this ensures that extra arguments do not get passed into prepareValue + //by accident, eg: from calling values.map(utils.prepareValue) + return prepareValue(value); + }, + normalizeQueryConfig: normalizeQueryConfig +}; diff --git a/lib/writer.js b/lib/writer.js deleted file mode 100644 index 49aed26da..000000000 --- a/lib/writer.js +++ /dev/null @@ -1,130 +0,0 @@ -//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 = Buffer(this.size + 5); - this.offset = 5; - this.headerPosition = 0; -}; - -var p = Writer.prototype; - -//resizes internal buffer if not enough size left -p._ensure = function(size) { - var remaining = this.buffer.length - this.offset; - if(remaining < size) { - var oldBuffer = this.buffer; - this.buffer = new Buffer(oldBuffer.length + size); - oldBuffer.copy(this.buffer); - } -} - -p.addInt32 = function(num) { - this._ensure(4) - this.buffer[this.offset++] = (num >>> 24 & 0xFF) - this.buffer[this.offset++] = (num >>> 16 & 0xFF) - this.buffer[this.offset++] = (num >>> 8 & 0xFF) - this.buffer[this.offset++] = (num >>> 0 & 0xFF) - return this; -} - -p.addInt16 = function(num) { - this._ensure(2) - this.buffer[this.offset++] = (num >>> 8 & 0xFF) - this.buffer[this.offset++] = (num >>> 0 & 0xFF) - return this; -} - -//for versions of node requiring 'length' as 3rd argument to buffer.write -var writeString = function(buffer, string, offset, len) { - buffer.write(string, offset, len); -} - -//overwrite function for older versions of node -if(Buffer.prototype.write.length === 3) { - writeString = function(buffer, string, offset, len) { - buffer.write(string, offset); - } -} - -p.addCString = function(string) { - //just write a 0 for empty or null strings - if(!string) { - this._ensure(1); - } else { - var len = Buffer.byteLength(string); - this._ensure(len + 1); //+1 for null terminator - writeString(this.buffer, string, this.offset, len); - this.offset += len; - } - - this.buffer[this.offset++] = 0; // null terminator - return this; -} - -p.addChar = function(char) { - this._ensure(1); - writeString(this.buffer, char, this.offset, 1); - this.offset++; - return this; -} - -p.addString = function(string) { - var string = string || ""; - var len = Buffer.byteLength(string); - this._ensure(len); - this.buffer.write(string, this.offset); - this.offset += len; - return this; -} - -p.getByteLength = function() { - return this.offset - 5; -} - -p.add = function(otherBuffer) { - this._ensure(otherBuffer.length); - otherBuffer.copy(this.buffer, this.offset); - this.offset += otherBuffer.length; - return this; -} - -p.clear = function() { - this.offset = 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) { - this.addHeader(code, true); - } - return this.buffer.slice(code ? 0 : 5, this.offset); -} - -p.flush = function(code) { - var result = this.join(code); - this.clear(); - return result; -} - -module.exports = Writer; diff --git a/package.json b/package.json index 51019e2a8..f22d223ed 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,46 @@ -{ "name": "pg", - "version": "0.8.6", +{ + "name": "pg", + "version": "6.2.0", "description": "PostgreSQL client - pure javascript & libpq with the same API", - "keywords" : ["postgres", "pg", "libpq", "postgre", "database", "rdbms"], + "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" + "repository": { + "type": "git", + "url": "git://github.com/brianc/node-postgres.git" }, - "author" : "Brian Carlson ", - "main" : "./lib", - "dependencies" : { - "generic-pool" : "1.0.12" + "author": "Brian Carlson ", + "main": "./lib", + "dependencies": { + "buffer-writer": "1.0.1", + "packet-reader": "0.2.0", + "pg-connection-string": "0.1.3", + "pg-pool": "1.*", + "pg-types": "1.*", + "pgpass": "1.x", + "semver": "4.3.2" }, - "scripts" : { - "test" : "make test-all connectionString=pg://postgres@localhost:5432/postgres", - "prepublish": "rm -r build || (exit 0)", - "install" : "node-gyp rebuild || (exit 0)" + "devDependencies": { + "async": "0.9.0", + "co": "4.6.0", + "jshint": "2.5.2", + "lodash": "4.13.1", + "pg-copy-streams": "0.3.0", + "promise-polyfill": "5.2.1" }, - "engines" : { "node": ">= 0.8.0" } + "minNativeVersion": "1.7.0", + "scripts": { + "changelog": "npm i github-changes && ./node_modules/.bin/github-changes -o brianc -r node-postgres -d pulls -a -v", + "test": "make test-all connectionString=postgres://postgres@localhost:5432/postgres" + }, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } } diff --git a/script/create-test-tables.js b/script/create-test-tables.js index d8cbc4c69..fa5d1b6e5 100644 --- a/script/create-test-tables.js +++ b/script/create-test-tables.js @@ -38,23 +38,17 @@ var con = new pg.Client({ database: args.database }); con.connect(); -if(args.down) { - console.log("Dropping table 'person'") - var query = con.query("drop table person"); - query.on('end', function() { - console.log("Dropped!"); - con.end(); - }); -} else { - console.log("Creating table 'person'"); - con.query("create table person(id serial, name varchar(10), age integer)").on('end', function(){ - console.log("Created!"); - console.log("Filling it with people"); - });; - people.map(function(person) { - return con.query("insert into person(name, age) values('"+person.name + "', '" + person.age + "')"); - }).pop().on('end', function(){ - console.log("Inserted 26 people"); - con.end(); - }); -} +var query = con.query("drop table if exists person"); +query.on('end', function() { + console.log("Dropped table 'person'") +}); +con.query("create table person(id serial, name varchar(10), age integer)").on('end', function(){ + console.log("Created table person"); + console.log("Filling it with people"); +}); +people.map(function(person) { + return con.query("insert into person(name, age) values('"+person.name + "', '" + person.age + "')"); +}).pop().on('end', function(){ + console.log("Inserted 26 people"); + con.end(); +}); diff --git a/script/test-connection.js b/script/test-connection.js deleted file mode 100644 index 811286104..000000000 --- a/script/test-connection.js +++ /dev/null @@ -1,23 +0,0 @@ -var helper = require(__dirname + '/../test/test-helper'); - -console.log(); -console.log("testing ability to connect to '%j'", helper.config); -var pg = require(__dirname + '/../lib'); -pg.connect(helper.config, function(err, client) { - if(err !== null) { - console.error("Recieved connection error when attempting to contact PostgreSQL:"); - console.error(err); - process.exit(255); - } - console.log("Checking for existance of required test table 'person'") - client.query("SELECT COUNT(name) FROM person", function(err, callback) { - if(err != null) { - console.error("Recieved error when executing query 'SELECT COUNT(name) FROM person'") - console.error("It is possible you have not yet run the table create script under script/create-test-tables") - console.error("Consult the postgres-node wiki under the 'Testing' section for more information") - console.error(err); - process.exit(255); - } - pg.end(); - }) -}) diff --git a/src/binding.cc b/src/binding.cc deleted file mode 100644 index eb927bf92..000000000 --- a/src/binding.cc +++ /dev/null @@ -1,713 +0,0 @@ -#include -#include -#include -#include -#include - -#define LOG(msg) printf("%s\n",msg); -#define TRACE(msg) //printf("%s\n", msg); - - -#define THROW(msg) return ThrowException(Exception::Error(String::New(msg))); - -using namespace v8; -using namespace node; - -static Persistent severity_symbol; -static Persistent code_symbol; -static Persistent detail_symbol; -static Persistent hint_symbol; -static Persistent position_symbol; -static Persistent internalPosition_symbol; -static Persistent internalQuery_symbol; -static Persistent where_symbol; -static Persistent file_symbol; -static Persistent line_symbol; -static Persistent routine_symbol; -static Persistent name_symbol; -static Persistent value_symbol; -static Persistent type_symbol; -static Persistent channel_symbol; -static Persistent payload_symbol; -static Persistent emit_symbol; -static Persistent command_symbol; - -class Connection : public ObjectWrap { - -public: - - //creates the V8 objects & attaches them to the module (target) - static void - Init (Handle target) - { - HandleScope scope; - Local t = FunctionTemplate::New(New); - - t->InstanceTemplate()->SetInternalFieldCount(1); - t->SetClassName(String::NewSymbol("Connection")); - - emit_symbol = NODE_PSYMBOL("emit"); - severity_symbol = NODE_PSYMBOL("severity"); - code_symbol = NODE_PSYMBOL("code"); - 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"); - command_symbol = NODE_PSYMBOL("command"); - - - 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, "cancel", Cancel); - NODE_SET_PROTOTYPE_METHOD(t, "end", End); - - target->Set(String::NewSymbol("Connection"), t->GetFunction()); - TRACE("created class"); - } - - //static function called by libuv as callback entrypoint - static void - io_event(uv_poll_t* w, int status, int revents) - { - - TRACE("Received IO event"); - - if(status == -1) { - LOG("Connection error."); - return; - } - - Connection *connection = static_cast(w->data); - connection->HandleIOEvent(revents); - } - - //v8 entry point into Connection#connect - static Handle - Connect(const Arguments& args) - { - HandleScope scope; - Connection *self = ObjectWrap::Unwrap(args.This()); - if(args.Length() == 0 || !args[0]->IsString()) { - THROW("Must include connection string as only argument to connect"); - } - - String::Utf8Value conninfo(args[0]->ToString()); - bool success = self->Connect(*conninfo); - if(!success) { - self -> EmitLastError(); - self -> DestroyConnection(); - } - - return Undefined(); - } - - //v8 entry point into Connection#cancel - static Handle - Cancel(const Arguments& args) - { - HandleScope scope; - Connection *self = ObjectWrap::Unwrap(args.This()); - - bool success = self->Cancel(); - if(!success) { - self -> EmitLastError(); - self -> DestroyConnection(); - } - - return Undefined(); - } - - //v8 entry point into Connection#_sendQuery - static Handle - SendQuery(const Arguments& args) - { - HandleScope scope; - Connection *self = ObjectWrap::Unwrap(args.This()); - const char *lastErrorMessage; - 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) { - lastErrorMessage = self->GetLastError(); - THROW(lastErrorMessage); - } - //TODO should we flush before throw? - self->Flush(); - return Undefined(); - } - - //v8 entry point into Connection#_sendQueryWithParams - static Handle - SendQueryWithParams(const Arguments& args) - { - HandleScope scope; - //dispatch non-prepared parameterized query - return DispatchParameterizedQuery(args, false); - } - - //v8 entry point into Connection#_sendPrepare(string queryName, string queryText, int nParams) - static Handle - SendPrepare(const Arguments& args) - { - HandleScope scope; - - Connection *self = ObjectWrap::Unwrap(args.This()); - String::Utf8Value queryName(args[0]); - String::Utf8Value queryText(args[1]); - int length = args[2]->Int32Value(); - self->SendPrepare(*queryName, *queryText, length); - - return Undefined(); - } - - //v8 entry point into Connection#_sendQueryPrepared(string queryName, string[] paramValues) - static Handle - SendQueryPrepared(const Arguments& args) - { - HandleScope scope; - //dispatch prepared parameterized query - return DispatchParameterizedQuery(args, true); - } - - static Handle - DispatchParameterizedQuery(const Arguments& args, bool isPrepared) - { - HandleScope scope; - Connection *self = ObjectWrap::Unwrap(args.This()); - - String::Utf8Value queryName(args[0]); - //TODO this is much copy/pasta code - if(!args[0]->IsString()) { - THROW("First parameter must be a string"); - } - - if(!args[1]->IsArray()) { - THROW("Values must be an array"); - } - - Local jsParams = Local::Cast(args[1]); - int len = jsParams->Length(); - - - char** paramValues = ArgToCStringArray(jsParams); - if(!paramValues) { - THROW("Unable to allocate char **paramValues from Local of v8 params"); - } - - char* queryText = MallocCString(args[0]); - - int result = 0; - if(isPrepared) { - result = self->SendPreparedQuery(queryText, len, paramValues); - } else { - result = self->SendQueryParams(queryText, len, paramValues); - } - - free(queryText); - ReleaseCStringArray(paramValues, len); - if(result == 1) { - return Undefined(); - } - self->EmitLastError(); - THROW("Postgres returned non-1 result from query dispatch."); - } - - //v8 entry point into Connection#end - static Handle - End(const Arguments& args) - { - HandleScope scope; - - Connection *self = ObjectWrap::Unwrap(args.This()); - - self->End(); - return Undefined(); - } - - uv_poll_t read_watcher_; - uv_poll_t write_watcher_; - PGconn *connection_; - bool connecting_; - bool ioInitialized_; - Connection () : ObjectWrap () - { - connection_ = NULL; - connecting_ = false; - ioInitialized_ = false; - - TRACE("Initializing ev watchers"); - read_watcher_.data = this; - write_watcher_.data = this; - } - - ~Connection () - { - } - -protected: - //v8 entry point to constructor - static Handle - New (const Arguments& args) - { - HandleScope scope; - Connection *connection = new Connection(); - connection->Wrap(args.This()); - - return args.This(); - } - - int Send(const char *queryText) - { - int rv = PQsendQuery(connection_, queryText); - StartWrite(); - return rv; - } - - int SendQueryParams(const char *command, const int nParams, const char * const *paramValues) - { - int rv = PQsendQueryParams(connection_, command, nParams, NULL, paramValues, NULL, NULL, 0); - StartWrite(); - return rv; - } - - int SendPrepare(const char *name, const char *command, const int nParams) - { - int rv = PQsendPrepare(connection_, name, command, nParams, NULL); - StartWrite(); - return rv; - } - - int SendPreparedQuery(const char *name, int nParams, const char * const *paramValues) - { - int rv = PQsendQueryPrepared(connection_, name, nParams, paramValues, NULL, NULL, 0); - StartWrite(); - return rv; - } - - bool Cancel() - { - PGcancel* pgCancel = PQgetCancel(connection_); - char errbuf[256]; - int result = PQcancel(pgCancel, errbuf, 256); - StartWrite(); - PQfreeCancel(pgCancel); - return result; - } - - //flushes socket - void Flush() - { - if(PQflush(connection_) == 1) { - TRACE("Flushing"); - uv_poll_start(&write_watcher_, UV_WRITABLE, io_event); - } - } - - //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"); - } - - ConnStatusType status = PQstatus(connection_); - - if(CONNECTION_BAD == status) { - return false; - } - - if (PQsetnonblocking(connection_, 1) == -1) { - LOG("Unable to set connection to non-blocking"); - 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"); - uv_poll_init(uv_default_loop(), &read_watcher_, fd); - uv_poll_init(uv_default_loop(), &write_watcher_, fd); - - ioInitialized_ = true; - - connecting_ = true; - StartWrite(); - - Ref(); - return true; - } - - static void NoticeReceiver(void *arg, const char *message) - { - Connection *self = (Connection*)arg; - self->HandleNotice(message); - } - - void HandleNotice(const char *message) - { - HandleScope scope; - Handle notice = String::New(message); - Emit("notice", ¬ice); - } - - //called to process io_events from libuv - void HandleIOEvent(int revents) - { - - if(connecting_) { - TRACE("Processing connecting_ io"); - HandleConnectionIO(); - return; - } - - if(revents & UV_READABLE) { - TRACE("revents & UV_READABLE"); - if(PQconsumeInput(connection_) == 0) { - End(); - EmitLastError(); - LOG("Something happened, consume input is 0"); - return; - } - - //declare handlescope as this method is entered via a libuv 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("_readyForQuery"); - } - } - - PGnotify *notify; - while ((notify = PQnotifies(connection_))) { - Local result = Object::New(); - result->Set(channel_symbol, String::New(notify->relname)); - result->Set(payload_symbol, String::New(notify->extra)); - Handle res = (Handle)result; - Emit("notification", &res); - PQfreemem(notify); - } - - } - - if(revents & UV_WRITABLE) { - TRACE("revents & UV_WRITABLE"); - if (PQflush(connection_) == 0) { - StopWrite(); - } - } - } - - void HandleResult(PGresult* result) - { - ExecStatusType status = PQresultStatus(result); - switch(status) { - case PGRES_TUPLES_OK: - { - HandleTuplesResult(result); - EmitCommandMetaData(result); - } - break; - case PGRES_FATAL_ERROR: - HandleErrorResult(result); - break; - case PGRES_COMMAND_OK: - case PGRES_EMPTY_QUERY: - EmitCommandMetaData(result); - break; - default: - printf("Unrecogized query status: %s\n", PQresStatus(status)); - break; - } - } - - void EmitCommandMetaData(PGresult* result) - { - HandleScope scope; - Local info = Object::New(); - info->Set(command_symbol, String::New(PQcmdStatus(result))); - info->Set(value_symbol, String::New(PQcmdTuples(result))); - Handle e = (Handle)info; - Emit("_cmdStatus", &e); - } - - //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) - { - HandleScope scope; - int rowCount = PQntuples(result); - for(int rowNumber = 0; rowNumber < rowCount; rowNumber++) { - //create result object for this row - Local row = Array::New(); - int fieldCount = PQnfields(result); - for(int fieldNumber = 0; fieldNumber < fieldCount; fieldNumber++) { - Local field = Object::New(); - //name of field - char* fieldName = PQfname(result, fieldNumber); - field->Set(name_symbol, String::New(fieldName)); - - //oid of type of field - int fieldType = PQftype(result, fieldNumber); - field->Set(type_symbol, Integer::New(fieldType)); - - //value of field - if(PQgetisnull(result, rowNumber, fieldNumber)) { - field->Set(value_symbol, Null()); - } else { - char* fieldValue = PQgetvalue(result, rowNumber, fieldNumber); - field->Set(value_symbol, String::New(fieldValue)); - } - - row->Set(Integer::New(fieldNumber), field); - } - - Handle e = (Handle)row; - Emit("_row", &e); - } - } - - void HandleErrorResult(const PGresult* result) - { - HandleScope scope; - //instantiate the return object as an Error with the summary Postgres message - Local msg = Local::Cast(Exception::Error(String::New(PQresultErrorField(result, PG_DIAG_MESSAGE_PRIMARY)))); - - //add the other information returned by Postgres to the error object - AttachErrorField(result, msg, severity_symbol, PG_DIAG_SEVERITY); - AttachErrorField(result, msg, code_symbol, PG_DIAG_SQLSTATE); - AttachErrorField(result, msg, detail_symbol, PG_DIAG_MESSAGE_DETAIL); - AttachErrorField(result, msg, hint_symbol, PG_DIAG_MESSAGE_HINT); - AttachErrorField(result, msg, position_symbol, PG_DIAG_STATEMENT_POSITION); - AttachErrorField(result, msg, internalPosition_symbol, PG_DIAG_INTERNAL_POSITION); - AttachErrorField(result, msg, internalQuery_symbol, PG_DIAG_INTERNAL_QUERY); - AttachErrorField(result, msg, where_symbol, PG_DIAG_CONTEXT); - AttachErrorField(result, msg, file_symbol, PG_DIAG_SOURCE_FILE); - AttachErrorField(result, msg, line_symbol, PG_DIAG_SOURCE_LINE); - AttachErrorField(result, msg, routine_symbol, PG_DIAG_SOURCE_FUNCTION); - Handle m = msg; - Emit("_error", &m); - } - - void AttachErrorField(const PGresult *result, const Local msg, const Persistent symbol, int fieldcode) - { - char *val = PQresultErrorField(result, fieldcode); - if(val) { - msg->Set(symbol, String::New(val)); - } - } - - void End() - { - StopRead(); - StopWrite(); - DestroyConnection(); - } - -private: - //EventEmitter was removed from c++ in node v0.5.x - void Emit(const char* message) { - HandleScope scope; - Handle args[1] = { String::New(message) }; - Emit(1, args); - } - - void Emit(const char* message, Handle* arg) { - HandleScope scope; - Handle args[2] = { String::New(message), *arg }; - Emit(2, args); - } - - void Emit(int length, Handle *args) { - HandleScope scope; - - Local emit_v = this->handle_->Get(emit_symbol); - assert(emit_v->IsFunction()); - Local emit_f = emit_v.As(); - - TryCatch tc; - emit_f->Call(this->handle_, length, args); - if(tc.HasCaught()) { - FatalException(tc); - } - } - - 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"); - default: - //printf("Unknown polling status: %d\n", status); - break; - } - } - - void EmitError(const char *message) - { - Local exception = Exception::Error(String::New(message)); - Emit("_error", &exception); - } - - void EmitLastError() - { - EmitError(PQerrorMessage(connection_)); - } - - const char *GetLastError() - { - return PQerrorMessage(connection_); - } - - void StopWrite() - { - TRACE("Stoping write watcher"); - if(ioInitialized_) { - uv_poll_stop(&write_watcher_); - } - } - - void StartWrite() - { - TRACE("Starting write watcher"); - uv_poll_start(&write_watcher_, UV_WRITABLE, io_event); - } - - void StopRead() - { - TRACE("Stoping read watcher"); - if(ioInitialized_) { - uv_poll_stop(&read_watcher_); - } - } - - void StartRead() - { - TRACE("Starting read watcher"); - uv_poll_start(&read_watcher_, UV_READABLE, io_event); - } - //Converts a v8 array to an array of cstrings - //the result char** array must be free() when it is no longer needed - //if for any reason the array cannot be created, returns 0 - static char** ArgToCStringArray(Local params) - { - int len = params->Length(); - char** paramValues = new char*[len]; - for(int i = 0; i < len; i++) { - Handle val = params->Get(i); - if(val->IsString()) { - char* cString = MallocCString(val); - //will be 0 if could not malloc - if(!cString) { - LOG("ArgToCStringArray: OUT OF MEMORY OR SOMETHING BAD!"); - ReleaseCStringArray(paramValues, i-1); - return 0; - } - paramValues[i] = cString; - } else if(val->IsNull()) { - paramValues[i] = NULL; - } else { - //a paramter was not a string - LOG("Parameter not a string"); - ReleaseCStringArray(paramValues, i-1); - return 0; - } - } - return paramValues; - } - - //helper function to release cString arrays - static void ReleaseCStringArray(char **strArray, int len) - { - for(int i = 0; i < len; i++) { - free(strArray[i]); - } - delete [] strArray; - } - - //helper function to malloc new string from v8string - static char* MallocCString(v8::Handle v8String) - { - String::Utf8Value utf8String(v8String->ToString()); - char *cString = (char *) malloc(strlen(*utf8String) + 1); - if(!cString) { - return cString; - } - strcpy(cString, *utf8String); - return cString; - } -}; - - -extern "C" void init (Handle target) -{ - HandleScope scope; - Connection::Init(target); -} diff --git a/test/cli.js b/test/cli.js index 45bb5ae7f..bec0f3fb3 100644 --- a/test/cli.js +++ b/test/cli.js @@ -1,14 +1,5 @@ -var config = {}; -if(process.argv[2]) { - config = require(__dirname + '/../lib/utils').parseConnectionString(process.argv[2]); -} -//TODO use these environment variables in lib/ code -//http://www.postgresql.org/docs/8.4/static/libpq-envars.html -config.host = config.host || process.env['PGHOST'] || process.env['PGHOSTADDR']; -config.port = config.port || process.env['PGPORT']; -config.database = config.database || process.env['PGDATABASE']; -config.user = config.user || process.env['PGUSER']; -config.password = config.password || process.env['PGPASSWORD']; +var ConnectionParameters = require(__dirname + '/../lib/connection-parameters'); +var config = new ConnectionParameters(process.argv[2]); for(var i = 0; i < process.argv.length; i++) { switch(process.argv[i].toLowerCase()) { @@ -18,9 +9,16 @@ for(var i = 0; i < process.argv.length; i++) { case 'binary': config.binary = true; break; + case 'down': + config.down = true; + break; default: break; } } +if(process.env['PG_TEST_NATIVE']) { + config.native = true; +} + module.exports = config; diff --git a/test/integration/client/api-tests.js b/test/integration/client/api-tests.js index 0fd941089..f8f78f195 100644 --- a/test/integration/client/api-tests.js +++ b/test/integration/client/api-tests.js @@ -1,9 +1,5 @@ var helper = require(__dirname + '/../test-helper'); -var pg = require(__dirname + '/../../../lib'); - -if(helper.args.native) { - pg = require(__dirname + '/../../../lib').native; -} +var pg = helper.pg; var log = function() { //console.log.apply(console, arguments); @@ -16,9 +12,21 @@ var sink = new helper.Sink(5, 10000, function() { test('api', function() { log("connecting to %j", helper.config) - pg.connect(helper.config, assert.calls(function(err, client) { + //test weird callback behavior with node-pool + pg.connect(helper.config, function(err) { + assert.isNull(err); + arguments[1].emit('drain'); + arguments[2](); + }); + pg.connect(helper.config, assert.calls(function(err, client, done) { assert.equal(err, null, "Failed to connect: " + helper.sys.inspect(err)); + if (helper.args.native) { + assert(client.native) + } else { + assert(!client.native) + } + client.query('CREATE TEMP TABLE band(name varchar(100))'); ['the flaming lips', 'wolf parade', 'radiohead', 'bright eyes', 'the beach boys', 'dead black hearts'].forEach(function(bandName) { @@ -51,14 +59,14 @@ test('api', function() { assert.equal(result.rows.pop().name, 'the flaming lips'); assert.equal(result.rows.pop().name, 'the beach boys'); sink.add(); + done(); })) })) - })) }) test('executing nested queries', function() { - pg.connect(helper.config, assert.calls(function(err, client) { + pg.connect(helper.config, assert.calls(function(err, client, done) { assert.isNull(err); log("connected for nested queriese") client.query('select now as now from NOW()', assert.calls(function(err, result) { @@ -68,6 +76,7 @@ test('executing nested queries', function() { log('all nested queries recieved') assert.ok('all queries hit') sink.add(); + done(); })) })) })) @@ -77,27 +86,29 @@ test('executing nested queries', function() { test('raises error if cannot connect', function() { var connectionString = "pg://sfalsdkf:asdf@localhost/ieieie"; log("trying to connect to invalid place for error") - pg.connect(connectionString, assert.calls(function(err, client) { + pg.connect(connectionString, assert.calls(function(err, client, done) { assert.ok(err, 'should have raised an error') log("invalid connection supplied error to callback") sink.add(); + done(); })) }) test("query errors are handled and do not bubble if callback is provded", function() { - pg.connect(helper.config, assert.calls(function(err, client) { + pg.connect(helper.config, assert.calls(function(err, client, done) { assert.isNull(err) log("checking for query error") client.query("SELECT OISDJF FROM LEIWLISEJLSE", assert.calls(function(err, result) { assert.ok(err); log("query error supplied error to callback") sink.add(); + done(); })) })) }) test('callback is fired once and only once', function() { - pg.connect(helper.config, assert.calls(function(err, client) { + pg.connect(helper.config, assert.calls(function(err, client, done) { assert.isNull(err); client.query("CREATE TEMP TABLE boom(name varchar(10))"); var callCount = 0; @@ -108,12 +119,13 @@ test('callback is fired once and only once', function() { ].join(";"), function(err, callback) { assert.equal(callCount++, 0, "Call count should be 0. More means this callback fired more than once."); sink.add(); + done(); }) })) }) test('can provide callback and config object', function() { - pg.connect(helper.config, assert.calls(function(err, client) { + pg.connect(helper.config, assert.calls(function(err, client, done) { assert.isNull(err); client.query({ name: 'boom', @@ -121,12 +133,13 @@ test('can provide callback and config object', function() { }, assert.calls(function(err, result) { assert.isNull(err); assert.equal(result.rows[0].now.getYear(), new Date().getYear()) + done(); })) })) }) test('can provide callback and config and parameters', function() { - pg.connect(helper.config, assert.calls(function(err, client) { + pg.connect(helper.config, assert.calls(function(err, client, done) { assert.isNull(err); var config = { text: 'select $1::text as val' @@ -135,12 +148,13 @@ test('can provide callback and config and parameters', function() { assert.isNull(err); assert.equal(result.rows.length, 1); assert.equal(result.rows[0].val, 'hi'); + done(); })) })) }) test('null and undefined are both inserted as NULL', function() { - pg.connect(helper.config, assert.calls(function(err, client) { + pg.connect(helper.config, assert.calls(function(err, client, done) { assert.isNull(err); client.query("CREATE TEMP TABLE my_nulls(a varchar(1), b varchar(1), c integer, d integer, e date, f date)"); client.query("INSERT INTO my_nulls(a,b,c,d,e,f) VALUES ($1,$2,$3,$4,$5,$6)", [ null, undefined, null, undefined, null, undefined ]); @@ -153,6 +167,7 @@ test('null and undefined are both inserted as NULL', function() { assert.isNull(result.rows[0].d); assert.isNull(result.rows[0].e); assert.isNull(result.rows[0].f); + done(); })) })) }) diff --git a/test/integration/client/appname-tests.js b/test/integration/client/appname-tests.js new file mode 100644 index 000000000..ca074ecc3 --- /dev/null +++ b/test/integration/client/appname-tests.js @@ -0,0 +1,96 @@ +return; +var helper = require('./test-helper'); +var Client = helper.Client; + +var conInfo = helper.config; + +function getConInfo(override) { + var newConInfo = {}; + Object.keys(conInfo).forEach(function(k){ + newConInfo[k] = conInfo[k]; + }); + Object.keys(override || {}).forEach(function(k){ + newConInfo[k] = override[k]; + }); + return newConInfo; +} + +function getAppName(conf, cb) { + var client = new Client(conf); + client.connect(assert.success(function(){ + client.query('SHOW application_name', assert.success(function(res){ + var appName = res.rows[0].application_name; + cb(appName); + client.end(); + })); + })); +} + +test('No default appliation_name ', function(){ + var conf = getConInfo(); + getAppName(conf, function(res){ + assert.strictEqual(res, ''); + }); +}); + +test('fallback_application_name is used', function(){ + var fbAppName = 'this is my app'; + var conf = getConInfo({ + 'fallback_application_name' : fbAppName + }); + getAppName(conf, function(res){ + assert.strictEqual(res, fbAppName); + }); +}); + +test('application_name is used', function(){ + var appName = 'some wired !@#$% application_name'; + var conf = getConInfo({ + 'application_name' : appName + }); + getAppName(conf, function(res){ + assert.strictEqual(res, appName); + }); +}); + +test('application_name has precedence over fallback_application_name', function(){ + var appName = 'some wired !@#$% application_name'; + var fbAppName = 'some other strange $$test$$ appname'; + var conf = getConInfo({ + 'application_name' : appName , + 'fallback_application_name' : fbAppName + }); + getAppName(conf, function(res){ + assert.strictEqual(res, appName); + }); +}); + +test('application_name from connection string', function(){ + var appName = 'my app'; + var conParams = require(__dirname + '/../../../lib/connection-parameters'); + var conf; + if (process.argv[2]) { + conf = new conParams(process.argv[2]+'?application_name='+appName); + } else { + conf = 'postgres://?application_name='+appName; + } + getAppName(conf, function(res){ + assert.strictEqual(res, appName); + }); +}); + + + +// TODO: make the test work for native client too +if (!helper.args.native) { + test('application_name is read from the env', function(){ + var appName = process.env.PGAPPNAME = 'testest'; + var conf = getConInfo({ + 'just some bla' : 'to fool the pool' + }); + getAppName(conf, function(res){ + delete process.env.PGAPPNAME; + assert.strictEqual(res, appName); + }); + }); +} diff --git a/test/integration/client/array-tests.js b/test/integration/client/array-tests.js index dde3e5dd1..2525b8a23 100644 --- a/test/integration/client/array-tests.js +++ b/test/integration/client/array-tests.js @@ -1,8 +1,38 @@ var helper = require(__dirname + "/test-helper"); var pg = helper.pg; +test('serializing arrays', function() { + pg.connect(helper.config, assert.calls(function(err, client, done) { + assert.isNull(err); + + test('nulls', function() { + client.query('SELECT $1::text[] as array', [[null]], assert.success(function(result) { + var array = result.rows[0].array; + assert.lengthIs(array, 1); + assert.isNull(array[0]); + })); + }); + + test('elements containing JSON-escaped characters', function() { + var param = '\\"\\"'; + + for (var i = 1; i <= 0x1f; i++) { + param += String.fromCharCode(i); + } + + client.query('SELECT $1::text[] as array', [[param]], assert.success(function(result) { + var array = result.rows[0].array; + assert.lengthIs(array, 1); + assert.equal(array[0], param); + })); + + done(); + }); + })); +}); + test('parsing array results', function() { - pg.connect(helper.config, assert.calls(function(err, client) { + pg.connect(helper.config, assert.calls(function(err, client, done) { assert.isNull(err); client.query("CREATE TEMP TABLE why(names text[], numbors integer[])"); client.query('INSERT INTO why(names, numbors) VALUES(\'{"aaron", "brian","a b c" }\', \'{1, 2, 3}\')').on('error', console.log); @@ -23,7 +53,6 @@ test('parsing array results', function() { assert.equal(names[0], 'aaron'); assert.equal(names[1], 'brian'); assert.equal(names[2], "a b c"); - pg.end(); })) }) @@ -31,7 +60,6 @@ test('parsing array results', function() { client.query("SELECT '{}'::text[] as names", assert.success(function(result) { var names = result.rows[0].names; assert.lengthIs(names, 0); - pg.end(); })) }) @@ -41,7 +69,6 @@ test('parsing array results', function() { assert.lengthIs(names, 2); assert.equal(names[0], 'joe,bob'); assert.equal(names[1], 'jim'); - pg.end(); })) }) @@ -51,7 +78,6 @@ test('parsing array results', function() { assert.lengthIs(names, 2); assert.equal(names[0], '{'); assert.equal(names[1], '}'); - pg.end(); })) }) @@ -63,7 +89,6 @@ test('parsing array results', function() { assert.equal(names[1], null); assert.equal(names[2], 'bob'); assert.equal(names[3], 'NULL'); - pg.end(); })) }) @@ -74,7 +99,6 @@ test('parsing array results', function() { assert.equal(names[0], 'joe\''); assert.equal(names[1], 'jim'); assert.equal(names[2], 'bob"'); - pg.end(); })) }) @@ -91,7 +115,6 @@ test('parsing array results', function() { assert.equal(names[1][0], '2'); assert.equal(names[1][1], 'bob'); - pg.end(); })) }) @@ -102,7 +125,6 @@ test('parsing array results', function() { assert.equal(names[0], 1); assert.equal(names[1], 2); assert.equal(names[2], 3); - pg.end(); })) }) @@ -118,6 +140,22 @@ test('parsing array results', function() { assert.equal(names[2][0], 3); assert.equal(names[2][1], 100); + })) + }) + + test('JS array parameter', function(){ + client.query("SELECT $1::integer[] as names", [[[1,100],[2,100],[3,100]]], assert.success(function(result) { + var names = result.rows[0].names; + assert.lengthIs(names, 3); + assert.equal(names[0][0], 1); + assert.equal(names[0][1], 100); + + assert.equal(names[1][0], 2); + assert.equal(names[1][1], 100); + + assert.equal(names[2][0], 3); + assert.equal(names[2][1], 100); + done(); pg.end(); })) }) diff --git a/test/integration/client/cancel-query-tests.js b/test/integration/client/cancel-query-tests.js index 842b471ae..b6c0c7f4d 100644 --- a/test/integration/client/cancel-query-tests.js +++ b/test/integration/client/cancel-query-tests.js @@ -5,42 +5,36 @@ test("cancellation of a query", function() { var client = helper.client(); - var qry = client.query("select name from person order by name"); + var qry = "select name from person order by name"; client.on('drain', client.end.bind(client)); - var rows1 = 0, rows2 = 0, rows3 = 0, rows4 = 0; - - var query1 = client.query(qry); - query1.on('row', function(row) { - rows1++; - }); - var query2 = client.query(qry); - query2.on('row', function(row) { - rows2++; - }); - var query3 = client.query(qry); - query3.on('row', function(row) { - rows3++; - }); - var query4 = client.query(qry); - query4.on('row', function(row) { - rows4++; - }); - - helper.pg.cancel(helper.config, client, query1); - helper.pg.cancel(helper.config, client, query2); - helper.pg.cancel(helper.config, client, query4); - - setTimeout(function() { - assert.equal(rows1, 0); - assert.equal(rows2, 0); - assert.equal(rows4, 0); - }, 2000); + var rows3 = 0; + + var query1 = client.query(qry); + query1.on('row', function(row) { + throw new Error('Should not emit a row') + }); + var query2 = client.query(qry); + query2.on('row', function(row) { + throw new Error('Should not emit a row') + }); + var query3 = client.query(qry); + query3.on('row', function(row) { + rows3++; + }); + var query4 = client.query(qry); + query4.on('row', function(row) { + throw new Error('Should not emit a row') + }); + + helper.pg.cancel(helper.config, client, query1); + helper.pg.cancel(helper.config, client, query2); + helper.pg.cancel(helper.config, client, query4); assert.emits(query3, 'end', function() { - test("returned right number of rows", function() { - assert.equal(rows3, 26); - }); - }); + test("returned right number of rows", function() { + assert.equal(rows3, 26); + }); + }); }); diff --git a/test/integration/client/configuration-tests.js b/test/integration/client/configuration-tests.js index c641b3009..e922a4e78 100644 --- a/test/integration/client/configuration-tests.js +++ b/test/integration/client/configuration-tests.js @@ -1,6 +1,13 @@ var helper = require(__dirname + '/test-helper'); var pg = helper.pg; +//clear process.env +var realEnv = {}; +for(var key in process.env) { + realEnv[key] = process.env[key]; + if(!key.indexOf('PG')) delete process.env[key]; +} + test('default values', function() { assert.same(pg.defaults,{ user: process.env.USER, @@ -44,3 +51,8 @@ if(!helper.args.native) { }) } + +//restore process.env +for(var key in realEnv) { + process.env[key] = realEnv[key]; +} diff --git a/test/integration/client/custom-types-tests.js b/test/integration/client/custom-types-tests.js new file mode 100644 index 000000000..38479229d --- /dev/null +++ b/test/integration/client/custom-types-tests.js @@ -0,0 +1,18 @@ +var helper = require(__dirname + '/test-helper'); +return console.log('TODO: get this working for non-native client'); + +helper.config.types = { + getTypeParser: function() { + return function() { + return 'okay!' + } + } +}; + +helper.pg.connect(helper.config, assert.success(function(client, done) { + client.query('SELECT NOW() as val', assert.success(function(res) { + assert.equal(res.rows[0].val, 'okay!'); + done(); + helper.pg.end(); + })); +})); diff --git a/test/integration/client/drain-tests.js b/test/integration/client/drain-tests.js deleted file mode 100644 index b6a2434d4..000000000 --- a/test/integration/client/drain-tests.js +++ /dev/null @@ -1,55 +0,0 @@ -var helper = require(__dirname + '/test-helper'); -var pg = require(__dirname + '/../../../lib'); - -if(helper.args.native) { - pg = require(__dirname + '/../../../lib').native; -} - -var testDrainOfClientWithPendingQueries = function() { - pg.connect(helper.config, assert.success(function(client) { - test('when there are pending queries and client is resumed', function() { - var drainCount = 0; - client.on('drain', function() { - drainCount++; - }); - client.pauseDrain(); - client.query('SELECT NOW()', function() { - client.query('SELECT NOW()', function() { - assert.equal(drainCount, 0); - process.nextTick(function() { - assert.equal(drainCount, 1); - pg.end(); - }); - }); - client.resumeDrain(); - assert.equal(drainCount, 0); - }); - }); - })); -}; - -pg.connect(helper.config, assert.success(function(client) { - var drainCount = 0; - client.on('drain', function() { - drainCount++; - }); - test('pauseDrain and resumeDrain on simple client', function() { - client.pauseDrain(); - client.resumeDrain(); - process.nextTick(assert.calls(function() { - assert.equal(drainCount, 0); - test('drain is paused', function() { - client.pauseDrain(); - client.query('SELECT NOW()', assert.success(function() { - process.nextTick(function() { - assert.equal(drainCount, 0); - client.resumeDrain(); - assert.equal(drainCount, 1); - testDrainOfClientWithPendingQueries(); - }); - })); - }); - })); - }); -})); - diff --git a/test/integration/client/empty-query-tests.js b/test/integration/client/empty-query-tests.js index 3eb207c4a..6f0d574d1 100644 --- a/test/integration/client/empty-query-tests.js +++ b/test/integration/client/empty-query-tests.js @@ -5,11 +5,11 @@ test("empty query message handling", function() { assert.emits(client, 'drain', function() { client.end(); }); - client.query({text: "", binary: false}); + client.query({text: ""}); }); test('callback supported', assert.calls(function() { - client.query({text: "", binary: false}, function(err, result) { + client.query("", function(err, result) { assert.isNull(err); assert.empty(result.rows); }) diff --git a/test/integration/client/end-callback-tests.js b/test/integration/client/end-callback-tests.js new file mode 100644 index 000000000..997cfb0cc --- /dev/null +++ b/test/integration/client/end-callback-tests.js @@ -0,0 +1,6 @@ +var helper = require('./test-helper') + +var client = helper.client(assert.success(function() { + client.end(assert.success(function() { + })) +})) diff --git a/test/integration/client/error-handling-tests.js b/test/integration/client/error-handling-tests.js index b35588c56..d3bf36c29 100644 --- a/test/integration/client/error-handling-tests.js +++ b/test/integration/client/error-handling-tests.js @@ -3,8 +3,9 @@ var util = require('util'); var createErorrClient = function() { var client = helper.client(); - client.on('error', function(err) { - assert.ok(false, "client should not throw query error: " + util.inspect(err)); + client.once('error', function(err) { + //console.log('error', util.inspect(err)); + assert.fail('Client shoud not throw error during query execution'); }); client.on('drain', client.end.bind(client)); return client; @@ -18,11 +19,8 @@ test('error handling', function(){ var query = client.query("select omfg from yodas_dsflsd where pixistix = 'zoiks!!!'"); assert.emits(query, 'error', function(error) { - test('error is a psql error', function() { - assert.equal(error.severity, "ERROR"); - }); + assert.equal(error.severity, "ERROR"); }); - }); test('within a prepared statement', function() { @@ -108,31 +106,41 @@ test('non-error calls supplied callback', function() { }); client.connect(assert.calls(function(err) { - assert.isNull(err); + assert.ifError(err); client.end(); })) }); test('when connecting to invalid host', function() { - return false; + //this test fails about 30% on travis and only on travis... + //I'm not sure what the cause could be + if(process.env.TRAVIS) return false; + var client = new Client({ user: 'aslkdjfsdf', password: '1234', host: 'asldkfjasdf!!#1308140.com' }); - assert.emits(client, 'error'); + + var delay = 5000; + var tid = setTimeout(function() { + var msg = "When connecting to an invalid host the error event should be emitted but it has been " + delay + " and still no error event." + assert(false, msg); + }, delay); + client.on('error', function() { + clearTimeout(tid); + }) client.connect(); }); test('when connecting to invalid host with callback', function() { - return false; var client = new Client({ user: 'brian', password: '1234', host: 'asldkfjasdf!!#1308140.com' }); client.connect(function(error, client) { - assert.ok(error); + assert(error); }); }); @@ -155,9 +163,19 @@ test('multiple connection errors (gh#31)', function() { }); test('with callback method', function() { - var badConString = "tcp://aslkdfj:oi14081@"+helper.args.host+":"+helper.args.port+"/"+helper.args.database; + var badConString = "postgres://aslkdfj:oi14081@"+helper.args.host+":"+helper.args.port+"/"+helper.args.database; return false; }); +}); +test('query receives error on client shutdown', function() { + var client = new Client(helper.config); + client.connect(assert.calls(function() { + client.query('SELECT pg_sleep(5)', assert.calls(function(err, res) { + assert(err); + })); + client.end(); + assert.emits(client, 'end'); + })); }); diff --git a/test/integration/client/force-native-with-envvar-tests.js b/test/integration/client/force-native-with-envvar-tests.js new file mode 100644 index 000000000..0ac3098e2 --- /dev/null +++ b/test/integration/client/force-native-with-envvar-tests.js @@ -0,0 +1,39 @@ +return; +/** + * helper needs to be loaded for the asserts but it alos proloads + * client which we don't want here + * + */ +var helper = require(__dirname+"/test-helper") + , path = require('path') +; + +var paths = { + 'pg' : path.join(__dirname, '..', '..', '..', 'lib', 'index.js') , + 'query_js' : path.join(__dirname, '..', '..', '..', 'lib', 'query.js') , + 'query_native' : path.join(__dirname, '..', '..', '..', 'lib', 'native', 'query.js') , +}; + +/** + * delete the modules we are concerned about from the + * module cache, so they get loaded cleanly and the env + * var can kick in ... + */ +function emptyCache(){ + Object.keys(require.cache).forEach(function(key){ + delete require.cache[key]; + }); +}; + +emptyCache(); +process.env.NODE_PG_FORCE_NATIVE = '1'; + +var pg = require( paths.pg ); +var query_native = require( paths.query_native ); +var query_js = require( paths.query_js ); + +assert.deepEqual(pg.Client.Query, query_native); +assert.notDeepEqual(pg.Client.Query, query_js); + +emptyCache(); +delete process.env.NODE_PG_FORCE_NATIVE diff --git a/test/integration/client/huge-numeric-tests.js b/test/integration/client/huge-numeric-tests.js index b2a89f122..8db3f2965 100644 --- a/test/integration/client/huge-numeric-tests.js +++ b/test/integration/client/huge-numeric-tests.js @@ -1,7 +1,7 @@ var helper = require(__dirname + '/test-helper'); -helper.pg.connect(helper.config, assert.success(function(client) { - var types = require(__dirname + '/../../../lib/types'); +helper.pg.connect(helper.config, assert.success(function(client, done) { + var types = require('pg-types'); //1231 = numericOID types.setTypeParser(1700, function(){ return 'yes'; @@ -15,6 +15,7 @@ helper.pg.connect(helper.config, assert.success(function(client) { client.query('SELECT * FROM bignumz', assert.success(function(result) { assert.equal(result.rows[0].id, 'yes') helper.pg.end(); + done(); })) })); diff --git a/test/integration/client/json-type-parsing-tests.js b/test/integration/client/json-type-parsing-tests.js new file mode 100644 index 000000000..1c0759bf3 --- /dev/null +++ b/test/integration/client/json-type-parsing-tests.js @@ -0,0 +1,38 @@ +var helper = require(__dirname + '/test-helper'); +var assert = require('assert'); +//if you want binary support, pull request me! +if (helper.config.binary) { + console.log('binary mode does not support JSON right now'); + return; +} + +test('can read and write json', function() { + helper.pg.connect(helper.config, function(err, client, done) { + assert.ifError(err); + helper.versionGTE(client, '9.2.0', assert.success(function(jsonSupported) { + if(!jsonSupported) { + console.log('skip json test on older versions of postgres'); + done(); + return helper.pg.end(); + } + client.query('CREATE TEMP TABLE stuff(id SERIAL PRIMARY KEY, data JSON)'); + var value ={name: 'Brian', age: 250, alive: true, now: new Date()}; + client.query('INSERT INTO stuff (data) VALUES ($1)', [value]); + client.query('SELECT * FROM stuff', assert.success(function(result) { + assert.equal(result.rows.length, 1); + assert.equal(typeof result.rows[0].data, 'object'); + var row = result.rows[0].data; + assert.strictEqual(row.name, value.name); + assert.strictEqual(row.age, value.age); + assert.strictEqual(row.alive, value.alive); + test('row should have "now" as a date', function() { + return false; + assert(row.now instanceof Date, 'row.now should be a date instance but is ' + typeof row.now); + }); + assert.equal(JSON.stringify(row.now), JSON.stringify(value.now)); + done(); + helper.pg.end(); + })); + })); + }); +}); diff --git a/test/integration/client/no-row-result-tests.js b/test/integration/client/no-row-result-tests.js new file mode 100644 index 000000000..5555ff6f2 --- /dev/null +++ b/test/integration/client/no-row-result-tests.js @@ -0,0 +1,23 @@ +var helper = require(__dirname + '/test-helper'); +var pg = helper.pg; +var config = helper.config; + +test('can access results when no rows are returned', function() { + if(config.native) return false; + var checkResult = function(result) { + assert(result.fields, 'should have fields definition'); + assert.equal(result.fields.length, 1); + assert.equal(result.fields[0].name, 'val'); + assert.equal(result.fields[0].dataTypeID, 25); + pg.end(); + }; + + pg.connect(config, assert.success(function(client, done) { + var query = client.query('select $1::text as val limit 0', ['hi'], assert.success(function(result) { + checkResult(result); + done(); + })); + + assert.emits(query, 'end', checkResult); + })); +}); diff --git a/test/integration/client/notice-tests.js b/test/integration/client/notice-tests.js index 4c6920ac7..764b45cd1 100644 --- a/test/integration/client/notice-tests.js +++ b/test/integration/client/notice-tests.js @@ -1,4 +1,5 @@ var helper = require(__dirname + '/test-helper'); + test('emits notice message', function() { //TODO this doesn't work on all versions of postgres return false; diff --git a/test/integration/client/parse-int-8-tests.js b/test/integration/client/parse-int-8-tests.js new file mode 100644 index 000000000..42228cb8b --- /dev/null +++ b/test/integration/client/parse-int-8-tests.js @@ -0,0 +1,25 @@ + +var helper = require(__dirname + '/../test-helper'); +var pg = helper.pg; +test('ability to turn on and off parser', function() { + if(helper.args.binary) return false; + pg.connect(helper.config, assert.success(function(client, done) { + pg.defaults.parseInt8 = true; + client.query('CREATE TEMP TABLE asdf(id SERIAL PRIMARY KEY)'); + client.query('SELECT COUNT(*) as "count", \'{1,2,3}\'::bigint[] as array FROM asdf', assert.success(function(res) { + assert.strictEqual(0, res.rows[0].count); + assert.strictEqual(1, res.rows[0].array[0]); + assert.strictEqual(2, res.rows[0].array[1]); + assert.strictEqual(3, res.rows[0].array[2]); + pg.defaults.parseInt8 = false; + client.query('SELECT COUNT(*) as "count", \'{1,2,3}\'::bigint[] as array FROM asdf', assert.success(function(res) { + done(); + assert.strictEqual('0', res.rows[0].count); + assert.strictEqual('1', res.rows[0].array[0]); + assert.strictEqual('2', res.rows[0].array[1]); + assert.strictEqual('3', res.rows[0].array[2]); + pg.end(); + })); + })); + })); +}); diff --git a/test/integration/client/prepared-statement-tests.js b/test/integration/client/prepared-statement-tests.js index ff2fac0d2..478cd007c 100644 --- a/test/integration/client/prepared-statement-tests.js +++ b/test/integration/client/prepared-statement-tests.js @@ -17,6 +17,33 @@ test("simple, unnamed prepared statement", function(){ }); }); +test("use interval in prepared statement", function(){ + return; + var client = helper.client(); + + client.query('SELECT interval \'15 days 2 months 3 years 6:12:05\' as interval', assert.success(function(result) { + var interval = result.rows[0].interval; + + var query = client.query({ + text: 'select cast($1 as interval) as interval', + values: [interval] + }); + + assert.emits(query, 'row', function(row) { + assert.equal(row.interval.seconds, 5); + assert.equal(row.interval.minutes, 12); + assert.equal(row.interval.hours, 6); + assert.equal(row.interval.days, 15); + assert.equal(row.interval.months, 2); + assert.equal(row.interval.years, 3); + }); + + assert.emits(query, 'end', function() { + client.end(); + }); + })); +}); + test("named prepared statement", function() { var client = helper.client(); @@ -82,8 +109,8 @@ test("named prepared statement", function() { test("prepared statements on different clients", function() { var statementName = "differ"; - var statement1 = "select count(*) as count from person"; - var statement2 = "select count(*) as count from person where age < $1"; + var statement1 = "select count(*)::int4 as count from person"; + var statement2 = "select count(*)::int4 as count from person where age < $1"; var client1Finished = false; var client2Finished = false; diff --git a/test/integration/client/query-as-promise-tests.js b/test/integration/client/query-as-promise-tests.js new file mode 100644 index 000000000..8dcdeb517 --- /dev/null +++ b/test/integration/client/query-as-promise-tests.js @@ -0,0 +1,33 @@ +var helper = require(__dirname + '/../test-helper'); +var pg = helper.pg; +var semver = require('semver') + +if (semver.lt(process.version, '0.12.0')) { + return console.log('promises are not supported in node < v0.10') +} + +process.on('unhandledRejection', function(e) { + console.error(e, e.stack) + process.exit(1) +}) + +pg.connect(helper.config, assert.success(function(client, done) { + client.query('SELECT $1::text as name', ['foo']) + .then(function(result) { + assert.equal(result.rows[0].name, 'foo') + return client + }) + .then(function(client) { + client.query('ALKJSDF') + .catch(function(e) { + assert(e instanceof Error) + }) + }) + + client.query('SELECT 1 as num') + .then(function(result) { + assert.equal(result.rows[0].num, 1) + done() + pg.end() + }) +})) diff --git a/test/integration/client/query-column-names-tests.js b/test/integration/client/query-column-names-tests.js new file mode 100644 index 000000000..811d673a0 --- /dev/null +++ b/test/integration/client/query-column-names-tests.js @@ -0,0 +1,13 @@ +var helper = require(__dirname + '/../test-helper'); +var pg = helper.pg; + +test('support for complex column names', function() { + pg.connect(helper.config, assert.success(function(client, done) { + client.query("CREATE TEMP TABLE t ( \"complex''column\" TEXT )"); + client.query('SELECT * FROM t', assert.success(function(res) { + done(); + assert.strictEqual(res.fields[0].name, "complex''column"); + pg.end(); + })); + })); +}); \ No newline at end of file diff --git a/test/integration/client/query-error-handling-prepared-statement-tests.js b/test/integration/client/query-error-handling-prepared-statement-tests.js new file mode 100644 index 000000000..151646586 --- /dev/null +++ b/test/integration/client/query-error-handling-prepared-statement-tests.js @@ -0,0 +1,83 @@ +var helper = require(__dirname + '/test-helper'); +var util = require('util'); + +function killIdleQuery(targetQuery) { + var client2 = new Client(helper.args); + var pidColName = 'procpid' + var queryColName = 'current_query'; + client2.connect(assert.success(function() { + helper.versionGTE(client2, '9.2.0', assert.success(function(isGreater) { + if(isGreater) { + pidColName = 'pid'; + queryColName = 'query'; + } + var killIdleQuery = "SELECT " + pidColName + ", (SELECT pg_terminate_backend(" + pidColName + ")) AS killed FROM pg_stat_activity WHERE " + queryColName + " = $1"; + client2.query(killIdleQuery, [targetQuery], assert.calls(function(err, res) { + assert.ifError(err); + assert.equal(res.rows.length, 1); + client2.end(); + assert.emits(client2, 'end'); + })); + })); + })); +} + +test('query killed during query execution of prepared statement', function() { + if(helper.args.native) { + return false; + } + var client = new Client(helper.args); + client.connect(assert.success(function() { + var sleepQuery = 'select pg_sleep($1)'; + var query1 = client.query({ + name: 'sleep query', + text: sleepQuery, + values: [5] }, + assert.calls(function(err, result) { + assert.equal(err.message, 'terminating connection due to administrator command'); + })); + + query1.on('error', function(err) { + assert.fail('Prepared statement should not emit error'); + }); + + query1.on('row', function(row) { + assert.fail('Prepared statement should not emit row'); + }); + + query1.on('end', function(err) { + assert.fail('Prepared statement when executed should not return before being killed'); + }); + + killIdleQuery(sleepQuery); + })); +}); + + +test('client end during query execution of prepared statement', function() { + var client = new Client(helper.args); + client.connect(assert.success(function() { + var sleepQuery = 'select pg_sleep($1)'; + var query1 = client.query({ + name: 'sleep query', + text: sleepQuery, + values: [5] }, + assert.calls(function(err, result) { + assert.equal(err.message, 'Connection terminated'); + })); + + query1.on('error', function(err) { + assert.fail('Prepared statement should not emit error'); + }); + + query1.on('row', function(row) { + assert.fail('Prepared statement should not emit row'); + }); + + query1.on('end', function(err) { + assert.fail('Prepared statement when executed should not return before being killed'); + }); + + client.end(); + })); +}); diff --git a/test/integration/client/query-error-handling-tests.js b/test/integration/client/query-error-handling-tests.js new file mode 100644 index 000000000..2618a49df --- /dev/null +++ b/test/integration/client/query-error-handling-tests.js @@ -0,0 +1,87 @@ +var helper = require(__dirname + '/test-helper'); +var util = require('util'); + +test('error during query execution', function() { + var client = new Client(helper.args); + client.connect(assert.success(function() { + var sleepQuery = 'select pg_sleep(5)'; + var pidColName = 'procpid' + var queryColName = 'current_query'; + helper.versionGTE(client, '9.2.0', assert.success(function(isGreater) { + if(isGreater) { + pidColName = 'pid'; + queryColName = 'query'; + } + var query1 = client.query(sleepQuery, assert.calls(function(err, result) { + assert(err); + client.end(); + })); + //ensure query1 does not emit an 'end' event + //because it was killed and received an error + //https://github.com/brianc/node-postgres/issues/547 + query1.on('end', function() { + assert.fail('Query with an error should not emit "end" event') + }) + setTimeout(function() { + var client2 = new Client(helper.args); + client2.connect(assert.success(function() { + var killIdleQuery = "SELECT " + pidColName + ", (SELECT pg_terminate_backend(" + pidColName + ")) AS killed FROM pg_stat_activity WHERE " + queryColName + " = $1"; + client2.query(killIdleQuery, [sleepQuery], assert.calls(function(err, res) { + assert.ifError(err); + assert.equal(res.rows.length, 1); + client2.end(); + assert.emits(client2, 'end'); + })); + })); + }, 100) + })); + })); +}); + +if(helper.config.native) return; + +test('9.3 column error fields', function() { + var client = new Client(helper.args); + client.connect(assert.success(function() { + helper.versionGTE(client, '9.3.0', assert.success(function(isGreater) { + if(!isGreater) { + return client.end(); + } + + client.query('DROP TABLE IF EXISTS column_err_test'); + client.query('CREATE TABLE column_err_test(a int NOT NULL)'); + client.query('INSERT INTO column_err_test(a) VALUES (NULL)', function (err) { + assert.equal(err.severity, 'ERROR'); + assert.equal(err.code, '23502'); + assert.equal(err.schema, 'public'); + assert.equal(err.table, 'column_err_test'); + assert.equal(err.column, 'a'); + return client.end(); + }); + })); + })); +}); + +test('9.3 constraint error fields', function() { + var client = new Client(helper.args); + client.connect(assert.success(function() { + helper.versionGTE(client, '9.3.0', assert.success(function(isGreater) { + if(!isGreater) { + console.log('skip 9.3 error field on older versions of postgres'); + return client.end(); + } + + client.query('DROP TABLE IF EXISTS constraint_err_test'); + client.query('CREATE TABLE constraint_err_test(a int PRIMARY KEY)'); + client.query('INSERT INTO constraint_err_test(a) VALUES (1)'); + client.query('INSERT INTO constraint_err_test(a) VALUES (1)', function (err) { + assert.equal(err.severity, 'ERROR'); + assert.equal(err.code, '23505'); + assert.equal(err.schema, 'public'); + assert.equal(err.table, 'constraint_err_test'); + assert.equal(err.constraint, 'constraint_err_test_pkey'); + return client.end(); + }); + })); + })); +}); diff --git a/test/integration/client/quick-disconnect-tests.js b/test/integration/client/quick-disconnect-tests.js new file mode 100644 index 000000000..a1b6bab61 --- /dev/null +++ b/test/integration/client/quick-disconnect-tests.js @@ -0,0 +1,7 @@ +//test for issue #320 +// +var helper = require('./test-helper'); + +var client = new helper.pg.Client(helper.config); +client.connect(); +client.end(); diff --git a/test/integration/client/result-metadata-tests.js b/test/integration/client/result-metadata-tests.js index 74457bae5..013630033 100644 --- a/test/integration/client/result-metadata-tests.js +++ b/test/integration/client/result-metadata-tests.js @@ -2,22 +2,35 @@ var helper = require(__dirname + "/test-helper"); var pg = helper.pg; test('should return insert metadata', function() { - pg.connect(helper.config, assert.calls(function(err, client) { + pg.connect(helper.config, assert.calls(function(err, client, done) { assert.isNull(err); - client.query("CREATE TEMP TABLE zugzug(name varchar(10))", assert.calls(function(err, result) { - assert.isNull(err); - assert.equal(result.oid, null); - assert.equal(result.command, 'CREATE'); - client.query("INSERT INTO zugzug(name) VALUES('more work?')", assert.calls(function(err, result) { - assert.equal(result.command, "INSERT"); - assert.equal(result.rowCount, 1); - client.query('SELECT * FROM zugzug', assert.calls(function(err, result) { + + helper.versionGTE(client, '9.0.0', assert.success(function(hasRowCount) { + client.query("CREATE TEMP TABLE zugzug(name varchar(10))", assert.calls(function(err, result) { + assert.isNull(err); + assert.equal(result.oid, null); + assert.equal(result.command, 'CREATE'); + + var q = client.query("INSERT INTO zugzug(name) VALUES('more work?')", assert.calls(function(err, result) { assert.isNull(err); + assert.equal(result.command, "INSERT"); assert.equal(result.rowCount, 1); - assert.equal(result.command, 'SELECT'); - process.nextTick(pg.end.bind(pg)); - })) - })) - })) - })) -}) + + client.query('SELECT * FROM zugzug', assert.calls(function(err, result) { + assert.isNull(err); + if(hasRowCount) assert.equal(result.rowCount, 1); + assert.equal(result.command, 'SELECT'); + process.nextTick(pg.end.bind(pg)); + })); + })); + + assert.emits(q, 'end', function(result) { + assert.equal(result.command, "INSERT"); + if(hasRowCount) assert.equal(result.rowCount, 1); + done(); + }); + + })); + })); + })); +}); diff --git a/test/integration/client/results-as-array-tests.js b/test/integration/client/results-as-array-tests.js new file mode 100644 index 000000000..ef11a891c --- /dev/null +++ b/test/integration/client/results-as-array-tests.js @@ -0,0 +1,33 @@ +var util = require('util'); +var helper = require('./test-helper'); + +var Client = helper.Client; + +var conInfo = helper.config; + +test('returns results as array', function() { + var client = new Client(conInfo); + var checkRow = function(row) { + assert(util.isArray(row), 'row should be an array'); + assert.equal(row.length, 4); + assert.equal(row[0].getFullYear(), new Date().getFullYear()); + assert.strictEqual(row[1], 1); + assert.strictEqual(row[2], 'hai'); + assert.strictEqual(row[3], null); + } + client.connect(assert.success(function() { + var config = { + text: 'SELECT NOW(), 1::int, $1::text, null', + values: ['hai'], + rowMode: 'array' + }; + var query = client.query(config, assert.success(function(result) { + assert.equal(result.rows.length, 1); + checkRow(result.rows[0]); + client.end(); + })); + assert.emits(query, 'row', function(row) { + checkRow(row); + }); + })); +}); diff --git a/test/integration/client/row-description-on-results-tests.js b/test/integration/client/row-description-on-results-tests.js new file mode 100644 index 000000000..22c929653 --- /dev/null +++ b/test/integration/client/row-description-on-results-tests.js @@ -0,0 +1,37 @@ +var helper = require('./test-helper'); + +var Client = helper.Client; + +var conInfo = helper.config; + +var checkResult = function(result) { + assert(result.fields); + assert.equal(result.fields.length, 3); + var fields = result.fields; + assert.equal(fields[0].name, 'now'); + assert.equal(fields[1].name, 'num'); + assert.equal(fields[2].name, 'texty'); + assert.equal(fields[0].dataTypeID, 1184); + assert.equal(fields[1].dataTypeID, 23); + assert.equal(fields[2].dataTypeID, 25); +}; + +test('row descriptions on result object', function() { + var client = new Client(conInfo); + client.connect(assert.success(function() { + client.query('SELECT NOW() as now, 1::int as num, $1::text as texty', ["hello"], assert.success(function(result) { + checkResult(result); + client.end(); + })); + })); +}); + +test('row description on no rows', function() { + var client = new Client(conInfo); + client.connect(assert.success(function() { + client.query('SELECT NOW() as now, 1::int as num, $1::text as texty LIMIT 0', ["hello"], assert.success(function(result) { + checkResult(result); + client.end(); + })); + })); +}); diff --git a/test/integration/client/simple-query-tests.js b/test/integration/client/simple-query-tests.js index 2e7791091..e7ffc04a0 100644 --- a/test/integration/client/simple-query-tests.js +++ b/test/integration/client/simple-query-tests.js @@ -36,9 +36,55 @@ test("simple query interface", function() { }); }); +test("simple query interface using addRow", function() { + + var client = helper.client(); + + var query = client.query("select name from person order by name"); + + client.on('drain', client.end.bind(client)); + + query.on('row', function(row, result) { + assert.ok(result); + result.addRow(row); + }); + + query.on('end', function(result) { + assert.lengthIs(result.rows, 26, "result returned wrong number of rows"); + assert.lengthIs(result.rows, result.rowCount); + assert.equal(result.rows[0].name, "Aaron"); + assert.equal(result.rows[25].name, "Zanzabar"); + }); +}); + +test("prepared statements do not mutate params", function() { + + var client = helper.client(); + + var params = [1] + + var query = client.query("select name from person where $1 = 1 order by name", params); + + assert.deepEqual(params, [1]) + + client.on('drain', client.end.bind(client)); + + query.on('row', function(row, result) { + assert.ok(result); + result.addRow(row); + }); + + query.on('end', function(result) { + assert.lengthIs(result.rows, 26, "result returned wrong number of rows"); + assert.lengthIs(result.rows, result.rowCount); + assert.equal(result.rows[0].name, "Aaron"); + assert.equal(result.rows[25].name, "Zanzabar"); + }); +}); + test("multiple simple queries", function() { var client = helper.client(); - client.query({ text: "create temp table bang(id serial, name varchar(5));insert into bang(name) VALUES('boom');", binary: false }) + client.query({ text: "create temp table bang(id serial, name varchar(5));insert into bang(name) VALUES('boom');"}) client.query("insert into bang(name) VALUES ('yes');"); var query = client.query("select name from bang"); assert.emits(query, 'row', function(row) { @@ -52,9 +98,9 @@ test("multiple simple queries", function() { test("multiple select statements", function() { var client = helper.client(); - client.query({text: "create temp table boom(age integer); insert into boom(age) values(1); insert into boom(age) values(2); insert into boom(age) values(3)", binary: false}); - client.query({text: "create temp table bang(name varchar(5)); insert into bang(name) values('zoom');", binary: false}); - var result = client.query({text: "select age from boom where age < 2; select name from bang", binary: false}); + client.query("create temp table boom(age integer); insert into boom(age) values(1); insert into boom(age) values(2); insert into boom(age) values(3)"); + client.query({text: "create temp table bang(name varchar(5)); insert into bang(name) values('zoom');"}); + var result = client.query({text: "select age from boom where age < 2; select name from bang"}); assert.emits(result, 'row', function(row) { assert.strictEqual(row['age'], 1); assert.emits(result, 'row', function(row) { diff --git a/test/integration/client/timezone-tests.js b/test/integration/client/timezone-tests.js new file mode 100644 index 000000000..b355550df --- /dev/null +++ b/test/integration/client/timezone-tests.js @@ -0,0 +1,29 @@ +var helper = require(__dirname + '/../test-helper'); +var exec = require('child_process').exec; + +var oldTz = process.env.TZ; +process.env.TZ = 'Europe/Berlin'; + +var date = new Date(); + +helper.pg.connect(helper.config, function(err, client, done) { + assert.isNull(err); + + test('timestamp without time zone', function() { + client.query("SELECT CAST($1 AS TIMESTAMP WITHOUT TIME ZONE) AS \"val\"", [ date ], function(err, result) { + assert.isNull(err); + assert.equal(result.rows[0].val.getTime(), date.getTime()); + + test('timestamp with time zone', function() { + client.query("SELECT CAST($1 AS TIMESTAMP WITH TIME ZONE) AS \"val\"", [ date ], function(err, result) { + assert.isNull(err); + assert.equal(result.rows[0].val.getTime(), date.getTime()); + + done(); + helper.pg.end(); + process.env.TZ = oldTz; + }); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/integration/client/transaction-tests.js b/test/integration/client/transaction-tests.js index 4fbfd18b9..85ee7e539 100644 --- a/test/integration/client/transaction-tests.js +++ b/test/integration/client/transaction-tests.js @@ -5,8 +5,7 @@ var sink = new helper.Sink(2, function() { }); test('a single connection transaction', function() { - helper.pg.connect(helper.config, assert.calls(function(err, client) { - assert.isNull(err); + helper.pg.connect(helper.config, assert.success(function(client, done) { client.query('begin'); @@ -39,6 +38,7 @@ test('a single connection transaction', function() { client.query(getZed, assert.calls(function(err, result) { assert.isNull(err); assert.empty(result.rows); + done(); sink.add(); })) }) @@ -46,8 +46,7 @@ test('a single connection transaction', function() { }) test('gh#36', function() { - helper.pg.connect(helper.config, function(err, client) { - if(err) throw err; + helper.pg.connect(helper.config, assert.success(function(client, done) { client.query("BEGIN"); client.query({ name: 'X', @@ -67,6 +66,7 @@ test('gh#36', function() { })) client.query("COMMIT", function() { sink.add(); + done(); }) - }) + })); }) diff --git a/test/integration/client/type-coercion-tests.js b/test/integration/client/type-coercion-tests.js index fce8ff33f..d961c291d 100644 --- a/test/integration/client/type-coercion-tests.js +++ b/test/integration/client/type-coercion-tests.js @@ -2,7 +2,7 @@ var helper = require(__dirname + '/test-helper'); var sink; var testForTypeCoercion = function(type){ - helper.pg.connect(helper.config, function(err, client) { + helper.pg.connect(helper.config, function(err, client, done) { assert.isNull(err); client.query("create temp table test_type(col " + type.name + ")", assert.calls(function(err, result) { assert.isNull(err); @@ -23,7 +23,9 @@ var testForTypeCoercion = function(type){ }); assert.emits(query, 'row', function(row) { - assert.strictEqual(row.col, val, "expected " + type.name + " of " + val + " but got " + row.col); + var expected = val + " (" + typeof val + ")"; + var returned = row.col + " (" + typeof row.col + ")"; + assert.strictEqual(row.col, val, "expected " + type.name + " of " + expected + " but got " + returned); }, "row should have been called for " + type.name + " of " + val); client.query('delete from test_type'); @@ -31,6 +33,7 @@ var testForTypeCoercion = function(type){ client.query('drop table test_type', function() { sink.add(); + done(); }); }) })); @@ -39,13 +42,21 @@ var testForTypeCoercion = function(type){ var types = [{ name: 'integer', - values: [1, -1, null] + values: [-2147483648, -1, 0, 1, 2147483647, null] },{ name: 'smallint', - values: [-1, 0, 1, null] + values: [-32768, -1, 0, 1, 32767, null] },{ name: 'bigint', - values: [-10000, 0, 10000, null] + values: [ + '-9223372036854775808', + '-9007199254740992', + '0', + '9007199254740992', + '72057594037928030', + '9223372036854775807', + null + ] },{ name: 'varchar(5)', values: ['yo', '', 'zomg!', null] @@ -56,15 +67,21 @@ var types = [{ name: 'bool', values: [true, false, null] },{ - //TODO get some actual huge numbers here name: 'numeric', - values: [-12.34, 0, 12.34, null] + values: [ + '-12.34', + '0', + '12.34', + '-3141592653589793238462643383279502.1618033988749894848204586834365638', + '3141592653589793238462643383279502.1618033988749894848204586834365638', + null + ] },{ name: 'real', - values: [101.1, 0, -101.3, null] + values: [-101.3, -1.2, 0, 1.2, 101.1, null] },{ name: 'double precision', - values: [-1.2, 0, 1.2, null] + values: [-101.3, -1.2, 0, 1.2, 101.1, null] },{ name: 'timestamptz', values: [null] @@ -82,7 +99,7 @@ var types = [{ // ignore some tests in binary mode if (helper.config.binary) { types = types.filter(function(type) { - return !(type.name in {'real':1, 'timetz':1, 'time':1}); + return !(type.name in {'real': 1, 'timetz':1, 'time':1, 'numeric': 1, 'bigint': 1}); }); } @@ -133,8 +150,43 @@ test("timestampz round trip", function() { client.on('drain', client.end.bind(client)); }); -helper.pg.connect(helper.config, assert.calls(function(err, client) { - assert.isNull(err); +if(!helper.config.binary) { + test('date range extremes', function() { + var client = helper.client(); + client.on('error', function(err) { + console.log(err); + client.end(); + }); + + // Set the server timeszone to the same as used for the test, + // otherwise (if server's timezone is ahead of GMT) in + // textParsers.js::parseDate() the timezone offest is added to the date; + // in the case of "275760-09-13 00:00:00 GMT" the timevalue overflows. + client.query('SET TIMEZONE TO GMT', assert.success(function(res){ + + // PostgreSQL supports date range of 4713 BCE to 294276 CE + // http://www.postgresql.org/docs/9.2/static/datatype-datetime.html + // ECMAScript supports date range of Apr 20 271821 BCE to Sep 13 275760 CE + // http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.1 + client.query('SELECT $1::TIMESTAMPTZ as when', ["275760-09-13 00:00:00 GMT"], assert.success(function(res) { + assert.equal(res.rows[0].when.getFullYear(), 275760); + })); + + client.query('SELECT $1::TIMESTAMPTZ as when', ["4713-12-31 12:31:59 BC GMT"], assert.success(function(res) { + assert.equal(res.rows[0].when.getFullYear(), -4713); + })); + + client.query('SELECT $1::TIMESTAMPTZ as when', ["275760-09-13 00:00:00 -15:00"], assert.success(function(res) { + assert( isNaN(res.rows[0].when.getTime()) ); + })); + + client.on('drain', client.end.bind(client)); + })); + }); +} + +helper.pg.connect(helper.config, assert.calls(function(err, client, done) { + assert.ifError(err); client.query('select null as res;', assert.calls(function(err, res) { assert.isNull(err); assert.strictEqual(res.rows[0].res, null) @@ -143,20 +195,6 @@ helper.pg.connect(helper.config, assert.calls(function(err, client) { assert.isNull(err); assert.strictEqual(res.rows[0].res, null); sink.add(); + done(); }) })) - -if(!helper.config.binary) { - test("postgres date type", function() { - var client = helper.client(); - client.on('error', function(err) { - console.log(err); - client.end(); - }); - client.query("SELECT '2010-10-31'::date", assert.calls(function(err, result){ - assert.isNull(err); - assert.UTCDate(result.rows[0].date, 2010, 9, 31, 0, 0, 0, 0); - })); - client.on('drain', client.end.bind(client)); - }); -} diff --git a/test/integration/client/type-parser-override-tests.js b/test/integration/client/type-parser-override-tests.js new file mode 100644 index 000000000..68a5de7f7 --- /dev/null +++ b/test/integration/client/type-parser-override-tests.js @@ -0,0 +1,34 @@ +var helper = require(__dirname + '/test-helper'); + +function testTypeParser(client, expectedResult, done) { + var boolValue = true; + client.query('CREATE TEMP TABLE parserOverrideTest(id bool)'); + client.query('INSERT INTO parserOverrideTest(id) VALUES ($1)', [boolValue]); + client.query('SELECT * FROM parserOverrideTest', assert.success(function(result) { + assert.equal(result.rows[0].id, expectedResult); + helper.pg.end(); + done(); + })); +} + +helper.pg.connect(helper.config, assert.success(function(client1, done1) { + helper.pg.connect(helper.config, assert.success(function(client2, done2) { + var boolTypeOID = 16; + client1.setTypeParser(boolTypeOID, function(){ + return 'first client'; + }); + client2.setTypeParser(boolTypeOID, function(){ + return 'second client'; + }); + + client1.setTypeParser(boolTypeOID, 'binary', function(){ + return 'first client binary'; + }); + client2.setTypeParser(boolTypeOID, 'binary', function(){ + return 'second client binary'; + }); + + testTypeParser(client1, 'first client', done1); + testTypeParser(client2, 'second client', done2); + })); +})); diff --git a/test/integration/connection-pool/ending-empty-pool-tests.js b/test/integration/connection-pool/ending-empty-pool-tests.js new file mode 100644 index 000000000..4f5dd80ad --- /dev/null +++ b/test/integration/connection-pool/ending-empty-pool-tests.js @@ -0,0 +1,15 @@ +var helper = require(__dirname + '/test-helper') + +var called = false; +test('disconnects', function() { + called = true; + var eventSink = new helper.Sink(1, function() {}); + helper.pg.on('end', function() { + eventSink.add(); + }); + + //this should exit the process + helper.pg.end(); +}) + + diff --git a/test/integration/connection-pool/ending-pool-tests.js b/test/integration/connection-pool/ending-pool-tests.js index e46c0fc1b..83f4b1bc2 100644 --- a/test/integration/connection-pool/ending-pool-tests.js +++ b/test/integration/connection-pool/ending-pool-tests.js @@ -4,17 +4,23 @@ var called = false; test('disconnects', function() { var sink = new helper.Sink(4, function() { called = true; + var eventSink = new helper.Sink(1, function() {}); + helper.pg.on('end', function() { + eventSink.add(); + }); + //this should exit the process, killing each connection pool helper.pg.end(); }); [helper.config, helper.config, helper.config, helper.config].forEach(function(config) { - helper.pg.connect(config, function(err, client) { + helper.pg.connect(config, function(err, client, done) { assert.isNull(err); client.query("SELECT * FROM NOW()", function(err, result) { - process.nextTick(function() { + setTimeout(function() { assert.equal(called, false, "Should not have disconnected yet") sink.add(); - }) + done(); + }, 0) }) }) }) diff --git a/test/integration/connection-pool/error-tests.js b/test/integration/connection-pool/error-tests.js index 11badf04a..2cf0501fa 100644 --- a/test/integration/connection-pool/error-tests.js +++ b/test/integration/connection-pool/error-tests.js @@ -1,28 +1,40 @@ var helper = require(__dirname + "/../test-helper"); var pg = require(__dirname + "/../../../lib"); -helper.pg = pg; //first make pool hold 2 clients -helper.pg.defaults.poolSize = 2; - -var killIdleQuery = 'SELECT procpid, (SELECT pg_terminate_backend(procpid)) AS killed FROM pg_stat_activity WHERE current_query LIKE \'\''; +pg.defaults.poolSize = 2; //get first client -helper.pg.connect(helper.config, assert.success(function(client) { +pg.connect(helper.config, assert.success(function(client, done) { client.id = 1; - helper.pg.connect(helper.config, assert.success(function(client2) { - client2.id = 2; - //subscribe to the pg error event - assert.emits(helper.pg, 'error', function(error, brokenClient) { - assert.ok(error); - assert.ok(brokenClient); - assert.equal(client.id, brokenClient.id); - helper.pg.end(); - }); - //kill the connection from client - client2.query(killIdleQuery, assert.success(function(res) { - //check to make sure client connection actually was killed - assert.lengthIs(res.rows, 1); + client.query('SELECT NOW()', function() { + pg.connect(helper.config, assert.success(function(client2, done2) { + client2.id = 2; + var pidColName = 'procpid'; + helper.versionGTE(client2, '9.2.0', assert.success(function(isGreater) { + var killIdleQuery = 'SELECT pid, (SELECT pg_terminate_backend(pid)) AS killed FROM pg_stat_activity WHERE state = $1'; + var params = ['idle']; + if(!isGreater) { + killIdleQuery = 'SELECT procpid, (SELECT pg_terminate_backend(procpid)) AS killed FROM pg_stat_activity WHERE current_query LIKE $1'; + params = ['%IDLE%'] + } + + //subscribe to the pg error event + assert.emits(pg, 'error', function(error, brokenClient) { + assert.ok(error); + assert.ok(brokenClient); + assert.equal(client.id, brokenClient.id); + }); + + //kill the connection from client + client2.query(killIdleQuery, params, assert.success(function(res) { + //check to make sure client connection actually was killed + //return client2 to the pool + done2(); + pg.end(); + })); + })); })); - })); + + }) })); diff --git a/test/integration/connection-pool/idle-timeout-tests.js b/test/integration/connection-pool/idle-timeout-tests.js index c6cbbd9f6..0a60ce504 100644 --- a/test/integration/connection-pool/idle-timeout-tests.js +++ b/test/integration/connection-pool/idle-timeout-tests.js @@ -1,12 +1,14 @@ var helper = require(__dirname + '/test-helper'); +var _ = require('lodash') -helper.pg.defaults.poolIdleTimeout = 200; +const config = _.extend({ }, helper.config, { idleTimeoutMillis: 50 }) test('idle timeout', function() { - helper.pg.connect(helper.config, assert.calls(function(err, client) { + helper.pg.connect(config, assert.calls(function(err, client, done) { assert.isNull(err); client.query('SELECT NOW()'); - //just let this one time out + //just let this one time out //test will hang if pool doesn't timeout + done(); })); }); diff --git a/test/integration/connection-pool/max-connection-tests.js b/test/integration/connection-pool/max-connection-tests.js index 61755a0b8..68c0773f9 100644 --- a/test/integration/connection-pool/max-connection-tests.js +++ b/test/integration/connection-pool/max-connection-tests.js @@ -1,3 +1,2 @@ var helper = require(__dirname + "/test-helper") -helper.testPoolSize(10); -helper.testPoolSize(11); +helper.testPoolSize(40); diff --git a/test/integration/connection-pool/native-instance-tests.js b/test/integration/connection-pool/native-instance-tests.js new file mode 100644 index 000000000..06fbdb45b --- /dev/null +++ b/test/integration/connection-pool/native-instance-tests.js @@ -0,0 +1,15 @@ +var helper = require(__dirname + "/../test-helper") +var pg = helper.pg +var native = helper.args.native + +var pool = new pg.Pool() + +pool.connect(assert.calls(function(err, client, done) { + if (native) { + assert(client.native) + } else { + assert(!client.native) + } + done() + pool.end() +})) diff --git a/test/integration/connection-pool/optional-config-tests.js b/test/integration/connection-pool/optional-config-tests.js index d3ddc509c..f0ba2e76e 100644 --- a/test/integration/connection-pool/optional-config-tests.js +++ b/test/integration/connection-pool/optional-config-tests.js @@ -8,7 +8,13 @@ helper.pg.defaults.port = helper.args.port; helper.pg.defaults.database = helper.args.database; helper.pg.defaults.poolSize = 1; -helper.pg.connect(assert.calls(function(err, client) { +helper.pg.connect(assert.calls(function(err, client, done) { assert.isNull(err); - client.end(); + client.query('SELECT NOW()'); + client.once('drain', function() { + setTimeout(function() { + helper.pg.end(); + done(); + }, 10); + }); })); diff --git a/test/integration/connection-pool/test-helper.js b/test/integration/connection-pool/test-helper.js index cc86677d7..199407cd5 100644 --- a/test/integration/connection-pool/test-helper.js +++ b/test/integration/connection-pool/test-helper.js @@ -9,7 +9,7 @@ helper.testPoolSize = function(max) { for(var i = 0; i < max; i++) { helper.pg.poolSize = 10; test("connection #" + i + " executes", function() { - helper.pg.connect(helper.config, function(err, client) { + helper.pg.connect(helper.config, function(err, client, done) { assert.isNull(err); client.query("select * from person", function(err, result) { assert.lengthIs(result.rows, 26) @@ -19,7 +19,8 @@ helper.testPoolSize = function(max) { }) var query = client.query("SELECT * FROM NOW()") query.on('end',function() { - sink.add() + sink.add(); + done(); }) }) }) diff --git a/test/integration/connection-pool/unique-name-tests.js b/test/integration/connection-pool/unique-name-tests.js deleted file mode 100644 index a92a00414..000000000 --- a/test/integration/connection-pool/unique-name-tests.js +++ /dev/null @@ -1,63 +0,0 @@ -var helper = require(__dirname + '/test-helper'); - -helper.pg.defaults.poolSize = 1; -helper.pg.defaults.user = helper.args.user; -helper.pg.defaults.password = helper.args.password; -helper.pg.defaults.database = helper.args.database; -helper.pg.defaults.port = helper.args.port; -helper.pg.defaults.host = helper.args.host; -helper.pg.defaults.binary = helper.args.binary; -helper.pg.defaults.poolIdleTimeout = 100; - -var moreArgs = {}; -for (c in helper.config) { - moreArgs[c] = helper.config[c]; -} -moreArgs.zomg = true; - -var badArgs = {}; -for (c in helper.config) { - badArgs[c] = helper.config[c]; -} - -badArgs.user = badArgs.user + 'laksdjfl'; -badArgs.password = badArgs.password + 'asldkfjlas'; -badArgs.zomg = true; - -test('connecting with complete config', function() { - - helper.pg.connect(helper.config, assert.calls(function(err, client) { - assert.isNull(err); - client.iGotAccessed = true; - client.query("SELECT NOW()") - })); - -}); - -test('connecting with different config object', function() { - - helper.pg.connect(moreArgs, assert.calls(function(err, client) { - assert.isNull(err); - assert.ok(client.iGotAccessed === true) - client.query("SELECT NOW()"); - })) - -}); - -test('connecting with all defaults', function() { - - helper.pg.connect(assert.calls(function(err, client) { - assert.isNull(err); - assert.ok(client.iGotAccessed === true); - client.end(); - })); - -}); - -test('connecting with invalid config', function() { - - helper.pg.connect(badArgs, assert.calls(function(err, client) { - assert.ok(err != null, "Expected connection error using invalid connection credentials"); - })); - -}); diff --git a/test/integration/connection-pool/yield-support-body.js b/test/integration/connection-pool/yield-support-body.js new file mode 100644 index 000000000..943ab3a2e --- /dev/null +++ b/test/integration/connection-pool/yield-support-body.js @@ -0,0 +1,28 @@ +var helper = require('./test-helper') +var co = require('co') + +var tid = setTimeout(function() { + throw new Error('Tests did not complete in time') +}, 1000) + +co(function * () { + var client = yield helper.pg.connect() + var res = yield client.query('SELECT $1::text as name', ['foo']) + assert.equal(res.rows[0].name, 'foo') + + var threw = false + try { + yield client.query('SELECT LKDSJDSLKFJ') + } catch(e) { + threw = true + } + assert(threw) + client.release() + helper.pg.end() + clearTimeout(tid) +}) +.catch(function(e) { + setImmediate(function() { + throw e + }) +}) diff --git a/test/integration/connection-pool/yield-support-tests.js b/test/integration/connection-pool/yield-support-tests.js new file mode 100644 index 000000000..fb79ddc4c --- /dev/null +++ b/test/integration/connection-pool/yield-support-tests.js @@ -0,0 +1,5 @@ +var semver = require('semver') +if (semver.lt(process.version, '1.0.0')) { + return console.log('yield is not supported in node <= v0.12') +} +require('./yield-support-body') diff --git a/test/integration/connection/bound-command-tests.js b/test/integration/connection/bound-command-tests.js index a079f50d7..9d40e5bce 100644 --- a/test/integration/connection/bound-command-tests.js +++ b/test/integration/connection/bound-command-tests.js @@ -1,5 +1,5 @@ var helper = require(__dirname + '/test-helper'); -http://developer.postgresql.org/pgdocs/postgres/protocol-flow.html#PROTOCOL-FLOW-EXT-QUERY +//http://developer.postgresql.org/pgdocs/postgres/protocol-flow.html#PROTOCOL-FLOW-EXT-QUERY test('flushing once', function() { helper.connect(function(con) { diff --git a/test/integration/connection/copy-tests.js b/test/integration/connection/copy-tests.js new file mode 100644 index 000000000..ee4a71c59 --- /dev/null +++ b/test/integration/connection/copy-tests.js @@ -0,0 +1,44 @@ +var helper = require(__dirname+"/test-helper"); +var assert = require('assert'); + +test('COPY FROM events check', function () { + helper.connect(function (con) { + var stdinStream = con.query('COPY person FROM STDIN'); + con.on('copyInResponse', function () { + con.endCopyFrom(); + }); + assert.emits(con, 'copyInResponse', + function () { + con.endCopyFrom(); + }, + "backend should emit copyInResponse after COPY FROM query" + ); + assert.emits(con, 'commandComplete', + function () { + con.end(); + }, + "backend should emit commandComplete after COPY FROM stream ends" + ) + }); +}); +test('COPY TO events check', function () { + helper.connect(function (con) { + var stdoutStream = con.query('COPY person TO STDOUT'); + assert.emits(con, 'copyOutResponse', + function () { + }, + "backend should emit copyOutResponse after COPY TO query" + ); + assert.emits(con, 'copyData', + function () { + }, + "backend should emit copyData on every data row" + ); + assert.emits(con, 'copyDone', + function () { + con.end(); + }, + "backend should emit copyDone after all data rows" + ); + }); +}); diff --git a/test/integration/domain-tests.js b/test/integration/domain-tests.js new file mode 100644 index 000000000..a51f13dda --- /dev/null +++ b/test/integration/domain-tests.js @@ -0,0 +1,58 @@ +var helper = require('./test-helper') +var async = require('async') + +var testWithoutDomain = function(cb) { + test('no domain', function() { + assert(!process.domain) + helper.pg.connect(helper.config, assert.success(function(client, done) { + assert(!process.domain) + done() + cb() + })) + }) +} + +var testWithDomain = function(cb) { + test('with domain', function() { + assert(!process.domain) + var domain = require('domain').create() + domain.run(function() { + var startingDomain = process.domain + assert(startingDomain) + helper.pg.connect(helper.config, assert.success(function(client, done) { + assert(process.domain, 'no domain exists in connect callback') + assert.equal(startingDomain, process.domain, 'domain was lost when checking out a client') + var query = client.query('SELECT NOW()', assert.success(function() { + assert(process.domain, 'no domain exists in query callback') + assert.equal(startingDomain, process.domain, 'domain was lost when checking out a client') + done(true) + process.domain.exit() + cb() + })) + })) + }) + }) +} + +var testErrorWithDomain = function(cb) { + test('error on domain', function() { + var domain = require('domain').create() + domain.on('error', function() { + cb() + }) + domain.run(function() { + helper.pg.connect(helper.config, assert.success(function(client, done) { + client.query('SELECT SLDKJFLSKDJF') + client.on('drain', done) + })) + }) + }) +} + +async.series([ + testWithoutDomain, + testWithDomain, + testErrorWithDomain +], function() { + helper.pg.end() +}) diff --git a/test/integration/gh-issues/130.js b/test/integration/gh-issues/130-tests.js similarity index 56% rename from test/integration/gh-issues/130.js rename to test/integration/gh-issues/130-tests.js index 34670a69b..fba9d4b45 100644 --- a/test/integration/gh-issues/130.js +++ b/test/integration/gh-issues/130-tests.js @@ -6,7 +6,11 @@ helper.pg.defaults.poolIdleTimeout = 1000; helper.pg.connect(helper.config, function(err,client) { client.query("SELECT pg_backend_pid()", function(err, result) { var pid = result.rows[0].pg_backend_pid; - exec('psql -c "select pg_terminate_backend('+pid+')" template1', assert.calls(function (error, stdout, stderr) { + var psql = 'psql'; + if (helper.args.host) psql = psql+' -h '+helper.args.host; + if (helper.args.port) psql = psql+' -p '+helper.args.port; + if (helper.args.user) psql = psql+' -U '+helper.args.user; + exec(psql+' -c "select pg_terminate_backend('+pid+')" template1', assert.calls(function (error, stdout, stderr) { assert.isNull(error); })); }); diff --git a/test/integration/gh-issues/131.js b/test/integration/gh-issues/131-tests.js similarity index 90% rename from test/integration/gh-issues/131.js rename to test/integration/gh-issues/131-tests.js index 74f35c121..eee5086a6 100644 --- a/test/integration/gh-issues/131.js +++ b/test/integration/gh-issues/131-tests.js @@ -2,7 +2,7 @@ var helper = require(__dirname + "/../test-helper"); var pg = helper.pg; test('parsing array results', function() { - pg.connect(helper.config, assert.calls(function(err, client) { + pg.connect(helper.config, assert.calls(function(err, client, done) { assert.isNull(err); client.query("CREATE TEMP TABLE why(names text[], numbors integer[], decimals double precision[])"); client.query('INSERT INTO why(names, numbors, decimals) VALUES(\'{"aaron", "brian","a b c" }\', \'{1, 2, 3}\', \'{.1, 0.05, 3.654}\')').on('error', console.log); @@ -12,6 +12,7 @@ test('parsing array results', function() { assert.equal(result.rows[0].decimals[0], 0.1); assert.equal(result.rows[0].decimals[1], 0.05); assert.equal(result.rows[0].decimals[2], 3.654); + done() pg.end(); })) }) diff --git a/test/integration/gh-issues/199-tests.js b/test/integration/gh-issues/199-tests.js new file mode 100644 index 000000000..b60477fdc --- /dev/null +++ b/test/integration/gh-issues/199-tests.js @@ -0,0 +1,21 @@ +var helper = require('../test-helper'); +var client = helper.client(); + +client.query('CREATE TEMP TABLE arrtest (n integer, s varchar)'); +client.query("INSERT INTO arrtest VALUES (4, 'foo'), (5, 'bar'), (6, 'baz');"); + +var qText = "SELECT \ +ARRAY[1, 2, 3] AS b,\ +ARRAY['xx', 'yy', 'zz'] AS c,\ +ARRAY(SELECT n FROM arrtest) AS d,\ +ARRAY(SELECT s FROM arrtest) AS e;"; + +client.query(qText, function(err, result) { + if(err) throw err; + var row = result.rows[0]; + for(var key in row) { + assert.equal(typeof row[key], 'object'); + assert.equal(row[key].length, 3); + } + client.end(); +}); diff --git a/test/integration/gh-issues/507-tests.js b/test/integration/gh-issues/507-tests.js new file mode 100644 index 000000000..ef75effc6 --- /dev/null +++ b/test/integration/gh-issues/507-tests.js @@ -0,0 +1,15 @@ +var helper = require(__dirname + "/../test-helper"); +var pg = helper.pg; + +test('parsing array results', function() { + pg.connect(helper.config, assert.success(function(client, done) { + client.query('CREATE TEMP TABLE test_table(bar integer, "baz\'s" integer)') + client.query('INSERT INTO test_table(bar, "baz\'s") VALUES(1, 1), (2, 2)') + client.query('SELECT * FROM test_table', function(err, res) { + assert.equal(res.rows[0]["baz's"], 1) + assert.equal(res.rows[1]["baz's"], 2) + done() + pg.end() + }) + })) +}) diff --git a/test/integration/gh-issues/600-tests.js b/test/integration/gh-issues/600-tests.js new file mode 100644 index 000000000..0476d42d9 --- /dev/null +++ b/test/integration/gh-issues/600-tests.js @@ -0,0 +1,77 @@ +var async = require('async'); +var helper = require('../test-helper'); + +var db = helper.client(); + +function createTableFoo(callback){ + db.query("create temp table foo(column1 int, column2 int)", callback); +} + +function createTableBar(callback){ + db.query("create temp table bar(column1 text, column2 text)", callback); +} + +function insertDataFoo(callback){ + db.query({ + name: 'insertFoo', + text: 'insert into foo values($1,$2)', + values:['one','two'] + }, callback ); +} + +function insertDataBar(callback){ + db.query({ + name: 'insertBar', + text: 'insert into bar values($1,$2)', + values:['one','two'] + }, callback ); +} + +function startTransaction(callback) { + db.query('BEGIN', callback); +} +function endTransaction(callback) { + db.query('COMMIT', callback); +} + +function doTransaction(callback) { + // The transaction runs startTransaction, then all queries, then endTransaction, + // no matter if there has been an error in a query in the middle. + startTransaction(function() { + insertDataFoo(function() { + insertDataBar(function() { + endTransaction( callback ); + }); + }); + }); +} + +var steps = [ + createTableFoo, + createTableBar, + doTransaction, + insertDataBar +] + +test('test if query fails', function() { + async.series(steps, assert.success(function() { + db.end() + })) +}) + +test('test if prepare works but bind fails', function() { + var client = helper.client(); + var q = { + text: 'SELECT $1::int as name', + values: ['brian'], + name: 'test' + }; + client.query(q, assert.calls(function(err, res) { + q.values = [1]; + client.query(q, assert.calls(function(err, res) { + assert.ifError(err); + client.end(); + })); + })); +}); + diff --git a/test/integration/gh-issues/675-tests.js b/test/integration/gh-issues/675-tests.js new file mode 100644 index 000000000..f7d95427d --- /dev/null +++ b/test/integration/gh-issues/675-tests.js @@ -0,0 +1,28 @@ +var helper = require('../test-helper'); +var assert = require('assert'); + +helper.pg.connect(helper.config, function(err, client, done) { + if (err) throw err; + + var c = 'CREATE TEMP TABLE posts (body TEXT)'; + + client.query(c, function(err) { + if (err) throw err; + + c = 'INSERT INTO posts (body) VALUES ($1) RETURNING *'; + + var body = new Buffer('foo'); + client.query(c, [body], function(err) { + if (err) throw err; + + body = new Buffer([]); + client.query(c, [body], function(err, res) { + done(); + + if (err) throw err; + assert.equal(res.rows[0].body, '') + helper.pg.end(); + }); + }); + }); +}); diff --git a/test/integration/gh-issues/699-tests.js b/test/integration/gh-issues/699-tests.js new file mode 100644 index 000000000..2918c9aec --- /dev/null +++ b/test/integration/gh-issues/699-tests.js @@ -0,0 +1,27 @@ +var helper = require('../test-helper'); +var assert = require('assert'); +var copyFrom = require('pg-copy-streams').from; + +if(helper.args.native) return; + +helper.pg.connect(helper.config, function (err, client, done) { + if (err) throw err; + + var c = 'CREATE TEMP TABLE employee (id integer, fname varchar(400), lname varchar(400))'; + + client.query(c, function (err) { + if (err) throw err; + + var stream = client.query(copyFrom("COPY employee FROM STDIN")); + stream.on('end', function () { + done(); + helper.pg.end(); + }); + + for (var i = 1; i <= 5; i++) { + var line = ['1\ttest', i, '\tuser', i, '\n']; + stream.write(line.join('')); + } + stream.end(); + }); +}); diff --git a/test/integration/gh-issues/787-tests.js b/test/integration/gh-issues/787-tests.js new file mode 100644 index 000000000..e75c67666 --- /dev/null +++ b/test/integration/gh-issues/787-tests.js @@ -0,0 +1,11 @@ +var helper = require(__dirname + '/../test-helper'); + +helper.pg.connect(helper.config, function(err,client) { + var q = { + name: 'This is a super long query name just so I can test that an error message is properly spit out to console.error without throwing an exception or anything', + text: 'SELECT NOW()' + }; + client.query(q, function() { + client.end(); + }); +}); diff --git a/test/integration/gh-issues/882-tests.js b/test/integration/gh-issues/882-tests.js new file mode 100644 index 000000000..1818b0c6b --- /dev/null +++ b/test/integration/gh-issues/882-tests.js @@ -0,0 +1,8 @@ +//client should not hang on an empty query +var helper = require('../test-helper'); +var client = helper.client(); +client.query({ name: 'foo1', text: null}); +client.query({ name: 'foo2', text: ' ' }); +client.query({ name: 'foo3', text: '' }, function(err, res) { + client.end(); +}); diff --git a/test/integration/gh-issues/981-tests.js b/test/integration/gh-issues/981-tests.js new file mode 100644 index 000000000..ed3c3123a --- /dev/null +++ b/test/integration/gh-issues/981-tests.js @@ -0,0 +1,27 @@ +var helper = require(__dirname + '/../test-helper'); + +//native bindings are only installed for native tests +if(!helper.args.native) { + return; +} + +var assert = require('assert') +var pg = require('../../../lib') +var native = require('../../../lib').native + +var JsClient = require('../../../lib/client') +var NativeClient = require('../../../lib/native') + +assert(pg.Client === JsClient); +assert(native.Client === NativeClient); + +pg.connect(function(err, client, done) { + assert(client instanceof JsClient); + client.end(); + + native.connect(function(err, client, done) { + assert(client instanceof NativeClient); + client.end(); + }); +}); + diff --git a/test/integration/test-helper.js b/test/integration/test-helper.js index 55d114206..c6a4922dc 100644 --- a/test/integration/test-helper.js +++ b/test/integration/test-helper.js @@ -7,12 +7,21 @@ if(helper.args.native) { } //creates a client from cli parameters -helper.client = function() { +helper.client = function(cb) { var client = new Client(helper.config); - client.connect(); + client.connect(cb); return client; }; +var semver = require('semver'); +helper.versionGTE = function(client, versionString, callback) { + client.query('SELECT version()', assert.calls(function(err, result) { + if(err) return callback(err); + var version = result.rows[0].version.split(' ')[1]; + return callback(null, semver.gte(version, versionString)); + })); +}; + //export parent helper stuffs module.exports = helper; diff --git a/test/native/callback-api-tests.js b/test/native/callback-api-tests.js index 450066822..95e9a8ff3 100644 --- a/test/native/callback-api-tests.js +++ b/test/native/callback-api-tests.js @@ -1,3 +1,4 @@ +var domain = require('domain'); var helper = require(__dirname + "/../test-helper"); var Client = require(__dirname + "/../../lib/native"); @@ -7,6 +8,7 @@ test('fires callback with results', function() { client.query('SELECT 1 as num', assert.calls(function(err, result) { assert.isNull(err); assert.equal(result.rows[0].num, 1); + assert.strictEqual(result.rowCount, 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'); @@ -14,3 +16,17 @@ test('fires callback with results', function() { })) })); }) + +test('preserves domain', function() { + var dom = domain.create(); + + dom.run(function() { + var client = new Client(helper.config); + assert.ok(dom === require('domain').active, 'domain is active'); + client.connect() + client.query('select 1', function() { + assert.ok(dom === require('domain').active, 'domain is still active'); + client.end(); + }); + }); +}) diff --git a/test/native/connection-tests.js b/test/native/connection-tests.js index 1cb0ed88e..be84be6e8 100644 --- a/test/native/connection-tests.js +++ b/test/native/connection-tests.js @@ -1,5 +1,6 @@ var helper = require(__dirname + "/../test-helper"); var Client = require(__dirname + "/../../lib/native"); +var domain = require('domain'); test('connecting with wrong parameters', function() { var con = new Client("user=asldfkj hostaddr=127.0.0.1 port=5432 dbname=asldkfj"); @@ -20,3 +21,16 @@ test('connects', function() { }) }) }) + +test('preserves domain', function() { + var dom = domain.create(); + + dom.run(function() { + var con = new Client(helper.config); + assert.ok(dom === require('domain').active, 'domain is active'); + con.connect(function() { + assert.ok(dom === require('domain').active, 'domain is still active'); + con.end(); + }); + }); +}) diff --git a/test/native/error-tests.js b/test/native/error-tests.js index 3184df57b..3a9327050 100644 --- a/test/native/error-tests.js +++ b/test/native/error-tests.js @@ -5,26 +5,30 @@ test('query with non-text as first parameter throws error', function() { var client = new Client(helper.config); client.connect(); assert.emits(client, 'connect', function() { - assert.throws(function() { - client.query({text:{fail: true}}); - }) client.end(); - }) -}) + assert.emits(client, 'end', function() { + assert.throws(function() { + client.query({text:{fail: true}}); + }); + }); + }); +}); test('parameterized query with non-text as first parameter throws error', function() { var client = new Client(helper.config); client.connect(); assert.emits(client, 'connect', function() { - assert.throws(function() { - client.query({ - text: {fail: true}, - values: [1, 2] - }) - }) client.end(); - }) -}) + assert.emits(client, 'end', function() { + assert.throws(function() { + client.query({ + text: {fail: true}, + values: [1, 2] + }) + }); + }); + }); +}); var connect = function(callback) { var client = new Client(helper.config); @@ -37,24 +41,28 @@ var connect = function(callback) { 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(); - }) - }) + assert.emits(client, 'end', function() { + assert.throws(function() { + client.query("SELECT *", "LKSDJF") + }); + }); + }); + }); test('config', function() { connect(function(client) { - assert.throws(function() { - client.query({ - text: "SELECT *", - values: "ALSDKFJ" - }) - }) client.end(); - }) - }) -}) + assert.emits(client, 'end', function() { + assert.throws(function() { + client.query({ + text: "SELECT *", + values: "ALSDKFJ" + }); + }); + }); + }); + }); +}); diff --git a/test/native/evented-api-tests.js b/test/native/evented-api-tests.js index db93f5bff..9bff34109 100644 --- a/test/native/evented-api-tests.js +++ b/test/native/evented-api-tests.js @@ -10,31 +10,32 @@ var setupClient = function() { return client; } -test('connects', function() { - var client = new Client(helper.config); - 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('connects', function() { + //var client = new Client(helper.config); + //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() { @@ -48,10 +49,10 @@ test('multiple results', function() { }) 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) { + var q2 = client.query({text:'SELECT 1 as num'}); + assert.emits(q2, 'row', function(row) { assert.strictEqual(row.num, 1); - assert.emits(q, 'end', function() { + assert.emits(q2, 'end', function() { client.end(); }) }) diff --git a/test/native/missing-native.js b/test/native/missing-native.js new file mode 100644 index 000000000..775c6186e --- /dev/null +++ b/test/native/missing-native.js @@ -0,0 +1,6 @@ +//this test assumes it has been run from the Makefile +//and that node_modules/pg-native has been deleted + +var assert = require('assert'); + +assert.equal(require('../../lib').native, null); diff --git a/test/native/stress-tests.js b/test/native/stress-tests.js index cac03d037..bd2bca5a0 100644 --- a/test/native/stress-tests.js +++ b/test/native/stress-tests.js @@ -24,13 +24,13 @@ test('many queries', function() { 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 = []; diff --git a/test/test-helper.js b/test/test-helper.js index d24f2f03f..d8e068764 100644 --- a/test/test-helper.js +++ b/test/test-helper.js @@ -1,6 +1,11 @@ //make assert a global... assert = require('assert'); +//support for node@0.10.x +if (typeof Promise == 'undefined') { + global.Promise = require('promise-polyfill') +} + var EventEmitter = require('events').EventEmitter; var sys = require('util'); var BufferList = require(__dirname+'/buffer-list') @@ -28,7 +33,7 @@ assert.same = function(actual, expected) { assert.emits = function(item, eventName, callback, message) { var called = false; var id = setTimeout(function() { - test("Should have called " + eventName, function() { + test("Should have called '" + eventName + "' event", function() { assert.ok(called, message || "Expected '" + eventName + "' to be called.") }); },5000); @@ -96,13 +101,25 @@ assert.empty = function(actual) { }; assert.success = function(callback) { - return assert.calls(function(err, arg) { - if(err) { - console.log(err); - } - assert.isNull(err); - callback(arg); - }) + if(callback.length === 1 || callback.length === 0) { + return assert.calls(function(err, arg) { + if(err) { + console.log(err); + } + assert(!err); + callback(arg); + }); + } else if (callback.length === 2) { + return assert.calls(function(err, arg1, arg2) { + if(err) { + console.log(err); + } + assert(!err); + callback(arg1, arg2); + }); + } else { + throw new Error('need to preserve arrity of wrapped function'); + } } assert.throws = function(offender) { @@ -121,17 +138,36 @@ assert.lengthIs = function(actual, expectedLength) { var expect = function(callback, timeout) { var executed = false; + timeout = timeout || parseInt(process.env.TEST_TIMEOUT) || 5000; var id = setTimeout(function() { - assert.ok(executed, "Expected execution of function to be fired"); - }, timeout || 5000) - - return function(err, queryResult) { - clearTimeout(id); - if (err) { - assert.ok(err instanceof Error, "Expected errors to be instances of Error: " + sys.inspect(err)); + assert.ok(executed, + "Expected execution of function to be fired within " + timeout + + " milliseconds " + + + " (hint: export TEST_TIMEOUT=" + + " to change timeout globally)" + + callback.toString()); + }, timeout) + + if(callback.length < 3) { + return function(err, queryResult) { + clearTimeout(id); + if (err) { + assert.ok(err instanceof Error, "Expected errors to be instances of Error: " + sys.inspect(err)); + } + callback.apply(this, arguments) } - callback.apply(this, arguments) + } else if(callback.length == 3) { + return function(err, arg1, arg2) { + clearTimeout(id); + if (err) { + assert.ok(err instanceof Error, "Expected errors to be instances of Error: " + sys.inspect(err)); + } + callback.apply(this, arguments) + } + } else { + throw new Error("Unsupported arrity " + callback.length); } + } assert.calls = expect; @@ -142,7 +178,8 @@ assert.isNull = function(item, message) { test = function(name, action) { test.testCount ++; - var result = action(); + test[name] = action; + var result = test[name](); if(result === false) { process.stdout.write('?'); }else{ @@ -196,6 +233,15 @@ var Sink = function(expected, timeout, callback) { } } +var getTimezoneOffset = Date.prototype.getTimezoneOffset; + +var setTimezoneOffset = function(minutesOffset) { + Date.prototype.getTimezoneOffset = function () { return minutesOffset; }; +} + +var resetTimezoneOffset = function() { + Date.prototype.getTimezoneOffset = getTimezoneOffset; +} module.exports = { Sink: Sink, @@ -203,7 +249,9 @@ module.exports = { args: args, config: args, sys: sys, - Client: Client + Client: Client, + setTimezoneOffset: setTimezoneOffset, + resetTimezoneOffset: resetTimezoneOffset }; diff --git a/test/unit/client/cleartext-password-tests.js b/test/unit/client/cleartext-password-tests.js index 13a9c9bd6..e880908be 100644 --- a/test/unit/client/cleartext-password-tests.js +++ b/test/unit/client/cleartext-password-tests.js @@ -1,5 +1,10 @@ require(__dirname+'/test-helper'); +/* + * TODO: Add _some_ comments to explain what it is we're testing, and how the + * code-being-tested works behind the scenes. + */ + test('cleartext password authentication', function(){ var client = createClient(); diff --git a/test/unit/client/configuration-tests.js b/test/unit/client/configuration-tests.js index cb60119ba..0204af22b 100644 --- a/test/unit/client/configuration-tests.js +++ b/test/unit/client/configuration-tests.js @@ -1,12 +1,17 @@ require(__dirname+'/test-helper'); +var pguser = process.env['PGUSER'] || process.env.USER; +var pgdatabase = process.env['PGDATABASE'] || process.env.USER; +var pgport = process.env['PGPORT'] || 5432; + test('client settings', function() { test('defaults', function() { var client = new Client(); - assert.equal(client.user, process.env.USER); - assert.equal(client.database, process.env.USER); - assert.equal(client.port, 5432); + assert.equal(client.user, pguser); + assert.equal(client.database, pgdatabase); + assert.equal(client.port, pgport); + assert.equal(client.ssl, false); }); test('custom', function() { @@ -17,13 +22,37 @@ test('client settings', function() { user: user, database: database, port: 321, - password: password + password: password, + ssl: true }); assert.equal(client.user, user); assert.equal(client.database, database); assert.equal(client.port, 321); assert.equal(client.password, password); + assert.equal(client.ssl, true); + }); + + test('custom ssl default on', function() { + var old = process.env.PGSSLMODE; + process.env.PGSSLMODE = "prefer"; + + var client = new Client(); + process.env.PGSSLMODE = old; + + assert.equal(client.ssl, true); + }); + + test('custom ssl force off', function() { + var old = process.env.PGSSLMODE; + process.env.PGSSLMODE = "prefer"; + + var client = new Client({ + ssl: false + }); + process.env.PGSSLMODE = old; + + assert.equal(client.ssl, false); }); }); @@ -31,25 +60,89 @@ test('client settings', function() { test('initializing from a config string', function() { test('uses the correct values from the config string', function() { - var client = new Client("pg://brian:pass@host1:333/databasename") - assert.equal(client.user, 'brian') - assert.equal(client.password, "pass") - assert.equal(client.host, "host1") - assert.equal(client.port, 333) - assert.equal(client.database, "databasename") - }) + var client = new Client("postgres://brian:pass@host1:333/databasename") + assert.equal(client.user, 'brian'); + assert.equal(client.password, "pass"); + assert.equal(client.host, "host1"); + assert.equal(client.port, 333); + assert.equal(client.database, "databasename"); + }); + + test('uses the correct values from the config string with space in password', function() { + var client = new Client("postgres://brian:pass word@host1:333/databasename") + assert.equal(client.user, 'brian'); + assert.equal(client.password, "pass word"); + assert.equal(client.host, "host1"); + assert.equal(client.port, 333); + assert.equal(client.database, "databasename"); + }); test('when not including all values the defaults are used', function() { - var client = new Client("pg://host1") - assert.equal(client.user, process.env.USER) - assert.equal(client.password, null) - assert.equal(client.host, "host1") - assert.equal(client.port, 5432) - assert.equal(client.database, process.env.USER) - }) + var client = new Client("postgres://host1"); + assert.equal(client.user, process.env['PGUSER'] || process.env.USER); + assert.equal(client.password, process.env['PGPASSWORD'] || null); + assert.equal(client.host, "host1"); + assert.equal(client.port, process.env['PGPORT'] || 5432); + assert.equal(client.database, process.env['PGDATABASE'] || process.env.USER); + }); + + test('when not including all values the environment variables are used', function() { + var envUserDefined = process.env['PGUSER'] !== undefined; + var envPasswordDefined = process.env['PGPASSWORD'] !== undefined; + var envDBDefined = process.env['PGDATABASE'] !== undefined; + var envHostDefined = process.env['PGHOST'] !== undefined; + var envPortDefined = process.env['PGPORT'] !== undefined; + + var savedEnvUser = process.env['PGUSER']; + var savedEnvPassword = process.env['PGPASSWORD']; + var savedEnvDB = process.env['PGDATABASE']; + var savedEnvHost = process.env['PGHOST']; + var savedEnvPort = process.env['PGPORT']; + + process.env['PGUSER'] = 'utUser1'; + process.env['PGPASSWORD'] = 'utPass1'; + process.env['PGDATABASE'] = 'utDB1'; + process.env['PGHOST'] = 'utHost1'; + process.env['PGPORT'] = 5464; + + var client = new Client("postgres://host1"); + assert.equal(client.user, process.env['PGUSER']); + assert.equal(client.password, process.env['PGPASSWORD']); + assert.equal(client.host, "host1"); + assert.equal(client.port, process.env['PGPORT']); + assert.equal(client.database, process.env['PGDATABASE']); + if (envUserDefined) { + process.env['PGUSER'] = savedEnvUser; + } else { + delete process.env['PGUSER']; + } -}) + if (envPasswordDefined) { + process.env['PGPASSWORD'] = savedEnvPassword; + } else { + delete process.env['PGPASSWORD']; + } + + if (envDBDefined) { + process.env['PGDATABASE'] = savedEnvDB; + } else { + delete process.env['PGDATABASE']; + } + + if (envHostDefined) { + process.env['PGHOST'] = savedEnvHost; + } else { + delete process.env['PGHOST']; + } + + if (envPortDefined) { + process.env['PGPORT'] = savedEnvPort; + } else { + delete process.env['PGPORT']; + } + }); +}); test('calls connect correctly on connection', function() { var client = new Client("/tmp"); @@ -60,7 +153,7 @@ test('calls connect correctly on connection', function() { usedHost = host; }; client.connect(); - assert.equal(usedPort, "/tmp/.s.PGSQL.5432"); - assert.strictEqual(usedHost, undefined) -}) + assert.equal(usedPort, "/tmp/.s.PGSQL." + pgport); + assert.strictEqual(usedHost, undefined); +}); diff --git a/test/unit/client/connection-string-tests.js b/test/unit/client/connection-string-tests.js index 5c08a0fd2..9316daa9b 100644 --- a/test/unit/client/connection-string-tests.js +++ b/test/unit/client/connection-string-tests.js @@ -1,21 +1,27 @@ require(__dirname + '/test-helper'); +/* + * Perhaps duplicate of test named 'initializing from a config string' in + * configuration-tests.js + */ + test("using connection string in client constructor", function() { var client = new Client("postgres://brian:pw@boom:381/lala"); + test("parses user", function() { assert.equal(client.user,'brian'); - }) + }); test("parses password", function() { assert.equal(client.password, 'pw'); - }) + }); test("parses host", function() { assert.equal(client.host, 'boom'); - }) + }); test('parses port', function() { assert.equal(client.port, 381) - }) + }); test('parses database', function() { assert.equal(client.database, 'lala') - }) -}) + }); +}); diff --git a/test/unit/client/early-disconnect-tests.js b/test/unit/client/early-disconnect-tests.js new file mode 100644 index 000000000..c67a783ad --- /dev/null +++ b/test/unit/client/early-disconnect-tests.js @@ -0,0 +1,23 @@ +var helper = require(__dirname + '/test-helper'); +var net = require('net'); +var pg = require('../../..//lib/index.js'); + +/* console.log() messages show up in `make test` output. TODO: fix it. */ +var server = net.createServer(function(c) { + console.log('server connected'); + c.destroy(); + console.log('server socket destroyed.'); + server.close(function() { console.log('server closed'); }); +}); + +server.listen(7777, function() { + console.log('server listening'); + var client = new pg.Client('postgres://localhost:7777'); + console.log('client connecting'); + client.connect(assert.calls(function(err) { + if (err) console.log("Error on connect: "+err); + else console.log('client connected'); + assert(err); + })); + +}); diff --git a/test/unit/client/escape-tests.js b/test/unit/client/escape-tests.js new file mode 100644 index 000000000..e3f638ac1 --- /dev/null +++ b/test/unit/client/escape-tests.js @@ -0,0 +1,72 @@ +var helper = require(__dirname + '/test-helper'); + +function createClient(callback) { + var client = new Client(helper.config); + client.connect(function(err) { + return callback(client); + }); +} + +var testLit = function(testName, input, expected) { + test(testName, function(){ + var client = new Client(helper.config); + var actual = client.escapeLiteral(input); + assert.equal(expected, actual); + }); +}; + +var testIdent = function(testName, input, expected) { + test(testName, function(){ + var client = new Client(helper.config); + var actual = client.escapeIdentifier(input); + assert.equal(expected, actual); + }); +}; + +testLit('escapeLiteral: no special characters', + 'hello world', "'hello world'"); + +testLit('escapeLiteral: contains double quotes only', + 'hello " world', "'hello \" world'"); + +testLit('escapeLiteral: contains single quotes only', + 'hello \' world', "'hello \'\' world'"); + +testLit('escapeLiteral: contains backslashes only', + 'hello \\ world', " E'hello \\\\ world'"); + +testLit('escapeLiteral: contains single quotes and double quotes', + 'hello \' " world', "'hello '' \" world'"); + +testLit('escapeLiteral: contains double quotes and backslashes', + 'hello \\ " world', " E'hello \\\\ \" world'"); + +testLit('escapeLiteral: contains single quotes and backslashes', + 'hello \\ \' world', " E'hello \\\\ '' world'"); + +testLit('escapeLiteral: contains single quotes, double quotes, and backslashes', + 'hello \\ \' " world', " E'hello \\\\ '' \" world'"); + +testIdent('escapeIdentifier: no special characters', + 'hello world', '"hello world"'); + +testIdent('escapeIdentifier: contains double quotes only', + 'hello " world', '"hello "" world"'); + +testIdent('escapeIdentifier: contains single quotes only', + 'hello \' world', '"hello \' world"'); + +testIdent('escapeIdentifier: contains backslashes only', + 'hello \\ world', '"hello \\ world"'); + +testIdent('escapeIdentifier: contains single quotes and double quotes', + 'hello \' " world', '"hello \' "" world"'); + +testIdent('escapeIdentifier: contains double quotes and backslashes', + 'hello \\ " world', '"hello \\ "" world"'); + +testIdent('escapeIdentifier: contains single quotes and backslashes', + 'hello \\ \' world', '"hello \\ \' world"'); + +testIdent('escapeIdentifier: contains single quotes, double quotes, and backslashes', + 'hello \\ \' " world', '"hello \\ \' "" world"'); diff --git a/test/unit/client/md5-password-tests.js b/test/unit/client/md5-password-tests.js index 2cd929bb7..3c5403685 100644 --- a/test/unit/client/md5-password-tests.js +++ b/test/unit/client/md5-password-tests.js @@ -1,4 +1,5 @@ -require(__dirname + '/test-helper') +require(__dirname + '/test-helper'); + test('md5 authentication', function() { var client = createClient(); client.password = "!"; @@ -13,8 +14,11 @@ test('md5 authentication', function() { var password = "md5" + encrypted //how do we want to test this? assert.equalBuffers(client.connection.stream.packets[0], new BufferList() - .addCString(password).join(true,'p')) + .addCString(password).join(true,'p')); }); }); +}); +test('md5 of utf-8 strings', function() { + assert.equal(Client.md5('😊'), '5deda34cd95f304948d2bc1b4a62c11e'); }); diff --git a/test/unit/client/notification-tests.js b/test/unit/client/notification-tests.js index 24d746043..e6b0dff13 100644 --- a/test/unit/client/notification-tests.js +++ b/test/unit/client/notification-tests.js @@ -1,4 +1,5 @@ var helper = require(__dirname + "/test-helper"); + test('passes connection notification', function() { var client = helper.client(); assert.emits(client, 'notice', function(msg) { diff --git a/test/unit/client/prepared-statement-tests.js b/test/unit/client/prepared-statement-tests.js index 15bb74418..6e26c7967 100644 --- a/test/unit/client/prepared-statement-tests.js +++ b/test/unit/client/prepared-statement-tests.js @@ -50,14 +50,14 @@ test('bound command', function() { assert.ok(client.connection.emit('readyForQuery')); var query = client.query({ - text: 'select * where name = $1', + text: 'select * from X where name = $1', values: ['hi'] }); assert.emits(query,'end', function() { test('parse argument', function() { assert.equal(parseArg.name, null); - assert.equal(parseArg.text, 'select * where name = $1'); + assert.equal(parseArg.text, 'select * from X where name = $1'); assert.equal(parseArg.types, null); }); diff --git a/test/unit/client/query-queue-tests.js b/test/unit/client/query-queue-tests.js index cd87cfe99..62b38bd58 100644 --- a/test/unit/client/query-queue-tests.js +++ b/test/unit/client/query-queue-tests.js @@ -50,63 +50,3 @@ test('drain', function() { }); }); }); - -test('with drain paused', function() { - //mock out a fake connection - var con = new Connection({stream: "NO"}); - con.connect = function() { - con.emit('connect'); - }; - con.query = function() { - }; - - var client = new Client({connection:con}); - - client.connect(); - - var drainCount = 0; - client.on('drain', function() { - drainCount++; - }); - - test('normally unpaused', function() { - con.emit('readyForQuery'); - client.query('boom'); - assert.emits(client, 'drain', function() { - assert.equal(drainCount, 1); - }); - con.emit('readyForQuery'); - }); - - test('pausing', function() { - test('unpaused with no queries in between', function() { - client.pauseDrain(); - client.resumeDrain(); - assert.equal(drainCount, 1); - }); - - test('paused', function() { - test('resumeDrain after empty', function() { - client.pauseDrain(); - client.query('asdf'); - con.emit('readyForQuery'); - assert.equal(drainCount, 1); - client.resumeDrain(); - assert.equal(drainCount, 2); - }); - - test('resumDrain while still pending', function() { - client.pauseDrain(); - client.query('asdf'); - client.query('asdf1'); - con.emit('readyForQuery'); - client.resumeDrain(); - assert.equal(drainCount, 2); - con.emit('readyForQuery'); - assert.equal(drainCount, 3); - }); - - }); - }); - -}); diff --git a/test/unit/client/query-tests.js b/test/unit/client/query-tests.js deleted file mode 100644 index ad8865cd8..000000000 --- a/test/unit/client/query-tests.js +++ /dev/null @@ -1,74 +0,0 @@ -var helper = require(__dirname + '/test-helper'); -var q = {}; -q.dateParser = require(__dirname + "/../../../lib/types").getTypeParser(1114, 'text'); -q.stringArrayParser = require(__dirname + "/../../../lib/types").getTypeParser(1009, 'text'); - -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()); -}); - -var testForMs = function(part, expected) { - var dateString = "2010-01-01 01:01:01" + part; - test('testing for correcting parsing of ' + dateString, function() { - var ms = q.dateParser(dateString).getMilliseconds(); - assert.equal(ms, expected) - }) -} - -testForMs('.1', 100); -testForMs('.01', 10); -testForMs('.74', 740); - -test("testing 2dateParser", function() { - var actual = "2010-12-11 09:09:04.1"; - var expected = "\"2010-12-11T09:09:04.100Z\""; - assert.equal(JSON.stringify(q.dateParser(actual)),expected); -}); - -test("testing 2dateParser", function() { - var actual = "2011-01-23 22:15:51.28-06"; - var expected = "\"2011-01-24T04:15:51.280Z\""; - assert.equal(JSON.stringify(q.dateParser(actual)),expected); -}); - -test("testing 2dateParser", function() { - var actual = "2011-01-23 22:15:51.280843-06"; - var expected = "\"2011-01-24T04:15:51.280Z\""; - assert.equal(JSON.stringify(q.dateParser(actual)),expected); -}); - -test("testing empty array", function(){ - var input = '{}'; - var expected = []; - assert.deepEqual(q.stringArrayParser(input), expected); -}); - -test("testing empty string array", function(){ - var input = '{""}'; - var expected = [""]; - assert.deepEqual(q.stringArrayParser(input), expected); -}); - -test("testing numeric array", function(){ - var input = '{1,2,3,4}'; - var expected = [1,2,3,4]; - assert.deepEqual(q.stringArrayParser(input), expected); -}); - -test("testing stringy array", function(){ - var input = '{a,b,c,d}'; - var expected = ['a','b','c','d']; - assert.deepEqual(q.stringArrayParser(input), expected); -}); - -test("testing stringy array containing escaped strings", function(){ - var input = '{"\\"\\"\\"","\\\\\\\\\\\\"}'; - var expected = ['"""','\\\\\\']; - assert.deepEqual(q.stringArrayParser(input), expected); -}); - -test("testing NULL array", function(){ - var input = '{NULL,NULL}'; - var expected = [null,null]; - assert.deepEqual(q.stringArrayParser(input), expected); -}); diff --git a/test/unit/client/stream-and-query-error-interaction-tests.js b/test/unit/client/stream-and-query-error-interaction-tests.js new file mode 100644 index 000000000..02d66c628 --- /dev/null +++ b/test/unit/client/stream-and-query-error-interaction-tests.js @@ -0,0 +1,26 @@ +var helper = require(__dirname + '/test-helper'); +var Connection = require(__dirname + '/../../../lib/connection'); +var Client = require(__dirname + '/../../../lib/client'); + +test('emits end when not in query', function() { + var stream = new (require('events').EventEmitter)(); + stream.write = function() { + //NOOP + } + var client = new Client({connection: new Connection({stream: stream})}); + client.connect(assert.calls(function() { + client.query('SELECT NOW()', assert.calls(function(err, result) { + assert(err); + })); + })); + assert.emits(client, 'end'); + client.connection.emit('connect'); + process.nextTick(function() { + client.connection.emit('readyForQuery'); + assert.equal(client.queryQueue.length, 0); + assert(client.activeQuery, 'client should have issued query'); + process.nextTick(function() { + stream.emit('close'); + }); + }); +}); diff --git a/test/unit/client/throw-in-type-parser-tests.js b/test/unit/client/throw-in-type-parser-tests.js new file mode 100644 index 000000000..ed3711376 --- /dev/null +++ b/test/unit/client/throw-in-type-parser-tests.js @@ -0,0 +1,112 @@ +var helper = require(__dirname + "/test-helper"); +var types = require('pg-types') + +test('handles throws in type parsers', function() { + var typeParserError = new Error('TEST: Throw in type parsers'); + + types.setTypeParser('special oid that will throw', function () { + throw typeParserError; + }); + + test('emits error', function() { + var handled; + var client = helper.client(); + var con = client.connection; + var query = client.query('whatever'); + + handled = con.emit('readyForQuery'); + assert.ok(handled, "should have handled ready for query"); + + con.emit('rowDescription',{ + fields: [{ + name: 'boom', + dataTypeID: 'special oid that will throw' + }] + }); + assert.ok(handled, "should have handled row description"); + + assert.emits(query, 'error', function(err) { + assert.equal(err, typeParserError); + }); + + handled = con.emit('dataRow', { fields: ["hi"] }); + assert.ok(handled, "should have handled first data row message"); + + handled = con.emit('commandComplete', { text: 'INSERT 31 1' }); + assert.ok(handled, "should have handled command complete"); + + handled = con.emit('readyForQuery'); + assert.ok(handled, "should have handled ready for query"); + }); + + test('calls callback with error', function() { + var handled; + + var callbackCalled = 0; + + var client = helper.client(); + var con = client.connection; + var query = client.query('whatever', assert.calls(function (err) { + callbackCalled += 1; + + assert.equal(callbackCalled, 1); + assert.equal(err, typeParserError); + })); + + handled = con.emit('readyForQuery'); + assert.ok(handled, "should have handled ready for query"); + + handled = con.emit('rowDescription',{ + fields: [{ + name: 'boom', + dataTypeID: 'special oid that will throw' + }] + }); + assert.ok(handled, "should have handled row description"); + + handled = con.emit('dataRow', { fields: ["hi"] }); + assert.ok(handled, "should have handled first data row message"); + + handled = con.emit('dataRow', { fields: ["hi"] }); + assert.ok(handled, "should have handled second data row message"); + + con.emit('commandComplete', { text: 'INSERT 31 1' }); + assert.ok(handled, "should have handled command complete"); + + handled = con.emit('readyForQuery'); + assert.ok(handled, "should have handled ready for query"); + }); + + test('rejects promise with error', function() { + var handled; + var client = helper.client(); + var con = client.connection; + var query = client.query('whatever'); + var queryPromise = query.promise(); + + handled = con.emit('readyForQuery'); + assert.ok(handled, "should have handled ready for query"); + + handled = con.emit('rowDescription',{ + fields: [{ + name: 'boom', + dataTypeID: 'special oid that will throw' + }] + }); + assert.ok(handled, "should have handled row description"); + + handled = con.emit('dataRow', { fields: ["hi"] }); + assert.ok(handled, "should have handled first data row message"); + + handled = con.emit('commandComplete', { text: 'INSERT 31 1' }); + assert.ok(handled, "should have handled command complete"); + + handled = con.emit('readyForQuery'); + assert.ok(handled, "should have handled ready for query"); + + queryPromise.catch(assert.calls(function (err) { + assert.equal(err, typeParserError); + })); + }); + +}); diff --git a/test/unit/client/typed-query-results-tests.js b/test/unit/client/typed-query-results-tests.js deleted file mode 100644 index af3e3f977..000000000 --- a/test/unit/client/typed-query-results-tests.js +++ /dev/null @@ -1,280 +0,0 @@ -var helper = require(__dirname + '/test-helper'); -//http://www.postgresql.org/docs/8.4/static/datatype.html -test('typed results', function() { - var client = helper.client(); - var con = client.connection; - con.emit('readyForQuery'); - var query = client.query("the bums lost"); - - - //TODO refactor to this style - var tests = [{ - name: 'string/varchar', - format: 'text', - dataTypeID: 1043, - actual: 'bang', - expected: 'bang' - },{ - name: 'integer/int4', - format: 'text', - dataTypeID: 23, - actual: '100', - expected: 100 - },{ - name: 'smallint/int2', - format: 'text', - dataTypeID: 21, - actual: '101', - expected: 101 - },{ - name: 'bigint/int8', - format: 'text', - dataTypeID: 20, - actual: '102', - expected: 102 - },{ - name: 'oid', - format: 'text', - dataTypeID: 26, - actual: '103', - expected: 103 - },{ - name: 'numeric', - format: 'text', - dataTypeID: 1700, - actual: '12.34', - expected: 12.34 - },{ - name: 'real/float4', - dataTypeID: 700, - format: 'text', - actual: '123.456', - expected: 123.456 - },{ - name: 'double precision / float8', - format: 'text', - dataTypeID: 701, - actual: '1.2', - expected: 1.2 - },{ - name: 'boolean true', - format: 'text', - dataTypeID: 16, - actual: 't', - expected: true - },{ - name: 'boolean false', - format: 'text', - dataTypeID: 16, - actual: 'f', - expected: false - },{ - name: 'boolean null', - format: 'text', - dataTypeID: 16, - actual: null, - expected: null - },{ - name: 'timestamptz with minutes in timezone', - format: 'text', - dataTypeID: 1184, - actual: '2010-10-31 14:54:13.74-0530', - expected: function(val) { - assert.UTCDate(val, 2010, 9, 31, 20, 24, 13, 740); - } - },{ - name: 'timestamptz with other milisecond digits dropped', - format: 'text', - dataTypeID: 1184, - actual: '2011-01-23 22:05:00.68-06', - expected: function(val) { - assert.UTCDate(val, 2011, 0, 24, 4, 5, 00, 680); - } - }, { - name: 'timestampz with huge miliseconds in UTC', - format: 'text', - dataTypeID: 1184, - actual: '2010-10-30 14:11:12.730838Z', - expected: function(val) { - assert.UTCDate(val, 2010, 9, 30, 14, 11, 12, 730); - } - },{ - name: 'timestampz with no miliseconds', - format: 'text', - dataTypeID: 1184, - actual: '2010-10-30 13:10:01+05', - expected: function(val) { - assert.UTCDate(val, 2010, 9, 30, 8, 10, 01, 0); - } - },{ - name: 'timestamp', - format: 'text', - dataTypeID: 1114, - actual: '2010-10-31 00:00:00', - expected: function(val) { - assert.UTCDate(val, 2010, 9, 31, 0, 0, 0, 0); - } - },{ - name: 'date', - format: 'text', - dataTypeID: 1082, - actual: '2010-10-31', - 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}) - } - },{ - name: 'bytea', - format: 'text', - dataTypeID: 17, - actual: 'foo\\000\\200\\\\\\377', - expected: function(val) { - assert.deepEqual(val, new Buffer([102, 111, 111, 0, 128, 92, 255])); - } - },{ - name: 'empty bytea', - format: 'text', - dataTypeID: 17, - actual: '', - expected: function(val) { - assert.deepEqual(val, new Buffer(0)); - } - }, - - - { - name: 'binary-string/varchar', - format: 'binary', - dataTypeID: 1043, - actual: 'bang', - expected: 'bang' - },{ - name: 'binary-integer/int4', - format: 'binary', - dataTypeID: 23, - actual: [0, 0, 0, 100], - expected: 100 - },{ - name: 'binary-smallint/int2', - format: 'binary', - dataTypeID: 21, - actual: [0, 101], - expected: 101 - },{ - name: 'binary-bigint/int8', - format: 'binary', - dataTypeID: 20, - actual: [0, 0, 0, 0, 0, 0, 0, 102], - expected: 102 - },{ - name: 'binary-bigint/int8-full', - format: 'binary', - dataTypeID: 20, - actual: [1, 0, 0, 0, 0, 0, 0, 102], - expected: 72057594037928030 - },{ - name: 'binary-oid', - format: 'binary', - dataTypeID: 26, - actual: [0, 0, 0, 103], - expected: 103 - },{ - name: 'binary-numeric', - format: 'binary', - dataTypeID: 1700, - actual: [0,2,0,0,0,0,0,0x64,0,12,0xd,0x48,0,0,0,0], - expected: 12.34 - },{ - name: 'binary-real/float4', - dataTypeID: 700, - format: 'binary', - actual: [0x41, 0x48, 0x00, 0x00], - expected: 12.5 - },{ - name: 'binary-double precision / float8', - format: 'binary', - dataTypeID: 701, - actual: [0x3F,0xF3,0x33,0x33,0x33,0x33,0x33,0x33], - expected: 1.2 - },{ - name: 'binary-boolean true', - format: 'binary', - dataTypeID: 16, - actual: [1], - expected: true - },{ - name: 'binary-boolean false', - format: 'binary', - dataTypeID: 16, - actual: [0], - expected: false - },{ - name: 'binary-boolean null', - format: 'binary', - dataTypeID: 16, - actual: null, - expected: null - },{ - name: 'binary-timestamp', - format: 'binary', - dataTypeID: 1184, - actual: [0x00, 0x01, 0x36, 0xee, 0x3e, 0x66, 0x9f, 0xe0], - expected: function(val) { - assert.UTCDate(val, 2010, 9, 31, 20, 24, 13, 740); - } - },{ - name: 'binary-string', - format: 'binary', - dataTypeID: 25, - actual: new Buffer([0x73, 0x6c, 0x61, 0x64, 0x64, 0x61]), - expected: 'sladda' - }]; - - - con.emit('rowDescription', { - fieldCount: tests.length, - fields: tests - }); - - assert.emits(query, 'row', function(row) { - for(var i = 0; i < tests.length; i++) { - test('parses ' + tests[i].name, function() { - var expected = tests[i].expected; - if(typeof expected === 'function') { - return expected(row[tests[i].name]); - } - assert.strictEqual(row[tests[i].name], expected); - }); - } - }); - - assert.ok(con.emit('dataRow', { - fields: tests.map(function(x) { - return x.actual; - }) - })); - -}); diff --git a/test/unit/connection-parameters/creation-tests.js b/test/unit/connection-parameters/creation-tests.js new file mode 100644 index 000000000..33ee7eeeb --- /dev/null +++ b/test/unit/connection-parameters/creation-tests.js @@ -0,0 +1,234 @@ +var helper = require(__dirname + '/../test-helper'); +var assert = require('assert'); +var ConnectionParameters = require(__dirname + '/../../../lib/connection-parameters'); +var defaults = require(__dirname + '/../../../lib').defaults; + +//clear process.env +for(var key in process.env) { + delete process.env[key]; +} + +test('ConnectionParameters construction', function() { + assert.ok(new ConnectionParameters(), 'with null config'); + assert.ok(new ConnectionParameters({user: 'asdf'}), 'with config object'); + assert.ok(new ConnectionParameters('postgres://localhost/postgres'), 'with connection string'); +}); + +var compare = function(actual, expected, type) { + assert.equal(actual.user, expected.user, type + ' user'); + assert.equal(actual.database, expected.database, type + ' database'); + assert.equal(actual.port, expected.port, type + ' port'); + assert.equal(actual.host, expected.host, type + ' host'); + assert.equal(actual.password, expected.password, type + ' password'); + assert.equal(actual.binary, expected.binary, type + ' binary'); +}; + +test('ConnectionParameters initializing from defaults', function() { + var subject = new ConnectionParameters(); + compare(subject, defaults, 'defaults'); + assert.ok(subject.isDomainSocket === false); +}); + +test('ConnectionParameters initializing from defaults with connectionString set', function() { + var config = { + user : 'brians-are-the-best', + database : 'scoobysnacks', + port : 7777, + password : 'mypassword', + host : 'foo.bar.net', + binary : defaults.binary + }; + + var original_value = defaults.connectionString; + // Just changing this here doesn't actually work because it's no longer in scope when viewed inside of + // of ConnectionParameters() so we have to pass in the defaults explicitly to test it + defaults.connectionString = 'postgres://brians-are-the-best:mypassword@foo.bar.net:7777/scoobysnacks'; + var subject = new ConnectionParameters(defaults); + defaults.connectionString = original_value; + compare(subject, config, 'defaults-connectionString'); +}); + +test('ConnectionParameters initializing from config', function() { + var config = { + user: 'brian', + database: 'home', + port: 7777, + password: 'pizza', + binary: true, + encoding: 'utf8', + host: 'yo', + ssl: { + asdf: 'blah' + } + }; + var subject = new ConnectionParameters(config); + compare(subject, config, 'config'); + assert.ok(subject.isDomainSocket === false); +}); + +test('escape spaces if present', function() { + subject = new ConnectionParameters('postgres://localhost/post gres'); + assert.equal(subject.database, 'post gres'); +}); + +test('do not double escape spaces', function() { + subject = new ConnectionParameters('postgres://localhost/post%20gres'); + assert.equal(subject.database, 'post gres'); +}); + +test('initializing with unix domain socket', function() { + var subject = new ConnectionParameters('/var/run/'); + assert.ok(subject.isDomainSocket); + assert.equal(subject.host, '/var/run/'); + assert.equal(subject.database, defaults.user); +}); + +test('initializing with unix domain socket and a specific database, the simple way', function() { + var subject = new ConnectionParameters('/var/run/ mydb'); + assert.ok(subject.isDomainSocket); + assert.equal(subject.host, '/var/run/'); + assert.equal(subject.database, 'mydb'); +}); + +test('initializing with unix domain socket, the health way', function() { + var subject = new ConnectionParameters('socket:/some path/?db=my[db]&encoding=utf8'); + assert.ok(subject.isDomainSocket); + assert.equal(subject.host, '/some path/'); + assert.equal(subject.database, 'my[db]', 'must to be escaped and unescaped trough "my%5Bdb%5D"'); + assert.equal(subject.client_encoding, 'utf8'); +}); + +test('initializing with unix domain socket, the escaped health way', function() { + var subject = new ConnectionParameters('socket:/some%20path/?db=my%2Bdb&encoding=utf8'); + assert.ok(subject.isDomainSocket); + assert.equal(subject.host, '/some path/'); + assert.equal(subject.database, 'my+db'); + assert.equal(subject.client_encoding, 'utf8'); +}); + +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' + } + var subject = new ConnectionParameters(config); + subject.getLibpqConnectionString(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: 'localhost' + }; + var subject = new ConnectionParameters(config); + subject.getLibpqConnectionString(assert.calls(function(err, constring) { + assert.isNull(err); + var parts = constring.split(" "); + checkForPart(parts, "user='brian'"); + checkForPart(parts, "hostaddr=127.0.0.1"); + })); + }); + + test('error when dns fails', function() { + var config = { + user: 'brian', + password: 'asf', + port: 5432, + host: 'asdlfkjasldfkksfd#!$!!!!..com' + }; + var subject = new ConnectionParameters(config); + subject.getLibpqConnectionString(assert.calls(function(err, constring) { + assert.ok(err); + assert.isNull(constring) + })); + }); + + test('connecting to unix domain socket', function() { + var config = { + user: 'brian', + password: 'asf', + port: 5432, + host: '/tmp/' + }; + var subject = new ConnectionParameters(config); + subject.getLibpqConnectionString(assert.calls(function(err, constring) { + assert.isNull(err); + var parts = constring.split(" "); + checkForPart(parts, "user='brian'"); + checkForPart(parts, "host=/tmp/"); + })); + }); + + test("encoding can be specified by config", function() { + var config = { + client_encoding: "utf-8" + } + var subject = new ConnectionParameters(config); + subject.getLibpqConnectionString(assert.calls(function(err, constring) { + assert.isNull(err); + var parts = constring.split(" "); + checkForPart(parts, "client_encoding='utf-8'"); + })); + }) + + test('password contains < and/or > characters', function () { + return false; + var sourceConfig = { + user:'brian', + password: 'helloe', + port: 5432, + host: 'localhost', + database: 'postgres' + } + var connectionString = 'postgres://' + sourceConfig.user + ':' + sourceConfig.password + '@' + sourceConfig.host + ':' + sourceConfig.port + '/' + sourceConfig.database; + var subject = new ConnectionParameters(connectionString); + assert.equal(subject.password, sourceConfig.password); + }); + + test('username or password contains weird characters', function() { + var defaults = require('../../../lib/defaults'); + defaults.ssl = true; + var strang = 'pg://my f%irst name:is&%awesome!@localhost:9000'; + var subject = new ConnectionParameters(strang); + assert.equal(subject.user, 'my f%irst name'); + assert.equal(subject.password, 'is&%awesome!'); + assert.equal(subject.host, 'localhost'); + assert.equal(subject.ssl, true); + }); + + test("url is properly encoded", function() { + var encoded = "pg://bi%25na%25%25ry%20:s%40f%23@localhost/%20u%2520rl"; + var subject = new ConnectionParameters(encoded); + assert.equal(subject.user, "bi%na%%ry "); + assert.equal(subject.password, "s@f#"); + assert.equal(subject.host, 'localhost'); + assert.equal(subject.database, " u%20rl"); + }); + + test('ssl is set on client', function() { + var Client = require('../../../lib/client') + var defaults = require('../../../lib/defaults'); + defaults.ssl = true; + var c = new Client('postgres://user@password:host/database') + assert(c.ssl, 'Client should have ssl enabled via defaults') + }) + +}); diff --git a/test/unit/connection-parameters/environment-variable-tests.js b/test/unit/connection-parameters/environment-variable-tests.js new file mode 100644 index 000000000..5481915fd --- /dev/null +++ b/test/unit/connection-parameters/environment-variable-tests.js @@ -0,0 +1,114 @@ +var helper = require(__dirname + '/../test-helper'); +var assert = require('assert'); +var ConnectionParameters = require(__dirname + '/../../../lib/connection-parameters'); +var defaults = require(__dirname + '/../../../lib').defaults; + +//clear process.env +var realEnv = {}; +for(var key in process.env) { + realEnv[key] = process.env[key]; + delete process.env[key]; +} + +test('ConnectionParameters initialized from environment variables', function(t) { + process.env['PGHOST'] = 'local'; + process.env['PGUSER'] = 'bmc2'; + process.env['PGPORT'] = 7890; + process.env['PGDATABASE'] = 'allyerbase'; + process.env['PGPASSWORD'] = 'open'; + + var subject = new ConnectionParameters(); + assert.equal(subject.host, 'local', 'env host'); + assert.equal(subject.user, 'bmc2', 'env user'); + assert.equal(subject.port, 7890, 'env port'); + assert.equal(subject.database, 'allyerbase', 'env database'); + assert.equal(subject.password, 'open', 'env password'); +}); + +test('ConnectionParameters initialized from mix', function(t) { + delete process.env['PGPASSWORD']; + delete process.env['PGDATABASE']; + var subject = new ConnectionParameters({ + user: 'testing', + database: 'zugzug' + }); + assert.equal(subject.host, 'local', 'env host'); + assert.equal(subject.user, 'testing', 'config user'); + assert.equal(subject.port, 7890, 'env port'); + assert.equal(subject.database, 'zugzug', 'config database'); + assert.equal(subject.password, defaults.password, 'defaults password'); +}); + +//clear process.env +for(var key in process.env) { + delete process.env[key]; +} + +test('connection string parsing', function(t) { + var string = 'postgres://brian:pw@boom:381/lala'; + var subject = new ConnectionParameters(string); + assert.equal(subject.host, 'boom', 'string host'); + assert.equal(subject.user, 'brian', 'string user'); + assert.equal(subject.password, 'pw', 'string password'); + assert.equal(subject.port, 381, 'string port'); + assert.equal(subject.database, 'lala', 'string database'); +}); + +test('connection string parsing - ssl', function(t) { + var string = 'postgres://brian:pw@boom:381/lala?ssl=true'; + var subject = new ConnectionParameters(string); + assert.equal(subject.ssl, true, 'ssl'); + + string = 'postgres://brian:pw@boom:381/lala?ssl=1'; + subject = new ConnectionParameters(string); + assert.equal(subject.ssl, true, 'ssl'); + + string = 'postgres://brian:pw@boom:381/lala?other&ssl=true'; + subject = new ConnectionParameters(string); + assert.equal(subject.ssl, true, 'ssl'); + + string = 'postgres://brian:pw@boom:381/lala?ssl=0'; + subject = new ConnectionParameters(string); + assert.equal(!!subject.ssl, false, 'ssl'); + + string = 'postgres://brian:pw@boom:381/lala'; + subject = new ConnectionParameters(string); + assert.equal(!!subject.ssl, false, 'ssl'); +}); + +//clear process.env +for(var key in process.env) { + delete process.env[key]; +} + + +test('ssl is false by default', function() { + var subject = new ConnectionParameters() + assert.equal(subject.ssl, false) +}) + +var testVal = function(mode, expected) { + //clear process.env + for(var key in process.env) { + delete process.env[key]; + } + process.env.PGSSLMODE = mode; + test('ssl is ' + expected + ' when $PGSSLMODE=' + mode, function() { + var subject = new ConnectionParameters(); + assert.equal(subject.ssl, expected); + }); +}; + +testVal('', false); +testVal('disable', false); +testVal('allow', false); +testVal('prefer', true); +testVal('require', true); +testVal('verify-ca', true); +testVal('verify-full', true); + + +//restore process.env +for(var key in realEnv) { + process.env[key] = realEnv[key]; +} diff --git a/test/unit/connection/error-tests.js b/test/unit/connection/error-tests.js index bccffac4b..98eb20a8a 100644 --- a/test/unit/connection/error-tests.js +++ b/test/unit/connection/error-tests.js @@ -1,10 +1,30 @@ var helper = require(__dirname + '/test-helper'); var Connection = require(__dirname + '/../../../lib/connection'); -var con = new Connection({stream: new MemoryStream()}); test("connection emits stream errors", function() { + var con = new Connection({stream: new MemoryStream()}); assert.emits(con, 'error', function(err) { assert.equal(err.message, "OMG!"); }); con.connect(); con.stream.emit('error', new Error("OMG!")); }); + +test('connection emits ECONNRESET errors during normal operation', function() { + var con = new Connection({stream: new MemoryStream()}); + con.connect(); + assert.emits(con, 'error', function(err) { + assert.equal(err.code, 'ECONNRESET'); + }); + var e = new Error('Connection Reset'); + e.code = 'ECONNRESET'; + con.stream.emit('error', e); +}); + +test('connection does not emit ECONNRESET errors during disconnect', function() { + var con = new Connection({stream: new MemoryStream()}); + con.connect(); + var e = new Error('Connection Reset'); + e.code = 'ECONNRESET'; + con.end(); + con.stream.emit('error', e); +}); diff --git a/test/unit/connection/inbound-parser-tests.js b/test/unit/connection/inbound-parser-tests.js index 13e6fd9eb..a9910966b 100644 --- a/test/unit/connection/inbound-parser-tests.js +++ b/test/unit/connection/inbound-parser-tests.js @@ -347,6 +347,13 @@ test('Connection', function() { name: 'portalSuspended' }); }); + + test('parses replication start message', function() { + testForMessage(new Buffer([0x57, 0x00, 0x00, 0x00, 0x04]), { + name: 'replicationStart', + length: 4 + }); + }); }); //since the data message on a stream can randomly divide the incomming @@ -465,5 +472,4 @@ test('split buffer, multiple message parsing', function() { splitAndVerifyTwoMessages(1); }); }); - }); diff --git a/test/unit/connection/outbound-sending-tests.js b/test/unit/connection/outbound-sending-tests.js index 731a46dec..3dde8a3cb 100644 --- a/test/unit/connection/outbound-sending-tests.js +++ b/test/unit/connection/outbound-sending-tests.js @@ -23,6 +23,8 @@ test("sends startup message", function() { .addCString('brian') .addCString('database') .addCString('bang') + .addCString('client_encoding') + .addCString("'utf-8'") .addCString('').join(true)) }); @@ -114,6 +116,32 @@ test('bind messages', function() { }); }); +test('with named statement, portal, and buffer value', function() { + con.bind({ + portal: 'bang', + statement: 'woo', + values: ['1', 'hi', null, new Buffer('zing', 'UTF-8')] + }); + var expectedBuffer = new BufferList() + .addCString('bang') //portal name + .addCString('woo') //statement name + .addInt16(4)//value count + .addInt16(0)//string + .addInt16(0)//string + .addInt16(0)//string + .addInt16(1)//binary + .addInt16(4) + .addInt32(1) + .add(Buffer("1")) + .addInt32(2) + .add(Buffer("hi")) + .addInt32(-1) + .addInt32(4) + .add(new Buffer('zing', 'UTF-8')) + .addInt16(0) + .join(true, 'B'); + assert.received(stream, expectedBuffer); +}); test("sends execute message", function() { diff --git a/test/unit/connection/startup-tests.js b/test/unit/connection/startup-tests.js index e3b419917..622f47374 100644 --- a/test/unit/connection/startup-tests.js +++ b/test/unit/connection/startup-tests.js @@ -7,13 +7,18 @@ test('connection can take existing stream', function() { }); test('using closed stream', function() { - var stream = new MemoryStream(); - stream.readyState = 'closed'; - stream.connect = function(port, host) { - this.connectCalled = true; - this.port = port; - this.host = host; - } + var makeStream = function() { + var stream = new MemoryStream(); + stream.readyState = 'closed'; + stream.connect = function(port, host) { + this.connectCalled = true; + this.port = port; + this.host = host; + } + return stream; + }; + + var stream = makeStream(); var con = new Connection({stream: stream}); @@ -43,6 +48,24 @@ test('using closed stream', function() { assert.ok(hit); }); + + test('after stream emits connected event init TCP-keepalive', function() { + + var stream = makeStream(); + var con = new Connection({ stream: stream, keepAlive: true }); + con.connect(123, 'test'); + + var res = false; + + stream.setKeepAlive = function(bit) { + res = bit; + }; + + assert.ok(stream.emit('connect')); + setTimeout(function() { + assert.equal(res, true); + }) + }); }); test('using opened stream', function() { diff --git a/test/unit/test-helper.js b/test/unit/test-helper.js index 3bcd21e04..878898d8b 100644 --- a/test/unit/test-helper.js +++ b/test/unit/test-helper.js @@ -15,6 +15,8 @@ p.write = function(packet) { this.packets.push(packet); }; +p.setKeepAlive = function(){}; + p.writable = true; createClient = function() { diff --git a/test/unit/utils-tests.js b/test/unit/utils-tests.js index 1a9fb9907..d640f9880 100644 --- a/test/unit/utils-tests.js +++ b/test/unit/utils-tests.js @@ -1,11 +1,19 @@ -require(__dirname + '/test-helper'); +var helper = require(__dirname + '/test-helper'); var utils = require(__dirname + "/../../lib/utils"); var defaults = require(__dirname + "/../../lib").defaults; + +test('ensure types is exported on root object', function() { + var pg = require('../../lib') + assert(pg.types) + assert(pg.types.getTypeParser) + assert(pg.types.setTypeParser) +}) + //this tests the monkey patching //to ensure comptability with older //versions of node -test("EventEmitter.once", function() { +test("EventEmitter.once", function(t) { //an event emitter var stream = new MemoryStream(); @@ -21,145 +29,167 @@ test("EventEmitter.once", function() { }); -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('normalizing query configs', function() { + var config + var callback = function () {} + + config = utils.normalizeQueryConfig({text: 'TEXT'}) + assert.same(config, {text: 'TEXT'}) + + config = utils.normalizeQueryConfig({text: 'TEXT'}, [10]) + assert.deepEqual(config, {text: 'TEXT', values: [10]}) + + config = utils.normalizeQueryConfig({text: 'TEXT', values: [10]}) + assert.deepEqual(config, {text: 'TEXT', values: [10]}) + + config = utils.normalizeQueryConfig('TEXT', [10], callback) + assert.deepEqual(config, {text: 'TEXT', values: [10], callback: callback}) + + config = utils.normalizeQueryConfig({text: 'TEXT', values: [10]}, callback) + assert.deepEqual(config, {text: 'TEXT', values: [10], callback: callback}) }) -test('libpq connection string building', function() { - var checkForPart = function(array, part) { - assert.ok(array.indexOf(part) > -1, array.join(" ") + " did not contain " + part); - } +test('prepareValues: buffer prepared properly', function() { + var buf = new Buffer("quack"); + var out = utils.prepareValue(buf); + assert.strictEqual(buf, out); +}); + +test('prepareValues: date prepared properly', function() { + helper.setTimezoneOffset(-330); + + var date = new Date(2014, 1, 1, 11, 11, 1, 7); + var out = utils.prepareValue(date); + assert.strictEqual(out, "2014-02-01T11:11:01.007+05:30"); + + helper.resetTimezoneOffset(); +}); + +test('prepareValues: date prepared properly as UTC', function() { + defaults.parseInputDatesAsUTC = true; + + // make a date in the local timezone that represents a specific UTC point in time + var date = new Date(Date.UTC(2014, 1, 1, 11, 11, 1, 7)); + var out = utils.prepareValue(date); + assert.strictEqual(out, "2014-02-01T11:11:01.007+00:00"); + + defaults.parseInputDatesAsUTC = false; +}); + +test('prepareValues: undefined prepared properly', function() { + var out = utils.prepareValue(void 0); + assert.strictEqual(out, null); +}); + +test('prepareValue: null prepared properly', function() { + var out = utils.prepareValue(null); + assert.strictEqual(out, null); +}); + +test('prepareValue: true prepared properly', function() { + var out = utils.prepareValue(true); + assert.strictEqual(out, 'true'); +}); + +test('prepareValue: false prepared properly', function() { + var out = utils.prepareValue(false); + assert.strictEqual(out, 'false'); +}); - test('builds simple string', function() { - var config = { - user: 'brian', - password: 'xyz', - port: 888, - host: 'localhost', - database: 'bam' +test('prepareValue: number prepared properly', function () { + var out = utils.prepareValue(3.042); + assert.strictEqual(out, '3.042'); +}); + +test('prepareValue: string prepared properly', function() { + var out = utils.prepareValue('big bad wolf'); + assert.strictEqual(out, 'big bad wolf'); +}); + +test('prepareValue: simple array prepared properly', function() { + var out = utils.prepareValue([1, null, 3, undefined, [5, 6, "squ,awk"]]); + assert.strictEqual(out, '{"1",NULL,"3",NULL,{"5","6","squ,awk"}}'); +}); + +test('prepareValue: complex array prepared properly', function() { + var out = utils.prepareValue([{ x: 42 }, { y: 84 }]); + assert.strictEqual(out, '{"{\\"x\\":42}","{\\"y\\":84}"}'); +}); + +test('prepareValue: date array prepared properly', function() { + helper.setTimezoneOffset(-330); + + var date = new Date(2014, 1, 1, 11, 11, 1, 7); + var out = utils.prepareValue([date]); + assert.strictEqual(out, '{"2014-02-01T11:11:01.007+05:30"}'); + + helper.resetTimezoneOffset(); +}); + +test('prepareValue: arbitrary objects prepared properly', function() { + var out = utils.prepareValue({ x: 42 }); + assert.strictEqual(out, '{"x":42}'); +}); + +test('prepareValue: objects with simple toPostgres prepared properly', function() { + var customType = { + toPostgres: function() { + return "zomgcustom!"; } - 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: 'localhost' + }; + var out = utils.prepareValue(customType); + assert.strictEqual(out, "zomgcustom!"); +}); + +test('prepareValue: objects with complex toPostgres prepared properly', function() { + var buf = new Buffer("zomgcustom!"); + var customType = { + toPostgres: function() { + return [1, 2]; } - utils.buildLibpqConnectionString(config, assert.calls(function(err, constring) { - assert.isNull(err); - var parts = constring.split(" "); - checkForPart(parts, "user='brian'") - checkForPart(parts, "hostaddr=127.0.0.1") - })) - }) - - test('error when dns fails', function() { - var config = { - user: 'brian', - password: 'asf', - port: 5432, - host: 'asdlfkjasldfkksfd#!$!!!!..com' + }; + var out = utils.prepareValue(customType); + assert.strictEqual(out, '{"1","2"}'); +}); + +test('prepareValue: objects with toPostgres receive prepareValue', function() { + var customRange = { + lower: { toPostgres: function() { return 5; } }, + upper: { toPostgres: function() { return 10; } }, + toPostgres: function(prepare) { + return "[" + prepare(this.lower) + "," + prepare(this.upper) + "]"; } - utils.buildLibpqConnectionString(config, assert.calls(function(err, constring) { - assert.ok(err); - assert.isNull(constring) - })) - }) - - test('password contains < and/or > characters', function () { - return false; - var sourceConfig = { - user:'brian', - password: 'helloe', - port: 5432, - host: 'localhost', - database: 'postgres' + }; + var out = utils.prepareValue(customRange); + assert.strictEqual(out, "[5,10]"); +}); + +test('prepareValue: objects with circular toPostgres rejected', function() { + var buf = new Buffer("zomgcustom!"); + var customType = { + toPostgres: function() { + return { toPostgres: function () { return customType; } }; } - var connectionString = 'pg://' + sourceConfig.user + ':' + sourceConfig.password + '@' + sourceConfig.host + ':' + sourceConfig.port + '/' + sourceConfig.database; - var config = utils.parseConnectionString(connectionString); - assert.same(config, sourceConfig); - }); + }; + + //can't use `assert.throws` since we need to distinguish circular reference + //errors from call stack exceeded errors + try { + utils.prepareValue(customType); + } catch (e) { + assert.ok(e.message.match(/circular/), "Expected circular reference error but got " + e); + return; + } + throw new Error("Expected prepareValue to throw exception"); +}); +test('prepareValue: can safely be used to map an array of values including those with toPostgres functions', function() { + var customType = { + toPostgres: function() { + return "zomgcustom!"; + } + }; + var values = [1, "test", customType] + var out = values.map(utils.prepareValue) + assert.deepEqual(out, [1, "test", "zomgcustom!"]) }) - -test('types are exported', function() { - var pg = require(__dirname + '/../../lib/index'); - assert.ok(pg.types); -}); diff --git a/test/unit/writer-tests.js b/test/unit/writer-tests.js deleted file mode 100644 index e5ade3207..000000000 --- a/test/unit/writer-tests.js +++ /dev/null @@ -1,196 +0,0 @@ -require(__dirname + "/test-helper"); -var Writer = require(__dirname + "/../../lib/writer"); - -test('adding int32', function() { - var testAddingInt32 = function(int, expectedBuffer) { - test('writes ' + int, function() { - var subject = new Writer(); - var result = subject.addInt32(int).join(); - assert.equalBuffers(result, expectedBuffer); - }) - } - - testAddingInt32(0, [0, 0, 0, 0]); - testAddingInt32(1, [0, 0, 0, 1]); - testAddingInt32(256, [0, 0, 1, 0]); - test('writes largest int32', function() { - //todo need to find largest int32 when I have internet access - return false; - }) - - test('writing multiple int32s', function() { - var subject = new Writer(); - var result = subject.addInt32(1).addInt32(10).addInt32(0).join(); - assert.equalBuffers(result, [0, 0, 0, 1, 0, 0, 0, 0x0a, 0, 0, 0, 0]); - }) - - test('having to resize the buffer', function() { - test('after resize correct result returned', function() { - var subject = new Writer(10); - subject.addInt32(1).addInt32(1).addInt32(1) - assert.equalBuffers(subject.join(), [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1]) - }) - }) -}) - -test('int16', function() { - test('writes 0', function() { - var subject = new Writer(); - var result = subject.addInt16(0).join(); - assert.equalBuffers(result, [0,0]); - }) - - test('writes 400', function() { - var subject = new Writer(); - var result = subject.addInt16(400).join(); - assert.equalBuffers(result, [1, 0x90]) - }) - - test('writes many', function() { - var subject = new Writer(); - var result = subject.addInt16(0).addInt16(1).addInt16(2).join(); - assert.equalBuffers(result, [0, 0, 0, 1, 0, 2]) - }) - - test('resizes if internal buffer fills up', function() { - var subject = new Writer(3); - var result = subject.addInt16(2).addInt16(3).join(); - assert.equalBuffers(result, [0, 2, 0, 3]) - }) - -}) - -test('cString', function() { - test('writes empty cstring', function() { - var subject = new Writer(); - var result = subject.addCString().join(); - assert.equalBuffers(result, [0]) - }) - - test('writes two empty cstrings', function() { - var subject = new Writer(); - var result = subject.addCString("").addCString("").join(); - assert.equalBuffers(result, [0, 0]) - }) - - - test('writes non-empty cstring', function() { - var subject = new Writer(); - var result = subject.addCString("!!!").join(); - assert.equalBuffers(result, [33, 33, 33, 0]); - }) - - test('resizes if reached end', function() { - var subject = new Writer(3); - var result = subject.addCString("!!!").join(); - assert.equalBuffers(result, [33, 33, 33, 0]); - }) - - test('writes multiple cstrings', function() { - var subject = new Writer(); - var result = subject.addCString("!").addCString("!").join(); - assert.equalBuffers(result, [33, 0, 33, 0]); - }) - -}) - -test('writes char', function() { - var subject = new Writer(2); - var result = subject.addChar('a').addChar('b').addChar('c').join(); - assert.equalBuffers(result, [0x61, 0x62, 0x63]) -}) - -test('gets correct byte length', function() { - var subject = new Writer(5); - assert.equal(subject.getByteLength(), 0) - subject.addInt32(0) - assert.equal(subject.getByteLength(), 4) - subject.addCString("!") - assert.equal(subject.getByteLength(), 6) -}) - -test('can add arbitrary buffer to the end', function() { - var subject = new Writer(4); - subject.addCString("!!!") - var result = subject.add(Buffer("@@@")).join(); - assert.equalBuffers(result, [33, 33, 33, 0, 0x40, 0x40, 0x40]); -}) - -test('can write normal string', function() { - var subject = new Writer(4); - var result = subject.addString("!").join(); - assert.equalBuffers(result, [33]); - test('can write cString too', function() { - var result = subject.addCString("!").join(); - assert.equalBuffers(result, [33, 33, 0]); - test('can resize', function() { - var result = subject.addString("!!").join(); - assert.equalBuffers(result, [33, 33, 0, 33, 33]); - }) - - }) - -}) - - -test('clearing', function() { - var subject = new Writer(); - subject.addCString("@!!#!#"); - subject.addInt32(10401); - subject.clear(); - assert.equalBuffers(subject.join(), []); - test('can keep writing', function() { - var joinedResult = subject.addCString("!").addInt32(9).addInt16(2).join(); - assert.equalBuffers(joinedResult, [33, 0, 0, 0, 0, 9, 0, 2]); - test('flush', function() { - var flushedResult = subject.flush(); - test('returns result', function() { - assert.equalBuffers(flushedResult, [33, 0, 0, 0, 0, 9, 0, 2]) - }) - test('clears the writer', function() { - assert.equalBuffers(subject.join(), []) - assert.equalBuffers(subject.flush(), []) - }) - }) - }) - -}) - -test("resizing to much larger", function() { - var subject = new Writer(2); - var string = "!!!!!!!!"; - var result = subject.addCString(string).flush(); - assert.equalBuffers(result, [33, 33, 33, 33, 33, 33, 33, 33, 0]) -}) - -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) - assert.equalBuffers(result, [0x50, 0, 0, 0, 6, 33, 0]); - }) - - test('added as a hex code to a non-full writer', function() { - var subject = new Writer(10).addCString("!"); - var joinedResult = subject.join(0x50); - var result = subject.flush(0x50); - assert.equalBuffers(result, [0x50, 0, 0, 0, 6, 33, 0]); - }) - - test('added as a hex code to a buffer which requires resizing', function() { - var result = new Writer(2).addCString("!!!!!!!!").flush(0x50); - assert.equalBuffers(result, [0x50, 0, 0, 0, 0x0D, 33, 33, 33, 33, 33, 33, 33, 33, 0]); - }) -}) - -test("header", function() { - test('adding two packets with headers', function() { - var subject = new Writer(10).addCString("!"); - subject.addHeader(0x50); - subject.addCString("!!"); - subject.addHeader(0x40); - subject.addCString("!"); - var result = subject.flush(0x10); - assert.equalBuffers(result, [0x50, 0, 0, 0, 6, 33, 0, 0x40, 0, 0, 0, 7, 33, 33, 0, 0x10, 0, 0, 0, 6, 33, 0 ]); - }) -}) diff --git a/wscript b/wscript deleted file mode 100644 index 2a340d5bb..000000000 --- a/wscript +++ /dev/null @@ -1,32 +0,0 @@ -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")