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 enumerate_relational_csg_cmp_joins_with_cost_contexts(
177 problem,
178 required_properties,
179 config,
180 right_input_policy,
181 implementations,
182 &crate::RelationalJoinCostContexts::default(),
183 )
184}
185
186pub fn enumerate_relational_csg_cmp_joins_with_cost_contexts(
187 problem: &RelationalJoinRewriteProblem,
188 required_properties: &RequiredProperties,
189 config: RelationalJoinEnumerationConfig,
190 right_input_policy: RelationalCsgCmpRightInputPolicy,
191 implementations: &[RelationalCsgCmpJoinImplementation],
192 cost_contexts: &crate::RelationalJoinCostContexts,
193) -> Result<RelationalCsgCmpEnumeration, RelationalJoinRewriteError> {
194 let analysis = analyze_relational_join_conflicts(
195 &problem.initial_tree,
196 problem.post_join_filter.as_ref(),
197 )?;
198 validate_problem_relations(problem, &analysis)?;
199 memo::enumerate_csg_cmp(
200 problem,
201 analysis,
202 required_properties,
203 config,
204 right_input_policy,
205 implementations,
206 cost_contexts,
207 )
208}
209
210fn binding_union(left: &BindingSet, right: &BindingSet) -> BindingSet {
211 left.iter().chain(right.iter()).collect()
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 mod cost_contexts;
218 mod implementations;
219 mod memo_contracts;
220 use crate::{
221 RelationalAccessPathDescriptor, RelationalAccessPathKind, RelationalJoinEnumerationError,
222 RelationalJoinOperator, RelationalJoinRelation, RelationalJoinTree,
223 };
224 use hawdb_expression::{BoundPredicate, BoundScalarExpression, ScalarNullability};
225 use std::collections::BTreeSet;
226
227 const A: BindingId = BindingId::new(1);
228 const B: BindingId = BindingId::new(2);
229 const C: BindingId = BindingId::new(3);
230 const D: BindingId = BindingId::new(4);
231
232 fn value(binding: BindingId) -> BoundScalarExpression {
233 BoundScalarExpression::BindingValue {
234 binding,
235 nullability: ScalarNullability::MaybeNull,
236 }
237 }
238
239 fn predicate(bindings: &[BindingId]) -> BoundPredicate {
240 BoundPredicate::And(
241 bindings
242 .windows(2)
243 .map(|pair| BoundPredicate::Comparison {
244 left: value(pair[0]),
245 right: value(pair[1]),
246 })
247 .collect(),
248 )
249 }
250
251 fn operator(
252 id: u32,
253 kind: RelationalJoinOperatorKind,
254 bindings: &[BindingId],
255 ) -> RelationalJoinOperator {
256 RelationalJoinOperator {
257 id: RelationalJoinOperatorId::new(id),
258 kind,
259 predicate_ids: vec![RelationalJoinPredicateId::new(id)],
260 predicate: predicate(bindings),
261 }
262 }
263
264 fn relation(binding: BindingId, rows: usize) -> RelationalJoinRelation {
265 RelationalJoinRelation {
266 binding,
267 access_paths: vec![RelationalJoinAccessPath::base_and_probe(
268 RelationalAccessPathDescriptor {
269 kind: RelationalAccessPathKind::FullScan,
270 name: "__full_scan".to_string(),
271 index_columns: Vec::new(),
272 access_columns: BTreeSet::new(),
273 equality_prefix_len: 0,
274 order_prefix_len: 0,
275 exclusive_range: false,
276 range: None,
277 reverse_order: false,
278 unique_point: false,
279 covering: false,
280 requires_row_fetch: false,
281 estimated_rows: rows,
282 },
283 )],
284 }
285 }
286
287 fn indexed_relation(
288 binding: BindingId,
289 rows: usize,
290 required: BindingId,
291 ) -> RelationalJoinRelation {
292 let mut relation = relation(binding, rows);
293 relation.access_paths.push(RelationalJoinAccessPath::probe(
294 RelationalAccessPathDescriptor {
295 kind: RelationalAccessPathKind::Index,
296 name: "by_outer".to_string(),
297 index_columns: vec!["id".to_string()],
298 access_columns: BTreeSet::from(["id".to_string()]),
299 equality_prefix_len: 1,
300 order_prefix_len: 0,
301 exclusive_range: false,
302 range: None,
303 reverse_order: false,
304 unique_point: true,
305 covering: false,
306 requires_row_fetch: true,
307 estimated_rows: 1,
308 },
309 required.into(),
310 ));
311 relation
312 }
313
314 fn problem(
315 relations: &[BindingId],
316 tree: RelationalJoinTree,
317 filter: Option<BoundPredicate>,
318 ) -> RelationalJoinRewriteProblem {
319 RelationalJoinRewriteProblem {
320 relations: relations
321 .iter()
322 .enumerate()
323 .map(|(index, binding)| relation(*binding, (index + 1) * 10))
324 .collect(),
325 initial_tree: tree,
326 post_join_filter: filter,
327 }
328 }
329
330 #[test]
331 fn csg_cmp_enumerates_bushy_hypergraph_partitions_into_one_semantic_group() {
332 let left = RelationalJoinTree::join(
333 operator(1, RelationalJoinOperatorKind::Inner, &[A, B]),
334 RelationalJoinTree::Relation(A),
335 RelationalJoinTree::Relation(B),
336 );
337 let right = RelationalJoinTree::join(
338 operator(2, RelationalJoinOperatorKind::Inner, &[C, D]),
339 RelationalJoinTree::Relation(C),
340 RelationalJoinTree::Relation(D),
341 );
342 let tree = RelationalJoinTree::join(
343 operator(3, RelationalJoinOperatorKind::Inner, &[B, C]),
344 left,
345 right,
346 );
347 let result = enumerate_relational_csg_cmp_joins(
348 &problem(&[A, B, C, D], tree, None),
349 &RequiredProperties::default(),
350 RelationalJoinEnumerationConfig::default(),
351 )
352 .unwrap();
353
354 assert_eq!(result.plan.root.bindings(), BindingSet::from([A, B, C, D]));
355 assert!(result.root_alternatives > 1);
356 assert!(result.alternatives.iter().any(|alternative| {
357 alternative.operator_id == RelationalJoinOperatorId::new(3)
358 && alternative.left_bindings == BindingSet::from([A, B])
359 && alternative.right_bindings == BindingSet::from([C, D])
360 }));
361 }
362
363 #[test]
364 fn csg_cmp_probe_only_policy_excludes_unbounded_materialization() {
365 let left = RelationalJoinTree::join(
366 operator(1, RelationalJoinOperatorKind::Inner, &[A, B]),
367 RelationalJoinTree::Relation(A),
368 RelationalJoinTree::Relation(B),
369 );
370 let right = RelationalJoinTree::join(
371 operator(2, RelationalJoinOperatorKind::Inner, &[C, D]),
372 RelationalJoinTree::Relation(C),
373 RelationalJoinTree::Relation(D),
374 );
375 let tree = RelationalJoinTree::join(
376 operator(3, RelationalJoinOperatorKind::Inner, &[B, C]),
377 left,
378 right,
379 );
380
381 let result = enumerate_relational_csg_cmp_joins_with_right_input_policy(
382 &problem(&[A, B, C, D], tree, None),
383 &RequiredProperties::default(),
384 RelationalJoinEnumerationConfig::default(),
385 RelationalCsgCmpRightInputPolicy::ProbeOnly,
386 )
387 .expect("probe-only CSG-CMP can choose a left-deep alternative");
388
389 assert!(!result.plan.root.has_materialized_right());
390 }
391
392 #[test]
393 fn csg_cmp_costing_uses_a_singleton_index_probe_from_the_outer_group() {
394 let tree = RelationalJoinTree::join(
395 operator(1, RelationalJoinOperatorKind::Inner, &[A, B]),
396 RelationalJoinTree::Relation(A),
397 RelationalJoinTree::Relation(B),
398 );
399 let problem = RelationalJoinRewriteProblem {
400 relations: vec![relation(A, 1), indexed_relation(B, 1_000, A)],
401 initial_tree: tree,
402 post_join_filter: None,
403 };
404 let result = enumerate_relational_csg_cmp_joins(
405 &problem,
406 &RequiredProperties::default(),
407 RelationalJoinEnumerationConfig::default(),
408 )
409 .unwrap();
410
411 assert_eq!(
412 result.plan.cost(),
413 PlanCost {
414 estimated_rows: 1,
415 cost: 14,
416 }
417 );
418 let RelationalCsgCmpPlanNode::Join { left, right, .. } = result.plan.root else {
419 panic!("expected costed join root");
420 };
421 assert_eq!(left.bindings(), A.into());
422 let RelationalCsgCmpPlanNode::Relation { access_path, .. } = *right else {
423 panic!("expected singleton probe on the right");
424 };
425 assert_eq!(access_path.descriptor.name, "by_outer");
426 }
427
428 #[test]
429 fn csg_cmp_probe_prefers_a_cheaper_scan_over_a_longer_equality_prefix() {
430 let tree = RelationalJoinTree::join(
431 operator(1, RelationalJoinOperatorKind::Inner, &[A, B]),
432 RelationalJoinTree::Relation(A),
433 RelationalJoinTree::Relation(B),
434 );
435 let mut inner = relation(B, 10);
436 inner.access_paths.push(RelationalJoinAccessPath::probe(
437 RelationalAccessPathDescriptor {
438 kind: RelationalAccessPathKind::Index,
439 name: "by_outer".to_string(),
440 index_columns: vec!["outer_id".to_string()],
441 access_columns: BTreeSet::from(["outer_id".to_string()]),
442 equality_prefix_len: 1,
443 order_prefix_len: 0,
444 exclusive_range: false,
445 range: None,
446 reverse_order: false,
447 unique_point: false,
448 covering: false,
449 requires_row_fetch: true,
450 estimated_rows: 10,
451 },
452 A.into(),
453 ));
454 let problem = RelationalJoinRewriteProblem {
455 relations: vec![relation(A, 1), inner],
456 initial_tree: tree,
457 post_join_filter: None,
458 };
459
460 let result = enumerate_relational_csg_cmp_joins(
461 &problem,
462 &RequiredProperties::default(),
463 RelationalJoinEnumerationConfig::default(),
464 )
465 .unwrap();
466
467 let RelationalCsgCmpPlanNode::Join { right, .. } = result.plan.root else {
468 panic!("expected a join root");
469 };
470 let RelationalCsgCmpPlanNode::Relation { access_path, .. } = *right else {
471 panic!("expected a singleton probe");
472 };
473 assert_eq!(access_path.descriptor.name, "__full_scan");
474 }
475
476 #[test]
477 fn multi_relation_hyperedge_waits_for_its_complete_endpoint() {
478 let lower = RelationalJoinTree::join(
479 operator(1, RelationalJoinOperatorKind::Inner, &[A, B]),
480 RelationalJoinTree::Relation(A),
481 RelationalJoinTree::Relation(B),
482 );
483 let tree = RelationalJoinTree::join(
484 operator(2, RelationalJoinOperatorKind::Inner, &[A, B, C]),
485 lower,
486 RelationalJoinTree::Relation(C),
487 );
488 let result = enumerate_relational_csg_cmp_joins(
489 &problem(&[A, B, C], tree, None),
490 &RequiredProperties::default(),
491 RelationalJoinEnumerationConfig::default(),
492 )
493 .unwrap();
494
495 assert!(result
496 .alternatives
497 .iter()
498 .filter(|alternative| alternative.operator_id == RelationalJoinOperatorId::new(2))
499 .all(|alternative| {
500 alternative.left_bindings == BindingSet::from([A, B])
501 || alternative.right_bindings == BindingSet::from([A, B])
502 }));
503 }
504
505 #[test]
506 fn csg_cmp_never_inserts_the_illegal_outer_join_association() {
507 let lower = RelationalJoinTree::join(
508 operator(1, RelationalJoinOperatorKind::LeftOuter, &[A, B]),
509 RelationalJoinTree::Relation(A),
510 RelationalJoinTree::Relation(B),
511 );
512 let tree = RelationalJoinTree::join(
513 operator(2, RelationalJoinOperatorKind::Inner, &[B, C]),
514 lower,
515 RelationalJoinTree::Relation(C),
516 );
517 let result = enumerate_relational_csg_cmp_joins(
518 &problem(&[A, B, C], tree, None),
519 &RequiredProperties::default(),
520 RelationalJoinEnumerationConfig::default(),
521 )
522 .unwrap();
523
524 assert!(!result.alternatives.iter().any(|alternative| {
525 alternative.operator_id == RelationalJoinOperatorId::new(2)
526 && binding_union(&alternative.left_bindings, &alternative.right_bindings)
527 == BindingSet::from([B, C])
528 }));
529 }
530
531 #[test]
532 fn null_rejection_opens_the_inner_hyperedge_only_after_outer_simplification() {
533 let lower = RelationalJoinTree::join(
534 operator(1, RelationalJoinOperatorKind::LeftOuter, &[A, B]),
535 RelationalJoinTree::Relation(A),
536 RelationalJoinTree::Relation(B),
537 );
538 let tree = RelationalJoinTree::join(
539 operator(2, RelationalJoinOperatorKind::Inner, &[B, C]),
540 lower,
541 RelationalJoinTree::Relation(C),
542 );
543 let filter = BoundPredicate::IsNotNull(value(B));
544 let result = enumerate_relational_csg_cmp_joins(
545 &problem(&[A, B, C], tree, Some(filter)),
546 &RequiredProperties::default(),
547 RelationalJoinEnumerationConfig::default(),
548 )
549 .unwrap();
550
551 assert!(result.alternatives.iter().any(|alternative| {
552 alternative.operator_id == RelationalJoinOperatorId::new(2)
553 && binding_union(&alternative.left_bindings, &alternative.right_bindings)
554 == BindingSet::from([B, C])
555 }));
556 }
557}