Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1139,7 +1139,8 @@ export class PreAggregations {
private cubesHintsFromPreAggregation(preAggObj: PreAggregationForQuery): string[][] {
return R.uniq(
preAggObj.references.measures.concat(
preAggObj.references.dimensions
preAggObj.references.dimensions,
(preAggObj.references.timeDimensions || []).map(td => td.dimension)

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.

references.timeDimensions is non-optional in PreAggregationReferences (CubeEvaluator.ts:152) and evaluatePreAggregationReferences always sets it; other call sites read it unguarded (e.g. mergePartitionTimeDimensions at PreAggregations.ts:1388). The || [] is dead defensiveness that suggests the field can be missing.

Suggested change
(preAggObj.references.timeDimensions || []).map(td => td.dimension)
preAggObj.references.timeDimensions.map(td => td.dimension)

).map(p => p.split('.').slice(0, -1))
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { PostgresQuery } from '../../src/adapter/PostgresQuery';
import { prepareYamlCompiler } from './PrepareCompiler';

// `rollup_join` builds its join tree from cube hints collected off the
// pre-aggregation's own references. Those hints have to cover every cube the
// join is meant to span, including a cube whose single reference is its
// `time_dimension` - otherwise the join tree covers one cube, `targetJoins`
// comes back empty and the pre-aggregation is rejected with "Nothing to join in
// rollup join".
//
// The shape has no workaround: naming a dimension of that cube would put the
// hint in, but then that dimension has to be requested by every query for the
// rollup to match at all.
//
// Both planners build the pre-aggregation description here, so both are covered.
Comment on lines +3 to +15

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.

This 13-line header re-tells the PR description (the "no workaround" paragraph and the "both planners" note are commit-message material, not something a later editor needs in order to avoid reintroducing the bug). The load-bearing sentence is the first one; the rest can go. The two per-fixture one-liners below already say what each shape exercises.

const model = (joinedPreAggregation: string) => `
cubes:
- name: locations
sql: >
SELECT 1 AS id, 'A' AS board_id, '2026-01-01'::timestamp AS ts UNION ALL
SELECT 2 AS id, 'A' AS board_id, '2026-01-02'::timestamp
joins:
- name: boards
sql: "{CUBE.board_id} = {boards.board_id}"
relationship: many_to_one
dimensions:
- name: id
sql: "{CUBE}.id"
type: number
primary_key: true
- name: board_id
sql: "{CUBE}.board_id"
type: string
- name: ts
sql: "{CUBE}.ts"
type: time
measures:
- name: count
type: count
pre_aggregations:
- name: locations_rollup
type: rollup
dimensions:
- board_id
time_dimension: ts
granularity: day

- name: boards
sql: >
SELECT 'A' AS board_id, 1 AS good
dimensions:
- name: board_id
sql: "{CUBE}.board_id"
type: string
primary_key: true
measures:
- name: good_count
sql: "{CUBE}.good"
type: sum
pre_aggregations:
- name: boards_rollup
type: rollup
measures:
- good_count
dimensions:
- board_id
${joinedPreAggregation}
`;

// Reaches `locations` only through its time dimension.
const timeDimensionOnly = `
- name: joined
type: rollup_join
rollups:
- locations.locations_rollup
- boards.boards_rollup
measures:
- good_count
time_dimension: locations.ts
granularity: day
`;

// Same, plus a dimension from `locations`.
const withAnchorDimension = `
- name: joined
type: rollup_join
rollups:
- locations.locations_rollup
- boards.boards_rollup
measures:
- good_count
dimensions:
- locations.board_id
time_dimension: locations.ts
granularity: day
`;

const preAggregationSql = async (useNativeSqlPlanner: boolean, joinedPreAggregation: string, query: any) => {
const compilers = prepareYamlCompiler(model(joinedPreAggregation));
await compilers.compiler.compile();

return new PostgresQuery(compilers, {
timezone: 'UTC',
useNativeSqlPlanner,
...query,
})
.preAggregations.preAggregationsDescription()
.map((d: any) => d.loadSql?.[0] ?? '')
.join('\n');
};

describe.each([
['legacy', false],
['tesseract', true],
])('rollup_join cube hints (%s planner)', (_name, useNativeSqlPlanner) => {
const timeDimensions = [{
dimension: 'locations.ts',
granularity: 'day',
dateRange: ['2026-01-01', '2026-01-31'],
}];

it('builds the join when a cube is referenced only by time_dimension', async () => {
const sql = await preAggregationSql(useNativeSqlPlanner, timeDimensionOnly, {
measures: ['boards.good_count'],
timeDimensions,
});

expect(sql).toContain('locations_locations_rollup');
Comment on lines +118 to +128

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.

Neither test covers the other consumer of cubesHintsFromPreAggregation that this change also feeds: existingJoins (PreAggregations.ts:1058-1062). Both leg rollups here declare a same-cube time dimension, so their hint sets are unchanged by the fix. A leg rollup whose time_dimension is reached through a join now contributes an extra hint, which can cancel a target join and turn a previously-buildable rollup_join into Nothing to join in rollup join. The PR argues that cancellation is semantically right, and I agree — but it is a behavior change for existing models with no test pinning it. Worth adding one leg-side case (or explicitly noting that rollup join existing joins in the Postgres integration suite is the only coverage).

Also, the assertions only check that both leg table names appear somewhere in the concatenated loadSql; asserting on the number of descriptions or on the join SQL would fail more informatively if the rollup stops matching (an empty description array currently yields '' and a toContain failure that reads like a SQL mismatch).

expect(sql).toContain('boards_boards_rollup');
});

it('builds the join when a dimension of that cube is referenced too', async () => {
const sql = await preAggregationSql(useNativeSqlPlanner, withAnchorDimension, {
measures: ['boards.good_count'],
dimensions: ['locations.board_id'],
timeDimensions,
});

expect(sql).toContain('locations_locations_rollup');
expect(sql).toContain('boards_boards_rollup');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -1284,4 +1284,32 @@ mod tests {
_ => panic!("Expected PreAggregationSource::Join"),
}
}

#[test]
fn test_compile_rollup_join_cube_reached_only_by_time_dimension() {
let schema = MockSchema::from_yaml_file("common/rollup_join_time_dimension_hints.yaml");
let test_context = TestContext::new(schema).unwrap();
let query_tools = test_context.query_tools().clone();

let cube_names = vec!["boards".to_string(), "locations".to_string()];
let mut compiler = PreAggregationsCompiler::try_new(query_tools, &cube_names).unwrap();

let pre_agg_name = PreAggregationFullName::new("boards".to_string(), "joined".to_string());
let compiled = compiler.compile_pre_aggregation(&pre_agg_name).unwrap();

let single_name = |source: &PreAggregationSource| match source {
PreAggregationSource::Single(table) => table.name.clone(),
_ => panic!("Expected Single source"),
};

match compiled.source.as_ref() {
PreAggregationSource::Join(join) => {
assert_eq!(join.items.len(), 1);
assert_eq!(single_name(&join.items[0].from), "locations_rollup");
assert_eq!(single_name(&join.items[0].to), "boards_rollup");
assert_eq!(single_name(&join.root), "locations_rollup");
}
_ => panic!("Expected PreAggregationSource::Join"),
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
cubes:
- name: locations
sql: "SELECT 1 as id, 'A' as board_id, '2026-01-01'::timestamp as ts"

joins:
- name: boards
relationship: many_to_one
sql: "{CUBE.board_id} = {boards.board_id}"
dimensions:
- name: id
type: number
sql: id
primary_key: true
- name: board_id
type: string
sql: board_id
- name: ts
type: time
sql: ts
measures:
- name: count
type: count
pre_aggregations:
- name: locations_rollup
type: rollup
dimensions:
- board_id
time_dimension: ts
granularity: day

- name: boards
sql: "SELECT 'A' as board_id, 1 as good"

dimensions:
- name: board_id
type: string
sql: board_id
primary_key: true
measures:
- name: good_count
type: sum
sql: good
pre_aggregations:
- name: boards_rollup
type: rollup
measures:
- good_count
dimensions:
- board_id
# `locations` is reached only through its time dimension.
- name: joined
type: rollupJoin
measures:
- good_count
time_dimension: locations.ts
granularity: day
rollups:
- locations.locations_rollup
- boards.boards_rollup
Loading