Skip to main content

icydb_core/model/
index.rs

1//! Module: model::index
2//! Responsibility: module-local ownership and contracts for model::index.
3//! Does not own: cross-module orchestration outside this module.
4//! Boundary: exposes this module API while keeping implementation details internal.
5
6use std::fmt::{self, Display};
7
8///
9/// IndexExpression
10///
11/// Canonical deterministic expression key metadata for expression indexes.
12/// This enum is semantic authority across schema/runtime/planner boundaries.
13///
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub enum IndexExpression {
16    Lower(&'static str),
17    Upper(&'static str),
18    Trim(&'static str),
19    LowerTrim(&'static str),
20    Date(&'static str),
21    Year(&'static str),
22    Month(&'static str),
23    Day(&'static str),
24}
25
26impl IndexExpression {
27    /// Borrow the referenced field for this expression key item.
28    #[must_use]
29    pub const fn field(&self) -> &'static str {
30        match self {
31            Self::Lower(field)
32            | Self::Upper(field)
33            | Self::Trim(field)
34            | Self::LowerTrim(field)
35            | Self::Date(field)
36            | Self::Year(field)
37            | Self::Month(field)
38            | Self::Day(field) => field,
39        }
40    }
41
42    /// Return one stable discriminant for fingerprint hashing.
43    #[must_use]
44    pub const fn kind_tag(&self) -> u8 {
45        match self {
46            Self::Lower(_) => 0x01,
47            Self::Upper(_) => 0x02,
48            Self::Trim(_) => 0x03,
49            Self::LowerTrim(_) => 0x04,
50            Self::Date(_) => 0x05,
51            Self::Year(_) => 0x06,
52            Self::Month(_) => 0x07,
53            Self::Day(_) => 0x08,
54        }
55    }
56
57    /// Return whether planner/access Eq/In lookup lowering supports this expression
58    /// under `TextCasefold` coercion in the current release.
59    #[must_use]
60    pub const fn supports_text_casefold_lookup(&self) -> bool {
61        matches!(self, Self::Lower(_))
62    }
63}
64
65impl Display for IndexExpression {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        match self {
68            Self::Lower(field) => write!(f, "LOWER({field})"),
69            Self::Upper(field) => write!(f, "UPPER({field})"),
70            Self::Trim(field) => write!(f, "TRIM({field})"),
71            Self::LowerTrim(field) => write!(f, "LOWER(TRIM({field}))"),
72            Self::Date(field) => write!(f, "DATE({field})"),
73            Self::Year(field) => write!(f, "YEAR({field})"),
74            Self::Month(field) => write!(f, "MONTH({field})"),
75            Self::Day(field) => write!(f, "DAY({field})"),
76        }
77    }
78}
79
80///
81/// IndexKeyItem
82///
83/// Canonical index key-item metadata.
84/// `Field` preserves field-key behavior.
85/// `Expression` reserves deterministic expression-key identity metadata.
86///
87#[derive(Clone, Copy, Debug, Eq, PartialEq)]
88pub enum IndexKeyItem {
89    Field(&'static str),
90    Expression(IndexExpression),
91}
92
93impl IndexKeyItem {
94    /// Borrow this key-item's referenced field.
95    #[must_use]
96    pub const fn field(&self) -> &'static str {
97        match self {
98            Self::Field(field) => field,
99            Self::Expression(expression) => expression.field(),
100        }
101    }
102
103    /// Render one deterministic canonical text form for diagnostics/display.
104    #[must_use]
105    pub fn canonical_text(&self) -> String {
106        match self {
107            Self::Field(field) => (*field).to_string(),
108            Self::Expression(expression) => expression.to_string(),
109        }
110    }
111}
112
113///
114/// IndexKeyItemsRef
115///
116/// Borrowed view over index key-item metadata.
117/// Field-only indexes use `Fields`; mixed/explicit key metadata uses `Items`.
118///
119#[derive(Clone, Copy, Debug, Eq, PartialEq)]
120pub enum IndexKeyItemsRef {
121    Fields(&'static [&'static str]),
122    Items(&'static [IndexKeyItem]),
123}
124
125///
126/// IndexModel
127///
128/// Runtime-only descriptor for an index used by the executor and stores.
129/// Keeps core decoupled from the schema `Index` shape.
130/// Indexing is hash-based over `Value` equality for all variants.
131/// Unique indexes enforce value equality; hash collisions surface as corruption.
132///
133
134#[derive(Clone, Copy, Debug, Eq, PartialEq)]
135pub struct IndexModel {
136    /// Stable per-entity ordinal used for runtime index identity.
137    ordinal: u16,
138
139    /// Stable index name used for diagnostics and planner identity.
140    name: &'static str,
141    store: &'static str,
142    fields: &'static [&'static str],
143    key_items: Option<&'static [IndexKeyItem]>,
144    unique: bool,
145    // Raw schema-declared predicate text is input metadata only.
146    // Runtime/planner semantics must flow through canonical_index_predicate(...).
147    predicate: Option<&'static str>,
148}
149
150impl IndexModel {
151    #[must_use]
152    pub const fn new(
153        name: &'static str,
154        store: &'static str,
155        fields: &'static [&'static str],
156        unique: bool,
157    ) -> Self {
158        Self::new_with_ordinal_and_key_items_and_predicate(
159            0, name, store, fields, None, unique, None,
160        )
161    }
162
163    /// Construct one index descriptor with one explicit stable ordinal.
164    #[must_use]
165    pub const fn new_with_ordinal(
166        ordinal: u16,
167        name: &'static str,
168        store: &'static str,
169        fields: &'static [&'static str],
170        unique: bool,
171    ) -> Self {
172        Self::new_with_ordinal_and_key_items_and_predicate(
173            ordinal, name, store, fields, None, unique, None,
174        )
175    }
176
177    /// Construct one index descriptor with an optional conditional predicate.
178    #[must_use]
179    pub const fn new_with_predicate(
180        name: &'static str,
181        store: &'static str,
182        fields: &'static [&'static str],
183        unique: bool,
184        predicate: Option<&'static str>,
185    ) -> Self {
186        Self::new_with_ordinal_and_key_items_and_predicate(
187            0, name, store, fields, None, unique, predicate,
188        )
189    }
190
191    /// Construct one index descriptor with an explicit stable ordinal and optional predicate.
192    #[must_use]
193    pub const fn new_with_ordinal_and_predicate(
194        ordinal: u16,
195        name: &'static str,
196        store: &'static str,
197        fields: &'static [&'static str],
198        unique: bool,
199        predicate: Option<&'static str>,
200    ) -> Self {
201        Self::new_with_ordinal_and_key_items_and_predicate(
202            ordinal, name, store, fields, None, unique, predicate,
203        )
204    }
205
206    /// Construct one index descriptor with explicit canonical key-item metadata.
207    #[must_use]
208    pub const fn new_with_key_items(
209        name: &'static str,
210        store: &'static str,
211        fields: &'static [&'static str],
212        key_items: &'static [IndexKeyItem],
213        unique: bool,
214    ) -> Self {
215        Self::new_with_ordinal_and_key_items_and_predicate(
216            0,
217            name,
218            store,
219            fields,
220            Some(key_items),
221            unique,
222            None,
223        )
224    }
225
226    /// Construct one index descriptor with an explicit stable ordinal and key-item metadata.
227    #[must_use]
228    pub const fn new_with_ordinal_and_key_items(
229        ordinal: u16,
230        name: &'static str,
231        store: &'static str,
232        fields: &'static [&'static str],
233        key_items: &'static [IndexKeyItem],
234        unique: bool,
235    ) -> Self {
236        Self::new_with_ordinal_and_key_items_and_predicate(
237            ordinal,
238            name,
239            store,
240            fields,
241            Some(key_items),
242            unique,
243            None,
244        )
245    }
246
247    /// Construct one index descriptor with explicit key-item + predicate metadata.
248    #[must_use]
249    pub const fn new_with_key_items_and_predicate(
250        name: &'static str,
251        store: &'static str,
252        fields: &'static [&'static str],
253        key_items: Option<&'static [IndexKeyItem]>,
254        unique: bool,
255        predicate: Option<&'static str>,
256    ) -> Self {
257        Self::new_with_ordinal_and_key_items_and_predicate(
258            0, name, store, fields, key_items, unique, predicate,
259        )
260    }
261
262    /// Construct one index descriptor with full explicit runtime identity metadata.
263    #[must_use]
264    pub const fn new_with_ordinal_and_key_items_and_predicate(
265        ordinal: u16,
266        name: &'static str,
267        store: &'static str,
268        fields: &'static [&'static str],
269        key_items: Option<&'static [IndexKeyItem]>,
270        unique: bool,
271        predicate: Option<&'static str>,
272    ) -> Self {
273        Self {
274            ordinal,
275            name,
276            store,
277            fields,
278            key_items,
279            unique,
280            predicate,
281        }
282    }
283
284    /// Return the stable index name.
285    #[must_use]
286    pub const fn name(&self) -> &'static str {
287        self.name
288    }
289
290    /// Return the stable per-entity index ordinal.
291    #[must_use]
292    pub const fn ordinal(&self) -> u16 {
293        self.ordinal
294    }
295
296    /// Return the backing index store path.
297    #[must_use]
298    pub const fn store(&self) -> &'static str {
299        self.store
300    }
301
302    /// Return the canonical index field list.
303    #[must_use]
304    pub const fn fields(&self) -> &'static [&'static str] {
305        self.fields
306    }
307
308    /// Borrow canonical key-item metadata for this index.
309    #[must_use]
310    pub const fn key_items(&self) -> IndexKeyItemsRef {
311        if let Some(items) = self.key_items {
312            IndexKeyItemsRef::Items(items)
313        } else {
314            IndexKeyItemsRef::Fields(self.fields)
315        }
316    }
317
318    /// Return whether this index includes expression key items.
319    #[must_use]
320    pub const fn has_expression_key_items(&self) -> bool {
321        let Some(items) = self.key_items else {
322            return false;
323        };
324
325        let mut index = 0usize;
326        while index < items.len() {
327            if matches!(items[index], IndexKeyItem::Expression(_)) {
328                return true;
329            }
330            index = index.saturating_add(1);
331        }
332
333        false
334    }
335
336    /// Return whether the index enforces value uniqueness.
337    #[must_use]
338    pub const fn is_unique(&self) -> bool {
339        self.unique
340    }
341
342    /// Return optional schema-declared conditional index predicate text metadata.
343    ///
344    /// This string is input-only and must be lowered through the canonical
345    /// index-predicate boundary before semantic use.
346    #[must_use]
347    pub const fn predicate(&self) -> Option<&'static str> {
348        self.predicate
349    }
350
351    /// Whether this index's field prefix matches the start of another index.
352    #[must_use]
353    pub fn is_prefix_of(&self, other: &Self) -> bool {
354        self.fields().len() < other.fields().len() && other.fields().starts_with(self.fields())
355    }
356
357    fn joined_key_items(&self) -> String {
358        match self.key_items() {
359            IndexKeyItemsRef::Fields(fields) => fields.join(", "),
360            IndexKeyItemsRef::Items(items) => items
361                .iter()
362                .map(IndexKeyItem::canonical_text)
363                .collect::<Vec<_>>()
364                .join(", "),
365        }
366    }
367}
368
369impl Display for IndexModel {
370    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
371        let fields = self.joined_key_items();
372        if self.is_unique() {
373            if let Some(predicate) = self.predicate() {
374                write!(
375                    f,
376                    "{}: UNIQUE {}({}) WHERE {}",
377                    self.name(),
378                    self.store(),
379                    fields,
380                    predicate
381                )
382            } else {
383                write!(f, "{}: UNIQUE {}({})", self.name(), self.store(), fields)
384            }
385        } else if let Some(predicate) = self.predicate() {
386            write!(
387                f,
388                "{}: {}({}) WHERE {}",
389                self.name(),
390                self.store(),
391                fields,
392                predicate
393            )
394        } else {
395            write!(f, "{}: {}({})", self.name(), self.store(), fields)
396        }
397    }
398}
399
400///
401/// TESTS
402///
403
404#[cfg(test)]
405mod tests {
406    use crate::model::index::{IndexExpression, IndexKeyItem, IndexKeyItemsRef, IndexModel};
407
408    #[test]
409    fn index_model_with_predicate_exposes_predicate_metadata() {
410        let model = IndexModel::new_with_predicate(
411            "users|email|active",
412            "users::index",
413            &["email"],
414            false,
415            Some("active = true"),
416        );
417
418        assert_eq!(model.predicate(), Some("active = true"));
419        assert_eq!(
420            model.to_string(),
421            "users|email|active: users::index(email) WHERE active = true"
422        );
423    }
424
425    #[test]
426    fn index_model_without_predicate_preserves_legacy_display_shape() {
427        let model = IndexModel::new("users|email", "users::index", &["email"], true);
428
429        assert_eq!(model.predicate(), None);
430        assert_eq!(model.to_string(), "users|email: UNIQUE users::index(email)");
431    }
432
433    #[test]
434    fn index_model_with_explicit_key_items_exposes_expression_items() {
435        static KEY_ITEMS: [IndexKeyItem; 2] = [
436            IndexKeyItem::Field("tenant_id"),
437            IndexKeyItem::Expression(IndexExpression::Lower("email")),
438        ];
439        let model = IndexModel::new_with_key_items(
440            "users|tenant|email_expr",
441            "users::index",
442            &["tenant_id"],
443            &KEY_ITEMS,
444            false,
445        );
446
447        assert!(model.has_expression_key_items());
448        assert_eq!(
449            model.to_string(),
450            "users|tenant|email_expr: users::index(tenant_id, LOWER(email))"
451        );
452        assert!(matches!(
453            model.key_items(),
454            IndexKeyItemsRef::Items(items)
455                if items == KEY_ITEMS.as_slice()
456        ));
457    }
458
459    #[test]
460    fn index_expression_lookup_support_matrix_is_explicit() {
461        assert!(IndexExpression::Lower("email").supports_text_casefold_lookup());
462        assert!(!IndexExpression::Upper("email").supports_text_casefold_lookup());
463        assert!(!IndexExpression::Trim("email").supports_text_casefold_lookup());
464        assert!(!IndexExpression::LowerTrim("email").supports_text_casefold_lookup());
465        assert!(!IndexExpression::Date("created_at").supports_text_casefold_lookup());
466        assert!(!IndexExpression::Year("created_at").supports_text_casefold_lookup());
467        assert!(!IndexExpression::Month("created_at").supports_text_casefold_lookup());
468        assert!(!IndexExpression::Day("created_at").supports_text_casefold_lookup());
469    }
470}