Skip to content

Commit 076bc9f

Browse files
committed
fix(oracle): add support for time filters and rolling windows
- Fix AS keyword in subquery aliases (Oracle doesn't support it) - Handle time dimensions without granularity to prevent TypeError - Implement Oracle-specific interval arithmetic using ADD_MONTHS and NUMTODSINTERVAL - Add comprehensive test suite for Oracle query generation These changes enable Oracle users to execute queries with: - Time dimension filters without granularity specification - Rolling windows and time-based calculations - Multiple subqueries with proper aliasing All changes maintain backward compatibility with other database adapters.
1 parent ea3ba21 commit 076bc9f

File tree

3 files changed

+135
-1
lines changed

3 files changed

+135
-1
lines changed

packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3857,6 +3857,11 @@ export class BaseQuery {
38573857
* @return {string}
38583858
*/
38593859
dimensionTimeGroupedColumn(dimension, granularity) {
3860+
// Handle case when granularity is not specified (e.g., time dimension used only for filtering)
3861+
if (!granularity) {
3862+
return this.timeGroupedColumn(null, dimension);
3863+
}
3864+
38603865
let dtDate;
38613866

38623867
// Interval is aligned with natural calendar, so we can use DATE_TRUNC

packages/cubejs-schema-compiler/src/adapter/OracleQuery.ts

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { parseSqlInterval } from '@cubejs-backend/shared';
12
import { BaseQuery } from './BaseQuery';
23
import { BaseFilter } from './BaseFilter';
34
import { UserError } from '../compiler/UserError';
4-
import { BaseDimension } from './BaseDimension';
5+
import type { BaseDimension } from './BaseDimension';
56

67
const GRANULARITY_VALUE = {
78
day: 'DD',
@@ -89,6 +90,88 @@ export class OracleQuery extends BaseQuery {
8990
return `TRUNC(${dimension}, '${GRANULARITY_VALUE[granularity]}')`;
9091
}
9192

93+
/**
94+
* Oracle uses ADD_MONTHS for year/month/quarter intervals
95+
* and NUMTODSINTERVAL for day/hour/minute/second intervals
96+
*/
97+
public addInterval(date: string, interval: string): string {
98+
const intervalParsed = parseSqlInterval(interval);
99+
let res = date;
100+
101+
// Handle year/month/quarter using ADD_MONTHS
102+
let totalMonths = 0;
103+
if (intervalParsed.year) {
104+
totalMonths += intervalParsed.year * 12;
105+
}
106+
if (intervalParsed.quarter) {
107+
totalMonths += intervalParsed.quarter * 3;
108+
}
109+
if (intervalParsed.month) {
110+
totalMonths += intervalParsed.month;
111+
}
112+
113+
if (totalMonths !== 0) {
114+
res = `ADD_MONTHS(${res}, ${totalMonths})`;
115+
}
116+
117+
// Handle day/hour/minute/second using NUMTODSINTERVAL
118+
if (intervalParsed.day) {
119+
res = `${res} + NUMTODSINTERVAL(${intervalParsed.day}, 'DAY')`;
120+
}
121+
if (intervalParsed.hour) {
122+
res = `${res} + NUMTODSINTERVAL(${intervalParsed.hour}, 'HOUR')`;
123+
}
124+
if (intervalParsed.minute) {
125+
res = `${res} + NUMTODSINTERVAL(${intervalParsed.minute}, 'MINUTE')`;
126+
}
127+
if (intervalParsed.second) {
128+
res = `${res} + NUMTODSINTERVAL(${intervalParsed.second}, 'SECOND')`;
129+
}
130+
131+
return res;
132+
}
133+
134+
/**
135+
* Oracle subtraction uses ADD_MONTHS with negative values
136+
* and subtracts NUMTODSINTERVAL for time units
137+
*/
138+
public subtractInterval(date: string, interval: string): string {
139+
const intervalParsed = parseSqlInterval(interval);
140+
let res = date;
141+
142+
// Handle year/month/quarter using ADD_MONTHS with negative values
143+
let totalMonths = 0;
144+
if (intervalParsed.year) {
145+
totalMonths += intervalParsed.year * 12;
146+
}
147+
if (intervalParsed.quarter) {
148+
totalMonths += intervalParsed.quarter * 3;
149+
}
150+
if (intervalParsed.month) {
151+
totalMonths += intervalParsed.month;
152+
}
153+
154+
if (totalMonths !== 0) {
155+
res = `ADD_MONTHS(${res}, -${totalMonths})`;
156+
}
157+
158+
// Handle day/hour/minute/second using NUMTODSINTERVAL with subtraction
159+
if (intervalParsed.day) {
160+
res = `${res} - NUMTODSINTERVAL(${intervalParsed.day}, 'DAY')`;
161+
}
162+
if (intervalParsed.hour) {
163+
res = `${res} - NUMTODSINTERVAL(${intervalParsed.hour}, 'HOUR')`;
164+
}
165+
if (intervalParsed.minute) {
166+
res = `${res} - NUMTODSINTERVAL(${intervalParsed.minute}, 'MINUTE')`;
167+
}
168+
if (intervalParsed.second) {
169+
res = `${res} - NUMTODSINTERVAL(${intervalParsed.second}, 'SECOND')`;
170+
}
171+
172+
return res;
173+
}
174+
92175
public newFilter(filter) {
93176
return new OracleFilter(this, filter);
94177
}

packages/cubejs-schema-compiler/test/unit/oracle-query.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,4 +305,50 @@ describe('OracleQuery', () => {
305305
expect(sql).toMatch(/GROUP BY.*"visitors"\.source/i);
306306
expect(sql).not.toMatch(/GROUP BY\s+\d+/);
307307
});
308+
309+
it('handles time dimension without granularity in filter', async () => {
310+
await compiler.compile();
311+
312+
const query = new OracleQuery({ joinGraph, cubeEvaluator, compiler }, {
313+
measures: [
314+
'visitors.count'
315+
],
316+
timeDimensions: [{
317+
dimension: 'visitors.createdAt',
318+
dateRange: ['2020-01-01', '2020-12-31']
319+
// No granularity specified - used only for filtering
320+
}],
321+
timezone: 'UTC'
322+
});
323+
324+
const queryAndParams = query.buildSqlAndParams();
325+
const sql = queryAndParams[0];
326+
327+
// Key test: no GROUP BY on time dimension when granularity is missing
328+
expect(sql).not.toMatch(/GROUP BY.*created_at/i);
329+
});
330+
331+
it('uses Oracle-specific interval arithmetic', async () => {
332+
await compiler.compile();
333+
334+
const query = new OracleQuery({ joinGraph, cubeEvaluator, compiler }, {
335+
measures: [
336+
'visitors.thisPeriod',
337+
'visitors.priorPeriod'
338+
],
339+
timeDimensions: [{
340+
dimension: 'visitors.createdAt',
341+
granularity: 'year',
342+
dateRange: ['2020-01-01', '2022-12-31']
343+
}],
344+
timezone: 'UTC'
345+
});
346+
347+
const queryAndParams = query.buildSqlAndParams();
348+
const sql = queryAndParams[0];
349+
350+
// Key test: Oracle uses ADD_MONTHS, not PostgreSQL interval syntax
351+
expect(sql).toMatch(/ADD_MONTHS/i);
352+
expect(sql).not.toMatch(/interval '1 year'/i);
353+
});
308354
});

0 commit comments

Comments
 (0)