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
58impl Display for IndexExpression {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 match self {
61 Self::Lower(field) => write!(f, "LOWER({field})"),
62 Self::Upper(field) => write!(f, "UPPER({field})"),
63 Self::Trim(field) => write!(f, "TRIM({field})"),
64 Self::LowerTrim(field) => write!(f, "LOWER(TRIM({field}))"),
65 Self::Date(field) => write!(f, "DATE({field})"),
66 Self::Year(field) => write!(f, "YEAR({field})"),
67 Self::Month(field) => write!(f, "MONTH({field})"),
68 Self::Day(field) => write!(f, "DAY({field})"),
69 }
70 }
71}
72
73#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81pub enum IndexKeyItem {
82 Field(&'static str),
83 Expression(IndexExpression),
84}
85
86impl IndexKeyItem {
87 #[must_use]
89 pub const fn field(&self) -> &'static str {
90 match self {
91 Self::Field(field) => field,
92 Self::Expression(expression) => expression.field(),
93 }
94 }
95
96 #[must_use]
98 pub fn canonical_text(&self) -> String {
99 match self {
100 Self::Field(field) => (*field).to_string(),
101 Self::Expression(expression) => expression.to_string(),
102 }
103 }
104}
105
106#[derive(Clone, Copy, Debug, Eq, PartialEq)]
113pub enum IndexKeyItemsRef {
114 Fields(&'static [&'static str]),
115 Items(&'static [IndexKeyItem]),
116}
117
118#[derive(Clone, Copy, Debug, Eq, PartialEq)]
128pub struct IndexModel {
129 name: &'static str,
131 store: &'static str,
132 fields: &'static [&'static str],
133 key_items: Option<&'static [IndexKeyItem]>,
134 unique: bool,
135 predicate: Option<&'static str>,
138}
139
140impl IndexModel {
141 #[must_use]
142 pub const fn new(
143 name: &'static str,
144 store: &'static str,
145 fields: &'static [&'static str],
146 unique: bool,
147 ) -> Self {
148 Self::new_with_key_items_and_predicate(name, store, fields, None, unique, None)
149 }
150
151 #[must_use]
153 pub const fn new_with_predicate(
154 name: &'static str,
155 store: &'static str,
156 fields: &'static [&'static str],
157 unique: bool,
158 predicate: Option<&'static str>,
159 ) -> Self {
160 Self::new_with_key_items_and_predicate(name, store, fields, None, unique, predicate)
161 }
162
163 #[must_use]
165 pub const fn new_with_key_items(
166 name: &'static str,
167 store: &'static str,
168 fields: &'static [&'static str],
169 key_items: &'static [IndexKeyItem],
170 unique: bool,
171 ) -> Self {
172 Self::new_with_key_items_and_predicate(name, store, fields, Some(key_items), unique, None)
173 }
174
175 #[must_use]
177 pub const fn new_with_key_items_and_predicate(
178 name: &'static str,
179 store: &'static str,
180 fields: &'static [&'static str],
181 key_items: Option<&'static [IndexKeyItem]>,
182 unique: bool,
183 predicate: Option<&'static str>,
184 ) -> Self {
185 Self {
186 name,
187 store,
188 fields,
189 key_items,
190 unique,
191 predicate,
192 }
193 }
194
195 #[must_use]
197 pub const fn name(&self) -> &'static str {
198 self.name
199 }
200
201 #[must_use]
203 pub const fn store(&self) -> &'static str {
204 self.store
205 }
206
207 #[must_use]
209 pub const fn fields(&self) -> &'static [&'static str] {
210 self.fields
211 }
212
213 #[must_use]
215 pub const fn key_items(&self) -> IndexKeyItemsRef {
216 if let Some(items) = self.key_items {
217 IndexKeyItemsRef::Items(items)
218 } else {
219 IndexKeyItemsRef::Fields(self.fields)
220 }
221 }
222
223 #[must_use]
225 pub const fn has_expression_key_items(&self) -> bool {
226 let Some(items) = self.key_items else {
227 return false;
228 };
229
230 let mut index = 0usize;
231 while index < items.len() {
232 if matches!(items[index], IndexKeyItem::Expression(_)) {
233 return true;
234 }
235 index = index.saturating_add(1);
236 }
237
238 false
239 }
240
241 #[must_use]
243 pub const fn is_unique(&self) -> bool {
244 self.unique
245 }
246
247 #[must_use]
252 pub const fn predicate(&self) -> Option<&'static str> {
253 self.predicate
254 }
255
256 #[must_use]
258 pub fn is_prefix_of(&self, other: &Self) -> bool {
259 self.fields().len() < other.fields().len() && other.fields().starts_with(self.fields())
260 }
261
262 fn joined_key_items(&self) -> String {
263 match self.key_items() {
264 IndexKeyItemsRef::Fields(fields) => fields.join(", "),
265 IndexKeyItemsRef::Items(items) => items
266 .iter()
267 .map(IndexKeyItem::canonical_text)
268 .collect::<Vec<_>>()
269 .join(", "),
270 }
271 }
272}
273
274impl Display for IndexModel {
275 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276 let fields = self.joined_key_items();
277 if self.is_unique() {
278 if let Some(predicate) = self.predicate() {
279 write!(
280 f,
281 "{}: UNIQUE {}({}) WHERE {}",
282 self.name(),
283 self.store(),
284 fields,
285 predicate
286 )
287 } else {
288 write!(f, "{}: UNIQUE {}({})", self.name(), self.store(), fields)
289 }
290 } else if let Some(predicate) = self.predicate() {
291 write!(
292 f,
293 "{}: {}({}) WHERE {}",
294 self.name(),
295 self.store(),
296 fields,
297 predicate
298 )
299 } else {
300 write!(f, "{}: {}({})", self.name(), self.store(), fields)
301 }
302 }
303}
304
305#[cfg(test)]
310mod tests {
311 use crate::model::index::{IndexExpression, IndexKeyItem, IndexKeyItemsRef, IndexModel};
312
313 #[test]
314 fn index_model_with_predicate_exposes_predicate_metadata() {
315 let model = IndexModel::new_with_predicate(
316 "users|email|active",
317 "users::index",
318 &["email"],
319 false,
320 Some("active = true"),
321 );
322
323 assert_eq!(model.predicate(), Some("active = true"));
324 assert_eq!(
325 model.to_string(),
326 "users|email|active: users::index(email) WHERE active = true"
327 );
328 }
329
330 #[test]
331 fn index_model_without_predicate_preserves_legacy_display_shape() {
332 let model = IndexModel::new("users|email", "users::index", &["email"], true);
333
334 assert_eq!(model.predicate(), None);
335 assert_eq!(model.to_string(), "users|email: UNIQUE users::index(email)");
336 }
337
338 #[test]
339 fn index_model_with_explicit_key_items_exposes_expression_items() {
340 static KEY_ITEMS: [IndexKeyItem; 2] = [
341 IndexKeyItem::Field("tenant_id"),
342 IndexKeyItem::Expression(IndexExpression::Lower("email")),
343 ];
344 let model = IndexModel::new_with_key_items(
345 "users|tenant|email_expr",
346 "users::index",
347 &["tenant_id"],
348 &KEY_ITEMS,
349 false,
350 );
351
352 assert!(model.has_expression_key_items());
353 assert_eq!(
354 model.to_string(),
355 "users|tenant|email_expr: users::index(tenant_id, LOWER(email))"
356 );
357 assert!(matches!(
358 model.key_items(),
359 IndexKeyItemsRef::Items(items)
360 if items == KEY_ITEMS.as_slice()
361 ));
362 }
363}