From ff32629101f326bcd66f9c24c03537982c636c01 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 v2 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:
Discussion: https://postgr.es/m/CAOzEurQetW=-1+OnMo8baeVQF=-kAr-wNtFcgRNo+ErPk=xsDQ@mail.gmail.com
---
 src/backend/catalog/system_functions.sql |  6 +++
 src/backend/utils/adt/xid.c              | 64 ++++++++++++++++++++++++
 src/include/catalog/pg_operator.dat      | 12 +++++
 src/include/catalog/pg_proc.dat          | 13 +++++
 src/test/regress/expected/xid.out        | 46 +++++++++++++++++
 src/test/regress/sql/xid.sql             | 14 ++++++
 6 files changed, 155 insertions(+)

diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index eb9e31ae1bf..7aee4e3e65d 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -103,6 +103,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..2b770e011cd 100644
--- a/src/backend/utils/adt/xid.c
+++ b/src/backend/utils/adt/xid.c
@@ -312,6 +312,70 @@ 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		result;
+
+	result = val + (uint64) delta;
+
+	/* Check for over/underflow */
+	if ((delta > 0 && result < val) || (delta < 0 && result > val))
+		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		result;
+
+	result = val - (uint64) delta;
+
+	/* Check for over/underflow */
+	if ((delta > 0 && result > val) || (delta < 0 && result < val))
+		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
+	{
+		if (val2 - val1 > (uint64) PG_INT64_MAX + 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+					 errmsg("bigint out of range")));
+		PG_RETURN_INT64(-((int64) (val2 - val1)));
+	}
+}
+
 Datum
 xid8_larger(PG_FUNCTION_ARGS)
 {
diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat
index 1465f13120a..4cc38e7e294 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 => '5107', descr => 'add',
+  oprname => '+', oprleft => 'xid8', oprright => 'int8', oprresult => 'xid8',
+  oprcom => '+(int8,xid8)', oprcode => 'xid8pl' },
+{ oid => '5108', descr => 'add',
+  oprname => '+', oprleft => 'int8', oprright => 'xid8', oprresult => 'xid8',
+  oprcom => '+(xid8,int8)', oprcode => 'int8_pl_xid8' },
+{ oid => '5109', descr => 'subtract',
+  oprname => '-', oprleft => 'xid8', oprright => 'int8', oprresult => 'xid8',
+  oprcode => 'xid8mi' },
+{ oid => '5110', 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 83f6501df38..f9c9690a376 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 => '5101',
+  proname => 'xid8pl', prorettype => 'xid8', proargtypes => 'xid8 int8',
+  prosrc => 'xid8pl' },
+{ oid => '5102',
+  proname => 'xid8mi', prorettype => 'xid8', proargtypes => 'xid8 int8',
+  prosrc => 'xid8mi' },
+{ oid => '5103',
+  proname => 'xid8_mi_xid8', prorettype => 'int8', proargtypes => 'xid8 xid8',
+  prosrc => 'xid8_mi_xid8' },
+{ oid => '5106',
+  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..4d5f4006072 100644
--- a/src/test/regress/expected/xid.out
+++ b/src/test/regress/expected/xid.out
@@ -175,6 +175,52 @@ 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
 -- 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..f6bacc04428 100644
--- a/src/test/regress/sql/xid.sql
+++ b/src/test/regress/sql/xid.sql
@@ -59,6 +59,20 @@ 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;
+
 
 -- pg_snapshot data type and related functions
 
-- 
2.47.3

