Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b677c8f
consolidate min/max in SearchStats and substitue date_trunc with roun…
fang-xing-esql May 29, 2025
08c3d18
Update docs/changelog/128639.yaml
fang-xing-esql May 29, 2025
3f96c15
Merge branch 'main' into date-trunc-search-stats
fang-xing-esql May 29, 2025
c3ffa8f
skip multi-typed fields with mixed date and date_nanos
fang-xing-esql May 29, 2025
f78fa18
Merge branch 'main' into date-trunc-search-stats
fang-xing-esql May 29, 2025
88dbdf8
support bucket SubstituteSurrogateExpressionsWithSearchStats
fang-xing-esql Jun 3, 2025
511faeb
Merge branch 'main' into date-trunc-search-stats
fang-xing-esql Jun 3, 2025
3d28f88
Merge branch 'main' into date-trunc-search-stats
fang-xing-esql Jun 3, 2025
3f1cd81
Merge branch 'main' into date-trunc-search-stats
fang-xing-esql Jun 9, 2025
e59ea5a
add tests for min/max in SearchContextStats, disallow substitution fo…
fang-xing-esql Jun 10, 2025
2f1aef9
add tests for min/max in SearchContextStats, disallow substitution fo…
fang-xing-esql Jun 10, 2025
debd20e
Merge branch 'main' into date-trunc-search-stats
fang-xing-esql Jun 10, 2025
5d6d9c9
Merge branch 'main' into date-trunc-search-stats
fang-xing-esql Jun 13, 2025
5a152ab
Merge branch 'main' into date-trunc-search-stats
fang-xing-esql Jun 23, 2025
7b669b8
fix test failures
fang-xing-esql Jun 23, 2025
54567d4
safe guard prepare with min max
fang-xing-esql Jun 24, 2025
3208b5f
Merge branch 'main' into date-trunc-search-stats
fang-xing-esql Jun 24, 2025
ee24a7e
Merge branch 'main' into date-trunc-search-stats
fang-xing-esql Jun 24, 2025
a76362f
Merge branch 'main' into date-trunc-search-stats
fang-xing-esql Jun 27, 2025
57dfe15
add trace and clean up
fang-xing-esql Jun 27, 2025
d42ea8e
Merge branch 'main' into date-trunc-search-stats
fang-xing-esql Jul 1, 2025
89039cd
Merge branch 'main' into date-trunc-search-stats
fang-xing-esql Jul 3, 2025
1d453f3
Merge branch 'main' into date-trunc-search-stats
fang-xing-esql Jul 10, 2025
4e82da7
refactor according to review comments
fang-xing-esql Jul 11, 2025
a2b80be
Merge branch 'main' into date-trunc-search-stats
fang-xing-esql Jul 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/changelog/128639.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pr: 128639
summary: Substitue `date_trunc` with `round_to` when the pre-calculated rounding points
are available
area: ES|QL
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -290,12 +290,12 @@ public long count(FieldName field, BytesRef value) {
}

@Override
public byte[] min(FieldName field, DataType dataType) {
public Object min(FieldName field) {
return null;
}

@Override
public byte[] max(FieldName field, DataType dataType) {
public Object max(FieldName field) {
return null;
}

Expand Down Expand Up @@ -381,6 +381,27 @@ public String toString() {
}
}

public static class TestSearchStatsWithMinMax extends TestSearchStats {

private final Map<String, Object> minValues;
private final Map<String, Object> maxValues;

public TestSearchStatsWithMinMax(Map<String, Object> minValues, Map<String, Object> maxValues) {
this.minValues = minValues;
this.maxValues = maxValues;
}

@Override
public Object min(FieldName field) {
return minValues.get(field.string());
}

@Override
public Object max(FieldName field) {
return maxValues.get(field.string());
}
}

public static final TestSearchStats TEST_SEARCH_STATS = new TestSearchStats();

private static final Map<String, Map<String, Column>> TABLES = tables();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.xpack.esql.expression;

import org.elasticsearch.xpack.esql.core.expression.Expression;
import org.elasticsearch.xpack.esql.stats.SearchStats;

/**
* Interface signaling to the local logical plan optimizer that the declaring expression
* has to be replaced by a different form.
* Implement this on {@code Function}s when:
* <ul>
* <li>The expression can be rewritten to another expression on data node, with the statistics available in SearchStats.
* Like {@code DateTrunc} and {@code Bucket} could be rewritten to {@code RoundTo} with the min/max values on the date field.
* </li>
* </ul>
*/
public interface LocalSurrogateExpression {
/**
* Returns the expression to be replaced by or {@code null} if this cannot be replaced.
*/
Expression surrogate(SearchStats searchStats);
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
import org.elasticsearch.xpack.esql.core.tree.Source;
import org.elasticsearch.xpack.esql.core.type.DataType;
import org.elasticsearch.xpack.esql.expression.LocalSurrogateExpression;
import org.elasticsearch.xpack.esql.expression.function.Example;
import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
import org.elasticsearch.xpack.esql.expression.function.FunctionType;
Expand All @@ -35,10 +36,9 @@
import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Div;
import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Mul;
import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput;
import org.elasticsearch.xpack.esql.stats.SearchStats;

import java.io.IOException;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;

Expand All @@ -50,6 +50,8 @@
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNumeric;
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType;
import static org.elasticsearch.xpack.esql.expression.Validations.isFoldable;
import static org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTrunc.maybeSubstituteWithRoundTo;
import static org.elasticsearch.xpack.esql.session.Configuration.DEFAULT_TZ;
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.dateTimeToLong;

/**
Expand All @@ -61,7 +63,8 @@
public class Bucket extends GroupingFunction.EvaluatableGroupingFunction
implements
PostOptimizationVerificationAware,
TwoOptionalArguments {
TwoOptionalArguments,
LocalSurrogateExpression {
public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Bucket", Bucket::new);

// TODO maybe we should just cover the whole of representable dates here - like ten years, 100 years, 1000 years, all the way up.
Expand All @@ -87,8 +90,6 @@ public class Bucket extends GroupingFunction.EvaluatableGroupingFunction
Rounding.builder(TimeValue.timeValueMillis(10)).build(),
Rounding.builder(TimeValue.timeValueMillis(1)).build(), };

private static final ZoneId DEFAULT_TZ = ZoneOffset.UTC; // TODO: plug in the config

private final Expression field;
private final Expression buckets;
private final Expression from;
Expand Down Expand Up @@ -301,15 +302,22 @@ public Rounding.Prepared getDateRoundingOrNull(FoldContext foldCtx) {
}

private Rounding.Prepared getDateRounding(FoldContext foldContext) {
return getDateRounding(foldContext, null, null);
}

private Rounding.Prepared getDateRounding(FoldContext foldContext, Long min, Long max) {
assert field.dataType() == DataType.DATETIME || field.dataType() == DataType.DATE_NANOS : "expected date type; got " + field;
if (buckets.dataType().isWholeNumber()) {
int b = ((Number) buckets.fold(foldContext)).intValue();
long f = foldToLong(foldContext, from);
long t = foldToLong(foldContext, to);
if (min != null && max != null) {
return new DateRoundingPicker(b, f, t).pickRounding().prepare(min, max);
}
return new DateRoundingPicker(b, f, t).pickRounding().prepareForUnknown();
} else {
assert DataType.isTemporalAmount(buckets.dataType()) : "Unexpected span data type [" + buckets.dataType() + "]";
return DateTrunc.createRounding(buckets.fold(foldContext), DEFAULT_TZ);
return DateTrunc.createRounding(buckets.fold(foldContext), DEFAULT_TZ, min, max);
}
}

Expand Down Expand Up @@ -488,4 +496,17 @@ public Expression to() {
public String toString() {
return "Bucket{" + "field=" + field + ", buckets=" + buckets + ", from=" + from + ", to=" + to + '}';
}

@Override
public Expression surrogate(SearchStats searchStats) {
// LocalSubstituteSurrogateExpressions should make sure this doesn't happen
assert searchStats != null : "SearchStats cannot be null";
return maybeSubstituteWithRoundTo(
source(),
field(),
buckets(),
searchStats,
(interval, minValue, maxValue) -> getDateRounding(FoldContext.small(), minValue, maxValue)
);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
public class DateDiff extends EsqlScalarFunction {
public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "DateDiff", DateDiff::new);

public static final ZoneId UTC = ZoneId.of("Z");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExpressionBuilder, Add and Sub all refer to DateUtils.UTC, which is the same as ZoneId.of("Z"), refactor this to point to DateUtils.UTC as well. This seems to be the only place that directly references ZoneId.of("Z").

public static final ZoneId UTC = org.elasticsearch.xpack.esql.core.util.DateUtils.UTC;

private final Expression unit;
private final Expression startTimestamp;
Expand Down
Loading
Loading