Skip to content

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

Merged
pull[bot] merged 6 commits into
code:masterfrom
cube-js:master
Aug 31, 2026
Merged

[pull] master from cube-js:master#702
pull[bot] merged 6 commits into
code:masterfrom
cube-js:master

Conversation

@pull

@pull pull Bot commented Aug 31, 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 : )

waralexrom and others added 6 commits August 31, 2026 16:11
…11707)

* fix(tesseract): keep expression measures out of nested group merging

Folding a nested multi-fact group into the wider tree is decided from the
leaf measures the group carries, and a member expression or a calculated
measure is not a leaf: an aggregate written in its body is a member of
nothing, so no leaf accounts for it. `COUNT(*) - {cube.unique_id}` passed
the check on its distinct-count leaf alone while the raw count beside it
was recomputed over the fanned-out join.

A group now moves only when every measure it carries is built entirely
out of plain aggregated measures, which gives up the merge for composite
and expression-based measures - the shapes whose value the checked leaves
do not determine.

* docs(tesseract): trim the group_survives_join docstring

* fix(tesseract): follow bare references when guarding nested merging

Every measure of a view is a `type: number` reference to the cube measure,
so treating a calculated kind as an unmergeable shape took all view queries
out of nested group merging. A reference writes no SQL of its own, and its
value is the referenced measure's, so the traversal follows it instead and
the referenced leaves decide as they do on the cube path.
Update the security contact email listed to the new address:

https://cube.dev/security
#11614)

Interval and cron based `refreshKey` values can now be computed from the API
instance clock instead of via `SELECT FLOOR(...) as refresh_key`, gated by
`CUBEJS_REFRESH_KEY_LOCAL_TIME` (default `false`).
* fix(tesseract): keep grain.include when a rolling window is declared

A multi-stage measure declaring both `grain.include` and `rolling_window`
computed its inner stage at the query's granularity instead of the declared
one, and lost its outer aggregation entirely when the query carried no time
dimension. Nothing failed: the model compiled and the query returned a
plausible wrong number.

Two independent losses in the rolling path. The base member is built by
unrolling the measure, which drops the multi-stage properties, so the
recursion that plans the base CTE never saw the grain — extend the base
rolling state with `grain.include` instead. And with no time dimension the
rolling description is returned directly, so no outer stage carries the
aggregation, while unrolling also collapses the aggregate kind to a
calculated one; plan such a measure as its plain multi-stage self, which is
what a window resolving to a single bucket computes.

`grain.exclude` / `grain.keep_only` next to a rolling window were silently
ignored the same way. Narrowing the reported grain needs the narrowed value
broadcast back onto the query grid, which the rolling assembly has no side to
do, so they are now rejected with a message rather than quietly dropped.

`QueryProperties::add_dimensions` deduplicated only within `dimensions`, so a
`grain.include` naming a granularity the query already groups by rendered the
same column twice and the query failed as ambiguous. A time dimension's full
name pins its granularity, so deduplicating added dimensions against the time
dimensions by full name is exact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tesseract): name the older grain spellings in the rolling window error

`reduce_by` and `group_by` compile into the same lists as `grain.exclude` and
`grain.keep_only`, so a model written in the older spelling hits the rejection
while the message names keys that appear nowhere in it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tesseract): reject an empty keep_only next to a rolling window

`keep_only` is an intersection, so an empty list narrows the grain to nothing
rather than meaning no key was given — `partition_filter` collapses on it. The
rolling-window guard tested for a non-empty list and let it through, so the
measure was computed at the unnarrowed grain and reported as if the key had
been honoured. `exclude` subtracts and an empty list there really is a no-op,
so only `keep_only` changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(tesseract): reject a narrowing grain only where the window needs it

The rejection fired on the keys being declared, so it also refused queries the
keys never touch: `exclude` of a member the grain does not carry subtracts
nothing, and `keep_only` listing everything the query groups by intersects to
the same list. Those queries were answered correctly before the guard existed,
and the answer is the one the measure would give with no keys at all.

Reject the narrowing actually happening instead — compare the reshaped grain
with the one it came from, since `partition_filter` only ever removes.

The check also moves below the no-time-dimension branch. There the window has
no frame to build and the measure is planned through the ordinary multi-stage
path, which narrows the grain and broadcasts it back the usual way, so the
combination is supported and no longer refused.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.