Skip to main content

hawdb_cascades/
search.rs

1// Copyright 2026 Nowledge
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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}