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) => items
223 .iter()
224 .map(IndexKeyItem::canonical_text)
225 .collect::<Vec<_>>()
226 .join(", "),
227 }
228 }
229}
230
231impl Display for Index {
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 let fields = self.joined_key_items();
234
235 if self.is_unique() {
236 if let Some(predicate) = self.predicate() {
237 write!(f, "UNIQUE ({fields}) WHERE {predicate}")
238 } else {
239 write!(f, "UNIQUE ({fields})")
240 }
241 } else if let Some(predicate) = self.predicate() {
242 write!(f, "({fields}) WHERE {predicate}")
243 } else {
244 write!(f, "({fields})")
245 }
246 }
247}
248
249impl MacroNode for Index {
250 fn as_any(&self) -> &dyn std::any::Any {
251 self
252 }
253}
254
255impl ValidateNode for Index {}
256
257impl VisitableNode for Index {
258 fn route_key(&self) -> String {
259 self.joined_key_items()
260 }
261}
262
263#[cfg(test)]
268mod tests {
269 use crate::node::index::{Index, IndexExpression, IndexKeyItem, IndexKeyItemsRef};
270
271 #[test]
272 fn index_with_predicate_reports_conditional_shape() {
273 let index = Index::new_with_predicate(&["email"], false, Some("active = true"));
274
275 assert_eq!(index.predicate(), Some("active = true"));
276 assert_eq!(index.to_string(), "(email) WHERE active = true");
277 }
278
279 #[test]
280 fn index_without_predicate_preserves_legacy_shape() {
281 let index = Index::new(&["email"], true);
282
283 assert_eq!(index.predicate(), None);
284 assert_eq!(index.to_string(), "UNIQUE (email)");
285 }
286
287 #[test]
288 fn index_with_explicit_key_items_exposes_expression_items() {
289 static KEY_ITEMS: [IndexKeyItem; 2] = [
290 IndexKeyItem::Field("tenant_id"),
291 IndexKeyItem::Expression(IndexExpression::Lower("email")),
292 ];
293 let index = Index::new_with_key_items(&["tenant_id"], &KEY_ITEMS, false);
294
295 assert!(index.has_expression_key_items());
296 assert_eq!(index.to_string(), "(tenant_id, LOWER(email))");
297 assert!(matches!(
298 index.key_items(),
299 IndexKeyItemsRef::Items(items)
300 if items == KEY_ITEMS.as_slice()
301 ));
302 }
303}