Skip to content

fix(tesseract): plan multiplied measures per cube and join tree - #11436

Open
waralexrom wants to merge 10 commits into
masterfrom
tesseract-fix-distinct-over-text-id
Open

fix(tesseract): plan multiplied measures per cube and join tree#11436
waralexrom wants to merge 10 commits into
masterfrom
tesseract-fix-distinct-over-text-id

Conversation

@waralexrom

@waralexrom waralexrom commented Jul 31, 2026

Copy link
Copy Markdown
Member

Summary

Two related defects in Tesseract's planning of multiplied measures — measures under a
one-to-many join, which go through a keys subquery to deduplicate row fan-out. Both surface on
the same shape: a calculated measure next to a measure that needs a wider join, grouped by a
dimension reached through hasMany. That combination was either refused outright or compiled to
invalid SQL.

Reported against a payments-analytics model: an authorization-rate measure (a ratio of two
countDistinct measures) together with an FX-converted value measure that joins a rates cube,
grouped by a metadata key/value cube.

Changes

Group multiplied measures by (cube, join tree), not by cube alone.
The bucket was keyed on the owning cube, and the whole bucket was then required to resolve to a
single multi-fact join group — so two measures of one cube needing different join trees were
rejected with Expected just one multi-fact join group. The key cube fixes the primary keys to
deduplicate on, the join tree fixes the joins to build, and neither determines the other, so one
AggregateMultipliedSubquery is now emitted per pair. This mirrors the regular-measure branch a
few lines above, which already emits one CTE per join group.

Trees are kept apart rather than merged so a measure's value cannot depend on which other
measures share the query. Merging sibling one-to-many branches introduces a cross product that
neither original tree had — it flips measures from regular to multiplied, i.e. it changes what
the query means.

Refuse measures the measure subquery cannot carry.
Reaching a cube beyond its own makes a multiplied measure go through the measure subquery, which
renders measures without their aggregate so the select above can re-apply it. A calculated
measure (type: number or number_agg) has no aggregate of its own, so nothing is re-applied: its
components lose theirs inside the subquery and the expression comes out neither aggregated nor
grouped, reaching the database as SQL it rejects. Those shapes now stop at the planner with a
message naming the measure.

Splitting such a measure into its components was tried and taken back out. It put the two halves of
an expression on separate legs, and when those legs root at different cubes they see different
rows, so the ratio came out over a denominator its numerator never saw — answered, with no error,
differently depending on whether the query carried a fan-out dimension. It also never covered the
shape it was written for: whether the subquery is built is decided per join group, while the
condition for splitting was read off a single measure.

Testing

Unit tests for the decomposition gate in tests/multiplied_measures_collector.rs (5 tests), each
pinning one arm of it: decompose when multiplied and reaching, keep whole when not multiplied, when
reaching nothing, when reaching past the components, and when reading the own cube directly.

New integration suite calculated_multi_fact.rs (17 tests, 2 #[ignore]d) on a dedicated
fixture with a TEXT primary key, two lookup cubes and two fan-out branches. Covers nested and
sibling join trees, a measure reading three cubes, a star with two fan-out branches, split groups
rooted at different cubes, and the shapes that must not decompose. Results are asserted against
Postgres via insta snapshots; every expected value is hand-checkable from the seed.

A matching JS integration test runs under both planners. The legacy planner still has the
de-aggregation defect, so it asserts the legacy failure explicitly.

Both fixes were verified to be genuinely guarded: reverting each one alone turns exactly the
intended tests red and leaves the controls green.

  • cargo test --features integration-postgres — 1120 passed, 0 failed, 11 ignored
  • schema-compiler Postgres integration — 433 passed under the legacy planner, 508 under
    Tesseract, 0 failed
  • cargo fmt --check and eslint clean

Queries that already worked keep their plans: with a single join group per cube the loop emits the
same single CTE with the same measures, and the calculated-measure gate is scoped to the multiplied
case the defect lives in.

Two shapes remain broken and are #[ignore]d with the reason recorded: a measure whose aggregate
is hand-written over a joined cube's column (MAX({rates.fx_rate})), with or without a component
measure. Those have never worked — verified to fail identically at v1.7.15 and on the legacy
planner — and they need a different mechanism, since there are no components to hand the
aggregation down to. Tracked separately.

Multiplied measures were bucketed by owning cube and the whole bucket was
then required to resolve to a single multi-fact join group, so two measures
of one cube needing different join trees were refused with "Expected just
one multi-fact join group".

The key cube fixes the primary keys to deduplicate on and the join tree
fixes the joins to build; neither determines the other. Group by both and
emit one AggregateMultipliedSubquery per pair, mirroring the regular-measure
branch, which already emits one CTE per join group.

Trees are kept apart rather than merged so that a measure's value does not
depend on which other measures share the query: merging sibling one-to-many
branches introduces a cross product that neither original tree had.

With a single group per cube the plan is unchanged, so queries that planned
before are unaffected.
Reaching a cube beyond its own makes a multiplied measure go through the
measure-join subquery, which renders measures without their aggregate so the
select above can re-aggregate them. A calculated measure has no aggregate of
its own, so nothing was re-applied: its components lost theirs inside the
subquery and the expression came out neither aggregated nor grouped.

Mark such a measure composite instead, so its components travel in its place
- each on the join tree its own definition asks for - and the expression is
rebuilt above them.

Only when the reach runs entirely through those components. A dimension
dependency, a raw cube reference or no measure dependency at all leaves the
measure with something to evaluate where it stands, and the select above has
no cube joined in to evaluate it against; those keep going whole.
@waralexrom
waralexrom requested a review from a team as a code owner July 31, 2026 07:45
@github-actions github-actions Bot added rust Pull requests that update Rust code javascript Pull requests that update Javascript code labels Jul 31, 2026
@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Claude finished @waralexrom's task in 5m 50s —— View job


Review complete — 2 medium, 2 low, no blockers. Both fixes look sound; the main question is whether the calculated-measure decomposition should be gated on the measure actually being multiplied.

Full review
  • Gather PR context and diff
  • Review multiplied_measures_query_planner.rs changes
  • Review multiplied_measures_collector.rs changes
  • Review test fixtures, seed data and integration tests
  • Hand-verify snapshot values against the seed
  • Post review

What's good

  • The (cube, join tree) keying is the right shape and genuinely mirrors the regular-measure branch above it. Group order comes from MultiFactJoinGroups::build_groups' key_order, not the HashMap, so CTE numbering stays deterministic — the same property the regular branch already relies on.
  • Keeping trees apart rather than merging them is the correct call, and the commit message explains why (merging sibling one-to-many branches introduces a cross product that flips measures from regular to multiplied).
  • The fixture is well built for the job: TEXT primary key so a lost aggregation surfaces as numeric * text rather than a wrong number, p3/p5 with no customer so a tree rooted at customers is genuinely unreachable, p1 carrying two meta rows and two tags so both fan-out branches are exercised.
  • I hand-checked every snapshot value against the seed (all 14) — success_rate A = 100·2/3, rate_vs_commission C = 100·500/50, converted_per_max_amount SUCCESS = 1000/500, the NULL gold_amount for B/C, payment_tags__count = 4 for SUCCESS — they all reconcile.
  • Good negative controls: test_star_with_two_fan_out_branches_is_rejected asserts the error names the offending measure, and the three "must not decompose" shapes (converted_per_max_amount, amount_over_fx, max_fx_rate) pin down both halves of the gate. The #[ignore] reasons are honest about what still doesn't work.
  • The JS test asserting the legacy failure explicitly, rather than skipping it, is the right way to record a planner divergence.

Findings

Sev Where Issue
Medium multiplied_measures_collector.rs:74-84 New composite gate is join-independent, so it also decomposes calculated measures in multi-fact queries where nothing is multiplied — a plan change for queries that already worked, with a NULL-vs-value semantic risk when component legs root at different cubes. collect_multiplied_measures already has the join, so the gate can be narrowed with join.is_multiplied(...).
Medium multiplied_measures_query_planner.rs:122-132 debug_assert_eq! guarding "no measure was dropped by grouping" compiles away in release; the failure mode there is a plan silently missing a measure. Cheap to make a returned CubeError::internal.
Low multiplied_measures_collector.rs:38-44 travels_only_through_measures inspects the un-stripped node while reaches_other_cube strips the join prefix — inconsistent input to the two halves of the same gate, plus a repeated tree walk.
Low calculated_multi_fact.rs:368 #[ignore] reasons don't link the tracking issue; no plan-shape assertion, so without the integration-postgres feature nothing pins "one subquery per (cube, join tree)".

Details are in the four inline comments.

Checked and clear

  • Multi-stage interactionfull_key_aggregate_measures routes multi-stage measures away before collect_multiplied_measures, so the new gate can't pull a multi-stage component into a leaf/multiplied bucket.
  • Pre-existing composite rule overlap — the parent-cube-differs rule already marked calculated measures whose direct measure children live elsewhere, so the new rule only adds the transitive case. Scope is narrower than the PR body's framing suggests, which is good.
  • Symbol identitybuild_groups returns the same Rcs it was handed, so MultiStageSubqueryRef::symbols(group_measures) still matches what the caller joins over.
  • single_join() removal — the replacement empty-check preserves the previous "no groups" error and drops only the multi-group rejection, which is the point of the fix.
  • Error-message typo fix (stray ' before references cubes) — nice catch, and the new integration test asserts on that string.
  • Security / input handling — nothing user-facing here; no new SQL string interpolation beyond existing rendering paths.
  • Docs — no user-visible surface changes, so no /docs-mintlify update needed.
· branch `tesseract-fix-distinct-over-text-id`

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.43%. Comparing base (6ca51d8) to head (eb87dac).
⚠️ Report is 8 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #11436      +/-   ##
==========================================
- Coverage   83.94%   79.43%   -4.52%     
==========================================
  Files         257      480     +223     
  Lines       80887    98778   +17891     
  Branches        0     3636    +3636     
==========================================
+ Hits        67904    78464   +10560     
- Misses      12983    19796    +6813     
- Partials        0      518     +518     
Flag Coverage Δ
cube-backend 59.00% <ø> (?)
cubesql 83.95% <ø> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Decomposition exists to work around the measure-join subquery, which is
built only for a measure that is multiplied by its join tree. Elsewhere the
measure is read off a leaf-measure query that keeps its aggregate, and
splitting it changes the answer: components can root at different cubes, so
each divides by the rows its own leg sees rather than by the query's.

Fail loudly instead of asserting when join grouping drops a measure of a
bucket - a debug assertion leaves release builds emitting a plan with the
measure missing.
@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Claude finished @waralexrom's task in 5m 19s —— View job


Re-review of 30a2bc6 — both prior mediums fixed, 1 low remaining, no blockers. The is_multiplied narrowing is correct and now exactly aligned with where the measure-join subquery is built.

Full review
  • Gather diff for the new commit 30a2bc6
  • Verify the is_multiplied gate narrowing against check_should_build_join_for_measure_select
  • Verify the debug_assertCubeError::internal conversion and its reachability
  • Check the new gold_share fixture / snapshot values against the seed
  • Review the new tests/multiplied_measures_collector.rs unit suite
  • Re-check prior findings, resolve stale threads, post review

Prior findings — status

Prior finding Status
Medium: composite gate was join-independent Fixed. Gate now requires self.join.is_multiplied(&owned.cube_name()); join is threaded into CompositeMeasuresCollector from collect_multiplied_measures.
Medium: debug_assert_eq! compiles away in release Fixed. Now a returned CubeError::internal naming the cube and both counts.
Low: travels_only_through_measures vs reaches_other_cube read differently-stripped nodes Fixed. Both take &owned; reaches_other_cube no longer strips internally, so it's stripped once per node instead of twice.
Low: #[ignore] reasons carry no tracking link; nothing pins plan shape without integration-postgres Partly. The new unit suite gives the decomposition gate feature-free coverage, which is the bigger half. Still no issue link on the two ignores and still nothing pins "one AggregateMultipliedSubquery per (cube, join tree)" without a database. Prior thread left unresolved.

Verification of the narrowing

The gate is now exactly as tight as the defect. I traced the two paths:

  • Classification and planning agree on the same join. full_key_aggregate_measures computes the per-measure join (query_properties.rs:530-533) and hands it to collect_multiplied_measures; MultipliedMeasuresCollector sets multiplied from that same join.is_multiplied(cube) (multiplied_measures_collector.rs:140). So is_multiplied == false in the gate ⇒ the measure is classified regular ⇒ it never reaches aggregate_subquery_plan, hence never meets the ungrouped measure-join subquery. No case where the gate says "don't decompose" but the subquery is built anyway.
  • owned.cube_name() is safe to compare against the unstripped collector's. CompiledMemberPath::strip_join_prefix (compiled_member_path.rs:61-69) rewrites only path, keeping cube and full_name, so stripping cannot shift which cube is_multiplied is asked about, and get_dependencies()/get_cube_refs() are unaffected too — the two halves of the gate now genuinely judge the same symbol.
  • The new hard error is unreachable rather than newly-throwing. The only measure-dropping path into build_groups is MeasuresJoinHints::from_base_hints' has_multi_stage_members(m, true) filter (multi_fact_join_groups.rs:97-101); upstream full_key_aggregate_measures filters with has_multi_stage_members(m, self.ignore_cumulative || self.pre_aggregation_query), which is a superset (flag false also counts cumulative). So anything surviving upstream survives the filter, and build_groups is otherwise a strict partition (key_order/grouped, lines 222-236). Duplicate-inflation of measures.len() is also ruled out — multiplied_measures is unique_by(full_name) at query_properties.rs:577-581.

New test suite

tests/multiplied_measures_collector.rs is a good addition and the right level: it pins the gate directly rather than through SQL, and it runs without the integration-postgres feature. The five cases cover both halves of the conjunction plus the new multiplication half, and each asserts is_multiplied and the collected names, so a test can't pass for the wrong reason (e.g. test_calculated_measure_kept_whole_when_it_reaches_no_other_cube asserts multiplied-but-not-decomposed, which is the case that would otherwise silently swap with the not-multiplied one). measure_and_join builds the join the same way full_key_aggregate_measures does, so it's testing the real input.

Fixture / snapshot check

gold_share and its snapshot reconcile against the seed: SUCCESS = 100·300/700 (p1+p2 gold, p5 excluded for having no customer), DECLINED = NULL via NULLIF(0,0) since p3 has no customer, sibling total_amount 1200/300 over all rows. One readability note posted inline — the 700 denominator is what makes 42.857 hand-checkable and the doc comment doesn't name it.

Findings

Sev Where Issue
Low calculated_multi_fact.rs:414-418 New control test's snapshot shows a ratio (/700) and a total (1200) that don't reconcile; doc comment argues the opposite direction and doesn't name the denominator. Inline comment posted.
Low calculated_multi_fact.rs:369,393 Prior thread re-affirmed: #[ignore] reasons still carry no tracking-issue link, and no plan-shape assertion pins the per-(cube, join tree) subquery split in feature-less runs.

Checked and clear

  • Pre-aggregation matching — making decomposition join-dependent means MultiFactJoinGroups::multiplied_measures() (group join) and full_key_aggregate_measures (per-measure join) can in principle disagree about which leaves are reported. I worked the guard at optimizer.rs:524-530 through both directions: a calculated measure is reported by name only when multiplied, and it decomposes exactly when multiplied, so pre_aggr_multiplied's contents are unchanged by this commit for the shapes that guard compares. No regression.
  • Pre-existing parent-cube-differs rule (multiplied_measures_collector.rs:68-72) is deliberately left join-independent — correct, since a direct measure child on another cube must split regardless of multiplication. The two composite rules disagree about whether multiplication matters, but for the right reason.
  • Error message formatting — mixed inline-capture/positional format! args are valid; message names the cube and both counts, which is what you'd want from a bug report.
  • Security / docs — unchanged from the first pass: no new interpolation, no user-visible surface, no /docs-mintlify change needed.

Not run: cargo test / cargo fmt — the sandbox denied cargo invocations here, so the new unit suite and the is_multiplied change are reviewed by reading only. CI results stand as the check.

· branch `tesseract-fix-distinct-over-text-id`

The join grouping that multiplied measures rely on was only covered through
Postgres-backed snapshots, so a run without the integration feature asserted
nothing about it. Cover it where the grouping is decided.

Record the denominator the non-multiplied control divides by: the ratio and
the sibling total on that row are taken over different rows and are not meant
to reconcile, which is not evident from the snapshot alone.
@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Claude finished @waralexrom's task in 1m 31s —— View job


Re-review of 5342465 — no blockers, 1 low. Doc-comment finding is addressed; the new unit test guards a precondition of the fix rather than the fix itself.

Full review
  • Diff the new commit 5342465
  • Verify the new multi_fact_join_groups unit test against build_groups
  • Verify the updated gold_share doc comment against the snapshot
  • Reconcile against prior review threads, resolve stale ones, post review

Prior findings — status

Prior finding Status
Low: gold_share snapshot's 700 denominator vs sibling 1200 not recorded Fixed. Doc comment now names both numbers and states they aren't meant to reconcile. Thread resolved.
Low: nothing pins the per-(cube, join tree) split without integration-postgres Partly. New unit test adds feature-free coverage at the grouping layer, but that layer was already correct pre-PR — see below. New inline comment posted; prior thread left unresolved (the #[ignore] tracking-link half is also still open).

On the new test

test_two_groups_for_measures_of_one_cube is a reasonable regression guard and reads well — assert_eq!(total_amount.cube_name(), converted_value.cube_name()) up front makes the point of the test explicit, and asserting the grouped full_names (not just num_groups() == 2) rules out passing for the wrong reason.

But it doesn't guard this PR's first fix. build_groups is unchanged by the PR and already keyed on JoinKey, never on the owning cube (multi_fact_join_groups.rs:222-236); the cube-keyed bucketing lived in MultipliedMeasuresQueryPlanner::plan_queries. So this test passes on master as well, and reverting plan_queries to single_join() leaves it green. The commit message's "cover it where the grouping is decided" is accurate about the layer, just not about which layer the defect was in.

Doc-comment update checks out: SUCCESS = 100·300/700 = 42.857 (p1+p2 gold over p1+p2+p4, p5 having no customer), sibling payments.total_amount = 1200 over all rows. Naming both is exactly what makes the snapshot hand-checkable, and the "not meant to reconcile" sentence removes the wrong reading a reader would otherwise reach first.

Findings

Sev Where Issue
Low multi_fact_join_groups.rs:562 New unit test pins a precondition (build_groups splits same-cube measures) that already held on master, not the planner-side split the PR fixes; the per-(cube, join tree) subquery emission is still snapshot-only. Inline comment posted.

Checked and clear

  • Test placement — the test lives in multi_fact_join_groups' own mod tests, uses the same MeasuresJoinHints::builder(...).add_dimensions(...).build(...) shape as the neighbouring test_multi_fact_two_groups, and reuses the new PR fixture rather than adding another. Consistent with the file.
  • Group-order assertion — asserting vec![total_amount], vec![converted_value] in order is safe, not flaky: build_groups returns key_order (insertion order of measure_hints), not HashMap order.
  • Production code — unchanged in this commit; the second fix's is_multiplied narrowing and the CubeError::internal conversion, both verified in the previous pass, are untouched.
  • No new snapshot files and no snapshot value changes — only a doc comment moved in calculated_multi_fact.rs.

Not run: cargo test / cargo fmt — cargo invocations are denied in this sandbox, so the new unit test is reviewed by reading only. CI stands as the check.

· branch tesseract-fix-distinct-over-text-id ·

The suite proved the numbers through Postgres but said nothing about how the
plan reached them, so a run without the integration feature let a regression
in the join grouping through as long as the query still planned.

Count the deduplicating key subselects - one per (key cube, join tree) - and
check whether a calculated measure's components get columns of their own.
Both read structure out of the generated SQL rather than pinning it.
@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Claude finished @waralexrom's task in 2m 54s —— View job


Re-review of 7c77708 — no blockers, 1 low. The plan-shape assertions close the feature-free coverage gap; counts and the AS "keys" proxy both check out.

Full review
  • Diff the new commit 7c77708
  • Verify keys_subquery_count really counts one per multiplied subquery
  • Verify each asserted count against the fixture's join trees
  • Verify projects_column_for against the alias rendering
  • Reconcile prior threads, resolve stale ones, post review

Prior findings — status

Prior finding Status
Low: nothing pins the per-(cube, join tree) subquery split without integration-postgres Fixed. Seven tests now assert keys_subquery_count, and two assert component-column presence. Thread on multi_fact_join_groups.rs:562 resolved.
Low: #[ignore] reasons carry no tracking-issue link Still open (lines 398, 422). Thread left unresolved; not a blocker.

Verification

The AS "keys" proxy is sound. Each AggregateMultipliedSubquery renders exactly one keys subselect under the hardcoded alias keys (aggregate_multiplied_subquery.rs:53), which reaches SQL through SingleAliasedSource::to_sqltemplates.query_aliased{{ query }} AS {{ quoted_alias }} (BaseQuery.js:4617), i.e. ) AS "keys" — the space and the quotes the helper matches are both real. No collision with the other keys-shaped alias in the planner: keys_aggregate_strategy.rs:127 uses fk_aggregate_keys, which the leading " in the pattern excludes.

Every asserted count reconciles with the fixture's join trees:

Test Count Legs
calculated_measure_alone 1 success_rate whole on {payments, payment_meta}
two_sums_of_one_cube_needing_different_joins 2 +rates vs +merchants
calculated_measure_with_joined_measure 2 success_rate (bare) vs converted_value (+rates)
three_nested_join_trees_of_one_cube 3 bare / +rates / +rates,+merchants
calculated_measure_reaching_other_cubes_alone 2 the decomposition itself — converted_value and commissioned_value on their own trees
split_groups_rooted_at_different_cubes 2 rooted at payments vs at customers
..._without_multiplication 0 no fan-out dimension → nothing multiplied

The reaching_other_cubes_alone case is the one that actually pins the second fix: without decomposition rate_vs_commission is one whole measure, so both the count of 2 and the two component columns would go away. Good choice of anchor.

projects_column_for matches the real alias shape — the fully-quoted "cube__member" form with the closing quote rules out prefix collisions (payments__converted_value vs a longer alias), and all the member names here are well under any truncation threshold.

Findings

Sev Where Issue
Low calculated_multi_fact.rs:468-471 The negated projects_column_for assertion passes vacuously if the alias convention the helper assumes stops holding; a positive assertion for payments.gold_share in the same test would anchor it. Inline comment posted.

Checked and clear

  • Dialect coupling — both helpers assume double-quote identifiers. TestContext builds against the Postgres templates, and these are the SQL-shape assertions rather than the portable ones, so this is fine; the failure mode of a dialect change is a loud count mismatch, not a silent pass (except in the one negated case above).
  • Failure messages — every assertion carries "sql: {sql}", so a mismatch prints the plan it disagrees with. Exactly what you want from a structural assertion whose expected value is a bare integer.
  • No production changes in this commit; the is_multiplied narrowing and the CubeError::internal conversion verified in the previous passes are untouched. No snapshot values changed.
  • Comment accuracy — the keys_subquery_count doc comment states the invariant it relies on ("one per (key cube, join tree)") and that it runs without a database, which is the part a future reader needs.

Not run: cargo test / cargo fmt — cargo invocations are denied in this sandbox, so the new assertions are reviewed by reading only. CI stands as the check.

· branch tesseract-fix-distinct-over-text-id ·

A result snapshot records the numbers a query returned but not how it got
them, so what these cases exist to pin was only in their names. Assert the
leg count everywhere the strategy is the point: aggregated in place or
deduplicated through the keys path, measures of one cube split across trees
or grouped onto one.

The mixed-shapes case wanted three legs and gets two - the calculated measure
shares a tree with the bare aggregate, which is grouping working rather than
a shape being missed.
@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Claude finished @waralexrom's task in 2m 20s —— View job


Re-review of 6bd2879 — no blockers, 2 low. Every new leg count reconciles with the fixture; one doc comment now says the opposite of what its assertion pins.

Full review
  • Diff the new commit 6bd2879
  • Verify each new keys_subquery_count assertion against the fixture's joins and multiplication factors
  • Verify the new negated projects_column_for assertion
  • Reconcile prior threads, post review

Verification of the new counts

I worked each one out from the fixture rather than from the snapshot. find_multiplication_factor_for (mock_join_graph.rs:356-358) marks a cube multiplied when it is the from side of a one_to_many on the tree — so payments is multiplied whenever payment_meta/payment_tags are in the tree, and the fan-out cubes themselves never are.

Test Asserted Why
calculated_measure_components_by_fan_out_dimension 0 both components are count_distinct — multiplication-safe, aggregated in place
joined_measure_with_distinct_count_by_fan_out_dimension 1 converted_value (sum) takes the keys path, count doesn't
calculated_measure_over_sums_with_joined_measure 2 success_amount_rate on {payments}, converted_value on +rates
two_sibling_lookup_join_trees_of_one_cube 2 +rates vs +merchants, neither containing the other
calculated_measure_over_components_with_different_joins 3 the two decomposed components plus total_amount on the bare tree
measures_of_two_sibling_fan_out_cubes 0 nothing multiplied — see finding
several_join_tree_shapes_in_one_query 2 success_rate shares {payments} with total_amount; net_value gets its own
calculated_measure_reading_its_own_cube_directly 0 dimension is payments.status, no fan-out
calculated_measure_with_joined_measure_without_fan_out 0 same

The several_join_tree_shapes case is the interesting one, and the commit handles it the right way: the count came out 2 rather than the 3 the test's name suggests, and instead of contorting the query to reach 3 the doc comment now records why two is correct (grouping is by join tree, so the calculated measure shares the bare aggregate's). That's the more useful assertion — it pins that grouping actually merges, which nothing else in the suite did.

Worth noting the pair calculated_measure_alone (1) and calculated_measure_components (0) now makes a real planner behaviour visible: success_rate is routed through the keys path even though both its components are multiplication-safe on their own, because a type: number measure isn't classified safe. Conservative, correct, and previously invisible.

Prior findings — status

Prior finding Status
Low: #[ignore] reasons carry no tracking-issue link Still open (lines 418, 442). Thread re-affirmed, left unresolved. Its second half — nothing pins plan shape without the feature — was fixed by 7c77708 and this commit extends that coverage to nine tests.
Low: negated projects_column_for for gold_amount can pass vacuously Still open (line 490). Thread re-affirmed, and this commit adds a second instance at line 404 — new inline comment posted there, since that one has no positive assertion anywhere in its test.

Findings

Sev Where Issue
Low calculated_multi_fact.rs:328-346 Doc comment says the three measures are "each needing its own deduplication" while the new assertion pins zero keys legs. What they need is one leaf query each via FullKeyAggregate — a different mechanism. Inline comment posted.
Low calculated_multi_fact.rs:401-406 New negated projects_column_for has no positive counterpart in its test, so it reads "not decomposed" for any input if the alias convention shifts. Inline comment posted.

Checked and clear

  • No production changes in this commit. The is_multiplied narrowing and the CubeError::internal conversion, verified in earlier passes, are untouched; no snapshot values changed.
  • AS "keys" proxy — re-checked, still one per AggregateMultipliedSubquery (aggregate_multiplied_subquery.rs:53templates.query_aliased), and the leading quote excludes fk_aggregate_keys.
  • Failure messages — every new assertion carries "sql: {sql}", so a count mismatch prints the plan it disagrees with. That matters more here than for a snapshot, where the diff is self-explanatory.
  • Helper doc comments — the rewritten keys_subquery_count comment states why a count assertion earns its place next to a result snapshot, which is the part a future reader would otherwise delete as redundant. Good.
  • Coverage direction — the two #[ignore]d tests are deliberately left without count assertions, which is right: their expected plan shape isn't known yet.

Not run: cargo test / cargo fmt — cargo invocations are denied in this sandbox, so the new assertions are reviewed by reading only. CI stands as the check.

· branch [`tesseract-fix-distinct-over-text-id`](https://github.com/cube-js/cube/tree/tesseract-fix-distinct-over-text-id)

… unit test

A negated substring match passes both when a measure genuinely stays whole
and when the alias convention the helper reads stops holding, at which point
the assertion is inert without failing. Pair each with the positive case from
the same query, which cannot hold unless the convention does.

Say what the same-cube grouping unit test covers - the precondition callers
slice on, not what any of them emits per group - and correct the sibling
fan-out case, whose prose claimed a deduplication its own assertion denies.
@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Claude finished @waralexrom's task in 1m 28s —— View job


Re-review of 4e7593b — no blockers, 1 low (pre-existing). Both negated assertions are now anchored, and the two doc-comment corrections are accurate. Tests only.

Full review
  • Diff the new commit 4e7593b
  • Verify each anchoring positive assertion actually pins the alias convention
  • Verify the corrected two_sibling_fan_out doc comment against the fixture's join directions
  • Verify the multi_fact_join_groups scope note
  • Reconcile prior threads, resolve the addressed ones, post review

Prior findings — status

Prior finding Status
Low: negated projects_column_for for gold_amount can pass vacuously (line 504) Fixed. Positive assertion on payments.gold_share added before it, with a comment saying why it comes first. Thread resolved.
Low: same pattern for converted_value with no positive anchor in its test (line 413) Fixed. Positive assertion on payments.converted_per_max_amount added. Thread resolved.
Low: two_sibling_fan_out prose claimed a dedup its own assertion denies (line 328) Fixed. Now names FullKeyAggregate and gives both reasons nothing is multiplied. Thread resolved.
Low: #[ignore] reasons carry no tracking-issue link Still open (lines 425, 449). Re-affirmed, thread left unresolved. Not a blocker — the reasons themselves are honest about the mechanism.

Verification

The anchoring works, and in the specific way that matters. A positive projects_column_for on a requested measure holds in both the whole and decomposed plans, so it can't distinguish them — that's not its job here; it fails exactly when the convention the helper reads stops holding, which is the failure mode that made the negated assertion inert. And the anchor is the longer name in both pairs (payments__converted_per_max_amount at 33 chars vs payments__converted_value at 25; gold_share vs gold_amount is a wash but the quote/separator half is covered), so a truncation or hashing scheme trips the positive before it silently satisfies the negative. Neither name is a substring of its counterpart in either direction, so no accidental match.

The corrected two_sibling_fan_out doc comment checks out against the fixture. payment_meta and payment_tags are each the to side of a one_to_many declared on payments, so find_multiplication_factor_for never marks them multiplied and their counts aggregate in place; payments.total_amount's own tree is {payments} because the sole dimension is payments.status, so payments isn't multiplied either. Zero keys legs, three leaf queries joined by FullKeyAggregate — the prose and the assertion now name the same mechanism, which was the point.

The multi_fact_join_groups scope note is the right correction rather than a hedge. It says the test pins the precondition callers slice on and explicitly disclaims covering what any caller emits per group — which is exactly the boundary I'd flagged, and it leaves the planner-side split to keys_subquery_count, where 7c77708 put it. The two layers now each say what they cover.

Findings

None new. One prior low re-affirmed (see table).

Checked and clear

  • No production changes in this commit — the is_multiplied narrowing and the CubeError::internal conversion verified in earlier passes are untouched, and no snapshot values moved.
  • Assertion ordering — putting the positive first means a convention drift reports as the convention failing rather than as a spurious "measure was decomposed", so the failure message points at the real cause.
  • Comment style — both corrected comments state the reason inline (the two fan-out cubes sit on the many side of their own joins) rather than just the conclusion, matching the rest of the suite.
  • Coverage of the two fixes — unchanged and still intact: calculated_measure_reaching_other_cubes_alone (2 legs + two component columns) anchors the decomposition fix, two_sums_of_one_cube_needing_different_joins / three_nested_join_trees_of_one_cube anchor the per-(cube, join tree) split.

Not run: cargo test / cargo fmt — cargo invocations are denied in this sandbox, so the new assertions are reviewed by reading only. CI stands as the check.

· branch tesseract-fix-distinct-over-text-id ·

Splitting a calculated measure into its components put the two halves of an
expression on separate legs. When those legs root at different cubes they see
different rows, so the ratio came out over a denominator its numerator never
saw - answered, with no error, differently depending on whether the query
carried a fan-out dimension.

The split also never covered the shape it was written for: whether the
measure subquery is built is decided per join group, while the condition for
splitting was read off a single measure, so a calculated measure whose own
dependencies stay inside its cube still lands in that subquery whenever a
group member reaches out of it.

Refuse instead. A measure carrying no aggregate of its own cannot survive a
subquery that renders measures without theirs, whichever way it got there, so
say that where the subquery is built and name the measure. Shapes that used
to reach the database as SQL it rejects now stop at the planner.
@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Claude finished @waralexrom's task in 4m 33s —— View job


Re-review of aaff392 — no blockers, 2 low. The direction change from decompose to refuse is the right call and I could not find a query it newly breaks; the user-facing message is the one thing that doesn't hold in all the shapes it now fires on.

Full review
  • Diff the new commit aaff392
  • Verify the refusal fires only where the ungrouped measure subquery is built
  • Check whether any shape that previously produced correct SQL is now refused
  • Verify the JS integration test still passes under Tesseract with decomposition gone
  • Check what the deleted unit suite and two snapshots cost in coverage
  • Reconcile prior threads, resolve stale ones, post review

What this commit does

It reverts the second fix. Instead of decomposing a calculated measure into its components, the planner now refuses any measure whose aggregate_wrap() is PassThrough at the point where the measure subquery is built. Two reasons given, both of which I was able to confirm:

  • The split was semantically unsound. MeasureSubquery's processor sets set_ungrouped_measure(true), and FinalMeasureSqlNode propagates the pass-through visitor to children (final_measure.rs:65), so components on separate legs rooted at different cubes genuinely see different row sets. The prior gold_share snapshot (100·300/700 next to a sibling reading 1200) was that divergence written down as an expected value — the thread I opened on it was a symptom of the design, not of the comment.
  • The gate never covered its own case. check_should_build_join_for_measure_select decides per group, while the old is_multiplied && travels_only_through_measures && reaches_other_cube conjunction was read off one symbol. The new test_calculated_measure_pulled_into_a_shared_measure_subquery is the proof: add rates.currency as a dimension and both measures land on one join key, so success_rate enters the subquery without satisfying any part of the old gate. That is the shape that made the split unfixable in place.

No regression, checked in both directions

AggregateWrap::PassThrough is Calculated(_) (all type: number/string/time/boolean), NumberAgg, and Rank (measure_kinds/mod.rs:214-225). I worked through each way one could land in the subquery and produce valid SQL before this commit:

Shape Before Now
calculated over own-cube sums ungrouped subquery drops the children's aggregates → must appear in the GROUP BY clause refused
calculated with a {other.measure} component components de-aggregated → numeric * text on the TEXT pk fixture refused
number_agg with a hand-written MAX(...) MAX inside a select with no GROUP BY → invalid refused
constant type: number (sql: "100") subquery projects it, outer aggregating select references it unaggregated → invalid refused
Rank filtered upstream by has_multi_stage_members, never reaches this bucket unchanged

So every refused shape previously reached the database as SQL it rejects. Nothing that worked stops working.

The JS suite still passes under Tesseract, and for a non-obvious reason worth recording: calculated-measure-multi-fact.test.ts asserts successRate + convertedValue by Meta.value succeeds on Tesseract, and it still does — but only because fix #1 splits them. successRate's tree is {Payments, Meta}, convertedValue's is +Rates, so successRate sits in a group of its own where collect_cube_names is {payments}, should_build_join is false, and it reads the pk cube directly with its aggregate intact over the deduplicated key set. The two fixes are now load-bearing for each other: without the per-join-tree split, that test's query would hit the new refusal. Worth a sentence in the PR body, since it's the difference between "works" and "refused" for the originally reported model.

Findings

Sev Where Issue
Low multiplied_measures_query_planner.rs:236-242 The message claims the named measure "reaches another cube" and advises rewriting it "over measures of its own cube". In the shared-group case both are false — success_rate reaches nothing and is already over its own cube's measures; the reach belongs to a sibling. check_should_build_join_for_measure_select already has the triggering measure in hand. Inline comment posted.
Low multiplied_measures_query_planner.rs:231-234 let Ok(symbol) = ... else { continue } exempts member-expression measures, which the sibling check handles explicitly. Not a regression, but the exemption reads as an oversight without a note. Inline comment posted.

Prior findings — status

Prior finding Status
Low: #[ignore] reasons carry no tracking link; plan shape unpinned without the feature Resolved. Both #[ignore]s are gone — those tests now assert the refusal and run feature-free. Thread resolved.
Everything else from the earlier passes Already resolved; the two deleted threads' subjects (the composite gate, the travels_only_through_measures asymmetry) no longer exist in the diff.

Coverage

Net loss is small and in the right direction: the deleted tests/multiplied_measures_collector.rs tested a gate that no longer exists, and the two deleted snapshots recorded the unsound split's output. The five shapes that now error are each asserted through expect_no_own_aggregate_error, which checks the reason string and that the message names the offending measure — so a refusal for the wrong reason, or one that doesn't tell the user which measure to change, fails the test. build_sql returning Err means all five run without integration-postgres.

One gap: nothing asserts the JS-test property above at the Rust level — that success_rate + converted_value by payment_meta.value alone still succeeds while adding rates.currency refuses it. test_calculated_measure_with_joined_measure covers the success half and the new test the refusal half, but they're in different places and neither says it's the same query one dimension apart. A cross-reference in either doc comment would keep the pair from drifting.

Documentation

The PR body is now stale in three places: it still presents the second fix as "Decompose calculated measures that reach other cubes" with the reasoning for the decomposition gate, still says "17 tests, 2 #[ignore]d", and still describes the two MAX({rates.fx_rate}) shapes as tracked-separately follow-ups when they're now refused with a message. Since this is the text that lands in the squashed commit, worth rewriting before merge.

Checked and clear

  • Call sitesaggregate_subquery_plan has exactly one caller, so the refusal cannot leak into a path that doesn't build the subquery. CubeError::user (not internal) is right: this is a modeling limitation, and it surfaces as a 4xx.
  • Importcrate::planner::AggregateWrap resolves via pub use symbols::* in planner/mod.rs:41.
  • Fix ReferenceError: self is not defined - Issue with whatwg-fetch when using the core lib with Node.js #1 untouched — the per-(cube, join tree) loop, the CubeError::internal grouping invariant, and all the count assertions from 7c77708/6bd2879 are unchanged.
  • Security / performance — no new interpolation; the removed collector helpers drop a repeated collect_cube_names walk per calculated node, so this is marginally cheaper than the previous commit.

Not run: cargo test / cargo fmt — cargo invocations are denied in this sandbox, so the new check and the rewritten tests are reviewed by reading only. CI stands as the check.

· branch tesseract-fix-distinct-over-text-id ·

Refusing during logical planning ran ahead of the pre-aggregation optimizer,
which may replace the whole node with a rollup and never render the subquery
at all - a query a rollup answers was turned into an error. Move the check to
where the subquery is actually built, past the point the override returns.

Say what the user can do about it. The subquery is forced by whichever
measure of the group reaches another cube, which need not be the one being
refused, so telling that measure to be rewritten over its own cube asks for
something it may already satisfy; ask for a separate query instead.
@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Claude finished @waralexrom's task in 4m 35s —— View job


Re-review of eb87dac — no blockers, 3 low. Moving the check to where the subquery renders is correct and I confirmed the motivating path is real; the message now misses the single-measure case instead of the shared-group one.

Full review
  • Diff the new commit eb87dac
  • Verify the pre-aggregation override really precedes the check now
  • Verify the check still fires through build_sql, and on the same measure set
  • Check the new message against each of the five refusal tests
  • Assess what the trimmed doc comments and deleted negated assertions cost
  • Reconcile prior threads, resolve the outdated ones, post review

The move is right, and the motivation checks out

I traced the path the commit message describes rather than taking it on trust. optimizer.rs:255-277 rewrites an AggregateMultipliedSubquery into a clone carrying pre_aggregation_override: Some(rewritten), and process returns that override at lines 60-62 — before the MeasureSubquery arm where the check now sits. So with the check in plan_queries it fired during logical planning, ahead of the optimizer, and a query a rollup could have answered was refused. That's a genuine behavioural improvement, not just a relocation.

The relocation is also equivalent where it should be:

  • Same measure set. aggregate_subquery_measure builds the MeasureSubquery's schema with set_measures(measures.clone()) — the group's measures, i.e. exactly the list the old check iterated.
  • Still reaches the user. TestContext::build_sqlTopLevelPlanner::plan() includes physical plan building, which is why the five refusal tests still pass through build_sql alone with no integration-postgres feature. CubeError::user is preserved, so it still surfaces as a modeling error rather than a 500.
  • Correctly ordered against the other early return. The check sits after if context.dimensions_query { return Ok(keys_query) }, so a dimensions-only render — which projects no measures and therefore has nothing to de-aggregate — isn't refused.
  • Single producer. AggregateMultipliedSubquerySource::MeasureSubquery is constructed only in aggregate_subquery_plan, so there's no second path into this arm that would now be checked for the first time.
  • Import moved cleanly (crate::planner::{AggregateWrap, MemberSymbol}); AggregateWrap dropped from the planner's imports, MemberSymbol/Rc still used there.

Findings

Sev Where Issue
Low aggregate_multiplied_subquery.rs:23-28 The message presumes a sibling pulled the measure in ("alongside a measure that joins another cube", "request it in a separate query"), but four of the five tests asserting it request the measure alone and it still fails. test_calculated_measure_over_components_with_different_joins' own doc comment says splitting doesn't help. Inline comment with wording that holds in both arms.
Low aggregate_multiplied_subquery.rs:142 The behaviour the commit exists for is untested — the fixture declares no pre_aggregations, so no test exercises the override path and reverting the move leaves the suite green. Inline comment posted.
Low aggregate_multiplied_subquery.rs:21 Re-affirmed from the outdated thread (the continue moved verbatim): member-expression measures are silently exempt, and the exemption reads as an oversight without a note.

On the first one — only test_calculated_measure_pulled_into_a_shared_measure_subquery is actually helped by a separate query. I checked why: check_should_build_join_for_measure_select reads cube names off the measures of the group, never the dimensions, so success_rate alone has cubes == {payments} == key_cube and plans fine. In the other four the measure reaches out itself, so it's refused alone too.

Prior threads

Both remaining threads were on code this commit moved out of multiplied_measures_query_planner.rs, so both are resolved. The message-accuracy one is genuinely addressed for the case it was opened about — the text no longer claims the named measure reaches another cube — hence the new thread for the arm it now gets wrong, rather than a reopen.

Test-file changes

  • Dropping the two negated projects_column_for assertions (converted_value, gold_amount) is right: with decomposition gone there is no plan in which those columns would appear, so the assertions were vestigial rather than load-bearing. The positive anchors added in 4e7593b are kept, which is the half that still says something.
  • expect_no_own_aggregate_error tracks the reworded message (has no aggregate of its own) and still asserts the message names the measure, so a refusal that doesn't tell the user what to change fails the test.
  • Helper doc comments were trimmed hard. keys_subquery_count keeps the sentence a future reader needs ("which the result snapshot alone cannot"), so the loss is acceptable; projects_column_for and expect_no_own_aggregate_error are now bare, and the latter's deleted comment was the only place recording why these shapes are refused rather than attempted. Not worth a finding, but the check_measures_survive_measure_subquery doc comment is now carrying that explanation alone.
  • test_calculated_measure_reaching_other_cubes_alone's doc comment is corrected to describe refusal rather than the removed decomposition — consistent with aaff392's direction.

Checked and clear

  • Fix ReferenceError: self is not defined - Issue with whatwg-fetch when using the core lib with Node.js #1 untouched. The per-(cube, join tree) loop, the CubeError::internal grouping invariant, and every keys_subquery_count assertion are unchanged by this commit.
  • No snapshot churn. No .snap files added, removed, or edited — consistent with a change that only moves where an error is raised.
  • Rollup correctness on the override path. Worth noting the override replaces the node wholesale, so the add_ungrouped_measure_reference registrations in the non-override branch never run and the parent FullKeyAggregate consumes an ordinary grouped select — the same contract the Cube source arm already relies on. The de-aggregation the check guards against simply doesn't arise there.
  • Wasted work before the error. The check runs after keys_subquery has already been processed and the join builder set up. Harmless, and hoisting it above that would mean matching on source twice — not worth it.
  • Security / performance — no new interpolation; the check is a linear scan over a group's measures once per rendered node.

Not run: cargo test / cargo fmt — cargo invocations are denied in this sandbox, so the moved check is reviewed by reading only. CI (only the labeler has reported so far) stands as the check.

· branch tesseract-fix-distinct-over-text-id ·

Comment on lines +23 to +28
return Err(CubeError::user(format!(
"{} has no aggregate of its own, so it cannot be computed alongside a measure \
that joins another cube under a dimension that multiplies its rows. Please \
request it in a separate query.",
measure.full_name()
)));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new wording fixes the shared-group case my earlier thread was about, but it now misses the opposite one — and that's four of the five tests that assert this message.

"cannot be computed alongside a measure that joins another cube" and "please request it in a separate query" both presume a sibling is what pulled the measure in. In test_calculated_measure_reaching_other_cubes_alone there is no sibling: rate_vs_commission is the only measure requested, and it is the one reaching rates/merchants. Same for test_calculated_measure_reaching_past_its_components and test_calculated_measure_over_components_rooted_at_different_cubes — all three are single-measure queries. And test_calculated_measure_over_components_with_different_joins says so in its own doc comment: "splitting the query does not give the expression an aggregate" — which is exactly what the error tells the user to try.

By my reading only test_calculated_measure_pulled_into_a_shared_measure_subquery is helped by a separate query: there success_rate's own collect_cube_names is {payments}, so alone it lands in a group where should_build_join is false. In the other four the measure reaches out itself, so it is refused alone too and the advice sends the user in a circle.

Since the reach is what makes the subquery necessary, naming it covers both arms without needing to know which measure caused it:

Suggested change
return Err(CubeError::user(format!(
"{} has no aggregate of its own, so it cannot be computed alongside a measure \
that joins another cube under a dimension that multiplies its rows. Please \
request it in a separate query.",
measure.full_name()
)));
if matches!(symbol.kind().aggregate_wrap(), AggregateWrap::PassThrough) {
return Err(CubeError::user(format!(
"{} has no aggregate of its own, so it cannot be re-aggregated over the \
deduplicated rows this query needs - a measure of its group reaches another \
cube, under a dimension that multiplies its rows. Please drop the multiplying \
dimension, request the measures that reach out separately, or move the \
aggregation into a measure.",
measure.full_name()
)));
}

If you'd rather keep the message short, dropping just the "alongside a measure" / "separate query" clauses already leaves it true in all five shapes.

Fix this →

}
}
AggregateMultipliedSubquerySource::MeasureSubquery(measure_subquery) => {
check_measures_survive_measure_subquery(&measure_subquery.schema.measures)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The move itself is right — I traced it and the motivation holds: optimizer.rs:269-276 builds a replacement AggregateMultipliedSubquery with pre_aggregation_override: Some(rewritten), and process returns at line 60-62 before reaching this arm. So with the check in the logical planner, a query a rollup could answer was refused before the optimizer ever ran.

But nothing tests that. integration_calculated_multi_fact.yaml declares no pre_aggregations, so none of the five refusal tests can exercise the override path — put the check back in plan_queries and the whole suite stays green. Given the commit exists for exactly this case, one rollup in the fixture covering one of the refused shapes plus build_sql_with_used_pre_aggregations asserting the usage (rather than an error) would make the move self-guarding.

Fix this →

for measure in measures.iter() {
let Ok(symbol) = measure.as_measure() else {
continue;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Carrying over the note from the now-outdated thread on the old location, since the continue moved verbatim: it silently exempts member-expression measures (the SQL API's ad-hoc symbols, for which as_measure() is Err). check_should_build_join_for_measure_select goes out of its way to handle exactly those (cube_names_if_dimension_only_expression), so an expression measure with a hand-written aggregate over a joined cube's column still reaches the database as the SQL this guard exists to stop.

Not a regression and I wouldn't widen the guard speculatively — but one line saying member expressions are deliberately out of scope would keep the next reader from reading continue as an oversight.

Nothing held the check to where it runs: the fixture declared no rollups, so
moving it back ahead of the pre-aggregation optimizer left the suite green
while turning a query a rollup answers into an error. Store one of the
refused shapes and assert the usage.

Name the reach rather than a sibling in the message. Four of the five refused
shapes are single-measure queries where the measure reaches out itself, so
asking for it in a separate query sends the user round in a circle; what
forces the subquery is the reach, whoever carries it.

Say that member expressions are left out deliberately, so the skip does not
read as an oversight.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

javascript Pull requests that update Javascript code rust Pull requests that update Rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant