Skip to content

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

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

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

Conversation

@pull

@pull pull Bot commented Sep 1, 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 : )

ovr and others added 5 commits September 1, 2026 19:29
… ignores the calendar (#11709)

* fix(schema-compiler): reject sql granularities under a name of their own (CORE-780)

A granularity declared with `sql` carries no interval — the validator forbids
combining `sql` with `interval`. `resolveGranularity()` supplies a synthetic
`1 <name>` interval for predefined names only, so a `sql` granularity named
after something else resolved without one, and `Granularity` read it unguarded.

`granularityHierarchies()` builds a `Granularity` for every custom granularity
of every time dimension in the model, and pre-aggregation matching runs it on
every request regardless of the SQL planner. A single such granularity anywhere
in the model therefore failed every query of the deployment, including queries
that never referenced the cube declaring it.

Such a granularity is now rejected at compile time, naming the granularity and
the predefined names it may take. The check is reported outside the cube schema
so it stands on its own: rejecting the granularity within the schema lists it
among the reasons every other dimension alternative failed, which buries it.

As defense in depth, `granularityHierarchies()` skips granularities that have no
interval to derive a hierarchy from — rollups can only match those by name,
which a missing hierarchy entry already expresses — and `Granularity` reports a
readable error instead of reading the interval unguarded.

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

* fix(tesseract): bound a to_date window by the calendar, not by interval math

A calendar cube's granularity is honored where the time dimension is projected,
but a `to_date` rolling window bounded itself with `date_bin(interval, point,
origin)` — the granularity's synthetic `1 <name>` interval anchored at the start
of the current year. A retail week-to-date therefore reset on the weekday that
year started on rather than on the week the calendar defines, silently, with no
error to notice.

The period a point belongs to cannot be derived from the point: only the
calendar knows it. The series driving such a window is now read off the calendar
cube, pairing every point with the period it falls into, and both bounds take it
from there — the window's join condition and the lower bound widening the scan
of its source. The period ends where the next one starts, read from the next
point of the series, because a 4-5-4 month runs 28 or 35 days against a nominal
`1 month`.

Rolling windows share one series, so its points carry one boundary column per
granularity and each window reads its own: week-to-date and month-to-date over
the same calendar resolve independently. A regular trailing window on that
series is unaffected. Series that no calendar window drives are untouched.

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

* docs: revert the calendar granularity note

Documenting the naming rule belongs with the docs owners, not here.

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

* fix(tesseract): let the calendar series look past its own range restriction

A period ends where the next one starts, and the point opening that next period
may sit outside the queried range — a range closing exactly on a period end left
the last point with no next row to read, falling back to the nominal interval it
was there to replace. A 35-day retail month closing the range came back four
days short.

The period bounds are now derived over the unrestricted calendar and the range
is applied outside that select, so the point past the range still bounds the last
one inside it. Both selects render through the sql templates, so dialects
overriding statement rendering apply to the calendar series too.

Also addresses review notes:

- a granularity named `Week` with `sql` resolves, because predefined names are
  matched case-insensitively; the new rejection matched case-sensitively and
  would have failed a model that works today
- the predefined names quoted in that message now come from the set backing the
  check, rather than a fourth copy of it
- a cube whose granularities were rejected is no longer recorded as valid
- `granularityHierarchies()` skips a granularity that resolves without an
  interval, but keeps reporting one that does not resolve at all
- `Granularity` no longer blames `sql` for an interval missing for another reason
- a calendar period dimension without a granularity is an internal error rather
  than a column name the consuming side cannot match
- the ordering the time series' granularity list rests on is now stated where
  the list is declared and where the loops it depends on run

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

* test(tesseract): cover calendar to_date windows in the planner suite

The feature was only covered from the schema compiler's Postgres suite, which
leaves the planner's own integration suite silent about it. The retail calendar
fixture there already runs 4-5-4 months, so the case a nominal interval cannot
reproduce needs nothing but exposing that column as a granularity.

`month` is exposed on the primary key dimension only: an existing test reads
`retail_date` at `month` and must keep seeing a natural one.

Five cases, all failing before the window learned to read its bounds off the
calendar — the retail month then bucketed the rolling measure by calendar months
while the plain measure kept retail ones, so the two axes never met and half of
each row came back NULL:

- week-to-date resetting on the retail week rather than the ISO one
- month-to-date resetting on the retail month rather than the first of the month
- a 5-week month grouped by itself, where a nominal bound folds the next one in
- a range closing exactly on a period end, whose bounding point is outside it
- both windows at once, the weekly one restarting while the monthly accumulates

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

* fix(tesseract): keep the calendar period a range opens inside of

The series restricted itself with the aligned date range, whose start
`get_range_for_time_series` snaps to the granularity's interval — for a calendar
granularity, anchored at the start of the current year, the very arithmetic the
period bounds no longer use. That point lands inside the period the range opens
in, and `date_from >= range_from` then drops that period from the series: a
month-to-date over a range opening on 2024-04-01 lost the retail month running
2024-03-03..2024-04-06 entirely, reporting NULL where the period had five orders.

The calendar branch now takes the range as stated and keeps a period by overlap
rather than by where it starts, which is also what the generated series does —
it starts at the aligned point and labels it with that period's start.

Two of the existing cases had nothing but NULLs after the boundary they were
pinning, so a bound that collapsed to an empty window for the rest of the range
would have satisfied them; both now extend far enough for the new period to
count again.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
#11712)

* fix(tesseract): convert a view's raw time dimension timezone only once

A `type: time` dimension requested without a granularity was converted
into the query timezone twice when it was reached through a view: the
view member applied the conversion and then rendered through the owning
cube's dimension, which applied it again. On BigQuery this produced
`TIMESTAMP(DATETIME(TIMESTAMP(DATETIME(col, tz)), tz))`, shifting the
value by the offset twice and moving timestamps across day boundaries.

Gate the conversion on `owned_by_cube()`: only the cube that owns the
column reads it from the database, so only there is a conversion needed.

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

* test(tesseract): address review notes on the timezone conversion tests

Move the plain-cube guard next to the other raw time dimension coverage
in time_dimensions.rs, leaving views.rs to the cases that involve a view.
Say in the date-bound table which reading of a bare date is the
consistent one, so a later change to either planner is self-explanatory.
Drop an eslint suppression the file does not need.

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

* test(tesseract): pin the converted values, not just how many conversions

The count says how many timezone conversions a query applies, but not
where they land: moving one onto the granularity wrapper, or applying it
with the offset reversed, keeps the count intact. Run the two view
queries against Postgres and snapshot the rows as well, so the column
the conversion lands on and the direction of the shift are pinned too.

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

* test(tesseract): let the environment pick the planner for the tz tests

The suite already runs under both planners through
CUBEJS_TESSERACT_SQL_PLANNER, so selecting one inside the test duplicated
that coverage and made the file carry a per-planner expectation table.
Write each case once and let the environment decide.

The date-bound cases are dropped with the table: their expected value
differs per planner, so they cannot be stated once. The bound a
single-date operator picks is covered on the Rust side.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Alex Qyoun-ae <4062971+MazterQyou@users.noreply.github.com>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.