Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import org.elasticsearch.compute.data.LongRangeBlockBuilder;
import org.elasticsearch.compute.data.Page;
import org.elasticsearch.compute.data.TDigestHolder;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.PathUtils;
import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.core.Tuple;
Expand Down Expand Up @@ -96,6 +97,7 @@
import org.elasticsearch.xpack.esql.core.expression.ReferenceAttribute;
import org.elasticsearch.xpack.esql.core.expression.predicate.regex.RLikePattern;
import org.elasticsearch.xpack.esql.core.expression.predicate.regex.WildcardPattern;
import org.elasticsearch.xpack.esql.core.querydsl.QueryDslTimestampBoundsExtractor.TimestampBounds;
import org.elasticsearch.xpack.esql.core.tree.Source;
import org.elasticsearch.xpack.esql.core.type.DataType;
import org.elasticsearch.xpack.esql.core.type.EsField;
Expand Down Expand Up @@ -569,6 +571,28 @@ public static MutableAnalyzerContext testAnalyzerContext(
EnrichResolution enrichResolution,
InferenceResolution inferenceResolution,
UnmappedResolution unmappedResolution
) {
return testAnalyzerContext(
configuration,
functionRegistry,
indexResolutions,
lookupResolution,
enrichResolution,
inferenceResolution,
unmappedResolution,
null
);
}

public static MutableAnalyzerContext testAnalyzerContext(
Configuration configuration,
EsqlFunctionRegistry functionRegistry,
Map<IndexPattern, IndexResolution> indexResolutions,
Map<String, IndexResolution> lookupResolution,
EnrichResolution enrichResolution,
InferenceResolution inferenceResolution,
UnmappedResolution unmappedResolution,
@Nullable TimestampBounds timestampBounds
) {
return new MutableAnalyzerContext(
configuration,
Expand All @@ -578,7 +602,8 @@ public static MutableAnalyzerContext testAnalyzerContext(
enrichResolution,
inferenceResolution,
randomMinimumVersion(),
unmappedResolution
unmappedResolution,
timestampBounds
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
package org.elasticsearch.xpack.esql.analysis;

import org.elasticsearch.TransportVersion;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.test.TransportVersionUtils;
import org.elasticsearch.xpack.esql.core.querydsl.QueryDslTimestampBoundsExtractor.TimestampBounds;
import org.elasticsearch.xpack.esql.datasources.ExternalSourceResolution;
import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry;
import org.elasticsearch.xpack.esql.index.IndexResolution;
import org.elasticsearch.xpack.esql.inference.InferenceResolution;
Expand All @@ -33,16 +36,43 @@ public MutableAnalyzerContext(
InferenceResolution inferenceResolution,
TransportVersion minimumVersion,
UnmappedResolution unmappedResolution
) {
this(
configuration,
functionRegistry,
indexResolution,
lookupResolution,
enrichResolution,
inferenceResolution,
minimumVersion,
unmappedResolution,
null
);
}

public MutableAnalyzerContext(
Configuration configuration,
EsqlFunctionRegistry functionRegistry,
Map<IndexPattern, IndexResolution> indexResolution,
Map<String, IndexResolution> lookupResolution,
EnrichResolution enrichResolution,
InferenceResolution inferenceResolution,
TransportVersion minimumVersion,
UnmappedResolution unmappedResolution,
@Nullable TimestampBounds timestampBounds
) {
super(
configuration,
functionRegistry,
null,
indexResolution,
lookupResolution,
enrichResolution,
inferenceResolution,
ExternalSourceResolution.EMPTY,
minimumVersion,
unmappedResolution
unmappedResolution,
timestampBounds
);
this.currentVersion = minimumVersion;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
import org.elasticsearch.xpack.esql.expression.function.AggregateMetricDoubleNativeSupport;
import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry;
import org.elasticsearch.xpack.esql.expression.function.FunctionDefinition;
import org.elasticsearch.xpack.esql.expression.function.TimestampBoundsAware;
import org.elasticsearch.xpack.esql.expression.function.UnresolvedFunction;
import org.elasticsearch.xpack.esql.expression.function.UnsupportedAttribute;
import org.elasticsearch.xpack.esql.expression.function.aggregate.Absent;
Expand Down Expand Up @@ -227,6 +228,7 @@ public class Analyzer extends ParameterizedRuleExecutor<LogicalPlan, AnalyzerCon
"Initialize",
Limiter.ONCE,
new ResolveConfigurationAware(),
new ResolveTimestampBoundsAware(),
new ResolveTable(),
new ResolveExternalRelations(),
new PruneEmptyUnionAllBranch(),
Expand Down Expand Up @@ -1722,6 +1724,41 @@ private static Expression resolveConfigurationAware(Expression expression, Confi
}
}

private static class ResolveTimestampBoundsAware extends ParameterizedAnalyzerRule<LogicalPlan, AnalyzerContext> {

@Override
protected boolean skipResolved() {
return false;
}

@Override
protected LogicalPlan rule(LogicalPlan plan, AnalyzerContext context) {
var bounds = context.timestampBounds();
if (bounds == null) {
return plan;
}
if (plan instanceof TimestampBoundsAware<?> tba && tba.needsTimestampBounds()) {
@SuppressWarnings("unchecked")
var planAware = (TimestampBoundsAware<LogicalPlan>) tba;
Comment thread
felixbarny marked this conversation as resolved.
Outdated
plan = planAware.withTimestampBounds(
Literal.dateTime(plan.source(), bounds.start()),
Literal.dateTime(plan.source(), bounds.end())
);
}
return plan.transformExpressionsUp(Expression.class, expression -> {
if (expression instanceof TimestampBoundsAware<?> tba && tba.needsTimestampBounds()) {
@SuppressWarnings("unchecked")
var exprAware = (TimestampBoundsAware<Expression>) tba;
return exprAware.withTimestampBounds(
Literal.dateTime(expression.source(), bounds.start()),
Literal.dateTime(expression.source(), bounds.end())
);
}
return expression;
});
}
}

private static class ResolveFunctions extends ParameterizedAnalyzerRule<LogicalPlan, AnalyzerContext> {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
import org.elasticsearch.TransportVersion;
import org.elasticsearch.cluster.metadata.Metadata;
import org.elasticsearch.cluster.metadata.ProjectMetadata;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.xpack.esql.core.expression.MetadataAttribute;
import org.elasticsearch.xpack.esql.core.querydsl.QueryDslTimestampBoundsExtractor.TimestampBounds;
import org.elasticsearch.xpack.esql.datasources.ExternalSourceResolution;
import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry;
import org.elasticsearch.xpack.esql.index.IndexResolution;
Expand All @@ -36,6 +38,7 @@ public class AnalyzerContext {
private final ProjectMetadata projectMetadata;
private Boolean hasRemoteIndices;
private final UnmappedResolution unmappedResolution;
private final TimestampBounds timestampBounds;

public AnalyzerContext(
Configuration configuration,
Expand All @@ -48,6 +51,34 @@ public AnalyzerContext(
ExternalSourceResolution externalSourceResolution,
TransportVersion minimumVersion,
UnmappedResolution unmappedResolution
) {
this(
configuration,
functionRegistry,
projectMetadata,
indexResolution,
lookupResolution,
enrichResolution,
inferenceResolution,
externalSourceResolution,
minimumVersion,
unmappedResolution,
null
);
}

public AnalyzerContext(
Configuration configuration,
EsqlFunctionRegistry functionRegistry,
ProjectMetadata projectMetadata,
Map<IndexPattern, IndexResolution> indexResolution,
Map<String, IndexResolution> lookupResolution,
EnrichResolution enrichResolution,
InferenceResolution inferenceResolution,
ExternalSourceResolution externalSourceResolution,
TransportVersion minimumVersion,
UnmappedResolution unmappedResolution,
@Nullable TimestampBounds timestampBounds
) {
this.configuration = configuration;
this.functionRegistry = functionRegistry;
Expand All @@ -59,6 +90,7 @@ public AnalyzerContext(
this.externalSourceResolution = externalSourceResolution;
this.minimumVersion = minimumVersion;
this.unmappedResolution = unmappedResolution;
this.timestampBounds = timestampBounds;

assert minimumVersion != null : "AnalyzerContext must have a minimum transport version";
assert TransportVersion.current().supports(minimumVersion)
Expand Down Expand Up @@ -138,6 +170,14 @@ public UnmappedResolution unmappedResolution() {
return unmappedResolution;
}

/**
* Returns the {@code @timestamp} bounds extracted from the query DSL filter, or {@code null} if not available.
*/
@Nullable
public TimestampBounds timestampBounds() {
return timestampBounds;
}

public Set<String> allowedTags() {
Set<String> result = new HashSet<>();
result.addAll(MetadataAttribute.ATTRIBUTES_MAP.keySet());
Expand All @@ -164,6 +204,17 @@ public AnalyzerContext(
UnmappedResolution unmappedResolution,
ProjectMetadata projectMetadata,
EsqlSession.PreAnalysisResult result
) {
this(configuration, functionRegistry, unmappedResolution, projectMetadata, result, null);
}

public AnalyzerContext(
Configuration configuration,
EsqlFunctionRegistry functionRegistry,
UnmappedResolution unmappedResolution,
ProjectMetadata projectMetadata,
EsqlSession.PreAnalysisResult result,
@Nullable TimestampBounds timestampBounds
) {
this(
configuration,
Expand All @@ -175,7 +226,8 @@ public AnalyzerContext(
result.inferenceResolution(),
result.externalSourceResolution(),
result.minimumTransportVersion(),
unmappedResolution
unmappedResolution,
timestampBounds
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.elasticsearch.xpack.esql.core.tree.Node;
import org.elasticsearch.xpack.esql.core.type.DataType;
import org.elasticsearch.xpack.esql.core.util.Holder;
import org.elasticsearch.xpack.esql.expression.function.TimestampBoundsAware;
import org.elasticsearch.xpack.esql.expression.function.UnsupportedAttribute;
import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Neg;
import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.Equals;
Expand Down Expand Up @@ -95,6 +96,7 @@ Collection<Failure> verify(LogicalPlan plan, BitSet partialMetrics) {
checkUnresolvedAttributes(plan, failures);

ConfigurationAware.verifyNoMarkerConfiguration(plan, failures);
checkUnresolvedTimestampBounds(plan, failures);

// in case of failures bail-out as all other checks will be redundant
if (failures.hasFailures()) {
Expand Down Expand Up @@ -220,6 +222,20 @@ else if (p instanceof PromqlCommand promql) {
});
}

private static void checkUnresolvedTimestampBounds(LogicalPlan plan, Failures failures) {
Comment thread
felixbarny marked this conversation as resolved.
Outdated
plan.forEachDown(p -> p.forEachExpression(Expression.class, e -> {
if (e instanceof TimestampBoundsAware<?> tba && tba.needsTimestampBounds()) {
failures.add(
fail(
e,
"[{}] requires a time range; provide explicit from/to parameters or add a @timestamp range to the query filter",
e.sourceText()
)
);
}
}));
}

/**
* Build a list of checkers based on the components in the plan.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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.function;

import org.elasticsearch.xpack.esql.analysis.Analyzer;
import org.elasticsearch.xpack.esql.analysis.Verifier;
import org.elasticsearch.xpack.esql.capabilities.ConfigurationAware;
import org.elasticsearch.xpack.esql.capabilities.PostAnalysisVerificationAware;
import org.elasticsearch.xpack.esql.core.expression.Literal;

/**
* Marker interface for nodes (expressions or plans) that require {@code @timestamp} bounds derived from the query DSL filter.
* <p>
* Implementations are resolved during analysis by {@link Analyzer}'s {@code ResolveTimestampBoundsAware} rule,
* following the same pattern as {@link ConfigurationAware}.
* </p>
* <p>
Comment thread
felixbarny marked this conversation as resolved.
* Expression implementations that still {@link #needsTimestampBounds() need bounds} after analysis are automatically
* rejected by the {@link Verifier} with a client error.
* LogicalPlan implementations are responsible for their own validation via {@link PostAnalysisVerificationAware#postAnalysisVerification}.
* </p>
*
* @param <T> the type returned by {@link #withTimestampBounds}, typically {@code Expression} or {@code LogicalPlan}
*/
public interface TimestampBoundsAware<T> {

/**
* Returns {@code true} if this node still needs timestamp bounds to be injected.
*/
boolean needsTimestampBounds();

/**
* Returns a copy of this node with the given timestamp bounds applied.
*/
T withTimestampBounds(Literal start, Literal end);
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.elasticsearch.xpack.esql.core.type.DataType;
import org.elasticsearch.xpack.esql.core.util.Holder;
import org.elasticsearch.xpack.esql.expression.function.TimestampAware;
import org.elasticsearch.xpack.esql.expression.function.TimestampBoundsAware;
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan;
import org.elasticsearch.xpack.esql.plan.logical.UnaryPlan;
import org.elasticsearch.xpack.esql.plan.logical.promql.operator.VectorBinaryComparison;
Expand All @@ -44,7 +45,12 @@
* Container plan for embedded PromQL queries.
* Gets eliminated by the analyzer once the query is validated.
*/
public class PromqlCommand extends UnaryPlan implements TelemetryAware, PostAnalysisVerificationAware, TimestampAware {
public class PromqlCommand extends UnaryPlan
implements
TelemetryAware,
PostAnalysisVerificationAware,
TimestampAware,
TimestampBoundsAware<LogicalPlan> {

/**
* The name of the column containing the step value (aka time bucket) in range queries.
Expand Down Expand Up @@ -162,7 +168,18 @@ public PromqlCommand withPromqlPlan(LogicalPlan newPromqlPlan) {
);
}

public PromqlCommand withStartEnd(Literal start, Literal end) {
/**
* Bounds are only needed when {@code buckets} is specified without an explicit time range.
* When {@code step} alone is set, the query can proceed without start/end because the step
* directly defines the bucket size; {@link #postAnalysisVerification} validates that case.
*/
@Override
public boolean needsTimestampBounds() {
return buckets.value() != null && hasTimeRange() == false;
}

@Override
public LogicalPlan withTimestampBounds(Literal start, Literal end) {
return new PromqlCommand(
source(),
child(),
Expand Down
Loading