Skip to content

Support nested query functions for PromQL and SQL (testing + implementation) #27

Description

@zzylol

Summary

The ASAPQuery query engine should support nested query functions — applying a function on top of a subquery — for both PromQL and SQL. This issue tracks the testing and implementation of general nested-query support.

What the architecture already supports (grounding)

Nesting is structural at L3: intent_algebra::QueryExpr is a recursive, box-owned tree, and every operator already accepts an arbitrary child. The pieces that consume it are recursive too, so most of the "nesting" plumbing is already in place:

  • L3 IR (crates/core/src/intent_algebra/query_expr.rs) — Aggregate { child }, Sort { child }, Limit { child }, Subquery { child }, BinaryOp { lhs, rhs }, etc. all box an arbitrary subtree. Schema derivation (output_schema_in) recurses and already handles a cross-series aggregate freezing an open schema to closed, per-series (label-preserving) reductions under TimeRange, etc.
  • L2→L3 converter (crates/core/src/intent_algebra/lower.rs) — convert recurses through every node, including an outer Aggregate over an arbitrary child, Sort/Limit over arbitrary children, PromQLSubquery, and BinaryOp.
  • Binder (crates/core/src/intent_algebra/binder.rs) — walks the whole tree to seed the usage-derived schema, so nested group keys / matchers resolve positionally.
  • SQL front end (crates/lower/src/sql/mod.rs) — lower_plan recurses through the DataFusion LogicalPlan, so a nested aggregate / filter / projection chain already lowers. (Gaps below.)

So the binary form Q1 <op> Q2 was never the real ceiling — BinaryOp already nests both sides arbitrarily (see crates/e2e/tests/nested.rs::q25_div_over_complex_subtrees).

The actual gap (PromQL)

The ceiling was in the PromQL L1→L2 front end (crates/lower/src/promql.rs). walk_aggregate lowered an aggregate's argument through a flat two-level template (Inner + Outer enums): an outer aggregator over exactly one inner selector or range-vector function. Any composite argument was rejected:

  • max(sum by (job) (rate(m[5m]))) — outer aggregate over a nested aggregate
  • topk(3, sum by (instance) (rate(m[5m])))topk over a nested aggregate (was pinned as topk_over_aggregate_arg_is_rejected__GAP)
  • sum(rate(a[5m]) + rate(b[5m])) — aggregate over a binary op
  • sum(histogram_quantile(0.9, sum by (le) (rate(h_bucket[5m])))) — aggregate over a function lowered on a separate path

Done in this issue (PromQL) — PR linked below

  • walk_aggregate now tries the flat fast path first (keeping heavy-hitter topk(k, count_over_time(...)) recognition intact), then falls back to recursing through the same walk used at top level and wrapping the result in the outer aggregation (outer_kind + build_over_subtree). This lifts the two-level limit to arbitrary function nesting.
  • Genuinely unsupported inner expressions (e.g. unary negation) still surface their error — nothing is silently mislowered.
  • ColumnRef::SampleValue resolution (crates/core/src/intent_algebra/column_resolution.rs) now also resolves to the sole numeric non-timestamp column, so an outer ranking over a cross-series aggregate (topk(k, sum by (job) (…)), value column renamed sum) finds its sort key.
  • Tests: gap test flipped to a passing conformance test; added nested-aggregate / aggregate-over-binary-op / outer-over-nested-aggregate conformance tests, an exact-tree e2e test (q27_max_over_sum_by_job_over_rate), and SampleValue resolution unit tests.

Remaining implementation plan (tasks)

PromQL

  • Generalize walk_aggregate to recurse over composite arguments (this PR).
  • Range-vector function over a sub-querymax_over_time(rate(m[5m])[1h:]). extract_matrix only accepts a (parenthesised) matrix selector; teach the InnerFunc path to accept a PromQLSubquery child so *_over_time/rate can wrap a sub-query. Pinned today as over_time_of_subquery_is_rejected__GAP (crates/lower/tests/promql_conformance.rs).
  • topk(k, <bare instant selector>) by (labels) — e.g. topk(3, http_requests_total) by (job). The non-heavy-hitter path defaults a bare selector to an implicit cross-series Sum, which destroys the by labels before Sort.partition_by resolves them (and is semantically wrong — PromQL ranks the raw samples). Rank the selector's own value, keep labels, route by to Sort.partition_by. (Surfaced reviewing PR feat(core)!: collapse L3 grouping — remove Partition node, unify on GroupKeys (#12, #13) #18.)
  • Unary negation / scalar-literal operands inside nestingsum(-m), v > 10*1024*1024. Needs a negate/scalar node in the L2 PromQL path (separate from this issue; tracked by the __GAP tests in section G).

SQL

  • Derived tables / inline viewsSELECT … FROM (SELECT … ) t. lower_plan rejects LogicalPlan::SubqueryAlias over a non-TableScan input and bare LogicalPlan::Subquery (crates/lower/src/sql/mod.rs). Lower the inner plan recursively and re-qualify its output columns with the alias so t.col resolves. This is the SQL counterpart of PromQL function nesting.
  • Scalar / IN / EXISTS subqueries in predicates — decide representation (correlated vs uncorrelated) or reject cleanly with a pinned gap test.
  • Conformance + e2e tests mirroring the PromQL nested cases (outer aggregate over a derived-table aggregate; ranking over a subquery).

Cross-cutting

  • Boundary decision per node (sketch vs exact) is an L4 concern, not L3: L3 carries intent only. No engine change needed for nesting itself; confirm L4 rules fire per node over nested trees once the L4 framework lands.
  • Document the nesting contract (which composite arguments lower vs. are rejected) in docs/promql-lowering.md.

Open questions

  • Full set of nested patterns query authors need (gather more concrete examples).
  • For SQL, how far to go on correlated subqueries vs. rejecting them cleanly in v1.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions