diff --git a/packages/cubejs-schema-compiler/src/adapter/PostgresQuery.ts b/packages/cubejs-schema-compiler/src/adapter/PostgresQuery.ts index f8b91ffccdb67..f6eea59be725d 100644 --- a/packages/cubejs-schema-compiler/src/adapter/PostgresQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/PostgresQuery.ts @@ -1,3 +1,4 @@ +import { parseSqlInterval } from '@cubejs-backend/shared'; import { BaseQuery } from './BaseQuery'; import { ParamAllocator } from './ParamAllocator'; @@ -38,13 +39,26 @@ export class PostgresQuery extends BaseQuery { * This implementation should also work for AWS RedShift. */ public dateBin(interval: string, source: string, origin: string): string { - return `('${origin}'::timestamp + INTERVAL '${interval}' * + const intervalStr = this.intervalString(interval); + return `('${origin}'::timestamp + INTERVAL ${intervalStr} * FLOOR( EXTRACT(EPOCH FROM (${source} - '${origin}'::timestamp)) / - EXTRACT(EPOCH FROM INTERVAL '${interval}') + EXTRACT(EPOCH FROM INTERVAL ${intervalStr}) ))`; } + public override intervalString(interval: string): string { + const parsed = parseSqlInterval(interval); + if (parsed.quarter) { + parsed.month = (parsed.month || 0) + parsed.quarter * 3; + delete parsed.quarter; + } + const normalized = Object.entries(parsed) + .map(([unit, value]) => `${value} ${unit}${value !== 1 ? 's' : ''}`) + .join(' '); + return `'${normalized}'`; + } + public hllInit(sql) { return `hll_add_agg(hll_hash_any(${sql}))`; } 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 97854c4834c38..dbda56eafe09b 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 @@ -109,6 +109,14 @@ describe('SQL Generation', () => { offset: 'start' } }, + revenueRollingQuarter: { + type: 'sum', + sql: 'amount', + rollingWindow: { + trailing: '2 quarters', + offset: 'start' + } + }, revenueRollingThreeDay: { type: 'sum', sql: 'amount', @@ -1143,6 +1151,48 @@ SELECT 1 AS revenue, cast('2024-01-01' AS timestamp) as time UNION ALL { visitors__created_at_day: '2017-01-10T00:00:00.000Z', visitors__revenue_rolling: null } ])); + it('rolling quarter', async () => runQueryTest({ + measures: [ + 'visitors.revenueRolling' + ], + timeDimensions: [{ + dimension: 'visitors.created_at', + granularity: 'quarter', + dateRange: ['2016-01-01', '2017-01-10'] + }], + order: [{ + id: 'visitors.created_at' + }], + timezone: 'America/Los_Angeles' + }, [ + { visitors__created_at_quarter: '2016-01-01T00:00:00.000Z', visitors__revenue_rolling: null }, + { visitors__created_at_quarter: '2016-04-01T00:00:00.000Z', visitors__revenue_rolling: null }, + { visitors__created_at_quarter: '2016-07-01T00:00:00.000Z', visitors__revenue_rolling: null }, + { visitors__created_at_quarter: '2016-10-01T00:00:00.000Z', visitors__revenue_rolling: null }, + { visitors__created_at_quarter: '2017-01-01T00:00:00.000Z', visitors__revenue_rolling: null } + ])); + + it('rolling over 2 quarters', async () => runQueryTest({ + measures: [ + 'visitors.revenueRollingQuarter' + ], + timeDimensions: [{ + dimension: 'visitors.created_at', + granularity: 'quarter', + dateRange: ['2016-01-01', '2017-01-10'] + }], + order: [{ + id: 'visitors.created_at' + }], + timezone: 'America/Los_Angeles' + }, [ + { visitors__created_at_quarter: '2016-01-01T00:00:00.000Z', visitors__revenue_rolling_quarter: null }, + { visitors__created_at_quarter: '2016-04-01T00:00:00.000Z', visitors__revenue_rolling_quarter: null }, + { visitors__created_at_quarter: '2016-07-01T00:00:00.000Z', visitors__revenue_rolling_quarter: null }, + { visitors__created_at_quarter: '2016-10-01T00:00:00.000Z', visitors__revenue_rolling_quarter: '500' }, + { visitors__created_at_quarter: '2017-01-01T00:00:00.000Z', visitors__revenue_rolling_quarter: '500' } + ])); + if (getEnv('nativeSqlPlanner')) { it('rolling day ago', async () => runQueryTest({ measures: [ diff --git a/packages/cubejs-schema-compiler/test/unit/base-query.test.ts b/packages/cubejs-schema-compiler/test/unit/base-query.test.ts index 5aacf5344dcff..125de7ab3b016 100644 --- a/packages/cubejs-schema-compiler/test/unit/base-query.test.ts +++ b/packages/cubejs-schema-compiler/test/unit/base-query.test.ts @@ -880,9 +880,9 @@ describe('SQL Generation', () => { if (q.measures[0].includes('count')) { expect(queryString.includes('INTERVAL \'6 months\'')).toBeTruthy(); } else if (q.measures[0].includes('rollingCountByTrailing2Day')) { - expect(queryString.includes('- interval \'2 day\'')).toBeTruthy(); + expect(queryString.includes('- interval \'2 days\'')).toBeTruthy(); } else if (q.measures[0].includes('rollingCountByLeading2Day')) { - expect(queryString.includes('+ interval \'3 day\'')).toBeTruthy(); + expect(queryString.includes('+ interval \'3 days\'')).toBeTruthy(); } }); });