|
@@ -50,6 +50,7 @@ import org.elasticsearch.xpack.esql.plan.physical.EstimatesRowSize;
|
|
|
import org.elasticsearch.xpack.esql.plan.physical.EvalExec;
|
|
|
import org.elasticsearch.xpack.esql.plan.physical.ExchangeExec;
|
|
|
import org.elasticsearch.xpack.esql.plan.physical.FieldExtractExec;
|
|
|
+import org.elasticsearch.xpack.esql.plan.physical.FilterExec;
|
|
|
import org.elasticsearch.xpack.esql.plan.physical.LimitExec;
|
|
|
import org.elasticsearch.xpack.esql.plan.physical.LocalSourceExec;
|
|
|
import org.elasticsearch.xpack.esql.plan.physical.PhysicalPlan;
|
|
@@ -373,6 +374,241 @@ public class LocalPhysicalPlanOptimizerTests extends MapperServiceTestCase {
|
|
|
assertThat(plan.anyMatch(EsQueryExec.class::isInstance), is(true));
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Expecting
|
|
|
+ * LimitExec[1000[INTEGER]]
|
|
|
+ * \_ExchangeExec[[_meta_field{f}#8, emp_no{f}#2, first_name{f}#3, gender{f}#4, job{f}#9, job.raw{f}#10, languages{f}#5, last_na
|
|
|
+ * me{f}#6, long_noidx{f}#11, salary{f}#7],false]
|
|
|
+ * \_ProjectExec[[_meta_field{f}#8, emp_no{f}#2, first_name{f}#3, gender{f}#4, job{f}#9, job.raw{f}#10, languages{f}#5, last_na
|
|
|
+ * me{f}#6, long_noidx{f}#11, salary{f}#7]]
|
|
|
+ * \_FieldExtractExec[_meta_field{f}#8, emp_no{f}#2, first_name{f}#3, gen]
|
|
|
+ * \_EsQueryExec[test], indexMode[standard], query[{"query_string":{"query":"last_name: Smith","fields":[]}}]
|
|
|
+ */
|
|
|
+ public void testQueryStringFunction() {
|
|
|
+ assumeTrue("skipping because QSTR_FUNCTION is not enabled", EsqlCapabilities.Cap.QSTR_FUNCTION.isEnabled());
|
|
|
+ var plan = plannerOptimizer.plan("""
|
|
|
+ from test
|
|
|
+ | where qstr("last_name: Smith")
|
|
|
+ """, IS_SV_STATS);
|
|
|
+
|
|
|
+ var limit = as(plan, LimitExec.class);
|
|
|
+ var exchange = as(limit.child(), ExchangeExec.class);
|
|
|
+ var project = as(exchange.child(), ProjectExec.class);
|
|
|
+ var field = as(project.child(), FieldExtractExec.class);
|
|
|
+ var query = as(field.child(), EsQueryExec.class);
|
|
|
+ assertThat(query.limit().fold(), is(1000));
|
|
|
+ var expected = QueryBuilders.queryStringQuery("last_name: Smith");
|
|
|
+ assertThat(query.query().toString(), is(expected.toString()));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Expecting
|
|
|
+ * LimitExec[1000[INTEGER]]
|
|
|
+ * \_ExchangeExec[[_meta_field{f}#1419, emp_no{f}#1413, first_name{f}#1414, gender{f}#1415, job{f}#1420, job.raw{f}#1421, langua
|
|
|
+ * ges{f}#1416, last_name{f}#1417, long_noidx{f}#1422, salary{f}#1418],false]
|
|
|
+ * \_ProjectExec[[_meta_field{f}#1419, emp_no{f}#1413, first_name{f}#1414, gender{f}#1415, job{f}#1420, job.raw{f}#1421, langua
|
|
|
+ * ges{f}#1416, last_name{f}#1417, long_noidx{f}#1422, salary{f}#1418]]
|
|
|
+ * \_FieldExtractExec[_meta_field{f}#1419, emp_no{f}#1413, first_name{f}#]
|
|
|
+ * \_EsQueryExec[test], indexMode[standard], query[{"bool":{"must":[{"query_string":{"query":"last_name: Smith","fields":[]}}
|
|
|
+ * ,{"esql_single_value":{"field":"emp_no","next":{"range":{"emp_no":{"gt":10010,"boost":1.0}}},"source":"emp_no > 10010"}}],
|
|
|
+ * "boost":1.0}}][_doc{f}#1423], limit[1000], sort[] estimatedRowSize[324]
|
|
|
+ */
|
|
|
+ public void testQueryStringFunctionConjunctionWhereOperands() {
|
|
|
+ assumeTrue("skipping because QSTR_FUNCTION is not enabled", EsqlCapabilities.Cap.QSTR_FUNCTION.isEnabled());
|
|
|
+ String queryText = """
|
|
|
+ from test
|
|
|
+ | where qstr("last_name: Smith") and emp_no > 10010
|
|
|
+ """;
|
|
|
+ var plan = plannerOptimizer.plan(queryText, IS_SV_STATS);
|
|
|
+
|
|
|
+ var limit = as(plan, LimitExec.class);
|
|
|
+ var exchange = as(limit.child(), ExchangeExec.class);
|
|
|
+ var project = as(exchange.child(), ProjectExec.class);
|
|
|
+ var field = as(project.child(), FieldExtractExec.class);
|
|
|
+ var query = as(field.child(), EsQueryExec.class);
|
|
|
+ assertThat(query.limit().fold(), is(1000));
|
|
|
+
|
|
|
+ Source filterSource = new Source(2, 37, "emp_no > 10000");
|
|
|
+ var range = wrapWithSingleQuery(queryText, QueryBuilders.rangeQuery("emp_no").gt(10010), "emp_no", filterSource);
|
|
|
+ var queryString = QueryBuilders.queryStringQuery("last_name: Smith");
|
|
|
+ var expected = QueryBuilders.boolQuery().must(queryString).must(range);
|
|
|
+ assertThat(query.query().toString(), is(expected.toString()));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Expecting
|
|
|
+ * LimitExec[1000[INTEGER]]
|
|
|
+ * \_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
|
|
|
+ * ame{f}#7, long_noidx{f}#12, salary{f}#8],false]
|
|
|
+ * \_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
|
|
|
+ * ame{f}#7, long_noidx{f}#12, salary{f}#8]]
|
|
|
+ * \_FieldExtractExec[_meta_field{f}#9, emp_no{f}#3, first_name{f}#4, gen]
|
|
|
+ * \_EsQueryExec[test], indexMode[standard], query[{"bool":{"should":[{"query_string":{"query":"last_name: Smith","fields":[]}},
|
|
|
+ * {"esql_single_value":{"field":"emp_no","next":{"range":{"emp_no":{"gt":10010,"boost":1.0}}},"source":"emp_no > 10010@2:37"}}],
|
|
|
+ * "boost":1.0}}][_doc{f}#13], limit[1000], sort[] estimatedRowSize[324]
|
|
|
+ */
|
|
|
+ public void testQueryStringFunctionDisjunctionWhereClauses() {
|
|
|
+ assumeTrue("skipping because QSTR_FUNCTION is not enabled", EsqlCapabilities.Cap.QSTR_FUNCTION.isEnabled());
|
|
|
+ String queryText = """
|
|
|
+ from test
|
|
|
+ | where qstr("last_name: Smith") or emp_no > 10010
|
|
|
+ """;
|
|
|
+ var plan = plannerOptimizer.plan(queryText, IS_SV_STATS);
|
|
|
+
|
|
|
+ var limit = as(plan, LimitExec.class);
|
|
|
+ var exchange = as(limit.child(), ExchangeExec.class);
|
|
|
+ var project = as(exchange.child(), ProjectExec.class);
|
|
|
+ var field = as(project.child(), FieldExtractExec.class);
|
|
|
+ var query = as(field.child(), EsQueryExec.class);
|
|
|
+ assertThat(query.limit().fold(), is(1000));
|
|
|
+
|
|
|
+ Source filterSource = new Source(2, 36, "emp_no > 10000");
|
|
|
+ var range = wrapWithSingleQuery(queryText, QueryBuilders.rangeQuery("emp_no").gt(10010), "emp_no", filterSource);
|
|
|
+ var queryString = QueryBuilders.queryStringQuery("last_name: Smith");
|
|
|
+ var expected = QueryBuilders.boolQuery().should(queryString).should(range);
|
|
|
+ assertThat(query.query().toString(), is(expected.toString()));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Expecting
|
|
|
+ * LimitExec[1000[INTEGER]]
|
|
|
+ * \_ExchangeExec[[!alias_integer, boolean{f}#4, byte{f}#5, constant_keyword-foo{f}#6, date{f}#7, double{f}#8, float{f}#9, half_
|
|
|
+ * float{f}#10, integer{f}#12, ip{f}#13, keyword{f}#14, long{f}#15, scaled_float{f}#11, short{f}#17, text{f}#18, unsigned_long{f}#16],
|
|
|
+ * false]
|
|
|
+ * \_ProjectExec[[!alias_integer, boolean{f}#4, byte{f}#5, constant_keyword-foo{f}#6, date{f}#7, double{f}#8, float{f}#9, half_
|
|
|
+ * float{f}#10, integer{f}#12, ip{f}#13, keyword{f}#14, long{f}#15, scaled_float{f}#11, short{f}#17, text{f}#18, unsigned_long{f}#16]
|
|
|
+ * \_FieldExtractExec[!alias_integer, boolean{f}#4, byte{f}#5, constant_k..]
|
|
|
+ * \_EsQueryExec[test], indexMode[standard], query[{"bool":{"must":[{"query_string":{"query":"last_name: Smith","fields":[]}},{
|
|
|
+ * "esql_single_value":{"field":"ip","next":{"terms":{"ip":["127.0.0.1/32"],"boost":1.0}},
|
|
|
+ * "source":"cidr_match(ip, \"127.0.0.1/32\")@2:38"}}],"boost":1.0}}][_doc{f}#21], limit[1000], sort[] estimatedRowSize[354]
|
|
|
+ */
|
|
|
+ public void testQueryStringFunctionWithFunctionsPushedToLucene() {
|
|
|
+ assumeTrue("skipping because QSTR_FUNCTION is not enabled", EsqlCapabilities.Cap.QSTR_FUNCTION.isEnabled());
|
|
|
+ String queryText = """
|
|
|
+ from test
|
|
|
+ | where qstr("last_name: Smith") and cidr_match(ip, "127.0.0.1/32")
|
|
|
+ """;
|
|
|
+ var analyzer = makeAnalyzer("mapping-all-types.json", new EnrichResolution());
|
|
|
+ var plan = plannerOptimizer.plan(queryText, IS_SV_STATS, analyzer);
|
|
|
+
|
|
|
+ var limit = as(plan, LimitExec.class);
|
|
|
+ var exchange = as(limit.child(), ExchangeExec.class);
|
|
|
+ var project = as(exchange.child(), ProjectExec.class);
|
|
|
+ var field = as(project.child(), FieldExtractExec.class);
|
|
|
+ var query = as(field.child(), EsQueryExec.class);
|
|
|
+ assertThat(query.limit().fold(), is(1000));
|
|
|
+
|
|
|
+ Source filterSource = new Source(2, 37, "cidr_match(ip, \"127.0.0.1/32\")");
|
|
|
+ var terms = wrapWithSingleQuery(queryText, QueryBuilders.termsQuery("ip", "127.0.0.1/32"), "ip", filterSource);
|
|
|
+ var queryString = QueryBuilders.queryStringQuery("last_name: Smith");
|
|
|
+ var expected = QueryBuilders.boolQuery().must(queryString).must(terms);
|
|
|
+ assertThat(query.query().toString(), is(expected.toString()));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Expecting
|
|
|
+ *LimitExec[1000[INTEGER]]
|
|
|
+ * \_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
|
|
|
+ * ame{f}#7, long_noidx{f}#12, salary{f}#8],false]
|
|
|
+ * \_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
|
|
|
+ * ame{f}#7, long_noidx{f}#12, salary{f}#8]]
|
|
|
+ * \_FieldExtractExec[_meta_field{f}#9, emp_no{f}#3, gender{f}#5, job{f}#]
|
|
|
+ * \_LimitExec[1000[INTEGER]]
|
|
|
+ * \_FilterExec[LENGTH(first_name{f}#4) > 10[INTEGER]]
|
|
|
+ * \_FieldExtractExec[first_name{f}#4]
|
|
|
+ * \_EsQueryExec[test], indexMode[standard],
|
|
|
+ * query[{"query_string":{"query":"last_name: Smith","fields":[]}}][_doc{f}#13], limit[], sort[] estimatedRowSize[324]
|
|
|
+ */
|
|
|
+ public void testQueryStringFunctionWithFunctionNotPushedDown() {
|
|
|
+ assumeTrue("skipping because QSTR_FUNCTION is not enabled", EsqlCapabilities.Cap.QSTR_FUNCTION.isEnabled());
|
|
|
+ String queryText = """
|
|
|
+ from test
|
|
|
+ | where qstr("last_name: Smith") and length(first_name) > 10
|
|
|
+ """;
|
|
|
+ var plan = plannerOptimizer.plan(queryText, IS_SV_STATS);
|
|
|
+
|
|
|
+ var firstLimit = as(plan, LimitExec.class);
|
|
|
+ var exchange = as(firstLimit.child(), ExchangeExec.class);
|
|
|
+ var project = as(exchange.child(), ProjectExec.class);
|
|
|
+ var field = as(project.child(), FieldExtractExec.class);
|
|
|
+ var secondLimit = as(field.child(), LimitExec.class);
|
|
|
+ var filter = as(secondLimit.child(), FilterExec.class);
|
|
|
+ var fieldExtract = as(filter.child(), FieldExtractExec.class);
|
|
|
+ var query = as(fieldExtract.child(), EsQueryExec.class);
|
|
|
+
|
|
|
+ var expected = QueryBuilders.queryStringQuery("last_name: Smith");
|
|
|
+ assertThat(query.query().toString(), is(expected.toString()));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Expecting
|
|
|
+ * LimitExec[1000[INTEGER]]
|
|
|
+ * \_ExchangeExec[[_meta_field{f}#1163, emp_no{f}#1157, first_name{f}#1158, gender{f}#1159, job{f}#1164, job.raw{f}#1165, langua
|
|
|
+ * ges{f}#1160, last_name{f}#1161, long_noidx{f}#1166, salary{f}#1162],false]
|
|
|
+ * \_ProjectExec[[_meta_field{f}#1163, emp_no{f}#1157, first_name{f}#1158, gender{f}#1159, job{f}#1164, job.raw{f}#1165, langua
|
|
|
+ * ges{f}#1160, last_name{f}#1161, long_noidx{f}#1166, salary{f}#1162]]
|
|
|
+ * \_FieldExtractExec[_meta_field{f}#1163, emp_no{f}#1157, first_name{f}#]
|
|
|
+ * \_EsQueryExec[test], indexMode[standard],
|
|
|
+ * query[{"bool":{"must":[{"query_string":{"query":"last_name: Smith","fields":[]}},
|
|
|
+ * {"esql_single_value":{"field":"emp_no","next":{"range":{"emp_no":{"gt":10010,"boost":1.0}}},"source":"emp_no > 10010@3:9"}}],
|
|
|
+ * "boost":1.0}}][_doc{f}#1167], limit[1000], sort[] estimatedRowSize[324]
|
|
|
+ */
|
|
|
+ public void testQueryStringFunctionMultipleWhereClauses() {
|
|
|
+ assumeTrue("skipping because QSTR_FUNCTION is not enabled", EsqlCapabilities.Cap.QSTR_FUNCTION.isEnabled());
|
|
|
+ String queryText = """
|
|
|
+ from test
|
|
|
+ | where qstr("last_name: Smith")
|
|
|
+ | where emp_no > 10010
|
|
|
+ """;
|
|
|
+ var plan = plannerOptimizer.plan(queryText, IS_SV_STATS);
|
|
|
+
|
|
|
+ var limit = as(plan, LimitExec.class);
|
|
|
+ var exchange = as(limit.child(), ExchangeExec.class);
|
|
|
+ var project = as(exchange.child(), ProjectExec.class);
|
|
|
+ var field = as(project.child(), FieldExtractExec.class);
|
|
|
+ var query = as(field.child(), EsQueryExec.class);
|
|
|
+ assertThat(query.limit().fold(), is(1000));
|
|
|
+
|
|
|
+ Source filterSource = new Source(3, 8, "emp_no > 10000");
|
|
|
+ var range = wrapWithSingleQuery(queryText, QueryBuilders.rangeQuery("emp_no").gt(10010), "emp_no", filterSource);
|
|
|
+ var queryString = QueryBuilders.queryStringQuery("last_name: Smith");
|
|
|
+ var expected = QueryBuilders.boolQuery().must(queryString).must(range);
|
|
|
+ assertThat(query.query().toString(), is(expected.toString()));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Expecting
|
|
|
+ * LimitExec[1000[INTEGER]]
|
|
|
+ * \_ExchangeExec[[_meta_field{f}#8, emp_no{f}#2, first_name{f}#3, gender{f}#4, job{f}#9, job.raw{f}#10, languages{f}#5, last_na
|
|
|
+ * me{f}#6, long_noidx{f}#11, salary{f}#7],false]
|
|
|
+ * \_ProjectExec[[_meta_field{f}#8, emp_no{f}#2, first_name{f}#3, gender{f}#4, job{f}#9, job.raw{f}#10, languages{f}#5, last_na
|
|
|
+ * me{f}#6, long_noidx{f}#11, salary{f}#7]]
|
|
|
+ * \_FieldExtractExec[_meta_field{f}#8, emp_no{f}#2, first_name{f}#3, gen]
|
|
|
+ * \_EsQueryExec[test], indexMode[standard], query[{"bool":
|
|
|
+ * {"must":[{"query_string":{"query":"last_name: Smith","fields":[]}},
|
|
|
+ * {"query_string":{"query":"emp_no: [10010 TO *]","fields":[]}}],"boost":1.0}}]
|
|
|
+ */
|
|
|
+ public void testQueryStringFunctionMultipleQstrClauses() {
|
|
|
+ assumeTrue("skipping because QSTR_FUNCTION is not enabled", EsqlCapabilities.Cap.QSTR_FUNCTION.isEnabled());
|
|
|
+ String queryText = """
|
|
|
+ from test
|
|
|
+ | where qstr("last_name: Smith") and qstr("emp_no: [10010 TO *]")
|
|
|
+ """;
|
|
|
+ var plan = plannerOptimizer.plan(queryText, IS_SV_STATS);
|
|
|
+
|
|
|
+ var limit = as(plan, LimitExec.class);
|
|
|
+ var exchange = as(limit.child(), ExchangeExec.class);
|
|
|
+ var project = as(exchange.child(), ProjectExec.class);
|
|
|
+ var field = as(project.child(), FieldExtractExec.class);
|
|
|
+ var query = as(field.child(), EsQueryExec.class);
|
|
|
+ assertThat(query.limit().fold(), is(1000));
|
|
|
+
|
|
|
+ var queryStringLeft = QueryBuilders.queryStringQuery("last_name: Smith");
|
|
|
+ var queryStringRight = QueryBuilders.queryStringQuery("emp_no: [10010 TO *]");
|
|
|
+ var expected = QueryBuilders.boolQuery().must(queryStringLeft).must(queryStringRight);
|
|
|
+ assertThat(query.query().toString(), is(expected.toString()));
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Expecting
|
|
|
* LimitExec[1000[INTEGER]]
|