Skip to main content

icydb_model/node/
index.rs

1use crate::prelude::*;
2use icydb_schema::{SchemaContractError, SourceCheckExpr};
3use std::{
4    fmt::{self, Display},
5    ops::Not,
6};
7
8use crate::node::{Schema, SourceExpressionResolver};
9
10///
11/// IndexExpression
12///
13/// Canonical deterministic expression key metadata for expression indexes.
14/// This enum is semantic authority across schema/runtime/planner boundaries.
15///
16#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
17pub enum IndexExpression {
18    Lower(&'static str),
19    Upper(&'static str),
20    Trim(&'static str),
21    LowerTrim(&'static str),
22    Date(&'static str),
23    Year(&'static str),
24    Month(&'static str),
25    Day(&'static str),
26}
27
28impl IndexExpression {
29    /// Borrow the referenced field for this expression key item.
30    #[must_use]
31    pub const fn field(&self) -> &'static str {
32        match self {
33            Self::Lower(field)
34            | Self::Upper(field)
35            | Self::Trim(field)
36            | Self::LowerTrim(field)
37            | Self::Date(field)
38            | Self::Year(field)
39            | Self::Month(field)
40            | Self::Day(field) => field,
41        }
42    }
43}
44
45impl Display for IndexExpression {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        match self {
48            Self::Lower(field) => write!(f, "LOWER({field})"),
49            Self::Upper(field) => write!(f, "UPPER({field})"),
50            Self::Trim(field) => write!(f, "TRIM({field})"),
51            Self::LowerTrim(field) => write!(f, "LOWER(TRIM({field}))"),
52            Self::Date(field) => write!(f, "DATE({field})"),
53            Self::Year(field) => write!(f, "YEAR({field})"),
54            Self::Month(field) => write!(f, "MONTH({field})"),
55            Self::Day(field) => write!(f, "DAY({field})"),
56        }
57    }
58}
59
60///
61/// IndexKeyItem
62///
63/// Canonical index key-item metadata.
64/// `Field` preserves field-key behavior.
65/// `Expression` reserves deterministic expression-key identity metadata.
66///
67#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
68pub enum IndexKeyItem {
69    Field(&'static str),
70    Expression(IndexExpression),
71}
72
73impl IndexKeyItem {
74    /// Borrow this key-item's referenced field.
75    #[must_use]
76    pub const fn field(&self) -> &'static str {
77        match self {
78            Self::Field(field) => field,
79            Self::Expression(expression) => expression.field(),
80        }
81    }
82
83    /// Render one deterministic canonical text form for diagnostics/display.
84    #[must_use]
85    pub fn canonical_text(&self) -> String {
86        match self {
87            Self::Field(field) => (*field).to_string(),
88            Self::Expression(expression) => expression.to_string(),
89        }
90    }
91}
92
93///
94/// IndexKeyItemsRef
95///
96/// Borrowed view over index key-item metadata.
97/// Field-only indexes use `Fields`; mixed/explicit key metadata uses `Items`.
98///
99#[derive(Clone, Copy, Debug, Eq, PartialEq)]
100pub enum IndexKeyItemsRef {
101    Fields(&'static [&'static str]),
102    Items(&'static [IndexKeyItem]),
103}
104
105///
106/// Index
107///
108
109#[derive(Clone, Debug, Serialize)]
110pub struct Index {
111    name: &'static str,
112    fields: &'static [&'static str],
113
114    #[serde(skip_serializing_if = "Option::is_none")]
115    key_items: Option<&'static [IndexKeyItem]>,
116
117    #[serde(skip_serializing_if = "Not::not")]
118    unique: bool,
119
120    // Raw predicate SQL remains input metadata until lowered into canonical
121    // predicate semantics at runtime schema boundary.
122    #[serde(skip_serializing_if = "Option::is_none")]
123    predicate: Option<&'static str>,
124
125    #[serde(skip)]
126    predicate_expression: Option<SourceExpressionResolver>,
127}
128
129impl Index {
130    /// Build one index declaration from field-list and uniqueness metadata.
131    #[must_use]
132    pub const fn new(name: &'static str, fields: &'static [&'static str], unique: bool) -> Self {
133        Self::new_with_key_items_and_predicate(name, fields, None, unique, None, None)
134    }
135
136    /// Build one index declaration with optional conditional predicate metadata.
137    #[must_use]
138    pub const fn new_with_predicate(
139        name: &'static str,
140        fields: &'static [&'static str],
141        unique: bool,
142        predicate: Option<&'static str>,
143        predicate_expression: Option<SourceExpressionResolver>,
144    ) -> Self {
145        Self::new_with_key_items_and_predicate(
146            name,
147            fields,
148            None,
149            unique,
150            predicate,
151            predicate_expression,
152        )
153    }
154
155    /// Build one index declaration with explicit canonical key-item metadata.
156    #[must_use]
157    pub const fn new_with_key_items(
158        name: &'static str,
159        fields: &'static [&'static str],
160        key_items: &'static [IndexKeyItem],
161        unique: bool,
162    ) -> Self {
163        Self::new_with_key_items_and_predicate(name, fields, Some(key_items), unique, None, None)
164    }
165
166    /// Build one index declaration with explicit key items + predicate metadata.
167    #[must_use]
168    pub const fn new_with_key_items_and_predicate(
169        name: &'static str,
170        fields: &'static [&'static str],
171        key_items: Option<&'static [IndexKeyItem]>,
172        unique: bool,
173        predicate: Option<&'static str>,
174        predicate_expression: Option<SourceExpressionResolver>,
175    ) -> Self {
176        Self {
177            name,
178            fields,
179            key_items,
180            unique,
181            predicate,
182            predicate_expression,
183        }
184    }
185
186    /// Borrow the current index name.
187    #[must_use]
188    pub const fn name(&self) -> &'static str {
189        self.name
190    }
191
192    /// Borrow index field sequence.
193    #[must_use]
194    pub const fn fields(&self) -> &'static [&'static str] {
195        self.fields
196    }
197
198    /// Borrow canonical key-item metadata for this index.
199    #[must_use]
200    pub const fn key_items(&self) -> IndexKeyItemsRef {
201        if let Some(items) = self.key_items {
202            IndexKeyItemsRef::Items(items)
203        } else {
204            IndexKeyItemsRef::Fields(self.fields)
205        }
206    }
207
208    /// Return whether this index includes expression key items.
209    #[must_use]
210    pub const fn has_expression_key_items(&self) -> bool {
211        let Some(items) = self.key_items else {
212            return false;
213        };
214
215        let mut index = 0usize;
216        while index < items.len() {
217            if matches!(items[index], IndexKeyItem::Expression(_)) {
218                return true;
219            }
220            index = index.saturating_add(1);
221        }
222
223        false
224    }
225
226    /// Return whether the index enforces uniqueness.
227    #[must_use]
228    pub const fn is_unique(&self) -> bool {
229        self.unique
230    }
231
232    /// Return optional conditional-index predicate SQL metadata.
233    ///
234    /// This text is input-only; runtime/planner semantics must consume the
235    /// canonical lowered predicate form.
236    #[must_use]
237    pub const fn predicate(&self) -> Option<&'static str> {
238        self.predicate
239    }
240
241    /// Lower the optional compiler-validated predicate into the public source
242    /// AST.
243    ///
244    /// # Errors
245    ///
246    /// Returns a typed proposal error when an enum literal no longer resolves
247    /// through the sealed graph or the expression violates public bounds.
248    pub fn source_predicate(
249        &self,
250        schema: &Schema,
251    ) -> Result<Option<SourceCheckExpr>, SchemaContractError> {
252        match (self.predicate, self.predicate_expression) {
253            (None, None) => Ok(None),
254            (Some(_), Some(resolve)) => resolve(schema).map(Some),
255            (None, Some(_)) | (Some(_), None) => Err(SchemaContractError::InvalidExpression),
256        }
257    }
258
259    #[must_use]
260    pub fn is_prefix_of(&self, other: &Self) -> bool {
261        self.fields().len() < other.fields().len() && other.fields().starts_with(self.fields())
262    }
263
264    fn joined_key_items(&self) -> String {
265        match self.key_items() {
266            IndexKeyItemsRef::Fields(fields) => fields.join(", "),
267            IndexKeyItemsRef::Items(items) => {
268                let mut joined = String::new();
269
270                for item in items {
271                    if !joined.is_empty() {
272                        joined.push_str(", ");
273                    }
274                    joined.push_str(item.canonical_text().as_str());
275                }
276
277                joined
278            }
279        }
280    }
281}
282
283impl Display for Index {
284    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285        let fields = self.joined_key_items();
286
287        if self.is_unique() {
288            if let Some(predicate) = self.predicate() {
289                write!(f, "UNIQUE ({fields}) WHERE {predicate}")
290            } else {
291                write!(f, "UNIQUE ({fields})")
292            }
293        } else if let Some(predicate) = self.predicate() {
294            write!(f, "({fields}) WHERE {predicate}")
295        } else {
296            write!(f, "({fields})")
297        }
298    }
299}
300
301impl MacroNode for Index {
302    fn as_any(&self) -> &dyn std::any::Any {
303        self
304    }
305}
306
307impl ValidateNode for Index {
308    fn validate(&self) -> Result<(), ErrorTree> {
309        let mut errs = ErrorTree::new();
310        validate_source_name(
311            &mut errs,
312            "index",
313            self.name(),
314            icydb_schema::IndexSourceKey::try_new,
315        );
316        errs.result()
317    }
318}
319
320impl VisitableNode for Index {
321    fn route_key(&self) -> String {
322        self.joined_key_items()
323    }
324}
325
326///
327/// TESTS
328///
329
330#[cfg(test)]
331mod tests {
332    use crate::node::index::{Index, IndexExpression, IndexKeyItem, IndexKeyItemsRef};
333
334    #[test]
335    fn index_with_predicate_reports_conditional_shape() {
336        let index = Index::new_with_predicate(
337            "idx_user__email",
338            &["email"],
339            false,
340            Some("active = true"),
341            None,
342        );
343
344        assert_eq!(index.predicate(), Some("active = true"));
345        assert_eq!(index.to_string(), "(email) WHERE active = true");
346    }
347
348    #[test]
349    fn index_without_predicate_preserves_unconditional_shape() {
350        let index = Index::new("uidx_user__email", &["email"], true);
351
352        assert_eq!(index.predicate(), None);
353        assert_eq!(index.to_string(), "UNIQUE (email)");
354    }
355
356    #[test]
357    fn index_with_explicit_key_items_exposes_expression_items() {
358        static KEY_ITEMS: [IndexKeyItem; 2] = [
359            IndexKeyItem::Field("tenant_id"),
360            IndexKeyItem::Expression(IndexExpression::Lower("email")),
361        ];
362        let index = Index::new_with_key_items(
363            "idx_user__tenant_id__lower_email",
364            &["tenant_id"],
365            &KEY_ITEMS,
366            false,
367        );
368
369        assert!(index.has_expression_key_items());
370        assert_eq!(index.to_string(), "(tenant_id, LOWER(email))");
371        std::assert_matches!(
372            index.key_items(),
373            IndexKeyItemsRef::Items(items)
374                if items == KEY_ITEMS.as_slice()
375        );
376    }
377}