@@ -259,11 +259,67 @@ private TranslationResult translateNode(LogicalPlan node, LogicalPlan currentPla
259259 }
260260
261261 /**
262- * Translates an AcrossSeriesAggregate (sum, avg, max, min, count, etc.).
263- * Creates TimeSeriesAggregate if child has no aggregation or Aggregate if child already aggregated.
264- * NOTE: Only AcrossSeriesAggregate nodes create plan-level aggregation nodes.
265- * WithinSeriesAggregate and other PromqlFunctionCall nodes produce
266- * expressions embedded inside the aggregation, but do not create Aggregate plan nodes themselves.
262+ * Translates {@code AcrossSeriesAggregate} to an ESQL {@code Aggregate}.
263+ * <p>
264+ * PromQL aggregation shape is dynamic and can't be expressed in ESQL directly without enumerating the full label set.
265+ * We avoid that at plan time (for performance), so the translator carries aggregation shape as a triplet
266+ * {@code (G, O, X)} where G = grouping labels, O = output labels, X = excluded labels.
267+ * G is either a concrete set of label names or an opaque runtime representation {@code T...} (backed by _timeseries).
268+ * Labels in O but not in G are null-filled in the output.
269+ * <p>
270+ * <ul>
271+ * <li>WITHOUT(E): (G, O, X) -> (G\E, G\E, X u E)</li>
272+ * <li>BY(W): (G, O, X) -> (W\X, W, X)</li>
273+ * </ul>
274+ * <p>
275+ * Leaf state is {@code [G=T..., O=T..., X={}]} (all labels present but unknown by name).
276+ * <p>
277+ * Examples:
278+ *
279+ * <pre>
280+ * sum without(pod) (
281+ * avg without(region) (
282+ * cpu_util
283+ * ) [G=T...\{region}, O=T...\{region}, X={region}]
284+ * ) [G=T...\{region,pod}, O=T...\{region,pod}, X={region,pod}]
285+ * </pre>
286+ *
287+ * <pre>
288+ * sum by(cluster,region) (
289+ * avg without(region) (
290+ * cpu_util
291+ * ) [G=T...\{region}, O=T...\{region}, X={region}]
292+ * ) [G={cluster}, O={cluster,region}, X={region}] // region is null-filled
293+ * </pre>
294+ *
295+ * <pre>
296+ * max without(cluster) (
297+ * sum by(cluster,region) (
298+ * avg without(region) (
299+ * cpu_util
300+ * ) [G=T...\{region}, O=T...\{region}, X={region}]
301+ * ) [G={cluster}, O={cluster,region}, X={region}] // region is null-filled
302+ * ) [G={}, O={}, X={region,cluster}]
303+ * </pre>
304+ *
305+ * <pre>
306+ * sum without(pod) (
307+ * avg by(cluster,pod) (
308+ * cpu_util
309+ * ) [G={cluster,pod}, O={cluster,pod}, X={}]
310+ * ) [G={cluster}, O={cluster}, X={pod}]
311+ * </pre>
312+ * <pre>
313+ * sum by(cluster) (
314+ * avg by(cluster,pod) (
315+ * cpu_util
316+ * ) [G={cluster,pod}, O={cluster,pod}, X={}]
317+ * ) [G={cluster}, O={cluster}, X={}]
318+ * </pre>
319+ *
320+ * <p>Only {@code AcrossSeriesAggregate} creates plan-level aggregation nodes.
321+ * {@code WithinSeriesAggregate} and other {@code PromqlFunctionCall} nodes are
322+ * lowered to expressions and folded into the aggregate.
267323 */
268324 private TranslationResult translateAcrossSeriesAggregate (AcrossSeriesAggregate agg , LogicalPlan currentPlan , TranslationContext ctx ) {
269325 List <Attribute > groupingLabels = normalizeLabels (agg .groupings ());
0 commit comments