Skip to content

Commit 60221ba

Browse files
committed
Check that all elements on each disjunction side have full text functions
1 parent da32a06 commit 60221ba

File tree

5 files changed

+121
-94
lines changed

5 files changed

+121
-94
lines changed

x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/MatchFunctionIT.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ public void testWhereMatchWithFunctions() {
240240
error.getMessage(),
241241
containsString(
242242
"Invalid condition [content:\"fox\" OR to_upper(content) == \"FOX\"]. "
243-
+ "[:] operator can't be used as part of an or condition"
243+
+ "[:] operator can be used in an OR condition, but only if just full text functions are used in the OR condition"
244244
)
245245
);
246246
}

x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/MatchOperatorIT.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,7 @@ public void testWhereMatchWithFunctions() {
215215
assertThat(
216216
error.getMessage(),
217217
containsString(
218-
"Invalid condition [content:\"fox\" OR to_upper(content) == \"FOX\"]. "
219-
+ "[:] operator can't be used as part of an or condition"
218+
"Invalid condition [content:\"fox\" OR to_upper(content) == \"FOX\"]. " + "[:] operator can't be used in an OR condition"
220219
)
221220
);
222221
}

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

Lines changed: 73 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -768,9 +768,11 @@ private static void checkRemoteEnrich(LogicalPlan plan, Set<Failure> failures) {
768768
}
769769

770770
/**
771-
* Checks whether a condition contains a disjunction with the specified typeToken. Adds to failure if it does.
771+
* Checks whether a condition contains a disjunction with a full text search.
772+
* If it does, check that every element of the disjunction is a full text search or combinations (AND, OR, NOT) of them.
773+
* If not, add a failure to the failures collection.
772774
*
773-
* @param condition condition to check for disjunctions
775+
* @param condition condition to check for disjunctions of full text searches
774776
* @param typeNameProvider provider for the type name to add in the failure message
775777
* @param failures failures collection to add to
776778
*/
@@ -779,29 +781,56 @@ private static void checkFullTextSearchDisjunctions(
779781
java.util.function.Function<FullTextFunction, String> typeNameProvider,
780782
Set<Failure> failures
781783
) {
782-
condition.forEachUp(Or.class, or -> {
783-
boolean left = onlyFullTextFunctionsInExpression(or.left());
784-
boolean right = onlyFullTextFunctionsInExpression(or.right());
785-
if (left ^ right) {
786-
Holder<String> elementName = new Holder<>();
787-
if (left) {
788-
or.left().forEachDown(FullTextFunction.class, ftf -> elementName.set(typeNameProvider.apply(ftf)));
789-
} else {
790-
or.right().forEachDown(FullTextFunction.class, ftf -> elementName.set(typeNameProvider.apply(ftf)));
791-
}
792-
failures.add(
793-
fail(
794-
or,
795-
"Invalid condition [{}]. {} can be used as part of an OR condition, "
796-
+ "but only if other full text functions are used as part of the condition",
797-
or.sourceText(),
798-
elementName.get()
799-
)
800-
);
784+
int failuresCount = failures.size();
785+
condition.forEachDown(Or.class, or -> {
786+
if (failures.size() > failuresCount) {
787+
// Exit early if we already have a failures
788+
return;
789+
}
790+
Expression left = or.left();
791+
boolean leftHasFullText = left.anyMatch(FullTextFunction.class::isInstance);
792+
boolean leftHasOnlyFullText = onlyFullTextFunctionsInExpression(left);
793+
Expression right = or.right();
794+
boolean rightHasFullText = right.anyMatch(FullTextFunction.class::isInstance);
795+
boolean rightHasOnlyFullText = onlyFullTextFunctionsInExpression(right);
796+
797+
if (leftHasFullText && leftHasOnlyFullText == false) {
798+
disjunctionFailure(or, left, typeNameProvider, failures);
799+
} else if (rightHasFullText && rightHasOnlyFullText == false) {
800+
disjunctionFailure(or, right, typeNameProvider, failures);
801+
} else if (leftHasFullText ^ rightHasFullText) {
802+
disjunctionFailure(or, leftHasFullText ? left : right, typeNameProvider, failures);
801803
}
802804
});
803805
}
804806

807+
/**
808+
* Add a disjunction failure to the failures collection.
809+
* @param parentExpression parent expression to include in the failure
810+
* @param expressionWithError expression that provoked the failure
811+
* @param typeNameProvider provider for the type name to add in the failure message
812+
* @param failures failures collection to add to
813+
*/
814+
private static void disjunctionFailure(
815+
Expression parentExpression,
816+
Expression expressionWithError,
817+
java.util.function.Function<FullTextFunction, String> typeNameProvider,
818+
Set<Failure> failures
819+
) {
820+
Holder<String> elementName = new Holder<>();
821+
// Get first function name to add to the failure message
822+
expressionWithError.forEachDown(FullTextFunction.class, ftf -> elementName.set(typeNameProvider.apply(ftf)));
823+
failures.add(
824+
fail(
825+
parentExpression,
826+
"Invalid condition [{}]. {} can be used in an OR condition, "
827+
+ "but only if just full text functions are used in the OR condition",
828+
parentExpression.sourceText(),
829+
elementName.get()
830+
)
831+
);
832+
}
833+
805834
/**
806835
* Checks whether an expression contains just full text functions or negations (NOT) and combinations (AND, OR) of full text functions
807836
*
@@ -811,15 +840,32 @@ private static void checkFullTextSearchDisjunctions(
811840
private static boolean onlyFullTextFunctionsInExpression(Expression expression) {
812841
if (expression instanceof FullTextFunction) {
813842
return true;
814-
} else if (expression instanceof Not || expression instanceof BinaryLogic) {
815-
for (Expression child : expression.children()) {
816-
if (onlyFullTextFunctionsInExpression(child) == false) {
817-
return false;
818-
}
819-
}
843+
} else if (expression instanceof Not) {
844+
return onlyFullTextFunctionsInExpression(expression.children().get(0));
845+
} else if (expression instanceof BinaryLogic binaryLogic) {
846+
return onlyFullTextFunctionsInExpression(binaryLogic.left()) && onlyFullTextFunctionsInExpression(binaryLogic.right());
847+
}
848+
849+
return false;
850+
}
851+
852+
/**
853+
* Checks whether an expression contains a full text function as part of it
854+
*
855+
* @param expression expression to check
856+
* @return true if the expression or any of its children is a full text function, false otherwise
857+
*/
858+
private static boolean anyFullTextFunctionsInExpression(Expression expression) {
859+
if (expression instanceof FullTextFunction) {
820860
return true;
821861
}
822862

863+
for (Expression child : expression.children()) {
864+
if (anyFullTextFunctionsInExpression(child)) {
865+
return true;
866+
}
867+
}
868+
823869
return false;
824870
}
825871

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

Lines changed: 32 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1168,14 +1168,14 @@ public void testMatchInsideEval() throws Exception {
11681168
public void testMatchFilter() throws Exception {
11691169
assertEquals(
11701170
"1:19: Invalid condition [first_name:\"Anna\" or starts_with(first_name, \"Anne\")]. "
1171-
+ "[:] operator can be used as part of an OR condition, "
1172-
+ "but only if other full text functions are used as part of the condition",
1171+
+ "[:] operator can be used in an OR condition, "
1172+
+ "but only if just full text functions are used in the OR condition",
11731173
error("from test | where first_name:\"Anna\" or starts_with(first_name, \"Anne\")")
11741174
);
11751175

11761176
assertEquals(
1177-
"1:51: Invalid condition [first_name:\"Anna\" OR new_salary > 100]. [:] operator can be used as part of an OR "
1178-
+ "condition, but only if other full text functions are used as part of the condition",
1177+
"1:51: Invalid condition [first_name:\"Anna\" OR new_salary > 100]. [:] operator can be used in an OR "
1178+
+ "condition, but only if just full text functions are used in the OR condition",
11791179
error("from test | eval new_salary = salary + 10 | where first_name:\"Anna\" OR new_salary > 100")
11801180
);
11811181
}
@@ -1413,45 +1413,26 @@ public void testMatchOperatorWithDisjunctions() {
14131413
}
14141414

14151415
private void checkWithDisjunctions(String functionName, String functionInvocation, String functionType) {
1416+
String expression = functionInvocation + " or length(first_name) > 12";
1417+
checkdisjunctionError("1:19", expression, functionName, functionType);
1418+
expression = "(" + functionInvocation + " or first_name is not null) or (length(first_name) > 12 and match(last_name, \"Smith\"))";
1419+
checkdisjunctionError("1:19", expression, functionName, functionType);
1420+
expression = functionInvocation + " or (last_name is not null and first_name is null)";
1421+
checkdisjunctionError("1:19", expression, functionName, functionType);
1422+
}
1423+
1424+
private void checkdisjunctionError(String position, String expression, String functionName, String functionType) {
14161425
assertEquals(
14171426
LoggerMessageFormat.format(
14181427
null,
1419-
"1:19: Invalid condition [{} or length(first_name) > 12]. "
1420-
+ "[{}] "
1421-
+ functionType
1422-
+ " can be used as part of an OR condition, but only if other full text functions are used as part of the condition",
1423-
functionInvocation,
1424-
functionName
1425-
),
1426-
error("from test | where " + functionInvocation + " or length(first_name) > 12")
1427-
);
1428-
assertEquals(
1429-
LoggerMessageFormat.format(
1430-
null,
1431-
"1:20: Invalid condition [{} or first_name is not null]. "
1432-
+ "[{}] "
1433-
+ functionType
1434-
+ " can be used as part of an OR condition, but only if other full text functions are used as part of the condition",
1435-
functionInvocation,
1436-
functionName
1437-
),
1438-
error(
1439-
"from test | where ("
1440-
+ functionInvocation
1441-
+ " or first_name is not null) or (length(first_name) > 12 and match(last_name, \"Smith\"))"
1442-
)
1443-
);
1444-
assertEquals(
1445-
LoggerMessageFormat.format(
1446-
null,
1447-
"1:19: Invalid condition [{} or (last_name is not null and first_name is null)]. "
1448-
+ "[{}] "
1449-
+ functionType
1450-
+ " can be used as part of an OR condition, but only if other full text functions are used as part of the condition",
1451-
functionInvocation,
1452-
functionName
1428+
"{}: Invalid condition [{}]. [{}] {} can be used in an OR condition, "
1429+
+ "but only if just full text functions are used in the OR condition",
1430+
position,
1431+
expression,
1432+
functionName,
1433+
functionType
14531434
),
1454-
error("from test | where " + functionInvocation + " or (last_name is not null and first_name is null)")
1435+
error("from test | where " + expression)
14551436
);
14561437
}
14571438

@@ -1465,35 +1446,23 @@ public void testFullTextFunctionsDisjunctions() {
14651446
}
14661447

14671448
private void checkWithFullTextFunctionsDisjunctions(String functionName, String functionInvocation, String functionType) {
1449+
1450+
String expression = functionInvocation + " or length(first_name) > 10";
1451+
checkdisjunctionError("1:19", expression, functionName, functionType);
1452+
1453+
expression = "match(last_name, \"Anneke\") or (" + functionInvocation + " and length(first_name) > 10)";
1454+
checkdisjunctionError("1:19", expression, functionName, functionType);
1455+
1456+
expression = "("
1457+
+ functionInvocation
1458+
+ " and length(first_name) > 0) or (match(last_name, \"Anneke\") and length(first_name) > 10)";
1459+
checkdisjunctionError("1:19", expression, functionName, functionType);
1460+
14681461
passes("from test | where " + functionInvocation + " or match(first_name, \"Anna\")");
14691462
passes("from test | where " + functionInvocation + " or not match(first_name, \"Anna\")");
14701463
passes("from test | where (" + functionInvocation + " or match(first_name, \"Anna\")) and length(first_name) > 10");
14711464
passes("from test | where (" + functionInvocation + " or match(first_name, \"Anna\")) and match(last_name, \"Smith\")");
14721465
passes("from test | where " + functionInvocation + " or (match(first_name, \"Anna\") and match(last_name, \"Smith\"))");
1473-
1474-
assertEquals(
1475-
LoggerMessageFormat.format(
1476-
null,
1477-
"1:19: Invalid condition [{} or length(first_name) > 10]. [{}] {} can be used as part of an OR condition, "
1478-
+ "but only if other full text functions are used as part of the condition",
1479-
functionInvocation,
1480-
functionName,
1481-
functionType
1482-
),
1483-
error("from test | where " + functionInvocation + " or length(first_name) > 10")
1484-
);
1485-
assertEquals(
1486-
LoggerMessageFormat.format(
1487-
null,
1488-
"1:19: Invalid condition [{} or (match(last_name, \"Anneke\") and length(first_name) > 10)]."
1489-
+ " [{}] {} can be used as part of an OR condition, "
1490-
+ "but only if other full text functions are used as part of the condition",
1491-
functionInvocation,
1492-
functionName,
1493-
functionType
1494-
),
1495-
error("from test | where " + functionInvocation + " or (match(last_name, \"Anneke\") and length(first_name) > 10)")
1496-
);
14971466
}
14981467

14991468
public void testQueryStringFunctionWithNonBooleanFunctions() {

x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/180_match_operator.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ setup:
170170
- match: { error.reason: "Found 1 problem\nline 1:36: Unknown column [content], did you mean [count(*)]?" }
171171

172172
---
173-
"match with functions":
173+
"match with disjunctions":
174174
- do:
175175
catch: bad_request
176176
allowed_warnings_regex:
@@ -183,6 +183,19 @@ setup:
183183
- match: { error.type: verification_exception }
184184
- match: { error.reason: "/.+Invalid\\ condition\\ \\[content\\:\"fox\"\\ OR\\ to_upper\\(content\\)\\ ==\\ \"FOX\"\\]\\.\\ \\[\\:\\]\\ operator.+/" }
185185

186+
- do:
187+
catch: bad_request
188+
allowed_warnings_regex:
189+
- "No limit defined, adding default limit of \\[.*\\]"
190+
esql.query:
191+
body:
192+
query: 'FROM test | WHERE content:"fox" OR to_upper(content) == "FOX"'
193+
194+
- match: { status: 400 }
195+
- match: { error.type: verification_exception }
196+
- match: { error.reason: "/.+Invalid\\ condition\\ \\[content\\:\"fox\"\\ OR\\ to_upper\\(content\\)\\ ==\\ \"FOX\"\\]\\.\\ \\[\\:\\]\\ operator.+/" }
197+
198+
186199
---
187200
"match within eval":
188201
- do:

0 commit comments

Comments
 (0)