1use std::fmt::{self, Display};
7
8#[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 #[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 #[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 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
88pub enum IndexKeyItem {
89 Field(&'static str),
90 Expression(IndexExpression),
91}
92
93impl IndexKeyItem {
94 #[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 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
120pub enum IndexKeyItemsRef {
121 Fields(&'static [&'static str]),
122 Items(&'static [IndexKeyItem]),
123}
124
125#[derive(Clone, Copy, Debug, Eq, PartialEq)]
135pub struct IndexModel {
136 ordinal: u16,
138
139 name: &'static str,
141 store: &'static str,
142 fields: &'static [&'static str],
143 key_items: Option<&'static [IndexKeyItem]>,
144 unique: bool,
145 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
286 pub const fn name(&self) -> &'static str {
287 self.name
288 }
289
290 #[must_use]
292 pub const fn ordinal(&self) -> u16 {
293 self.ordinal
294 }
295
296 #[must_use]
298 pub const fn store(&self) -> &'static str {
299 self.store
300 }
301
302 #[must_use]
304 pub const fn fields(&self) -> &'static [&'static str] {
305 self.fields
306 }
307
308 #[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 #[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 #[must_use]
338 pub const fn is_unique(&self) -> bool {
339 self.unique
340 }
341
342 #[must_use]
347 pub const fn predicate(&self) -> Option<&'static str> {
348 self.predicate
349 }
350
351 #[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#[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}