1use crate::db::Predicate;
7use std::fmt::{self, Display};
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum IndexExpression {
17 Lower(&'static str),
18 Upper(&'static str),
19 Trim(&'static str),
20 LowerTrim(&'static str),
21 Date(&'static str),
22 Year(&'static str),
23 Month(&'static str),
24 Day(&'static str),
25}
26
27impl IndexExpression {
28 #[must_use]
30 pub const fn field(&self) -> &'static str {
31 match self {
32 Self::Lower(field)
33 | Self::Upper(field)
34 | Self::Trim(field)
35 | Self::LowerTrim(field)
36 | Self::Date(field)
37 | Self::Year(field)
38 | Self::Month(field)
39 | Self::Day(field) => field,
40 }
41 }
42
43 #[must_use]
45 pub const fn kind_tag(&self) -> u8 {
46 match self {
47 Self::Lower(_) => 0x01,
48 Self::Upper(_) => 0x02,
49 Self::Trim(_) => 0x03,
50 Self::LowerTrim(_) => 0x04,
51 Self::Date(_) => 0x05,
52 Self::Year(_) => 0x06,
53 Self::Month(_) => 0x07,
54 Self::Day(_) => 0x08,
55 }
56 }
57
58 #[must_use]
61 pub const fn supports_text_casefold_lookup(&self) -> bool {
62 matches!(self, Self::Lower(_) | Self::Upper(_))
63 }
64}
65
66impl Display for IndexExpression {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 match self {
69 Self::Lower(field) => write!(f, "LOWER({field})"),
70 Self::Upper(field) => write!(f, "UPPER({field})"),
71 Self::Trim(field) => write!(f, "TRIM({field})"),
72 Self::LowerTrim(field) => write!(f, "LOWER(TRIM({field}))"),
73 Self::Date(field) => write!(f, "DATE({field})"),
74 Self::Year(field) => write!(f, "YEAR({field})"),
75 Self::Month(field) => write!(f, "MONTH({field})"),
76 Self::Day(field) => write!(f, "DAY({field})"),
77 }
78 }
79}
80
81#[derive(Clone, Copy, Debug, Eq, PartialEq)]
89pub enum IndexKeyItem {
90 Field(&'static str),
91 Expression(IndexExpression),
92}
93
94impl IndexKeyItem {
95 #[must_use]
97 pub const fn field(&self) -> &'static str {
98 match self {
99 Self::Field(field) => field,
100 Self::Expression(expression) => expression.field(),
101 }
102 }
103
104 #[must_use]
106 pub fn canonical_text(&self) -> String {
107 match self {
108 Self::Field(field) => (*field).to_string(),
109 Self::Expression(expression) => expression.to_string(),
110 }
111 }
112}
113
114#[derive(Clone, Copy, Debug, Eq, PartialEq)]
121pub enum IndexKeyItemsRef {
122 Fields(&'static [&'static str]),
123 Items(&'static [IndexKeyItem]),
124}
125
126pub type GeneratedIndexPredicateResolver = fn() -> &'static Predicate;
134
135#[derive(Clone, Copy, Debug)]
143pub struct IndexPredicateMetadata {
144 sql: &'static str,
145 semantics: GeneratedIndexPredicateResolver,
146}
147
148impl IndexPredicateMetadata {
149 #[must_use]
151 #[doc(hidden)]
152 pub const fn generated(sql: &'static str, semantics: GeneratedIndexPredicateResolver) -> Self {
153 Self { sql, semantics }
154 }
155
156 #[must_use]
158 pub const fn sql(&self) -> &'static str {
159 self.sql
160 }
161
162 #[must_use]
164 pub fn semantics(&self) -> &'static Predicate {
165 (self.semantics)()
166 }
167}
168
169impl PartialEq for IndexPredicateMetadata {
170 fn eq(&self, other: &Self) -> bool {
171 self.sql == other.sql && std::ptr::fn_addr_eq(self.semantics, other.semantics)
172 }
173}
174
175impl Eq for IndexPredicateMetadata {}
176
177#[derive(Clone, Copy, Debug, Eq, PartialEq)]
187pub struct IndexModel {
188 ordinal: u16,
190
191 name: &'static str,
193 store: &'static str,
194 fields: &'static [&'static str],
195 key_items: Option<&'static [IndexKeyItem]>,
196 unique: bool,
197 predicate: Option<IndexPredicateMetadata>,
200}
201
202impl IndexModel {
203 #[must_use]
209 #[doc(hidden)]
210 pub const fn generated(
211 name: &'static str,
212 store: &'static str,
213 fields: &'static [&'static str],
214 unique: bool,
215 ) -> Self {
216 Self::generated_with_ordinal_and_key_items_and_predicate(
217 0, name, store, fields, None, unique, None,
218 )
219 }
220
221 #[must_use]
223 #[doc(hidden)]
224 pub const fn generated_with_ordinal(
225 ordinal: u16,
226 name: &'static str,
227 store: &'static str,
228 fields: &'static [&'static str],
229 unique: bool,
230 ) -> Self {
231 Self::generated_with_ordinal_and_key_items_and_predicate(
232 ordinal, name, store, fields, None, unique, None,
233 )
234 }
235
236 #[must_use]
238 #[doc(hidden)]
239 pub const fn generated_with_predicate(
240 name: &'static str,
241 store: &'static str,
242 fields: &'static [&'static str],
243 unique: bool,
244 predicate: Option<IndexPredicateMetadata>,
245 ) -> Self {
246 Self::generated_with_ordinal_and_key_items_and_predicate(
247 0, name, store, fields, None, unique, predicate,
248 )
249 }
250
251 #[must_use]
253 #[doc(hidden)]
254 pub const fn generated_with_ordinal_and_predicate(
255 ordinal: u16,
256 name: &'static str,
257 store: &'static str,
258 fields: &'static [&'static str],
259 unique: bool,
260 predicate: Option<IndexPredicateMetadata>,
261 ) -> Self {
262 Self::generated_with_ordinal_and_key_items_and_predicate(
263 ordinal, name, store, fields, None, unique, predicate,
264 )
265 }
266
267 #[must_use]
269 #[doc(hidden)]
270 pub const fn generated_with_key_items(
271 name: &'static str,
272 store: &'static str,
273 fields: &'static [&'static str],
274 key_items: &'static [IndexKeyItem],
275 unique: bool,
276 ) -> Self {
277 Self::generated_with_ordinal_and_key_items_and_predicate(
278 0,
279 name,
280 store,
281 fields,
282 Some(key_items),
283 unique,
284 None,
285 )
286 }
287
288 #[must_use]
290 #[doc(hidden)]
291 pub const fn generated_with_ordinal_and_key_items(
292 ordinal: u16,
293 name: &'static str,
294 store: &'static str,
295 fields: &'static [&'static str],
296 key_items: &'static [IndexKeyItem],
297 unique: bool,
298 ) -> Self {
299 Self::generated_with_ordinal_and_key_items_and_predicate(
300 ordinal,
301 name,
302 store,
303 fields,
304 Some(key_items),
305 unique,
306 None,
307 )
308 }
309
310 #[must_use]
312 #[doc(hidden)]
313 pub const fn generated_with_key_items_and_predicate(
314 name: &'static str,
315 store: &'static str,
316 fields: &'static [&'static str],
317 key_items: Option<&'static [IndexKeyItem]>,
318 unique: bool,
319 predicate: Option<IndexPredicateMetadata>,
320 ) -> Self {
321 Self::generated_with_ordinal_and_key_items_and_predicate(
322 0, name, store, fields, key_items, unique, predicate,
323 )
324 }
325
326 #[must_use]
328 #[doc(hidden)]
329 pub const fn generated_with_ordinal_and_key_items_and_predicate(
330 ordinal: u16,
331 name: &'static str,
332 store: &'static str,
333 fields: &'static [&'static str],
334 key_items: Option<&'static [IndexKeyItem]>,
335 unique: bool,
336 predicate: Option<IndexPredicateMetadata>,
337 ) -> Self {
338 Self {
339 ordinal,
340 name,
341 store,
342 fields,
343 key_items,
344 unique,
345 predicate,
346 }
347 }
348
349 #[must_use]
351 pub const fn name(&self) -> &'static str {
352 self.name
353 }
354
355 #[must_use]
357 pub const fn ordinal(&self) -> u16 {
358 self.ordinal
359 }
360
361 #[must_use]
363 pub const fn store(&self) -> &'static str {
364 self.store
365 }
366
367 #[must_use]
369 pub const fn fields(&self) -> &'static [&'static str] {
370 self.fields
371 }
372
373 #[must_use]
375 pub const fn key_items(&self) -> IndexKeyItemsRef {
376 if let Some(items) = self.key_items {
377 IndexKeyItemsRef::Items(items)
378 } else {
379 IndexKeyItemsRef::Fields(self.fields)
380 }
381 }
382
383 #[must_use]
385 pub const fn has_expression_key_items(&self) -> bool {
386 let Some(items) = self.key_items else {
387 return false;
388 };
389
390 let mut index = 0usize;
391 while index < items.len() {
392 if matches!(items[index], IndexKeyItem::Expression(_)) {
393 return true;
394 }
395 index = index.saturating_add(1);
396 }
397
398 false
399 }
400
401 #[must_use]
403 pub const fn is_unique(&self) -> bool {
404 self.unique
405 }
406
407 #[must_use]
411 pub const fn predicate(&self) -> Option<&'static str> {
412 match self.predicate {
413 Some(predicate) => Some(predicate.sql()),
414 None => None,
415 }
416 }
417
418 #[must_use]
420 pub fn predicate_semantics(&self) -> Option<&'static Predicate> {
421 self.predicate.map(|predicate| predicate.semantics())
422 }
423
424 #[must_use]
426 pub fn is_prefix_of(&self, other: &Self) -> bool {
427 self.fields().len() < other.fields().len() && other.fields().starts_with(self.fields())
428 }
429
430 fn joined_key_items(&self) -> String {
431 match self.key_items() {
432 IndexKeyItemsRef::Fields(fields) => fields.join(", "),
433 IndexKeyItemsRef::Items(items) => {
434 let mut joined = String::new();
435
436 for item in items {
437 if !joined.is_empty() {
438 joined.push_str(", ");
439 }
440 joined.push_str(item.canonical_text().as_str());
441 }
442
443 joined
444 }
445 }
446 }
447}
448
449impl Display for IndexModel {
450 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
451 let fields = self.joined_key_items();
452 if self.is_unique() {
453 if let Some(predicate) = self.predicate() {
454 write!(
455 f,
456 "{}: UNIQUE {}({}) WHERE {}",
457 self.name(),
458 self.store(),
459 fields,
460 predicate
461 )
462 } else {
463 write!(f, "{}: UNIQUE {}({})", self.name(), self.store(), fields)
464 }
465 } else if let Some(predicate) = self.predicate() {
466 write!(
467 f,
468 "{}: {}({}) WHERE {}",
469 self.name(),
470 self.store(),
471 fields,
472 predicate
473 )
474 } else {
475 write!(f, "{}: {}({})", self.name(), self.store(), fields)
476 }
477 }
478}
479
480#[cfg(test)]
485mod tests {
486 use crate::{
487 db::Predicate,
488 model::index::{
489 IndexExpression, IndexKeyItem, IndexKeyItemsRef, IndexModel, IndexPredicateMetadata,
490 },
491 };
492 use std::sync::LazyLock;
493
494 static ACTIVE_TRUE_PREDICATE: LazyLock<Predicate> =
495 LazyLock::new(|| Predicate::eq("active".to_string(), true.into()));
496
497 fn active_true_predicate() -> &'static Predicate {
498 &ACTIVE_TRUE_PREDICATE
499 }
500
501 #[test]
502 fn index_model_with_predicate_exposes_predicate_metadata() {
503 let model = IndexModel::generated_with_predicate(
504 "users|email|active",
505 "users::index",
506 &["email"],
507 false,
508 Some(IndexPredicateMetadata::generated(
509 "active = true",
510 active_true_predicate,
511 )),
512 );
513
514 assert_eq!(model.predicate(), Some("active = true"));
515 assert_eq!(model.predicate_semantics(), Some(active_true_predicate()),);
516 assert_eq!(
517 model.to_string(),
518 "users|email|active: users::index(email) WHERE active = true"
519 );
520 }
521
522 #[test]
523 fn index_model_without_predicate_preserves_display_shape() {
524 let model = IndexModel::generated("users|email", "users::index", &["email"], true);
525
526 assert_eq!(model.predicate(), None);
527 assert_eq!(model.to_string(), "users|email: UNIQUE users::index(email)");
528 }
529
530 #[test]
531 fn index_model_with_explicit_key_items_exposes_expression_items() {
532 static KEY_ITEMS: [IndexKeyItem; 2] = [
533 IndexKeyItem::Field("tenant_id"),
534 IndexKeyItem::Expression(IndexExpression::Lower("email")),
535 ];
536 let model = IndexModel::generated_with_key_items(
537 "users|tenant|email_expr",
538 "users::index",
539 &["tenant_id"],
540 &KEY_ITEMS,
541 false,
542 );
543
544 assert!(model.has_expression_key_items());
545 assert_eq!(
546 model.to_string(),
547 "users|tenant|email_expr: users::index(tenant_id, LOWER(email))"
548 );
549 assert!(matches!(
550 model.key_items(),
551 IndexKeyItemsRef::Items(items)
552 if items == KEY_ITEMS.as_slice()
553 ));
554 }
555
556 #[test]
557 fn index_expression_lookup_support_matrix_is_explicit() {
558 assert!(IndexExpression::Lower("email").supports_text_casefold_lookup());
559 assert!(IndexExpression::Upper("email").supports_text_casefold_lookup());
560 assert!(!IndexExpression::Trim("email").supports_text_casefold_lookup());
561 assert!(!IndexExpression::LowerTrim("email").supports_text_casefold_lookup());
562 assert!(!IndexExpression::Date("created_at").supports_text_casefold_lookup());
563 assert!(!IndexExpression::Year("created_at").supports_text_casefold_lookup());
564 assert!(!IndexExpression::Month("created_at").supports_text_casefold_lookup());
565 assert!(!IndexExpression::Day("created_at").supports_text_casefold_lookup());
566 }
567}