From fb9c9ac640b469aa8d245ba1055e53292e19d084 Mon Sep 17 00:00:00 2001
From: Pavlo Golub <pavlo.golub@cybertec.at>
Date: Mon, 8 Dec 2025 11:37:26 +0000
Subject: [PATCH v1] Add pg_current_vxact_id() function to get current virtual
 transaction ID

This patch introduces a new SQL-callable function pg_current_vxact_id()
that returns the current backend's virtual transaction ID (VXID) 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.

This provides a clean API for applications that previously had to query
pg_locks or parse log files to obtain virtual transaction IDs.

---
 doc/src/sgml/func/func-info.sgml  | 22 +++++++++++++++++++++
 doc/src/sgml/xact.sgml            |  4 +++-
 src/backend/utils/adt/xid8funcs.c | 26 +++++++++++++++++++++++++
 src/include/catalog/pg_proc.dat   |  3 +++
 src/test/regress/expected/xid.out | 32 +++++++++++++++++++++++++++++++
 src/test/regress/sql/xid.sql      | 13 +++++++++++++
 6 files changed, 99 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index d4508114a48..7e5e8e4d31c 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -2849,6 +2849,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/localTransactionId</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/xid8funcs.c b/src/backend/utils/adt/xid8funcs.c
index 4b3f7a69b3b..fc8e2975c2f 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), "%d/%u",
+			 MyProc->vxid.procNumber, MyProc->vxid.lxid);
+
+	PG_RETURN_TEXT_P(cstring_to_text(vxidstr));
+}
+
 /*
  * pg_current_snapshot() returns pg_snapshot
  *
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fd9448ec7b9..9777c69102e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10676,6 +10676,9 @@
 { oid => '5066', descr => 'commit status of transaction',
   proname => 'pg_xact_status', provolatile => 'v', prorettype => 'text',
   proargtypes => 'xid8', prosrc => 'pg_xact_status' },
+{ oid => '5101', 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/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

