1use std::fmt::{Display, Formatter};
16use std::str::FromStr;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum SearchMode {
20 Memo,
21 DirectFallback,
22}
23
24#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
25pub enum OptimizerSearchDirective {
26 #[default]
27 Auto,
28 Memo,
29 DirectFallback,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum OptimizerSearchDirectiveError {
34 MemoGroupBudgetExceeded {
35 required_groups: usize,
36 max_groups: usize,
37 },
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct RuleEvent {
42 rule: String,
43 outcome: RuleOutcome,
44 detail: String,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum RuleOutcome {
49 Applied,
50 Skipped,
51 Estimated,
52 Selected,
53}
54
55impl RuleEvent {
56 pub fn applied(rule: impl Into<String>, detail: impl Into<String>) -> Self {
57 Self::new(rule, RuleOutcome::Applied, detail)
58 }
59
60 pub fn skipped(rule: impl Into<String>, detail: impl Into<String>) -> Self {
61 Self::new(rule, RuleOutcome::Skipped, detail)
62 }
63
64 pub fn estimated(rule: impl Into<String>, detail: impl Into<String>) -> Self {
65 Self::new(rule, RuleOutcome::Estimated, detail)
66 }
67
68 pub fn selected(rule: impl Into<String>, detail: impl Into<String>) -> Self {
69 Self::new(rule, RuleOutcome::Selected, detail)
70 }
71
72 pub fn new(rule: impl Into<String>, outcome: RuleOutcome, detail: impl Into<String>) -> Self {
73 Self {
74 rule: rule.into(),
75 outcome,
76 detail: detail.into(),
77 }
78 }
79
80 pub fn rule(&self) -> &str {
81 &self.rule
82 }
83
84 pub fn outcome(&self) -> RuleOutcome {
85 self.outcome
86 }
87
88 pub fn detail(&self) -> &str {
89 &self.detail
90 }
91
92 pub fn into_decision(self) -> String {
93 format!("{} {}: {}", self.outcome.as_str(), self.rule, self.detail)
94 }
95
96 pub fn from_decision(decision: &str) -> Option<Self> {
97 let (outcome, rest) = decision.split_once(' ')?;
98 let (rule, detail) = rest.split_once(": ")?;
99 Some(Self::new(
100 rule,
101 outcome.parse::<RuleOutcome>().ok()?,
102 detail,
103 ))
104 }
105}
106
107impl SearchMode {
108 pub fn all() -> &'static [Self] {
109 const ALL: &[SearchMode] = &[SearchMode::Memo, SearchMode::DirectFallback];
110 ALL
111 }
112
113 pub fn as_str(self) -> &'static str {
114 match self {
115 SearchMode::Memo => "memo",
116 SearchMode::DirectFallback => "direct_fallback",
117 }
118 }
119}
120
121impl OptimizerSearchDirective {
122 pub const fn as_str(self) -> &'static str {
123 match self {
124 Self::Auto => "auto",
125 Self::Memo => "memo",
126 Self::DirectFallback => "direct_fallback",
127 }
128 }
129}
130
131impl FromStr for OptimizerSearchDirective {
132 type Err = &'static str;
133
134 fn from_str(value: &str) -> Result<Self, Self::Err> {
135 match value {
136 "auto" => Ok(Self::Auto),
137 "memo" => Ok(Self::Memo),
138 "direct_fallback" => Ok(Self::DirectFallback),
139 _ => Err("unknown optimizer search directive"),
140 }
141 }
142}
143
144impl Display for OptimizerSearchDirectiveError {
145 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
146 match self {
147 Self::MemoGroupBudgetExceeded {
148 required_groups,
149 max_groups,
150 } => write!(
151 formatter,
152 "memo search directive requires {required_groups} groups but max_groups is {max_groups}"
153 ),
154 }
155 }
156}
157
158impl std::error::Error for OptimizerSearchDirectiveError {}
159
160impl RuleOutcome {
161 pub fn all() -> &'static [Self] {
162 const ALL: &[RuleOutcome] = &[
163 RuleOutcome::Applied,
164 RuleOutcome::Skipped,
165 RuleOutcome::Estimated,
166 RuleOutcome::Selected,
167 ];
168 ALL
169 }
170
171 pub fn as_str(self) -> &'static str {
172 match self {
173 RuleOutcome::Applied => "apply",
174 RuleOutcome::Skipped => "skip",
175 RuleOutcome::Estimated => "estimate",
176 RuleOutcome::Selected => "selected",
177 }
178 }
179}
180
181impl FromStr for SearchMode {
182 type Err = &'static str;
183
184 fn from_str(value: &str) -> Result<Self, Self::Err> {
185 Self::all()
186 .iter()
187 .copied()
188 .find(|mode| mode.as_str() == value)
189 .ok_or("unknown optimizer search mode")
190 }
191}
192
193impl FromStr for RuleOutcome {
194 type Err = &'static str;
195
196 fn from_str(value: &str) -> Result<Self, Self::Err> {
197 Self::all()
198 .iter()
199 .copied()
200 .find(|outcome| outcome.as_str() == value)
201 .ok_or("unknown optimizer rule outcome")
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::{OptimizerSearchDirective, RuleEvent, RuleOutcome, SearchMode};
208
209 #[test]
210 fn search_directive_strings_round_trip() {
211 for directive in [
212 OptimizerSearchDirective::Auto,
213 OptimizerSearchDirective::Memo,
214 OptimizerSearchDirective::DirectFallback,
215 ] {
216 assert_eq!(
217 directive.as_str().parse::<OptimizerSearchDirective>(),
218 Ok(directive)
219 );
220 }
221 assert!("unknown".parse::<OptimizerSearchDirective>().is_err());
222 }
223
224 #[test]
225 fn search_mode_and_rule_outcome_strings_round_trip() {
226 for mode in SearchMode::all() {
227 assert_eq!(mode.as_str().parse::<SearchMode>(), Ok(*mode));
228 }
229 assert!("unknown".parse::<SearchMode>().is_err());
230 for outcome in RuleOutcome::all() {
231 assert_eq!(outcome.as_str().parse::<RuleOutcome>(), Ok(*outcome));
232 }
233 assert!("unknown".parse::<RuleOutcome>().is_err());
234 }
235
236 #[test]
237 fn rule_event_round_trips_through_legacy_decision_text() {
238 let event = RuleEvent::estimated("index_seek", "rows=1 cost=3");
239
240 assert_eq!(event.into_decision(), "estimate index_seek: rows=1 cost=3");
241 let parsed = RuleEvent::from_decision("estimate index_seek: rows=1 cost=3").unwrap();
242 assert_eq!(parsed.rule(), "index_seek");
243 assert_eq!(parsed.outcome(), RuleOutcome::Estimated);
244 assert_eq!(parsed.detail(), "rows=1 cost=3");
245 }
246}