Skip to content

Commit d86ab6f

Browse files
authored
chore(tesseract): resolve join trees once per query (#10982)
1 parent 181753f commit d86ab6f

14 files changed

Lines changed: 278 additions & 118 deletions

rust/cube/cubesqlplanner/cubesqlplanner/src/planner/collectors/multiplied_measures_collector.rs

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
use crate::cube_bridge::join_definition::JoinDefinition;
2-
use crate::planner::{MemberSymbol, TraversalVisitor};
1+
use crate::planner::{JoinTree, MemberSymbol, TraversalVisitor};
32
use cubenativeutils::CubeError;
43
use std::collections::HashSet;
54
use std::rc::Rc;
@@ -67,11 +66,11 @@ pub struct MeasureResult {
6766
pub struct MultipliedMeasuresCollector {
6867
composite_measures: HashSet<String>,
6968
colllected_measures: Vec<MeasureResult>,
70-
join: Rc<dyn JoinDefinition>,
69+
join: Rc<JoinTree>,
7170
}
7271

7372
impl MultipliedMeasuresCollector {
74-
pub fn new(composite_measures: HashSet<String>, join: Rc<dyn JoinDefinition>) -> Self {
73+
pub fn new(composite_measures: HashSet<String>, join: Rc<JoinTree>) -> Self {
7574
Self {
7675
composite_measures,
7776
join,
@@ -94,13 +93,7 @@ impl TraversalVisitor for MultipliedMeasuresCollector {
9493
let res = match node.as_ref() {
9594
MemberSymbol::Measure(e) => {
9695
let full_name = e.full_name();
97-
let multiplied = self
98-
.join
99-
.static_data()
100-
.multiplication_factor
101-
.get(&e.cube_name())
102-
.unwrap_or(&false)
103-
.clone();
96+
let multiplied = self.join.is_multiplied(&e.cube_name());
10497

10598
if !self.composite_measures.contains(&full_name) {
10699
self.colllected_measures.push(MeasureResult {
@@ -126,7 +119,7 @@ impl TraversalVisitor for MultipliedMeasuresCollector {
126119

127120
pub fn collect_multiplied_measures(
128121
node: &Rc<MemberSymbol>,
129-
join: Rc<dyn JoinDefinition>,
122+
join: &Rc<JoinTree>,
130123
) -> Result<Vec<MeasureResult>, CubeError> {
131124
if let Ok(member_expression) = node.as_member_expression() {
132125
if let Some(cube_names) = member_expression.cube_names_if_dimension_only_expression()? {
@@ -138,12 +131,7 @@ pub fn collect_multiplied_measures(
138131
}]
139132
} else if cube_names.len() == 1 {
140133
let cube_name = cube_names[0].clone();
141-
let multiplied = join
142-
.static_data()
143-
.multiplication_factor
144-
.get(&cube_name)
145-
.unwrap_or(&false)
146-
.clone();
134+
let multiplied = join.is_multiplied(&cube_name);
147135

148136
vec![MeasureResult {
149137
measure: node.clone(),

rust/cube/cubesqlplanner/cubesqlplanner/src/planner/collectors/sub_query_dimensions.rs

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
use crate::cube_bridge::join_definition::JoinDefinition;
2-
use crate::planner::planners::JoinPlanner;
3-
use crate::planner::{DimensionSymbol, MemberSymbol, TraversalVisitor};
1+
use crate::planner::{DimensionSymbol, JoinTree, MemberSymbol, TraversalVisitor};
42
use cubenativeutils::CubeError;
53
use itertools::Itertools;
64
use std::rc::Rc;
@@ -61,24 +59,21 @@ impl TraversalVisitor for SubQueryDimensionsCollector {
6159

6260
pub fn collect_sub_query_dimensions_from_members(
6361
members: &Vec<Rc<MemberSymbol>>,
64-
join_planner: &JoinPlanner,
65-
join: &Rc<dyn JoinDefinition>,
62+
join: &JoinTree,
6663
) -> Result<Vec<Rc<MemberSymbol>>, CubeError> {
67-
collect_sub_query_dimensions_from_symbols(&members, join_planner, join)
64+
collect_sub_query_dimensions_from_symbols(members, join)
6865
}
6966

7067
pub fn collect_sub_query_dimensions_from_symbols(
7168
members: &Vec<Rc<MemberSymbol>>,
72-
join_planner: &JoinPlanner,
73-
join: &Rc<dyn JoinDefinition>,
69+
join: &JoinTree,
7470
) -> Result<Vec<Rc<MemberSymbol>>, CubeError> {
7571
let mut visitor = SubQueryDimensionsCollector::new();
7672
for member in members.iter() {
77-
visitor.apply(&member, &())?;
73+
visitor.apply(member, &())?;
7874
}
79-
for join_item in join.joins()? {
80-
let condition = join_planner.compile_join_condition(join_item.clone())?;
81-
for dep in condition.get_dependencies() {
75+
for join_item in join.joins() {
76+
for dep in join_item.on_sql().get_dependencies() {
8277
visitor.apply(&dep, &())?;
8378
}
8479
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use crate::planner::BaseCube;
2+
use crate::planner::SqlCall;
3+
use std::collections::HashMap;
4+
use std::rc::Rc;
5+
6+
/// One non-root cube of a `JoinTree` with its parent cube and the
7+
/// compiled ON SQL connecting them. `original_from` is the parent in
8+
/// the join graph; the cube itself is the `original_to`.
9+
pub struct JoinTreeItem {
10+
cube: Rc<BaseCube>,
11+
original_from: String,
12+
on_sql: Rc<SqlCall>,
13+
}
14+
15+
impl JoinTreeItem {
16+
pub fn new(cube: Rc<BaseCube>, original_from: String, on_sql: Rc<SqlCall>) -> Self {
17+
Self {
18+
cube,
19+
original_from,
20+
on_sql,
21+
}
22+
}
23+
24+
pub fn cube(&self) -> &Rc<BaseCube> {
25+
&self.cube
26+
}
27+
28+
pub fn original_from(&self) -> &str {
29+
&self.original_from
30+
}
31+
32+
pub fn on_sql(&self) -> &Rc<SqlCall> {
33+
&self.on_sql
34+
}
35+
}
36+
37+
/// A resolved join tree: the root cube plus its joined cubes with the
38+
/// ON SQL already compiled into `SqlCall`. Built once from a
39+
/// `JoinDefinition` so the rest of planning can assemble `LogicalJoin`
40+
/// nodes (and collect sub-query dimensions) without recompiling the
41+
/// join conditions on every use.
42+
pub struct JoinTree {
43+
root: Rc<BaseCube>,
44+
joins: Vec<JoinTreeItem>,
45+
multiplication_factor: HashMap<String, bool>,
46+
}
47+
48+
impl JoinTree {
49+
pub fn new(
50+
root: Rc<BaseCube>,
51+
joins: Vec<JoinTreeItem>,
52+
multiplication_factor: HashMap<String, bool>,
53+
) -> Rc<Self> {
54+
Rc::new(Self {
55+
root,
56+
joins,
57+
multiplication_factor,
58+
})
59+
}
60+
61+
pub fn root(&self) -> &Rc<BaseCube> {
62+
&self.root
63+
}
64+
65+
pub fn joins(&self) -> &Vec<JoinTreeItem> {
66+
&self.joins
67+
}
68+
69+
/// Whether joining `cube_name` into this tree multiplies its rows.
70+
pub fn is_multiplied(&self, cube_name: &str) -> bool {
71+
self.multiplication_factor
72+
.get(cube_name)
73+
.copied()
74+
.unwrap_or(false)
75+
}
76+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use crate::planner::join_hints::JoinHints;
2+
use crate::planner::query_tools::JoinKey;
3+
use crate::planner::JoinTree;
4+
use cubenativeutils::CubeError;
5+
use std::cell::RefCell;
6+
use std::collections::HashMap;
7+
use std::rc::Rc;
8+
9+
/// Per-query cache of join trees keyed by the `JoinHints` that produced
10+
/// them. The join graph is immutable within a `QueryTools` lifetime, so
11+
/// the same hints always resolve to the same join — caching avoids
12+
/// re-crossing the JS bridge and recompiling ON SQL when the same hints
13+
/// are resolved repeatedly (e.g. once per pre-aggregation candidate).
14+
#[derive(Default)]
15+
pub struct JoinTreeCache {
16+
by_hints: RefCell<HashMap<JoinHints, (JoinKey, Rc<JoinTree>)>>,
17+
}
18+
19+
impl JoinTreeCache {
20+
/// Returns the cached `(JoinKey, JoinTree)` for `hints`, building and
21+
/// storing it via `build` on a miss. `build` is supplied per call so
22+
/// the cache holds no reference back to `QueryTools`.
23+
pub fn get_or_build(
24+
&self,
25+
hints: &JoinHints,
26+
build: impl FnOnce() -> Result<(JoinKey, Rc<JoinTree>), CubeError>,
27+
) -> Result<(JoinKey, Rc<JoinTree>), CubeError> {
28+
// The lookup borrow is dropped before `build` runs and re-acquired for
29+
// the insert, so `build` may itself call back into the cache. Do not
30+
// fold this into a single `entry()` call — that would hold the borrow
31+
// across `build` and panic on any re-entrant access.
32+
if let Some(cached) = self.by_hints.borrow().get(hints) {
33+
return Ok(cached.clone());
34+
}
35+
let built = build()?;
36+
self.by_hints
37+
.borrow_mut()
38+
.insert(hints.clone(), built.clone());
39+
Ok(built)
40+
}
41+
}

rust/cube/cubesqlplanner/cubesqlplanner/src/planner/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ pub mod collectors;
55
pub mod compiler;
66
pub mod filter;
77
pub mod join_hints;
8+
pub mod join_tree;
9+
pub mod join_tree_cache;
810
pub mod multi_fact_join_groups;
911
pub mod sql_call;
1012
mod sql_call_builder;
@@ -28,6 +30,8 @@ pub use base_join_condition::{BaseJoinCondition, SqlJoinCondition};
2830
pub use base_query::BaseQuery;
2931
pub use compiler::Compiler;
3032
pub use join_hints::JoinHints;
33+
pub use join_tree::{JoinTree, JoinTreeItem};
34+
pub use join_tree_cache::JoinTreeCache;
3135
pub use params_allocator::ParamsAllocator;
3236
pub use query_properties::{FullKeyAggregateMeasures, OrderByItem, QueryProperties};
3337
pub use query_properties_compiler::QueryPropertiesCompiler;

rust/cube/cubesqlplanner/cubesqlplanner/src/planner/multi_fact_join_groups.rs

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
use crate::cube_bridge::join_definition::JoinDefinition;
21
use crate::planner::collectors::{collect_join_hints, has_multi_stage_members};
32
use crate::planner::filter::FilterItem;
43
use crate::planner::join_hints::JoinHints;
4+
use crate::planner::planners::JoinTreeBuilder;
55
use crate::planner::query_tools::JoinKey;
66
use crate::planner::query_tools::QueryTools;
7+
use crate::planner::JoinTree;
78
use crate::planner::MemberSymbol;
89
use cubenativeutils::CubeError;
910
use itertools::Itertools;
@@ -148,7 +149,7 @@ impl MeasuresJoinHints {
148149
pub struct MultiFactJoinGroups {
149150
query_tools: Rc<QueryTools>,
150151
measures_join_hints: MeasuresJoinHints,
151-
groups: Vec<(Rc<dyn JoinDefinition>, Vec<Rc<MemberSymbol>>)>,
152+
groups: Vec<(Rc<JoinTree>, Vec<Rc<MemberSymbol>>)>,
152153
/// cube_name → join path from root, computed from the first group (shared for dimensions).
153154
dimension_paths: HashMap<String, Vec<String>>,
154155
/// measure full_name → join path from root, computed per group.
@@ -163,7 +164,7 @@ impl MultiFactJoinGroups {
163164
measures_join_hints: MeasuresJoinHints,
164165
) -> Result<Self, CubeError> {
165166
let groups = Self::build_groups(&query_tools, &measures_join_hints)?;
166-
let (dimension_paths, measure_paths) = Self::precompute_paths(&groups)?;
167+
let (dimension_paths, measure_paths) = Self::precompute_paths(&groups);
167168
Ok(Self {
168169
query_tools,
169170
measures_join_hints,
@@ -183,34 +184,41 @@ impl MultiFactJoinGroups {
183184
fn build_groups(
184185
query_tools: &Rc<QueryTools>,
185186
hints: &MeasuresJoinHints,
186-
) -> Result<Vec<(Rc<dyn JoinDefinition>, Vec<Rc<MemberSymbol>>)>, CubeError> {
187+
) -> Result<Vec<(Rc<JoinTree>, Vec<Rc<MemberSymbol>>)>, CubeError> {
188+
let join_tree_builder = JoinTreeBuilder::new(query_tools.clone());
189+
let resolve = |join_hints: &JoinHints| -> Result<(JoinKey, Rc<JoinTree>), CubeError> {
190+
query_tools.join_tree_cache().get_or_build(join_hints, || {
191+
let (key, join) = query_tools.join_for_hints(join_hints)?;
192+
Ok((key, join_tree_builder.build(join)?))
193+
})
194+
};
195+
187196
let measures_to_join = if hints.measure_hints.is_empty() {
188197
if hints.base_hints.is_empty() {
189198
vec![]
190199
} else {
191-
let (key, join) = query_tools.join_for_hints(&hints.base_hints)?;
192-
vec![(Vec::new(), key, join)]
200+
let (key, join_tree) = resolve(&hints.base_hints)?;
201+
vec![(Vec::new(), key, join_tree)]
193202
}
194203
} else {
195204
hints
196205
.measure_hints
197206
.iter()
198207
.map(|mh| -> Result<_, CubeError> {
199-
let (key, join) = query_tools.join_for_hints(&mh.hints)?;
200-
Ok((vec![mh.measure.clone()], key, join))
208+
let (key, join_tree) = resolve(&mh.hints)?;
209+
Ok((vec![mh.measure.clone()], key, join_tree))
201210
})
202211
.collect::<Result<Vec<_>, _>>()?
203212
};
204213

205214
let mut key_order: Vec<JoinKey> = Vec::new();
206-
let mut grouped: HashMap<JoinKey, (Rc<dyn JoinDefinition>, Vec<Rc<MemberSymbol>>)> =
207-
HashMap::new();
208-
for (measures, key, join) in measures_to_join {
215+
let mut grouped: HashMap<JoinKey, (Rc<JoinTree>, Vec<Rc<MemberSymbol>>)> = HashMap::new();
216+
for (measures, key, join_tree) in measures_to_join {
209217
if let Some(entry) = grouped.get_mut(&key) {
210218
entry.1.extend(measures);
211219
} else {
212220
key_order.push(key.clone());
213-
grouped.insert(key, (join, measures));
221+
grouped.insert(key, (join_tree, measures));
214222
}
215223
}
216224

@@ -229,7 +237,7 @@ impl MultiFactJoinGroups {
229237
self.groups.len() > 1
230238
}
231239

232-
pub fn groups(&self) -> &[(Rc<dyn JoinDefinition>, Vec<Rc<MemberSymbol>>)] {
240+
pub fn groups(&self) -> &[(Rc<JoinTree>, Vec<Rc<MemberSymbol>>)] {
233241
&self.groups
234242
}
235243

@@ -258,55 +266,54 @@ impl MultiFactJoinGroups {
258266
}
259267

260268
fn precompute_paths(
261-
groups: &[(Rc<dyn JoinDefinition>, Vec<Rc<MemberSymbol>>)],
262-
) -> Result<(HashMap<String, Vec<String>>, HashMap<String, Vec<String>>), CubeError> {
269+
groups: &[(Rc<JoinTree>, Vec<Rc<MemberSymbol>>)],
270+
) -> (HashMap<String, Vec<String>>, HashMap<String, Vec<String>>) {
263271
let dimension_paths = if groups.is_empty() {
264272
HashMap::new()
265273
} else {
266-
Self::build_cube_paths(&*groups[0].0)?
274+
Self::build_cube_paths(&groups[0].0)
267275
};
268276

269277
let mut measure_paths = HashMap::new();
270278
for (join, measures) in groups {
271279
if measures.is_empty() {
272280
continue;
273281
}
274-
let cube_paths = Self::build_cube_paths(&**join)?;
282+
let cube_paths = Self::build_cube_paths(join);
275283
for m in measures {
276284
if let Some(path) = cube_paths.get(&m.cube_name()) {
277285
measure_paths.insert(m.full_name(), path.clone());
278286
}
279287
}
280288
}
281289

282-
Ok((dimension_paths, measure_paths))
290+
(dimension_paths, measure_paths)
283291
}
284292

285-
fn build_cube_paths(
286-
join: &dyn JoinDefinition,
287-
) -> Result<HashMap<String, Vec<String>>, CubeError> {
288-
let root = join.static_data().root.clone();
293+
fn build_cube_paths(join: &JoinTree) -> HashMap<String, Vec<String>> {
294+
let root = join.root().name().clone();
289295
let mut paths: HashMap<String, Vec<String>> = HashMap::new();
290296
paths.insert(root.clone(), vec![root]);
291297

292-
for join_item in join.joins()? {
293-
let sd = join_item.static_data();
298+
for join_item in join.joins() {
299+
let original_from = join_item.original_from().to_string();
300+
let original_to = join_item.cube().name().clone();
294301
let parent_path = paths
295-
.get(&sd.original_from)
302+
.get(&original_from)
296303
.cloned()
297-
.unwrap_or_else(|| vec![sd.original_from.clone()]);
304+
.unwrap_or_else(|| vec![original_from]);
298305
let mut path = parent_path;
299-
path.push(sd.original_to.clone());
300-
paths.insert(sd.original_to.clone(), path);
306+
path.push(original_to.clone());
307+
paths.insert(original_to, path);
301308
}
302309

303-
Ok(paths)
310+
paths
304311
}
305312

306313
/// The only join when the query has exactly one group; errors if
307314
/// the query is multi-fact, and `Ok(None)` if it has no groups
308315
/// at all.
309-
pub fn single_join(&self) -> Result<Option<Rc<dyn JoinDefinition>>, CubeError> {
316+
pub fn single_join(&self) -> Result<Option<Rc<JoinTree>>, CubeError> {
310317
if self.groups.is_empty() {
311318
return Ok(None);
312319
}

0 commit comments

Comments
 (0)