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 index name used for diagnostics and planner identity.
137    name: &'static str,
138    store: &'static str,
139    fields: &'static [&'static str],
140    key_items: Option<&'static [IndexKeyItem]>,
141    unique: bool,
142    // Raw schema-declared predicate text is input metadata only.
143    // Runtime/planner semantics must flow through canonical_index_predicate(...).
144    predicate: Option<&'static str>,
145}
146
147impl IndexModel {
148    #[must_use]
149    pub const fn new(
150        name: &'static str,
151        store: &'static str,
152        fields: &'static [&'static str],
153        unique: bool,
154    ) -> Self {
155        Self::new_with_key_items_and_predicate(name, store, fields, None, unique, None)
156    }
157
158    /// Construct one index descriptor with an optional conditional predicate.
159    #[must_use]
160    pub const fn new_with_predicate(
161        name: &'static str,
162        store: &'static str,
163        fields: &'static [&'static str],
164        unique: bool,
165        predicate: Option<&'static str>,
166    ) -> Self {
167        Self::new_with_key_items_and_predicate(name, store, fields, None, unique, predicate)
168    }
169
170    /// Construct one index descriptor with explicit canonical key-item metadata.
171    #[must_use]
172    pub const fn new_with_key_items(
173        name: &'static str,
174        store: &'static str,
175        fields: &'static [&'static str],
176        key_items: &'static [IndexKeyItem],
177        unique: bool,
178    ) -> Self {
179        Self::new_with_key_items_and_predicate(name, store, fields, Some(key_items), unique, None)
180    }
181
182    /// Construct one index descriptor with explicit key-item + predicate metadata.
183    #[must_use]
184    pub const fn new_with_key_items_and_predicate(
185        name: &'static str,
186        store: &'static str,
187        fields: &'static [&'static str],
188        key_items: Option<&'static [IndexKeyItem]>,
189        unique: bool,
190        predicate: Option<&'static str>,
191    ) -> Self {
192        Self {
193            name,
194            store,
195            fields,
196            key_items,
197            unique,
198            predicate,
199        }
200    }
201
202    /// Return the stable index name.
203    #[must_use]
204    pub const fn name(&self) -> &'static str {
205        self.name
206    }
207
208    /// Return the backing index store path.
209    #[must_use]
210    pub const fn store(&self) -> &'static str {
211        self.store
212    }
213
214    /// Return the canonical index field list.
215    #[must_use]
216    pub const fn fields(&self) -> &'static [&'static str] {
217        self.fields
218    }
219
220    /// Borrow canonical key-item metadata for this index.
221    #[must_use]
222    pub const fn key_items(&self) -> IndexKeyItemsRef {
223        if let Some(items) = self.key_items {
224            IndexKeyItemsRef::Items(items)
225        } else {
226            IndexKeyItemsRef::Fields(self.fields)
227        }
228    }
229
230    /// Return whether this index includes expression key items.
231    #[must_use]
232    pub const fn has_expression_key_items(&self) -> bool {
233        let Some(items) = self.key_items else {
234            return false;
235        };
236
237        let mut index = 0usize;
238        while index < items.len() {
239            if matches!(items[index], IndexKeyItem::Expression(_)) {
240                return true;
241            }
242            index = index.saturating_add(1);
243        }
244
245        false
246    }
247
248    /// Return whether the index enforces value uniqueness.
249    #[must_use]
250    pub const fn is_unique(&self) -> bool {
251        self.unique
252    }
253
254    /// Return optional schema-declared conditional index predicate text metadata.
255    ///
256    /// This string is input-only and must be lowered through the canonical
257    /// index-predicate boundary before semantic use.
258    #[must_use]
259    pub const fn predicate(&self) -> Option<&'static str> {
260        self.predicate
261    }
262
263    /// Whether this index's field prefix matches the start of another index.
264    #[must_use]
265    pub fn is_prefix_of(&self, other: &Self) -> bool {
266        self.fields().len() < other.fields().len() && other.fields().starts_with(self.fields())
267    }
268
269    fn joined_key_items(&self) -> String {
270        match self.key_items() {
271            IndexKeyItemsRef::Fields(fields) => fields.join(", "),
272            IndexKeyItemsRef::Items(items) => items
273                .iter()
274                .map(IndexKeyItem::canonical_text)
275                .collect::<Vec<_>>()
276                .join(", "),
277        }
278    }
279}
280
281impl Display for IndexModel {
282    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283        let fields = self.joined_key_items();
284        if self.is_unique() {
285            if let Some(predicate) = self.predicate() {
286                write!(
287                    f,
288                    "{}: UNIQUE {}({}) WHERE {}",
289                    self.name(),
290                    self.store(),
291                    fields,
292                    predicate
293                )
294            } else {
295                write!(f, "{}: UNIQUE {}({})", self.name(), self.store(), fields)
296            }
297        } else if let Some(predicate) = self.predicate() {
298            write!(
299                f,
300                "{}: {}({}) WHERE {}",
301                self.name(),
302                self.store(),
303                fields,
304                predicate
305            )
306        } else {
307            write!(f, "{}: {}({})", self.name(), self.store(), fields)
308        }
309    }
310}
311
312///
313/// TESTS
314///
315
316#[cfg(test)]
317mod tests {
318    use crate::model::index::{IndexExpression, IndexKeyItem, IndexKeyItemsRef, IndexModel};
319
320    #[test]
321    fn index_model_with_predicate_exposes_predicate_metadata() {
322        let model = IndexModel::new_with_predicate(
323            "users|email|active",
324            "users::index",
325            &["email"],
326            false,
327            Some("active = true"),
328        );
329
330        assert_eq!(model.predicate(), Some("active = true"));
331        assert_eq!(
332            model.to_string(),
333            "users|email|active: users::index(email) WHERE active = true"
334        );
335    }
336
337    #[test]
338    fn index_model_without_predicate_preserves_legacy_display_shape() {
339        let model = IndexModel::new("users|email", "users::index", &["email"], true);
340
341        assert_eq!(model.predicate(), None);
342        assert_eq!(model.to_string(), "users|email: UNIQUE users::index(email)");
343    }
344
345    #[test]
346    fn index_model_with_explicit_key_items_exposes_expression_items() {
347        static KEY_ITEMS: [IndexKeyItem; 2] = [
348            IndexKeyItem::Field("tenant_id"),
349            IndexKeyItem::Expression(IndexExpression::Lower("email")),
350        ];
351        let model = IndexModel::new_with_key_items(
352            "users|tenant|email_expr",
353            "users::index",
354            &["tenant_id"],
355            &KEY_ITEMS,
356            false,
357        );
358
359        assert!(model.has_expression_key_items());
360        assert_eq!(
361            model.to_string(),
362            "users|tenant|email_expr: users::index(tenant_id, LOWER(email))"
363        );
364        assert!(matches!(
365            model.key_items(),
366            IndexKeyItemsRef::Items(items)
367                if items == KEY_ITEMS.as_slice()
368        ));
369    }
370
371    #[test]
372    fn index_expression_lookup_support_matrix_is_explicit() {
373        assert!(IndexExpression::Lower("email").supports_text_casefold_lookup());
374        assert!(!IndexExpression::Upper("email").supports_text_casefold_lookup());
375        assert!(!IndexExpression::Trim("email").supports_text_casefold_lookup());
376        assert!(!IndexExpression::LowerTrim("email").supports_text_casefold_lookup());
377        assert!(!IndexExpression::Date("created_at").supports_text_casefold_lookup());
378        assert!(!IndexExpression::Year("created_at").supports_text_casefold_lookup());
379        assert!(!IndexExpression::Month("created_at").supports_text_casefold_lookup());
380        assert!(!IndexExpression::Day("created_at").supports_text_casefold_lookup());
381    }
382}