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(_) | Self::Upper(_))
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) => {
361 let mut joined = String::new();
362
363 for item in items {
364 if !joined.is_empty() {
365 joined.push_str(", ");
366 }
367 joined.push_str(item.canonical_text().as_str());
368 }
369
370 joined
371 }
372 }
373 }
374}
375
376impl Display for IndexModel {
377 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
378 let fields = self.joined_key_items();
379 if self.is_unique() {
380 if let Some(predicate) = self.predicate() {
381 write!(
382 f,
383 "{}: UNIQUE {}({}) WHERE {}",
384 self.name(),
385 self.store(),
386 fields,
387 predicate
388 )
389 } else {
390 write!(f, "{}: UNIQUE {}({})", self.name(), self.store(), fields)
391 }
392 } else if let Some(predicate) = self.predicate() {
393 write!(
394 f,
395 "{}: {}({}) WHERE {}",
396 self.name(),
397 self.store(),
398 fields,
399 predicate
400 )
401 } else {
402 write!(f, "{}: {}({})", self.name(), self.store(), fields)
403 }
404 }
405}
406
407#[cfg(test)]
412mod tests {
413 use crate::model::index::{IndexExpression, IndexKeyItem, IndexKeyItemsRef, IndexModel};
414
415 #[test]
416 fn index_model_with_predicate_exposes_predicate_metadata() {
417 let model = IndexModel::new_with_predicate(
418 "users|email|active",
419 "users::index",
420 &["email"],
421 false,
422 Some("active = true"),
423 );
424
425 assert_eq!(model.predicate(), Some("active = true"));
426 assert_eq!(
427 model.to_string(),
428 "users|email|active: users::index(email) WHERE active = true"
429 );
430 }
431
432 #[test]
433 fn index_model_without_predicate_preserves_display_shape() {
434 let model = IndexModel::new("users|email", "users::index", &["email"], true);
435
436 assert_eq!(model.predicate(), None);
437 assert_eq!(model.to_string(), "users|email: UNIQUE users::index(email)");
438 }
439
440 #[test]
441 fn index_model_with_explicit_key_items_exposes_expression_items() {
442 static KEY_ITEMS: [IndexKeyItem; 2] = [
443 IndexKeyItem::Field("tenant_id"),
444 IndexKeyItem::Expression(IndexExpression::Lower("email")),
445 ];
446 let model = IndexModel::new_with_key_items(
447 "users|tenant|email_expr",
448 "users::index",
449 &["tenant_id"],
450 &KEY_ITEMS,
451 false,
452 );
453
454 assert!(model.has_expression_key_items());
455 assert_eq!(
456 model.to_string(),
457 "users|tenant|email_expr: users::index(tenant_id, LOWER(email))"
458 );
459 assert!(matches!(
460 model.key_items(),
461 IndexKeyItemsRef::Items(items)
462 if items == KEY_ITEMS.as_slice()
463 ));
464 }
465
466 #[test]
467 fn index_expression_lookup_support_matrix_is_explicit() {
468 assert!(IndexExpression::Lower("email").supports_text_casefold_lookup());
469 assert!(IndexExpression::Upper("email").supports_text_casefold_lookup());
470 assert!(!IndexExpression::Trim("email").supports_text_casefold_lookup());
471 assert!(!IndexExpression::LowerTrim("email").supports_text_casefold_lookup());
472 assert!(!IndexExpression::Date("created_at").supports_text_casefold_lookup());
473 assert!(!IndexExpression::Year("created_at").supports_text_casefold_lookup());
474 assert!(!IndexExpression::Month("created_at").supports_text_casefold_lookup());
475 assert!(!IndexExpression::Day("created_at").supports_text_casefold_lookup());
476 }
477}