From f0a4c9addd8c4410aca26525dae3bed64aa0668a Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 25 Nov 2024 23:42:22 +0200 Subject: [PATCH 1/2] fix(schema-compiler): Fix incorrect sql generation for view queries referencing measures from joined cubes --- packages/cubejs-schema-compiler/src/adapter/BaseQuery.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js index 3c1bc87fa3f85..d12fe9be9e242 100644 --- a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js +++ b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js @@ -1521,13 +1521,13 @@ export class BaseQuery { this.queryCache ); if (m.expressionName && !collectedMeasures.length && !m.isMemberExpression) { - throw new UserError(`Subquery dimension ${m.expressionName} should reference at least one measure`); + throw new UserError(`Subquery measure ${m.expressionName} should reference at least one member`); } if (!collectedMeasures.length && m.isMemberExpression && m.query.allCubeNames.length > 1 && m.measureSql() === 'COUNT(*)') { const cubeName = m.expressionCubeName ? `\`${m.expressionCubeName}\` ` : ''; throw new UserError(`The query contains \`COUNT(*)\` expression but cube/view ${cubeName}is missing \`count\` measure`); } - return [m.measure, collectedMeasures]; + return [typeof m.measure === 'string' ? m.measure : `${m.measure.cubeName}.${m.measure.name}`, collectedMeasures]; })); } From 2f9d816b01bae522dc3c7714119c1841b99f0434 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Tue, 26 Nov 2024 12:47:20 +0200 Subject: [PATCH 2/2] add tests --- .../postgresql/schema/Orders.js | 95 +++++++++++++++++++ .../__snapshots__/smoke-cubesql.test.ts.snap | 11 +++ .../cubejs-testing/test/smoke-cubesql.test.ts | 28 ++++++ 3 files changed, 134 insertions(+) diff --git a/packages/cubejs-testing/birdbox-fixtures/postgresql/schema/Orders.js b/packages/cubejs-testing/birdbox-fixtures/postgresql/schema/Orders.js index 71d2dee68968a..f7a51d286e613 100644 --- a/packages/cubejs-testing/birdbox-fixtures/postgresql/schema/Orders.js +++ b/packages/cubejs-testing/birdbox-fixtures/postgresql/schema/Orders.js @@ -10,10 +10,40 @@ cube(`Orders`, { UNION ALL select 5 as id, 600 as amount, 'shipped' status, '2024-01-05'::timestamptz created_at `, + joins: { + OrderItems: { + relationship: 'one_to_many', + sql: `${CUBE.id} = ${OrderItems.order_id}` + }, + }, measures: { count: { type: `count`, }, + orderCount: { + type: `count_distinct`, + sql: `CASE WHEN ${Orders.status} = 'shipped' THEN ${CUBE}.id END` + }, + netCollectionCompleted: { + type: `sum`, + sql: `CASE WHEN ${Orders.status} = 'shipped' THEN ${CUBE}.amount END` + }, + arpu: { + type: `number`, + sql: `1.0 * ${netCollectionCompleted} / ${orderCount}` + }, + refundRate: { + type: `number`, + sql: `1.0 * ${refundOrdersCount} / ${overallOrders}` + }, + refundOrdersCount: { + type: `count_distinct`, + sql: `CASE WHEN ${Orders.status} = 'refunded' THEN ${CUBE}.id END` + }, + overallOrders: { + type: `count_distinct`, + sql: `CASE WHEN ${Orders.status} != 'cancelled' THEN ${CUBE}.id END` + }, totalAmount: { sql: `amount`, type: `sum`, @@ -96,6 +126,57 @@ cube(`Orders`, { }, }); +cube(`OrderItems`, { + sql: ` + select 1 as id, 1 as order_id, 'Phone' AS name, 'Electronics' AS type, '2024-01-01'::timestamptz created_at + UNION ALL + select 2 as id, 2 as order_id, 'Keyboard' AS name, 'Electronics' AS type, '2024-01-02'::timestamptz created_at + UNION ALL + select 3 as id, 3 as order_id, 'Glass' AS name, 'Home' AS type, '2024-01-03'::timestamptz created_at + UNION ALL + select 4 as id, 4 as order_id, 'Lamp' AS name, 'Home' AS type, '2024-01-04'::timestamptz created_at + UNION ALL + select 5 as id, 5 as order_id, 'Pen' AS name, 'Office' AS type, '2024-01-05'::timestamptz created_at + `, + measures: { + count: { + type: `count`, + }, + }, + dimensions: { + id: { + sql: `id`, + type: `number`, + primaryKey: true, + public: true, + }, + + order_id: { + sql: `order_id`, + type: `number`, + public: true, + }, + + name: { + sql: `name`, + type: `string`, + public: true, + }, + + type: { + sql: `type`, + type: `string`, + public: true, + }, + + createdAt: { + sql: `created_at`, + type: `time`, + public: true, + } + }, +}); + view(`OrdersView`, { cubes: [{ joinPath: Orders, @@ -103,3 +184,17 @@ view(`OrdersView`, { excludes: [`toRemove`] }] }); + +view(`OrdersItemsPrefixView`, { + cubes: [{ + joinPath: Orders, + includes: `*`, + excludes: [`toRemove`], + prefix: true + }, + { + joinPath: Orders.OrderItems, + includes: `*`, + prefix: true + }] +}); diff --git a/packages/cubejs-testing/test/__snapshots__/smoke-cubesql.test.ts.snap b/packages/cubejs-testing/test/__snapshots__/smoke-cubesql.test.ts.snap index 8cadb4f5f5d81..ae9fc2de07c05 100644 --- a/packages/cubejs-testing/test/__snapshots__/smoke-cubesql.test.ts.snap +++ b/packages/cubejs-testing/test/__snapshots__/smoke-cubesql.test.ts.snap @@ -111,6 +111,17 @@ Array [ ] `; +exports[`SQL API Postgres (Data) query views with deep joins: query-view-deep-joins 1`] = ` +Array [ + Object { + "Calculation_1055547778125863": 2024-01-01T00:00:00.000Z, + "Orders_arpu": null, + "Orders_netCollectionCompleted": null, + "Orders_refundRate": 0, + }, +] +`; + exports[`SQL API Postgres (Data) query with intervals (SQL PUSH DOWN): timestamps 1`] = ` Array [ Object { diff --git a/packages/cubejs-testing/test/smoke-cubesql.test.ts b/packages/cubejs-testing/test/smoke-cubesql.test.ts index 9d0eec2177bda..1e8e5583b4e28 100644 --- a/packages/cubejs-testing/test/smoke-cubesql.test.ts +++ b/packages/cubejs-testing/test/smoke-cubesql.test.ts @@ -471,5 +471,33 @@ describe('SQL API', () => { const res = await connection.query(query); expect(res.rows).toMatchSnapshot('timestamps'); }); + + test('query views with deep joins', async () => { + const query = ` + SELECT + CAST( + DATE_TRUNC( + 'MONTH', + CAST( + CAST("OrdersItemsPrefixView"."Orders_createdAt" AS DATE) AS TIMESTAMP + ) + ) AS DATE + ) AS "Calculation_1055547778125863", + SUM("OrdersItemsPrefixView"."Orders_arpu") AS "Orders_arpu", + SUM("OrdersItemsPrefixView"."Orders_refundRate") AS "Orders_refundRate", + SUM("OrdersItemsPrefixView"."Orders_netCollectionCompleted") AS "Orders_netCollectionCompleted" + FROM + OrdersItemsPrefixView + WHERE + OrdersItemsPrefixView.Orders_createdAt >= '2024-01-01T00:00:00.000' + AND OrdersItemsPrefixView.Orders_createdAt <= '2024-12-31T23:59:59.999' + AND (OrdersItemsPrefixView.Orders_status IN ('shipped', 'processed')) + AND (OrdersItemsPrefixView.OrderItems_type IN ('Electronics', 'Home')) + GROUP BY 1 + `; + + const res = await connection.query(query); + expect(res.rows).toMatchSnapshot('query-view-deep-joins'); + }); }); });