From dafc9b4e5a5ce939b5b3b3c97d244decab3b20db Mon Sep 17 00:00:00 2001 From: Leonardo Zanivan Date: Wed, 1 Apr 2026 08:37:41 -0300 Subject: [PATCH 1/6] feat: add new client.getTransactionStatus() method Adds a new public method to retrieve the current transaction status of the client connection. Returns 'I' (idle), 'T' (in transaction), 'E' (error/aborted), or null (initial state/native client). The transaction status is tracked from PostgreSQL's ReadyForQuery message after each query completes. Native client returns null as it does not support this feature yet. --- docs/pages/apis/client.mdx | 61 ++++++++++++- packages/pg/lib/client.js | 6 ++ packages/pg/lib/native/client.js | 5 ++ .../test/integration/client/txstatus-tests.js | 85 +++++++++++++++++++ 4 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 packages/pg/test/integration/client/txstatus-tests.js diff --git a/docs/pages/apis/client.mdx b/docs/pages/apis/client.mdx index 5867ad5a6..ad8aadf4d 100644 --- a/docs/pages/apis/client.mdx +++ b/docs/pages/apis/client.mdx @@ -23,9 +23,7 @@ type Config = { lock_timeout?: number, // number of milliseconds a query is allowed to be en lock state before it's cancelled due to lock timeout application_name?: string, // The name of the application that created this Client instance connectionTimeoutMillis?: number, // number of milliseconds to wait for connection, default is no timeout - keepAlive?: boolean, // if true, enable keepalive on the `net.Socket` - keepAliveInitialDelayMillis?: number, // number of milliseconds between last data packet received and first keepalive probe, default is 0 (keep existing value); - // no effect unless `keepAlive` is true + keepAliveInitialDelayMillis?: number, // set the initial delay before the first keepalive probe is sent on an idle socket idle_in_transaction_session_timeout?: number, // number of milliseconds before terminating any session with an open idle transaction, default is no timeout client_encoding?: string, // specifies the character set encoding that the database uses for sending data to the client fallback_application_name?: string, // provide an application name to use if application_name is not set @@ -175,6 +173,63 @@ await client.end() console.log('client has disconnected') ``` +## client.getTransactionStatus + +`client.getTransactionStatus() => string | null` + +Returns the current transaction status of the client connection. This can be useful for debugging transaction state issues or implementing custom transaction management logic. + +**Return values:** + +- `'I'` - Idle (not in a transaction) +- `'T'` - Transaction active (BEGIN has been issued) +- `'E'` - Error (transaction aborted, requires ROLLBACK) +- `null` - Initial state or not supported (native client) + +The transaction status is updated after each query completes based on the PostgreSQL backend's `ReadyForQuery` message. + +**Example: Checking transaction state** + +```js +import { Client } from 'pg' +const client = new Client() +await client.connect() + +await client.query('BEGIN') +console.log(client.getTransactionStatus()) // 'T' - in transaction + +await client.query('SELECT * FROM users') +console.log(client.getTransactionStatus()) // 'T' - still in transaction + +await client.query('COMMIT') +console.log(client.getTransactionStatus()) // 'I' - idle + +await client.end() +``` + +**Example: Handling transaction errors** + +```js +import { Client } from 'pg' +const client = new Client() +await client.connect() + +await client.query('BEGIN') +try { + await client.query('INVALID SQL') +} catch (err) { + console.log(client.getTransactionStatus()) // 'E' - error state + + // Must rollback to recover + await client.query('ROLLBACK') + console.log(client.getTransactionStatus()) // 'I' - idle again +} + +await client.end() +``` + +**Note:** This method is not supported in the native client and will always return `null`. + ## events ### error diff --git a/packages/pg/lib/client.js b/packages/pg/lib/client.js index 9200dded6..48d3a595b 100644 --- a/packages/pg/lib/client.js +++ b/packages/pg/lib/client.js @@ -71,6 +71,7 @@ class Client extends EventEmitter { this._connectionError = false this._queryable = true this._activeQuery = null + this._txStatus = null this.enableChannelBinding = Boolean(c.enableChannelBinding) // set true to use SCRAM-SHA-256-PLUS when offered this.connection = @@ -359,6 +360,7 @@ class Client extends EventEmitter { } const activeQuery = this._getActiveQuery() this._activeQuery = null + this._txStatus = msg?.status ?? null this.readyForQuery = true if (activeQuery) { activeQuery.handleReadyForQuery(this.connection) @@ -703,6 +705,10 @@ class Client extends EventEmitter { this.connection.unref() } + getTransactionStatus() { + return this._txStatus + } + end(cb) { this._ending = true diff --git a/packages/pg/lib/native/client.js b/packages/pg/lib/native/client.js index d8bb4dce5..9d09d770d 100644 --- a/packages/pg/lib/native/client.js +++ b/packages/pg/lib/native/client.js @@ -321,3 +321,8 @@ Client.prototype.getTypeParser = function (oid, format) { Client.prototype.isConnected = function () { return this._connected } + +Client.prototype.getTransactionStatus = function () { + // not supported in native client + return null +} diff --git a/packages/pg/test/integration/client/txstatus-tests.js b/packages/pg/test/integration/client/txstatus-tests.js new file mode 100644 index 000000000..3529ce0fe --- /dev/null +++ b/packages/pg/test/integration/client/txstatus-tests.js @@ -0,0 +1,85 @@ +'use strict' +const helper = require('./test-helper') +const suite = new helper.Suite() +const pg = helper.pg +const assert = require('assert') + +// txStatus tracking is not supported in native client +if (!helper.args.native) { + suite.test('txStatus tracking', function (done) { + const client = new pg.Client() + client.connect( + assert.success(function () { + // Run a simple query to initialize txStatus + client.query( + 'SELECT 1', + assert.success(function () { + // Test 1: Initial state after query (should be idle) + assert.equal(client.getTransactionStatus(), 'I', 'should start in idle state') + + // Test 2: BEGIN transaction + client.query( + 'BEGIN', + assert.success(function () { + assert.equal(client.getTransactionStatus(), 'T', 'should be in transaction state') + + // Test 3: COMMIT + client.query( + 'COMMIT', + assert.success(function () { + assert.equal(client.getTransactionStatus(), 'I', 'should return to idle after commit') + + client.end(done) + }) + ) + }) + ) + }) + ) + }) + ) + }) + + suite.test('txStatus error state', function (done) { + const client = new pg.Client() + client.connect( + assert.success(function () { + // Run a simple query to initialize txStatus + client.query( + 'SELECT 1', + assert.success(function () { + client.query( + 'BEGIN', + assert.success(function () { + // Execute invalid SQL to trigger error state + client.query('INVALID SQL SYNTAX', function (err) { + assert(err, 'should receive error from invalid query') + + // Issue a sync query to ensure ReadyForQuery has been processed + // This guarantees transaction status has been updated + client.query('SELECT 1', function () { + // This callback fires after ReadyForQuery is processed + assert.equal(client.getTransactionStatus(), 'E', 'should be in error state') + + // Rollback to recover + client.query( + 'ROLLBACK', + assert.success(function () { + assert.equal( + client.getTransactionStatus(), + 'I', + 'should return to idle after rollback from error' + ) + client.end(done) + }) + ) + }) + }) + }) + ) + }) + ) + }) + ) + }) +} From a22408ce996326350f3f7baa927571476c175595 Mon Sep 17 00:00:00 2001 From: Leonardo Zanivan Date: Wed, 1 Apr 2026 08:51:23 -0300 Subject: [PATCH 2/6] feat: add native client support for getTransactionStatus() - Add getTransactionStatus() to pg-native using libpq's PQtransactionStatus() with status mapping (0->I, 2->T, 3->E) - Update pg native client wrapper to delegate to pg-native - Remove native guard from txstatus tests (now runs in both modes) - Bump libpq to ^1.10.0 for transactionStatus() binding support --- packages/pg-native/index.js | 8 + packages/pg-native/package.json | 2 +- packages/pg/lib/native/client.js | 3 +- .../test/integration/client/txstatus-tests.js | 139 +++++++++--------- yarn.lock | 15 +- 5 files changed, 88 insertions(+), 79 deletions(-) diff --git a/packages/pg-native/index.js b/packages/pg-native/index.js index 8c83406bb..1c18241db 100644 --- a/packages/pg-native/index.js +++ b/packages/pg-native/index.js @@ -6,6 +6,10 @@ const types = require('pg-types') const buildResult = require('./lib/build-result') const CopyStream = require('./lib/copy-stream') +// https://www.postgresql.org/docs/current/libpq-status.html#LIBPQ-PQTRANSACTIONSTATUS +// 0=IDLE, 1=ACTIVE, 2=INTRANS, 3=INERROR +const statusMap = { 0: 'I', 2: 'T', 3: 'E' } + const Client = (module.exports = function (config) { if (!(this instanceof Client)) { return new Client(config) @@ -145,6 +149,10 @@ Client.prototype.escapeIdentifier = function (value) { return this.pq.escapeIdentifier(value) } +Client.prototype.getTransactionStatus = function () { + return statusMap[this.pq.transactionStatus()] ?? null +} + // export the version number so we can check it in node-postgres module.exports.version = require('./package.json').version diff --git a/packages/pg-native/package.json b/packages/pg-native/package.json index 33ee9d50f..573cd1656 100644 --- a/packages/pg-native/package.json +++ b/packages/pg-native/package.json @@ -34,7 +34,7 @@ }, "homepage": "https://github.com/brianc/node-postgres/tree/master/packages/pg-native", "dependencies": { - "libpq": "^1.8.15", + "libpq": "^1.10.0", "pg-types": "2.2.0" }, "devDependencies": { diff --git a/packages/pg/lib/native/client.js b/packages/pg/lib/native/client.js index 9d09d770d..6df471b83 100644 --- a/packages/pg/lib/native/client.js +++ b/packages/pg/lib/native/client.js @@ -323,6 +323,5 @@ Client.prototype.isConnected = function () { } Client.prototype.getTransactionStatus = function () { - // not supported in native client - return null + return this.native.getTransactionStatus() } diff --git a/packages/pg/test/integration/client/txstatus-tests.js b/packages/pg/test/integration/client/txstatus-tests.js index 3529ce0fe..cb8b740f8 100644 --- a/packages/pg/test/integration/client/txstatus-tests.js +++ b/packages/pg/test/integration/client/txstatus-tests.js @@ -4,82 +4,79 @@ const suite = new helper.Suite() const pg = helper.pg const assert = require('assert') -// txStatus tracking is not supported in native client -if (!helper.args.native) { - suite.test('txStatus tracking', function (done) { - const client = new pg.Client() - client.connect( - assert.success(function () { - // Run a simple query to initialize txStatus - client.query( - 'SELECT 1', - assert.success(function () { - // Test 1: Initial state after query (should be idle) - assert.equal(client.getTransactionStatus(), 'I', 'should start in idle state') +suite.test('txStatus tracking', function (done) { + const client = new pg.Client() + client.connect( + assert.success(function () { + // Run a simple query to initialize txStatus + client.query( + 'SELECT 1', + assert.success(function () { + // Test 1: Initial state after query (should be idle) + assert.equal(client.getTransactionStatus(), 'I', 'should start in idle state') - // Test 2: BEGIN transaction - client.query( - 'BEGIN', - assert.success(function () { - assert.equal(client.getTransactionStatus(), 'T', 'should be in transaction state') + // Test 2: BEGIN transaction + client.query( + 'BEGIN', + assert.success(function () { + assert.equal(client.getTransactionStatus(), 'T', 'should be in transaction state') - // Test 3: COMMIT - client.query( - 'COMMIT', - assert.success(function () { - assert.equal(client.getTransactionStatus(), 'I', 'should return to idle after commit') + // Test 3: COMMIT + client.query( + 'COMMIT', + assert.success(function () { + assert.equal(client.getTransactionStatus(), 'I', 'should return to idle after commit') - client.end(done) - }) - ) - }) - ) - }) - ) - }) - ) - }) + client.end(done) + }) + ) + }) + ) + }) + ) + }) + ) +}) - suite.test('txStatus error state', function (done) { - const client = new pg.Client() - client.connect( - assert.success(function () { - // Run a simple query to initialize txStatus - client.query( - 'SELECT 1', - assert.success(function () { - client.query( - 'BEGIN', - assert.success(function () { - // Execute invalid SQL to trigger error state - client.query('INVALID SQL SYNTAX', function (err) { - assert(err, 'should receive error from invalid query') +suite.test('txStatus error state', function (done) { + const client = new pg.Client() + client.connect( + assert.success(function () { + // Run a simple query to initialize txStatus + client.query( + 'SELECT 1', + assert.success(function () { + client.query( + 'BEGIN', + assert.success(function () { + // Execute invalid SQL to trigger error state + client.query('INVALID SQL SYNTAX', function (err) { + assert(err, 'should receive error from invalid query') - // Issue a sync query to ensure ReadyForQuery has been processed - // This guarantees transaction status has been updated - client.query('SELECT 1', function () { - // This callback fires after ReadyForQuery is processed - assert.equal(client.getTransactionStatus(), 'E', 'should be in error state') + // Issue a sync query to ensure ReadyForQuery has been processed + // This guarantees transaction status has been updated + client.query('SELECT 1', function () { + // This callback fires after ReadyForQuery is processed + assert.equal(client.getTransactionStatus(), 'E', 'should be in error state') - // Rollback to recover - client.query( - 'ROLLBACK', - assert.success(function () { - assert.equal( - client.getTransactionStatus(), - 'I', - 'should return to idle after rollback from error' - ) - client.end(done) - }) - ) - }) + // Rollback to recover + client.query( + 'ROLLBACK', + assert.success(function () { + assert.equal( + client.getTransactionStatus(), + 'I', + 'should return to idle after rollback from error' + ) + client.end(done) + }) + ) }) }) - ) - }) - ) - }) - ) - }) -} + }) + ) + }) + ) + }) + ) +}) diff --git a/yarn.lock b/yarn.lock index dd6662852..b6f87ef79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5955,13 +5955,13 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -libpq@^1.8.15: - version "1.8.15" - resolved "https://registry.yarnpkg.com/libpq/-/libpq-1.8.15.tgz#bf9cea8e59e1a4a911d06df01d408213a09925ad" - integrity sha512-4lSWmly2Nsj3LaTxxtFmJWuP3Kx+0hYHEd+aNrcXEWT0nKWaPd9/QZPiMkkC680zeALFGHQdQWjBvnilL+vgWA== +libpq@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/libpq/-/libpq-1.10.0.tgz#238d01d416abca8768aab09bc82d81af9c7ffa23" + integrity sha512-PHY+JGD3+9X5b2emXLh+WJEnz1jhczO1xs25ZH0xbMWvQi+Hd9X/mTZOrGA99Rcw/DvNjsBRlegroqigpNfaJA== dependencies: bindings "1.5.0" - nan "~2.22.2" + nan "~2.23.1" lines-and-columns@^1.1.6: version "1.1.6" @@ -6686,6 +6686,11 @@ nan@~2.22.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.22.2.tgz#6b504fd029fb8f38c0990e52ad5c26772fdacfbb" integrity sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ== +nan@~2.23.1: + version "2.23.1" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.23.1.tgz#6f86a31dd87e3d1eb77512bf4b9e14c8aded3975" + integrity sha512-r7bBUGKzlqk8oPBDYxt6Z0aEdF1G1rwlMcLk8LCOMbOzf0mG+JUfUzG4fIMWwHWP0iyaLWEQZJmtB7nOHEm/qw== + nanoid@^3.3.11: version "3.3.11" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" From 1a6aaafe2af2c24325902a20101d708d1ef82052 Mon Sep 17 00:00:00 2001 From: Leonardo Zanivan Date: Wed, 1 Apr 2026 08:54:31 -0300 Subject: [PATCH 3/6] docs --- docs/pages/apis/client.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/pages/apis/client.mdx b/docs/pages/apis/client.mdx index ad8aadf4d..e86f06cc9 100644 --- a/docs/pages/apis/client.mdx +++ b/docs/pages/apis/client.mdx @@ -23,7 +23,9 @@ type Config = { lock_timeout?: number, // number of milliseconds a query is allowed to be en lock state before it's cancelled due to lock timeout application_name?: string, // The name of the application that created this Client instance connectionTimeoutMillis?: number, // number of milliseconds to wait for connection, default is no timeout - keepAliveInitialDelayMillis?: number, // set the initial delay before the first keepalive probe is sent on an idle socket + keepAlive?: boolean, // if true, enable keepalive on the `net.Socket` + keepAliveInitialDelayMillis?: number, // number of milliseconds between last data packet received and first keepalive probe, default is 0 (keep existing value); + // no effect unless `keepAlive` is true idle_in_transaction_session_timeout?: number, // number of milliseconds before terminating any session with an open idle transaction, default is no timeout client_encoding?: string, // specifies the character set encoding that the database uses for sending data to the client fallback_application_name?: string, // provide an application name to use if application_name is not set From ef0e8a9f1916c5c2dcac374d062fd25112dbffae Mon Sep 17 00:00:00 2001 From: Leonardo Zanivan Date: Wed, 1 Apr 2026 09:04:31 -0300 Subject: [PATCH 4/6] Tests From ac25875753606d14dfc189fa4b7ef1db8266430c Mon Sep 17 00:00:00 2001 From: Leonardo Zanivan Date: Mon, 6 Apr 2026 15:05:20 -0300 Subject: [PATCH 5/6] fix: docs --- docs/pages/apis/client.mdx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/pages/apis/client.mdx b/docs/pages/apis/client.mdx index e86f06cc9..c1a7ed003 100644 --- a/docs/pages/apis/client.mdx +++ b/docs/pages/apis/client.mdx @@ -186,7 +186,7 @@ Returns the current transaction status of the client connection. This can be use - `'I'` - Idle (not in a transaction) - `'T'` - Transaction active (BEGIN has been issued) - `'E'` - Error (transaction aborted, requires ROLLBACK) -- `null` - Initial state or not supported (native client) +- `null` - Initial state The transaction status is updated after each query completes based on the PostgreSQL backend's `ReadyForQuery` message. @@ -230,8 +230,6 @@ try { await client.end() ``` -**Note:** This method is not supported in the native client and will always return `null`. - ## events ### error From 690e2e949b417527c267d6678ecf0f77e547a14c Mon Sep 17 00:00:00 2001 From: Leonardo Zanivan Date: Mon, 6 Apr 2026 15:06:38 -0300 Subject: [PATCH 6/6] clear docs --- docs/pages/apis/client.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pages/apis/client.mdx b/docs/pages/apis/client.mdx index c1a7ed003..ecfd67fca 100644 --- a/docs/pages/apis/client.mdx +++ b/docs/pages/apis/client.mdx @@ -186,7 +186,7 @@ Returns the current transaction status of the client connection. This can be use - `'I'` - Idle (not in a transaction) - `'T'` - Transaction active (BEGIN has been issued) - `'E'` - Error (transaction aborted, requires ROLLBACK) -- `null` - Initial state +- `null` - Initial state (before first query) The transaction status is updated after each query completes based on the PostgreSQL backend's `ReadyForQuery` message.