Skip to content

Commit 0dd782c

Browse files
committed
Full text functions can't be used as part of OR conditions
1 parent 43d2463 commit 0dd782c

File tree

5 files changed

+31
-245
lines changed

5 files changed

+31
-245
lines changed

x-pack/plugin/esql/qa/testFixtures/src/main/resources/match-function.csv-spec

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -102,21 +102,6 @@ book_no:keyword | author:text | stars:
102102
3293 | Danny Faulkner | 2
103103
;
104104

105-
matchWithPushableDisjunction
106-
required_capability: match_function
107-
108-
from books
109-
| where match(title, "Return") or book_no == "7140" or year > 2020
110-
| keep book_no, title;
111-
ignoreOrder:true
112-
113-
book_no:keyword | title:text
114-
2714 | Return of the King Being the Third Part of The Lord of the Rings
115-
7350 | Return of the Shadow
116-
7140 | The Lord of the Rings Poster Collection: Six Paintings by Alan Lee (No. 1)
117-
6818 | Hadji Murad
118-
;
119-
120105
matchWithConjunction
121106
required_capability: match_function
122107

@@ -151,7 +136,7 @@ from books
151136
ignoreOrder:true
152137

153138
book_no:keyword | title:text
154-
4023 |A Tolkien Compass: Including J. R. R. Tolkien's Guide to the Names in The Lord of the Rings
139+
4023 | A Tolkien Compass: Including J. R. R. Tolkien's Guide to the Names in The Lord of the Rings
155140
;
156141

157142
matchWithMultipleWhereClauses
@@ -191,30 +176,12 @@ testMultiValuedFieldWithConjunction
191176
required_capability: match_function
192177

193178
from employees
194-
| where (match(job_positions, "Data Scientist") or match(job_positions, "Support Engineer")) and gender == "F"
179+
| where match(job_positions, "Data Scientist") and match(job_positions, "Support Engineer")
195180
| keep emp_no, first_name, last_name;
196181
ignoreOrder:true
197182

198183
emp_no:integer | first_name:keyword | last_name:keyword
199-
10023 | Bojan | Montemayor
200-
10041 | Uri | Lenart
201-
10044 | Mingsen | Casley
202-
10053 | Sanjiv | Zschoche
203-
10069 | Margareta | Bierman
204-
;
205-
206-
testFieldWithKeywordComparisonDisjunction
207-
required_capability: match_function
208-
209-
from employees
210-
| where match(last_name, "Kalloufi") or last_name == "Piveteau"
211-
| keep emp_no, first_name, last_name;
212-
ignoreOrder:true
213-
214-
emp_no:integer | first_name:keyword | last_name:keyword
215-
10008 | Saniya | Kalloufi
216-
10010 | Duangkaew | Piveteau
217-
10084 | Tuval | Kalloufi
184+
10043 | Yishay | Tzvieli
218185
;
219186

220187
testMatchAndQueryStringFunctions

x-pack/plugin/esql/qa/testFixtures/src/main/resources/qstr-function.csv-spec

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,21 +49,6 @@ book_no:keyword | title:text
4949
7350 | Return of the Shadow
5050
;
5151

52-
qstrWithPushableDisjunction
53-
required_capability: qstr_function
54-
55-
from books
56-
| where qstr("title:Return") or book_no == "7140" or year > 2020
57-
| keep book_no, title;
58-
ignoreOrder:true
59-
60-
book_no:keyword | title:text
61-
2714 | Return of the King Being the Third Part of The Lord of the Rings
62-
7350 | Return of the Shadow
63-
7140 | The Lord of the Rings Poster Collection: Six Paintings by Alan Lee (No. 1)
64-
6818 | Hadji Murad
65-
;
66-
6752
qstrWithConjunction
6853
required_capability: qstr_function
6954

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/Verifier.java

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -698,26 +698,21 @@ private static void checkCommandsBeforeMatchFunction(LogicalPlan plan, Expressio
698698

699699
private static void checkFullTextFunctionsConditions(Expression condition, Set<Failure> failures) {
700700
condition.forEachUp(Or.class, or -> {
701-
Expression left = or.left();
702-
Expression right = or.right();
703-
checkDisjunction(failures, or, left, right);
704-
checkDisjunction(failures, or, right, left);
701+
checkFullTextFunctionInDisjunction(failures, or, or.left());
702+
checkFullTextFunctionInDisjunction(failures, or, or.right());
705703
});
706704
}
707705

708-
private static void checkDisjunction(Set<Failure> failures, Or or, Expression left, Expression right) {
706+
private static void checkFullTextFunctionInDisjunction(Set<Failure> failures, Or or, Expression left) {
709707
left.forEachDown(FullTextFunction.class, ftf -> {
710-
if (canPushToSource(right, x -> false) == false) {
711-
failures.add(
712-
fail(
713-
or,
714-
"Invalid condition [{}]. Function {} can't be used as part of an or condition that includes [{}]",
715-
or.sourceText(),
716-
ftf.functionName(),
717-
right.sourceText()
718-
)
719-
);
720-
}
708+
failures.add(
709+
fail(
710+
or,
711+
"Invalid condition [{}]. Function {} can't be used as part of an or condition",
712+
or.sourceText(),
713+
ftf.functionName()
714+
)
715+
);
721716
});
722717
}
723718

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,24 +1196,23 @@ public void testQueryStringFunctionArgNotNullOrConstant() throws Exception {
11961196
// Other value types are tested in QueryStringFunctionTests
11971197
}
11981198

1199-
public void testQueryStringWithDisjunctionsThatCannotBePushedDown() {
1199+
public void testQueryStringWithDisjunctions() {
12001200
assumeTrue("skipping because QSTR is not enabled", EsqlCapabilities.Cap.QSTR_FUNCTION.isEnabled());
12011201

1202-
checkWithDisjunctionsThatCannotBePushedDown("QSTR", "qstr(\"first_name: Anna\")");
1202+
checkWithDisjunctions("QSTR", "qstr(\"first_name: Anna\")");
12031203
}
12041204

1205-
public void testMatchWithDisjunctionsThatCannotBePushedDown() {
1205+
public void testMatchWithDisjunctions() {
12061206
assumeTrue("skipping because MATCH is not enabled", EsqlCapabilities.Cap.MATCH_FUNCTION.isEnabled());
12071207

1208-
checkWithDisjunctionsThatCannotBePushedDown("MATCH", "match(first_name, \"Anna\")");
1208+
checkWithDisjunctions("MATCH", "match(first_name, \"Anna\")");
12091209
}
12101210

1211-
private void checkWithDisjunctionsThatCannotBePushedDown(String functionName, String functionInvocation) {
1211+
private void checkWithDisjunctions(String functionName, String functionInvocation) {
12121212
assertEquals(
12131213
LoggerMessageFormat.format(
12141214
null,
1215-
"1:19: Invalid condition [{} or length(first_name) > 12]. "
1216-
+ "Function {} can't be used as part of an or condition that includes [length(first_name) > 12]",
1215+
"1:19: Invalid condition [{} or length(first_name) > 12]. " + "Function {} can't be used as part of an or condition",
12171216
functionInvocation,
12181217
functionName
12191218
),
@@ -1223,7 +1222,7 @@ private void checkWithDisjunctionsThatCannotBePushedDown(String functionName, St
12231222
LoggerMessageFormat.format(
12241223
null,
12251224
"1:19: Invalid condition [({} and first_name is not null) or (length(first_name) > 12 and first_name is null)]. "
1226-
+ "Function {} can't be used as part of an or condition that includes [length(first_name) > 12 and first_name is null]",
1225+
+ "Function {} can't be used as part of an or condition",
12271226
functionInvocation,
12281227
functionName
12291228
),
@@ -1233,6 +1232,16 @@ private void checkWithDisjunctionsThatCannotBePushedDown(String functionName, St
12331232
+ " and first_name is not null) or (length(first_name) > 12 and first_name is null)"
12341233
)
12351234
);
1235+
assertEquals(
1236+
LoggerMessageFormat.format(
1237+
null,
1238+
"1:19: Invalid condition [({} and first_name is not null) or first_name is null]. "
1239+
+ "Function {} can't be used as part of an or condition",
1240+
functionInvocation,
1241+
functionName
1242+
),
1243+
error("from test | where (" + functionInvocation + " and first_name is not null) or first_name is null")
1244+
);
12361245
}
12371246

12381247
public void testQueryStringFunctionWithNonBooleanFunctions() {

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LocalPhysicalPlanOptimizerTests.java

Lines changed: 0 additions & 170 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
import org.apache.lucene.search.IndexSearcher;
1313
import org.elasticsearch.Build;
14-
import org.elasticsearch.common.logging.LoggerMessageFormat;
1514
import org.elasticsearch.common.settings.Settings;
1615
import org.elasticsearch.core.Tuple;
1716
import org.elasticsearch.index.IndexMode;
@@ -437,40 +436,6 @@ public void testQueryStringFunctionConjunctionWhereOperands() {
437436
assertThat(query.query().toString(), is(expected.toString()));
438437
}
439438

440-
/**
441-
* Expecting
442-
* LimitExec[1000[INTEGER]]
443-
* \_ExchangeExec[[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gender{f}#5, job{f}#10, job.raw{f}#11, languages{f}#6, last_n
444-
* ame{f}#7, long_noidx{f}#12, salary{f}#8],false]
445-
* \_ProjectExec[[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gender{f}#5, job{f}#10, job.raw{f}#11, languages{f}#6, last_n
446-
* ame{f}#7, long_noidx{f}#12, salary{f}#8]]
447-
* \_FieldExtractExec[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gen]
448-
* \_EsQueryExec[test], indexMode[standard], query[{"bool":{"should":[{"query_string":{"query":"last_name: Smith","fields":[]}},
449-
* {"esql_single_value":{"field":"emp_no","next":{"range":{"emp_no":{"gt":10010,"boost":1.0}}},"source":"emp_no > 10010@2:37"}}],
450-
* "boost":1.0}}][_doc{f}#13], limit[1000], sort[] estimatedRowSize[324]
451-
*/
452-
public void testQueryStringFunctionDisjunctionWhereClauses() {
453-
assumeTrue("skipping because QSTR_FUNCTION is not enabled", EsqlCapabilities.Cap.QSTR_FUNCTION.isEnabled());
454-
String queryText = """
455-
from test
456-
| where qstr("last_name: Smith") or emp_no > 10010
457-
""";
458-
var plan = plannerOptimizer.plan(queryText, IS_SV_STATS);
459-
460-
var limit = as(plan, LimitExec.class);
461-
var exchange = as(limit.child(), ExchangeExec.class);
462-
var project = as(exchange.child(), ProjectExec.class);
463-
var field = as(project.child(), FieldExtractExec.class);
464-
var query = as(field.child(), EsQueryExec.class);
465-
assertThat(query.limit().fold(), is(1000));
466-
467-
Source filterSource = new Source(2, 36, "emp_no > 10000");
468-
var range = wrapWithSingleQuery(queryText, QueryBuilders.rangeQuery("emp_no").gt(10010), "emp_no", filterSource);
469-
var queryString = QueryBuilders.queryStringQuery("last_name: Smith");
470-
var expected = QueryBuilders.boolQuery().should(queryString).should(range);
471-
assertThat(query.query().toString(), is(expected.toString()));
472-
}
473-
474439
/**
475440
* Expecting
476441
* LimitExec[1000[INTEGER]]
@@ -637,40 +602,6 @@ public void testMatchFunctionConjunctionWhereOperands() {
637602
assertThat(query.query().toString(), is(expected.toString()));
638603
}
639604

640-
/**
641-
* Expecting
642-
* LimitExec[1000[INTEGER]]
643-
* \_ExchangeExec[[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gender{f}#5, job{f}#10, job.raw{f}#11, languages{f}#6, last_n
644-
* ame{f}#7, long_noidx{f}#12, salary{f}#8],false]
645-
* \_ProjectExec[[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gender{f}#5, job{f}#10, job.raw{f}#11, languages{f}#6, last_n
646-
* ame{f}#7, long_noidx{f}#12, salary{f}#8]]
647-
* \_FieldExtractExec[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gen]
648-
* \_EsQueryExec[test], indexMode[standard], query[{"bool":{"should":[{"match":{"last_name":{"query":"Smith"}}},
649-
* {"esql_single_value":{"field":"emp_no","next":{"range":{"emp_no":{"gt":10010,"boost":1.0}}},"source":"emp_no > 10010@2:38"}}],
650-
* "boost":1.0}}][_doc{f}#14], limit[1000], sort[] estimatedRowSize[324]
651-
*/
652-
public void testMatchFunctionDisjunctionWhereClauses() {
653-
assumeTrue("skipping because MATCH function is not enabled", EsqlCapabilities.Cap.MATCH_FUNCTION.isEnabled());
654-
String queryText = """
655-
from test
656-
| where match(last_name, "Smith") or emp_no > 10010
657-
""";
658-
var plan = plannerOptimizer.plan(queryText, IS_SV_STATS);
659-
660-
var limit = as(plan, LimitExec.class);
661-
var exchange = as(limit.child(), ExchangeExec.class);
662-
var project = as(exchange.child(), ProjectExec.class);
663-
var field = as(project.child(), FieldExtractExec.class);
664-
var query = as(field.child(), EsQueryExec.class);
665-
assertThat(query.limit().fold(), is(1000));
666-
667-
Source filterSource = new Source(2, 37, "emp_no > 10000");
668-
var range = wrapWithSingleQuery(queryText, QueryBuilders.rangeQuery("emp_no").gt(10010), "emp_no", filterSource);
669-
var queryString = QueryBuilders.matchQuery("last_name", "Smith");
670-
var expected = QueryBuilders.boolQuery().should(queryString).should(range);
671-
assertThat(query.query().toString(), is(expected.toString()));
672-
}
673-
674605
/**
675606
* Expecting
676607
* LimitExec[1000[INTEGER]]
@@ -742,73 +673,6 @@ public void testMatchFunctionMultipleWhereClauses() {
742673
assertThat(query.query().toString(), is(expected.toString()));
743674
}
744675

745-
public void testQueryStringWithDisjunctionsThatCannotBePushedDown() {
746-
assumeTrue("skipping because QSTR is not enabled", EsqlCapabilities.Cap.QSTR_FUNCTION.isEnabled());
747-
748-
checkWithDisjunctionsThatCannotBePushedDown("QSTR", "qstr(\"first_name: Anna\")");
749-
}
750-
751-
public void testMatchWithDisjunctionsThatCannotBePushedDown() {
752-
assumeTrue("skipping because MATCH function is not enabled", EsqlCapabilities.Cap.MATCH_FUNCTION.isEnabled());
753-
754-
checkWithDisjunctionsThatCannotBePushedDown("MATCH", "match(first_name, \"Anna\")");
755-
}
756-
757-
private void checkWithDisjunctionsThatCannotBePushedDown(String functionName, String functionInvocation) {
758-
VerificationException ve = expectThrows(
759-
VerificationException.class,
760-
() -> plannerOptimizer.plan("from test | where " + functionInvocation + " or length(first_name) > 12", IS_SV_STATS)
761-
);
762-
assertThat(
763-
ve.getMessage(),
764-
containsString(
765-
LoggerMessageFormat.format(
766-
null,
767-
"1:19: Invalid condition [{} or length(first_name) > 12]. "
768-
+ "Function {} can't be used as part of an or condition that includes [length(first_name) > 12]",
769-
functionInvocation,
770-
functionName
771-
)
772-
)
773-
);
774-
ve = expectThrows(
775-
VerificationException.class,
776-
() -> plannerOptimizer.plan(
777-
"from test | where (" + functionInvocation + " and emp_no < 1000) or (length(first_name) > 12 and emp_no > 1000)",
778-
IS_SV_STATS
779-
)
780-
);
781-
assertThat(
782-
ve.getMessage(),
783-
containsString(
784-
LoggerMessageFormat.format(
785-
null,
786-
"1:19: Invalid condition [({} and emp_no < 1000) or (length(first_name) > 12 and emp_no > 1000)]. "
787-
+ "Function {} can't be used as part of an or condition that includes [length(first_name) > 12 and emp_no > 1000]",
788-
functionInvocation,
789-
functionName
790-
)
791-
)
792-
);
793-
}
794-
795-
public void testMatchFunctionDisjunctionNonPushableClauses() {
796-
assumeTrue("skipping because MATCH function is not enabled", EsqlCapabilities.Cap.MATCH_FUNCTION.isEnabled());
797-
String queryText = """
798-
from test
799-
| where match(first_name, "Peter") or length(last_name) > 10
800-
""";
801-
802-
VerificationException ve = expectThrows(VerificationException.class, () -> plannerOptimizer.plan(queryText, IS_SV_STATS));
803-
assertThat(
804-
ve.getMessage(),
805-
containsString(
806-
"Invalid condition [match(first_name, \"Peter\") or length(last_name) > 10]. "
807-
+ "Function MATCH can't be used as part of an or condition that includes [length(last_name) > 10]"
808-
)
809-
);
810-
}
811-
812676
public void testMatchFunctionIsNotNullable() {
813677
assumeTrue("skipping because MATCH function is not enabled", EsqlCapabilities.Cap.MATCH_FUNCTION.isEnabled());
814678

@@ -823,40 +687,6 @@ public void testMatchFunctionIsNotNullable() {
823687
);
824688
}
825689

826-
/**
827-
* Expected:
828-
* LimitExec[1000[INTEGER]]
829-
* \_ExchangeExec[[_meta_field{f}#10, emp_no{f}#4, first_name{f}#5, gender{f}#6, job{f}#11, job.raw{f}#12, languages{f}#7, last_
830-
* name{f}#8, long_noidx{f}#13, salary{f}#9],false]
831-
* \_ProjectExec[[_meta_field{f}#10, emp_no{f}#4, first_name{f}#5, gender{f}#6, job{f}#11, job.raw{f}#12, languages{f}#7, last_
832-
* name{f}#8, long_noidx{f}#13, salary{f}#9]]
833-
* \_FieldExtractExec[_meta_field{f}#10, emp_no{f}#4, first_name{f}
834-
* \_EsQueryExec[test], indexMode[standard],
835-
* query[{"bool":{"should":[{"match":{"first_name":{"query":"Peter"}}},{"esql_single_value":{"field":"last_name",
836-
* "next":{"term":{"last_name":{"value":"Griffin"}}},"source":"last_name == \"Griffin\"@2:39"}}],"boost":1.0}}]
837-
*/
838-
public void testMatchFunctionDisjunctionPushableClauses() {
839-
assumeTrue("skipping because MATCH function is not enabled", EsqlCapabilities.Cap.MATCH_FUNCTION.isEnabled());
840-
String queryText = """
841-
from test
842-
| where match(first_name, "Peter") or last_name == "Griffin"
843-
""";
844-
var plan = plannerOptimizer.plan(queryText, IS_SV_STATS);
845-
846-
var limit = as(plan, LimitExec.class);
847-
var exchange = as(limit.child(), ExchangeExec.class);
848-
var project = as(exchange.child(), ProjectExec.class);
849-
var field = as(project.child(), FieldExtractExec.class);
850-
var query = as(field.child(), EsQueryExec.class);
851-
assertThat(query.limit().fold(), is(1000));
852-
853-
var queryStringLeft = QueryBuilders.matchQuery("first_name", "Peter");
854-
Source termSource = new Source(2, 38, "last_name == \"Griffin\"");
855-
var queryStringRight = wrapWithSingleQuery(queryText, QueryBuilders.termQuery("last_name", "Griffin"), "last_name", termSource);
856-
var expected = QueryBuilders.boolQuery().should(queryStringLeft).should(queryStringRight);
857-
assertThat(query.query().toString(), is(expected.toString()));
858-
}
859-
860690
/**
861691
* Expecting
862692
* LimitExec[1000[INTEGER]]

0 commit comments

Comments
 (0)