From 1f83c2983d58a3141112d8f09ab8a90448d01ac2 Mon Sep 17 00:00:00 2001
From: shihao zhong <zhong950419@gmail.com>
Date: Mon, 5 Oct 2026 11:41:30 -0400
Subject: [PATCH v1 1/2] Remove equalimage support from bpchar_ops and
 bpchar_pattern_ops

bpchar equality ignores trailing spaces, so equal values in a column
with no length can have different images.  Deduplication could make an
index-only scan return the wrong value, and eager aggregation could
merge such rows below a join.  Drop support function 4 from both
opclasses, like interval_ops.  Existing indexes need a REINDEX.

Bug: #19749
Reported-by: Ke <kehan5800@gmail.com>
Discussion: https://postgr.es/m/19749-d46adf7c7d910b07@postgresql.org
---
 contrib/amcheck/verify_nbtree.c          | 15 ++++++---
 doc/src/sgml/btree.sgml                  |  9 +++++-
 src/include/catalog/pg_amproc.dat        |  5 ---
 src/test/regress/expected/opr_sanity.out | 39 ++++++++++++------------
 4 files changed, 39 insertions(+), 29 deletions(-)

diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index 455eb90f38c..aa367e5133e 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -331,13 +331,18 @@ bt_index_check_callback(Relation indrel, Relation heaprel, void *state, bool rea
 	if (allequalimage && !_bt_allequalimage(indrel, false))
 	{
 		bool		has_interval_ops = false;
+		bool		has_bpchar_ops = false;
 
 		for (int i = 0; i < IndexRelationGetNumberOfKeyAttributes(indrel); i++)
-			if (indrel->rd_opfamily[i] == INTERVAL_BTREE_FAM_OID)
-			{
+		{
+			Oid			opfamily = indrel->rd_opfamily[i];
+
+			if (opfamily == INTERVAL_BTREE_FAM_OID)
 				has_interval_ops = true;
-				break;
-			}
+			else if (opfamily == BPCHAR_BTREE_FAM_OID ||
+					 opfamily == BPCHAR_PATTERN_BTREE_FAM_OID)
+				has_bpchar_ops = true;
+		}
 
 		ereport(ERROR,
 				(errcode(ERRCODE_INDEX_CORRUPTED),
@@ -345,6 +350,8 @@ bt_index_check_callback(Relation indrel, Relation heaprel, void *state, bool rea
 						RelationGetRelationName(indrel)),
 				 has_interval_ops
 				 ? errhint("This is known of \"interval\" indexes last built on a version predating 2023-11.")
+				 : has_bpchar_ops
+				 ? errhint("This is known of \"character\" indexes last built on a version predating 2026-11.")
 				 : 0));
 	}
 
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 027361f20bb..3e5ca7a4430 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -875,13 +875,20 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
    <itemizedlist>
     <listitem>
      <para>
-      <type>text</type>, <type>varchar</type>, and <type>char</type>
+      <type>text</type> and <type>varchar</type>
       cannot use deduplication when a
       <emphasis>nondeterministic</emphasis> collation is used.  Case
       and accent differences must be preserved among equal datums.
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <type>char</type> cannot use deduplication.  Trailing spaces
+      must be preserved among equal datums.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <type>numeric</type> cannot use deduplication.  Numeric display
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 4a1efdbc899..3504d2d135d 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -30,9 +30,6 @@
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
   amprocrighttype => 'bpchar', amprocnum => '2',
   amproc => 'bpchar_sortsupport' },
-{ amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
-  amprocrighttype => 'bpchar', amprocnum => '4',
-  amproc => 'btvarstrequalimage' },
 { amprocfamily => 'btree/bytea_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '1', amproc => 'byteacmp' },
 { amprocfamily => 'btree/bytea_ops', amproclefttype => 'bytea',
@@ -273,8 +270,6 @@
 { amprocfamily => 'btree/bpchar_pattern_ops', amproclefttype => 'bpchar',
   amprocrighttype => 'bpchar', amprocnum => '2',
   amproc => 'btbpchar_pattern_sortsupport' },
-{ amprocfamily => 'btree/bpchar_pattern_ops', amproclefttype => 'bpchar',
-  amprocrighttype => 'bpchar', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/money_ops', amproclefttype => 'money',
   amprocrighttype => 'money', amprocnum => '1', amproc => 'cash_cmp' },
 { amprocfamily => 'btree/money_ops', amproclefttype => 'money',
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 67cae397861..d703fdce536 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2225,25 +2225,26 @@ LEFT JOIN pg_amproc AS amp ON amp.amprocfamily = opf.oid AND
 WHERE am.amname = 'btree' AND
     amp.amproc IS DISTINCT FROM 'btequalimage'::regproc
 ORDER BY 1, 2, 3;
-        proc        |  opfamily_name   |   opclass_name   |    opcintype     
---------------------+------------------+------------------+------------------
- btvarstrequalimage | bpchar_ops       | bpchar_ops       | character
- btvarstrequalimage | text_ops         | name_ops         | name
- btvarstrequalimage | text_ops         | text_ops         | text
- btvarstrequalimage | text_ops         | varchar_ops      | text
-                    | array_ops        | array_ops        | anyarray
-                    | float_ops        | float4_ops       | real
-                    | float_ops        | float8_ops       | double precision
-                    | interval_ops     | interval_ops     | interval
-                    | jsonb_ops        | jsonb_ops        | jsonb
-                    | multirange_ops   | multirange_ops   | anymultirange
-                    | numeric_ops      | numeric_ops      | numeric
-                    | range_ops        | range_ops        | anyrange
-                    | record_image_ops | record_image_ops | record
-                    | record_ops       | record_ops       | record
-                    | tsquery_ops      | tsquery_ops      | tsquery
-                    | tsvector_ops     | tsvector_ops     | tsvector
-(16 rows)
+        proc        |   opfamily_name    |    opclass_name    |    opcintype     
+--------------------+--------------------+--------------------+------------------
+ btvarstrequalimage | text_ops           | name_ops           | name
+ btvarstrequalimage | text_ops           | text_ops           | text
+ btvarstrequalimage | text_ops           | varchar_ops        | text
+                    | array_ops          | array_ops          | anyarray
+                    | bpchar_ops         | bpchar_ops         | character
+                    | bpchar_pattern_ops | bpchar_pattern_ops | character
+                    | float_ops          | float4_ops         | real
+                    | float_ops          | float8_ops         | double precision
+                    | interval_ops       | interval_ops       | interval
+                    | jsonb_ops          | jsonb_ops          | jsonb
+                    | multirange_ops     | multirange_ops     | anymultirange
+                    | numeric_ops        | numeric_ops        | numeric
+                    | range_ops          | range_ops          | anyrange
+                    | record_image_ops   | record_image_ops   | record
+                    | record_ops         | record_ops         | record
+                    | tsquery_ops        | tsquery_ops        | tsquery
+                    | tsvector_ops       | tsvector_ops       | tsvector
+(17 rows)
 
 -- **************** pg_index ****************
 -- Look for illegal values in pg_index fields.
-- 
2.37.1 (Apple Git-137.1)

