Technical Analysis: Replacing Deprecated txid_current() with pg_current_xact_id() and the xid8 Arithmetic Operator Question
Core Problem
PostgreSQL has a legacy problem in its transaction ID function API. Commit 4c04be9b05a introduced a new family of xid8-based functions (e.g., pg_current_xact_id()) to replace the older txid_XXX family (e.g., txid_current()). The old functions were effectively deprecated but never removed. The immediate issue is straightforward: PostgreSQL's own test suite still uses the deprecated txid_current() in several places, which is both a bad example for users and inconsistent with the project's own modernization effort.
However, this seemingly simple cleanup exposed a deeper architectural gap in the xid8 type system: unlike the old bigint-returning txid_current(), the newer pg_current_xact_id() returns xid8, which has no arithmetic operators defined. This matters because several test cases perform arithmetic on transaction IDs (e.g., txid_current() + 1 to fabricate a future XID for testing visibility rules, wraparound scenarios, etc.). A direct migration from txid_current() to pg_current_xact_id() forces ugly workarounds like pg_current_xact_id()::text::bigint + 1, which is arguably worse than using the deprecated function.
Why This Matters Architecturally
The xid8 Type Design Philosophy
The xid8 type was introduced as an epoch-aware, 64-bit transaction identifier that avoids the wraparound issues of the 32-bit xid type. A deliberate design decision was made to not support arithmetic operators on xid8. The rationale, referenced in the thread via a prior mailing list discussion, was to treat transaction IDs as opaque identifiers rather than numeric values — arithmetic on XIDs is semantically questionable in user-facing SQL because XIDs have modular-arithmetic wraparound semantics that don't map cleanly to standard integer arithmetic expectations.
The Unsigned Integer Problem in PostgreSQL
Tom Lane's response illuminates a long-standing PostgreSQL design constraint: PostgreSQL has never introduced SQL-level unsigned integer types (uint4, uint8, etc.) because of the risk of ambiguous operator resolution. When the parser encounters an expression like some_value + 1, it must resolve which + operator to use. Adding unsigned types creates a combinatorial explosion of potential operator matches, leading to user-facing "ambiguous operator" errors that degrade usability. This is why xid8 arithmetic was avoided in the first place — it would effectively introduce partial unsigned integer semantics through a side door.
However, Lane notes a potential safe path: if there are no implicit casts between xid8 and ordinary numeric types, then defining operators like xid8 + int8 → xid8 should not create ambiguity, because the type resolver can unambiguously match the operand types. This is the key insight that unlocks the v2 approach.
Proposed Solutions
v1: Direct Replacement with Cast Workarounds
The initial patch simply replaced all txid_current() calls in tests with pg_current_xact_id(), using ::text::bigint casts where arithmetic was needed. This is a two-hop cast (xid8 → text → bigint) that:
- Is verbose and ugly
- Loses type safety (goes through text representation)
- Obscures the intent of the code
- Was rejected by Tom Lane as "not an improvement"
v2: Define xid8 Arithmetic Operators (Accepted Direction)
The v2 patch set takes a fundamentally different approach, split into two patches:
v2-0001: Add xid8 arithmetic operators
Defines four operators with carefully chosen type signatures:
| Expression | Return Type | Semantics |
|---|---|---|
xid8 + int8 → xid8 |
xid8 | Advance XID by N |
int8 + xid8 → xid8 |
xid8 | Commutative addition |
xid8 - int8 → xid8 |
xid8 | Retreat XID by N |
xid8 - xid8 → int8 |
int8 | Distance between two XIDs |
This operator set is modeled on the pattern used by other PostgreSQL types with similar semantics (e.g., timestamp - timestamp → interval, timestamp + interval → timestamp). The xid8 - xid8 → int8 signature is particularly notable: it returns a signed 64-bit integer representing the "distance" between two transaction IDs, which is the natural semantic for comparing XIDs.
The choice to use int8 (signed) rather than introducing an unsigned type avoids the ambiguous-operator problem Lane identified. Since there are no implicit casts between xid8 and numeric types, operator resolution remains unambiguous.
v2-0002: Replace deprecated function calls in tests
With operators available, the test migration becomes clean: pg_current_xact_id() + 1 works directly without casts, making the code more readable than the original txid_current() + 1 while using the modern API.
Key Design Tradeoffs
-
Purity vs. Practicality: The original xid8 design treated XIDs as opaque, but practical usage (especially in tests and monitoring queries) demands arithmetic. The v2 approach pragmatically adds operators while keeping the type distinct from general-purpose integers.
-
Operator Ambiguity Risk: The operators are safe only because no implicit casts exist between xid8 and numeric types. This is an invariant that must be maintained going forward — if anyone adds such a cast in the future, operator ambiguity could emerge.
-
Scope Creep: What started as a simple test cleanup became a catalog-level change (new operators, new C functions in the backend). This increases the patch's review burden and risk, but produces a cleaner long-term result.
Current Status
As of the last message (May 2026), the patches have been rebased but there is no indication of committer review of v2 or commitment to the approach. Tom Lane's feedback shaped the direction but he has not explicitly endorsed the v2 implementation. The patch needs further review, particularly of:
- The C implementation of the arithmetic functions (overflow handling, wraparound semantics)
- Catalog entries for the new operators
- Whether additional operators (comparison, etc.) should be added at the same time
- Regression test coverage for the new operators themselves