Skip to content

fix(schema-compiler): resolve time dimension granularity columns in pre-aggregation indexes#10894

Draft
paveltiunov wants to merge 5 commits into
masterfrom
cursor/fix-index-granularity-column-alias-ae48
Draft

fix(schema-compiler): resolve time dimension granularity columns in pre-aggregation indexes#10894
paveltiunov wants to merge 5 commits into
masterfrom
cursor/fix-index-granularity-column-alias-ae48

Conversation

@paveltiunov
Copy link
Copy Markdown
Member

@paveltiunov paveltiunov commented May 15, 2026

Problem

When using time dimension columns with granularity references in pre-aggregation indexes (e.g., CUBE.timestamp.hour), the generated column alias was incorrect:

  • Expected: orders__created_at_hour (single underscore before granularity, matching the pre-aggregation table column name)
  • Actual: orders__created_at__hour (double underscore before granularity)

This happened because evaluateIndexColumns in BaseQuery.js treated the full 3-part dotted path through aliasName(), which converts all dots to double underscores. However, the actual pre-aggregation table column is generated by BaseTimeDimension.unescapedAliasName(), which uses the format {dimensionAlias}_{granularity} with a single underscore separator.

The mismatch caused index creation to fail or reference non-existent columns.

Fix

In evaluateIndexColumns, when a 3-part column path is detected where:

  1. The first two parts form a valid time dimension
  2. The third part is a valid granularity

...the alias is now generated as aliasName(cube.dimension) + '_' + granularity, matching the format used by BaseTimeDimension.unescapedAliasName().

Documentation

Updated both Mintlify and legacy docs to show the new CUBE.<time_dimension>.<granularity> syntax as the primary way to reference time dimension columns in pre-aggregation indexes. The previous string-based format (orders__created_at_day) is kept as a documented alternative.

Tests

Unit tests (cubejs-schema-compiler):

  • JS format: Verifies CUBE.created_at.hour in index columns produces "orders__created_at_hour"
  • YAML format: Verifies created_at.hour in YAML index columns produces the same correct alias
  • Both tests verify the correct alias appears in indexesSql and createTableIndexes

E2E tests (added to existing smoke-postgres.test.ts in cubejs-testing):

  • Queries data through a pre-aggregation with granularity-based indexes to verify it builds and returns correct results
  • Validates the system API partitions endpoint returns index SQL with the correct column alias (single underscore before granularity)

All unit and E2E tests pass.

Open in Web Open in Cursor 

…re-aggregation indexes

When using time dimension columns with granularity in pre-aggregation
indexes (e.g., CUBE.timestamp.hour), the generated column alias used
double underscores (orders__timestamp__hour) instead of the correct
format with a single underscore before the granularity
(orders__timestamp_hour), causing a column name mismatch with the
actual pre-aggregation table column.

The fix detects 3-part column paths (cube.dimension.granularity) in
evaluateIndexColumns and generates the alias consistent with
BaseTimeDimension.unescapedAliasName().

Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
@github-actions github-actions Bot added the javascript Pull requests that update Javascript code label May 15, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 15, 2026

Claude finished @paveltiunov's task in 3m 9s —— View job


  • Gather context and read changed files
  • Run tests (unable to run — missing sandbox permissions for yarn test)
  • Review code quality and correctness
  • Review test coverage
  • Post final review

Review Summary

Overall: This is a clean, well-targeted fix. The bug diagnosis is accurate and the fix is minimal.


Correctness

The root cause is correctly identified: evaluateIndexColumns receives a 3-part path like Orders.created_at.hour for time dimension granularity columns. Previously, this entire path was passed to aliasName(), which converts all dots to __, producing orders__created_at__hour. But the actual pre-aggregation table column is generated by BaseTimeDimension.unescapedAliasName() using aliasName(dimension) + '_' + granularity, producing orders__created_at_hour.

The fix correctly mirrors the BaseTimeDimension.unescapedAliasName() logic (line 78 of BaseTimeDimension.ts):

  • Both use this.query.aliasName(dimensionPath) for the dimension part (handles sqlAlias correctly)
  • Both append _ + granularity with a single underscore

The guard conditions are solid:

  • path.length === 3 — only triggers for granularity-qualified paths
  • isDimension(path.slice(0, 2)) — confirms the first two parts are a real dimension
  • dimensionDef.type === 'time' — only time dimensions can have granularities
  • resolveGranularity(...) — validates the granularity actually exists (handles both built-in and custom granularities)

Both consumers of evaluateIndexColumnsindexSql (line 4257, for CREATE INDEX) and createTableIndexes (line 436 of PreAggregations.ts, for external table indexes) — benefit from this fix.

I left one inline comment about a minor memberToAlias discrepancy with BaseTimeDimension.unescapedAliasName(), but it's unlikely to matter in practice.

Test Coverage

The two new tests are well-structured:

  • JS format test: Covers both a single granularity column index and a mixed (granularity + regular dimension) index. Validates both indexesSql (SQL string) and createTableIndexes (column list).
  • YAML format test: Covers the YAML path where references are resolved differently. Good to have since YAML index columns use a different syntax (created_at.hour vs CUBE.created_at.hour).

Negative assertions (not.toContain for the double-underscore form) are a nice touch for regression protection.

Potential additions to consider (non-blocking):

  • A test with a custom granularity (not just built-in hour)
  • A test with a cube that has a sqlAlias to verify the alias substitution still works correctly through this path

Security & Performance

No concerns. The fix adds a few extra checks (length check, isDimension, dimensionByPath, resolveGranularity) but only for 3-part paths inside index column evaluation — this is a cold path that runs during pre-aggregation description building, not query execution.


…index columns

Update both Mintlify and legacy docs to show the new
CUBE.<time_dimension>.<granularity> syntax as the primary way to
reference time dimension columns in pre-aggregation indexes.

The previous string-based format (e.g., orders__created_at_day) is
kept as an alternative approach.

Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
Comment on lines +4276 to +4282
if (path.length === 3 && this.cubeEvaluator.isDimension(path.slice(0, 2))) {
const dimensionPath = `${path[0]}.${path[1]}`;
const dimensionDef = this.cubeEvaluator.dimensionByPath(dimensionPath);
if (dimensionDef.type === 'time' &&
this.cubeEvaluator.resolveGranularity([path[0], path[1], 'granularities', path[2]])) {
return `${this.aliasName(dimensionPath)}_${path[2]}`;
}
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.

Minor: BaseTimeDimension.unescapedAliasName() checks this.query.options.memberToAlias?.[fullName] (where fullName = dimension + '.' + granularity) before falling through to the aliasName(dimension) + '_' + granularity format. This new code path skips that check.

In practice this likely doesn't matter for pre-aggregation index columns since memberToAlias is typically set for query-level aliasing, not pre-aggregation evaluation. But if you want exact parity with BaseTimeDimension.unescapedAliasName():

Suggested change
if (path.length === 3 && this.cubeEvaluator.isDimension(path.slice(0, 2))) {
const dimensionPath = `${path[0]}.${path[1]}`;
const dimensionDef = this.cubeEvaluator.dimensionByPath(dimensionPath);
if (dimensionDef.type === 'time' &&
this.cubeEvaluator.resolveGranularity([path[0], path[1], 'granularities', path[2]])) {
return `${this.aliasName(dimensionPath)}_${path[2]}`;
}
if (path.length === 3 && this.cubeEvaluator.isDimension(path.slice(0, 2))) {
const dimensionPath = `${path[0]}.${path[1]}`;
const dimensionDef = this.cubeEvaluator.dimensionByPath(dimensionPath);
if (dimensionDef.type === 'time' &&
this.cubeEvaluator.resolveGranularity([path[0], path[1], 'granularities', path[2]])) {
const fullName = `${dimensionPath}.${path[2]}`;
if (this.options.memberToAlias?.[fullName]) {
return this.options.memberToAlias[fullName];
}
return `${this.aliasName(dimensionPath)}_${path[2]}`;
}
}

Nit — feel free to disregard if memberToAlias is never populated for index column evaluation paths.

…th granularity columns

Add an E2E test that verifies time dimension columns with granularity
(CUBE.createdAt.day) work correctly in pre-aggregation index definitions.

The test:
- Starts a Postgres container with Birdbox
- Defines a cube with a pre-aggregation that has indexes using
  CUBE.createdAt.day syntax
- Queries data through the pre-aggregation to verify it builds and
  returns correct results
- Checks the system API partitions endpoint to verify the index SQL
  uses the correct column alias (single underscore before granularity)

Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 15, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 58.38%. Comparing base (233e86f) to head (f2d521b).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #10894      +/-   ##
==========================================
+ Coverage   58.36%   58.38%   +0.01%     
==========================================
  Files         216      216              
  Lines       16944    16949       +5     
  Branches     3435     3435              
==========================================
+ Hits         9890     9895       +5     
  Misses       6552     6552              
  Partials      502      502              
Flag Coverage Δ
cube-backend 58.38% <100.00%> (+0.01%) ⬆️

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

☔ View full report in Codecov by Sentry.
📢 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.

cursoragent and others added 2 commits May 15, 2026 22:10
…test

The cube name OrdersPAIndexGranularity gets underscored as
orders_p_a_index_granularity (not orders_pa_index_granularity)
by inflection.underscore. Fixed the expected strings to match.

Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
…tgres

Merge the index granularity E2E tests into the existing
smoke-postgres.test.ts instead of having a separate test file.
Both share the same Birdbox setup (smoke/schema dir, smoke/cube.js).

Co-authored-by: Pavel Tiunov <pavel.tiunov@gmail.com>
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants