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