diff --git a/packages/cubejs-schema-compiler/src/adapter/OracleQuery.ts b/packages/cubejs-schema-compiler/src/adapter/OracleQuery.ts index d5e4871b88497..46fdfaa0e7ed7 100644 --- a/packages/cubejs-schema-compiler/src/adapter/OracleQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/OracleQuery.ts @@ -71,11 +71,14 @@ export class OracleQuery extends BaseQuery { } public dateTimeCast(value) { - return `to_date(:"${value}", 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`; + // Use timezone-aware parsing for ISO 8601 with milliseconds and trailing 'Z', then cast to DATE + // to preserve index-friendly comparisons against DATE columns. + return `CAST(TO_TIMESTAMP_TZ(:"${value}", 'YYYY-MM-DD"T"HH24:MI:SS.FF"Z"') AS DATE)`; } public timeStampCast(value) { - return this.dateTimeCast(value); + // Return timezone-aware timestamp for TIMESTAMP comparisons + return `TO_TIMESTAMP_TZ(:"${value}", 'YYYY-MM-DD"T"HH24:MI:SS.FF"Z"')`; } public timeStampParam(timeDimension) { diff --git a/packages/cubejs-schema-compiler/test/unit/oracle-query.test.ts b/packages/cubejs-schema-compiler/test/unit/oracle-query.test.ts new file mode 100644 index 0000000000000..c3680d796f384 --- /dev/null +++ b/packages/cubejs-schema-compiler/test/unit/oracle-query.test.ts @@ -0,0 +1,111 @@ +import { OracleQuery } from '../../src/adapter/OracleQuery'; +import { prepareJsCompiler } from './PrepareCompiler'; + +describe('OracleQuery', () => { + const { compiler, joinGraph, cubeEvaluator } = prepareJsCompiler(` + cube(\`visitors\`, { + sql: \` + select * from visitors + \`, + + measures: { + count: { + type: 'count' + } + }, + + dimensions: { + id: { + sql: 'id', + type: 'number', + primaryKey: true + }, + createdAt: { + type: 'time', + sql: 'created_at' + } + } + }) + `, { adapter: 'oracle' }); + + it('generates TO_TIMESTAMP_TZ with millisecond precision for date range filters', async () => { + await compiler.compile(); + + const query = new OracleQuery( + { joinGraph, cubeEvaluator, compiler }, + { + measures: ['visitors.count'], + timeDimensions: [ + { + dimension: 'visitors.createdAt', + dateRange: ['2024-02-01', '2024-02-02'], + granularity: 'day' + } + ], + timezone: 'UTC' + } + ); + + const [sql, params] = query.buildSqlAndParams(); + + // Verify TO_TIMESTAMP_TZ is used with proper ISO 8601 format including milliseconds + expect(sql).toContain('TO_TIMESTAMP_TZ(:"?", \'YYYY-MM-DD"T"HH24:MI:SS.FF"Z"\')'); + expect(sql).toMatch(/created_at\s+>=\s+TO_TIMESTAMP_TZ/); + expect(sql).toMatch(/created_at\s+<=\s+TO_TIMESTAMP_TZ/); + + // Verify parameters include millisecond precision + expect(params).toEqual(['2024-02-01T00:00:00.000Z', '2024-02-02T23:59:59.999Z']); + }); + + it('generates TRUNC function for day granularity grouping', async () => { + await compiler.compile(); + + const query = new OracleQuery( + { joinGraph, cubeEvaluator, compiler }, + { + measures: ['visitors.count'], + timeDimensions: [ + { + dimension: 'visitors.createdAt', + dateRange: ['2024-01-01', '2024-01-31'], + granularity: 'day' + } + ], + timezone: 'UTC' + } + ); + + const [sql, params] = query.buildSqlAndParams(); + + // Verify TRUNC with DD format for day grouping + expect(sql).toContain('TRUNC("visitors".created_at, \'DD\')'); + expect(sql).toMatch(/GROUP BY\s+TRUNC/); + expect(params).toEqual(['2024-01-01T00:00:00.000Z', '2024-01-31T23:59:59.999Z']); + }); + + it('generates TRUNC function for month granularity grouping', async () => { + await compiler.compile(); + + const query = new OracleQuery( + { joinGraph, cubeEvaluator, compiler }, + { + measures: ['visitors.count'], + timeDimensions: [ + { + dimension: 'visitors.createdAt', + dateRange: ['2024-01-01', '2024-12-31'], + granularity: 'month' + } + ], + timezone: 'UTC' + } + ); + + const [sql, params] = query.buildSqlAndParams(); + + // Verify TRUNC with MM format for month grouping + expect(sql).toContain('TRUNC("visitors".created_at, \'MM\')'); + expect(sql).toMatch(/GROUP BY\s+TRUNC/); + expect(params).toEqual(['2024-01-01T00:00:00.000Z', '2024-12-31T23:59:59.999Z']); + }); +});