Skip to main content

icydb_schema/node/
index.rs

1use crate::prelude::*;
2use std::{
3    fmt::{self, Display},
4    ops::Not,
5};
6
7///
8/// IndexExpression
9///
10/// Canonical deterministic expression key metadata for expression indexes.
11/// This enum is semantic authority across schema/runtime/planner boundaries.
12///
13#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
14pub enum IndexExpression {
15    Lower(&'static str),
16    Upper(&'static str),
17    Trim(&'static str),
18    LowerTrim(&'static str),
19    Date(&'static str),
20    Year(&'static str),
21    Month(&'static str),
22    Day(&'static str),
23}
24
25impl IndexExpression {
26    /// Borrow the referenced field for this expression key item.
27    #[must_use]
28    pub const fn field(&self) -> &'static str {
29        match self {
30            Self::Lower(field)
31            | Self::Upper(field)
32            | Self::Trim(field)
33            | Self::LowerTrim(field)
34            | Self::Date(field)
35            | Self::Year(field)
36            | Self::Month(field)
37            | Self::Day(field) => field,
38        }
39    }
40}
41
42impl Display for IndexExpression {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self {
45            Self::Lower(field) => write!(f, "LOWER({field})"),
46            Self::Upper(field) => write!(f, "UPPER({field})"),
47            Self::Trim(field) => write!(f, "TRIM({field})"),
48            Self::LowerTrim(field) => write!(f, "LOWER(TRIM({field}))"),
49            Self::Date(field) => write!(f, "DATE({field})"),
50            Self::Year(field) => write!(f, "YEAR({field})"),
51            Self::Month(field) => write!(f, "MONTH({field})"),
52            Self::Day(field) => write!(f, "DAY({field})"),
53        }
54    }
55}
56
57///
58/// IndexKeyItem
59///
60/// Canonical index key-item metadata.
61/// `Field` preserves field-key behavior.
62/// `Expression` reserves deterministic expression-key identity metadata.
63///
64#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
65pub enum IndexKeyItem {
66    Field(&'static str),
67    Expression(IndexExpression),
68}
69
70impl IndexKeyItem {
71    /// Borrow this key-item's referenced field.
72    #[must_use]
73    pub const fn field(&self) -> &'static str {
74        match self {
75            Self::Field(field) => field,
76            Self::Expression(expression) => expression.field(),
77        }
78    }
79
80    /// Render one deterministic canonical text form for diagnostics/display.
81    #[must_use]
82    pub fn canonical_text(&self) -> String {
83        match self {
84            Self::Field(field) => (*field).to_string(),
85            Self::Expression(expression) => expression.to_string(),
86        }
87    }
88}
89
90///
91/// IndexKeyItemsRef
92///
93/// Borrowed view over index key-item metadata.
94/// Field-only indexes use `Fields`; mixed/explicit key metadata uses `Items`.
95///
96#[derive(Clone, Copy, Debug, Eq, PartialEq)]
97pub enum IndexKeyItemsRef {
98    Fields(&'static [&'static str]),
99    Items(&'static [IndexKeyItem]),
100}
101
102///
103/// Index
104///
105
106#[derive(Clone, Debug, Serialize)]
107pub struct Index {
108    fields: &'static [&'static str],
109
110    #[serde(default, skip_serializing_if = "Option::is_none")]
111    key_items: Option<&'static [IndexKeyItem]>,
112
113    #[serde(default, skip_serializing_if = "Not::not")]
114    unique: bool,
115
116    // Raw predicate SQL remains input metadata until lowered into canonical
117    // predicate semantics at runtime schema boundary.
118    #[serde(default, skip_serializing_if = "Option::is_none")]
119    predicate: Option<&'static str>,
120}
121
122impl Index {
123    /// Build one index declaration from field-list and uniqueness metadata.
124    #[must_use]
125    pub const fn new(fields: &'static [&'static str], unique: bool) -> Self {
126        Self::new_with_key_items_and_predicate(fields, None, unique, None)
127    }
128
129    /// Build one index declaration with optional conditional predicate metadata.
130    #[must_use]
131    pub const fn new_with_predicate(
132        fields: &'static [&'static str],
133        unique: bool,
134        predicate: Option<&'static str>,
135    ) -> Self {
136        Self::new_with_key_items_and_predicate(fields, None, unique, predicate)
137    }
138
139    /// Build one index declaration with explicit canonical key-item metadata.
140    #[must_use]
141    pub const fn new_with_key_items(
142        fields: &'static [&'static str],
143        key_items: &'static [IndexKeyItem],
144        unique: bool,
145    ) -> Self {
146        Self::new_with_key_items_and_predicate(fields, Some(key_items), unique, None)
147    }
148
149    /// Build one index declaration with explicit key items + predicate metadata.
150    #[must_use]
151    pub const fn new_with_key_items_and_predicate(
152        fields: &'static [&'static str],
153        key_items: Option<&'static [IndexKeyItem]>,
154        unique: bool,
155        predicate: Option<&'static str>,
156    ) -> Self {
157        Self {
158            fields,
159            key_items,
160            unique,
161            predicate,
162        }
163    }
164
165    /// Borrow index field sequence.
166    #[must_use]
167    pub const fn fields(&self) -> &'static [&'static str] {
168        self.fields
169    }
170
171    /// Borrow canonical key-item metadata for this index.
172    #[must_use]
173    pub const fn key_items(&self) -> IndexKeyItemsRef {
174        if let Some(items) = self.key_items {
175            IndexKeyItemsRef::Items(items)
176        } else {
177            IndexKeyItemsRef::Fields(self.fields)
178        }
179    }
180
181    /// Return whether this index includes expression key items.
182    #[must_use]
183    pub const fn has_expression_key_items(&self) -> bool {
184        let Some(items) = self.key_items else {
185            return false;
186        };
187
188        let mut index = 0usize;
189        while index < items.len() {
190            if matches!(items[index], IndexKeyItem::Expression(_)) {
191                return true;
192            }
193            index = index.saturating_add(1);
194        }
195
196        false
197    }
198
199    /// Return whether the index enforces uniqueness.
200    #[must_use]
201    pub const fn is_unique(&self) -> bool {
202        self.unique
203    }
204
205    /// Return optional conditional-index predicate SQL metadata.
206    ///
207    /// This text is input-only; runtime/planner semantics must consume the
208    /// canonical lowered predicate form.
209    #[must_use]
210    pub const fn predicate(&self) -> Option<&'static str> {
211        self.predicate
212    }
213
214    #[must_use]
215    pub fn is_prefix_of(&self, other: &Self) -> bool {
216        self.fields().len() < other.fields().len() && other.fields().starts_with(self.fields())
217    }
218
219    fn joined_key_items(&self) -> String {
220        match self.key_items() {
221            IndexKeyItemsRef::Fields(fields) => fields.join(", "),
222            IndexKeyItemsRef::Items(items) => items
223                .iter()
224                .map(IndexKeyItem::canonical_text)
225                .collect::<Vec<_>>()
226                .join(", "),
227        }
228    }
229}
230
231impl Display for Index {
232    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233        let fields = self.joined_key_items();
234
235        if self.is_unique() {
236            if let Some(predicate) = self.predicate() {
237                write!(f, "UNIQUE ({fields}) WHERE {predicate}")
238            } else {
239                write!(f, "UNIQUE ({fields})")
240            }
241        } else if let Some(predicate) = self.predicate() {
242            write!(f, "({fields}) WHERE {predicate}")
243        } else {
244            write!(f, "({fields})")
245        }
246    }
247}
248
249impl MacroNode for Index {
250    fn as_any(&self) -> &dyn std::any::Any {
251        self
252    }
253}
254
255impl ValidateNode for Index {}
256
257impl VisitableNode for Index {
258    fn route_key(&self) -> String {
259        self.joined_key_items()
260    }
261}
262
263///
264/// TESTS
265///
266
267#[cfg(test)]
268mod tests {
269    use crate::node::index::{Index, IndexExpression, IndexKeyItem, IndexKeyItemsRef};
270
271    #[test]
272    fn index_with_predicate_reports_conditional_shape() {
273        let index = Index::new_with_predicate(&["email"], false, Some("active = true"));
274
275        assert_eq!(index.predicate(), Some("active = true"));
276        assert_eq!(index.to_string(), "(email) WHERE active = true");
277    }
278
279    #[test]
280    fn index_without_predicate_preserves_legacy_shape() {
281        let index = Index::new(&["email"], true);
282
283        assert_eq!(index.predicate(), None);
284        assert_eq!(index.to_string(), "UNIQUE (email)");
285    }
286
287    #[test]
288    fn index_with_explicit_key_items_exposes_expression_items() {
289        static KEY_ITEMS: [IndexKeyItem; 2] = [
290            IndexKeyItem::Field("tenant_id"),
291            IndexKeyItem::Expression(IndexExpression::Lower("email")),
292        ];
293        let index = Index::new_with_key_items(&["tenant_id"], &KEY_ITEMS, false);
294
295        assert!(index.has_expression_key_items());
296        assert_eq!(index.to_string(), "(tenant_id, LOWER(email))");
297        assert!(matches!(
298            index.key_items(),
299            IndexKeyItemsRef::Items(items)
300                if items == KEY_ITEMS.as_slice()
301        ));
302    }
303}