From dfcf263959f39c7076dc9e99944749b8b3330539 Mon Sep 17 00:00:00 2001
From: Zexin Li <lizi.openmind@gmail.com>
Date: Wed, 2 Sep 2026 02:39:29 +0000
Subject: [PATCH] Check partition key opclass against nondeterministic
 collations
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Commit 2810396312 made index creation refuse text_pattern_ops (and its
varchar and bpchar siblings) in combination with a nondeterministic
collation, because those opclasses compare bytewise while their
equality operator follows the collation.  The same check was never
applied to partition keys, so a partitioned table could be declared
with a pattern opclass over a nondeterministically collated column.
Tuple routing and partition pruning then both use the opclass's
bytewise comparison, while the query's equality operator uses the
column's collation, so pruning can discard partitions that contain
matching rows.  SELECT, UPDATE and DELETE return wrong results unless
partition pruning is disabled.

Move the existing check from index_create() into a new function
CheckOpclassCollation() and call it from ComputePartitionAttrs() as
well.  As a result, a partitioned table whose partition key combines
such an opclass with a nondeterministic collation can no longer be
created.  Such declarations already produced wrong answers, so nothing
that worked correctly is lost.

Bug: #19645
Reported-by: 放空 <jj-zhang25@mails.tsinghua.edu.cn>
Discussion: https://postgr.es/m/19645-60a963f2e91478e0@postgresql.org
Backpatch-through: 14
---
 src/backend/catalog/index.c                   | 92 ++++++++++---------
 src/backend/commands/tablecmds.c              |  6 ++
 src/include/catalog/index.h                   |  2 +
 .../regress/expected/collate.icu.utf8.out     |  5 +
 src/test/regress/sql/collate.icu.utf8.sql     |  5 +
 5 files changed, 69 insertions(+), 41 deletions(-)

diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index ec21b83b..8f8ab52a 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -676,6 +676,54 @@ UpdateIndexRelation(Oid indexoid,
 }
 
 
+/*
+ * CheckOpclassCollation
+ *
+ * Verify that the given operator class can be used with the given collation,
+ * throwing an error if not.  This is applied to index columns and to
+ * partition key columns.
+ *
+ * Btree text_pattern_ops uses texteq as the equality operator, which is
+ * fine as long as the collation is deterministic; texteq then reduces to
+ * bitwise equality and so it is semantically compatible with the other
+ * operators and functions in that opclass.  But with a nondeterministic
+ * collation, texteq could yield results that are incompatible with the
+ * actual behavior of the index or partition key (which is determined by
+ * the opclass's comparison function).  We prevent such problems by refusing
+ * that opclass in combination with a nondeterministic collation.
+ *
+ * The same applies to varchar_pattern_ops and bpchar_pattern_ops.  If we
+ * find more cases, we might decide to create a real mechanism for marking
+ * opclasses as incompatible with nondeterminism; but for now, this small
+ * hack suffices.
+ *
+ * Another solution is to use a special operator, not texteq, as the
+ * equality opclass member; but that is undesirable because it would
+ * prevent index usage in many queries that work fine today.
+ */
+void
+CheckOpclassCollation(Oid opclass, Oid collation)
+{
+	if (!OidIsValid(collation))
+		return;
+
+	if ((opclass == TEXT_BTREE_PATTERN_OPS_OID ||
+		 opclass == VARCHAR_BTREE_PATTERN_OPS_OID ||
+		 opclass == BPCHAR_BTREE_PATTERN_OPS_OID) &&
+		!get_collation_isdeterministic(collation))
+	{
+		HeapTuple	classtup;
+
+		classtup = SearchSysCache1(CLAOID, ObjectIdGetDatum(opclass));
+		if (!HeapTupleIsValid(classtup))
+			elog(ERROR, "cache lookup failed for operator class %u", opclass);
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("nondeterministic collations are not supported for operator class \"%s\"",
+						NameStr(((Form_pg_opclass) GETSTRUCT(classtup))->opcname))));
+	}
+}
+
 /*
  * index_create
  *
@@ -811,49 +859,11 @@ index_create(Relation heapRelation,
 				 errmsg("user-defined indexes on system catalog tables are not supported")));
 
 	/*
-	 * Btree text_pattern_ops uses texteq as the equality operator, which is
-	 * fine as long as the collation is deterministic; texteq then reduces to
-	 * bitwise equality and so it is semantically compatible with the other
-	 * operators and functions in that opclass.  But with a nondeterministic
-	 * collation, texteq could yield results that are incompatible with the
-	 * actual behavior of the index (which is determined by the opclass's
-	 * comparison function).  We prevent such problems by refusing creation of
-	 * an index with that opclass and a nondeterministic collation.
-	 *
-	 * The same applies to varchar_pattern_ops and bpchar_pattern_ops.  If we
-	 * find more cases, we might decide to create a real mechanism for marking
-	 * opclasses as incompatible with nondeterminism; but for now, this small
-	 * hack suffices.
-	 *
-	 * Another solution is to use a special operator, not texteq, as the
-	 * equality opclass member; but that is undesirable because it would
-	 * prevent index usage in many queries that work fine today.
+	 * Check that each operator class can be used with its collation; see
+	 * CheckOpclassCollation for the rationale.
 	 */
 	for (i = 0; i < indexInfo->ii_NumIndexKeyAttrs; i++)
-	{
-		Oid			collation = collationIds[i];
-		Oid			opclass = opclassIds[i];
-
-		if (collation)
-		{
-			if ((opclass == TEXT_BTREE_PATTERN_OPS_OID ||
-				 opclass == VARCHAR_BTREE_PATTERN_OPS_OID ||
-				 opclass == BPCHAR_BTREE_PATTERN_OPS_OID) &&
-				!get_collation_isdeterministic(collation))
-			{
-				HeapTuple	classtup;
-
-				classtup = SearchSysCache1(CLAOID, ObjectIdGetDatum(opclass));
-				if (!HeapTupleIsValid(classtup))
-					elog(ERROR, "cache lookup failed for operator class %u", opclass);
-				ereport(ERROR,
-						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-						 errmsg("nondeterministic collations are not supported for operator class \"%s\"",
-								NameStr(((Form_pg_opclass) GETSTRUCT(classtup))->opcname))));
-				ReleaseSysCache(classtup);
-			}
-		}
-	}
+		CheckOpclassCollation(opclassIds[i], collationIds[i]);
 
 	/*
 	 * Concurrent index build on a system catalog is unsafe because we tend to
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index fd144d78..ad4733a0 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -20772,6 +20772,12 @@ ComputePartitionAttrs(ParseState *pstate, Relation rel, List *partParams, AttrNu
 											   am_oid == HASH_AM_OID ? "hash" : "btree",
 											   am_oid);
 
+		/*
+		 * Check that the operator class can be used with the collation, in
+		 * the same way as index_create does for index columns.
+		 */
+		CheckOpclassCollation(partopclass[attn], partcollation[attn]);
+
 		attn++;
 	}
 }
diff --git a/src/include/catalog/index.h b/src/include/catalog/index.h
index b952ad07..fc00407b 100644
--- a/src/include/catalog/index.h
+++ b/src/include/catalog/index.h
@@ -64,6 +64,8 @@ extern void index_check_primary_key(Relation heapRel,
 									bool is_alter_table,
 									const IndexStmt *stmt);
 
+extern void CheckOpclassCollation(Oid opclass, Oid collation);
+
 #define	INDEX_CREATE_IS_PRIMARY				(1 << 0)
 #define	INDEX_CREATE_ADD_CONSTRAINT			(1 << 1)
 #define	INDEX_CREATE_SKIP_BUILD				(1 << 2)
diff --git a/src/test/regress/expected/collate.icu.utf8.out b/src/test/regress/expected/collate.icu.utf8.out
index fcfcc658..15f58be7 100644
--- a/src/test/regress/expected/collate.icu.utf8.out
+++ b/src/test/regress/expected/collate.icu.utf8.out
@@ -3211,6 +3211,11 @@ SELECT (SELECT count(*) FROM test23a_0) <> (SELECT count(*) FROM test23a_1);
  t
 (1 row)
 
+-- operator classes that cannot be used with a nondeterministic collation
+-- are rejected for partition keys, as they are for indexes
+CREATE TABLE test24 (a int, b text COLLATE case_insensitive) PARTITION BY RANGE (b text_pattern_ops);  -- error
+ERROR:  nondeterministic collations are not supported for operator class "text_pattern_ops"
+CREATE TABLE test24 (a int, b text COLLATE case_insensitive) PARTITION BY RANGE (b COLLATE "C" text_pattern_ops);  -- ok
 CREATE TABLE test30 (a int, b char(3) COLLATE case_insensitive) PARTITION BY LIST (b);
 CREATE TABLE test30_1 PARTITION OF test30 FOR VALUES IN ('abc');
 INSERT INTO test30 VALUES (1, 'abc');
diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql
index ce4e2bb3..2a9ed589 100644
--- a/src/test/regress/sql/collate.icu.utf8.sql
+++ b/src/test/regress/sql/collate.icu.utf8.sql
@@ -1180,6 +1180,11 @@ INSERT INTO test23a VALUES (2, ARRAY['DEF']);
 -- they end up in the same partition (but it's platform-dependent which one)
 SELECT (SELECT count(*) FROM test23a_0) <> (SELECT count(*) FROM test23a_1);
 
+-- operator classes that cannot be used with a nondeterministic collation
+-- are rejected for partition keys, as they are for indexes
+CREATE TABLE test24 (a int, b text COLLATE case_insensitive) PARTITION BY RANGE (b text_pattern_ops);  -- error
+CREATE TABLE test24 (a int, b text COLLATE case_insensitive) PARTITION BY RANGE (b COLLATE "C" text_pattern_ops);  -- ok
+
 CREATE TABLE test30 (a int, b char(3) COLLATE case_insensitive) PARTITION BY LIST (b);
 CREATE TABLE test30_1 PARTITION OF test30 FOR VALUES IN ('abc');
 INSERT INTO test30 VALUES (1, 'abc');
-- 
2.34.1

