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/// IndexKeyItem
10///
11/// Canonical index key-item metadata.
12/// `Field` preserves legacy field-key behavior.
13/// `Expression` reserves deterministic expression-key identity metadata.
14///
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum IndexKeyItem {
17    Field(&'static str),
18    Expression(&'static str),
19}
20
21impl IndexKeyItem {
22    /// Borrow this key-item's canonical text payload.
23    #[must_use]
24    pub const fn text(&self) -> &'static str {
25        match self {
26            Self::Field(field) | Self::Expression(field) => field,
27        }
28    }
29}
30
31///
32/// IndexKeyItemsRef
33///
34/// Borrowed view over index key-item metadata.
35/// Field-only indexes use `Fields`; mixed/explicit key metadata uses `Items`.
36///
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum IndexKeyItemsRef {
39    Fields(&'static [&'static str]),
40    Items(&'static [IndexKeyItem]),
41}
42
43///
44/// IndexModel
45///
46/// Runtime-only descriptor for an index used by the executor and stores.
47/// Keeps core decoupled from the schema `Index` shape.
48/// Indexing is hash-based over `Value` equality for all variants.
49/// Unique indexes enforce value equality; hash collisions surface as corruption.
50///
51
52#[derive(Clone, Copy, Debug, Eq, PartialEq)]
53pub struct IndexModel {
54    /// Stable index name used for diagnostics and planner identity.
55    name: &'static str,
56    store: &'static str,
57    fields: &'static [&'static str],
58    key_items: Option<&'static [IndexKeyItem]>,
59    unique: bool,
60    // Raw schema-declared predicate text is input metadata only.
61    // Runtime/planner semantics must flow through canonical_index_predicate(...).
62    predicate: Option<&'static str>,
63}
64
65impl IndexModel {
66    #[must_use]
67    pub const fn new(
68        name: &'static str,
69        store: &'static str,
70        fields: &'static [&'static str],
71        unique: bool,
72    ) -> Self {
73        Self::new_with_key_items_and_predicate(name, store, fields, None, unique, None)
74    }
75
76    /// Construct one index descriptor with an optional conditional predicate.
77    #[must_use]
78    pub const fn new_with_predicate(
79        name: &'static str,
80        store: &'static str,
81        fields: &'static [&'static str],
82        unique: bool,
83        predicate: Option<&'static str>,
84    ) -> Self {
85        Self::new_with_key_items_and_predicate(name, store, fields, None, unique, predicate)
86    }
87
88    /// Construct one index descriptor with explicit canonical key-item metadata.
89    #[must_use]
90    pub const fn new_with_key_items(
91        name: &'static str,
92        store: &'static str,
93        fields: &'static [&'static str],
94        key_items: &'static [IndexKeyItem],
95        unique: bool,
96    ) -> Self {
97        Self::new_with_key_items_and_predicate(name, store, fields, Some(key_items), unique, None)
98    }
99
100    /// Construct one index descriptor with explicit key-item + predicate metadata.
101    #[must_use]
102    pub const fn new_with_key_items_and_predicate(
103        name: &'static str,
104        store: &'static str,
105        fields: &'static [&'static str],
106        key_items: Option<&'static [IndexKeyItem]>,
107        unique: bool,
108        predicate: Option<&'static str>,
109    ) -> Self {
110        Self {
111            name,
112            store,
113            fields,
114            key_items,
115            unique,
116            predicate,
117        }
118    }
119
120    /// Return the stable index name.
121    #[must_use]
122    pub const fn name(&self) -> &'static str {
123        self.name
124    }
125
126    /// Return the backing index store path.
127    #[must_use]
128    pub const fn store(&self) -> &'static str {
129        self.store
130    }
131
132    /// Return the canonical index field list.
133    #[must_use]
134    pub const fn fields(&self) -> &'static [&'static str] {
135        self.fields
136    }
137
138    /// Borrow canonical key-item metadata for this index.
139    #[must_use]
140    pub const fn key_items(&self) -> IndexKeyItemsRef {
141        if let Some(items) = self.key_items {
142            IndexKeyItemsRef::Items(items)
143        } else {
144            IndexKeyItemsRef::Fields(self.fields)
145        }
146    }
147
148    /// Return whether this index includes expression key items.
149    #[must_use]
150    pub const fn has_expression_key_items(&self) -> bool {
151        let Some(items) = self.key_items else {
152            return false;
153        };
154
155        let mut index = 0usize;
156        while index < items.len() {
157            if matches!(items[index], IndexKeyItem::Expression(_)) {
158                return true;
159            }
160            index = index.saturating_add(1);
161        }
162
163        false
164    }
165
166    /// Return whether the index enforces value uniqueness.
167    #[must_use]
168    pub const fn is_unique(&self) -> bool {
169        self.unique
170    }
171
172    /// Return optional schema-declared conditional index predicate text metadata.
173    ///
174    /// This string is input-only and must be lowered through the canonical
175    /// index-predicate boundary before semantic use.
176    #[must_use]
177    pub const fn predicate(&self) -> Option<&'static str> {
178        self.predicate
179    }
180
181    /// Whether this index's field prefix matches the start of another index.
182    #[must_use]
183    pub fn is_prefix_of(&self, other: &Self) -> bool {
184        self.fields().len() < other.fields().len() && other.fields().starts_with(self.fields())
185    }
186
187    fn joined_key_items(&self) -> String {
188        match self.key_items() {
189            IndexKeyItemsRef::Fields(fields) => fields.join(", "),
190            IndexKeyItemsRef::Items(items) => items
191                .iter()
192                .map(IndexKeyItem::text)
193                .collect::<Vec<_>>()
194                .join(", "),
195        }
196    }
197}
198
199impl Display for IndexModel {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        let fields = self.joined_key_items();
202        if self.is_unique() {
203            if let Some(predicate) = self.predicate() {
204                write!(
205                    f,
206                    "{}: UNIQUE {}({}) WHERE {}",
207                    self.name(),
208                    self.store(),
209                    fields,
210                    predicate
211                )
212            } else {
213                write!(f, "{}: UNIQUE {}({})", self.name(), self.store(), fields)
214            }
215        } else if let Some(predicate) = self.predicate() {
216            write!(
217                f,
218                "{}: {}({}) WHERE {}",
219                self.name(),
220                self.store(),
221                fields,
222                predicate
223            )
224        } else {
225            write!(f, "{}: {}({})", self.name(), self.store(), fields)
226        }
227    }
228}
229
230///
231/// TESTS
232///
233
234#[cfg(test)]
235mod tests {
236    use crate::model::index::{IndexKeyItem, IndexKeyItemsRef, IndexModel};
237
238    #[test]
239    fn index_model_with_predicate_exposes_predicate_metadata() {
240        let model = IndexModel::new_with_predicate(
241            "users|email|active",
242            "users::index",
243            &["email"],
244            false,
245            Some("active = true"),
246        );
247
248        assert_eq!(model.predicate(), Some("active = true"));
249        assert_eq!(
250            model.to_string(),
251            "users|email|active: users::index(email) WHERE active = true"
252        );
253    }
254
255    #[test]
256    fn index_model_without_predicate_preserves_legacy_display_shape() {
257        let model = IndexModel::new("users|email", "users::index", &["email"], true);
258
259        assert_eq!(model.predicate(), None);
260        assert_eq!(model.to_string(), "users|email: UNIQUE users::index(email)");
261    }
262
263    #[test]
264    fn index_model_with_explicit_key_items_exposes_expression_items() {
265        static KEY_ITEMS: [IndexKeyItem; 2] = [
266            IndexKeyItem::Field("tenant_id"),
267            IndexKeyItem::Expression("LOWER(email)"),
268        ];
269        let model = IndexModel::new_with_key_items(
270            "users|tenant|email_expr",
271            "users::index",
272            &["tenant_id"],
273            &KEY_ITEMS,
274            false,
275        );
276
277        assert!(model.has_expression_key_items());
278        assert_eq!(
279            model.to_string(),
280            "users|tenant|email_expr: users::index(tenant_id, LOWER(email))"
281        );
282        assert!(matches!(
283            model.key_items(),
284            IndexKeyItemsRef::Items(items)
285                if items == KEY_ITEMS.as_slice()
286        ));
287    }
288}