From 555d7d95e2acacb91ea49582b3e126faa1cfc280 Mon Sep 17 00:00:00 2001 From: Fang Xing Date: Thu, 23 Jul 2026 21:48:58 -0400 Subject: [PATCH 1/2] fix in suqbuery with QSTR/KQL --- .../src/main/resources/in_subquery.csv-spec | 172 ++++++++++++++++++ .../xpack/esql/action/EsqlCapabilities.java | 8 + .../esql/analysis/InSubqueryResolver.java | 5 +- .../function/fulltext/FullTextFunction.java | 77 +++++++- .../logical/PushDownAndCombineFilters.java | 12 +- ...calPlanOptimizerInSubqueryGoldenTests.java | 30 +++ .../logical_optimization.expected | 10 +- .../testKqlOrInSubquery/analysis.expected | 8 + .../logical_optimization.expected | 8 + .../testKqlOrInSubquery/query.esql | 2 + .../testKqlOrNotInSubquery/analysis.expected | 8 + .../logical_optimization.expected | 8 + .../testKqlOrNotInSubquery/query.esql | 2 + .../testQstrOrInSubquery/analysis.expected | 8 + .../logical_optimization.expected | 8 + .../testQstrOrInSubquery/query.esql | 2 + .../testQstrOrNotInSubquery/analysis.expected | 8 + .../logical_optimization.expected | 8 + .../testQstrOrNotInSubquery/query.esql | 2 + 19 files changed, 376 insertions(+), 10 deletions(-) create mode 100644 x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrInSubquery/analysis.expected create mode 100644 x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrInSubquery/logical_optimization.expected create mode 100644 x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrInSubquery/query.esql create mode 100644 x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrNotInSubquery/analysis.expected create mode 100644 x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrNotInSubquery/logical_optimization.expected create mode 100644 x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrNotInSubquery/query.esql create mode 100644 x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrInSubquery/analysis.expected create mode 100644 x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrInSubquery/logical_optimization.expected create mode 100644 x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrInSubquery/query.esql create mode 100644 x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrNotInSubquery/analysis.expected create mode 100644 x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrNotInSubquery/logical_optimization.expected create mode 100644 x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrNotInSubquery/query.esql diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/in_subquery.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/in_subquery.csv-spec index 9a2e816be0336..5fa9d4d4c612e 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/in_subquery.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/in_subquery.csv-spec @@ -2863,3 +2863,175 @@ FROM employees, employees_incompatible METADATA _index c:long | _index:keyword 100 | employees_incompatible ; + +kqlOrInSubquery +required_capability: where_in_subquery_without_view +required_capability: kql_function +required_capability: where_in_subquery_with_qstr_kql_fix + +FROM books +| WHERE kql("author: William") OR book_no IN (FROM books | WHERE book_no == "9032" | KEEP book_no) +| SORT book_no +| KEEP book_no, author +; + +book_no:keyword | author:text +2713 | William Faulkner +2883 | William Faulkner +4724 | William Faulkner +4977 | William Faulkner +5119 | William Faulkner +5404 | William Faulkner +5578 | William Faulkner +8077 | William Faulkner +9032 | Tolstoy Leo +9896 | William Faulkner +; + + +qstrOrNotInSubquery +required_capability: where_in_subquery_without_view +required_capability: qstr_function +required_capability: where_in_subquery_with_qstr_kql_fix + +FROM books +| WHERE qstr("author: William") OR book_no NOT IN (FROM books | WHERE book_no == "9032" | KEEP book_no) +| SORT book_no +| KEEP book_no, author +| LIMIT 10 +; + +book_no:keyword | author:text +1211 | Fyodor Dostoevsky +1463 | J. R. R. Tolkien +1502 | Nikolai Vasilevich Gogol +1937 | Fyodor Dostoevsky +1985 | Fyodor Dostoevsky +2130 | [Christopher Tolkien, John Ronald Reuel Tolkien] +2236 | Ursula K. Le Guin +2301 | John Ronald Reuel Tolkien +2378 | [Carol Faulkner, Holly Byers Ochoa, Lucretia Mott] +2382 | Ursula K. Le Guin +; + +// Tests for the hash-join execution path ($$sv synthetic attribute). +// pragma: in_subquery_hash_join_threshold=0 forces any non-empty subquery result into +// the hash-join branch regardless of row count. The join key (job_positions) is a +// genuinely multi-valued keyword field in the employees index, so MvSingleValueOrNull +// is exercised on real multi-valued data. Employees whose job_positions is multi-valued +// are excluded by the MvSingleValueOrNull + IsNotNull guard that the hash-join inserts. +// +// AND variants (SemiJoin / AntiJoin): QSTR/KQL filters run before MvSingleValueOrNull, +// so only the employees that already passed the full-text filter are evaluated against +// the guard; we pick a last_name that matches only single-valued employees to keep the +// expected output deterministic and warning-free. +// +// OR variants (MarkJoin): all rows are evaluated by MvSingleValueOrNull regardless of +// whether they match the full-text condition, so multi-valued employees produce warnings. +// warningRegex:.* permits those warnings without constraining their exact text. + +qstrAndInSubqueryHashJoinPathMultiValuedField +required_capability: where_in_subquery_without_view +required_capability: qstr_function +required_capability: where_in_subquery_with_qstr_kql_fix + +pragma: in_subquery_hash_join_threshold=0 + +FROM employees +| WHERE QSTR("last_name: Simmel") AND job_positions IN (FROM employees | WHERE emp_no == 10002 | KEEP job_positions) +| SORT emp_no +| KEEP emp_no, last_name +; + +emp_no:integer | last_name:keyword +10002 | Simmel +; + +kqlAndInSubqueryHashJoinPathMultiValuedField +required_capability: where_in_subquery_without_view +required_capability: kql_function +required_capability: where_in_subquery_with_qstr_kql_fix + +pragma: in_subquery_hash_join_threshold=0 + +FROM employees +| WHERE KQL("last_name: Simmel") AND job_positions IN (FROM employees | WHERE emp_no == 10002 | KEEP job_positions) +| SORT emp_no +| KEEP emp_no, last_name +; + +emp_no:integer | last_name:keyword +10002 | Simmel +; + +qstrAndNotInSubqueryHashJoinPathMultiValuedField +required_capability: where_in_subquery_without_view +required_capability: qstr_function +required_capability: where_in_subquery_with_qstr_kql_fix + +pragma: in_subquery_hash_join_threshold=0 + +FROM employees +| WHERE QSTR("last_name: Warwick") AND job_positions NOT IN (FROM employees | WHERE emp_no == 10002 | KEEP job_positions) +| SORT emp_no +| KEEP emp_no, last_name +; + +emp_no:integer | last_name:keyword +10020 | Warwick +; + +kqlAndNotInSubqueryHashJoinPathMultiValuedField +required_capability: where_in_subquery_without_view +required_capability: kql_function +required_capability: where_in_subquery_with_qstr_kql_fix + +pragma: in_subquery_hash_join_threshold=0 + +FROM employees +| WHERE KQL("last_name: Warwick") AND job_positions NOT IN (FROM employees | WHERE emp_no == 10002 | KEEP job_positions) +| SORT emp_no +| KEEP emp_no, last_name +; + +emp_no:integer | last_name:keyword +10020 | Warwick +; + +qstrOrInSubqueryHashJoinPathMultiValuedField +required_capability: where_in_subquery_without_view +required_capability: qstr_function +required_capability: where_in_subquery_with_qstr_kql_fix + +pragma: in_subquery_hash_join_threshold=0 + +FROM employees +| WHERE QSTR("last_name: Simmel") OR job_positions IN (FROM employees | WHERE emp_no == 10056 | KEEP job_positions) +| SORT emp_no +| KEEP emp_no, last_name +; +warningRegex:.* + +emp_no:integer | last_name:keyword +10002 | Simmel +10056 | Bernini +; + +kqlOrInSubqueryHashJoinPathMultiValuedField +required_capability: where_in_subquery_without_view +required_capability: kql_function +required_capability: where_in_subquery_with_qstr_kql_fix + +pragma: in_subquery_hash_join_threshold=0 + +FROM employees +| WHERE KQL("last_name: Simmel") OR job_positions IN (FROM employees | WHERE emp_no == 10056 | KEEP job_positions) +| SORT emp_no +| KEEP emp_no, last_name +; +warningRegex:.* + +emp_no:integer | last_name:keyword +10002 | Simmel +10056 | Bernini +; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index 5b1ba094ce760..91b0be12e2399 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java @@ -1481,6 +1481,14 @@ public enum Cap { * correct time-series index when a join presents. */ WHERE_IN_SUBQUERY_WITH_TS, + + /** + * Allow disjunctive IN subquery work with QSTR/KQL, including the fix for the KQL/QSTR positional check failing with a + * VerificationException when the subquery is inlined via the hash-join and there is a synthetic attribute in the Eval before + * QSTR or KQL. + */ + WHERE_IN_SUBQUERY_WITH_QSTR_KQL_FIX, + /** * Support for views in cluster state (and REST API). */ diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/InSubqueryResolver.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/InSubqueryResolver.java index acf8085fa0849..efa3006ba7310 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/InSubqueryResolver.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/InSubqueryResolver.java @@ -315,12 +315,15 @@ private static String syntheticConstName(Expression value, LogicalPlan subquery) return "$$in_subquery_const$" + value.hashCode() + "$" + subquery.hashCode(); } + /** Name prefix shared by all synthetic boolean mark attributes created for {@link MarkJoin}. */ + public static final String MARK_ATTRIBUTE_NAME_PREFIX = "$$in_subquery_mark$"; + /** * Generates a unique synthetic name for the boolean mark attribute produced by a * {@link MarkJoin} in place of an {@link InSubquery}. */ private static String syntheticMarkName(InSubquery inSubquery) { - return "$$in_subquery_mark$" + inSubquery.value().hashCode() + "$" + inSubquery.subquery().hashCode(); + return MARK_ATTRIBUTE_NAME_PREFIX + inSubquery.value().hashCode() + "$" + inSubquery.subquery().hashCode(); } public static void verify(LogicalPlan plan) { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextFunction.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextFunction.java index 39aedaf815fc3..d38cc6e41d6f6 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextFunction.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/FullTextFunction.java @@ -17,6 +17,7 @@ import org.elasticsearch.compute.operator.ScoreOperator; import org.elasticsearch.index.IndexMode; import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.xpack.esql.analysis.InSubqueryResolver; import org.elasticsearch.xpack.esql.capabilities.PostAnalysisPlanVerificationAware; import org.elasticsearch.xpack.esql.capabilities.PostOptimizationPlanVerificationAware; import org.elasticsearch.xpack.esql.capabilities.PostOptimizationVerificationAware; @@ -50,6 +51,7 @@ import org.elasticsearch.xpack.esql.plan.logical.Aggregate; import org.elasticsearch.xpack.esql.plan.logical.Dedup; import org.elasticsearch.xpack.esql.plan.logical.EsRelation; +import org.elasticsearch.xpack.esql.plan.logical.Eval; import org.elasticsearch.xpack.esql.plan.logical.ExternalRelation; import org.elasticsearch.xpack.esql.plan.logical.Filter; import org.elasticsearch.xpack.esql.plan.logical.Fork; @@ -64,6 +66,7 @@ import org.elasticsearch.xpack.esql.plan.logical.Sample; import org.elasticsearch.xpack.esql.plan.logical.TopNBy; import org.elasticsearch.xpack.esql.plan.logical.UnionAll; +import org.elasticsearch.xpack.esql.plan.logical.join.AbstractSubqueryJoin; import org.elasticsearch.xpack.esql.plan.logical.join.InlineJoin; import org.elasticsearch.xpack.esql.plan.logical.join.Join; import org.elasticsearch.xpack.esql.plan.logical.join.LookupJoin; @@ -77,6 +80,7 @@ import java.util.Locale; import java.util.Objects; import java.util.function.BiConsumer; +import java.util.function.Consumer; import java.util.function.Predicate; import static org.elasticsearch.xpack.esql.common.Failure.fail; @@ -299,7 +303,8 @@ private static void checkFullTextQueryFunctionForCondition( || lp instanceof OrderBy || lp instanceof EsRelation || lp instanceof ParameterizedQuery - || lp instanceof Sample), + || lp instanceof Sample + || lp instanceof AbstractSubqueryJoin), fullTextFunction -> "[" + fullTextFunction.functionName() + "] " + fullTextFunction.functionType(), failures ); @@ -358,6 +363,72 @@ private static void checkFullTextFunctionsInAggs(Aggregate agg, Failures failure * @param failures failures to add errors to * @param class of the type to look for */ + /** + * Traverses the plan depth-first like {@link LogicalPlan#forEachDown}, but with two exceptions: + *
    + *
  • The right (subquery) side of any {@link AbstractSubqueryJoin} is skipped entirely — + * it executes independently and its nodes are not "between" the full-text function and + * its data source.
  • + *
  • Nodes synthesised by {@link AbstractSubqueryJoin#inlineData} to materialise + * {@link org.elasticsearch.xpack.esql.plan.logical.join.MarkJoin} mark attributes are + * treated as opaque boundaries: traversal stops at these nodes and does not + * recurse into their children. Stopping is correct for two independent reasons: + *
      + *
    1. Synthetic nodes are not user commands. Directly below the boundary node + * sit implementation artefacts introduced by {@code inlineData} — a mark + * {@link Eval}, a {@link org.elasticsearch.xpack.esql.plan.logical.join.Join}, + * an {@code $$<field>$sv} {@link Eval}, and a + * {@link org.elasticsearch.xpack.esql.plan.logical.local.LocalRelation} — none of + * which are in the QSTR/KQL allowlist, but none of which are user-written commands + * that could place a full-text function in a disallowed position.
    2. + *
    3. The user-written subtree was already verified. The only user-written + * content embedded below the boundary is {@link AbstractSubqueryJoin#left()}. + * Before {@code inlineData} ran, the coordinator-side verifier walked through the + * {@link org.elasticsearch.xpack.esql.plan.logical.join.MarkJoin} node (allowed as + * an {@link AbstractSubqueryJoin}) and recursed into {@code left()}, checking every + * node there. Any disallowed user command in {@code left()} would have been caught + * at that point.
    4. + *
    + * See {@link #isMarkJoinInlinedNode} for the boundary patterns detected.
  • + *
+ */ + private static void forEachDownExcludingSubqueryRightSide(LogicalPlan plan, Consumer consumer) { + if (isMarkJoinInlinedNode(plan)) { + return; // synthetic inlining artefacts above; user-written left() already verified at coordinator time + } + consumer.accept(plan); + List children = plan instanceof AbstractSubqueryJoin subqueryJoin ? List.of(subqueryJoin.left()) : plan.children(); + for (LogicalPlan child : children) { + forEachDownExcludingSubqueryRightSide(child, consumer); + } + } + + /** + * Returns {@code true} when {@code plan} is a node synthesised by + * {@link AbstractSubqueryJoin#inlineData} to materialise a boolean mark attribute, which comes + * in two shapes: + *
    + *
  • Filter path / empty / short-circuit: a bare {@link Eval} whose every alias + * carries the {@link InSubqueryResolver#MARK_ATTRIBUTE_NAME_PREFIX} name prefix.
  • + *
  • Hash-join path: a {@link Project} whose direct child is such a mark + * {@link Eval} (produced by + * {@link org.elasticsearch.xpack.esql.plan.logical.join.MarkJoin#buildHashJoinPathPlan}). + *
  • + *
+ * Both shapes are implementation artefacts that must not be treated as user-written commands + * in the positional check. + */ + private static boolean isMarkJoinInlinedNode(LogicalPlan plan) { + if (plan instanceof Eval eval) { + return eval.fields().isEmpty() == false + && eval.fields().stream().allMatch(alias -> alias.name().startsWith(InSubqueryResolver.MARK_ATTRIBUTE_NAME_PREFIX)); + } + if (plan instanceof Project project && project.child() instanceof Eval eval) { + return isMarkJoinInlinedNode(eval); + } + return false; + } + private static void checkCommandsBeforeExpression( LogicalPlan plan, Expression condition, @@ -367,7 +438,7 @@ private static void checkCommandsBeforeExpression( Failures failures ) { condition.forEachDown(typeToken, exp -> { - plan.forEachDown(LogicalPlan.class, lp -> { + forEachDownExcludingSubqueryRightSide(plan, lp -> { // `checkCommandsBeforeExpression` should be completely skipped for search functions that do not operate on index fields, // but for now all checks apply, except for MV_EXPAND which can be used before a runtime search function if ((lp instanceof MvExpand && exp instanceof FullTextFunction ftf && ftf.isRuntimeSearch()) == false @@ -712,7 +783,7 @@ private static boolean isInCurrentNode(LogicalPlan plan, FullTextFunction functi private static boolean hasSubqueryInChildrenPlans(LogicalPlan plan) { Holder hasSubquery = new Holder<>(false); plan.forEachDown(p -> { - if (p instanceof UnionAll) { + if (p instanceof UnionAll || p instanceof AbstractSubqueryJoin) { hasSubquery.set(true); } }); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineFilters.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineFilters.java index 700ff6b72bfd5..adc81bd5f1a26 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineFilters.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/PushDownAndCombineFilters.java @@ -30,6 +30,7 @@ import org.elasticsearch.xpack.esql.plan.logical.RegexExtract; import org.elasticsearch.xpack.esql.plan.logical.UnaryPlan; import org.elasticsearch.xpack.esql.plan.logical.inference.InferencePlan; +import org.elasticsearch.xpack.esql.plan.logical.join.AbstractSubqueryJoin; import org.elasticsearch.xpack.esql.plan.logical.join.InlineJoin; import org.elasticsearch.xpack.esql.plan.logical.join.Join; import org.elasticsearch.xpack.esql.plan.logical.join.JoinTypes; @@ -174,7 +175,7 @@ private static LogicalPlan pushDownPastJoin(Filter filter, Join join, FoldContex LogicalPlan plan = filter; // pushdown only through LEFT joins // TODO: generalize this for other join types - if (join.config().type() == JoinTypes.LEFT) { + if (join.config().type() == JoinTypes.LEFT || join instanceof AbstractSubqueryJoin) { LogicalPlan left = join.left(); LogicalPlan right = join.right(); var conjunctions = Predicates.splitAnd(filter.condition()); @@ -208,7 +209,14 @@ private static LogicalPlan pushDownPastJoin(Filter filter, Join join, FoldContex // motivation does not apply. The duplicate filter above the join already enforces the // predicate. The only producer today is AbstractSubqueryJoin.inlineAsHashJoin's sentinel filter // (which is a no-op against the constant-TRUE sentinel column anyway). - if (scoped.rightFilters.isEmpty() == false && join instanceof InlineJoin == false && right instanceof LocalRelation == false) { + // AbstractSubqueryJoin: never push filters to the right (subquery) side. The subquery executes + // independently and produces the IN-list values; injecting a filter there would silently + // shrink that value set and corrupt the join semantics. Left-side filters have already + // been handled above. + if (scoped.rightFilters.isEmpty() == false + && join instanceof InlineJoin == false + && right instanceof LocalRelation == false + && join instanceof AbstractSubqueryJoin == false) { List rightPushableFilters = buildRightPushableFilters(scoped.rightFilters, foldCtx); if (rightPushableFilters.isEmpty() == false) { if (join.right() instanceof Filter existingRightFilter) { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerInSubqueryGoldenTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerInSubqueryGoldenTests.java index be1e8e531aa03..1ffe7820ff77c 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerInSubqueryGoldenTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/LogicalPlanOptimizerInSubqueryGoldenTests.java @@ -202,6 +202,36 @@ public void testMultipleTsSubqueriesInsideNotInSubquery() { """, STAGES, TransportVersion.current()); } + // -- KQL / QSTR combined with OR (NOT) IN subqueries -- + + public void testQstrOrInSubquery() { + runGoldenTest(""" + FROM employees + | WHERE QSTR("last_name: Smith") OR emp_no IN (FROM employees | WHERE salary > 70000 | KEEP emp_no) + """, STAGES); + } + + public void testKqlOrInSubquery() { + runGoldenTest(""" + FROM employees + | WHERE KQL("last_name: Smith") OR emp_no IN (FROM employees | WHERE salary > 70000 | KEEP emp_no) + """, STAGES); + } + + public void testQstrOrNotInSubquery() { + runGoldenTest(""" + FROM employees + | WHERE QSTR("last_name: Smith") OR emp_no NOT IN (FROM employees | WHERE salary > 70000 | KEEP emp_no) + """, STAGES); + } + + public void testKqlOrNotInSubquery() { + runGoldenTest(""" + FROM employees + | WHERE KQL("last_name: Smith") OR emp_no NOT IN (FROM employees | WHERE salary > 70000 | KEEP emp_no) + """, STAGES); + } + // -- IN / NOT IN subqueries referencing views -- public void testInSubqueryReferencingView() { diff --git a/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testDisjunctiveInSubqueryWithFork/logical_optimization.expected b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testDisjunctiveInSubqueryWithFork/logical_optimization.expected index 6c09207de125a..66ca8f5aaaf95 100644 --- a/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testDisjunctiveInSubqueryWithFork/logical_optimization.expected +++ b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testDisjunctiveInSubqueryWithFork/logical_optimization.expected @@ -4,16 +4,18 @@ Project[[avg_worked_seconds{r}#0, birth_date{r}#1, emp_no{r}#2, first_name{r}#3, |_Project[[avg_worked_seconds{f}#25, birth_date{f}#26, emp_no{f}#27, first_name{f}#28, gender{f}#29, height{f}#30, height.float{f}#31, height.half_float{f}#32, height.scaled_float{f}#33, hire_date{f}#34, is_rehired{f}#35, job_positions{f}#36, languages{f}#37, languages.byte{f}#38, languages.long{f}#39, languages.short{f}#40, last_name{f}#41, salary{f}#42, salary_change{f}#43, salary_change.int{f}#44, salary_change.keyword{f}#45, salary_change.long{f}#46, still_hired{f}#47, $$in_subquery_mark$0{r$}#48, _fork{r}#49]] | \_Eval[[fork1[KEYWORD] AS _fork#49]] | \_Limit[1000[INTEGER],false,false] - | \_Filter[$$in_subquery_mark$0{r$}#48 OR salary{f}#42 > 50000[INTEGER] AND emp_no{f}#27 > 10000[INTEGER]] + | \_Filter[$$in_subquery_mark$0{r$}#48 OR salary{f}#42 > 50000[INTEGER]] | \_MarkJoin[[emp_no{f}#27],[emp_no{f}#50],$$in_subquery_mark$0{r$}#48] - | |_EsRelation[employees][avg_worked_seconds{f}#25, birth_date{f}#26, emp_no{f}#27, first_name{f}#28, gender{f}#29, height{f}#30, height.float{f}#31, height.half_float{f}#32, height.scaled_float{f}#33, hire_date{f}#34, is_rehired{f}#35, job_positions{f}#36, languages{f}#37, languages.byte{f}#38, languages.long{f}#39, languages.short{f}#40, last_name{f}#41, salary{f}#42, salary_change{f}#43, salary_change.int{f}#44, salary_change.keyword{f}#45, salary_change.long{f}#46, still_hired{f}#47] + | |_Filter[emp_no{f}#27 > 10000[INTEGER]] + | | \_EsRelation[employees][avg_worked_seconds{f}#25, birth_date{f}#26, emp_no{f}#27, first_name{f}#28, gender{f}#29, height{f}#30, height.float{f}#31, height.half_float{f}#32, height.scaled_float{f}#33, hire_date{f}#34, is_rehired{f}#35, job_positions{f}#36, languages{f}#37, languages.byte{f}#38, languages.long{f}#39, languages.short{f}#40, last_name{f}#41, salary{f}#42, salary_change{f}#43, salary_change.int{f}#44, salary_change.keyword{f}#45, salary_change.long{f}#46, still_hired{f}#47] | \_Project[[emp_no{f}#50]] | \_EsRelation[employees][avg_worked_seconds{f}#51, birth_date{f}#52, emp_no{f}#50, first_name{f}#53, gender{f}#54, height{f}#55, height.float{f}#56, height.half_float{f}#57, height.scaled_float{f}#58, hire_date{f}#59, is_rehired{f}#60, job_positions{f}#61, languages{f}#62, languages.byte{f}#63, languages.long{f}#64, languages.short{f}#65, last_name{f}#66, salary{f}#67, salary_change{f}#68, salary_change.int{f}#69, salary_change.keyword{f}#70, salary_change.long{f}#71, still_hired{f}#72] \_Project[[avg_worked_seconds{f}#73, birth_date{f}#74, emp_no{f}#75, first_name{f}#76, gender{f}#77, height{f}#78, height.float{f}#79, height.half_float{f}#80, height.scaled_float{f}#81, hire_date{f}#82, is_rehired{f}#83, job_positions{f}#84, languages{f}#85, languages.byte{f}#86, languages.long{f}#87, languages.short{f}#88, last_name{f}#89, salary{f}#90, salary_change{f}#91, salary_change.int{f}#92, salary_change.keyword{f}#93, salary_change.long{f}#94, still_hired{f}#95, $$in_subquery_mark$0{r$}#96, _fork{r}#97]] \_Eval[[fork2[KEYWORD] AS _fork#97]] \_Limit[1000[INTEGER],false,false] - \_Filter[$$in_subquery_mark$0{r$}#96 OR salary{f}#90 > 50000[INTEGER] AND emp_no{f}#75 < 10050[INTEGER]] + \_Filter[$$in_subquery_mark$0{r$}#96 OR salary{f}#90 > 50000[INTEGER]] \_MarkJoin[[emp_no{f}#75],[emp_no{f}#98],$$in_subquery_mark$0{r$}#96] - |_EsRelation[employees][avg_worked_seconds{f}#73, birth_date{f}#74, emp_no{f}#75, first_name{f}#76, gender{f}#77, height{f}#78, height.float{f}#79, height.half_float{f}#80, height.scaled_float{f}#81, hire_date{f}#82, is_rehired{f}#83, job_positions{f}#84, languages{f}#85, languages.byte{f}#86, languages.long{f}#87, languages.short{f}#88, last_name{f}#89, salary{f}#90, salary_change{f}#91, salary_change.int{f}#92, salary_change.keyword{f}#93, salary_change.long{f}#94, still_hired{f}#95] + |_Filter[emp_no{f}#75 < 10050[INTEGER]] + | \_EsRelation[employees][avg_worked_seconds{f}#73, birth_date{f}#74, emp_no{f}#75, first_name{f}#76, gender{f}#77, height{f}#78, height.float{f}#79, height.half_float{f}#80, height.scaled_float{f}#81, hire_date{f}#82, is_rehired{f}#83, job_positions{f}#84, languages{f}#85, languages.byte{f}#86, languages.long{f}#87, languages.short{f}#88, last_name{f}#89, salary{f}#90, salary_change{f}#91, salary_change.int{f}#92, salary_change.keyword{f}#93, salary_change.long{f}#94, still_hired{f}#95] \_Project[[emp_no{f}#98]] \_EsRelation[employees][avg_worked_seconds{f}#99, birth_date{f}#100, emp_no{f}#98, first_name{f}#101, gender{f}#102, height{f}#103, height.float{f}#104, height.half_float{f}#105, height.scaled_float{f}#106, hire_date{f}#107, is_rehired{f}#108, job_positions{f}#109, languages{f}#110, languages.byte{f}#111, languages.long{f}#112, languages.short{f}#113, last_name{f}#114, salary{f}#115, salary_change{f}#116, salary_change.int{f}#117, salary_change.keyword{f}#118, salary_change.long{f}#119, still_hired{f}#120] \ No newline at end of file diff --git a/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrInSubquery/analysis.expected b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrInSubquery/analysis.expected new file mode 100644 index 0000000000000..28d19bb665d32 --- /dev/null +++ b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrInSubquery/analysis.expected @@ -0,0 +1,8 @@ +Project[[avg_worked_seconds{f}#0, birth_date{f}#1, emp_no{f}#2, first_name{f}#3, gender{f}#4, height{f}#5, height.float{f}#6, height.half_float{f}#7, height.scaled_float{f}#8, hire_date{f}#9, is_rehired{f}#10, job_positions{f}#11, languages{f}#12, languages.byte{f}#13, languages.long{f}#14, languages.short{f}#15, last_name{f}#16, salary{f}#17, salary_change{f}#18, salary_change.int{f}#19, salary_change.keyword{f}#20, salary_change.long{f}#21, still_hired{f}#22]] +\_Limit[1000[INTEGER],false,false] + \_Filter[KQL(last_name: Smith[KEYWORD]) OR $$in_subquery_mark$0{r$}#23] + \_MarkJoin[[emp_no{f}#2],[emp_no{f}#24],$$in_subquery_mark$0{r$}#23] + |_EsRelation[employees][avg_worked_seconds{f}#0, birth_date{f}#1, emp_no{f}#2, first_name{f}#3, gender{f}#4, height{f}#5, height.float{f}#6, height.half_float{f}#7, height.scaled_float{f}#8, hire_date{f}#9, is_rehired{f}#10, job_positions{f}#11, languages{f}#12, languages.byte{f}#13, languages.long{f}#14, languages.short{f}#15, last_name{f}#16, salary{f}#17, salary_change{f}#18, salary_change.int{f}#19, salary_change.keyword{f}#20, salary_change.long{f}#21, still_hired{f}#22] + \_Project[[emp_no{f}#24]] + \_Filter[salary{f}#25 > 70000[INTEGER]] + \_EsRelation[employees][avg_worked_seconds{f}#26, birth_date{f}#27, emp_no{f}#24, first_name{f}#28, gender{f}#29, height{f}#30, height.float{f}#31, height.half_float{f}#32, height.scaled_float{f}#33, hire_date{f}#34, is_rehired{f}#35, job_positions{f}#36, languages{f}#37, languages.byte{f}#38, languages.long{f}#39, languages.short{f}#40, last_name{f}#41, salary{f}#25, salary_change{f}#42, salary_change.int{f}#43, salary_change.keyword{f}#44, salary_change.long{f}#45, still_hired{f}#46] \ No newline at end of file diff --git a/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrInSubquery/logical_optimization.expected b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrInSubquery/logical_optimization.expected new file mode 100644 index 0000000000000..28d19bb665d32 --- /dev/null +++ b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrInSubquery/logical_optimization.expected @@ -0,0 +1,8 @@ +Project[[avg_worked_seconds{f}#0, birth_date{f}#1, emp_no{f}#2, first_name{f}#3, gender{f}#4, height{f}#5, height.float{f}#6, height.half_float{f}#7, height.scaled_float{f}#8, hire_date{f}#9, is_rehired{f}#10, job_positions{f}#11, languages{f}#12, languages.byte{f}#13, languages.long{f}#14, languages.short{f}#15, last_name{f}#16, salary{f}#17, salary_change{f}#18, salary_change.int{f}#19, salary_change.keyword{f}#20, salary_change.long{f}#21, still_hired{f}#22]] +\_Limit[1000[INTEGER],false,false] + \_Filter[KQL(last_name: Smith[KEYWORD]) OR $$in_subquery_mark$0{r$}#23] + \_MarkJoin[[emp_no{f}#2],[emp_no{f}#24],$$in_subquery_mark$0{r$}#23] + |_EsRelation[employees][avg_worked_seconds{f}#0, birth_date{f}#1, emp_no{f}#2, first_name{f}#3, gender{f}#4, height{f}#5, height.float{f}#6, height.half_float{f}#7, height.scaled_float{f}#8, hire_date{f}#9, is_rehired{f}#10, job_positions{f}#11, languages{f}#12, languages.byte{f}#13, languages.long{f}#14, languages.short{f}#15, last_name{f}#16, salary{f}#17, salary_change{f}#18, salary_change.int{f}#19, salary_change.keyword{f}#20, salary_change.long{f}#21, still_hired{f}#22] + \_Project[[emp_no{f}#24]] + \_Filter[salary{f}#25 > 70000[INTEGER]] + \_EsRelation[employees][avg_worked_seconds{f}#26, birth_date{f}#27, emp_no{f}#24, first_name{f}#28, gender{f}#29, height{f}#30, height.float{f}#31, height.half_float{f}#32, height.scaled_float{f}#33, hire_date{f}#34, is_rehired{f}#35, job_positions{f}#36, languages{f}#37, languages.byte{f}#38, languages.long{f}#39, languages.short{f}#40, last_name{f}#41, salary{f}#25, salary_change{f}#42, salary_change.int{f}#43, salary_change.keyword{f}#44, salary_change.long{f}#45, still_hired{f}#46] \ No newline at end of file diff --git a/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrInSubquery/query.esql b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrInSubquery/query.esql new file mode 100644 index 0000000000000..9b867f752bad9 --- /dev/null +++ b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrInSubquery/query.esql @@ -0,0 +1,2 @@ +FROM employees +| WHERE KQL("last_name: Smith") OR emp_no IN (FROM employees | WHERE salary > 70000 | KEEP emp_no) diff --git a/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrNotInSubquery/analysis.expected b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrNotInSubquery/analysis.expected new file mode 100644 index 0000000000000..e06fff9890ce2 --- /dev/null +++ b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrNotInSubquery/analysis.expected @@ -0,0 +1,8 @@ +Project[[avg_worked_seconds{f}#0, birth_date{f}#1, emp_no{f}#2, first_name{f}#3, gender{f}#4, height{f}#5, height.float{f}#6, height.half_float{f}#7, height.scaled_float{f}#8, hire_date{f}#9, is_rehired{f}#10, job_positions{f}#11, languages{f}#12, languages.byte{f}#13, languages.long{f}#14, languages.short{f}#15, last_name{f}#16, salary{f}#17, salary_change{f}#18, salary_change.int{f}#19, salary_change.keyword{f}#20, salary_change.long{f}#21, still_hired{f}#22]] +\_Limit[1000[INTEGER],false,false] + \_Filter[KQL(last_name: Smith[KEYWORD]) OR NOT($$in_subquery_mark$0{r$}#23)] + \_MarkJoin[[emp_no{f}#2],[emp_no{f}#24],$$in_subquery_mark$0{r$}#23] + |_EsRelation[employees][avg_worked_seconds{f}#0, birth_date{f}#1, emp_no{f}#2, first_name{f}#3, gender{f}#4, height{f}#5, height.float{f}#6, height.half_float{f}#7, height.scaled_float{f}#8, hire_date{f}#9, is_rehired{f}#10, job_positions{f}#11, languages{f}#12, languages.byte{f}#13, languages.long{f}#14, languages.short{f}#15, last_name{f}#16, salary{f}#17, salary_change{f}#18, salary_change.int{f}#19, salary_change.keyword{f}#20, salary_change.long{f}#21, still_hired{f}#22] + \_Project[[emp_no{f}#24]] + \_Filter[salary{f}#25 > 70000[INTEGER]] + \_EsRelation[employees][avg_worked_seconds{f}#26, birth_date{f}#27, emp_no{f}#24, first_name{f}#28, gender{f}#29, height{f}#30, height.float{f}#31, height.half_float{f}#32, height.scaled_float{f}#33, hire_date{f}#34, is_rehired{f}#35, job_positions{f}#36, languages{f}#37, languages.byte{f}#38, languages.long{f}#39, languages.short{f}#40, last_name{f}#41, salary{f}#25, salary_change{f}#42, salary_change.int{f}#43, salary_change.keyword{f}#44, salary_change.long{f}#45, still_hired{f}#46] \ No newline at end of file diff --git a/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrNotInSubquery/logical_optimization.expected b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrNotInSubquery/logical_optimization.expected new file mode 100644 index 0000000000000..e06fff9890ce2 --- /dev/null +++ b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrNotInSubquery/logical_optimization.expected @@ -0,0 +1,8 @@ +Project[[avg_worked_seconds{f}#0, birth_date{f}#1, emp_no{f}#2, first_name{f}#3, gender{f}#4, height{f}#5, height.float{f}#6, height.half_float{f}#7, height.scaled_float{f}#8, hire_date{f}#9, is_rehired{f}#10, job_positions{f}#11, languages{f}#12, languages.byte{f}#13, languages.long{f}#14, languages.short{f}#15, last_name{f}#16, salary{f}#17, salary_change{f}#18, salary_change.int{f}#19, salary_change.keyword{f}#20, salary_change.long{f}#21, still_hired{f}#22]] +\_Limit[1000[INTEGER],false,false] + \_Filter[KQL(last_name: Smith[KEYWORD]) OR NOT($$in_subquery_mark$0{r$}#23)] + \_MarkJoin[[emp_no{f}#2],[emp_no{f}#24],$$in_subquery_mark$0{r$}#23] + |_EsRelation[employees][avg_worked_seconds{f}#0, birth_date{f}#1, emp_no{f}#2, first_name{f}#3, gender{f}#4, height{f}#5, height.float{f}#6, height.half_float{f}#7, height.scaled_float{f}#8, hire_date{f}#9, is_rehired{f}#10, job_positions{f}#11, languages{f}#12, languages.byte{f}#13, languages.long{f}#14, languages.short{f}#15, last_name{f}#16, salary{f}#17, salary_change{f}#18, salary_change.int{f}#19, salary_change.keyword{f}#20, salary_change.long{f}#21, still_hired{f}#22] + \_Project[[emp_no{f}#24]] + \_Filter[salary{f}#25 > 70000[INTEGER]] + \_EsRelation[employees][avg_worked_seconds{f}#26, birth_date{f}#27, emp_no{f}#24, first_name{f}#28, gender{f}#29, height{f}#30, height.float{f}#31, height.half_float{f}#32, height.scaled_float{f}#33, hire_date{f}#34, is_rehired{f}#35, job_positions{f}#36, languages{f}#37, languages.byte{f}#38, languages.long{f}#39, languages.short{f}#40, last_name{f}#41, salary{f}#25, salary_change{f}#42, salary_change.int{f}#43, salary_change.keyword{f}#44, salary_change.long{f}#45, still_hired{f}#46] \ No newline at end of file diff --git a/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrNotInSubquery/query.esql b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrNotInSubquery/query.esql new file mode 100644 index 0000000000000..c64a8c164c2bd --- /dev/null +++ b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testKqlOrNotInSubquery/query.esql @@ -0,0 +1,2 @@ +FROM employees +| WHERE KQL("last_name: Smith") OR emp_no NOT IN (FROM employees | WHERE salary > 70000 | KEEP emp_no) diff --git a/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrInSubquery/analysis.expected b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrInSubquery/analysis.expected new file mode 100644 index 0000000000000..4e09b17500d6d --- /dev/null +++ b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrInSubquery/analysis.expected @@ -0,0 +1,8 @@ +Project[[avg_worked_seconds{f}#0, birth_date{f}#1, emp_no{f}#2, first_name{f}#3, gender{f}#4, height{f}#5, height.float{f}#6, height.half_float{f}#7, height.scaled_float{f}#8, hire_date{f}#9, is_rehired{f}#10, job_positions{f}#11, languages{f}#12, languages.byte{f}#13, languages.long{f}#14, languages.short{f}#15, last_name{f}#16, salary{f}#17, salary_change{f}#18, salary_change.int{f}#19, salary_change.keyword{f}#20, salary_change.long{f}#21, still_hired{f}#22]] +\_Limit[1000[INTEGER],false,false] + \_Filter[QSTR(last_name: Smith[KEYWORD]) OR $$in_subquery_mark$0{r$}#23] + \_MarkJoin[[emp_no{f}#2],[emp_no{f}#24],$$in_subquery_mark$0{r$}#23] + |_EsRelation[employees][avg_worked_seconds{f}#0, birth_date{f}#1, emp_no{f}#2, first_name{f}#3, gender{f}#4, height{f}#5, height.float{f}#6, height.half_float{f}#7, height.scaled_float{f}#8, hire_date{f}#9, is_rehired{f}#10, job_positions{f}#11, languages{f}#12, languages.byte{f}#13, languages.long{f}#14, languages.short{f}#15, last_name{f}#16, salary{f}#17, salary_change{f}#18, salary_change.int{f}#19, salary_change.keyword{f}#20, salary_change.long{f}#21, still_hired{f}#22] + \_Project[[emp_no{f}#24]] + \_Filter[salary{f}#25 > 70000[INTEGER]] + \_EsRelation[employees][avg_worked_seconds{f}#26, birth_date{f}#27, emp_no{f}#24, first_name{f}#28, gender{f}#29, height{f}#30, height.float{f}#31, height.half_float{f}#32, height.scaled_float{f}#33, hire_date{f}#34, is_rehired{f}#35, job_positions{f}#36, languages{f}#37, languages.byte{f}#38, languages.long{f}#39, languages.short{f}#40, last_name{f}#41, salary{f}#25, salary_change{f}#42, salary_change.int{f}#43, salary_change.keyword{f}#44, salary_change.long{f}#45, still_hired{f}#46] \ No newline at end of file diff --git a/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrInSubquery/logical_optimization.expected b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrInSubquery/logical_optimization.expected new file mode 100644 index 0000000000000..4e09b17500d6d --- /dev/null +++ b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrInSubquery/logical_optimization.expected @@ -0,0 +1,8 @@ +Project[[avg_worked_seconds{f}#0, birth_date{f}#1, emp_no{f}#2, first_name{f}#3, gender{f}#4, height{f}#5, height.float{f}#6, height.half_float{f}#7, height.scaled_float{f}#8, hire_date{f}#9, is_rehired{f}#10, job_positions{f}#11, languages{f}#12, languages.byte{f}#13, languages.long{f}#14, languages.short{f}#15, last_name{f}#16, salary{f}#17, salary_change{f}#18, salary_change.int{f}#19, salary_change.keyword{f}#20, salary_change.long{f}#21, still_hired{f}#22]] +\_Limit[1000[INTEGER],false,false] + \_Filter[QSTR(last_name: Smith[KEYWORD]) OR $$in_subquery_mark$0{r$}#23] + \_MarkJoin[[emp_no{f}#2],[emp_no{f}#24],$$in_subquery_mark$0{r$}#23] + |_EsRelation[employees][avg_worked_seconds{f}#0, birth_date{f}#1, emp_no{f}#2, first_name{f}#3, gender{f}#4, height{f}#5, height.float{f}#6, height.half_float{f}#7, height.scaled_float{f}#8, hire_date{f}#9, is_rehired{f}#10, job_positions{f}#11, languages{f}#12, languages.byte{f}#13, languages.long{f}#14, languages.short{f}#15, last_name{f}#16, salary{f}#17, salary_change{f}#18, salary_change.int{f}#19, salary_change.keyword{f}#20, salary_change.long{f}#21, still_hired{f}#22] + \_Project[[emp_no{f}#24]] + \_Filter[salary{f}#25 > 70000[INTEGER]] + \_EsRelation[employees][avg_worked_seconds{f}#26, birth_date{f}#27, emp_no{f}#24, first_name{f}#28, gender{f}#29, height{f}#30, height.float{f}#31, height.half_float{f}#32, height.scaled_float{f}#33, hire_date{f}#34, is_rehired{f}#35, job_positions{f}#36, languages{f}#37, languages.byte{f}#38, languages.long{f}#39, languages.short{f}#40, last_name{f}#41, salary{f}#25, salary_change{f}#42, salary_change.int{f}#43, salary_change.keyword{f}#44, salary_change.long{f}#45, still_hired{f}#46] \ No newline at end of file diff --git a/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrInSubquery/query.esql b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrInSubquery/query.esql new file mode 100644 index 0000000000000..4b14c085682cd --- /dev/null +++ b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrInSubquery/query.esql @@ -0,0 +1,2 @@ +FROM employees +| WHERE QSTR("last_name: Smith") OR emp_no IN (FROM employees | WHERE salary > 70000 | KEEP emp_no) diff --git a/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrNotInSubquery/analysis.expected b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrNotInSubquery/analysis.expected new file mode 100644 index 0000000000000..2c9a123710f2f --- /dev/null +++ b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrNotInSubquery/analysis.expected @@ -0,0 +1,8 @@ +Project[[avg_worked_seconds{f}#0, birth_date{f}#1, emp_no{f}#2, first_name{f}#3, gender{f}#4, height{f}#5, height.float{f}#6, height.half_float{f}#7, height.scaled_float{f}#8, hire_date{f}#9, is_rehired{f}#10, job_positions{f}#11, languages{f}#12, languages.byte{f}#13, languages.long{f}#14, languages.short{f}#15, last_name{f}#16, salary{f}#17, salary_change{f}#18, salary_change.int{f}#19, salary_change.keyword{f}#20, salary_change.long{f}#21, still_hired{f}#22]] +\_Limit[1000[INTEGER],false,false] + \_Filter[QSTR(last_name: Smith[KEYWORD]) OR NOT($$in_subquery_mark$0{r$}#23)] + \_MarkJoin[[emp_no{f}#2],[emp_no{f}#24],$$in_subquery_mark$0{r$}#23] + |_EsRelation[employees][avg_worked_seconds{f}#0, birth_date{f}#1, emp_no{f}#2, first_name{f}#3, gender{f}#4, height{f}#5, height.float{f}#6, height.half_float{f}#7, height.scaled_float{f}#8, hire_date{f}#9, is_rehired{f}#10, job_positions{f}#11, languages{f}#12, languages.byte{f}#13, languages.long{f}#14, languages.short{f}#15, last_name{f}#16, salary{f}#17, salary_change{f}#18, salary_change.int{f}#19, salary_change.keyword{f}#20, salary_change.long{f}#21, still_hired{f}#22] + \_Project[[emp_no{f}#24]] + \_Filter[salary{f}#25 > 70000[INTEGER]] + \_EsRelation[employees][avg_worked_seconds{f}#26, birth_date{f}#27, emp_no{f}#24, first_name{f}#28, gender{f}#29, height{f}#30, height.float{f}#31, height.half_float{f}#32, height.scaled_float{f}#33, hire_date{f}#34, is_rehired{f}#35, job_positions{f}#36, languages{f}#37, languages.byte{f}#38, languages.long{f}#39, languages.short{f}#40, last_name{f}#41, salary{f}#25, salary_change{f}#42, salary_change.int{f}#43, salary_change.keyword{f}#44, salary_change.long{f}#45, still_hired{f}#46] \ No newline at end of file diff --git a/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrNotInSubquery/logical_optimization.expected b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrNotInSubquery/logical_optimization.expected new file mode 100644 index 0000000000000..2c9a123710f2f --- /dev/null +++ b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrNotInSubquery/logical_optimization.expected @@ -0,0 +1,8 @@ +Project[[avg_worked_seconds{f}#0, birth_date{f}#1, emp_no{f}#2, first_name{f}#3, gender{f}#4, height{f}#5, height.float{f}#6, height.half_float{f}#7, height.scaled_float{f}#8, hire_date{f}#9, is_rehired{f}#10, job_positions{f}#11, languages{f}#12, languages.byte{f}#13, languages.long{f}#14, languages.short{f}#15, last_name{f}#16, salary{f}#17, salary_change{f}#18, salary_change.int{f}#19, salary_change.keyword{f}#20, salary_change.long{f}#21, still_hired{f}#22]] +\_Limit[1000[INTEGER],false,false] + \_Filter[QSTR(last_name: Smith[KEYWORD]) OR NOT($$in_subquery_mark$0{r$}#23)] + \_MarkJoin[[emp_no{f}#2],[emp_no{f}#24],$$in_subquery_mark$0{r$}#23] + |_EsRelation[employees][avg_worked_seconds{f}#0, birth_date{f}#1, emp_no{f}#2, first_name{f}#3, gender{f}#4, height{f}#5, height.float{f}#6, height.half_float{f}#7, height.scaled_float{f}#8, hire_date{f}#9, is_rehired{f}#10, job_positions{f}#11, languages{f}#12, languages.byte{f}#13, languages.long{f}#14, languages.short{f}#15, last_name{f}#16, salary{f}#17, salary_change{f}#18, salary_change.int{f}#19, salary_change.keyword{f}#20, salary_change.long{f}#21, still_hired{f}#22] + \_Project[[emp_no{f}#24]] + \_Filter[salary{f}#25 > 70000[INTEGER]] + \_EsRelation[employees][avg_worked_seconds{f}#26, birth_date{f}#27, emp_no{f}#24, first_name{f}#28, gender{f}#29, height{f}#30, height.float{f}#31, height.half_float{f}#32, height.scaled_float{f}#33, hire_date{f}#34, is_rehired{f}#35, job_positions{f}#36, languages{f}#37, languages.byte{f}#38, languages.long{f}#39, languages.short{f}#40, last_name{f}#41, salary{f}#25, salary_change{f}#42, salary_change.int{f}#43, salary_change.keyword{f}#44, salary_change.long{f}#45, still_hired{f}#46] \ No newline at end of file diff --git a/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrNotInSubquery/query.esql b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrNotInSubquery/query.esql new file mode 100644 index 0000000000000..1ef896f2cce8b --- /dev/null +++ b/x-pack/plugin/esql/src/test/resources/org/elasticsearch/xpack/esql/optimizer/golden_tests/LogicalPlanOptimizerInSubqueryGoldenTests/testQstrOrNotInSubquery/query.esql @@ -0,0 +1,2 @@ +FROM employees +| WHERE QSTR("last_name: Smith") OR emp_no NOT IN (FROM employees | WHERE salary > 70000 | KEEP emp_no) From 10b2e0caa79fc5f2a60f55dca31c6f648009298a Mon Sep 17 00:00:00 2001 From: Fang Xing <155562079+fang-xing-esql@users.noreply.github.com> Date: Fri, 24 Jul 2026 20:42:22 -0400 Subject: [PATCH 2/2] Update docs/changelog/155010.yaml --- docs/changelog/155010.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/changelog/155010.yaml diff --git a/docs/changelog/155010.yaml b/docs/changelog/155010.yaml new file mode 100644 index 0000000000000..488e02a18ffee --- /dev/null +++ b/docs/changelog/155010.yaml @@ -0,0 +1,6 @@ +area: ES|QL +issues: + - 154758 +pr: 155010 +summary: Fix `VerificationException` when in subquery is used together with `QSTR`/`KQL` +type: bug