From 894c37ed58d79a208ac985020e8e8c22d47c4bea Mon Sep 17 00:00:00 2001
From: Zhong ShiHao <zhongshihao@ZhongdeMacBook-Pro.local>
Date: Tue, 28 Jul 2026 23:47:12 -0400
Subject: [PATCH] Add planner support function to two-argument regexp_like()

The ~ operator's underlying function textregexeq() has a planner support
function that lets the planner derive index conditions from a pattern
with a fixed prefix.  regexp_like(), added in PG 15 as the SQL-standard
spelling of the same test, was never given one, so the standard spelling
silently loses the optimization.

For the two-argument form this is a pure oversight: regexp_like_no_flags()
calls regexp_like(), which with a NULL flags argument leaves
parse_re_flags() at its REG_ADVANCED default and then invokes
RE_compile_and_execute() with exactly the arguments textregexeq() uses.
The two are the same test.  like_regex_support() already handles being
invoked on a FuncExpr rather than an OpExpr -- see the is_funcclause()
branch, which asserts a two-argument call -- so no new C code is needed;
the function just has to be pointed at the existing support function.

On a 20000-row table with a btree index on the column, this turns

    WHERE regexp_like(s, '^item19999')

from a sequential scan into an index scan with
Index Cond: s >= 'item19999' AND s < 'item1999:', matching what the
equivalent s ~ '^item19999' has always produced.  Selectivity estimation
now also matches the operator spelling, since patternsel_common() is
reached through the same support function.

The three-argument form is deliberately left alone.  It cannot share
textregexeq_support() as-is: like_regex_support() asserts a two-argument
call, and more importantly several flags would make prefix extraction
unsound.  With 'n', for example, '^' matches after a newline, so
E'xyz\nabc' satisfies '^abc' while sorting outside the ['abc','abd')
range the index condition would scan.  Supporting flags requires a
separate support function that inspects them and declines on anything
other than 'i'/'c'.

Note that regexp_like() only has a (text, text) signature, so a column of
a type that is not binary-coercible to text -- name, for instance -- still
gets no index condition, because the cast keeps the Var from matching the
index column.  varchar works, being binary-coercible.

This changes pg_proc, so catversion needs a bump.

---
 src/include/catalog/pg_proc.dat           |  3 +-
 src/test/regress/expected/btree_index.out | 51 +++++++++++++++++++++++
 src/test/regress/sql/btree_index.sql      | 26 ++++++++++++
 3 files changed, 79 insertions(+), 1 deletion(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f8a021987b5..6d142bf9564 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -3856,7 +3856,8 @@
   proargnames => '{string, pattern, start, N, endoption, flags, subexpr}',
   prosrc => 'regexp_instr' },
 { oid => '6263', descr => 'test for regexp match',
-  proname => 'regexp_like', prorettype => 'bool', proargtypes => 'text text',
+  proname => 'regexp_like', prosupport => 'textregexeq_support',
+  prorettype => 'bool', proargtypes => 'text text',
   proargnames => '{string, pattern}', prosrc => 'regexp_like_no_flags' },
 { oid => '6264', descr => 'test for regexp match',
   proname => 'regexp_like', prorettype => 'bool',
diff --git a/src/test/regress/expected/btree_index.out b/src/test/regress/expected/btree_index.out
index 3a83e9a0534..9ab0b15e2fc 100644
--- a/src/test/regress/expected/btree_index.out
+++ b/src/test/regress/expected/btree_index.out
@@ -767,6 +767,57 @@ select * from btree_bpchar where f1::bpchar like 'foo%';
 
 -- get test coverage for "single value" deduplication strategy:
 insert into btree_bpchar select 'foo' from generate_series(1,1500);
+-- Check that the two-argument form of regexp_like() gets the same index
+-- optimization as the equivalent ~ operator, via the planner support
+-- function attached to it.
+create temp table btree_regexp_like (f1 text collate "C");
+create index on btree_regexp_like(f1);
+insert into btree_regexp_like
+  select 'item' || g from generate_series(1, 1000) g;
+analyze btree_regexp_like;
+set enable_seqscan to false;
+explain (costs off)
+select * from btree_regexp_like where regexp_like(f1, '^item999');
+                             QUERY PLAN                              
+---------------------------------------------------------------------
+ Index Only Scan using btree_regexp_like_f1_idx on btree_regexp_like
+   Index Cond: ((f1 >= 'item999'::text) AND (f1 < 'item99:'::text))
+   Filter: regexp_like(f1, '^item999'::text)
+(3 rows)
+
+select * from btree_regexp_like where regexp_like(f1, '^item999') order by 1;
+   f1    
+---------
+ item999
+(1 row)
+
+-- must agree with the operator spelling
+select * from btree_regexp_like where f1 ~ '^item999' order by 1;
+   f1    
+---------
+ item999
+(1 row)
+
+reset enable_seqscan;
+-- a pattern with no fixed prefix yields no index condition
+explain (costs off)
+select * from btree_regexp_like where regexp_like(f1, 'item999$');
+                 QUERY PLAN                  
+---------------------------------------------
+ Seq Scan on btree_regexp_like
+   Filter: regexp_like(f1, 'item999$'::text)
+(2 rows)
+
+-- the three-argument form has no support function, since some flags would
+-- make prefix extraction unsound
+explain (costs off)
+select * from btree_regexp_like where regexp_like(f1, '^item999', 'c');
+                       QUERY PLAN                       
+--------------------------------------------------------
+ Seq Scan on btree_regexp_like
+   Filter: regexp_like(f1, '^item999'::text, 'c'::text)
+(2 rows)
+
 --
 -- Perform unique checking, with and without the use of deduplication
 --
diff --git a/src/test/regress/sql/btree_index.sql b/src/test/regress/sql/btree_index.sql
index a08bb101c20..0a1525a8bdc 100644
--- a/src/test/regress/sql/btree_index.sql
+++ b/src/test/regress/sql/btree_index.sql
@@ -414,6 +414,32 @@ select * from btree_bpchar where f1::bpchar like 'foo%';
 -- get test coverage for "single value" deduplication strategy:
 insert into btree_bpchar select 'foo' from generate_series(1,1500);
 
+-- Check that the two-argument form of regexp_like() gets the same index
+-- optimization as the equivalent ~ operator, via the planner support
+-- function attached to it.
+
+create temp table btree_regexp_like (f1 text collate "C");
+create index on btree_regexp_like(f1);
+insert into btree_regexp_like
+  select 'item' || g from generate_series(1, 1000) g;
+analyze btree_regexp_like;
+
+set enable_seqscan to false;
+explain (costs off)
+select * from btree_regexp_like where regexp_like(f1, '^item999');
+select * from btree_regexp_like where regexp_like(f1, '^item999') order by 1;
+-- must agree with the operator spelling
+select * from btree_regexp_like where f1 ~ '^item999' order by 1;
+reset enable_seqscan;
+
+-- a pattern with no fixed prefix yields no index condition
+explain (costs off)
+select * from btree_regexp_like where regexp_like(f1, 'item999$');
+-- the three-argument form has no support function, since some flags would
+-- make prefix extraction unsound
+explain (costs off)
+select * from btree_regexp_like where regexp_like(f1, '^item999', 'c');
+
 --
 -- Perform unique checking, with and without the use of deduplication
 --
-- 
2.37.1 (Apple Git-137.1)

