From e9e231a90624e6d669202f3a4b79de974846cfcf 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 v3] 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 always do that conversion if the expression
collation is 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          | 23 +++++++++++++++----
 .../regress/expected/collate.icu.utf8.out     | 19 +++++++++++++++
 src/test/regress/expected/collate.out         | 22 +++++++++++++++++-
 src/test/regress/sql/collate.icu.utf8.sql     | 12 ++++++++++
 src/test/regress/sql/collate.sql              | 13 +++++++++++
 5 files changed, 84 insertions(+), 5 deletions(-)

diff --git a/src/backend/utils/adt/like_support.c b/src/backend/utils/adt/like_support.c
index 01cd6b10730..960e118307b 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,25 @@ 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, we can use an index whose collation differs from the
+	 * expression's, so long as the expression's collation is deterministic.
+	 * It doesn't matter whether the index collation is deterministic or not:
+	 * if it's deterministic it agrees on equality with the expression
+	 * collation, and if it's nondeterministic the "=" indexqual merely treats
+	 * a superset of values as equal.  The latter is fine because the
+	 * indexqual is lossy (the original pattern is rechecked), so the recheck
+	 * filters out the rows where the index collation disagrees with the
+	 * expression collation.  A nondeterministic expression collation, on the
+	 * other hand, is only safe when the index uses that exact same collation,
+	 * since otherwise the indexqual could omit matching rows.  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(expr_coll))
 			return NIL;
 		expr = make_opclause(eqopr, BOOLOID, false,
 							 (Expr *) leftop, (Expr *) prefix,
@@ -400,10 +417,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.icu.utf8.out b/src/test/regress/expected/collate.icu.utf8.out
index 04e2f6df037..eb58009e57a 100644
--- a/src/test/regress/expected/collate.icu.utf8.out
+++ b/src/test/regress/expected/collate.icu.utf8.out
@@ -1561,6 +1561,25 @@ SELECT x FROM test3cs WHERE x ~ 'a';
  abc
 (1 row)
 
+-- An exact-match regex is converted to an "=" indexqual.  We can use an index
+-- whose collation differs from the expression's as long as the expression's
+-- collation is deterministic, even when the index's collation is
+-- nondeterministic.  Check that we get an index scan rather than a seqscan.
+CREATE INDEX test3cs_ci ON test3cs (x COLLATE case_insensitive);
+SET enable_seqscan = off;
+SET enable_bitmapscan = off;
+EXPLAIN (costs off)
+SELECT x FROM test3cs WHERE x ~ '^(abc)$';
+                 QUERY PLAN                  
+---------------------------------------------
+ Index Only Scan using test3cs_ci on test3cs
+   Index Cond: (x = 'abc'::text)
+   Filter: (x ~ '^(abc)$'::text)
+(3 rows)
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+DROP INDEX test3cs_ci;
 SET enable_hashagg TO off;
 SELECT x FROM test1cs UNION SELECT x FROM test2cs ORDER BY x;
   x  
diff --git a/src/test/regress/expected/collate.out b/src/test/regress/expected/collate.out
index 25818f09ad2..c7252c79b0d 100644
--- a/src/test/regress/expected/collate.out
+++ b/src/test/regress/expected/collate.out
@@ -768,13 +768,32 @@ 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;
+SET enable_bitmapscan = off;
+EXPLAIN (costs off)
+SELECT * FROM regex_idx_test WHERE x ~ '^(abc)$' COLLATE "default";
+                          QUERY PLAN                          
+--------------------------------------------------------------
+ Index Only Scan using regex_idx_test_x_idx on regex_idx_test
+   Index Cond: (x = 'abc'::text)
+   Filter: (x ~ '^(abc)$'::text)
+(3 rows)
+
+RESET enable_seqscan;
+RESET enable_bitmapscan;
 --
 -- 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 +814,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.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql
index 18c47e6e05a..49003af1bb1 100644
--- a/src/test/regress/sql/collate.icu.utf8.sql
+++ b/src/test/regress/sql/collate.icu.utf8.sql
@@ -594,6 +594,18 @@ SELECT x FROM test3cs WHERE x LIKE 'a%';
 SELECT x FROM test3cs WHERE x ILIKE 'a%';
 SELECT x FROM test3cs WHERE x SIMILAR TO 'a%';
 SELECT x FROM test3cs WHERE x ~ 'a';
+-- An exact-match regex is converted to an "=" indexqual.  We can use an index
+-- whose collation differs from the expression's as long as the expression's
+-- collation is deterministic, even when the index's collation is
+-- nondeterministic.  Check that we get an index scan rather than a seqscan.
+CREATE INDEX test3cs_ci ON test3cs (x COLLATE case_insensitive);
+SET enable_seqscan = off;
+SET enable_bitmapscan = off;
+EXPLAIN (costs off)
+SELECT x FROM test3cs WHERE x ~ '^(abc)$';
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+DROP INDEX test3cs_ci;
 SET enable_hashagg TO off;
 SELECT x FROM test1cs UNION SELECT x FROM test2cs ORDER BY x;
 SELECT x FROM test2cs UNION SELECT x FROM test1cs ORDER BY x;
diff --git a/src/test/regress/sql/collate.sql b/src/test/regress/sql/collate.sql
index 4b0e4472c3f..68345068430 100644
--- a/src/test/regress/sql/collate.sql
+++ b/src/test/regress/sql/collate.sql
@@ -302,6 +302,19 @@ 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;
+SET enable_bitmapscan = off;
+EXPLAIN (costs off)
+SELECT * FROM regex_idx_test WHERE x ~ '^(abc)$' COLLATE "default";
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
 --
 -- 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

base-commit: e0ff7fd9aa2e6f77c38825e71200ced742220d55
-- 
2.54.0

