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
58impl Display for IndexExpression {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            Self::Lower(field) => write!(f, "LOWER({field})"),
62            Self::Upper(field) => write!(f, "UPPER({field})"),
63            Self::Trim(field) => write!(f, "TRIM({field})"),
64            Self::LowerTrim(field) => write!(f, "LOWER(TRIM({field}))"),
65            Self::Date(field) => write!(f, "DATE({field})"),
66            Self::Year(field) => write!(f, "YEAR({field})"),
67            Self::Month(field) => write!(f, "MONTH({field})"),
68            Self::Day(field) => write!(f, "DAY({field})"),
69        }
70    }
71}
72
73///
74/// IndexKeyItem
75///
76/// Canonical index key-item metadata.
77/// `Field` preserves legacy field-key behavior.
78/// `Expression` reserves deterministic expression-key identity metadata.
79///
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81pub enum IndexKeyItem {
82    Field(&'static str),
83    Expression(IndexExpression),
84}
85
86impl IndexKeyItem {
87    /// Borrow this key-item's referenced field.
88    #[must_use]
89    pub const fn field(&self) -> &'static str {
90        match self {
91            Self::Field(field) => field,
92            Self::Expression(expression) => expression.field(),
93        }
94    }
95
96    /// Render one deterministic canonical text form for diagnostics/display.
97    #[must_use]
98    pub fn canonical_text(&self) -> String {
99        match self {
100            Self::Field(field) => (*field).to_string(),
101            Self::Expression(expression) => expression.to_string(),
102        }
103    }
104}
105
106///
107/// IndexKeyItemsRef
108///
109/// Borrowed view over index key-item metadata.
110/// Field-only indexes use `Fields`; mixed/explicit key metadata uses `Items`.
111///
112#[derive(Clone, Copy, Debug, Eq, PartialEq)]
113pub enum IndexKeyItemsRef {
114    Fields(&'static [&'static str]),
115    Items(&'static [IndexKeyItem]),
116}
117
118///
119/// IndexModel
120///
121/// Runtime-only descriptor for an index used by the executor and stores.
122/// Keeps core decoupled from the schema `Index` shape.
123/// Indexing is hash-based over `Value` equality for all variants.
124/// Unique indexes enforce value equality; hash collisions surface as corruption.
125///
126
127#[derive(Clone, Copy, Debug, Eq, PartialEq)]
128pub struct IndexModel {
129    /// Stable index name used for diagnostics and planner identity.
130    name: &'static str,
131    store: &'static str,
132    fields: &'static [&'static str],
133    key_items: Option<&'static [IndexKeyItem]>,
134    unique: bool,
135    // Raw schema-declared predicate text is input metadata only.
136    // Runtime/planner semantics must flow through canonical_index_predicate(...).
137    predicate: Option<&'static str>,
138}
139
140impl IndexModel {
141    #[must_use]
142    pub const fn new(
143        name: &'static str,
144        store: &'static str,
145        fields: &'static [&'static str],
146        unique: bool,
147    ) -> Self {
148        Self::new_with_key_items_and_predicate(name, store, fields, None, unique, None)
149    }
150
151    /// Construct one index descriptor with an optional conditional predicate.
152    #[must_use]
153    pub const fn new_with_predicate(
154        name: &'static str,
155        store: &'static str,
156        fields: &'static [&'static str],
157        unique: bool,
158        predicate: Option<&'static str>,
159    ) -> Self {
160        Self::new_with_key_items_and_predicate(name, store, fields, None, unique, predicate)
161    }
162
163    /// Construct one index descriptor with explicit canonical key-item metadata.
164    #[must_use]
165    pub const fn new_with_key_items(
166        name: &'static str,
167        store: &'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(name, store, fields, Some(key_items), unique, None)
173    }
174
175    /// Construct one index descriptor with explicit key-item + predicate metadata.
176    #[must_use]
177    pub const fn new_with_key_items_and_predicate(
178        name: &'static str,
179        store: &'static str,
180        fields: &'static [&'static str],
181        key_items: Option<&'static [IndexKeyItem]>,
182        unique: bool,
183        predicate: Option<&'static str>,
184    ) -> Self {
185        Self {
186            name,
187            store,
188            fields,
189            key_items,
190            unique,
191            predicate,
192        }
193    }
194
195    /// Return the stable index name.
196    #[must_use]
197    pub const fn name(&self) -> &'static str {
198        self.name
199    }
200
201    /// Return the backing index store path.
202    #[must_use]
203    pub const fn store(&self) -> &'static str {
204        self.store
205    }
206
207    /// Return the canonical index field list.
208    #[must_use]
209    pub const fn fields(&self) -> &'static [&'static str] {
210        self.fields
211    }
212
213    /// Borrow canonical key-item metadata for this index.
214    #[must_use]
215    pub const fn key_items(&self) -> IndexKeyItemsRef {
216        if let Some(items) = self.key_items {
217            IndexKeyItemsRef::Items(items)
218        } else {
219            IndexKeyItemsRef::Fields(self.fields)
220        }
221    }
222
223    /// Return whether this index includes expression key items.
224    #[must_use]
225    pub const fn has_expression_key_items(&self) -> bool {
226        let Some(items) = self.key_items else {
227            return false;
228        };
229
230        let mut index = 0usize;
231        while index < items.len() {
232            if matches!(items[index], IndexKeyItem::Expression(_)) {
233                return true;
234            }
235            index = index.saturating_add(1);
236        }
237
238        false
239    }
240
241    /// Return whether the index enforces value uniqueness.
242    #[must_use]
243    pub const fn is_unique(&self) -> bool {
244        self.unique
245    }
246
247    /// Return optional schema-declared conditional index predicate text metadata.
248    ///
249    /// This string is input-only and must be lowered through the canonical
250    /// index-predicate boundary before semantic use.
251    #[must_use]
252    pub const fn predicate(&self) -> Option<&'static str> {
253        self.predicate
254    }
255
256    /// Whether this index's field prefix matches the start of another index.
257    #[must_use]
258    pub fn is_prefix_of(&self, other: &Self) -> bool {
259        self.fields().len() < other.fields().len() && other.fields().starts_with(self.fields())
260    }
261
262    fn joined_key_items(&self) -> String {
263        match self.key_items() {
264            IndexKeyItemsRef::Fields(fields) => fields.join(", "),
265            IndexKeyItemsRef::Items(items) => items
266                .iter()
267                .map(IndexKeyItem::canonical_text)
268                .collect::<Vec<_>>()
269                .join(", "),
270        }
271    }
272}
273
274impl Display for IndexModel {
275    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276        let fields = self.joined_key_items();
277        if self.is_unique() {
278            if let Some(predicate) = self.predicate() {
279                write!(
280                    f,
281                    "{}: UNIQUE {}({}) WHERE {}",
282                    self.name(),
283                    self.store(),
284                    fields,
285                    predicate
286                )
287            } else {
288                write!(f, "{}: UNIQUE {}({})", self.name(), self.store(), fields)
289            }
290        } else if let Some(predicate) = self.predicate() {
291            write!(
292                f,
293                "{}: {}({}) WHERE {}",
294                self.name(),
295                self.store(),
296                fields,
297                predicate
298            )
299        } else {
300            write!(f, "{}: {}({})", self.name(), self.store(), fields)
301        }
302    }
303}
304
305///
306/// TESTS
307///
308
309#[cfg(test)]
310mod tests {
311    use crate::model::index::{IndexExpression, IndexKeyItem, IndexKeyItemsRef, IndexModel};
312
313    #[test]
314    fn index_model_with_predicate_exposes_predicate_metadata() {
315        let model = IndexModel::new_with_predicate(
316            "users|email|active",
317            "users::index",
318            &["email"],
319            false,
320            Some("active = true"),
321        );
322
323        assert_eq!(model.predicate(), Some("active = true"));
324        assert_eq!(
325            model.to_string(),
326            "users|email|active: users::index(email) WHERE active = true"
327        );
328    }
329
330    #[test]
331    fn index_model_without_predicate_preserves_legacy_display_shape() {
332        let model = IndexModel::new("users|email", "users::index", &["email"], true);
333
334        assert_eq!(model.predicate(), None);
335        assert_eq!(model.to_string(), "users|email: UNIQUE users::index(email)");
336    }
337
338    #[test]
339    fn index_model_with_explicit_key_items_exposes_expression_items() {
340        static KEY_ITEMS: [IndexKeyItem; 2] = [
341            IndexKeyItem::Field("tenant_id"),
342            IndexKeyItem::Expression(IndexExpression::Lower("email")),
343        ];
344        let model = IndexModel::new_with_key_items(
345            "users|tenant|email_expr",
346            "users::index",
347            &["tenant_id"],
348            &KEY_ITEMS,
349            false,
350        );
351
352        assert!(model.has_expression_key_items());
353        assert_eq!(
354            model.to_string(),
355            "users|tenant|email_expr: users::index(tenant_id, LOWER(email))"
356        );
357        assert!(matches!(
358            model.key_items(),
359            IndexKeyItemsRef::Items(items)
360                if items == KEY_ITEMS.as_slice()
361        ));
362    }
363}