branch: elpa/pg
commit f8a63e17266802144f96648be79e8755eebc381f
Author: Lucius <[email protected]>
Commit: Lucius <[email protected]>

    Expose ReadyForQuery transaction status
---
 CHANGELOG.md    |  6 ++++++
 doc/src/API.md  |  9 ++++++++-
 pg.el           | 44 +++++++++++++++++++++++++-------------------
 test/test-pg.el | 16 ++++++++++++++++
 4 files changed, 55 insertions(+), 20 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 56157d3634..079ce7ceb1 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Changelog
 
+## [Unreleased]
+
+- Expose the latest `ReadyForQuery` transaction status on connections via
+  `pgcon-transaction-status`.
+
+
 ## [0.67] - 2026-06-07
 
 - Implement support for PostgreSQL variant Datahike with pg-datahike adapter.
diff --git a/doc/src/API.md b/doc/src/API.md
index f960618b44..8bd05ab9e5 100644
--- a/doc/src/API.md
+++ b/doc/src/API.md
@@ -107,6 +107,14 @@ PostgreSQL (e.g. `keepalives_interval`). The supported 
paramspec keywords and th
 environment variables are the same as for pg-connect/string (see above).
 
 
+    (pgcon-transaction-status con) -> status
+
+Return the transaction status reported by the latest `ReadyForQuery` message 
on connection `CON`.
+The value is one of the raw PostgreSQL protocol status bytes: `?I` for idle, 
`?T` for in a
+transaction block, or `?E` for in a failed transaction block. The value is 
`nil` before the first
+`ReadyForQuery` message has been received.
+
+
     (pg-exec con &rest sql) -> pgresult
 
 Concatenate the SQL strings and send to the PostgreSQL backend over connection 
`CON`. Retrieve the
@@ -292,4 +300,3 @@ to disable the library's type coercion facility. Default is 
`t`.
 >
 >    You can omit the port argument if you chose 5432 as the local end of the 
 > tunnel, since pg.el
 >    defaults to this value.
-
diff --git a/pg.el b/pg.el
index 377d370acd..fbf88a0108 100644
--- a/pg.el
+++ b/pg.el
@@ -298,7 +298,10 @@ SQL queries. To avoid this overhead on establishing a 
connection, remove
     :accessor pgcon-connect-info)
    (connect-plist
     :initform nil
-    :accessor pgcon-connect-plist)))
+    :accessor pgcon-connect-plist)
+   (transaction-status
+    :initform nil
+    :accessor pgcon-transaction-status)))
 
 (defun make-pgcon (&rest args)
   (apply #'make-instance (cons 'pgcon args)))
@@ -330,6 +333,14 @@ SQL queries. To avoid this overhead on establishing a 
connection, remove
   (with-current-buffer (process-buffer (pgcon-process con))
     pgcon--busy))
 
+(defun pg--set-transaction-status (con status)
+  "Store the latest ReadyForQuery transaction STATUS for CON.
+STATUS is one of ?I, ?T or ?E, as defined by the PostgreSQL wire
+protocol."
+  (setf (pgcon-transaction-status con) status)
+  (when (eql ?E status)
+    (message "PostgreSQL ReadyForQuery message with error status")))
+
 (defun pg-enable-query-log (con)
   "Enable logging of PostgreSQL queries on connection CON.
 Queries are logged to a buffer identified by `pgcon-query-log'."
@@ -642,10 +653,12 @@ presented to the user."
       (let ((c (pg--read-char con)))
         (unless (member c '(?Z ?E))
           (message "Unexpected message type after ErrorMsg (error was %s): %s" 
e c)
-          (pg--unread-char con)))
-      ;; Read message length then status, which we discard.
-      (pg--read-net-int con 4)
-      (pg--read-char con))
+          (pg--unread-char con))
+        ;; Read message length then status.
+        (pg--read-net-int con 4)
+        (let ((status (pg--read-char con)))
+          (when (eql ?Z c)
+            (pg--set-transaction-status con status)))))
     (let ((msg (format "%s%s: %s (%s)"
                        (pgerror-severity e)
                        (if context (concat " " context) "")
@@ -796,8 +809,7 @@ Uses database DBNAME, user USER and password PASSWORD."
       (let ((_msglen (pg--read-net-int con 4))
             (status (pg--read-char con)))
         ;; status is 'I' or 'T' or 'E', Idle or InTransaction or Error
-        (when (eql ?E status)
-          (message "PostgreSQL ReadyForQuery message with error status"))
+        (pg--set-transaction-status con status)
         (and (not pg-disable-type-coercion)
              (zerop (hash-table-count (pgcon-parser-by-oid con)))
              (pg-initialize-parsers con))
@@ -1723,8 +1735,7 @@ Return a result structure which can be decoded using 
`pg-result'."
              (let ((_msglen (pg--read-net-int con 4))
                    (status (pg--read-char con)))
                ;; status is 'I' or 'T' or 'E', Idle or InTransaction or Error
-               (when (eql ?E status)
-                 (message "PostgreSQL ReadyForQuery message with error 
status"))
+               (pg--set-transaction-status con status)
                (setf (pgresult-tuples result) (nreverse tuples))
                (setf (pgresult-attributes result) attributes)
                (pg-connection-set-busy con nil)
@@ -2108,8 +2119,7 @@ Returns a pgresult structure (see function `pg-result')."
         (let ((_msglen (pg--read-net-int con 4))
               (status (pg--read-char con)))
           ;; status is 'I' or 'T' or 'E', Idle or InTransaction or Error
-          (when (eql ?E status)
-            (message "PostgreSQL ReadyForQuery message with error status"))
+          (pg--set-transaction-status con status)
           (setf (pgresult-tuples result) (nreverse tuples))
           (pg-connection-set-busy con nil)
           (cl-return-from pg-fetch result)))
@@ -2228,8 +2238,7 @@ Uses PostgreSQL connection CON."
         (let ((_msglen (pg--read-net-int con 4))
               (status (pg--read-char con)))
           ;; status is 'I' or 'T' or 'E'
-          (when (eql ?E status)
-            (message "PostgreSQL ReadyForQuery message with error status"))
+          (pg--set-transaction-status con status)
           (cl-return-from pg-close-portal nil)))
 
        (t
@@ -2368,8 +2377,7 @@ can be decoded using `pg-result'."
        (?Z
         (let ((_msglen (pg--read-net-int con 4))
               (status (pg--read-char con)))
-          (when (eql ?E status)
-            (message "PostgreSQL ReadyForQuery message with error status"))
+          (pg--set-transaction-status con status)
           (pg--trim-connection-buffers con)
           (pg-connection-set-busy con nil)
           (cl-return-from pg-copy-from-buffer result)))
@@ -2494,8 +2502,7 @@ can be decoded using `pg-result', but with data in BUF."
          (?Z
           (let ((_msglen (pg--read-net-int con 4))
                 (status (pg--read-char con)))
-            (when (eql ?E status)
-              (message "PostgreSQL ReadyForQuery message with error status"))
+            (pg--set-transaction-status con status)
             (pg--trim-connection-buffers con)
             (pg-connection-set-busy con nil)
             (cl-return-from pg-copy-to-buffer result)))
@@ -2560,8 +2567,7 @@ can be decoded using `pg-result', but with data in BUF."
      (?Z
       (let ((_msglen (pg--read-net-int con 4))
             (status (pg--read-char con)))
-        (when (eql ?E status)
-          (message "PostgreSQL ReadyForQuery message with error status"))
+        (pg--set-transaction-status con status)
         (pg-connection-set-busy con nil)
         (cl-return-from pg-sync nil)))
 
diff --git a/test/test-pg.el b/test/test-pg.el
index 817f4d159b..ba38dc81e5 100755
--- a/test/test-pg.el
+++ b/test/test-pg.el
@@ -373,6 +373,7 @@
       (unless (member (pgcon-server-variant con) '(clickhouse risingwave 
stoolap arcadedb pgsqlite picodata))
         (pg-vector-setup con))
       (pgtest-add #'pg-test-basic)
+      (pgtest-add #'pg-test-transaction-status)
       (pgtest-add #'pg-test-extended)
       (pgtest-add #'pg-test-insert)
       (pgtest-add #'pg-test-edge-cases)
@@ -807,6 +808,21 @@
 ;; 
https://github.com/postgres/postgres/blob/master/src/test/regress/sql/insert.sql
 ;;
 ;; https://github.com/denodrivers/postgres/blob/main/tests/data_types_test.ts
+(defun pg-test-transaction-status (con)
+  (should (eql ?I (pgcon-transaction-status con)))
+  (pg-exec con "SELECT 1")
+  (should (eql ?I (pgcon-transaction-status con)))
+  (when (eq 'postgresql (pgcon-server-variant con))
+    (unwind-protect
+        (progn
+          (pg-exec con "BEGIN")
+          (should (eql ?T (pgcon-transaction-status con)))
+          (should-error (pg-exec con "SELECT 1/0") :type 'pg-error)
+          (should (eql ?E (pgcon-transaction-status con))))
+      (ignore-errors (pg-exec con "ROLLBACK")))
+    (should (eql ?I (pgcon-transaction-status con)))))
+
+
 (defun pg-test-basic (con)
   (cl-labels ((row (sql) (pg-result (pg-exec con sql) :tuple 0))
               (scalar (sql) (cl-first (pg-result (pg-exec con sql) :tuple 0))))

Reply via email to