Skip to content

[pull] master from cube-js:master - #651

Merged
pull[bot] merged 1 commit into
code:masterfrom
cube-js:master
Aug 5, 2026
Merged

[pull] master from cube-js:master#651
pull[bot] merged 1 commit into
code:masterfrom
cube-js:master

Conversation

@pull

@pull pull Bot commented Aug 5, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

* fix(tesseract): plan multiplied measures per join tree, not per cube

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.

* fix(tesseract): decompose calculated measures that reach other cubes

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.

* fix(tesseract): decompose calculated measures only where multiplied

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.

* test(tesseract): pin the same-cube split without a database

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.

* test(tesseract): assert the plan shape, not only the rows it returns

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.

* test(tesseract): state which planning strategy each case is holding

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.

* test(tesseract): anchor the negated assertions and scope the grouping 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.

* fix(tesseract): refuse measures the measure subquery cannot carry

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.

* fix(tesseract): check the measure subquery where it is rendered

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.

* test(tesseract): cover the rollup that renders no measure subquery

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.
@pull pull Bot locked and limited conversation to collaborators Aug 5, 2026
@pull pull Bot added the ⤵️ pull label Aug 5, 2026
@pull
pull Bot merged commit f7ff67a into code:master Aug 5, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant