1212import org .elasticsearch .xpack .esql .capabilities .ConfigurationAware ;
1313import org .elasticsearch .xpack .esql .core .QlIllegalArgumentException ;
1414import org .elasticsearch .xpack .esql .core .expression .Alias ;
15+ import org .elasticsearch .xpack .esql .core .expression .Attribute ;
1516import org .elasticsearch .xpack .esql .core .expression .Expression ;
1617import org .elasticsearch .xpack .esql .core .expression .Literal ;
1718import org .elasticsearch .xpack .esql .core .expression .NamedExpression ;
1819import org .elasticsearch .xpack .esql .core .expression .function .Function ;
1920import org .elasticsearch .xpack .esql .core .expression .predicate .regex .RLikePattern ;
2021import org .elasticsearch .xpack .esql .core .tree .Source ;
21- import org .elasticsearch .xpack .esql .expression .Order ;
2222import org .elasticsearch .xpack .esql .expression .function .grouping .Bucket ;
23+ import org .elasticsearch .xpack .esql .expression .function .scalar .convert .ToDouble ;
2324import org .elasticsearch .xpack .esql .expression .function .scalar .string .EndsWith ;
2425import org .elasticsearch .xpack .esql .expression .function .scalar .string .StartsWith ;
2526import org .elasticsearch .xpack .esql .expression .function .scalar .string .regex .RLike ;
3738import org .elasticsearch .xpack .esql .plan .logical .Eval ;
3839import org .elasticsearch .xpack .esql .plan .logical .Filter ;
3940import org .elasticsearch .xpack .esql .plan .logical .LogicalPlan ;
40- import org .elasticsearch .xpack .esql .plan .logical .OrderBy ;
4141import org .elasticsearch .xpack .esql .plan .logical .TimeSeriesAggregate ;
4242import org .elasticsearch .xpack .esql .plan .logical .promql .AcrossSeriesAggregate ;
4343import org .elasticsearch .xpack .esql .plan .logical .promql .PlaceholderRelation ;
5555import java .util .List ;
5656import java .util .Map ;
5757
58- import static java .util .Arrays .asList ;
59-
6058/**
6159 * Translates PromQL logical plans into ESQL TimeSeriesAggregate nodes.
6260 *
@@ -132,15 +130,15 @@ private record MapResult(LogicalPlan plan, Map<String, Expression> extras) {}
132130 // - Selector -> EsRelation + Filter
133131 private static MapResult map (PromqlCommand promqlCommand , LogicalPlan p ) {
134132 if (p instanceof Selector selector ) {
135- return map ( promqlCommand , selector );
133+ return mapSelector ( selector );
136134 }
137135 if (p instanceof PromqlFunctionCall functionCall ) {
138- return map (promqlCommand , functionCall );
136+ return mapFunction (promqlCommand , functionCall );
139137 }
140138 throw new QlIllegalArgumentException ("Unsupported PromQL plan node: {}" , p );
141139 }
142140
143- private static MapResult map ( PromqlCommand promqlCommand , Selector selector ) {
141+ private static MapResult mapSelector ( Selector selector ) {
144142 // Create a placeholder relation to be replaced later
145143 var matchers = selector .labelMatchers ();
146144 Expression matcherCondition = translateLabelMatchers (selector .source (), selector .labels (), matchers );
@@ -162,7 +160,7 @@ private static MapResult map(PromqlCommand promqlCommand, Selector selector) {
162160 return new MapResult (p , extras );
163161 }
164162
165- private static MapResult map (PromqlCommand promqlCommand , PromqlFunctionCall functionCall ) {
163+ private static MapResult mapFunction (PromqlCommand promqlCommand , PromqlFunctionCall functionCall ) {
166164 MapResult childResult = map (promqlCommand , functionCall .child ());
167165 Map <String , Expression > extras = childResult .extras ;
168166
@@ -180,59 +178,14 @@ private static MapResult map(PromqlCommand promqlCommand, PromqlFunctionCall fun
180178 extras .put ("field" , esqlFunction );
181179 result = new MapResult (childResult .plan , extras );
182180 } else if (functionCall instanceof AcrossSeriesAggregate acrossAggregate ) {
183- // expects
184- Function esqlFunction = PromqlFunctionRegistry .INSTANCE .buildEsqlFunction (
185- acrossAggregate .functionName (),
186- acrossAggregate .source (),
187- List .of (target )
188- );
189-
190181 List <NamedExpression > aggs = new ArrayList <>();
191- aggs .add (new Alias (acrossAggregate .source (), acrossAggregate .sourceText (), esqlFunction ));
192-
193182 List <Expression > groupings = new ArrayList <>(acrossAggregate .groupings ().size ());
194-
195- // add groupings
196- for (Expression grouping : acrossAggregate .groupings ()) {
197- NamedExpression named ;
198- if (grouping instanceof NamedExpression ne ) {
199- named = ne ;
200- } else {
201- named = new Alias (grouping .source (), grouping .sourceText (), grouping );
202- }
203- aggs .add (named );
204- groupings .add (named .toAttribute ());
205- }
206-
207- Expression timeBucketSize ;
208- if (promqlCommand .isRangeQuery ()) {
209- timeBucketSize = promqlCommand .step ();
210- } else {
211- // use default lookback for instant queries
212- timeBucketSize = Literal .timeDuration (promqlCommand .source (), DEFAULT_LOOKBACK );
213- }
214- Bucket b = new Bucket (
215- promqlCommand .source (),
216- promqlCommand .timestamp (),
217- timeBucketSize ,
218- null ,
219- null ,
220- ConfigurationAware .CONFIGURATION_MARKER
221- );
222- String bucketName = "TBUCKET" ;
223- Alias tbucket = new Alias (b .source (), bucketName , b );
224- aggs .add (tbucket .toAttribute ());
225- groupings .add (tbucket .toAttribute ());
183+ Alias stepBucket = createStepBucketAlias (promqlCommand , acrossAggregate );
184+ initAggregatesAndGroupings (acrossAggregate , target , aggs , groupings , stepBucket .toAttribute ());
226185
227186 LogicalPlan p = childResult .plan ;
228- p = new Eval (tbucket .source (), p , List .of (tbucket ));
187+ p = new Eval (stepBucket .source (), p , List .of (stepBucket ));
229188 p = new TimeSeriesAggregate (acrossAggregate .source (), p , groupings , aggs , null );
230- // sort the data ascending by time bucket
231- p = new OrderBy (
232- acrossAggregate .source (),
233- p ,
234- asList (new Order (acrossAggregate .source (), tbucket .toAttribute (), Order .OrderDirection .ASC , Order .NullsPosition .FIRST ))
235- );
236189 result = new MapResult (p , extras );
237190 } else {
238191 throw new QlIllegalArgumentException ("Unsupported PromQL function call: {}" , functionCall );
@@ -241,6 +194,55 @@ private static MapResult map(PromqlCommand promqlCommand, PromqlFunctionCall fun
241194 return result ;
242195 }
243196
197+ private static void initAggregatesAndGroupings (
198+ AcrossSeriesAggregate acrossAggregate ,
199+ Expression target ,
200+ List <NamedExpression > aggs ,
201+ List <Expression > groupings ,
202+ Attribute stepBucket
203+ ) {
204+ // main aggregation
205+ Function esqlFunction = PromqlFunctionRegistry .INSTANCE .buildEsqlFunction (
206+ acrossAggregate .functionName (),
207+ acrossAggregate .source (),
208+ // to double conversion of the metric to ensure a consistent output type
209+ // TODO it's probably more efficient to wrap the function in the ToDouble
210+ // but for some reason this doesn't work if you have an inner and outer aggregation
211+ List .of (new ToDouble (target .source (), target ))
212+ );
213+
214+ aggs .add (new Alias (acrossAggregate .source (), acrossAggregate .sourceText (), esqlFunction , acrossAggregate .valueId ()));
215+
216+ // timestamp/step
217+ aggs .add (stepBucket );
218+ groupings .add (stepBucket );
219+
220+ // additional groupings (by)
221+ for (NamedExpression grouping : acrossAggregate .groupings ()) {
222+ aggs .add (grouping );
223+ groupings .add (grouping .toAttribute ());
224+ }
225+ }
226+
227+ private static Alias createStepBucketAlias (PromqlCommand promqlCommand , AcrossSeriesAggregate acrossAggregate ) {
228+ Expression timeBucketSize ;
229+ if (promqlCommand .isRangeQuery ()) {
230+ timeBucketSize = promqlCommand .step ();
231+ } else {
232+ // use default lookback for instant queries
233+ timeBucketSize = Literal .timeDuration (promqlCommand .source (), DEFAULT_LOOKBACK );
234+ }
235+ Bucket b = new Bucket (
236+ promqlCommand .source (),
237+ promqlCommand .timestamp (),
238+ timeBucketSize ,
239+ null ,
240+ null ,
241+ ConfigurationAware .CONFIGURATION_MARKER
242+ );
243+ return new Alias (b .source (), "step" , b , acrossAggregate .stepId ());
244+ }
245+
244246 /**
245247 * Translates PromQL label matchers into ESQL filter expressions.
246248 *
0 commit comments