1use crate::prelude::*;
2use std::{
3 fmt::{self, Display},
4 ops::Not,
5};
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
14pub enum IndexExpression {
15 Lower(&'static str),
16 Upper(&'static str),
17 Trim(&'static str),
18 LowerTrim(&'static str),
19 Date(&'static str),
20 Year(&'static str),
21 Month(&'static str),
22 Day(&'static str),
23}
24
25impl IndexExpression {
26 #[must_use]
28 pub const fn field(&self) -> &'static str {
29 match self {
30 Self::Lower(field)
31 | Self::Upper(field)
32 | Self::Trim(field)
33 | Self::LowerTrim(field)
34 | Self::Date(field)
35 | Self::Year(field)
36 | Self::Month(field)
37 | Self::Day(field) => field,
38 }
39 }
40}
41
42impl Display for IndexExpression {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 match self {
45 Self::Lower(field) => write!(f, "LOWER({field})"),
46 Self::Upper(field) => write!(f, "UPPER({field})"),
47 Self::Trim(field) => write!(f, "TRIM({field})"),
48 Self::LowerTrim(field) => write!(f, "LOWER(TRIM({field}))"),
49 Self::Date(field) => write!(f, "DATE({field})"),
50 Self::Year(field) => write!(f, "YEAR({field})"),
51 Self::Month(field) => write!(f, "MONTH({field})"),
52 Self::Day(field) => write!(f, "DAY({field})"),
53 }
54 }
55}
56
57#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
65pub enum IndexKeyItem {
66 Field(&'static str),
67 Expression(IndexExpression),
68}
69
70impl IndexKeyItem {
71 #[must_use]
73 pub const fn field(&self) -> &'static str {
74 match self {
75 Self::Field(field) => field,
76 Self::Expression(expression) => expression.field(),
77 }
78 }
79
80 #[must_use]
82 pub fn canonical_text(&self) -> String {
83 match self {
84 Self::Field(field) => (*field).to_string(),
85 Self::Expression(expression) => expression.to_string(),
86 }
87 }
88}
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
97pub enum IndexKeyItemsRef {
98 Fields(&'static [&'static str]),
99 Items(&'static [IndexKeyItem]),
100}
101
102#[derive(Clone, Debug, Serialize)]
107pub struct Index {
108 fields: &'static [&'static str],
109
110 #[serde(default, skip_serializing_if = "Option::is_none")]
111 key_items: Option<&'static [IndexKeyItem]>,
112
113 #[serde(default, skip_serializing_if = "Not::not")]
114 unique: bool,
115
116 #[serde(default, skip_serializing_if = "Option::is_none")]
119 predicate: Option<&'static str>,
120}
121
122impl Index {
123 #[must_use]
125 pub const fn new(fields: &'static [&'static str], unique: bool) -> Self {
126 Self::new_with_key_items_and_predicate(fields, None, unique, None)
127 }
128
129 #[must_use]
131 pub const fn new_with_predicate(
132 fields: &'static [&'static str],
133 unique: bool,
134 predicate: Option<&'static str>,
135 ) -> Self {
136 Self::new_with_key_items_and_predicate(fields, None, unique, predicate)
137 }
138
139 #[must_use]
141 pub const fn new_with_key_items(
142 fields: &'static [&'static str],
143 key_items: &'static [IndexKeyItem],
144 unique: bool,
145 ) -> Self {
146 Self::new_with_key_items_and_predicate(fields, Some(key_items), unique, None)
147 }
148
149 #[must_use]
151 pub const fn new_with_key_items_and_predicate(
152 fields: &'static [&'static str],
153 key_items: Option<&'static [IndexKeyItem]>,
154 unique: bool,
155 predicate: Option<&'static str>,
156 ) -> Self {
157 Self {
158 fields,
159 key_items,
160 unique,
161 predicate,
162 }
163 }
164
165 #[must_use]
167 pub const fn fields(&self) -> &'static [&'static str] {
168 self.fields
169 }
170
171 #[must_use]
173 pub const fn key_items(&self) -> IndexKeyItemsRef {
174 if let Some(items) = self.key_items {
175 IndexKeyItemsRef::Items(items)
176 } else {
177 IndexKeyItemsRef::Fields(self.fields)
178 }
179 }
180
181 #[must_use]
183 pub const fn has_expression_key_items(&self) -> bool {
184 let Some(items) = self.key_items else {
185 return false;
186 };
187
188 let mut index = 0usize;
189 while index < items.len() {
190 if matches!(items[index], IndexKeyItem::Expression(_)) {
191 return true;
192 }
193 index = index.saturating_add(1);
194 }
195
196 false
197 }
198
199 #[must_use]
201 pub const fn is_unique(&self) -> bool {
202 self.unique
203 }
204
205 #[must_use]
210 pub const fn predicate(&self) -> Option<&'static str> {
211 self.predicate
212 }
213
214 #[must_use]
215 pub fn is_prefix_of(&self, other: &Self) -> bool {
216 self.fields().len() < other.fields().len() && other.fields().starts_with(self.fields())
217 }
218
219 fn joined_key_items(&self) -> String {
220 match self.key_items() {
221 IndexKeyItemsRef::Fields(fields) => fields.join(", "),
222 IndexKeyItemsRef::Items(items) => {
223 let mut joined = String::new();
224
225 for item in items {
226 if !joined.is_empty() {
227 joined.push_str(", ");
228 }
229 joined.push_str(item.canonical_text().as_str());
230 }
231
232 joined
233 }
234 }
235 }
236}
237
238impl Display for Index {
239 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240 let fields = self.joined_key_items();
241
242 if self.is_unique() {
243 if let Some(predicate) = self.predicate() {
244 write!(f, "UNIQUE ({fields}) WHERE {predicate}")
245 } else {
246 write!(f, "UNIQUE ({fields})")
247 }
248 } else if let Some(predicate) = self.predicate() {
249 write!(f, "({fields}) WHERE {predicate}")
250 } else {
251 write!(f, "({fields})")
252 }
253 }
254}
255
256impl MacroNode for Index {
257 fn as_any(&self) -> &dyn std::any::Any {
258 self
259 }
260}
261
262impl ValidateNode for Index {}
263
264impl VisitableNode for Index {
265 fn route_key(&self) -> String {
266 self.joined_key_items()
267 }
268}
269
270#[cfg(test)]
275mod tests {
276 use crate::node::index::{Index, IndexExpression, IndexKeyItem, IndexKeyItemsRef};
277
278 #[test]
279 fn index_with_predicate_reports_conditional_shape() {
280 let index = Index::new_with_predicate(&["email"], false, Some("active = true"));
281
282 assert_eq!(index.predicate(), Some("active = true"));
283 assert_eq!(index.to_string(), "(email) WHERE active = true");
284 }
285
286 #[test]
287 fn index_without_predicate_preserves_unconditional_shape() {
288 let index = Index::new(&["email"], true);
289
290 assert_eq!(index.predicate(), None);
291 assert_eq!(index.to_string(), "UNIQUE (email)");
292 }
293
294 #[test]
295 fn index_with_explicit_key_items_exposes_expression_items() {
296 static KEY_ITEMS: [IndexKeyItem; 2] = [
297 IndexKeyItem::Field("tenant_id"),
298 IndexKeyItem::Expression(IndexExpression::Lower("email")),
299 ];
300 let index = Index::new_with_key_items(&["tenant_id"], &KEY_ITEMS, false);
301
302 assert!(index.has_expression_key_items());
303 assert_eq!(index.to_string(), "(tenant_id, LOWER(email))");
304 assert!(matches!(
305 index.key_items(),
306 IndexKeyItemsRef::Items(items)
307 if items == KEY_ITEMS.as_slice()
308 ));
309 }
310}