From 841b299cfd0910e9c324252953824365a725ecd3 Mon Sep 17 00:00:00 2001
From: Tom Lane <tgl@sss.pgh.pa.us>
Date: Sat, 4 Jul 2026 16:21:42 -0400
Subject: [PATCH v2] Fix LIKE/regex optimization for indexscan with exact-match
 pattern.

Commit 85b7efa1c introduced support for LIKE with non-deterministic
collations.  By moving some conditionals around, it accidentally broke
the optimization for converting a LIKE or regex exact-match pattern
to an equality indexqual when the index collation doesn't match the
filter collation.  We can't do that conversion if either collation is
nondeterministic, but we can if both are deterministic.  This patch
re-introduces the optimization for that common case.

One important beneficiary of this optimization is the "\d tablename"
command in psql.  Without this fix that will do a seqscan on pg_class
instead of an index point lookup.

Reported-by: Andres Freund <andres@anarazel.de>
Author: Jelte Fennema-Nio <postgres@jeltef.nl>
Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
Discussion: https://postgr.es/m/DHBQIZX8SZVI.ZX614ZMFL645@jeltef.nl
Backpatch-through: 18
---
 src/backend/utils/adt/like_support.c  | 15 +++++++++++----
 src/test/regress/expected/collate.out | 21 ++++++++++++++++++++-
 src/test/regress/sql/collate.sql      | 11 +++++++++++
 3 files changed, 42 insertions(+), 5 deletions(-)

diff --git a/src/backend/utils/adt/like_support.c b/src/backend/utils/adt/like_support.c
index 01cd6b10730..dea6a147c69 100644
--- a/src/backend/utils/adt/like_support.c
+++ b/src/backend/utils/adt/like_support.c
@@ -69,6 +69,10 @@ typedef enum
 	Pattern_Prefix_None, Pattern_Prefix_Partial, Pattern_Prefix_Exact,
 } Pattern_Prefix_Status;
 
+/* non-collatable comparisons, eg for bytea, are always deterministic */
+#define NONDETERMINISTIC(coll) \
+	(OidIsValid(coll) && !get_collation_isdeterministic(coll))
+
 static Node *like_regex_support(Node *rawreq, Pattern_Type ptype);
 static List *match_pattern_prefix(Node *leftop,
 								  Node *rightop,
@@ -381,12 +385,17 @@ match_pattern_prefix(Node *leftop,
 	 * us to not be concerned with specific opclasses (except for the legacy
 	 * "pattern" cases); any index that correctly implements the operators
 	 * will work.
+	 *
+	 * Also, since all deterministic collations agree on equality, we can use
+	 * an index that uses a different collation so long as both collations are
+	 * deterministic.  Otherwise, fail quietly.
 	 */
 	if (pstatus == Pattern_Prefix_Exact)
 	{
 		if (!op_in_opfamily(eqopr, opfamily))
 			return NIL;
-		if (indexcollation != expr_coll)
+		if (indexcollation != expr_coll &&
+			(NONDETERMINISTIC(indexcollation) || NONDETERMINISTIC(expr_coll)))
 			return NIL;
 		expr = make_opclause(eqopr, BOOLOID, false,
 							 (Expr *) leftop, (Expr *) prefix,
@@ -400,10 +409,8 @@ match_pattern_prefix(Node *leftop,
 	 * expression collation is nondeterministic.  The optimized equality or
 	 * prefix tests use bytewise comparisons, which is not consistent with
 	 * nondeterministic collations.
-	 *
-	 * expr_coll is not set for a non-collation-aware data type such as bytea.
 	 */
-	if (expr_coll && !get_collation_isdeterministic(expr_coll))
+	if (NONDETERMINISTIC(expr_coll))
 		return NIL;
 
 	/*
diff --git a/src/test/regress/expected/collate.out b/src/test/regress/expected/collate.out
index 25818f09ad2..a6c46347647 100644
--- a/src/test/regress/expected/collate.out
+++ b/src/test/regress/expected/collate.out
@@ -768,13 +768,31 @@ DETAIL:  LOCALE cannot be specified together with LC_COLLATE or LC_CTYPE.
 CREATE COLLATION coll_dup_chk (FROM = "C", VERSION = "1");
 ERROR:  conflicting or redundant options
 DETAIL:  FROM cannot be specified together with any other options.
+-- Regex exact-match optimization should use index even when the expression
+-- has COLLATE "default" and the index has a different (but deterministic)
+-- collation OID, because equality is collation-insensitive for deterministic
+-- collations.
+CREATE TABLE regex_idx_test (x text);
+CREATE INDEX ON regex_idx_test (x COLLATE "C");
+SET enable_seqscan = off;
+EXPLAIN (costs off)
+SELECT * FROM regex_idx_test WHERE x ~ '^(abc)$' COLLATE "default";
+                   QUERY PLAN                    
+-------------------------------------------------
+ Bitmap Heap Scan on regex_idx_test
+   Filter: (x ~ '^(abc)$'::text)
+   ->  Bitmap Index Scan on regex_idx_test_x_idx
+         Index Cond: (x = 'abc'::text)
+(4 rows)
+
+RESET enable_seqscan;
 --
 -- Clean up.  Many of these table names will be re-used if the user is
 -- trying to run any platform-specific collation tests later, so we
 -- must get rid of them.
 --
 DROP SCHEMA collate_tests CASCADE;
-NOTICE:  drop cascades to 20 other objects
+NOTICE:  drop cascades to 21 other objects
 DETAIL:  drop cascades to table collate_test1
 drop cascades to table collate_test_like
 drop cascades to table collate_test2
@@ -795,3 +813,4 @@ drop cascades to collation builtin_c
 drop cascades to collation mycoll2
 drop cascades to table collate_test23
 drop cascades to view collate_on_int
+drop cascades to table regex_idx_test
diff --git a/src/test/regress/sql/collate.sql b/src/test/regress/sql/collate.sql
index 4b0e4472c3f..78507e5a89b 100644
--- a/src/test/regress/sql/collate.sql
+++ b/src/test/regress/sql/collate.sql
@@ -302,6 +302,17 @@ CREATE COLLATION coll_dup_chk (LC_CTYPE = "POSIX", LOCALE = '');
 -- FROM conflicts with any other option
 CREATE COLLATION coll_dup_chk (FROM = "C", VERSION = "1");
 
+-- Regex exact-match optimization should use index even when the expression
+-- has COLLATE "default" and the index has a different (but deterministic)
+-- collation OID, because equality is collation-insensitive for deterministic
+-- collations.
+CREATE TABLE regex_idx_test (x text);
+CREATE INDEX ON regex_idx_test (x COLLATE "C");
+SET enable_seqscan = off;
+EXPLAIN (costs off)
+SELECT * FROM regex_idx_test WHERE x ~ '^(abc)$' COLLATE "default";
+RESET enable_seqscan;
+
 --
 -- Clean up.  Many of these table names will be re-used if the user is
 -- trying to run any platform-specific collation tests later, so we
-- 
2.52.0

