From 861d4e305c3f695ae7fe531067598c026bf87025 Mon Sep 17 00:00:00 2001
From: igorlukanin <3852894+igorlukanin@users.noreply.github.com>
Date: Thu, 3 Sep 2026 13:07:54 +0200
Subject: [PATCH 1/6] docs: rewrite the custom calendar recipe onto calendar
cubes
---
.../data-modeling/concepts/calendar-cubes.mdx | 22 +-
.../recipes/data-modeling/custom-calendar.mdx | 469 ++++++++++++------
docs-mintlify/recipes/index.mdx | 2 +-
3 files changed, 325 insertions(+), 168 deletions(-)
diff --git a/docs-mintlify/docs/data-modeling/concepts/calendar-cubes.mdx b/docs-mintlify/docs/data-modeling/concepts/calendar-cubes.mdx
index 43f08680d411a..58895f08826f5 100644
--- a/docs-mintlify/docs/data-modeling/concepts/calendar-cubes.mdx
+++ b/docs-mintlify/docs/data-modeling/concepts/calendar-cubes.mdx
@@ -95,16 +95,16 @@ cube('fiscal_calendar', {
type: 'time',
time_shift: [
- { type: 'prior', interval: '1 week', sql: '{CUBE}.week_ago' },
- { type: 'prior', interval: '1 month', sql: '{CUBE}.month_ago' },
- { type: 'prior', interval: '1 year', sql: '{CUBE}.year_ago' }
+ { type: `prior`, interval: `1 week`, sql: `${CUBE}.week_ago` },
+ { type: `prior`, interval: `1 month`, sql: `${CUBE}.month_ago` },
+ { type: `prior`, interval: `1 year`, sql: `${CUBE}.year_ago` }
],
- granularities: [
- { name: 'week', sql: '{CUBE}.start_of_week' },
- { name: 'month', sql: '{CUBE}.start_of_month' },
- { name: 'year', sql: '{CUBE}.start_of_year' }
- ]
+ granularities: {
+ week: { sql: `${CUBE}.start_of_week` },
+ month: { sql: `${CUBE}.start_of_month` },
+ year: { sql: `${CUBE}.start_of_year` }
+ }
}
}
})
@@ -421,9 +421,9 @@ cube(`custom_calendar`, {
type: `time`,
primary_key: true,
- granularities: [
- { name: `month`, sql: `${CUBE}.mid_month::TIMESTAMP` }
- ]
+ granularities: {
+ month: { sql: `${CUBE}.mid_month::TIMESTAMP` }
+ }
}
}
})
diff --git a/docs-mintlify/recipes/data-modeling/custom-calendar.mdx b/docs-mintlify/recipes/data-modeling/custom-calendar.mdx
index 0952cd965d301..603abb6507970 100644
--- a/docs-mintlify/recipes/data-modeling/custom-calendar.mdx
+++ b/docs-mintlify/recipes/data-modeling/custom-calendar.mdx
@@ -1,167 +1,164 @@
---
title: Implementing custom calendars
-description: Map fact tables to irregular retail or fiscal calendars—illustrated with the 4-5-4 pattern—using dedicated calendar cubes and proxy dimensions.
+description: Model a 4-5-4 retail calendar as a calendar cube, overriding the month, week, and year granularities with pre-calculated columns.
---
-This recipe explains the implementation of the [4-5-4 calendar][link-454], a common
-retail calendar used in the US and Canada. However, the same approach can be used
-to implement other custom calendars.
+A _custom calendar_ divides the year into periods that do not line up with the Gregorian
+calendar. This recipe implements the [4-5-4 calendar][link-454], a retail calendar common
+in the US and Canada, as a [calendar cube][ref-calendar-cubes]. The same approach applies
+to any other custom calendar, such as a fiscal one.
-Unlike [custom time dimension granularities][ref-custom-granularities], custom
-calendars provide more flexibility and can be used when time units have variable
-lengths, such as the months and quarters in the 4-5-4 calendar. See the [custom
-granularities recipe][ref-custom-granularities-recipe] for more information.
+
+
+Calendar cubes are powered by Tesseract, the [next-generation data modeling
+engine][link-tesseract]. In versions before v1.7.0, it was not enabled by default.
+Querying a [to-date rolling window][ref-rolling-window] over an overridden granularity
+also requires v1.7.32 or later.
+
+
## Use case
-The 4-5-4 calendar ensures sales comparability between years by dividing the year
-into months based on a 4 weeks – 5 weeks – 4 weeks format. The layout of the calendar
-lines up holidays and ensures the same number of Saturdays and Sundays in comparable
-months. Hence, like days are compared to like days for sales reporting purposes.
+The 4-5-4 calendar makes sales comparable between years. It divides each retail year into
+quarters of three months, and each quarter into weeks in a 4 – 5 – 4 pattern, so a retail
+month is either four or five weeks long. Every month therefore begins on the same weekday
+and contains the same number of Saturdays and Sundays as its counterpart a year earlier,
+which is what makes like-for-like sales reporting possible.
-## Data modeling
+Because a retail month varies in length, it cannot be derived arithmetically from a
+fixed-length interval. It has to be read from a calendar table that states, for every
+date, which retail period that date belongs to.
-The data modeling includes the following steps:
+## Data modeling
-* Create a calendar cube, e.g., `calendar_454`.
-* Extend it a number of times, so there's one calendar cube for every time dimension
-in cubes with facts that needs translation to a custom calendar.
-* Define joins from your cubes with facts to those calendar cubes, e.g., `base_orders`,
-and bring relevant calendar attributes as [proxy dimensions][ref-proxy-dimensions].
+The implementation has two parts:
-The last two steps require a few lines of code but it can totally be optimized with
-a [Jinja macro][ref-jinja-macro] if needed.
+* A [calendar cube][ref-calendar-cubes] over the calendar table, where the `month`,
+`week`, and `year` granularities are overridden with pre-calculated columns.
+* A join from each cube with facts to that calendar cube.
### Calendar table
-Consider the following calendar cube. It was generated using a large language model
-(LLM) and then tested against the [official calendar][link-454-official-calendar].
-In this example, it's generated on the fly, however, in production, it should be
-materialized as a table using a data transformation tool:
+Consider the following calendar table. Every row is a date, and the remaining columns
+state the retail periods that the date belongs to. In production, generate it with a data
+transformation tool and materialize it as a table:
+
+| `date_value` | `retail_week_begins` | `retail_month_begins` | `retail_year_begins` | `date_prev_month` | `date_prev_year` |
+| --- | --- | --- | --- | --- | --- |
+| 2024-02-04 | 2024-02-04 | 2024-02-04 | 2024-02-04 | 2024-01-07 | 2023-02-05 |
+| 2024-02-05 | 2024-02-04 | 2024-02-04 | 2024-02-04 | 2024-01-08 | 2023-02-06 |
+| … | … | … | … | … | … |
+| 2024-03-03 | 2024-03-03 | 2024-03-03 | 2024-02-04 | 2024-02-04 | 2023-03-05 |
+| … | … | … | … | … | … |
+| 2024-04-07 | 2024-04-07 | 2024-04-07 | 2024-02-04 | 2024-03-03 | 2023-04-09 |
+
+The retail year 2024 begins on 2024-02-04. The month beginning on that date is four weeks
+long, so the next one begins on 2024-03-03; that one is five weeks long, so the third
+begins on 2024-04-07. That 4 – 5 – 4 sequence is exactly what no `interval` can express,
+and it is why these dates are pre-calculated rather than computed at query time.
+
+The last two columns hold the date one retail month and one retail year earlier. They are
+what make [time shifts](#comparing-with-a-prior-period) follow the retail calendar as
+well.
+
+### Calendar cube
+
+Set [`calendar`][ref-cubes-calendar] to `true` on the cube over the calendar table, and
+override the granularities of its [`primary_key`][ref-primary-key] dimension:
+
+
-```yaml
+```yaml title="YAML"
cubes:
- - name: calendar_454
- public: false
- sql: |
- WITH RECURSIVE fiscal_weeks AS (
- -- Step 1: Define the start of the fiscal years (Sunday closest to Feb 1st)
- SELECT
- year AS fiscal_year,
- CASE
- WHEN strftime('%w', date_trunc('week', make_date(year, 2, 1)))::INTEGER <= 3
- THEN date_trunc('week', make_date(year, 2, 1)) + INTERVAL 6 DAY
- ELSE date_trunc('week', make_date(year, 2, 1) + INTERVAL 7 DAY) + INTERVAL 7 DAY
- END AS week_start,
- 1 AS week_number,
- 1 AS month_number,
- 1 AS month_week_count
- FROM range(2015, 2031) t(year)
-
- UNION ALL
-
- -- Step 2: Generate weeks recursively following the 4-5-4 pattern
- SELECT
- fiscal_year,
- week_start + INTERVAL 7 DAY AS week_start,
- week_number + 1,
- CASE
- WHEN month_number = 12 AND ((month_week_count = 4 AND month_number % 3 = 1) OR
- (month_week_count = 5 AND month_number % 3 = 2) OR
- (month_week_count = 4 AND month_number % 3 = 0))
- THEN 1
- WHEN month_week_count = 4 AND (month_number % 3 = 1) THEN month_number + 1
- WHEN month_week_count = 5 AND (month_number % 3 = 2) THEN month_number + 1
- WHEN month_week_count = 4 AND (month_number % 3 = 0) THEN month_number + 1
- ELSE month_number
- END AS month_number,
- CASE
- WHEN month_week_count = 4 AND (month_number % 3 = 1) THEN 1
- WHEN month_week_count = 5 AND (month_number % 3 = 2) THEN 1
- WHEN month_week_count = 4 AND (month_number % 3 = 0) THEN 1
- ELSE month_week_count + 1
- END AS month_week_count
- FROM fiscal_weeks
- WHERE week_number < 52 OR (week_number = 52 AND (fiscal_year % 5 = 2)) -- Account for 53rd week
- )
-
- SELECT
- fiscal_year,
- week_number,
- month_number,
- make_timestamp(fiscal_year, month_number, 1, 0, 0, 0) AS fiscal_month_date,
- week_start AS week_start_date,
- make_timestamp(year(week_start + INTERVAL 6 DAY),
- month(week_start + INTERVAL 6 DAY),
- day(week_start + INTERVAL 6 DAY),
- 23, 59, 59.999) AS week_end_date
- FROM fiscal_weeks
- ORDER BY fiscal_year, week_number
+ - name: retail_calendar
+ calendar: true
+ sql_table: retail_calendar
dimensions:
- - name: retail_year
- sql: fiscal_year
- type: number
+ - name: date
+ sql: date_value
+ type: time
+ primary_key: true
- - name: week_number
- sql: week_number
- type: number
+ granularities:
+ # 4 or 5 weeks long, so no interval can reproduce it
+ - name: month
+ sql: "{CUBE}.retail_month_begins"
- - name: month_number
- sql: month_number
- type: number
+ - name: week
+ sql: "{CUBE}.retail_week_begins"
- - name: retail_month_date
- sql: fiscal_month_date
- type: time
+ - name: year
+ sql: "{CUBE}.retail_year_begins"
- - name: week_start_date
- sql: week_start_date
- type: time
+ time_shift:
+ - type: prior
+ interval: 1 month
+ sql: "{CUBE}.date_prev_month"
- - name: week_end_date
- sql: week_end_date
- type: time
+ - type: prior
+ interval: 1 year
+ sql: "{CUBE}.date_prev_year"
+```
+
+```javascript title="JavaScript"
+cube(`retail_calendar`, {
+ calendar: true,
+ sql_table: `retail_calendar`,
+
+ dimensions: {
+ date: {
+ sql: `date_value`,
+ type: `time`,
+ primary_key: true,
+
+ granularities: {
+ // 4 or 5 weeks long, so no interval can reproduce it
+ month: { sql: `${CUBE}.retail_month_begins` },
+ week: { sql: `${CUBE}.retail_week_begins` },
+ year: { sql: `${CUBE}.retail_year_begins` }
+ },
+
+ time_shift: [
+ { type: `prior`, interval: `1 month`, sql: `${CUBE}.date_prev_month` },
+ { type: `prior`, interval: `1 year`, sql: `${CUBE}.date_prev_year` }
+ ]
+ }
+ }
+})
```
-As you can see, this cube defines `week_start_date` and `week_end_date` time dimensions
-as the start and end dates of the retail week. They can be used to join this cube to
-cubes with facts.
+
-### Auxiliary calendar cubes
+Each granularity keeps the name of the default granularity it replaces. A granularity
+defined with `sql` must be named after a default one; `retail_month` would not compile.
+See [naming a granularity defined with `sql`][ref-calendar-cubes-naming] for the rule and
+for when to use `interval` instead.
-We will also extend the `calendar_454` cube to create auxiliary calendar cubes for
-three time dimensions that we'd like to translate to the 4-5-4 calendar:
+
-```yaml
-cubes:
- - name: calendar_454__base_orders__created_at
- extends: calendar_454
+**Override the granularities on the dimension that other cubes join to.** A calendar cube
+can expose more than one time dimension, and an override applies only to the dimension it
+is defined on. A query that groups by a dimension without the override falls back to
+`DATE_TRUNC` and returns Gregorian months, with no error.
- - name: calendar_454__base_orders__completed_at
- extends: calendar_454
-```
+
### Cubes with facts
-Finally, we define joins from the `base_orders` cube to the auxiliary calendar cubes.
-We also bring the `week_number` and `month_number` attributes as proxy dimensions:
+Join each cube with facts to the calendar cube on its own time dimension:
-```yaml
+
+
+```yaml title="YAML"
cubes:
- - name: base_orders
- sql: SELECT * FROM 's3://cube-tutorial/orders.csv'
+ - name: orders
+ sql_table: orders
joins:
- # BEGIN — Joins to calendar tables
- - name: calendar_454__base_orders__created_at
- sql: "{CUBE.created_at} BETWEEN {calendar_454__base_orders__created_at.week_start_date} AND {calendar_454__base_orders__created_at.week_end_date}"
- relationship: many_to_one
-
- - name: calendar_454__base_orders__completed_at
- sql: "{CUBE.completed_at} BETWEEN {calendar_454__base_orders__completed_at.week_start_date} AND {calendar_454__base_orders__completed_at.week_end_date}"
+ - name: retail_calendar
+ sql: "{CUBE}.created_at = {retail_calendar.date}"
relationship: many_to_one
- # END — Joins to calendar tables
dimensions:
- name: id
@@ -169,49 +166,209 @@ cubes:
type: number
primary_key: true
- - name: status
- sql: status
- type: string
-
- # BEGIN — Regular time dimension + ones derived from calendar table
- name: created_at
- sql: "{CUBE}.created_at::TIMESTAMP"
+ sql: created_at
type: time
- - name: created_at_retail_month
- sql: "{calendar_454__base_orders__created_at.retail_month_date}"
- type: time
+ measures:
+ - name: count
+ type: count
+```
- - name: created_at_retail_week
- sql: "{calendar_454__base_orders__created_at.week_number}"
- type: number
+```javascript title="JavaScript"
+cube(`orders`, {
+ sql_table: `orders`,
+
+ joins: {
+ retail_calendar: {
+ sql: `${CUBE}.created_at = ${retail_calendar.date}`,
+ relationship: `many_to_one`
+ }
+ },
+
+ dimensions: {
+ id: {
+ sql: `id`,
+ type: `number`,
+ primary_key: true
+ },
+
+ created_at: {
+ sql: `created_at`,
+ type: `time`
+ }
+ },
+
+ measures: {
+ count: {
+ type: `count`
+ }
+ }
+})
+```
- - name: completed_at
- sql: "{CUBE}.completed_at::TIMESTAMP"
- type: time
+
- - name: completed_at_retail_month
- sql: "{calendar_454__base_orders__completed_at.retail_month_date}"
- type: time
+Both sides of the join must be time dimensions, and the calendar cube's side must be its
+`primary_key`.
- - name: completed_at_retail_week
- sql: "{calendar_454__base_orders__completed_at.week_number}"
- type: number
- # END — Regular time dimension + ones derived from calendar table
+A pair of cubes can only be joined once, so translating a second time dimension, such as
+`completed_at`, needs a second calendar cube. Define it with
+[`extends`][ref-extending-cubes] to inherit the granularities and time shifts, and repeat
+`calendar` on it:
+
+
+
+```yaml title="YAML"
+cubes:
+ - name: retail_calendar_completed
+ extends: retail_calendar
+ calendar: true
+```
+
+```javascript title="JavaScript"
+cube(`retail_calendar_completed`, {
+ extends: retail_calendar,
+ calendar: true
+})
+```
+
+
+
+
+
+**Repeat `calendar: true` on the extending cube.** A cube inherits it from its parent, but
+an inherited value is not passed to the query engine. Without it, the extending cube is
+not treated as a calendar cube and its granularity overrides are ignored.
+
+
+
+## Querying
+
+Query `orders.count` by `retail_calendar.date` with the `month` granularity. The result is
+grouped by retail months, not Gregorian ones:
+
+| `retail_calendar.date` | `orders.count` |
+| --- | --- |
+| 2024-02-04 | 3 |
+| 2024-03-03 | 5 |
+| 2024-04-07 | 4 |
+
+The month beginning on 2024-03-03 spans five weeks; the ones around it span four. Grouping
+by `week` and `year` works the same way, and each returns the retail period rather than
+the Gregorian one.
+
+### Comparing with a prior period
+
+Because the calendar cube also overrides the time shifts, a [period-over-period
+measure][ref-recipe-period-over-period] compares a retail month with the retail month
+before it. Define it on the cube with facts, next to the measure it shifts:
+
+
+
+```yaml title="YAML"
+cubes:
+ - name: orders
+ # ...
measures:
- name: count
type: count
- - name: completed_count
+ - name: count_prior_month
+ type: number
+ multi_stage: true
+ sql: "{count}"
+ time_shift:
+ - interval: 1 month
+ type: prior
+```
+
+```javascript title="JavaScript"
+cube(`orders`, {
+ // ...
+
+ measures: {
+ count: {
+ type: `count`
+ },
+
+ count_prior_month: {
+ type: `number`,
+ multi_stage: true,
+ sql: `${count}`,
+ time_shift: [
+ { interval: `1 month`, type: `prior` }
+ ]
+ }
+ }
+})
+```
+
+
+
+The shift resolves through the `date_prev_month` column, so it lands on the equivalent day
+of the previous retail month rather than a calendar month earlier.
+
+### Measuring a period to date
+
+A [rolling window][ref-rolling-window] of type `to_date` also follows the calendar. It
+belongs on the cube with facts as well:
+
+
+
+```yaml title="YAML"
+cubes:
+ - name: orders
+ # ...
+
+ measures:
+ - name: count_month_to_date
type: count
- filters:
- - sql: "{CUBE}.status = 'completed'"
+ rolling_window:
+ type: to_date
+ granularity: month
+```
+
+```javascript title="JavaScript"
+cube(`orders`, {
+ // ...
+
+ measures: {
+ count_month_to_date: {
+ type: `count`,
+ rolling_window: {
+ type: `to_date`,
+ granularity: `month`
+ }
+ }
+ }
+})
```
+
+
+Each window opens on the retail month's own first day and closes on its last, so a
+five-week month accumulates over all five of its weeks.
+
+## Pre-aggregations
+
+A [pre-aggregation][ref-pre-aggregations] over an overridden granularity must declare that
+granularity. A rollup on `month` is built from the `retail_month_begins` column and serves
+queries at `month`.
+
+Declare it explicitly rather than relying on a finer rollup. Retail months cannot be
+assembled from Gregorian ones, so a rollup that does not name the overridden granularity
+is not a correct source for these queries.
+
+
[link-454]: https://nrf.com/resources/4-5-4-calendar
-[link-454-official-calendar]: https://2fb5c46100c1b71985e2-011e70369171d43105aff38e48482379.ssl.cf1.rackcdn.com/4-5-4%20calendar/3-Year-Calendar-5-27.pdf
-[ref-custom-granularities]: /reference/data-modeling/dimensions#granularities
-[ref-custom-granularities-recipe]: /recipes/data-modeling/custom-granularity
-[ref-proxy-dimensions]: /docs/data-modeling/dimensions#proxy-dimensions
-[ref-jinja-macro]: /docs/data-modeling/dynamic/jinja#macros
\ No newline at end of file
+[link-tesseract]: https://cube.dev/blog/introducing-next-generation-data-modeling-engine
+[ref-calendar-cubes]: /docs/data-modeling/concepts/calendar-cubes
+[ref-calendar-cubes-naming]: /docs/data-modeling/concepts/calendar-cubes#naming-a-granularity-defined-with-sql
+[ref-cubes-calendar]: /reference/data-modeling/cube#calendar
+[ref-primary-key]: /reference/data-modeling/dimensions#primary_key
+[ref-extending-cubes]: /docs/data-modeling/extending-cubes
+[ref-rolling-window]: /reference/data-modeling/measures#rolling_window
+[ref-pre-aggregations]: /docs/pre-aggregations/matching-pre-aggregations
+[ref-recipe-period-over-period]: /recipes/data-modeling/period-over-period
diff --git a/docs-mintlify/recipes/index.mdx b/docs-mintlify/recipes/index.mdx
index 4462abde37e77..de37a05c703c2 100644
--- a/docs-mintlify/recipes/index.mdx
+++ b/docs-mintlify/recipes/index.mdx
@@ -83,7 +83,7 @@ member to query), see [Dynamic data models](/recipes/data-modeling/using-dynamic
Implement custom time dimension granularities like fiscal quarters or custom week definitions.
- Map fact tables to retail or fiscal calendars using dedicated calendar cubes and proxy dimensions.
+ Model a 4-5-4 retail calendar as a calendar cube that overrides the month, week, and year granularities.
Build point-in-time snapshots from change-history data to report status as of any date.
From 8c6ac92489290161f9eeff1f1b0a0ccf7ddd31f9 Mon Sep 17 00:00:00 2001
From: igorlukanin <3852894+igorlukanin@users.noreply.github.com>
Date: Thu, 3 Sep 2026 14:02:31 +0200
Subject: [PATCH 2/6] docs: add calendar cubes to the sidebar and soften the
extends warning
---
docs-mintlify/docs.json | 1 +
docs-mintlify/recipes/data-modeling/custom-calendar.mdx | 4 ++--
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/docs-mintlify/docs.json b/docs-mintlify/docs.json
index 69a509c241060..0c4683c65b2f2 100644
--- a/docs-mintlify/docs.json
+++ b/docs-mintlify/docs.json
@@ -157,6 +157,7 @@
"docs/data-modeling/joins",
"docs/data-modeling/measures",
"docs/data-modeling/dimensions",
+ "docs/data-modeling/concepts/calendar-cubes",
"docs/data-modeling/ai-context",
"docs/data-modeling/concepts/syntax",
{
diff --git a/docs-mintlify/recipes/data-modeling/custom-calendar.mdx b/docs-mintlify/recipes/data-modeling/custom-calendar.mdx
index 603abb6507970..bccdbef1b91e5 100644
--- a/docs-mintlify/recipes/data-modeling/custom-calendar.mdx
+++ b/docs-mintlify/recipes/data-modeling/custom-calendar.mdx
@@ -238,8 +238,8 @@ cube(`retail_calendar_completed`, {
**Repeat `calendar: true` on the extending cube.** A cube inherits it from its parent, but
-an inherited value is not passed to the query engine. Without it, the extending cube is
-not treated as a calendar cube and its granularity overrides are ignored.
+inherited cube-level parameters are not always passed to the query engine. Setting it
+again keeps the extending cube a calendar cube.
From 29f9ee47719e708f75f26c6708b7cbadd257a24b Mon Sep 17 00:00:00 2001
From: igorlukanin <3852894+igorlukanin@users.noreply.github.com>
Date: Thu, 3 Sep 2026 14:24:43 +0200
Subject: [PATCH 3/6] docs: fix calendar cube examples that could not compile,
add the quarter override
---
.../data-modeling/concepts/calendar-cubes.mdx | 87 ++++++--------
.../recipes/data-modeling/custom-calendar.mdx | 113 ++++++++++++++----
2 files changed, 123 insertions(+), 77 deletions(-)
diff --git a/docs-mintlify/docs/data-modeling/concepts/calendar-cubes.mdx b/docs-mintlify/docs/data-modeling/concepts/calendar-cubes.mdx
index 58895f08826f5..b793252d3aefa 100644
--- a/docs-mintlify/docs/data-modeling/concepts/calendar-cubes.mdx
+++ b/docs-mintlify/docs/data-modeling/concepts/calendar-cubes.mdx
@@ -20,7 +20,7 @@ engine][link-tesseract]. In versions before v1.7.0, it was not enabled by defaul
## Configuration
Calendar cubes are [cubes][ref-cubes] where the [`calendar` parameter][ref-cubes-calendar]
-is set to `true`. This indicates that the cube is a calendar cube and allow the use of
+is set to `true`. This indicates that the cube is a calendar cube and allows the use of
custom time shifts and granularities.
@@ -31,21 +31,16 @@ cubes:
calendar: true
sql: >
SELECT
- date_key,
calendar_date,
start_of_week, start_of_month, start_of_year,
week_ago, month_ago, year_ago
FROM calendar_table
dimensions:
- - name: date_key
- sql: date
- type: time
- primary_key: true
-
- name: date
- sql: date
+ sql: calendar_date
type: time
+ primary_key: true
time_shift:
- type: prior
@@ -72,11 +67,10 @@ cubes:
```
```javascript title="JavaScript"
-cube('fiscal_calendar', {
+cube(`fiscal_calendar`, {
calendar: true,
sql: `
SELECT
- date_key,
calendar_date,
start_of_week, start_of_month, start_of_year,
week_ago, month_ago, year_ago
@@ -84,15 +78,10 @@ cube('fiscal_calendar', {
`,
dimensions: {
- date_key: {
- sql: 'date_key',
- type: 'time',
- primary_key: true
- },
-
date: {
- sql: 'calendar_date',
- type: 'time',
+ sql: `calendar_date`,
+ type: `time`,
+ primary_key: true,
time_shift: [
{ type: `prior`, interval: `1 week`, sql: `${CUBE}.week_ago` },
@@ -112,6 +101,14 @@ cube('fiscal_calendar', {
+
+
+A calendar cube must have exactly one [`primary_key`][ref-primary-key] dimension. A second
+one compiles without error but makes every query that references the cube fail with
+`Cube '...' has multiple primary keys, but only one is allowed for calendar cubes`.
+
+
+
### Joins
Calendar cubes are only useful when they are joined with other cubes in the data model.
@@ -125,7 +122,7 @@ cubes:
joins:
- name: fiscal_calendar
- sql: "{CUBE}.date = {fiscal_calendar.date_key}"
+ sql: "{CUBE}.date = {fiscal_calendar.date}"
relationship: many_to_one
# ...
@@ -137,7 +134,7 @@ cube(`sales`, {
joins: {
fiscal_calendar: {
- sql: `${CUBE}.date = ${fiscal_calendar.date_key}`,
+ sql: `${CUBE}.date = ${fiscal_calendar.date}`,
relationship: `many_to_one`
}
},
@@ -184,15 +181,11 @@ cubes:
SELECT '2025-06-01' AS date, '2025-05-15' AS month_ago
dimensions:
- - name: date_key
+ - name: date
sql: "{CUBE}.date::TIMESTAMP"
type: time
primary_key: true
- - name: date
- sql: "{CUBE}.date::TIMESTAMP"
- type: time
-
time_shift:
- type: prior
interval: 1 month
@@ -213,7 +206,7 @@ cubes:
joins:
- name: custom_calendar
- sql: "{CUBE}.date = {custom_calendar.date_key}"
+ sql: "{CUBE}.date = {custom_calendar.date}"
relationship: many_to_one
dimensions:
@@ -230,6 +223,7 @@ cubes:
- name: total_sales_prior_month
sql: "{total_sales}"
type: number
+ multi_stage: true
time_shift:
- type: prior
interval: 1 month
@@ -237,6 +231,7 @@ cubes:
- name: total_sales_few_days_ago
sql: "{total_sales}"
type: number
+ multi_stage: true
time_shift:
- name: my_favorite_time_shift
```
@@ -254,15 +249,10 @@ cube(`custom_calendar`, {
`,
dimensions: {
- date_key: {
- sql: `${CUBE}.date::TIMESTAMP`,
- type: `time`,
- primary_key: true
- },
-
date: {
sql: `${CUBE}.date::TIMESTAMP`,
type: `time`,
+ primary_key: true,
time_shift: [
{ type: `prior`, interval: `1 month`, sql: `${CUBE}.month_ago::TIMESTAMP` },
@@ -284,7 +274,7 @@ cube(`sales`, {
joins: {
custom_calendar: {
- sql: `${CUBE}.date = ${custom_calendar.date_key}`,
+ sql: `${CUBE}.date = ${custom_calendar.date}`,
relationship: `many_to_one`
}
},
@@ -304,16 +294,18 @@ cube(`sales`, {
},
total_sales_prior_month: {
- sql: `{total_sales}`,
+ sql: `${total_sales}`,
type: `number`,
+ multi_stage: true,
time_shift: [
{ type: `prior`, interval: `1 month` }
]
},
total_sales_few_days_ago: {
- sql: `{total_sales}`,
+ sql: `${total_sales}`,
type: `number`,
+ multi_stage: true,
time_shift: [
{ name: `my_favorite_time_shift` }
]
@@ -324,8 +316,8 @@ cube(`sales`, {
-Whe `sales.total_sales_prior_month` and `sales.total_sales_few_days_ago` measures are
-queried together with the `calendar.date` time dimension, the generate SQL will use the
+When the `sales.total_sales_prior_month` and `sales.total_sales_few_days_ago` measures are
+queried together with the `custom_calendar.date` time dimension, the generated SQL uses the
custom time shifts defined in the `custom_calendar` cube: one with the `month_ago`
column and another with `INTERVAL '42 days'`.
@@ -339,8 +331,8 @@ such as `day`, `month`, or `year`. However, custom calendars often have differen
definitions for these periods, e.g., a retail calendar might use 4-5-4 week patterns.
Calendar cubes allow you to define custom SQL expressions for each granularity.
-In the following example, the `fiscal_calendar` cube overrides the default `month`
-granularity to the to a pre-calculated `mid_month` column:
+In the following example, the `custom_calendar` cube overrides the default `month`
+granularity with a pre-calculated `mid_month` column:
@@ -357,16 +349,11 @@ cubes:
SELECT '2025-06-30' AS date, '2025-06-15' AS mid_month
dimensions:
- - name: date_key
- sql: date
- type: time
- primary_key: true
-
- name: date
- sql: date
+ sql: "{CUBE}.date::TIMESTAMP"
type: time
primary_key: true
-
+
granularities:
- name: month
sql: "{CUBE}.mid_month::TIMESTAMP"
@@ -410,14 +397,8 @@ cube(`custom_calendar`, {
`,
dimensions: {
- date_key: {
- sql: `date`,
- type: `time`,
- primary_key: true
- },
-
date: {
- sql: `date`,
+ sql: `${CUBE}.date::TIMESTAMP`,
type: `time`,
primary_key: true,
diff --git a/docs-mintlify/recipes/data-modeling/custom-calendar.mdx b/docs-mintlify/recipes/data-modeling/custom-calendar.mdx
index bccdbef1b91e5..4d1e694ff9ffe 100644
--- a/docs-mintlify/recipes/data-modeling/custom-calendar.mdx
+++ b/docs-mintlify/recipes/data-modeling/custom-calendar.mdx
@@ -1,6 +1,6 @@
---
title: Implementing custom calendars
-description: Model a 4-5-4 retail calendar as a calendar cube, overriding the month, week, and year granularities with pre-calculated columns.
+description: Model a 4-5-4 retail calendar as a calendar cube, overriding the week, month, quarter, and year granularities with pre-calculated columns.
---
A _custom calendar_ divides the year into periods that do not line up with the Gregorian
@@ -33,8 +33,8 @@ date, which retail period that date belongs to.
The implementation has two parts:
-* A [calendar cube][ref-calendar-cubes] over the calendar table, where the `month`,
-`week`, and `year` granularities are overridden with pre-calculated columns.
+* A [calendar cube][ref-calendar-cubes] over the calendar table, where the `week`,
+`month`, `quarter`, and `year` granularities are overridden with pre-calculated columns.
* A join from each cube with facts to that calendar cube.
### Calendar table
@@ -43,19 +43,22 @@ Consider the following calendar table. Every row is a date, and the remaining co
state the retail periods that the date belongs to. In production, generate it with a data
transformation tool and materialize it as a table:
-| `date_value` | `retail_week_begins` | `retail_month_begins` | `retail_year_begins` | `date_prev_month` | `date_prev_year` |
-| --- | --- | --- | --- | --- | --- |
-| 2024-02-04 | 2024-02-04 | 2024-02-04 | 2024-02-04 | 2024-01-07 | 2023-02-05 |
-| 2024-02-05 | 2024-02-04 | 2024-02-04 | 2024-02-04 | 2024-01-08 | 2023-02-06 |
-| … | … | … | … | … | … |
-| 2024-03-03 | 2024-03-03 | 2024-03-03 | 2024-02-04 | 2024-02-04 | 2023-03-05 |
-| … | … | … | … | … | … |
-| 2024-04-07 | 2024-04-07 | 2024-04-07 | 2024-02-04 | 2024-03-03 | 2023-04-09 |
+| `date_value` | `retail_week_begins` | `retail_month_begins` | `retail_quarter_begins` | `retail_year_begins` | `date_prev_month` | `date_prev_year` |
+| --- | --- | --- | --- | --- | --- | --- |
+| 2024-02-04 | 2024-02-04 | 2024-02-04 | 2024-02-04 | 2024-02-04 | 2024-01-07 | 2023-02-05 |
+| 2024-02-05 | 2024-02-04 | 2024-02-04 | 2024-02-04 | 2024-02-04 | 2024-01-08 | 2023-02-06 |
+| … | … | … | … | … | … | … |
+| 2024-03-03 | 2024-03-03 | 2024-03-03 | 2024-02-04 | 2024-02-04 | 2024-02-04 | 2023-03-05 |
+| … | … | … | … | … | … | … |
+| 2024-04-07 | 2024-04-07 | 2024-04-07 | 2024-02-04 | 2024-02-04 | 2024-03-03 | 2023-04-09 |
+| … | … | … | … | … | … | … |
+| 2024-05-05 | 2024-05-05 | 2024-05-05 | 2024-05-05 | 2024-02-04 | 2024-04-07 | 2023-05-07 |
The retail year 2024 begins on 2024-02-04. The month beginning on that date is four weeks
long, so the next one begins on 2024-03-03; that one is five weeks long, so the third
-begins on 2024-04-07. That 4 – 5 – 4 sequence is exactly what no `interval` can express,
-and it is why these dates are pre-calculated rather than computed at query time.
+begins on 2024-04-07. Those three months make up the first retail quarter, and the second
+one begins on 2024-05-05. That 4 – 5 – 4 sequence is exactly what no `interval` can
+express, and it is why these dates are pre-calculated rather than computed at query time.
The last two columns hold the date one retail month and one retail year earlier. They are
what make [time shifts](#comparing-with-a-prior-period) follow the retail calendar as
@@ -81,12 +84,15 @@ cubes:
primary_key: true
granularities:
+ - name: week
+ sql: "{CUBE}.retail_week_begins"
+
# 4 or 5 weeks long, so no interval can reproduce it
- name: month
sql: "{CUBE}.retail_month_begins"
- - name: week
- sql: "{CUBE}.retail_week_begins"
+ - name: quarter
+ sql: "{CUBE}.retail_quarter_begins"
- name: year
sql: "{CUBE}.retail_year_begins"
@@ -113,9 +119,10 @@ cube(`retail_calendar`, {
primary_key: true,
granularities: {
+ week: { sql: `${CUBE}.retail_week_begins` },
// 4 or 5 weeks long, so no interval can reproduce it
month: { sql: `${CUBE}.retail_month_begins` },
- week: { sql: `${CUBE}.retail_week_begins` },
+ quarter: { sql: `${CUBE}.retail_quarter_begins` },
year: { sql: `${CUBE}.retail_year_begins` }
},
@@ -137,10 +144,11 @@ for when to use `interval` instead.
-**Override the granularities on the dimension that other cubes join to.** A calendar cube
-can expose more than one time dimension, and an override applies only to the dimension it
-is defined on. A query that groups by a dimension without the override falls back to
-`DATE_TRUNC` and returns Gregorian months, with no error.
+**Override the granularities on the dimension you group by.** A calendar cube can expose
+more than one time dimension, and an override applies only to the dimension it is defined
+on. A query that groups by a dimension without the override falls back to `DATE_TRUNC` and
+returns Gregorian months, with no error. The same is true per granularity: this cube still
+answers `day` with `DATE_TRUNC`, because `day` is not overridden.
@@ -235,11 +243,68 @@ cube(`retail_calendar_completed`, {
+Then join it to `orders` as well, on the second time dimension:
+
+
+
+```yaml title="YAML"
+cubes:
+ - name: orders
+ sql_table: orders
+
+ joins:
+ - name: retail_calendar
+ sql: "{CUBE}.created_at = {retail_calendar.date}"
+ relationship: many_to_one
+
+ - name: retail_calendar_completed
+ sql: "{CUBE}.completed_at = {retail_calendar_completed.date}"
+ relationship: many_to_one
+
+ dimensions:
+ # ...
+
+ - name: completed_at
+ sql: completed_at
+ type: time
+```
+
+```javascript title="JavaScript"
+cube(`orders`, {
+ sql_table: `orders`,
+
+ joins: {
+ retail_calendar: {
+ sql: `${CUBE}.created_at = ${retail_calendar.date}`,
+ relationship: `many_to_one`
+ },
+
+ retail_calendar_completed: {
+ sql: `${CUBE}.completed_at = ${retail_calendar_completed.date}`,
+ relationship: `many_to_one`
+ }
+ },
+
+ dimensions: {
+ // ...
+
+ completed_at: {
+ sql: `completed_at`,
+ type: `time`
+ }
+ }
+})
+```
+
+
+
**Repeat `calendar: true` on the extending cube.** A cube inherits it from its parent, but
-inherited cube-level parameters are not always passed to the query engine. Setting it
-again keeps the extending cube a calendar cube.
+inherited cube-level parameters are not always passed to the query engine. Without it, the
+granularity overrides still apply, but the time shifts silently revert to interval
+arithmetic: `prior` + `1 month` adds `INTERVAL '1 month'` instead of reading
+`date_prev_month`, and returns different numbers with no error.
@@ -255,8 +320,8 @@ grouped by retail months, not Gregorian ones:
| 2024-04-07 | 4 |
The month beginning on 2024-03-03 spans five weeks; the ones around it span four. Grouping
-by `week` and `year` works the same way, and each returns the retail period rather than
-the Gregorian one.
+by `week`, `quarter`, and `year` works the same way, and each returns the retail period
+rather than the Gregorian one.
### Comparing with a prior period
From 0ef1eee3324cf22580ca0c28c38cff001394ff9d Mon Sep 17 00:00:00 2001
From: igorlukanin <3852894+igorlukanin@users.noreply.github.com>
Date: Thu, 3 Sep 2026 14:40:21 +0200
Subject: [PATCH 4/6] docs: warn that pre-aggregations do not honour calendar
granularity overrides
---
.../data-modeling/concepts/calendar-cubes.mdx | 13 +++++++------
.../recipes/data-modeling/custom-calendar.mdx | 18 ++++++++++++------
2 files changed, 19 insertions(+), 12 deletions(-)
diff --git a/docs-mintlify/docs/data-modeling/concepts/calendar-cubes.mdx b/docs-mintlify/docs/data-modeling/concepts/calendar-cubes.mdx
index b793252d3aefa..39a920be270fc 100644
--- a/docs-mintlify/docs/data-modeling/concepts/calendar-cubes.mdx
+++ b/docs-mintlify/docs/data-modeling/concepts/calendar-cubes.mdx
@@ -483,14 +483,15 @@ length and cannot be derived arithmetically, such as the months and quarters of
retail calendar. The [custom calendar recipe][ref-recipe-custom-calendar] models a 4-5-4
calendar in full.
-
+
-**A [pre-aggregation][ref-pre-aggregations] over an overridden granularity must declare
-that granularity.** A rollup on the `month` granularity above serves queries at `month`,
-and it is built from the `mid_month` column rather than `DATE_TRUNC`. A rollup at another
-granularity will not serve those queries correctly.
+**Do not [pre-aggregate][ref-pre-aggregations] a query that groups by an overridden
+granularity.** A rollup is built with `DATE_TRUNC` on the underlying column rather than
+from the overriding column, so it holds Gregorian buckets — even when the rollup declares
+the overridden granularity itself. Queries served from it return Gregorian periods, with
+no error.
-
+
[ref-time-shift]: /docs/data-modeling/measures#time-shift
diff --git a/docs-mintlify/recipes/data-modeling/custom-calendar.mdx b/docs-mintlify/recipes/data-modeling/custom-calendar.mdx
index 4d1e694ff9ffe..91be0c028eec2 100644
--- a/docs-mintlify/recipes/data-modeling/custom-calendar.mdx
+++ b/docs-mintlify/recipes/data-modeling/custom-calendar.mdx
@@ -418,14 +418,20 @@ five-week month accumulates over all five of its weeks.
## Pre-aggregations
-A [pre-aggregation][ref-pre-aggregations] over an overridden granularity must declare that
-granularity. A rollup on `month` is built from the `retail_month_begins` column and serves
-queries at `month`.
+
+
+**Do not pre-aggregate a query that groups by an overridden granularity.** A rollup is
+built with `DATE_TRUNC` on the underlying column rather than from the calendar column, so
+it holds Gregorian buckets. This applies even when the rollup declares the overridden
+granularity itself.
-Declare it explicitly rather than relying on a finer rollup. Retail months cannot be
-assembled from Gregorian ones, so a rollup that does not name the overridden granularity
-is not a correct source for these queries.
+Queries served from such a rollup return Gregorian months, not retail ones, with no error.
+
+
+Query the calendar cube directly for these groupings. Other
+[pre-aggregations][ref-pre-aggregations] on the same cube are unaffected — those
+grouped by a granularity you have not overridden, or by non-time dimensions.
[link-454]: https://nrf.com/resources/4-5-4-calendar
[link-tesseract]: https://cube.dev/blog/introducing-next-generation-data-modeling-engine
From 441c1a67a737e696182cecd8786a0f2ca92c489c Mon Sep 17 00:00:00 2001
From: igorlukanin <3852894+igorlukanin@users.noreply.github.com>
Date: Thu, 3 Sep 2026 14:50:23 +0200
Subject: [PATCH 5/6] docs: correct the calendar pre-aggregation guidance, the
declared granularity works
---
.../data-modeling/concepts/calendar-cubes.mdx | 14 ++++++---
.../recipes/data-modeling/custom-calendar.mdx | 31 +++++++++++++------
2 files changed, 31 insertions(+), 14 deletions(-)
diff --git a/docs-mintlify/docs/data-modeling/concepts/calendar-cubes.mdx b/docs-mintlify/docs/data-modeling/concepts/calendar-cubes.mdx
index 39a920be270fc..6347a14668239 100644
--- a/docs-mintlify/docs/data-modeling/concepts/calendar-cubes.mdx
+++ b/docs-mintlify/docs/data-modeling/concepts/calendar-cubes.mdx
@@ -485,11 +485,15 @@ calendar in full.
-**Do not [pre-aggregate][ref-pre-aggregations] a query that groups by an overridden
-granularity.** A rollup is built with `DATE_TRUNC` on the underlying column rather than
-from the overriding column, so it holds Gregorian buckets — even when the rollup declares
-the overridden granularity itself. Queries served from it return Gregorian periods, with
-no error.
+**A [pre-aggregation][ref-pre-aggregations] must declare the overridden granularity
+itself.** A rollup that names it is built from the overriding column and serves those
+queries correctly.
+
+A rollup at a finer granularity is not a correct source. Cube can still match one through
+the granularity hierarchy — a `day` rollup for a `month` query — but the rollup holds
+`DATE_TRUNC` buckets that the overriding column cannot be recovered from, so the query
+either fails or returns Gregorian periods. Custom periods cannot be assembled from
+predefined ones.
diff --git a/docs-mintlify/recipes/data-modeling/custom-calendar.mdx b/docs-mintlify/recipes/data-modeling/custom-calendar.mdx
index 91be0c028eec2..3b0baa5cedb57 100644
--- a/docs-mintlify/recipes/data-modeling/custom-calendar.mdx
+++ b/docs-mintlify/recipes/data-modeling/custom-calendar.mdx
@@ -418,21 +418,34 @@ five-week month accumulates over all five of its weeks.
## Pre-aggregations
+A [pre-aggregation][ref-pre-aggregations] over an overridden granularity must declare that
+granularity. A rollup on `month` is built from the `retail_month_begins` column and serves
+queries at `month`:
+
+```yaml
+cubes:
+ - name: orders
+ # ...
+
+ pre_aggregations:
+ - name: orders_by_retail_month
+ measures:
+ - count
+ time_dimension: retail_calendar.date
+ granularity: month
+```
+
-**Do not pre-aggregate a query that groups by an overridden granularity.** A rollup is
-built with `DATE_TRUNC` on the underlying column rather than from the calendar column, so
-it holds Gregorian buckets. This applies even when the rollup declares the overridden
-granularity itself.
+**Declare the overridden granularity explicitly rather than relying on a finer rollup.**
+Cube can match a `day` rollup for a `month` query through the granularity hierarchy, but
+that rollup holds `DATE_TRUNC` buckets, and retail months cannot be assembled from them.
+The query then either fails or returns Gregorian months.
-Queries served from such a rollup return Gregorian months, not retail ones, with no error.
+Add a rollup for each retail period you query.
-Query the calendar cube directly for these groupings. Other
-[pre-aggregations][ref-pre-aggregations] on the same cube are unaffected — those
-grouped by a granularity you have not overridden, or by non-time dimensions.
-
[link-454]: https://nrf.com/resources/4-5-4-calendar
[link-tesseract]: https://cube.dev/blog/introducing-next-generation-data-modeling-engine
[ref-calendar-cubes]: /docs/data-modeling/concepts/calendar-cubes
From d58a409c45fe7e86151ba5223e4a446aa0842974 Mon Sep 17 00:00:00 2001
From: igorlukanin <3852894+igorlukanin@users.noreply.github.com>
Date: Thu, 3 Sep 2026 14:54:36 +0200
Subject: [PATCH 6/6] docs: move the calendar pre-aggregation warning up a
level, add the JavaScript rollup
---
.../data-modeling/concepts/calendar-cubes.mdx | 27 ++++++++++---------
.../recipes/data-modeling/custom-calendar.mdx | 20 +++++++++++++-
2 files changed, 33 insertions(+), 14 deletions(-)
diff --git a/docs-mintlify/docs/data-modeling/concepts/calendar-cubes.mdx b/docs-mintlify/docs/data-modeling/concepts/calendar-cubes.mdx
index 6347a14668239..f97708ff65e1a 100644
--- a/docs-mintlify/docs/data-modeling/concepts/calendar-cubes.mdx
+++ b/docs-mintlify/docs/data-modeling/concepts/calendar-cubes.mdx
@@ -449,6 +449,20 @@ When querying `sales.revenue` by `custom_calendar.date` with monthly granularity
`mid_month` column will be used instead of the standard `DATE_TRUNC('month', date)`
expression in the generated SQL.
+
+
+**A [pre-aggregation][ref-pre-aggregations] must declare the overridden granularity
+itself.** A rollup that names it is built from the overriding column and serves those
+queries correctly.
+
+A rollup at a finer granularity is not a correct source. Cube can still match one through
+the granularity hierarchy — a `day` rollup for a `month` query — but the rollup holds
+`DATE_TRUNC` buckets that the overriding column cannot be recovered from, so the query
+either fails or returns Gregorian periods. Custom periods cannot be assembled from
+predefined ones.
+
+
+
### Naming a granularity defined with `sql`
A granularity defined with `sql` must be named after a default granularity: `second`,
@@ -483,19 +497,6 @@ length and cannot be derived arithmetically, such as the months and quarters of
retail calendar. The [custom calendar recipe][ref-recipe-custom-calendar] models a 4-5-4
calendar in full.
-
-
-**A [pre-aggregation][ref-pre-aggregations] must declare the overridden granularity
-itself.** A rollup that names it is built from the overriding column and serves those
-queries correctly.
-
-A rollup at a finer granularity is not a correct source. Cube can still match one through
-the granularity hierarchy — a `day` rollup for a `month` query — but the rollup holds
-`DATE_TRUNC` buckets that the overriding column cannot be recovered from, so the query
-either fails or returns Gregorian periods. Custom periods cannot be assembled from
-predefined ones.
-
-
[ref-time-shift]: /docs/data-modeling/measures#time-shift
diff --git a/docs-mintlify/recipes/data-modeling/custom-calendar.mdx b/docs-mintlify/recipes/data-modeling/custom-calendar.mdx
index 3b0baa5cedb57..645dd4af6a91d 100644
--- a/docs-mintlify/recipes/data-modeling/custom-calendar.mdx
+++ b/docs-mintlify/recipes/data-modeling/custom-calendar.mdx
@@ -422,7 +422,9 @@ A [pre-aggregation][ref-pre-aggregations] over an overridden granularity must de
granularity. A rollup on `month` is built from the `retail_month_begins` column and serves
queries at `month`:
-```yaml
+
+
+```yaml title="YAML"
cubes:
- name: orders
# ...
@@ -435,6 +437,22 @@ cubes:
granularity: month
```
+```javascript title="JavaScript"
+cube(`orders`, {
+ // ...
+
+ pre_aggregations: {
+ orders_by_retail_month: {
+ measures: [count],
+ time_dimension: retail_calendar.date,
+ granularity: `month`
+ }
+ }
+})
+```
+
+
+
**Declare the overridden granularity explicitly rather than relying on a finer rollup.**