From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: oga5 <atsushi.ogawa@gmail.com>
Date: Sat, 11 Jul 2026 14:33:52 +0900
Subject: [PATCH] Use Boyer-Moore-Horspool for simple LIKE patterns

Add a Boyer-Moore-Horspool fast path for ordinary positive LIKE
contains-search patterns such as LIKE '%literal%'.  Dispatch at execution
time from textlike and namelike, and cache either the BMH search state or a
generic fallback marker in FmgrInfo.fn_extra.

Use BMH only for stable patterns containing one literal of at least four
bytes between leading and trailing percent wildcards.  Restrict the
byte-oriented search to deterministic collations and single-byte or UTF-8
database encodings.  Do not cache ScalarArrayOpExpr patterns because the
array elements vary between operator calls.  Other patterns, encodings, and
collations continue to use the existing matcher.

Keep the BMH implementation in a separate translation unit so the existing
SB_MatchText, UTF8_MatchText, and MB_MatchText instantiations remain
unchanged.  Add regression coverage for runtime matching, escapes, fallback
cases, scalar arrays, prepared statements, name input, UTF-8 literals, and
nondeterministic ICU collations.
---
 src/backend/utils/adt/Makefile                |   1 +
 src/backend/utils/adt/like.c                  |  24 +-
 src/backend/utils/adt/like_bmh.c              | 198 ++++++++++++++
 src/backend/utils/adt/meson.build             |   1 +
 src/include/utils/like_bmh.h                  |  34 +++
 .../regress/expected/collate.icu.utf8.out     |  19 ++
 src/test/regress/expected/like_bmh.out        | 246 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   1 +
 src/test/regress/sql/collate.icu.utf8.sql     |   8 +
 src/test/regress/sql/like_bmh.sql             | 113 ++++++++
 10 files changed, 643 insertions(+), 2 deletions(-)
 create mode 100644 src/backend/utils/adt/like_bmh.c
 create mode 100644 src/include/utils/like_bmh.h
 create mode 100644 src/test/regress/expected/like_bmh.out
 create mode 100644 src/test/regress/sql/like_bmh.sql

diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 0c76219..f2cc5b1 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -61,6 +61,7 @@ OBJS = \
 	jsonpath_gram.o \
 	jsonpath_scan.o \
 	like.o \
+	like_bmh.o \
 	like_support.o \
 	lockfuncs.o \
 	mac.o \
diff --git a/src/backend/utils/adt/like.c b/src/backend/utils/adt/like.c
index 64147bc..66ee563 100644
--- a/src/backend/utils/adt/like.c
+++ b/src/backend/utils/adt/like.c
@@ -23,6 +23,7 @@
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
 #include "utils/fmgrprotos.h"
+#include "utils/like_bmh.h"
 #include "utils/pg_locale.h"
 #include "varatt.h"
 
@@ -161,6 +162,23 @@ GenericMatchText(const char *s, int slen, const char *p, int plen, Oid collation
 		return MB_MatchText(s, slen, p, plen, locale);
 }
 
+static inline int
+LikeMatchText(const char *s, int slen, const char *p, int plen,
+			  FmgrInfo *flinfo, Oid collation)
+{
+	LikeBMHState *state = flinfo->fn_extra;
+	int			result;
+
+	if (state != NULL && state->mode == LIKE_BMH_GENERIC)
+		return GenericMatchText(s, slen, p, plen, collation);
+
+	result = like_bmh_match(s, slen, p, plen, flinfo, collation);
+	if (result == LIKE_BMH_FALLBACK)
+		return GenericMatchText(s, slen, p, plen, collation);
+
+	return result;
+}
+
 static inline int
 Generic_Text_IC_like(text *str, text *pat, Oid collation)
 {
@@ -246,7 +264,8 @@ namelike(PG_FUNCTION_ARGS)
 	p = VARDATA_ANY(pat);
 	plen = VARSIZE_ANY_EXHDR(pat);
 
-	result = (GenericMatchText(s, slen, p, plen, PG_GET_COLLATION()) == LIKE_TRUE);
+	result = (LikeMatchText(s, slen, p, plen, fcinfo->flinfo,
+							PG_GET_COLLATION()) == LIKE_TRUE);
 
 	PG_RETURN_BOOL(result);
 }
@@ -288,7 +307,8 @@ textlike(PG_FUNCTION_ARGS)
 	p = VARDATA_ANY(pat);
 	plen = VARSIZE_ANY_EXHDR(pat);
 
-	result = (GenericMatchText(s, slen, p, plen, PG_GET_COLLATION()) == LIKE_TRUE);
+	result = (LikeMatchText(s, slen, p, plen, fcinfo->flinfo,
+							PG_GET_COLLATION()) == LIKE_TRUE);
 
 	PG_RETURN_BOOL(result);
 }
diff --git a/src/backend/utils/adt/like_bmh.c b/src/backend/utils/adt/like_bmh.c
new file mode 100644
index 0000000..56c174a
--- /dev/null
+++ b/src/backend/utils/adt/like_bmh.c
@@ -0,0 +1,198 @@
+/*-------------------------------------------------------------------------
+ *
+ * like_bmh.c
+ *	  Boyer-Moore-Horspool search for LIKE patterns containing one literal.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/like_bmh.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "catalog/pg_collation.h"
+#include "mb/pg_wchar.h"
+#include "nodes/primnodes.h"
+#include "utils/like_bmh.h"
+#include "utils/pg_locale.h"
+
+#define LIKE_BMH_SKIP_TABLE_SIZE	256
+#define LIKE_BMH_MIN_LITERAL_LEN	4
+#define LIKE_TRUE					1
+#define LIKE_FALSE					0
+
+typedef struct LikeBMHSearchState
+{
+	LikeBMHState base;
+	Oid			collation;
+	int			literal_len;
+	int			skip_table[LIKE_BMH_SKIP_TABLE_SIZE];
+	char		literal[FLEXIBLE_ARRAY_MEMBER];
+} LikeBMHSearchState;
+
+/*
+ * Check whether the escape-normalized pattern is a single literal surrounded
+ * by '%' wildcards.  Remove backslash escapes while building the search state.
+ */
+static bool
+like_bmh_pattern_is_eligible(const char *p, int plen, int *literal_len)
+{
+	int			i;
+
+	*literal_len = 0;
+	if (plen < LIKE_BMH_MIN_LITERAL_LEN + 2 ||
+		p[0] != '%' || p[plen - 1] != '%')
+		return false;
+
+	for (i = 1; i < plen - 1; i++)
+	{
+		if (p[i] == '%' || p[i] == '_')
+			return false;
+		if (p[i] == '\\')
+		{
+			if (i + 1 >= plen - 1)
+				return false;
+			i++;
+		}
+		(*literal_len)++;
+	}
+
+	return *literal_len >= LIKE_BMH_MIN_LITERAL_LEN;
+}
+
+static LikeBMHState *
+like_bmh_init(const char *p, int plen, FmgrInfo *flinfo, Oid collation)
+{
+	LikeBMHState *state;
+	LikeBMHSearchState *search_state;
+	pg_locale_t locale;
+	bool		pattern_stable;
+	int			literal_len;
+	int			i;
+	int			j;
+
+	pattern_stable = get_fn_expr_arg_stable(flinfo, 1);
+
+	/*
+	 * ScalarArrayOpExpr invokes the operator once per array element.  The
+	 * array expression can be stable while the pattern passed to this function
+	 * changes between calls, so it must not use a cached search state.
+	 */
+	if (flinfo->fn_expr != NULL && IsA(flinfo->fn_expr, ScalarArrayOpExpr))
+		pattern_stable = false;
+
+	/*
+	 * A byte search is safe in single-byte encodings and UTF-8.  In UTF-8,
+	 * neither a leading byte nor an ASCII byte can occur as a continuation
+	 * byte, so a valid literal cannot match starting inside a character.
+	 *
+	 * Cache all rejected cases so subsequent rows go directly to the generic
+	 * matcher.  In particular, reject other multibyte encodings before scanning
+	 * the pattern.
+	 */
+	if ((pg_database_encoding_max_length() > 1 &&
+		 GetDatabaseEncoding() != PG_UTF8) ||
+		!pattern_stable ||
+		!like_bmh_pattern_is_eligible(p, plen, &literal_len))
+	{
+		state = MemoryContextAlloc(flinfo->fn_mcxt, sizeof(LikeBMHState));
+		state->mode = LIKE_BMH_GENERIC;
+		flinfo->fn_extra = state;
+		return state;
+	}
+
+	if (!OidIsValid(collation))
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INDETERMINATE_COLLATION),
+				 errmsg("could not determine which collation to use for LIKE"),
+				 errhint("Use the COLLATE clause to set the collation explicitly.")));
+	}
+
+	locale = pg_newlocale_from_collation(collation);
+	if (!locale->deterministic)
+	{
+		state = MemoryContextAlloc(flinfo->fn_mcxt, sizeof(LikeBMHState));
+		state->mode = LIKE_BMH_GENERIC;
+		flinfo->fn_extra = state;
+		return state;
+	}
+
+	search_state = MemoryContextAlloc(flinfo->fn_mcxt,
+									  sizeof(LikeBMHSearchState) + literal_len);
+	search_state->base.mode = LIKE_BMH_SEARCH;
+	search_state->collation = collation;
+	search_state->literal_len = literal_len;
+
+	for (i = 0; i < LIKE_BMH_SKIP_TABLE_SIZE; i++)
+		search_state->skip_table[i] = literal_len;
+
+	for (i = 1, j = 0; i < plen - 1; i++, j++)
+	{
+		if (p[i] == '\\')
+			i++;
+		search_state->literal[j] = p[i];
+	}
+	Assert(j == literal_len);
+
+	for (i = 0; i < literal_len - 1; i++)
+		search_state->skip_table[(unsigned char) search_state->literal[i]] =
+			literal_len - i - 1;
+
+	flinfo->fn_extra = search_state;
+	return (LikeBMHState *) search_state;
+}
+
+static int
+like_bmh_search(const char *s, int slen, const LikeBMHSearchState *state)
+{
+	const char *literal = state->literal;
+	int			literal_len = state->literal_len;
+	int			pos = literal_len - 1;
+
+	while (pos < slen)
+	{
+		int			i = literal_len - 1;
+		int			text_pos = pos;
+		unsigned char last = (unsigned char) s[pos];
+
+		while (i >= 0 && literal[i] == s[text_pos])
+		{
+			i--;
+			text_pos--;
+		}
+		if (i < 0)
+			return LIKE_TRUE;
+
+		pos += state->skip_table[last];
+	}
+
+	return LIKE_FALSE;
+}
+
+/*
+ * Use or initialize the BMH state cached in the caller's FmgrInfo.
+ * LIKE_BMH_FALLBACK tells the caller to use the generic matcher.
+ */
+int
+like_bmh_match(const char *s, int slen, const char *p, int plen,
+			   FmgrInfo *flinfo, Oid collation)
+{
+	LikeBMHState *state = flinfo->fn_extra;
+	LikeBMHSearchState *search_state;
+
+	if (state == NULL)
+		state = like_bmh_init(p, plen, flinfo, collation);
+
+	if (state->mode == LIKE_BMH_GENERIC)
+		return LIKE_BMH_FALLBACK;
+
+	search_state = (LikeBMHSearchState *) state;
+	if (unlikely(search_state->collation != collation))
+		return LIKE_BMH_FALLBACK;
+
+	return like_bmh_search(s, slen, search_state);
+}
diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build
index d793f81..56073fe 100644
--- a/src/backend/utils/adt/meson.build
+++ b/src/backend/utils/adt/meson.build
@@ -58,6 +58,7 @@ backend_sources += files(
   'jsonpath.c',
   'jsonpath_exec.c',
   'like.c',
+  'like_bmh.c',
   'like_support.c',
   'lockfuncs.c',
   'mac.c',
diff --git a/src/include/utils/like_bmh.h b/src/include/utils/like_bmh.h
new file mode 100644
index 0000000..915528d
--- /dev/null
+++ b/src/include/utils/like_bmh.h
@@ -0,0 +1,34 @@
+/*-------------------------------------------------------------------------
+ *
+ * like_bmh.h
+ *	  Runtime state for BMH-optimized LIKE searches.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/like_bmh.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef LIKE_BMH_H
+#define LIKE_BMH_H
+
+#include "fmgr.h"
+
+#define LIKE_BMH_FALLBACK	(-2)
+
+typedef enum LikeBMHMode
+{
+	LIKE_BMH_GENERIC,
+	LIKE_BMH_SEARCH,
+} LikeBMHMode;
+
+typedef struct LikeBMHState
+{
+	LikeBMHMode mode;
+} LikeBMHState;
+
+extern int	like_bmh_match(const char *s, int slen, const char *p, int plen,
+						   FmgrInfo *flinfo, Oid collation);
+
+#endif							/* LIKE_BMH_H */
diff --git a/src/test/regress/expected/collate.icu.utf8.out b/src/test/regress/expected/collate.icu.utf8.out
index fcfcc65..dcaf6b0 100644
--- a/src/test/regress/expected/collate.icu.utf8.out
+++ b/src/test/regress/expected/collate.icu.utf8.out
@@ -1441,6 +1441,25 @@ SELECT * FROM test6 WHERE b LIKE 'zyäbc' COLLATE ctest_nondet;
  2 | zyäbc
 (2 rows)
 
+-- A four-byte multibyte literal can use BMH with a deterministic collation.
+-- The nondeterministic case must preserve canonical equivalence by falling
+-- back to the collation-aware matcher.
+SELECT a, b LIKE U&'%\00E4bc%' COLLATE ctest_det AS matched
+FROM test6 ORDER BY a;
+ a | matched 
+---+---------
+ 1 | t
+ 2 | f
+(2 rows)
+
+SELECT a, b LIKE U&'%\00E4bc%' COLLATE ctest_nondet AS matched
+FROM test6 ORDER BY a;
+ a | matched 
+---+---------
+ 1 | t
+ 2 | t
+(2 rows)
+
 -- same with arrays
 CREATE TABLE test6a (a int, b text[]);
 INSERT INTO test6a VALUES (1, ARRAY[U&'\00E4bc']);
diff --git a/src/test/regress/expected/like_bmh.out b/src/test/regress/expected/like_bmh.out
new file mode 100644
index 0000000..244907a
--- /dev/null
+++ b/src/test/regress/expected/like_bmh.out
@@ -0,0 +1,246 @@
+--
+-- LIKE BMH optimization
+--
+-- Exercise the fast path for simple contains patterns ('%literal%') and the
+-- fallback cases that must preserve existing LIKE semantics.
+--
+-- Use table values so the planner cannot fold the LIKE expressions to Consts.
+CREATE TEMP TABLE like_bmh_text_data(id int, val text);
+INSERT INTO like_bmh_text_data VALUES
+	(1, ''),
+	(2, 'abc'),
+	(3, 'abcd'),
+	(4, 'xabcd'),
+	(5, 'abcdx'),
+	(6, 'xabcdx'),
+	(7, 'abxd'),
+	(8, NULL);
+SELECT id, val LIKE '%abcd%' AS matched
+FROM like_bmh_text_data
+ORDER BY id;
+ id | matched 
+----+---------
+  1 | f
+  2 | f
+  3 | t
+  4 | t
+  5 | t
+  6 | t
+  7 | f
+  8 | 
+(8 rows)
+
+-- Exercise escape removal in both eligible and short fallback literals.
+CREATE TEMP TABLE like_bmh_escape_data(id int, val text);
+INSERT INTO like_bmh_escape_data VALUES
+	(1, 'xxa%cdexx'),
+	(2, 'xxa%zzzxx'),
+	(3, 'xxa_cdexx'),
+	(4, 'xxa\cdexx'),
+	(5, 'xxa%cxx');
+SELECT id, val LIKE '%a\%cde%' AS matched
+FROM like_bmh_escape_data WHERE id IN (1, 2) ORDER BY id;
+ id | matched 
+----+---------
+  1 | t
+  2 | f
+(2 rows)
+
+SELECT id, val LIKE '%a\_cde%' AS matched
+FROM like_bmh_escape_data WHERE id = 3;
+ id | matched 
+----+---------
+  3 | t
+(1 row)
+
+SELECT id, val LIKE '%a\\cde%' AS matched
+FROM like_bmh_escape_data WHERE id = 4;
+ id | matched 
+----+---------
+  4 | t
+(1 row)
+
+SELECT id, val LIKE '%a#%cde%' ESCAPE '#' AS matched
+FROM like_bmh_escape_data WHERE id = 1;
+ id | matched 
+----+---------
+  1 | t
+(1 row)
+
+SELECT id, val LIKE '%a\%c%' AS matched
+FROM like_bmh_escape_data WHERE id = 5;
+ id | matched 
+----+---------
+  5 | t
+(1 row)
+
+-- Check the threshold and the main pattern-shape fallback cases.
+SELECT val LIKE '%abc%' AS len3_fallback,
+       val LIKE '%abcd%' AS len4_bmh
+FROM like_bmh_text_data WHERE id = 6;
+ len3_fallback | len4_bmh 
+---------------+----------
+ t             | t
+(1 row)
+
+SELECT val LIKE 'abcd' AS exact_fallback
+FROM like_bmh_text_data WHERE id = 3;
+ exact_fallback 
+----------------
+ t
+(1 row)
+
+SELECT val LIKE 'abcd%' AS prefix_fallback
+FROM like_bmh_text_data WHERE id = 5;
+ prefix_fallback 
+-----------------
+ t
+(1 row)
+
+SELECT val LIKE '%abcd' AS suffix_fallback
+FROM like_bmh_text_data WHERE id = 4;
+ suffix_fallback 
+-----------------
+ t
+(1 row)
+
+SELECT val LIKE '%%' AS empty_literal_fallback
+FROM like_bmh_text_data WHERE id = 6;
+ empty_literal_fallback 
+------------------------
+ t
+(1 row)
+
+SELECT val LIKE '%%abcd%%' AS extra_percent_fallback
+FROM like_bmh_text_data WHERE id = 6;
+ extra_percent_fallback 
+------------------------
+ t
+(1 row)
+
+SELECT val LIKE '%abc\%' AS escaped_trailing_percent_fallback
+FROM (VALUES ('abc%')) AS v(val);
+ escaped_trailing_percent_fallback 
+-----------------------------------
+ t
+(1 row)
+
+-- name input goes through the namelike entry point.
+CREATE TEMP TABLE like_bmh_name_data(id int, val name);
+INSERT INTO like_bmh_name_data VALUES
+	(1, 'xxclassxx'),
+	(2, 'xxotherxx'),
+	(3, NULL);
+SELECT id, val LIKE '%class%' AS matched
+FROM like_bmh_name_data
+ORDER BY id;
+ id | matched 
+----+---------
+  1 | t
+  2 | f
+  3 | 
+(3 rows)
+
+-- Row-varying patterns must use the generic matcher.
+CREATE TEMP TABLE like_bmh_patterns(p text);
+INSERT INTO like_bmh_patterns VALUES
+	('%abcd%'), ('%wxyz%'), ('%b_d%'), ('%b%e%');
+SELECT p, 'xxabcdxx' LIKE p AS matched
+FROM like_bmh_patterns
+ORDER BY p;
+   p    | matched 
+--------+---------
+ %abcd% | t
+ %b%e%  | f
+ %b_d%  | t
+ %wxyz% | f
+(4 rows)
+
+-- A ScalarArrayOpExpr must not reuse state across different array elements.
+SELECT val LIKE ANY (ARRAY['%abcd%', '%wxyz%']) AS text_any
+FROM (VALUES ('xxwxyzxx')) AS v(val);
+ text_any 
+----------
+ t
+(1 row)
+
+SELECT val LIKE ALL (ARRAY['%abcd%', '%wxyz%']) AS text_all
+FROM (VALUES ('xxabcdxx')) AS v(val);
+ text_all 
+----------
+ f
+(1 row)
+
+SELECT val LIKE ANY (ARRAY['%abcd%', NULL, '%wxyz%']) AS text_any_null
+FROM (VALUES ('xxwxyzxx')) AS v(val);
+ text_any_null 
+---------------
+ t
+(1 row)
+
+SELECT val::name LIKE ANY (ARRAY['%other%', '%class%']) AS name_any
+FROM (VALUES ('xxclassxx')) AS v(val);
+ name_any 
+----------
+ t
+(1 row)
+
+-- A custom plan sees a Const pattern; a generic plan sees a stable Param.
+SET plan_cache_mode = force_custom_plan;
+PREPARE like_bmh_text(text) AS
+	SELECT id, val LIKE $1 AS matched
+	FROM like_bmh_text_data WHERE id IN (6, 7) ORDER BY id;
+EXECUTE like_bmh_text('%abcd%');
+ id | matched 
+----+---------
+  6 | t
+  7 | f
+(2 rows)
+
+EXECUTE like_bmh_text('%wxyz%');
+ id | matched 
+----+---------
+  6 | f
+  7 | f
+(2 rows)
+
+DEALLOCATE like_bmh_text;
+RESET plan_cache_mode;
+SET plan_cache_mode = force_generic_plan;
+PREPARE like_bmh_text(text) AS
+	SELECT id, val LIKE $1 AS matched
+	FROM like_bmh_text_data WHERE id IN (6, 7) ORDER BY id;
+EXECUTE like_bmh_text('%abcd%');
+ id | matched 
+----+---------
+  6 | t
+  7 | f
+(2 rows)
+
+EXECUTE like_bmh_text('%wxyz%');
+ id | matched 
+----+---------
+  6 | f
+  7 | f
+(2 rows)
+
+DEALLOCATE like_bmh_text;
+PREPARE like_bmh_name(text) AS
+	SELECT id, val LIKE $1 AS matched
+	FROM like_bmh_name_data WHERE id IN (1, 2) ORDER BY id;
+EXECUTE like_bmh_name('%class%');
+ id | matched 
+----+---------
+  1 | t
+  2 | f
+(2 rows)
+
+EXECUTE like_bmh_name('%other%');
+ id | matched 
+----+---------
+  1 | f
+  2 | t
+(2 rows)
+
+DEALLOCATE like_bmh_name;
+RESET plan_cache_mode;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c..e891e42 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -22,6 +22,7 @@ test: boolean char name varchar text int2 int4 int8 oid float4 float8 bit numeri
 # multirangetypes shouldn't run concurrently with type_sanity
 # ----------
 test: strings md5 numerology point lseg line box path polygon circle date time timetz timestamp timestamptz interval inet macaddr macaddr8 multirangetypes
+test: like_bmh
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql
index ce4e2bb..420c40f 100644
--- a/src/test/regress/sql/collate.icu.utf8.sql
+++ b/src/test/regress/sql/collate.icu.utf8.sql
@@ -554,6 +554,14 @@ SELECT a, string_to_array(b COLLATE ctest_nondet, U&'\00E4b') FROM test6;
 SELECT * FROM test6 WHERE b LIKE 'zyäbc' COLLATE ctest_det;
 SELECT * FROM test6 WHERE b LIKE 'zyäbc' COLLATE ctest_nondet;
 
+-- A four-byte multibyte literal can use BMH with a deterministic collation.
+-- The nondeterministic case must preserve canonical equivalence by falling
+-- back to the collation-aware matcher.
+SELECT a, b LIKE U&'%\00E4bc%' COLLATE ctest_det AS matched
+FROM test6 ORDER BY a;
+SELECT a, b LIKE U&'%\00E4bc%' COLLATE ctest_nondet AS matched
+FROM test6 ORDER BY a;
+
 -- same with arrays
 CREATE TABLE test6a (a int, b text[]);
 INSERT INTO test6a VALUES (1, ARRAY[U&'\00E4bc']);
diff --git a/src/test/regress/sql/like_bmh.sql b/src/test/regress/sql/like_bmh.sql
new file mode 100644
index 0000000..ad10c1a
--- /dev/null
+++ b/src/test/regress/sql/like_bmh.sql
@@ -0,0 +1,113 @@
+--
+-- LIKE BMH optimization
+--
+-- Exercise the fast path for simple contains patterns ('%literal%') and the
+-- fallback cases that must preserve existing LIKE semantics.
+--
+
+-- Use table values so the planner cannot fold the LIKE expressions to Consts.
+CREATE TEMP TABLE like_bmh_text_data(id int, val text);
+INSERT INTO like_bmh_text_data VALUES
+	(1, ''),
+	(2, 'abc'),
+	(3, 'abcd'),
+	(4, 'xabcd'),
+	(5, 'abcdx'),
+	(6, 'xabcdx'),
+	(7, 'abxd'),
+	(8, NULL);
+
+SELECT id, val LIKE '%abcd%' AS matched
+FROM like_bmh_text_data
+ORDER BY id;
+
+-- Exercise escape removal in both eligible and short fallback literals.
+CREATE TEMP TABLE like_bmh_escape_data(id int, val text);
+INSERT INTO like_bmh_escape_data VALUES
+	(1, 'xxa%cdexx'),
+	(2, 'xxa%zzzxx'),
+	(3, 'xxa_cdexx'),
+	(4, 'xxa\cdexx'),
+	(5, 'xxa%cxx');
+
+SELECT id, val LIKE '%a\%cde%' AS matched
+FROM like_bmh_escape_data WHERE id IN (1, 2) ORDER BY id;
+SELECT id, val LIKE '%a\_cde%' AS matched
+FROM like_bmh_escape_data WHERE id = 3;
+SELECT id, val LIKE '%a\\cde%' AS matched
+FROM like_bmh_escape_data WHERE id = 4;
+SELECT id, val LIKE '%a#%cde%' ESCAPE '#' AS matched
+FROM like_bmh_escape_data WHERE id = 1;
+SELECT id, val LIKE '%a\%c%' AS matched
+FROM like_bmh_escape_data WHERE id = 5;
+
+-- Check the threshold and the main pattern-shape fallback cases.
+SELECT val LIKE '%abc%' AS len3_fallback,
+       val LIKE '%abcd%' AS len4_bmh
+FROM like_bmh_text_data WHERE id = 6;
+SELECT val LIKE 'abcd' AS exact_fallback
+FROM like_bmh_text_data WHERE id = 3;
+SELECT val LIKE 'abcd%' AS prefix_fallback
+FROM like_bmh_text_data WHERE id = 5;
+SELECT val LIKE '%abcd' AS suffix_fallback
+FROM like_bmh_text_data WHERE id = 4;
+SELECT val LIKE '%%' AS empty_literal_fallback
+FROM like_bmh_text_data WHERE id = 6;
+SELECT val LIKE '%%abcd%%' AS extra_percent_fallback
+FROM like_bmh_text_data WHERE id = 6;
+SELECT val LIKE '%abc\%' AS escaped_trailing_percent_fallback
+FROM (VALUES ('abc%')) AS v(val);
+
+-- name input goes through the namelike entry point.
+CREATE TEMP TABLE like_bmh_name_data(id int, val name);
+INSERT INTO like_bmh_name_data VALUES
+	(1, 'xxclassxx'),
+	(2, 'xxotherxx'),
+	(3, NULL);
+SELECT id, val LIKE '%class%' AS matched
+FROM like_bmh_name_data
+ORDER BY id;
+
+-- Row-varying patterns must use the generic matcher.
+CREATE TEMP TABLE like_bmh_patterns(p text);
+INSERT INTO like_bmh_patterns VALUES
+	('%abcd%'), ('%wxyz%'), ('%b_d%'), ('%b%e%');
+SELECT p, 'xxabcdxx' LIKE p AS matched
+FROM like_bmh_patterns
+ORDER BY p;
+
+-- A ScalarArrayOpExpr must not reuse state across different array elements.
+SELECT val LIKE ANY (ARRAY['%abcd%', '%wxyz%']) AS text_any
+FROM (VALUES ('xxwxyzxx')) AS v(val);
+SELECT val LIKE ALL (ARRAY['%abcd%', '%wxyz%']) AS text_all
+FROM (VALUES ('xxabcdxx')) AS v(val);
+SELECT val LIKE ANY (ARRAY['%abcd%', NULL, '%wxyz%']) AS text_any_null
+FROM (VALUES ('xxwxyzxx')) AS v(val);
+SELECT val::name LIKE ANY (ARRAY['%other%', '%class%']) AS name_any
+FROM (VALUES ('xxclassxx')) AS v(val);
+
+-- A custom plan sees a Const pattern; a generic plan sees a stable Param.
+SET plan_cache_mode = force_custom_plan;
+PREPARE like_bmh_text(text) AS
+	SELECT id, val LIKE $1 AS matched
+	FROM like_bmh_text_data WHERE id IN (6, 7) ORDER BY id;
+EXECUTE like_bmh_text('%abcd%');
+EXECUTE like_bmh_text('%wxyz%');
+DEALLOCATE like_bmh_text;
+RESET plan_cache_mode;
+
+SET plan_cache_mode = force_generic_plan;
+PREPARE like_bmh_text(text) AS
+	SELECT id, val LIKE $1 AS matched
+	FROM like_bmh_text_data WHERE id IN (6, 7) ORDER BY id;
+EXECUTE like_bmh_text('%abcd%');
+EXECUTE like_bmh_text('%wxyz%');
+DEALLOCATE like_bmh_text;
+
+PREPARE like_bmh_name(text) AS
+	SELECT id, val LIKE $1 AS matched
+	FROM like_bmh_name_data WHERE id IN (1, 2) ORDER BY id;
+EXECUTE like_bmh_name('%class%');
+EXECUTE like_bmh_name('%other%');
+DEALLOCATE like_bmh_name;
+RESET plan_cache_mode;
