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
17 changes: 16 additions & 1 deletion packages/cubejs-schema-compiler/src/adapter/BaseQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -3346,7 +3346,16 @@ export class BaseQuery {
}
}
const primaryKeys = this.cubeEvaluator.primaryKeys[cubeName];
const orderBySql = (symbol.orderBy || []).map(o => ({ sql: this.evaluateSql(cubeName, o.sql), dir: o.dir }));
// Evaluate the measure's order_by template in the cube context where
// it was authored, not the consumer's queried cube/view context. When
// a multi_stage measure (e.g. type: rank) is exposed on a view, its
// order_by template may reference other cube members by bare name
// (e.g. `{num_parcels}`) which only exist on the underlying cube.
// CubeSymbols.generateIncludeMembers tags view-generated measure
// definitions with `orderByCubeName` for this purpose; falls back to
// `cubeName` for direct cube access. See cube-js/cube#10856.
const orderByCubeName = symbol.orderByCubeName || cubeName;
const orderBySql = (symbol.orderBy || []).map(o => ({ sql: this.evaluateSql(orderByCubeName, o.sql), dir: o.dir }));
let sql;
let patchedSymbol = symbol;
if (symbol.type !== 'rank') {
Expand Down Expand Up @@ -3589,6 +3598,12 @@ export class BaseQuery {
cubeName,
name
);
if (!resolvedSymbol) {
throw new UserError(
`Symbol '${name}' could not be resolved on cube '${cubeName}'. ` +
'Check that the referenced member exists and is accessible from this context.'
);
}
// eslint-disable-next-line no-underscore-dangle
if (resolvedSymbol._objectWithResolvedProperties) {
return resolvedSymbol;
Expand Down
10 changes: 9 additions & 1 deletion packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -986,7 +986,15 @@ export class CubeSymbols implements TranspilerSymbolResolver, CompilerInterface
...(propagatedCurrency ? { currency: propagatedCurrency } : {}),
...(resolvedMember.multiStage && { multiStage: resolvedMember.multiStage }),
...(resolvedMember.timeShift && { timeShift: resolvedMember.timeShift }),
...(resolvedMember.orderBy && { orderBy: resolvedMember.orderBy }),
// Carry the owning cube's name through to the view-generated
// measure definition so that internal templates (e.g. order_by,
// which references other cube members by bare name) can be
// resolved against the cube context where they were authored,
// not the consumer view's context. See cube-js/cube#10856.
...(resolvedMember.orderBy && {
orderBy: resolvedMember.orderBy,
orderByCubeName: path[path.length - 2],
}),
...(processedDrillMembers && { drillMembers: processedDrillMembers }),
...(resolvedMember.drillMembersGrouped && { drillMembersGrouped: resolvedMember.drillMembersGrouped }),
...(resolvedMember.mask !== undefined ? { mask: resolvedMember.mask } : {}),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* eslint-disable no-restricted-syntax */
import { PostgresQuery } from '../../src/adapter/PostgresQuery';
import { prepareJsCompiler } from './PrepareCompiler';

describe('Multi-stage rank order_by template resolution through a view (issue #10856)', () => {
const { compiler, joinGraph, cubeEvaluator } = prepareJsCompiler(`
cube(\`orders\`, {
sql: \`SELECT 1 as id, 5 as parcels, '2024-01-01'::timestamp as created_at\`,

dimensions: {
id: {
sql: \`id\`,
type: \`number\`,
primary_key: true,
public: true
},

created_at: {
sql: \`created_at\`,
type: \`time\`
}
},

measures: {
num_parcels: {
type: \`sum\`,
sql: \`parcels\`
},

volume_by_day_rank: {
multi_stage: true,
type: \`rank\`,
order_by: [{
sql: \`\${num_parcels}\`,
dir: 'desc'
}],
reduce_by: [created_at]
}
}
});

view(\`orders_view\`, {
cubes: [{
join_path: orders,
includes: [\`created_at\`, \`volume_by_day_rank\`]
}]
});
`);

it('resolves rank order_by against the owning cube when queried through a view that does not expose the referenced measure', async () => {
await compiler.compile();

const query = new PostgresQuery({ joinGraph, cubeEvaluator, compiler }, {
measures: ['orders_view.volume_by_day_rank'],
timeDimensions: [{
dimension: 'orders_view.created_at',
granularity: 'day',
}],
timezone: 'UTC',
});

// Before the fix, buildSqlAndParams crashes with:
// TypeError: Cannot read properties of undefined (reading '_objectWithResolvedProperties')
// because the rank's order_by template `${num_parcels}` was being resolved
// against the view's symbol table (which doesn't expose num_parcels) rather
// than the cube's.
const [sql] = query.buildSqlAndParams();

// The cube column `parcels` (which num_parcels aggregates) must appear in
// the rank's ORDER BY clause.
expect(sql).toMatch(/ORDER BY[^)]*parcels/i);
});
});
Loading