Skip to main content

icydb_core/model/
index.rs

1//! Module: model::index
2//! Responsibility: runtime index metadata and expression-key contracts.
3//! Does not own: index storage persistence or route-selection policy.
4//! Boundary: authoritative index-level runtime model consumed by planner and executor code.
5
6use crate::db::Predicate;
7use std::fmt::{self, Display};
8
9///
10/// IndexExpression
11///
12/// Canonical deterministic expression key metadata for expression indexes.
13/// This enum is semantic authority across schema/runtime/planner boundaries.
14///
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum IndexExpression {
17    Lower(&'static str),
18    Upper(&'static str),
19    Trim(&'static str),
20    LowerTrim(&'static str),
21    Date(&'static str),
22    Year(&'static str),
23    Month(&'static str),
24    Day(&'static str),
25}
26
27impl IndexExpression {
28    /// Borrow the referenced field for this expression key item.
29    #[must_use]
30    pub const fn field(&self) -> &'static str {
31        match self {
32            Self::Lower(field)
33            | Self::Upper(field)
34            | Self::Trim(field)
35            | Self::LowerTrim(field)
36            | Self::Date(field)
37            | Self::Year(field)
38            | Self::Month(field)
39            | Self::Day(field) => field,
40        }
41    }
42
43    /// Return one stable discriminant for fingerprint hashing.
44    #[must_use]
45    pub const fn kind_tag(&self) -> u8 {
46        match self {
47            Self::Lower(_) => 0x01,
48            Self::Upper(_) => 0x02,
49            Self::Trim(_) => 0x03,
50            Self::LowerTrim(_) => 0x04,
51            Self::Date(_) => 0x05,
52            Self::Year(_) => 0x06,
53            Self::Month(_) => 0x07,
54            Self::Day(_) => 0x08,
55        }
56    }
57
58    /// Return whether planner/access Eq/In lookup lowering supports this expression
59    /// under `TextCasefold` coercion in the current release.
60    #[must_use]
61    pub const fn supports_text_casefold_lookup(&self) -> bool {
62        matches!(self, Self::Lower(_) | Self::Upper(_))
63    }
64}
65
66impl Display for IndexExpression {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        match self {
69            Self::Lower(field) => write!(f, "LOWER({field})"),
70            Self::Upper(field) => write!(f, "UPPER({field})"),
71            Self::Trim(field) => write!(f, "TRIM({field})"),
72            Self::LowerTrim(field) => write!(f, "LOWER(TRIM({field}))"),
73            Self::Date(field) => write!(f, "DATE({field})"),
74            Self::Year(field) => write!(f, "YEAR({field})"),
75            Self::Month(field) => write!(f, "MONTH({field})"),
76            Self::Day(field) => write!(f, "DAY({field})"),
77        }
78    }
79}
80
81///
82/// IndexKeyItem
83///
84/// Canonical index key-item metadata.
85/// `Field` preserves field-key behavior.
86/// `Expression` reserves deterministic expression-key identity metadata.
87///
88#[derive(Clone, Copy, Debug, Eq, PartialEq)]
89pub enum IndexKeyItem {
90    Field(&'static str),
91    Expression(IndexExpression),
92}
93
94impl IndexKeyItem {
95    /// Borrow this key-item's referenced field.
96    #[must_use]
97    pub const fn field(&self) -> &'static str {
98        match self {
99            Self::Field(field) => field,
100            Self::Expression(expression) => expression.field(),
101        }
102    }
103
104    /// Render one deterministic canonical text form for diagnostics/display.
105    #[must_use]
106    pub fn canonical_text(&self) -> String {
107        match self {
108            Self::Field(field) => (*field).to_string(),
109            Self::Expression(expression) => expression.to_string(),
110        }
111    }
112}
113
114///
115/// IndexKeyItemsRef
116///
117/// Borrowed view over index key-item metadata.
118/// Field-only indexes use `Fields`; mixed/explicit key metadata uses `Items`.
119///
120#[derive(Clone, Copy, Debug, Eq, PartialEq)]
121pub enum IndexKeyItemsRef {
122    Fields(&'static [&'static str]),
123    Items(&'static [IndexKeyItem]),
124}
125
126///
127/// GeneratedIndexPredicateResolver
128///
129/// Generated filtered indexes resolve canonical predicate semantics through
130/// one zero-argument function so runtime planning can borrow a shared static
131/// AST without reparsing SQL text.
132///
133pub type GeneratedIndexPredicateResolver = fn() -> &'static Predicate;
134
135///
136/// IndexPredicateMetadata
137///
138/// Canonical generated filtered-index predicate metadata.
139/// Raw SQL text is retained for diagnostics/display only.
140/// Runtime semantics always flow through `semantics()`.
141///
142#[derive(Clone, Copy, Debug)]
143pub struct IndexPredicateMetadata {
144    sql: &'static str,
145    semantics: GeneratedIndexPredicateResolver,
146}
147
148impl IndexPredicateMetadata {
149    /// Build one generated filtered-index predicate metadata bundle.
150    #[must_use]
151    #[doc(hidden)]
152    pub const fn generated(sql: &'static str, semantics: GeneratedIndexPredicateResolver) -> Self {
153        Self { sql, semantics }
154    }
155
156    /// Borrow the original schema-declared predicate text for diagnostics.
157    #[must_use]
158    pub const fn sql(&self) -> &'static str {
159        self.sql
160    }
161
162    /// Borrow the canonical generated predicate semantics.
163    #[must_use]
164    pub fn semantics(&self) -> &'static Predicate {
165        (self.semantics)()
166    }
167}
168
169impl PartialEq for IndexPredicateMetadata {
170    fn eq(&self, other: &Self) -> bool {
171        self.sql == other.sql && std::ptr::fn_addr_eq(self.semantics, other.semantics)
172    }
173}
174
175impl Eq for IndexPredicateMetadata {}
176
177///
178/// IndexModel
179///
180/// Runtime-only descriptor for an index used by the executor and stores.
181/// Keeps core decoupled from the schema `Index` shape.
182/// Indexing is hash-based over `Value` equality for all variants.
183/// Unique indexes enforce value equality; hash collisions surface as corruption.
184///
185
186#[derive(Clone, Copy, Debug, Eq, PartialEq)]
187pub struct IndexModel {
188    /// Stable per-entity ordinal used for runtime index identity.
189    ordinal: u16,
190
191    /// Stable index name used for diagnostics and planner identity.
192    name: &'static str,
193    store: &'static str,
194    fields: &'static [&'static str],
195    key_items: Option<&'static [IndexKeyItem]>,
196    unique: bool,
197    // Raw schema text remains for diagnostics/display only.
198    // Runtime/planner semantics must use the generated canonical predicate AST.
199    predicate: Option<IndexPredicateMetadata>,
200}
201
202impl IndexModel {
203    /// Construct one generated index descriptor.
204    ///
205    /// This constructor exists for derive/codegen output and trusted test
206    /// fixtures. Runtime planning and execution treat `IndexModel` values as
207    /// build-time-validated metadata.
208    #[must_use]
209    #[doc(hidden)]
210    pub const fn generated(
211        name: &'static str,
212        store: &'static str,
213        fields: &'static [&'static str],
214        unique: bool,
215    ) -> Self {
216        Self::generated_with_ordinal_and_key_items_and_predicate(
217            0, name, store, fields, None, unique, None,
218        )
219    }
220
221    /// Construct one index descriptor with one explicit stable ordinal.
222    #[must_use]
223    #[doc(hidden)]
224    pub const fn generated_with_ordinal(
225        ordinal: u16,
226        name: &'static str,
227        store: &'static str,
228        fields: &'static [&'static str],
229        unique: bool,
230    ) -> Self {
231        Self::generated_with_ordinal_and_key_items_and_predicate(
232            ordinal, name, store, fields, None, unique, None,
233        )
234    }
235
236    /// Construct one index descriptor with an optional conditional predicate.
237    #[must_use]
238    #[doc(hidden)]
239    pub const fn generated_with_predicate(
240        name: &'static str,
241        store: &'static str,
242        fields: &'static [&'static str],
243        unique: bool,
244        predicate: Option<IndexPredicateMetadata>,
245    ) -> Self {
246        Self::generated_with_ordinal_and_key_items_and_predicate(
247            0, name, store, fields, None, unique, predicate,
248        )
249    }
250
251    /// Construct one index descriptor with an explicit stable ordinal and optional predicate.
252    #[must_use]
253    #[doc(hidden)]
254    pub const fn generated_with_ordinal_and_predicate(
255        ordinal: u16,
256        name: &'static str,
257        store: &'static str,
258        fields: &'static [&'static str],
259        unique: bool,
260        predicate: Option<IndexPredicateMetadata>,
261    ) -> Self {
262        Self::generated_with_ordinal_and_key_items_and_predicate(
263            ordinal, name, store, fields, None, unique, predicate,
264        )
265    }
266
267    /// Construct one index descriptor with explicit canonical key-item metadata.
268    #[must_use]
269    #[doc(hidden)]
270    pub const fn generated_with_key_items(
271        name: &'static str,
272        store: &'static str,
273        fields: &'static [&'static str],
274        key_items: &'static [IndexKeyItem],
275        unique: bool,
276    ) -> Self {
277        Self::generated_with_ordinal_and_key_items_and_predicate(
278            0,
279            name,
280            store,
281            fields,
282            Some(key_items),
283            unique,
284            None,
285        )
286    }
287
288    /// Construct one index descriptor with an explicit stable ordinal and key-item metadata.
289    #[must_use]
290    #[doc(hidden)]
291    pub const fn generated_with_ordinal_and_key_items(
292        ordinal: u16,
293        name: &'static str,
294        store: &'static str,
295        fields: &'static [&'static str],
296        key_items: &'static [IndexKeyItem],
297        unique: bool,
298    ) -> Self {
299        Self::generated_with_ordinal_and_key_items_and_predicate(
300            ordinal,
301            name,
302            store,
303            fields,
304            Some(key_items),
305            unique,
306            None,
307        )
308    }
309
310    /// Construct one index descriptor with explicit key-item + predicate metadata.
311    #[must_use]
312    #[doc(hidden)]
313    pub const fn generated_with_key_items_and_predicate(
314        name: &'static str,
315        store: &'static str,
316        fields: &'static [&'static str],
317        key_items: Option<&'static [IndexKeyItem]>,
318        unique: bool,
319        predicate: Option<IndexPredicateMetadata>,
320    ) -> Self {
321        Self::generated_with_ordinal_and_key_items_and_predicate(
322            0, name, store, fields, key_items, unique, predicate,
323        )
324    }
325
326    /// Construct one index descriptor with full explicit runtime identity metadata.
327    #[must_use]
328    #[doc(hidden)]
329    pub const fn generated_with_ordinal_and_key_items_and_predicate(
330        ordinal: u16,
331        name: &'static str,
332        store: &'static str,
333        fields: &'static [&'static str],
334        key_items: Option<&'static [IndexKeyItem]>,
335        unique: bool,
336        predicate: Option<IndexPredicateMetadata>,
337    ) -> Self {
338        Self {
339            ordinal,
340            name,
341            store,
342            fields,
343            key_items,
344            unique,
345            predicate,
346        }
347    }
348
349    /// Return the stable index name.
350    #[must_use]
351    pub const fn name(&self) -> &'static str {
352        self.name
353    }
354
355    /// Return the stable per-entity index ordinal.
356    #[must_use]
357    pub const fn ordinal(&self) -> u16 {
358        self.ordinal
359    }
360
361    /// Return the backing index store path.
362    #[must_use]
363    pub const fn store(&self) -> &'static str {
364        self.store
365    }
366
367    /// Return the canonical index field list.
368    #[must_use]
369    pub const fn fields(&self) -> &'static [&'static str] {
370        self.fields
371    }
372
373    /// Borrow canonical key-item metadata for this index.
374    #[must_use]
375    pub const fn key_items(&self) -> IndexKeyItemsRef {
376        if let Some(items) = self.key_items {
377            IndexKeyItemsRef::Items(items)
378        } else {
379            IndexKeyItemsRef::Fields(self.fields)
380        }
381    }
382
383    /// Return whether this index includes expression key items.
384    #[must_use]
385    pub const fn has_expression_key_items(&self) -> bool {
386        let Some(items) = self.key_items else {
387            return false;
388        };
389
390        let mut index = 0usize;
391        while index < items.len() {
392            if matches!(items[index], IndexKeyItem::Expression(_)) {
393                return true;
394            }
395            index = index.saturating_add(1);
396        }
397
398        false
399    }
400
401    /// Return whether the index enforces value uniqueness.
402    #[must_use]
403    pub const fn is_unique(&self) -> bool {
404        self.unique
405    }
406
407    /// Return optional schema-declared conditional index predicate text metadata.
408    ///
409    /// Runtime planning and execution treat this as display metadata only.
410    #[must_use]
411    pub const fn predicate(&self) -> Option<&'static str> {
412        match self.predicate {
413            Some(predicate) => Some(predicate.sql()),
414            None => None,
415        }
416    }
417
418    /// Return the canonical generated conditional index predicate semantics.
419    #[must_use]
420    pub fn predicate_semantics(&self) -> Option<&'static Predicate> {
421        self.predicate.map(|predicate| predicate.semantics())
422    }
423
424    /// Whether this index's field prefix matches the start of another index.
425    #[must_use]
426    pub fn is_prefix_of(&self, other: &Self) -> bool {
427        self.fields().len() < other.fields().len() && other.fields().starts_with(self.fields())
428    }
429
430    fn joined_key_items(&self) -> String {
431        match self.key_items() {
432            IndexKeyItemsRef::Fields(fields) => fields.join(", "),
433            IndexKeyItemsRef::Items(items) => {
434                let mut joined = String::new();
435
436                for item in items {
437                    if !joined.is_empty() {
438                        joined.push_str(", ");
439                    }
440                    joined.push_str(item.canonical_text().as_str());
441                }
442
443                joined
444            }
445        }
446    }
447}
448
449impl Display for IndexModel {
450    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
451        let fields = self.joined_key_items();
452        if self.is_unique() {
453            if let Some(predicate) = self.predicate() {
454                write!(
455                    f,
456                    "{}: UNIQUE {}({}) WHERE {}",
457                    self.name(),
458                    self.store(),
459                    fields,
460                    predicate
461                )
462            } else {
463                write!(f, "{}: UNIQUE {}({})", self.name(), self.store(), fields)
464            }
465        } else if let Some(predicate) = self.predicate() {
466            write!(
467                f,
468                "{}: {}({}) WHERE {}",
469                self.name(),
470                self.store(),
471                fields,
472                predicate
473            )
474        } else {
475            write!(f, "{}: {}({})", self.name(), self.store(), fields)
476        }
477    }
478}
479
480///
481/// TESTS
482///
483
484#[cfg(test)]
485mod tests {
486    use crate::{
487        db::Predicate,
488        model::index::{
489            IndexExpression, IndexKeyItem, IndexKeyItemsRef, IndexModel, IndexPredicateMetadata,
490        },
491    };
492    use std::sync::LazyLock;
493
494    static ACTIVE_TRUE_PREDICATE: LazyLock<Predicate> =
495        LazyLock::new(|| Predicate::eq("active".to_string(), true.into()));
496
497    fn active_true_predicate() -> &'static Predicate {
498        &ACTIVE_TRUE_PREDICATE
499    }
500
501    #[test]
502    fn index_model_with_predicate_exposes_predicate_metadata() {
503        let model = IndexModel::generated_with_predicate(
504            "users|email|active",
505            "users::index",
506            &["email"],
507            false,
508            Some(IndexPredicateMetadata::generated(
509                "active = true",
510                active_true_predicate,
511            )),
512        );
513
514        assert_eq!(model.predicate(), Some("active = true"));
515        assert_eq!(model.predicate_semantics(), Some(active_true_predicate()),);
516        assert_eq!(
517            model.to_string(),
518            "users|email|active: users::index(email) WHERE active = true"
519        );
520    }
521
522    #[test]
523    fn index_model_without_predicate_preserves_display_shape() {
524        let model = IndexModel::generated("users|email", "users::index", &["email"], true);
525
526        assert_eq!(model.predicate(), None);
527        assert_eq!(model.to_string(), "users|email: UNIQUE users::index(email)");
528    }
529
530    #[test]
531    fn index_model_with_explicit_key_items_exposes_expression_items() {
532        static KEY_ITEMS: [IndexKeyItem; 2] = [
533            IndexKeyItem::Field("tenant_id"),
534            IndexKeyItem::Expression(IndexExpression::Lower("email")),
535        ];
536        let model = IndexModel::generated_with_key_items(
537            "users|tenant|email_expr",
538            "users::index",
539            &["tenant_id"],
540            &KEY_ITEMS,
541            false,
542        );
543
544        assert!(model.has_expression_key_items());
545        assert_eq!(
546            model.to_string(),
547            "users|tenant|email_expr: users::index(tenant_id, LOWER(email))"
548        );
549        assert!(matches!(
550            model.key_items(),
551            IndexKeyItemsRef::Items(items)
552                if items == KEY_ITEMS.as_slice()
553        ));
554    }
555
556    #[test]
557    fn index_expression_lookup_support_matrix_is_explicit() {
558        assert!(IndexExpression::Lower("email").supports_text_casefold_lookup());
559        assert!(IndexExpression::Upper("email").supports_text_casefold_lookup());
560        assert!(!IndexExpression::Trim("email").supports_text_casefold_lookup());
561        assert!(!IndexExpression::LowerTrim("email").supports_text_casefold_lookup());
562        assert!(!IndexExpression::Date("created_at").supports_text_casefold_lookup());
563        assert!(!IndexExpression::Year("created_at").supports_text_casefold_lookup());
564        assert!(!IndexExpression::Month("created_at").supports_text_casefold_lookup());
565        assert!(!IndexExpression::Day("created_at").supports_text_casefold_lookup());
566    }
567}