Skip to content

Commit e79d10a

Browse files
authored
fix(tesseract): Issue with custom granularities for pre-aggregations (cube-js#10424)
1 parent 45d1278 commit e79d10a

21 files changed

+538
-49
lines changed

packages/cubejs-schema-compiler/src/adapter/BaseQuery.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,6 +1000,7 @@ export class BaseQuery {
10001000
ungrouped: this.options.ungrouped,
10011001
exportAnnotatedSql: false,
10021002
preAggregationQuery: this.options.preAggregationQuery,
1003+
preAggregationId: this.options.preAggregationId || null,
10031004
securityContext: this.contextSymbols.securityContext,
10041005
cubestoreSupportMultistage: this.options.cubestoreSupportMultistage ?? getEnv('cubeStoreRollingWindowJoin'),
10051006
disableExternalPreAggregations: !!this.options.disableExternalPreAggregations,

rust/cubesqlplanner/cubesqlplanner/src/cube_bridge/base_query_options.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ pub struct BaseQueryOptionsStatic {
7171
pub cubestore_support_multistage: Option<bool>,
7272
#[serde(rename = "disableExternalPreAggregations")]
7373
pub disable_external_pre_aggregations: bool,
74+
#[serde(rename = "preAggregationId")]
75+
pub pre_aggregation_id: Option<String>,
7476
}
7577

7678
#[nativebridge::native_bridge(BaseQueryOptionsStatic)]

rust/cubesqlplanner/cubesqlplanner/src/logical_plan/optimizers/pre_aggregation/dimension_matcher.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,7 @@ impl<'a> DimensionMatcher<'a> {
227227
add_to_matched_dimension: bool,
228228
) -> Result<MatchState, CubeError> {
229229
let granularity = if self.pre_aggregation.allow_non_strict_date_range_match {
230-
if let Some(granularity) = time_dimension.granularity_obj() {
231-
granularity.min_granularity()?
232-
} else {
233-
time_dimension.granularity().clone()
234-
}
230+
time_dimension.granularity().clone()
235231
} else {
236232
time_dimension.rollup_granularity(self.query_tools.clone())?
237233
};

rust/cubesqlplanner/cubesqlplanner/src/logical_plan/optimizers/pre_aggregation/optimizer.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ impl PreAggregationOptimizer {
2828
&mut self,
2929
plan: Rc<Query>,
3030
disable_external_pre_aggregations: bool,
31+
pre_aggregation_id: Option<&str>,
3132
) -> Result<Option<Rc<Query>>, CubeError> {
3233
let cube_names = collect_cube_names_from_node(&plan)?;
3334
let mut compiler = PreAggregationsCompiler::try_new(self.query_tools.clone(), &cube_names)?;
@@ -36,6 +37,12 @@ impl PreAggregationOptimizer {
3637
compiler.compile_all_pre_aggregations(disable_external_pre_aggregations)?;
3738

3839
for pre_aggregation in compiled_pre_aggregations.iter() {
40+
if let Some(id) = pre_aggregation_id {
41+
let full_name = format!("{}.{}", pre_aggregation.cube_name, pre_aggregation.name);
42+
if full_name != id {
43+
continue;
44+
}
45+
}
3946
let new_query = self.try_rewrite_query(plan.clone(), pre_aggregation)?;
4047
if new_query.is_some() {
4148
return Ok(new_query);

rust/cubesqlplanner/cubesqlplanner/src/planner/query_properties.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ pub struct QueryProperties {
108108
query_join_hints: Rc<Vec<JoinHintItem>>,
109109
allow_multi_stage: bool,
110110
disable_external_pre_aggregations: bool,
111+
pre_aggregation_id: Option<String>,
111112
}
112113

113114
impl QueryProperties {
@@ -410,6 +411,7 @@ impl QueryProperties {
410411
let total_query = options.static_data().total_query.unwrap_or(false);
411412
let disable_external_pre_aggregations =
412413
options.static_data().disable_external_pre_aggregations;
414+
let pre_aggregation_id = options.static_data().pre_aggregation_id.clone();
413415

414416
let mut res = Self {
415417
measures,
@@ -431,6 +433,7 @@ impl QueryProperties {
431433
query_join_hints,
432434
allow_multi_stage: true,
433435
disable_external_pre_aggregations,
436+
pre_aggregation_id,
434437
};
435438
res.apply_static_filters()?;
436439
Ok(Rc::new(res))
@@ -482,6 +485,7 @@ impl QueryProperties {
482485
query_join_hints,
483486
allow_multi_stage,
484487
disable_external_pre_aggregations,
488+
pre_aggregation_id: None,
485489
};
486490
res.apply_static_filters()?;
487491

@@ -753,6 +757,10 @@ impl QueryProperties {
753757
self.disable_external_pre_aggregations
754758
}
755759

760+
pub fn pre_aggregation_id(&self) -> Option<&str> {
761+
self.pre_aggregation_id.as_deref()
762+
}
763+
756764
pub fn all_filters(&self) -> Option<Filter> {
757765
let items = self
758766
.time_dimensions_filters

rust/cubesqlplanner/cubesqlplanner/src/planner/top_level_planner.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,12 @@ impl TopLevelPlanner {
7676
);
7777
let disable_external_pre_aggregations =
7878
self.request.disable_external_pre_aggregations();
79-
if let Some(result) = pre_aggregation_optimizer
80-
.try_optimize(plan.clone(), disable_external_pre_aggregations)?
81-
{
79+
let pre_aggregation_id = self.request.pre_aggregation_id();
80+
if let Some(result) = pre_aggregation_optimizer.try_optimize(
81+
plan.clone(),
82+
disable_external_pre_aggregations,
83+
pre_aggregation_id,
84+
)? {
8285
if pre_aggregation_optimizer.get_used_pre_aggregations().len() == 1 {
8386
(
8487
result,

rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/base_query_options.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ pub struct MockBaseQueryOptions {
6565
cubestore_support_multistage: Option<bool>,
6666
#[builder(default = false)]
6767
disable_external_pre_aggregations: bool,
68+
#[builder(default)]
69+
pre_aggregation_id: Option<String>,
6870
}
6971

7072
impl_static_data!(
@@ -82,7 +84,8 @@ impl_static_data!(
8284
pre_aggregation_query,
8385
total_query,
8486
cubestore_support_multistage,
85-
disable_external_pre_aggregations
87+
disable_external_pre_aggregations,
88+
pre_aggregation_id
8689
);
8790

8891
pub fn members_from_strings<S: ToString>(strings: Vec<S>) -> Vec<OptionsMember> {

rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_dimension_definition.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ impl MockDimensionDefinition {
5959
pub fn from_yaml(yaml: &str) -> Result<Rc<Self>, CubeError> {
6060
let yaml_def: YamlDimensionDefinition = serde_yaml::from_str(yaml)
6161
.map_err(|e| CubeError::user(format!("Failed to parse YAML: {}", e)))?;
62-
Ok(yaml_def.build())
62+
Ok(Rc::new(yaml_def.build().definition))
6363
}
6464
}
6565

rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_evaluator.rs

Lines changed: 87 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -233,23 +233,29 @@ impl CubeEvaluator for MockCubeEvaluator {
233233

234234
let granularity = &path[3];
235235

236-
let valid_granularities = [
236+
// Check custom granularities in schema first
237+
if let Some(custom) = self.schema.get_granularity(&path[0], &path[1], granularity) {
238+
return Ok(custom as Rc<dyn GranularityDefinition>);
239+
}
240+
241+
// Fall back to predefined granularities
242+
let predefined = [
237243
"second", "minute", "hour", "day", "week", "month", "quarter", "year",
238244
];
239245

240-
if !valid_granularities.contains(&granularity.as_str()) {
241-
return Err(CubeError::user(format!(
242-
"Unsupported granularity: '{}'. Supported: second, minute, hour, day, week, month, quarter, year",
246+
if predefined.contains(&granularity.as_str()) {
247+
use crate::test_fixtures::cube_bridge::MockGranularityDefinition;
248+
Ok(Rc::new(
249+
MockGranularityDefinition::builder()
250+
.interval(format!("1 {}", granularity))
251+
.build(),
252+
) as Rc<dyn GranularityDefinition>)
253+
} else {
254+
Err(CubeError::user(format!(
255+
"Granularity '{}' not found",
243256
granularity
244-
)));
257+
)))
245258
}
246-
247-
use crate::test_fixtures::cube_bridge::MockGranularityDefinition;
248-
Ok(Rc::new(
249-
MockGranularityDefinition::builder()
250-
.interval(granularity.clone())
251-
.build(),
252-
) as Rc<dyn GranularityDefinition>)
253259
}
254260

255261
fn pre_aggregations_for_cube_as_array(
@@ -308,3 +314,72 @@ impl CubeEvaluator for MockCubeEvaluator {
308314
self
309315
}
310316
}
317+
318+
#[cfg(test)]
319+
mod tests {
320+
use super::*;
321+
use crate::test_fixtures::cube_bridge::MockSchema;
322+
323+
fn create_custom_granularity_schema() -> MockSchema {
324+
MockSchema::from_yaml_file("common/custom_granularity_test.yaml")
325+
}
326+
327+
fn resolve(
328+
evaluator: &MockCubeEvaluator,
329+
granularity: &str,
330+
) -> Result<Rc<dyn GranularityDefinition>, CubeError> {
331+
evaluator.resolve_granularity(vec![
332+
"orders".to_string(),
333+
"created_at".to_string(),
334+
"granularities".to_string(),
335+
granularity.to_string(),
336+
])
337+
}
338+
339+
#[test]
340+
fn test_resolve_predefined_granularity() {
341+
let schema = create_custom_granularity_schema();
342+
let evaluator = schema.create_evaluator();
343+
344+
let result = resolve(&evaluator, "day").expect("should resolve predefined granularity");
345+
assert_eq!(result.static_data().interval, "1 day");
346+
assert_eq!(result.static_data().origin, None);
347+
assert_eq!(result.static_data().offset, None);
348+
}
349+
350+
#[test]
351+
fn test_resolve_custom_granularity() {
352+
let schema = create_custom_granularity_schema();
353+
let evaluator = schema.create_evaluator();
354+
355+
let result = resolve(&evaluator, "half_year").expect("should resolve custom granularity");
356+
assert_eq!(result.static_data().interval, "6 months");
357+
assert_eq!(result.static_data().origin, Some("2024-01-01".to_string()));
358+
assert_eq!(result.static_data().offset, None);
359+
}
360+
361+
#[test]
362+
fn test_resolve_custom_granularity_with_offset() {
363+
let schema = create_custom_granularity_schema();
364+
let evaluator = schema.create_evaluator();
365+
366+
let result = resolve(&evaluator, "fiscal_year").expect("should resolve custom granularity");
367+
assert_eq!(result.static_data().interval, "1 year");
368+
assert_eq!(result.static_data().offset, Some("1 month".to_string()));
369+
}
370+
371+
#[test]
372+
fn test_resolve_unknown_granularity_error() {
373+
let schema = create_custom_granularity_schema();
374+
let evaluator = schema.create_evaluator();
375+
376+
let result = resolve(&evaluator, "nonexistent");
377+
assert!(result.is_err());
378+
let err = result.err().unwrap();
379+
assert!(
380+
err.message.contains("Granularity 'nonexistent' not found"),
381+
"unexpected error: {}",
382+
err.message
383+
);
384+
}
385+
}

rust/cubesqlplanner/cubesqlplanner/src/test_fixtures/cube_bridge/mock_granularity_definition.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ use typed_builder::TypedBuilder;
1212
pub struct MockGranularityDefinition {
1313
#[builder(setter(into))]
1414
interval: String,
15-
#[builder(default, setter(strip_option, into))]
15+
#[builder(default, setter(strip_option(fallback = origin_opt), into))]
1616
origin: Option<String>,
17-
#[builder(default, setter(strip_option, into))]
17+
#[builder(default, setter(strip_option(fallback = offset_opt), into))]
1818
offset: Option<String>,
1919
#[builder(default, setter(strip_option))]
2020
sql: Option<Rc<dyn MemberSql>>,

0 commit comments

Comments
 (0)