From c2cf434d356c74d5de6f0b56d26af5dd7ba07903 Mon Sep 17 00:00:00 2001
From: Shibagaki Koshi <shibagaki.koshi@fujitsu.com>
Date: Fri, 23 Oct 2026 16:18:20 +0900
Subject: [PATCH v1] Add an assignment cast from numeric to oid8

Integer literals above INT64_MAX are parsed as numeric, so a value such
as 18446744073709551615 cannot currently be cast directly to oid8 even
though it is representable.  The built-in signed integer types cannot
represent the upper half of oid8's range, while numeric can represent
every oid8 value exactly.

Allow numeric-to-oid8 conversion in assignment and explicit contexts.
Round to the nearest integer, with ties away from zero, and require the
rounded magnitude to fit in uint64.  Wrap negative results modulo 2^64,
consistently with oid8 text input.  Reject NaN and infinite values,
reporting soft errors when requested by the caller.

Keep the cast out of implicit expression contexts to avoid rounding
numeric values during comparisons.  Include documentation and focused
regression coverage for conversion, assignment and expression coercion.
---
 doc/src/sgml/datatype.sgml         | 12 +++++++++
 src/backend/utils/adt/numeric.c    | 34 ++++++++++++++++++++++++
 src/include/catalog/pg_cast.dat    |  4 ++-
 src/include/catalog/pg_proc.dat    |  3 +++
 src/test/regress/expected/oid8.out | 42 ++++++++++++++++++++++++++++++
 src/test/regress/sql/oid8.sql      | 15 +++++++++++
 6 files changed, 109 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 83bf0ad8ed..8a10e52899 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -4843,6 +4843,18 @@ INSERT INTO mytable VALUES(-1);  -- fails
     signed-versus-unsigned confusion if you do this.)
    </para>
 
+   <para>
+    Values of type <type>numeric</type> can be cast to <type>oid8</type>,
+    either explicitly or when assigned to an <type>oid8</type> column.
+    They are rounded to the nearest integer, with ties
+    away from zero.  The absolute value of the rounded result must not
+    exceed <literal>18446744073709551615</literal>.
+    Negative results wrap around modulo 2<superscript>64</superscript>,
+    as with negative textual input; for example, <literal>-1</literal>
+    converts to <literal>18446744073709551615</literal>.
+    <literal>NaN</literal> and infinite values cannot be converted.
+   </para>
+
    <para>
     The OID alias types have no operations of their own except
     for specialized input and output routines.  These routines are able
diff --git a/src/backend/utils/adt/numeric.c b/src/backend/utils/adt/numeric.c
index 405cea6b6a..504aa47a1d 100644
--- a/src/backend/utils/adt/numeric.c
+++ b/src/backend/utils/adt/numeric.c
@@ -4747,6 +4747,40 @@ numeric_pg_lsn(PG_FUNCTION_ARGS)
 	PG_RETURN_LSN(result);
 }
 
+Datum
+numeric_oid8(PG_FUNCTION_ARGS)
+{
+	Numeric		num = PG_GETARG_NUMERIC(0);
+	NumericVar	x;
+	uint64		result;
+
+	if (NUMERIC_IS_SPECIAL(num))
+	{
+		if (NUMERIC_IS_NAN(num))
+			ereturn(fcinfo->context, (Datum) 0,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("cannot convert NaN to %s", "oid8")));
+		else
+			ereturn(fcinfo->context, (Datum) 0,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("cannot convert infinity to %s", "oid8")));
+	}
+
+	/* Convert the rounded magnitude, then wrap negatives as oid8in() does. */
+	init_var_from_num(num, &x);
+	x.sign = NUMERIC_POS;
+
+	if (!numericvar_to_uint64(&x, &result))
+		ereturn(fcinfo->context, (Datum) 0,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("oid8 out of range")));
+
+	if (NUMERIC_SIGN(num) == NUMERIC_NEG)
+		result = -result;
+
+	PG_RETURN_OID8(result);
+}
+
 
 /* ----------------------------------------------------------------------
  *
diff --git a/src/include/catalog/pg_cast.dat b/src/include/catalog/pg_cast.dat
index a7b6d812c5..2fc4ad614f 100644
--- a/src/include/catalog/pg_cast.dat
+++ b/src/include/catalog/pg_cast.dat
@@ -306,9 +306,11 @@
   castcontext => 'i', castmethod => 'f' },
 { castsource => 'oid8', casttarget => 'int8', castfunc => '0',
   castcontext => 'a', castmethod => 'b' },
-# Assignment coercion from oid to oid8.
+# Assignment coercion from oid and numeric to oid8.
 { castsource => 'oid', casttarget => 'oid8', castfunc => 'oid8(oid)',
   castcontext => 'a', castmethod => 'f' },
+{ castsource => 'numeric', casttarget => 'oid8', castfunc => 'oid8(numeric)',
+  castcontext => 'a', castmethod => 'f' },
 
 # String category
 { castsource => 'text', casttarget => 'bpchar', castfunc => '0',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5e4c9e503f..6a5515c5f1 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -4790,6 +4790,9 @@
 { oid => '6103', descr => 'convert numeric to pg_lsn',
   proname => 'pg_lsn', prorettype => 'pg_lsn', proargtypes => 'numeric',
   prosrc => 'numeric_pg_lsn' },
+{ oid => '2775', descr => 'convert numeric to oid8',
+  proname => 'oid8', prorettype => 'oid8', proargtypes => 'numeric',
+  prosrc => 'numeric_oid8' },
 
 { oid => '3556', descr => 'convert jsonb to boolean',
   proname => 'bool', prorettype => 'bool', proargtypes => 'jsonb',
diff --git a/src/test/regress/expected/oid8.out b/src/test/regress/expected/oid8.out
index 2e114f1ce7..a5bed7163c 100644
--- a/src/test/regress/expected/oid8.out
+++ b/src/test/regress/expected/oid8.out
@@ -202,6 +202,48 @@ SELECT 1::oid8::oid; -- not ok
 ERROR:  cannot cast type oid8 to oid
 LINE 1: SELECT 1::oid8::oid;
                       ^
+-- Numeric casts round the magnitude, then wrap negative values.
+BEGIN;
+INSERT INTO OID8_TBL VALUES (18446744073709551615) RETURNING f1;
+          f1          
+----------------------
+ 18446744073709551615
+(1 row)
+
+ROLLBACK;
+SELECT 18446744073709551616::numeric::oid8;
+ERROR:  oid8 out of range
+SELECT (-1)::numeric::oid8;
+         oid8         
+----------------------
+ 18446744073709551615
+(1 row)
+
+SELECT (-1.5)::numeric::oid8;
+         oid8         
+----------------------
+ 18446744073709551614
+(1 row)
+
+SELECT (-18446744073709551615)::numeric::oid8;
+ oid8 
+------
+    1
+(1 row)
+
+SELECT (-18446744073709551616)::numeric::oid8;
+ERROR:  oid8 out of range
+SELECT 'NaN'::numeric::oid8;
+ERROR:  cannot convert NaN to oid8
+SELECT 'Infinity'::numeric::oid8;
+ERROR:  cannot convert infinity to oid8
+-- Numeric casts are not implicit in expressions.
+SELECT 2::oid8 = 1.5;
+ERROR:  operator does not exist: oid8 = numeric
+LINE 1: SELECT 2::oid8 = 1.5;
+                       ^
+DETAIL:  No operator of that name accepts the given argument types.
+HINT:  You might need to add explicit type casts.
 -- Aggregates
 SELECT min(f1), max(f1) FROM OID8_TBL;
  min |         max          
diff --git a/src/test/regress/sql/oid8.sql b/src/test/regress/sql/oid8.sql
index 44cb79dd55..24445f9ad8 100644
--- a/src/test/regress/sql/oid8.sql
+++ b/src/test/regress/sql/oid8.sql
@@ -56,6 +56,21 @@ SELECT 1::oid8::int8;
 SELECT 1::oid::oid8; -- ok
 SELECT 1::oid8::oid; -- not ok
 
+-- Numeric casts round the magnitude, then wrap negative values.
+BEGIN;
+INSERT INTO OID8_TBL VALUES (18446744073709551615) RETURNING f1;
+ROLLBACK;
+SELECT 18446744073709551616::numeric::oid8;
+SELECT (-1)::numeric::oid8;
+SELECT (-1.5)::numeric::oid8;
+SELECT (-18446744073709551615)::numeric::oid8;
+SELECT (-18446744073709551616)::numeric::oid8;
+SELECT 'NaN'::numeric::oid8;
+SELECT 'Infinity'::numeric::oid8;
+
+-- Numeric casts are not implicit in expressions.
+SELECT 2::oid8 = 1.5;
+
 -- Aggregates
 SELECT min(f1), max(f1) FROM OID8_TBL;
 

base-commit: 7787aec8ab000932a8704a988b175b39ff816f2f
-- 
2.34.1

