diff --git a/docs-mintlify/docs.json b/docs-mintlify/docs.json index 724945f33bb93..ce4e970f9f1b1 100644 --- a/docs-mintlify/docs.json +++ b/docs-mintlify/docs.json @@ -515,6 +515,7 @@ "group": "DAX API", "root": "reference/core-data-apis/dax-api/index", "pages": [ + "reference/core-data-apis/dax-api/cross-view-filter", "reference/core-data-apis/dax-api/reference" ] } diff --git a/docs-mintlify/reference/core-data-apis/dax-api/cross-view-filter.mdx b/docs-mintlify/reference/core-data-apis/dax-api/cross-view-filter.mdx new file mode 100644 index 0000000000000..fddf78c726cda --- /dev/null +++ b/docs-mintlify/reference/core-data-apis/dax-api/cross-view-filter.mdx @@ -0,0 +1,88 @@ +--- +title: Cross-view filtering +description: "Enable cross-view filtering in the DAX API to build dashboards that span multiple views in Power BI." +--- + +By default, the [DAX API][ref-dax-api] exposes each [view][ref-views] as a +separate perspective in [Power BI][ref-powerbi]. Visualizations from different +views can't share the same filters, and a single report can't combine measures +and dimensions across views. + +Cross-view filtering removes this limitation, letting you build dashboards +that span multiple views and apply filters consistently across visualizations. +It is supported in both [Live connection](#live-connection-mode) and +[DirectQuery](#directquery-mode) modes. + +To use cross-view filters such as slicers, you must enable single-perspective mode +in the DAX API. To enable single-perspective mode in the DAX API, +set the `CUBEJS_DAX_SINGLE_PERSPECTIVE` environment variable to `true`. + +In single-perspective mode, all views are exposed as part of a single perspective. +This allows you to use a single connection and show visualizations from different +views on the same dashboard. + + + +Single-perspective mode changes the way views are exposed by the DAX API. +Because of that, it is not backwards-compatible with dashboards created before +enabling this environment variable. + + + +## Live connection mode + +In Live connection mode, cross-view filters work automatically, provided that +the following two conditions are met: + +- The column used in the filter must be present in every view that the +cross-filter has to be applied to, and named exactly the same in each view. +- The column in all such views must resolve to the same cube member. + +For example, if views `orders_view` and `users_view` both expose a column named +`address_country` pointing to the `country.country` cube member, cross-filtering +will be applied across these views. + +If the `address_country` column in `orders_view` points to `orders.country` +while the `address_country` column in `users_view` points to `users.country`, +cross-filtering will not be applied, because the columns resolve to different +cube members. + +Likewise, if `orders_view` exposes an `address_country` column pointing to +`country.country` and `users_view` exposes a `country` column also pointing to +`country.country`, cross-filtering will not be applied, because the column +names differ between the views. + +## DirectQuery mode + +In DirectQuery mode, cross-view filters require the same two conditions as in +[Live connection mode](#live-connection-mode): the filter column must be +present in every view with the exact same name, and it must resolve to the +same cube member across those views. + +In addition, you must manually configure relationships between tables in Power +BI so that filters propagate across views: + +- Open **Model view** in the left sidebar. +- On the right, open the **Data** sidebar, then the **Model** tab. +- Right-click **Relationships** and choose **New relationship**. +- In the **Properties** sidebar, select a table and a column. +- For cardinality, choose **Many to many (\*:\*)**. Ignore the warning. +- Select another table with the same column. +- For cross-filter direction, select **Both**. +- Click **Apply changes**. + +Once the relationship is configured correctly, cross-view filters will work in +DirectQuery mode. + +The steps above describe configuring a relationship between two tables. When +many views need to be cross-filtered, the best practice is to create a single +view in the Cube data model that includes all the columns from the views that +need to be cross-filtered, and then create relationships between this large +view and each of the other views. This keeps the relationship graph in Power +BI simple and avoids configuring pairwise relationships between every +combination of views. + + +[ref-dax-api]: /reference/core-data-apis/dax-api +[ref-views]: /docs/data-modeling/views +[ref-powerbi]: /admin/connect-to-data/visualization-tools/powerbi diff --git a/docs-mintlify/reference/core-data-apis/dax-api/index.mdx b/docs-mintlify/reference/core-data-apis/dax-api/index.mdx index 99981ff9b7ac1..a1e49ed4468ae 100644 --- a/docs-mintlify/reference/core-data-apis/dax-api/index.mdx +++ b/docs-mintlify/reference/core-data-apis/dax-api/index.mdx @@ -73,8 +73,20 @@ The DAX API only exposes [views][ref-views], not cubes. +## Cross-view filtering + +By default, each view is exposed as a separate perspective in Power BI, so +visualizations from different views can't share the same filters. Cross-view +filtering lets you build dashboards that span multiple views and apply filters +consistently across visualizations, in both Live connection and DirectQuery +modes. + +See [Cross-view filtering][ref-cross-view-filter] for details on how to enable +and use it. + [ref-powerbi]: /admin/connect-to-data/visualization-tools/powerbi +[ref-cross-view-filter]: /reference/core-data-apis/dax-api/cross-view-filter [link-dax]: https://learn.microsoft.com/en-us/dax/ [ref-sql-api]: /reference/sql-api [ref-ref-dax-api]: /reference/dax-api/reference diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/base_tools.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/base_tools.rs index a80f802569558..a784f4ad24a71 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/base_tools.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/base_tools.rs @@ -13,6 +13,11 @@ use cubenativeutils::CubeError; use std::any::Any; use std::rc::Rc; +/// Dialect-independent callbacks to the JavaScript side, used +/// during compilation and planning: SQL templates, time-series +/// generation, allocated params, pre-aggregation lookup, join-tree +/// resolution. Dialect-specific helpers live behind `DriverTools`, +/// reachable via `driver_tools()`. #[nativebridge::native_bridge] pub trait BaseTools { fn driver_tools(&self, external: bool) -> Result, CubeError>; diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/driver_tools.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/driver_tools.rs index d11cd5c8ad073..2bb6cd8eff12a 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/driver_tools.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/driver_tools.rs @@ -8,6 +8,10 @@ use cubenativeutils::CubeError; use std::any::Any; use std::rc::Rc; +/// Dialect-specific SQL helpers implemented by the JS driver. Used +/// only at SQL-generation time, once the target dialect is known: +/// timezone conversion, granularity-grouped columns, HLL functions, +/// interval arithmetic, type casts. #[nativebridge::native_bridge] pub trait DriverTools { fn convert_tz(&self, field: String) -> Result; diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/member_sql.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/member_sql.rs index f913103fff43a..97fcc5545c2eb 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/member_sql.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/member_sql.rs @@ -16,6 +16,9 @@ use cubenativeutils::CubeError; use std::rc::Rc; use std::{any::Any, cell::RefCell, rc::Weak}; +/// Result of evaluating a member's `sql` JS function: a single SQL +/// string, or — for pre-aggregation `dimensions:` / `measures:` +/// reference lists — one string per referenced member. #[derive(Clone, Debug, PartialEq, Eq)] pub enum SqlTemplate { String(String), @@ -40,6 +43,9 @@ impl NativeDeserialize for SqlTemplate { } } +/// Column argument passed to +/// `FILTER_PARAMS.cube.member.filter(...)`: either a plain column +/// name string, or a JS callback that produces the SQL snippet. #[derive(Clone)] pub enum FilterParamsColumn { String(String), @@ -179,6 +185,9 @@ pub struct SecutityContextProps { pub values: Vec, } +/// Dependencies collected while compiling a member `sql` function. +/// Each `{arg:N}` / `{fp:N}` / `{fg:N}` / `{sv:N}` placeholder in +/// the produced `SqlTemplate` indexes into one of these vectors. #[derive(Default, Clone, Debug)] pub struct SqlTemplateArgs { pub symbol_paths: Vec>, @@ -296,6 +305,11 @@ impl ProxyStateWeak { } } +/// A member's `sql:` function as provided by the JS schema compiler. +/// `compile_template_sql` invokes the function under proxied +/// arguments (`{CUBE}`, `FILTER_PARAMS`, `FILTER_GROUP`, +/// `SECURITY_CONTEXT`, `SQL_UTILS`) and returns the resulting SQL +/// template together with the dependencies the function touched. pub trait MemberSql { fn args_names(&self) -> &Vec; fn as_any(self: Rc) -> Rc; @@ -306,6 +320,12 @@ pub trait MemberSql { ) -> Result<(SqlTemplate, SqlTemplateArgs), CubeError>; } +/// Neon-backed implementation of `MemberSql`. `compile_template_sql` +/// calls the JS function with proxy objects that record every +/// accessed member path, `FILTER_PARAMS` / `FILTER_GROUP` call, and +/// `SECURITY_CONTEXT.x.filter(...)` / `unsafeValue()` reference into +/// a shared state, then returns the produced template together with +/// that state as `SqlTemplateArgs`. pub struct NativeMemberSql { native_object: NativeObjectHandle, args_names: Vec, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/mod.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/mod.rs index 508c2841db880..c9d878012c283 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/mod.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/mod.rs @@ -1,3 +1,10 @@ +//! DTO layer for talking to the JavaScript side of Cube. +//! +//! Every type here mirrors the shape of an object delivered by the +//! schema compiler — cube and member definitions, filter params, +//! callbacks, security context, and so on. Tesseract reads these +//! types as input; no business logic lives here. + pub mod base_query_options; pub mod base_tools; pub mod case_definition; diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/security_context.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/security_context.rs index 95630b44f3035..6f664253222c7 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/security_context.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/security_context.rs @@ -5,5 +5,8 @@ use cubenativeutils::CubeError; use std::any::Any; use std::rc::Rc; +/// Type-erased reference to the JS-side security context object. +/// Used to materialise `SECURITY_CONTEXT.x.filter(...)` / +/// `unsafeValue()` proxies when compiling a member SQL function. #[nativebridge::native_bridge] pub trait SecurityContext {} diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/sql_templates_render.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/sql_templates_render.rs index c28d6bd26494a..c9deac641a914 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/sql_templates_render.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/sql_templates_render.rs @@ -6,6 +6,9 @@ use minijinja::{value::Value, Environment}; use std::collections::HashMap; use std::marker::PhantomData; +/// Jinja2 template rendering for SQL templates. The native +/// implementation keeps the parsed templates locally and renders +/// without crossing the JS boundary. pub trait SqlTemplatesRender { fn contains_template(&self, template_name: &str) -> bool; fn render_template(&self, name: &str, ctx: Value) -> Result; diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/sql_utils.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/sql_utils.rs index 84d78dd137e34..f56954ea9e5d8 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/sql_utils.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/sql_utils.rs @@ -5,5 +5,8 @@ use cubenativeutils::CubeError; use std::any::Any; use std::rc::Rc; +/// Opaque holder for the JS-side `SQL_UTILS` object. Tesseract only +/// forwards it back into member `sql` functions; the methods +/// invoked on it are a JS concern. #[nativebridge::native_bridge] pub trait SqlUtils {} diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/aggregate_multiplied_subquery.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/aggregate_multiplied_subquery.rs index d0f8af0350a46..85ef84e7b2c05 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/aggregate_multiplied_subquery.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/aggregate_multiplied_subquery.rs @@ -5,6 +5,12 @@ use std::rc::Rc; logical_source_enum!(AggregateMultipliedSubquerySource, [Cube, MeasureSubquery]); +/// Subquery that aggregates a multiplied measure: a `keys_subquery` +/// produces the unique key set, a `source` (cube or +/// `MeasureSubquery`) supplies the values, optional +/// `dimension_subqueries` materialise sub-query dimensions, and +/// `pre_aggregation_override` lets a matched pre-aggregation +/// short-circuit the whole CTE. pub struct AggregateMultipliedSubquery { pub schema: Rc, pub keys_subquery: Rc, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/calc_groups_cross_join.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/calc_groups_cross_join.rs deleted file mode 100644 index 8d96a50d65b1f..0000000000000 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/calc_groups_cross_join.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::planner::MemberSymbol; - -use super::*; -use cubenativeutils::CubeError; -use std::rc::Rc; -use typed_builder::TypedBuilder; - -#[derive(Clone)] -pub struct CalcGroupDescription { - pub symbol: Rc, - pub values: Vec, -} - -impl PrettyPrint for CalcGroupDescription { - fn pretty_print(&self, result: &mut PrettyPrintResult, state: &PrettyPrintState) { - let state = state.new_level(); - result.println( - &format!("{}:{}", self.symbol.full_name(), self.values.join(", ")), - &state, - ); - } -} - -#[derive(Clone, TypedBuilder)] -pub struct CalcGroupsCrossJoin { - source: BaseQuerySource, - calc_groups: Vec>, -} - -impl CalcGroupsCrossJoin { - pub fn source(&self) -> &BaseQuerySource { - &self.source - } - - pub fn calc_groups(&self) -> &Vec> { - &self.calc_groups - } -} - -impl LogicalNode for CalcGroupsCrossJoin { - fn as_plan_node(self: &Rc) -> PlanNode { - PlanNode::CalcGroupsCrossJoin(self.clone()) - } - - fn inputs(&self) -> Vec { - vec![self.source.as_plan_node()] - } - - fn with_inputs(self: Rc, inputs: Vec) -> Result, CubeError> { - check_inputs_len(&inputs, 1, self.node_name())?; - let source = &inputs[0]; - - Ok(Rc::new(Self { - calc_groups: self.calc_groups.clone(), - source: self.source.with_plan_node(source.clone())?, - })) - } - - fn node_name(&self) -> &'static str { - "CalcGroupsCrossJoin" - } - - fn try_from_plan_node(plan_node: PlanNode) -> Result, CubeError> { - if let PlanNode::CalcGroupsCrossJoin(item) = plan_node { - Ok(item) - } else { - Err(cast_error(&plan_node, "MultiStageGetDateRange")) - } - } -} - -impl PrettyPrint for CalcGroupsCrossJoin { - fn pretty_print(&self, result: &mut PrettyPrintResult, state: &PrettyPrintState) { - let state = state.new_level(); - result.println("CaclGroupsCrossJoin: ", &state); - let state = state.new_level(); - result.println("Groups: ", &state); - let details_state = state.new_level(); - for group in &self.calc_groups { - group.pretty_print(result, &details_state); - } - result.println("Source: ", &state); - self.source.pretty_print(result, &details_state); - } -} diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/cube.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/cube.rs index 746423d404626..dfd2f638bbb73 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/cube.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/cube.rs @@ -4,6 +4,9 @@ use cubenativeutils::CubeError; use std::rc::Rc; use typed_builder::TypedBuilder; +/// Marker for an "original SQL" pre-aggregation attached to a cube +/// — the physical builder uses its name to substitute the cube's +/// table expression with the matching pre-aggregation source. #[derive(Clone, TypedBuilder)] pub struct OriginalSqlPreAggregation { name: String, @@ -24,6 +27,9 @@ impl PrettyPrint for OriginalSqlPreAggregation { } } +/// A cube referenced from the logical plan — wraps the planner's +/// `BaseCube` and optionally pins a matching "original SQL" +/// pre-aggregation as the cube's source. #[derive(Clone, TypedBuilder)] pub struct Cube { cube: Rc, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/dimension_subquery.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/dimension_subquery.rs index 5037e7b5419b8..aca02901ce637 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/dimension_subquery.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/dimension_subquery.rs @@ -4,6 +4,10 @@ use crate::planner::MemberSymbol; use cubenativeutils::CubeError; use std::rc::Rc; +/// Materialisation of a `sub_query: true` dimension: a subquery +/// that groups by the owning cube's primary keys and computes the +/// dimension's measure expression, then is joined back to the host +/// query on those keys. pub struct DimensionSubQuery { pub query: Rc, pub primary_keys_dimensions: Vec>, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/filter.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/filter.rs index 246b84a91208c..2cc4fe45c2b83 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/filter.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/filter.rs @@ -2,6 +2,10 @@ use super::pretty_print::*; use crate::planner::filter::{Filter, FilterItem}; use itertools::Itertools; +/// All filters of a query split by what they target. WHERE-style +/// filters (`time_dimensions_filters`, `dimensions_filters`, +/// `segments`) are surfaced together via `all_filters`; HAVING-style +/// `measures_filter` stays separate. #[derive(Default)] pub struct LogicalFilter { pub dimensions_filters: Vec, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/full_key_aggregate.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/full_key_aggregate.rs index 90323e39f952a..6d6a8d1fbaf57 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/full_key_aggregate.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/full_key_aggregate.rs @@ -4,6 +4,8 @@ use cubenativeutils::CubeError; use std::rc::Rc; use typed_builder::TypedBuilder; +/// Reference to a multi-stage CTE consumed by `FullKeyAggregate`: +/// the CTE's name plus the symbols it exposes. #[derive(TypedBuilder)] pub struct MultiStageSubqueryRef { name: String, @@ -37,6 +39,10 @@ impl PrettyPrint for MultiStageSubqueryRef { } } +/// Top-level aggregating source that stitches together several +/// multi-stage / multi-fact CTEs into one keyed result. The +/// physical builder picks a join strategy from `multi_stage_subquery_refs` +/// and `use_full_join_and_coalesce`. #[derive(Clone, TypedBuilder)] pub struct FullKeyAggregate { schema: Rc, @@ -50,6 +56,9 @@ impl FullKeyAggregate { &self.schema } + /// When true, multi-fact branches are stitched together via a + /// FULL OUTER JOIN over keys with COALESCE on dimension columns; + /// otherwise an INNER JOIN is used. pub fn use_full_join_and_coalesce(&self) -> bool { self.use_full_join_and_coalesce } diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/helper.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/helper.rs index c9f12e751aa19..7292414da03cf 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/helper.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/helper.rs @@ -4,6 +4,9 @@ use crate::planner::MemberSymbol; use super::*; use std::rc::Rc; +/// All member symbols reachable from a schema and its filters — +/// schema members concatenated with every member referenced in +/// dimension, segment and measure filter trees. pub fn all_symbols(schema: &Rc, filters: &LogicalFilter) -> Vec> { let mut symbols = schema.all_members().cloned().collect_vec(); diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/join.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/join.rs index 7faecbcaf09a8..6910aa959bd29 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/join.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/join.rs @@ -5,6 +5,8 @@ use cubenativeutils::CubeError; use std::rc::Rc; use typed_builder::TypedBuilder; +/// One non-root cube of a `LogicalJoin`, with the SQL expression +/// that joins it to the rest of the tree. #[derive(Clone, TypedBuilder)] pub struct LogicalJoinItem { cube: Rc, @@ -29,6 +31,9 @@ impl PrettyPrint for LogicalJoinItem { } } +/// Join of cubes that backs a query source: a `root` cube plus +/// non-root cubes (`joins`), optionally extended by sub-query +/// dimensions that contribute their own joined-in CTEs. #[derive(Clone, TypedBuilder)] pub struct LogicalJoin { #[builder(default)] diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/keys_subquery.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/keys_subquery.rs index 05ab6254f8ddf..5b78c98fcc2bd 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/keys_subquery.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/keys_subquery.rs @@ -4,6 +4,10 @@ use cubenativeutils::CubeError; use std::rc::Rc; use typed_builder::TypedBuilder; +/// Subquery that produces the primary-key set of `pk_cube` after +/// applying the query's filters. Used as the outer key set in +/// `AggregateMultipliedSubquery`: a measure subquery aggregates +/// values and is joined back against this set on the primary keys. #[derive(Clone, TypedBuilder)] pub struct KeysSubQuery { pk_cube: Rc, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/logical_node.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/logical_node.rs index 3cb7b557a1b92..040be06612757 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/logical_node.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/logical_node.rs @@ -2,6 +2,9 @@ use super::*; use cubenativeutils::CubeError; use std::rc::Rc; +/// Node of the logical-plan tree. Exposes its child nodes through +/// `inputs()` / `with_inputs()` so generic passes can walk and +/// rewrite the tree without knowing concrete node types. pub trait LogicalNode { fn inputs(&self) -> Vec; @@ -14,6 +17,9 @@ pub trait LogicalNode { fn node_name(&self) -> &'static str; } +/// Type-erased handle for a logical-plan node. Generic traversal +/// works in terms of `PlanNode`; concrete typed access is recovered +/// via `into_logical_node` / each node's `try_from_plan_node`. #[derive(Clone)] pub enum PlanNode { Query(Rc), diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/logical_query_modifers.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/logical_query_modifers.rs index 2c78e45b7966f..3f2ac64831b67 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/logical_query_modifers.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/logical_query_modifers.rs @@ -1,6 +1,8 @@ use super::*; use crate::planner::query_properties::OrderByItem; +/// Per-query modifiers that sit outside the result schema: paging, +/// ordering, and the ungrouped flag. pub struct LogicalQueryModifiers { pub offset: Option, pub limit: Option, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/logical_source.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/logical_source.rs index 09fa35796e1ed..8a8b2af935ebe 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/logical_source.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/logical_source.rs @@ -1,6 +1,10 @@ use super::*; use cubenativeutils::CubeError; +/// Enum-of-children adapter used by nodes that accept one of several +/// `LogicalNode` types as their source. Lets the generic `with_inputs` +/// machinery rebuild the source from a freshly transformed `PlanNode` +/// while keeping the variant unchanged. pub trait LogicalSource: Sized + PrettyPrint { fn as_plan_node(&self) -> PlanNode; fn with_plan_node(&self, plan_node: PlanNode) -> Result; diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/measure_subquery.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/measure_subquery.rs index 2e99dc5c7681d..518702de43221 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/measure_subquery.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/measure_subquery.rs @@ -2,6 +2,9 @@ use super::*; use cubenativeutils::CubeError; use std::rc::Rc; +/// Inner aggregating subquery for a measure inside a multiplied +/// aggregate flow — one of the two `AggregateMultipliedSubquerySource` +/// variants (the other being a raw `Cube`). pub struct MeasureSubquery { pub schema: Rc, pub source: Rc, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/mod.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/mod.rs index 95e6aab42c5ed..b7903fea5598c 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/mod.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/mod.rs @@ -1,3 +1,10 @@ +//! Logical representation of a query as a tree of `PlanNode`s. +//! +//! Each node implements `LogicalNode` (children via `inputs()` / +//! `with_inputs()`, plus a name for diagnostics). The tree is built +//! by the planner and consumed by `physical_plan_builder`, which +//! turns it into a `QueryPlan`. No SQL is produced here. + #[macro_use] mod logical_source; mod aggregate_multiplied_subquery; diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/calculation.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/calculation.rs index 2e8e8b2783fa0..50d2f477a360d 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/calculation.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/calculation.rs @@ -6,6 +6,8 @@ use itertools::Itertools; use std::rc::Rc; use typed_builder::TypedBuilder; +/// Semantic category of a multi-stage measure CTE — drives how the +/// physical builder shapes the rendered expression. #[derive(PartialEq, Clone)] pub enum MultiStageCalculationType { Rank, @@ -23,6 +25,8 @@ impl ToString for MultiStageCalculationType { } } +/// Which SQL window-function flavour, if any, the calculation +/// renders as. #[derive(PartialEq, Clone)] pub enum MultiStageCalculationWindowFunction { Rank, @@ -40,6 +44,9 @@ impl ToString for MultiStageCalculationWindowFunction { } } +/// Measure CTE in a multi-stage chain — wraps a `FullKeyAggregate` +/// source with the partition / window function / ordering decided +/// by `calculation_type`. #[derive(TypedBuilder)] pub struct MultiStageMeasureCalculation { schema: Rc, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/dimension.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/dimension.rs index 5402942163270..279412a2914c3 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/dimension.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/dimension.rs @@ -7,6 +7,8 @@ use itertools::Itertools; use std::rc::Rc; use typed_builder::TypedBuilder; +/// Dimension CTE in a multi-stage chain — materialises a +/// multi-stage dimension on top of a `FullKeyAggregate` source. #[derive(TypedBuilder)] pub struct MultiStageDimensionCalculation { schema: Rc, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/get_date_range.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/get_date_range.rs index 76e7ebfcf2fe4..ecf5abe58a2df 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/get_date_range.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/get_date_range.rs @@ -3,6 +3,9 @@ use crate::planner::MemberSymbol; use cubenativeutils::CubeError; use std::rc::Rc; +/// CTE that resolves the actual date range of a time dimension at +/// query time (used by rolling windows when no literal range is +/// given). pub struct MultiStageGetDateRange { pub time_dimension: Rc, pub source: Rc, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/leaf_measure.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/leaf_measure.rs index 95d4c2989f432..09719fab65ce4 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/leaf_measure.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/leaf_measure.rs @@ -4,6 +4,10 @@ use crate::planner::MemberSymbol; use cubenativeutils::CubeError; use std::rc::Rc; +/// Leaf CTE of a multi-stage chain — a base query that produces the +/// raw aggregated values feeding the rest of the chain. Optional +/// state rendering (`render_measure_as_state`) and time shifts +/// happen here. pub struct MultiStageLeafMeasure { pub measures: Vec>, pub render_measure_as_state: bool, //Render measure as state, for example hll state for count_approx diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/member.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/member.rs index bd436b65c97ea..fcd81fd9ecb74 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/member.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/member.rs @@ -2,6 +2,8 @@ use crate::logical_plan::*; use cubenativeutils::CubeError; use std::rc::Rc; +/// Body of a `LogicalMultiStageMember` — one of the multi-stage +/// CTE shapes the planner can produce. pub enum MultiStageMemberLogicalType { LeafMeasure(Rc), MultipliedMeasure(Rc), @@ -54,6 +56,8 @@ impl PrettyPrint for MultiStageMemberLogicalType { } } +/// Named CTE in a multi-stage chain. `Query.multistage_members` +/// holds one per CTE the source depends on. pub struct LogicalMultiStageMember { pub name: String, pub member_type: MultiStageMemberLogicalType, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/rolling_window.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/rolling_window.rs index 367e08aea54f6..f35661f81342c 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/rolling_window.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/rolling_window.rs @@ -5,6 +5,8 @@ use crate::planner::MemberSymbol; use cubenativeutils::CubeError; use std::rc::Rc; +/// Regular rolling window: trailing and/or leading bounds, plus a +/// time-series offset. pub struct MultiStageRegularRollingWindow { pub trailing: Option, pub leading: Option, @@ -25,6 +27,8 @@ impl PrettyPrint for MultiStageRegularRollingWindow { } } +/// `to_date` rolling window — bounded by the start of the +/// specified granularity (month-to-date, year-to-date, …). pub struct MultiStageToDateRollingWindow { pub granularity_obj: Rc, } @@ -40,6 +44,8 @@ impl PrettyPrint for MultiStageToDateRollingWindow { } } +/// Flavour of rolling-window calculation: regular trailing/leading +/// window, `to_date` window, or a running-total accumulation. pub enum MultiStageRollingWindowType { Regular(MultiStageRegularRollingWindow), ToDate(MultiStageToDateRollingWindow), @@ -58,6 +64,9 @@ impl PrettyPrint for MultiStageRollingWindowType { } } +/// Rolling-window CTE — combines a time-series CTE (the date axis) +/// with a measure CTE and applies the chosen rolling computation +/// to each point on the series. pub struct MultiStageRollingWindow { pub schema: Rc, pub is_ungrouped: bool, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/time_series.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/time_series.rs index bb0cf5cc42608..f11be1823f9d3 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/time_series.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/multistage/time_series.rs @@ -4,6 +4,10 @@ use cubenativeutils::CubeError; use std::rc::Rc; use typed_builder::TypedBuilder; +/// Date-axis CTE for a rolling window — generates the series of +/// points the rolling computation walks over. The range comes +/// either from a literal `date_range` or from a sibling +/// `MultiStageGetDateRange` CTE (`get_date_range_multistage_ref`). #[derive(TypedBuilder)] pub struct MultiStageTimeSeries { time_dimension: Rc, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/optimizers/pre_aggregation/measure_matcher.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/optimizers/pre_aggregation/measure_matcher.rs index 9869fe47db6aa..1651b47a5fc56 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/optimizers/pre_aggregation/measure_matcher.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/optimizers/pre_aggregation/measure_matcher.rs @@ -5,20 +5,20 @@ use std::collections::HashSet; use std::rc::Rc; pub struct MeasureMatcher { - only_addictive: bool, + only_additive: bool, pre_aggregation_measures: HashSet, matched_measures: HashSet, } impl MeasureMatcher { - pub fn new(pre_aggregation: &CompiledPreAggregation, only_addictive: bool) -> Self { + pub fn new(pre_aggregation: &CompiledPreAggregation, only_additive: bool) -> Self { let pre_aggregation_measures = pre_aggregation .measures .iter() .map(|m| m.full_name()) .collect(); Self { - only_addictive, + only_additive, pre_aggregation_measures, matched_measures: HashSet::new(), } @@ -38,7 +38,7 @@ impl MeasureMatcher { return Ok(false); } if self.pre_aggregation_measures.contains(&measure.full_name()) - && (!self.only_addictive || measure.is_addictive()) + && (!self.only_additive || measure.is_additive()) { self.matched_measures.insert(measure.full_name()); return Ok(true); diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/optimizers/pre_aggregation/optimizer.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/optimizers/pre_aggregation/optimizer.rs index 6f1f71f8787b4..7d939319c307c 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/optimizers/pre_aggregation/optimizer.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/optimizers/pre_aggregation/optimizer.rs @@ -504,9 +504,9 @@ impl PreAggregationOptimizer { &self, measures: &Vec>, pre_aggregation: &CompiledPreAggregation, - only_addictive: bool, + only_additive: bool, ) -> Result>, CubeError> { - let mut matcher = MeasureMatcher::new(pre_aggregation, only_addictive); + let mut matcher = MeasureMatcher::new(pre_aggregation, only_additive); for measure in measures.iter() { if !matcher.try_match(measure)? { return Ok(None); diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/pre_aggregation.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/pre_aggregation.rs index 18c33982f527a..e3f0a0d4893dc 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/pre_aggregation.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/pre_aggregation.rs @@ -5,6 +5,10 @@ use itertools::Itertools; use std::{collections::HashMap, rc::Rc}; use typed_builder::TypedBuilder; +/// A matched pre-aggregation usable as a query source — its table +/// (or rollup join / union), the members it exposes and its +/// granularity. The physical builder reads members through the +/// `*_refererences` helpers. #[derive(TypedBuilder)] pub struct PreAggregation { name: String, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/query.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/query.rs index 62abb99fd23b8..c0ddeacf9116b 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/query.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/query.rs @@ -3,6 +3,10 @@ use cubenativeutils::CubeError; use std::rc::Rc; use typed_builder::TypedBuilder; +/// Root node of a query in the logical plan: the data `source` +/// (join / aggregate / pre-aggregation), its output `schema`, +/// `filter` tree, query-level `modifers` (limit / offset / order / +/// ungrouped), and the multi-stage CTEs the source depends on. #[derive(Clone, TypedBuilder)] pub struct Query { #[builder(default)] diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/query_source.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/query_source.rs index 88c163a617928..1b87d04d6cf39 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/query_source.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/query_source.rs @@ -2,4 +2,7 @@ use super::*; use cubenativeutils::CubeError; use std::rc::Rc; +// Top-level source of a `Query`: a plain `LogicalJoin`, an aggregated +// `FullKeyAggregate` (for multi-stage / multi-fact rewrites), or a +// matched `PreAggregation`. logical_source_enum!(QuerySource, [LogicalJoin, FullKeyAggregate, PreAggregation]); diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/schema.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/schema.rs index 7a1e1a419eb01..0a11a28aab067 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/schema.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/logical_plan/schema.rs @@ -8,6 +8,9 @@ use std::collections::HashSet; use std::fmt; use std::rc::Rc; +/// Output shape of a logical-plan node: the dimensions, time +/// dimensions and measures it exposes, plus the full names of +/// measures that need multiplied-aggregate handling downstream. #[derive(Default, Clone)] pub struct LogicalSchema { pub time_dimensions: Vec>, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/auto_prefix.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/auto_prefix.rs index 8fd8350fbec90..5f77d862ddffe 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/auto_prefix.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/auto_prefix.rs @@ -11,6 +11,9 @@ use std::any::Any; use std::collections::HashMap; use std::rc::Rc; +/// Prefixes a bare member-name SQL fragment with its cube's quoted +/// alias (e.g. `id` → `"users".id`). Skips fragments that are not a +/// single identifier — those already carry their own qualification. pub struct AutoPrefixSqlNode { input: Rc, cube_references: HashMap, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/calendar_time_shift.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/calendar_time_shift.rs index c421ddee3a33f..a6bea18c29d5f 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/calendar_time_shift.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/calendar_time_shift.rs @@ -9,6 +9,10 @@ use std::any::Any; use std::collections::HashMap; use std::rc::Rc; +/// Applies a calendar-cube time shift: when the dimension's +/// resolved time-shift primary key matches an entry in `shifts`, +/// renders the shifted reference (interval / named slot / custom +/// SQL) declared on the calendar cube. pub struct CalendarTimeShiftSqlNode { shifts: HashMap, // Key is the full pk name of the calendar cube input: Rc, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/case.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/case.rs index fc8ac0d0d1ac3..ae5c3e7d2dfc9 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/case.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/case.rs @@ -8,6 +8,10 @@ use cubenativeutils::CubeError; use std::any::Any; use std::rc::Rc; +/// Renders members defined via a `case:` body — both the classic +/// `CASE WHEN cond THEN label …` and the switch-style +/// `CASE switch WHEN value THEN sql …`. Members without a case body +/// pass through to `input`. pub struct CaseSqlNode { input: Rc, } diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/evaluate_sql.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/evaluate_sql.rs index 8871b58aa1952..68a6d6ddb3580 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/evaluate_sql.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/evaluate_sql.rs @@ -8,6 +8,9 @@ use cubenativeutils::CubeError; use std::any::Any; use std::rc::Rc; +/// Base case of the SQL-node chain: invokes the member symbol's own +/// `to_sql` to produce the raw SQL fragment. All wrapping nodes +/// eventually delegate down to this. pub struct EvaluateSqlNode {} impl EvaluateSqlNode { diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/factory.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/factory.rs index bfcad5b6fe073..58e2e3473f2ca 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/factory.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/factory.rs @@ -13,6 +13,10 @@ use crate::planner::symbols::CalendarDimensionTimeShift; use std::collections::{HashMap, HashSet}; use std::rc::Rc; +/// Builds the SQL-node chain for a query. Carries all the flags and +/// reference maps the query needs (time shifts, render references, +/// pre-aggregation refs, multi-stage partitions, etc.) and assembles +/// them into a layered `SqlNode` via `default_node_processor`. #[derive(Clone, Default)] pub struct SqlNodesFactory { time_shifts: TimeShiftState, @@ -150,6 +154,14 @@ impl SqlNodesFactory { ) } + /// Assembles the full SQL-node chain for the configured options. + /// Three sub-chains hang off a `RootSqlNode` keyed by member + /// kind: a dimension chain (geo / case / time-shift / calendar + /// time-shift wraps), a time-dimension chain, and a measure + /// chain (case → measure filter → final-measure / ungrouped / + /// multi-stage wraps → mask). The whole tree is then wrapped in + /// a top-level `RenderReferencesSqlNode` for query-wide reference + /// substitution. pub fn default_node_processor(&self) -> Rc { let evaluate_sql_processor = MaskedSqlNode::new(EvaluateSqlNode::new()); let auto_prefix_processor = AutoPrefixSqlNode::new( diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/final_measure.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/final_measure.rs index 71c6579ec41e7..93420c4825bde 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/final_measure.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/final_measure.rs @@ -9,6 +9,11 @@ use std::any::Any; use std::collections::HashSet; use std::rc::Rc; +/// Applies the final aggregation wrap to a measure (sum / avg / +/// count_distinct / pass-through, etc.) using `MeasureKind::aggregate_wrap`. +/// Tracks measures that were classified as multiplied so the wrap +/// switches to a distinct-count form, and routes +/// `count_distinct_approx` through an HLL state when requested. pub struct FinalMeasureSqlNode { input: Rc, rendered_as_multiplied_measures: HashSet, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/final_pre_aggregation_measure.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/final_pre_aggregation_measure.rs index e650233c4b46c..1faa974a86c20 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/final_pre_aggregation_measure.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/final_pre_aggregation_measure.rs @@ -10,6 +10,10 @@ use cubenativeutils::CubeError; use std::any::Any; use std::rc::Rc; +/// Substitutes a measure with the matching pre-aggregation column +/// reference (rolled up via the measure's `pre_aggregate_wrap`), +/// or falls through to `input` when the measure has no +/// pre-aggregation entry. pub struct FinalPreAggregationMeasureSqlNode { input: Rc, references: RenderReferences, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/geo_dimension.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/geo_dimension.rs index f09bac3323cfd..45847de76343c 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/geo_dimension.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/geo_dimension.rs @@ -8,6 +8,9 @@ use cubenativeutils::CubeError; use std::any::Any; use std::rc::Rc; +/// Renders a `Geo` dimension by combining its latitude and +/// longitude SQL expressions into a paired projection. Pass-through +/// for non-geo dimensions. pub struct GeoDimensionSqlNode { input: Rc, } diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/masked.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/masked.rs index af28236221bc2..2c2c9d312f762 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/masked.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/masked.rs @@ -9,6 +9,10 @@ use cubenativeutils::CubeError; use std::any::Any; use std::rc::Rc; +/// Intercepts rendering for members masked by query-tools and +/// substitutes the configured `mask_sql` expression. When a mask +/// filter is set, wraps the result in a `CASE WHEN filter THEN +/// original ELSE mask END`. Pass-through for non-masked members. pub struct MaskedSqlNode { input: Rc, ungrouped: bool, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/measure_filter.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/measure_filter.rs index 2488df55e1173..256cbfce6d793 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/measure_filter.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/measure_filter.rs @@ -7,6 +7,9 @@ use cubenativeutils::CubeError; use std::any::Any; use std::rc::Rc; +/// Wraps a measure's rendered SQL in the `measure_filters` declared +/// on its data-model definition (e.g. `FILTER (WHERE ...)` / CASE +/// gating depending on the dialect templates). pub struct MeasureFilterSqlNode { input: Rc, } diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/mod.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/mod.rs index 35f81ee5d4708..74ffbf7fdf481 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/mod.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/mod.rs @@ -1,3 +1,11 @@ +//! Chain of SQL-rendering nodes assembled by `SqlNodesFactory` and +//! consumed by `SqlEvaluatorVisitor`. Each node decides — for a +//! given `MemberSymbol` — whether to handle the rendering itself +//! or to delegate to its `input`, so the chain progressively wraps +//! the base SQL with cube prefixing, parenthesisation, CASE +//! expressions, masking, aggregation, window functions, and +//! pre-aggregation reference substitution. + pub mod auto_prefix; pub mod calendar_time_shift; pub mod case; diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/multi_stage_rank.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/multi_stage_rank.rs index b290402215eac..46404fdd0f199 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/multi_stage_rank.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/multi_stage_rank.rs @@ -8,6 +8,8 @@ use cubenativeutils::CubeError; use std::any::Any; use std::rc::Rc; +/// Renders a `Rank` measure as a SQL window function partitioned +/// by `partition`. Non-rank measures go through `else_processor`. pub struct MultiStageRankNode { else_processor: Rc, partition: Vec, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/multi_stage_window.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/multi_stage_window.rs index 2738e1788e8cd..66806445ca297 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/multi_stage_window.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/multi_stage_window.rs @@ -7,6 +7,10 @@ use cubenativeutils::CubeError; use std::any::Any; use std::rc::Rc; +/// Wraps a measure as a SQL window function partitioned by +/// `partition`. Used for multi-stage measures whose partition is +/// narrower than the full dimension set. Non-window measures go +/// through `else_processor`. pub struct MultiStageWindowNode { input: Rc, else_processor: Rc, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/render_references.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/render_references.rs index 8657a573dd8c8..c681f0fe91174 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/render_references.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/render_references.rs @@ -12,6 +12,9 @@ use std::rc::Rc; #[derive(Clone)] pub struct RawReferenceValue(pub String); +/// Replacement form for a member that is rendered as a reference +/// instead of being evaluated: a qualified column, a quoted string +/// literal, or a raw SQL fragment. #[derive(Clone)] pub enum RenderReferencesType { QualifiedColumnName(QualifiedColumnName), @@ -37,6 +40,8 @@ impl From for RenderReferencesType { } } +/// `full_name → RenderReferencesType` map keyed by a member's +/// `MemberSymbol::full_name`. #[derive(Default, Clone)] pub struct RenderReferences { references: HashMap, @@ -60,6 +65,8 @@ impl RenderReferences { } } +/// Substitutes a member with a pre-allocated reference from +/// `references`, falling through to `input` when no entry exists. pub struct RenderReferencesSqlNode { input: Rc, references: RenderReferences, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/rolling_window.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/rolling_window.rs index 0acc52dab3b07..77bea9c59666c 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/rolling_window.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/rolling_window.rs @@ -8,6 +8,9 @@ use cubenativeutils::CubeError; use std::any::Any; use std::rc::Rc; +/// Renders cumulative measures (rolling window / running total) +/// through `input`, delegating non-cumulative measures to +/// `default_processor`. pub struct RollingWindowNode { input: Rc, default_processor: Rc, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/root_processor.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/root_processor.rs index 8d6abdc040a3d..aff9774f3010a 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/root_processor.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/root_processor.rs @@ -7,6 +7,8 @@ use cubenativeutils::CubeError; use std::any::Any; use std::rc::Rc; +/// Dispatches rendering to a kind-specific sub-chain based on the +/// member's variant: dimension / time dimension / measure / other. pub struct RootSqlNode { dimension_processor: Rc, time_dimesions_processor: Rc, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/sql_node.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/sql_node.rs index bbff4d082e20d..f04ab893ddd28 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/sql_node.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/sql_node.rs @@ -7,6 +7,10 @@ use cubenativeutils::CubeError; use std::any::Any; use std::rc::Rc; +/// One link in the SQL-rendering chain. Given a `MemberSymbol` and +/// the surrounding context, returns the rendered SQL — either by +/// computing it directly or by delegating up the chain through +/// `node_processor`. pub trait SqlNode { fn to_sql( &self, @@ -22,6 +26,8 @@ pub trait SqlNode { fn childs(&self) -> Vec>; } +/// Specialised renderer for `{CUBE}` / `{TABLE}` placeholders that +/// only need the cube's name. pub trait CubeNameNode { fn to_sql(&self, cube_name: &String) -> Result; } diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/time_dimension.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/time_dimension.rs index 9095b6b90528e..e22b4408b5bfe 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/time_dimension.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/time_dimension.rs @@ -8,6 +8,10 @@ use std::any::Any; use std::collections::HashSet; use std::rc::Rc; +/// Renders a time dimension: applies the granularity (predefined +/// or calendar SQL) and timezone conversion, unless the dimension's +/// full name is listed in `dimensions_with_ignored_timezone` (used +/// for pre-aggregation column references). pub struct TimeDimensionNode { dimensions_with_ignored_timezone: HashSet, input: Rc, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/time_shift.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/time_shift.rs index da4e2d38da858..6516be32ad44b 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/time_shift.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/time_shift.rs @@ -8,6 +8,9 @@ use cubenativeutils::CubeError; use std::any::Any; use std::rc::Rc; +/// Applies a per-dimension time shift to time dimensions whose +/// full name is in `shifts`, by rendering the dimension expression +/// shifted by the configured interval. pub struct TimeShiftSqlNode { shifts: TimeShiftState, input: Rc, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/ungroupped_measure.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/ungroupped_measure.rs index c3654e7f5de4f..6e63351e0f46d 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/ungroupped_measure.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/ungroupped_measure.rs @@ -7,6 +7,9 @@ use cubenativeutils::CubeError; use std::any::Any; use std::rc::Rc; +/// Renders measures for ungrouped contexts (an ungrouped measure +/// member-level flag) — leaves the inner SQL unwrapped instead of +/// applying the usual aggregation. pub struct UngroupedMeasureSqlNode { input: Rc, } diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/ungroupped_query_final_measure.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/ungroupped_query_final_measure.rs index d3e828a407ba5..e8fa504552f51 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/ungroupped_query_final_measure.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan/sql_nodes/ungroupped_query_final_measure.rs @@ -8,6 +8,9 @@ use cubenativeutils::CubeError; use std::any::Any; use std::rc::Rc; +/// Renders measures inside an ungrouped query. Count-like measures +/// keep their counting wrap to stay correct without GROUP BY; other +/// measures pass through without aggregation. pub struct UngroupedQueryFinalMeasureSqlNode { input: Rc, } diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/mod.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/mod.rs index 9b90542acc78b..01a7b2c998bd5 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/mod.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/physical_plan_builder/processors/mod.rs @@ -13,4 +13,3 @@ mod multi_stage_time_series; mod pre_aggregation; mod query; mod query_source; -//mod calc_groups_cross_join; diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/compiler.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/compiler.rs index 9d8ae3f9b4b36..8c40103760673 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/compiler.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/compiler.rs @@ -1,14 +1,12 @@ -use super::collectors::JoinHintsCollector; use super::symbols::{MemberExpressionExpression, MemberExpressionSymbol, MemberSymbol}; use super::SymbolPath; use super::SymbolPathType; use super::{ CubeNameSymbol, CubeNameSymbolFactory, CubeTableSymbol, CubeTableSymbolFactory, - DimensionSymbolFactory, MeasureSymbolFactory, SqlCall, SymbolFactory, TraversalVisitor, + DimensionSymbolFactory, MeasureSymbolFactory, SqlCall, SymbolFactory, }; use crate::cube_bridge::base_tools::BaseTools; use crate::cube_bridge::evaluator::CubeEvaluator; -use crate::cube_bridge::join_hints::JoinHintItem; use crate::cube_bridge::member_sql::MemberSql; use crate::cube_bridge::security_context::SecurityContext; use crate::planner::sql_call_builder::SqlCallBuilder; @@ -18,6 +16,11 @@ use cubenativeutils::CubeError; use std::collections::HashMap; use std::rc::Rc; +/// Compilation context for the planner. Resolves data-model +/// declarations into `MemberSymbol`s, caches them by `SymbolPath`, +/// and holds the JS-side interfaces (cube evaluator, base tools, +/// security context) together with query-level metadata (timezone, +/// alias overrides). pub struct Compiler { cube_evaluator: Rc, base_tools: Rc, @@ -49,6 +52,9 @@ impl Compiler { } } + /// Parses `name` as a `SymbolPath` and resolves it as the + /// appropriate member kind (dimension, measure, or segment). + /// Errors if the path points at a cube reference. pub fn add_auto_resolved_member_evaluator( &mut self, name: String, @@ -65,6 +71,8 @@ impl Compiler { } } + /// Resolves a measure by data-model path (`cube.measure` or a + /// cross-cube form). Cached. pub fn add_measure_evaluator( &mut self, measure: String, @@ -87,6 +95,9 @@ impl Compiler { } } + /// Resolves a dimension by data-model path. When the path turns + /// out to point at a segment, falls back to a parenthesized + /// member-expression wrapper. pub fn add_dimension_evaluator( &mut self, dimension: String, @@ -117,6 +128,9 @@ impl Compiler { } } + /// Resolves a segment by data-model path. Segments are + /// materialised as `MemberExpression` members so they plug into + /// the same machinery as other members. pub fn add_segment_evaluator(&mut self, name: String) -> Result, CubeError> { let path = SymbolPath::parse(self.cube_evaluator.clone(), &name)?; self.add_segment_evaluator_by_path(path) @@ -149,6 +163,8 @@ impl Compiler { Ok(result) } + /// Resolves a cube as an identifier — for `{CUBE}` / `{TABLE}` + /// placeholders. Cached by the normalised path. pub fn add_cube_name_evaluator( &mut self, cube_name: String, @@ -166,6 +182,8 @@ impl Compiler { } } + /// Resolves a cube as a table expression — for `{CUBE.sql()}` + /// placeholders. Cached by the normalised path. pub fn add_cube_table_evaluator( &mut self, cube_name: String, @@ -183,24 +201,20 @@ impl Compiler { } } - pub fn join_hints(&self) -> Result, CubeError> { - let mut collector = JoinHintsCollector::new(); - for member in self.members.values() { - collector.apply(member, &())?; - } - Ok(collector.extract_result()) - } - pub fn timezone(&self) -> Tz { self.timezone.clone() } + /// Looks up an explicit alias override for a member's full name; + /// `None` when no override is set. pub fn alias_for_member(&self, full_name: &str) -> Option { self.member_to_alias .as_ref() .and_then(|m| m.get(full_name).cloned()) } + /// Compiles a JS `MemberSql` declaration into a `SqlCall` bound + /// to the given owning cube, via `SqlCallBuilder`. pub fn compile_sql_call( &mut self, cube_name: &String, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/base_filter.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/base_filter.rs index dbe2377747118..57b2991ebadd9 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/base_filter.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/base_filter.rs @@ -5,13 +5,17 @@ use cubenativeutils::CubeError; use itertools::Itertools; use std::rc::Rc; +/// Classifies a filter by the kind of member it targets. Drives where +/// the filter is placed when the query is rendered (WHERE vs HAVING). #[derive(Debug, Clone, PartialEq, Eq)] pub enum FilterType { Dimension, Measure, } -// TODO: temporary compatibility proxy — collapse into TypedFilter and update FilterItem consumers +/// Filter on a single member; thin wrapper over `TypedFilter`. +// TODO: temporary compatibility proxy — collapse into TypedFilter +// and update FilterItem consumers. #[derive(Clone)] pub struct BaseFilter { typed_filter: TypedFilter, @@ -61,10 +65,16 @@ impl BaseFilter { Ok(Rc::new(Self { typed_filter })) } + /// Member this filter applies to, with `TimeDimension` wrappers + /// peeled off to the underlying base dimension. Use + /// `raw_member_evaluator` to keep the wrapper. pub fn member_evaluator(&self) -> Rc { resolve_base_symbol(self.typed_filter.member_evaluator()) } + /// Member this filter applies to, exactly as it was given — + /// `TimeDimension` wrappers are kept. See `member_evaluator` + /// for the unwrapped form. pub fn raw_member_evaluator(&self) -> Rc { self.typed_filter.member_evaluator().clone() } @@ -86,6 +96,8 @@ impl BaseFilter { Ok(Rc::new(Self { typed_filter })) } + /// The filtered member as a time-dimension symbol when it is one; + /// `None` otherwise. pub fn time_dimension_symbol(&self) -> Option> { if self .typed_filter @@ -103,10 +115,15 @@ impl BaseFilter { self.typed_filter.values() } + /// Raw filter operator enum, matching the value declared in the + /// data model (`equals`, `in`, `inDateRange`, ...). See + /// `operation` for the decoded form ready for rendering. pub fn filter_operator(&self) -> &FilterOperator { self.typed_filter.operator() } + /// Decoded, typed form of the filter operation, ready for + /// rendering. See `filter_operator` for the raw enum. pub fn operation(&self) -> &super::typed_filter::FilterOp { self.typed_filter.operation() } @@ -123,11 +140,15 @@ impl BaseFilter { self.member_evaluator().full_name() } + /// True when the filter compares its member to exactly one value + /// with the `Equal` operator. pub fn is_single_value_equal(&self) -> bool { self.typed_filter.values().len() == 1 && *self.typed_filter.operator() == FilterOperator::Equal } + /// Concrete allowed values when the operator is `In` or `Equal`, + /// otherwise `None`. `NULL`s in the value list are discarded. pub fn get_value_restrictions(&self) -> Option> { if *self.typed_filter.operator() == FilterOperator::In || *self.typed_filter.operator() == FilterOperator::Equal diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/base_segment.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/base_segment.rs index ac2a889ad069d..629ad8aa35fb1 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/base_segment.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/base_segment.rs @@ -4,6 +4,10 @@ use crate::planner::{ use cubenativeutils::CubeError; use std::rc::Rc; +/// One `segments:` entry from the data model — a boolean expression +/// attached to a cube under a name, materialised as a synthetic +/// `MemberExpression` so it plugs into the same member machinery as +/// dimensions and measures. #[derive(Clone)] pub struct BaseSegment { full_name: String, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/compiler.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/compiler.rs index e897a3fab1f08..e17abc1fbf922 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/compiler.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/compiler.rs @@ -8,6 +8,11 @@ use cubenativeutils::CubeError; use std::rc::Rc; use std::str::FromStr; +/// Compiles the data-model filter tree into three streams of +/// `FilterItem`s — filters on dimensions, on time dimensions, and +/// on measures. Time-dimension `date_range` attributes arrive +/// separately via `add_time_dimension_item` and are normalised into +/// `InDateRange` filters. pub struct FilterCompiler<'a> { evaluator_compiler: &'a mut Compiler, query_tools: Rc, @@ -38,6 +43,9 @@ impl<'a> FilterCompiler<'a> { Ok(()) } + /// Lifts the optional `date_range` of a time-dimension request + /// into an explicit `InDateRange` filter on + /// `time_dimension_filters`. pub fn add_time_dimension_item(&mut self, item: &Rc) -> Result<(), CubeError> { if let Ok(td_item) = item.as_time_dimension() { if let Some(date_range) = td_item.date_range_vec() { @@ -54,6 +62,9 @@ impl<'a> FilterCompiler<'a> { Ok(()) } + /// Consumes the compiler and returns the three collected + /// streams: `(dimension_filters, time_dimension_filters, + /// measure_filters)`. pub fn extract_result(self) -> (Vec, Vec, Vec) { ( self.dimension_filters, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/filter_operator.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/filter_operator.rs index 5c6ca3a669d63..ca956c68655e3 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/filter_operator.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/filter_operator.rs @@ -1,6 +1,10 @@ use cubenativeutils::CubeError; use std::str::FromStr; +/// Filter operator declared in the data model (`equals`, `in`, +/// `gt`, `inDateRange`, ...). `RegularRollingWindowDateRange` and +/// `ToDateRollingWindowDateRange` are synthetic — manufactured by +/// the rolling-window planner, never coming from a query. #[derive(Clone, PartialEq, Debug)] pub enum FilterOperator { Equal, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/comparison.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/comparison.rs index 8b24cc443f251..76a7fdf40a32b 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/comparison.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/comparison.rs @@ -6,6 +6,8 @@ pub enum ComparisonKind { Lte, } +/// `Comparison` filter operation: compares the member against a +/// single value with `>`, `>=`, `<`, or `<=`. #[derive(Clone, Debug)] pub struct ComparisonOp { pub(crate) kind: ComparisonKind, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/date_range.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/date_range.rs index 53db04f09d895..8f67d098f5aaf 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/date_range.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/date_range.rs @@ -7,6 +7,8 @@ pub enum DateRangeKind { NotInRange, } +/// `DateRange` filter operation: tests the member's date against an +/// inclusive `[from, to]` range; `kind` chooses in-range vs not. #[derive(Clone, Debug)] pub struct DateRangeOp { pub(crate) kind: DateRangeKind, @@ -19,6 +21,8 @@ impl DateRangeOp { Self { kind, from, to } } + /// Formats `from` / `to` with the given fractional-seconds + /// precision, ready to be embedded into rendered SQL. pub fn formatted_date_range(&self, precision: u32) -> Result<(String, String), CubeError> { let from = QueryDateTimeHelper::format_from_date(&self.from, precision)?; let to = QueryDateTimeHelper::format_to_date(&self.to, precision)?; diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/date_single.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/date_single.rs index 8b162057f5282..207b9a2588f3b 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/date_single.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/date_single.rs @@ -6,6 +6,8 @@ pub enum DateSingleKind { AfterOrOn, } +/// `DateSingle` filter operation: compares the member's date to a +/// single boundary (`Before`, `BeforeOrOn`, `After`, `AfterOrOn`). #[derive(Clone, Debug)] pub struct DateSingleOp { pub(crate) kind: DateSingleKind, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/equality.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/equality.rs index 04f1ae22fd663..93924b3843a75 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/equality.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/equality.rs @@ -1,3 +1,5 @@ +/// `Equality` filter operation: tests the member for equality, or +/// inequality when `negated`, against a single value. #[derive(Clone, Debug)] pub struct EqualityOp { pub(crate) negated: bool, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/in_list.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/in_list.rs index 50c6eb20d05d9..58cc91731fb2a 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/in_list.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/in_list.rs @@ -1,3 +1,5 @@ +/// `InList` filter operation: tests whether the member belongs to, +/// or — when `negated` — does not belong to, a list of values. #[derive(Clone, Debug)] pub struct InListOp { pub(crate) negated: bool, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/like.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/like.rs index 2a424fa6c7728..ad693df6562a1 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/like.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/like.rs @@ -1,3 +1,5 @@ +/// `Like` filter operation: substring, prefix or suffix match +/// (negated for the `not_*` variants). #[derive(Clone, Debug)] pub struct LikeOp { pub(crate) negated: bool, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/measure_filter.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/measure_filter.rs index 7c6a34d365811..48a825c93abe0 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/measure_filter.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/measure_filter.rs @@ -1,3 +1,5 @@ +/// `MeasureFilter` filter operation — marker for the `measureFilter` +/// data-model operator; carries no parameters of its own. #[derive(Clone, Debug)] pub struct MeasureFilterOp; diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/nullability.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/nullability.rs index 45a7a3285ef71..dd13315cd9568 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/nullability.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/nullability.rs @@ -1,3 +1,5 @@ +/// `Nullability` filter operation: `IS NULL`, or `IS NOT NULL` when +/// `negated`. #[derive(Clone, Debug)] pub struct NullabilityOp { pub(crate) negated: bool, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/rolling_window.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/rolling_window.rs index b03568d9c32f7..8c57685d1ce01 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/rolling_window.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/rolling_window.rs @@ -1,3 +1,6 @@ +/// `RegularRollingWindow` filter operation: trailing and leading +/// interval bounds of a rolling window relative to each time-series +/// point. #[derive(Clone, Debug)] pub struct RegularRollingWindowOp { pub(crate) trailing: Option, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/to_date_rolling_window.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/to_date_rolling_window.rs index 2541847f45e4c..90589d9b145fc 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/to_date_rolling_window.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/operators/to_date_rolling_window.rs @@ -1,5 +1,7 @@ use crate::planner::Granularity; +/// `ToDateRollingWindow` filter operation: bounds a "since the start +/// of " window — e.g. month-to-date, year-to-date. #[derive(Clone)] pub struct ToDateRollingWindowOp { pub(crate) granularity: Granularity, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/tree.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/tree.rs index ba5b64aa85830..6333acada6128 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/tree.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/tree.rs @@ -3,12 +3,14 @@ use crate::planner::MemberSymbol; use std::fmt; use std::rc::Rc; +/// Boolean operator combining the items of a `FilterGroup`. #[derive(Clone, PartialEq)] pub enum FilterGroupOperator { Or, And, } +/// Boolean combination of nested `FilterItem`s, joined by `operator`. #[derive(Clone)] pub struct FilterGroup { pub operator: FilterGroupOperator, @@ -27,6 +29,12 @@ impl FilterGroup { } } +/// Node in a filter tree: +/// +/// - `Group` — a nested boolean group (AND/OR). +/// - `Item` — a leaf filter on a single member. +/// - `Segment` — a segment-based filter (bool expression named in +/// the data model). #[derive(Clone, PartialEq)] pub enum FilterItem { Group(Rc), @@ -34,6 +42,8 @@ pub enum FilterItem { Segment(Rc), } +/// Top-level filter tree of a query — its `items` are implicitly +/// AND-joined. #[derive(Clone)] pub struct Filter { pub items: Vec, @@ -49,6 +59,8 @@ impl fmt::Display for FilterGroupOperator { } impl Filter { + /// All members referenced anywhere in the filter tree, flattened + /// recursively through groups. pub fn all_member_evaluators(&self) -> Vec> { let mut result = Vec::new(); for item in self.items.iter() { @@ -57,6 +69,9 @@ impl Filter { result } + /// Collapses the filter into a single `FilterItem`: `None` when + /// empty, the only item directly when one is present, or an + /// AND-`Group` wrapping the rest. pub fn to_filter_item(&self) -> Option { if self.items.is_empty() { None @@ -90,9 +105,9 @@ impl FilterItem { } } - /// Extract all member symbols from this filter tree - /// Returns None if filter tree is invalid (e.g., empty group) - /// Returns Some(set) with all member symbols found in the tree + // Extracts all member symbols from this filter tree. + // Returns None when the tree is invalid (e.g. an empty group, or + // anything below a Segment item), Some(members) otherwise. fn extract_filter_members(&self) -> Option>> { match self { FilterItem::Group(group) => { @@ -118,11 +133,10 @@ impl FilterItem { } } - /// Find subtree of filters that only contains filters for the specified members - /// Returns None if no matching filters found - /// Returns Some(FilterItem) with the subtree containing only filters for target members - /// - /// This only processes AND groups - OR groups are not supported and will return None + /// Returns the largest subtree that only references the given + /// `target_members`, or `None` if no such subtree exists. Only + /// AND groups are traversed for partial matching — OR groups + /// either match as a whole or are dropped. pub fn find_subtree_for_members(&self, target_members: &[&String]) -> Option { match self { FilterItem::Group(group) => { @@ -190,11 +204,14 @@ impl FilterItem { } } - /// Find value restrictions for a given symbol across filter tree - /// Returns: - /// - None: no restrictions found for this symbol - /// - Some(vec![]): restrictions exist but result in empty set (contradiction) - /// - Some(values): list of allowed values for this symbol + /// Collects the set of allowed values for `symbol` from the + /// filter tree. + /// + /// - `None` — no restriction is placed on `symbol`. + /// - `Some(vec![])` — restrictions exist but contradict each + /// other (empty set). + /// - `Some(values)` — explicit list of values the symbol may + /// take. AND groups intersect, OR groups union. pub fn find_value_restriction(&self, symbol: &Rc) -> Option> { match self { FilterItem::Item(item) => { diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/typed_filter.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/typed_filter.rs index a015a6ad2fb6f..cad84a60fd0e9 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/typed_filter.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/filter/typed_filter.rs @@ -26,6 +26,10 @@ pub fn resolve_base_symbol(symbol: &Rc) -> Rc { } } +/// Typed, ready-to-render filter operation. Decoded once at filter +/// construction from a `FilterOperator` plus its value list, each +/// variant carrying exactly the parameters its rendering needs +/// (compared value, date bounds, granularity, etc.). #[derive(Clone, Debug)] pub enum FilterOp { Comparison(ComparisonOp), @@ -40,6 +44,10 @@ pub enum FilterOp { ToDateRollingWindow(ToDateRollingWindowOp), } +/// Filter bound to a member and decoded into a typed `FilterOp`. +/// Carries both the raw operator + value list (the form that came +/// from the data model) and the decoded form, so renderers can pick +/// whichever view they need. #[derive(Clone)] pub struct TypedFilter { query_tools: Rc, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/join_hints.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/join_hints.rs index f590b21e32274..f7efa2650b8d5 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/join_hints.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/join_hints.rs @@ -1,5 +1,9 @@ use crate::cube_bridge::join_hints::JoinHintItem; +/// Ordered list of cube-join hints. Adjacent redundant entries are +/// silently dropped on `push` / `extend` — a `Single` is skipped when +/// it duplicates either the previous `Single` or the tail of the +/// previous `Vector`. #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct JoinHints { items: Vec, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/multi_fact_join_groups.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/multi_fact_join_groups.rs index b00908fa23313..51966b1d15bf7 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/multi_fact_join_groups.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/multi_fact_join_groups.rs @@ -10,12 +10,17 @@ use itertools::Itertools; use std::collections::HashMap; use std::rc::Rc; +/// A single measure paired with the complete set of join hints it +/// needs — base query hints plus the measure's own incremental hints. #[derive(Clone, Debug)] pub struct MeasureJoinHints { pub measure: Rc, pub hints: JoinHints, } +/// Accumulates the join-hint context of a query (initial +/// `query_join_hints`, dimensions, filters) and produces +/// `MeasuresJoinHints` for a given set of measures. pub struct MeasuresJoinHintsBuilder { initial_hints: JoinHints, dimensions: Vec>, @@ -52,6 +57,13 @@ impl MeasuresJoinHintsBuilder { } } +/// Join-hint context of a query split into: +/// +/// - `base_hints` — hints shared across all measures (query-level +/// hints plus those collected from dimensions and filters). +/// - `measure_hints` — per-measure incremental hints, one entry per +/// non-multi-stage measure. Multi-stage measures plan their joins +/// separately and are skipped here. #[derive(Clone, Debug)] pub struct MeasuresJoinHints { base_hints: JoinHints, @@ -59,6 +71,7 @@ pub struct MeasuresJoinHints { } impl MeasuresJoinHints { + /// Start a builder seeded with the query-level join hints. pub fn builder(query_join_hints: &JoinHints) -> MeasuresJoinHintsBuilder { MeasuresJoinHintsBuilder { initial_hints: query_join_hints.clone(), @@ -67,6 +80,8 @@ impl MeasuresJoinHints { } } + /// Reuse the existing `base_hints` to produce a new + /// `MeasuresJoinHints` over a different measure subset. pub fn for_measures(&self, measures: &[Rc]) -> Result { Self::from_base_hints(self.base_hints.clone(), measures) } @@ -108,6 +123,9 @@ impl MeasuresJoinHints { &self.measure_hints } + /// Combined hints for a specific measure (base plus the measure's + /// own), or `None` when the measure has no entry — typically + /// because it is multi-stage. pub fn hints_for_measure(&self, measure: &MemberSymbol) -> Option { self.measure_hints .iter() @@ -118,6 +136,14 @@ impl MeasuresJoinHints { // --- MultiFactJoinGroups: builds actual join trees --- +/// Resolves a query's `MeasuresJoinHints` into concrete join trees +/// and groups measures by the `JoinKey` of the cube graph they share. +/// More than one group means the query is **multi-fact**: it touches +/// measures from cubes that cannot be combined under a single join. +/// +/// Per-cube join paths from the root of each group are precomputed so +/// `resolve_join_path_for_dimension` / `resolve_join_path_for_measure` +/// are cheap lookups at render time. #[derive(Clone)] pub struct MultiFactJoinGroups { query_tools: Rc, @@ -130,6 +156,8 @@ pub struct MultiFactJoinGroups { } impl MultiFactJoinGroups { + /// Builds the join trees from `measures_join_hints` and groups + /// measures by the resulting `JoinKey`. pub fn try_new( query_tools: Rc, measures_join_hints: MeasuresJoinHints, @@ -145,6 +173,8 @@ impl MultiFactJoinGroups { }) } + /// Rebuilds the groups for a different measure subset, reusing + /// the shared `base_hints`. pub fn for_measures(&self, measures: &[Rc]) -> Result { let new_hints = self.measures_join_hints.for_measures(measures)?; Self::try_new(self.query_tools.clone(), new_hints) @@ -194,6 +224,7 @@ impl MultiFactJoinGroups { &self.measures_join_hints } + /// True when the query's measures span more than one join tree. pub fn is_multi_fact(&self) -> bool { self.groups.len() > 1 } @@ -272,6 +303,9 @@ impl MultiFactJoinGroups { Ok(paths) } + /// The only join when the query has exactly one group; errors if + /// the query is multi-fact, and `Ok(None)` if it has no groups + /// at all. pub fn single_join(&self) -> Result>, CubeError> { if self.groups.is_empty() { return Ok(None); diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/common_utils.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/common_utils.rs index 9b48178bdba24..76efcc10beed9 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/common_utils.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/common_utils.rs @@ -4,6 +4,8 @@ use crate::planner::MemberSymbol; use cubenativeutils::CubeError; use std::rc::Rc; +/// Small helpers shared between planners — cube resolution and the +/// list of primary-key dimensions for a given cube. pub struct CommonUtils { query_tools: Rc, } @@ -13,6 +15,7 @@ impl CommonUtils { Self { query_tools } } + /// Resolves the planner-level `BaseCube` for the given cube path. pub fn cube_from_path(&self, cube_path: String) -> Result, CubeError> { let evaluator_compiler_cell = self.query_tools.evaluator_compiler().clone(); let mut evaluator_compiler = evaluator_compiler_cell.borrow_mut(); @@ -22,6 +25,8 @@ impl CommonUtils { BaseCube::try_new(cube_path.to_string(), self.query_tools.clone(), evaluator) } + /// Primary-key dimensions of `cube_name` as planner + /// `MemberSymbol`s. pub fn primary_keys_dimensions( &self, cube_name: &String, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/dimension_subquery_planner.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/dimension_subquery_planner.rs index 64dda0181cd8a..c91fbd7cf6bf1 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/dimension_subquery_planner.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/dimension_subquery_planner.rs @@ -11,6 +11,10 @@ use std::cell::{Ref, RefCell}; use std::collections::HashMap; use std::rc::Rc; +/// Plans `DimensionSubQuery` nodes for `sub_query: true` dimensions. +/// Each subquery dimension becomes its own `Query` over the owning +/// cube's primary keys plus the dimension's measure expression, then +/// gets joined back into the host query on those keys. pub struct DimensionSubqueryPlanner { utils: CommonUtils, query_tools: Rc, @@ -20,6 +24,8 @@ pub struct DimensionSubqueryPlanner { } impl DimensionSubqueryPlanner { + /// Planner with no sub-query dimensions — used when the host + /// query has none. pub fn empty(query_tools: Rc, query_properties: Rc) -> Self { Self { sub_query_dims: HashMap::new(), @@ -29,6 +35,8 @@ impl DimensionSubqueryPlanner { dimensions_refs: RefCell::new(HashMap::new()), } } + /// Builds a planner over the given sub-query dimensions, indexed + /// by owning cube. pub fn try_new( dimensions: &Vec>, query_tools: Rc, @@ -52,6 +60,7 @@ impl DimensionSubqueryPlanner { }) } + /// Plans one `DimensionSubQuery` per dimension in the input list. pub fn plan_queries( &self, dimensions: &Vec>, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/full_key_query_aggregate_planner.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/full_key_query_aggregate_planner.rs index 6fbf06027f756..66af9afd708da 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/full_key_query_aggregate_planner.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/full_key_query_aggregate_planner.rs @@ -3,6 +3,9 @@ use crate::planner::QueryProperties; use cubenativeutils::CubeError; use std::rc::Rc; +/// Final assembly step for non-simple queries: takes the collected +/// multi-stage / multiplied subquery refs and members and wraps +/// them in a `Query` whose source is a `FullKeyAggregate`. pub struct FullKeyAggregateQueryPlanner { query_properties: Rc, } @@ -12,6 +15,9 @@ impl FullKeyAggregateQueryPlanner { Self { query_properties } } + /// Builds the `FullKeyAggregate` source from the collected + /// multi-stage subquery refs. Always renders as FULL OUTER JOIN + /// + COALESCE. pub fn plan_logical_source( &self, multi_stage_subqueries: Vec>, @@ -29,6 +35,8 @@ impl FullKeyAggregateQueryPlanner { )) } + /// Wraps `plan_logical_source` in a `Query` with the request's + /// filters, modifiers and multi-stage members. pub fn plan_logical_plan( &self, multi_stage_subqueries: Vec>, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/join_planner.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/join_planner.rs index 7d7064bbca5a4..0f2fc936500fc 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/join_planner.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/join_planner.rs @@ -9,6 +9,10 @@ use crate::planner::SqlCall; use cubenativeutils::CubeError; use std::rc::Rc; +/// Join item with its members resolved to `MemberSymbol`s on both +/// sides — produced from a `JoinDefinition` by `JoinPlanner` so the +/// rest of the planner can reason about the actual columns involved +/// without re-parsing the JS-side ON clause. #[derive(Clone, Debug)] pub struct ResolvedJoinItem { pub original_from: String, @@ -19,6 +23,8 @@ pub struct ResolvedJoinItem { } impl ResolvedJoinItem { + /// Equality on the cube pair and the resolved member sets — used + /// to detect duplicates without comparing the compiled `on_sql`. pub fn is_same_as(&self, other: &Self) -> bool { self.original_from == other.original_from && self.original_to == other.original_to @@ -27,6 +33,9 @@ impl ResolvedJoinItem { } } +/// Builds `LogicalJoin` trees from `JoinDefinition`s (or join +/// hints), compiles each item's ON SQL into a `SqlCall`, and exposes +/// helpers for resolving the members each ON clause references. pub struct JoinPlanner { utils: CommonUtils, query_tools: Rc, @@ -40,6 +49,8 @@ impl JoinPlanner { } } + /// Builds a `LogicalJoin` from join hints, asking the join graph + /// to materialise the matching `JoinDefinition`. pub fn make_join_logical_plan_with_join_hints( &self, join_hints: JoinHints, @@ -52,10 +63,15 @@ impl JoinPlanner { self.make_join_logical_plan(join, dimension_subqueries) } + /// Empty `LogicalJoin` — used when the query needs no joins + /// (e.g. it touches a single cube and pulls nothing extra). pub fn make_empty_join_logical_plan(&self) -> Rc { Rc::new(LogicalJoin::builder().build()) } + /// Builds a `LogicalJoin` from an already-resolved + /// `JoinDefinition`, compiling each item's ON SQL and attaching + /// the given sub-query dimensions. pub fn make_join_logical_plan( &self, join: Rc, @@ -83,6 +99,8 @@ impl JoinPlanner { )) } + /// Compiles the ON SQL of a join item into a `SqlCall` rooted at + /// the `from` cube. pub fn compile_join_condition( &self, join_item: Rc, @@ -94,6 +112,8 @@ impl JoinPlanner { .compile_sql_call(&join_item.static_data().original_from, definition.sql()?) } + /// Materialises the join from `join_hints` and resolves the + /// members each ON clause references. pub fn resolve_join_members_by_hints( &self, join_hints: &JoinHints, @@ -104,6 +124,8 @@ impl JoinPlanner { .build_join(join_hints.items().to_vec())?; self.resolve_join_members(join) } + /// Resolves the members each ON clause references for an + /// already-built `JoinDefinition`. pub fn resolve_join_members( &self, join: Rc, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/cte_state.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/cte_state.rs index aabac32044f2c..73c35ce5442a1 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/cte_state.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/cte_state.rs @@ -1,6 +1,11 @@ use crate::logical_plan::{LogicalMultiStageMember, MultiStageSubqueryRef}; use std::rc::Rc; +/// Accumulator the multi-stage / multiplied planners write CTEs +/// into during planning: a monotonic counter for generated CTE +/// names (`cte_0`, `cte_1`, ...), the list of +/// `LogicalMultiStageMember`s, and the subquery refs that the +/// outer `FullKeyAggregate` will join over. pub struct CteState { counter: usize, members: Vec>, @@ -16,6 +21,7 @@ impl CteState { } } + /// Generates the next unique CTE name (`cte_0`, `cte_1`, ...). pub fn next_cte_name(&mut self) -> String { let name = format!("cte_{}", self.counter); self.counter += 1; @@ -30,6 +36,8 @@ impl CteState { self.subquery_refs.push(subquery_ref); } + /// Consumes the state, returning the accumulated CTE members + /// and subquery refs. pub fn into_results( self, ) -> ( diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/member.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/member.rs index 082146a9a90c6..e1b2f596c8851 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/member.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/member.rs @@ -1,12 +1,18 @@ use crate::planner::{MeasureTimeShifts, MemberSymbol}; use std::rc::Rc; +/// Description of the time-series CTE driving a rolling-window +/// computation: the time dimension and, optionally, the alias of a +/// sibling CTE that resolves its date range at query time. #[derive(Clone)] pub struct TimeSeriesDescription { pub time_dimension: Rc, pub date_range_cte: Option, } +/// Kind of leaf CTE in a multi-stage chain: a base measure query, +/// a time-series axis, or a query that just resolves the date range +/// of a time dimension. #[derive(Clone)] pub enum MultiStageLeafMemberType { Measure, @@ -14,6 +20,8 @@ pub enum MultiStageLeafMemberType { TimeSeriesGetRange(Rc), } +/// Bounds of a regular rolling window: trailing / leading interval +/// strings and a time-series offset. #[derive(Clone)] pub struct RegularRollingWindow { pub trailing: Option, @@ -21,11 +29,15 @@ pub struct RegularRollingWindow { pub offset: String, } +/// To-date rolling window — accumulates since the start of +/// `granularity` (month-to-date, year-to-date, …). #[derive(Clone)] pub struct ToDateRollingWindow { pub granularity: String, } +/// Flavour of rolling-window computation: regular trailing/leading +/// window, to-date window, or a running-total accumulation. #[derive(Clone)] pub enum RollingWindowType { Regular(RegularRollingWindow), @@ -33,6 +45,9 @@ pub enum RollingWindowType { RunningTotal, } +/// Planner-side description of a rolling window: the time +/// dimension used for windowing (and its lower-granularity base +/// version produced in the leaf CTE) plus the chosen window type. #[derive(Clone)] pub struct RollingWindowDescription { pub time_dimension: Rc, @@ -84,6 +99,9 @@ impl RollingWindowDescription { } } +/// Semantic shape of a non-leaf multi-stage CTE: a rank window, +/// an aggregate (possibly window-rendered), a non-aggregating +/// calculation, a dimension calculation, or a rolling window. #[derive(Clone)] pub enum MultiStageInodeMemberType { Rank, @@ -93,6 +111,11 @@ pub enum MultiStageInodeMemberType { RollingWindow(RollingWindowDescription), } +/// Non-leaf node in a multi-stage tree. Bundles the semantic +/// `inode_type` (Rank / Aggregate / Calculate / Dimension / +/// RollingWindow) with the partition-shaping flags driven by the +/// measure's data-model directives: `reduce_by`, `add_group_by`, +/// `group_by`, `time_shift`. #[derive(Clone)] pub struct MultiStageInodeMember { inode_type: MultiStageInodeMemberType, @@ -154,6 +177,9 @@ impl MultiStageInodeMember { } } +/// Position of a CTE in the multi-stage tree: either an inner node +/// (depends on other CTEs) or a leaf (queries the underlying data +/// directly). #[derive(Clone)] pub enum MultiStageMemberType { Inode(MultiStageInodeMember), @@ -169,6 +195,9 @@ impl MultiStageMemberType { } } +/// One node in a multi-stage tree: its position (`member_type`), +/// the member symbol it renders, and a few rendering flags +/// (ungrouped, has-aggregates-on-top, is-without-member-leaf). pub struct MultiStageMember { member_type: MultiStageMemberType, member_symbol: Rc, @@ -193,6 +222,10 @@ impl MultiStageMember { }) } + /// Builds a leaf node whose base CTE selects only dimensions — + /// the member itself is not computed by the leaf (e.g. for a + /// `Rank` measure where the value comes purely from a window + /// function applied on top). pub fn new_without_member_leaf( member_type: MultiStageMemberType, evaluation_node: Rc, @@ -216,6 +249,9 @@ impl MultiStageMember { &self.member_symbol } + /// True when this node is a leaf whose CTE selects dimensions + /// only, without computing the member's value (see + /// `new_without_member_leaf`). pub fn is_without_member_leaf(&self) -> bool { self.is_without_member_leaf } diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/member_query_planner.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/member_query_planner.rs index b19bad335bcc8..017ada18774c4 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/member_query_planner.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/member_query_planner.rs @@ -14,6 +14,11 @@ use itertools::Itertools; use std::rc::Rc; use std::vec; +/// Renders one `MultiStageQueryDescription` into a +/// `LogicalMultiStageMember`. The shape of the output is dispatched +/// from the description's `MultiStageMemberType`: rolling-window / +/// dimension / measure inode, or a leaf (base measure / +/// time-series / time-series-get-range). pub struct MultiStageMemberQueryPlanner { query_tools: Rc, query_properties: Rc, @@ -33,6 +38,9 @@ impl MultiStageMemberQueryPlanner { } } + /// Builds the `LogicalMultiStageMember` for this description, + /// dispatching on `MultiStageMemberType` to the appropriate + /// `plan_*` builder. pub fn plan_logical_query(&self) -> Result, CubeError> { match self.description.member().member_type() { MultiStageMemberType::Inode(member) => match member.inode_type() { @@ -54,6 +62,10 @@ impl MultiStageMemberQueryPlanner { } } + /// Builds the leaf `GetDateRange` CTE used when a rolling-window + /// time dimension has no explicit date range — runs a + /// `SimpleQueryPlanner` to compute the actual bounds at query + /// time. fn plan_time_series_get_range_query( &self, time_dimension: Rc, @@ -85,6 +97,10 @@ impl MultiStageMemberQueryPlanner { Ok(Rc::new(member)) } + /// Builds the leaf `TimeSeries` CTE — the date axis a rolling + /// window walks over. References a sibling `GetDateRange` CTE + /// for its bounds when the time dimension has no explicit + /// `date_range`. fn plan_time_series_query( &self, time_series_description: Rc, @@ -101,6 +117,10 @@ impl MultiStageMemberQueryPlanner { })) } + /// Builds the rolling-window CTE that combines a time-series + /// input with a measure input, dispatching on + /// `RollingWindowDescription` into the regular / to-date / + /// running-total variant. fn plan_rolling_window_query( &self, rolling_window_desc: &RollingWindowDescription, @@ -174,6 +194,12 @@ impl MultiStageMemberQueryPlanner { })) } + /// Builds a measure-calculation CTE (Rank / Aggregate / + /// Calculate). Picks the partition-by from the inode's + /// `reduce_by` / `group_by` settings, chooses a window-function + /// flavour (Rank, Window, or None) when the partition is + /// narrower than the full dimension set, and wires the input + /// CTEs into a `FullKeyAggregate` source. fn plan_for_cte_query( &self, multi_stage_member: &MultiStageInodeMember, @@ -255,6 +281,11 @@ impl MultiStageMemberQueryPlanner { Ok(Rc::new(result)) } + /// Builds a dimension-calculation CTE for a multi-stage + /// dimension. Includes the dimension itself plus every + /// non-multi-stage dimension reachable from the subtree, so the + /// resulting CTE can be joined back into the host query on + /// those dimensions. fn plan_for_cte_dimension_query( &self, _multi_stage_member: &MultiStageInodeMember, @@ -339,6 +370,12 @@ impl MultiStageMemberQueryPlanner { Ok(Rc::new(result)) } + /// Builds the leaf CTE for a base measure — runs a fresh + /// `QueryPlanner` on the description's state with + /// `allow_multi_stage = false`, then wraps the result in a + /// `MultiStageLeafMeasure`. Respects the `without-member-leaf` + /// shape for cases like `Rank` where the leaf selects only the + /// dimension grid. fn plan_for_leaf_cte_query(&self) -> Result, CubeError> { let member_node = self.description.member_node(); let mut dimensions = self.description.state().dimensions().clone(); diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs index 9ebb0aab879a7..2a74aeb8e0506 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/multi_stage_query_planner.rs @@ -26,6 +26,13 @@ use itertools::Itertools; use std::collections::HashSet; use std::rc::Rc; +/// Plans the multi-stage CTE tree of a query. For every multi-stage +/// member it encounters in `all_used_symbols`, it recursively +/// produces `MultiStageQueryDescription`s for the member and its +/// dependencies, then asks `MultiStageMemberQueryPlanner` to render +/// each into a `LogicalMultiStageMember`. CTEs are deduplicated by +/// `(member, state)` so the same multi-stage subquery isn't +/// emitted twice. pub struct MultiStageQueryPlanner { query_tools: Rc, query_properties: Rc, @@ -39,6 +46,9 @@ impl MultiStageQueryPlanner { } } + /// Populates `cte_state` with multi-stage CTEs (and their + /// subquery refs) for every multi-stage member used by the + /// query. No-op when the query has none. pub fn plan_queries(&self, cte_state: &mut CteState) -> Result<(), CubeError> { let multi_stage_members = self .query_properties @@ -105,6 +115,12 @@ impl MultiStageQueryPlanner { Ok(()) } + /// Classifies `base_member` into a `MultiStageInodeMember` — picks + /// the inode kind (Rank / Aggregate / Calculate for a measure, + /// Dimension for a dimension) and pulls the partition-shaping + /// flags (`reduce_by`, `add_group_by`, `group_by`, `time_shift`) + /// out of the data-model definition. Returns the inode together + /// with the leaf's `is_ungrupped` flag. fn create_multi_stage_inode_member( &self, base_member: Rc, @@ -159,6 +175,10 @@ impl MultiStageQueryPlanner { Ok(inode) } + /// Builds child descriptions for `member`'s inode. Switches to + /// `try_make_childs_for_case_switch` when the member's body is a + /// CASE-SWITCH expression; otherwise falls through to + /// `default_make_childs`. fn make_childs( &self, member: Rc, @@ -190,6 +210,8 @@ impl MultiStageQueryPlanner { ) } + /// True if `member` is a dimension that has multi-stage members + /// somewhere in its dependency tree. fn is_multi_stage_dimension(member: &Rc) -> Result { if member.is_dimension() { has_multi_stage_members(member, false) @@ -198,6 +220,12 @@ impl MultiStageQueryPlanner { } } + /// Default child-generation path: for each measure or + /// multi-stage-dimension dependency, recurses into + /// `make_queries_descriptions` and adds the result as an input + /// CTE. If the member has no such deps (e.g. a `Rank` measure + /// that only needs the dimension grid), produces a single + /// "without-member" leaf instead. fn default_make_childs( &self, member: Rc, @@ -245,6 +273,13 @@ impl MultiStageQueryPlanner { Ok(()) } + /// Plans CASE-SWITCH dependencies: collects, per dependency, the + /// union of switch values it covers and renders each dependency + /// under a state with an equality filter on the switch member + /// restricted to those values. An open `ELSE` branch makes the + /// dependency unrestricted. Returns `false` when the switch is + /// not a member reference, in which case the caller falls back + /// to `default_make_childs`. fn try_make_childs_for_case_switch( &self, case: &CaseSwitchDefinition, @@ -322,6 +357,15 @@ impl MultiStageQueryPlanner { Ok(true) } + /// Core recursive step. Given a `member` and the current + /// `state`, resolves the reference chain, applies static filters + /// from the dimensions filters, deduplicates against + /// already-built descriptions, tries a rolling-window path + /// (`try_plan_rolling_window`), and otherwise returns either a + /// leaf `Measure` or an inode description whose children come + /// from `make_childs`. Adjusts the state for inodes with any + /// `add_group_by`, time-shift or per-member filter changes the + /// inode demands. fn make_queries_descriptions( &self, member: Rc, @@ -431,6 +475,10 @@ impl MultiStageQueryPlanner { Ok(description) } + /// If `member` is a cumulative measure, plans the time-series + /// and rolling-window CTEs and returns the rolling-window + /// description. Returns `None` for other measures and for + /// non-measure members. pub fn try_plan_rolling_window( &self, member: Rc, @@ -470,7 +518,7 @@ impl MultiStageQueryPlanner { } } - let ungrouped = measure.is_rolling_window() && !measure.is_addictive(); + let ungrouped = measure.is_rolling_window() && !measure.is_additive(); let mut time_dimensions = self .query_properties @@ -604,6 +652,9 @@ impl MultiStageQueryPlanner { } } + /// Adds (or reuses) the `time_series_get_range` leaf CTE — used + /// by `add_time_series` when the requested time dimension has no + /// explicit date range and the planner needs to compute one. fn add_time_series_get_range_query( &self, time_dimension: Rc, @@ -635,6 +686,10 @@ impl MultiStageQueryPlanner { Ok(description) } + /// Adds (or reuses) the `time_series` leaf CTE that drives a + /// rolling window. When the time dimension has no `date_range`, + /// also arranges for a sibling `time_series_get_range` CTE to + /// feed it. fn add_time_series( &self, time_dimension: Rc, @@ -681,6 +736,10 @@ impl MultiStageQueryPlanner { Ok(description) } + /// Adds the leaf CTE that produces the base values for a + /// rolling window — selects the requested dimensions plus the + /// unrolled measure, marked `has_aggregates_on_top` so callers + /// know an outer rolling computation will consume it. fn add_rolling_window_base( &self, member: Rc, @@ -705,6 +764,9 @@ impl MultiStageQueryPlanner { Ok(description) } + /// Returns the granularity of a `to_date` rolling window. Errors + /// if the window is declared as `to_date` without a granularity, + /// and returns `None` for window kinds that don't carry one. fn get_to_date_rolling_granularity( &self, rolling_window: &RollingWindow, @@ -750,6 +812,12 @@ impl MultiStageQueryPlanner { Ok(Rc::new(new_state)) } + /// Builds the state for a rolling-window base CTE: reduces the + /// time dimension to the minimum granularity required by the + /// window, drops other dimensions that resolve to time + /// dimensions, and rewrites the time-dimension date-range + /// filter to either a `to_date` bound or a regular trailing / + /// leading bound. fn make_rolling_base_state( &self, time_dimension: Rc, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/query_description.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/query_description.rs index c264ae10da684..c412b93125648 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/query_description.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/query_description.rs @@ -1,10 +1,15 @@ use super::MultiStageMember; use crate::logical_plan::LogicalSchema; +use crate::planner::collectors::has_multi_stage_members; use crate::planner::{MemberSymbol, QueryProperties}; use cubenativeutils::CubeError; use itertools::Itertools; use std::rc::Rc; +/// One CTE in the multi-stage tree as the planner sees it: the +/// `member` rendered, the `state` (`QueryProperties` snapshot for +/// this CTE's scope), the input CTEs it depends on, and the alias +/// it will be referenced by. pub struct MultiStageQueryDescription { member: Rc, state: Rc, @@ -67,6 +72,10 @@ impl MultiStageQueryDescription { self.input.is_empty() } + /// Walks the description subtree and returns + /// `(dimensions, time_dimensions)` whose chain-resolved members + /// have no multi-stage members of their own. Duplicates are + /// removed by full name. pub fn collect_all_non_multi_stage_dimension( &self, ) -> Result<(Vec>, Vec>), CubeError> { @@ -76,14 +85,9 @@ impl MultiStageQueryDescription { let dimensions = dimensions .into_iter() .unique_by(|d| d.full_name()) - .filter_map(|d| match d.is_basic_dimension() { - Ok(res) => { - if res { - None - } else { - Some(Ok(d)) - } - } + .filter_map(|d| match has_multi_stage_members(&d, true) { + Ok(true) => None, + Ok(false) => Some(Ok(d)), Err(e) => Some(Err(e)), }) .collect::, _>>()?; @@ -91,14 +95,9 @@ impl MultiStageQueryDescription { let time_dimensions = time_dimensions .into_iter() .unique_by(|d| d.full_name()) - .filter_map(|d| match d.is_basic_dimension() { - Ok(res) => { - if res { - None - } else { - Some(Ok(d)) - } - } + .filter_map(|d| match has_multi_stage_members(&d, true) { + Ok(true) => None, + Ok(false) => Some(Ok(d)), Err(e) => Some(Err(e)), }) .collect::, _>>()?; @@ -117,6 +116,10 @@ impl MultiStageQueryDescription { } } + /// True if this description renders `member_node` under an + /// equivalent state — used to deduplicate CTEs when the same + /// member is reached through different paths in the dependency + /// graph. pub fn is_match_member_and_state( &self, member_node: &Rc, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/time_shift_state.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/time_shift_state.rs index 921546ac8cf17..e603f0e21860d 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/time_shift_state.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multi_stage/time_shift_state.rs @@ -3,6 +3,9 @@ use crate::planner::DimensionTimeShift; use cubenativeutils::CubeError; use std::collections::HashMap; +/// Per-dimension time-shift accumulator used during multi-stage +/// planning. Keyed by dimension full name; aggregates the shifts +/// applied to each dimension across nested multi-stage scopes. #[derive(Clone, Default, Debug, PartialEq)] pub struct TimeShiftState { pub dimensions_shifts: HashMap, @@ -13,6 +16,11 @@ impl TimeShiftState { self.dimensions_shifts.is_empty() } + /// Splits the accumulated shifts into two maps: regular + /// `DimensionTimeShift`s applied at render time, and + /// `CalendarDimensionTimeShift`s that come from a calendar + /// cube's own time-shift declarations. A named shift that does + /// not match a calendar declaration is an error. pub fn extract_time_shifts( &self, ) -> Result< diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multiplied_measures_query_planner.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multiplied_measures_query_planner.rs index 79429a52095df..115f89f1c70bb 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multiplied_measures_query_planner.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/multiplied_measures_query_planner.rs @@ -13,6 +13,11 @@ use cubenativeutils::CubeError; use itertools::Itertools; use std::rc::Rc; +/// Plans the per-measure CTEs that feed `FullKeyAggregate` in +/// non-simple queries: regular measures become `MultiStageLeafMeasure` +/// CTEs (one per multi-fact group), and multiplied measures become +/// `AggregateMultipliedSubquery` CTEs (one per owning cube). Both +/// kinds are registered into the shared `CteState`. pub struct MultipliedMeasuresQueryPlanner { query_tools: Rc, query_properties: Rc, @@ -22,6 +27,9 @@ pub struct MultipliedMeasuresQueryPlanner { } impl MultipliedMeasuresQueryPlanner { + /// Constructs the planner and caches + /// `full_key_aggregate_measures` for later use in + /// `plan_queries`. pub fn try_new( query_tools: Rc, query_properties: Rc, @@ -36,6 +44,10 @@ impl MultipliedMeasuresQueryPlanner { }) } + /// Registers per-measure CTEs into `cte_state`: regular measures + /// become leaf-measure CTEs grouped by multi-fact join, multiplied + /// measures become `AggregateMultipliedSubquery` CTEs grouped by + /// owning cube. Errors if called on a simple query. pub fn plan_queries(&self, cte_state: &mut CteState) -> Result<(), CubeError> { if self.query_properties.is_simple_query()? { return Err(CubeError::internal(format!( diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/order_planner.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/order_planner.rs index 97acff93c32e3..fb3beb5ab8b58 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/order_planner.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/order_planner.rs @@ -3,6 +3,9 @@ use crate::planner::MemberSymbol; use crate::planner::{OrderByItem, QueryProperties}; use std::rc::Rc; +/// Maps the query's `OrderByItem`s onto physical `OrderBy` +/// expressions, resolving each item against the list of selected +/// members so it can be rendered as a positional reference. pub struct OrderPlanner { query_properties: Rc, } @@ -12,6 +15,8 @@ impl OrderPlanner { Self { query_properties } } + /// Renders the request's `order_by` against the full set of + /// selected members. pub fn default_order(&self) -> Vec { Self::custom_order( self.query_properties.order_by(), @@ -19,6 +24,8 @@ impl OrderPlanner { ) } + /// Resolves an explicit `order_by` list against an explicit + /// member list, producing positional `OrderBy` entries. pub fn custom_order(order_by: &[OrderByItem], members: &[Rc]) -> Vec { let mut result = Vec::new(); for itm in order_by.iter() { diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/query_planner.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/query_planner.rs index 72857199e0efd..1a41e59325926 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/query_planner.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/query_planner.rs @@ -9,6 +9,12 @@ use crate::planner::QueryProperties; use cubenativeutils::CubeError; use std::rc::Rc; +/// Entry point of the logical-plan construction. Dispatches by +/// `QueryProperties::is_simple_query()`: simple queries go through +/// `SimpleQueryPlanner`; everything else is built up from +/// multi-stage and multiplied-measure CTEs (`MultiStageQueryPlanner`, +/// `MultipliedMeasuresQueryPlanner`) and stitched together by +/// `FullKeyAggregateQueryPlanner`. pub struct QueryPlanner { query_tools: Rc, request: Rc, @@ -22,6 +28,9 @@ impl QueryPlanner { } } + /// Dispatches to `SimpleQueryPlanner` for simple queries; otherwise + /// builds the multi-stage / multiplied CTEs and assembles them via + /// `FullKeyAggregateQueryPlanner`. pub fn plan(&self) -> Result, CubeError> { if self.request.is_simple_query()? { let planner = SimpleQueryPlanner::new(self.query_tools.clone(), self.request.clone()); diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/simple_query_planer.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/simple_query_planer.rs index bbd98b4cde749..6572000af9ba2 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/simple_query_planer.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/planners/simple_query_planer.rs @@ -6,6 +6,9 @@ use crate::planner::QueryProperties; use cubenativeutils::CubeError; use std::rc::Rc; +/// Plans a `Query` for the simple case: a single `LogicalJoin` +/// source, no multi-stage or multiplied CTEs. Sub-query dimensions +/// are still woven into the join. pub struct SimpleQueryPlanner { query_tools: Rc, query_properties: Rc, @@ -20,6 +23,7 @@ impl SimpleQueryPlanner { } } + /// Builds the `Query` for a simple-case request. pub fn plan(&self) -> Result, CubeError> { let source = self.source_and_subquery_dimensions()?; @@ -54,6 +58,8 @@ impl SimpleQueryPlanner { Ok(Rc::new(result)) } + /// Resolves the query's join and the sub-query dimensions that + /// plug into it, returning the assembled `LogicalJoin` source. pub fn source_and_subquery_dimensions(&self) -> Result, CubeError> { let join = self.query_properties.simple_query_join()?; let subquery_dimensions = if let Some(join) = &join { diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs index 32e141429022d..3c4ab1e57d91c 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs @@ -487,7 +487,7 @@ impl QueryProperties { } let is_multiplied_measure = if item.multiplied { if let Ok(measure) = item.measure.as_measure() { - if measure.can_used_as_addictive_in_multplied() { + if measure.is_additive_in_multiplied() { false } else { true diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/sql_call.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/sql_call.rs index 85063a0740733..2422cd83711a7 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/sql_call.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/sql_call.rs @@ -11,6 +11,14 @@ use itertools::Itertools; use std::collections::HashMap; use std::rc::Rc; +/// Reference to a cube from a SQL template. +/// +/// - `Name` — the cube as an identifier (rendered from `{CUBE}` or +/// `{TABLE}` placeholders); resolves to the cube's quoted name or +/// alias. +/// - `Table` — the cube's table expression (rendered from +/// `{CUBE.sql()}`); resolves to the result of the cube's `sql:` +/// function, or to a registered pre-aggregation source. #[derive(Clone, Debug)] pub enum CubeRef { Name(Rc), @@ -47,6 +55,8 @@ impl CubeRef { } } +/// One `{arg:N}` binding inside a `SqlCall` template: either a +/// member symbol or a cube reference. #[derive(Clone, Debug)] pub enum SqlDependency { Symbol(Rc), @@ -77,6 +87,13 @@ impl SqlDependency { } } +/// Namespace for the placeholder prefixes recognised inside a +/// `SqlCall` template: +/// +/// - `arg:N` — Nth `SqlDependency` (member symbol or cube ref). +/// - `fp:N` — Nth filter param. +/// - `fg:N` — Nth filter group. +/// - `sv:N` — Nth security-context value. pub struct SqlCallArg; impl SqlCallArg { @@ -99,17 +116,32 @@ impl SqlCallArg { } } +/// One `FILTER_PARAMS` binding from the data-model SQL: the filter +/// member whose predicate should be substituted at render time, +/// together with the column information used to format it. #[derive(Debug, Clone)] pub struct SqlCallFilterParamsItem { pub filter_symbol_name: String, pub column: FilterParamsColumn, } +/// `FILTER_GROUP` binding from the data-model SQL: several +/// `FILTER_PARAMS` items combined into a single substitution. #[derive(Clone, Debug)] pub struct SqlCallFilterGroupItem { pub filter_params: Vec, } +/// Tesseract representation of a SQL-like function declared in the +/// data model (member `sql:`, `mask_sql:`, case branches, measure +/// filters, and so on). Plays two roles: it stores the template's +/// dependencies — already resolved to live member symbols and cube +/// references, not symbolic paths — and it knows how to turn those +/// dependencies into the final SQL. Placeholders `{arg:N}` / +/// `{fp:N}` / `{fg:N}` / `{sv:N}` are substituted with the rendered +/// SQL of the bound dependency, filter param, filter group or +/// security-context value when the call is evaluated (`eval` / +/// `eval_vec`). #[derive(Clone, Debug)] pub struct SqlCall { template: SqlTemplate, @@ -117,9 +149,9 @@ pub struct SqlCall { filter_params: Vec, filter_groups: Vec, security_context: SecutityContextProps, - /// Per `{arg:N}` index: whether the surrounding context in the template - /// would make a compound substitution unsafe (requiring parentheses). - /// Computed once at construction from the template. + // Per `{arg:N}` index: whether the surrounding context in the template + // would make a compound substitution unsafe (requiring parentheses). + // Computed once at construction from the template. arg_paren_contexts: HashMap, } @@ -154,6 +186,8 @@ impl SqlCall { } } + /// Renders the template into a single SQL string. Errors when + /// the template is a `StringVec` — use `eval_vec` for that case. pub fn eval( &self, visitor: &SqlEvaluatorVisitor, @@ -179,6 +213,11 @@ impl SqlCall { } } + /// Renders the template into one SQL string per element when it + /// is a `StringVec`, or a single-element vector for plain + /// `String` templates. `StringVec` is produced by pre-aggregation + /// dimension / measure reference lists, where the data-model + /// definition returns the SQL of all referenced members at once. pub fn eval_vec( &self, visitor: &SqlEvaluatorVisitor, @@ -215,6 +254,9 @@ impl SqlCall { Ok(result) } + /// True when the SQL belongs to a single cube — either it has no + /// dependencies at all (a constant expression), or it references + /// the owning cube directly through a `{CUBE}` placeholder. pub fn is_owned_by_cube(&self) -> bool { if self.deps.is_empty() { true @@ -223,6 +265,8 @@ impl SqlCall { } } + /// All `CubeRef::Name` dependencies — cube-name placeholders the + /// template refers to. pub fn cube_name_deps(&self) -> Vec> { self.deps .iter() @@ -415,11 +459,16 @@ impl SqlCall { .collect() } + /// True if the entire template is exactly `{arg:0}` with a single + /// member-symbol dependency — i.e. the SQL is a plain reference + /// to another member, with no wrapping expression. pub fn is_direct_reference(&self) -> bool { self.dependencies_count() == 1 && self.template == SqlTemplate::String(SqlCallArg::dependency(0)) } + /// The single member this call references when `is_direct_reference` + /// is true; `None` otherwise. pub fn resolve_direct_reference(&self) -> Option> { if self.is_direct_reference() { self.deps[0].as_symbol().cloned() @@ -428,10 +477,14 @@ impl SqlCall { } } + /// Number of member-symbol dependencies. Cube refs are not + /// counted. pub fn dependencies_count(&self) -> usize { self.deps.iter().filter(|d| d.is_symbol()).count() } + /// Member-symbol dependencies of the call. Cube refs are excluded + /// — use `get_cube_refs` for those. pub fn get_dependencies(&self) -> Vec> { self.deps .iter() @@ -455,6 +508,9 @@ impl SqlCall { } } + /// Returns a new `SqlCall` with `f` applied recursively to every + /// member-symbol dependency. Cube refs and other placeholders + /// pass through unchanged. pub fn apply_recursive) -> Result, CubeError>>( &self, f: &F, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/sql_call_builder.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/sql_call_builder.rs index 5617bad221a07..cc27a56af3fee 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/sql_call_builder.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/sql_call_builder.rs @@ -10,6 +10,10 @@ use crate::cube_bridge::security_context::SecurityContext; use cubenativeutils::CubeError; use std::rc::Rc; +/// Builds a `SqlCall` from a `MemberSql` declaration: compiles the +/// template, resolves each path placeholder to a `SqlDependency` +/// (member symbol or cube reference), and attaches filter params, +/// filter groups and security-context bindings. pub struct SqlCallBuilder<'a> { compiler: &'a mut Compiler, cube_evaluator: Rc, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/common/aggregation_type.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/common/aggregation_type.rs index 346c3a1fdec94..c482de005862f 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/common/aggregation_type.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/common/aggregation_type.rs @@ -1,5 +1,7 @@ use cubenativeutils::CubeError; +/// Aggregation function declared on a measure in the data model +/// (`sum`, `avg`, `min`, `max`, distinct counts, etc.). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum AggregationType { Sum, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/common/case.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/common/case.rs index 83a75ec9dac1d..a4730b839fb28 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/common/case.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/common/case.rs @@ -322,6 +322,14 @@ impl CaseSwitchDefinition { } } +/// Body of a case-defined member, mapped from the `case` field of +/// the data-model definition. +/// +/// - `Case` — classic `CASE WHEN condition THEN label ELSE label END`. +/// - `CaseSwitch` — switch-style `CASE switch WHEN value THEN sql +/// ELSE sql END`. `switch` may resolve to a direct reference to +/// another dimension (typically a `type: switch` dimension whose +/// `values` drive the branches). #[derive(Clone)] pub enum Case { Case(CaseDefinition), diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/common/dimension_type.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/common/dimension_type.rs index a0a4c435e16e3..a603c80e937db 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/common/dimension_type.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/common/dimension_type.rs @@ -1,5 +1,6 @@ use cubenativeutils::CubeError; +/// Value type of a dimension as declared in the data model. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum DimensionType { String, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/common/symbol_path.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/common/symbol_path.rs index 29f28fb0d56bc..be1e164a1d9b2 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/common/symbol_path.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/common/symbol_path.rs @@ -14,6 +14,10 @@ pub enum SymbolPathType { CubeTable, } +/// Resolved path of a member or cube reference in the data model: +/// parsed from a dotted string (`cube.member`, `cube.cube2.dim`, +/// `cube.time_dim.day`, `CUBE`, `CUBE.__sql_fn`), with its +/// `SymbolPathType` and the chain of cubes traversed along the way. #[derive(Debug, Clone)] pub struct SymbolPath { path_type: SymbolPathType, @@ -47,11 +51,16 @@ impl SymbolPath { } } + /// Parses a dotted path string by walking the data model through + /// the provided `CubeEvaluator`. pub fn parse(cube_evaluator: Rc, path: &str) -> Result { let parts: Vec = path.split('.').map(|s| s.to_string()).collect(); Self::parse_parts(cube_evaluator, None, &parts) } + /// Parses pre-split path segments resolved relative to + /// `current_cube` — so the first segment may refer to a member + /// of `current_cube` directly. pub fn parse_parts( cube_evaluator: Rc, current_cube: Option<&str>, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/cube_symbol.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/cube_symbol.rs index 33cb7c129c236..a47a0926582aa 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/cube_symbol.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/cube_symbol.rs @@ -11,6 +11,8 @@ use lazy_static::lazy_static; use regex::Regex; use std::rc::Rc; +/// Symbol for a cube referenced as an identifier (`{CUBE}` / +/// `{TABLE}`); renders to the cube's quoted name or alias. #[derive(Debug)] pub struct CubeNameSymbol { cube_name: String, @@ -70,6 +72,9 @@ impl CubeNameSymbolFactory { } } +/// Symbol for a cube referenced as a table expression +/// (`{CUBE.sql()}`); renders by evaluating the cube's `sql:` +/// function or its raw `sql_table:` value. #[derive(Debug)] pub struct CubeTableSymbol { cube_name: String, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_kinds/case_dimension.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_kinds/case_dimension.rs index 9be1340ff60bc..262d2421e64e3 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_kinds/case_dimension.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_kinds/case_dimension.rs @@ -4,6 +4,9 @@ use crate::planner::{CubeRef, SqlCall}; use cubenativeutils::CubeError; use std::rc::Rc; +/// Dimension whose value is defined via the `case` field of the +/// data-model definition. Comes in classic and switch-style forms; +/// see `Case`. #[derive(Clone)] pub struct CaseDimension { dimension_type: DimensionType, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_kinds/geo.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_kinds/geo.rs index 2b1ddad336b68..07ead104bf554 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_kinds/geo.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_kinds/geo.rs @@ -3,6 +3,8 @@ use crate::planner::{CubeRef, SqlCall}; use cubenativeutils::CubeError; use std::rc::Rc; +/// `type: geo` dimension from the data model: a geographic dimension +/// defined by `latitude` and `longitude` SQL expressions. #[derive(Clone)] pub struct GeoDimension { latitude: Rc, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_kinds/mod.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_kinds/mod.rs index 0cab09d97aa03..defaaf27aab85 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_kinds/mod.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_kinds/mod.rs @@ -14,6 +14,16 @@ use crate::planner::{CubeRef, SqlCall}; use cubenativeutils::CubeError; use std::rc::Rc; +/// Form of a dimension's value, classified from its data-model +/// definition. +/// +/// - `Regular` — plain dimension (a single `sql` expression). +/// - `Geo` — `type: geo` dimension with `latitude` / `longitude`. +/// - `Switch` — `type: switch` dimension; without a `sql` it becomes +/// a **calc group** — an abstract enumeration cross-joined into the +/// query rather than read from a column. +/// - `Case` — dimension defined via a `case` body, classic or +/// switch-style. #[derive(Clone)] pub enum DimensionKind { Regular(RegularDimension), @@ -91,6 +101,9 @@ impl DimensionKind { matches!(self, Self::Case(_)) } + /// True for a `Switch` dimension declared without a `sql` — a + /// calc group: an abstract enumeration cross-joined into the query + /// rather than read from a column. pub fn is_calc_group(&self) -> bool { match self { Self::Switch(s) => s.is_calc_group(), diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_kinds/regular.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_kinds/regular.rs index a1fcf9d056e95..a75e0c44fe477 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_kinds/regular.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_kinds/regular.rs @@ -4,6 +4,8 @@ use crate::planner::{CubeRef, SqlCall}; use cubenativeutils::CubeError; use std::rc::Rc; +/// Plain dimension from the data model — a single `sql` expression +/// typed by `DimensionType`. #[derive(Clone)] pub struct RegularDimension { dimension_type: DimensionType, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_kinds/switch.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_kinds/switch.rs index 6560365ce6877..be378c9cb57ec 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_kinds/switch.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_kinds/switch.rs @@ -3,6 +3,11 @@ use crate::planner::{CubeRef, SqlCall}; use cubenativeutils::CubeError; use std::rc::Rc; +/// `type: switch` dimension from the data model: an enum with a +/// fixed list of allowed string values. With a `sql` — an ordinary +/// enum dimension reading from a real column. Without a `sql` — a +/// **calc group**: an abstract enumeration cross-joined into the +/// query as a virtual table of values. #[derive(Clone)] pub struct SwitchDimension { values: Vec, @@ -22,6 +27,9 @@ impl SwitchDimension { self.member_sql.as_ref() } + /// True when the switch dimension was declared without a `sql` — + /// a calc group: an abstract enumeration cross-joined into the + /// query rather than read from a column. pub fn is_calc_group(&self) -> bool { self.member_sql.is_none() } diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_symbol.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_symbol.rs index f715a086ce876..63901340e23f9 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_symbol.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/dimension_symbol.rs @@ -16,6 +16,9 @@ use crate::planner::{Compiler, CubeRef, SqlCall}; use cubenativeutils::CubeError; use std::rc::Rc; +/// Time-shift entry on a dimension of a calendar cube: shifts the +/// date range by either a fixed interval, a named slot (e.g. +/// `prev_year`), or a custom SQL expression. #[derive(Clone)] pub struct CalendarDimensionTimeShift { pub interval: Option, @@ -23,6 +26,9 @@ pub struct CalendarDimensionTimeShift { pub sql: Option>, } +/// `MemberSymbol::Dimension` body: Tesseract representation of a +/// `dimension` declared in the data model — a value the query can +/// group, filter or order by, but never aggregate. #[derive(Clone)] pub struct DimensionSymbol { compiled_path: CompiledMemberPath, @@ -74,6 +80,8 @@ impl DimensionSymbol { self.kind.is_calc_group() } + /// String values declared on a `Switch` dimension; empty for any + /// other kind. pub fn values(&self) -> &[String] { match &self.kind { DimensionKind::Switch(s) => s.values(), @@ -93,6 +101,7 @@ impl DimensionSymbol { Rc::new(new) } + /// Case-expression body for `DimensionKind::Case`; `None` otherwise. pub fn case(&self) -> Option<&Case> { match &self.kind { DimensionKind::Case(c) => Some(c.case()), @@ -100,6 +109,7 @@ impl DimensionSymbol { } } + /// `None` if the dimension has no primary SQL expression of its own. pub fn member_sql(&self) -> Option<&Rc> { match &self.kind { DimensionKind::Regular(r) => Some(r.member_sql()), @@ -121,18 +131,29 @@ impl DimensionSymbol { &self.compiled_path } + /// Trims the join-chain prefix from `compiled_path` in place so the + /// path points only at the owning cube. pub fn strip_join_prefix(&mut self) { self.compiled_path = self.compiled_path.strip_join_prefix(); } + /// Full unique identifier of the symbol: cube path, member name and + /// any suffix that distinguishes one symbol from another. pub fn full_name(&self) -> String { self.compiled_path.full_name().clone() } + /// Default alias of the dimension, derived from the compiled member + /// path. pub fn alias(&self) -> String { self.compiled_path.alias().clone() } + /// True when the cube on the symbol's path actually owns this + /// dimension — the cube is required in the join to read the + /// dimension from the database. False for view-exposed dimensions, + /// multi-stage dimensions, switches, and members defined as pure + /// compositions of other members (no `{CUBE}` references). pub fn owned_by_cube(&self) -> bool { !self.is_multi_stage && !self.kind.is_switch() && self.kind.is_owned_by_cube() } @@ -141,10 +162,14 @@ impl DimensionSymbol { self.is_multi_stage } + /// Direct mapping from the `sub_query` field of the dimension + /// definition in the data model. pub fn is_sub_query(&self) -> bool { self.is_sub_query } + /// Optional SQL expression that wraps the dimension's rendered + /// output to mask its value (data hiding / column-level masking). pub fn mask_sql(&self) -> &Option> { &self.mask_sql } @@ -177,6 +202,8 @@ impl DimensionSymbol { self.kind.is_case() } + /// Direct mapping from the `propagate_filters_to_sub_query` field + /// of the dimension definition in the data model. pub fn propagate_filters_to_sub_query(&self) -> bool { self.propagate_filters_to_sub_query } @@ -189,6 +216,8 @@ impl DimensionSymbol { self.is_view } + /// The member this dimension references, or `None` if it is not a + /// reference. pub fn reference_member(&self) -> Option> { if !self.is_reference() { return None; @@ -212,16 +241,16 @@ impl DimensionSymbol { Ok(MemberSymbol::new_dimension(Rc::new(result))) } + /// SQL calls inside the kind body. `mask_sql` is intentionally + /// excluded: it is compiled against the cube that owns the + /// dimension, which differs from the symbol's own `cube_name` when + /// the dimension is exposed through a view. Including it in + /// cube-ref validation would produce false foreign-cube errors. pub fn iter_sql_calls(&self) -> Box> + '_> { - // mask_sql is intentionally excluded here: it's compiled in the - // context of the cube that owns the dimension (via aliasMember when - // the dimension is exposed through a view), which may legitimately - // differ from the current cube_name of the symbol. Including it in - // the generic validate_regular_member_cube_refs would produce false - // foreign-cube errors for view members. self.kind.iter_sql_calls() } + /// All member dependencies of the dimension. pub fn get_dependencies(&self) -> Vec> { let mut deps = self.kind.get_dependencies(); if let Some(mask) = &self.mask_sql { @@ -230,6 +259,7 @@ impl DimensionSymbol { deps } + /// All cube references of the dimension. pub fn get_cube_refs(&self) -> Vec { let mut refs = self.kind.get_cube_refs(); if let Some(mask) = &self.mask_sql { @@ -254,6 +284,9 @@ impl DimensionSymbol { self.compiled_path.path() } + /// Finds the calendar time-shift defined for the exact `interval` + /// and returns it together with the primary-key full name. `None` + /// when either the matching shift or the primary key is missing. pub fn calendar_time_shift_for_interval( &self, interval: &SqlInterval, @@ -272,6 +305,10 @@ impl DimensionSymbol { None } + /// Finds the named calendar time-shift and returns it together + /// with the primary-key full name. Falls back to this dimension's + /// own full name when the dimension is itself the calendar primary + /// key. pub fn calendar_time_shift_for_named_interval( &self, interval_name: &String, @@ -293,6 +330,9 @@ impl DimensionSymbol { } } +/// Builds a `DimensionSymbol` from a dimension definition pulled out +/// of the cube schema. When the requested path includes a granularity, +/// the result is wrapped in a `TimeDimensionSymbol` instead. pub struct DimensionSymbolFactory { path: SymbolPath, sql: Option>, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/measure_kinds/aggregated.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/measure_kinds/aggregated.rs index d2cda76e7a09e..732e679a93f97 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/measure_kinds/aggregated.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/measure_kinds/aggregated.rs @@ -4,6 +4,8 @@ use crate::planner::{CubeRef, SqlCall}; use cubenativeutils::CubeError; use std::rc::Rc; +/// `Aggregated` measure kind. `sql` may be absent when the measure +/// is built from a `case` body. #[derive(Clone)] pub struct AggregatedMeasure { agg_type: AggregationType, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/measure_kinds/calculated.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/measure_kinds/calculated.rs index bf7d74ef95c22..97992769070d0 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/measure_kinds/calculated.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/measure_kinds/calculated.rs @@ -3,6 +3,8 @@ use crate::planner::{CubeRef, SqlCall}; use cubenativeutils::CubeError; use std::rc::Rc; +/// Value type of a calculated (non-aggregating) measure as declared +/// in the data model. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum CalculatedMeasureType { Number, @@ -32,6 +34,8 @@ impl CalculatedMeasureType { } } +/// `Calculated` measure kind: a non-aggregating expression. `sql` +/// may be absent when the value is provided through a `case` body. #[derive(Clone)] pub struct CalculatedMeasure { calc_type: CalculatedMeasureType, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/measure_kinds/count.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/measure_kinds/count.rs index 83ad06e447717..1a7706b89e425 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/measure_kinds/count.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/measure_kinds/count.rs @@ -3,12 +3,20 @@ use crate::planner::{CubeRef, SqlCall}; use cubenativeutils::CubeError; use std::rc::Rc; +/// Source of a `Count` measure's SQL. +/// +/// - `Auto` — no explicit `sql` was declared; the count falls back +/// to the cube's primary-key expressions. +/// - `Explicit` — `sql` was declared on the measure. #[derive(Clone)] pub enum CountSql { Auto(Vec>), Explicit(Rc), } +/// `Count` measure kind: counts rows of the underlying source. +/// Without an explicit `sql` falls back to counting the cube's +/// primary-key tuples. #[derive(Clone)] pub struct CountMeasure { sql: CountSql, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/measure_kinds/mod.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/measure_kinds/mod.rs index 17be5c1f2e305..d2529a4895f57 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/measure_kinds/mod.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/measure_kinds/mod.rs @@ -12,6 +12,9 @@ use crate::planner::{CubeRef, SqlCall}; use cubenativeutils::CubeError; use std::rc::Rc; +/// How a measure kind wraps its inner SQL when rendered: no wrapper +/// at all, a named SQL aggregate function, or one of the distinct- +/// count special forms. pub enum AggregateWrap<'a> { PassThrough, Function(&'a str), @@ -19,6 +22,17 @@ pub enum AggregateWrap<'a> { CountDistinctApprox, } +/// Form of a measure's aggregation, classified from the data-model +/// `type`. +/// +/// - `Count` — `type: count`. Counts rows; falls back to the cube's +/// primary keys when no explicit `sql` is given. +/// - `Aggregated` — built-in aggregation (`sum`, `avg`, `min`, `max`, +/// `count_distinct`, `count_distinct_approx`, `number_agg`, +/// `running_total`). +/// - `Calculated` — `type: number / string / time / boolean`. A +/// plain expression with no aggregation wrapper. +/// - `Rank` — `type: rank`. Window-function rank, no `sql`. #[derive(Clone)] pub enum MeasureKind { Count(CountMeasure), @@ -112,6 +126,9 @@ impl MeasureKind { matches!(self, Self::Calculated(_)) } + /// True if the kind's aggregation distributes over row union. + /// Counts are always additive; aggregated measures delegate to + /// their `AggregationType`. Calculated and rank are not additive. pub fn is_additive(&self) -> bool { match self { Self::Count(_) => true, @@ -129,6 +146,11 @@ impl MeasureKind { } } + /// True when `new_type` is a compatible aggregation replacement. + /// Only `Aggregated` measures can have their type replaced, and + /// only within compatibility groups: `sum`/`avg`/`min`/`max` are + /// interchangeable among themselves, distinct counts among + /// themselves. pub fn can_replace_type_with(&self, new_type: &str) -> bool { match self { Self::Aggregated(a) => { @@ -151,6 +173,9 @@ impl MeasureKind { } } + /// True if extra `measure_filters` can be merged into the kind. + /// Counts and the basic aggregations support it; `number_agg`, + /// `running_total`, calculated and rank measures do not. pub fn supports_additional_filters(&self) -> bool { match self { Self::Count(_) => true, @@ -179,6 +204,10 @@ impl MeasureKind { } } + /// How the kind wraps its inner SQL when rendered as a top-level + /// query measure. `is_multiplied` is true when the join below the + /// measure can produce duplicate rows — non-distinct counts then + /// switch to `count_distinct` over primary keys to stay correct. pub fn aggregate_wrap(&self, is_multiplied: bool) -> AggregateWrap<'_> { match self { Self::Calculated(_) => AggregateWrap::PassThrough, @@ -200,6 +229,10 @@ impl MeasureKind { } } + /// How the kind wraps its inner SQL when rolled up from a + /// pre-aggregation. Counts and most aggregations roll up via + /// `sum`; `min` / `max` preserve themselves; calculated string / + /// time / boolean values roll up via `max`. pub fn pre_aggregate_wrap(&self) -> AggregateWrap<'_> { match self { Self::Count(_) => AggregateWrap::Function("sum"), diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/measure_symbol.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/measure_symbol.rs index 20bb039c75f69..ac192d73dc3dd 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/measure_symbol.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/measure_symbol.rs @@ -15,6 +15,7 @@ use std::cmp::{Eq, PartialEq}; use std::collections::HashMap; use std::rc::Rc; +/// Per-measure `order_by` entry from the data-model definition: a #[derive(Clone)] pub struct MeasureOrderBy { sql_call: Rc, @@ -42,6 +43,9 @@ impl MeasureOrderBy { } } +/// Time-shift entry attached to a specific time dimension. Shifts +/// that dimension's date range by either a fixed interval or a named +/// slot. #[derive(Clone, Debug)] pub struct DimensionTimeShift { pub interval: Option, @@ -59,6 +63,13 @@ impl PartialEq for DimensionTimeShift { impl Eq for DimensionTimeShift {} +/// Form of a measure's `time_shift` declaration. +/// +/// - `Dimensions` — one or more shifts, each bound to a specific time +/// dimension. +/// - `Common` — a single interval applied to every time dimension in +/// the query. +/// - `Named` — a single named slot applied to every time dimension. #[derive(Clone, Debug)] pub enum MeasureTimeShifts { Dimensions(Vec), @@ -66,6 +77,9 @@ pub enum MeasureTimeShifts { Named(String), } +/// `MemberSymbol::Measure` body: Tesseract representation of a +/// `measure` declared in the data model — an aggregation, count or +/// calculated value the query exposes. #[derive(Clone)] pub struct MeasureSymbol { compiled_path: CompiledMemberPath, @@ -82,7 +96,6 @@ pub struct MeasureSymbol { reduce_by: Option>>, add_group_by: Option>>, group_by: Option>>, - is_splitted_source: bool, mask_sql: Option>, } @@ -116,7 +129,6 @@ impl MeasureSymbol { measure_order_by, is_multi_stage, time_shift, - is_splitted_source: false, reduce_by, add_group_by, group_by, @@ -124,6 +136,12 @@ impl MeasureSymbol { }) } + /// Returns a non-rolling copy of the symbol. A rolling-window + /// measure carries both the windowing context and the SQL of the + /// inner value it operates on; unrolling drops the window and + /// yields that inner value. Multi-stage rolling measures collapse + /// to a `Calculated` kind so they can be rendered without window- + /// function machinery. pub fn new_unrolling(&self) -> Rc { if self.is_rolling_window() { let kind = if self.is_multi_stage { @@ -155,7 +173,6 @@ impl MeasureSymbol { reduce_by: self.reduce_by.clone(), add_group_by: self.add_group_by.clone(), group_by: self.group_by.clone(), - is_splitted_source: self.is_splitted_source, mask_sql: self.mask_sql.clone(), }) } else { @@ -163,6 +180,9 @@ impl MeasureSymbol { } } + /// Returns a copy of the symbol with the measure type optionally + /// replaced (subject to per-kind compatibility checks) and + /// additional measure filters merged in. pub fn new_patched( &self, new_measure_type: Option, @@ -208,7 +228,6 @@ impl MeasureSymbol { reduce_by: self.reduce_by.clone(), add_group_by: self.add_group_by.clone(), group_by: self.group_by.clone(), - is_splitted_source: self.is_splitted_source, mask_sql: self.mask_sql.clone(), })) } @@ -223,22 +242,24 @@ impl MeasureSymbol { &self.compiled_path } + /// Trims the join-chain prefix from `compiled_path` in place so + /// the path points only at the owning cube. pub fn strip_join_prefix(&mut self) { self.compiled_path = self.compiled_path.strip_join_prefix(); } + /// Full unique identifier of the symbol: cube path, member name + /// and any suffix that distinguishes one symbol from another. pub fn full_name(&self) -> String { self.compiled_path.full_name().clone() } + /// Default alias of the measure, derived from the compiled member + /// path. pub fn alias(&self) -> String { self.compiled_path.alias().clone() } - pub fn is_splitted_source(&self) -> bool { - self.is_splitted_source - } - pub fn time_shift(&self) -> &Option { &self.time_shift } @@ -251,11 +272,16 @@ impl MeasureSymbol { self.case.as_ref() } + /// Optional SQL expression that wraps the measure's rendered + /// output to mask its value (data hiding / column-level masking). pub fn mask_sql(&self) -> &Option> { &self.mask_sql } - pub fn is_addictive(&self) -> bool { + /// True when the measure's aggregation distributes over row union + /// (sum-like). Multi-stage measures are never additive — their + /// value depends on the windowed stage, not on a plain sum. + pub fn is_additive(&self) -> bool { if self.is_multi_stage() { false } else { @@ -293,15 +319,14 @@ impl MeasureSymbol { Ok(MemberSymbol::new_measure(Rc::new(result))) } + /// SQL calls inside the measure's kind and `case` body. + /// `mask_sql` is intentionally excluded: it is compiled against + /// the cube that owns the measure, which differs from the symbol's + /// own `cube_name` when the measure is exposed through a view. + /// `measure_filters` and `measure_order_by` are also skipped here + /// — the legacy BaseQuery validator does not check them, and we + /// preserve that behaviour for compatibility. pub fn iter_sql_calls(&self) -> Box> + '_> { - //FIXME We don't include filters and order_by here for backward compatibility - // because BaseQuery doesn't validate these SQL calls - // mask_sql is intentionally excluded here: it's compiled in the - // context of the cube that owns the measure (via aliasMember when - // the measure is exposed through a view), which may legitimately - // differ from the current cube_name of the symbol. Including it in - // the generic validate_regular_member_cube_refs would produce false - // foreign-cube errors for view members. let result = self .kind .iter_sql_calls() @@ -349,7 +374,11 @@ impl MeasureSymbol { refs } - pub fn can_used_as_addictive_in_multplied(&self) -> bool { + /// True if the measure stays correct when joined into a context + /// that multiplies rows (one-to-many join). Distinct aggregations + /// are naturally immune; primary-key-based counts (`type: count` + /// without an explicit `sql`) are too. + pub fn is_additive_in_multiplied(&self) -> bool { match &self.kind { MeasureKind::Aggregated(agg) => agg.agg_type().is_distinct(), MeasureKind::Count(count) => count.is_owned_by_cube(), @@ -357,6 +386,10 @@ impl MeasureSymbol { } } + /// True when the cube on the symbol's path is required in the + /// join to read the measure from the database. Multi-stage + /// measures are never owned by a cube; otherwise ownership is the + /// union of the kind, the measure filters and the `case` body. pub fn owned_by_cube(&self) -> bool { if self.is_multi_stage { return false; @@ -382,6 +415,8 @@ impl MeasureSymbol { self.is_view } + /// The member this measure references, or `None` if it is not a + /// reference. pub fn reference_member(&self) -> Option> { if !self.is_reference() { return None; @@ -413,6 +448,8 @@ impl MeasureSymbol { matches!(&self.kind, MeasureKind::Aggregated(a) if a.agg_type() == AggregationType::RunningTotal) } + /// True for rolling-window measures and running-total + /// aggregations. pub fn is_cumulative(&self) -> bool { self.is_rolling_window() || self.is_running_total() } @@ -462,6 +499,8 @@ impl MeasureSymbol { } } +/// Builds a `MeasureSymbol` from a measure definition pulled out of +/// the cube schema. pub struct MeasureSymbolFactory { path: SymbolPath, sql: Option>, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/member_expression_symbol.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/member_expression_symbol.rs index 18f93065e243f..1f42462541f6d 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/member_expression_symbol.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/member_expression_symbol.rs @@ -8,12 +8,22 @@ use cubenativeutils::CubeError; use itertools::Itertools; use std::rc::Rc; +/// Body of a member expression. +/// +/// - `SqlCall` — an arbitrary SQL expression provided directly by the +/// query input. +/// - `PatchedSymbol` — an existing member with query-time +/// modifications applied on top. #[derive(Clone)] pub enum MemberExpressionExpression { SqlCall(Rc), PatchedSymbol(Rc), } +/// `MemberSymbol::MemberExpression` body: a synthetic member built +/// at query time from a SQL expression or from another member with +/// query-time modifications. Not declared in the data model. Its +/// full name lives in the `expr:` namespace. #[derive(Clone)] pub struct MemberExpressionSymbol { compiled_path: CompiledMemberPath, @@ -49,6 +59,8 @@ impl MemberExpressionSymbol { })) } + /// Returns a copy of the symbol marked as parenthesized when + /// rendered. pub fn with_parenthesized(self: &Rc) -> Rc { let mut result = self.as_ref().clone(); result.parenthesized = true; @@ -67,14 +79,20 @@ impl MemberExpressionSymbol { &self.compiled_path } + /// Trims the join-chain prefix from `compiled_path` in place so + /// the path points only at the owning cube. pub fn strip_join_prefix(&mut self) { self.compiled_path = self.compiled_path.strip_join_prefix(); } + /// Full unique identifier of the symbol; lives in the `expr:` + /// namespace to keep it disjoint from data-model member names. pub fn full_name(&self) -> String { self.compiled_path.full_name().clone() } + /// Default alias of the expression, derived from the compiled + /// member path. pub fn alias(&self) -> String { self.compiled_path.alias().clone() } @@ -83,6 +101,9 @@ impl MemberExpressionSymbol { self.is_reference } + /// The member this expression references, or `None` if it is not + /// a reference. An expression is a reference only when its body + /// is a `SqlCall` that is itself a direct member reference. pub fn reference_member(&self) -> Option> { if !self.is_reference() { return None; @@ -133,6 +154,10 @@ impl MemberExpressionSymbol { refs } + /// If every leaf member referenced by the expression is a + /// dimension, returns the list of cube names those dimensions + /// belong to. Returns `None` if any leaf is a measure or other + /// non-dimension member. pub fn cube_names_if_dimension_only_expression( self: Rc, ) -> Result>, CubeError> { diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/member_symbol.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/member_symbol.rs index 0fe37af8902d1..7e043285de13c 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/member_symbol.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/member_symbol.rs @@ -1,7 +1,6 @@ use cubenativeutils::CubeError; use itertools::Itertools; -use crate::planner::collectors::has_multi_stage_members; use crate::planner::{Case, CubeRef, SqlCall}; use super::common::CompiledMemberPath; @@ -9,6 +8,21 @@ use super::{DimensionSymbol, MeasureSymbol, MemberExpressionSymbol, TimeDimensio use std::fmt::Debug; use std::rc::Rc; +/// First-class business object of the planner: the atomic unit of +/// query planning, identifying one thing the query can select, filter, +/// group or order by. The same `MemberSymbol` value carries through +/// every layer — logical planning, physical-plan construction and SQL +/// rendering. +/// +/// A symbol is either bound to the data model — `Dimension` / `Measure` +/// declared on a cube — or derived at query time: `TimeDimension` +/// (a dimension at a chosen granularity and date range) or +/// `MemberExpression` (synthetic, built from a SQL expression or a +/// patched symbol). Identity is `full_name` + variant. +/// +/// Indivisible: renders as a single SQL expression. A symbol may depend +/// on other symbols (`get_dependencies`); whether those deps are +/// inlined or pushed into a CTE / subquery is a physical-plan decision. pub enum MemberSymbol { Dimension(Rc), TimeDimension(Rc), @@ -66,10 +80,14 @@ impl MemberSymbol { } } + /// Full unique identifier of the symbol: cube path, member name + /// and any suffix that distinguishes one symbol from another. pub fn full_name(&self) -> String { self.compiled_path().full_name().clone() } + /// Optional SQL expression that wraps the rendered member output to + /// mask its value (data hiding / column-level masking). pub fn mask_sql(&self) -> Option<&Rc> { match self { Self::Dimension(d) => d.mask_sql().as_ref(), @@ -95,6 +113,7 @@ impl MemberSymbol { self.compiled_path().path() } + /// Join-path metadata proxied from the owning cube definition. pub fn join_map(&self) -> &Option>> { self.compiled_path().join_map() } @@ -108,6 +127,7 @@ impl MemberSymbol { } } + /// Case-expression body, if the member is declared via `case:`. pub fn case(&self) -> Option<&Case> { match self { MemberSymbol::Dimension(dimension_symbol) => dimension_symbol.case(), @@ -123,10 +143,13 @@ impl MemberSymbol { matches!(self, Self::Measure(_)) } + /// True for both `Dimension` and `TimeDimension`. pub fn is_dimension(&self) -> bool { matches!(self, Self::Dimension(_) | Self::TimeDimension(_)) } + /// Applies `f` to this symbol, then recurses into the dependencies of + /// the result returned by `f` — not of the original symbol. pub fn apply_recursive) -> Result, CubeError>>( self: &Rc, f: &F, @@ -165,6 +188,8 @@ impl MemberSymbol { } } + /// True if the symbol is a transparent alias for another member, with + /// no calculation of its own. pub fn is_reference(&self) -> bool { match self { Self::Dimension(d) => d.is_reference(), @@ -174,6 +199,7 @@ impl MemberSymbol { } } + /// The member this one references, or `None` if it is not a reference. pub fn reference_member(&self) -> Option> { match self { Self::Dimension(d) => d.reference_member(), @@ -183,6 +209,8 @@ impl MemberSymbol { } } + /// Follows `reference_member` repeatedly and returns the first symbol + /// in the chain that is not itself a reference. pub fn resolve_reference_chain(self: Rc) -> Rc { let mut current = self; while let Some(reference) = current.reference_member() { @@ -191,6 +219,8 @@ impl MemberSymbol { current } + /// True if `member` is this symbol or any symbol reachable via + /// `reference_member`. Self is included. pub fn has_member_in_reference_chain(&self, member: &Rc) -> bool { if self.full_name() == member.full_name() { return true; @@ -233,6 +263,8 @@ impl MemberSymbol { } } + /// `MemberExpression` symbols are never owned by a cube; for the other + /// variants, the answer comes from the underlying member definition. pub fn owned_by_cube(&self) -> bool { match self { Self::Dimension(d) => d.owned_by_cube(), @@ -282,6 +314,8 @@ impl MemberSymbol { } } + /// Granularity suffix appended to the alias of `TimeDimension`; `None` + /// for all other variants. pub fn alias_suffix(&self) -> Option { match self { Self::TimeDimension(d) => Some(d.alias_suffix()), @@ -289,21 +323,13 @@ impl MemberSymbol { } } - pub fn is_basic_dimension(self: &Rc) -> Result { - if self.as_dimension().is_ok() { - has_multi_stage_members(self, true) - } else { - Ok(false) - } - } - - pub fn is_leaf(&self) -> bool { - self.get_dependencies().is_empty() - } - + /// Checks the SQL-call dependencies: regular members may only + /// reference their own cube; multi-stage members may only reference + /// other members, and must reference at least one. pub fn validate(&self) -> Result<(), CubeError> { self.validate_cube_refs() } + fn validate_cube_refs(&self) -> Result<(), CubeError> { let sql_calls = match self { Self::Dimension(dim) => dim.iter_sql_calls(), diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/symbol_factory.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/symbol_factory.rs index b9cee3860e6c4..a431c0099b727 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/symbol_factory.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/symbol_factory.rs @@ -3,6 +3,9 @@ use crate::planner::Compiler; use cubenativeutils::CubeError; use std::rc::Rc; +/// Builds a `MemberSymbol` from a `SymbolPath`. Implementations +/// hold the data-model definition; `build` runs through the +/// `Compiler` to resolve any dependencies. pub trait SymbolFactory: Sized { fn build(self, compiler: &mut Compiler) -> Result, CubeError>; } diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/time_dimension_symbol.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/time_dimension_symbol.rs index de83d5875ad9c..da568383668dd 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/time_dimension_symbol.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/planner/symbols/time_dimension_symbol.rs @@ -9,6 +9,10 @@ use chrono_tz::Tz; use cubenativeutils::CubeError; use std::rc::Rc; +/// `MemberSymbol::TimeDimension` body: a base time dimension viewed +/// at a chosen granularity and (optionally) restricted to a date +/// range. Not declared in the data model — created at query time on +/// top of an existing time dimension. #[derive(Clone)] pub struct TimeDimensionSymbol { base_symbol: Rc, @@ -75,6 +79,8 @@ impl TimeDimensionSymbol { Ok(res) } + /// Returns a copy of the symbol with the granularity replaced; + /// the base symbol and date range are preserved. pub fn change_granularity( &self, query_tools: Rc, @@ -104,14 +110,21 @@ impl TimeDimensionSymbol { &self.compiled_path } + /// Trims the join-chain prefix from `compiled_path` in place so + /// the path points only at the owning cube. pub fn strip_join_prefix(&mut self) { self.compiled_path = self.compiled_path.strip_join_prefix(); } + /// Full unique identifier of the symbol: cube path, base + /// dimension name and the granularity suffix. pub fn full_name(&self) -> String { self.compiled_path.full_name().clone() } + /// Granularity name appended to the base symbol's alias and full + /// name (e.g. `day`, `month`). Defaults to `day` when no + /// granularity is set. pub fn alias_suffix(&self) -> String { self.alias_suffix.clone() } @@ -128,6 +141,9 @@ impl TimeDimensionSymbol { self.date_range.clone().map(|(from, to)| vec![from, to]) } + /// Like `get_dependencies`, but wraps any time-dimension dep in + /// a `TimeDimensionSymbol` carrying this symbol's granularity and + /// date range. Non-time-dimension deps pass through unchanged. pub fn get_dependencies_as_time_dimensions(&self) -> Vec> { self.get_dependencies() .into_iter() @@ -201,6 +217,9 @@ impl TimeDimensionSymbol { self.base_symbol.is_multi_stage() } + /// False if the granularity defines its own calendar SQL — the + /// symbol then materially computes a value rather than forwarding + /// to its base. Otherwise delegates to the base symbol. pub fn is_reference(&self) -> bool { if let Some(granularity_obj) = &self.granularity_obj { if granularity_obj.calendar_sql().is_some() { @@ -211,6 +230,9 @@ impl TimeDimensionSymbol { self.base_symbol.is_reference() } + /// The base member this time dimension references, re-wrapped in + /// a `TimeDimensionSymbol` with the same granularity and date + /// range. `None` if the base is not a reference. pub fn reference_member(&self) -> Option> { if let Some(base_symbol) = self.base_symbol.clone().reference_member() { let new_time_dim = Self::new( @@ -229,6 +251,8 @@ impl TimeDimensionSymbol { self.compiled_path.name().clone() } + /// Minimum granularity implied by the `date_range` bounds — the + /// smallest unit that still fully contains both endpoints. pub fn date_range_granularity( &self, query_tools: Rc, @@ -248,6 +272,10 @@ impl TimeDimensionSymbol { } } + /// Granularity to roll up to when matching this time dimension + /// against a pre-aggregation. For predefined granularities or + /// custom granularities not aligned with the requested date range, + /// returns the minimum granularity needed to cover both. pub fn rollup_granularity( &self, query_tools: Rc, @@ -289,6 +317,10 @@ impl TimeDimensionSymbol { } } + /// Adjusts `date_range` so the start aligns with the + /// granularity's origin when the granularity is custom + /// (non-predefined). Predefined granularities and missing ranges + /// pass through unchanged. pub fn get_range_for_time_series( &self, date_range: Option>, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/measure_symbol.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/measure_symbol.rs index bd3cc8d67ac29..2be37ffc2ab65 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/measure_symbol.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/tests/measure_symbol.rs @@ -28,7 +28,7 @@ fn measure_count_properties() { assert!(!measure.is_running_total()); assert!(!measure.is_rolling_window()); assert!(!measure.is_cumulative()); - assert!(measure.is_addictive()); + assert!(measure.is_additive()); } #[test] @@ -45,7 +45,7 @@ fn measure_sum_properties() { assert!(!measure.is_running_total()); assert!(!measure.is_rolling_window()); assert!(!measure.is_cumulative()); - assert!(measure.is_addictive()); + assert!(measure.is_additive()); } #[test] @@ -61,7 +61,7 @@ fn measure_avg_properties() { assert!(!measure.is_calculated()); assert!(!measure.is_running_total()); assert!(!measure.is_cumulative()); - assert!(!measure.is_addictive()); + assert!(!measure.is_additive()); } #[test] @@ -77,7 +77,7 @@ fn measure_min_properties() { assert!(!measure.is_calculated()); assert!(!measure.is_running_total()); assert!(!measure.is_cumulative()); - assert!(measure.is_addictive()); + assert!(measure.is_additive()); } #[test] @@ -93,7 +93,7 @@ fn measure_max_properties() { assert!(!measure.is_calculated()); assert!(!measure.is_running_total()); assert!(!measure.is_cumulative()); - assert!(measure.is_addictive()); + assert!(measure.is_additive()); } #[test] @@ -109,7 +109,7 @@ fn measure_count_distinct_properties() { assert!(!measure.is_calculated()); assert!(!measure.is_running_total()); assert!(!measure.is_cumulative()); - assert!(!measure.is_addictive()); + assert!(!measure.is_additive()); } #[test] @@ -125,7 +125,7 @@ fn measure_count_distinct_approx_properties() { assert!(!measure.is_calculated()); assert!(!measure.is_running_total()); assert!(!measure.is_cumulative()); - assert!(measure.is_addictive()); + assert!(measure.is_additive()); } #[test] @@ -142,7 +142,7 @@ fn measure_running_total_properties() { assert!(measure.is_running_total()); assert!(!measure.is_rolling_window()); assert!(measure.is_cumulative()); - assert!(measure.is_addictive()); + assert!(measure.is_additive()); } #[test] @@ -158,7 +158,7 @@ fn measure_number_agg_properties() { assert!(!measure.is_calculated()); assert!(!measure.is_running_total()); assert!(!measure.is_cumulative()); - assert!(!measure.is_addictive()); + assert!(!measure.is_additive()); } #[test] @@ -174,7 +174,7 @@ fn measure_calculated_number_properties() { assert!(measure.is_calculated()); assert!(!measure.is_running_total()); assert!(!measure.is_cumulative()); - assert!(!measure.is_addictive()); + assert!(!measure.is_additive()); } #[test] @@ -187,7 +187,7 @@ fn measure_rank_properties() { assert!(!measure.is_calculated()); assert!(!measure.is_running_total()); assert!(!measure.is_cumulative()); - assert!(!measure.is_addictive()); + assert!(!measure.is_additive()); } #[test]