Split out from #377 / #433, which deliberately scoped this out.
Symptom
count(node_a or node_b) translates to:
FROM metrics-*
| WHERE @timestamp >= ?_tstart AND @timestamp <= ?_tend
| STATS node_a = COUNT(node_a) BY time_bucket = BUCKET(@timestamp, 50, ?_tstart, ?_tend)
node_b is absent. The panel reports the count of node_a alone and is labelled migrated_with_warnings, so an operator sees a number rather than a disclosure. This is the same failure class as #377: a plausible value that nobody re-derives by hand.
Why #433 did not fix it
#433 added a closing guard in _ast_aggregate_fragment that refuses agg(A op B) for every operator except or. or is deferred because it carries two established reductions that later stages own:
- the Grafana same-metric range-window fallback,
max_over_time(M[$interval]) or max_over_time(M[5m])
- the live-absent operand drop in
colocated_binary_agg_plan
Both need a resolver that parse time does not have — deciding whether an operand is absent requires knowing what the target actually contains. Refusing or at parse time was tried in #433 and broke test_clamp_wrapper_uses_real_output_field_when_panel_drops_unmigrated_target, which is a legitimate use of reduction (1).
So the gap is specifically: or between two genuinely different metrics, where neither reduction applies and there is nothing to fall back to.
Suggested direction
Refuse at the stage that has the resolver rather than at parse time. colocated_binary_agg_plan already decides whether an operand can be dropped; when it drops one because the operand is a different metric (rather than absent-in-target or a same-metric window fallback), that is the case that should mark not_feasible instead of silently proceeding.
Worth checking whether the same hole exists for agg(A or B) under by() grouping and for nested or chains.
Where it is documented today
tests/test_issue377_agg_over_binary_operator.py::test_no_binary_operator_silently_drops_an_operand sweeps every binary operator and asserts none drops an operand. or is explicitly excluded from that sweep with a docstring pointing here, so the gap is visible in the test rather than hidden inside the deferred branch. That exclusion is the thing to remove when this is fixed.
Split out from #377 / #433, which deliberately scoped this out.
Symptom
count(node_a or node_b)translates to:node_bis absent. The panel reports the count ofnode_aalone and is labelledmigrated_with_warnings, so an operator sees a number rather than a disclosure. This is the same failure class as #377: a plausible value that nobody re-derives by hand.Why #433 did not fix it
#433 added a closing guard in
_ast_aggregate_fragmentthat refusesagg(A op B)for every operator exceptor.oris deferred because it carries two established reductions that later stages own:max_over_time(M[$interval]) or max_over_time(M[5m])colocated_binary_agg_planBoth need a resolver that parse time does not have — deciding whether an operand is absent requires knowing what the target actually contains. Refusing
orat parse time was tried in #433 and broketest_clamp_wrapper_uses_real_output_field_when_panel_drops_unmigrated_target, which is a legitimate use of reduction (1).So the gap is specifically:
orbetween two genuinely different metrics, where neither reduction applies and there is nothing to fall back to.Suggested direction
Refuse at the stage that has the resolver rather than at parse time.
colocated_binary_agg_planalready decides whether an operand can be dropped; when it drops one because the operand is a different metric (rather than absent-in-target or a same-metric window fallback), that is the case that should marknot_feasibleinstead of silently proceeding.Worth checking whether the same hole exists for
agg(A or B)underby()grouping and for nestedorchains.Where it is documented today
tests/test_issue377_agg_over_binary_operator.py::test_no_binary_operator_silently_drops_an_operandsweeps every binary operator and asserts none drops an operand.oris explicitly excluded from that sweep with a docstring pointing here, so the gap is visible in the test rather than hidden inside the deferred branch. That exclusion is the thing to remove when this is fixed.