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    source_key: &'static str,
112    name: &'static str,
113    fields: &'static [&'static str],
114
115    #[serde(skip_serializing_if = "Option::is_none")]
116    key_items: Option<&'static [IndexKeyItem]>,
117
118    #[serde(skip_serializing_if = "Not::not")]
119    unique: bool,
120
121    // Raw predicate SQL remains input metadata until lowered into canonical
122    // predicate semantics at runtime schema boundary.
123    #[serde(skip_serializing_if = "Option::is_none")]
124    predicate: Option<&'static str>,
125
126    #[serde(skip)]
127    predicate_expression: Option<SourceExpressionResolver>,
128}
129
130impl Index {
131    /// Build one index declaration from field-list and uniqueness metadata.
132    #[must_use]
133    pub const fn new(
134        source_key: &'static str,
135        name: &'static str,
136        fields: &'static [&'static str],
137        unique: bool,
138    ) -> Self {
139        Self::new_with_key_items_and_predicate(source_key, name, fields, None, unique, None, None)
140    }
141
142    /// Build one index declaration with optional conditional predicate metadata.
143    #[must_use]
144    pub const fn new_with_predicate(
145        source_key: &'static str,
146        name: &'static str,
147        fields: &'static [&'static str],
148        unique: bool,
149        predicate: Option<&'static str>,
150        predicate_expression: Option<SourceExpressionResolver>,
151    ) -> Self {
152        Self::new_with_key_items_and_predicate(
153            source_key,
154            name,
155            fields,
156            None,
157            unique,
158            predicate,
159            predicate_expression,
160        )
161    }
162
163    /// Build one index declaration with explicit canonical key-item metadata.
164    #[must_use]
165    pub const fn new_with_key_items(
166        source_key: &'static str,
167        name: &'static str,
168        fields: &'static [&'static str],
169        key_items: &'static [IndexKeyItem],
170        unique: bool,
171    ) -> Self {
172        Self::new_with_key_items_and_predicate(
173            source_key,
174            name,
175            fields,
176            Some(key_items),
177            unique,
178            None,
179            None,
180        )
181    }
182
183    /// Build one index declaration with explicit key items + predicate metadata.
184    #[must_use]
185    pub const fn new_with_key_items_and_predicate(
186        source_key: &'static str,
187        name: &'static str,
188        fields: &'static [&'static str],
189        key_items: Option<&'static [IndexKeyItem]>,
190        unique: bool,
191        predicate: Option<&'static str>,
192        predicate_expression: Option<SourceExpressionResolver>,
193    ) -> Self {
194        Self {
195            source_key,
196            name,
197            fields,
198            key_items,
199            unique,
200            predicate,
201            predicate_expression,
202        }
203    }
204
205    /// Borrow the immutable index source key.
206    #[must_use]
207    pub const fn source_key(&self) -> &'static str {
208        self.source_key
209    }
210
211    /// Borrow the editable index name.
212    #[must_use]
213    pub const fn name(&self) -> &'static str {
214        self.name
215    }
216
217    /// Borrow index field sequence.
218    #[must_use]
219    pub const fn fields(&self) -> &'static [&'static str] {
220        self.fields
221    }
222
223    /// Borrow canonical key-item metadata for this index.
224    #[must_use]
225    pub const fn key_items(&self) -> IndexKeyItemsRef {
226        if let Some(items) = self.key_items {
227            IndexKeyItemsRef::Items(items)
228        } else {
229            IndexKeyItemsRef::Fields(self.fields)
230        }
231    }
232
233    /// Return whether this index includes expression key items.
234    #[must_use]
235    pub const fn has_expression_key_items(&self) -> bool {
236        let Some(items) = self.key_items else {
237            return false;
238        };
239
240        let mut index = 0usize;
241        while index < items.len() {
242            if matches!(items[index], IndexKeyItem::Expression(_)) {
243                return true;
244            }
245            index = index.saturating_add(1);
246        }
247
248        false
249    }
250
251    /// Return whether the index enforces uniqueness.
252    #[must_use]
253    pub const fn is_unique(&self) -> bool {
254        self.unique
255    }
256
257    /// Return optional conditional-index predicate SQL metadata.
258    ///
259    /// This text is input-only; runtime/planner semantics must consume the
260    /// canonical lowered predicate form.
261    #[must_use]
262    pub const fn predicate(&self) -> Option<&'static str> {
263        self.predicate
264    }
265
266    /// Lower the optional compiler-validated predicate into the public source
267    /// AST.
268    ///
269    /// # Errors
270    ///
271    /// Returns a typed proposal error when an enum literal no longer resolves
272    /// through the sealed graph or the expression violates public bounds.
273    pub fn source_predicate(
274        &self,
275        schema: &Schema,
276    ) -> Result<Option<SourceCheckExpr>, SchemaContractError> {
277        match (self.predicate, self.predicate_expression) {
278            (None, None) => Ok(None),
279            (Some(_), Some(resolve)) => resolve(schema).map(Some),
280            (None, Some(_)) | (Some(_), None) => Err(SchemaContractError::InvalidExpression),
281        }
282    }
283
284    #[must_use]
285    pub fn is_prefix_of(&self, other: &Self) -> bool {
286        self.fields().len() < other.fields().len() && other.fields().starts_with(self.fields())
287    }
288
289    fn joined_key_items(&self) -> String {
290        match self.key_items() {
291            IndexKeyItemsRef::Fields(fields) => fields.join(", "),
292            IndexKeyItemsRef::Items(items) => {
293                let mut joined = String::new();
294
295                for item in items {
296                    if !joined.is_empty() {
297                        joined.push_str(", ");
298                    }
299                    joined.push_str(item.canonical_text().as_str());
300                }
301
302                joined
303            }
304        }
305    }
306}
307
308impl Display for Index {
309    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310        let fields = self.joined_key_items();
311
312        if self.is_unique() {
313            if let Some(predicate) = self.predicate() {
314                write!(f, "UNIQUE ({fields}) WHERE {predicate}")
315            } else {
316                write!(f, "UNIQUE ({fields})")
317            }
318        } else if let Some(predicate) = self.predicate() {
319            write!(f, "({fields}) WHERE {predicate}")
320        } else {
321            write!(f, "({fields})")
322        }
323    }
324}
325
326impl MacroNode for Index {
327    fn as_any(&self) -> &dyn std::any::Any {
328        self
329    }
330}
331
332impl ValidateNode for Index {
333    fn validate(&self) -> Result<(), ErrorTree> {
334        let mut errs = ErrorTree::new();
335        validate_source_key(
336            &mut errs,
337            "index",
338            self.source_key(),
339            icydb_schema::IndexSourceKey::try_new,
340        );
341        errs.result()
342    }
343}
344
345impl VisitableNode for Index {
346    fn route_key(&self) -> String {
347        self.joined_key_items()
348    }
349}
350
351///
352/// TESTS
353///
354
355#[cfg(test)]
356mod tests {
357    use crate::node::index::{Index, IndexExpression, IndexKeyItem, IndexKeyItemsRef};
358
359    #[test]
360    fn index_with_predicate_reports_conditional_shape() {
361        let index = Index::new_with_predicate(
362            "email_active",
363            "idx_user__email",
364            &["email"],
365            false,
366            Some("active = true"),
367            None,
368        );
369
370        assert_eq!(index.predicate(), Some("active = true"));
371        assert_eq!(index.to_string(), "(email) WHERE active = true");
372    }
373
374    #[test]
375    fn index_without_predicate_preserves_unconditional_shape() {
376        let index = Index::new("email", "uidx_user__email", &["email"], true);
377
378        assert_eq!(index.predicate(), None);
379        assert_eq!(index.to_string(), "UNIQUE (email)");
380    }
381
382    #[test]
383    fn index_with_explicit_key_items_exposes_expression_items() {
384        static KEY_ITEMS: [IndexKeyItem; 2] = [
385            IndexKeyItem::Field("tenant_id"),
386            IndexKeyItem::Expression(IndexExpression::Lower("email")),
387        ];
388        let index = Index::new_with_key_items(
389            "tenant_lower_email",
390            "idx_user__tenant_id__lower_email",
391            &["tenant_id"],
392            &KEY_ITEMS,
393            false,
394        );
395
396        assert!(index.has_expression_key_items());
397        assert_eq!(index.to_string(), "(tenant_id, LOWER(email))");
398        std::assert_matches!(
399            index.key_items(),
400            IndexKeyItemsRef::Items(items)
401                if items == KEY_ITEMS.as_slice()
402        );
403    }
404}