Skip to content

fix: calculated measures over a joined model's measures - #309

Draft
deepyaman wants to merge 2 commits into
boringdata:mainfrom
deepyaman:fix/yaml-calc-measures-joined
Draft

deepyaman wants to merge 2 commits into
boringdata:mainfrom
deepyaman:fix/yaml-calc-measures-joined

Conversation

@deepyaman

@deepyaman deepyaman commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A YAML calculated_measures entry that reads a joined model's measure fails at query time with DefinitionError:

orders:
  measures:
    revenue: _.amount.sum()
  calculated_measures:
    revenue_per_customer: _.revenue / _['customers.n']
  joins:
    customers: {model: customers, type: one, left_on: customer_id, right_on: id}

Ratios over the model's own measures already work on a joined model; only cross-model references fail.

While fixing this, I found a silent wrong-answer bug in the Python API that the YAML fix would otherwise have exposed. with_measures(ratio=lambda t: t.revenue / t["customers.n"]) on a join_one returns a fanned-out result when ratio is requested alone, but the correct result when it's requested alongside its dependencies.

Changes

Two commits. The first stands on its own and can be split into a separate PR if you prefer.

  1. Route calc measures by their dependencies. SemanticAggregateOp.to_untagged chose between the source pre-aggregation path and the flattened-join path by looking only at the requested measure names. A calc measure owned by the root but reading customers.n took the flattened path, where customers.n gets counted over order rows. Routing now also considers the transitive dependencies of requested calc measures.

  2. Allow YAML calculated measures to read joined models. from_config attaches calculated_measures before joins, so _['customers.n'] can't be evaluated at definition time. _classify_measure then demoted the expression to a base measure, which ran against the raw table at query time. YAML calculated measures are measure expressions by contract (__bsl_prefer_known__). They now stay calc measures marked CalcMeasure.unresolved; the aggregation plan recovers their dependencies against the joined scope, and the router sends unresolved calcs through pre-aggregation.

    I kept the attach-before-join order on purpose. Attaching after the join would rename the measure: orders.revenue_per_customer would no longer resolve.

Tests

  • test_bi_traps.py::TestCalcOverJoinedMeasure: the Python API calc gives the same answer requested alone as with its dependencies (150 / 2, not 150 / 5).
  • test_yaml.py::test_yaml_calculated_measures_over_joined_model: both bracket spellings, ungrouped and grouped by a dimension.

All 4 new tests fail on main. Full suite: 1074 passed, which is the 1070 baseline plus these 4, with the same skipped/xfail/xpass counts.

Not addressed

  • The dot spelling _.customers.n still fails, in the Python API too (UnknownMeasureRefError: 'customers' is not a known measure or column). Only _['customers.n'] works.
  • Serialization (serialization/extract.py) rebuilds CalcMeasure without unresolved, just as it already drops prefer_known. YAML calc closures don't appear to be serializable through expr_struct anyway.

🤖 Generated with Claude Code

A calc measure that reads a joined model's measure, e.g.
`t.revenue / t["customers.n"]` on orders join_one customers, was routed
by its own (root-owned) name onto the flattened-join path, so
`customers.n` was counted over fanned-out order rows. Requesting the
calc alone returned a silently wrong answer; requesting it alongside
its dependencies went through source pre-aggregation and was correct.

Route on the transitive dependencies of requested calc measures too.

Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>
`from_config` attaches calculated_measures before joins, so an
expression such as `_.revenue / _['customers.n']` could not be analyzed
at definition time. Classification then demoted it to a base measure,
which ran against the raw table at query time and raised DefinitionError.

YAML calculated_measures are measure expressions by contract, so keep
them as calc measures marked `unresolved` instead; the aggregation plan
recovers their dependencies against the joined scope, and the router
sends them through source pre-aggregation so they cannot fan out.

Assisted-by: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant