From dd3221a7695767ef975bddb84532aaaa689a0bf0 Mon Sep 17 00:00:00 2001 From: iamkhush Date: Wed, 22 Mar 2023 22:19:16 +0100 Subject: [PATCH 01/19] add pg-batch-query package --- packages/pg-batch-query/LICENSE | 9 ++ packages/pg-batch-query/README.md | 65 ++++++++++ packages/pg-batch-query/package.json | 45 +++++++ packages/pg-batch-query/src/index.ts | 131 +++++++++++++++++++++ packages/pg-batch-query/test/test-batch.ts | 47 ++++++++ packages/pg-batch-query/tsconfig.json | 26 ++++ 6 files changed, 323 insertions(+) create mode 100644 packages/pg-batch-query/LICENSE create mode 100644 packages/pg-batch-query/README.md create mode 100644 packages/pg-batch-query/package.json create mode 100644 packages/pg-batch-query/src/index.ts create mode 100644 packages/pg-batch-query/test/test-batch.ts create mode 100644 packages/pg-batch-query/tsconfig.json diff --git a/packages/pg-batch-query/LICENSE b/packages/pg-batch-query/LICENSE new file mode 100644 index 000000000..6b06ef98d --- /dev/null +++ b/packages/pg-batch-query/LICENSE @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2013 Ankush Chadda + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/pg-batch-query/README.md b/packages/pg-batch-query/README.md new file mode 100644 index 000000000..0d30dca98 --- /dev/null +++ b/packages/pg-batch-query/README.md @@ -0,0 +1,65 @@ +# pg-batch-query + + + +## installation + +```bash +$ npm install pg --save +$ npm install pg-batch-query --save +``` + +## use + +```js +const pg = require('pg') +var pool = new pg.Pool() +const BatchQuery = require('pg-batch-query') + +const batch = new BatchQuery({ + name: 'optional', + text: 'INSERT INTO foo (bar) VALUES ($1)', + values: [ + ['first'], + ['second'] + ] +}) + +pool.connect((err, client, done) => { + if (err) throw err + const result = client.query(batch).execute() + for (const res of result) { + for (const row of res) { + consolel.log(row) + } + } +}) +``` + +## contribution + +I'm very open to contribution! Open a pull request with your code or idea and we'll talk about it. If it's not way insane we'll merge it in too: isn't open source awesome? + +## license + +The MIT License (MIT) + +Copyright (c) 2013-2020 Ankush Chadda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/pg-batch-query/package.json b/packages/pg-batch-query/package.json new file mode 100644 index 000000000..cee2ec018 --- /dev/null +++ b/packages/pg-batch-query/package.json @@ -0,0 +1,45 @@ +{ + "name": "pg-batch-query", + "version": "1.0.0", + "description": "Postgres Batch Query for performant response time.", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "build": "rimraf dist && tsc", + "test": "mocha -r ts-node/register test/**/*.ts" + }, + "repository": { + "type": "git", + "url": "git://github.com/brianc/node-postgres.git", + "directory": "packages/pg-batch-query" + }, + "keywords": [ + "postgres", + "batch-query", + "pg", + "query" + ], + "files": [ + "/dist/*{js,ts,map}", + "/src" + ], + "author": "Ankush Chadda", + "license": "MIT", + "bugs": { + "url": "https://github.com/brianc/node-postgres/issues" + }, + "devDependencies": { + "@types/chai": "^4.2.13", + "@types/mocha": "^8.0.3", + "@types/node": "^14.0.0", + "@types/pg": "^7.14.5", + "eslint-plugin-promise": "^6.0.1", + "mocha": "^7.1.2", + "pg": "^8.10.0", + "ts-node": "^8.5.4", + "typescript": "^4.0.3" + }, + "peerDependencies": { + "pg": "^8" + } +} diff --git a/packages/pg-batch-query/src/index.ts b/packages/pg-batch-query/src/index.ts new file mode 100644 index 000000000..5f91b0939 --- /dev/null +++ b/packages/pg-batch-query/src/index.ts @@ -0,0 +1,131 @@ +import { Submittable, Connection, QueryResult } from 'pg' +const Result = require('pg/lib/result.js') +const EventEmitter = require('events').EventEmitter + +let nextUniqueID = 1 // concept borrowed from org.postgresql.core.v3.QueryExecutorImpl + +interface BatchQueryConfig { + name?: string + text: string + values?: string[][] +} + +class BatchQuery extends EventEmitter implements Submittable { + + name: string | null + text: string + values: string[][] + connection: Connection | null + _portal: string | null + _result: typeof Result | null + _results: typeof Result[] + + public constructor(batchQuery: BatchQueryConfig) { + super() + const { name, values, text } = batchQuery + + this.name = name + this.values = values + this.text = text + this.connection = null + this._portal = null + this._result = new Result() + this._results = [] + + for (const row of values) { + if (!Array.isArray(values)) { + throw new Error('Batch commands require each set of values to be an array. e.g. values: any[][]') + } + } + } + + public submit(connection: Connection): void { + this.connection = connection + + // creates a named prepared statement + this.connection.parse( + { + text: this.text, + name: this.name, + types: [] + }, + true + ) + + this.values.map(val => { + this._portal = 'C_' + nextUniqueID++ + this.connection.bind({ + statement: this.name, + values: val, + portal: this._portal + }, true) + + this.connection.describe({ + type: 'P', + name: this._portal, + }, true) + + this.connection.execute({portal: this._portal}, true) + }) + + this.connection.sync() + } + + execute(): Promise { + let promise + + // TODO: handle if there is a callback provided? + if (!this.callback) { + promise = new Promise((resolve, reject) => { + this.callback = (err, rows) => (err ? reject(err) : resolve(rows)) + }) + } + + // Return the promise (or undefined) + return promise + } + + handleError(err, connection) { + console.log(err) + } + + handleReadyForQuery(con) { + if (this._canceledDueToError) { + return this.handleError(this._canceledDueToError, con) + } + if (this.callback) { + try { + this.callback(null, this._results) + } + catch(err) { + process.nextTick(() => { + throw err + }) + } + } + this.emit('end', this._results) + } + + handleRowDescription(msg) { + this._result.addFields(msg.fields) + } + + handleDataRow(msg) { + const row = this._result.parseRow(msg.fields) + this._result.addRow(row) + } + + handleCommandComplete(msg) { + this._result.addCommandComplete(msg) + this._results.push(this._result) + this._result = new Result() + this.connection.close({ type: 'P', name: this._portal }, true) + } + + + handleEmptyQuery() { + this.connection.sync() + } +} + +export = BatchQuery diff --git a/packages/pg-batch-query/test/test-batch.ts b/packages/pg-batch-query/test/test-batch.ts new file mode 100644 index 000000000..03c908f3a --- /dev/null +++ b/packages/pg-batch-query/test/test-batch.ts @@ -0,0 +1,47 @@ +import { QueryResult } from "pg" +import Result from "pg/lib/result" + +const assert = require('assert') +const BatchQuery = require('../') +const pg = require('pg') + +describe('batch query', function () { + beforeEach(async function () { + const client = (this.client = new pg.Client()) + await client.connect() + await client.query('CREATE TEMP TABLE foo(name TEXT, id SERIAL PRIMARY KEY)') + }) + + afterEach(function () { + this.client.end() + }) + + it('batch insert works', async function () { + await this.client.query(new BatchQuery({ + text: 'INSERT INTO foo (name) VALUES ($1)', + values: [ + ['first'], + ['second'] + ] + })).execute() + const resp = await this.client.query('SELECT COUNT(*) from foo') + assert.strictEqual(resp.rows[0]['count'], '2') + }) + + it('batch select works', async function () { + await this.client.query('INSERT INTO foo (name) VALUES ($1)', ['first']) + await this.client.query('INSERT INTO foo (name) VALUES ($1)', ['second']) + const responses = await this.client.query(new BatchQuery({ + text: 'SELECT * from foo where name = $1', + values: [ + ['first'], + ['second'] + ], + name: 'optional' + })).execute() + console.log(responses) + for ( const response of responses) { + assert.strictEqual(response.rowCount, 1) + } + }) +}) diff --git a/packages/pg-batch-query/tsconfig.json b/packages/pg-batch-query/tsconfig.json new file mode 100644 index 000000000..15b962dd9 --- /dev/null +++ b/packages/pg-batch-query/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "module": "commonjs", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": false, + "target": "es6", + "noImplicitAny": false, + "moduleResolution": "node", + "sourceMap": true, + "pretty": true, + "outDir": "dist", + "incremental": true, + "baseUrl": ".", + "declaration": true, + "types": [ + "node", + "pg", + "mocha", + "chai" + ] + }, + "include": [ + "src/**/*" + ] +} From 330b0eeb96d685f25af4981889b83f6ff601b993 Mon Sep 17 00:00:00 2001 From: iamkhush Date: Fri, 24 Mar 2023 22:24:10 +0100 Subject: [PATCH 02/19] add reference for new ts package --- tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 53fb70c6e..7351372f3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "include": [], "references": [ {"path": "./packages/pg-query-stream"}, - {"path": "./packages/pg-protocol"} + {"path": "./packages/pg-protocol"}, + {"path": "./packages/pg-batch-query"} ] } From b0f7a432368f31da51de00ccee89e097f65f3bf8 Mon Sep 17 00:00:00 2001 From: iamkhush Date: Fri, 24 Mar 2023 22:59:52 +0100 Subject: [PATCH 03/19] remove logs from test --- packages/pg-batch-query/test/test-batch.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/pg-batch-query/test/test-batch.ts b/packages/pg-batch-query/test/test-batch.ts index 03c908f3a..b0b659dd8 100644 --- a/packages/pg-batch-query/test/test-batch.ts +++ b/packages/pg-batch-query/test/test-batch.ts @@ -1,6 +1,3 @@ -import { QueryResult } from "pg" -import Result from "pg/lib/result" - const assert = require('assert') const BatchQuery = require('../') const pg = require('pg') @@ -39,7 +36,6 @@ describe('batch query', function () { ], name: 'optional' })).execute() - console.log(responses) for ( const response of responses) { assert.strictEqual(response.rowCount, 1) } From f10c230e23cc92bf062f1845e7fe417f501b435c Mon Sep 17 00:00:00 2001 From: iamkhush Date: Wed, 29 Mar 2023 23:18:00 +0200 Subject: [PATCH 04/19] add test files --- packages/pg-batch-query/bench.ts | 79 +++++++++++++++++++ packages/pg-batch-query/src/index.ts | 27 +++---- packages/pg-batch-query/test/test-pool.ts | 55 +++++++++++++ .../test/{test-batch.ts => test-query.ts} | 6 +- 4 files changed, 147 insertions(+), 20 deletions(-) create mode 100644 packages/pg-batch-query/bench.ts create mode 100644 packages/pg-batch-query/test/test-pool.ts rename packages/pg-batch-query/test/{test-batch.ts => test-query.ts} (92%) diff --git a/packages/pg-batch-query/bench.ts b/packages/pg-batch-query/bench.ts new file mode 100644 index 000000000..256b1770d --- /dev/null +++ b/packages/pg-batch-query/bench.ts @@ -0,0 +1,79 @@ +import pg from 'pg' +import BatchQuery from './dist' + +const insert = (value) => ({ + text: 'INSERT INTO foobar(name, age) VALUES ($1, $2)', + values: ['brian', value], +}) + +const seq = { + text: 'SELECT * FROM generate_series(1, 1000)', +} + +let counter = 0 + +const simpleExec = async (client, getQuery, count) => { + const query = getQuery(count) + await client.query({ + text: query.text, + values: query.values, + rowMode: 'array', + }) +} + +const batchExec = async (client, getQuery, count) => { + const query = getQuery(count) + + const batchQuery = new BatchQuery({ + name: 'optional'+ counter++, + text: query.text, + values: [ + ['brian1', '1'], + ['brian2', '1'], + ['brian3', '1'], + ['brian4', '1'], + ['brian5', '1'] + ] + }) + await client.query(batchQuery).execute() +} + +const bench = async (client, mainMethod, q, time) => { + let start = Date.now() + let count = 0 + while (true) { + await mainMethod(client, q, count) + count++ + if (Date.now() - start > time) { + return count + } + } +} + +const run = async () => { + const client = new pg.Client() + await client.connect() + console.log('start') + await client.query('CREATE TEMP TABLE foobar(name TEXT, age NUMERIC)') + console.log('warmup done') + const seconds = 5 + + for (let i = 0; i < 4; i++) { + let queries = await bench(client, simpleExec, insert, 5 * 1000) + console.log('') + console.log('insert queries:', queries) + console.log('qps', queries / seconds) + console.log('on my laptop best so far seen 12467 qps') + + queries = await bench(client, batchExec, insert, 5 * 1000) + console.log('') + console.log('insert batch queries:', queries * 5) + console.log('qps', queries * 5 / seconds) + console.log('on my laptop best so far seen 28796 qps') + } + + await client.end() + await client.end() +} + +run().catch((e) => Boolean(console.error(e)) || process.exit(-1)) diff --git a/packages/pg-batch-query/src/index.ts b/packages/pg-batch-query/src/index.ts index 5f91b0939..67768cf8b 100644 --- a/packages/pg-batch-query/src/index.ts +++ b/packages/pg-batch-query/src/index.ts @@ -10,7 +10,7 @@ interface BatchQueryConfig { values?: string[][] } -class BatchQuery extends EventEmitter implements Submittable { +class BatchQuery implements Submittable { name: string | null text: string @@ -19,9 +19,10 @@ class BatchQuery extends EventEmitter implements Submittable { _portal: string | null _result: typeof Result | null _results: typeof Result[] + callback: Function | null + _canceledDueToError: Boolean public constructor(batchQuery: BatchQueryConfig) { - super() const { name, values, text } = batchQuery this.name = name @@ -31,6 +32,8 @@ class BatchQuery extends EventEmitter implements Submittable { this._portal = null this._result = new Result() this._results = [] + this.callback = null + this._canceledDueToError = false for (const row of values) { if (!Array.isArray(values)) { @@ -60,6 +63,7 @@ class BatchQuery extends EventEmitter implements Submittable { portal: this._portal }, true) + // maybe we could avoid this for non-select queries this.connection.describe({ type: 'P', name: this._portal, @@ -72,27 +76,17 @@ class BatchQuery extends EventEmitter implements Submittable { } execute(): Promise { - let promise - - // TODO: handle if there is a callback provided? - if (!this.callback) { - promise = new Promise((resolve, reject) => { - this.callback = (err, rows) => (err ? reject(err) : resolve(rows)) - }) - } - - // Return the promise (or undefined) - return promise + return new Promise((resolve, reject) => { + this.callback = (err, rows) => (err ? reject(err) : resolve(rows)) + }) } handleError(err, connection) { console.log(err) + this.connection.sync() } handleReadyForQuery(con) { - if (this._canceledDueToError) { - return this.handleError(this._canceledDueToError, con) - } if (this.callback) { try { this.callback(null, this._results) @@ -103,7 +97,6 @@ class BatchQuery extends EventEmitter implements Submittable { }) } } - this.emit('end', this._results) } handleRowDescription(msg) { diff --git a/packages/pg-batch-query/test/test-pool.ts b/packages/pg-batch-query/test/test-pool.ts new file mode 100644 index 000000000..c6beaaf65 --- /dev/null +++ b/packages/pg-batch-query/test/test-pool.ts @@ -0,0 +1,55 @@ +import assert from 'assert' +import BatchQuery from '../' +import pg from 'pg' + +describe('batch pool query', function () { + beforeEach(async function () { + this.pool = new pg.Pool({ max: 1 }) + // await this.pool.query('CREATE TEMP TABLE foo(name TEXT, id SERIAL PRIMARY KEY)') + }) + + afterEach(function () { + this.pool.end() + }) + + it('batch insert works', async function () { + const batchQueryPromise = new BatchQuery({ + text: 'INSERT INTO foo (name) VALUES ($1)', + values: [ + ['first'], + ['second'] + ] + }) + this.pool.connect(async (err, client, done) => { + if (err) throw err + await client.query('CREATE TEMP TABLE foo(name TEXT, id SERIAL PRIMARY KEY)') + await client.query(batchQueryPromise).execute() + const resp = await client.query('SELECT COUNT(*) from foo') + await client.release() + assert.strictEqual(resp.rows[0]['count'], '2') + }) + + }) + + it('batch select works', async function () { + const batchQueryPromise = new BatchQuery({ + text: 'SELECT * from foo where name = $1', + values: [ + ['first'], + ['second'] + ], + name: 'optional' + }) + this.pool.connect(async (err, client, done) => { + if (err) throw err + await client.query('CREATE TEMP TABLE foo(name TEXT, id SERIAL PRIMARY KEY)') + await client.query('INSERT INTO foo (name) VALUES ($1)', ['first']) + await client.query('INSERT INTO foo (name) VALUES ($1)', ['second']) + const responses = await client.query(batchQueryPromise).execute() + await client.release() + for ( const response of responses) { + assert.strictEqual(response.rowCount, 1) + } + }) + }) +}) diff --git a/packages/pg-batch-query/test/test-batch.ts b/packages/pg-batch-query/test/test-query.ts similarity index 92% rename from packages/pg-batch-query/test/test-batch.ts rename to packages/pg-batch-query/test/test-query.ts index b0b659dd8..460275271 100644 --- a/packages/pg-batch-query/test/test-batch.ts +++ b/packages/pg-batch-query/test/test-query.ts @@ -1,6 +1,6 @@ -const assert = require('assert') -const BatchQuery = require('../') -const pg = require('pg') +import assert from 'assert' +import BatchQuery from '../' +import pg from 'pg' describe('batch query', function () { beforeEach(async function () { From 5ef3546f35fc084ddaf2722ed65fe1ba09082389 Mon Sep 17 00:00:00 2001 From: iamkhush Date: Wed, 29 Mar 2023 23:19:23 +0200 Subject: [PATCH 05/19] fix readme --- packages/pg-batch-query/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pg-batch-query/README.md b/packages/pg-batch-query/README.md index 0d30dca98..d406e52d4 100644 --- a/packages/pg-batch-query/README.md +++ b/packages/pg-batch-query/README.md @@ -18,7 +18,7 @@ const BatchQuery = require('pg-batch-query') const batch = new BatchQuery({ name: 'optional', - text: 'INSERT INTO foo (bar) VALUES ($1)', + text: 'SELECT from foo where bar = $1', values: [ ['first'], ['second'] From 424ac4249032980d8a49757e376506e6ec167d2b Mon Sep 17 00:00:00 2001 From: iamkhush Date: Fri, 31 Mar 2023 22:17:30 +0200 Subject: [PATCH 06/19] fix import --- packages/pg-batch-query/bench.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pg-batch-query/bench.ts b/packages/pg-batch-query/bench.ts index 256b1770d..c668a48c2 100644 --- a/packages/pg-batch-query/bench.ts +++ b/packages/pg-batch-query/bench.ts @@ -1,5 +1,5 @@ import pg from 'pg' -import BatchQuery from './dist' +import BatchQuery from './src' const insert = (value) => ({ text: 'INSERT INTO foobar(name, age) VALUES ($1, $2)', From 49885b28c456798c74177a2faaf7499985810f54 Mon Sep 17 00:00:00 2001 From: iamkhush Date: Fri, 31 Mar 2023 22:18:07 +0200 Subject: [PATCH 07/19] remove console.log --- packages/pg-batch-query/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/pg-batch-query/src/index.ts b/packages/pg-batch-query/src/index.ts index 67768cf8b..a11108a76 100644 --- a/packages/pg-batch-query/src/index.ts +++ b/packages/pg-batch-query/src/index.ts @@ -82,7 +82,6 @@ class BatchQuery implements Submittable { } handleError(err, connection) { - console.log(err) this.connection.sync() } From 5b08fba5873bd0ca824b246ccebd1867f6941fdd Mon Sep 17 00:00:00 2001 From: iamkhush Date: Sat, 8 Apr 2023 23:55:02 +0200 Subject: [PATCH 08/19] handle error by rejecting the promise --- packages/pg-batch-query/src/index.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/pg-batch-query/src/index.ts b/packages/pg-batch-query/src/index.ts index a11108a76..8f0e30031 100644 --- a/packages/pg-batch-query/src/index.ts +++ b/packages/pg-batch-query/src/index.ts @@ -20,7 +20,6 @@ class BatchQuery implements Submittable { _result: typeof Result | null _results: typeof Result[] callback: Function | null - _canceledDueToError: Boolean public constructor(batchQuery: BatchQueryConfig) { const { name, values, text } = batchQuery @@ -33,7 +32,6 @@ class BatchQuery implements Submittable { this._result = new Result() this._results = [] this.callback = null - this._canceledDueToError = false for (const row of values) { if (!Array.isArray(values)) { @@ -82,7 +80,10 @@ class BatchQuery implements Submittable { } handleError(err, connection) { - this.connection.sync() + this.connection.flush() + if (this.callback) { + this.callback(err) + } } handleReadyForQuery(con) { @@ -91,9 +92,7 @@ class BatchQuery implements Submittable { this.callback(null, this._results) } catch(err) { - process.nextTick(() => { throw err - }) } } } From c014b729cee5426ee6cee2eba1fb82a2ce284758 Mon Sep 17 00:00:00 2001 From: iamkhush Date: Sat, 8 Apr 2023 23:55:18 +0200 Subject: [PATCH 09/19] add error handling tests --- .../test/test-error-handling.ts | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 packages/pg-batch-query/test/test-error-handling.ts diff --git a/packages/pg-batch-query/test/test-error-handling.ts b/packages/pg-batch-query/test/test-error-handling.ts new file mode 100644 index 000000000..f556a1ddc --- /dev/null +++ b/packages/pg-batch-query/test/test-error-handling.ts @@ -0,0 +1,71 @@ +import assert from 'assert' +import pg from 'pg' +import { DatabaseError } from 'pg-protocol' +import BatchQuery from "../src" + +describe('BatchQuery error handling', function () { + beforeEach(async function () { + this.client = new pg.Client() + await this.client.connect() + }) + + afterEach(function (){ + this.client.end() + }) + + it('handles error in parsing but can continue with another client', async function() { + const batch = new BatchQuery({ + text: 'INSERT INTO foo (name) VALUES ($1)', + values: [ + ['first'], + ['second'] + ] + }) + // fails since table is not yet created + try { + await this.client.query(batch).execute() + } catch (e) { + assert.equal(e.message, 'relation "foo" does not exist') + } + await this.client.query('Select now()') + }) + + it('handles error in insert of some of the values provided and reverts transaction', async function (){ + await this.client.query('CREATE TEMP TABLE foo(value int, id SERIAL PRIMARY KEY)') + const batch = new BatchQuery({ + text: 'INSERT INTO foo (value) VALUES ($1)', + values: [ + ['1'], + ['3'], + ['xxx'] + ], + }) + // fails since xyz is not an int + try { + await this.client.query(batch).execute() + } catch (e) { + assert.equal(e.message, 'invalid input syntax for type integer: "xxx"') + } + const response = await this.client.query('Select sum(value) from foo') + console.log(response) + assert.equal(response.rows[0]['sum'], null) + }) + + it('handles error in select batch query', async function (){ + await this.client.query('CREATE TEMP TABLE foo(value int, id SERIAL PRIMARY KEY)') + const batch = new BatchQuery({ + text: 'SELECT * from foo where value = ($1)', + values: [ + ['1'], + ['3'], + ['xxx'] + ], + }) + // fails since xxx is not an int + try { + await this.client.query(batch).execute() + } catch (e) { + assert.equal(e.message, 'invalid input syntax for type integer: "xxx"') + } + }) +}) \ No newline at end of file From 9545859ad6aba15025a512fac512f9df5ff19490 Mon Sep 17 00:00:00 2001 From: iamkhush Date: Sat, 8 Apr 2023 23:55:48 +0200 Subject: [PATCH 10/19] fix import --- packages/pg-batch-query/test/test-pool.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/pg-batch-query/test/test-pool.ts b/packages/pg-batch-query/test/test-pool.ts index c6beaaf65..783202273 100644 --- a/packages/pg-batch-query/test/test-pool.ts +++ b/packages/pg-batch-query/test/test-pool.ts @@ -1,11 +1,10 @@ import assert from 'assert' -import BatchQuery from '../' +import BatchQuery from '../src' import pg from 'pg' describe('batch pool query', function () { beforeEach(async function () { this.pool = new pg.Pool({ max: 1 }) - // await this.pool.query('CREATE TEMP TABLE foo(name TEXT, id SERIAL PRIMARY KEY)') }) afterEach(function () { From 2890c052c2ba6baa6d6bf83d0eee1ee16c615a8d Mon Sep 17 00:00:00 2001 From: iamkhush Date: Sat, 8 Apr 2023 23:55:58 +0200 Subject: [PATCH 11/19] add test --- packages/pg-batch-query/test/test-query.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/pg-batch-query/test/test-query.ts b/packages/pg-batch-query/test/test-query.ts index 460275271..705b7ed86 100644 --- a/packages/pg-batch-query/test/test-query.ts +++ b/packages/pg-batch-query/test/test-query.ts @@ -40,4 +40,18 @@ describe('batch query', function () { assert.strictEqual(response.rowCount, 1) } }) + + it('batch insert with non string values', async function () { + await this.client.query('CREATE TEMP TABLE bar(value INT, id SERIAL PRIMARY KEY)') + const batchInsert = new BatchQuery({ + text: 'INSERT INTO bar (value) VALUES ($1)', + values: [ + ['1'], + ['2'] + ] + }) + await this.client.query(batchInsert).execute() + const resp = await this.client.query('SELECT SUM(value) from bar') + assert.strictEqual(resp.rows[0]['sum'], '3') + }) }) From c9d6d119f12b5a71d3d6b8342760a11ef008363a Mon Sep 17 00:00:00 2001 From: iamkhush Date: Sun, 9 Apr 2023 00:00:31 +0200 Subject: [PATCH 12/19] fix in comments --- packages/pg-batch-query/test/test-error-handling.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pg-batch-query/test/test-error-handling.ts b/packages/pg-batch-query/test/test-error-handling.ts index f556a1ddc..6abfe5aad 100644 --- a/packages/pg-batch-query/test/test-error-handling.ts +++ b/packages/pg-batch-query/test/test-error-handling.ts @@ -40,7 +40,7 @@ describe('BatchQuery error handling', function () { ['xxx'] ], }) - // fails since xyz is not an int + // fails since xxx is not an int try { await this.client.query(batch).execute() } catch (e) { From 758890b9bb2ac5dcd61fd5a08a58363273328bc5 Mon Sep 17 00:00:00 2001 From: iamkhush Date: Sun, 9 Apr 2023 00:32:47 +0200 Subject: [PATCH 13/19] fix for postgress 11 --- packages/pg-batch-query/test/test-error-handling.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pg-batch-query/test/test-error-handling.ts b/packages/pg-batch-query/test/test-error-handling.ts index 6abfe5aad..7c570238e 100644 --- a/packages/pg-batch-query/test/test-error-handling.ts +++ b/packages/pg-batch-query/test/test-error-handling.ts @@ -44,7 +44,7 @@ describe('BatchQuery error handling', function () { try { await this.client.query(batch).execute() } catch (e) { - assert.equal(e.message, 'invalid input syntax for type integer: "xxx"') + assert.equal(e.message, 'invalid input syntax for integer: "xxx"') } const response = await this.client.query('Select sum(value) from foo') console.log(response) @@ -65,7 +65,7 @@ describe('BatchQuery error handling', function () { try { await this.client.query(batch).execute() } catch (e) { - assert.equal(e.message, 'invalid input syntax for type integer: "xxx"') + assert.equal(e.message, 'invalid input syntax for integer: "xxx"') } }) }) \ No newline at end of file From b2ea290fa43bc71d8243bcdc8e1ae3009cef43ce Mon Sep 17 00:00:00 2001 From: iamkhush Date: Sun, 9 Apr 2023 00:36:58 +0200 Subject: [PATCH 14/19] remove log --- packages/pg-batch-query/test/test-error-handling.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/pg-batch-query/test/test-error-handling.ts b/packages/pg-batch-query/test/test-error-handling.ts index 7c570238e..8961ac3a5 100644 --- a/packages/pg-batch-query/test/test-error-handling.ts +++ b/packages/pg-batch-query/test/test-error-handling.ts @@ -47,7 +47,6 @@ describe('BatchQuery error handling', function () { assert.equal(e.message, 'invalid input syntax for integer: "xxx"') } const response = await this.client.query('Select sum(value) from foo') - console.log(response) assert.equal(response.rows[0]['sum'], null) }) From fb832558c53f38c39dd0d07cf34b36d6d9c0556c Mon Sep 17 00:00:00 2001 From: iamkhush Date: Wed, 19 Apr 2023 05:44:13 +0200 Subject: [PATCH 15/19] add link in the readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 967431358..3acc4636d 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ This repo is a monorepo which contains the core [pg](https://github.com/brianc/n - [pg-query-stream](https://github.com/brianc/node-postgres/tree/master/packages/pg-query-stream) - [pg-connection-string](https://github.com/brianc/node-postgres/tree/master/packages/pg-connection-string) - [pg-protocol](https://github.com/brianc/node-postgres/tree/master/packages/pg-protocol) +- [pg-batch-query](https://github.com/brianc/node-postgres/tree/master/packages/pg-batch-query) ## Documentation From 194e1fafef81e260fdcbc574e3628a5c18f7eeba Mon Sep 17 00:00:00 2001 From: iamkhush Date: Sat, 29 Apr 2023 01:13:35 +0200 Subject: [PATCH 16/19] add valuemapper to bind method --- packages/pg-batch-query/src/index.ts | 8 ++++---- packages/pg-batch-query/test/test-query.ts | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/pg-batch-query/src/index.ts b/packages/pg-batch-query/src/index.ts index 8f0e30031..2bc0063ab 100644 --- a/packages/pg-batch-query/src/index.ts +++ b/packages/pg-batch-query/src/index.ts @@ -1,13 +1,12 @@ import { Submittable, Connection, QueryResult } from 'pg' const Result = require('pg/lib/result.js') -const EventEmitter = require('events').EventEmitter - +const utils = require('pg/lib/utils.js') let nextUniqueID = 1 // concept borrowed from org.postgresql.core.v3.QueryExecutorImpl interface BatchQueryConfig { name?: string text: string - values?: string[][] + values?: any[][] } class BatchQuery implements Submittable { @@ -58,7 +57,8 @@ class BatchQuery implements Submittable { this.connection.bind({ statement: this.name, values: val, - portal: this._portal + portal: this._portal, + valueMapper: utils.prepareValue, }, true) // maybe we could avoid this for non-select queries diff --git a/packages/pg-batch-query/test/test-query.ts b/packages/pg-batch-query/test/test-query.ts index 705b7ed86..d923b434d 100644 --- a/packages/pg-batch-query/test/test-query.ts +++ b/packages/pg-batch-query/test/test-query.ts @@ -1,5 +1,5 @@ import assert from 'assert' -import BatchQuery from '../' +import BatchQuery from '../src' import pg from 'pg' describe('batch query', function () { @@ -54,4 +54,20 @@ describe('batch query', function () { const resp = await this.client.query('SELECT SUM(value) from bar') assert.strictEqual(resp.rows[0]['sum'], '3') }) + + it('If query is for an array', async function() { + await this.client.query('INSERT INTO foo (name) VALUES ($1)', ['first']) + await this.client.query('INSERT INTO foo (name) VALUES ($1)', ['second']) + const responses = await this.client.query(new BatchQuery({ + text: `SELECT * from foo where name = ANY($1)`, + values: [ + [['first', 'third']], + [['second', 'fourth']] + ], + name: 'optional' + })).execute() + assert.equal(responses.length, 2) + for ( const response of responses) { + assert.strictEqual(response.rowCount, 1) + }}) }) From e244f03fed25214293a521a519d59179eacc27df Mon Sep 17 00:00:00 2001 From: iamkhush Date: Thu, 18 May 2023 19:21:37 +0200 Subject: [PATCH 17/19] adding private repo for types/pg for testing and updated some other packages --- packages/pg-batch-query/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/pg-batch-query/package.json b/packages/pg-batch-query/package.json index cee2ec018..9bb61e5a8 100644 --- a/packages/pg-batch-query/package.json +++ b/packages/pg-batch-query/package.json @@ -32,12 +32,12 @@ "@types/chai": "^4.2.13", "@types/mocha": "^8.0.3", "@types/node": "^14.0.0", - "@types/pg": "^7.14.5", + "@types/pg": "iamkhush/pg-types", "eslint-plugin-promise": "^6.0.1", "mocha": "^7.1.2", "pg": "^8.10.0", - "ts-node": "^8.5.4", - "typescript": "^4.0.3" + "ts-node": "^10.9.1", + "typescript": "^5.0.4" }, "peerDependencies": { "pg": "^8" From 7573a20de9c87a793944845463678f508de5f21c Mon Sep 17 00:00:00 2001 From: iamkhush Date: Sat, 20 May 2023 01:13:06 +0200 Subject: [PATCH 18/19] add readme info --- packages/pg-batch-query/README.md | 8 ++++++- packages/pg-batch-query/bench.ts | 35 +++++++++++++++++++++---------- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/packages/pg-batch-query/README.md b/packages/pg-batch-query/README.md index d406e52d4..6125bb715 100644 --- a/packages/pg-batch-query/README.md +++ b/packages/pg-batch-query/README.md @@ -1,6 +1,12 @@ # pg-batch-query +Batches queries by using the [Extended query protocol](https://www.postgresql.org/docs/current/protocol-flow.html#PROTOCOL-FLOW-EXT-QUERY). +Essentially we do the following +- send a single PARSE command to create a named statement. +- send a pair of BIND and EXECUTE commands +- Finally send a SYNC to close the current transaction. +As [per benchmark tests](./bench.ts), number of queries per seconds gets tripled using batched queries. ## installation @@ -30,7 +36,7 @@ pool.connect((err, client, done) => { const result = client.query(batch).execute() for (const res of result) { for (const row of res) { - consolel.log(row) + console.log(row) } } }) diff --git a/packages/pg-batch-query/bench.ts b/packages/pg-batch-query/bench.ts index c668a48c2..29e75d43a 100644 --- a/packages/pg-batch-query/bench.ts +++ b/packages/pg-batch-query/bench.ts @@ -3,12 +3,13 @@ import BatchQuery from './src' const insert = (value) => ({ text: 'INSERT INTO foobar(name, age) VALUES ($1, $2)', - values: ['brian', value], + values: ['joe' + value, value], }) -const seq = { - text: 'SELECT * FROM generate_series(1, 1000)', -} +const select = (value) => ({ + text: 'SELECT FROM foobar where name = $1 and age = $2', + values: ['joe' + value, value], +}) let counter = 0 @@ -28,11 +29,11 @@ const batchExec = async (client, getQuery, count) => { name: 'optional'+ counter++, text: query.text, values: [ - ['brian1', '1'], - ['brian2', '1'], - ['brian3', '1'], - ['brian4', '1'], - ['brian5', '1'] + ['joe1', count], + ['joe2', count], + ['joe3', count], + ['joe4', count], + ['joe5', count] ] }) await client.query(batchQuery).execute() @@ -63,13 +64,25 @@ const run = async () => { console.log('') console.log('insert queries:', queries) console.log('qps', queries / seconds) - console.log('on my laptop best so far seen 12467 qps') + console.log('on my laptop best so far seen 16115.4 qps') queries = await bench(client, batchExec, insert, 5 * 1000) console.log('') console.log('insert batch queries:', queries * 5) console.log('qps', queries * 5 / seconds) - console.log('on my laptop best so far seen 28796 qps') + console.log('on my laptop best so far seen 42646 qps') + + queries = await bench(client, simpleExec, select, 5 * 1000) + console.log('') + console.log('select queries:', queries) + console.log('qps', queries / seconds) + console.log('on my laptop best so far seen 18579.8 qps') + + queries = await bench(client, batchExec, select, 5 * 1000) + console.log('') + console.log('select batch queries:', queries * 5) + console.log('qps', queries * 5 / seconds) + console.log('on my laptop best so far seen 44887 qps') } await client.end() From 7f4ef4c80dc815cec799bbee53a3b93576102ece Mon Sep 17 00:00:00 2001 From: iamkhush Date: Tue, 6 Jun 2023 08:13:11 +0200 Subject: [PATCH 19/19] update pg package --- packages/pg-batch-query/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pg-batch-query/package.json b/packages/pg-batch-query/package.json index 9bb61e5a8..62a5b96b2 100644 --- a/packages/pg-batch-query/package.json +++ b/packages/pg-batch-query/package.json @@ -32,7 +32,7 @@ "@types/chai": "^4.2.13", "@types/mocha": "^8.0.3", "@types/node": "^14.0.0", - "@types/pg": "iamkhush/pg-types", + "@types/pg": "^8.10.2", "eslint-plugin-promise": "^6.0.1", "mocha": "^7.1.2", "pg": "^8.10.0",