From 1a823a92e4dfe5d835bfc36093db89a373047bd8 Mon Sep 17 00:00:00 2001
From: Matthias van de Meent <boekewurm+postgres@gmail.com>
Date: Wed, 10 Dec 2025 18:45:19 +0100
Subject: [PATCH v4] Add SQL-level datum equality tests

This enables improved performance for users that need to test for
exact bytewise differences in SQL; e.g. to see if an UPDATE is
really necessary with a set of externally provided values.

A workaround around this limitation was often possible, but no
generic method was available that was this performant, nor as
accessible to all types.
---
 doc/src/sgml/func/func-comparison.sgml       | 25 ++++++
 src/backend/utils/adt/pseudotypes.c          | 51 ++++++++++++
 src/include/catalog/pg_proc.dat              |  7 ++
 src/test/regress/expected/misc_functions.out | 81 ++++++++++++++++++++
 src/test/regress/expected/opr_sanity.out     |  1 +
 src/test/regress/sql/misc_functions.sql      | 25 ++++++
 6 files changed, 190 insertions(+)

diff --git a/doc/src/sgml/func/func-comparison.sgml b/doc/src/sgml/func/func-comparison.sgml
index ecb1d89463a..df806d992c3 100644
--- a/doc/src/sgml/func/func-comparison.sgml
+++ b/doc/src/sgml/func/func-comparison.sgml
@@ -653,6 +653,31 @@ SELECT NOT(ROW(table.*) IS NOT NULL) FROM TABLE; -- detect at least one null in
         <returnvalue>1</returnvalue>
        </para></entry>
       </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_datum_image_equal</primary>
+        </indexterm>
+        <function>pg_datum_image_equal</function> ( <type>anyelement</type>, <type>anyelement</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Returns whether the values have the same exact binary representation.
+       </para>
+       <para>
+        <literal>pg_datum_image_equal('1.0'::numeric, '1.0'::numeric)</literal>
+        <returnvalue>true</returnvalue>
+       </para>
+       <para>
+        <literal>pg_datum_image_equal('1.0'::numeric, '1.00'::numeric)</literal>
+        <returnvalue>false</returnvalue>
+       </para>
+       <para>
+        <literal>pg_datum_image_equal(NULL::numeric, NULL::numeric)</literal>
+        <returnvalue>true</returnvalue>
+       </para></entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/utils/adt/pseudotypes.c b/src/backend/utils/adt/pseudotypes.c
index 4581c4b1697..93a67e53026 100644
--- a/src/backend/utils/adt/pseudotypes.c
+++ b/src/backend/utils/adt/pseudotypes.c
@@ -23,7 +23,9 @@
 #include "postgres.h"
 
 #include "libpq/pqformat.h"
+#include "utils/datum.h"
 #include "utils/fmgrprotos.h"
+#include "utils/lsyscache.h"
 
 
 /*
@@ -375,3 +377,52 @@ PSEUDOTYPE_DUMMY_IO_FUNCS(anyelement);
 PSEUDOTYPE_DUMMY_IO_FUNCS(anynonarray);
 PSEUDOTYPE_DUMMY_IO_FUNCS(anycompatible);
 PSEUDOTYPE_DUMMY_IO_FUNCS(anycompatiblenonarray);
+
+/*
+ * Compares two datums of the same (any) type, and returns whether they have
+ * the same binary representation.
+ */
+Datum
+pg_datum_image_equal(PG_FUNCTION_ARGS)
+{
+	bool		eq;
+
+	if (PG_ARGISNULL(0) != PG_ARGISNULL(1))
+	{
+		eq = false;
+	}
+	else if (PG_ARGISNULL(0))
+	{
+		/* both NULL */
+		eq = true;
+	}
+	else
+	{
+		Oid		typ;
+		Datum	arg0;
+		Datum	arg1;
+		bool	typbyval;
+		char	typalign;
+		int16	typlen;
+
+		typ = get_fn_expr_argtype(fcinfo->flinfo, 0);
+
+		if (!OidIsValid(typ))
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("could not determine type")));
+		}
+
+		Assert(typ == get_fn_expr_argtype(fcinfo->flinfo, 1));
+
+		arg0 = PG_GETARG_DATUM(0);
+		arg1 = PG_GETARG_DATUM(1);
+
+		get_typlenbyvalalign(typ, &typlen, &typbyval, &typalign);
+
+		eq = datum_image_eq(arg0, arg1, typbyval, typlen);
+	}
+
+	PG_RETURN_BOOL(eq);
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..d0cf65552b0 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12715,4 +12715,11 @@
   proname => 'hashoid8extended', prorettype => 'int8',
   proargtypes => 'oid8 int8', prosrc => 'hashoid8extended' },
 
+{ oid => '9200',
+  descr => 'test if two values have the same binary representation',
+  proname => 'pg_datum_image_equal', proisstrict => 'f',
+  proleakproof => 't', prorettype => 'bool',
+  proargtypes => 'anyelement anyelement',
+  prosrc => 'pg_datum_image_equal' },
+
 ]
diff --git a/src/test/regress/expected/misc_functions.out b/src/test/regress/expected/misc_functions.out
index c3261bff209..4e35db11696 100644
--- a/src/test/regress/expected/misc_functions.out
+++ b/src/test/regress/expected/misc_functions.out
@@ -861,3 +861,84 @@ SELECT test_instr_time();
  t
 (1 row)
 
+-- test pg_datum_image_equal(..., ...)
+WITH values AS (
+    SELECT val::numeric FROM (
+        VALUES ('1.0'),
+               ('1.00'),
+               ('2.0'),
+               ('2.1'),
+               (NULL)
+    ) AS v(val)
+)
+SELECT a.val a, b.val b, a.val = b.val eq, pg_datum_image_equal(a.val, b.val) pdie
+FROM values a CROSS JOIN values b;
+  a   |  b   | eq | pdie 
+------+------+----+------
+  1.0 |  1.0 | t  | t
+  1.0 | 1.00 | t  | f
+  1.0 |  2.0 | f  | f
+  1.0 |  2.1 | f  | f
+  1.0 |      |    | f
+ 1.00 |  1.0 | t  | f
+ 1.00 | 1.00 | t  | t
+ 1.00 |  2.0 | f  | f
+ 1.00 |  2.1 | f  | f
+ 1.00 |      |    | f
+  2.0 |  1.0 | f  | f
+  2.0 | 1.00 | f  | f
+  2.0 |  2.0 | t  | t
+  2.0 |  2.1 | f  | f
+  2.0 |      |    | f
+  2.1 |  1.0 | f  | f
+  2.1 | 1.00 | f  | f
+  2.1 |  2.0 | f  | f
+  2.1 |  2.1 | t  | t
+  2.1 |      |    | f
+      |  1.0 |    | f
+      | 1.00 |    | f
+      |  2.0 |    | f
+      |  2.1 |    | f
+      |      |    | t
+(25 rows)
+
+WITH values AS (
+    SELECT val::jsonb FROM (
+        VALUES ('{"key": 1.0}'),
+               ('{"key": 1.00}'),
+               ('{"key": 2.0}'),
+               ('{"key": null}'),
+               (NULL)
+    ) AS v(val)
+)
+SELECT a.val a, b.val b, a.val = b.val eq, pg_datum_image_equal(a.val, b.val) pdie
+FROM values a CROSS JOIN values b;
+       a       |       b       | eq | pdie 
+---------------+---------------+----+------
+ {"key": 1.0}  | {"key": 1.0}  | t  | t
+ {"key": 1.0}  | {"key": 1.00} | t  | f
+ {"key": 1.0}  | {"key": 2.0}  | f  | f
+ {"key": 1.0}  | {"key": null} | f  | f
+ {"key": 1.0}  |               |    | f
+ {"key": 1.00} | {"key": 1.0}  | t  | f
+ {"key": 1.00} | {"key": 1.00} | t  | t
+ {"key": 1.00} | {"key": 2.0}  | f  | f
+ {"key": 1.00} | {"key": null} | f  | f
+ {"key": 1.00} |               |    | f
+ {"key": 2.0}  | {"key": 1.0}  | f  | f
+ {"key": 2.0}  | {"key": 1.00} | f  | f
+ {"key": 2.0}  | {"key": 2.0}  | t  | t
+ {"key": 2.0}  | {"key": null} | f  | f
+ {"key": 2.0}  |               |    | f
+ {"key": null} | {"key": 1.0}  | f  | f
+ {"key": null} | {"key": 1.00} | f  | f
+ {"key": null} | {"key": 2.0}  | f  | f
+ {"key": null} | {"key": null} | t  | t
+ {"key": null} |               |    | f
+               | {"key": 1.0}  |    | f
+               | {"key": 1.00} |    | f
+               | {"key": 2.0}  |    | f
+               | {"key": null} |    | f
+               |               |    | t
+(25 rows)
+
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 6b519a65cc9..323a5071d50 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -892,6 +892,7 @@ tid_block(tid)
 tid_offset(tid)
 uuid_larger(uuid,uuid)
 uuid_smaller(uuid,uuid)
+pg_datum_image_equal(anyelement,anyelement)
 -- Check that functions without argument are not marked as leakproof.
 SELECT p1.oid::regprocedure
 FROM pg_proc p1 JOIN pg_namespace pn
diff --git a/src/test/regress/sql/misc_functions.sql b/src/test/regress/sql/misc_functions.sql
index 946ee5726cd..77c2f6d60ca 100644
--- a/src/test/regress/sql/misc_functions.sql
+++ b/src/test/regress/sql/misc_functions.sql
@@ -356,3 +356,28 @@ CREATE FUNCTION test_instr_time()
     AS :'regresslib'
     LANGUAGE C;
 SELECT test_instr_time();
+
+-- test pg_datum_image_equal(..., ...)
+WITH values AS (
+    SELECT val::numeric FROM (
+        VALUES ('1.0'),
+               ('1.00'),
+               ('2.0'),
+               ('2.1'),
+               (NULL)
+    ) AS v(val)
+)
+SELECT a.val a, b.val b, a.val = b.val eq, pg_datum_image_equal(a.val, b.val) pdie
+FROM values a CROSS JOIN values b;
+
+WITH values AS (
+    SELECT val::jsonb FROM (
+        VALUES ('{"key": 1.0}'),
+               ('{"key": 1.00}'),
+               ('{"key": 2.0}'),
+               ('{"key": null}'),
+               (NULL)
+    ) AS v(val)
+)
+SELECT a.val a, b.val b, a.val = b.val eq, pg_datum_image_equal(a.val, b.val) pdie
+FROM values a CROSS JOIN values b;
-- 
2.50.1 (Apple Git-155)

