|
8 | 8 | package org.elasticsearch.xpack.esql.optimizer.rules.physical.local; |
9 | 9 |
|
10 | 10 | import org.elasticsearch.common.logging.LoggerMessageFormat; |
| 11 | +import org.elasticsearch.common.settings.Settings; |
11 | 12 | import org.elasticsearch.index.query.BoolQueryBuilder; |
12 | 13 | import org.elasticsearch.index.query.MatchQueryBuilder; |
13 | 14 | import org.elasticsearch.index.query.QueryBuilder; |
|
27 | 28 | import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTrunc; |
28 | 29 | import org.elasticsearch.xpack.esql.expression.function.scalar.math.RoundTo; |
29 | 30 | import org.elasticsearch.xpack.esql.optimizer.LocalPhysicalPlanOptimizerTests; |
| 31 | +import org.elasticsearch.xpack.esql.optimizer.TestPlannerOptimizer; |
30 | 32 | import org.elasticsearch.xpack.esql.plan.physical.AggregateExec; |
31 | 33 | import org.elasticsearch.xpack.esql.plan.physical.EsQueryExec; |
32 | 34 | import org.elasticsearch.xpack.esql.plan.physical.EvalExec; |
|
38 | 40 | import org.elasticsearch.xpack.esql.plan.physical.PhysicalPlan; |
39 | 41 | import org.elasticsearch.xpack.esql.plan.physical.ProjectExec; |
40 | 42 | import org.elasticsearch.xpack.esql.plan.physical.TopNExec; |
| 43 | +import org.elasticsearch.xpack.esql.plugin.EsqlFlags; |
| 44 | +import org.elasticsearch.xpack.esql.plugin.QueryPragmas; |
41 | 45 | import org.elasticsearch.xpack.esql.session.Configuration; |
42 | 46 | import org.elasticsearch.xpack.esql.stats.SearchStats; |
43 | 47 |
|
44 | 48 | import java.util.ArrayList; |
45 | 49 | import java.util.HashMap; |
46 | 50 | import java.util.List; |
| 51 | +import java.util.Locale; |
47 | 52 | import java.util.Map; |
48 | 53 | import java.util.stream.Collectors; |
49 | 54 |
|
|
55 | 60 | import static org.elasticsearch.index.query.QueryBuilders.rangeQuery; |
56 | 61 | import static org.elasticsearch.index.query.QueryBuilders.termQuery; |
57 | 62 | import static org.elasticsearch.xpack.esql.EsqlTestUtils.as; |
| 63 | +import static org.elasticsearch.xpack.esql.EsqlTestUtils.configuration; |
58 | 64 | import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.DEFAULT_DATE_NANOS_FORMATTER; |
59 | 65 | import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.DEFAULT_DATE_TIME_FORMATTER; |
60 | 66 | import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.dateNanosToLong; |
@@ -391,9 +397,11 @@ public void testDateTruncBucketNotTransformToQueryAndTagsWithFork() { |
391 | 397 | } |
392 | 398 | } |
393 | 399 |
|
394 | | - // If the number of rounding points is 127 or less, the query is rewritten to use QueryAndTags. |
395 | | - // If the number of rounding points is 128 or more, the query is not rewritten. |
396 | | - public void testRoundToTransformToQueryAndTagsUpperLimit() { |
| 400 | + /** |
| 401 | + * If the number of rounding points is 127 or less, the query is rewritten to QueryAndTags. |
| 402 | + * If the number of rounding points is 128 or more, the query is not rewritten. |
| 403 | + */ |
| 404 | + public void testRoundToTransformToQueryAndTagsWithDefaultUpperLimit() { |
397 | 405 | for (int numOfPoints : List.of(127, 128)) { |
398 | 406 | StringBuilder points = new StringBuilder(); |
399 | 407 | for (int i = 0; i < numOfPoints; i++) { |
@@ -445,6 +453,83 @@ public void testRoundToTransformToQueryAndTagsUpperLimit() { |
445 | 453 | } |
446 | 454 | } |
447 | 455 |
|
| 456 | + /** |
| 457 | + * Query level threshold(if greater than -1) set in QueryPragmas overrides the cluster level threshold set in EsqlFlags. |
| 458 | + */ |
| 459 | + public void testRoundToTransformToQueryAndTagsWithCustomizedUpperLimit() { |
| 460 | + for (int clusterLevelThreshold : List.of(-1, 0, 60, 126, 128, 256)) { |
| 461 | + for (int queryLevelThreshold : List.of(-1, 0, 60, 126, 128, 256)) { |
| 462 | + StringBuilder points = new StringBuilder(); // there are 127 rounding points |
| 463 | + for (int i = 0; i < 127; i++) { |
| 464 | + if (i > 0) { |
| 465 | + points.append(", "); |
| 466 | + } |
| 467 | + points.append(i); |
| 468 | + } |
| 469 | + String query = LoggerMessageFormat.format(null, """ |
| 470 | + from test |
| 471 | + | stats count(*) by x = round_to(integer, {}) |
| 472 | + """, points.toString()); |
| 473 | + |
| 474 | + TestPlannerOptimizer plannerOptimizerWithPragmas = new TestPlannerOptimizer( |
| 475 | + configuration( |
| 476 | + new QueryPragmas( |
| 477 | + Settings.builder() |
| 478 | + .put(QueryPragmas.ROUNDTO_PUSHDOWN_THRESHOLD.getKey().toLowerCase(Locale.ROOT), queryLevelThreshold) |
| 479 | + .build() |
| 480 | + ), |
| 481 | + query |
| 482 | + ), |
| 483 | + makeAnalyzer("mapping-all-types.json") |
| 484 | + ); |
| 485 | + EsqlFlags esqlFlags = new EsqlFlags(clusterLevelThreshold); |
| 486 | + assertEquals(clusterLevelThreshold, esqlFlags.roundToPushdownThreshold()); |
| 487 | + assertTrue(esqlFlags.stringLikeOnIndex()); |
| 488 | + PhysicalPlan plan = plannerOptimizerWithPragmas.plan(query, searchStats, esqlFlags); |
| 489 | + boolean pushdown = false; |
| 490 | + if (queryLevelThreshold > -1) { |
| 491 | + pushdown = queryLevelThreshold >= 127; |
| 492 | + } else { |
| 493 | + pushdown = clusterLevelThreshold >= 127; |
| 494 | + } |
| 495 | + |
| 496 | + LimitExec limit = as(plan, LimitExec.class); |
| 497 | + AggregateExec agg = as(limit.child(), AggregateExec.class); |
| 498 | + assertThat(agg.getMode(), is(FINAL)); |
| 499 | + List<? extends Expression> groupings = agg.groupings(); |
| 500 | + NamedExpression grouping = as(groupings.get(0), NamedExpression.class); |
| 501 | + assertEquals("x", grouping.name()); |
| 502 | + assertEquals(DataType.INTEGER, grouping.dataType()); |
| 503 | + assertEquals(List.of("count(*)", "x"), Expressions.names(agg.aggregates())); |
| 504 | + ExchangeExec exchange = as(agg.child(), ExchangeExec.class); |
| 505 | + assertThat(exchange.inBetweenAggs(), is(true)); |
| 506 | + agg = as(exchange.child(), AggregateExec.class); |
| 507 | + EvalExec evalExec = as(agg.child(), EvalExec.class); |
| 508 | + List<Alias> aliases = evalExec.fields(); |
| 509 | + assertEquals(1, aliases.size()); |
| 510 | + if (pushdown) { |
| 511 | + FieldAttribute roundToTag = as(aliases.get(0).child(), FieldAttribute.class); |
| 512 | + assertTrue(roundToTag.name().startsWith("$$integer$round_to$")); |
| 513 | + EsQueryExec esQueryExec = as(evalExec.child(), EsQueryExec.class); |
| 514 | + List<EsQueryExec.QueryBuilderAndTags> queryBuilderAndTags = esQueryExec.queryBuilderAndTags(); |
| 515 | + assertEquals(128, queryBuilderAndTags.size()); // 127 + nullBucket |
| 516 | + assertThrows(UnsupportedOperationException.class, esQueryExec::query); |
| 517 | + } else { // query rewrite does not happen |
| 518 | + RoundTo roundTo = as(aliases.get(0).child(), RoundTo.class); |
| 519 | + assertEquals(127, roundTo.points().size()); |
| 520 | + FieldExtractExec fieldExtractExec = as(evalExec.child(), FieldExtractExec.class); |
| 521 | + EsQueryExec esQueryExec = as(fieldExtractExec.child(), EsQueryExec.class); |
| 522 | + List<EsQueryExec.QueryBuilderAndTags> queryBuilderAndTags = esQueryExec.queryBuilderAndTags(); |
| 523 | + assertEquals(1, queryBuilderAndTags.size()); |
| 524 | + EsQueryExec.QueryBuilderAndTags queryBuilder = queryBuilderAndTags.get(0); |
| 525 | + assertNull(queryBuilder.query()); |
| 526 | + assertTrue(queryBuilder.tags().isEmpty()); |
| 527 | + assertNull(esQueryExec.query()); |
| 528 | + } |
| 529 | + } |
| 530 | + } |
| 531 | + } |
| 532 | + |
448 | 533 | private static void verifyQueryAndTags(List<EsQueryExec.QueryBuilderAndTags> expected, List<EsQueryExec.QueryBuilderAndTags> actual) { |
449 | 534 | assertEquals(expected.size(), actual.size()); |
450 | 535 | for (int i = 0; i < expected.size(); i++) { |
|
0 commit comments