test: add raw-string LIKE escape-model coverage (#50843)

## Summary

This PR now contains **only supplementary test coverage** for the `LIKE`
escape
model — no production code.

The production fix it originally carried (`scanLikePattern` in
`internal/parser/planparserv2/pattern_match.go`, aligning the Go
optimizer with
the C++ canonical matcher in `RegexQuery.cpp`) **already landed in
master via
#50845**, which was stacked on this PR. After rebasing onto master that
production commit is dropped as already-upstream, so the diff here
reduces to
tests only.

`git diff master` touches exactly three files, all tests:

| file | layer |
|---|---|
| `internal/parser/planparserv2/plan_parser_v2_test.go` | Go optimizer
(parser-level) |
| `internal/core/src/common/RegexQueryUtilTest.cpp` | C++ executor /
canonical matcher |
| `tests/python_client/.../test_milvus_client_scalar_filtering.py` |
Python e2e |

## What the tests add

Now that raw string literals `r"..."` exist (#50845), the escape tests
read with
a **single** backslash instead of the doubled/quadrupled backslashes the
normal
string-literal layer forced — exercising the raw-string path end to end.

**Go — `TestExpr_RawString_LikeEscapeModel`** drives the optimizer
end-to-end
through `r"..."` LIKE patterns and asserts the lowered op + literal
operand:
- escaped wildcard → literal byte (`r"a\%bc"` → `Equal "a%bc"`);
- a literal `%` coexisting with an **unescaped** `%` boundary
(`r"abc\%def%"` → `PrefixMatch "abc%def"`, `r"%abc\%def%"` → `InnerMatch
"abc%def"`);
- `\\` collapsing to one literal `\`;
- a dangling trailing backslash — un-expressible as a raw string (parse
error),
  and `OpType_Match` fallback via a normal string.

**C++ — `RegexQueryUtilTest.cpp`** keeps the canonical-matcher escape
tests
(literal `%`/`_` matched verbatim, multi-char decoys, long-span
positives). Raw
strings are a Go-parser-only feature, so the C++ executor tests use
plan-level
inputs.

**Python e2e** — the escaped-wildcard `LIKE` expressions and
`test_like_escaped_wildcard_is_literal` now use `r"..."` (one
backslash); the
eval oracle gains a raw-`LIKE` branch that re-quotes the verbatim raw
content via
`repr()` so it stays in lock-step with the server. A raw-string
dangling-backslash case (rejected at lex time) is added; the
matcher-level
dangling case stays a normal string since a raw string cannot end in an
odd
number of backslashes.

## Verification

```
go test -tags dynamic,test -gcflags="all=-N -l" -count=1 ./internal/parser/planparserv2/
```
planparserv2 package passes; `gofmt` clean; Python `ruff check` / `ruff
format`
clean; the oracle's raw-`LIKE` branch verified to reproduce the
documented
expected result sets.

issue: #43864

---------

Signed-off-by: xiaofanluan <xf@hjjaq.com>
Signed-off-by: xiaofanluan <xiaofan.luan@zilliz.com>
Co-authored-by: xiaofanluan <xf@hjjaq.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
James
2026-07-02 22:52:43 -07:00
committed by GitHub
co-authored by xiaofanluan Claude Opus 4.8
parent 8764b71bba
commit 3353b53de7
3 changed files with 813 additions and 293 deletions
@@ -2721,6 +2721,56 @@ TEST(InvalidUTF8ConsistencyTest, InvalidContinuationByte) {
<< "RE2/Like must agree on invalid continuation byte";
}
// ============== Literal-wildcard operand Match Tests ==============
// The LIKE optimizer (Go side) unescapes "\%" / "\_" into LITERAL bytes when it
// lowers a pattern to Equal/Prefix/Postfix/InnerMatch, so the operand handed to
// these executors can itself CONTAIN a '%' or '_'. They must be compared
// verbatim as bytes — never re-interpreted as wildcards. (issue #43864)
TEST(LiteralWildcardMatch, OperandContainsLiteralWildcardByte) {
using namespace milvus;
// InnerMatch: operand "abc%" is a literal substring needle.
EXPECT_TRUE(InnerMatch("xxabc%yy", "abc%"));
EXPECT_FALSE(InnerMatch("xxabcZyy", "abc%"))
<< "literal '%' in the operand must not act as a wildcard";
EXPECT_FALSE(InnerMatch("abcdef", "abc%"));
// PrefixMatch: operand "abc%" is a literal prefix.
EXPECT_TRUE(PrefixMatch("abc%def", "abc%"));
EXPECT_FALSE(PrefixMatch("abcXdef", "abc%"));
// PostfixMatch: operand "abc%" is a literal suffix.
EXPECT_TRUE(PostfixMatch("xyzabc%", "abc%"));
EXPECT_FALSE(PostfixMatch("xyzabcX", "abc%"));
// A literal underscore must not act as the single-char wildcard either.
EXPECT_TRUE(InnerMatch("aa_bb", "a_b"));
EXPECT_FALSE(InnerMatch("aaXbb", "a_b"));
EXPECT_TRUE(PrefixMatch("a_bc", "a_b"));
EXPECT_FALSE(PrefixMatch("aXbc", "a_b"));
// Stronger decoys: a literal '%' must not behave like a MULTI-char wildcard
// ('.*') either. Each value would match if the operand's '%' were treated
// as a wildcard spanning several bytes, but it must stay a literal byte.
EXPECT_FALSE(InnerMatch("xxabcZZZZyy", "abc%"));
EXPECT_FALSE(PrefixMatch("abcXYZdef", "abc%"));
EXPECT_FALSE(PostfixMatch("xyzabcXYZ", "abc%"));
// The UNescaped '%' boundary — realized as the optimized Prefix/Postfix/
// Inner op — DOES match an arbitrarily long span on the wildcard side.
// (operand has no literal wildcard; long surroundings prove '%' is '.*').
const std::string longx(1000, 'x');
const std::string longy(1000, 'y');
EXPECT_TRUE(InnerMatch(longx + "abc" + longy, "abc")); // from "%abc%"
EXPECT_TRUE(PrefixMatch("abc" + longy, "abc")); // from "abc%"
EXPECT_TRUE(PostfixMatch(longx + "abc", "abc")); // from "%abc"
// Combined: a literal '%' in the operand is matched verbatim while the
// wildcard boundary still spans the long surrounding text.
EXPECT_TRUE(InnerMatch(longx + "abc%" + longy, "abc%"));
EXPECT_FALSE(InnerMatch(longx + "abcZ" + longy, "abc%"));
}
// ============== NUL-aware Prefix/Postfix/Inner Match Tests ==============
// Verify that PrefixMatch, PostfixMatch, and InnerMatch use length-aware
// byte comparison (memcmp) instead of C-string semantics (strncmp),
@@ -414,6 +414,60 @@ func TestExpr_RawString(t *testing.T) {
assert.Equal(t, []string{`\u0041`}, jSq.GetVectorAnns().GetPredicates().GetUnaryRangeExpr().GetColumnInfo().GetNestedPath())
}
// TestExpr_RawString_LikeEscapeModel exercises the LIKE escape model (issue
// #43864) end-to-end through the raw-string literal r"...". Because a raw string
// drops the string-literal Unquote layer, a backslash reaches the LIKE pattern
// layer verbatim, so these read with the same single backslash the C++ canonical
// matcher (RegexQuery.cpp) uses — no doubled/quadrupled backslashes. Each case
// asserts the optimized op and the literal operand the executor must match
// verbatim.
func TestExpr_RawString_LikeEscapeModel(t *testing.T) {
schema := newTestSchema(true)
helper, err := typeutil.CreateSchemaHelper(schema)
assert.NoError(t, err)
check := func(expr string, wantOp planpb.OpType, wantVal string) {
plan, err := CreateSearchPlan(helper, expr, "FloatVectorField", &planpb.QueryInfo{}, nil, nil)
assert.NoError(t, err, expr)
assert.NotNil(t, plan, expr)
ur := plan.GetVectorAnns().GetPredicates().GetUnaryRangeExpr()
assert.Equal(t, wantOp, ur.GetOp(), expr)
assert.Equal(t, wantVal, ur.GetValue().GetStringVal(), expr)
}
// An escaped wildcard is a LITERAL byte in the operand: the optimized op
// carries a literal '%'/'_', which the C++ side matches verbatim and must NOT
// re-interpret as a wildcard.
check(`A like r"a\%bc"`, planpb.OpType_Equal, `a%bc`)
check(`A like r"a\_bc"`, planpb.OpType_Equal, `a_bc`)
check(`A like r"abc\%%"`, planpb.OpType_PrefixMatch, `abc%`)
check(`A like r"%abc\%"`, planpb.OpType_PostfixMatch, `abc%`)
check(`A like r"%abc\%%"`, planpb.OpType_InnerMatch, `abc%`)
// A literal '\%' and an UNescaped '%' coexist: the literal lands in the
// operand verbatim, while the bare '%' is the prefix/postfix/inner boundary
// the C++ matcher expands to an ANY-length span.
check(`A like r"abc\%def%"`, planpb.OpType_PrefixMatch, `abc%def`)
check(`A like r"%abc\%def"`, planpb.OpType_PostfixMatch, `abc%def`)
check(`A like r"%abc\%def%"`, planpb.OpType_InnerMatch, `abc%def`)
// A backslash escapes ANY next byte, not only wildcards: r"\a" -> literal "a".
check(`A like r"\a"`, planpb.OpType_Equal, `a`)
// A raw "\\" collapses to one literal backslash at the pattern layer.
check(`A like r"a\\b"`, planpb.OpType_Equal, `a\b`)
check(`A like r"%a\\b%"`, planpb.OpType_InnerMatch, `a\b`)
// A dangling trailing backslash cannot be written as a raw string at all — a
// raw string may not end in an odd number of backslashes (it would not
// terminate). So the unterminated raw form is a parse error, and the literal
// trailing-backslash pattern can only be expressed via a normal string, where
// it is not optimizable and falls back to OpType_Match (the C++ matcher then
// raises ExprInvalid at execution).
assertInvalidExpr(t, helper, `A like r"abc\"`)
check(`A like "abc\\"`, planpb.OpType_Match, `abc\`)
}
func TestExpr_RegexMatch(t *testing.T) {
schema := newTestSchema(true)
helper, err := typeutil.CreateSchemaHelper(schema)
File diff suppressed because it is too large Load Diff