icydb_core/db/query/
planner.rs

1use crate::{
2    db::primitives::filter::{Cmp, FilterExpr},
3    obs::sink::{self, MetricsEvent, PlanKind},
4    prelude::*,
5    traits::EntityKind,
6};
7use std::fmt::{self, Display};
8
9///
10/// QueryPlan
11///
12
13#[derive(Debug)]
14pub enum QueryPlan {
15    FullScan,
16    Index(IndexPlan),
17    Keys(Vec<Key>),
18    /// Inclusive range over primary keys.
19    Range(Key, Key),
20}
21
22impl fmt::Display for QueryPlan {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        match self {
25            Self::Index(plan) => write!(f, "Index({plan})"),
26
27            Self::Keys(keys) => {
28                // Show up to 5 keys, then ellipsize
29                let preview: Vec<String> = keys.iter().take(5).map(|k| format!("{k:?}")).collect();
30
31                if keys.len() > 5 {
32                    write!(f, "Keys[{}… total {}]", preview.join(", "), keys.len())
33                } else {
34                    write!(f, "Keys[{}]", preview.join(", "))
35                }
36            }
37
38            Self::Range(start, end) => {
39                write!(f, "Range({start:?} → {end:?})")
40            }
41
42            Self::FullScan => write!(f, "FullScan"),
43        }
44    }
45}
46
47///
48/// IndexPlan
49///
50
51#[derive(Debug)]
52pub struct IndexPlan {
53    pub index: &'static IndexModel,
54    pub values: Vec<Value>,
55}
56
57impl Display for IndexPlan {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        let values: Vec<String> = self.values.iter().map(|v| format!("{v:?}")).collect();
60        write!(f, "index={} values=[{}]", self.index, values.join(", "))
61    }
62}
63
64///
65/// QueryPlanner
66///
67
68#[derive(Debug)]
69pub struct QueryPlanner {
70    pub filter: Option<FilterExpr>,
71}
72
73impl QueryPlanner {
74    #[must_use]
75    /// Create a planner from an optional filter expression.
76    pub fn new(filter: Option<&FilterExpr>) -> Self {
77        Self {
78            filter: filter.cloned(),
79        }
80    }
81
82    #[must_use]
83    /// Generate a query plan for the given entity type.
84    ///
85    /// Index plans are only produced when all equality values are indexable;
86    /// otherwise the planner falls back to a scan.
87    pub fn plan<E: EntityKind>(&self) -> QueryPlan {
88        // If filter is a primary key match
89        // this would handle One and Many queries
90        if let Some(plan) = self.extract_from_filter::<E>() {
91            sink::record(MetricsEvent::Plan {
92                kind: match &plan {
93                    QueryPlan::Keys(_) => PlanKind::Keys,
94                    QueryPlan::Index(_) => PlanKind::Index,
95                    QueryPlan::Range(_, _) => PlanKind::Range,
96                    QueryPlan::FullScan => PlanKind::FullScan,
97                },
98            });
99            return plan;
100        }
101
102        // check for index matches
103        // THIS WILL DO THE INDEX LOOKUPS
104        if !E::INDEXES.is_empty()
105            && let Some(plan) = self.extract_from_index::<E>()
106        {
107            sink::record(MetricsEvent::Plan {
108                kind: PlanKind::Index,
109            });
110            return plan;
111        }
112
113        // Fallback: do a full scan
114        sink::record(MetricsEvent::Plan {
115            kind: PlanKind::FullScan,
116        });
117
118        QueryPlan::FullScan
119    }
120
121    // extract_from_filter
122    fn extract_from_filter<E: EntityKind>(&self) -> Option<QueryPlan> {
123        let Some(filter) = &self.filter else {
124            return None;
125        };
126
127        match filter {
128            FilterExpr::Clause(clause) if clause.field == E::PRIMARY_KEY => match clause.cmp {
129                Cmp::Eq => clause.value.as_key().map(|key| QueryPlan::Keys(vec![key])),
130
131                Cmp::In => {
132                    if let Value::List(values) = &clause.value {
133                        let mut keys = values
134                            .iter()
135                            .filter_map(Value::as_key_coerced)
136                            .collect::<Vec<_>>();
137                        if keys.is_empty() {
138                            return Some(QueryPlan::Keys(Vec::new()));
139                        }
140                        keys.sort_unstable();
141                        keys.dedup();
142                        Some(QueryPlan::Keys(keys))
143                    } else {
144                        None
145                    }
146                }
147
148                _ => None,
149            },
150
151            _ => None,
152        }
153    }
154
155    // extract_from_index: build a leftmost equality prefix in terms of Value.
156    // Skip index planning when any equality value is not indexable.
157    fn extract_from_index<E: EntityKind>(&self) -> Option<QueryPlan> {
158        let Some(filter) = &self.filter else {
159            return None;
160        };
161
162        let mut best: Option<(usize, IndexPlan)> = None;
163
164        for index in E::INDEXES {
165            // Build leftmost equality prefix (only == supported for hashed indexes)
166            let mut values: Vec<Value> = Vec::with_capacity(index.fields.len());
167            let mut unusable = false;
168
169            for field in index.fields {
170                if let Some(v) = Self::find_eq_value(filter, field) {
171                    if v.to_index_fingerprint().is_none() {
172                        unusable = true;
173                        break;
174                    }
175                    values.push(v);
176                } else {
177                    break; // stop at first non-match
178                }
179            }
180
181            // Skip indexes that produced no equality prefix
182            if unusable || values.is_empty() {
183                continue;
184            }
185
186            let score = values.len();
187            let cand = (score, IndexPlan { index, values });
188
189            match &best {
190                Some((best_score, _)) if *best_score >= score => { /* keep current best */ }
191                _ => best = Some(cand),
192            }
193        }
194
195        best.map(|(_, plan)| QueryPlan::Index(plan))
196    }
197
198    /// Find an equality clause (`field == ?`) anywhere in the filter tree and return the Value.
199    fn find_eq_value(filter: &FilterExpr, field: &str) -> Option<Value> {
200        match filter {
201            FilterExpr::Clause(c) if c.field == field && matches!(c.cmp, Cmp::Eq) => {
202                Some(c.value.clone())
203            }
204            // Walk conjunctive subtrees
205            FilterExpr::And(list) => list.iter().find_map(|f| Self::find_eq_value(f, field)),
206            _ => None,
207        }
208    }
209}