diff --git a/libpq/index.d.ts b/libpq/index.d.ts new file mode 100644 index 0000000000..df2135046a --- /dev/null +++ b/libpq/index.d.ts @@ -0,0 +1,428 @@ +// Type definitions for libpq 1.8 +// Project: https://github.com/brianc/node-libpq#readme +// Definitions by: Vlad Rindevich +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +/// + +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): 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): 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): 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; diff --git a/libpq/libpq-tests.ts b/libpq/libpq-tests.ts new file mode 100644 index 0000000000..dda341a4bb --- /dev/null +++ b/libpq/libpq-tests.ts @@ -0,0 +1,828 @@ +/// +/// + +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 = 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( 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'); + }); + }); +}); diff --git a/libpq/tsconfig.json b/libpq/tsconfig.json new file mode 100644 index 0000000000..3fe28b1ad1 --- /dev/null +++ b/libpq/tsconfig.json @@ -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" + ] +} diff --git a/libpq/tslint.json b/libpq/tslint.json new file mode 100644 index 0000000000..377cc837d4 --- /dev/null +++ b/libpq/tslint.json @@ -0,0 +1 @@ +{ "extends": "../tslint.json" }