From 350c660aaf4f2a5d367cca90b7dc85ef62720e41 Mon Sep 17 00:00:00 2001 From: Atsushi Ogawa Date: Thu, 16 Jul 2026 23:38:52 +0900 Subject: [PATCH v3] 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 prepared search state or a generic fallback marker in FmgrInfo.fn_extra. Const patterns use a comparison-free cached state. All other eligible patterns, including Params, retain the verbatim pattern and revalidate it on each call, because a PL/pgSQL simple expression reuses its ExprState while a variable used as the pattern changes. If the value ever changes, permanently select the existing generic matcher instead of rebuilding the skip table. This also handles ScalarArrayOpExpr calls safely. Restrict the byte-oriented search to deterministic collations and single-byte or UTF-8 database encodings. 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 | 303 ++++++++++++++++++ src/backend/utils/adt/meson.build | 1 + src/include/utils/like_bmh.h | 35 ++ .../regress/expected/collate.icu.utf8.out | 19 ++ src/test/regress/expected/like_bmh.out | 293 +++++++++++++++++ src/test/regress/parallel_schedule | 1 + src/test/regress/sql/collate.icu.utf8.sql | 8 + src/test/regress/sql/like_bmh.sql | 142 ++++++++ 10 files changed, 825 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 0c7621957c1..f2cc5b151a2 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 64147bc5b5c..66ee5638f6a 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 00000000000..0f9f812b40b --- /dev/null +++ b/src/backend/utils/adt/like_bmh.c @@ -0,0 +1,303 @@ +/*------------------------------------------------------------------------- + * + * like_bmh.c + * Boyer-Moore-Horspool search for LIKE patterns containing one literal. + * + * varlena.c already uses Boyer-Moore-Horspool for the position/replace + * built-ins, but that code searches a single (haystack, needle) pair with an + * adaptively sized skip table. This file keeps its own small implementation + * because LIKE must interpret its internal backslash escapes while extracting + * the literal and caches the compiled search state in FmgrInfo across rows, + * so sharing the varlena.c machinery did not look worthwhile. + * + * 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; + +typedef struct LikeBMHRevalidateState +{ + LikeBMHState base; + Oid collation; + int pattern_len; + int literal_len; + int skip_table[LIKE_BMH_SKIP_TABLE_SIZE]; + char buf[FLEXIBLE_ARRAY_MEMBER]; +} LikeBMHRevalidateState; + +/* + * Check whether a pattern in LIKE's internal backslash-escape form is a + * single literal surrounded by '%' wildcards. Explicit ESCAPE characters + * have already been converted to backslashes by like_escape(). + * + * Report the literal length with escapes skipped. The escapes are removed + * later, when the search state is built. + */ +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] == '\\') + { + /* + * A backslash must escape a real literal byte, not the trailing + * '%'. Reject patterns such as '%foo\%', where the escape would + * consume the closing wildcard. + */ + if (i + 1 >= plen - 1) + return false; + i++; + } + (*literal_len)++; + } + + return *literal_len >= LIKE_BMH_MIN_LITERAL_LEN; +} + +/* + * A Param can look stable, but PL/pgSQL simple expressions reuse one + * ExprState while the variable changes. Cache only Const patterns without + * revalidation; other patterns use the revalidation path. A + * ScalarArrayOpExpr is also rejected: its array argument can be a Const while + * the element passed to this function changes between calls. + */ +static bool +like_bmh_pattern_arg_is_const(FmgrInfo *flinfo) +{ + List *args; + Node *expr; + Node *arg; + + if (!flinfo->fn_expr) + return false; + + expr = flinfo->fn_expr; + if (IsA(expr, FuncExpr)) + args = ((FuncExpr *) expr)->args; + else if (IsA(expr, OpExpr)) + args = ((OpExpr *) expr)->args; + else if (IsA(expr, DistinctExpr)) + args = ((DistinctExpr *) expr)->args; + else if (IsA(expr, NullIfExpr)) + args = ((NullIfExpr *) expr)->args; + else + return false; + + if (list_length(args) < 2) + return false; + + arg = (Node *) list_nth(args, 1); + return IsA(arg, Const); +} + +/* Keep one-time setup out of the stable-pattern matching hot path. */ +static pg_noinline LikeBMHState * +like_bmh_init(const char *p, int plen, FmgrInfo *flinfo, Oid collation) +{ + LikeBMHState *state; + LikeBMHSearchState *search_state; + LikeBMHRevalidateState *revalidate_state; + int *skip_table; + char *literal; + pg_locale_t locale; + bool pattern_stable; + int literal_len; + int i; + int j; + + pattern_stable = like_bmh_pattern_arg_is_const(flinfo); + + /* + * 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. An unresolved collation is left for GenericMatchText to + * report, so that the fallback path raises the usual error. + */ + if ((pg_database_encoding_max_length() > 1 && + GetDatabaseEncoding() != PG_UTF8) || + !OidIsValid(collation) || + !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; + } + + 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; + } + + if (pattern_stable) + { + 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; + state = (LikeBMHState *) search_state; + skip_table = search_state->skip_table; + literal = search_state->literal; + } + else + { + revalidate_state = MemoryContextAlloc(flinfo->fn_mcxt, + sizeof(LikeBMHRevalidateState) + + literal_len + plen); + revalidate_state->base.mode = LIKE_BMH_SEARCH_REVALIDATE; + revalidate_state->collation = collation; + revalidate_state->pattern_len = plen; + revalidate_state->literal_len = literal_len; + state = (LikeBMHState *) revalidate_state; + skip_table = revalidate_state->skip_table; + literal = revalidate_state->buf; + } + + for (i = 0; i < LIKE_BMH_SKIP_TABLE_SIZE; i++) + skip_table[i] = literal_len; + + for (i = 1, j = 0; i < plen - 1; i++, j++) + { + if (p[i] == '\\') + i++; + literal[j] = p[i]; + } + Assert(j == literal_len); + if (!pattern_stable) + memcpy(literal + literal_len, p, plen); + + for (i = 0; i < literal_len - 1; i++) + skip_table[(unsigned char) literal[i]] = + literal_len - i - 1; + + flinfo->fn_extra = state; + return state; +} + +static int +like_bmh_search(const char *s, int slen, const char *literal, int literal_len, + const int *skip_table) +{ + 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 += skip_table[last]; + } + + return LIKE_FALSE; +} + +/* Keep variable-pattern revalidation out of the stable-pattern hot path. */ +static pg_noinline int +like_bmh_match_revalidate(const char *s, int slen, const char *p, int plen, + LikeBMHState *state, Oid collation) +{ + LikeBMHRevalidateState *revalidate_state; + + if (state->mode == LIKE_BMH_GENERIC) + return LIKE_BMH_FALLBACK; + + Assert(state->mode == LIKE_BMH_SEARCH_REVALIDATE); + revalidate_state = (LikeBMHRevalidateState *) state; + if (unlikely(revalidate_state->collation != collation)) + return LIKE_BMH_FALLBACK; + + if (revalidate_state->pattern_len != plen || + memcmp(revalidate_state->buf + revalidate_state->literal_len, + p, plen) != 0) + { + /* + * A changing pattern would make rebuilding the skip table every row + * expensive. Once variation is observed, use the generic matcher for + * the rest of this expression's execution. + */ + state->mode = LIKE_BMH_GENERIC; + return LIKE_BMH_FALLBACK; + } + + return like_bmh_search(s, slen, revalidate_state->buf, + revalidate_state->literal_len, + revalidate_state->skip_table); +} + +/* + * 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 (unlikely(state->mode != LIKE_BMH_SEARCH)) + return like_bmh_match_revalidate(s, slen, p, plen, state, collation); + + search_state = (LikeBMHSearchState *) state; + if (unlikely(search_state->collation != collation)) + return LIKE_BMH_FALLBACK; + + return like_bmh_search(s, slen, search_state->literal, + search_state->literal_len, search_state->skip_table); +} diff --git a/src/backend/utils/adt/meson.build b/src/backend/utils/adt/meson.build index d793f8145f6..56073fe6a62 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 00000000000..66fdca0a7bf --- /dev/null +++ b/src/include/utils/like_bmh.h @@ -0,0 +1,35 @@ +/*------------------------------------------------------------------------- + * + * 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, + LIKE_BMH_SEARCH_REVALIDATE, +} 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 fcfcc658bea..dcaf6b075ac 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 | zyä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 00000000000..e8007048cae --- /dev/null +++ b/src/test/regress/expected/like_bmh.out @@ -0,0 +1,293 @@ +-- +-- 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 COLLATE "C"; + 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; +CREATE FUNCTION like_bmh_plpgsql_like(s text, p text) RETURNS boolean +LANGUAGE plpgsql AS $$ +BEGIN + RETURN s LIKE p; +END +$$; +SELECT s, p, like_bmh_plpgsql_like(s, p) +FROM (VALUES ('xxabcdxx', '%abcd%'), + ('xxabcdxx', '%wxyz%'), + ('xxwxyzxx', '%wxyz%')) AS v(s, p); + s | p | like_bmh_plpgsql_like +----------+--------+----------------------- + xxabcdxx | %abcd% | t + xxabcdxx | %wxyz% | f + xxwxyzxx | %wxyz% | t +(3 rows) + +SELECT like_bmh_plpgsql_like(s, p) +FROM (VALUES ('xxabcdxx', '%abcd%'), + ('xabcdx', '%abcd%'), + ('abcd', '%abcd%')) AS v(s, p); + like_bmh_plpgsql_like +----------------------- + t + t + t +(3 rows) + +CREATE FUNCTION like_bmh_plpgsql_like_name(s name, p name) RETURNS boolean +LANGUAGE plpgsql AS $$ +BEGIN + RETURN s LIKE p; +END +$$; +SELECT s, p, like_bmh_plpgsql_like_name(s, p) +FROM (VALUES ('xxclassxx'::name, '%class%'::name), + ('xxclassxx'::name, '%other%'::name), + ('xxotherxx'::name, '%other%'::name)) AS v(s, p); + s | p | like_bmh_plpgsql_like_name +-----------+---------+---------------------------- + xxclassxx | %class% | t + xxclassxx | %other% | f + xxotherxx | %other% | t +(3 rows) + +DROP FUNCTION like_bmh_plpgsql_like(text, text); +DROP FUNCTION like_bmh_plpgsql_like_name(name, name); diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 8fa0a6c47fb..e891e424089 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 ce4e2bb3ffd..420c40f38a4 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 00000000000..ccd2d432175 --- /dev/null +++ b/src/test/regress/sql/like_bmh.sql @@ -0,0 +1,142 @@ +-- +-- 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 COLLATE "C"; + +-- 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; + +CREATE FUNCTION like_bmh_plpgsql_like(s text, p text) RETURNS boolean +LANGUAGE plpgsql AS $$ +BEGIN + RETURN s LIKE p; +END +$$; +SELECT s, p, like_bmh_plpgsql_like(s, p) +FROM (VALUES ('xxabcdxx', '%abcd%'), + ('xxabcdxx', '%wxyz%'), + ('xxwxyzxx', '%wxyz%')) AS v(s, p); +SELECT like_bmh_plpgsql_like(s, p) +FROM (VALUES ('xxabcdxx', '%abcd%'), + ('xabcdx', '%abcd%'), + ('abcd', '%abcd%')) AS v(s, p); + +CREATE FUNCTION like_bmh_plpgsql_like_name(s name, p name) RETURNS boolean +LANGUAGE plpgsql AS $$ +BEGIN + RETURN s LIKE p; +END +$$; +SELECT s, p, like_bmh_plpgsql_like_name(s, p) +FROM (VALUES ('xxclassxx'::name, '%class%'::name), + ('xxclassxx'::name, '%other%'::name), + ('xxotherxx'::name, '%other%'::name)) AS v(s, p); + +DROP FUNCTION like_bmh_plpgsql_like(text, text); +DROP FUNCTION like_bmh_plpgsql_like_name(name, name); -- 2.34.1