diff --git a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js index 6c53412570bd1..c7ad1e24fb817 100644 --- a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js +++ b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js @@ -950,6 +950,7 @@ export class BaseQuery { ] : []) .concat( R.pipe( + // TODO for member expressions this can be misleading: it can be a name of view, which does not have PK R.groupBy(m => m.cube().name), R.toPairs, R.map( @@ -1054,18 +1055,29 @@ export class BaseQuery { outerMeasuresJoinFullKeyQueryAggregate(innerMembers, outerMembers, toJoin) { const renderedReferenceContext = { renderedReference: R.pipe( - R.map(m => [m.measure || m.dimension, m.aliasName()]), + R.map(m => { + let memberPath; + if (m.measure) { + memberPath = typeof m.measure === 'string' ? m.measure : this.cubeEvaluator.pathFromArray([m.expressionCubeName, m.expressionName]); + } else { + memberPath = typeof m.dimension === 'string' ? m.dimension : this.cubeEvaluator.pathFromArray([m.expressionCubeName, m.expressionName]); + } + return [memberPath, m.aliasName()]; + }), R.fromPairs, - // eslint-disable-next-line @typescript-eslint/no-unused-vars )(innerMembers), }; const join = R.drop(1, toJoin) - .map( - (q, i) => (this.dimensionAliasNames().length ? - `INNER JOIN ${this.wrapInParenthesis((q))} as q_${i + 1} ON ${this.dimensionsJoinCondition(`q_${i}`, `q_${i + 1}`)}` : - `, ${this.wrapInParenthesis(q)} as q_${i + 1}`), - ).join('\n'); + .map((q, i) => { + console.log("outerMeasuresJoinFullKeyQueryAggregate generating join, this.dimensionAliasNames()", this.dimensionAliasNames()); + return this.dimensionAliasNames().length + ? `INNER JOIN ${this.wrapInParenthesis(q)} as q_${ + i + 1 + } ON ${this.dimensionsJoinCondition(`q_${i}`, `q_${i + 1}`)}` + : `, ${this.wrapInParenthesis(q)} as q_${i + 1}`; + }) + .join("\n"); const columnsToSelect = this.evaluateSymbolSqlWithContext( () => this.dimensionColumns('q_0').concat(outerMembers.map(m => m.selectColumns())).join(', '), @@ -1120,9 +1132,22 @@ export class BaseQuery { return allMemberChildren[m]?.some(c => hasMultiStageMembers(c)) || false; }; + // This is a bit of a hack + // For now object can only come from dimension-only measure branch inside collectRootMeasureToHieararchy + // So just filters those out, and treat them as regular measures + // TODO teach rest of code here to work with member expressions + const dimensionOnlyMeasures = Object.values(measureToHierarchy) + .flat() + .map((m) => m.measure) + .filter((m) => typeof m === 'object'); + const measuresToRender = (multiplied, cumulative) => R.pipe( R.values, R.flatten, + R.filter( + // To filter out member expressions + m => typeof m.measure === 'string' + ), R.filter( m => m.multiplied === multiplied && this.newMeasure(m.measure).isCumulative() === cumulative && !hasMultiStageMembers(m.measure) ), @@ -1131,8 +1156,11 @@ export class BaseQuery { R.map(m => this.newMeasure(m)) ); - const multipliedMeasures = measuresToRender(true, false)(measureToHierarchy); - const regularMeasures = measuresToRender(false, false)(measureToHierarchy); + // TODO dimensionOnlyMeasures should belong to proper subquery depending on join tree, not to regular measures + const multipliedMeasures = measuresToRender(true, false)(measureToHierarchy) + // .concat(dimensionOnlyMeasures); + const regularMeasures = measuresToRender(false, false)(measureToHierarchy) + .concat(dimensionOnlyMeasures); const cumulativeMeasures = R.pipe( @@ -1709,6 +1737,19 @@ export class BaseQuery { const cubeName = m.expressionCubeName ? `\`${m.expressionCubeName}\` ` : ''; throw new UserError(`The query contains \`COUNT(*)\` expression but cube/view ${cubeName}is missing \`count\` measure`); } + if (collectedMeasures.length === 0 && m.isMemberExpression) { + // `m` is member expression measure, but does not reference any other measure + // Consider this dimensions-only measure. This can happen at least in 2 cases: + // 1. Ad-hoc aggregation over dimension: SELECT MAX(dim) FROM cube + // 2. Ungrouped query with SQL pushdown will render every column as measure: SELECT dim1 FROM cube WHERE LOWER(dim2) = 'foo'; + // Measures like this considered regular: they depend only on dimensions and join tree + // This would return measure object in `measure`, not path + // TODO return measure object for every measure + return [`${m.measure.cubeName}.${m.measure.name}`, [{ + multiplied: false, + measure: m, + }]]; + } return [typeof m.measure === 'string' ? m.measure : `${m.measure.cubeName}.${m.measure.name}`, collectedMeasures]; })); } diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/sql-generation.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/sql-generation.test.ts index e11b7f01b3af3..d0f73959234d9 100644 --- a/packages/cubejs-schema-compiler/test/integration/postgres/sql-generation.test.ts +++ b/packages/cubejs-schema-compiler/test/integration/postgres/sql-generation.test.ts @@ -649,6 +649,176 @@ SELECT 1 AS revenue, cast('2024-01-01' AS timestamp) as time UNION ALL } } }); + + cube('orders', { + sql: \` + SELECT 1 AS order_id, 100 AS total, 1 AS user_id UNION ALL + SELECT 2 AS order_id, 200 AS total, 1 AS user_id UNION ALL + SELECT 3 AS order_id, 500 AS total, 2 AS user_id + \`, + joins: { + users: { + relationship: 'belongsTo', + sql: \`\${CUBE.user_id} = \${users.user_id}\` + } + }, + measures: { + sum_total: { + type: 'sum', + sql: 'total' + } + }, + dimensions: { + order_id: { + type: 'number', + sql: 'order_id', + primaryKey: true + }, + user_id: { + type: 'number', + sql: 'user_id', + } + } + }); + cube('users', { + sql: \` + SELECT 1 AS user_id, false AS internal UNION ALL + SELECT 2 AS user_id, true AS internal + \`, + dimensions: { + user_id: { + type: 'number', + sql: 'user_id', + primaryKey: true + }, + internal: { + type: 'boolean', + sql: 'internal' + }, + } + }); + view('revenue_view', { + cubes: [ + { + join_path: 'orders', + includes: ['sum_total', 'user_id'] + }, + { + join_path: 'orders.users', + includes: ['internal'] + } + ] + }); + + + cube('A', { + sql: \` + SELECT 1 AS a_id, 'foo' AS dim_a, 100 AS val_a UNION ALL + SELECT 2 AS a_id, 'foo' AS dim_a, 200 AS val_a UNION ALL + SELECT 3 AS a_id, 'bar' AS dim_a, 500 AS val_a + \`, + joins: { + B: { + relationship: 'hasMany', + sql: \`\${CUBE.a_id} = \${B.a_id}\` + }, + C: { + relationship: 'hasMany', + sql: \`\${CUBE.a_id} = \${C.a_id}\` + }, + }, + measures: { + sum_a: { + type: 'sum', + sql: 'val_a' + } + }, + dimensions: { + a_id: { + type: 'number', + sql: 'a_id', + primaryKey: true + }, + dim_a: { + type: 'string', + sql: 'dim_a', + } + } + }); + cube('B', { + sql: \` + SELECT 1 AS b_id, 1 AS a_id, 'foo' AS dim_b, 100 AS val_b UNION ALL + SELECT 2 AS b_id, 2 AS a_id, 'foo' AS dim_b, 200 AS val_b UNION ALL + SELECT 3 AS b_id, 2 AS a_id, 'bar' AS dim_b, 500 AS val_b + \`, + measures: { + sum_b: { + type: 'sum', + sql: 'val_b' + } + }, + dimensions: { + b_id: { + type: 'number', + sql: 'b_id', + primaryKey: true + }, + a_id: { + type: 'number', + sql: 'a_id', + }, + dim_b: { + type: 'string', + sql: 'dim_b', + } + } + }); + cube('C', { + sql: \` + SELECT 1 AS c_id, 2 AS a_id, 'foo' AS dim_c, 100 AS val_c UNION ALL + SELECT 2 AS c_id, 3 AS a_id, 'foo' AS dim_c, 200 AS val_c UNION ALL + SELECT 3 AS c_id, 3 AS a_id, 'bar' AS dim_c, 500 AS val_c UNION ALL + SELECT 4 AS c_id, 2 AS a_id, 'qux' AS dim_c, 7 AS val_c + \`, + measures: { + sum_c: { + type: 'sum', + sql: 'val_c' + } + }, + dimensions: { + c_id: { + type: 'number', + sql: 'c_id', + primaryKey: true + }, + a_id: { + type: 'number', + sql: 'a_id', + }, + dim_c: { + type: 'string', + sql: 'dim_c', + } + } + }); + view('ABC_view', { + cubes: [ + { + join_path: 'A', + includes: ['a_id', 'dim_a', 'sum_a'] + }, + { + join_path: 'A.B', + includes: ['b_id', 'dim_b', 'sum_b'] + }, + { + join_path: 'A.C', + includes: ['c_id', 'dim_c', 'sum_c'] + }, + ] + }); + `); it('simple join', async () => { @@ -3322,6 +3492,165 @@ SELECT 1 AS revenue, cast('2024-01-01' AS timestamp) as time UNION ALL ] )); + it('multiplied measures missing column grouped', async () => runQueryTest( + { + measures: [ + { + // eslint-disable-next-line no-new-func + expression: new Function( + 'revenue_view', + // eslint-disable-next-line no-template-curly-in-string + 'return `${revenue_view.sum_total}`' + ), + name: 'revenue_total', + expressionName: 'revenue_total', + // eslint-disable-next-line no-template-curly-in-string + definition: '${revenue_view.sum_total}', + cubeName: 'revenue_view', + // cubeName: 'orders', + }, + { + // eslint-disable-next-line no-new-func + expression: new Function( + 'revenue_view', + // eslint-disable-next-line no-template-curly-in-string + 'return `COUNT(DISTINCT ${revenue_view.user_id})`' + ), + name: 'distinct_users', + expressionName: 'distinct_users', + // eslint-disable-next-line no-template-curly-in-string + definition: 'COUNT(DISTINCT ${revenue_view.user_id})', + cubeName: 'revenue_view', + // cubeName: 'orders', + }, + ], + dimensions: [ + { + // eslint-disable-next-line no-new-func + expression: new Function( + 'revenue_view', + // eslint-disable-next-line no-template-curly-in-string + 'return `${revenue_view.internal}`' + ), + name: 'internal_user', + expressionName: 'internal_user', + // eslint-disable-next-line no-template-curly-in-string + definition: '${revenue_view.internal}', + cubeName: 'revenue_view', + // cubeName: 'users', + }, + ], + order: [{ + 'revenue_view.internal': 'asc' + }] + }, + [ + { + distinct_users: '1', + internal_user: false, + revenue_total: '300', + }, + { + distinct_users: '1', + internal_user: true, + revenue_total: '500', + }, + ] + )); + + it('multiplied measures missing column ungrouped', async () => runQueryTest( + { + measures: [ + { + // eslint-disable-next-line no-new-func + expression: new Function( + 'revenue_view', + // eslint-disable-next-line no-template-curly-in-string + 'return `${revenue_view.sum_total}`' + ), + name: 'revenue_total', + expressionName: 'revenue_total', + // eslint-disable-next-line no-template-curly-in-string + definition: '${revenue_view.sum_total}', + cubeName: 'revenue_view', + // cubeName: 'orders', + }, + { + // eslint-disable-next-line no-new-func + expression: new Function( + 'revenue_view', + // eslint-disable-next-line no-template-curly-in-string + 'return `${revenue_view.user_id}`' + ), + name: 'distinct_users', + expressionName: 'distinct_users', + // eslint-disable-next-line no-template-curly-in-string + definition: '${revenue_view.user_id}', + cubeName: 'revenue_view', + // cubeName: 'orders', + }, + { + // eslint-disable-next-line no-new-func + expression: new Function( + 'revenue_view', + // eslint-disable-next-line no-template-curly-in-string + 'return `${revenue_view.internal}`' + ), + name: 'internal_user', + expressionName: 'internal_user', + // eslint-disable-next-line no-template-curly-in-string + definition: '${revenue_view.internal}', + cubeName: 'revenue_view', + // cubeName: 'users', + }, + ], + order: [{ + 'revenue_view.order_id': 'asc' + }], + ungrouped: true, + allowUngroupedWithoutPrimaryKey: true, + }, + [ + // { + // distinct_users: '1', + // internal_user: true, + // revenue_total: '500', + // }, + // { + // distinct_users: '1', + // internal_user: false, + // revenue_total: '300', + // }, + ] + )); + + it('abc', async () => runQueryTest( + { + measures: [ + // 'ABC_view.sum_a', + // 'ABC_view.sum_b', + // 'ABC_view.sum_c', + ], + dimensions: [ + 'ABC_view.a_id', + 'ABC_view.b_id', + 'ABC_view.c_id', + ], + }, + [ + // { + // distinct_users: '1', + // internal_user: true, + // revenue_total: '500', + // }, + // { + // distinct_users: '1', + // internal_user: false, + // revenue_total: '300', + // }, + ] + )); + // TODO not implemented // it('multi stage bucketing', async () => runQueryTest( // {