Add node-libpq typings (#14044)

* add type definition for package: libpq

* style(libpq): change indent size to 4 white-spaces

* test(libpq): add tests for libpq

* test(libpq): more tests for libpq

* chore(libpq): create project by 'npm run new-project' script

* test(libpq): fix call signature

* fix(libpq): update in accordance with test information

* doc(libpq): improve documentation in accordance with typedoc

* fix(libpq): connection callback argument type should be Error, not string

* doc(libpq): stylize documentation

* feat(libpq): add exporting inner interfaces
This commit is contained in:
Vlad Rindevich
2017-01-18 00:25:14 +03:00
committed by Mohamed Hegazy
parent a661f615b6
commit 00221afddf
4 changed files with 1277 additions and 0 deletions

428
libpq/index.d.ts vendored Normal file
View File

@@ -0,0 +1,428 @@
// Type definitions for libpq 1.8
// Project: https://github.com/brianc/node-libpq#readme
// Definitions by: Vlad Rindevich <https://github.com/Lodin>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
/// <reference types="node" />
import {EventEmitter} from 'events';
import {Buffer} from 'buffer';
declare namespace Libpq {
export interface NotifyMsg {
relname: string;
extra: string;
be_pid: number;
}
export interface ResultError {
severity: string;
sqlState: string;
messagePrimary: string;
messageDetail?: string;
messageHint?: string;
statementPosition?: string;
internalPosition?: string;
internalQuery?: string;
context?: string;
schemaName?: string;
tableName?: string;
dataTypeName?: string;
constraintName?: string;
sourceFile: string;
sourceLine: string;
sourceFunction: string;
}
}
declare class Libpq extends EventEmitter {
/**
* Current connection state.
*/
connected: boolean;
/**
* Issues a request to cancel the currently executing query on this instance of libpq.
*
* @returns {(boolean|string)} true if the cancel request was sent; a string error message if
* the cancel request failed for any reason. The string will
* contain the error message provided by libpq.
*/
cancel(): true|string;
/**
* Manually frees the memory associated with a PGresult pointer. Generally this is called
* for you, but if you absolutely want to free the pointer yourself, you can.
*/
clear(): void;
/**
* @returns {string} the status string associated with a result. Something akin to INSERT 3 0
* if you inserted 3 rows.
*/
cmdStatus(): string;
/**
* @returns {string} the number of tuples (rows) affected by the command. Even though this is a
* number, it is returned as a string to mirror libpq's behavior.
*/
cmdTuples(): string;
/**
* (async) Connects to a PostgreSQL backend server process.
*
* This function actually calls the PQconnectdb blocking connection method in a background
* thread within node's internal thread-pool. There is a way to do non-blocking network I/O
* for some of the connecting with libpq directly, but it still blocks when your local file
* system looking for config files, SSL certificates, .pgpass file, and doing possible dns
* resolution. Because of this, the best way to get fully non-blocking is to juse use
* libuv_queue_work and let node do it's magic and so that's what I do. This function does
* not block.
*
* @param {string} connectParams an optional string
* @param {Function} callback mandatory. It is called when the connection has successfully been
* established.
*/
connect(connectParams: string, callback: (err?: Error) => void): void;
connect(callback: (err?: Error) => void): void;
/**
* (sync) Attempts to connect to a PostgreSQL server. BLOCKS until it either succeedes, or
* fails. If it fails it will throw an exception.
*
* @param {string} connectionParams an optional string
*/
connectSync(connectionParams?: string): void;
/**
* Reads waiting data from the socket. If the socket is not readable and you call this it will
* block so be careful and only call it within the readable callback for the most part.
*
* @returns {boolean} true if data was read; false if there was an error. You can access
* error details with [[Libpq.errorMessage]].
*/
consumeInput(): boolean
/**
* Retrieves the last error message from the connection. This is intended to be used after most
* functions which return an error code to get more detailed error information about the
* connection. You can also check this before issuing queries to see if your connection has
* been lost.
*
* @returns {string}
*/
errorMessage(): string;
/**
* Exact copy of the PQescapeIdentifier function within libpq. Requires an established
* connection but does not perform any I/O.
*
* @param {string} input
*/
escapeIdentifier(input: string): string;
/**
* Exact copy of the PQescapeLiteral function within libpq. Requires an established connection
* but does not perform any I/O.
*
* @param {string} input
*/
escapeLiteral(input: string): string;
/**
* (sync) Sends a command to the backend and blocks until a result is received.
*
* @param {string} [commandText=""] a required string of the query.
*/
exec(commandText?: string): void;
/**
* (sync) Sends a command and parameters to the backend and blocks until a result is received.
*
* @param {string} [commandText=""] a required string of the query.
* @param {Array.<(string|number)>} [parameters=[]] a required array of string values
* corresponding to each parameter in the
* commandText.
*/
execParams(commandText?: string, parameters?: Array<string|number>): void;
/**
* (sync) Sends a command to the server to execute a previously prepared statement. Blocks
* until the results are returned.
*
* @param {string} [statementName=""] a required string of the name of the prepared statement.
* @param {Array.<(string|number)>} [parameters=[]] the parameters to pass to the prepared
* statement.
*/
execPrepared(statementName?: string, parameters?: Array<string|number>): void;
/**
* Disconnects from the backend and cleans up all memory used by the libpq connection.
*/
finish(): void;
/**
* Flushes buffered data to the socket.
*
* @returns {number} 1 if socket is not write-ready at which case you should call
* [[Libpq.writable]] with a callback and wait for the socket to be writable
* and then call [[Libpq.flush]] again; 0 if all data was flushed; -1 if
* there was an error.
*/
flush(): number;
/**
* Retrieve the name of the field (column) at the given offset. Offset starts at 0.
*
* @param {number} fieldNumber
* @returns {string}
*/
fname(fieldNumber: number): string;
/**
* Retrieve the Oid of the field (column) at the given offset. Offset starts at 0.
*
* @param {number} fieldNumber
* @returns {number}
*/
ftype(fieldNumber: number): number;
/**
* After issuing a successfuly command like COPY table TO stdout gets copy data from the
* connection.
*
* @param {boolean} [async=false] a boolean. Pass false to block waiting for data from the
* backend. Defaults to false.
*
* @returns {Buffer|number} a node buffer if there is data available; 0 if the copy is still in
* progress (only if you have called [[Libpq.setNonBlocking]](true));
* -1 if the copy is completed; -2 if there was an error.
*/
getCopyData(async?: boolean): Buffer|number;
/**
* @param {number} tupleNumber
* @param {number} fieldNumber
* @returns {boolean} true if the value at the given offsets is actually null. Otherwise
* returns false. This is because [[Libpq.getvalue]] returns an empty
* string for both an actual empty string and for a null value. Weird, huh?
*/
getisnull(tupleNumber: number, fieldNumber: number): boolean;
/**
* Parses received data from the server into a PGresult struct and sets a pointer internally to
* the connection object to this result.
*
* Warning: this function will block if libpq is waiting on async results to be returned from
* the server. Call [[Libpq.isBusy]] to determine if this command will block.
*
* @returns {boolean} true if libpq was able to read buffered data & parse a result object;
* false if there are no results waiting to be parsed. Generally doing async
* style queries you'll call this repeadedly until it returns false and then
* use the result accessor methods to pull results out of the current result
* set.
*/
getResult(): boolean;
/**
* Retrieve the text value at a given tuple (row) and field (column) offset. Both offsets start
* at 0. A null value is returned as the empty string ''.
*
* @param {number} tupleNumber
* @param {number} [fieldNumber]
* @returns {string}
*/
getvalue(tupleNumber: number, fieldNumber?: number): string;
/**
* @returns {boolean} true if calling [[Libpq.consumeInput]] would block waiting for more
* data; false if all data has been read from the socket. Once this returns false it is
* safe to call [[Libpq.getResult]].
*/
isBusy(): boolean;
/**
* @returns {boolean} true if non-blocking mode is enabled; false if disabled.
*/
isNonBlocking(): boolean;
/**
* Retrieve the number of fields (columns) from the result.
*
* @returns {number}
*/
nfields(): number;
/**
* Checks for NOTIFY messages that have come in. If any have been received they will be in the
* following format:
*
* @example ```ts
*
* var msg = {
* relname: 'name of channel',
* extra: 'message passed to notify command',
* be_pid: 130
* }
* ```
*
* @returns {Libpq.NotifyMsg}
*/
notifies(): Libpq.NotifyMsg;
/**
* Retrieve the number of tuples (rows) from the result.
*
* @returns {number}
*/
ntuples(): number;
/**
* After issuing a successful command like COPY table FROM stdin you can start putting buffers
* directly into the databse with this function.
*
* @param {Buffer} buffer a required node buffer of text data such as
* Buffer('column1\tcolumn2\n')
*
* @returns {number} 1 if sent succesfully; 0 if the command would block (only if you have
* called [[Libpq.setNonBlocking]](true)); -1 if there was an error sending
* the command.
*/
putCopyData(buffer: Buffer): number;
/**
* Signals the backed your copy procedure is complete. If you pass errorMessage it will be sent
* to the backend and effectively cancel the copy operation.
*
* @param {string} [errorMessage] an optional string you can pass to cancel the copy operation.
*
* @returns {number} 1 if sent succesfully; 0 if the command would block (only if you have
* called [[Libpq.setNonBlocking]](true)); -1 if there was an error sending
* the command.
*/
putCopyEnd(errorMessage?: string): number;
/**
* (sync) Sends a named statement to the server to be prepared for later execution. blocks
* until a result from the prepare operation is received.
*
* @param {string} [statementName=""] a required string of name of the statement to prepare.
* @param {string} [commandText=""] a required string of the query.
* @param {number} [nParams=0] a count of the number of parameters in the commandText.
*/
prepare(statementName?: string, commandText?: string, nParams?: number): void;
/**
* Retrieves detailed error information from the current result object. Very similar to
* PQresultErrorField() except instead of passing a fieldCode and retrieving a single field,
* retrieves all fields from the error at once on a single object. The object returned is a
* simple hash, not an instance of an error object.
*
* If you wanted to access PG_DIAG_MESSAGE_DETAIL you would do the following:
* @example ```ts
*
* console.log(pq.errorFields().messageDetail);
* ```
* @returns {Libpq.ResultError}
*/
resultErrorFields(): Libpq.ResultError;
/**
* Retrieves the error message from the result. This will return null if the result does not
* have an error.
*
* @returns {string}
*/
resultErrorMessage(): string;
/**
* @returns {string} either PGRES_COMMAND_OK or PGRES_FATAL_ERROR depending on the status of
* the last executed command.
*/
resultStatus(): string;
/**
* (async) Sends a query to the server to be processed.
*
* @param {string} [commandText=""] a required string containing the query text.
* @returns {boolean} true if the command was sent succesfully or false if it failed to send.
*/
sendQuery(commandText?: string): boolean;
/**
* (async) Sends a query and to the server to be processed.
*
* @param {string} [commandText=""] a required string containing the query text.
* @param {Array.<(string|number)>} [parameters=[]] an array of parameters as strings used in
* the parameterized query.
* @returns {boolean} true if the command was sent succesfully or false if it failed to send.
*/
sendQueryParams(commandText?: string, parameters?: Array<string|number>): boolean;
/**
* (async) Sends a request to the backend to prepare a named statement with the given name.
*
* @param {string} [statementName=""] a required string of name of the statement to prepare.
* @param {string} [commandText=""] a required string of the query.
* @param {number} [nParams=0] a count of the number of parameters in the commandText.
* @returns {boolean} true if the command was sent succesfully or false if it failed to send.
*/
sendPrepare(statementName?: string, commandText?: string, nParams?: number): boolean;
/**
* (async) Sends a request to execute a previously prepared statement.
*
* @param {string} [statementName=""] a required string of the name of the prepared statement.
* @param {string[]} [parameters=[]] the parameters to pass to the prepared statement.
* @returns {boolean} true if the command was sent succesfully or false if it failed to send.
*/
sendQueryPrepared(statementName?: string, parameters?: string[]): boolean;
/**
* @returns the version of the connected PostgreSQL backend server as a number.
*/
serverVersion(): number;
/**
* Toggle the socket blocking on write.
*
* @param {boolean} [nonBlocking] true to set the connection to use non-blocking writes, false to
* use blocking writes.
*
* @returns {boolean} true if the socket's state was succesfully toggled, false if there was
* an error.
*/
setNonBlocking(nonBlocking?: boolean): boolean;
/**
* @returns {number} an int representing the file descriptor for the socket used internally by
* the connection.
*/
socket(): number;
/**
* This uses libuv to start a read watcher on the socket open to the backend. As soon as this
* socket becomes readable the pq instance will emit a readable event. It is up to you to call
* [[Libpq.consumeInput]] one or more times to clear this read notification or it will
* continue to emit read events over and over and over. The exact flow is outlined [here] under
* the documentation for PQisBusy.
*/
startReader(): void;
/**
* Tells libuv to stop the read watcher on the connection socket.
*/
stopReader(): void;
/**
* Call this to make sure the socket has flushed all data to the operating system. Once the
* socket is writable, your callback will be called. Usefully when using PQsetNonBlocking
* and PQflush for async writing.
*
* @param {Function} callback
*/
writable(callback: () => void): void;
}
export = Libpq;

828
libpq/libpq-tests.ts Normal file
View File

@@ -0,0 +1,828 @@
/// <reference types="node" />
/// <reference types="mocha" />
import {Buffer} from 'buffer';
import * as assert from 'assert';
import * as async from 'async';
import * as PQ from 'libpq';
import * as _ from 'lodash';
declare const ok: Function;
const createTable = (pq: PQ) => {
pq.exec('CREATE TEMP TABLE test_data(name text, age int)');
console.log(pq.resultErrorMessage());
pq.exec("INSERT INTO test_data(name, age) VALUES ('brian', 32), ('aaron', 30), ('', null);")
};
const blink = (n: number, cb: Function) => {
const connections: PQ[] = [];
for (let i = 0; i < 30; i++) {
connections.push(new PQ())
}
const connect = (con: PQ, cb: (err?: Error) => void) => {
con.connect(cb);
};
async.each(connections, connect, ok(() => {
connections.forEach((con) => {
con.finish();
});
cb();
}))
};
const queryText = "SELECT * FROM generate_series(1, 1000)";
const query = (pq: PQ, cb: Function) => {
const readError = (message?: string) => {
cleanup();
return cb(new Error(message || pq.errorMessage()));
};
const onReadable = () => {
if (!pq.consumeInput()) {
return readError();
}
if (pq.isBusy()) {
return;
}
pq.getResult();
if (pq.getResult()) {
return readError('Only one result at a time is accepted');
}
cleanup();
return cb(null, [])
};
const sent = pq.sendQuery(queryText);
if (!sent) return cb(new Error(pq.errorMessage()));
console.log('sent query');
const cleanup = () => {
pq.removeListener('readable', onReadable);
pq.stopReader();
};
pq.on('readable', onReadable);
pq.startReader();
};
describe('async connection', () => {
it('works', (done) => {
const pq = new PQ();
assert(!pq.connected, 'should have connected set to falsy');
pq.connect(err => {
assert.ifError(err);
pq.exec('SELECT NOW()');
assert.equal(pq.ntuples(), 1);
done();
});
});
it('works with hard-coded connection parameters', (done) => {
const pq = new PQ();
const conString = `host=${process.env.PGHOST || 'localhost'}`;
pq.connect(conString, done);
});
it('returns an error to the callback if connection fails', (done) => {
new PQ().connect('host=asldkfjasldkfjalskdfjasdf', err => {
assert(err, 'should have passed an error');
done();
});
});
it('respects the active domain', (done) => {
const pq = new PQ();
const domain = require('domain').create();
domain.run(() => {
const activeDomain = process.domain;
assert(activeDomain, 'Should have an active domain');
pq.connect(() => {
assert.strictEqual(process.domain, activeDomain, 'Active domain is lost');
done();
});
});
});
});
const consume = (pq: PQ, cb: Function) => {
if (!pq.isBusy()) return cb();
pq.startReader();
const onReadable = () => {
assert(pq.consumeInput(), pq.errorMessage());
if (pq.isBusy()) {
console.log('consuming a 2nd buffer of input later...');
return;
}
pq.removeListener('readable', onReadable);
pq.stopReader();
cb();
};
pq.on('readable', onReadable);
};
describe('async simple query', () => {
let pq: PQ;
it('dispatches simple query', (done: Function) => {
assert(pq.setNonBlocking(true));
pq.writable(() => {
const success = pq.sendQuery('SELECT 1');
assert.strictEqual(pq.flush(), 0, 'Should have flushed all data to socket');
assert(success, pq.errorMessage());
consume(pq, () => {
assert.ifError(pq.errorMessage());
assert(pq.getResult());
assert.strictEqual(pq.getResult(), false);
assert.strictEqual(pq.ntuples(), 1);
assert.strictEqual(pq.getvalue(0, 0), '1');
done();
});
});
});
it('dispatches parameterized query', (done: Function) => {
const success = pq.sendQueryParams('SELECT $1::text as name', ['Brian']);
assert(success, pq.errorMessage());
assert.strictEqual(pq.flush(), 0, 'Should have flushed query text & parameters');
consume(pq, () => {
assert.ifError(pq.errorMessage());
assert(pq.getResult());
assert.strictEqual(pq.getResult(), false);
assert.strictEqual(pq.ntuples(), 1);
assert.equal(pq.getvalue(0, 0), 'Brian');
done();
})
});
it('dispatches named query', (done: Function) => {
const statementName = 'async-get-name';
const success = pq.sendPrepare(statementName, 'SELECT $1::text as name', 1);
assert(success, pq.errorMessage());
assert.strictEqual(pq.flush(), 0, 'Should have flushed query text');
consume(pq, () => {
assert.ifError(pq.errorMessage());
//first time there should be a result
assert(pq.getResult());
//call 'getResult' until it returns false indicating
//there is no more input to consume
assert.strictEqual(pq.getResult(), false);
//since we only prepared a statement there should be
//0 tuples in the result
assert.equal(pq.ntuples(), 0);
//now execute the previously prepared statement
const success = pq.sendQueryPrepared(statementName, ['Brian']);
assert(success, pq.errorMessage());
assert.strictEqual(pq.flush(), 0, 'Should have flushed parameters');
consume(pq, () => {
assert.ifError(pq.errorMessage());
//consume the result of the query execution
assert(pq.getResult());
assert.equal(pq.ntuples(), 1);
assert.equal(pq.getvalue(0, 0), 'Brian');
//call 'getResult' again to ensure we're finished
assert.strictEqual(pq.getResult(), false);
done();
});
});
});
});
describe('cancel a request', () => {
it('works', (done) => {
const pq = new PQ();
pq.connectSync();
const sent = pq.sendQuery('pg_sleep(5000)');
assert(sent, 'should have sent');
const canceled = pq.cancel();
assert.strictEqual(canceled, true, 'should have canceled');
const hasResult = pq.getResult();
assert(hasResult, 'should have a result');
assert.equal(pq.resultStatus(), 'PGRES_FATAL_ERROR');
assert.equal(pq.getResult(), false);
pq.exec('SELECT NOW()');
done();
});
});
describe('Constructing multiple', () => {
it('works all at once', () => {
for (let i = 0; i < 1000; i++) {
const pq = new PQ();
}
});
it('connects and disconnects each client', (done) => {
const connect = (n: number, cb: (err?: Error) => void) => {
const pq = new PQ();
pq.connect(cb);
};
async.times(30, connect, done);
});
});
describe('COPY IN', () => {
let pq: PQ;
before(() => {
pq = new PQ();
pq.connectSync();
createTable(pq);
});
after(() => {
pq.finish();
});
it('check existing data assuptions', () => {
pq.exec('SELECT COUNT(*) FROM test_data');
assert.equal(pq.getvalue(0, 0), 3);
});
it('copies data in', () => {
const success = pq.exec('COPY test_data FROM stdin');
assert.equal(pq.resultStatus(), 'PGRES_COPY_IN');
const buffer = new Buffer("bob\t100\n", 'utf8');
const res1 = pq.putCopyData(buffer);
assert.strictEqual(res1, 1);
const res2 = pq.putCopyEnd();
assert.strictEqual(res2, 1);
while (pq.getResult()) {
}
pq.exec('SELECT COUNT(*) FROM test_data');
assert.equal(pq.getvalue(0, 0), 4);
});
it('can cancel copy data in', () => {
const success = pq.exec('COPY test_data FROM stdin');
assert.equal(pq.resultStatus(), 'PGRES_COPY_IN');
const buffer = new Buffer("bob\t100\n", 'utf8');
const res1 = pq.putCopyData(buffer);
assert.strictEqual(res1, 1);
const res2 = pq.putCopyEnd('cancel!');
assert.strictEqual(res2, 1);
while (pq.getResult()) {
}
assert(pq.errorMessage());
assert(
pq.errorMessage().includes('cancel!'),
`${pq.errorMessage()} should have contained "cancel!"`
);
pq.exec('SELECT COUNT(*) FROM test_data');
assert.equal(pq.getvalue(0, 0), 4);
});
});
describe('COPY OUT', () => {
let pq: PQ;
before(() => {
pq = new PQ();
pq.connectSync();
createTable(pq);
});
after(() => {
pq.finish();
});
const getRow = (pq: PQ, expected: string) => {
const result = <Buffer> pq.getCopyData(false);
assert(result instanceof Buffer, 'Result should be a buffer');
assert.equal(result.toString('utf8'), expected);
};
it('copies data out', () => {
pq.exec('COPY test_data TO stdin');
assert.equal(pq.resultStatus(), 'PGRES_COPY_OUT');
getRow(pq, 'brian\t32\n');
getRow(pq, 'aaron\t30\n');
getRow(pq, '\t\\N\n');
assert.strictEqual(<number> pq.getCopyData(), -1);
});
});
describe('without being connected', () => {
it('exec fails', () => {
const pq = new PQ();
pq.exec();
assert.equal(pq.resultStatus(), 'PGRES_FATAL_ERROR');
assert(pq.errorMessage());
});
it('fails on async query', () => {
const pq = new PQ();
const success = pq.sendQuery('blah');
assert.strictEqual(success, false);
assert.equal(pq.resultStatus(), 'PGRES_FATAL_ERROR');
assert(pq.errorMessage());
});
it('throws when reading while not connected', () => {
const pq = new PQ();
assert.throws(() => {
pq.startReader();
});
});
it('throws when writing while not connected', () => {
const pq = new PQ();
assert.throws(() => {
pq.writable(() => {
});
});
});
});
describe('error info', () => {
let pq: PQ;
before(() => {
pq = new PQ();
pq.connectSync();
createTable(pq);
});
after(() => {
pq.finish();
});
describe('when there is no error', () => {
it('everything is null', () => {
pq.exec('SELECT NOW()');
assert(!pq.errorMessage(), pq.errorMessage());
assert.equal(pq.ntuples(), 1);
assert(pq.resultErrorFields(), undefined);
});
});
describe('when there is an error', () => {
it('sets all error codes', () => {
pq.exec('INSERT INTO test_data VALUES(1, NOW())');
assert(pq.errorMessage());
const err = pq.resultErrorFields();
assert.notEqual(err, null);
assert.equal(err.severity, 'ERROR');
assert.equal(err.sqlState, 42804);
assert.equal(err.messagePrimary, 'column "age" is of type integer but expression is of type timestamp with time zone');
assert.equal(err.messageDetail, undefined);
assert.equal(err.messageHint, 'You will need to rewrite or cast the expression.');
assert.equal(err.statementPosition, 33);
assert.equal(err.internalPosition, undefined);
assert.equal(err.internalQuery, undefined);
assert.equal(err.context, undefined);
assert.equal(err.schemaName, undefined);
assert.equal(err.tableName, undefined);
assert.equal(err.dataTypeName, undefined);
assert.equal(err.constraintName, undefined);
assert.equal(err.sourceFile, "parse_target.c");
assert(parseInt(err.sourceLine));
assert.equal(err.sourceFunction, "transformAssignedExpr");
});
});
});
describe('escapeLiteral', () => {
it('fails to escape when the server is not connected', () => {
const pq = new PQ();
const result = pq.escapeLiteral('test');
assert.strictEqual(result, null);
assert(pq.errorMessage());
});
it('escapes a simple string', () => {
const pq = new PQ();
pq.connectSync();
const result = pq.escapeLiteral('bang');
assert.equal(result, "'bang'");
});
it('escapes a bad string', () => {
const pq = new PQ();
pq.connectSync();
const result = pq.escapeLiteral("'; TRUNCATE TABLE blah;");
assert.equal(result, "'''; TRUNCATE TABLE blah;'");
});
});
describe('escapeIdentifier', () => {
it('fails when the server is not connected', () => {
const pq = new PQ();
const result = pq.escapeIdentifier('test');
assert.strictEqual(result, null);
assert(pq.errorMessage());
});
it('escapes a simple string', () => {
const pq = new PQ();
pq.connectSync();
const result = pq.escapeIdentifier('bang');
assert.equal(result, '"bang"');
});
});
describe('connecting', () => {
it('works', () => {
const client = new PQ();
client.connectSync();
});
});
describe('many connections', () => {
it('works', (done) => {
async.timesSeries(10, blink, done)
})
});
describe('connectSync', () => {
it('works 50 times in a row', () => {
const pqs = _.times(50, () => new PQ());
pqs.forEach((pq) => {
pq.connectSync();
});
pqs.forEach((pq) => {
pq.finish();
});
});
});
describe('connect async', () => {
const total = 50;
it(`works ${total} times in a row`, (done) => {
const pqs = _.times(total, () => new PQ());
let count = 0;
const connect = (cb: Function) => {
pqs.forEach((pq) => {
pq.connect((err) => {
assert.ifError(err);
count++;
pq.startReader();
if (count == total) {
cb();
}
});
});
};
connect(() => {
pqs.forEach((pq) => {
pq.stopReader();
pq.finish();
});
done();
});
});
});
describe('multiple queries', () => {
const pq = new PQ();
before((done) => {
pq.connect(done)
});
it('first query works', (done) => {
query(pq, done);
});
it('second query works', (done) => {
query(pq, done);
});
it('third query works', (done) => {
query(pq, done);
});
});
describe('set & get non blocking', () => {
let pq: PQ;
before(() => {
pq = new PQ();
pq.connectSync();
createTable(pq);
});
after(() => {
pq.finish();
});
it('is initially set to false', () => {
assert.strictEqual(pq.isNonBlocking(), false);
});
it('can switch back and forth', () => {
assert.strictEqual(pq.setNonBlocking(true), true);
assert.strictEqual(pq.isNonBlocking(), true);
assert.strictEqual(pq.setNonBlocking(), true);
assert.strictEqual(pq.isNonBlocking(), false);
});
});
describe('LISTEN/NOTIFY', () => {
let listener: PQ;
let notifier: PQ;
before(() => {
listener = new PQ();
notifier = new PQ();
listener.connectSync();
notifier.connectSync();
});
it('works', () => {
notifier.exec("NOTIFY testing, 'My Payload'");
let notice = listener.notifies();
assert.equal(notice, null);
listener.exec('LISTEN testing');
notifier.exec("NOTIFY testing, 'My Second Payload'");
listener.exec('SELECT NOW()');
notice = listener.notifies();
assert(notice, 'listener should have had a notification come in');
assert.equal(notice.relname, 'testing', 'missing relname == testing');
assert.equal(notice.extra, 'My Second Payload');
assert(notice.be_pid);
});
after(() => {
listener.finish();
notifier.finish();
});
});
describe('result accessors', () => {
let pq: PQ;
before(() => {
pq = new PQ();
pq.connectSync();
createTable(pq);
});
after(() => {
pq.finish();
});
before(() => {
pq.exec("INSERT INTO test_data(name, age) VALUES ('bob', 80) RETURNING *");
assert(!pq.errorMessage());
});
it('has ntuples', () => {
assert.strictEqual(pq.ntuples(), 1);
});
it('has cmdStatus', () => {
assert.equal(pq.cmdStatus(), 'INSERT 0 1');
});
it('has command tuples', () => {
assert.strictEqual(pq.cmdTuples(), '1');
});
});
describe('Retrieve server version from connection', () => {
it('return version number when connected', () => {
const pq = new PQ();
pq.connectSync();
const version = pq.serverVersion();
assert.equal(typeof version, 'number');
assert(version > 60000);
});
it('return zero when not connected', () => {
const pq = new PQ();
const version = pq.serverVersion();
assert.equal(typeof version, 'number');
assert.equal(version, 0);
});
});
describe('getting socket', () => {
let pq: PQ;
before(() => {
pq = new PQ();
pq.connectSync();
createTable(pq);
});
after(() => {
pq.finish();
});
it('returns -1 when not connected', () => {
const pq = new PQ();
assert.equal(pq.socket(), -1);
});
it('returns value when connected', () => {
assert(pq.socket() > 0);
});
});
describe('connecting with bad credentials', () => {
it('throws an error', () => {
try {
new PQ().connectSync('asldkfjlasdf');
} catch (e) {
assert.equal(e.toString().indexOf('connection pointer is NULL'), -1);
return;
}
assert.fail(null, null, 'Should have thrown an exception', '');
});
});
describe('connecting with no credentials', () => {
let pq: PQ;
before(() => {
pq = new PQ();
pq.connectSync();
});
it('is connected', () => {
assert(pq.connected, 'should have connected == true');
});
after(() => {
pq.finish();
assert(!pq.connected);
});
});
describe('result checking', () => {
let pq: PQ;
before(() => {
pq = new PQ();
pq.connectSync();
});
after(() => {
pq.finish();
});
it('executes query', () => {
pq.exec('SELECT NOW() as my_col');
assert.equal(pq.resultStatus(), 'PGRES_TUPLES_OK');
});
it('has 1 tuple', () => {
assert.equal(pq.ntuples(), 1);
});
it('has 1 field', () => {
assert.strictEqual(pq.nfields(), 1);
});
it('has column name', () => {
assert.equal(pq.fname(0), 'my_col');
});
it('has oid type of timestamptz', () => {
assert.strictEqual(pq.ftype(0), 1184);
});
it('has value as a date', () => {
const now = new Date();
const val = pq.getvalue(0);
const date = new Date(Date.parse(val));
assert.equal(date.getFullYear(), now.getFullYear());
assert.equal(date.getMonth(), now.getMonth());
});
it('can manually clear result multiple times', () => {
pq.clear();
pq.clear();
pq.clear();
});
});
describe('low-level query integration tests', () => {
let pq: PQ;
before(() => {
pq = new PQ();
pq.connectSync();
createTable(pq);
});
after(() => {
pq.finish();
});
describe('exec', () => {
before(() => {
pq.exec('SELECT * FROM test_data');
});
it('has correct tuples', () => {
assert.strictEqual(pq.ntuples(), 3);
});
it('has correct field count', () => {
assert.strictEqual(pq.nfields(), 2);
});
it('has correct rows', () => {
assert.strictEqual(pq.getvalue(0, 0), 'brian');
assert.strictEqual(pq.getvalue(1, 1), '30');
assert.strictEqual(pq.getvalue(2, 0), '');
assert.strictEqual(pq.getisnull(2, 0), false);
assert.strictEqual(pq.getvalue(2, 1), '');
assert.strictEqual(pq.getisnull(2, 1), true);
});
});
});
describe('sync query with parameters', () => {
let pq: PQ;
before(() => {
pq = new PQ();
pq.connectSync();
createTable(pq);
});
after(() => {
pq.finish();
});
it('works with single string parameter', () => {
const queryText = 'SELECT $1::text as name';
pq.execParams(queryText, ['Brian']);
assert.strictEqual(pq.ntuples(), 1);
assert.strictEqual(pq.getvalue(0, 0), 'Brian');
});
it('works with a number parameter', () => {
const queryText = 'SELECT $1::int as age';
pq.execParams(queryText, [32]);
assert.strictEqual(pq.ntuples(), 1);
assert.strictEqual(pq.getvalue(0, 0), '32');
});
it('works with multiple parameters', () => {
const queryText = 'INSERT INTO test_data(name, age) VALUES($1, $2)';
pq.execParams(queryText, ['Barkley', 4]);
assert.equal(pq.resultErrorMessage(), '');
});
});
describe('prepare and execPrepared', () => {
let pq: PQ;
before(() => {
pq = new PQ();
pq.connectSync();
createTable(pq);
});
after(() => {
pq.finish();
});
const statementName = 'get-name';
describe('preparing a statement', () => {
it('works properly', () => {
pq.prepare(statementName, 'SELECT $1::text as name', 1);
assert.ifError(pq.resultErrorMessage());
assert.equal(pq.resultStatus(), 'PGRES_COMMAND_OK');
});
});
describe('executing a prepared statement', () => {
it('works properly', () => {
pq.execPrepared(statementName, ['Brian']);
assert.ifError(pq.resultErrorMessage());
assert.strictEqual(pq.ntuples(), 1);
assert.strictEqual(pq.nfields(), 1);
assert.strictEqual(pq.getvalue(0, 0), 'Brian');
});
});
});

20
libpq/tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"baseUrl": "../",
"typeRoots": [
"../"
],
"types": [],
"noEmit": true,
"forceConsistentCasingInFileNames": true
},
"files": [
"index.d.ts",
"libpq-tests.ts"
]
}

1
libpq/tslint.json Normal file
View File

@@ -0,0 +1 @@
{ "extends": "../tslint.json" }