From f3c86d977629244b4de45eb0c611bdbee0368505 Mon Sep 17 00:00:00 2001
From: Pavlo Golub <pavlo.golub@cybertec.at>
Date: Thu, 5 Feb 2026 12:15:40 +0000
Subject: [PATCH] Add pg_current_vxact_id() function

This patch introduces pg_current_vxact_id() to expose the current
backend's virtual transaction ID as text in the format 'procNumber/lxid'
(e.g., '3/42').

Virtual transaction IDs are always assigned to every transaction, unlike
regular XIDs which are only assigned when a transaction modifies data.
This makes VXIDs useful for tracking and correlating all transactions,
including read-only ones.

The VXID format matches what's used in:
- elog %v placeholder for logging
- pg_locks.virtualtransaction column
- Internal PostgreSQL transaction tracking

The function returns NULL during recovery or when no valid VXID exists.

Changes in v3:
- Add VXID_FMT macro to lock.h to eliminate format string duplication
- Update lockfuncs.c, elog.c, and xid8funcs.c to use VXID_FMT
- Fix documentation to use 'localXID' terminology for consistency
- Add comprehensive regression tests in xid.sql
- Add documentation in func-info.sgml and xact.sgml
- Use OID 9538 (in development range 8000-9999)

Addresses review comments from Henson Choi.

Discussion: https://www.postgresql.org/message-id/CAAAe_zBUd3epVqcDAMVmLDt4-dhxVY5W09%2Bgp5ND_P--b90eeA%40mail.gmail.com
---
 doc/src/sgml/func/func-info.sgml  | 22 +++++++++++++++++++++
 doc/src/sgml/xact.sgml            |  4 +++-
 src/backend/utils/adt/lockfuncs.c |  2 +-
 src/backend/utils/adt/xid8funcs.c | 26 +++++++++++++++++++++++++
 src/backend/utils/error/elog.c    |  4 ++--
 src/include/catalog/pg_proc.dat   |  3 +++
 src/include/storage/lock.h        |  7 +++++++
 src/test/regress/expected/xid.out | 32 +++++++++++++++++++++++++++++++
 src/test/regress/sql/xid.sql      | 13 +++++++++++++
 9 files changed, 109 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 294f45e82a3..4efe01d63b9 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -2864,6 +2864,28 @@ acl      | {postgres=arwdDxtm/postgres,foo=r/postgres}
        </para></entry>
       </row>
 
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_current_vxact_id</primary>
+        </indexterm>
+        <function>pg_current_vxact_id</function> ()
+        <returnvalue>text</returnvalue>
+       </para>
+       <para>
+        Returns the current virtual transaction ID (VXID) in the
+        format <literal>procNumber/localXID</literal>
+        (for example, <literal>3/42</literal>).
+        Virtual transaction ID is always assigned when a transaction starts,
+        unlike regular transaction IDs which are only assigned when the
+        transaction performs a database write.  VXIDs are session-scoped and
+        do not persist across server restarts.  They are primarily useful for
+        correlating transactions with log entries that use the <literal>%v</literal> placeholder
+        in <xref linkend="guc-log-line-prefix"/>.
+        See <xref linkend="transaction-id"/> for details.
+       </para></entry>
+      </row>
+
       <row>
        <entry role="func_table_entry"><para role="func_signature">
         <indexterm>
diff --git a/doc/src/sgml/xact.sgml b/doc/src/sgml/xact.sgml
index 3aa7ee1383e..4a5753178e0 100644
--- a/doc/src/sgml/xact.sgml
+++ b/doc/src/sgml/xact.sgml
@@ -31,7 +31,9 @@
    <literal>localXID</literal>.  For example, the virtual transaction
    ID <literal>4/12532</literal> has a <literal>procNumber</literal>
    of <literal>4</literal> and a <literal>localXID</literal> of
-   <literal>12532</literal>.
+   <literal>12532</literal>.  The function
+   <function>pg_current_vxact_id</function> returns the current
+   transaction's VXID.
   </para>
 
   <para>
diff --git a/src/backend/utils/adt/lockfuncs.c b/src/backend/utils/adt/lockfuncs.c
index 9dadd6da672..36772ffd3f1 100644
--- a/src/backend/utils/adt/lockfuncs.c
+++ b/src/backend/utils/adt/lockfuncs.c
@@ -80,7 +80,7 @@ VXIDGetDatum(ProcNumber procNumber, LocalTransactionId lxid)
 	 */
 	char		vxidstr[32];
 
-	snprintf(vxidstr, sizeof(vxidstr), "%d/%u", procNumber, lxid);
+	snprintf(vxidstr, sizeof(vxidstr), VXID_FMT, procNumber, lxid);
 
 	return CStringGetTextDatum(vxidstr);
 }
diff --git a/src/backend/utils/adt/xid8funcs.c b/src/backend/utils/adt/xid8funcs.c
index c607e78d9ac..bc9e1076cc9 100644
--- a/src/backend/utils/adt/xid8funcs.c
+++ b/src/backend/utils/adt/xid8funcs.c
@@ -33,6 +33,7 @@
 #include "libpq/pqformat.h"
 #include "miscadmin.h"
 #include "storage/lwlock.h"
+#include "storage/proc.h"
 #include "storage/procarray.h"
 #include "storage/procnumber.h"
 #include "utils/builtins.h"
@@ -360,6 +361,31 @@ pg_current_xact_id_if_assigned(PG_FUNCTION_ARGS)
 	PG_RETURN_FULLTRANSACTIONID(topfxid);
 }
 
+/*
+ * pg_current_vxact_id() returns text
+ *
+ *	Return the current virtual transaction ID (vxid).
+ *	vxid is always assigned and available, unlike regular transaction IDs.
+ *	Returns NULL if no valid vxid exists (e.g., during startup/recovery).
+ */
+Datum
+pg_current_vxact_id(PG_FUNCTION_ARGS)
+{
+	char		vxidstr[32];
+
+	/*
+	 * Check if we have a valid vxid.  The vxid format matches what's used
+	 * in elog.c for the %v placeholder and in pg_locks.virtualtransaction.
+	 */
+	if (MyProc == NULL || MyProc->vxid.procNumber == INVALID_PROC_NUMBER)
+		PG_RETURN_NULL();
+
+	snprintf(vxidstr, sizeof(vxidstr), VXID_FMT,
+			 MyProc->vxid.procNumber, MyProc->vxid.lxid);
+
+	PG_RETURN_TEXT_P(cstring_to_text(vxidstr));
+}
+
 /*
  * pg_current_snapshot() returns pg_snapshot
  *
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index e6a4ef99059..fd64a4847ef 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -3181,12 +3181,12 @@ log_status_format(StringInfo buf, const char *format, ErrorData *edata)
 					{
 						char		strfbuf[128];
 
-						snprintf(strfbuf, sizeof(strfbuf) - 1, "%d/%u",
+						snprintf(strfbuf, sizeof(strfbuf) - 1, VXID_FMT,
 								 MyProc->vxid.procNumber, MyProc->vxid.lxid);
 						appendStringInfo(buf, "%*s", padding, strfbuf);
 					}
 					else
-						appendStringInfo(buf, "%d/%u", MyProc->vxid.procNumber, MyProc->vxid.lxid);
+						appendStringInfo(buf, VXID_FMT, MyProc->vxid.procNumber, MyProc->vxid.lxid);
 				}
 				else if (padding != 0)
 					appendStringInfoSpaces(buf,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 83f6501df38..fb2f9324b02 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10692,6 +10692,9 @@
 { oid => '5066', descr => 'commit status of transaction',
   proname => 'pg_xact_status', provolatile => 'v', prorettype => 'text',
   proargtypes => 'xid8', prosrc => 'pg_xact_status' },
+{ oid => '9538', descr => 'get current virtual transaction ID',
+  proname => 'pg_current_vxact_id', provolatile => 's', proparallel => 'u',
+  prorettype => 'text', proargtypes => '', prosrc => 'pg_current_vxact_id' },
 
 # record comparison using normal comparison rules
 { oid => '2981',
diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h
index 55ffaa5e4a5..962009bde08 100644
--- a/src/include/storage/lock.h
+++ b/src/include/storage/lock.h
@@ -64,6 +64,13 @@ typedef struct
 	LocalTransactionId localTransactionId;	/* lxid from PGPROC */
 } VirtualTransactionId;
 
+/*
+ * Format string for VXID display.
+ * Used by elog.c (%v placeholder), lockfuncs.c (VXIDGetDatum),
+ * and xid8funcs.c (pg_current_vxact_id).
+ */
+#define VXID_FMT "%d/%u"
+
 #define InvalidLocalTransactionId		0
 #define LocalTransactionIdIsValid(lxid) ((lxid) != InvalidLocalTransactionId)
 #define VirtualTransactionIdIsValid(vxid) \
diff --git a/src/test/regress/expected/xid.out b/src/test/regress/expected/xid.out
index 1ce7826cf90..55649cc4358 100644
--- a/src/test/regress/expected/xid.out
+++ b/src/test/regress/expected/xid.out
@@ -463,6 +463,38 @@ SELECT pg_current_xact_id_if_assigned() IS NOT DISTINCT FROM xid8 :'pg_current_x
  t
 (1 row)
 
+COMMIT;
+-- test pg_current_vxact_id
+BEGIN;
+SELECT pg_current_vxact_id() IS NOT NULL AS vxid_assigned;
+ vxid_assigned 
+---------------
+ t
+(1 row)
+
+SELECT pg_current_vxact_id() ~ '^\d+/\d+$' AS vxid_format_ok;
+ vxid_format_ok 
+----------------
+ t
+(1 row)
+
+SELECT pg_current_vxact_id() AS vxid1 \gset
+SELECT pg_current_vxact_id() = :'vxid1' AS vxid_stable;
+ vxid_stable 
+-------------
+ t
+(1 row)
+
+COMMIT;
+-- start new transaction, vxid should change
+BEGIN;
+SELECT pg_current_vxact_id() AS vxid2 \gset
+SELECT :'vxid2' <> :'vxid1' AS vxid_changed;
+ vxid_changed 
+--------------
+ t
+(1 row)
+
 COMMIT;
 -- test xid status functions
 BEGIN;
diff --git a/src/test/regress/sql/xid.sql b/src/test/regress/sql/xid.sql
index 9f716b3653a..5a48f914a1f 100644
--- a/src/test/regress/sql/xid.sql
+++ b/src/test/regress/sql/xid.sql
@@ -132,6 +132,19 @@ SELECT pg_current_xact_id() \gset
 SELECT pg_current_xact_id_if_assigned() IS NOT DISTINCT FROM xid8 :'pg_current_xact_id';
 COMMIT;
 
+-- test pg_current_vxact_id
+BEGIN;
+SELECT pg_current_vxact_id() IS NOT NULL AS vxid_assigned;
+SELECT pg_current_vxact_id() ~ '^\d+/\d+$' AS vxid_format_ok;
+SELECT pg_current_vxact_id() AS vxid1 \gset
+SELECT pg_current_vxact_id() = :'vxid1' AS vxid_stable;
+COMMIT;
+-- start new transaction, vxid should change
+BEGIN;
+SELECT pg_current_vxact_id() AS vxid2 \gset
+SELECT :'vxid2' <> :'vxid1' AS vxid_changed;
+COMMIT;
+
 -- test xid status functions
 BEGIN;
 SELECT pg_current_xact_id() AS committed \gset
-- 
2.52.0

