Skip to content
Draft
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
20 changes: 19 additions & 1 deletion packages/cubejs-schema-compiler/src/adapter/BaseQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -1780,7 +1780,25 @@ export class BaseQuery {
// TODO not working yet
const membersToSelect = measures?.concat(multiStageDimensions).concat(multiStageTimeDimensions);
const select = fromSubQuery && fromSubQuery.outerMeasuresJoinFullKeyQueryAggregate(membersToSelect, membersToSelect, withQuery.memberFrom.map(f => f.alias));
const fromSql = select && this.wrapInParenthesis(select);
// Leaf multi_stage measure: sql references a raw column with no measure dependencies,
// so fromMeasures is null, fromSubQuery was never built, and select is null.
// Fall back to querying the cube's own table directly instead of producing an empty FROM.
// See: https://github.com/cube-js/cube/issues/9241
let fromSql = select && this.wrapInParenthesis(select);
if (!fromSql && !fromSubQuery && withQuery.measures?.length) {
const leafFromQuery = this.newSubQuery({
measures: withQuery.measures,
dimensions: withQuery.dimensions,
timeDimensions: withQuery.timeDimensions,
multiStageDimensions: withQuery.multiStageDimensions,
multiStageTimeDimensions: withQuery.multiStageTimeDimensions,
filters: withQuery.filters,
segments: withQuery.segments,
multiStageQuery: true,
disableExternalPreAggregations: true,
});
fromSql = this.wrapInParenthesis(leafFromQuery.buildParamAnnotatedSql());
}

const subQueryOptions = {
measures: withQuery.measures,
Expand Down
106 changes: 106 additions & 0 deletions packages/cubejs-schema-compiler/test/unit/base-query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2683,4 +2683,110 @@ describe('Class unit tests', () => {
const re = new RegExp('(b__aid).*(b__bval_sum).*(b__count).*');
expect(re.test(sql[0])).toBeTruthy();
});

describe('multi_stage leaf measure (raw column sql, no measure dependencies)', () => {
// Regression tests for https://github.com/cube-js/cube/issues/9241
//
// The bug: a multi_stage measure whose sql references a raw column (no {} measure dependency)
// has no children in the member dependency graph. When a higher-level multi_stage measure
// references it via {leaf_measure}, renderWithQuery is called for the leaf withQuery with
// memberFrom=null. This left fromSubQuery=null and fromSql=null, causing the error check
// at the end of renderWithQuery to throw "lacks FROM clause".
//
// The two-level schema is required to trigger the path: the outer measure forces
// renderWithQuery to be called for the leaf, which is where the fix applies.
const compilers = prepareYamlCompiler(`
cubes:
- name: orders
sql_table: orders

dimensions:
- name: id
sql: id
type: number
primary_key: true

- name: status
sql: status
type: string

measures:
- name: raw_sum
sql: amount
type: sum
multi_stage: true

- name: raw_avg
sql: amount
type: avg
multi_stage: true

- name: computed_from_raw_sum
sql: "{raw_sum}"
type: number
multi_stage: true

- name: computed_from_raw_avg
sql: "{raw_avg}"
type: number
multi_stage: true
`);

it('does not throw "lacks FROM clause" (sum leaf)', async () => {
await compilers.compiler.compile();

const query = new PostgresQuery(compilers, {
measures: ['orders.computed_from_raw_sum'],
dimensions: ['orders.status'],
timeDimensions: [],
filters: [],
});

expect(() => query.buildSqlAndParams()).not.toThrow();
});

it('generates a CTE query (multi_stage path is taken)', async () => {
await compilers.compiler.compile();

const query = new PostgresQuery(compilers, {
measures: ['orders.computed_from_raw_sum'],
dimensions: ['orders.status'],
timeDimensions: [],
filters: [],
});

const [sql] = query.buildSqlAndParams();
expect(sql).toMatch(/WITH\s+cte_\d+\s+AS/i);
expect(sql).toContain('orders');
});

it('propagates filters through the leaf fallback subquery', async () => {
await compilers.compiler.compile();

const query = new PostgresQuery(compilers, {
measures: ['orders.computed_from_raw_sum'],
dimensions: ['orders.status'],
timeDimensions: [],
filters: [{ member: 'orders.status', operator: 'equals', values: ['completed'] }],
});

const [sql] = query.buildSqlAndParams();
expect(sql).toMatch(/WHERE/i);
});

it('works for avg type as well as sum', async () => {
await compilers.compiler.compile();

const query = new PostgresQuery(compilers, {
measures: ['orders.computed_from_raw_avg'],
dimensions: ['orders.status'],
timeDimensions: [],
filters: [],
});

expect(() => query.buildSqlAndParams()).not.toThrow();
const [sql] = query.buildSqlAndParams();
expect(sql).toMatch(/WITH\s+cte_\d+\s+AS/i);
});
});
});