1use std::fmt::{self, Display};
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum IndexKeyItem {
17 Field(&'static str),
18 Expression(&'static str),
19}
20
21impl IndexKeyItem {
22 #[must_use]
24 pub const fn text(&self) -> &'static str {
25 match self {
26 Self::Field(field) | Self::Expression(field) => field,
27 }
28 }
29}
30
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum IndexKeyItemsRef {
39 Fields(&'static [&'static str]),
40 Items(&'static [IndexKeyItem]),
41}
42
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
53pub struct IndexModel {
54 name: &'static str,
56 store: &'static str,
57 fields: &'static [&'static str],
58 key_items: Option<&'static [IndexKeyItem]>,
59 unique: bool,
60 predicate: Option<&'static str>,
63}
64
65impl IndexModel {
66 #[must_use]
67 pub const fn new(
68 name: &'static str,
69 store: &'static str,
70 fields: &'static [&'static str],
71 unique: bool,
72 ) -> Self {
73 Self::new_with_key_items_and_predicate(name, store, fields, None, unique, None)
74 }
75
76 #[must_use]
78 pub const fn new_with_predicate(
79 name: &'static str,
80 store: &'static str,
81 fields: &'static [&'static str],
82 unique: bool,
83 predicate: Option<&'static str>,
84 ) -> Self {
85 Self::new_with_key_items_and_predicate(name, store, fields, None, unique, predicate)
86 }
87
88 #[must_use]
90 pub const fn new_with_key_items(
91 name: &'static str,
92 store: &'static str,
93 fields: &'static [&'static str],
94 key_items: &'static [IndexKeyItem],
95 unique: bool,
96 ) -> Self {
97 Self::new_with_key_items_and_predicate(name, store, fields, Some(key_items), unique, None)
98 }
99
100 #[must_use]
102 pub const fn new_with_key_items_and_predicate(
103 name: &'static str,
104 store: &'static str,
105 fields: &'static [&'static str],
106 key_items: Option<&'static [IndexKeyItem]>,
107 unique: bool,
108 predicate: Option<&'static str>,
109 ) -> Self {
110 Self {
111 name,
112 store,
113 fields,
114 key_items,
115 unique,
116 predicate,
117 }
118 }
119
120 #[must_use]
122 pub const fn name(&self) -> &'static str {
123 self.name
124 }
125
126 #[must_use]
128 pub const fn store(&self) -> &'static str {
129 self.store
130 }
131
132 #[must_use]
134 pub const fn fields(&self) -> &'static [&'static str] {
135 self.fields
136 }
137
138 #[must_use]
140 pub const fn key_items(&self) -> IndexKeyItemsRef {
141 if let Some(items) = self.key_items {
142 IndexKeyItemsRef::Items(items)
143 } else {
144 IndexKeyItemsRef::Fields(self.fields)
145 }
146 }
147
148 #[must_use]
150 pub const fn has_expression_key_items(&self) -> bool {
151 let Some(items) = self.key_items else {
152 return false;
153 };
154
155 let mut index = 0usize;
156 while index < items.len() {
157 if matches!(items[index], IndexKeyItem::Expression(_)) {
158 return true;
159 }
160 index = index.saturating_add(1);
161 }
162
163 false
164 }
165
166 #[must_use]
168 pub const fn is_unique(&self) -> bool {
169 self.unique
170 }
171
172 #[must_use]
177 pub const fn predicate(&self) -> Option<&'static str> {
178 self.predicate
179 }
180
181 #[must_use]
183 pub fn is_prefix_of(&self, other: &Self) -> bool {
184 self.fields().len() < other.fields().len() && other.fields().starts_with(self.fields())
185 }
186
187 fn joined_key_items(&self) -> String {
188 match self.key_items() {
189 IndexKeyItemsRef::Fields(fields) => fields.join(", "),
190 IndexKeyItemsRef::Items(items) => items
191 .iter()
192 .map(IndexKeyItem::text)
193 .collect::<Vec<_>>()
194 .join(", "),
195 }
196 }
197}
198
199impl Display for IndexModel {
200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201 let fields = self.joined_key_items();
202 if self.is_unique() {
203 if let Some(predicate) = self.predicate() {
204 write!(
205 f,
206 "{}: UNIQUE {}({}) WHERE {}",
207 self.name(),
208 self.store(),
209 fields,
210 predicate
211 )
212 } else {
213 write!(f, "{}: UNIQUE {}({})", self.name(), self.store(), fields)
214 }
215 } else if let Some(predicate) = self.predicate() {
216 write!(
217 f,
218 "{}: {}({}) WHERE {}",
219 self.name(),
220 self.store(),
221 fields,
222 predicate
223 )
224 } else {
225 write!(f, "{}: {}({})", self.name(), self.store(), fields)
226 }
227 }
228}
229
230#[cfg(test)]
235mod tests {
236 use crate::model::index::{IndexKeyItem, IndexKeyItemsRef, IndexModel};
237
238 #[test]
239 fn index_model_with_predicate_exposes_predicate_metadata() {
240 let model = IndexModel::new_with_predicate(
241 "users|email|active",
242 "users::index",
243 &["email"],
244 false,
245 Some("active = true"),
246 );
247
248 assert_eq!(model.predicate(), Some("active = true"));
249 assert_eq!(
250 model.to_string(),
251 "users|email|active: users::index(email) WHERE active = true"
252 );
253 }
254
255 #[test]
256 fn index_model_without_predicate_preserves_legacy_display_shape() {
257 let model = IndexModel::new("users|email", "users::index", &["email"], true);
258
259 assert_eq!(model.predicate(), None);
260 assert_eq!(model.to_string(), "users|email: UNIQUE users::index(email)");
261 }
262
263 #[test]
264 fn index_model_with_explicit_key_items_exposes_expression_items() {
265 static KEY_ITEMS: [IndexKeyItem; 2] = [
266 IndexKeyItem::Field("tenant_id"),
267 IndexKeyItem::Expression("LOWER(email)"),
268 ];
269 let model = IndexModel::new_with_key_items(
270 "users|tenant|email_expr",
271 "users::index",
272 &["tenant_id"],
273 &KEY_ITEMS,
274 false,
275 );
276
277 assert!(model.has_expression_key_items());
278 assert_eq!(
279 model.to_string(),
280 "users|tenant|email_expr: users::index(tenant_id, LOWER(email))"
281 );
282 assert!(matches!(
283 model.key_items(),
284 IndexKeyItemsRef::Items(items)
285 if items == KEY_ITEMS.as_slice()
286 ));
287 }
288}