1use crate::{
18 analyze_relational_join_conflicts,
19 relational_join_cost::{RelationalJoinRightInput, RelationalJoinSelectivity},
20 relational_join_rewrite::validate_problem_relations,
21 PhysicalProperties, PlanCost, PlanCostBreakdown, RelationalJoinAccessPath,
22 RelationalJoinConflictAnalysis, RelationalJoinEnumerationConfig, RelationalJoinOperatorId,
23 RelationalJoinOperatorKind, RelationalJoinPredicateId, RelationalJoinRewriteError,
24 RelationalJoinRewriteProblem, RequiredProperties,
25};
26use hawdb_expression::{BindingId, BindingSet};
27
28#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
34pub enum RelationalCsgCmpRightInputPolicy {
35 #[default]
36 AllowMaterialized,
37 ProbeOnly,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
41pub enum RelationalEquiJoinAlgorithm {
42 Hash,
43 Merge,
44}
45
46impl RelationalEquiJoinAlgorithm {
47 pub const fn right_input(self) -> RelationalJoinRightInput {
48 match self {
49 Self::Hash => RelationalJoinRightInput::Hash,
50 Self::Merge => RelationalJoinRightInput::Merge,
51 }
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct RelationalCsgCmpJoinImplementation {
60 pub operator_id: RelationalJoinOperatorId,
61 pub operator_kind: RelationalJoinOperatorKind,
62 pub predicate_ids: Vec<RelationalJoinPredicateId>,
63 pub left_binding: BindingId,
64 pub left_access: RelationalJoinAccessPath,
65 pub right_binding: BindingId,
66 pub right_access: RelationalJoinAccessPath,
67 pub algorithm: RelationalEquiJoinAlgorithm,
68 pub selectivity: RelationalJoinSelectivity,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct RelationalCsgCmpAlternative {
73 pub left_bindings: BindingSet,
74 pub right_bindings: BindingSet,
75 pub operator_id: RelationalJoinOperatorId,
76 pub operator_kind: RelationalJoinOperatorKind,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum RelationalCsgCmpPlanNode {
81 Relation {
82 binding: BindingId,
83 access_path: RelationalJoinAccessPath,
84 },
85 Join {
86 operator_id: RelationalJoinOperatorId,
87 operator_kind: RelationalJoinOperatorKind,
88 predicate_ids: Vec<RelationalJoinPredicateId>,
89 implementation: Option<Box<RelationalCsgCmpJoinImplementation>>,
90 left: Box<Self>,
91 right: Box<Self>,
92 },
93}
94
95impl RelationalCsgCmpPlanNode {
96 pub fn bindings(&self) -> BindingSet {
97 match self {
98 Self::Relation { binding, .. } => (*binding).into(),
99 Self::Join { left, right, .. } => binding_union(&left.bindings(), &right.bindings()),
100 }
101 }
102
103 pub fn has_materialized_right(&self) -> bool {
104 match self {
105 Self::Relation { .. } => false,
106 Self::Join { left, right, .. } => {
107 matches!(right.as_ref(), Self::Join { .. })
108 || left.has_materialized_right()
109 || right.has_materialized_right()
110 }
111 }
112 }
113}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct RelationalCsgCmpPlan {
117 pub root: RelationalCsgCmpPlanNode,
118 pub cost_breakdown: PlanCostBreakdown,
119 pub properties: PhysicalProperties,
120}
121
122impl RelationalCsgCmpPlan {
123 pub fn cost(&self) -> PlanCost {
124 self.cost_breakdown.as_plan_cost()
125 }
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct RelationalCsgCmpEnumeration {
130 pub plan: RelationalCsgCmpPlan,
131 pub conflict_analysis: RelationalJoinConflictAnalysis,
132 pub alternatives: Vec<RelationalCsgCmpAlternative>,
133 pub memo_groups: usize,
134 pub memo_expressions: usize,
135 pub root_alternatives: usize,
136}
137
138mod memo;
139pub(crate) use memo::{enumerate_inner_graph, enumerate_left_deep_rewrites};
140
141pub fn enumerate_relational_csg_cmp_joins(
142 problem: &RelationalJoinRewriteProblem,
143 required_properties: &RequiredProperties,
144 config: RelationalJoinEnumerationConfig,
145) -> Result<RelationalCsgCmpEnumeration, RelationalJoinRewriteError> {
146 enumerate_relational_csg_cmp_joins_with_right_input_policy(
147 problem,
148 required_properties,
149 config,
150 RelationalCsgCmpRightInputPolicy::default(),
151 )
152}
153
154pub fn enumerate_relational_csg_cmp_joins_with_right_input_policy(
155 problem: &RelationalJoinRewriteProblem,
156 required_properties: &RequiredProperties,
157 config: RelationalJoinEnumerationConfig,
158 right_input_policy: RelationalCsgCmpRightInputPolicy,
159) -> Result<RelationalCsgCmpEnumeration, RelationalJoinRewriteError> {
160 enumerate_relational_csg_cmp_joins_with_implementations(
161 problem,
162 required_properties,
163 config,
164 right_input_policy,
165 &[],
166 )
167}
168
169pub fn enumerate_relational_csg_cmp_joins_with_implementations(
170 problem: &RelationalJoinRewriteProblem,
171 required_properties: &RequiredProperties,
172 config: RelationalJoinEnumerationConfig,
173 right_input_policy: RelationalCsgCmpRightInputPolicy,
174 implementations: &[RelationalCsgCmpJoinImplementation],
175) -> Result<RelationalCsgCmpEnumeration, RelationalJoinRewriteError> {
176 let analysis = analyze_relational_join_conflicts(
177 &problem.initial_tree,
178 problem.post_join_filter.as_ref(),
179 )?;
180 validate_problem_relations(problem, &analysis)?;
181 memo::enumerate_csg_cmp(
182 problem,
183 analysis,
184 required_properties,
185 config,
186 right_input_policy,
187 implementations,
188 )
189}
190
191fn binding_union(left: &BindingSet, right: &BindingSet) -> BindingSet {
192 left.iter().chain(right.iter()).collect()
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 mod implementations;
199 mod memo_contracts;
200 use crate::{
201 RelationalAccessPathDescriptor, RelationalAccessPathKind, RelationalJoinEnumerationError,
202 RelationalJoinOperator, RelationalJoinRelation, RelationalJoinTree,
203 };
204 use hawdb_expression::{BoundPredicate, BoundScalarExpression, ScalarNullability};
205 use std::collections::BTreeSet;
206
207 const A: BindingId = BindingId::new(1);
208 const B: BindingId = BindingId::new(2);
209 const C: BindingId = BindingId::new(3);
210 const D: BindingId = BindingId::new(4);
211
212 fn value(binding: BindingId) -> BoundScalarExpression {
213 BoundScalarExpression::BindingValue {
214 binding,
215 nullability: ScalarNullability::MaybeNull,
216 }
217 }
218
219 fn predicate(bindings: &[BindingId]) -> BoundPredicate {
220 BoundPredicate::And(
221 bindings
222 .windows(2)
223 .map(|pair| BoundPredicate::Comparison {
224 left: value(pair[0]),
225 right: value(pair[1]),
226 })
227 .collect(),
228 )
229 }
230
231 fn operator(
232 id: u32,
233 kind: RelationalJoinOperatorKind,
234 bindings: &[BindingId],
235 ) -> RelationalJoinOperator {
236 RelationalJoinOperator {
237 id: RelationalJoinOperatorId::new(id),
238 kind,
239 predicate_ids: vec![RelationalJoinPredicateId::new(id)],
240 predicate: predicate(bindings),
241 }
242 }
243
244 fn relation(binding: BindingId, rows: usize) -> RelationalJoinRelation {
245 RelationalJoinRelation {
246 binding,
247 access_paths: vec![RelationalJoinAccessPath::base_and_probe(
248 RelationalAccessPathDescriptor {
249 kind: RelationalAccessPathKind::FullScan,
250 name: "__full_scan".to_string(),
251 index_columns: Vec::new(),
252 access_columns: BTreeSet::new(),
253 equality_prefix_len: 0,
254 order_prefix_len: 0,
255 exclusive_range: false,
256 reverse_order: false,
257 unique_point: false,
258 covering: false,
259 requires_row_fetch: false,
260 estimated_rows: rows,
261 },
262 )],
263 }
264 }
265
266 fn indexed_relation(
267 binding: BindingId,
268 rows: usize,
269 required: BindingId,
270 ) -> RelationalJoinRelation {
271 let mut relation = relation(binding, rows);
272 relation.access_paths.push(RelationalJoinAccessPath::probe(
273 RelationalAccessPathDescriptor {
274 kind: RelationalAccessPathKind::Index,
275 name: "by_outer".to_string(),
276 index_columns: vec!["id".to_string()],
277 access_columns: BTreeSet::from(["id".to_string()]),
278 equality_prefix_len: 1,
279 order_prefix_len: 0,
280 exclusive_range: false,
281 reverse_order: false,
282 unique_point: true,
283 covering: false,
284 requires_row_fetch: true,
285 estimated_rows: 1,
286 },
287 required.into(),
288 ));
289 relation
290 }
291
292 fn problem(
293 relations: &[BindingId],
294 tree: RelationalJoinTree,
295 filter: Option<BoundPredicate>,
296 ) -> RelationalJoinRewriteProblem {
297 RelationalJoinRewriteProblem {
298 relations: relations
299 .iter()
300 .enumerate()
301 .map(|(index, binding)| relation(*binding, (index + 1) * 10))
302 .collect(),
303 initial_tree: tree,
304 post_join_filter: filter,
305 }
306 }
307
308 #[test]
309 fn csg_cmp_enumerates_bushy_hypergraph_partitions_into_one_semantic_group() {
310 let left = RelationalJoinTree::join(
311 operator(1, RelationalJoinOperatorKind::Inner, &[A, B]),
312 RelationalJoinTree::Relation(A),
313 RelationalJoinTree::Relation(B),
314 );
315 let right = RelationalJoinTree::join(
316 operator(2, RelationalJoinOperatorKind::Inner, &[C, D]),
317 RelationalJoinTree::Relation(C),
318 RelationalJoinTree::Relation(D),
319 );
320 let tree = RelationalJoinTree::join(
321 operator(3, RelationalJoinOperatorKind::Inner, &[B, C]),
322 left,
323 right,
324 );
325 let result = enumerate_relational_csg_cmp_joins(
326 &problem(&[A, B, C, D], tree, None),
327 &RequiredProperties::default(),
328 RelationalJoinEnumerationConfig::default(),
329 )
330 .unwrap();
331
332 assert_eq!(result.plan.root.bindings(), BindingSet::from([A, B, C, D]));
333 assert!(result.root_alternatives > 1);
334 assert!(result.alternatives.iter().any(|alternative| {
335 alternative.operator_id == RelationalJoinOperatorId::new(3)
336 && alternative.left_bindings == BindingSet::from([A, B])
337 && alternative.right_bindings == BindingSet::from([C, D])
338 }));
339 }
340
341 #[test]
342 fn csg_cmp_probe_only_policy_excludes_unbounded_materialization() {
343 let left = RelationalJoinTree::join(
344 operator(1, RelationalJoinOperatorKind::Inner, &[A, B]),
345 RelationalJoinTree::Relation(A),
346 RelationalJoinTree::Relation(B),
347 );
348 let right = RelationalJoinTree::join(
349 operator(2, RelationalJoinOperatorKind::Inner, &[C, D]),
350 RelationalJoinTree::Relation(C),
351 RelationalJoinTree::Relation(D),
352 );
353 let tree = RelationalJoinTree::join(
354 operator(3, RelationalJoinOperatorKind::Inner, &[B, C]),
355 left,
356 right,
357 );
358
359 let result = enumerate_relational_csg_cmp_joins_with_right_input_policy(
360 &problem(&[A, B, C, D], tree, None),
361 &RequiredProperties::default(),
362 RelationalJoinEnumerationConfig::default(),
363 RelationalCsgCmpRightInputPolicy::ProbeOnly,
364 )
365 .expect("probe-only CSG-CMP can choose a left-deep alternative");
366
367 assert!(!result.plan.root.has_materialized_right());
368 }
369
370 #[test]
371 fn csg_cmp_costing_uses_a_singleton_index_probe_from_the_outer_group() {
372 let tree = RelationalJoinTree::join(
373 operator(1, RelationalJoinOperatorKind::Inner, &[A, B]),
374 RelationalJoinTree::Relation(A),
375 RelationalJoinTree::Relation(B),
376 );
377 let problem = RelationalJoinRewriteProblem {
378 relations: vec![relation(A, 1), indexed_relation(B, 1_000, A)],
379 initial_tree: tree,
380 post_join_filter: None,
381 };
382 let result = enumerate_relational_csg_cmp_joins(
383 &problem,
384 &RequiredProperties::default(),
385 RelationalJoinEnumerationConfig::default(),
386 )
387 .unwrap();
388
389 assert_eq!(
390 result.plan.cost(),
391 PlanCost {
392 estimated_rows: 1,
393 cost: 14,
394 }
395 );
396 let RelationalCsgCmpPlanNode::Join { left, right, .. } = result.plan.root else {
397 panic!("expected costed join root");
398 };
399 assert_eq!(left.bindings(), A.into());
400 let RelationalCsgCmpPlanNode::Relation { access_path, .. } = *right else {
401 panic!("expected singleton probe on the right");
402 };
403 assert_eq!(access_path.descriptor.name, "by_outer");
404 }
405
406 #[test]
407 fn csg_cmp_probe_prefers_a_cheaper_scan_over_a_longer_equality_prefix() {
408 let tree = RelationalJoinTree::join(
409 operator(1, RelationalJoinOperatorKind::Inner, &[A, B]),
410 RelationalJoinTree::Relation(A),
411 RelationalJoinTree::Relation(B),
412 );
413 let mut inner = relation(B, 10);
414 inner.access_paths.push(RelationalJoinAccessPath::probe(
415 RelationalAccessPathDescriptor {
416 kind: RelationalAccessPathKind::Index,
417 name: "by_outer".to_string(),
418 index_columns: vec!["outer_id".to_string()],
419 access_columns: BTreeSet::from(["outer_id".to_string()]),
420 equality_prefix_len: 1,
421 order_prefix_len: 0,
422 exclusive_range: false,
423 reverse_order: false,
424 unique_point: false,
425 covering: false,
426 requires_row_fetch: true,
427 estimated_rows: 10,
428 },
429 A.into(),
430 ));
431 let problem = RelationalJoinRewriteProblem {
432 relations: vec![relation(A, 1), inner],
433 initial_tree: tree,
434 post_join_filter: None,
435 };
436
437 let result = enumerate_relational_csg_cmp_joins(
438 &problem,
439 &RequiredProperties::default(),
440 RelationalJoinEnumerationConfig::default(),
441 )
442 .unwrap();
443
444 let RelationalCsgCmpPlanNode::Join { right, .. } = result.plan.root else {
445 panic!("expected a join root");
446 };
447 let RelationalCsgCmpPlanNode::Relation { access_path, .. } = *right else {
448 panic!("expected a singleton probe");
449 };
450 assert_eq!(access_path.descriptor.name, "__full_scan");
451 }
452
453 #[test]
454 fn multi_relation_hyperedge_waits_for_its_complete_endpoint() {
455 let lower = RelationalJoinTree::join(
456 operator(1, RelationalJoinOperatorKind::Inner, &[A, B]),
457 RelationalJoinTree::Relation(A),
458 RelationalJoinTree::Relation(B),
459 );
460 let tree = RelationalJoinTree::join(
461 operator(2, RelationalJoinOperatorKind::Inner, &[A, B, C]),
462 lower,
463 RelationalJoinTree::Relation(C),
464 );
465 let result = enumerate_relational_csg_cmp_joins(
466 &problem(&[A, B, C], tree, None),
467 &RequiredProperties::default(),
468 RelationalJoinEnumerationConfig::default(),
469 )
470 .unwrap();
471
472 assert!(result
473 .alternatives
474 .iter()
475 .filter(|alternative| alternative.operator_id == RelationalJoinOperatorId::new(2))
476 .all(|alternative| {
477 alternative.left_bindings == BindingSet::from([A, B])
478 || alternative.right_bindings == BindingSet::from([A, B])
479 }));
480 }
481
482 #[test]
483 fn csg_cmp_never_inserts_the_illegal_outer_join_association() {
484 let lower = RelationalJoinTree::join(
485 operator(1, RelationalJoinOperatorKind::LeftOuter, &[A, B]),
486 RelationalJoinTree::Relation(A),
487 RelationalJoinTree::Relation(B),
488 );
489 let tree = RelationalJoinTree::join(
490 operator(2, RelationalJoinOperatorKind::Inner, &[B, C]),
491 lower,
492 RelationalJoinTree::Relation(C),
493 );
494 let result = enumerate_relational_csg_cmp_joins(
495 &problem(&[A, B, C], tree, None),
496 &RequiredProperties::default(),
497 RelationalJoinEnumerationConfig::default(),
498 )
499 .unwrap();
500
501 assert!(!result.alternatives.iter().any(|alternative| {
502 alternative.operator_id == RelationalJoinOperatorId::new(2)
503 && binding_union(&alternative.left_bindings, &alternative.right_bindings)
504 == BindingSet::from([B, C])
505 }));
506 }
507
508 #[test]
509 fn null_rejection_opens_the_inner_hyperedge_only_after_outer_simplification() {
510 let lower = RelationalJoinTree::join(
511 operator(1, RelationalJoinOperatorKind::LeftOuter, &[A, B]),
512 RelationalJoinTree::Relation(A),
513 RelationalJoinTree::Relation(B),
514 );
515 let tree = RelationalJoinTree::join(
516 operator(2, RelationalJoinOperatorKind::Inner, &[B, C]),
517 lower,
518 RelationalJoinTree::Relation(C),
519 );
520 let filter = BoundPredicate::IsNotNull(value(B));
521 let result = enumerate_relational_csg_cmp_joins(
522 &problem(&[A, B, C], tree, Some(filter)),
523 &RequiredProperties::default(),
524 RelationalJoinEnumerationConfig::default(),
525 )
526 .unwrap();
527
528 assert!(result.alternatives.iter().any(|alternative| {
529 alternative.operator_id == RelationalJoinOperatorId::new(2)
530 && binding_union(&alternative.left_bindings, &alternative.right_bindings)
531 == BindingSet::from([B, C])
532 }));
533 }
534}