1use crate::{
16 PlanCostBreakdown, RelationalJoinEnumerationConfig, RelationalJoinEnumerationError,
17 RelationalJoinRewriteError,
18};
19
20#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
21pub enum RelationalJoinPlanningDirective {
22 #[default]
23 Auto,
24 SyntaxOrder,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum RelationalJoinPlanningStrategy {
29 SyntaxOrder,
30 InnerJoinMemo,
31 InnerLeftJoinRewriteMemo,
32 CsgCmpMemo,
33}
34
35impl RelationalJoinPlanningStrategy {
36 pub const fn as_str(self) -> &'static str {
37 match self {
38 Self::SyntaxOrder => "syntax_order",
39 Self::InnerJoinMemo => "inner_join_memo",
40 Self::InnerLeftJoinRewriteMemo => "inner_left_join_rewrite_memo",
41 Self::CsgCmpMemo => "csg_cmp_memo",
42 }
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum RelationalJoinPlanningStatus {
48 NotEligible,
49 Selected,
50 Fallback,
51}
52
53impl RelationalJoinPlanningStatus {
54 pub const fn as_str(self) -> &'static str {
55 match self {
56 Self::NotEligible => "not_eligible",
57 Self::Selected => "selected",
58 Self::Fallback => "fallback",
59 }
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum RelationalJoinPlanningReason {
65 NoJoin,
66 UnsupportedJoinKind,
67 LockingSelect,
68 ExplicitSyntaxOrder,
69 UnresolvedColumns,
70 UnsupportedJoinPredicate,
71 UnavailableAccessBinding,
72 UnsupportedPostJoinFilter,
73 SpecializedJoinNotEnumerated,
74 GroupBudgetExceeded,
75 ExpressionBudgetExceeded,
76 DisconnectedGraph,
77 RequiredPropertiesUnsatisfied,
78 NoLegalPlan,
79 SyntaxFallback,
80 SyntaxOrderOptimal,
81 CostReordered,
82}
83
84impl RelationalJoinPlanningReason {
85 pub const fn as_str(self) -> &'static str {
86 match self {
87 Self::NoJoin => "no_join",
88 Self::UnsupportedJoinKind => "unsupported_join_kind",
89 Self::LockingSelect => "locking_select",
90 Self::ExplicitSyntaxOrder => "explicit_syntax_order",
91 Self::UnresolvedColumns => "unresolved_columns",
92 Self::UnsupportedJoinPredicate => "unsupported_join_predicate",
93 Self::UnavailableAccessBinding => "unavailable_access_binding",
94 Self::UnsupportedPostJoinFilter => "unsupported_post_join_filter",
95 Self::SpecializedJoinNotEnumerated => "specialized_join_not_enumerated",
96 Self::GroupBudgetExceeded => "group_budget_exceeded",
97 Self::ExpressionBudgetExceeded => "expression_budget_exceeded",
98 Self::DisconnectedGraph => "disconnected_graph",
99 Self::RequiredPropertiesUnsatisfied => "required_properties_unsatisfied",
100 Self::NoLegalPlan => "no_legal_plan",
101 Self::SyntaxFallback => "syntax_fallback",
102 Self::SyntaxOrderOptimal => "syntax_order_optimal",
103 Self::CostReordered => "cost_reordered",
104 }
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum RelationalJoinPlanningFallbackClass {
110 Budget,
111 Unsupported,
112}
113
114impl RelationalJoinPlanningFallbackClass {
115 pub const fn as_str(self) -> &'static str {
116 match self {
117 Self::Budget => "budget",
118 Self::Unsupported => "unsupported",
119 }
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub struct RelationalJoinPlanningBudget {
125 pub max_groups: usize,
126 pub max_expressions: usize,
127}
128
129impl From<RelationalJoinEnumerationConfig> for RelationalJoinPlanningBudget {
130 fn from(config: RelationalJoinEnumerationConfig) -> Self {
131 Self {
132 max_groups: config.max_groups,
133 max_expressions: config.max_expressions,
134 }
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub struct RelationalJoinPlanningCost {
140 pub estimated_rows: u64,
141 pub cost: u64,
142 pub cpu: u64,
143 pub random_io: u64,
144 pub sequential_io: u64,
145 pub output_rows: u64,
146}
147
148impl From<PlanCostBreakdown> for RelationalJoinPlanningCost {
149 fn from(cost: PlanCostBreakdown) -> Self {
150 Self {
151 estimated_rows: cost.estimated_rows,
152 cost: cost.cost,
153 cpu: cost.cpu,
154 random_io: cost.random_io,
155 sequential_io: cost.sequential_io,
156 output_rows: cost.output_rows,
157 }
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct RelationalJoinPlanningAttempt {
163 pub strategy: RelationalJoinPlanningStrategy,
164 pub status: RelationalJoinPlanningStatus,
165 pub reason: RelationalJoinPlanningReason,
166 pub fallback_class: Option<RelationalJoinPlanningFallbackClass>,
167 pub memo_groups: Option<usize>,
168 pub memo_expressions: Option<usize>,
169 pub cost: Option<RelationalJoinPlanningCost>,
170}
171
172impl RelationalJoinPlanningAttempt {
173 pub fn not_eligible(
174 strategy: RelationalJoinPlanningStrategy,
175 reason: RelationalJoinPlanningReason,
176 ) -> Self {
177 Self {
178 strategy,
179 status: RelationalJoinPlanningStatus::NotEligible,
180 reason,
181 fallback_class: None,
182 memo_groups: None,
183 memo_expressions: None,
184 cost: None,
185 }
186 }
187
188 pub fn selected(
189 strategy: RelationalJoinPlanningStrategy,
190 reordered: bool,
191 memo_groups: usize,
192 memo_expressions: usize,
193 cost: PlanCostBreakdown,
194 ) -> Self {
195 Self {
196 strategy,
197 status: RelationalJoinPlanningStatus::Selected,
198 reason: if reordered {
199 RelationalJoinPlanningReason::CostReordered
200 } else {
201 RelationalJoinPlanningReason::SyntaxOrderOptimal
202 },
203 fallback_class: None,
204 memo_groups: Some(memo_groups),
205 memo_expressions: Some(memo_expressions),
206 cost: Some(cost.into()),
207 }
208 }
209
210 pub fn syntax_fallback() -> Self {
211 Self {
212 strategy: RelationalJoinPlanningStrategy::SyntaxOrder,
213 status: RelationalJoinPlanningStatus::Selected,
214 reason: RelationalJoinPlanningReason::SyntaxFallback,
215 fallback_class: None,
216 memo_groups: None,
217 memo_expressions: None,
218 cost: None,
219 }
220 }
221
222 pub fn explicit_syntax_order() -> Self {
223 Self {
224 strategy: RelationalJoinPlanningStrategy::SyntaxOrder,
225 status: RelationalJoinPlanningStatus::Selected,
226 reason: RelationalJoinPlanningReason::ExplicitSyntaxOrder,
227 fallback_class: None,
228 memo_groups: None,
229 memo_expressions: None,
230 cost: None,
231 }
232 }
233
234 pub fn fallback_from_enumeration(
235 strategy: RelationalJoinPlanningStrategy,
236 error: &RelationalJoinEnumerationError,
237 ) -> Option<Self> {
238 let (reason, fallback_class, memo_groups, memo_expressions) = match error {
239 RelationalJoinEnumerationError::GroupBudgetExceeded {
240 required_groups, ..
241 } => (
242 RelationalJoinPlanningReason::GroupBudgetExceeded,
243 RelationalJoinPlanningFallbackClass::Budget,
244 Some(*required_groups),
245 None,
246 ),
247 RelationalJoinEnumerationError::ExpressionBudgetExceeded {
248 required_expressions,
249 ..
250 } => (
251 RelationalJoinPlanningReason::ExpressionBudgetExceeded,
252 RelationalJoinPlanningFallbackClass::Budget,
253 None,
254 Some(*required_expressions),
255 ),
256 RelationalJoinEnumerationError::Disconnected => (
257 RelationalJoinPlanningReason::DisconnectedGraph,
258 RelationalJoinPlanningFallbackClass::Unsupported,
259 None,
260 None,
261 ),
262 RelationalJoinEnumerationError::RequiredPropertiesUnsatisfied => (
263 RelationalJoinPlanningReason::RequiredPropertiesUnsatisfied,
264 RelationalJoinPlanningFallbackClass::Unsupported,
265 None,
266 None,
267 ),
268 RelationalJoinEnumerationError::EmptyGraph
269 | RelationalJoinEnumerationError::DuplicateBinding(_)
270 | RelationalJoinEnumerationError::DuplicatePredicate(_)
271 | RelationalJoinEnumerationError::PredicateHasFewerThanTwoBindings(_)
272 | RelationalJoinEnumerationError::UnknownPredicateBinding { .. }
273 | RelationalJoinEnumerationError::UnknownAccessBinding { .. }
274 | RelationalJoinEnumerationError::AccessWithoutPredicate { .. }
275 | RelationalJoinEnumerationError::SelfDependentAccess(_)
276 | RelationalJoinEnumerationError::MissingBaseAccess(_)
277 | RelationalJoinEnumerationError::InvalidAccessPath { .. } => return None,
278 };
279 Some(Self {
280 strategy,
281 status: RelationalJoinPlanningStatus::Fallback,
282 reason,
283 fallback_class: Some(fallback_class),
284 memo_groups,
285 memo_expressions,
286 cost: None,
287 })
288 }
289
290 pub fn fallback_from_rewrite(
291 strategy: RelationalJoinPlanningStrategy,
292 error: &RelationalJoinRewriteError,
293 ) -> Option<Self> {
294 match error {
295 RelationalJoinRewriteError::Enumeration(error) => {
296 Self::fallback_from_enumeration(strategy, error)
297 }
298 RelationalJoinRewriteError::NoLegalRewrite => Some(Self {
299 strategy,
300 status: RelationalJoinPlanningStatus::Fallback,
301 reason: RelationalJoinPlanningReason::NoLegalPlan,
302 fallback_class: Some(RelationalJoinPlanningFallbackClass::Unsupported),
303 memo_groups: None,
304 memo_expressions: None,
305 cost: None,
306 }),
307 RelationalJoinRewriteError::DuplicateTreeBinding(_)
308 | RelationalJoinRewriteError::DuplicateOperator(_)
309 | RelationalJoinRewriteError::PredicateOutsideOperatorSubtree { .. }
310 | RelationalJoinRewriteError::RelationSetMismatch
311 | RelationalJoinRewriteError::OperatorCountMismatch { .. } => None,
312 }
313 }
314}
315
316#[derive(Debug, Clone, PartialEq, Eq)]
317pub struct RelationalJoinPlanningOutcome {
318 pub strategy: RelationalJoinPlanningStrategy,
319 pub status: RelationalJoinPlanningStatus,
320 pub reason: RelationalJoinPlanningReason,
321 pub memo_groups: Option<usize>,
322 pub memo_expressions: Option<usize>,
323 pub budget: RelationalJoinPlanningBudget,
324 pub selected_order: Vec<String>,
325 pub cost: Option<RelationalJoinPlanningCost>,
326 pub attempts: Vec<RelationalJoinPlanningAttempt>,
327}
328
329impl RelationalJoinPlanningOutcome {
330 pub fn not_eligible(
331 reason: RelationalJoinPlanningReason,
332 selected_order: Vec<String>,
333 config: RelationalJoinEnumerationConfig,
334 ) -> Self {
335 Self::not_eligible_after_attempts(reason, selected_order, config, Vec::new())
336 }
337
338 pub fn not_eligible_after_attempts(
339 reason: RelationalJoinPlanningReason,
340 selected_order: Vec<String>,
341 config: RelationalJoinEnumerationConfig,
342 mut attempts: Vec<RelationalJoinPlanningAttempt>,
343 ) -> Self {
344 let attempt = RelationalJoinPlanningAttempt::not_eligible(
345 RelationalJoinPlanningStrategy::SyntaxOrder,
346 reason,
347 );
348 attempts.push(attempt);
349 Self {
350 strategy: RelationalJoinPlanningStrategy::SyntaxOrder,
351 status: RelationalJoinPlanningStatus::NotEligible,
352 reason,
353 memo_groups: None,
354 memo_expressions: None,
355 budget: config.into(),
356 selected_order,
357 cost: None,
358 attempts,
359 }
360 }
361
362 pub fn selected(
363 attempt: RelationalJoinPlanningAttempt,
364 selected_order: Vec<String>,
365 config: RelationalJoinEnumerationConfig,
366 mut attempts: Vec<RelationalJoinPlanningAttempt>,
367 ) -> Self {
368 debug_assert_eq!(attempt.status, RelationalJoinPlanningStatus::Selected);
369 debug_assert!(attempt.fallback_class.is_none());
370 let strategy = attempt.strategy;
371 let reason = attempt.reason;
372 let memo_groups = attempt.memo_groups;
373 let memo_expressions = attempt.memo_expressions;
374 let cost = attempt.cost;
375 attempts.push(attempt);
376 Self {
377 strategy,
378 status: RelationalJoinPlanningStatus::Selected,
379 reason,
380 memo_groups,
381 memo_expressions,
382 budget: config.into(),
383 selected_order,
384 cost,
385 attempts,
386 }
387 }
388
389 pub fn explicit_syntax_order(
390 selected_order: Vec<String>,
391 config: RelationalJoinEnumerationConfig,
392 ) -> Self {
393 let attempt = RelationalJoinPlanningAttempt::explicit_syntax_order();
394 Self {
395 strategy: attempt.strategy,
396 status: attempt.status,
397 reason: attempt.reason,
398 memo_groups: None,
399 memo_expressions: None,
400 budget: config.into(),
401 selected_order,
402 cost: None,
403 attempts: vec![attempt],
404 }
405 }
406
407 pub fn fallback_to_syntax(
408 mut attempts: Vec<RelationalJoinPlanningAttempt>,
409 selected_order: Vec<String>,
410 config: RelationalJoinEnumerationConfig,
411 ) -> Option<Self> {
412 let failed = attempts
413 .iter()
414 .rev()
415 .find(|attempt| attempt.status == RelationalJoinPlanningStatus::Fallback)
416 .cloned()?;
417 attempts.push(RelationalJoinPlanningAttempt::syntax_fallback());
418 Some(Self {
419 strategy: failed.strategy,
420 status: RelationalJoinPlanningStatus::Fallback,
421 reason: failed.reason,
422 memo_groups: failed.memo_groups,
423 memo_expressions: failed.memo_expressions,
424 budget: config.into(),
425 selected_order,
426 cost: None,
427 attempts,
428 })
429 }
430
431 pub fn join_order_reordered(&self) -> bool {
432 self.status == RelationalJoinPlanningStatus::Selected
433 && self.reason == RelationalJoinPlanningReason::CostReordered
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440
441 #[test]
442 fn planning_enums_expose_stable_diagnostic_names() {
443 assert_eq!(
444 RelationalJoinPlanningStrategy::InnerJoinMemo.as_str(),
445 "inner_join_memo"
446 );
447 assert_eq!(
448 RelationalJoinPlanningStrategy::CsgCmpMemo.as_str(),
449 "csg_cmp_memo"
450 );
451 assert_eq!(RelationalJoinPlanningStatus::Fallback.as_str(), "fallback");
452 assert_eq!(
453 RelationalJoinPlanningReason::ExpressionBudgetExceeded.as_str(),
454 "expression_budget_exceeded"
455 );
456 assert_eq!(
457 RelationalJoinPlanningReason::SpecializedJoinNotEnumerated.as_str(),
458 "specialized_join_not_enumerated"
459 );
460 }
461
462 #[test]
463 fn budget_failure_retains_required_and_configured_counts() {
464 let config = RelationalJoinEnumerationConfig {
465 max_groups: 8,
466 max_expressions: 16,
467 };
468 let attempt = RelationalJoinPlanningAttempt::fallback_from_enumeration(
469 RelationalJoinPlanningStrategy::InnerJoinMemo,
470 &RelationalJoinEnumerationError::ExpressionBudgetExceeded {
471 required_expressions: 17,
472 max_expressions: 16,
473 },
474 )
475 .expect("budget exhaustion is fallback eligible");
476 let outcome = RelationalJoinPlanningOutcome::fallback_to_syntax(
477 vec![attempt],
478 vec!["a".to_string(), "b".to_string()],
479 config,
480 )
481 .expect("budget exhaustion produces a syntax fallback outcome");
482
483 assert_eq!(outcome.status, RelationalJoinPlanningStatus::Fallback);
484 assert_eq!(outcome.memo_expressions, Some(17));
485 assert_eq!(outcome.budget.max_expressions, 16);
486 assert_eq!(outcome.attempts.len(), 2);
487 assert_eq!(
488 outcome.attempts[0].fallback_class,
489 Some(RelationalJoinPlanningFallbackClass::Budget)
490 );
491 assert_eq!(
492 outcome.attempts[1].strategy,
493 RelationalJoinPlanningStrategy::SyntaxOrder
494 );
495 assert!(!outcome.join_order_reordered());
496 }
497
498 #[test]
499 fn invalid_join_graph_is_not_fallback_eligible() {
500 let attempt = RelationalJoinPlanningAttempt::fallback_from_enumeration(
501 RelationalJoinPlanningStrategy::CsgCmpMemo,
502 &RelationalJoinEnumerationError::EmptyGraph,
503 );
504 assert!(attempt.is_none());
505
506 let rewrite = RelationalJoinPlanningAttempt::fallback_from_rewrite(
507 RelationalJoinPlanningStrategy::CsgCmpMemo,
508 &RelationalJoinRewriteError::RelationSetMismatch,
509 );
510 assert!(rewrite.is_none());
511 }
512}