1use std::collections::{BTreeMap, BTreeSet};
31
32use crate::cost_model::{CostEstimator, OperatorKind};
33use crate::join_graph::{JoinEdge, JoinGraph};
34
35pub const MAX_DP_RELATIONS: usize = 16;
37
38type StarLeaves = Vec<(usize, Vec<JoinEdge>)>;
39type StarShape = (usize, StarLeaves);
40
41#[derive(Clone, Copy)]
42struct StarState {
43 cardinality: f64,
44 cost: f64,
45 prev_mask: usize,
46 leaf_pos: usize,
47}
48
49#[derive(Debug, Clone)]
54pub struct JoinPlan {
55 pub relations: u64,
56 pub cardinality: f64,
57 pub cost: f64,
58 pub left: Option<Box<JoinPlan>>,
59 pub right: Option<Box<JoinPlan>>,
60 pub join_edge: Option<JoinEdge>,
61 pub kind: Option<OperatorKind>,
64}
65
66impl JoinPlan {
67 fn leaf(idx: usize, rows: f64, access_cost: f64) -> Self {
69 Self {
70 relations: 1u64 << idx,
71 cardinality: rows,
72 cost: access_cost,
73 left: None,
74 right: None,
75 join_edge: None,
76 kind: None,
77 }
78 }
79
80 pub fn rows(&self) -> f64 {
82 self.cardinality
83 }
84
85 pub fn cost(&self) -> f64 {
86 self.cost
87 }
88}
89
90pub fn enumerate_dpccp(graph: &JoinGraph) -> Option<JoinPlan> {
93 DPccp::new(graph).optimize()
94}
95
96pub fn enumerate_dpccp_with_cost_estimator(
98 graph: &JoinGraph,
99 cost_estimator: CostEstimator,
100) -> Option<JoinPlan> {
101 DPccp::with_cost_estimator(graph, cost_estimator).optimize()
102}
103
104pub struct DPccp<'g> {
108 graph: &'g JoinGraph,
109 dp: BTreeMap<u64, JoinPlan>,
110 all_mask: u64,
111 cost_estimator: CostEstimator,
112}
113
114impl<'g> DPccp<'g> {
115 pub fn new(graph: &'g JoinGraph) -> Self {
116 let all_mask = graph.full_set();
117 Self {
118 graph,
119 dp: BTreeMap::new(),
120 all_mask,
121 cost_estimator: CostEstimator::default(),
122 }
123 }
124
125 pub fn with_cost_estimator(graph: &'g JoinGraph, cost_estimator: CostEstimator) -> Self {
126 let all_mask = graph.full_set();
127 Self {
128 graph,
129 dp: BTreeMap::new(),
130 all_mask,
131 cost_estimator,
132 }
133 }
134
135 pub fn optimize(mut self) -> Option<JoinPlan> {
139 let n = self.graph.relation_count();
140 if n == 0 {
141 return None;
142 }
143 if n == 1 {
144 return Some(JoinPlan::leaf(
145 0,
146 self.graph.cardinalities[0],
147 self.graph.access_costs[0],
148 ));
149 }
150 for i in 0..n {
152 self.dp.insert(
153 1u64 << i,
154 JoinPlan::leaf(i, self.graph.cardinalities[i], self.graph.access_costs[i]),
155 );
156 }
157 if n <= MAX_DP_RELATIONS {
158 if let Some(plan) = self.optimize_star(n) {
159 return Some(plan);
160 }
161 }
162 if n > MAX_DP_RELATIONS {
163 return self.greedy_optimize();
164 }
165 self.enumerate_csg_cmp_pairs(n);
166 if let Some(plan) = self.dp.get(&self.all_mask).cloned() {
167 return Some(plan);
168 }
169 self.join_disconnected_components()
171 }
172
173 fn enumerate_csg_cmp_pairs(&mut self, n: usize) {
176 let neighbors: Vec<Vec<usize>> = (0..n).map(|i| self.graph.neighbors(i)).collect();
177
178 let mut connected = BTreeSet::new();
181 let mut prev_layer: Vec<u64> = Vec::with_capacity(n);
182 for i in 0..n {
183 let mask = 1u64 << i;
184 connected.insert(mask);
185 prev_layer.push(mask);
186 }
187
188 for _size in 2..=n {
189 let mut cur_layer: Vec<u64> = Vec::new();
190 for &s_mask in &prev_layer {
191 let Ok(min_node) = usize::try_from(s_mask.trailing_zeros()) else {
192 return;
193 };
194 let mut node = 0usize;
195 let mut tmp = s_mask;
196 while tmp != 0 {
197 if tmp & 1 == 1 {
198 for &nb in &neighbors[node] {
199 if nb > min_node && (s_mask & (1u64 << nb)) == 0 {
200 let new_mask = s_mask | (1u64 << nb);
201 if connected.insert(new_mask) {
202 cur_layer.push(new_mask);
203 }
204 }
205 }
206 }
207 tmp >>= 1;
208 node += 1;
209 }
210 }
211 for &subset_mask in &cur_layer {
212 self.enumerate_splits(subset_mask, &connected);
213 }
214 prev_layer = cur_layer;
215 }
216 }
217
218 fn optimize_star(&self, n: usize) -> Option<JoinPlan> {
219 let (centre, leaves) = self.star_shape(n)?;
220 let shift = u32::try_from(leaves.len()).ok()?;
221 let states = 1usize.checked_shl(shift)?;
222 let mut dp: Vec<Option<StarState>> = vec![None; states];
223 dp[0] = Some(StarState {
224 cardinality: self.graph.cardinalities[centre],
225 cost: self.graph.access_costs[centre],
226 prev_mask: 0,
227 leaf_pos: usize::MAX,
228 });
229
230 for mask in 0..states {
231 let Some(base) = dp[mask] else {
232 continue;
233 };
234 for (leaf_pos, (leaf_idx, edges)) in leaves.iter().enumerate() {
235 let bit = 1usize << leaf_pos;
236 if mask & bit != 0 {
237 continue;
238 }
239 let leaf_cardinality = self.graph.cardinalities[*leaf_idx];
240 let mut cardinality = base.cardinality * leaf_cardinality;
241 for edge in edges {
242 cardinality *= edge.selectivity;
243 }
244 let join_cost = self.join_cost(base.cardinality, leaf_cardinality).0;
245 let candidate = StarState {
246 cardinality,
247 cost: base.cost + self.graph.access_costs[*leaf_idx] + join_cost,
248 prev_mask: mask,
249 leaf_pos,
250 };
251 let next_mask = mask | bit;
252 let install = match &dp[next_mask] {
253 Some(existing) => candidate.cost < existing.cost,
254 None => true,
255 };
256 if install {
257 dp[next_mask] = Some(candidate);
258 }
259 }
260 }
261
262 dp[states - 1]?;
263 let mut order: Vec<usize> = Vec::with_capacity(leaves.len());
264 let mut mask = states - 1;
265 while mask != 0 {
266 let state = dp[mask]?;
267 order.push(state.leaf_pos);
268 mask = state.prev_mask;
269 }
270 order.reverse();
271
272 let mut plan = JoinPlan::leaf(
273 centre,
274 self.graph.cardinalities[centre],
275 self.graph.access_costs[centre],
276 );
277 for leaf_pos in order {
278 let (leaf_idx, edges) = &leaves[leaf_pos];
279 let leaf = JoinPlan::leaf(
280 *leaf_idx,
281 self.graph.cardinalities[*leaf_idx],
282 self.graph.access_costs[*leaf_idx],
283 );
284 plan = self.join_plans(&plan, &leaf, edges);
285 }
286 Some(plan)
287 }
288
289 fn star_shape(&self, n: usize) -> Option<StarShape> {
290 if n < 3 || self.graph.edges.is_empty() {
291 return None;
292 }
293 let mut neighbor_masks = vec![0u64; n];
294 for edge in &self.graph.edges {
295 if edge.left.count_ones() != 1 || edge.right.count_ones() != 1 {
296 return None;
297 }
298 let left = usize::try_from(edge.left.trailing_zeros()).ok()?;
299 let right = usize::try_from(edge.right.trailing_zeros()).ok()?;
300 if left == right || left >= n || right >= n {
301 return None;
302 }
303 neighbor_masks[left] |= edge.right;
304 neighbor_masks[right] |= edge.left;
305 }
306 let candidates: Vec<usize> = neighbor_masks
307 .iter()
308 .enumerate()
309 .filter_map(|(idx, mask)| {
310 usize::try_from(mask.count_ones())
311 .ok()
312 .is_some_and(|count| count == n - 1)
313 .then_some(idx)
314 })
315 .collect();
316 if candidates.len() != 1 {
317 return None;
318 }
319 for centre in candidates {
320 let centre_mask = 1u64 << centre;
321 let mut by_leaf: BTreeMap<usize, Vec<JoinEdge>> = BTreeMap::new();
322 let mut valid = true;
323 for edge in &self.graph.edges {
324 let leaf_mask = if edge.left == centre_mask {
325 edge.right
326 } else if edge.right == centre_mask {
327 edge.left
328 } else {
329 valid = false;
330 break;
331 };
332 if leaf_mask == 0 || leaf_mask == centre_mask {
333 valid = false;
334 break;
335 }
336 let leaf = usize::try_from(leaf_mask.trailing_zeros()).ok()?;
337 by_leaf.entry(leaf).or_default().push(edge.clone());
338 }
339 if valid && by_leaf.len() == n - 1 {
340 return Some((centre, by_leaf.into_iter().collect()));
341 }
342 }
343 None
344 }
345
346 fn enumerate_splits(&mut self, subset_mask: u64, connected: &BTreeSet<u64>) {
351 let lowest_bit = subset_mask & subset_mask.wrapping_neg();
352 let rest = subset_mask ^ lowest_bit;
353
354 let mut sub_rest = rest.wrapping_sub(1) & rest;
357 while sub_rest != 0 {
358 let sub = sub_rest | lowest_bit;
359 let comp = subset_mask ^ sub;
360 if connected.contains(&sub) && connected.contains(&comp) {
361 if let (Some(plan1), Some(plan2)) =
362 (self.dp.get(&sub).cloned(), self.dp.get(&comp).cloned())
363 {
364 let edges = self
365 .graph
366 .edges_between(plan1.relations, plan2.relations)
367 .into_iter()
368 .cloned()
369 .collect::<Vec<_>>();
370 if !edges.is_empty() {
371 self.emit_csg_cmp_pair(&plan1, &plan2, &edges, subset_mask);
372 }
373 }
374 }
375 sub_rest = sub_rest.wrapping_sub(1) & rest;
376 }
377 if connected.contains(&rest) {
379 if let (Some(plan1), Some(plan2)) = (
380 self.dp.get(&lowest_bit).cloned(),
381 self.dp.get(&rest).cloned(),
382 ) {
383 let edges = self
384 .graph
385 .edges_between(plan1.relations, plan2.relations)
386 .into_iter()
387 .cloned()
388 .collect::<Vec<_>>();
389 if !edges.is_empty() {
390 self.emit_csg_cmp_pair(&plan1, &plan2, &edges, subset_mask);
391 }
392 }
393 }
394 }
395
396 fn emit_csg_cmp_pair(
401 &mut self,
402 plan1: &JoinPlan,
403 plan2: &JoinPlan,
404 edges: &[JoinEdge],
405 combined_mask: u64,
406 ) {
407 let candidate = self.join_plans(plan1, plan2, edges);
408 let install = match self.dp.get(&combined_mask) {
409 Some(existing) => candidate.cost < existing.cost,
410 None => true,
411 };
412 if install {
413 self.dp.insert(combined_mask, candidate);
414 }
415 }
416
417 fn join_plans(&self, plan1: &JoinPlan, plan2: &JoinPlan, edges: &[JoinEdge]) -> JoinPlan {
418 let mut cardinality = plan1.cardinality * plan2.cardinality;
419 for edge in edges {
420 cardinality *= edge.selectivity;
421 }
422 let c1 = plan1.cardinality;
423 let c2 = plan2.cardinality;
424 let (join_cost, kind) = self.join_cost(c1, c2);
425 JoinPlan {
426 relations: plan1.relations | plan2.relations,
427 cardinality,
428 cost: join_cost + plan1.cost + plan2.cost,
429 left: Some(Box::new(plan1.clone())),
430 right: Some(Box::new(plan2.clone())),
431 join_edge: edges.first().cloned(),
432 kind: Some(kind),
433 }
434 }
435
436 fn join_cost(&self, c1: f64, c2: f64) -> (f64, OperatorKind) {
437 let kind = OperatorKind::HashJoinInner;
438 (
439 self.cost_estimator.estimate_join(kind, c1, c2).total(),
440 kind,
441 )
442 }
443
444 fn cross_join_cost(&self, c1: f64, c2: f64) -> f64 {
445 self.cost_estimator
446 .estimate_join(OperatorKind::CrossJoin, c1, c2)
447 .total()
448 }
449
450 fn join_disconnected_components(&mut self) -> Option<JoinPlan> {
452 let components = self.find_connected_components();
453 let mut component_plans: Vec<JoinPlan> = Vec::with_capacity(components.len());
454 for comp in &components {
455 if comp.len() == 1 {
456 let idx = *comp.first()?;
457 let plan = self.dp.get(&(1u64 << idx)).cloned()?;
458 component_plans.push(plan);
459 continue;
460 }
461 let mask: u64 = comp.iter().fold(0u64, |acc, i| acc | (1u64 << *i));
462 if let Some(plan) = self.dp.get(&mask).cloned() {
463 component_plans.push(plan);
464 continue;
465 }
466 let original_indices: Vec<usize> = {
469 let mut v: Vec<usize> = comp.clone();
470 v.sort_unstable();
471 v
472 };
473 let sub_graph = self.build_subgraph(&original_indices)?;
474 let sub_plan =
475 DPccp::with_cost_estimator(&sub_graph, self.cost_estimator.clone()).optimize()?;
476 component_plans.push(remap_plan(&sub_plan, &original_indices));
477 }
478 component_plans.sort_by(|a, b| a.cardinality.total_cmp(&b.cardinality));
479 let mut iter = component_plans.into_iter();
480 let mut result = iter.next()?;
481 for plan in iter {
482 let combined = result.relations | plan.relations;
483 let cardinality = result.cardinality * plan.cardinality;
484 let cost = self.cross_join_cost(result.cardinality, plan.cardinality)
485 + result.cost
486 + plan.cost;
487 result = JoinPlan {
488 relations: combined,
489 cardinality,
490 cost,
491 left: Some(Box::new(result)),
492 right: Some(Box::new(plan)),
493 join_edge: None,
494 kind: Some(OperatorKind::CrossJoin),
495 };
496 }
497 Some(result)
498 }
499
500 fn find_connected_components(&self) -> Vec<Vec<usize>> {
502 let n = self.graph.relation_count();
503 let mut remaining: std::collections::BTreeSet<usize> = (0..n).collect();
504 let mut components: Vec<Vec<usize>> = Vec::new();
505 while let Some(&start) = remaining.iter().next() {
506 let mut visited: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
507 visited.insert(start);
508 let mut stack: Vec<usize> = vec![start];
509 while let Some(node) = stack.pop() {
510 for nb in self.graph.neighbors(node) {
511 if remaining.contains(&nb) && !visited.contains(&nb) {
512 visited.insert(nb);
513 stack.push(nb);
514 }
515 }
516 }
517 for v in &visited {
518 remaining.remove(v);
519 }
520 components.push(visited.into_iter().collect());
521 }
522 components
523 }
524
525 fn build_subgraph(&self, nodes: &[usize]) -> Option<JoinGraph> {
529 let mut sub = JoinGraph::new();
530 let mut index_map: BTreeMap<usize, usize> = BTreeMap::new();
531 for &old_idx in nodes {
532 let new_idx = sub.relations.len();
533 sub.relations.push(self.graph.relations[old_idx].clone());
534 sub.cardinalities.push(self.graph.cardinalities[old_idx]);
535 sub.access_costs.push(self.graph.access_costs[old_idx]);
536 index_map.insert(old_idx, new_idx);
537 }
538 for edge in &self.graph.edges {
539 let l_idx = usize::try_from(edge.left.trailing_zeros()).ok()?;
540 let r_idx = usize::try_from(edge.right.trailing_zeros()).ok()?;
541 if let (Some(&l_new), Some(&r_new)) = (index_map.get(&l_idx), index_map.get(&r_idx)) {
542 sub.edges.push(JoinEdge {
543 left: 1_u64 << l_new,
544 right: 1_u64 << r_new,
545 selectivity: edge.selectivity,
546 });
547 }
548 }
549 Some(sub)
550 }
551
552 fn greedy_optimize(self) -> Option<JoinPlan> {
556 let mut active: BTreeMap<u64, JoinPlan> = self.dp.clone();
557 while active.len() > 1 {
558 let mut best_cost = f64::INFINITY;
559 let mut best_combined_mask: u64 = 0;
560 let mut best_plan: Option<JoinPlan> = None;
561 let items: Vec<(u64, JoinPlan)> = active.iter().map(|(k, v)| (*k, v.clone())).collect();
562 for i in 0..items.len() {
563 let (m1, ref p1) = items[i];
564 for (m2, p2) in items.iter().skip(i + 1) {
565 let edges = self
566 .graph
567 .edges_between(p1.relations, p2.relations)
568 .into_iter()
569 .cloned()
570 .collect::<Vec<_>>();
571 if edges.is_empty() {
572 continue;
573 }
574 let mut cardinality = p1.cardinality * p2.cardinality;
575 for edge in &edges {
576 cardinality *= edge.selectivity;
577 }
578 let (greedy_join_cost, kind) = self.join_cost(p1.cardinality, p2.cardinality);
579 let cost = greedy_join_cost + p1.cost + p2.cost;
580 if cost < best_cost {
581 best_cost = cost;
582 best_combined_mask = m1 | m2;
583 best_plan = Some(JoinPlan {
584 relations: p1.relations | p2.relations,
585 cardinality,
586 cost,
587 left: Some(Box::new(p1.clone())),
588 right: Some(Box::new(p2.clone())),
589 join_edge: edges.first().cloned(),
590 kind: Some(kind),
591 });
592 }
593 }
594 }
595 let Some(best_plan_unwrapped) = best_plan else {
596 let mut remaining: Vec<JoinPlan> = active.into_values().collect();
599 remaining.sort_by(|a, b| a.cardinality.total_cmp(&b.cardinality));
600 let mut iter = remaining.into_iter();
601 let mut result = iter.next()?;
602 for plan in iter {
603 let combined = result.relations | plan.relations;
604 let cardinality = result.cardinality * plan.cardinality;
605 let cost = self.cross_join_cost(result.cardinality, plan.cardinality)
606 + result.cost
607 + plan.cost;
608 result = JoinPlan {
609 relations: combined,
610 cardinality,
611 cost,
612 left: Some(Box::new(result)),
613 right: Some(Box::new(plan)),
614 join_edge: None,
615 kind: Some(OperatorKind::CrossJoin),
616 };
617 }
618 return Some(result);
619 };
620 let drop: Vec<u64> = active
623 .keys()
624 .copied()
625 .filter(|rel_mask| rel_mask & best_combined_mask == *rel_mask)
626 .collect();
627 for k in drop {
628 active.remove(&k);
629 }
630 active.insert(best_combined_mask, best_plan_unwrapped);
631 }
632 active.into_values().next()
633 }
634}
635
636fn remap_plan(plan: &JoinPlan, original_indices: &[usize]) -> JoinPlan {
639 let new_relations = remap_mask(plan.relations, original_indices);
640 JoinPlan {
641 relations: new_relations,
642 cardinality: plan.cardinality,
643 cost: plan.cost,
644 left: plan
645 .left
646 .as_deref()
647 .map(|l| Box::new(remap_plan(l, original_indices))),
648 right: plan
649 .right
650 .as_deref()
651 .map(|r| Box::new(remap_plan(r, original_indices))),
652 join_edge: plan.join_edge.clone(),
653 kind: plan.kind,
654 }
655}
656
657fn remap_mask(mask: u64, original_indices: &[usize]) -> u64 {
658 let mut out = 0u64;
659 let mut m = mask;
660 let mut i = 0;
661 while m != 0 {
662 if m & 1 == 1 {
663 out |= 1u64 << original_indices[i];
664 }
665 m >>= 1;
666 i += 1;
667 }
668 out
669}
670
671#[cfg(test)]
672mod tests {
673 use super::*;
674 use crate::cost_model::CostCoefficients;
675
676 fn assert_executable_join_kinds(plan: &JoinPlan) {
677 match (&plan.left, &plan.right) {
678 (Some(left), Some(right)) => {
679 if plan.join_edge.is_some() {
680 assert_eq!(plan.kind, Some(OperatorKind::HashJoinInner));
681 } else {
682 assert_eq!(plan.kind, Some(OperatorKind::CrossJoin));
683 }
684 assert_executable_join_kinds(left);
685 assert_executable_join_kinds(right);
686 }
687 (None, None) => assert!(plan.kind.is_none()),
688 _ => panic!("join plan contains exactly one child: {plan:?}"),
689 }
690 }
691
692 #[test]
693 fn three_way_chain_picks_smallest_first() {
694 let mut g = JoinGraph::new();
695 let a = g.add_relation("a", 10.0).unwrap();
696 let b = g.add_relation("b", 100.0).unwrap();
697 let c = g.add_relation("c", 10_000.0).unwrap();
698 g.add_edge(a, b, 0.01).unwrap();
699 g.add_edge(b, c, 0.001).unwrap();
700 let plan = enumerate_dpccp(&g).unwrap();
701 assert_eq!(plan.relations, 0b111);
702 assert!(plan.left.is_some() && plan.right.is_some());
703 assert_executable_join_kinds(&plan);
704 }
705
706 #[test]
707 fn single_relation_returns_leaf() {
708 let mut g = JoinGraph::new();
709 g.add_relation("solo", 1.0).unwrap();
710 let plan = enumerate_dpccp(&g).unwrap();
711 assert!(plan.left.is_none());
712 assert!(plan.right.is_none());
713 assert_eq!(plan.relations, 0b1);
714 }
715
716 #[test]
717 fn explicit_cost_estimator_drives_join_cost() {
718 let mut graph = JoinGraph::new();
719 let left = graph.add_relation_with_cost("left", 10.0, 7.0).unwrap();
720 let right = graph.add_relation_with_cost("right", 100.0, 11.0).unwrap();
721 graph.add_edge(left, right, 0.5).unwrap();
722
723 let coefficients = CostCoefficients {
724 hashjoin_build_per_row: 2.0,
725 hashjoin_probe_per_row: 3.0,
726 ..CostCoefficients::default()
727 };
728 let estimator = CostEstimator::new(coefficients);
729 let expected_join_cost = estimator
730 .estimate_join(OperatorKind::HashJoinInner, 10.0, 100.0)
731 .total();
732
733 let plan = enumerate_dpccp_with_cost_estimator(&graph, estimator).unwrap();
734
735 assert_eq!(plan.cost, 7.0 + 11.0 + expected_join_cost);
736 }
737
738 #[test]
739 fn empty_graph_returns_none() {
740 let g = JoinGraph::new();
741 assert!(enumerate_dpccp(&g).is_none());
742 }
743
744 #[test]
745 fn disconnected_graph_cross_joins_components() {
746 let mut g = JoinGraph::new();
747 let a = g.add_relation("a", 50.0).unwrap();
748 let b = g.add_relation("b", 60.0).unwrap();
749 let c = g.add_relation("c", 70.0).unwrap();
750 g.add_edge(a, b, 0.5).unwrap();
751 let _ = c;
753 let plan = enumerate_dpccp(&g).unwrap();
754 assert_eq!(plan.relations, 0b111);
756 assert_executable_join_kinds(&plan);
757 }
758
759 #[test]
760 fn star_query_picks_nested_plan() {
761 let mut g = JoinGraph::new();
764 let centre = g.add_relation("centre", 1_000.0).unwrap();
765 let leaf_b = g.add_relation("b", 10.0).unwrap();
766 let leaf_c = g.add_relation("c", 20.0).unwrap();
767 let leaf_d = g.add_relation("d", 30.0).unwrap();
768 g.add_edge(centre, leaf_b, 0.01).unwrap();
769 g.add_edge(centre, leaf_c, 0.01).unwrap();
770 g.add_edge(centre, leaf_d, 0.01).unwrap();
771 let plan = enumerate_dpccp(&g).unwrap();
772 assert_eq!(plan.relations, 0b1111);
773 assert!(plan.cost > 0.0);
774 assert_executable_join_kinds(&plan);
775 }
776
777 #[test]
778 fn greedy_fallback_kicks_in_above_threshold() {
779 let mut g = JoinGraph::new();
782 let n = MAX_DP_RELATIONS + 2;
783 let mut prev: usize = 0;
784 for i in 0..n {
785 let idx = g.add_relation(format!("t{i}"), 100.0).unwrap();
786 if i > 0 {
787 g.add_edge(prev, idx, 0.05).unwrap();
788 }
789 prev = idx;
790 }
791 let plan = enumerate_dpccp(&g).unwrap();
792 assert_eq!(plan.relations.count_ones() as usize, n);
793 assert_executable_join_kinds(&plan);
794 }
795
796 #[test]
797 fn threshold_sized_star_uses_exact_plan() {
798 let mut g = JoinGraph::new();
799 let centre = g.add_relation("centre", 1_000.0).unwrap();
800 for i in 1..MAX_DP_RELATIONS {
801 let leaf = g
802 .add_relation(format!("leaf{i}"), 100.0 + i as f64)
803 .unwrap();
804 g.add_edge(centre, leaf, 0.01).unwrap();
805 }
806
807 let plan = enumerate_dpccp(&g).unwrap();
808
809 assert_eq!(plan.relations.count_ones() as usize, MAX_DP_RELATIONS);
810 assert_eq!(plan.relations, g.full_set());
811 assert!(plan.cost > 0.0);
812 }
813}