diff --git a/packages/cubejs-backend-shared/src/time.ts b/packages/cubejs-backend-shared/src/time.ts index d21bc4e8cbdd7..1bd7867f3851b 100644 --- a/packages/cubejs-backend-shared/src/time.ts +++ b/packages/cubejs-backend-shared/src/time.ts @@ -104,7 +104,7 @@ export function parseSqlInterval(intervalStr: SqlInterval): ParsedInterval { for (let i = 0; i < parts.length; i += 2) { const value = parseInt(parts[i], 10); - const unit = parts[i + 1]; + const unit = parts[i + 1].toLowerCase(); // Remove ending 's' (e.g., 'days' -> 'day') const singularUnit = (unit.endsWith('s') ? unit.slice(0, -1) : unit) as unitOfTime.DurationConstructor; diff --git a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js index 72c6ad031d46e..de3278a966ada 100644 --- a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js +++ b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js @@ -1117,6 +1117,11 @@ export class BaseQuery { return `${date} + interval ${intervalStr}`; } + // For use in Tesseract + supportGeneratedSeriesForCustomTd() { + return false; + } + /** * @param {string} interval * @returns {string} @@ -2048,7 +2053,6 @@ export class BaseQuery { * Returns a tuple: (formatted interval, minimal time unit) */ intervalAndMinimalTimeUnit(interval) { - const intervalParsed = parseSqlInterval(interval); const minGranularity = this.diffTimeUnitForInterval(interval); return [interval, minGranularity]; } @@ -4138,7 +4142,6 @@ export class BaseQuery { sort: '{{ expr }} {% if asc %}ASC{% else %}DESC{% endif %} NULLS {% if nulls_first %}FIRST{% else %}LAST{% endif %}', order_by: '{% if index %} {{ index }} {% else %} {{ expr }} {% endif %} {% if asc %}ASC{% else %}DESC{% endif %}{% if nulls_first %} NULLS FIRST{% endif %}', cast: 'CAST({{ expr }} AS {{ data_type }})', - cast_to_string: 'CAST({{ expr }} AS TEXT)', window_function: '{{ fun_call }} OVER ({% if partition_by_concat %}PARTITION BY {{ partition_by_concat }}{% if order_by_concat or window_frame %} {% endif %}{% endif %}{% if order_by_concat %}ORDER BY {{ order_by_concat }}{% if window_frame %} {% endif %}{% endif %}{% if window_frame %}{{ window_frame }}{% endif %})', window_frame_bounds: '{{ frame_type }} BETWEEN {{ frame_start }} AND {{ frame_end }}', in_list: '{{ expr }} {% if negated %}NOT {% endif %}IN ({{ in_exprs_concat }})', diff --git a/packages/cubejs-schema-compiler/src/adapter/BigqueryQuery.ts b/packages/cubejs-schema-compiler/src/adapter/BigqueryQuery.ts index bd615ee791ca5..1d4e50649e41b 100644 --- a/packages/cubejs-schema-compiler/src/adapter/BigqueryQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/BigqueryQuery.ts @@ -127,10 +127,10 @@ export class BigqueryQuery extends BaseQuery { return [`'${intervalParsed.hour}:${intervalParsed.minute}:${intervalParsed.second}' HOUR TO SECOND`, 'SECOND']; } else if (intervalParsed.minute && intervalParsed.second && intKeys === 2) { return [`'${intervalParsed.minute}:${intervalParsed.second}' MINUTE TO SECOND`, 'SECOND']; + } else if (intervalParsed.millisecond && intKeys === 1) { + return [`'${intervalParsed.millisecond}' MILLISECOND`, 'MILLISECOND']; } - // No need to support microseconds. - throw new Error(`Cannot transform interval expression "${interval}" to BigQuery dialect`); } @@ -367,7 +367,6 @@ export class BigqueryQuery extends BaseQuery { templates.types.double = 'FLOAT64'; templates.types.decimal = 'BIGDECIMAL({{ precision }},{{ scale }})'; templates.types.binary = 'BYTES'; - templates.expressions.cast_to_string = 'CAST({{ expr }} AS STRING)'; templates.operators.is_not_distinct_from = 'IS NOT DISTINCT FROM'; templates.join_types.full = 'FULL'; templates.statements.time_series_select = 'SELECT DATETIME(TIMESTAMP(f)) date_from, DATETIME(TIMESTAMP(t)) date_to \n' + diff --git a/packages/cubejs-schema-compiler/src/adapter/PostgresQuery.ts b/packages/cubejs-schema-compiler/src/adapter/PostgresQuery.ts index 58cf3c240b440..f8b91ffccdb67 100644 --- a/packages/cubejs-schema-compiler/src/adapter/PostgresQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/PostgresQuery.ts @@ -57,6 +57,10 @@ export class PostgresQuery extends BaseQuery { return `round(hll_cardinality(hll_add_agg(hll_hash_any(${sql}))))`; } + public supportGeneratedSeriesForCustomTd() { + return true; + } + public sqlTemplates() { const templates = super.sqlTemplates(); // eslint-disable-next-line no-template-curly-in-string @@ -82,8 +86,8 @@ export class PostgresQuery extends BaseQuery { templates.types.double = 'DOUBLE PRECISION'; templates.types.binary = 'BYTEA'; templates.operators.is_not_distinct_from = 'IS NOT DISTINCT FROM'; - templates.statements.generated_time_series_select = 'SELECT d AS "date_from",\n' + - 'd + interval {{ granularity }} - interval \'1 millisecond\' AS "date_to" \n' + + templates.statements.generated_time_series_select = 'SELECT {{ date_from }} AS "date_from",\n' + + '{{ date_to }} AS "date_to" \n' + 'FROM generate_series({{ start }}::timestamp, {{ end }}:: timestamp, {{ granularity }}::interval) d '; templates.statements.generated_time_series_with_cte_range_source = 'SELECT d AS "date_from",\n' + 'd + interval {{ granularity }} - interval \'1 millisecond\' AS "date_to" \n' + diff --git a/packages/cubejs-schema-compiler/src/adapter/PrestodbQuery.ts b/packages/cubejs-schema-compiler/src/adapter/PrestodbQuery.ts index b04e5e55cb783..962f175c98c52 100644 --- a/packages/cubejs-schema-compiler/src/adapter/PrestodbQuery.ts +++ b/packages/cubejs-schema-compiler/src/adapter/PrestodbQuery.ts @@ -122,6 +122,10 @@ export class PrestodbQuery extends BaseQuery { return `approx_distinct(${sql})`; } + public supportGeneratedSeriesForCustomTd() { + return true; + } + protected limitOffsetClause(limit, offset) { const limitClause = limit != null ? ` LIMIT ${limit}` : ''; const offsetClause = offset != null ? ` OFFSET ${offset}` : ''; diff --git a/packages/cubejs-testing-drivers/fixtures/athena.json b/packages/cubejs-testing-drivers/fixtures/athena.json index 11ed669c88937..9b30cfacff973 100644 --- a/packages/cubejs-testing-drivers/fixtures/athena.json +++ b/packages/cubejs-testing-drivers/fixtures/athena.json @@ -149,6 +149,7 @@ "querying BigECommerce: rolling window YTD without date range", "week granularity is not supported for intervals", "querying BigECommerce: rolling window by 2 week", + "querying custom granularities ECommerce: count by two_mo_by_feb + no dimension + rollingCountByLeading without date range", "---------------------------------------", "Custom Granularities ", @@ -185,9 +186,6 @@ "querying custom granularities ECommerce: count by three_months_by_march + dimension", "querying custom granularities (with preaggregation) ECommerce: totalQuantity by half_year + no dimension", "querying custom granularities (with preaggregation) ECommerce: totalQuantity by half_year + dimension", - "querying custom granularities ECommerce: count by two_mo_by_feb + no dimension + rollingCountByUnbounded", - "querying custom granularities ECommerce: count by two_mo_by_feb + no dimension + rollingCountByTrailing", - "querying custom granularities ECommerce: count by two_mo_by_feb + no dimension + rollingCountByLeading", "pre-aggregations Customers: running total without time dimension", "querying BigECommerce: totalProfitYearAgo", "SQL API: post-aggregate percentage of total", diff --git a/packages/cubejs-testing-drivers/fixtures/bigquery.json b/packages/cubejs-testing-drivers/fixtures/bigquery.json index 9624a5c1fec98..0defe94516aa8 100644 --- a/packages/cubejs-testing-drivers/fixtures/bigquery.json +++ b/packages/cubejs-testing-drivers/fixtures/bigquery.json @@ -163,6 +163,7 @@ "querying BigECommerce: rolling window by 2 day without date range", "querying BigECommerce: rolling window by 2 month without date range", "querying BigECommerce: rolling window YTD without date range", + "querying custom granularities ECommerce: count by two_mo_by_feb + no dimension + rollingCountByLeading without date range", "---------------------------------------", "SKIPPED SQL API (Need work)", @@ -188,6 +189,7 @@ "querying ECommerce: total quantity, avg discount, total sales, total profit by product + order + total -- rounding in athena", "querying ECommerce: total sales, total profit by month + order (date) + total -- doesn't work with the BigQuery", "querying ECommerce: total quantity, avg discount, total sales, total profit by product + order + total -- noisy test", + "querying custom granularities ECommerce: count by two_mo_by_feb + no dimension + rollingCountByLeading without date range", "querying BigECommerce: null sum", "querying BigECommerce: null boolean", "querying BigECommerce: rolling window by 2 day without date range", diff --git a/packages/cubejs-testing-drivers/fixtures/clickhouse.json b/packages/cubejs-testing-drivers/fixtures/clickhouse.json index 292008f58f6a7..8ce44e3bf905e 100644 --- a/packages/cubejs-testing-drivers/fixtures/clickhouse.json +++ b/packages/cubejs-testing-drivers/fixtures/clickhouse.json @@ -200,6 +200,7 @@ "querying BigECommerce: rolling window by 2 day without date range", "querying BigECommerce: rolling window by 2 month without date range", "querying BigECommerce: rolling window YTD without date range", + "querying custom granularities ECommerce: count by two_mo_by_feb + no dimension + rollingCountByLeading without date range", "---------------------------------------", "Custom Granularities ", diff --git a/packages/cubejs-testing-drivers/fixtures/databricks-jdbc.json b/packages/cubejs-testing-drivers/fixtures/databricks-jdbc.json index 75c855a0a9063..13a0605c5d852 100644 --- a/packages/cubejs-testing-drivers/fixtures/databricks-jdbc.json +++ b/packages/cubejs-testing-drivers/fixtures/databricks-jdbc.json @@ -216,6 +216,7 @@ "querying BigECommerce: rolling window by 2 day without date range", "querying BigECommerce: rolling window by 2 month without date range", "querying BigECommerce: rolling window YTD without date range", + "querying custom granularities ECommerce: count by two_mo_by_feb + no dimension + rollingCountByLeading without date range", "---------------------------------------", "Custom Granularities ", diff --git a/packages/cubejs-testing-drivers/fixtures/mssql.json b/packages/cubejs-testing-drivers/fixtures/mssql.json index b4e5b3170787e..c2fef8fcb97d4 100644 --- a/packages/cubejs-testing-drivers/fixtures/mssql.json +++ b/packages/cubejs-testing-drivers/fixtures/mssql.json @@ -142,6 +142,7 @@ "querying BigECommerce: rolling window by 2 day without date range", "querying BigECommerce: rolling window by 2 month without date range", "querying BigECommerce: rolling window YTD without date range", + "querying custom granularities ECommerce: count by two_mo_by_feb + no dimension + rollingCountByLeading without date range", "---------------------------------------", "SKIPPED SQL API (Need work)", diff --git a/packages/cubejs-testing-drivers/fixtures/mysql.json b/packages/cubejs-testing-drivers/fixtures/mysql.json index 70c4e89d130d8..413226f28f5ba 100644 --- a/packages/cubejs-testing-drivers/fixtures/mysql.json +++ b/packages/cubejs-testing-drivers/fixtures/mysql.json @@ -138,6 +138,7 @@ "querying BigECommerce: rolling window by 2 day without date range", "querying BigECommerce: rolling window by 2 month without date range", "querying BigECommerce: rolling window YTD without date range", + "querying custom granularities ECommerce: count by two_mo_by_feb + no dimension + rollingCountByLeading without date range", "---------------------------------------", "Custom Granularities ", diff --git a/packages/cubejs-testing-drivers/fixtures/postgres.json b/packages/cubejs-testing-drivers/fixtures/postgres.json index 68984c53407ca..068481dedbeba 100644 --- a/packages/cubejs-testing-drivers/fixtures/postgres.json +++ b/packages/cubejs-testing-drivers/fixtures/postgres.json @@ -162,7 +162,8 @@ "---------------------------------------", "querying BigECommerce: rolling window by 2 day without date range", "querying BigECommerce: rolling window by 2 month without date range", - "querying BigECommerce: rolling window YTD without date range" + "querying BigECommerce: rolling window YTD without date range", + "querying custom granularities ECommerce: count by two_mo_by_feb + no dimension + rollingCountByLeading without date range" ], "tesseractSkip": [ "querying Products: dimensions -- doesn't work wo ordering", diff --git a/packages/cubejs-testing-drivers/fixtures/redshift.json b/packages/cubejs-testing-drivers/fixtures/redshift.json index 2181ec35dccc5..4807fc88d567b 100644 --- a/packages/cubejs-testing-drivers/fixtures/redshift.json +++ b/packages/cubejs-testing-drivers/fixtures/redshift.json @@ -174,6 +174,7 @@ "querying BigECommerce: rolling window by 2 day without date range", "querying BigECommerce: rolling window by 2 month without date range", "querying BigECommerce: rolling window YTD without date range", + "querying custom granularities ECommerce: count by two_mo_by_feb + no dimension + rollingCountByLeading without date range", "---------------------------------------", "SKIPPED SQL API (Need work) ", diff --git a/packages/cubejs-testing-drivers/fixtures/snowflake.json b/packages/cubejs-testing-drivers/fixtures/snowflake.json index 3458b34fe334c..6e3d2dc65e2c7 100644 --- a/packages/cubejs-testing-drivers/fixtures/snowflake.json +++ b/packages/cubejs-testing-drivers/fixtures/snowflake.json @@ -232,6 +232,7 @@ "querying ECommerce: total quantity, avg discount, total sales, total profit by product + order + total -- noisy test", "querying BigECommerce: null sum", "querying BigECommerce: null boolean", + "querying custom granularities ECommerce: count by two_mo_by_feb + no dimension + rollingCountByLeading without date range", "---------------------------------------", "Requires Tesseract. ", diff --git a/packages/cubejs-testing-drivers/src/tests/testQueries.ts b/packages/cubejs-testing-drivers/src/tests/testQueries.ts index 65e77cd8f7904..af3adffb0ed59 100644 --- a/packages/cubejs-testing-drivers/src/tests/testQueries.ts +++ b/packages/cubejs-testing-drivers/src/tests/testQueries.ts @@ -1974,6 +1974,19 @@ export function testQueries(type: string, { includeIncrementalSchemaSuite, exten expect(response.rawData()).toMatchSnapshot(); }); + execute('querying custom granularities ECommerce: count by two_mo_by_feb + no dimension + rollingCountByLeading without date range', async () => { + const response = await client.load({ + measures: [ + 'ECommerce.rollingCountByLeading', + ], + timeDimensions: [{ + dimension: 'ECommerce.customOrderDateNoPreAgg', + granularity: 'two_mo_by_feb', + }], + }); + expect(response.rawData()).toMatchSnapshot(); + }); + execute('querying custom granularities (with preaggregation) ECommerce: totalQuantity by half_year + no dimension', async () => { const response = await client.load({ measures: [ diff --git a/packages/cubejs-testing-drivers/test/__snapshots__/athena-export-bucket-s3-full.test.ts.snap b/packages/cubejs-testing-drivers/test/__snapshots__/athena-export-bucket-s3-full.test.ts.snap index 0386e57ffa1d1..a51e066219a71 100644 --- a/packages/cubejs-testing-drivers/test/__snapshots__/athena-export-bucket-s3-full.test.ts.snap +++ b/packages/cubejs-testing-drivers/test/__snapshots__/athena-export-bucket-s3-full.test.ts.snap @@ -9761,6 +9761,41 @@ Array [ ] `; +exports[`Queries with the @cubejs-backend/athena-driver querying custom granularities ECommerce: count by two_mo_by_feb + no dimension + rollingCountByLeading without date range 1`] = ` +Array [ + Object { + "ECommerce.customOrderDateNoPreAgg": "2020-02-01T10:00:00.000", + "ECommerce.customOrderDateNoPreAgg.two_mo_by_feb": "2020-02-01T10:00:00.000", + "ECommerce.rollingCountByLeading": "12", + }, + Object { + "ECommerce.customOrderDateNoPreAgg": "2020-04-01T10:00:00.000", + "ECommerce.customOrderDateNoPreAgg.two_mo_by_feb": "2020-04-01T10:00:00.000", + "ECommerce.rollingCountByLeading": "6", + }, + Object { + "ECommerce.customOrderDateNoPreAgg": "2020-06-01T10:00:00.000", + "ECommerce.customOrderDateNoPreAgg.two_mo_by_feb": "2020-06-01T10:00:00.000", + "ECommerce.rollingCountByLeading": "19", + }, + Object { + "ECommerce.customOrderDateNoPreAgg": "2020-08-01T10:00:00.000", + "ECommerce.customOrderDateNoPreAgg.two_mo_by_feb": "2020-08-01T10:00:00.000", + "ECommerce.rollingCountByLeading": "16", + }, + Object { + "ECommerce.customOrderDateNoPreAgg": "2020-10-01T10:00:00.000", + "ECommerce.customOrderDateNoPreAgg.two_mo_by_feb": "2020-10-01T10:00:00.000", + "ECommerce.rollingCountByLeading": null, + }, + Object { + "ECommerce.customOrderDateNoPreAgg": "2020-12-01T10:00:00.000", + "ECommerce.customOrderDateNoPreAgg.two_mo_by_feb": "2020-12-01T10:00:00.000", + "ECommerce.rollingCountByLeading": null, + }, +] +`; + exports[`Queries with the @cubejs-backend/athena-driver querying custom granularities ECommerce: count by two_mo_by_feb + no dimension + rollingCountByTrailing 1`] = ` Array [ Object { diff --git a/packages/cubejs-testing-drivers/test/__snapshots__/postgres-full.test.ts.snap b/packages/cubejs-testing-drivers/test/__snapshots__/postgres-full.test.ts.snap index 8aecfb381e798..10c36f86216c0 100644 --- a/packages/cubejs-testing-drivers/test/__snapshots__/postgres-full.test.ts.snap +++ b/packages/cubejs-testing-drivers/test/__snapshots__/postgres-full.test.ts.snap @@ -18465,6 +18465,46 @@ Array [ ] `; +exports[`Queries with the @cubejs-backend/postgres-driver querying custom granularities ECommerce: count by two_mo_by_feb + no dimension + rollingCountByLeading without date range 1`] = ` +Array [ + Object { + "ECommerce.customOrderDateNoPreAgg": "2019-12-01T10:00:00.000", + "ECommerce.customOrderDateNoPreAgg.two_mo_by_feb": "2019-12-01T10:00:00.000", + "ECommerce.rollingCountByLeading": "8", + }, + Object { + "ECommerce.customOrderDateNoPreAgg": "2020-02-01T10:00:00.000", + "ECommerce.customOrderDateNoPreAgg.two_mo_by_feb": "2020-02-01T10:00:00.000", + "ECommerce.rollingCountByLeading": "12", + }, + Object { + "ECommerce.customOrderDateNoPreAgg": "2020-04-01T10:00:00.000", + "ECommerce.customOrderDateNoPreAgg.two_mo_by_feb": "2020-04-01T10:00:00.000", + "ECommerce.rollingCountByLeading": "6", + }, + Object { + "ECommerce.customOrderDateNoPreAgg": "2020-06-01T10:00:00.000", + "ECommerce.customOrderDateNoPreAgg.two_mo_by_feb": "2020-06-01T10:00:00.000", + "ECommerce.rollingCountByLeading": "19", + }, + Object { + "ECommerce.customOrderDateNoPreAgg": "2020-08-01T10:00:00.000", + "ECommerce.customOrderDateNoPreAgg.two_mo_by_feb": "2020-08-01T10:00:00.000", + "ECommerce.rollingCountByLeading": "16", + }, + Object { + "ECommerce.customOrderDateNoPreAgg": "2020-10-01T10:00:00.000", + "ECommerce.customOrderDateNoPreAgg.two_mo_by_feb": "2020-10-01T10:00:00.000", + "ECommerce.rollingCountByLeading": null, + }, + Object { + "ECommerce.customOrderDateNoPreAgg": "2020-12-01T10:00:00.000", + "ECommerce.customOrderDateNoPreAgg.two_mo_by_feb": "2020-12-01T10:00:00.000", + "ECommerce.rollingCountByLeading": null, + }, +] +`; + exports[`Queries with the @cubejs-backend/postgres-driver querying custom granularities ECommerce: count by two_mo_by_feb + no dimension + rollingCountByTrailing 1`] = ` Array [ Object { diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/driver_tools.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/driver_tools.rs index 47bb919030e0c..d11cd5c8ad073 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/driver_tools.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/driver_tools.rs @@ -31,6 +31,7 @@ pub trait DriverTools { fn hll_merge(&self, sql: String) -> Result; fn hll_cardinality_merge(&self, sql: String) -> Result; fn count_distinct_approx(&self, sql: String) -> Result; + fn support_generated_series_for_custom_td(&self) -> Result; fn date_bin( &self, interval: String, diff --git a/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/builder.rs b/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/builder.rs index 2c66e0977b4a6..9c01d358f6ba0 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/builder.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/builder.rs @@ -965,8 +965,9 @@ impl PhysicalPlanBuilder { )); }; - let ts_date_range = if self.plan_sql_templates.supports_generated_time_series() - && granularity_obj.is_predefined_granularity() + let ts_date_range = if self + .plan_sql_templates + .supports_generated_time_series(granularity_obj.is_predefined_granularity())? { if let Some(date_range) = time_dimension_symbol .get_range_for_time_series(date_range, self.query_tools.timezone())? diff --git a/rust/cubesqlplanner/cubesqlplanner/src/plan/time_series.rs b/rust/cubesqlplanner/cubesqlplanner/src/plan/time_series.rs index 7129f6fb718e0..e8dabf3349a42 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/plan/time_series.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/plan/time_series.rs @@ -40,28 +40,35 @@ impl TimeSeries { } pub fn to_sql(&self, templates: &PlanSqlTemplates) -> Result { - if templates.supports_generated_time_series() - && self.granularity.is_predefined_granularity() - { + if templates.supports_generated_time_series(self.granularity.is_predefined_granularity())? { let interval_description = templates - .interval_and_minimal_time_unit(self.granularity.granularity_interval().clone())?; + .interval_and_minimal_time_unit(self.granularity.granularity_interval().to_sql())?; if interval_description.len() != 2 { return Err(CubeError::internal( "Interval description must have 2 elements".to_string(), )); } let interval = interval_description[0].clone(); - let interval = templates.interval_string(interval)?; let minimal_time_unit = interval_description[1].clone(); match &self.date_range { TimeSeriesDateRange::Filter(from_date, to_date) => { - let from_date = format!("'{}'", from_date); - let to_date = format!("'{}'", to_date); + let start = templates.quote_string(from_date)?; + let date_field = templates.quote_identifier("d")?; + let date_from = templates.time_stamp_cast(date_field.clone())?; + let end = templates.quote_string(to_date)?; + let date_to = format!( + "({})", + templates.add_interval(date_from.clone(), interval.clone())? + ); + let date_to = + templates.subtract_interval(date_to, "1 millisecond".to_string())?; templates.generated_time_series_select( - &from_date, - &to_date, - &interval, + &date_from, + &date_to, + &start, + &end, + &templates.interval_string(interval)?, &self.granularity.granularity_offset(), &minimal_time_unit, ) @@ -73,7 +80,7 @@ impl TimeSeries { &cte_name, &min_date_name, &max_date_name, - &interval, + &templates.interval_string(interval)?, &minimal_time_unit, ) } @@ -99,7 +106,7 @@ impl TimeSeries { )? } else { self.query_tools.base_tools().generate_custom_time_series( - self.granularity.granularity_interval().clone(), + self.granularity.granularity_interval().to_sql(), vec![raw_from_date.clone(), raw_to_date.clone()], self.granularity.origin_local_formatted(), )? diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/plan.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/plan.rs index 5221a6ddb502e..551aacd6847d9 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/plan.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/plan.rs @@ -244,12 +244,8 @@ impl PlanSqlTemplates { } pub fn cast_to_string(&self, expr: &str) -> Result { - self.render.render_template( - "expressions/cast_to_string", - context! { - expr => expr, - }, - ) + let string_type = self.render.render_template("types/string", context! {})?; + self.cast(expr, &string_type) } pub fn count_distinct(&self, expr: &str) -> Result { @@ -430,13 +426,23 @@ impl PlanSqlTemplates { .contains_template("operators/is_not_distinct_from") } - pub fn supports_generated_time_series(&self) -> bool { - self.render + pub fn supports_generated_time_series( + &self, + predifined_granularity: bool, + ) -> Result { + Ok(self + .render .contains_template("statements/generated_time_series_select") + && (predifined_granularity + || self + .driver_tools() + .support_generated_series_for_custom_td()?)) } pub fn generated_time_series_select( &self, + date_from: &str, + date_to: &str, start: &str, end: &str, granularity: &str, @@ -445,7 +451,7 @@ impl PlanSqlTemplates { ) -> Result { self.render.render_template( "statements/generated_time_series_select", - context! { start => start, end => end, granularity => granularity, granularity_offset => granularity_offset, minimal_time_unit => minimal_time_unit }, + context! {date_from => date_from, date_to => date_to, start => start, end => end, granularity => granularity, granularity_offset => granularity_offset, minimal_time_unit => minimal_time_unit }, ) } pub fn generated_time_series_with_cte_range_source( diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/time_dimension/granularity.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/time_dimension/granularity.rs index 4a480e11be3bb..3807c1ddfbea1 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/time_dimension/granularity.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/time_dimension/granularity.rs @@ -3,14 +3,13 @@ use crate::planner::sql_evaluator::SqlCall; use crate::planner::sql_templates::PlanSqlTemplates; use chrono_tz::Tz; use cubenativeutils::CubeError; -use itertools::Itertools; use std::rc::Rc; use std::str::FromStr; #[derive(Clone)] pub struct Granularity { granularity: String, - granularity_interval: String, + granularity_interval: SqlInterval, granularity_offset: Option, origin: QueryDateTime, is_predefined_granularity: bool, @@ -20,7 +19,7 @@ pub struct Granularity { impl Granularity { pub fn try_new_predefined(timezone: Tz, granularity: String) -> Result { - let granularity_interval = format!("1 {}", granularity); + let granularity_interval = format!("1 {}", granularity).parse()?; let origin = Self::default_origin(timezone)?; Ok(Self { @@ -42,6 +41,7 @@ impl Granularity { calendar_sql: Option>, ) -> Result { // sql() is mutual exclusive with interval and offset/origin + let granularity_interval = granularity_interval.parse::()?; if calendar_sql.is_some() { return Ok(Self { granularity, @@ -64,22 +64,7 @@ impl Granularity { Self::default_origin(timezone)? }; - let mut interval_parts = granularity_interval.split_whitespace().tuples::<(_, _)>(); - let first_part = interval_parts.next(); - let second_part = interval_parts.next(); - let is_natural_aligned = if second_part.is_none() { - if let Some((value, _)) = first_part { - let value = value - .parse::() - .map_err(|_| CubeError::user(format!("Invalid interval value: {}", value)))?; - value == 1 - } else { - false - } - } else { - false - }; - + let is_natural_aligned = granularity_interval.is_trivial(); Ok(Self { granularity, granularity_interval, @@ -107,7 +92,7 @@ impl Granularity { &self.granularity } - pub fn granularity_interval(&self) -> &String { + pub fn granularity_interval(&self) -> &SqlInterval { &self.granularity_interval } @@ -116,9 +101,7 @@ impl Granularity { } pub fn granularity_from_interval(&self) -> Result { - self.granularity_interval - .parse::()? - .min_granularity() + self.granularity_interval.min_granularity() } pub fn granularity_from_offset(&self) -> Result { @@ -160,8 +143,7 @@ impl Granularity { } pub fn align_date_to_origin(&self, date: QueryDateTime) -> Result { - let interval = self.granularity_interval.parse::()?; - date.align_to_origin(&self.origin, &interval) + date.align_to_origin(&self.origin, &self.granularity_interval) } fn default_origin(timezone: Tz) -> Result { @@ -184,7 +166,7 @@ impl Granularity { } } else { templates.date_bin( - self.granularity_interval.clone(), + self.granularity_interval.to_sql(), input, self.origin_local_formatted(), )? diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/time_dimension/sql_interval.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/time_dimension/sql_interval.rs index 8d5ce117d4d7c..c9700da3ba06b 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/time_dimension/sql_interval.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/time_dimension/sql_interval.rs @@ -63,6 +63,24 @@ impl SqlInterval { Ok(res.to_string()) } + pub fn is_trivial(&self) -> bool { + let fields = [ + self.year, + self.quarter, + self.month, + self.week, + self.day, + self.hour, + self.minute, + self.second, + ]; + + let count_ones = fields.iter().filter(|&&v| v == 1).count(); + let count_nonzeros = fields.iter().filter(|&&v| v != 0).count(); + + count_ones == 1 && count_nonzeros == 1 + } + pub fn to_sql(&self) -> String { let mut res = vec![]; if self.year != 0 {