Skip to content

fix(l2): resolve outer group keys (incl. __name__) over binary-op sides (#52) - #104

Merged
zzylol merged 1 commit into
mainfrom
feat/52-name-label-binary-op
Jul 6, 2026
Merged

zzylol merged 1 commit into
mainfrom
feat/52-name-label-binary-op

Conversation

@zzylol

@zzylol zzylol commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Closes #52.

The bug is more general than the title

sum by (__name__)(metric_total{env="1"} or rate(metric_total{env="2"}[5m])) failed with column __name__ not found in schema (have: ["ts", "value", "env"]). But it isn't __name__-specific — sum by (job)(metric_a or metric_b) fails identically. Any outer aggregate group key that a binary op's sides don't reference in their own matchers hits it.

Root cause. A BinaryOp's two sides re-run the Binder against their own sub-tree (convert_root per side), because the branches may scan different metrics with different label sets and each side's columns must bind to positions in its own leaf — threading the parent schema would bind the right side's columns to the wrong positions. The cost: a key referenced only by an enclosing node (the outer sum by (…)) is invisible when binding either side, so it fails to resolve against the binary op's output. __name__ was the reported instance because it's the metric-name label present on every series, but the mechanism is general.

Fix

When converting a BinaryOp, seed the inherited (ancestor-referenced) names into each side's binding, via the new Binder::bind_with_inherited. The inherited set is computed precisely:

inherited = (enclosing scope's referenced columns, already on `fallback`)
          − (names referenced within the binary op itself)

Subtracting the binary op's own references is the load-bearing part: it yields exactly the ancestor keys and stops one or side's labels from leaking into the other. My first cut seeded all of fallback's labels and regressed an existing exact-tree test (q25_div_over_complex_subtrees) by leaking a sibling status label across branches — the subtraction fixes that, and that test now passes unchanged.

Both independently-bound sides end up carrying the inherited key at the same position, so the outer group key resolves consistently across the union.

SQL is unaffected: table scans carry their own catalog schema (Source::Table) and ignore the usage-derived fallback, so the seeding is a no-op for them — confirmed by the unchanged SQL suite.

Tests

  • Binder unit test for bind_with_inherited (seeds the inherited name; plain bind doesn't conjure it).
  • Conformance test outer_group_key_over_binary_op_resolves_on_both_sides: the issue's __name__-over-or case resolves to a single positional id, both sides carry __name__ at the same position matching the outer key, and the general sum by (job)(a or b) case lowers.
  • Exact-tree e2e pin q52_outer_name_label_over_binary_op of the repro.
  • A note under "Why the Binder is its own pass" in docs/promql-lowering.md.
  • Existing binary-op exact-tree tests (q25, the binary_op.rs suite) are unchanged — no sibling-label leak.

Notes

  • Touches asap-l2 only; independent of other open work.
  • Verified: cargo test --workspace — 0 failures; cargo clippy --all-targets clean; source files carry zero net-new cargo fmt drift vs main.

🤖 Generated with Claude Code

… sides (#52)

`sum by (__name__)(a or b)` — and the general `sum by (job)(a or b)` —
failed column resolution: `column __name__ not found in schema`. A
binary op's two sides each re-run the Binder against their own sub-tree
(so different metrics bind their labels to the right positions), which
means a group key referenced only by an *enclosing* aggregate is invisible
when binding either side. `__name__` (the metric-name label, present on
every series) was the reported instance, but any outer key hit it.

Fix: when converting a `BinaryOp`, seed the ancestor-referenced names into
each side's binding via the new `Binder::bind_with_inherited`. The inherited
set is the enclosing scope's referenced columns (already on `fallback`)
minus the names referenced *within* the binary op itself — subtracting the
op's own refs is what stops one side's labels leaking into the other (which
would, and initially did, change unrelated exact-tree lowerings).

SQL is unaffected: table scans carry their own catalog schema and ignore
the usage-derived fallback.

Tests: a Binder unit test for `bind_with_inherited`; a conformance test
(both `or` sides carry `__name__` at a consistent position that the outer
key resolves to, plus the general `job` case); an exact-tree e2e pin of
the issue's repro; and a note in docs/promql-lowering.md. Existing
binary-op exact-tree tests are unchanged (no sibling-label leak).

Closes #52

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@zzylol
zzylol merged commit 4136956 into main Jul 6, 2026
1 check passed
@zzylol
zzylol deleted the feat/52-name-label-binary-op branch July 6, 2026 01:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

L3: sum by (__name__) fails column resolution (metric-name label not in usage-derived schema)

1 participant