diff --git a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js index f11bc37095114..dc266290b3ec7 100644 --- a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js +++ b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js @@ -1413,18 +1413,59 @@ export class BaseQuery { overTimeSeriesQuery(baseQueryFn, cumulativeMeasure, fromRollup) { const dateJoinCondition = cumulativeMeasure.dateJoinCondition(); + const uniqDateJoinCondition = R.uniqBy(djc => djc[0].dimension, dateJoinCondition); const cumulativeMeasures = [cumulativeMeasure]; if (!this.timeDimensions.find(d => d.granularity)) { - const filters = this.segments.concat(this.filters).concat(this.dateFromStartToEndConditionSql(dateJoinCondition, fromRollup, false)); + const filters = this.segments + .concat(this.filters) + .concat(this.dateFromStartToEndConditionSql( + // If the same time dimension is passed more than once, no need to build the same + // filter condition again and again. Different granularities don't play role here, + // as rollingWindow.granularity is used for filtering. + uniqDateJoinCondition, + fromRollup, + false + )); return baseQueryFn(cumulativeMeasures, filters, false); } - const dateSeriesSql = this.timeDimensions.map(d => this.dateSeriesSql(d)).join(', '); - const filters = this.segments.concat(this.filters).concat(this.dateFromStartToEndConditionSql(dateJoinCondition, fromRollup, true)); + + // We can't do meaningful query if few time dimensions with different ranges passed, + // it won't be possible to join them together without loosing some rows. + const rangedTimeDimensions = this.timeDimensions.filter(d => d.dateRange && d.granularity); + const uniqTimeDimensionWithRanges = R.uniqBy(d => d.dateRange, rangedTimeDimensions); + if (uniqTimeDimensionWithRanges.length > 1) { + throw new Error('Can\'t build query for time dimensions with different date ranges'); + } + + // We need to generate time series table for the lowest granularity among all time dimensions + let dateSeriesDimension; + const dateSeriesGranularity = this.timeDimensions.filter(d => d.granularity) + .reduce((acc, d) => { + const mg = this.minGranularity(acc, d.resolvedGranularity()); + if (mg === d.resolvedGranularity()) { + dateSeriesDimension = d; + } + return mg; + }, undefined); + + const dateSeriesSql = this.dateSeriesSql(dateSeriesDimension); + + // If the same time dimension is passed more than once, no need to build the same + // filter condition again and again. Different granularities don't play role here, + // as rollingWindow.granularity is used for filtering. + const filters = this.segments + .concat(this.filters) + .concat(this.dateFromStartToEndConditionSql( + uniqDateJoinCondition, + fromRollup, + true + )); const baseQuery = this.groupedUngroupedSelect( () => baseQueryFn(cumulativeMeasures, filters), cumulativeMeasure.shouldUngroupForCumulative(), !cumulativeMeasure.shouldUngroupForCumulative() && this.minGranularity( - cumulativeMeasure.windowGranularity(), this.timeDimensions.find(d => d.granularity).resolvedGranularity() + cumulativeMeasure.windowGranularity(), + dateSeriesGranularity ) || undefined ); const baseQueryAlias = this.cubeAlias('base'); @@ -1444,28 +1485,27 @@ export class BaseQuery { dateSeriesSql, baseQuery, dateJoinConditionSql, - baseQueryAlias + baseQueryAlias, + dateSeriesDimension.granularity, ); } - overTimeSeriesSelect(cumulativeMeasures, dateSeriesSql, baseQuery, dateJoinConditionSql, baseQueryAlias) { - const forSelect = this.overTimeSeriesForSelect(cumulativeMeasures); + overTimeSeriesSelect(cumulativeMeasures, dateSeriesSql, baseQuery, dateJoinConditionSql, baseQueryAlias, dateSeriesGranularity) { + const forSelect = this.overTimeSeriesForSelect(cumulativeMeasures, dateSeriesGranularity); return `SELECT ${forSelect} FROM ${dateSeriesSql}` + ` LEFT JOIN (${baseQuery}) ${this.asSyntaxJoin} ${baseQueryAlias} ON ${dateJoinConditionSql}` + this.groupByClause(); } - overTimeSeriesForSelect(cumulativeMeasures) { - return this.dimensions.map(s => s.cumulativeSelectColumns()).concat(this.dateSeriesSelect()).concat( - cumulativeMeasures.map(s => s.cumulativeSelectColumns()), - ).filter(c => !!c) + overTimeSeriesForSelect(cumulativeMeasures, dateSeriesGranularity) { + return this.dimensions + .map(s => s.cumulativeSelectColumns()) + .concat(this.timeDimensions.map(d => d.dateSeriesSelectColumn(null, dateSeriesGranularity))) + .concat(cumulativeMeasures.map(s => s.cumulativeSelectColumns())) + .filter(c => !!c) .join(', '); } - dateSeriesSelect() { - return this.timeDimensions.map(d => d.dateSeriesSelectColumn()); - } - dateFromStartToEndConditionSql(dateJoinCondition, fromRollup, isFromStartToEnd) { return dateJoinCondition.map( // TODO these weird conversions to be strict typed for big query. @@ -1646,7 +1686,8 @@ export class BaseQuery { /** * - * @param {{sql: string, on: {cubeName: string, expression: Function}, joinType: 'LEFT' | 'INNER', alias: string}} customJoin + * @param {{sql: string, on: {cubeName: string, expression: Function}, joinType: 'LEFT' | 'INNER', alias: string}} + * customJoin * @returns {JoinItem} */ customSubQueryJoin(customJoin) { diff --git a/packages/cubejs-schema-compiler/src/adapter/BaseTimeDimension.ts b/packages/cubejs-schema-compiler/src/adapter/BaseTimeDimension.ts index c5305250c0fa7..cab052d278528 100644 --- a/packages/cubejs-schema-compiler/src/adapter/BaseTimeDimension.ts +++ b/packages/cubejs-schema-compiler/src/adapter/BaseTimeDimension.ts @@ -85,10 +85,18 @@ export class BaseTimeDimension extends BaseFilter { return this.query.escapeColumnName(`${this.dimension}_series`); } - public dateSeriesSelectColumn(dateSeriesAliasName) { + public dateSeriesSelectColumn(dateSeriesAliasName: string, dateSeriesGranularity: string) { if (!this.granularityObj) { return null; } + + // In case of query with more than one granularity, the time series table was generated + // with the minimal granularity among all. If this is our granularity, we can save + // some cpu cycles without 'date_from' truncation. But if this is not our granularity, + // we need to truncate it to desired. + if (dateSeriesGranularity && this.granularityObj?.granularity !== dateSeriesGranularity) { + return `${this.query.dimensionTimeGroupedColumn(`${dateSeriesAliasName || this.dateSeriesAliasName()}.${this.query.escapeColumnName('date_from')}`, this.granularityObj)} ${this.aliasName()}`; + } return `${dateSeriesAliasName || this.dateSeriesAliasName()}.${this.query.escapeColumnName('date_from')} ${this.aliasName()}`; }