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 reverse_order: false,
277 unique_point: false,
278 covering: false,
279 requires_row_fetch: false,
280 estimated_rows: rows,
281 },
282 )],
283 }
284 }
285
286 fn indexed_relation(
287 binding: BindingId,
288 rows: usize,
289 required: BindingId,
290 ) -> RelationalJoinRelation {
291 let mut relation = relation(binding, rows);
292 relation.access_paths.push(RelationalJoinAccessPath::probe(
293 RelationalAccessPathDescriptor {
294 kind: RelationalAccessPathKind::Index,
295 name: "by_outer".to_string(),
296 index_columns: vec!["id".to_string()],
297 access_columns: BTreeSet::from(["id".to_string()]),
298 equality_prefix_len: 1,
299 order_prefix_len: 0,
300 exclusive_range: false,
301 reverse_order: false,
302 unique_point: true,
303 covering: false,
304 requires_row_fetch: true,
305 estimated_rows: 1,
306 },
307 required.into(),
308 ));
309 relation
310 }
311
312 fn problem(
313 relations: &[BindingId],
314 tree: RelationalJoinTree,
315 filter: Option<BoundPredicate>,
316 ) -> RelationalJoinRewriteProblem {
317 RelationalJoinRewriteProblem {
318 relations: relations
319 .iter()
320 .enumerate()
321 .map(|(index, binding)| relation(*binding, (index + 1) * 10))
322 .collect(),
323 initial_tree: tree,
324 post_join_filter: filter,
325 }
326 }
327
328 #[test]
329 fn csg_cmp_enumerates_bushy_hypergraph_partitions_into_one_semantic_group() {
330 let left = RelationalJoinTree::join(
331 operator(1, RelationalJoinOperatorKind::Inner, &[A, B]),
332 RelationalJoinTree::Relation(A),
333 RelationalJoinTree::Relation(B),
334 );
335 let right = RelationalJoinTree::join(
336 operator(2, RelationalJoinOperatorKind::Inner, &[C, D]),
337 RelationalJoinTree::Relation(C),
338 RelationalJoinTree::Relation(D),
339 );
340 let tree = RelationalJoinTree::join(
341 operator(3, RelationalJoinOperatorKind::Inner, &[B, C]),
342 left,
343 right,
344 );
345 let result = enumerate_relational_csg_cmp_joins(
346 &problem(&[A, B, C, D], tree, None),
347 &RequiredProperties::default(),
348 RelationalJoinEnumerationConfig::default(),
349 )
350 .unwrap();
351
352 assert_eq!(result.plan.root.bindings(), BindingSet::from([A, B, C, D]));
353 assert!(result.root_alternatives > 1);
354 assert!(result.alternatives.iter().any(|alternative| {
355 alternative.operator_id == RelationalJoinOperatorId::new(3)
356 && alternative.left_bindings == BindingSet::from([A, B])
357 && alternative.right_bindings == BindingSet::from([C, D])
358 }));
359 }
360
361 #[test]
362 fn csg_cmp_probe_only_policy_excludes_unbounded_materialization() {
363 let left = RelationalJoinTree::join(
364 operator(1, RelationalJoinOperatorKind::Inner, &[A, B]),
365 RelationalJoinTree::Relation(A),
366 RelationalJoinTree::Relation(B),
367 );
368 let right = RelationalJoinTree::join(
369 operator(2, RelationalJoinOperatorKind::Inner, &[C, D]),
370 RelationalJoinTree::Relation(C),
371 RelationalJoinTree::Relation(D),
372 );
373 let tree = RelationalJoinTree::join(
374 operator(3, RelationalJoinOperatorKind::Inner, &[B, C]),
375 left,
376 right,
377 );
378
379 let result = enumerate_relational_csg_cmp_joins_with_right_input_policy(
380 &problem(&[A, B, C, D], tree, None),
381 &RequiredProperties::default(),
382 RelationalJoinEnumerationConfig::default(),
383 RelationalCsgCmpRightInputPolicy::ProbeOnly,
384 )
385 .expect("probe-only CSG-CMP can choose a left-deep alternative");
386
387 assert!(!result.plan.root.has_materialized_right());
388 }
389
390 #[test]
391 fn csg_cmp_costing_uses_a_singleton_index_probe_from_the_outer_group() {
392 let tree = RelationalJoinTree::join(
393 operator(1, RelationalJoinOperatorKind::Inner, &[A, B]),
394 RelationalJoinTree::Relation(A),
395 RelationalJoinTree::Relation(B),
396 );
397 let problem = RelationalJoinRewriteProblem {
398 relations: vec![relation(A, 1), indexed_relation(B, 1_000, A)],
399 initial_tree: tree,
400 post_join_filter: None,
401 };
402 let result = enumerate_relational_csg_cmp_joins(
403 &problem,
404 &RequiredProperties::default(),
405 RelationalJoinEnumerationConfig::default(),
406 )
407 .unwrap();
408
409 assert_eq!(
410 result.plan.cost(),
411 PlanCost {
412 estimated_rows: 1,
413 cost: 14,
414 }
415 );
416 let RelationalCsgCmpPlanNode::Join { left, right, .. } = result.plan.root else {
417 panic!("expected costed join root");
418 };
419 assert_eq!(left.bindings(), A.into());
420 let RelationalCsgCmpPlanNode::Relation { access_path, .. } = *right else {
421 panic!("expected singleton probe on the right");
422 };
423 assert_eq!(access_path.descriptor.name, "by_outer");
424 }
425
426 #[test]
427 fn csg_cmp_probe_prefers_a_cheaper_scan_over_a_longer_equality_prefix() {
428 let tree = RelationalJoinTree::join(
429 operator(1, RelationalJoinOperatorKind::Inner, &[A, B]),
430 RelationalJoinTree::Relation(A),
431 RelationalJoinTree::Relation(B),
432 );
433 let mut inner = relation(B, 10);
434 inner.access_paths.push(RelationalJoinAccessPath::probe(
435 RelationalAccessPathDescriptor {
436 kind: RelationalAccessPathKind::Index,
437 name: "by_outer".to_string(),
438 index_columns: vec!["outer_id".to_string()],
439 access_columns: BTreeSet::from(["outer_id".to_string()]),
440 equality_prefix_len: 1,
441 order_prefix_len: 0,
442 exclusive_range: false,
443 reverse_order: false,
444 unique_point: false,
445 covering: false,
446 requires_row_fetch: true,
447 estimated_rows: 10,
448 },
449 A.into(),
450 ));
451 let problem = RelationalJoinRewriteProblem {
452 relations: vec![relation(A, 1), inner],
453 initial_tree: tree,
454 post_join_filter: None,
455 };
456
457 let result = enumerate_relational_csg_cmp_joins(
458 &problem,
459 &RequiredProperties::default(),
460 RelationalJoinEnumerationConfig::default(),
461 )
462 .unwrap();
463
464 let RelationalCsgCmpPlanNode::Join { right, .. } = result.plan.root else {
465 panic!("expected a join root");
466 };
467 let RelationalCsgCmpPlanNode::Relation { access_path, .. } = *right else {
468 panic!("expected a singleton probe");
469 };
470 assert_eq!(access_path.descriptor.name, "__full_scan");
471 }
472
473 #[test]
474 fn multi_relation_hyperedge_waits_for_its_complete_endpoint() {
475 let lower = RelationalJoinTree::join(
476 operator(1, RelationalJoinOperatorKind::Inner, &[A, B]),
477 RelationalJoinTree::Relation(A),
478 RelationalJoinTree::Relation(B),
479 );
480 let tree = RelationalJoinTree::join(
481 operator(2, RelationalJoinOperatorKind::Inner, &[A, B, C]),
482 lower,
483 RelationalJoinTree::Relation(C),
484 );
485 let result = enumerate_relational_csg_cmp_joins(
486 &problem(&[A, B, C], tree, None),
487 &RequiredProperties::default(),
488 RelationalJoinEnumerationConfig::default(),
489 )
490 .unwrap();
491
492 assert!(result
493 .alternatives
494 .iter()
495 .filter(|alternative| alternative.operator_id == RelationalJoinOperatorId::new(2))
496 .all(|alternative| {
497 alternative.left_bindings == BindingSet::from([A, B])
498 || alternative.right_bindings == BindingSet::from([A, B])
499 }));
500 }
501
502 #[test]
503 fn csg_cmp_never_inserts_the_illegal_outer_join_association() {
504 let lower = RelationalJoinTree::join(
505 operator(1, RelationalJoinOperatorKind::LeftOuter, &[A, B]),
506 RelationalJoinTree::Relation(A),
507 RelationalJoinTree::Relation(B),
508 );
509 let tree = RelationalJoinTree::join(
510 operator(2, RelationalJoinOperatorKind::Inner, &[B, C]),
511 lower,
512 RelationalJoinTree::Relation(C),
513 );
514 let result = enumerate_relational_csg_cmp_joins(
515 &problem(&[A, B, C], tree, None),
516 &RequiredProperties::default(),
517 RelationalJoinEnumerationConfig::default(),
518 )
519 .unwrap();
520
521 assert!(!result.alternatives.iter().any(|alternative| {
522 alternative.operator_id == RelationalJoinOperatorId::new(2)
523 && binding_union(&alternative.left_bindings, &alternative.right_bindings)
524 == BindingSet::from([B, C])
525 }));
526 }
527
528 #[test]
529 fn null_rejection_opens_the_inner_hyperedge_only_after_outer_simplification() {
530 let lower = RelationalJoinTree::join(
531 operator(1, RelationalJoinOperatorKind::LeftOuter, &[A, B]),
532 RelationalJoinTree::Relation(A),
533 RelationalJoinTree::Relation(B),
534 );
535 let tree = RelationalJoinTree::join(
536 operator(2, RelationalJoinOperatorKind::Inner, &[B, C]),
537 lower,
538 RelationalJoinTree::Relation(C),
539 );
540 let filter = BoundPredicate::IsNotNull(value(B));
541 let result = enumerate_relational_csg_cmp_joins(
542 &problem(&[A, B, C], tree, Some(filter)),
543 &RequiredProperties::default(),
544 RelationalJoinEnumerationConfig::default(),
545 )
546 .unwrap();
547
548 assert!(result.alternatives.iter().any(|alternative| {
549 alternative.operator_id == RelationalJoinOperatorId::new(2)
550 && binding_union(&alternative.left_bindings, &alternative.right_bindings)
551 == BindingSet::from([B, C])
552 }));
553 }
554}