From 3ed5b1e7ca0fd4effab326a6b6bdd96a5a36a41d Mon Sep 17 00:00:00 2001
From: Shinya Kato <shinya11.kato@gmail.com>
Date: Mon, 9 Feb 2026 13:35:07 +0900
Subject: [PATCH v6 1/2] Add arithmetic operators for xid8

Add +, - operators for xid8 type to allow direct arithmetic
without the need for casting through text and bigint:

  xid8 + int8 -> xid8
  int8 + xid8 -> xid8
  xid8 - int8 -> xid8
  xid8 - xid8 -> int8

These operators follow the same pattern as the existing pg_lsn
arithmetic operators.  Since there are no implicit casts between
xid8 and any ordinary numeric type, this avoids the "ambiguous
operator" concern.

Author: Shinya Kato <shinya11.kato@gmail.com>
Reviewed-by: lin teletele <teletele.lin@gmail.com>
Discussion: https://postgr.es/m/CAOzEurQetW=-1+OnMo8baeVQF=-kAr-wNtFcgRNo+ErPk=xsDQ@mail.gmail.com
---
 doc/src/sgml/datatype.sgml               | 16 +++++
 src/backend/catalog/system_functions.sql |  6 ++
 src/backend/utils/adt/xid.c              | 77 ++++++++++++++++++++++++
 src/include/catalog/pg_operator.dat      | 12 ++++
 src/include/catalog/pg_proc.dat          | 13 ++++
 src/test/regress/expected/xid.out        | 73 ++++++++++++++++++++++
 src/test/regress/sql/xid.sql             | 21 +++++++
 7 files changed, 218 insertions(+)

diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index d8d91678e86..28362fae80b 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -5096,6 +5096,22 @@ WHERE ...
     cluster.  See <xref linkend="transaction-id"/> for more details.
    </para>
 
+   <para>
+    The <type>xid8</type> type supports the standard comparison operators,
+    like <literal>=</literal> and <literal>&gt;</literal>.  Two
+    <type>xid8</type> values can be subtracted using the <literal>-</literal>
+    operator; the result is the signed distance between them as a
+    <type>bigint</type>.  A <type>bigint</type> can also be added to or
+    subtracted from an <type>xid8</type> using the
+    <literal>+(xid8,bigint)</literal>, <literal>+(bigint,xid8)</literal>,
+    and <literal>-(xid8,bigint)</literal> operators, respectively.  Note
+    that the calculated <type>xid8</type> should be in the range of the
+    <type>xid8</type> type, i.e., between <literal>0</literal> and
+    <literal>18446744073709551615</literal>, and that subtracting two
+    <type>xid8</type> values yielding a result outside the range of
+    <type>bigint</type> raises an error.
+   </para>
+
    <para>
     A third identifier type used by the system is <type>cid</type>, or
     command identifier.  This is the data type of the system columns
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index c3c0a6e84ed..1f9e889115d 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -93,6 +93,12 @@ CREATE OR REPLACE FUNCTION numeric_pl_pg_lsn(numeric, pg_lsn)
  IMMUTABLE PARALLEL SAFE STRICT COST 1
 RETURN $2 + $1;
 
+CREATE OR REPLACE FUNCTION int8_pl_xid8(bigint, xid8)
+ RETURNS xid8
+ LANGUAGE sql
+ IMMUTABLE PARALLEL SAFE STRICT COST 1
+RETURN $2 + $1;
+
 CREATE OR REPLACE FUNCTION path_contain_pt(path, point)
  RETURNS boolean
  LANGUAGE sql
diff --git a/src/backend/utils/adt/xid.c b/src/backend/utils/adt/xid.c
index f746a5f97dd..b47880e6945 100644
--- a/src/backend/utils/adt/xid.c
+++ b/src/backend/utils/adt/xid.c
@@ -312,6 +312,83 @@ hashxid8extended(PG_FUNCTION_ARGS)
 	return hashint8extended(fcinfo);
 }
 
+Datum
+xid8pl(PG_FUNCTION_ARGS)
+{
+	FullTransactionId fxid = PG_GETARG_FULLTRANSACTIONID(0);
+	int64		delta = PG_GETARG_INT64(1);
+	uint64		val = U64FromFullTransactionId(fxid);
+	uint64		abs_delta = pg_abs_s64(delta);
+	uint64		result;
+	bool		overflow;
+
+	if (delta >= 0)
+		overflow = pg_add_u64_overflow(val, abs_delta, &result);
+	else
+		overflow = pg_sub_u64_overflow(val, abs_delta, &result);
+
+	if (overflow)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("xid8 out of range")));
+
+	PG_RETURN_FULLTRANSACTIONID(FullTransactionIdFromU64(result));
+}
+
+Datum
+xid8mi(PG_FUNCTION_ARGS)
+{
+	FullTransactionId fxid = PG_GETARG_FULLTRANSACTIONID(0);
+	int64		delta = PG_GETARG_INT64(1);
+	uint64		val = U64FromFullTransactionId(fxid);
+	uint64		abs_delta = pg_abs_s64(delta);
+	uint64		result;
+	bool		overflow;
+
+	if (delta >= 0)
+		overflow = pg_sub_u64_overflow(val, abs_delta, &result);
+	else
+		overflow = pg_add_u64_overflow(val, abs_delta, &result);
+
+	if (overflow)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("xid8 out of range")));
+
+	PG_RETURN_FULLTRANSACTIONID(FullTransactionIdFromU64(result));
+}
+
+Datum
+xid8_mi_xid8(PG_FUNCTION_ARGS)
+{
+	FullTransactionId fxid1 = PG_GETARG_FULLTRANSACTIONID(0);
+	FullTransactionId fxid2 = PG_GETARG_FULLTRANSACTIONID(1);
+	uint64		val1 = U64FromFullTransactionId(fxid1);
+	uint64		val2 = U64FromFullTransactionId(fxid2);
+
+	if (val1 >= val2)
+	{
+		if (val1 - val2 > (uint64) PG_INT64_MAX)
+			ereport(ERROR,
+					(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+					 errmsg("bigint out of range")));
+		PG_RETURN_INT64((int64) (val1 - val2));
+	}
+	else
+	{
+		uint64		diff = val2 - val1;
+
+		if (diff > (uint64) PG_INT64_MAX + 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+					 errmsg("bigint out of range")));
+		/* diff == 2^63 maps to PG_INT64_MIN without signed overflow */
+		if (diff > (uint64) PG_INT64_MAX)
+			PG_RETURN_INT64(PG_INT64_MIN);
+		PG_RETURN_INT64(-(int64) diff);
+	}
+}
+
 Datum
 xid8_larger(PG_FUNCTION_ARGS)
 {
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index 1a8fd8b8645..1dab54c0c92 100644
--- a/src/include/catalog/pg_operator.dat
+++ b/src/include/catalog/pg_operator.dat
@@ -219,6 +219,18 @@
   oprname => '>=', oprleft => 'xid8', oprright => 'xid8', oprresult => 'bool',
   oprcom => '<=(xid8,xid8)', oprnegate => '<(xid8,xid8)', oprcode => 'xid8ge',
   oprrest => 'scalargesel', oprjoin => 'scalargejoinsel' },
+{ oid => '8926', descr => 'add',
+  oprname => '+', oprleft => 'xid8', oprright => 'int8', oprresult => 'xid8',
+  oprcom => '+(int8,xid8)', oprcode => 'xid8pl' },
+{ oid => '8927', descr => 'add',
+  oprname => '+', oprleft => 'int8', oprright => 'xid8', oprresult => 'xid8',
+  oprcom => '+(xid8,int8)', oprcode => 'int8_pl_xid8' },
+{ oid => '8928', descr => 'subtract',
+  oprname => '-', oprleft => 'xid8', oprright => 'int8', oprresult => 'xid8',
+  oprcode => 'xid8mi' },
+{ oid => '8929', descr => 'subtract',
+  oprname => '-', oprleft => 'xid8', oprright => 'xid8', oprresult => 'int8',
+  oprcode => 'xid8_mi_xid8' },
 { oid => '385', descr => 'equal',
   oprname => '=', oprcanhash => 't', oprleft => 'cid', oprright => 'cid',
   oprresult => 'bool', oprcom => '=(cid,cid)', oprcode => 'cideq',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be157a5fbe9..723b6a269ee 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -209,6 +209,19 @@
 { oid => '5098', descr => 'smaller of two',
   proname => 'xid8_smaller', prorettype => 'xid8', proargtypes => 'xid8 xid8',
   prosrc => 'xid8_smaller' },
+{ oid => '8920',
+  proname => 'xid8pl', prorettype => 'xid8', proargtypes => 'xid8 int8',
+  prosrc => 'xid8pl' },
+{ oid => '8921',
+  proname => 'xid8mi', prorettype => 'xid8', proargtypes => 'xid8 int8',
+  prosrc => 'xid8mi' },
+{ oid => '8922',
+  proname => 'xid8_mi_xid8', prorettype => 'int8', proargtypes => 'xid8 xid8',
+  prosrc => 'xid8_mi_xid8' },
+{ oid => '8925',
+  proname => 'int8_pl_xid8', prolang => 'sql',
+  prorettype => 'xid8', proargtypes => 'int8 xid8',
+  prosrc => 'see system_functions.sql' },
 { oid => '69',
   proname => 'cideq', proleakproof => 't', prorettype => 'bool',
   proargtypes => 'cid cid', prosrc => 'cideq' },
diff --git a/src/test/regress/expected/xid.out b/src/test/regress/expected/xid.out
index 1ce7826cf90..ba1781b9ee6 100644
--- a/src/test/regress/expected/xid.out
+++ b/src/test/regress/expected/xid.out
@@ -175,6 +175,79 @@ select min(x), max(x) from xid8_t1;
 create index on xid8_t1 using btree(x);
 create index on xid8_t1 using hash(x);
 drop table xid8_t1;
+-- xid8 arithmetic operators
+select '42'::xid8 + 3::bigint;
+ ?column? 
+----------
+       45
+(1 row)
+
+select 3::bigint + '42'::xid8;
+ ?column? 
+----------
+       45
+(1 row)
+
+select '42'::xid8 - 3::bigint;
+ ?column? 
+----------
+       39
+(1 row)
+
+select '100'::xid8 - '42'::xid8;
+ ?column? 
+----------
+       58
+(1 row)
+
+select '42'::xid8 + (-3)::bigint;
+ ?column? 
+----------
+       39
+(1 row)
+
+select '42'::xid8 - (-3)::bigint;
+ ?column? 
+----------
+       45
+(1 row)
+
+-- xid8 arithmetic overflow/underflow
+select '0'::xid8 - 1::bigint;
+ERROR:  xid8 out of range
+select '18446744073709551615'::xid8 + 1::bigint;
+ERROR:  xid8 out of range
+select '18446744073709551615'::xid8 - '0'::xid8;
+ERROR:  bigint out of range
+select '0'::xid8 - '18446744073709551615'::xid8;
+ERROR:  bigint out of range
+-- xid8 arithmetic at int8 boundaries
+select '0'::xid8 + 9223372036854775807::bigint;
+      ?column?       
+---------------------
+ 9223372036854775807
+(1 row)
+
+select '0'::xid8 - (-9223372036854775807 - 1)::bigint;
+      ?column?       
+---------------------
+ 9223372036854775808
+(1 row)
+
+select '9223372036854775807'::xid8 - (-9223372036854775807 - 1)::bigint;
+       ?column?       
+----------------------
+ 18446744073709551615
+(1 row)
+
+select '0'::xid8 - '9223372036854775808'::xid8;
+       ?column?       
+----------------------
+ -9223372036854775808
+(1 row)
+
+select '0'::xid8 - '9223372036854775809'::xid8;
+ERROR:  bigint out of range
 -- pg_snapshot data type and related functions
 -- Note: another set of tests similar to this exists in txid.sql, for a limited
 -- time (the relevant functions share C code)
diff --git a/src/test/regress/sql/xid.sql b/src/test/regress/sql/xid.sql
index 9f716b3653a..77d45797579 100644
--- a/src/test/regress/sql/xid.sql
+++ b/src/test/regress/sql/xid.sql
@@ -59,6 +59,27 @@ create index on xid8_t1 using btree(x);
 create index on xid8_t1 using hash(x);
 drop table xid8_t1;
 
+-- xid8 arithmetic operators
+select '42'::xid8 + 3::bigint;
+select 3::bigint + '42'::xid8;
+select '42'::xid8 - 3::bigint;
+select '100'::xid8 - '42'::xid8;
+select '42'::xid8 + (-3)::bigint;
+select '42'::xid8 - (-3)::bigint;
+
+-- xid8 arithmetic overflow/underflow
+select '0'::xid8 - 1::bigint;
+select '18446744073709551615'::xid8 + 1::bigint;
+select '18446744073709551615'::xid8 - '0'::xid8;
+select '0'::xid8 - '18446744073709551615'::xid8;
+
+-- xid8 arithmetic at int8 boundaries
+select '0'::xid8 + 9223372036854775807::bigint;
+select '0'::xid8 - (-9223372036854775807 - 1)::bigint;
+select '9223372036854775807'::xid8 - (-9223372036854775807 - 1)::bigint;
+select '0'::xid8 - '9223372036854775808'::xid8;
+select '0'::xid8 - '9223372036854775809'::xid8;
+
 
 -- pg_snapshot data type and related functions
 
-- 
2.47.3

