This is an automated email from the ASF dual-hosted git repository.
eldenmoon pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new cef5ac68bd2 [fix](search) Support keyword terms and quoted parentheses
in SEARCH DSL (#69460)
cef5ac68bd2 is described below
commit cef5ac68bd2e1d94fad257ed9ea7f2a24ee7b7f0
Author: lihangyu <[email protected]>
AuthorDate: Wed Oct 28 17:13:47 2026 +0800
[fix](search) Support keyword terms and quoted parentheses in SEARCH DSL
(#69460)
---
.../org/apache/doris/analysis/SearchLexer.g4 | 16 ++-
.../org/apache/doris/analysis/SearchParser.g4 | 10 +-
.../org/apache/doris/analysis/SearchDslParser.java | 32 ++++--
.../functions/scalar/SearchDslParserTest.java | 94 +++++++++++++----
.../test_search_dsl_keyword_quoted_syntax.groovy | 116 +++++++++++++++++++++
.../suites/search/test_search_escape.groovy | 43 ++++----
6 files changed, 254 insertions(+), 57 deletions(-)
diff --git
a/fe/fe-catalog/src/main/antlr4/org/apache/doris/analysis/SearchLexer.g4
b/fe/fe-catalog/src/main/antlr4/org/apache/doris/analysis/SearchLexer.g4
index 7b691a61337..d9ecc1d34d9 100644
--- a/fe/fe-catalog/src/main/antlr4/org/apache/doris/analysis/SearchLexer.g4
+++ b/fe/fe-catalog/src/main/antlr4/org/apache/doris/analysis/SearchLexer.g4
@@ -42,9 +42,12 @@ fragment QUOTED_CHAR
// ============== Default lexer rules ==============
-AND : 'AND' ;
-OR : 'OR' ;
-NOT : 'NOT' | '!' ;
+// Boolean operators are case-insensitive, matching the query string syntax
used by
+// Lucene/Elasticsearch: `a:x or b:y` is the same as `a:x OR b:y`. The parser
also accepts
+// these tokens in value position, so `field:and` keeps searching for the term
"and".
+AND : [Aa][Nn][Dd] ;
+OR : [Oo][Rr] ;
+NOT : [Nn][Oo][Tt] | '!' ;
LPAREN : '(' ;
RPAREN : ')' ;
@@ -94,6 +97,13 @@ LIST_WS : [ \t\r\n\u3000]+ -> skip ;
mode STRING_MODE;
+// A quoted section is tokenized as one unit so that parentheses inside the
quotes do not
+// terminate the clause early, e.g. ANY("All Quiet on the Western Front (2022
film)").
+// It is a separate rule (instead of an alternative inside STRING_CONTENT)
because a negated
+// set inside a closure lets the lexer run past the closing parenthesis when
another ANY/ALL
+// appears later in the same DSL.
+STRING_QUOTED : '"' (ESCAPED_CHAR | ~["\\])* '"' ;
+
STRING_CONTENT : (ESCAPED_CHAR | NON_ESCAPED)+ ;
STRING_RPAREN : ')' -> popMode ;
diff --git
a/fe/fe-catalog/src/main/antlr4/org/apache/doris/analysis/SearchParser.g4
b/fe/fe-catalog/src/main/antlr4/org/apache/doris/analysis/SearchParser.g4
index ee5e2bf644f..5df6bbcd567 100644
--- a/fe/fe-catalog/src/main/antlr4/org/apache/doris/analysis/SearchParser.g4
+++ b/fe/fe-catalog/src/main/antlr4/org/apache/doris/analysis/SearchParser.g4
@@ -38,7 +38,11 @@ nestedQuery : NESTED_LPAREN NESTED_PATH NESTED_COMMA clause
RPAREN ;
fieldGroupQuery : fieldPath COLON LPAREN clause RPAREN ;
// Support for variant subcolumn paths (e.g., field.subcolumn, field.sub1.sub2)
-fieldQuery : fieldPath COLON searchValue ;
+fieldQuery : fieldPath COLON fieldValue ;
+// Boolean keywords are also valid terms in value position, e.g. field:and.
Keeping this
+// separate from searchValue leaves the bare query rule free of operator
tokens, so lowercase
+// `or` between two clauses is still parsed as the OR operator.
+fieldValue : searchValue | AND | OR | NOT ;
// Bare query without field prefix - uses default_field
bareQuery : searchValue ;
@@ -65,5 +69,5 @@ rangeValue
rangeEndpoint : RANGE_NUMBER | RANGE_STAR ;
listValue : IN_LPAREN LIST_TERM* LIST_RPAREN ;
-anyAllValue : (ANY_LPAREN | ALL_LPAREN) STRING_CONTENT? STRING_RPAREN ;
-exactValue : EXACT_LPAREN STRING_CONTENT? STRING_RPAREN ;
+anyAllValue : (ANY_LPAREN | ALL_LPAREN) (STRING_CONTENT | STRING_QUOTED)*
STRING_RPAREN ;
+exactValue : EXACT_LPAREN (STRING_CONTENT | STRING_QUOTED)* STRING_RPAREN ;
diff --git
a/fe/fe-catalog/src/main/java/org/apache/doris/analysis/SearchDslParser.java
b/fe/fe-catalog/src/main/java/org/apache/doris/analysis/SearchDslParser.java
index a651e8ee7b4..4326ec5822d 100644
--- a/fe/fe-catalog/src/main/java/org/apache/doris/analysis/SearchDslParser.java
+++ b/fe/fe-catalog/src/main/java/org/apache/doris/analysis/SearchDslParser.java
@@ -842,10 +842,16 @@ MATCH_ALL_DOCS, // Matches all documents (used for pure
NOT query rewriting)
currentFieldName = fieldPath;
try {
- if (ctx.searchValue() == null) {
+ if (ctx.fieldValue() == null) {
throw new RuntimeException("Invalid field query: missing
search value");
}
- QsNode result = visit(ctx.searchValue());
+ QsNode result;
+ if (ctx.fieldValue().searchValue() != null) {
+ result = visit(ctx.fieldValue().searchValue());
+ } else {
+ // AND/OR/NOT in value position are ordinary terms:
`field:and`.
+ result = createTermNode(fieldPath,
ctx.fieldValue().getText());
+ }
if (result == null) {
throw new RuntimeException("Invalid search value");
}
@@ -996,7 +1002,9 @@ MATCH_ALL_DOCS, // Matches all documents (used for pure
NOT query rewriting)
private QsNode createAnyAllNode(String fieldName, String anyAllText) {
// Extract content between parentheses
String innerContent = extractParenthesesContent(anyAllText);
- String sanitizedContent = stripOuterQuotes(innerContent);
+ // Strip DSL-level quotes first, then resolve escapes such as \"
or \), so that a
+ // search for a value containing quote characters stays
expressible.
+ String sanitizedContent =
unescapeTermValue(stripOuterQuotes(innerContent));
if (anyAllText.toUpperCase().startsWith("ANY(") ||
anyAllText.toLowerCase().startsWith("any(")) {
return new QsNode(QsClauseType.ANY, fieldName,
sanitizedContent);
@@ -1013,9 +1021,10 @@ MATCH_ALL_DOCS, // Matches all documents (used for pure
NOT query rewriting)
}
private QsNode createExactNode(String fieldName, String exactText) {
- // Extract content between parentheses
+ // Extract content between parentheses and resolve escapes (for
example ABC\) is the
+ // value "ABC)"), otherwise the backslash is looked up as part of
the indexed term.
String innerContent = extractParenthesesContent(exactText);
- return new QsNode(QsClauseType.EXACT, fieldName, innerContent);
+ return new QsNode(QsClauseType.EXACT, fieldName,
unescapeTermValue(innerContent));
}
private String extractParenthesesContent(String text) {
@@ -2445,7 +2454,14 @@ MATCH_ALL_DOCS, // Matches all documents (used for pure
NOT query rewriting)
currentFieldName = fieldPath;
try {
- QsNode result = visit(ctx.searchValue());
+ QsNode result;
+ if (ctx.fieldValue().searchValue() != null) {
+ result = visit(ctx.fieldValue().searchValue());
+ } else {
+ // AND/OR/NOT in value position are ordinary terms:
`field:and`.
+ result = new QsNode(QsClauseType.TERM, fieldPath,
+ unescapeTermValue(ctx.fieldValue().getText()));
+ }
// Mark as explicit field - user wrote "field:term" syntax
result.setExplicitField(true);
return result;
@@ -2554,7 +2570,7 @@ MATCH_ALL_DOCS, // Matches all documents (used for pure
NOT query rewriting)
if (ctx.anyAllValue() != null) {
String text = ctx.anyAllValue().getText();
String innerContent = extractParenthesesContent(text);
- String sanitizedContent = stripOuterQuotes(innerContent);
+ String sanitizedContent =
unescapeTermValue(stripOuterQuotes(innerContent));
if (text.toUpperCase().startsWith("ANY(")) {
return new QsNode(QsClauseType.ANY, fieldName,
sanitizedContent);
}
@@ -2562,7 +2578,7 @@ MATCH_ALL_DOCS, // Matches all documents (used for pure
NOT query rewriting)
}
if (ctx.exactValue() != null) {
String innerContent =
extractParenthesesContent(ctx.exactValue().getText());
- return new QsNode(QsClauseType.EXACT, fieldName, innerContent);
+ return new QsNode(QsClauseType.EXACT, fieldName,
unescapeTermValue(innerContent));
}
return new QsNode(QsClauseType.TERM, fieldName,
unescapeTermValue(ctx.getText()));
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/SearchDslParserTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/SearchDslParserTest.java
index c078e569121..a391c428103 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/SearchDslParserTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/SearchDslParserTest.java
@@ -158,6 +158,68 @@ public class SearchDslParserTest {
Assertions.assertEquals("Mandy Patinkin", plan.getRoot().getValue());
}
+ @Test
+ public void testBooleanKeywordAsFieldValue() {
+ // DORIS-23194: AND/OR/NOT are case-insensitive keywords, but in value
position they are
+ // ordinary terms and must not be rejected by the parser.
+ String[][] cases = {
+ {"ch:AND", "AND"}, {"ch:and", "and"}, {"ch:And", "And"},
+ {"ch:OR", "OR"}, {"ch:or", "or"},
+ {"ch:NOT", "NOT"}, {"ch:not", "not"}};
+ for (String[] testCase : cases) {
+ QsPlan plan = SearchDslParser.parseDsl(testCase[0]);
+ Assertions.assertEquals(QsClauseType.TERM,
plan.getRoot().getType(), testCase[0]);
+ Assertions.assertEquals("ch", plan.getRoot().getField(),
testCase[0]);
+ Assertions.assertEquals(testCase[1], plan.getRoot().getValue(),
testCase[0]);
+ }
+ }
+
+ @Test
+ public void testLowercaseBooleanOperators() {
+ // DORIS-23236: lowercase boolean operators must behave like their
uppercase form.
+ QsPlan orPlan = SearchDslParser.parseDsl("ch:foo or ch:bar",
"{\"mode\":\"standard\"}");
+ Assertions.assertEquals(QsClauseType.OR, orPlan.getRoot().getType());
+ Assertions.assertEquals(2, orPlan.getRoot().getChildren().size());
+
+ QsPlan andPlan = SearchDslParser.parseDsl("ch:foo and ch:bar",
"{\"mode\":\"standard\"}");
+ Assertions.assertEquals(QsClauseType.AND, andPlan.getRoot().getType());
+ Assertions.assertEquals(2, andPlan.getRoot().getChildren().size());
+
+ QsPlan notPlan = SearchDslParser.parseDsl("ch:foo and not ch:bar",
"{\"mode\":\"standard\"}");
+ Assertions.assertEquals(QsClauseType.AND, notPlan.getRoot().getType());
+ Assertions.assertEquals(QsClauseType.NOT,
notPlan.getRoot().getChildren().get(1).getType());
+ }
+
+ @Test
+ public void testQuotedParenthesesInAnyAll() {
+ // DORIS-23236: parentheses inside a quoted value must not terminate
ANY/ALL early.
+ QsPlan anyPlan = SearchDslParser.parseDsl(
+ "ch:ANY(\"All Quiet on the Western Front (2022 film)\")");
+ Assertions.assertEquals(QsClauseType.ANY, anyPlan.getRoot().getType());
+ Assertions.assertEquals("All Quiet on the Western Front (2022 film)",
+ anyPlan.getRoot().getValue());
+
+ QsPlan bothPlan = SearchDslParser.parseDsl(
+ "ch:ANY(\"All Quiet on the Western Front (2022 film)\") "
+ + "or ch:ALL(\"F. Murray Abraham\")",
"{\"mode\":\"standard\"}");
+ Assertions.assertEquals(QsClauseType.OR, bothPlan.getRoot().getType());
+ Assertions.assertEquals(2, bothPlan.getRoot().getChildren().size());
+ Assertions.assertEquals(QsClauseType.ANY,
bothPlan.getRoot().getChildren().get(0).getType());
+ Assertions.assertEquals(QsClauseType.ALL,
bothPlan.getRoot().getChildren().get(1).getType());
+ }
+
+ @Test
+ public void testEscapedCharactersInExactAndAny() {
+ // DORIS-23239: escapes are resolved before the term is looked up.
+ QsPlan exactPlan = SearchDslParser.parseDsl("ch:exact(ABC\\))");
+ Assertions.assertEquals(QsClauseType.EXACT,
exactPlan.getRoot().getType());
+ Assertions.assertEquals("ABC)", exactPlan.getRoot().getValue());
+
+ QsPlan anyPlan = SearchDslParser.parseDsl("ch:any(\\\"ABC\\\")");
+ Assertions.assertEquals(QsClauseType.ANY, anyPlan.getRoot().getType());
+ Assertions.assertEquals("\"ABC\"", anyPlan.getRoot().getValue());
+ }
+
@Test
public void testAndQuery() {
String dsl = "title:hello AND content:world";
@@ -1182,13 +1244,12 @@ public class SearchDslParserTest {
@Test
public void testLowercaseAndOperator() {
- // Lowercase 'and' is NOT an operator in ANTLR grammar
(case-sensitive).
- // With bareQuery rule, it's parsed as a bare term without field.
- // Without default_field, bare term throws exception.
+ // Boolean operators are case-insensitive, matching Lucene/ES query
string syntax.
String dsl = "field:a and field:b";
- Assertions.assertThrows(RuntimeException.class, () -> {
- SearchDslParser.parseDsl(dsl);
- });
+ QsPlan plan = SearchDslParser.parseDsl(dsl, "{\"mode\":\"standard\"}");
+
+ Assertions.assertEquals(QsClauseType.AND, plan.getRoot().getType());
+ Assertions.assertEquals(2, plan.getRoot().getChildren().size());
}
@Test
@@ -1204,13 +1265,12 @@ public class SearchDslParserTest {
@Test
public void testLowercaseOrOperator() {
- // Lowercase 'or' is NOT an operator in ANTLR grammar (case-sensitive).
- // With bareQuery rule, it's parsed as a bare term without field.
- // Without default_field, bare term throws exception.
+ // Boolean operators are case-insensitive, matching Lucene/ES query
string syntax.
String dsl = "field:a or field:b";
- Assertions.assertThrows(RuntimeException.class, () -> {
- SearchDslParser.parseDsl(dsl);
- });
+ QsPlan plan = SearchDslParser.parseDsl(dsl, "{\"mode\":\"standard\"}");
+
+ Assertions.assertEquals(QsClauseType.OR, plan.getRoot().getType());
+ Assertions.assertEquals(2, plan.getRoot().getChildren().size());
}
@Test
@@ -1225,13 +1285,11 @@ public class SearchDslParserTest {
@Test
public void testLowercaseNotOperator() {
- // Lowercase 'not' is NOT an operator in ANTLR grammar
(case-sensitive).
- // With bareQuery rule, it's parsed as a bare term without field.
- // Without default_field, bare term throws exception.
+ // Boolean operators are case-insensitive, matching Lucene/ES query
string syntax.
String dsl = "not field:spam";
- Assertions.assertThrows(RuntimeException.class, () -> {
- SearchDslParser.parseDsl(dsl);
- });
+ QsPlan plan = SearchDslParser.parseDsl(dsl, "{\"mode\":\"standard\"}");
+
+ Assertions.assertEquals(QsClauseType.NOT, plan.getRoot().getType());
}
@Test
diff --git
a/regression-test/suites/search/test_search_dsl_keyword_quoted_syntax.groovy
b/regression-test/suites/search/test_search_dsl_keyword_quoted_syntax.groovy
new file mode 100644
index 00000000000..ca37b636d29
--- /dev/null
+++ b/regression-test/suites/search/test_search_dsl_keyword_quoted_syntax.groovy
@@ -0,0 +1,116 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_search_dsl_keyword_quoted_syntax", "p0") {
+ // SEARCH DSL parses boolean keywords case-insensitively. In value
position they stay
+ // ordinary terms, so `field:and` keeps searching for the term "and"
(DORIS-23194).
+ def keywordTable = "test_search_dsl_keyword_values"
+ sql "DROP TABLE IF EXISTS ${keywordTable}"
+ sql """
+ CREATE TABLE ${keywordTable} (
+ id INT,
+ ch VARCHAR(64),
+ INDEX idx_ch (ch) USING INVERTED PROPERTIES("lower_case" = "true")
+ ) ENGINE=OLAP
+ DUPLICATE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES ("replication_allocation" = "tag.location.default: 1")
+ """
+ sql """INSERT INTO ${keywordTable} VALUES
+ (1, 'and'), (2, 'or'), (3, 'not'), (4, 'AND'), (5, 'hello')"""
+ sql "sync"
+
+ // Every casing parses and resolves to the same term as the MATCH_ANY
control; the keyword
+ // index compares terms case-sensitively, so the expected ids differ per
casing.
+ for (String keyword : ["AND", "and", "And", "OR", "or", "NOT", "not"]) {
+ def searchRows = sql("SELECT id FROM ${keywordTable} WHERE
SEARCH('ch:${keyword}') ORDER BY id")
+ .collect { it[0] as int }
+ def matchRows = sql("SELECT id FROM ${keywordTable} WHERE ch MATCH_ANY
'${keyword}' ORDER BY id")
+ .collect { it[0] as int }
+ assertEquals(matchRows, searchRows, "SEARCH('ch:${keyword}') must
match MATCH_ANY")
+ }
+ assertEquals([4], sql("SELECT id FROM ${keywordTable} WHERE
SEARCH('ch:AND') ORDER BY id")
+ .collect { it[0] as int })
+ assertEquals([1], sql("SELECT id FROM ${keywordTable} WHERE
SEARCH('ch:and') ORDER BY id")
+ .collect { it[0] as int })
+ // Lowercase OR still combines two clauses instead of becoming a term.
+ assertEquals([1, 2], sql("""
+ SELECT id FROM ${keywordTable} WHERE SEARCH('ch:and or ch:or') ORDER
BY id""")
+ .collect { it[0] as int })
+
+ // Lowercase operators combine clauses instead of being parsed as a bare
term
+ // (DORIS-23236). Parentheses inside a quoted value must not terminate
ANY/ALL early.
+ def phraseTable = "test_search_dsl_quoted_paren"
+ sql "DROP TABLE IF EXISTS ${phraseTable}"
+ sql """
+ CREATE TABLE ${phraseTable} (
+ id INT,
+ ch VARCHAR(1024),
+ INDEX idx_ch (ch) USING INVERTED PROPERTIES(
+ "lower_case" = "true", "parser" = "unicode", "support_phrase"
= "true")
+ ) ENGINE=OLAP
+ DUPLICATE KEY(id)
+ DISTRIBUTED BY RANDOM BUCKETS AUTO
+ PROPERTIES ("replication_allocation" = "tag.location.default: 1")
+ """
+ sql """INSERT INTO ${phraseTable} VALUES
+ (1, 'All Quiet on the Western Front (2022 film)'),
+ (2, 'F. Murray Abraham'),
+ (3, 'unrelated text')"""
+ sql "sync"
+
+ assertEquals([1], sql("""
+ SELECT id FROM ${phraseTable}
+ WHERE SEARCH('ch:ANY("All Quiet on the Western Front (2022 film)")')
+ ORDER BY id""").collect { it[0] as int })
+ assertEquals([2], sql("""
+ SELECT id FROM ${phraseTable}
+ WHERE SEARCH('ch:ALL("F. Murray Abraham")')
+ ORDER BY id""").collect { it[0] as int })
+ assertEquals([1, 2], sql("""
+ SELECT id FROM ${phraseTable}
+ WHERE SEARCH('ch:ANY("All Quiet on the Western Front (2022 film)") or
ch:ALL("F. Murray Abraham")')
+ ORDER BY id""").collect { it[0] as int })
+
+ // Escapes are resolved before the term is looked up, so quoted values and
closing
+ // parentheses remain searchable (DORIS-23239).
+ def specialTable = "test_search_dsl_special_chars"
+ sql "DROP TABLE IF EXISTS ${specialTable}"
+ sql """
+ CREATE TABLE ${specialTable} (
+ id INT,
+ ch VARCHAR(1024),
+ INDEX idx_ch (ch) USING INVERTED PROPERTIES("lower_case" = "false")
+ ) ENGINE=OLAP
+ DUPLICATE KEY(id)
+ DISTRIBUTED BY RANDOM BUCKETS AUTO
+ PROPERTIES ("replication_allocation" = "tag.location.default: 1")
+ """
+ sql """INSERT INTO ${specialTable} VALUES (1, '\"ABC\"'), (2, 'ABC)'), (3,
'ABC')"""
+ sql "sync"
+
+ // The keyword index stores the value including its quote characters, so
the term has to
+ // keep the escaped quotes: SEARCH('ch:exact(\"ABC\")').
+ assertEquals([1], sql("SELECT id FROM ${specialTable} WHERE ch MATCH_ANY
'\"ABC\"'")
+ .collect { it[0] as int })
+ assertEquals([1], sql("""
+ SELECT id FROM ${specialTable} WHERE SEARCH('ch:exact(\\"ABC\\")')""")
+ .collect { it[0] as int })
+ assertEquals([2], sql("""
+ SELECT id FROM ${specialTable} WHERE SEARCH('ch:exact(ABC\\\\))')""")
+ .collect { it[0] as int })
+}
diff --git a/regression-test/suites/search/test_search_escape.groovy
b/regression-test/suites/search/test_search_escape.groovy
index 0a72a81bc2b..e4d5fea789d 100644
--- a/regression-test/suites/search/test_search_escape.groovy
+++ b/regression-test/suites/search/test_search_escape.groovy
@@ -146,31 +146,24 @@ suite("test_search_escape", "p0") {
ORDER BY id
"""
- // ============ Test 9: Lowercase 'and' should cause parse error
============
- // Per requirement: Only uppercase AND/OR/NOT are operators
- // Lowercase 'and' is treated as a bare term (no field), causing error
- test {
- sql """
- SELECT /*+SET_VAR(enable_segment_limit_pushdown=true) */ id,
content
- FROM ${tableName}
- WHERE search('content:first and content:fruit')
- ORDER BY id
- """
- exception "No field specified and no default_field configured"
- }
-
- // ============ Test 10: Lowercase 'or' should cause parse error
============
- // Per requirement: Only uppercase AND/OR/NOT are operators
- // Lowercase 'or' is treated as a bare term (no field), causing error
- test {
- sql """
- SELECT /*+SET_VAR(enable_segment_limit_pushdown=true) */ id,
content
- FROM ${tableName}
- WHERE search('content:first or content:second')
- ORDER BY id
- """
- exception "No field specified and no default_field configured"
- }
+ // ============ Test 9: Lowercase 'and' is an operator ============
+ // Boolean operators are case-insensitive, matching Lucene/ES query string
syntax.
+ def lowercaseAndRows = sql """
+ SELECT /*+SET_VAR(enable_segment_limit_pushdown=true) */ id
+ FROM ${tableName}
+ WHERE search('content:first and content:fruit')
+ ORDER BY id
+ """
+ assertEquals([[7]], lowercaseAndRows)
+
+ // ============ Test 10: Lowercase 'or' is an operator ============
+ def lowercaseOrRows = sql """
+ SELECT /*+SET_VAR(enable_segment_limit_pushdown=true) */ id
+ FROM ${tableName}
+ WHERE search('content:first or content:second')
+ ORDER BY id
+ """
+ assertEquals([[1], [2], [7], [8]], lowercaseOrRows)
// ============ Test 11: Exclamation NOT operator ============
qt_exclamation_not """
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]