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 name: &'static str,
138 store: &'static str,
139 fields: &'static [&'static str],
140 key_items: Option<&'static [IndexKeyItem]>,
141 unique: bool,
142 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 #[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 #[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 #[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 #[must_use]
204 pub const fn name(&self) -> &'static str {
205 self.name
206 }
207
208 #[must_use]
210 pub const fn store(&self) -> &'static str {
211 self.store
212 }
213
214 #[must_use]
216 pub const fn fields(&self) -> &'static [&'static str] {
217 self.fields
218 }
219
220 #[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 #[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 #[must_use]
250 pub const fn is_unique(&self) -> bool {
251 self.unique
252 }
253
254 #[must_use]
259 pub const fn predicate(&self) -> Option<&'static str> {
260 self.predicate
261 }
262
263 #[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#[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}