Skip to content

Fix PPL search command ignoring wildcards and over-matching quoted values (#5682) - #5697

Open
penghuo wants to merge 3 commits into
opensearch-project:mainfrom
penghuo:bugFix/5682
Open

Fix PPL search command ignoring wildcards and over-matching quoted values (#5682)#5697
penghuo wants to merge 3 commits into
opensearch-project:mainfrom
penghuo:bugFix/5682

Conversation

@penghuo

@penghuo penghuo commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Description

Two ways the search command could return the wrong documents, both on the Calcite path.

1. Wildcards were silently ignored when the value contained a space (#5682)

search source=logs name="foo bar*"

On a keyword field this returned nothing, instead of matching foo bar and foo barbaz. Any value containing whitespace was treated as a phrase, so the emitted filter was name:"foo bar*" — and inside a Lucene phrase * is an ordinary character, so the query went looking for documents with a literal asterisk in them.

2. On text fields, a quoted value could match far too much

search source=logs body="foo=bar"

This matched any document containing just foo, or just bar. Reported from a real log search, where body="isTombstone==false" came back with lines whose only relevant content was "endOfBatch":false.

The cause is shared with the first bug. Whether to emit a phrase was decided by SearchLiteral.isPhrase, set at parse time as value.contains(" ") — a test for whitespace standing in for the real question: will this field's analyzer split the value into multiple tokens? Whitespace is a poor proxy for that. foo=bar has none, yet the standard analyzer splits it into [foo, bar].

So the value went out unquoted, query_string kept it as a single field-scoped term, the analyzer split it, and default_operator=OR joined the halves.

How emission is decided now

The parser keeps whether the user quoted the value, and emission is selected from the field's index mapping — the thing that actually determines whether a value gets analyzed. The mapping is read from AbstractOpenSearchTable.getFieldTypes(); the Calcite RelDataType round trip can't supply it, because OpenSearchTypeFactory collapses text to plain VARCHAR and erases the distinction. A TODO marks moving this onto a RelDataType/scan annotation once the Calcite rule pipeline has been audited.

keyword | constant_keyword, value holds an unescaped wildcard?
├── yes → unquoted, specials escaped INCLUDING whitespace
└── no  → field mapping?
          │
          ├── text | match_only_text
          │     └── quoted AND NOT (wildcard without whitespace)?
          │           ├── yes → quoted phrase
          │           └── no  → unquoted, specials escaped, * and ? preserved
          │
          └── everything else — keyword without a wildcard, date, numeric,
              ip, boolean, unresolved
                └── legacy, untouched: contains a space ? quoted phrase : unquoted

keyword with a wildcard — the keyword analyzer is a no-op, so the value has to arrive at Lucene as one term for the pattern to apply to the whole stored value. Escaping the whitespace stops query_string splitting at the space and dropping the field binding on the tail. This is fix #1.

text — honor the user's quoting. Unquoted passes straight through, so * and ? keep working as operators. Quoted becomes a phrase, which is how a user asks for "this whole value, in order" against the analyzed tokens. This is fix #2.

There's one exception on text: a value with a wildcard and no whitespace stays unquoted. Quoting it would let the analyzer throw the wildcard away, so body="foo*" would stop matching foobar. The exception is gated on whitespace because with a space, an unquoted value splits into separate clauses and the tail loses its field binding.

everything else — left exactly as it was. Note that quoting genuinely carries no information on a keyword field: with a no-op analyzer, a quoted phrase and a bare term resolve to the same single term, because Lucene returns from createFieldQuery at numTokens == 1 before it reads the quoted flag. That's a reason not to rewrite the emission — an earlier revision of this PR did, and broke ~12 expected-plan fixtures for no functional gain.

The v2 engine is unaffected. It reaches emission through the no-arg SearchExpression.toQueryString(), which passes a null-returning resolver and lands in the legacy branch.

Measured behavior

Hit counts from CalciteSearchCommandIT, run against two indices holding identical documents — name mapped keyword in one, text (standard analyzer) in the other. 11 documents: foo, foobar, food, FOO, foo bar, foo barbaz, foo-bar, foo_bar, foo.bar, foo/bar, foo@bar.

Row PPL query Text Keyword
1.1 name=foo 7 1
1.2 name="foo" 7 1
2.1 name="foo_bar" 1 1
2.2 name="foo.bar" 1 1
2.3 name="foo-bar" 4 ← was 7 1
2.4 name="foo/bar" 4 ← was 7 1
2.5 name="foo@bar" 4 ← was 7 1
2.6 name="foo bar" 4 1
3.1 name=foo* 11 10
3.2 name="foo*" 11 10
3.3 name="foo_*" 1 1
3.4 name="foo.*" 1 1
3.5 name="foo-*" 0 1
3.6 name="foo/*" 0 1
3.7 name="foo bar*" 4 2 ← was 0 (#5682)
4.1 name="*foo" 7 1
4.2 name="*bar" 7 7
4.3 name="*foo bar" 4 1
5.1 name="f*r" 3 7
5.2 name="foo*bar" 3 7
5.3 name="foo *baz" 0 1
5.4 name="*foo bar*" 4 2
6.1 name="foo?" 1 1
6.2 name="?oo" 7 1
6.3 name="f?o" 7 1
6.4 name="foo?bar" 2 6
6.5 name="foo b?r" 0 1

Four cells move: the three Group 2 text rows stop over-matching, and 3.7 keyword is the reported bug. Every wildcard row on text is unchanged, and the keyword column is unchanged apart from 3.7.

A separate fixture — foo=bar, foo bar, foo, bar, baz — covers fix #2 with exact-row assertions rather than counts, so the regression is visible:

Query Field Result
name="foo=bar" text foo=bar, foo bar — the single-token foo and bar documents are absent; they matched before
name="foo=bar" keyword foo=bar — exact whole value
name="foo*" text 11 — the wildcard survives quoting
name=foo-bar / name="foo-bar" keyword identical results; quoting is irrelevant

Known limitation

On a text field, a wildcard combined with a character the analyzer splits on cannot match under any emission — body="foo=ba*" returns 0.

Unquoted, analyze_wildcard defaults to false, so the pattern is matched against the token dictionary, where no token contains =. Quoted, the analyzer discards the * and the residual token has to match exactly. The indexed tokens for foo=bar are [foo, bar] — the original value isn't stored anywhere the query can reach. Keyword fields handle this correctly (3 hits on the same data), because the value is matched whole.

Breaking change

Text fields only. A quoted value that the analyzer splits is now a phrase instead of an OR over its tokens, so queries relying on the wider behavior will return fewer rows.

Three examples in docs/user/ppl/cmd/search.md documented the over-matching and have been updated — one had ="cart-service" matching a service actually named cart, which the fix correctly stops.

No expected-plan fixture is modified.

Related Issues

Resolves #5682

Check List

  • New functionality includes testing.
  • New functionality has been documented.
  • New functionality has javadoc added.
  • New functionality has a user manual doc added.
  • New PPL command checklist all confirmed.
  • API changes companion pull request created.
  • Commits are signed per the DCO using --signoff or -s.
  • Public documentation issue/PR created.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.

…pensearch-project#5682)

On the Calcite path, `search source=idx name="foo bar*"` against a
keyword field returned 0 hits instead of matching the whole-value pattern
`foo bar*`. The parser marked whitespace-containing literals as phrases,
which emitted `name:"foo bar*"` — inside a Lucene phrase, `*` is a
literal character, so it looked for docs containing `*` in the stored
value and found none.

Route emission per field mapping in
SearchLiteral.toQueryString(ExprType):

- text-like (text, match_only_text) → quoted phrase (unchanged)
- non-text (keyword, etc.) with whitespace + unescaped wildcard →
  unquoted term with the space escaped, so query_string keeps the value
  as one whole-value pattern instead of splitting into two clauses
- everything else (no whitespace, or phrase without wildcard) →
  legacy branches (unquoted-with-escapes, quoted phrase)

The Calcite RelDataType round trip in CalciteRelNodeVisitor.visitSearch
collapses `text` mapping to plain VARCHAR (OpenSearchTypeFactory:208),
which erased the text/keyword distinction at the emitter. Read the
ExprType map directly from AbstractOpenSearchTable.getFieldTypes()
instead; TODO comment marks the follow-up to move this metadata onto a
RelDataType/scan annotation once the Calcite rule pipeline is audited.

Thread a `Function<String, ExprType>` resolver through the SearchExpression
hierarchy (SearchComparison, SearchIn, SearchAnd/Or/Not/Group,
SearchLiteral) so SearchLiteral can consult the resolved field's index
type at emit time.

Tests: 54 new Group1-Group6 tests in CalciteSearchCommandIT covering the
full text × keyword × wildcard-placement matrix on a shared fixture,
plus a core-level SearchLiteralTest for the emission decision table.
Verified with `./gradlew doctest -DignorePrometheus` (85 tests) and
`./gradlew -DignorePrometheus :integ-test:integTest` (30m36s, 0 failures).

Signed-off-by: Peng Huo <penghuo@gmail.com>
@penghuo penghuo added the PPL Piped processing language label Aug 12, 2026
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to af99e7a

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Handle consecutive backslashes in wildcard detection

The wildcard detection logic does not handle consecutive backslashes correctly. A
sequence like \ should be treated as an escaped backslash followed by an unescaped
wildcard, but the current implementation skips the
entirely. This could cause
incorrect query emission for edge cases with multiple backslashes.

core/src/main/java/org/opensearch/sql/ast/expression/SearchLiteral.java [124-136]

 private static boolean hasUnescapedWildcard(String s) {
   for (int i = 0; i < s.length(); i++) {
     char c = s.charAt(i);
-    if (c == '\\' && i + 1 < s.length()) {
-      i++;
+    if (c == '\\') {
+      // Count consecutive backslashes
+      int backslashCount = 0;
+      while (i < s.length() && s.charAt(i) == '\\') {
+        backslashCount++;
+        i++;
+      }
+      // If odd number of backslashes, the next char is escaped
+      if (backslashCount % 2 == 1 && i < s.length()) {
+        continue;
+      }
+      // Even backslashes: check if next char is wildcard
+      if (i < s.length() && (s.charAt(i) == '*' || s.charAt(i) == '?')) {
+        return true;
+      }
+      i--;
       continue;
     }
     if (c == '*' || c == '?') {
       return true;
     }
   }
   return false;
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a potential edge case with consecutive backslashes (e.g., \\*). However, the current implementation may be acceptable depending on the escaping semantics expected by the system. The improved logic is more robust but adds complexity. Given this is an edge case that may not occur in practice, the score reflects moderate importance.

Medium
General
Verify node type before table extraction

The code assumes context.relBuilder.peek() returns a scan node with a table, but
this may not always be true if the builder contains other node types. Add a type
check to verify the node is a TableScan before attempting to extract the table,
preventing potential ClassCastException or incorrect behavior.

core/src/main/java/org/opensearch/sql/calcite/CalciteRelNodeVisitor.java [312-320]

 java.util.Map<String, ExprType> typesByName = new java.util.HashMap<>();
 RelNode scan = context.relBuilder.peek();
-RelOptTable relOptTable = scan.getTable();
-if (relOptTable != null) {
-  AbstractOpenSearchTable osTable = relOptTable.unwrap(AbstractOpenSearchTable.class);
-  if (osTable != null) {
-    typesByName.putAll(osTable.getFieldTypes());
+if (scan instanceof TableScan) {
+  RelOptTable relOptTable = scan.getTable();
+  if (relOptTable != null) {
+    AbstractOpenSearchTable osTable = relOptTable.unwrap(AbstractOpenSearchTable.class);
+    if (osTable != null) {
+      typesByName.putAll(osTable.getFieldTypes());
+    }
   }
 }
Suggestion importance[1-10]: 6

__

Why: The suggestion adds a type check to verify scan is a TableScan before extracting the table. This is a defensive programming practice that could prevent issues if the builder contains unexpected node types. However, the context suggests the code path expects a scan node, so this may be overly cautious. The score reflects a minor improvement in robustness.

Low

Previous suggestions

Suggestions up to commit d9d6568
CategorySuggestion                                                                                                                                    Impact
General
Add null check for peek result

The code assumes context.relBuilder.peek() returns a RelNode with a table, but
doesn't verify the peek result is non-null before calling getTable(). If the
builder's stack is empty or contains a node without table metadata, this will throw
a NullPointerException.

core/src/main/java/org/opensearch/sql/calcite/CalciteRelNodeVisitor.java [312-320]

 java.util.Map<String, ExprType> typesByName = new java.util.HashMap<>();
 RelNode scan = context.relBuilder.peek();
-RelOptTable relOptTable = scan.getTable();
-if (relOptTable != null) {
-  AbstractOpenSearchTable osTable = relOptTable.unwrap(AbstractOpenSearchTable.class);
-  if (osTable != null) {
-    typesByName.putAll(osTable.getFieldTypes());
+if (scan != null) {
+  RelOptTable relOptTable = scan.getTable();
+  if (relOptTable != null) {
+    AbstractOpenSearchTable osTable = relOptTable.unwrap(AbstractOpenSearchTable.class);
+    if (osTable != null) {
+      typesByName.putAll(osTable.getFieldTypes());
+    }
   }
 }
Suggestion importance[1-10]: 7

__

Why: Valid defensive programming suggestion. While relBuilder.peek() typically returns a non-null RelNode in this context (after visiting the child relation), adding the null check prevents potential NullPointerException if the builder state is unexpected.

Medium
Detect all whitespace types consistently

The whitespace check uses contains(" ") which only detects ASCII space (U+0020).
Other whitespace characters like tabs, newlines, or non-breaking spaces will bypass
the phrase-emission logic, potentially causing incorrect query splitting when the
analyzer treats them as token separators.

core/src/main/java/org/opensearch/sql/ast/expression/SearchLiteral.java [73-80]

 if (isTextLike(indexType)) {
   ...
-  boolean wildcardTerm = hasUnescapedWildcard(str) && !str.contains(" ");
+  boolean hasWhitespace = str.chars().anyMatch(Character::isWhitespace);
+  boolean wildcardTerm = hasUnescapedWildcard(str) && !hasWhitespace;
   return userQuoted && !wildcardTerm ? quoted(str) : unquoted(str);
 }
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies that contains(" ") only checks for ASCII space. Using Character::isWhitespace would handle tabs, newlines, and other whitespace characters more comprehensively, improving robustness for edge cases where users might include non-standard whitespace in search values.

Low
Handle trailing backslash edge case

The escape-sequence handling is incomplete. When a backslash appears at the end of
the string (e.g., "foo"), the code skips incrementing i but doesn't validate that a
character follows. This could cause the trailing backslash to be treated as escaping
nothing, potentially misclassifying strings ending with * or ?.

core/src/main/java/org/opensearch/sql/ast/expression/SearchLiteral.java [120-132]

 private static boolean hasUnescapedWildcard(String s) {
   for (int i = 0; i < s.length(); i++) {
     char c = s.charAt(i);
-    if (c == '\\' && i + 1 < s.length()) {
-      i++;
-      continue;
+    if (c == '\\') {
+      if (i + 1 < s.length()) {
+        i++;
+        continue;
+      }
+      // Trailing backslash with no following character
+      break;
     }
     if (c == '*' || c == '?') {
       return true;
     }
   }
   return false;
 }
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies that a trailing backslash could be handled more explicitly. However, the current code already handles this case safely by checking i + 1 < s.length() before incrementing, so a trailing backslash simply won't match the condition and the loop continues normally. The improvement adds clarity but doesn't fix a bug.

Low
Suggestions up to commit 8ad2755
CategorySuggestion                                                                                                                                    Impact
Possible issue
Handle consecutive backslashes correctly

The method does not handle consecutive backslashes correctly. A sequence like \*
(escaped backslash followed by wildcard) will incorrectly skip the wildcard,
treating it as escaped when it should be considered unescaped. Track whether the
previous character was an unescaped backslash.

core/src/main/java/org/opensearch/sql/ast/expression/SearchLiteral.java [91-103]

 private static boolean hasUnescapedWildcard(String s) {
+  boolean escaped = false;
   for (int i = 0; i < s.length(); i++) {
     char c = s.charAt(i);
-    if (c == '\\' && i + 1 < s.length()) {
-      i++;
+    if (c == '\\' && !escaped) {
+      escaped = true;
       continue;
     }
-    if (c == '*' || c == '?') {
+    if ((c == '*' || c == '?') && !escaped) {
       return true;
     }
+    escaped = false;
   }
   return false;
 }
Suggestion importance[1-10]: 7

__

Why: The suggestion identifies a legitimate edge case where consecutive backslashes (\\*) could be mishandled. The improved logic with the escaped flag properly tracks escape state across iterations, fixing a potential bug in wildcard detection.

Medium
General
Add fallback for expression resolution

If getOriginalExpression() returns non-null but toQueryString() throws an exception
or returns null/empty, the fallback to getQueryString() is never attempted. Add
error handling to ensure the pre-computed query string is used when expression-based
resolution fails.

core/src/main/java/org/opensearch/sql/calcite/CalciteRelNodeVisitor.java [307-324]

 if (node.getOriginalExpression() != null) {
   ...
-  queryString = node.getOriginalExpression().toQueryString(typesByName::get);
+  try {
+    queryString = node.getOriginalExpression().toQueryString(typesByName::get);
+  } catch (Exception e) {
+    queryString = node.getQueryString();
+  }
 } else {
   queryString = node.getQueryString();
 }
Suggestion importance[1-10]: 6

__

Why: Adding error handling to fall back to getQueryString() when toQueryString() fails is a reasonable defensive programming practice. However, the suggestion assumes exceptions might occur without evidence from the PR context, making it a moderate improvement rather than a critical fix.

Low
Verify space escape ordering

The space replacement logic may fail if escapeLuceneSpecialCharacters introduces
backslashes before spaces. This could result in \ becoming \ (double-escaped).
Verify that the escape function does not already escape spaces, or apply space
replacement before escaping special characters.

core/src/main/java/org/opensearch/sql/ast/expression/SearchLiteral.java [62-64]

 if (isPhrase && !isTextLike(indexType) && hasUnescapedWildcard(str)) {
-  return QueryStringUtils.escapeLuceneSpecialCharacters(str).replace(" ", "\\ ");
+  String escaped = QueryStringUtils.escapeLuceneSpecialCharacters(str);
+  return escaped.replace(" ", "\\ ");
 }
Suggestion importance[1-10]: 3

__

Why: The suggestion raises a valid concern about escape ordering, but the improved_code is identical to the existing_code, making the practical impact minimal. The concern is worth verifying but doesn't constitute a critical fix.

Low

@penghuo

penghuo commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator Author

@vamsimanohar Please help review.

@penghuo penghuo self-assigned this Aug 12, 2026

@vamsimanohar vamsimanohar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit: java.util.Map and java.util.HashMap are used fully-qualified inline in visitSearch rather than imported at the top of the file. The rest of the file imports its types — consider moving these to the import block for consistency.

Otherwise LGTM — the decision tree is well-reasoned, test coverage is thorough, and the regression-safe fallback (unknown type → phrase form) is the right call.

Replaces the whitespace heuristic that decided phrase vs. term emission.
`SearchLiteral.isPhrase` was set at parse time as `value.contains(" ")`,
which is a syntactic test standing in for a semantic question: will the
field's analyzer split this value into multiple tokens? Whitespace is a
poor proxy — `foo=bar` and `foo-bar` hold none, yet the standard analyzer
splits both.

The consequence on a text field: the value was emitted unquoted,
query_string kept it as one field-scoped term, the analyzer split it, and
default_operator=OR combined the halves. `body="foo=bar"` therefore matched
any document holding just `foo` or just `bar`.

Emission is now selected by the enclosing field's mapping, read from
AbstractOpenSearchTable.getFieldTypes():

- text / match_only_text: honor the user's quoting. Unquoted passes
  through so `*` and `?` stay query_string operators; quoted becomes a
  phrase. Exception: a whitespace-free value carrying a wildcard stays
  unquoted, because quoting would let the analyzer discard the wildcard
  (`foo*` must keep matching `foobar`). That is only safe without
  whitespace — with a space, unquoted would split into separate clauses
  and the tail would lose its field binding.
- keyword / constant_keyword: quoting is irrelevant, since the analyzer is
  a no-op and a quoted phrase resolves to the same single term as a bare
  one. Emit whole-value semantics instead — a wildcard pattern when the
  value holds an unescaped wildcard, otherwise an exact term. Whitespace
  is escaped in the wildcard form so query_string keeps one clause.
- date / numeric / ip / boolean / unresolved: legacy behavior, untouched.

The v2 engine is unaffected. It reaches emission through the no-arg
SearchExpression.toQueryString(), which passes a null-returning resolver
and lands in the legacy branch.

Behavior change, text fields only: a quoted value the analyzer splits is
now a phrase rather than an OR over its tokens. On the test fixture,
`name="foo-bar"` / `"foo/bar"` / `"foo@bar"` go from 7 hits to 4. Wildcard
rows are unchanged. Three examples in docs/user/ppl/cmd/search.md
documented the old over-matching and have been updated.

Tests: Group 7 added to CalciteSearchCommandIT over a dedicated fixture
(foo=bar, foo bar, foo, bar, baz) so the single-token documents that used
to OR-match are asserted absent. SearchLiteralTest covers the three
mapping branches. Verified with CalciteSearchCommandIT, SearchCommandIT
(v2), :core:test, :ppl:test, doctest, and :integ-test:integTest.

Signed-off-by: Peng Huo <penghuo@gmail.com>
@penghuo penghuo changed the title Fix PPL search command dropping wildcards on values with whitespace (#5682) Drive PPL search command emission from the field's index mapping (#5682) Aug 14, 2026

```ppl
search severityText="INFO" AND `resource.attributes.service.name`="cart-service" source=otellogs
search severityText="INFO" AND `resource.attributes.service.name`="cart*" source=otellogs

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vamsimanohar current doc seems a bug. please help take a look.

|------------------------------|
| Microsoft.Extensions.Hosting |
+------------------------------+
fetched rows / total rows = 2/2

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vamsimanohar current doc seems a bug. please help take a look.

| instrumentationScope.name |
|-----------------------------------------------------------------------------|
| Microsoft.Extensions.Hosting |
| go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc |

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vamsimanohar current doc seems a bug. please help take a look.

The previous commit rewrote non-wildcard keyword values from `field:value`
to `field:"value"`. Both forms resolve to the same Lucene TermQuery — the
keyword analyzer is a no-op, so Lucene returns from createFieldQuery at
`numTokens == 1` before the quoted flag is read — but roughly a dozen
expected-plan fixtures compare the emitted query_string as a string, and
they broke. CalcitePPLBig5IT.sort_keyword_can_match_shortcut was the first
to fail in CI on `process.name=kernel`.

The rewrite was cosmetic. It was there to state "quoting is irrelevant on
keyword" in code, but it fixed nothing: `x=y` and `a>b` already match
correctly unquoted in term position, so there was no escaping gap to close
either.

Narrow the keyword branch to the case that is actually broken — a value
holding an unescaped wildcard, which must reach Lucene as a single term for
the pattern to apply to the whole stored value. Everything else on keyword
falls through to the legacy branch and is byte-identical to before. The
point about quoting being irrelevant now lives in the javadoc, as the
reason not to rewrite the emission rather than something enforced by
rewriting it.

Text-side behavior is unchanged: a quoted value the analyzer splits is
still a phrase rather than an OR over its tokens.

No expected-output fixture is modified. Verified with CalcitePPLBig5IT,
CalciteExplainIT, CalciteSearchCommandIT, :core:test, :ppl:test and
doctest.

Signed-off-by: Peng Huo <penghuo@gmail.com>
@github-actions

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🧪 PR contains tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ Recommended focus areas for review

Possible Issue

The hasUnescapedWildcard method does not handle the case where a backslash appears at the end of the string. If s ends with \, the loop increments i beyond s.length(), but the loop condition prevents accessing out-of-bounds. However, a trailing backslash is left unprocessed and could be misinterpreted as escaping nothing. This may cause incorrect wildcard detection when a value ends with a literal backslash.

private static boolean hasUnescapedWildcard(String s) {
  for (int i = 0; i < s.length(); i++) {
    char c = s.charAt(i);
    if (c == '\\' && i + 1 < s.length()) {
      i++;
      continue;
    }
    if (c == '*' || c == '?') {
      return true;
    }
  }
  return false;
}

@penghuo penghuo changed the title Drive PPL search command emission from the field's index mapping (#5682) Fix PPL search command ignoring wildcards and over-matching quoted values (#5682) Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugFix PPL Piped processing language

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] PPL search command drops wildcards (* / ?) when value contains a space

2 participants