From 06392cc8879e530265c29bf41256ce5a2c39ab77 Mon Sep 17 00:00:00 2001 From: Alexandr Romanenko Date: Thu, 4 Sep 2025 19:32:58 +0200 Subject: [PATCH 01/41] feat(tesseract): Switch dimensions and case measures --- .../src/adapter/BaseQuery.js | 16 +- .../src/compiler/CubeValidator.ts | 108 ++-- .../integration/postgres/calc-groups.test.ts | 505 ++++++++++++++++++ .../test/integration/utils/BaseDbRunner.ts | 1 + .../src/cube_bridge/dimension_definition.rs | 1 + .../physical_plan_builder/processors/query.rs | 16 +- .../sql_nodes/cube_calc_groups.rs | 102 ++++ .../sql_evaluator/sql_nodes/factory.rs | 25 +- .../planner/sql_evaluator/sql_nodes/mod.rs | 2 + .../sql_evaluator/symbols/cube_symbol.rs | 16 +- .../sql_evaluator/symbols/dimension_symbol.rs | 91 +++- .../sql_evaluator/symbols/measure_symbol.rs | 21 +- .../sql_evaluator/symbols/member_symbol.rs | 10 + .../src/planner/sql_templates/plan.rs | 17 + .../src/planner/sql_templates/structs.rs | 6 + 15 files changed, 849 insertions(+), 88 deletions(-) create mode 100644 packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/cube_calc_groups.rs diff --git a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js index 5f9498340b4ae..7501cc33d7e91 100644 --- a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js +++ b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js @@ -3232,6 +3232,9 @@ export class BaseQuery { } if (symbol.case) { return this.renderDimensionCase(symbol, cubeName); + } else if (symbol.type === 'switch') { + // Dimension of type switch is not supported in BaseQuery, return an empty string to make dependency resolution work. + return ''; } else if (symbol.type === 'geo') { return this.concatStringsSql([ this.autoPrefixAndEvaluateSql(cubeName, symbol.latitude.sql, isMemberExpr), @@ -4210,7 +4213,18 @@ export class BaseQuery { time_series_get_range: 'SELECT {{ max_expr }} as {{ quoted_max_name }},\n' + '{{ min_expr }} as {{ quoted_min_name }}\n' + 'FROM {{ from_prepared }}\n' + - '{% if filter %}WHERE {{ filter }}{% endif %}' + '{% if filter %}WHERE {{ filter }}{% endif %}', + calc_groups_join: 'SELECT \"{{ original_cube }}\".*, \"{{ groups | map(attribute=\'name\') | join(\'\", \"\') }}\"\n' + + 'FROM {{ original_cube_sql }} {{ original_cube }}\n' + + '{% for group in groups %}' + + 'CROSS JOIN\n' + + '(\n' + + '{% for value in group.values %}' + + 'SELECT \'{{ value }}\' as \"{{ group.name }}\"' + + '{% if not loop.last %} UNION ALL\n{% endif %}' + + '{% endfor %}' + + ') \"{{ group.name }}_values\"\n' + + '{% endfor %}' }, expressions: { column_reference: '{% if table_name %}{{ table_name }}.{% endif %}{{ name }}', diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts index 2d8bf36499aef..1ce82885b6594 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts @@ -132,6 +132,11 @@ const BaseDimensionWithoutSubQuery = { enableSuggestions: Joi.boolean().strict(), format: formatSchema, meta: Joi.any(), + values: Joi.when('type', { + is: 'switch', + then: Joi.array().items(Joi.string()), + otherwise: Joi.forbidden() + }), granularities: Joi.when('type', { is: 'time', then: Joi.object().pattern(identifierRegex, @@ -647,53 +652,62 @@ const CalendarTimeShiftItem = Joi.alternatives().try( }) ); -const DimensionsSchema = Joi.object().pattern(identifierRegex, Joi.alternatives().try( - inherit(BaseDimensionWithoutSubQuery, { - case: Joi.object().keys({ - when: Joi.array().items(Joi.object().keys({ - sql: Joi.func().required(), - label: Joi.alternatives([ - Joi.string(), - Joi.object().keys({ - sql: Joi.func().required() - }) - ]) - })), - else: Joi.object().keys({ - label: Joi.alternatives([ - Joi.string(), - Joi.object().keys({ - sql: Joi.func().required() - }) - ]) - }) - }).required() - }), - inherit(BaseDimensionWithoutSubQuery, { - latitude: Joi.object().keys({ - sql: Joi.func().required() - }).required(), - longitude: Joi.object().keys({ - sql: Joi.func().required() - }).required() - }), - inherit(BaseDimension, { - sql: Joi.func().required(), - }), - inherit(BaseDimension, { - multiStage: Joi.boolean().valid(true), - type: Joi.any().valid('number').required(), - sql: Joi.func().required(), - addGroupBy: Joi.func(), - }), - // TODO should be valid only for calendar cubes, but this requires significant refactoring - // of all schemas. Left for the future when we'll switch to zod. - inherit(BaseDimensionWithoutSubQuery, { - type: Joi.any().valid('time').required(), - sql: Joi.func().required(), - timeShift: Joi.array().items(CalendarTimeShiftItem), - }) -)); +const SwitchDimension = Joi.object({ + type: Joi.string().valid('switch').required(), + values: Joi.array().items(Joi.string()).min(1).required() +}); + +const DimensionsSchema = Joi.object().pattern(identifierRegex, Joi.alternatives().conditional(Joi.ref('.type'), { + is: 'switch', + then: SwitchDimension, + otherwise: Joi.alternatives().try( + inherit(BaseDimensionWithoutSubQuery, { + case: Joi.object().keys({ + when: Joi.array().items(Joi.object().keys({ + sql: Joi.func().required(), + label: Joi.alternatives([ + Joi.string(), + Joi.object().keys({ + sql: Joi.func().required() + }) + ]) + })), + else: Joi.object().keys({ + label: Joi.alternatives([ + Joi.string(), + Joi.object().keys({ + sql: Joi.func().required() + }) + ]) + }) + }).required() + }), + inherit(BaseDimensionWithoutSubQuery, { + latitude: Joi.object().keys({ + sql: Joi.func().required() + }).required(), + longitude: Joi.object().keys({ + sql: Joi.func().required() + }).required() + }), + inherit(BaseDimension, { + sql: Joi.func().required(), + }), + inherit(BaseDimension, { + multiStage: Joi.boolean().valid(true), + type: Joi.any().valid('number').required(), + sql: Joi.func().required(), + addGroupBy: Joi.func(), + }), + // TODO should be valid only for calendar cubes, but this requires significant refactoring + // of all schemas. Left for the future when we'll switch to zod. + inherit(BaseDimensionWithoutSubQuery, { + type: Joi.any().valid('time').required(), + sql: Joi.func().required(), + timeShift: Joi.array().items(CalendarTimeShiftItem), + }) + ) +})); const SegmentsSchema = Joi.object().pattern(identifierRegex, Joi.object().keys({ aliases: Joi.array().items(Joi.string()), diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts new file mode 100644 index 0000000000000..1f098f6ca1b12 --- /dev/null +++ b/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts @@ -0,0 +1,505 @@ +import { + getEnv, +} from '@cubejs-backend/shared'; +import { prepareYamlCompiler } from '../../unit/PrepareCompiler'; +import { dbRunner } from './PostgresDBRunner'; + +describe('Calc-Groups', () => { + jest.setTimeout(200000); + + const { compiler, joinGraph, cubeEvaluator } = prepareYamlCompiler(` +cubes: + - name: orders + sql: > + SELECT 9 as ID, 'completed' as STATUS, '2022-01-12T20:00:00.000Z'::timestamptz as CREATED_AT + union all + SELECT 10 as ID, 'completed' as STATUS, '2023-01-12T20:00:00.000Z'::timestamptz as CREATED_AT + union all + SELECT 11 as ID, 'completed' as STATUS, '2024-01-14T20:00:00.000Z'::timestamptz as CREATED_AT + union all + SELECT 12 as ID, 'completed' as STATUS, '2024-02-14T20:00:00.000Z'::timestamptz as CREATED_AT + union all + SELECT 13 as ID, 'completed' as STATUS, '2025-03-14T20:00:00.000Z'::timestamptz as CREATED_AT + joins: + - name: line_items + sql: "{CUBE}.ID = {line_items}.order_id" + relationship: many_to_one + + dimensions: + - name: id + sql: ID + type: number + primary_key: true + + - name: status + sql: STATUS + type: string + + - name: date + sql: CREATED_AT + type: time + + - name: amount + sql: '{line_items.total_amount}' + type: number + sub_query: true + + - name: currency + type: switch + values: + - USD + - EUR + - GBP + + - name: test + type: string + case: + when: + - sql: "{CUBE.currency} = 'USD'" + label: '111' + - sql: "{CUBE.currency} = 'EUR'" + label: '333' + else: + label: 'def' + + - name: strategy + type: switch + values: + - A + - B + + measures: + - name: count + type: count + + - name: completed_count + type: count + filters: + - sql: "{CUBE}.STATUS = 'completed'" + + - name: returned_count + type: count + filters: + - sql: "{CUBE}.STATUS = 'returned'" + + - name: return_rate + type: number + sql: "({returned_count} / NULLIF({completed_count}, 0)) * 100.0" + description: "Percentage of returned orders out of completed, exclude just placed orders." + format: percent + + - name: total_amount + sql: '{CUBE.amount}' + type: sum + + - name: revenue + sql: "CASE WHEN {CUBE}.status = 'completed' THEN {CUBE.amount} END" + type: sum + format: currency + + - name: average_order_value + sql: '{CUBE.amount}' + type: avg + + - name: revenue_1_y_ago + sql: "{revenue}" + multi_stage: true + type: number + format: currency + time_shift: + - time_dimension: date + interval: 1 year + type: prior + - time_dimension: orders_view.date + interval: 1 year + type: prior + + - name: cagr_1_y + sql: "(({revenue} / {revenue_1_y_ago}) - 1)" + type: number + format: percent + description: "Annual CAGR, year over year growth in revenue" + + - name: line_items + sql: > + SELECT 9 as ID, '2024-01-12T20:00:00.000Z'::timestamptz as CREATED_AT, 9 as ORDER_ID, 11 as PRODUCT_ID + union all + SELECT 10 as ID, '2024-01-12T20:00:00.000Z'::timestamptz as CREATED_AT, 10 as ORDER_ID, 10 as PRODUCT_ID + union all + SELECT 11 as ID, '2024-01-12T20:00:00.000Z'::timestamptz as CREATED_AT, 10 as ORDER_ID, 11 as PRODUCT_ID + union all + SELECT 12 as ID, '2024-01-12T20:00:00.000Z'::timestamptz as CREATED_AT, 11 as ORDER_ID, 10 as PRODUCT_ID + union all + SELECT 13 as ID, '2024-01-12T20:00:00.000Z'::timestamptz as CREATED_AT, 11 as ORDER_ID, 10 as PRODUCT_ID + union all + SELECT 14 as ID, '2024-01-12T20:00:00.000Z'::timestamptz as CREATED_AT, 12 as ORDER_ID, 10 as PRODUCT_ID + union all + SELECT 15 as ID, '2024-01-12T20:00:00.000Z'::timestamptz as CREATED_AT, 13 as ORDER_ID, 11 as PRODUCT_ID + public: false + + joins: + - name: products + sql: "{CUBE}.PRODUCT_ID = {products}.ID" + relationship: many_to_one + + dimensions: + - name: id + sql: ID + type: number + primary_key: true + + - name: created_at + sql: CREATED_AT + type: time + + - name: price + sql: "{products.price}" + type: number + + measures: + - name: count + type: count + + - name: total_amount + sql: "{price}" + type: sum + + - name: products + sql: > + SELECT 10 as ID, 'some category' as PRODUCT_CATEGORY, 'some name' as NAME, 10 as PRICE + union all + SELECT 11 as ID, 'some category' as PRODUCT_CATEGORY, 'some name' as NAME, 5 as PRICE + public: false + description: > + Products and categories in our e-commerce store. + + dimensions: + - name: id + sql: ID + type: number + primary_key: true + + - name: product_category + sql: PRODUCT_CATEGORY + type: string + + - name: name + sql: NAME + type: string + + - name: price + sql: PRICE + type: number + + measures: + - name: count + type: count +views: + - name: orders_view + + cubes: + - join_path: orders + includes: + - date + - revenue + - cagr_1_y + - return_rate + + - join_path: line_items.products + prefix: true + includes: + - product_category + + `); + + if (getEnv('nativeSqlPlanner')) { + it('basic cross join', async () => dbRunner.runQueryTest({ + dimensions: ['orders.currency'], + timeDimensions: [ + { + dimension: 'orders.date', + granularity: 'year' + } + ], + timezone: 'UTC' + }, [ + { + orders__currency: 'EUR', + orders__date_year: '2022-01-01T00:00:00.000Z' + }, + { + orders__currency: 'GBP', + orders__date_year: '2022-01-01T00:00:00.000Z' + }, + { + orders__currency: 'USD', + orders__date_year: '2022-01-01T00:00:00.000Z' + }, + { + orders__currency: 'EUR', + orders__date_year: '2023-01-01T00:00:00.000Z' + }, + { + orders__currency: 'GBP', + orders__date_year: '2023-01-01T00:00:00.000Z' + }, + { + orders__currency: 'USD', + orders__date_year: '2023-01-01T00:00:00.000Z' + }, + { + orders__currency: 'EUR', + orders__date_year: '2024-01-01T00:00:00.000Z' + }, + { + orders__currency: 'GBP', + orders__date_year: '2024-01-01T00:00:00.000Z' + }, + { + orders__currency: 'USD', + orders__date_year: '2024-01-01T00:00:00.000Z' + }, + { + orders__currency: 'EUR', + orders__date_year: '2025-01-01T00:00:00.000Z' + }, + { + orders__currency: 'GBP', + orders__date_year: '2025-01-01T00:00:00.000Z' + }, + { + orders__currency: 'USD', + orders__date_year: '2025-01-01T00:00:00.000Z' + } + ], + { joinGraph, cubeEvaluator, compiler })); + + it('basic double cross join', async () => dbRunner.runQueryTest({ + dimensions: ['orders.currency', 'orders.strategy'], + timeDimensions: [ + { + dimension: 'orders.date', + granularity: 'year', + dateRange: ['2024-01-01', '2026-01-01'] + } + ], + timezone: 'UTC', + order: [{ + id: 'orders.date' + }, { + id: 'orders.currency' + }, { + id: 'orders.strategy' + }, + ], + }, [ + { + orders__currency: 'EUR', + orders__strategy: 'A', + orders__date_year: '2024-01-01T00:00:00.000Z' + }, + { + orders__currency: 'EUR', + orders__strategy: 'B', + orders__date_year: '2024-01-01T00:00:00.000Z' + }, + { + orders__currency: 'GBP', + orders__strategy: 'A', + orders__date_year: '2024-01-01T00:00:00.000Z' + }, + { + orders__currency: 'GBP', + orders__strategy: 'B', + orders__date_year: '2024-01-01T00:00:00.000Z' + }, + { + orders__currency: 'USD', + orders__strategy: 'A', + orders__date_year: '2024-01-01T00:00:00.000Z' + }, + { + orders__currency: 'USD', + orders__strategy: 'B', + orders__date_year: '2024-01-01T00:00:00.000Z' + }, + { + orders__currency: 'EUR', + orders__strategy: 'A', + orders__date_year: '2025-01-01T00:00:00.000Z' + }, + { + orders__currency: 'EUR', + orders__strategy: 'B', + orders__date_year: '2025-01-01T00:00:00.000Z' + }, + { + orders__currency: 'GBP', + orders__strategy: 'A', + orders__date_year: '2025-01-01T00:00:00.000Z' + }, + { + orders__currency: 'GBP', + orders__strategy: 'B', + orders__date_year: '2025-01-01T00:00:00.000Z' + }, + { + orders__currency: 'USD', + orders__strategy: 'A', + orders__date_year: '2025-01-01T00:00:00.000Z' + }, + { + orders__currency: 'USD', + orders__strategy: 'B', + orders__date_year: '2025-01-01T00:00:00.000Z' + } + ], + { joinGraph, cubeEvaluator, compiler })); + + it('basic cross join with measure', async () => dbRunner.runQueryTest({ + dimensions: ['orders.strategy'], + measures: ['orders.revenue'], + timeDimensions: [ + { + dimension: 'orders.date', + granularity: 'year' + } + ], + timezone: 'UTC', + order: [{ + id: 'orders.date' + }, { + id: 'orders.currency' + }, + ], + }, [ + { + orders__date_year: '2022-01-01T00:00:00.000Z', + orders__strategy: 'A', + orders__revenue: '5', + }, + { + orders__date_year: '2022-01-01T00:00:00.000Z', + orders__strategy: 'B', + orders__revenue: '5', + }, + { + orders__date_year: '2023-01-01T00:00:00.000Z', + orders__strategy: 'A', + orders__revenue: '15', + }, + { + orders__date_year: '2023-01-01T00:00:00.000Z', + orders__strategy: 'B', + orders__revenue: '15', + }, + { + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__strategy: 'A', + orders__revenue: '30', + }, + { + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__strategy: 'B', + orders__revenue: '30', + }, + { + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__strategy: 'A', + orders__revenue: '5', + }, + { + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__strategy: 'B', + orders__revenue: '5', + } + ], + { joinGraph, cubeEvaluator, compiler })); + + it('basic cross join with measure with filters', async () => dbRunner.runQueryTest({ + dimensions: ['orders.strategy'], + measures: ['orders.revenue'], + timeDimensions: [ + { + dimension: 'orders.date', + granularity: 'year' + } + ], + filters: [ + { dimension: 'orders.strategy', operator: 'equals', values: ['B'] } + ], + timezone: 'UTC', + order: [{ + id: 'orders.date' + }, { + id: 'orders.currency' + }, + ], + }, [ + { + orders__date_year: '2022-01-01T00:00:00.000Z', + orders__strategy: 'B', + orders__revenue: '5', + }, + { + orders__date_year: '2023-01-01T00:00:00.000Z', + orders__strategy: 'B', + orders__revenue: '15', + }, + { + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__strategy: 'B', + orders__revenue: '30', + }, + { + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__strategy: 'B', + orders__revenue: '5', + } + ], + { joinGraph, cubeEvaluator, compiler })); + + it('measure switch', async () => dbRunner.runQueryTest({ + dimensions: ['orders.currency', 'orders.test'], + measures: ['orders.revenue'], + timeDimensions: [ + { + dimension: 'orders.date', + granularity: 'year' + } + ], + timezone: 'UTC', + order: [{ + id: 'orders.date' + }, { + id: 'orders.currency' + }, + ], + }, [ + { + orders__date_year: '2022-01-01T00:00:00.000Z', + orders__strategy: 'B', + orders__revenue: '5', + }, + { + orders__date_year: '2023-01-01T00:00:00.000Z', + orders__strategy: 'B', + orders__revenue: '15', + }, + { + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__strategy: 'B', + orders__revenue: '30', + }, + { + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__strategy: 'B', + orders__revenue: '5', + } + ], + { joinGraph, cubeEvaluator, compiler })); + } else { + // This test is working only in tesseract + test.skip('calc groups testst', () => { expect(1).toBe(1); }); + } +}); diff --git a/packages/cubejs-schema-compiler/test/integration/utils/BaseDbRunner.ts b/packages/cubejs-schema-compiler/test/integration/utils/BaseDbRunner.ts index 8d7e0ee64fd42..94b8228702df8 100644 --- a/packages/cubejs-schema-compiler/test/integration/utils/BaseDbRunner.ts +++ b/packages/cubejs-schema-compiler/test/integration/utils/BaseDbRunner.ts @@ -29,6 +29,7 @@ export class BaseDbRunner { const res = await this.testQuery(query.buildSqlAndParams()); console.log(JSON.stringify(res)); + console.log('!!! res: ', res); expect(res).toEqual( expectedResult diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/dimension_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/dimension_definition.rs index 4765e8b6eba3b..639de20d56309 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/dimension_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/dimension_definition.rs @@ -25,6 +25,7 @@ pub struct DimensionDefinitionStatic { pub sub_query: Option, #[serde(rename = "propagateFiltersToSubQuery")] pub propagate_filters_to_sub_query: Option, + pub values: Option>, } #[nativebridge::native_bridge(DimensionDefinitionStatic)] diff --git a/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/query.rs b/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/query.rs index 2af8508dabf55..628c97f94a1d2 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/query.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/query.rs @@ -2,7 +2,8 @@ use super::super::{LogicalNodeProcessor, ProcessableNode, PushDownBuilderContext use crate::logical_plan::{Query, QuerySource}; use crate::physical_plan_builder::PhysicalPlanBuilder; use crate::plan::{Cte, Expr, MemberExpression, Select, SelectBuilder}; -use crate::planner::sql_evaluator::ReferencesBuilder; +use crate::planner::sql_evaluator::sql_nodes::SqlNodesFactory; +use crate::planner::sql_evaluator::{MemberSymbol, ReferencesBuilder}; use cubenativeutils::CubeError; use std::collections::HashMap; use std::rc::Rc; @@ -19,6 +20,18 @@ impl QueryProcessor<'_> { QuerySource::LogicalJoin(_) => false, } } + + fn process_calc_group(&self, symbol: &Rc, context_factory: &mut SqlNodesFactory) { + if let Ok(dimension) = symbol.as_dimension() { + if dimension.is_calc_group() { + context_factory.add_calc_group_item( + dimension.cube_name().clone(), + dimension.name().clone(), + dimension.values().clone(), + ); + } + } + } } impl<'a> LogicalNodeProcessor<'a, Query> for QueryProcessor<'a> { @@ -94,6 +107,7 @@ impl<'a> LogicalNodeProcessor<'a, Query> for QueryProcessor<'a> { &None, &mut render_references, )?; + self.process_calc_group(member, &mut context_factory); if context.measure_subquery { select_builder.add_projection_member_without_schema(member, None); } else { diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/cube_calc_groups.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/cube_calc_groups.rs new file mode 100644 index 0000000000000..9180bdd1116f0 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/cube_calc_groups.rs @@ -0,0 +1,102 @@ +use super::SqlNode; +use crate::planner::query_tools::QueryTools; +use crate::planner::sql_evaluator::DimensionSymbol; +use crate::planner::sql_evaluator::MemberSymbol; +use crate::planner::sql_evaluator::SqlEvaluatorVisitor; +use crate::planner::sql_templates::structs::TemplateCalcGroup; +use crate::planner::sql_templates::PlanSqlTemplates; +use cubenativeutils::CubeError; +use itertools::Itertools; +use std::any::Any; +use std::collections::HashMap; +use std::rc::Rc; + +#[derive(Clone)] +pub struct CalcGroupItem { + pub name: String, + pub values: Vec, +} + +#[derive(Default, Clone)] +pub struct CalcGroupsItems { + items: HashMap>, +} + +impl CalcGroupsItems { + pub fn add(&mut self, cube_name: String, dimension_name: String, values: Vec) { + let items = self.items.entry(cube_name).or_default(); + if !items.iter().any(|itm| itm.name == dimension_name) { + items.push(CalcGroupItem { + name: dimension_name, + values, + }) + } + } + + pub fn get(&self, cube_name: &str) -> Option<&Vec> { + self.items.get(cube_name) + } + + pub fn is_empty(&self) -> bool { + self.items.is_empty() + } +} + +pub struct CubeCalcGroupsSqlNode { + input: Rc, + items: CalcGroupsItems, +} + +impl CubeCalcGroupsSqlNode { + pub fn new(input: Rc, items: CalcGroupsItems) -> Rc { + Rc::new(Self { input, items }) + } +} + +impl SqlNode for CubeCalcGroupsSqlNode { + fn to_sql( + &self, + visitor: &SqlEvaluatorVisitor, + node: &Rc, + query_tools: Rc, + node_processor: Rc, + templates: &PlanSqlTemplates, + ) -> Result { + let input = self.input.to_sql( + visitor, + node, + query_tools.clone(), + node_processor.clone(), + templates, + )?; + let res = match node.as_ref() { + MemberSymbol::CubeTable(ev) => { + let res = if let Some(groups) = self.items.get(ev.cube_name()) { + let template_groups = groups + .iter() + .map(|group| TemplateCalcGroup { + name: group.name.clone(), + values: group.values.clone(), + }) + .collect_vec(); + let res = templates.calc_groups_join(&ev.cube_name(), &input, template_groups)?; + format!("({})", res) + } else { + input + }; + + res + } + _ => input, + }; + Ok(res) + } + + fn as_any(self: Rc) -> Rc { + self.clone() + } + + fn childs(&self) -> Vec> { + vec![] + } +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/factory.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/factory.rs index 127315617f97d..3f16eb589a6b7 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/factory.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/factory.rs @@ -8,6 +8,8 @@ use super::{ use crate::plan::schema::QualifiedColumnName; use crate::planner::planners::multi_stage::TimeShiftState; use crate::planner::sql_evaluator::sql_nodes::calendar_time_shift::CalendarTimeShiftSqlNode; +use crate::planner::sql_evaluator::sql_nodes::cube_calc_groups::CalcGroupsItems; +use crate::planner::sql_evaluator::sql_nodes::CubeCalcGroupsSqlNode; use crate::planner::sql_evaluator::symbols::CalendarDimensionTimeShift; use std::collections::{HashMap, HashSet}; use std::rc::Rc; @@ -31,6 +33,7 @@ pub struct SqlNodesFactory { dimensions_with_ignored_timezone: HashSet, use_local_tz_in_date_range: bool, original_sql_pre_aggregations: HashMap, + calc_groups: CalcGroupsItems, } impl SqlNodesFactory { @@ -53,6 +56,7 @@ impl SqlNodesFactory { dimensions_with_ignored_timezone: HashSet::new(), use_local_tz_in_date_range: false, original_sql_pre_aggregations: HashMap::new(), + calc_groups: CalcGroupsItems::default(), } } @@ -91,6 +95,15 @@ impl SqlNodesFactory { &self.render_references } + pub fn add_calc_group_item( + &mut self, + cube_name: String, + dimension_name: String, + values: Vec, + ) { + self.calc_groups.add(cube_name, dimension_name, values); + } + pub fn set_rendered_as_multiplied_measures(&mut self, value: HashSet) { self.rendered_as_multiplied_measures = value; } @@ -192,13 +205,15 @@ impl SqlNodesFactory { } fn cube_table_processor(&self, default: Rc) -> Rc { - if !self.original_sql_pre_aggregations.is_empty() { - OriginalSqlPreAggregationSqlNode::new( - default, - self.original_sql_pre_aggregations.clone(), - ) + let input = if !self.calc_groups.is_empty() { + CubeCalcGroupsSqlNode::new(default, self.calc_groups.clone()) } else { default + }; + if !self.original_sql_pre_aggregations.is_empty() { + OriginalSqlPreAggregationSqlNode::new(input, self.original_sql_pre_aggregations.clone()) + } else { + input } } fn add_ungrouped_measure_reference_if_needed( diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/mod.rs index 222765f6adbba..c7a5525c56fab 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/mod.rs @@ -1,6 +1,7 @@ pub mod auto_prefix; pub mod calendar_time_shift; pub mod case_dimension; +pub mod cube_calc_groups; pub mod evaluate_sql; pub mod factory; pub mod final_measure; @@ -21,6 +22,7 @@ pub mod ungroupped_query_final_measure; pub use auto_prefix::AutoPrefixSqlNode; pub use case_dimension::CaseDimensionSqlNode; +pub use cube_calc_groups::CubeCalcGroupsSqlNode; pub use evaluate_sql::EvaluateSqlNode; pub use factory::SqlNodesFactory; pub use final_measure::FinalMeasureSqlNode; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/cube_symbol.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/cube_symbol.rs index 7afdb2e5b8c5a..b17c748d189d1 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/cube_symbol.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/cube_symbol.rs @@ -72,8 +72,7 @@ impl SymbolFactory for CubeNameSymbolFactory { pub struct CubeTableSymbol { cube_name: String, member_sql: Option>, - #[allow(dead_code)] - definition: Rc, + alias: String, is_table_sql: bool, } @@ -81,13 +80,13 @@ impl CubeTableSymbol { pub fn new( cube_name: String, member_sql: Option>, - definition: Rc, + alias: String, is_table_sql: bool, ) -> Rc { Rc::new(Self { cube_name, member_sql, - definition, + alias, is_table_sql, }) } @@ -132,7 +131,7 @@ impl CubeTableSymbol { } pub fn alias(&self) -> String { - PlanSqlTemplates::alias_name(&self.cube_name) + self.alias.clone() } } @@ -194,10 +193,15 @@ impl SymbolFactory for CubeTableSymbolFactory { } else { None }; + let alias = if let Some(alias) = definition.static_data().sql_alias.clone() { + alias.clone() + } else { + PlanSqlTemplates::alias_name(&cube_name) + }; Ok(MemberSymbol::new_cube_table(CubeTableSymbol::new( cube_name, sql, - definition, + alias, is_table_sql, ))) } diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs index f0246f5d71189..28619be26a633 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs @@ -4,8 +4,8 @@ use crate::cube_bridge::dimension_definition::DimensionDefinition; use crate::cube_bridge::evaluator::CubeEvaluator; use crate::cube_bridge::member_sql::MemberSql; use crate::planner::query_tools::QueryTools; -use crate::planner::sql_evaluator::TimeDimensionSymbol; use crate::planner::sql_evaluator::{sql_nodes::SqlNode, Compiler, SqlCall, SqlEvaluatorVisitor}; +use crate::planner::sql_evaluator::{CubeTableSymbol, TimeDimensionSymbol}; use crate::planner::sql_templates::PlanSqlTemplates; use crate::planner::GranularityHelper; use crate::planner::SqlInterval; @@ -35,53 +35,69 @@ pub struct CalendarDimensionTimeShift { } pub struct DimensionSymbol { - cube_name: String, + cube: Rc, name: String, + dimension_type: String, alias: String, member_sql: Option>, latitude: Option>, longitude: Option>, + values: Vec, case: Option, definition: Rc, is_reference: bool, // Symbol is a direct reference to another symbol without any calculations is_view: bool, time_shift: Vec, time_shift_pk_full_name: Option, - is_self_time_shift_pk: bool, // If the dimension itself is a primary key and has time shifts, - // we can not reevaluate itself again while processing time shifts - // to avoid infinite recursion. So we raise this flag instead. + is_self_time_shift_pk: bool, // If the dimension itself is a primary key and has time shifts, we can not reevaluate itself again while processing time shifts to avoid infinite recursion. So we raise this flag instead. + owned_by_cube: bool, + is_multi_stage: bool, + is_sub_query: bool, + propagate_filters_to_sub_query: bool, } impl DimensionSymbol { pub fn new( - cube_name: String, + cube: Rc, name: String, + dimension_type: String, alias: String, member_sql: Option>, is_reference: bool, is_view: bool, latitude: Option>, longitude: Option>, + values: Vec, case: Option, definition: Rc, time_shift: Vec, time_shift_pk_full_name: Option, is_self_time_shift_pk: bool, + owned_by_cube: bool, + is_multi_stage: bool, + is_sub_query: bool, + propagate_filters_to_sub_query: bool, ) -> Rc { Rc::new(Self { - cube_name, + cube, name, + dimension_type, alias, member_sql, is_reference, latitude, longitude, + values, definition, case, is_view, time_shift, time_shift_pk_full_name, is_self_time_shift_pk, + owned_by_cube, + is_multi_stage, + is_sub_query, + propagate_filters_to_sub_query, }) } @@ -92,7 +108,13 @@ impl DimensionSymbol { query_tools: Rc, templates: &PlanSqlTemplates, ) -> Result { - if let Some(member_sql) = &self.member_sql { + if self.dimension_type == "switch" { + Ok(format!( + "{}.{}", + templates.quote_identifier(&self.cube.alias())?, + templates.quote_identifier(&self.name)? + )) + } else if let Some(member_sql) = &self.member_sql { let sql = member_sql.eval(visitor, node_processor, query_tools, templates)?; Ok(sql) } else { @@ -103,6 +125,14 @@ impl DimensionSymbol { } } + pub fn is_calc_group(&self) -> bool { + self.dimension_type == "switch" + } + + pub fn values(&self) -> &Vec { + &self.values + } + pub fn latitude(&self) -> Option> { self.latitude.clone() } @@ -128,7 +158,7 @@ impl DimensionSymbol { } pub fn full_name(&self) -> String { - format!("{}.{}", self.cube_name, self.name) + format!("{}.{}", self.cube.cube_name(), self.name) } pub fn alias(&self) -> String { @@ -136,26 +166,23 @@ impl DimensionSymbol { } pub fn owned_by_cube(&self) -> bool { - self.definition.static_data().owned_by_cube.unwrap_or(true) + self.owned_by_cube } pub fn is_multi_stage(&self) -> bool { - self.definition.static_data().multi_stage.unwrap_or(false) + self.is_multi_stage } pub fn is_sub_query(&self) -> bool { - self.definition.static_data().sub_query.unwrap_or(false) + self.is_sub_query } pub fn dimension_type(&self) -> &String { - &self.definition.static_data().dimension_type + &self.dimension_type } pub fn propagate_filters_to_sub_query(&self) -> bool { - self.definition - .static_data() - .propagate_filters_to_sub_query - .unwrap_or(false) + self.propagate_filters_to_sub_query } pub fn is_reference(&self) -> bool { @@ -236,7 +263,7 @@ impl DimensionSymbol { } pub fn cube_name(&self) -> &String { - &self.cube_name + &self.cube.cube_name() } pub fn definition(&self) -> &Rc { @@ -348,6 +375,9 @@ impl SymbolFactory for DimensionSymbolFactory { definition, cube_evaluator, } = self; + + let dimension_type = definition.static_data().dimension_type.clone(); + let sql = if let Some(sql) = sql { Some(compiler.compile_sql_call(&cube_name, sql)?) } else { @@ -360,7 +390,7 @@ impl SymbolFactory for DimensionSymbolFactory { false }; - let (latitude, longitude) = if definition.static_data().dimension_type == "geo" { + let (latitude, longitude) = if dimension_type == "geo" { if let (Some(latitude_item), Some(longitude_item)) = (definition.latitude()?, definition.longitude()?) { @@ -471,6 +501,12 @@ impl SymbolFactory for DimensionSymbolFactory { None }; + let values = if definition.static_data().dimension_type == "switch" { + definition.static_data().values.clone().unwrap_or_default() + } else { + vec![] + }; + let owned_by_cube = definition.static_data().owned_by_cube.unwrap_or(true); let is_sub_query = definition.static_data().sub_query.unwrap_or(false); let is_multi_stage = definition.static_data().multi_stage.unwrap_or(false); @@ -483,20 +519,35 @@ impl SymbolFactory for DimensionSymbolFactory { && longitude.is_none() && !is_multi_stage); + let propagate_filters_to_sub_query = definition + .static_data() + .propagate_filters_to_sub_query + .unwrap_or(false); + + let cube_symbol = compiler + .add_cube_table_evaluator(cube_name.clone())? + .as_cube_table()?; + let symbol = MemberSymbol::new_dimension(DimensionSymbol::new( - cube_name.clone(), + cube_symbol, name.clone(), + dimension_type, alias, sql, is_reference, is_view, latitude, longitude, + values, case, definition, time_shift, time_shift_pk, is_self_time_shift_pk, + owned_by_cube, + is_multi_stage, + is_sub_query, + propagate_filters_to_sub_query, )); if let Some(granularity) = &granularity { diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/measure_symbol.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/measure_symbol.rs index 7452bf45f1448..c658082902d58 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/measure_symbol.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/measure_symbol.rs @@ -4,6 +4,7 @@ use crate::cube_bridge::measure_definition::{MeasureDefinition, RollingWindow}; use crate::cube_bridge::member_sql::MemberSql; use crate::planner::query_tools::QueryTools; use crate::planner::sql_evaluator::collectors::find_owned_by_cube_child; +use crate::planner::sql_evaluator::CubeTableSymbol; use crate::planner::sql_evaluator::{sql_nodes::SqlNode, Compiler, SqlCall, SqlEvaluatorVisitor}; use crate::planner::sql_templates::PlanSqlTemplates; use crate::planner::SqlInterval; @@ -62,7 +63,7 @@ pub enum MeasureTimeShifts { #[derive(Clone)] pub struct MeasureSymbol { - cube_name: String, + cube: Rc, name: String, alias: String, owned_by_cube: bool, @@ -85,7 +86,7 @@ pub struct MeasureSymbol { impl MeasureSymbol { pub fn new( - cube_name: String, + cube: Rc, name: String, alias: String, member_sql: Option>, @@ -106,7 +107,7 @@ impl MeasureSymbol { let rolling_window = definition.static_data().rolling_window.clone(); let is_multi_stage = definition.static_data().multi_stage.unwrap_or(false); Rc::new(Self { - cube_name, + cube, name, alias, member_sql, @@ -136,7 +137,7 @@ impl MeasureSymbol { self.measure_type.clone() }; Rc::new(Self { - cube_name: self.cube_name.clone(), + cube: self.cube.clone(), name: self.name.clone(), alias: self.alias.clone(), owned_by_cube: self.owned_by_cube, @@ -219,7 +220,7 @@ impl MeasureSymbol { measure_filters.extend(add_filters.into_iter()); } Ok(Rc::new(Self { - cube_name: self.cube_name.clone(), + cube: self.cube.clone(), name: self.name.clone(), alias: self.alias.clone(), owned_by_cube: self.owned_by_cube, @@ -242,7 +243,7 @@ impl MeasureSymbol { } pub fn full_name(&self) -> String { - format!("{}.{}", self.cube_name, self.name) + format!("{}.{}", self.cube.cube_name(), self.name) } pub fn alias(&self) -> String { @@ -447,7 +448,7 @@ impl MeasureSymbol { } pub fn cube_name(&self) -> &String { - &self.cube_name + &self.cube.cube_name() } pub fn name(&self) -> &String { &self.name @@ -713,8 +714,12 @@ impl SymbolFactory for MeasureSymbolFactory { && add_group_by.is_none() && group_by.is_none()); + let cube_symbol = compiler + .add_cube_table_evaluator(cube_name.clone())? + .as_cube_table()?; + Ok(MemberSymbol::new_measure(MeasureSymbol::new( - cube_name, + cube_symbol, name, alias, sql, diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/member_symbol.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/member_symbol.rs index 300310e360dcd..066970daedea5 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/member_symbol.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/member_symbol.rs @@ -245,6 +245,16 @@ impl MemberSymbol { } } + pub fn as_cube_table(&self) -> Result, CubeError> { + match self { + Self::CubeTable(c) => Ok(c.clone()), + _ => Err(CubeError::internal(format!( + "{} is not a cube table", + self.full_name() + ))), + } + } + pub fn as_member_expression(&self) -> Result, CubeError> { match self { Self::MemberExpression(m) => Ok(m.clone()), diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/plan.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/plan.rs index f5b9f47208f78..b60950db9497b 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/plan.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/plan.rs @@ -2,6 +2,7 @@ use super::{TemplateGroupByColumn, TemplateOrderByColumn, TemplateProjectionColu use crate::cube_bridge::driver_tools::DriverTools; use crate::cube_bridge::sql_templates_render::SqlTemplatesRender; use crate::plan::join::JoinType; +use crate::planner::sql_templates::structs::TemplateCalcGroup; use convert_case::{Boundary, Case, Casing}; use cubenativeutils::CubeError; use minijinja::context; @@ -344,6 +345,22 @@ impl PlanSqlTemplates { ) } + pub fn calc_groups_join( + &self, + original_cube: &str, + original_cube_sql: &str, + groups: Vec, + ) -> Result { + self.render.render_template( + "statements/calc_groups_join", + context! { + original_cube => original_cube, + original_cube_sql => original_cube_sql, + groups => groups + }, + ) + } + pub fn select( &self, ctes: Vec, diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/structs.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/structs.rs index 39924ba8793b4..80c1eea67dfc0 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/structs.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/structs.rs @@ -17,3 +17,9 @@ pub struct TemplateGroupByColumn { pub struct TemplateOrderByColumn { pub expr: String, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TemplateCalcGroup { + pub name: String, + pub values: Vec, +} From 1d8adb06e37c49bf43adb12ee6f3071bd4aec807 Mon Sep 17 00:00:00 2001 From: Alexandr Romanenko Date: Thu, 4 Sep 2025 21:04:42 +0200 Subject: [PATCH 02/41] in work --- .../src/compiler/CubeValidator.ts | 59 +++++++++++++------ .../transpilers/CubePropContextTranspiler.ts | 1 + .../integration/postgres/calc-groups.test.ts | 11 ++-- .../src/cube_bridge/case_switch_else_item.rs | 17 ++++++ .../src/cube_bridge/case_switch_item.rs | 18 ++++++ .../cubesqlplanner/src/cube_bridge/mod.rs | 2 + 6 files changed, 84 insertions(+), 24 deletions(-) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_else_item.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_item.rs diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts index 1ce82885b6594..35326ca3171c0 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts @@ -657,30 +657,51 @@ const SwitchDimension = Joi.object({ values: Joi.array().items(Joi.string()).min(1).required() }); +const CaseSchema = Joi.object().keys({ + when: Joi.array().items(Joi.object().keys({ + sql: Joi.func().required(), + label: Joi.alternatives([ + Joi.string(), + Joi.object().keys({ + sql: Joi.func().required() + }) + ]) + })), + else: Joi.object().keys({ + label: Joi.alternatives([ + Joi.string(), + Joi.object().keys({ + sql: Joi.func().required() + }) + ]) + }) +}).required(); + +const SwitchCaseSchema = Joi.object().keys({ + switch: Joi.alternatives([ + Joi.string(), + Joi.func() + ]).required(), + when: Joi.array().items(Joi.object().keys({ + value: Joi.string().required(), + sql: Joi.func().required() + })), + else: Joi.object().keys({ + sql: Joi.func().required() + }) +}).required(); + +const CaseVariants = Joi.alternatives().try( + CaseSchema, + SwitchCaseSchema +); + const DimensionsSchema = Joi.object().pattern(identifierRegex, Joi.alternatives().conditional(Joi.ref('.type'), { is: 'switch', then: SwitchDimension, otherwise: Joi.alternatives().try( inherit(BaseDimensionWithoutSubQuery, { - case: Joi.object().keys({ - when: Joi.array().items(Joi.object().keys({ - sql: Joi.func().required(), - label: Joi.alternatives([ - Joi.string(), - Joi.object().keys({ - sql: Joi.func().required() - }) - ]) - })), - else: Joi.object().keys({ - label: Joi.alternatives([ - Joi.string(), - Joi.object().keys({ - sql: Joi.func().required() - }) - ]) - }) - }).required() + case: CaseVariants }), inherit(BaseDimensionWithoutSubQuery, { latitude: Joi.object().keys({ diff --git a/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts b/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts index e027e2a0dff3b..8713e3e10e060 100644 --- a/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts +++ b/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts @@ -20,6 +20,7 @@ export const transpiledFieldsPatterns: Array = [ /^measures\.[_a-zA-Z][_a-zA-Z0-9]*\.(timeShift|time_shift)\.[0-9]+\.(timeDimension|time_dimension)$/, /^measures\.[_a-zA-Z][_a-zA-Z0-9]*\.(reduceBy|reduce_by|groupBy|group_by|addGroupBy|add_group_by)$/, /^dimensions\.[_a-zA-Z][_a-zA-Z0-9]*\.(reduceBy|reduce_by|groupBy|group_by|addGroupBy|add_group_by)$/, + /^dimensions\.[_a-zA-Z][_a-zA-Z0-9]*\.case\.switch$/, /^(preAggregations|pre_aggregations)\.[_a-zA-Z][_a-zA-Z0-9]*\.indexes\.[_a-zA-Z][_a-zA-Z0-9]*\.columns$/, /^(preAggregations|pre_aggregations)\.[_a-zA-Z][_a-zA-Z0-9]*\.(timeDimensionReference|timeDimension|time_dimension|segments|dimensions|measures|rollups|segmentReferences|dimensionReferences|measureReferences|rollupReferences)$/, /^(preAggregations|pre_aggregations)\.[_a-zA-Z][_a-zA-Z0-9]*\.(timeDimensions|time_dimensions)\.\d+\.dimension$/, diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts index 1f098f6ca1b12..82530cd5db830 100644 --- a/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts +++ b/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts @@ -54,13 +54,14 @@ cubes: - name: test type: string case: + switch: "{CUBE}.currency" when: - - sql: "{CUBE.currency} = 'USD'" - label: '111' - - sql: "{CUBE.currency} = 'EUR'" - label: '333' + - value: USD + sql: "'111'" + - value: EUR + sql: "'333'" else: - label: 'def' + sql: "'def'" - name: strategy type: switch diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_else_item.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_else_item.rs new file mode 100644 index 0000000000000..db85419848f04 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_else_item.rs @@ -0,0 +1,17 @@ +use crate::cube_bridge::member_sql::{MemberSql, NativeMemberSql}; + +use super::case_label::CaseLabel; +use cubenativeutils::wrappers::serializer::{ + NativeDeserialize, NativeDeserializer, NativeSerialize, +}; +use cubenativeutils::wrappers::NativeContextHolder; +use cubenativeutils::wrappers::NativeObjectHandle; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; + +#[nativebridge::native_bridge] +pub trait CaseSwitchElseItem { + #[nbridge(field)] + fn sql(&self) -> Result, CubeError>; +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_item.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_item.rs new file mode 100644 index 0000000000000..1d9a527ecbccf --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_item.rs @@ -0,0 +1,18 @@ +use super::case_label::CaseLabel; +use super::member_sql::{MemberSql, NativeMemberSql}; +use cubenativeutils::wrappers::serializer::{ + NativeDeserialize, NativeDeserializer, NativeSerialize, +}; +use cubenativeutils::wrappers::NativeContextHolder; +use cubenativeutils::wrappers::NativeObjectHandle; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; + +#[nativebridge::native_bridge] +pub trait CaseSwitchItem { + #[nbridge(field)] + fn value(&self) -> Result; + #[nbridge(field)] + fn sql(&self) -> Result, CubeError>; +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/mod.rs index 69286f9c1a44b..dd9c225dc5b31 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/mod.rs @@ -4,6 +4,8 @@ pub mod case_definition; pub mod case_else_item; pub mod case_item; pub mod case_label; +pub mod case_switch_else_item; +pub mod case_switch_item; pub mod cube_definition; pub mod dimension_definition; pub mod driver_tools; From ce13c9e70efa3184cdcb923bd414714f4f5fb76e Mon Sep 17 00:00:00 2001 From: Alexandr Romanenko Date: Fri, 5 Sep 2025 15:18:33 +0200 Subject: [PATCH 03/41] case expression --- .../src/compiler/CubeValidator.ts | 5 +- .../integration/postgres/calc-groups.test.ts | 87 +++++---- .../src/cube_bridge/case_else_item.rs | 4 +- .../src/cube_bridge/case_item.rs | 4 +- .../src/cube_bridge/case_switch_definition.rs | 24 +++ .../src/cube_bridge/case_switch_else_item.rs | 1 - .../src/cube_bridge/case_switch_item.rs | 11 +- .../src/cube_bridge/case_variant.rs | 28 +++ .../src/cube_bridge/dimension_definition.rs | 4 +- .../cubesqlplanner/src/cube_bridge/mod.rs | 4 +- .../{case_label.rs => string_or_sql.rs} | 6 +- .../src/planner/sql_evaluator/sql_call.rs | 16 +- .../sql_evaluator/sql_nodes/case_dimension.rs | 116 +++++++++--- .../sql_evaluator/symbols/common/case.rs | 169 ++++++++++++++++++ .../sql_evaluator/symbols/common/mod.rs | 3 + .../sql_evaluator/symbols/dimension_symbol.rs | 54 +----- .../src/planner/sql_evaluator/symbols/mod.rs | 2 + 17 files changed, 409 insertions(+), 129 deletions(-) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_definition.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_variant.rs rename rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/{case_label.rs => string_or_sql.rs} (91%) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/case.rs create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/mod.rs diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts index 35326ca3171c0..0150cf023b4de 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts @@ -678,10 +678,7 @@ const CaseSchema = Joi.object().keys({ }).required(); const SwitchCaseSchema = Joi.object().keys({ - switch: Joi.alternatives([ - Joi.string(), - Joi.func() - ]).required(), + switch: Joi.func().required(), when: Joi.array().items(Joi.object().keys({ value: Joi.string().required(), sql: Joi.func().required() diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts index 82530cd5db830..e88e8a8e5675e 100644 --- a/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts +++ b/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts @@ -51,23 +51,23 @@ cubes: - EUR - GBP - - name: test + - name: strategy + type: switch + values: + - A + - B + + - name: currency_full_name type: string case: - switch: "{CUBE}.currency" + switch: "{CUBE.currency}" when: - value: USD - sql: "'111'" + sql: "'dollars'" - value: EUR - sql: "'333'" + sql: "'euros'" else: - sql: "'def'" - - - name: strategy - type: switch - values: - - A - - B + sql: "'unknown'" measures: - name: count @@ -460,13 +460,14 @@ views: ], { joinGraph, cubeEvaluator, compiler })); - it('measure switch', async () => dbRunner.runQueryTest({ - dimensions: ['orders.currency', 'orders.test'], + it('dimension switch expression simple', async () => dbRunner.runQueryTest({ + dimensions: ['orders.currency', 'orders.currency_full_name'], measures: ['orders.revenue'], timeDimensions: [ { dimension: 'orders.date', - granularity: 'year' + granularity: 'year', + dateRange: ['2024-01-01', '2026-01-01'] } ], timezone: 'UTC', @@ -477,27 +478,43 @@ views: }, ], }, [ - { - orders__date_year: '2022-01-01T00:00:00.000Z', - orders__strategy: 'B', - orders__revenue: '5', - }, - { - orders__date_year: '2023-01-01T00:00:00.000Z', - orders__strategy: 'B', - orders__revenue: '15', - }, - { - orders__date_year: '2024-01-01T00:00:00.000Z', - orders__strategy: 'B', - orders__revenue: '30', - }, - { - orders__date_year: '2025-01-01T00:00:00.000Z', - orders__strategy: 'B', - orders__revenue: '5', - } - ], + { + orders__currency: 'EUR', + orders__currency_full_name: 'euros', + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__revenue: '30' + }, + { + orders__currency: 'GBP', + orders__currency_full_name: 'unknown', + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__revenue: '30' + }, + { + orders__currency: 'USD', + orders__currency_full_name: 'dollars', + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__revenue: '30' + }, + { + orders__currency: 'EUR', + orders__currency_full_name: 'euros', + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__revenue: '5' + }, + { + orders__currency: 'GBP', + orders__currency_full_name: 'unknown', + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__revenue: '5' + }, + { + orders__currency: 'USD', + orders__currency_full_name: 'dollars', + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__revenue: '5' + } + ], { joinGraph, cubeEvaluator, compiler })); } else { // This test is working only in tesseract diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_else_item.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_else_item.rs index 250dbd135479c..8dacb0f549b55 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_else_item.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_else_item.rs @@ -1,4 +1,4 @@ -use super::case_label::CaseLabel; +use super::string_or_sql::StringOrSql; use cubenativeutils::wrappers::serializer::{ NativeDeserialize, NativeDeserializer, NativeSerialize, }; @@ -11,5 +11,5 @@ use std::rc::Rc; #[nativebridge::native_bridge] pub trait CaseElseItem { #[nbridge(field)] - fn label(&self) -> Result; + fn label(&self) -> Result; } diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_item.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_item.rs index 2d6d4865cace5..7573ed861e03e 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_item.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_item.rs @@ -1,4 +1,4 @@ -use super::case_label::CaseLabel; +use super::string_or_sql::StringOrSql; use super::member_sql::{MemberSql, NativeMemberSql}; use cubenativeutils::wrappers::serializer::{ NativeDeserialize, NativeDeserializer, NativeSerialize, @@ -14,5 +14,5 @@ pub trait CaseItem { #[nbridge(field)] fn sql(&self) -> Result, CubeError>; #[nbridge(field)] - fn label(&self) -> Result; + fn label(&self) -> Result; } diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_definition.rs new file mode 100644 index 0000000000000..e811527c5d8a8 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_definition.rs @@ -0,0 +1,24 @@ +use crate::cube_bridge::member_sql::{MemberSql, NativeMemberSql}; + +use super::case_switch_else_item::{CaseSwitchElseItem, NativeCaseSwitchElseItem}; +use super::case_switch_item::{CaseSwitchItem, NativeCaseSwitchItem}; +use super::string_or_sql::StringOrSql; +use cubenativeutils::wrappers::serializer::{ + NativeDeserialize, NativeDeserializer, NativeSerialize, +}; +use cubenativeutils::wrappers::NativeArray; +use cubenativeutils::wrappers::NativeContextHolder; +use cubenativeutils::wrappers::NativeObjectHandle; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; + +#[nativebridge::native_bridge] +pub trait CaseSwitchDefinition { + #[nbridge(field)] + fn switch(&self) -> Result, CubeError>; + #[nbridge(field, vec)] + fn when(&self) -> Result>, CubeError>; + #[nbridge(field, rename = "else")] + fn else_sql(&self) -> Result, CubeError>; +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_else_item.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_else_item.rs index db85419848f04..1c1131714e032 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_else_item.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_else_item.rs @@ -1,6 +1,5 @@ use crate::cube_bridge::member_sql::{MemberSql, NativeMemberSql}; -use super::case_label::CaseLabel; use cubenativeutils::wrappers::serializer::{ NativeDeserialize, NativeDeserializer, NativeSerialize, }; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_item.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_item.rs index 1d9a527ecbccf..95406dad4444f 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_item.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_item.rs @@ -1,4 +1,3 @@ -use super::case_label::CaseLabel; use super::member_sql::{MemberSql, NativeMemberSql}; use cubenativeutils::wrappers::serializer::{ NativeDeserialize, NativeDeserializer, NativeSerialize, @@ -6,13 +5,17 @@ use cubenativeutils::wrappers::serializer::{ use cubenativeutils::wrappers::NativeContextHolder; use cubenativeutils::wrappers::NativeObjectHandle; use cubenativeutils::CubeError; +use serde::{Deserialize, Serialize}; use std::any::Any; use std::rc::Rc; -#[nativebridge::native_bridge] +#[derive(Serialize, Deserialize, Debug)] +pub struct CaseSwitchItemStatic { + pub value: String, +} + +#[nativebridge::native_bridge(CaseSwitchItemStatic)] pub trait CaseSwitchItem { - #[nbridge(field)] - fn value(&self) -> Result; #[nbridge(field)] fn sql(&self) -> Result, CubeError>; } diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_variant.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_variant.rs new file mode 100644 index 0000000000000..769014858ccce --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_variant.rs @@ -0,0 +1,28 @@ +use crate::cube_bridge::case_definition::{CaseDefinition, NativeCaseDefinition}; +use crate::cube_bridge::case_switch_definition::{ + CaseSwitchDefinition, NativeCaseSwitchDefinition, +}; + +use super::struct_with_sql_member::{NativeStructWithSqlMember, StructWithSqlMember}; +use cubenativeutils::wrappers::inner_types::InnerTypes; +use cubenativeutils::wrappers::serializer::NativeDeserialize; +use cubenativeutils::wrappers::NativeObjectHandle; +use cubenativeutils::CubeError; +use std::rc::Rc; + +pub enum CaseVariant { + Case(Rc), + CaseSwitch(Rc), +} + +impl NativeDeserialize for CaseVariant { + fn from_native(native_object: NativeObjectHandle) -> Result { + match NativeCaseSwitchDefinition::from_native(native_object.clone()) { + Ok(case) => Ok(Self::CaseSwitch(Rc::new(case))), + Err(_) => match NativeCaseDefinition::from_native(native_object) { + Ok(case) => Ok(Self::Case(Rc::new(case))), + Err(_) => Err(CubeError::user(format!("Case or Case Switch expected"))), + }, + } + } +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/dimension_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/dimension_definition.rs index 639de20d56309..7706f95455705 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/dimension_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/dimension_definition.rs @@ -1,4 +1,4 @@ -use super::case_definition::{CaseDefinition, NativeCaseDefinition}; +use super::case_variant::CaseVariant; use super::geo_item::{GeoItem, NativeGeoItem}; use super::member_sql::{MemberSql, NativeMemberSql}; use crate::cube_bridge::timeshift_definition::{NativeTimeShiftDefinition, TimeShiftDefinition}; @@ -34,7 +34,7 @@ pub trait DimensionDefinition { fn sql(&self) -> Result>, CubeError>; #[nbridge(field, optional)] - fn case(&self) -> Result>, CubeError>; + fn case(&self) -> Result, CubeError>; #[nbridge(field, optional)] fn latitude(&self) -> Result>, CubeError>; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/mod.rs index dd9c225dc5b31..83b2657850e2f 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/mod.rs @@ -3,9 +3,10 @@ pub mod base_tools; pub mod case_definition; pub mod case_else_item; pub mod case_item; -pub mod case_label; +pub mod case_switch_definition; pub mod case_switch_else_item; pub mod case_switch_item; +pub mod case_variant; pub mod cube_definition; pub mod dimension_definition; pub mod driver_tools; @@ -31,5 +32,6 @@ pub mod security_context; pub mod segment_definition; pub mod sql_templates_render; pub mod sql_utils; +pub mod string_or_sql; pub mod struct_with_sql_member; pub mod timeshift_definition; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_label.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/string_or_sql.rs similarity index 91% rename from rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_label.rs rename to rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/string_or_sql.rs index 6f9d8fafb1ecf..0c45e0ad071e0 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_label.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/string_or_sql.rs @@ -5,12 +5,12 @@ use cubenativeutils::wrappers::NativeObjectHandle; use cubenativeutils::CubeError; use std::rc::Rc; -pub enum CaseLabel { +pub enum StringOrSql { String(String), MemberSql(Rc), } -impl NativeDeserialize for CaseLabel { +impl NativeDeserialize for StringOrSql { fn from_native(native_object: NativeObjectHandle) -> Result { match String::from_native(native_object.clone()) { Ok(label) => Ok(Self::String(label)), @@ -22,4 +22,4 @@ impl NativeDeserialize for CaseLabel { }, } } -} +} \ No newline at end of file diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_call.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_call.rs index 464be582a6fd5..ca4535400631f 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_call.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_call.rs @@ -53,9 +53,16 @@ impl SqlCall { } pub fn is_direct_reference(&self, base_tools: Rc) -> Result { + Ok(self.resolve_direct_reference(base_tools)?.is_some()) + } + + pub fn resolve_direct_reference( + &self, + base_tools: Rc, + ) -> Result>, CubeError> { let dependencies = self.get_dependencies(); if dependencies.len() != 1 { - return Ok(false); + return Ok(None); } let reference_candidate = dependencies[0].clone(); @@ -67,7 +74,12 @@ impl SqlCall { .collect::, _>>()?; let eval_result = self.member_sql.call(args)?; - Ok(eval_result.trim() == reference_candidate.full_name()) + let res = if eval_result.trim() == reference_candidate.full_name() { + Some(reference_candidate.clone()) + } else { + None + }; + Ok(res) } pub fn get_dependencies(&self) -> Vec> { diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/case_dimension.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/case_dimension.rs index 64088650984b5..b1dd38f14ddf9 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/case_dimension.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/case_dimension.rs @@ -1,6 +1,8 @@ use super::SqlNode; use crate::planner::query_tools::QueryTools; -use crate::planner::sql_evaluator::DimenstionCaseLabel; +use crate::planner::sql_evaluator::symbols::{ + Case, CaseDefinition, CaseLabel, CaseSwitchDefinition, CaseSwitchItem, +}; use crate::planner::sql_evaluator::MemberSymbol; use crate::planner::sql_evaluator::SqlEvaluatorVisitor; use crate::planner::sql_templates::PlanSqlTemplates; @@ -20,6 +22,83 @@ impl CaseDimensionSqlNode { pub fn input(&self) -> &Rc { &self.input } + + pub fn case_to_sql( + &self, + visitor: &SqlEvaluatorVisitor, + case: &CaseDefinition, + query_tools: Rc, + node_processor: Rc, + templates: &PlanSqlTemplates, + ) -> Result { + let mut when_then = Vec::new(); + for itm in case.items.iter() { + let when = itm.sql.eval( + visitor, + node_processor.clone(), + query_tools.clone(), + templates, + )?; + let then = match &itm.label { + CaseLabel::String(s) => templates.quote_string(&s)?, + CaseLabel::Sql(sql) => sql.eval( + visitor, + node_processor.clone(), + query_tools.clone(), + templates, + )?, + }; + when_then.push((when, then)); + } + let else_label = match &case.else_label { + CaseLabel::String(s) => templates.quote_string(&s)?, + CaseLabel::Sql(sql) => sql.eval( + visitor, + node_processor.clone(), + query_tools.clone(), + templates, + )?, + }; + templates.case(None, when_then, Some(else_label)) + } + pub fn case_switch_to_sql( + &self, + visitor: &SqlEvaluatorVisitor, + case: &CaseSwitchDefinition, + query_tools: Rc, + node_processor: Rc, + templates: &PlanSqlTemplates, + ) -> Result { + let expr = match &case.switch { + CaseSwitchItem::Symbol(member_symbol) => { + visitor.apply(member_symbol, node_processor.clone(), templates)? + } + CaseSwitchItem::Sql(sql_call) => sql_call.eval( + visitor, + node_processor.clone(), + query_tools.clone(), + templates, + )?, + }; + let mut when_then = Vec::new(); + for itm in case.items.iter() { + let when = templates.quote_string(&itm.value)?; + let then = itm.sql.eval( + visitor, + node_processor.clone(), + query_tools.clone(), + templates, + )?; + when_then.push((when, then)); + } + let else_label = case.else_sql.eval( + visitor, + node_processor.clone(), + query_tools.clone(), + templates, + )?; + templates.case(Some(expr), when_then, Some(else_label)) + } } impl SqlNode for CaseDimensionSqlNode { @@ -34,35 +113,18 @@ impl SqlNode for CaseDimensionSqlNode { let res = match node.as_ref() { MemberSymbol::Dimension(ev) => { if let Some(case) = ev.case() { - let mut when_then = Vec::new(); - for itm in case.items.iter() { - let when = itm.sql.eval( + match case { + Case::Case(case) => { + self.case_to_sql(visitor, case, query_tools, node_processor, templates)? + } + Case::CaseSwitch(case) => self.case_switch_to_sql( visitor, - node_processor.clone(), - query_tools.clone(), - templates, - )?; - let then = match &itm.label { - DimenstionCaseLabel::String(s) => templates.quote_string(&s)?, - DimenstionCaseLabel::Sql(sql) => sql.eval( - visitor, - node_processor.clone(), - query_tools.clone(), - templates, - )?, - }; - when_then.push((when, then)); - } - let else_label = match &case.else_label { - DimenstionCaseLabel::String(s) => templates.quote_string(&s)?, - DimenstionCaseLabel::Sql(sql) => sql.eval( - visitor, - node_processor.clone(), - query_tools.clone(), + case, + query_tools, + node_processor, templates, )?, - }; - templates.case(None, when_then, Some(else_label))? + } } else { self.input.to_sql( visitor, diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/case.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/case.rs new file mode 100644 index 0000000000000..447a3547f601f --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/case.rs @@ -0,0 +1,169 @@ +use crate::{ + cube_bridge::{case_variant::CaseVariant, string_or_sql::StringOrSql}, + planner::sql_evaluator::{Compiler, MemberSymbol, SqlCall}, +}; +use cubenativeutils::CubeError; +use std::rc::Rc; + +pub enum CaseLabel { + String(String), + Sql(Rc), +} + +pub struct CaseWhenItem { + pub sql: Rc, + pub label: CaseLabel, +} + +pub struct CaseDefinition { + pub items: Vec, + pub else_label: CaseLabel, +} + +impl CaseDefinition { + fn extract_symbol_deps(&self, result: &mut Vec>) { + for itm in self.items.iter() { + itm.sql.extract_symbol_deps(result); + if let CaseLabel::Sql(sql) = &itm.label { + sql.extract_symbol_deps(result); + } + } + if let CaseLabel::Sql(sql) = &self.else_label { + sql.extract_symbol_deps(result); + } + } + fn extract_symbol_deps_with_path(&self, result: &mut Vec<(Rc, Vec)>) { + for itm in self.items.iter() { + itm.sql.extract_symbol_deps_with_path(result); + if let CaseLabel::Sql(sql) = &itm.label { + sql.extract_symbol_deps_with_path(result); + } + } + if let CaseLabel::Sql(sql) = &self.else_label { + sql.extract_symbol_deps_with_path(result); + } + } +} + +pub struct CaseSwitchWhenItem { + pub value: String, + pub sql: Rc, +} + +pub enum CaseSwitchItem { + Symbol(Rc), + Sql(Rc), +} + +pub struct CaseSwitchDefinition { + pub switch: CaseSwitchItem, + pub items: Vec, + pub else_sql: Rc, +} + +impl CaseSwitchDefinition { + fn extract_symbol_deps(&self, result: &mut Vec>) { + match &self.switch { + CaseSwitchItem::Symbol(member_symbol) => result.push(member_symbol.clone()), + CaseSwitchItem::Sql(sql) => sql.extract_symbol_deps(result), + } + for itm in self.items.iter() { + itm.sql.extract_symbol_deps(result); + } + self.else_sql.extract_symbol_deps(result); + } + fn extract_symbol_deps_with_path(&self, result: &mut Vec<(Rc, Vec)>) { + match &self.switch { + CaseSwitchItem::Symbol(member_symbol) => result.push((member_symbol.clone(), vec![])), + CaseSwitchItem::Sql(sql) => sql.extract_symbol_deps_with_path(result), + } + for itm in self.items.iter() { + itm.sql.extract_symbol_deps_with_path(result); + } + self.else_sql.extract_symbol_deps_with_path(result); + } +} + +pub enum Case { + Case(CaseDefinition), + CaseSwitch(CaseSwitchDefinition), +} + +impl Case { + pub fn try_new( + cube_name: &String, + definition: CaseVariant, + compiler: &mut Compiler, + ) -> Result { + let res = match definition { + CaseVariant::Case(case_definition) => { + let items = case_definition + .when()? + .iter() + .map(|item| -> Result<_, CubeError> { + let sql = compiler.compile_sql_call(&cube_name, item.sql()?)?; + let label = match item.label()? { + StringOrSql::String(s) => CaseLabel::String(s.clone()), + StringOrSql::MemberSql(sql_struct) => { + let sql = + compiler.compile_sql_call(&cube_name, sql_struct.sql()?)?; + CaseLabel::Sql(sql) + } + }; + Ok(CaseWhenItem { sql, label }) + }) + .collect::, _>>()?; + + let else_label = match case_definition.else_label()?.label()? { + StringOrSql::String(s) => CaseLabel::String(s.clone()), + StringOrSql::MemberSql(sql_struct) => { + let sql = compiler.compile_sql_call(&cube_name, sql_struct.sql()?)?; + CaseLabel::Sql(sql) + } + }; + Case::Case(CaseDefinition { items, else_label }) + } + CaseVariant::CaseSwitch(case_definition) => { + let items = case_definition + .when()? + .iter() + .map(|item| -> Result<_, CubeError> { + let sql = compiler.compile_sql_call(&cube_name, item.sql()?)?; + let value = item.static_data().value.clone(); + Ok(CaseSwitchWhenItem { sql, value }) + }) + .collect::, _>>()?; + let else_sql = + compiler.compile_sql_call(&cube_name, case_definition.else_sql()?.sql()?)?; + let switch_sql = + compiler.compile_sql_call(&cube_name, case_definition.switch()?)?; + let switch = if let Some(symbol) = + switch_sql.resolve_direct_reference(compiler.base_tools())? + { + CaseSwitchItem::Symbol(symbol) + } else { + CaseSwitchItem::Sql(switch_sql) + }; + Case::CaseSwitch(CaseSwitchDefinition { + switch, + items, + else_sql, + }) + } + }; + Ok(res) + } + + pub fn extract_symbol_deps(&self, result: &mut Vec>) { + match self { + Case::Case(def) => def.extract_symbol_deps(result), + Case::CaseSwitch(def) => def.extract_symbol_deps(result), + } + } + pub fn extract_symbol_deps_with_path(&self, result: &mut Vec<(Rc, Vec)>) { + match self { + Case::Case(def) => def.extract_symbol_deps_with_path(result), + Case::CaseSwitch(def) => def.extract_symbol_deps_with_path(result), + } + } +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/mod.rs new file mode 100644 index 0000000000000..a8f9c5389213c --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/mod.rs @@ -0,0 +1,3 @@ +mod case; + +pub use case::*; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs index 28619be26a633..913fe993e3d10 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs @@ -1,8 +1,9 @@ +use super::common::Case; use super::{MemberSymbol, SymbolFactory}; -use crate::cube_bridge::case_label::CaseLabel; use crate::cube_bridge::dimension_definition::DimensionDefinition; use crate::cube_bridge::evaluator::CubeEvaluator; use crate::cube_bridge::member_sql::MemberSql; +use crate::cube_bridge::string_or_sql::StringOrSql; use crate::planner::query_tools::QueryTools; use crate::planner::sql_evaluator::{sql_nodes::SqlNode, Compiler, SqlCall, SqlEvaluatorVisitor}; use crate::planner::sql_evaluator::{CubeTableSymbol, TimeDimensionSymbol}; @@ -43,7 +44,7 @@ pub struct DimensionSymbol { latitude: Option>, longitude: Option>, values: Vec, - case: Option, + case: Option, definition: Rc, is_reference: bool, // Symbol is a direct reference to another symbol without any calculations is_view: bool, @@ -68,7 +69,7 @@ impl DimensionSymbol { latitude: Option>, longitude: Option>, values: Vec, - case: Option, + case: Option, definition: Rc, time_shift: Vec, time_shift_pk_full_name: Option, @@ -141,7 +142,7 @@ impl DimensionSymbol { self.longitude.clone() } - pub fn case(&self) -> &Option { + pub fn case(&self) -> &Option { &self.case } @@ -216,15 +217,7 @@ impl DimensionSymbol { member_sql.extract_symbol_deps(&mut deps); } if let Some(case) = &self.case { - for itm in case.items.iter() { - itm.sql.extract_symbol_deps(&mut deps); - if let DimenstionCaseLabel::Sql(sql) = &itm.label { - sql.extract_symbol_deps(&mut deps); - } - } - if let DimenstionCaseLabel::Sql(sql) = &case.else_label { - sql.extract_symbol_deps(&mut deps); - } + case.extract_symbol_deps(&mut deps); } deps } @@ -241,15 +234,7 @@ impl DimensionSymbol { member_sql.extract_symbol_deps_with_path(&mut deps); } if let Some(case) = &self.case { - for itm in case.items.iter() { - itm.sql.extract_symbol_deps_with_path(&mut deps); - if let DimenstionCaseLabel::Sql(sql) = &itm.label { - sql.extract_symbol_deps_with_path(&mut deps); - } - } - if let DimenstionCaseLabel::Sql(sql) = &case.else_label { - sql.extract_symbol_deps_with_path(&mut deps); - } + case.extract_symbol_deps_with_path(&mut deps); } deps } @@ -408,30 +393,7 @@ impl SymbolFactory for DimensionSymbolFactory { }; let case = if let Some(native_case) = definition.case()? { - let items = native_case - .when()? - .iter() - .map(|item| -> Result<_, CubeError> { - let sql = compiler.compile_sql_call(&cube_name, item.sql()?)?; - let label = match item.label()? { - CaseLabel::String(s) => DimenstionCaseLabel::String(s.clone()), - CaseLabel::MemberSql(sql_struct) => { - let sql = compiler.compile_sql_call(&cube_name, sql_struct.sql()?)?; - DimenstionCaseLabel::Sql(sql) - } - }; - Ok(DimensionCaseWhenItem { sql, label }) - }) - .collect::, _>>()?; - - let else_label = match native_case.else_label()?.label()? { - CaseLabel::String(s) => DimenstionCaseLabel::String(s.clone()), - CaseLabel::MemberSql(sql_struct) => { - let sql = compiler.compile_sql_call(&cube_name, sql_struct.sql()?)?; - DimenstionCaseLabel::Sql(sql) - } - }; - Some(DimensionCaseDefinition { items, else_label }) + Some(Case::try_new(&cube_name, native_case, compiler)?) } else { None }; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/mod.rs index 3f56a7086df9a..84d02f0d1997c 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/mod.rs @@ -1,3 +1,4 @@ +mod common; mod cube_symbol; mod dimension_symbol; mod measure_symbol; @@ -6,6 +7,7 @@ mod member_symbol; mod symbol_factory; mod time_dimension_symbol; +pub use common::*; pub use cube_symbol::{ CubeNameSymbol, CubeNameSymbolFactory, CubeTableSymbol, CubeTableSymbolFactory, }; From 0f4f0c9341f8b3a59d954475d81b66dc0d0e8cd2 Mon Sep 17 00:00:00 2001 From: Alexandr Romanenko Date: Fri, 5 Sep 2025 16:10:12 +0200 Subject: [PATCH 04/41] measure switch --- .../src/compiler/CubeValidator.ts | 73 +++++----- .../transpilers/CubePropContextTranspiler.ts | 1 + .../integration/postgres/calc-groups.test.ts | 128 +++++++++++++++++- .../src/cube_bridge/measure_definition.rs | 4 + .../src/planner/sql_evaluator/mod.rs | 8 +- .../src/planner/sql_evaluator/sql_call.rs | 6 - .../sql_nodes/{case_dimension.rs => case.rs} | 32 ++++- .../sql_evaluator/sql_nodes/factory.rs | 8 +- .../planner/sql_evaluator/sql_nodes/mod.rs | 4 +- .../sql_evaluator/symbols/common/case.rs | 7 + .../sql_evaluator/symbols/dimension_symbol.rs | 24 ---- .../sql_evaluator/symbols/measure_symbol.rs | 44 +++--- 12 files changed, 232 insertions(+), 107 deletions(-) rename rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/{case_dimension.rs => case.rs} (81%) diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts index 0150cf023b4de..dfc232f473f45 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts @@ -588,6 +588,42 @@ const timeShiftItemOptional = Joi.object({ .xor('name', 'interval') .and('interval', 'type'); +const CaseSchema = Joi.object().keys({ + when: Joi.array().items(Joi.object().keys({ + sql: Joi.func().required(), + label: Joi.alternatives([ + Joi.string(), + Joi.object().keys({ + sql: Joi.func().required() + }) + ]) + })), + else: Joi.object().keys({ + label: Joi.alternatives([ + Joi.string(), + Joi.object().keys({ + sql: Joi.func().required() + }) + ]) + }) +}).required(); + +const SwitchCaseSchema = Joi.object().keys({ + switch: Joi.func().required(), + when: Joi.array().items(Joi.object().keys({ + value: Joi.string().required(), + sql: Joi.func().required() + })), + else: Joi.object().keys({ + sql: Joi.func().required() + }) +}).required(); + +const CaseVariants = Joi.alternatives().try( + CaseSchema, + SwitchCaseSchema +); + const MeasuresSchema = Joi.object().pattern(identifierRegex, Joi.alternatives().conditional(Joi.ref('.multiStage'), [ { is: true, @@ -595,6 +631,7 @@ const MeasuresSchema = Joi.object().pattern(identifierRegex, Joi.alternatives(). multiStage: Joi.boolean().strict(), type: multiStageMeasureType.required(), sql: Joi.func(), // TODO .required(), + case: CaseVariants, groupBy: Joi.func(), reduceBy: Joi.func(), addGroupBy: Joi.func(), @@ -657,42 +694,6 @@ const SwitchDimension = Joi.object({ values: Joi.array().items(Joi.string()).min(1).required() }); -const CaseSchema = Joi.object().keys({ - when: Joi.array().items(Joi.object().keys({ - sql: Joi.func().required(), - label: Joi.alternatives([ - Joi.string(), - Joi.object().keys({ - sql: Joi.func().required() - }) - ]) - })), - else: Joi.object().keys({ - label: Joi.alternatives([ - Joi.string(), - Joi.object().keys({ - sql: Joi.func().required() - }) - ]) - }) -}).required(); - -const SwitchCaseSchema = Joi.object().keys({ - switch: Joi.func().required(), - when: Joi.array().items(Joi.object().keys({ - value: Joi.string().required(), - sql: Joi.func().required() - })), - else: Joi.object().keys({ - sql: Joi.func().required() - }) -}).required(); - -const CaseVariants = Joi.alternatives().try( - CaseSchema, - SwitchCaseSchema -); - const DimensionsSchema = Joi.object().pattern(identifierRegex, Joi.alternatives().conditional(Joi.ref('.type'), { is: 'switch', then: SwitchDimension, diff --git a/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts b/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts index 8713e3e10e060..a4cfeabb933d2 100644 --- a/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts +++ b/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts @@ -19,6 +19,7 @@ export const transpiledFieldsPatterns: Array = [ /^measures\.[_a-zA-Z][_a-zA-Z0-9]*\.(orderBy|order_by)\.[0-9]+\.sql$/, /^measures\.[_a-zA-Z][_a-zA-Z0-9]*\.(timeShift|time_shift)\.[0-9]+\.(timeDimension|time_dimension)$/, /^measures\.[_a-zA-Z][_a-zA-Z0-9]*\.(reduceBy|reduce_by|groupBy|group_by|addGroupBy|add_group_by)$/, + /^measures\.[_a-zA-Z][_a-zA-Z0-9]*\.case\.switch$/, /^dimensions\.[_a-zA-Z][_a-zA-Z0-9]*\.(reduceBy|reduce_by|groupBy|group_by|addGroupBy|add_group_by)$/, /^dimensions\.[_a-zA-Z][_a-zA-Z0-9]*\.case\.switch$/, /^(preAggregations|pre_aggregations)\.[_a-zA-Z][_a-zA-Z0-9]*\.indexes\.[_a-zA-Z][_a-zA-Z0-9]*\.columns$/, diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts index e88e8a8e5675e..ce5530727edb3 100644 --- a/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts +++ b/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts @@ -11,15 +11,15 @@ describe('Calc-Groups', () => { cubes: - name: orders sql: > - SELECT 9 as ID, 'completed' as STATUS, '2022-01-12T20:00:00.000Z'::timestamptz as CREATED_AT + SELECT 9 as ID, 'completed' as STATUS, 100.0 as amount_usd, 97.4 as amount_eur, 80.6 as amount_gbp, '2022-01-12T20:00:00.000Z'::timestamptz as CREATED_AT union all - SELECT 10 as ID, 'completed' as STATUS, '2023-01-12T20:00:00.000Z'::timestamptz as CREATED_AT + SELECT 10 as ID, 'completed' as STATUS, 10.0 as amount_usd, 9.74 as amount_eur, 8.06 as amount_gbp, '2023-01-12T20:00:00.000Z'::timestamptz as CREATED_AT union all - SELECT 11 as ID, 'completed' as STATUS, '2024-01-14T20:00:00.000Z'::timestamptz as CREATED_AT + SELECT 11 as ID, 'completed' as STATUS, 1000.0 as amount_usd, 974 as amount_eur, 806 as amount_gbp,'2024-01-14T20:00:00.000Z'::timestamptz as CREATED_AT union all - SELECT 12 as ID, 'completed' as STATUS, '2024-02-14T20:00:00.000Z'::timestamptz as CREATED_AT + SELECT 12 as ID, 'completed' as STATUS, 30.0 as amount_usd, 28 as amount_eur, 22 as amount_gbp,'2024-02-14T20:00:00.000Z'::timestamptz as CREATED_AT union all - SELECT 13 as ID, 'completed' as STATUS, '2025-03-14T20:00:00.000Z'::timestamptz as CREATED_AT + SELECT 13 as ID, 'completed' as STATUS, 40.0 as amount_usd, 38 as amount_eur, 33 as amount_gbp, '2025-03-14T20:00:00.000Z'::timestamptz as CREATED_AT joins: - name: line_items sql: "{CUBE}.ID = {line_items}.order_id" @@ -78,6 +78,31 @@ cubes: filters: - sql: "{CUBE}.STATUS = 'completed'" + - name: amount_usd + type: sum + sql: amount_usd + + - name: amount_eur + type: sum + sql: amount_eur + + - name: amount_gbp + type: sum + sql: amount_gbp + + - name: amount_in_currency + type: number + multi_stage: true + case: + switch: "{CUBE.currency}" + when: + - value: USD + sql: "{CUBE.amount_usd}" + - value: EUR + sql: "{CUBE.amount_eur}" + else: + sql: "{CUBE.amount_gbp}" + - name: returned_count type: count filters: @@ -516,6 +541,99 @@ views: } ], { joinGraph, cubeEvaluator, compiler })); + + it('measure switch cross join', async () => dbRunner.runQueryTest({ + dimensions: ['orders.currency'], + measures: ['orders.amount_usd', 'orders.amount_in_currency'], + timeDimensions: [ + { + dimension: 'orders.date', + granularity: 'year', + dateRange: ['2024-01-01', '2026-01-01'] + } + ], + timezone: 'UTC', + order: [{ + id: 'orders.date' + }, { + id: 'orders.currency' + }, + ], + }, [ + { + orders__currency: 'EUR', + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__amount_usd: '1030.0', + orders__amount_in_currency: '1002' + }, + { + orders__currency: 'GBP', + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__amount_usd: '1030.0', + orders__amount_in_currency: '828' + }, + { + orders__currency: 'USD', + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__amount_usd: '1030.0', + orders__amount_in_currency: '1030.0' + }, + { + orders__currency: 'EUR', + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__amount_usd: '40.0', + orders__amount_in_currency: '38' + }, + { + orders__currency: 'GBP', + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__amount_usd: '40.0', + orders__amount_in_currency: '33' + }, + { + orders__currency: 'USD', + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__amount_usd: '40.0', + orders__amount_in_currency: '40.0' + } + ], + { joinGraph, cubeEvaluator, compiler })); + + it('measure switch with filter', async () => dbRunner.runQueryTest({ + dimensions: ['orders.currency'], + measures: ['orders.amount_usd', 'orders.amount_in_currency'], + timeDimensions: [ + { + dimension: 'orders.date', + granularity: 'year', + dateRange: ['2024-01-01', '2026-01-01'] + } + ], + filters: [ + { dimension: 'orders.currency', operator: 'equals', values: ['EUR'] } + ], + timezone: 'UTC', + order: [{ + id: 'orders.date' + }, { + id: 'orders.currency' + }, + ], + }, [ + { + orders__currency: 'EUR', + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__amount_usd: '1030.0', + orders__amount_in_currency: '1002' + }, + { + orders__currency: 'EUR', + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__amount_usd: '40.0', + orders__amount_in_currency: '38' + }, + ], + { joinGraph, cubeEvaluator, compiler })); } else { // This test is working only in tesseract test.skip('calc groups testst', () => { expect(1).toBe(1); }); diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/measure_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/measure_definition.rs index bc0e581ee01d8..126cd89901cec 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/measure_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/measure_definition.rs @@ -1,3 +1,4 @@ +use super::case_variant::CaseVariant; use super::cube_definition::{CubeDefinition, NativeCubeDefinition}; use super::member_order_by::{MemberOrderBy, NativeMemberOrderBy}; use super::member_sql::{MemberSql, NativeMemberSql}; @@ -60,6 +61,9 @@ pub trait MeasureDefinition { fn cube(&self) -> Result, CubeError>; + #[nbridge(field, optional)] + fn case(&self) -> Result, CubeError>; + #[nbridge(field, optional, vec)] fn filters(&self) -> Result>>, CubeError>; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/mod.rs index 570e6ae2db716..4d0bf0c1f7bf1 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/mod.rs @@ -13,11 +13,5 @@ pub use dependecy::{CubeDepProperty, Dependency}; pub use references_builder::ReferencesBuilder; pub use sql_call::SqlCall; pub use sql_visitor::SqlEvaluatorVisitor; -pub use symbols::{ - CubeNameSymbol, CubeNameSymbolFactory, CubeTableSymbol, CubeTableSymbolFactory, - DimensionCaseDefinition, DimensionCaseWhenItem, DimensionSymbol, DimensionSymbolFactory, - DimensionTimeShift, DimenstionCaseLabel, MeasureSymbol, MeasureSymbolFactory, - MeasureTimeShifts, MemberExpressionExpression, MemberExpressionSymbol, MemberSymbol, - SymbolFactory, TimeDimensionSymbol, -}; +pub use symbols::*; pub use visitor::TraversalVisitor; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_call.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_call.rs index ca4535400631f..d07af32730d85 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_call.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_call.rs @@ -128,12 +128,6 @@ impl SqlCall { } } - pub fn get_dependent_cubes(&self) -> Vec { - let mut deps = Vec::new(); - self.extract_cube_deps(&mut deps); - deps - } - pub fn extract_cube_deps(&self, result: &mut Vec) { for dep in self.deps.iter() { match dep { diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/case_dimension.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/case.rs similarity index 81% rename from rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/case_dimension.rs rename to rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/case.rs index b1dd38f14ddf9..a10e525e53a89 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/case_dimension.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/case.rs @@ -10,11 +10,11 @@ use cubenativeutils::CubeError; use std::any::Any; use std::rc::Rc; -pub struct CaseDimensionSqlNode { +pub struct CaseSqlNode { input: Rc, } -impl CaseDimensionSqlNode { +impl CaseSqlNode { pub fn new(input: Rc) -> Rc { Rc::new(Self { input }) } @@ -101,7 +101,7 @@ impl CaseDimensionSqlNode { } } -impl SqlNode for CaseDimensionSqlNode { +impl SqlNode for CaseSqlNode { fn to_sql( &self, visitor: &SqlEvaluatorVisitor, @@ -135,9 +135,33 @@ impl SqlNode for CaseDimensionSqlNode { )? } } + MemberSymbol::Measure(ev) => { + if let Some(case) = ev.case() { + match case { + Case::Case(case) => { + self.case_to_sql(visitor, case, query_tools, node_processor, templates)? + } + Case::CaseSwitch(case) => self.case_switch_to_sql( + visitor, + case, + query_tools, + node_processor, + templates, + )?, + } + } else { + self.input.to_sql( + visitor, + node, + query_tools.clone(), + node_processor.clone(), + templates, + )? + } + } _ => { return Err(CubeError::internal(format!( - "CaseDimension node processor called for wrong node", + "Case node processor called for wrong node", ))); } }; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/factory.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/factory.rs index 3f16eb589a6b7..14ede8944fb18 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/factory.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/factory.rs @@ -1,5 +1,5 @@ use super::{ - AutoPrefixSqlNode, CaseDimensionSqlNode, EvaluateSqlNode, FinalMeasureSqlNode, + AutoPrefixSqlNode, CaseSqlNode, EvaluateSqlNode, FinalMeasureSqlNode, FinalPreAggregationMeasureSqlNode, GeoDimensionSqlNode, MeasureFilterSqlNode, MultiStageRankNode, MultiStageWindowNode, OriginalSqlPreAggregationSqlNode, RenderReferencesSqlNode, RollingWindowNode, RootSqlNode, SqlNode, TimeDimensionNode, @@ -185,9 +185,9 @@ impl SqlNodesFactory { ); let measure_filter_processor = MeasureFilterSqlNode::new(auto_prefix_processor.clone()); + let measure_processor = CaseSqlNode::new(measure_filter_processor.clone()); - let measure_processor = - self.add_ungrouped_measure_reference_if_needed(measure_filter_processor.clone()); + let measure_processor = self.add_ungrouped_measure_reference_if_needed(measure_processor); let measure_processor = self.final_measure_node_processor(measure_processor); let measure_processor = self .add_multi_stage_window_if_needed(measure_processor, measure_filter_processor.clone()); @@ -279,7 +279,7 @@ impl SqlNodesFactory { RenderReferencesSqlNode::new(input, self.pre_aggregation_dimensions_references.clone()) } else { let input: Rc = GeoDimensionSqlNode::new(input); - let input: Rc = CaseDimensionSqlNode::new(input); + let input: Rc = CaseSqlNode::new(input); input }; let input: Rc = diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/mod.rs index c7a5525c56fab..6481fd50d64d9 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/mod.rs @@ -1,6 +1,6 @@ pub mod auto_prefix; pub mod calendar_time_shift; -pub mod case_dimension; +pub mod case; pub mod cube_calc_groups; pub mod evaluate_sql; pub mod factory; @@ -21,7 +21,7 @@ pub mod ungroupped_measure; pub mod ungroupped_query_final_measure; pub use auto_prefix::AutoPrefixSqlNode; -pub use case_dimension::CaseDimensionSqlNode; +pub use case::CaseSqlNode; pub use cube_calc_groups::CubeCalcGroupsSqlNode; pub use evaluate_sql::EvaluateSqlNode; pub use factory::SqlNodesFactory; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/case.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/case.rs index 447a3547f601f..8b336ea47a7bd 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/case.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/case.rs @@ -5,16 +5,19 @@ use crate::{ use cubenativeutils::CubeError; use std::rc::Rc; +#[derive(Clone)] pub enum CaseLabel { String(String), Sql(Rc), } +#[derive(Clone)] pub struct CaseWhenItem { pub sql: Rc, pub label: CaseLabel, } +#[derive(Clone)] pub struct CaseDefinition { pub items: Vec, pub else_label: CaseLabel, @@ -45,16 +48,19 @@ impl CaseDefinition { } } +#[derive(Clone)] pub struct CaseSwitchWhenItem { pub value: String, pub sql: Rc, } +#[derive(Clone)] pub enum CaseSwitchItem { Symbol(Rc), Sql(Rc), } +#[derive(Clone)] pub struct CaseSwitchDefinition { pub switch: CaseSwitchItem, pub items: Vec, @@ -84,6 +90,7 @@ impl CaseSwitchDefinition { } } +#[derive(Clone)] pub enum Case { Case(CaseDefinition), CaseSwitch(CaseSwitchDefinition), diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs index 913fe993e3d10..66d70f90c4e4e 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs @@ -3,7 +3,6 @@ use super::{MemberSymbol, SymbolFactory}; use crate::cube_bridge::dimension_definition::DimensionDefinition; use crate::cube_bridge::evaluator::CubeEvaluator; use crate::cube_bridge::member_sql::MemberSql; -use crate::cube_bridge::string_or_sql::StringOrSql; use crate::planner::query_tools::QueryTools; use crate::planner::sql_evaluator::{sql_nodes::SqlNode, Compiler, SqlCall, SqlEvaluatorVisitor}; use crate::planner::sql_evaluator::{CubeTableSymbol, TimeDimensionSymbol}; @@ -13,21 +12,6 @@ use crate::planner::SqlInterval; use cubenativeutils::CubeError; use std::rc::Rc; -pub enum DimenstionCaseLabel { - String(String), - Sql(Rc), -} - -pub struct DimensionCaseWhenItem { - pub sql: Rc, - pub label: DimenstionCaseLabel, -} - -pub struct DimensionCaseDefinition { - pub items: Vec, - pub else_label: DimenstionCaseLabel, -} - #[derive(Clone)] pub struct CalendarDimensionTimeShift { pub interval: Option, @@ -239,14 +223,6 @@ impl DimensionSymbol { deps } - pub fn get_dependent_cubes(&self) -> Vec { - let mut cubes = vec![]; - if let Some(member_sql) = &self.member_sql { - member_sql.extract_cube_deps(&mut cubes); - } - cubes - } - pub fn cube_name(&self) -> &String { &self.cube.cube_name() } diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/measure_symbol.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/measure_symbol.rs index c658082902d58..4fdf6cdf77fd1 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/measure_symbol.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/measure_symbol.rs @@ -1,3 +1,4 @@ +use super::common::Case; use super::{MemberSymbol, SymbolFactory}; use crate::cube_bridge::evaluator::CubeEvaluator; use crate::cube_bridge::measure_definition::{MeasureDefinition, RollingWindow}; @@ -72,6 +73,7 @@ pub struct MeasureSymbol { is_multi_stage: bool, is_reference: bool, is_view: bool, + case: Option, measure_filters: Vec>, measure_drill_filters: Vec>, time_shift: Option, @@ -92,6 +94,7 @@ impl MeasureSymbol { member_sql: Option>, is_reference: bool, is_view: bool, + case: Option, pk_sqls: Vec>, definition: Rc, measure_filters: Vec>, @@ -113,6 +116,7 @@ impl MeasureSymbol { member_sql, is_reference, is_view, + case, pk_sqls, owned_by_cube, measure_type, @@ -146,6 +150,7 @@ impl MeasureSymbol { is_multi_stage: false, is_reference: false, is_view: self.is_view, + case: self.case.clone(), measure_filters: self.measure_filters.clone(), measure_drill_filters: self.measure_drill_filters.clone(), time_shift: self.time_shift.clone(), @@ -229,6 +234,7 @@ impl MeasureSymbol { is_multi_stage: self.is_multi_stage, is_reference: self.is_reference, is_view: self.is_view, + case: self.case.clone(), measure_filters, measure_drill_filters: self.measure_drill_filters.clone(), time_shift: self.time_shift.clone(), @@ -273,6 +279,10 @@ impl MeasureSymbol { } } + pub fn case(&self) -> &Option { + &self.case + } + pub fn is_addictive(&self) -> bool { if self.is_multi_stage() { false @@ -323,6 +333,9 @@ impl MeasureSymbol { for order in self.measure_order_by.iter() { order.sql_call().extract_symbol_deps(&mut deps); } + if let Some(case) = &self.case { + case.extract_symbol_deps(&mut deps); + } deps } @@ -343,28 +356,13 @@ impl MeasureSymbol { for order in self.measure_order_by.iter() { order.sql_call().extract_symbol_deps_with_path(&mut deps); } + if let Some(case) = &self.case { + case.extract_symbol_deps_with_path(&mut deps); + } deps } - pub fn get_dependent_cubes(&self) -> Vec { - let mut cubes = vec![]; - if let Some(member_sql) = &self.member_sql { - member_sql.extract_cube_deps(&mut cubes); - } - for pk in self.pk_sqls.iter() { - pk.extract_cube_deps(&mut cubes); - } - for filter in self.measure_filters.iter() { - filter.extract_cube_deps(&mut cubes); - } - for filter in self.measure_drill_filters.iter() { - filter.extract_cube_deps(&mut cubes); - } - for order in self.measure_order_by.iter() { - order.sql_call().extract_cube_deps(&mut cubes); - } - cubes - } + pub fn can_used_as_addictive_in_multplied(&self) -> bool { if &self.measure_type == "countDistinct" || &self.measure_type == "countDistinctApprox" { @@ -659,6 +657,12 @@ impl SymbolFactory for MeasureSymbolFactory { None }; + let case = if let Some(native_case) = definition.case()? { + Some(Case::try_new(&cube_name, native_case, compiler)?) + } else { + None + }; + let reduce_by = if let Some(reduce_by) = &definition.static_data().reduce_by_references { let symbols = reduce_by .iter() @@ -706,6 +710,7 @@ impl SymbolFactory for MeasureSymbolFactory { && is_sql_is_direct_ref && is_calculated && !is_multi_stage + && case.is_none() && measure_filters.is_empty() && measure_drill_filters.is_empty() && time_shifts.is_none() @@ -725,6 +730,7 @@ impl SymbolFactory for MeasureSymbolFactory { sql, is_reference, is_view, + case, pk_sqls, definition, measure_filters, From 4d5cf6bac7ddcaa1c702c10557cc21b260fb3fbb Mon Sep 17 00:00:00 2001 From: Alexandr Romanenko Date: Fri, 5 Sep 2025 16:47:21 +0200 Subject: [PATCH 05/41] lint --- .../src/adapter/BaseQuery.js | 6 +- .../integration/postgres/calc-groups.test.ts | 174 +++++++++--------- 2 files changed, 90 insertions(+), 90 deletions(-) diff --git a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js index 7501cc33d7e91..38bf63af690ea 100644 --- a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js +++ b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js @@ -4214,16 +4214,16 @@ export class BaseQuery { '{{ min_expr }} as {{ quoted_min_name }}\n' + 'FROM {{ from_prepared }}\n' + '{% if filter %}WHERE {{ filter }}{% endif %}', - calc_groups_join: 'SELECT \"{{ original_cube }}\".*, \"{{ groups | map(attribute=\'name\') | join(\'\", \"\') }}\"\n' + + calc_groups_join: 'SELECT "{{ original_cube }}".*, "{{ groups | map(attribute=\'name\') | join(\'", "\') }}"\n' + 'FROM {{ original_cube_sql }} {{ original_cube }}\n' + '{% for group in groups %}' + 'CROSS JOIN\n' + '(\n' + '{% for value in group.values %}' + - 'SELECT \'{{ value }}\' as \"{{ group.name }}\"' + + 'SELECT \'{{ value }}\' as "{{ group.name }}"' + '{% if not loop.last %} UNION ALL\n{% endif %}' + '{% endfor %}' + - ') \"{{ group.name }}_values\"\n' + + ') "{{ group.name }}_values"\n' + '{% endfor %}' }, expressions: { diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts index ce5530727edb3..0d787b25f75d2 100644 --- a/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts +++ b/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts @@ -503,43 +503,43 @@ views: }, ], }, [ - { - orders__currency: 'EUR', - orders__currency_full_name: 'euros', - orders__date_year: '2024-01-01T00:00:00.000Z', - orders__revenue: '30' - }, - { - orders__currency: 'GBP', - orders__currency_full_name: 'unknown', - orders__date_year: '2024-01-01T00:00:00.000Z', - orders__revenue: '30' - }, - { - orders__currency: 'USD', - orders__currency_full_name: 'dollars', - orders__date_year: '2024-01-01T00:00:00.000Z', - orders__revenue: '30' - }, - { - orders__currency: 'EUR', - orders__currency_full_name: 'euros', - orders__date_year: '2025-01-01T00:00:00.000Z', - orders__revenue: '5' - }, - { - orders__currency: 'GBP', - orders__currency_full_name: 'unknown', - orders__date_year: '2025-01-01T00:00:00.000Z', - orders__revenue: '5' - }, - { - orders__currency: 'USD', - orders__currency_full_name: 'dollars', - orders__date_year: '2025-01-01T00:00:00.000Z', - orders__revenue: '5' - } - ], + { + orders__currency: 'EUR', + orders__currency_full_name: 'euros', + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__revenue: '30' + }, + { + orders__currency: 'GBP', + orders__currency_full_name: 'unknown', + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__revenue: '30' + }, + { + orders__currency: 'USD', + orders__currency_full_name: 'dollars', + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__revenue: '30' + }, + { + orders__currency: 'EUR', + orders__currency_full_name: 'euros', + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__revenue: '5' + }, + { + orders__currency: 'GBP', + orders__currency_full_name: 'unknown', + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__revenue: '5' + }, + { + orders__currency: 'USD', + orders__currency_full_name: 'dollars', + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__revenue: '5' + } + ], { joinGraph, cubeEvaluator, compiler })); it('measure switch cross join', async () => dbRunner.runQueryTest({ @@ -560,43 +560,43 @@ views: }, ], }, [ - { - orders__currency: 'EUR', - orders__date_year: '2024-01-01T00:00:00.000Z', - orders__amount_usd: '1030.0', - orders__amount_in_currency: '1002' - }, - { - orders__currency: 'GBP', - orders__date_year: '2024-01-01T00:00:00.000Z', - orders__amount_usd: '1030.0', - orders__amount_in_currency: '828' - }, - { - orders__currency: 'USD', - orders__date_year: '2024-01-01T00:00:00.000Z', - orders__amount_usd: '1030.0', - orders__amount_in_currency: '1030.0' - }, - { - orders__currency: 'EUR', - orders__date_year: '2025-01-01T00:00:00.000Z', - orders__amount_usd: '40.0', - orders__amount_in_currency: '38' - }, - { - orders__currency: 'GBP', - orders__date_year: '2025-01-01T00:00:00.000Z', - orders__amount_usd: '40.0', - orders__amount_in_currency: '33' - }, - { - orders__currency: 'USD', - orders__date_year: '2025-01-01T00:00:00.000Z', - orders__amount_usd: '40.0', - orders__amount_in_currency: '40.0' - } - ], + { + orders__currency: 'EUR', + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__amount_usd: '1030.0', + orders__amount_in_currency: '1002' + }, + { + orders__currency: 'GBP', + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__amount_usd: '1030.0', + orders__amount_in_currency: '828' + }, + { + orders__currency: 'USD', + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__amount_usd: '1030.0', + orders__amount_in_currency: '1030.0' + }, + { + orders__currency: 'EUR', + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__amount_usd: '40.0', + orders__amount_in_currency: '38' + }, + { + orders__currency: 'GBP', + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__amount_usd: '40.0', + orders__amount_in_currency: '33' + }, + { + orders__currency: 'USD', + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__amount_usd: '40.0', + orders__amount_in_currency: '40.0' + } + ], { joinGraph, cubeEvaluator, compiler })); it('measure switch with filter', async () => dbRunner.runQueryTest({ @@ -620,19 +620,19 @@ views: }, ], }, [ - { - orders__currency: 'EUR', - orders__date_year: '2024-01-01T00:00:00.000Z', - orders__amount_usd: '1030.0', - orders__amount_in_currency: '1002' - }, - { - orders__currency: 'EUR', - orders__date_year: '2025-01-01T00:00:00.000Z', - orders__amount_usd: '40.0', - orders__amount_in_currency: '38' - }, - ], + { + orders__currency: 'EUR', + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__amount_usd: '1030.0', + orders__amount_in_currency: '1002' + }, + { + orders__currency: 'EUR', + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__amount_usd: '40.0', + orders__amount_in_currency: '38' + }, + ], { joinGraph, cubeEvaluator, compiler })); } else { // This test is working only in tesseract From 40cb7fde3277de4ed5b6f8a0aa6367a48586ede8 Mon Sep 17 00:00:00 2001 From: Alexandr Romanenko Date: Fri, 5 Sep 2025 20:01:19 +0200 Subject: [PATCH 06/41] optimization --- .../src/adapter/BaseQuery.js | 9 +- .../integration/postgres/calc-groups.test.ts | 153 +++++++++--------- .../test/integration/utils/BaseDbRunner.ts | 5 +- .../src/cube_bridge/case_item.rs | 2 +- .../src/cube_bridge/case_switch_definition.rs | 1 - .../src/cube_bridge/case_variant.rs | 1 - .../src/cube_bridge/string_or_sql.rs | 2 +- .../cubesqlplanner/src/plan/filter.rs | 54 +++++++ .../src/planner/filter/base_filter.rs | 8 + .../src/planner/query_properties.rs | 18 ++- .../planner/sql_evaluator/sql_nodes/case.rs | 19 +-- .../sql_nodes/cube_calc_groups.rs | 27 +++- .../sql_evaluator/symbols/common/case.rs | 57 ++++--- .../sql_evaluator/symbols/common/mod.rs | 2 + .../symbols/common/static_filter.rs | 64 ++++++++ .../sql_evaluator/symbols/dimension_symbol.rs | 14 ++ .../sql_evaluator/symbols/measure_symbol.rs | 9 +- .../src/planner/sql_templates/plan.rs | 7 +- .../src/planner/sql_templates/structs.rs | 6 + 19 files changed, 338 insertions(+), 120 deletions(-) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/static_filter.rs diff --git a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js index 38bf63af690ea..133e4c543233b 100644 --- a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js +++ b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js @@ -4214,7 +4214,14 @@ export class BaseQuery { '{{ min_expr }} as {{ quoted_min_name }}\n' + 'FROM {{ from_prepared }}\n' + '{% if filter %}WHERE {{ filter }}{% endif %}', - calc_groups_join: 'SELECT "{{ original_cube }}".*, "{{ groups | map(attribute=\'name\') | join(\'", "\') }}"\n' + + calc_groups_join: 'SELECT "{{ original_cube }}".*, ' + + '{%for single_value in single_values %}' + + '\'{{ single_value.value }}\' as "{{ single_value.name }}"{% if not loop.last %}, {% endif %}' + + '{% endfor %}' + + '{% if single_values and groups %}, {% endif %}' + + '{%for group in groups %}' + + '"{{ group.name }}"{% if not loop.last %}, {% endif %}' + + '{% endfor %}' + 'FROM {{ original_cube_sql }} {{ original_cube }}\n' + '{% for group in groups %}' + 'CROSS JOIN\n' + diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts index 0d787b25f75d2..5777b2bd64e24 100644 --- a/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts +++ b/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts @@ -442,48 +442,52 @@ views: ], { joinGraph, cubeEvaluator, compiler })); - it('basic cross join with measure with filters', async () => dbRunner.runQueryTest({ - dimensions: ['orders.strategy'], - measures: ['orders.revenue'], - timeDimensions: [ + it('basic cross join with filters', async () => { + const sqlAndParams = await dbRunner.runQueryTest({ + dimensions: ['orders.strategy'], + measures: ['orders.revenue'], + timeDimensions: [ + { + dimension: 'orders.date', + granularity: 'year' + } + ], + filters: [ + { dimension: 'orders.strategy', operator: 'equals', values: ['B'] } + ], + timezone: 'UTC', + order: [{ + id: 'orders.date' + }, { + id: 'orders.currency' + }, + ], + }, [ { - dimension: 'orders.date', - granularity: 'year' + orders__date_year: '2022-01-01T00:00:00.000Z', + orders__strategy: 'B', + orders__revenue: '5', + }, + { + orders__date_year: '2023-01-01T00:00:00.000Z', + orders__strategy: 'B', + orders__revenue: '15', + }, + { + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__strategy: 'B', + orders__revenue: '30', + }, + { + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__strategy: 'B', + orders__revenue: '5', } ], - filters: [ - { dimension: 'orders.strategy', operator: 'equals', values: ['B'] } - ], - timezone: 'UTC', - order: [{ - id: 'orders.date' - }, { - id: 'orders.currency' - }, - ], - }, [ - { - orders__date_year: '2022-01-01T00:00:00.000Z', - orders__strategy: 'B', - orders__revenue: '5', - }, - { - orders__date_year: '2023-01-01T00:00:00.000Z', - orders__strategy: 'B', - orders__revenue: '15', - }, - { - orders__date_year: '2024-01-01T00:00:00.000Z', - orders__strategy: 'B', - orders__revenue: '30', - }, - { - orders__date_year: '2025-01-01T00:00:00.000Z', - orders__strategy: 'B', - orders__revenue: '5', - } - ], - { joinGraph, cubeEvaluator, compiler })); + { joinGraph, cubeEvaluator, compiler }); + + expect(sqlAndParams[0]).not.toMatch(/CROSS.+JOIN/); + }); it('dimension switch expression simple', async () => dbRunner.runQueryTest({ dimensions: ['orders.currency', 'orders.currency_full_name'], @@ -599,41 +603,46 @@ views: ], { joinGraph, cubeEvaluator, compiler })); - it('measure switch with filter', async () => dbRunner.runQueryTest({ - dimensions: ['orders.currency'], - measures: ['orders.amount_usd', 'orders.amount_in_currency'], - timeDimensions: [ + it('measure switch with filter', async () => { + const sqlAndParams = await dbRunner.runQueryTest({ + dimensions: ['orders.currency'], + measures: ['orders.amount_usd', 'orders.amount_in_currency'], + timeDimensions: [ + { + dimension: 'orders.date', + granularity: 'year', + dateRange: ['2024-01-01', '2026-01-01'] + } + ], + filters: [ + { dimension: 'orders.currency', operator: 'equals', values: ['EUR'] } + ], + timezone: 'UTC', + order: [{ + id: 'orders.date' + }, { + id: 'orders.currency' + }, + ], + }, [ { - dimension: 'orders.date', - granularity: 'year', - dateRange: ['2024-01-01', '2026-01-01'] - } - ], - filters: [ - { dimension: 'orders.currency', operator: 'equals', values: ['EUR'] } - ], - timezone: 'UTC', - order: [{ - id: 'orders.date' - }, { - id: 'orders.currency' - }, + orders__currency: 'EUR', + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__amount_usd: '1030.0', + orders__amount_in_currency: '1002' + }, + { + orders__currency: 'EUR', + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__amount_usd: '40.0', + orders__amount_in_currency: '38' + }, ], - }, [ - { - orders__currency: 'EUR', - orders__date_year: '2024-01-01T00:00:00.000Z', - orders__amount_usd: '1030.0', - orders__amount_in_currency: '1002' - }, - { - orders__currency: 'EUR', - orders__date_year: '2025-01-01T00:00:00.000Z', - orders__amount_usd: '40.0', - orders__amount_in_currency: '38' - }, - ], - { joinGraph, cubeEvaluator, compiler })); + { joinGraph, cubeEvaluator, compiler }); + + expect(sqlAndParams[0]).not.toMatch(/CASE/); + expect(sqlAndParams[0]).not.toMatch(/CROSS.+JOIN/); + }); } else { // This test is working only in tesseract test.skip('calc groups testst', () => { expect(1).toBe(1); }); diff --git a/packages/cubejs-schema-compiler/test/integration/utils/BaseDbRunner.ts b/packages/cubejs-schema-compiler/test/integration/utils/BaseDbRunner.ts index 94b8228702df8..80f3259ef4bbb 100644 --- a/packages/cubejs-schema-compiler/test/integration/utils/BaseDbRunner.ts +++ b/packages/cubejs-schema-compiler/test/integration/utils/BaseDbRunner.ts @@ -26,14 +26,15 @@ export class BaseDbRunner { const query = this.newTestQuery({ joinGraph, cubeEvaluator, compiler }, q); console.log(query.buildSqlAndParams()); + const sqlAndParams = query.buildSqlAndParams(); - const res = await this.testQuery(query.buildSqlAndParams()); + const res = await this.testQuery(sqlAndParams); console.log(JSON.stringify(res)); - console.log('!!! res: ', res); expect(res).toEqual( expectedResult ); + return sqlAndParams; } public async testQueries(queries, fixture: any = null) { diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_item.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_item.rs index 7573ed861e03e..4d6280b00f7f3 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_item.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_item.rs @@ -1,5 +1,5 @@ -use super::string_or_sql::StringOrSql; use super::member_sql::{MemberSql, NativeMemberSql}; +use super::string_or_sql::StringOrSql; use cubenativeutils::wrappers::serializer::{ NativeDeserialize, NativeDeserializer, NativeSerialize, }; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_definition.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_definition.rs index e811527c5d8a8..69b44d62c290a 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_definition.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_switch_definition.rs @@ -2,7 +2,6 @@ use crate::cube_bridge::member_sql::{MemberSql, NativeMemberSql}; use super::case_switch_else_item::{CaseSwitchElseItem, NativeCaseSwitchElseItem}; use super::case_switch_item::{CaseSwitchItem, NativeCaseSwitchItem}; -use super::string_or_sql::StringOrSql; use cubenativeutils::wrappers::serializer::{ NativeDeserialize, NativeDeserializer, NativeSerialize, }; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_variant.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_variant.rs index 769014858ccce..7b9fcd27e28d2 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_variant.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/case_variant.rs @@ -3,7 +3,6 @@ use crate::cube_bridge::case_switch_definition::{ CaseSwitchDefinition, NativeCaseSwitchDefinition, }; -use super::struct_with_sql_member::{NativeStructWithSqlMember, StructWithSqlMember}; use cubenativeutils::wrappers::inner_types::InnerTypes; use cubenativeutils::wrappers::serializer::NativeDeserialize; use cubenativeutils::wrappers::NativeObjectHandle; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/string_or_sql.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/string_or_sql.rs index 0c45e0ad071e0..0bbd10fe24dd4 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/string_or_sql.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/string_or_sql.rs @@ -22,4 +22,4 @@ impl NativeDeserialize for StringOrSql { }, } } -} \ No newline at end of file +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/plan/filter.rs b/rust/cubesqlplanner/cubesqlplanner/src/plan/filter.rs index 2a82eff0a3682..99516f9b5a29c 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/plan/filter.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/plan/filter.rs @@ -104,6 +104,60 @@ impl FilterItem { FilterItem::Segment(item) => result.push(item.member_evaluator().clone()), } } + pub fn find_single_value_restriction(&self, symbol: &Rc) -> Option { + match self { + FilterItem::Item(item) => { + if &item.member_evaluator() == symbol { + item.get_single_value_restriction() + } else { + None + } + } + + FilterItem::Group(group) => match group.operator { + FilterGroupOperator::Or => { + // Для OR: если хоть одна ветка не ограничивает -> нет единого ограничения + // Если все ограничивают и все одинаковые -> то это значение + let mut candidate: Option = None; + + for child in &group.items { + match child.find_single_value_restriction(symbol) { + None => return None, // хотя бы одна альтернатива без фиксации => OR не фиксирует + Some(v) => { + if let Some(prev) = &candidate { + if prev != &v { + return None; + } + } else { + candidate = Some(v); + } + } + } + } + + candidate + } + + FilterGroupOperator::And => { + let mut candidate: Option = None; + + for child in &group.items { + if let Some(v) = child.find_single_value_restriction(symbol) { + if let Some(prev) = &candidate { + if prev != &v { + return None; + } + } + candidate = Some(v); + } + } + + candidate + } + }, + FilterItem::Segment(_) => None, + } + } } impl Filter { diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/filter/base_filter.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/filter/base_filter.rs index a0a7b36fdd9f1..781792ed07307 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/filter/base_filter.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/filter/base_filter.rs @@ -112,6 +112,14 @@ impl BaseFilter { self.values.len() == 1 && self.filter_operator == FilterOperator::Equal } + pub fn get_single_value_restriction(&self) -> Option { + if self.is_single_value_equal() { + self.values[0].clone() + } else { + None + } + } + pub fn to_sql( &self, context: Rc, diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs index 6da48c4558ab9..1ae92abd2ef7c 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs @@ -16,6 +16,7 @@ use crate::plan::{Filter, FilterItem}; use crate::planner::sql_evaluator::collectors::{ collect_multiplied_measures, has_multi_stage_members, }; +use crate::planner::sql_evaluator::symbols::apply_static_filter_to_vec; use cubenativeutils::CubeError; use itertools::Itertools; use std::collections::HashSet; @@ -416,7 +417,7 @@ impl QueryProperties { let pre_aggregation_query = options.static_data().pre_aggregation_query.unwrap_or(false); let total_query = options.static_data().total_query.unwrap_or(false); - Ok(Rc::new(Self { + let mut res = Self { measures, dimensions, segments, @@ -434,7 +435,9 @@ impl QueryProperties { pre_aggregation_query, total_query, query_join_hints, - })) + }; + res.apply_static_filter(); + Ok(Rc::new(res)) } pub fn try_new_from_precompiled( @@ -473,7 +476,7 @@ impl QueryProperties { &segments, )?; - Ok(Rc::new(Self { + let mut res = Self { measures, dimensions, time_dimensions, @@ -491,7 +494,14 @@ impl QueryProperties { pre_aggregation_query, total_query, query_join_hints, - })) + }; + res.apply_static_filter(); + Ok(Rc::new(res)) + } + + fn apply_static_filter(&mut self) { + apply_static_filter_to_vec(&mut self.measures, &self.dimensions_filters); + apply_static_filter_to_vec(&mut self.dimensions, &self.dimensions_filters); } pub fn compute_join_multi_fact_groups_with_measures( diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/case.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/case.rs index a10e525e53a89..be02b3187e6b3 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/case.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/case.rs @@ -1,7 +1,7 @@ use super::SqlNode; use crate::planner::query_tools::QueryTools; use crate::planner::sql_evaluator::symbols::{ - Case, CaseDefinition, CaseLabel, CaseSwitchDefinition, CaseSwitchItem, + Case, CaseDefinition, CaseLabel, CaseSwitchDefinition, }; use crate::planner::sql_evaluator::MemberSymbol; use crate::planner::sql_evaluator::SqlEvaluatorVisitor; @@ -69,17 +69,12 @@ impl CaseSqlNode { node_processor: Rc, templates: &PlanSqlTemplates, ) -> Result { - let expr = match &case.switch { - CaseSwitchItem::Symbol(member_symbol) => { - visitor.apply(member_symbol, node_processor.clone(), templates)? - } - CaseSwitchItem::Sql(sql_call) => sql_call.eval( - visitor, - node_processor.clone(), - query_tools.clone(), - templates, - )?, - }; + let expr = case.switch.sql.eval( + visitor, + node_processor.clone(), + query_tools.clone(), + templates, + )?; let mut when_then = Vec::new(); for itm in case.items.iter() { let when = templates.quote_string(&itm.value)?; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/cube_calc_groups.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/cube_calc_groups.rs index 9180bdd1116f0..bb0a47bb815f4 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/cube_calc_groups.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/cube_calc_groups.rs @@ -1,9 +1,8 @@ use super::SqlNode; use crate::planner::query_tools::QueryTools; -use crate::planner::sql_evaluator::DimensionSymbol; use crate::planner::sql_evaluator::MemberSymbol; use crate::planner::sql_evaluator::SqlEvaluatorVisitor; -use crate::planner::sql_templates::structs::TemplateCalcGroup; +use crate::planner::sql_templates::structs::{TemplateCalcGroup, TemplateCalcSingleValue}; use crate::planner::sql_templates::PlanSqlTemplates; use cubenativeutils::CubeError; use itertools::Itertools; @@ -71,7 +70,22 @@ impl SqlNode for CubeCalcGroupsSqlNode { )?; let res = match node.as_ref() { MemberSymbol::CubeTable(ev) => { - let res = if let Some(groups) = self.items.get(ev.cube_name()) { + let res = if let Some(calc_groups) = self.items.get(ev.cube_name()) { + let mut single_values = vec![]; + let mut groups = vec![]; + for calc_group in calc_groups { + if calc_group.values.len() == 1 { + single_values.push(TemplateCalcSingleValue { + name: calc_group.name.clone(), + value: calc_group.values[0].clone(), + }) + } else { + groups.push(TemplateCalcGroup { + name: calc_group.name.clone(), + values: calc_group.values.clone(), + }) + } + } let template_groups = groups .iter() .map(|group| TemplateCalcGroup { @@ -79,7 +93,12 @@ impl SqlNode for CubeCalcGroupsSqlNode { values: group.values.clone(), }) .collect_vec(); - let res = templates.calc_groups_join(&ev.cube_name(), &input, template_groups)?; + let res = templates.calc_groups_join( + &ev.cube_name(), + &input, + single_values, + template_groups, + )?; format!("({})", res) } else { input diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/case.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/case.rs index 8b336ea47a7bd..33d321eb6325f 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/case.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/case.rs @@ -1,6 +1,7 @@ +use crate::plan::FilterItem; use crate::{ cube_bridge::{case_variant::CaseVariant, string_or_sql::StringOrSql}, - planner::sql_evaluator::{Compiler, MemberSymbol, SqlCall}, + planner::sql_evaluator::{find_single_value_restriction, Compiler, MemberSymbol, SqlCall}, }; use cubenativeutils::CubeError; use std::rc::Rc; @@ -46,6 +47,10 @@ impl CaseDefinition { sql.extract_symbol_deps_with_path(result); } } + + fn apply_static_filter(&self, _filters: &Vec) -> Option> { + None + } } #[derive(Clone)] @@ -55,9 +60,9 @@ pub struct CaseSwitchWhenItem { } #[derive(Clone)] -pub enum CaseSwitchItem { - Symbol(Rc), - Sql(Rc), +pub struct CaseSwitchItem { + pub sql: Rc, + pub symbol_reference: Option>, } #[derive(Clone)] @@ -69,25 +74,35 @@ pub struct CaseSwitchDefinition { impl CaseSwitchDefinition { fn extract_symbol_deps(&self, result: &mut Vec>) { - match &self.switch { - CaseSwitchItem::Symbol(member_symbol) => result.push(member_symbol.clone()), - CaseSwitchItem::Sql(sql) => sql.extract_symbol_deps(result), - } + self.switch.sql.extract_symbol_deps(result); for itm in self.items.iter() { itm.sql.extract_symbol_deps(result); } self.else_sql.extract_symbol_deps(result); } fn extract_symbol_deps_with_path(&self, result: &mut Vec<(Rc, Vec)>) { - match &self.switch { - CaseSwitchItem::Symbol(member_symbol) => result.push((member_symbol.clone(), vec![])), - CaseSwitchItem::Sql(sql) => sql.extract_symbol_deps_with_path(result), - } + self.switch.sql.extract_symbol_deps_with_path(result); for itm in self.items.iter() { itm.sql.extract_symbol_deps_with_path(result); } self.else_sql.extract_symbol_deps_with_path(result); } + + fn apply_static_filter(&self, filters: &Vec) -> Option> { + if let Some(switch_ref) = &self.switch.symbol_reference { + if let Some(single_value) = find_single_value_restriction(filters, switch_ref) { + if let Some(result) = self.items.iter().find(|itm| itm.value == single_value) { + Some(result.sql.clone()) + } else { + Some(self.else_sql.clone()) + } + } else { + None + } + } else { + None + } + } } #[derive(Clone)] @@ -144,12 +159,11 @@ impl Case { compiler.compile_sql_call(&cube_name, case_definition.else_sql()?.sql()?)?; let switch_sql = compiler.compile_sql_call(&cube_name, case_definition.switch()?)?; - let switch = if let Some(symbol) = - switch_sql.resolve_direct_reference(compiler.base_tools())? - { - CaseSwitchItem::Symbol(symbol) - } else { - CaseSwitchItem::Sql(switch_sql) + let switch_reference = + switch_sql.resolve_direct_reference(compiler.base_tools())?; + let switch = CaseSwitchItem { + sql: switch_sql, + symbol_reference: switch_reference, }; Case::CaseSwitch(CaseSwitchDefinition { switch, @@ -173,4 +187,11 @@ impl Case { Case::CaseSwitch(def) => def.extract_symbol_deps_with_path(result), } } + + pub fn apply_static_filter(&self, filters: &Vec) -> Option> { + match self { + Case::Case(case) => case.apply_static_filter(filters), + Case::CaseSwitch(case) => case.apply_static_filter(filters), + } + } } diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/mod.rs index a8f9c5389213c..173f61374fc7f 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/mod.rs @@ -1,3 +1,5 @@ mod case; +mod static_filter; pub use case::*; +pub use static_filter::*; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/static_filter.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/static_filter.rs new file mode 100644 index 0000000000000..aaddad6b1b73e --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/static_filter.rs @@ -0,0 +1,64 @@ +use crate::{plan::FilterItem, planner::sql_evaluator::MemberSymbol}; +use std::rc::Rc; + +pub fn find_single_value_restriction( + filters: &Vec, + symbol: &Rc, +) -> Option { + let mut candidate: Option = None; + + for child in filters { + if let Some(v) = child.find_single_value_restriction(symbol) { + if let Some(prev) = &candidate { + if prev != &v { + return None; + } + } + candidate = Some(v); + } + } + + candidate +} + +pub fn apply_static_filter( + symbol: &Rc, + filters: &Vec, +) -> Rc { + match symbol.as_ref() { + MemberSymbol::Dimension(dim) => { + if dim.dimension_type() == "switch" { + if let Some(value) = find_single_value_restriction(filters, symbol) { + if dim.values().iter().any(|v| v == &value) { + return MemberSymbol::new_dimension(dim.replace_values(vec![value])); + } + } + } + + if let Some(case) = dim.case() { + if let Some(case_replacement) = case.apply_static_filter(filters) { + return MemberSymbol::new_dimension( + dim.replace_case_with_sql_call(case_replacement), + ); + } + } + } + MemberSymbol::Measure(meas) => { + if let Some(case) = meas.case() { + if let Some(case_replacement) = case.apply_static_filter(filters) { + return MemberSymbol::new_measure( + meas.replace_case_with_sql_call(case_replacement), + ); + } + } + } + _ => {} + } + symbol.clone() +} + +pub fn apply_static_filter_to_vec(symbols: &mut Vec>, filters: &Vec) { + symbols + .iter_mut() + .for_each(|s| *s = apply_static_filter(&s, &filters)); +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs index 66d70f90c4e4e..de1b7eb0ecc8c 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs @@ -19,6 +19,7 @@ pub struct CalendarDimensionTimeShift { pub sql: Option>, } +#[derive(Clone)] pub struct DimensionSymbol { cube: Rc, name: String, @@ -118,6 +119,19 @@ impl DimensionSymbol { &self.values } + pub(super) fn replace_values(&self, values: Vec) -> Rc { + let mut new = self.clone(); + new.values = values; + Rc::new(new) + } + + pub(super) fn replace_case_with_sql_call(&self, sql: Rc) -> Rc { + let mut new = self.clone(); + new.case = None; + new.member_sql = Some(sql); + Rc::new(new) + } + pub fn latitude(&self) -> Option> { self.latitude.clone() } diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/measure_symbol.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/measure_symbol.rs index 4fdf6cdf77fd1..08d6d77c6e76a 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/measure_symbol.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/measure_symbol.rs @@ -248,6 +248,13 @@ impl MeasureSymbol { })) } + pub(super) fn replace_case_with_sql_call(&self, sql: Rc) -> Rc { + let mut new = self.clone(); + new.case = None; + new.member_sql = Some(sql); + Rc::new(new) + } + pub fn full_name(&self) -> String { format!("{}.{}", self.cube.cube_name(), self.name) } @@ -362,8 +369,6 @@ impl MeasureSymbol { deps } - - pub fn can_used_as_addictive_in_multplied(&self) -> bool { if &self.measure_type == "countDistinct" || &self.measure_type == "countDistinctApprox" { true diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/plan.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/plan.rs index b60950db9497b..a963523b2ce5b 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/plan.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/plan.rs @@ -1,8 +1,11 @@ use super::{TemplateGroupByColumn, TemplateOrderByColumn, TemplateProjectionColumn}; -use crate::cube_bridge::driver_tools::DriverTools; use crate::cube_bridge::sql_templates_render::SqlTemplatesRender; use crate::plan::join::JoinType; use crate::planner::sql_templates::structs::TemplateCalcGroup; +use crate::{ + cube_bridge::driver_tools::DriverTools, + planner::sql_templates::structs::TemplateCalcSingleValue, +}; use convert_case::{Boundary, Case, Casing}; use cubenativeutils::CubeError; use minijinja::context; @@ -349,6 +352,7 @@ impl PlanSqlTemplates { &self, original_cube: &str, original_cube_sql: &str, + single_values: Vec, groups: Vec, ) -> Result { self.render.render_template( @@ -356,6 +360,7 @@ impl PlanSqlTemplates { context! { original_cube => original_cube, original_cube_sql => original_cube_sql, + single_values => single_values, groups => groups }, ) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/structs.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/structs.rs index 80c1eea67dfc0..9ab8b8d325253 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/structs.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/structs.rs @@ -23,3 +23,9 @@ pub struct TemplateCalcGroup { pub name: String, pub values: Vec, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TemplateCalcSingleValue { + pub name: String, + pub value: String, +} From b00147fbb14ff1525debb969c0141f3166ddc7a1 Mon Sep 17 00:00:00 2001 From: Alexandr Romanenko Date: Fri, 5 Sep 2025 20:50:06 +0200 Subject: [PATCH 07/41] fix --- rust/cubesqlplanner/cubesqlplanner/src/plan/filter.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/plan/filter.rs b/rust/cubesqlplanner/cubesqlplanner/src/plan/filter.rs index 99516f9b5a29c..83859614ec7d7 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/plan/filter.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/plan/filter.rs @@ -116,13 +116,11 @@ impl FilterItem { FilterItem::Group(group) => match group.operator { FilterGroupOperator::Or => { - // Для OR: если хоть одна ветка не ограничивает -> нет единого ограничения - // Если все ограничивают и все одинаковые -> то это значение let mut candidate: Option = None; for child in &group.items { match child.find_single_value_restriction(symbol) { - None => return None, // хотя бы одна альтернатива без фиксации => OR не фиксирует + None => return None, Some(v) => { if let Some(prev) = &candidate { if prev != &v { From 628fcf4b0df38c080de69745703a507c68489852 Mon Sep 17 00:00:00 2001 From: Alexandr Romanenko Date: Mon, 8 Sep 2025 11:57:21 +0200 Subject: [PATCH 08/41] fix --- .../src/adapter/BaseQuery.js | 10 +++--- .../sql_nodes/cube_calc_groups.rs | 12 ++----- .../src/planner/sql_templates/plan.rs | 32 +++++++++++++++++++ .../src/planner/sql_templates/structs.rs | 1 + 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js index 133e4c543233b..2922a62fc18c2 100644 --- a/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js +++ b/packages/cubejs-schema-compiler/src/adapter/BaseQuery.js @@ -4214,23 +4214,23 @@ export class BaseQuery { '{{ min_expr }} as {{ quoted_min_name }}\n' + 'FROM {{ from_prepared }}\n' + '{% if filter %}WHERE {{ filter }}{% endif %}', - calc_groups_join: 'SELECT "{{ original_cube }}".*, ' + + calc_groups_join: 'SELECT {{ original_cube }}.*, ' + '{%for single_value in single_values %}' + - '\'{{ single_value.value }}\' as "{{ single_value.name }}"{% if not loop.last %}, {% endif %}' + + '{{ single_value.value }} as {{ single_value.name }}{% if not loop.last %}, {% endif %}' + '{% endfor %}' + '{% if single_values and groups %}, {% endif %}' + '{%for group in groups %}' + - '"{{ group.name }}"{% if not loop.last %}, {% endif %}' + + '{{ group.name }}{% if not loop.last %}, {% endif %}' + '{% endfor %}' + 'FROM {{ original_cube_sql }} {{ original_cube }}\n' + '{% for group in groups %}' + 'CROSS JOIN\n' + '(\n' + '{% for value in group.values %}' + - 'SELECT \'{{ value }}\' as "{{ group.name }}"' + + 'SELECT {{ value }} as {{ group.name }}' + '{% if not loop.last %} UNION ALL\n{% endif %}' + '{% endfor %}' + - ') "{{ group.name }}_values"\n' + + ') {{ group.alias }}\n' + '{% endfor %}' }, expressions: { diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/cube_calc_groups.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/cube_calc_groups.rs index bb0a47bb815f4..bb6760cd9414a 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/cube_calc_groups.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/cube_calc_groups.rs @@ -72,7 +72,7 @@ impl SqlNode for CubeCalcGroupsSqlNode { MemberSymbol::CubeTable(ev) => { let res = if let Some(calc_groups) = self.items.get(ev.cube_name()) { let mut single_values = vec![]; - let mut groups = vec![]; + let mut template_groups = vec![]; for calc_group in calc_groups { if calc_group.values.len() == 1 { single_values.push(TemplateCalcSingleValue { @@ -80,19 +80,13 @@ impl SqlNode for CubeCalcGroupsSqlNode { value: calc_group.values[0].clone(), }) } else { - groups.push(TemplateCalcGroup { + template_groups.push(TemplateCalcGroup { name: calc_group.name.clone(), + alias: format!("{}_values", calc_group.name), values: calc_group.values.clone(), }) } } - let template_groups = groups - .iter() - .map(|group| TemplateCalcGroup { - name: group.name.clone(), - values: group.values.clone(), - }) - .collect_vec(); let res = templates.calc_groups_join( &ev.cube_name(), &input, diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/plan.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/plan.rs index a963523b2ce5b..b27cc57e0e38c 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/plan.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/plan.rs @@ -355,6 +355,38 @@ impl PlanSqlTemplates { single_values: Vec, groups: Vec, ) -> Result { + let original_cube = self.quote_identifier(original_cube)?; + let single_values = single_values + .into_iter() + .map(|v| -> Result<_, CubeError> { + Ok(TemplateCalcSingleValue { + value: self.quote_string(&v.value)?, + name: self.quote_identifier(&v.name)?, + }) + }) + .collect::, _>>()?; + let groups = groups + .into_iter() + .map(|g| -> Result<_, CubeError> { + let TemplateCalcGroup { + name, + alias, + values, + } = g; + let name = self.quote_identifier(&name)?; + let alias = self.quote_identifier(&alias)?; + let values = values + .into_iter() + .map(|v| self.quote_string(&v)) + .collect::, _>>()?; + + Ok(TemplateCalcGroup { + name, + alias, + values, + }) + }) + .collect::, _>>()?; self.render.render_template( "statements/calc_groups_join", context! { diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/structs.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/structs.rs index 9ab8b8d325253..4e2e38494da72 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/structs.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_templates/structs.rs @@ -21,6 +21,7 @@ pub struct TemplateOrderByColumn { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TemplateCalcGroup { pub name: String, + pub alias: String, pub values: Vec, } From 11d516ddc778320f806c0c0ac4991963e83dab44 Mon Sep 17 00:00:00 2001 From: Alexandr Romanenko Date: Mon, 8 Sep 2025 13:33:42 +0200 Subject: [PATCH 09/41] in work --- .../integration/postgres/calc-groups.test.ts | 294 ++++++++++++++++++ .../test/integration/utils/BaseDbRunner.ts | 1 + .../physical_plan_builder/processors/query.rs | 36 ++- .../cubesqlplanner/src/plan/filter.rs | 4 +- .../multi_stage/multi_stage_query_planner.rs | 3 + .../src/planner/query_properties.rs | 12 +- .../collectors/calc_group_dims_collector.rs | 59 ++++ .../planner/sql_evaluator/collectors/mod.rs | 2 + .../sql_nodes/cube_calc_groups.rs | 1 - .../symbols/common/static_filter.rs | 38 ++- .../sql_evaluator/symbols/dimension_symbol.rs | 6 - 11 files changed, 407 insertions(+), 49 deletions(-) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/collectors/calc_group_dims_collector.rs diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts index 5777b2bd64e24..5356a982fb146 100644 --- a/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts +++ b/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts @@ -51,12 +51,29 @@ cubes: - EUR - GBP + - name: currency_ref + type: string + sql: "{currency}" + + - name: strategy type: switch values: - A - B + - name: strategy_ref + type: string + sql: "{strategy}" + + - name: currency_and_stategy + type: string + sql: "CONCAT({currency}, '-', {strategy})" + + - name: currency_and_strategy_ref + type: string + sql: "{currency_and_stategy}" + - name: currency_full_name type: string case: @@ -103,11 +120,20 @@ cubes: else: sql: "{CUBE.amount_gbp}" + - name: amount_in_currency_ref + type: number + sql: "{CUBE.amount_in_currency}" + - name: returned_count type: count filters: - sql: "{CUBE}.STATUS = 'returned'" + - name: amount_in_currency_percent_of_usd + type: number + sql: "FLOOR({CUBE.amount_in_currency_ref} / {CUBE.amount_usd} * 100)" + + - name: return_rate type: number sql: "({returned_count} / NULLIF({completed_count}, 0)) * 100.0" @@ -300,6 +326,67 @@ views: ], { joinGraph, cubeEvaluator, compiler })); + it('basic cross join by proxy dim', async () => dbRunner.runQueryTest({ + dimensions: ['orders.currency_ref'], + timeDimensions: [ + { + dimension: 'orders.date', + granularity: 'year' + } + ], + timezone: 'UTC' + }, [ + { + orders__currency_ref: 'EUR', + orders__date_year: '2022-01-01T00:00:00.000Z' + }, + { + orders__currency_ref: 'GBP', + orders__date_year: '2022-01-01T00:00:00.000Z' + }, + { + orders__currency_ref: 'USD', + orders__date_year: '2022-01-01T00:00:00.000Z' + }, + { + orders__currency_ref: 'EUR', + orders__date_year: '2023-01-01T00:00:00.000Z' + }, + { + orders__currency_ref: 'GBP', + orders__date_year: '2023-01-01T00:00:00.000Z' + }, + { + orders__currency_ref: 'USD', + orders__date_year: '2023-01-01T00:00:00.000Z' + }, + { + orders__currency_ref: 'EUR', + orders__date_year: '2024-01-01T00:00:00.000Z' + }, + { + orders__currency_ref: 'GBP', + orders__date_year: '2024-01-01T00:00:00.000Z' + }, + { + orders__currency_ref: 'USD', + orders__date_year: '2024-01-01T00:00:00.000Z' + }, + { + orders__currency_ref: 'EUR', + orders__date_year: '2025-01-01T00:00:00.000Z' + }, + { + orders__currency_ref: 'GBP', + orders__date_year: '2025-01-01T00:00:00.000Z' + }, + { + orders__currency_ref: 'USD', + orders__date_year: '2025-01-01T00:00:00.000Z' + } + ], + { joinGraph, cubeEvaluator, compiler })); + it('basic double cross join', async () => dbRunner.runQueryTest({ dimensions: ['orders.currency', 'orders.strategy'], timeDimensions: [ @@ -382,6 +469,76 @@ views: ], { joinGraph, cubeEvaluator, compiler })); + it('basic double cross join by proxy', async () => dbRunner.runQueryTest({ + dimensions: ['orders.currency_and_strategy_ref'], + timeDimensions: [ + { + dimension: 'orders.date', + granularity: 'year', + dateRange: ['2024-01-01', '2026-01-01'] + } + ], + timezone: 'UTC', + order: [{ + id: 'orders.date' + }, { + id: 'orders.currency' + }, { + id: 'orders.strategy' + }, + ], + }, [ + { + orders__currency_and_strategy_ref: 'EUR-A', + orders__date_year: '2024-01-01T00:00:00.000Z' + }, + { + orders__currency_and_strategy_ref: 'EUR-B', + orders__date_year: '2024-01-01T00:00:00.000Z' + }, + { + orders__currency_and_strategy_ref: 'GBP-A', + orders__date_year: '2024-01-01T00:00:00.000Z' + }, + { + orders__currency_and_strategy_ref: 'GBP-B', + orders__date_year: '2024-01-01T00:00:00.000Z' + }, + { + orders__currency_and_strategy_ref: 'USD-A', + orders__date_year: '2024-01-01T00:00:00.000Z' + }, + { + orders__currency_and_strategy_ref: 'USD-B', + orders__date_year: '2024-01-01T00:00:00.000Z' + }, + { + orders__currency_and_strategy_ref: 'EUR-A', + orders__date_year: '2025-01-01T00:00:00.000Z' + }, + { + orders__currency_and_strategy_ref: 'EUR-B', + orders__date_year: '2025-01-01T00:00:00.000Z' + }, + { + orders__currency_and_strategy_ref: 'GBP-A', + orders__date_year: '2025-01-01T00:00:00.000Z' + }, + { + orders__currency_and_strategy_ref: 'GBP-B', + orders__date_year: '2025-01-01T00:00:00.000Z' + }, + { + orders__currency_and_strategy_ref: 'USD-A', + orders__date_year: '2025-01-01T00:00:00.000Z' + }, + { + orders__currency_and_strategy_ref: 'USD-B', + orders__date_year: '2025-01-01T00:00:00.000Z' + } + ], + { joinGraph, cubeEvaluator, compiler })); + it('basic cross join with measure', async () => dbRunner.runQueryTest({ dimensions: ['orders.strategy'], measures: ['orders.revenue'], @@ -489,6 +646,53 @@ views: expect(sqlAndParams[0]).not.toMatch(/CROSS.+JOIN/); }); + it('basic cross join with filters proxy dim', async () => { + const sqlAndParams = await dbRunner.runQueryTest({ + dimensions: ['orders.strategy_ref'], + measures: ['orders.revenue'], + timeDimensions: [ + { + dimension: 'orders.date', + granularity: 'year' + } + ], + filters: [ + { dimension: 'orders.strategy_ref', operator: 'equals', values: ['B'] } + ], + timezone: 'UTC', + order: [{ + id: 'orders.date' + }, { + id: 'orders.currency' + }, + ], + }, [ + { + orders__date_year: '2022-01-01T00:00:00.000Z', + orders__strategy_ref: 'B', + orders__revenue: '5', + }, + { + orders__date_year: '2023-01-01T00:00:00.000Z', + orders__strategy_ref: 'B', + orders__revenue: '15', + }, + { + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__strategy_ref: 'B', + orders__revenue: '30', + }, + { + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__strategy_ref: 'B', + orders__revenue: '5', + } + ], + { joinGraph, cubeEvaluator, compiler }); + + expect(sqlAndParams[0]).not.toMatch(/CROSS.+JOIN/); + }); + it('dimension switch expression simple', async () => dbRunner.runQueryTest({ dimensions: ['orders.currency', 'orders.currency_full_name'], measures: ['orders.revenue'], @@ -603,6 +807,57 @@ views: ], { joinGraph, cubeEvaluator, compiler })); + it('complex measure switch cross join', async () => dbRunner.runQueryTest({ + dimensions: ['orders.currency'], + measures: ['orders.amount_in_currency_percent_of_usd'], + timeDimensions: [ + { + dimension: 'orders.date', + granularity: 'year', + dateRange: ['2024-01-01', '2026-01-01'] + } + ], + timezone: 'UTC', + order: [{ + id: 'orders.date' + }, { + id: 'orders.currency' + }, + ], + }, [ + { + orders__currency: 'EUR', + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__amount_in_currency_percent_of_usd: '97' + }, + { + orders__currency: 'GBP', + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__amount_in_currency_percent_of_usd: '80' + }, + { + orders__currency: 'USD', + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__amount_in_currency_percent_of_usd: '100' + }, + { + orders__currency: 'EUR', + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__amount_in_currency_percent_of_usd: '95' + }, + { + orders__currency: 'GBP', + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__amount_in_currency_percent_of_usd: '82' + }, + { + orders__currency: 'USD', + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__amount_in_currency_percent_of_usd: '100' + } + ], + { joinGraph, cubeEvaluator, compiler })); + it('measure switch with filter', async () => { const sqlAndParams = await dbRunner.runQueryTest({ dimensions: ['orders.currency'], @@ -643,6 +898,45 @@ views: expect(sqlAndParams[0]).not.toMatch(/CASE/); expect(sqlAndParams[0]).not.toMatch(/CROSS.+JOIN/); }); + + it('complex measure switch with filter', async () => { + const sqlAndParams = await dbRunner.runQueryTest({ + dimensions: ['orders.currency'], + measures: ['orders.amount_in_currency_percent_of_usd'], + timeDimensions: [ + { + dimension: 'orders.date', + granularity: 'year', + dateRange: ['2024-01-01', '2026-01-01'] + } + ], + filters: [ + { dimension: 'orders.currency', operator: 'equals', values: ['EUR'] } + ], + timezone: 'UTC', + order: [{ + id: 'orders.date' + }, { + id: 'orders.currency' + }, + ], + }, [ + { + orders__currency: 'EUR', + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__amount_in_currency_percent_of_usd: '97' + }, + { + orders__currency: 'EUR', + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__amount_in_currency_percent_of_usd: '95' + }, + ], + { joinGraph, cubeEvaluator, compiler }); + + expect(sqlAndParams[0]).not.toMatch(/CASE/); + expect(sqlAndParams[0]).not.toMatch(/CROSS.+JOIN/); + }); } else { // This test is working only in tesseract test.skip('calc groups testst', () => { expect(1).toBe(1); }); diff --git a/packages/cubejs-schema-compiler/test/integration/utils/BaseDbRunner.ts b/packages/cubejs-schema-compiler/test/integration/utils/BaseDbRunner.ts index 80f3259ef4bbb..9e1570d9df6d7 100644 --- a/packages/cubejs-schema-compiler/test/integration/utils/BaseDbRunner.ts +++ b/packages/cubejs-schema-compiler/test/integration/utils/BaseDbRunner.ts @@ -30,6 +30,7 @@ export class BaseDbRunner { const res = await this.testQuery(sqlAndParams); console.log(JSON.stringify(res)); + console.log("!!!! res: ", res); expect(res).toEqual( expectedResult diff --git a/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/query.rs b/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/query.rs index 628c97f94a1d2..f710b7793c252 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/query.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/query.rs @@ -1,9 +1,10 @@ use super::super::{LogicalNodeProcessor, ProcessableNode, PushDownBuilderContext}; use crate::logical_plan::{Query, QuerySource}; use crate::physical_plan_builder::PhysicalPlanBuilder; -use crate::plan::{Cte, Expr, MemberExpression, Select, SelectBuilder}; +use crate::plan::{Cte, Expr, Filter, MemberExpression, Select, SelectBuilder}; +use crate::planner::sql_evaluator::collectors::collect_calc_group_dims; use crate::planner::sql_evaluator::sql_nodes::SqlNodesFactory; -use crate::planner::sql_evaluator::{MemberSymbol, ReferencesBuilder}; +use crate::planner::sql_evaluator::{get_filtered_values, MemberSymbol, ReferencesBuilder}; use cubenativeutils::CubeError; use std::collections::HashMap; use std::rc::Rc; @@ -21,16 +22,21 @@ impl QueryProcessor<'_> { } } - fn process_calc_group(&self, symbol: &Rc, context_factory: &mut SqlNodesFactory) { - if let Ok(dimension) = symbol.as_dimension() { - if dimension.is_calc_group() { - context_factory.add_calc_group_item( - dimension.cube_name().clone(), - dimension.name().clone(), - dimension.values().clone(), - ); - } + fn process_calc_group( + &self, + symbol: &Rc, + context_factory: &mut SqlNodesFactory, + filter: &Option, + ) -> Result<(), CubeError> { + for dim in collect_calc_group_dims(symbol)? { + let values = get_filtered_values(&dim, filter); + context_factory.add_calc_group_item( + dim.cube_name().clone(), + dim.name().clone(), + values, + ); } + Ok(()) } } @@ -101,13 +107,16 @@ impl<'a> LogicalNodeProcessor<'a, Query> for QueryProcessor<'a> { select_builder.set_ctes(ctes); context_factory.set_ungrouped(logical_plan.modifers.ungrouped); + let filter = logical_plan.filter.all_filters(); + let having = logical_plan.filter.measures_filter(); + for member in logical_plan.schema.all_dimensions() { references_builder.resolve_references_for_member( member.clone(), &None, &mut render_references, )?; - self.process_calc_group(member, &mut context_factory); + self.process_calc_group(member, &mut context_factory, &filter)?; if context.measure_subquery { select_builder.add_projection_member_without_schema(member, None); } else { @@ -131,9 +140,6 @@ impl<'a> LogicalNodeProcessor<'a, Query> for QueryProcessor<'a> { } } - let filter = logical_plan.filter.all_filters(); - let having = logical_plan.filter.measures_filter(); - if self.is_over_full_aggregated_source(logical_plan) { references_builder.resolve_references_for_filter(&having, &mut render_references)?; select_builder.set_filter(having); diff --git a/rust/cubesqlplanner/cubesqlplanner/src/plan/filter.rs b/rust/cubesqlplanner/cubesqlplanner/src/plan/filter.rs index 83859614ec7d7..e2e99cc5f4bbb 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/plan/filter.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/plan/filter.rs @@ -107,7 +107,9 @@ impl FilterItem { pub fn find_single_value_restriction(&self, symbol: &Rc) -> Option { match self { FilterItem::Item(item) => { - if &item.member_evaluator() == symbol { + if item.member_evaluator().resolve_reference_chain() + == symbol.clone().resolve_reference_chain() + { item.get_single_value_restriction() } else { None diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs index eca4d78eeb19e..dd885278ce840 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs @@ -6,6 +6,7 @@ use super::{ use crate::cube_bridge::measure_definition::RollingWindow; use crate::logical_plan::*; use crate::planner::query_tools::QueryTools; +use crate::planner::sql_evaluator::apply_static_filter_to_symbol; use crate::planner::sql_evaluator::collectors::has_multi_stage_members; use crate::planner::sql_evaluator::collectors::member_childs; use crate::planner::sql_evaluator::MemberSymbol; @@ -156,6 +157,8 @@ impl MultiStageQueryPlanner { descriptions: &mut Vec>, ) -> Result, CubeError> { let member = member.resolve_reference_chain(); + let member = apply_static_filter_to_symbol(&member, state.dimensions_filters()); + let member_name = member.full_name(); if let Some(exists) = descriptions .iter() diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs index 1ae92abd2ef7c..14466f887969d 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs @@ -16,7 +16,6 @@ use crate::plan::{Filter, FilterItem}; use crate::planner::sql_evaluator::collectors::{ collect_multiplied_measures, has_multi_stage_members, }; -use crate::planner::sql_evaluator::symbols::apply_static_filter_to_vec; use cubenativeutils::CubeError; use itertools::Itertools; use std::collections::HashSet; @@ -417,7 +416,7 @@ impl QueryProperties { let pre_aggregation_query = options.static_data().pre_aggregation_query.unwrap_or(false); let total_query = options.static_data().total_query.unwrap_or(false); - let mut res = Self { + let res = Self { measures, dimensions, segments, @@ -436,7 +435,6 @@ impl QueryProperties { total_query, query_join_hints, }; - res.apply_static_filter(); Ok(Rc::new(res)) } @@ -476,7 +474,7 @@ impl QueryProperties { &segments, )?; - let mut res = Self { + let res = Self { measures, dimensions, time_dimensions, @@ -495,15 +493,9 @@ impl QueryProperties { total_query, query_join_hints, }; - res.apply_static_filter(); Ok(Rc::new(res)) } - fn apply_static_filter(&mut self) { - apply_static_filter_to_vec(&mut self.measures, &self.dimensions_filters); - apply_static_filter_to_vec(&mut self.dimensions, &self.dimensions_filters); - } - pub fn compute_join_multi_fact_groups_with_measures( &self, measures: &Vec>, diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/collectors/calc_group_dims_collector.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/collectors/calc_group_dims_collector.rs new file mode 100644 index 0000000000000..5c6dcf73e5b55 --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/collectors/calc_group_dims_collector.rs @@ -0,0 +1,59 @@ +use crate::planner::sql_evaluator::{MemberSymbol, TraversalVisitor}; +use cubenativeutils::CubeError; +use itertools::Itertools; +use std::rc::Rc; + +pub struct CalcGroupDimsCollector { + calc_groups: Vec>, +} + +impl CalcGroupDimsCollector { + pub fn new() -> Self { + Self { + calc_groups: Vec::new(), + } + } + + pub fn extract_result(self) -> Vec> { + self.calc_groups + .into_iter() + .unique_by(|dim| dim.full_name()) + .collect() + } +} + +impl TraversalVisitor for CalcGroupDimsCollector { + type State = (); + fn on_node_traverse( + &mut self, + node: &Rc, + path: &Vec, + _: &Self::State, + ) -> Result, CubeError> { + match node.as_ref() { + MemberSymbol::Dimension(e) => { + if e.is_calc_group() { + self.calc_groups.push(node.clone()); + return Ok(None); + } + } + MemberSymbol::TimeDimension(e) => { + return self.on_node_traverse(e.base_symbol(), path, &()) + } + MemberSymbol::Measure(_) => {} + MemberSymbol::CubeName(_) => {} + MemberSymbol::CubeTable(_) => {} + MemberSymbol::MemberExpression(_) => {} + }; + Ok(Some(())) + } +} + +pub fn collect_calc_group_dims( + node: &Rc, +) -> Result>, CubeError> { + let mut visitor = CalcGroupDimsCollector::new(); + visitor.apply(node, &())?; + let res = visitor.extract_result(); + Ok(res) +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/collectors/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/collectors/mod.rs index 04650c3131737..eb7245cc037db 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/collectors/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/collectors/mod.rs @@ -1,3 +1,4 @@ +mod calc_group_dims_collector; mod cube_names_collector; mod find_owned_by_cube; mod has_cumulative_members; @@ -10,6 +11,7 @@ mod sub_query_dimensions; pub use cube_names_collector::*; pub use find_owned_by_cube::*; +pub use calc_group_dims_collector::collect_calc_group_dims; pub use has_cumulative_members::{has_cumulative_members, HasCumulativeMembersCollector}; pub use has_multi_stage_members::{has_multi_stage_members, HasMultiStageMembersCollector}; pub use join_hints_collector::{ diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/cube_calc_groups.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/cube_calc_groups.rs index bb6760cd9414a..68bddc9f61fd5 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/cube_calc_groups.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/sql_nodes/cube_calc_groups.rs @@ -5,7 +5,6 @@ use crate::planner::sql_evaluator::SqlEvaluatorVisitor; use crate::planner::sql_templates::structs::{TemplateCalcGroup, TemplateCalcSingleValue}; use crate::planner::sql_templates::PlanSqlTemplates; use cubenativeutils::CubeError; -use itertools::Itertools; use std::any::Any; use std::collections::HashMap; use std::rc::Rc; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/static_filter.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/static_filter.rs index aaddad6b1b73e..340885095432a 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/static_filter.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/common/static_filter.rs @@ -1,4 +1,7 @@ -use crate::{plan::FilterItem, planner::sql_evaluator::MemberSymbol}; +use crate::{ + plan::{Filter, FilterItem}, + planner::sql_evaluator::MemberSymbol, +}; use std::rc::Rc; pub fn find_single_value_restriction( @@ -21,20 +24,29 @@ pub fn find_single_value_restriction( candidate } -pub fn apply_static_filter( - symbol: &Rc, - filters: &Vec, -) -> Rc { - match symbol.as_ref() { - MemberSymbol::Dimension(dim) => { - if dim.dimension_type() == "switch" { - if let Some(value) = find_single_value_restriction(filters, symbol) { +pub fn get_filtered_values(symbol: &Rc, filter: &Option) -> Vec { + if let Ok(dim) = symbol.as_dimension() { + if dim.dimension_type() == "switch" { + if let Some(filter) = filter { + if let Some(value) = find_single_value_restriction(&filter.items, symbol) { if dim.values().iter().any(|v| v == &value) { - return MemberSymbol::new_dimension(dim.replace_values(vec![value])); + return vec![value]; } } } + } + return dim.values().clone(); + } + + vec![] +} +pub fn apply_static_filter_to_symbol( + symbol: &Rc, + filters: &Vec, +) -> Rc { + match symbol.as_ref() { + MemberSymbol::Dimension(dim) => { if let Some(case) = dim.case() { if let Some(case_replacement) = case.apply_static_filter(filters) { return MemberSymbol::new_dimension( @@ -56,9 +68,3 @@ pub fn apply_static_filter( } symbol.clone() } - -pub fn apply_static_filter_to_vec(symbols: &mut Vec>, filters: &Vec) { - symbols - .iter_mut() - .for_each(|s| *s = apply_static_filter(&s, &filters)); -} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs index de1b7eb0ecc8c..9a7699146d8a8 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/planner/sql_evaluator/symbols/dimension_symbol.rs @@ -119,12 +119,6 @@ impl DimensionSymbol { &self.values } - pub(super) fn replace_values(&self, values: Vec) -> Rc { - let mut new = self.clone(); - new.values = values; - Rc::new(new) - } - pub(super) fn replace_case_with_sql_call(&self, sql: Rc) -> Rc { let mut new = self.clone(); new.case = None; From 507f024b7f98ed6295ec91991d52a164be23818d Mon Sep 17 00:00:00 2001 From: Alexandr Romanenko Date: Mon, 8 Sep 2025 18:34:00 +0200 Subject: [PATCH 10/41] almost work --- .../integration/postgres/calc-groups.test.ts | 360 ++++++++++++++++-- .../test/integration/utils/BaseDbRunner.ts | 2 +- .../src/physical_plan_builder/builder.rs | 25 +- .../aggregate_multiplied_subquery.rs | 2 +- .../processors/full_key_aggregate.rs | 2 +- .../processors/full_key_aggregate_query.rs | 99 ----- .../processors/keys_sub_query.rs | 12 +- .../multi_stage_measure_calculation.rs | 6 +- .../processors/multi_stage_rolling_window.rs | 4 +- .../processors/multi_stage_time_series.rs | 2 +- .../physical_plan_builder/processors/query.rs | 26 +- .../cubesqlplanner/src/plan/builder/select.rs | 12 +- .../cubesqlplanner/src/plan/filter.rs | 4 +- .../cubesqlplanner/src/plan/schema/column.rs | 9 +- .../cubesqlplanner/src/plan/schema/schema.rs | 11 +- .../cubesqlplanner/src/plan/time_series.rs | 11 +- .../multi_stage/multi_stage_query_planner.rs | 12 +- .../sql_evaluator/references_builder.rs | 31 +- .../sql_nodes/cube_calc_groups.rs | 4 +- .../sql_evaluator/symbols/dimension_symbol.rs | 12 +- .../sql_evaluator/symbols/measure_symbol.rs | 8 + 21 files changed, 438 insertions(+), 216 deletions(-) delete mode 100644 rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/full_key_aggregate_query.rs diff --git a/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts b/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts index 5356a982fb146..a32f715cb004a 100644 --- a/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts +++ b/packages/cubejs-schema-compiler/test/integration/postgres/calc-groups.test.ts @@ -246,7 +246,178 @@ cubes: measures: - name: count type: count + + - name: source_root + sql: > + SELECT 1 + public: false + + joins: + - name: source_a + sql: "1 = 1" + relationship: one_to_many + - name: source_b + sql: "1 = 1" + relationship: one_to_many + + dimensions: + - name: pk + type: number + sql: "1" + primary_key: true + + - name: source + type: switch + values: ["A", "B"] + + - name: currency + type: switch + values: ["USD", "EUR"] + + - name: product_category + type: string + case: + switch: "{CUBE.source}" + when: + - value: A + sql: "{source_a.product_category}" + - value: B + sql: "{source_b.product_category}" + else: + sql: "{source_a.product_category}" + + + measures: + - name: count + type: sum + multi_stage: true + case: + switch: "{CUBE.source}" + when: + - value: A + sql: "{source_a.count}" + - value: B + sql: "{source_b.count}" + else: + sql: "{source_a.count}" + + - name: price_eur + type: sum + multi_stage: true + case: + switch: "{CUBE.source}" + when: + - value: A + sql: "{source_a.price_eur}" + - value: B + sql: "{source_b.price_eur}" + else: + sql: "{source_a.price_eur}" + + - name: price_usd + type: sum + multi_stage: true + case: + switch: "{CUBE.source}" + when: + - value: A + sql: "{source_a.price_usd}" + - value: B + sql: "{source_b.price_usd}" + else: + sql: "{source_a.price_usd}" + + - name: price + type: sum + multi_stage: true + case: + switch: "{CUBE.currency}" + when: + - value: USD + sql: "{CUBE.price_usd}" + - value: EUR + sql: "{CUBE.price_eur}" + else: + sql: "{CUBE.price_usd}" + + + - name: source_a + sql: > + SELECT 10 as ID, 'some category' as PRODUCT_CATEGORY, 'some name' as NAME, 100 as PRICE_USD, 0 as PRICE_EUR + union all + SELECT 11 as ID, 'some category' as PRODUCT_CATEGORY, 'some name' as NAME, 500 as PRICE_USD, 0 as PRICE_EUR + union all + SELECT 12 as ID, 'some category A' as PRODUCT_CATEGORY, 'some name' as NAME, 200 as PRICE_USD, 0 as PRICE_EUR + union all + SELECT 13 as ID, 'some category A' as PRODUCT_CATEGORY, 'some name' as NAME, 300 as PRICE_USD, 0 as PRICE_EUR + public: false + + dimensions: + - name: pk + type: number + sql: ID + primary_key: true + + - name: product_category + sql: PRODUCT_CATEGORY + type: string + + measures: + - name: count + type: 'count' + + - name: price_usd + type: 'sum' + sql: PRICE_USD + + - name: price_eur + type: 'sum' + sql: PRICE_EUR + + + - name: source_b + sql: > + SELECT 10 as ID, 'some category' as PRODUCT_CATEGORY, 'some name' as NAME, 0 as PRICE_USD, 100 as PRICE_EUR + union all + SELECT 11 as ID, 'some category' as PRODUCT_CATEGORY, 'some name' as NAME, 0 as PRICE_USD, 500 as PRICE_EUR + union all + SELECT 12 as ID, 'some category B' as PRODUCT_CATEGORY, 'some name' as NAME, 0 as PRICE_USD, 200 as PRICE_EUR + union all + SELECT 13 as ID, 'some category B' as PRODUCT_CATEGORY, 'some name' as NAME, 0 as PRICE_USD, 300 as PRICE_EUR + union all + SELECT 14 as ID, 'some category B' as PRODUCT_CATEGORY, 'some name' as NAME, 0 as PRICE_USD, 300 as PRICE_EUR + public: false + + dimensions: + - name: pk + type: number + sql: ID + primary_key: true + + - name: product_category + sql: PRODUCT_CATEGORY + type: string + + measures: + - name: count + type: 'count' + + - name: price_usd + type: 'sum' + sql: PRICE_USD + + - name: price_eur + type: 'sum' + sql: PRICE_EUR + views: + - name: source + cubes: + - join_path: source_root + includes: "*" + + + - name: orders_view cubes: @@ -825,37 +996,37 @@ views: }, ], }, [ - { - orders__currency: 'EUR', - orders__date_year: '2024-01-01T00:00:00.000Z', - orders__amount_in_currency_percent_of_usd: '97' - }, - { - orders__currency: 'GBP', - orders__date_year: '2024-01-01T00:00:00.000Z', - orders__amount_in_currency_percent_of_usd: '80' - }, - { - orders__currency: 'USD', - orders__date_year: '2024-01-01T00:00:00.000Z', - orders__amount_in_currency_percent_of_usd: '100' - }, - { - orders__currency: 'EUR', - orders__date_year: '2025-01-01T00:00:00.000Z', - orders__amount_in_currency_percent_of_usd: '95' - }, - { - orders__currency: 'GBP', - orders__date_year: '2025-01-01T00:00:00.000Z', - orders__amount_in_currency_percent_of_usd: '82' - }, - { - orders__currency: 'USD', - orders__date_year: '2025-01-01T00:00:00.000Z', - orders__amount_in_currency_percent_of_usd: '100' - } - ], + { + orders__currency: 'EUR', + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__amount_in_currency_percent_of_usd: '97' + }, + { + orders__currency: 'GBP', + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__amount_in_currency_percent_of_usd: '80' + }, + { + orders__currency: 'USD', + orders__date_year: '2024-01-01T00:00:00.000Z', + orders__amount_in_currency_percent_of_usd: '100' + }, + { + orders__currency: 'EUR', + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__amount_in_currency_percent_of_usd: '95' + }, + { + orders__currency: 'GBP', + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__amount_in_currency_percent_of_usd: '82' + }, + { + orders__currency: 'USD', + orders__date_year: '2025-01-01T00:00:00.000Z', + orders__amount_in_currency_percent_of_usd: '100' + } + ], { joinGraph, cubeEvaluator, compiler })); it('measure switch with filter', async () => { @@ -937,6 +1108,133 @@ views: expect(sqlAndParams[0]).not.toMatch(/CASE/); expect(sqlAndParams[0]).not.toMatch(/CROSS.+JOIN/); }); + it('source switch cross join', async () => { + await dbRunner.runQueryTest({ + dimensions: ['source.source'], + measures: ['source.count'], + order: [{ + id: 'source.source' + } + ], + }, [ + { source__source: 'A', source__count: '4' }, + { source__source: 'B', source__count: '5' } + ], + { joinGraph, cubeEvaluator, compiler }); + }); + it('source switch cross join without dimension', async () => { + await dbRunner.runQueryTest({ + dimensions: ['source.product_category'], + measures: ['source.count'], + order: [{ + id: 'source.product_category' + } + ], + }, [ + { source__product_category: 'some category', source__count: '4' }, + { source__product_category: 'some category A', source__count: '2' }, + { source__product_category: 'some category B', source__count: '3' } + ], + { joinGraph, cubeEvaluator, compiler }); + }); + it('source full switch', async () => { + await dbRunner.runQueryTest({ + dimensions: ['source.currency', 'source.product_category'], + measures: ['source.price'], + order: [{ + id: 'source.product_category' + }, + { + id: 'source.currency' + } + ], + }, [ + { + source__currency: 'EUR', + source__product_category: 'some category', + source__price: '600' + }, + { + source__currency: 'USD', + source__product_category: 'some category', + source__price: '600' + }, + { + source__currency: 'EUR', + source__product_category: 'some category A', + source__price: '0' + }, + { + source__currency: 'USD', + source__product_category: 'some category A', + source__price: '500' + }, + { + source__currency: 'EUR', + source__product_category: 'some category B', + source__price: '800' + }, + { + source__currency: 'USD', + source__product_category: 'some category B', + source__price: '0' + } + ], + { joinGraph, cubeEvaluator, compiler }); + }); + it('source switch - source_a + usd', async () => { + await dbRunner.runQueryTest({ + dimensions: ['source.currency', 'source.product_category'], + measures: ['source.price'], + order: [{ + id: 'source.product_category' + }, + ], + filters: [ + { dimension: 'source.currency', operator: 'equals', values: ['USD'] }, + { dimension: 'source.source', operator: 'equals', values: ['A'] } + ], + }, [ + { + source__currency: 'USD', + source__product_category: 'some category', + source__price: '600' + }, + { + source__currency: 'USD', + source__product_category: 'some category A', + source__price: '500' + }, + ], + { joinGraph, cubeEvaluator, compiler }); + }); + + it('source switch - source_b + eur', async () => { + await dbRunner.runQueryTest({ + dimensions: ['source.currency', 'source.product_category'], + measures: ['source.price'], + order: [{ + id: 'source.product_category' + }, + ], + filters: [ + { dimension: 'source.currency', operator: 'equals', values: ['EUR'] }, + { dimension: 'source.source', operator: 'equals', values: ['B'] } + ], + }, [ + { + source__currency: 'EUR', + source__product_category: 'some category', + source__price: '600' + }, + { + source__currency: 'EUR', + source__product_category: 'some category B', + source__price: '800' + }, + ], + { joinGraph, cubeEvaluator, compiler }); + }); } else { // This test is working only in tesseract test.skip('calc groups testst', () => { expect(1).toBe(1); }); diff --git a/packages/cubejs-schema-compiler/test/integration/utils/BaseDbRunner.ts b/packages/cubejs-schema-compiler/test/integration/utils/BaseDbRunner.ts index 9e1570d9df6d7..a3d3ffbcfef85 100644 --- a/packages/cubejs-schema-compiler/test/integration/utils/BaseDbRunner.ts +++ b/packages/cubejs-schema-compiler/test/integration/utils/BaseDbRunner.ts @@ -30,7 +30,7 @@ export class BaseDbRunner { const res = await this.testQuery(sqlAndParams); console.log(JSON.stringify(res)); - console.log("!!!! res: ", res); + console.log('!!!! res: ', res); expect(res).toEqual( expectedResult diff --git a/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/builder.rs b/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/builder.rs index 5af384f4268d4..7b86c419b3b89 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/builder.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/builder.rs @@ -5,8 +5,10 @@ use crate::plan::schema::QualifiedColumnName; use crate::plan::*; use crate::planner::query_properties::OrderByItem; use crate::planner::query_tools::QueryTools; -use crate::planner::sql_evaluator::MemberSymbol; +use crate::planner::sql_evaluator::collectors::collect_calc_group_dims; +use crate::planner::sql_evaluator::sql_nodes::SqlNodesFactory; use crate::planner::sql_evaluator::ReferencesBuilder; +use crate::planner::sql_evaluator::{get_filtered_values, MemberSymbol}; use crate::planner::sql_templates::PlanSqlTemplates; use cubenativeutils::CubeError; use itertools::Itertools; @@ -150,9 +152,7 @@ impl PhysicalPlanBuilder { ) -> Result<(), CubeError> { for dimension_subquery in dimension_subqueries.iter() { if let Some(dim_ref) = references_builder.find_reference_for_member( - &dimension_subquery - .measure_for_subquery_dimension - .full_name(), + &dimension_subquery.measure_for_subquery_dimension, &None, ) { render_references @@ -167,6 +167,23 @@ impl PhysicalPlanBuilder { Ok(()) } + pub(crate) fn process_calc_group( + &self, + symbol: &Rc, + context_factory: &mut SqlNodesFactory, + filter: &Option, + ) -> Result<(), CubeError> { + for dim in collect_calc_group_dims(symbol)? { + let values = get_filtered_values(&dim, filter); + context_factory.add_calc_group_item( + dim.cube_name().clone(), + dim.name().clone(), + values, + ); + } + Ok(()) + } + pub(crate) fn make_order_by( &self, logical_schema: &LogicalSchema, diff --git a/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/aggregate_multiplied_subquery.rs b/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/aggregate_multiplied_subquery.rs index 709430f6b24de..c2e1dc4f3bedf 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/aggregate_multiplied_subquery.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/aggregate_multiplied_subquery.rs @@ -135,7 +135,7 @@ impl<'a> LogicalNodeProcessor<'a, AggregateMultipliedSubquery> &None, &mut render_references, )?; - let alias = references_builder.resolve_alias_for_member(&member.full_name(), &None); + let alias = references_builder.resolve_alias_for_member(&member, &None); group_by.push(Expr::Member(MemberExpression::new(member.clone()))); select_builder.add_projection_member(&member, alias); } diff --git a/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/full_key_aggregate.rs b/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/full_key_aggregate.rs index b49fa331e1169..307ad2642d0ff 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/full_key_aggregate.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/full_key_aggregate.rs @@ -130,7 +130,7 @@ impl FullKeyAggregateStrategy for KeysFullKeyAggregateStrategy<'_> { let mut keys_select_builder = SelectBuilder::new(keys_from); for member in full_key_aggregate.schema.all_dimensions() { - let alias = references_builder.resolve_alias_for_member(&member.full_name(), &None); + let alias = references_builder.resolve_alias_for_member(&member, &None); if alias.is_none() { return Err(CubeError::internal(format!( "Source for {} not found in full key aggregate subqueries", diff --git a/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/full_key_aggregate_query.rs b/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/full_key_aggregate_query.rs deleted file mode 100644 index 06d91dacd7248..0000000000000 --- a/rust/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/full_key_aggregate_query.rs +++ /dev/null @@ -1,99 +0,0 @@ -use super::super::{LogicalNodeProcessor, ProcessableNode, PushDownBuilderContext}; -use crate::logical_plan::{FullKeyAggregateQuery, SimpleQuery, SimpleQuerySource}; -use crate::physical_plan_builder::PhysicalPlanBuilder; -use crate::plan::{Expr, Filter, MemberExpression, QueryPlan, Select, SelectBuilder}; -use crate::planner::query_tools::QueryTools; -use crate::planner::sql_evaluator::ReferencesBuilder; -use crate::planner::sql_templates::PlanSqlTemplates; -use crate::planner::{BaseMember, MemberSymbolRef}; -use cubenativeutils::CubeError; -use itertools::Itertools; -use std::collections::HashMap; -use std::rc::Rc; - -pub struct FullKeyAggregateQueryProcessor<'a> { - builder: &'a PhysicalPlanBuilder, -} - -impl<'a> LogicalNodeProcessor<'a, FullKeyAggregateQuery> for FullKeyAggregateQueryProcessor<'a> { - type PhysycalNode = Rc