diff --git a/src/backend/utils/adt/like.c b/src/backend/utils/adt/like.c index 66ee5638f6a..5fc823b8fbc 100644 --- a/src/backend/utils/adt/like.c +++ b/src/backend/utils/adt/like.c @@ -168,13 +168,21 @@ LikeMatchText(const char *s, int slen, const char *p, int plen, { LikeBMHState *state = flinfo->fn_extra; int result; + int searched; 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); + result = like_bmh_match(s, slen, p, plen, flinfo, collation, &searched); if (result == LIKE_BMH_FALLBACK) - return GenericMatchText(s, slen, p, plen, collation); + { + /* in UTF-8, resume at a character boundary */ + if (GetDatabaseEncoding() == PG_UTF8) + while (searched > 0 && (s[searched] & 0xC0) == 0x80) + searched--; + return GenericMatchText(s + searched, slen - searched, p, plen, + collation); + } return result; } diff --git a/src/backend/utils/adt/like_bmh.c b/src/backend/utils/adt/like_bmh.c index 0f9f812b40b..d35c4371039 100644 --- a/src/backend/utils/adt/like_bmh.c +++ b/src/backend/utils/adt/like_bmh.c @@ -218,26 +218,45 @@ like_bmh_init(const char *p, int plen, FmgrInfo *flinfo, Oid collation) return state; } +/* + * Horspool skips only one byte per alignment when the haystack consists of + * few distinct bytes, and a literal that mismatches near its start then costs + * literal_len comparisons per alignment. Bound the bytes compared beyond the + * guard byte to slen / 2; past that, report how much text was fully rejected + * so the caller can let the generic matcher finish the row. + */ static int like_bmh_search(const char *s, int slen, const char *literal, int literal_len, - const int *skip_table) + const int *skip_table, int *searched) { int pos = literal_len - 1; + int budget = slen / 2; 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]) + if (literal[literal_len - 1] == s[pos]) { - i--; - text_pos--; + int i = literal_len - 2; + int text_pos = pos - 1; + + while (i >= 0 && literal[i] == s[text_pos]) + { + i--; + text_pos--; + } + if (i < 0) + return LIKE_TRUE; + + budget -= literal_len - 1 - i; + if (budget < 0) + { + /* every alignment ending at or before pos is rejected */ + *searched = pos - literal_len + 2; + return LIKE_BMH_FALLBACK; + } } - if (i < 0) - return LIKE_TRUE; - pos += skip_table[last]; } @@ -247,7 +266,8 @@ like_bmh_search(const char *s, int slen, const char *literal, int literal_len, /* 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) + LikeBMHState *state, Oid collation, + int *searched) { LikeBMHRevalidateState *revalidate_state; @@ -274,7 +294,7 @@ like_bmh_match_revalidate(const char *s, int slen, const char *p, int plen, return like_bmh_search(s, slen, revalidate_state->buf, revalidate_state->literal_len, - revalidate_state->skip_table); + revalidate_state->skip_table, searched); } /* @@ -283,21 +303,25 @@ like_bmh_match_revalidate(const char *s, int slen, const char *p, int plen, */ int like_bmh_match(const char *s, int slen, const char *p, int plen, - FmgrInfo *flinfo, Oid collation) + FmgrInfo *flinfo, Oid collation, int *searched) { LikeBMHState *state = flinfo->fn_extra; LikeBMHSearchState *search_state; + *searched = 0; + 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); + return like_bmh_match_revalidate(s, slen, p, plen, state, collation, + searched); 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); + search_state->literal_len, search_state->skip_table, + searched); } diff --git a/src/include/utils/like_bmh.h b/src/include/utils/like_bmh.h index 66fdca0a7bf..a3b312273d3 100644 --- a/src/include/utils/like_bmh.h +++ b/src/include/utils/like_bmh.h @@ -29,7 +29,11 @@ typedef struct LikeBMHState LikeBMHMode mode; } LikeBMHState; +/* + * If the result is LIKE_BMH_FALLBACK, *searched is the number of leading + * bytes that were fully rejected by the BMH search. + */ extern int like_bmh_match(const char *s, int slen, const char *p, int plen, - FmgrInfo *flinfo, Oid collation); + FmgrInfo *flinfo, Oid collation, int *searched); #endif /* LIKE_BMH_H */ diff --git a/src/test/regress/expected/like_bmh.out b/src/test/regress/expected/like_bmh.out index e8007048cae..54e6c222311 100644 --- a/src/test/regress/expected/like_bmh.out +++ b/src/test/regress/expected/like_bmh.out @@ -289,5 +289,43 @@ FROM (VALUES ('xxclassxx'::name, '%class%'::name), xxotherxx | %other% | t (3 rows) +-- Budget fallback: repetitive text, with both miss and hit outcomes. +SELECT repeat('a', 1024) LIKE '%~aaaaaaaaaaaaaaa%' AS f; + f +--- + f +(1 row) + +SELECT repeat('a', 1024) || '~aaaaaaaaaaaaaaa' LIKE '%~aaaaaaaaaaaaaaa%' AS t; + t +--- + t +(1 row) + +SELECT repeat('ab', 512) || 'xab' LIKE '%xabab%' AS f; + f +--- + f +(1 row) + +SELECT repeat('ab', 512) || 'xabab' LIKE '%xabab%' AS t; + t +--- + t +(1 row) + +-- UTF-8: resume position inside multibyte text. +SELECT repeat('あ', 512) || 'xあ' LIKE '%xあい%' AS f; + f +--- + f +(1 row) + +SELECT repeat('あ', 512) || 'xあい' LIKE '%xあい%' AS t; + t +--- + t +(1 row) + DROP FUNCTION like_bmh_plpgsql_like(text, text); DROP FUNCTION like_bmh_plpgsql_like_name(name, name); diff --git a/src/test/regress/sql/like_bmh.sql b/src/test/regress/sql/like_bmh.sql index ccd2d432175..696ea305dfb 100644 --- a/src/test/regress/sql/like_bmh.sql +++ b/src/test/regress/sql/like_bmh.sql @@ -138,5 +138,14 @@ FROM (VALUES ('xxclassxx'::name, '%class%'::name), ('xxclassxx'::name, '%other%'::name), ('xxotherxx'::name, '%other%'::name)) AS v(s, p); +-- Budget fallback: repetitive text, with both miss and hit outcomes. +SELECT repeat('a', 1024) LIKE '%~aaaaaaaaaaaaaaa%' AS f; +SELECT repeat('a', 1024) || '~aaaaaaaaaaaaaaa' LIKE '%~aaaaaaaaaaaaaaa%' AS t; +SELECT repeat('ab', 512) || 'xab' LIKE '%xabab%' AS f; +SELECT repeat('ab', 512) || 'xabab' LIKE '%xabab%' AS t; +-- UTF-8: resume position inside multibyte text. +SELECT repeat('あ', 512) || 'xあ' LIKE '%xあい%' AS f; +SELECT repeat('あ', 512) || 'xあい' LIKE '%xあい%' AS t; + DROP FUNCTION like_bmh_plpgsql_like(text, text); DROP FUNCTION like_bmh_plpgsql_like_name(name, name);