From d973abf8b370c1777007dcb07cf8f77254a8644b Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 27 Nov 2025 17:17:37 +0100 Subject: [PATCH 1/6] chore(tesseract): Mock for BaseQueryOptions --- .../src/cube_bridge/base_query_options.rs | 6 +- .../src/cube_bridge/options_member.rs | 9 ++ .../cube_bridge/base_query_options.rs | 142 ++++++++++++++++++ .../src/test_fixtures/cube_bridge/mod.rs | 1 + 4 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/base_query_options.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/base_query_options.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/base_query_options.rs index ae165d77ae179..05effb43fb170 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/base_query_options.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/base_query_options.rs @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; use std::any::Any; use std::rc::Rc; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct TimeDimension { pub dimension: String, pub granularity: Option, @@ -21,7 +21,7 @@ pub struct TimeDimension { pub date_range: Option>, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct FilterItem { pub or: Option>, pub and: Option>, @@ -31,7 +31,7 @@ pub struct FilterItem { pub values: Option>>, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct OrderByItem { pub id: String, pub desc: Option, diff --git a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/options_member.rs b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/options_member.rs index 88565dbc574d4..72075a3a89cdf 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/options_member.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/options_member.rs @@ -11,6 +11,15 @@ pub enum OptionsMember { MemberExpression(Rc), } +impl Clone for OptionsMember { + fn clone(&self) -> Self { + match self { + Self::MemberName(name) => Self::MemberName(name.clone()), + Self::MemberExpression(expr) => Self::MemberExpression(expr.clone()), + } + } +} + impl NativeDeserialize for OptionsMember { fn from_native(native_object: NativeObjectHandle) -> Result { match String::from_native(native_object.clone()) { diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/base_query_options.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/base_query_options.rs new file mode 100644 index 0000000000000..ef7b587cde91c --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/base_query_options.rs @@ -0,0 +1,142 @@ +use std::any::Any; +use std::rc::Rc; + +use cubenativeutils::CubeError; +use typed_builder::TypedBuilder; + +use crate::{ + cube_bridge::{ + base_query_options::{ + BaseQueryOptions, BaseQueryOptionsStatic, FilterItem, OrderByItem, TimeDimension, + }, + base_tools::BaseTools, + evaluator::CubeEvaluator, + join_graph::JoinGraph, + join_hints::JoinHintItem, + options_member::OptionsMember, + security_context::SecurityContext, + }, + impl_static_data, +}; + +/// Mock implementation of BaseQueryOptions for testing +#[derive(TypedBuilder, Clone)] +pub struct MockBaseQueryOptions { + // Required fields - dependencies that must be provided + cube_evaluator: Rc, + base_tools: Rc, + join_graph: Rc, + security_context: Rc, + + // Optional fields from trait methods + #[builder(default)] + measures: Option>, + #[builder(default)] + dimensions: Option>, + #[builder(default)] + segments: Option>, + #[builder(default)] + join_hints: Option>, + + // Fields from BaseQueryOptionsStatic + #[builder(default)] + time_dimensions: Option>, + #[builder(default)] + timezone: Option, + #[builder(default)] + filters: Option>, + #[builder(default)] + order: Option>, + #[builder(default)] + limit: Option, + #[builder(default)] + row_limit: Option, + #[builder(default)] + offset: Option, + #[builder(default)] + ungrouped: Option, + #[builder(default = false)] + export_annotated_sql: bool, + #[builder(default)] + pre_aggregation_query: Option, + #[builder(default)] + total_query: Option, + #[builder(default)] + cubestore_support_multistage: Option, + #[builder(default = false)] + disable_external_pre_aggregations: bool, +} + +impl_static_data!( + MockBaseQueryOptions, + BaseQueryOptionsStatic, + time_dimensions, + timezone, + filters, + order, + limit, + row_limit, + offset, + ungrouped, + export_annotated_sql, + pre_aggregation_query, + total_query, + cubestore_support_multistage, + disable_external_pre_aggregations +); + +impl BaseQueryOptions for MockBaseQueryOptions { + crate::impl_static_data_method!(BaseQueryOptionsStatic); + + fn has_measures(&self) -> Result { + Ok(self.measures.is_some()) + } + + fn measures(&self) -> Result>, CubeError> { + Ok(self.measures.clone()) + } + + fn has_dimensions(&self) -> Result { + Ok(self.dimensions.is_some()) + } + + fn dimensions(&self) -> Result>, CubeError> { + Ok(self.dimensions.clone()) + } + + fn has_segments(&self) -> Result { + Ok(self.segments.is_some()) + } + + fn segments(&self) -> Result>, CubeError> { + Ok(self.segments.clone()) + } + + fn cube_evaluator(&self) -> Result, CubeError> { + Ok(self.cube_evaluator.clone()) + } + + fn base_tools(&self) -> Result, CubeError> { + Ok(self.base_tools.clone()) + } + + fn join_graph(&self) -> Result, CubeError> { + Ok(self.join_graph.clone()) + } + + fn security_context(&self) -> Result, CubeError> { + Ok(self.security_context.clone()) + } + + fn has_join_hints(&self) -> Result { + Ok(self.join_hints.is_some()) + } + + fn join_hints(&self) -> Result>, CubeError> { + Ok(self.join_hints.clone()) + } + + fn as_any(self: Rc) -> Rc { + self + } +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs index d8fb8d1d84387..526557c8daf56 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -3,6 +3,7 @@ mod macros; mod yaml; +mod base_query_options; mod mock_base_tools; mod mock_case_definition; mod mock_case_else_item; From f7fc40ed2552593ee800b6c014e30c6db54f5b69 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Mon, 17 Nov 2025 14:57:30 +0100 Subject: [PATCH 2/6] chore --- .../cube_bridge/base_query_options.rs | 17 +++++++++++++++++ .../src/test_fixtures/cube_bridge/mod.rs | 4 ++++ 2 files changed, 21 insertions(+) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/base_query_options.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/base_query_options.rs index ef7b587cde91c..9b45a8cdce892 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/base_query_options.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/base_query_options.rs @@ -85,6 +85,23 @@ impl_static_data!( disable_external_pre_aggregations ); +/// Helper function to create Vec from Vec +/// +/// Converts a vector of strings into a vector of OptionsMember::MemberName variants. +/// +/// # Example +/// ```ignore +/// use crate::test_fixtures::cube_bridge::members_from_strings; +/// +/// let members = members_from_strings(vec!["orders.count", "orders.status"]); +/// ``` +pub fn members_from_strings(strings: Vec) -> Vec { + strings + .into_iter() + .map(|s| OptionsMember::MemberName(s.to_string())) + .collect() +} + impl BaseQueryOptions for MockBaseQueryOptions { crate::impl_static_data_method!(BaseQueryOptionsStatic); diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs index 526557c8daf56..7fadf44236127 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -34,6 +34,10 @@ mod mock_sql_utils; mod mock_struct_with_sql_member; mod mock_timeshift_definition; +#[cfg(test)] +mod mock_join_graph_tests; + +pub use base_query_options::{members_from_strings, MockBaseQueryOptions}; pub use mock_base_tools::MockBaseTools; pub use mock_case_definition::MockCaseDefinition; pub use mock_case_else_item::MockCaseElseItem; From 71301a5d256f8ce1dc3aafef472797616f042fc1 Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Sun, 16 Nov 2025 22:38:53 +0100 Subject: [PATCH 3/6] chore --- .../cube_bridge/base_query_options.rs | 54 +++++++++++++++++++ .../src/test_fixtures/cube_bridge/mod.rs | 4 +- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/base_query_options.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/base_query_options.rs index 9b45a8cdce892..5630fc2f94312 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/base_query_options.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/base_query_options.rs @@ -102,6 +102,60 @@ pub fn members_from_strings(strings: Vec) -> Vec .collect() } +/// Helper function to create a FilterItem with member, operator, and values +/// +/// # Arguments +/// * `member` - Member name (e.g., "orders.status") +/// * `operator` - Filter operator (e.g., "equals", "contains", "gt") +/// * `values` - Array of filter values +/// +pub fn filter_item( + member: M, + operator: O, + values: Vec, +) -> FilterItem { + FilterItem { + member: Some(member.to_string()), + dimension: None, + operator: Some(operator.to_string()), + values: Some(values.into_iter().map(|v| Some(v.to_string())).collect()), + or: None, + and: None, + } +} + +/// Helper function to create a FilterItem with OR logic +/// +/// # Arguments +/// * `items` - Array of FilterItems to combine with OR +/// +pub fn filter_or(items: Vec) -> FilterItem { + FilterItem { + or: Some(items), + member: None, + dimension: None, + operator: None, + values: None, + and: None, + } +} + +/// Helper function to create a FilterItem with AND logic +/// +/// # Arguments +/// * `items` - Array of FilterItems to combine with AND +/// +pub fn filter_and(items: Vec) -> FilterItem { + FilterItem { + and: Some(items), + member: None, + dimension: None, + operator: None, + values: None, + or: None, + } +} + impl BaseQueryOptions for MockBaseQueryOptions { crate::impl_static_data_method!(BaseQueryOptionsStatic); diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs index 7fadf44236127..e66f54512129d 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -37,7 +37,9 @@ mod mock_timeshift_definition; #[cfg(test)] mod mock_join_graph_tests; -pub use base_query_options::{members_from_strings, MockBaseQueryOptions}; +pub use base_query_options::{ + filter_and, filter_item, filter_or, members_from_strings, MockBaseQueryOptions, +}; pub use mock_base_tools::MockBaseTools; pub use mock_case_definition::MockCaseDefinition; pub use mock_case_else_item::MockCaseElseItem; From deb97fc8f556f7a99230b0d647514fad7d64ca0e Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 27 Nov 2025 17:23:05 +0100 Subject: [PATCH 4/6] in work --- .../cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs index e66f54512129d..688d71b3beafe 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -34,9 +34,6 @@ mod mock_sql_utils; mod mock_struct_with_sql_member; mod mock_timeshift_definition; -#[cfg(test)] -mod mock_join_graph_tests; - pub use base_query_options::{ filter_and, filter_item, filter_or, members_from_strings, MockBaseQueryOptions, }; From d2a6553e6d022881b1cba7dccf659d93c5543b5b Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Mon, 24 Nov 2025 14:03:51 +0100 Subject: [PATCH 5/6] base query options yaml --- .../src/test_fixtures/cube_bridge/mod.rs | 8 +- .../cube_bridge/yaml/base_query_options.rs | 136 +++++++++ .../src/test_fixtures/cube_bridge/yaml/mod.rs | 2 + .../test_fixtures/test_utils/test_context.rs | 257 +++++++++++++++++- 4 files changed, 395 insertions(+), 8 deletions(-) create mode 100644 rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/yaml/base_query_options.rs diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs index 688d71b3beafe..fe935eefaa260 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mod.rs @@ -1,9 +1,9 @@ #[macro_use] mod macros; -mod yaml; +pub mod yaml; -mod base_query_options; +pub mod base_query_options; mod mock_base_tools; mod mock_case_definition; mod mock_case_else_item; @@ -34,9 +34,7 @@ mod mock_sql_utils; mod mock_struct_with_sql_member; mod mock_timeshift_definition; -pub use base_query_options::{ - filter_and, filter_item, filter_or, members_from_strings, MockBaseQueryOptions, -}; +pub use base_query_options::{members_from_strings, MockBaseQueryOptions}; pub use mock_base_tools::MockBaseTools; pub use mock_case_definition::MockCaseDefinition; pub use mock_case_else_item::MockCaseElseItem; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/yaml/base_query_options.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/yaml/base_query_options.rs new file mode 100644 index 0000000000000..b7d29060ed31d --- /dev/null +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/yaml/base_query_options.rs @@ -0,0 +1,136 @@ +use crate::cube_bridge::base_query_options::{FilterItem, OrderByItem}; +use serde::de; +use serde::{Deserialize, Deserializer}; + +#[derive(Debug, Deserialize)] +pub struct YamlBaseQueryOptions { + #[serde(default)] + pub measures: Option>, + #[serde(default)] + pub dimensions: Option>, + #[serde(default)] + pub segments: Option>, + #[serde(default)] + pub order: Option>, + #[serde(default)] + pub filters: Option>, + #[serde(default)] + pub limit: Option, + #[serde(default)] + pub row_limit: Option, + #[serde(default)] + pub offset: Option, + #[serde(default)] + pub ungrouped: Option, + #[serde(default)] + pub export_annotated_sql: Option, + #[serde(default)] + pub pre_aggregation_query: Option, + #[serde(default)] + pub total_query: Option, + #[serde(default)] + pub cubestore_support_multistage: Option, + #[serde(default)] + pub disable_external_pre_aggregations: Option, +} + +#[derive(Debug, Deserialize)] +pub struct YamlOrderByItem { + pub id: String, + #[serde(default)] + pub desc: Option, +} + +impl YamlOrderByItem { + pub fn into_order_by_item(self) -> OrderByItem { + OrderByItem { + id: self.id, + desc: self.desc, + } + } +} + +#[derive(Debug)] +pub enum YamlFilterItem { + Group(YamlFilterGroup), + Base(YamlBaseFilter), +} + +impl<'de> Deserialize<'de> for YamlFilterItem { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = serde_yaml::Value::deserialize(deserializer)?; + + // Check if it has 'or' or 'and' keys - then it's a Group + if let serde_yaml::Value::Mapping(ref map) = value { + let has_or = map.contains_key(&serde_yaml::Value::String("or".to_string())); + let has_and = map.contains_key(&serde_yaml::Value::String("and".to_string())); + + if has_or || has_and { + return serde_yaml::from_value::(value) + .map(YamlFilterItem::Group) + .map_err(de::Error::custom); + } + } + + // Otherwise it's a Base filter + serde_yaml::from_value::(value) + .map(YamlFilterItem::Base) + .map_err(de::Error::custom) + } +} + +#[derive(Debug, Deserialize)] +pub struct YamlFilterGroup { + #[serde(default)] + pub or: Option>, + #[serde(default)] + pub and: Option>, +} + +#[derive(Debug, Deserialize)] +pub struct YamlBaseFilter { + #[serde(default)] + pub member: Option, + #[serde(default)] + pub dimension: Option, + #[serde(default)] + pub operator: Option, + #[serde(default)] + pub values: Option>>, +} + +impl YamlFilterItem { + pub fn into_filter_item(self) -> FilterItem { + match self { + YamlFilterItem::Group(group) => FilterItem { + or: group.or.map(|items| { + items + .into_iter() + .map(|item| item.into_filter_item()) + .collect() + }), + and: group.and.map(|items| { + items + .into_iter() + .map(|item| item.into_filter_item()) + .collect() + }), + member: None, + dimension: None, + operator: None, + values: None, + }, + YamlFilterItem::Base(base) => FilterItem { + or: None, + and: None, + member: base.member, + dimension: base.dimension, + operator: base.operator, + values: base.values, + }, + } + } +} diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/yaml/mod.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/yaml/mod.rs index 91ec14ad9cf37..ce7dabd02bbf6 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/yaml/mod.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/yaml/mod.rs @@ -1,3 +1,4 @@ +pub mod base_query_options; pub mod case; pub mod dimension; pub mod measure; @@ -5,6 +6,7 @@ pub mod schema; pub mod segment; pub mod timeshift; +pub use base_query_options::YamlBaseQueryOptions; pub use dimension::YamlDimensionDefinition; pub use measure::YamlMeasureDefinition; pub use schema::YamlSchema; diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/test_utils/test_context.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/test_utils/test_context.rs index 8c86ee77e5b0c..4f3ad99136345 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/test_utils/test_context.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/test_utils/test_context.rs @@ -1,15 +1,165 @@ +use crate::cube_bridge::base_query_options::BaseQueryOptions; use crate::planner::query_tools::QueryTools; use crate::planner::sql_evaluator::sql_nodes::SqlNodesFactory; use crate::planner::sql_evaluator::{MemberSymbol, SqlEvaluatorVisitor}; use crate::planner::sql_templates::PlanSqlTemplates; -use crate::test_fixtures::cube_bridge::{MockSchema, MockSecurityContext}; +use crate::test_fixtures::cube_bridge::yaml::YamlBaseQueryOptions; +use crate::test_fixtures::cube_bridge::{ + members_from_strings, MockBaseQueryOptions, MockSchema, MockSecurityContext, +}; use chrono_tz::Tz; use cubenativeutils::CubeError; use std::rc::Rc; +#[cfg(test)] +mod tests { + use super::*; + use crate::test_fixtures::cube_bridge::MockSchema; + + #[test] + fn test_yaml_filter_parsing() { + use indoc::indoc; + + let yaml = indoc! {" + filters: + - or: + - dimension: visitors.count + operator: gt + values: + - \"1\" + - dimension: visitors.source + operator: equals + values: + - google + - dimension: visitors.created_at + operator: gte + values: + - \"2024-01-01\" + "}; + let parsed: YamlBaseQueryOptions = serde_yaml::from_str(yaml).unwrap(); + let filters = parsed.filters.unwrap(); + + println!("Filter count: {}", filters.len()); + for (i, filter) in filters.iter().enumerate() { + println!("Filter {}: {:?}", i, filter); + } + + assert_eq!(filters.len(), 2); + } + + #[test] + fn test_create_query_options_from_yaml() { + use indoc::indoc; + + let schema = MockSchema::from_yaml_file("common/visitors.yaml"); + let ctx = TestContext::new(schema).unwrap(); + + let yaml = indoc! {" + measures: + - visitors.count + dimensions: + - visitors.source + order: + - id: visitors.count + desc: true + filters: + - or: + - dimension: visitors.count + operator: gt + values: + - \"1\" + - dimension: visitors.source + operator: equals + values: + - google + - dimension: visitors.created_at + operator: gte + values: + - \"2024-01-01\" + limit: \"100\" + offset: \"20\" + ungrouped: true + "}; + + let options = ctx.create_query_options_from_yaml(yaml); + + // Verify measures + let measures = options.measures().unwrap().unwrap(); + assert_eq!(measures.len(), 1); + + // Verify dimensions + let dimensions = options.dimensions().unwrap().unwrap(); + assert_eq!(dimensions.len(), 1); + + // Verify order and filters from static_data + let static_data = options.static_data(); + + let order = static_data.order.as_ref().unwrap(); + assert_eq!(order.len(), 1); + assert_eq!(order[0].id, "visitors.count"); + assert_eq!(order[0].is_desc(), true); + + let filters = static_data.filters.as_ref().unwrap(); + assert_eq!(filters.len(), 2, "Should have 2 filters"); + + assert!(filters[0].or.is_some(), "First filter should have 'or'"); + assert!( + filters[0].and.is_none(), + "First filter should not have 'and'" + ); + + assert!( + filters[1].or.is_none(), + "Second filter should not have 'or': {:?}", + filters[1].or + ); + assert!( + filters[1].and.is_none(), + "Second filter should not have 'and': {:?}", + filters[1].and + ); + assert!( + filters[1].dimension.is_some(), + "Second filter: member={:?}, dimension={:?}, operator={:?}, values={:?}", + filters[1].member, + filters[1].dimension, + filters[1].operator, + filters[1].values + ); + + // Verify other fields + assert_eq!(static_data.limit, Some("100".to_string())); + assert_eq!(static_data.offset, Some("20".to_string())); + assert_eq!(static_data.ungrouped, Some(true)); + } + + #[test] + fn test_create_query_options_minimal() { + let schema = MockSchema::from_yaml_file("common/visitors.yaml"); + let ctx = TestContext::new(schema).unwrap(); + + let yaml = r#" +measures: + - visitors.count +"#; + + let options = ctx.create_query_options_from_yaml(yaml); + let measures = options.measures().unwrap().unwrap(); + assert_eq!(measures.len(), 1); + + // All other fields should be None/empty + assert!(options.dimensions().unwrap().is_none()); + + let static_data = options.static_data(); + assert!(static_data.order.is_none()); + assert!(static_data.filters.is_none()); + } +} + /// Test context providing query tools and symbol creation helpers pub struct TestContext { query_tools: Rc, + security_context: Rc, } impl TestContext { @@ -26,14 +176,17 @@ impl TestContext { let query_tools = QueryTools::try_new( evaluator, - security_context, + security_context.clone(), Rc::new(base_tools), join_graph, Some(timezone.to_string()), false, // export_annotated_sql )?; - Ok(Self { query_tools }) + Ok(Self { + query_tools, + security_context, + }) } #[allow(dead_code)] @@ -72,4 +225,102 @@ impl TestContext { visitor.apply(symbol, node_processor, &templates) } + + /// Creates MockBaseQueryOptions from YAML string + /// + /// The YAML structure should match the JS query format: + /// ```yaml + /// measures: + /// - visitors.visitor_count + /// dimensions: + /// - visitors.source + /// order: + /// - id: visitors.visitor_count + /// desc: true + /// filters: + /// - or: + /// - dimension: visitors.visitor_count + /// operator: gt + /// values: + /// - "1" + /// - dimension: visitors.source + /// operator: equals + /// values: + /// - google + /// - dimension: visitors.created_at + /// operator: gte + /// values: + /// - "2024-01-01" + /// limit: "100" + /// offset: "20" + /// ungrouped: true + /// ``` + /// + /// Panics if YAML cannot be parsed. + pub fn create_query_options_from_yaml(&self, yaml: &str) -> Rc { + let yaml_options: YamlBaseQueryOptions = serde_yaml::from_str(yaml) + .unwrap_or_else(|e| panic!("Failed to parse YAML query options: {}", e)); + + let measures = yaml_options + .measures + .map(|m| members_from_strings(m)) + .filter(|m| !m.is_empty()); + + let dimensions = yaml_options + .dimensions + .map(|d| members_from_strings(d)) + .filter(|d| !d.is_empty()); + + let segments = yaml_options + .segments + .map(|s| members_from_strings(s)) + .filter(|s| !s.is_empty()); + + let order = yaml_options + .order + .map(|items| { + items + .into_iter() + .map(|item| item.into_order_by_item()) + .collect::>() + }) + .filter(|o| !o.is_empty()); + + let filters = yaml_options + .filters + .map(|items| { + items + .into_iter() + .map(|item| item.into_filter_item()) + .collect::>() + }) + .filter(|f| !f.is_empty()); + + Rc::new( + MockBaseQueryOptions::builder() + .cube_evaluator(self.query_tools.cube_evaluator().clone()) + .base_tools(self.query_tools.base_tools().clone()) + .join_graph(self.query_tools.join_graph().clone()) + .security_context(self.security_context.clone()) + .measures(measures) + .dimensions(dimensions) + .segments(segments) + .order(order) + .filters(filters) + .limit(yaml_options.limit) + .row_limit(yaml_options.row_limit) + .offset(yaml_options.offset) + .ungrouped(yaml_options.ungrouped) + .export_annotated_sql(yaml_options.export_annotated_sql.unwrap_or(false)) + .pre_aggregation_query(yaml_options.pre_aggregation_query) + .total_query(yaml_options.total_query) + .cubestore_support_multistage(yaml_options.cubestore_support_multistage) + .disable_external_pre_aggregations( + yaml_options + .disable_external_pre_aggregations + .unwrap_or(false), + ) + .build(), + ) + } } From 85b96ec9577fb6c92610110587afb33a909a871d Mon Sep 17 00:00:00 2001 From: Aleksandr Romanenko Date: Thu, 27 Nov 2025 17:25:53 +0100 Subject: [PATCH 6/6] in work --- .../cube_bridge/base_query_options.rs | 30 +- .../cube_bridge/yaml/base_query_options.rs | 4 +- .../test_fixtures/test_utils/test_context.rs | 290 +++++++++--------- 3 files changed, 150 insertions(+), 174 deletions(-) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/base_query_options.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/base_query_options.rs index 5630fc2f94312..b992ea7d4c59e 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/base_query_options.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/base_query_options.rs @@ -85,16 +85,6 @@ impl_static_data!( disable_external_pre_aggregations ); -/// Helper function to create Vec from Vec -/// -/// Converts a vector of strings into a vector of OptionsMember::MemberName variants. -/// -/// # Example -/// ```ignore -/// use crate::test_fixtures::cube_bridge::members_from_strings; -/// -/// let members = members_from_strings(vec!["orders.count", "orders.status"]); -/// ``` pub fn members_from_strings(strings: Vec) -> Vec { strings .into_iter() @@ -102,13 +92,7 @@ pub fn members_from_strings(strings: Vec) -> Vec .collect() } -/// Helper function to create a FilterItem with member, operator, and values -/// -/// # Arguments -/// * `member` - Member name (e.g., "orders.status") -/// * `operator` - Filter operator (e.g., "equals", "contains", "gt") -/// * `values` - Array of filter values -/// +#[allow(dead_code)] pub fn filter_item( member: M, operator: O, @@ -124,11 +108,7 @@ pub fn filter_item( } } -/// Helper function to create a FilterItem with OR logic -/// -/// # Arguments -/// * `items` - Array of FilterItems to combine with OR -/// +#[allow(dead_code)] pub fn filter_or(items: Vec) -> FilterItem { FilterItem { or: Some(items), @@ -140,11 +120,7 @@ pub fn filter_or(items: Vec) -> FilterItem { } } -/// Helper function to create a FilterItem with AND logic -/// -/// # Arguments -/// * `items` - Array of FilterItems to combine with AND -/// +#[allow(dead_code)] pub fn filter_and(items: Vec) -> FilterItem { FilterItem { and: Some(items), diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/yaml/base_query_options.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/yaml/base_query_options.rs index b7d29060ed31d..61a73de96cbe6 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/yaml/base_query_options.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/yaml/base_query_options.rs @@ -65,8 +65,8 @@ impl<'de> Deserialize<'de> for YamlFilterItem { // Check if it has 'or' or 'and' keys - then it's a Group if let serde_yaml::Value::Mapping(ref map) = value { - let has_or = map.contains_key(&serde_yaml::Value::String("or".to_string())); - let has_and = map.contains_key(&serde_yaml::Value::String("and".to_string())); + let has_or = map.contains_key(serde_yaml::Value::String("or".to_string())); + let has_and = map.contains_key(serde_yaml::Value::String("and".to_string())); if has_or || has_and { return serde_yaml::from_value::(value) diff --git a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/test_utils/test_context.rs b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/test_utils/test_context.rs index 4f3ad99136345..d869c50cfa6e2 100644 --- a/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/test_utils/test_context.rs +++ b/rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/test_utils/test_context.rs @@ -11,151 +11,6 @@ use chrono_tz::Tz; use cubenativeutils::CubeError; use std::rc::Rc; -#[cfg(test)] -mod tests { - use super::*; - use crate::test_fixtures::cube_bridge::MockSchema; - - #[test] - fn test_yaml_filter_parsing() { - use indoc::indoc; - - let yaml = indoc! {" - filters: - - or: - - dimension: visitors.count - operator: gt - values: - - \"1\" - - dimension: visitors.source - operator: equals - values: - - google - - dimension: visitors.created_at - operator: gte - values: - - \"2024-01-01\" - "}; - let parsed: YamlBaseQueryOptions = serde_yaml::from_str(yaml).unwrap(); - let filters = parsed.filters.unwrap(); - - println!("Filter count: {}", filters.len()); - for (i, filter) in filters.iter().enumerate() { - println!("Filter {}: {:?}", i, filter); - } - - assert_eq!(filters.len(), 2); - } - - #[test] - fn test_create_query_options_from_yaml() { - use indoc::indoc; - - let schema = MockSchema::from_yaml_file("common/visitors.yaml"); - let ctx = TestContext::new(schema).unwrap(); - - let yaml = indoc! {" - measures: - - visitors.count - dimensions: - - visitors.source - order: - - id: visitors.count - desc: true - filters: - - or: - - dimension: visitors.count - operator: gt - values: - - \"1\" - - dimension: visitors.source - operator: equals - values: - - google - - dimension: visitors.created_at - operator: gte - values: - - \"2024-01-01\" - limit: \"100\" - offset: \"20\" - ungrouped: true - "}; - - let options = ctx.create_query_options_from_yaml(yaml); - - // Verify measures - let measures = options.measures().unwrap().unwrap(); - assert_eq!(measures.len(), 1); - - // Verify dimensions - let dimensions = options.dimensions().unwrap().unwrap(); - assert_eq!(dimensions.len(), 1); - - // Verify order and filters from static_data - let static_data = options.static_data(); - - let order = static_data.order.as_ref().unwrap(); - assert_eq!(order.len(), 1); - assert_eq!(order[0].id, "visitors.count"); - assert_eq!(order[0].is_desc(), true); - - let filters = static_data.filters.as_ref().unwrap(); - assert_eq!(filters.len(), 2, "Should have 2 filters"); - - assert!(filters[0].or.is_some(), "First filter should have 'or'"); - assert!( - filters[0].and.is_none(), - "First filter should not have 'and'" - ); - - assert!( - filters[1].or.is_none(), - "Second filter should not have 'or': {:?}", - filters[1].or - ); - assert!( - filters[1].and.is_none(), - "Second filter should not have 'and': {:?}", - filters[1].and - ); - assert!( - filters[1].dimension.is_some(), - "Second filter: member={:?}, dimension={:?}, operator={:?}, values={:?}", - filters[1].member, - filters[1].dimension, - filters[1].operator, - filters[1].values - ); - - // Verify other fields - assert_eq!(static_data.limit, Some("100".to_string())); - assert_eq!(static_data.offset, Some("20".to_string())); - assert_eq!(static_data.ungrouped, Some(true)); - } - - #[test] - fn test_create_query_options_minimal() { - let schema = MockSchema::from_yaml_file("common/visitors.yaml"); - let ctx = TestContext::new(schema).unwrap(); - - let yaml = r#" -measures: - - visitors.count -"#; - - let options = ctx.create_query_options_from_yaml(yaml); - let measures = options.measures().unwrap().unwrap(); - assert_eq!(measures.len(), 1); - - // All other fields should be None/empty - assert!(options.dimensions().unwrap().is_none()); - - let static_data = options.static_data(); - assert!(static_data.order.is_none()); - assert!(static_data.filters.is_none()); - } -} - /// Test context providing query tools and symbol creation helpers pub struct TestContext { query_tools: Rc, @@ -324,3 +179,148 @@ impl TestContext { ) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_fixtures::cube_bridge::MockSchema; + + #[test] + fn test_yaml_filter_parsing() { + use indoc::indoc; + + let yaml = indoc! {" + filters: + - or: + - dimension: visitors.count + operator: gt + values: + - \"1\" + - dimension: visitors.source + operator: equals + values: + - google + - dimension: visitors.created_at + operator: gte + values: + - \"2024-01-01\" + "}; + let parsed: YamlBaseQueryOptions = serde_yaml::from_str(yaml).unwrap(); + let filters = parsed.filters.unwrap(); + + println!("Filter count: {}", filters.len()); + for (i, filter) in filters.iter().enumerate() { + println!("Filter {}: {:?}", i, filter); + } + + assert_eq!(filters.len(), 2); + } + + #[test] + fn test_create_query_options_from_yaml() { + use indoc::indoc; + + let schema = MockSchema::from_yaml_file("common/visitors.yaml"); + let ctx = TestContext::new(schema).unwrap(); + + let yaml = indoc! {" + measures: + - visitors.count + dimensions: + - visitors.source + order: + - id: visitors.count + desc: true + filters: + - or: + - dimension: visitors.count + operator: gt + values: + - \"1\" + - dimension: visitors.source + operator: equals + values: + - google + - dimension: visitors.created_at + operator: gte + values: + - \"2024-01-01\" + limit: \"100\" + offset: \"20\" + ungrouped: true + "}; + + let options = ctx.create_query_options_from_yaml(yaml); + + // Verify measures + let measures = options.measures().unwrap().unwrap(); + assert_eq!(measures.len(), 1); + + // Verify dimensions + let dimensions = options.dimensions().unwrap().unwrap(); + assert_eq!(dimensions.len(), 1); + + // Verify order and filters from static_data + let static_data = options.static_data(); + + let order = static_data.order.as_ref().unwrap(); + assert_eq!(order.len(), 1); + assert_eq!(order[0].id, "visitors.count"); + assert!(order[0].is_desc()); + + let filters = static_data.filters.as_ref().unwrap(); + assert_eq!(filters.len(), 2, "Should have 2 filters"); + + assert!(filters[0].or.is_some(), "First filter should have 'or'"); + assert!( + filters[0].and.is_none(), + "First filter should not have 'and'" + ); + + assert!( + filters[1].or.is_none(), + "Second filter should not have 'or': {:?}", + filters[1].or + ); + assert!( + filters[1].and.is_none(), + "Second filter should not have 'and': {:?}", + filters[1].and + ); + assert!( + filters[1].dimension.is_some(), + "Second filter: member={:?}, dimension={:?}, operator={:?}, values={:?}", + filters[1].member, + filters[1].dimension, + filters[1].operator, + filters[1].values + ); + + // Verify other fields + assert_eq!(static_data.limit, Some("100".to_string())); + assert_eq!(static_data.offset, Some("20".to_string())); + assert_eq!(static_data.ungrouped, Some(true)); + } + + #[test] + fn test_create_query_options_minimal() { + let schema = MockSchema::from_yaml_file("common/visitors.yaml"); + let ctx = TestContext::new(schema).unwrap(); + + let yaml = r#" +measures: + - visitors.count +"#; + + let options = ctx.create_query_options_from_yaml(yaml); + let measures = options.measures().unwrap().unwrap(); + assert_eq!(measures.len(), 1); + + // All other fields should be None/empty + assert!(options.dimensions().unwrap().is_none()); + + let static_data = options.static_data(); + assert!(static_data.order.is_none()); + assert!(static_data.filters.is_none()); + } +}