1use crate::prelude::*;
2use icydb_schema::{SchemaContractError, SourceCheckExpr};
3use std::{
4 fmt::{self, Display},
5 ops::Not,
6};
7
8use crate::node::{Schema, SourceExpressionResolver};
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
17pub enum IndexExpression {
18 Lower(&'static str),
19 Upper(&'static str),
20 Trim(&'static str),
21 LowerTrim(&'static str),
22 Date(&'static str),
23 Year(&'static str),
24 Month(&'static str),
25 Day(&'static str),
26}
27
28impl IndexExpression {
29 #[must_use]
31 pub const fn field(&self) -> &'static str {
32 match self {
33 Self::Lower(field)
34 | Self::Upper(field)
35 | Self::Trim(field)
36 | Self::LowerTrim(field)
37 | Self::Date(field)
38 | Self::Year(field)
39 | Self::Month(field)
40 | Self::Day(field) => field,
41 }
42 }
43}
44
45impl Display for IndexExpression {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 match self {
48 Self::Lower(field) => write!(f, "LOWER({field})"),
49 Self::Upper(field) => write!(f, "UPPER({field})"),
50 Self::Trim(field) => write!(f, "TRIM({field})"),
51 Self::LowerTrim(field) => write!(f, "LOWER(TRIM({field}))"),
52 Self::Date(field) => write!(f, "DATE({field})"),
53 Self::Year(field) => write!(f, "YEAR({field})"),
54 Self::Month(field) => write!(f, "MONTH({field})"),
55 Self::Day(field) => write!(f, "DAY({field})"),
56 }
57 }
58}
59
60#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
68pub enum IndexKeyItem {
69 Field(&'static str),
70 Expression(IndexExpression),
71}
72
73impl IndexKeyItem {
74 #[must_use]
76 pub const fn field(&self) -> &'static str {
77 match self {
78 Self::Field(field) => field,
79 Self::Expression(expression) => expression.field(),
80 }
81 }
82
83 #[must_use]
85 pub fn canonical_text(&self) -> String {
86 match self {
87 Self::Field(field) => (*field).to_string(),
88 Self::Expression(expression) => expression.to_string(),
89 }
90 }
91}
92
93#[derive(Clone, Copy, Debug, Eq, PartialEq)]
100pub enum IndexKeyItemsRef {
101 Fields(&'static [&'static str]),
102 Items(&'static [IndexKeyItem]),
103}
104
105#[derive(Clone, Debug, Serialize)]
110pub struct Index {
111 name: &'static str,
112 fields: &'static [&'static str],
113
114 #[serde(skip_serializing_if = "Option::is_none")]
115 key_items: Option<&'static [IndexKeyItem]>,
116
117 #[serde(skip_serializing_if = "Not::not")]
118 unique: bool,
119
120 #[serde(skip_serializing_if = "Option::is_none")]
123 predicate: Option<&'static str>,
124
125 #[serde(skip)]
126 predicate_expression: Option<SourceExpressionResolver>,
127}
128
129impl Index {
130 #[must_use]
132 pub const fn new(name: &'static str, fields: &'static [&'static str], unique: bool) -> Self {
133 Self::new_with_key_items_and_predicate(name, fields, None, unique, None, None)
134 }
135
136 #[must_use]
138 pub const fn new_with_predicate(
139 name: &'static str,
140 fields: &'static [&'static str],
141 unique: bool,
142 predicate: Option<&'static str>,
143 predicate_expression: Option<SourceExpressionResolver>,
144 ) -> Self {
145 Self::new_with_key_items_and_predicate(
146 name,
147 fields,
148 None,
149 unique,
150 predicate,
151 predicate_expression,
152 )
153 }
154
155 #[must_use]
157 pub const fn new_with_key_items(
158 name: &'static str,
159 fields: &'static [&'static str],
160 key_items: &'static [IndexKeyItem],
161 unique: bool,
162 ) -> Self {
163 Self::new_with_key_items_and_predicate(name, fields, Some(key_items), unique, None, None)
164 }
165
166 #[must_use]
168 pub const fn new_with_key_items_and_predicate(
169 name: &'static str,
170 fields: &'static [&'static str],
171 key_items: Option<&'static [IndexKeyItem]>,
172 unique: bool,
173 predicate: Option<&'static str>,
174 predicate_expression: Option<SourceExpressionResolver>,
175 ) -> Self {
176 Self {
177 name,
178 fields,
179 key_items,
180 unique,
181 predicate,
182 predicate_expression,
183 }
184 }
185
186 #[must_use]
188 pub const fn name(&self) -> &'static str {
189 self.name
190 }
191
192 #[must_use]
194 pub const fn fields(&self) -> &'static [&'static str] {
195 self.fields
196 }
197
198 #[must_use]
200 pub const fn key_items(&self) -> IndexKeyItemsRef {
201 if let Some(items) = self.key_items {
202 IndexKeyItemsRef::Items(items)
203 } else {
204 IndexKeyItemsRef::Fields(self.fields)
205 }
206 }
207
208 #[must_use]
210 pub const fn has_expression_key_items(&self) -> bool {
211 let Some(items) = self.key_items else {
212 return false;
213 };
214
215 let mut index = 0usize;
216 while index < items.len() {
217 if matches!(items[index], IndexKeyItem::Expression(_)) {
218 return true;
219 }
220 index = index.saturating_add(1);
221 }
222
223 false
224 }
225
226 #[must_use]
228 pub const fn is_unique(&self) -> bool {
229 self.unique
230 }
231
232 #[must_use]
237 pub const fn predicate(&self) -> Option<&'static str> {
238 self.predicate
239 }
240
241 pub fn source_predicate(
249 &self,
250 schema: &Schema,
251 ) -> Result<Option<SourceCheckExpr>, SchemaContractError> {
252 match (self.predicate, self.predicate_expression) {
253 (None, None) => Ok(None),
254 (Some(_), Some(resolve)) => resolve(schema).map(Some),
255 (None, Some(_)) | (Some(_), None) => Err(SchemaContractError::InvalidExpression),
256 }
257 }
258
259 #[must_use]
260 pub fn is_prefix_of(&self, other: &Self) -> bool {
261 self.fields().len() < other.fields().len() && other.fields().starts_with(self.fields())
262 }
263
264 fn joined_key_items(&self) -> String {
265 match self.key_items() {
266 IndexKeyItemsRef::Fields(fields) => fields.join(", "),
267 IndexKeyItemsRef::Items(items) => {
268 let mut joined = String::new();
269
270 for item in items {
271 if !joined.is_empty() {
272 joined.push_str(", ");
273 }
274 joined.push_str(item.canonical_text().as_str());
275 }
276
277 joined
278 }
279 }
280 }
281}
282
283impl Display for Index {
284 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285 let fields = self.joined_key_items();
286
287 if self.is_unique() {
288 if let Some(predicate) = self.predicate() {
289 write!(f, "UNIQUE ({fields}) WHERE {predicate}")
290 } else {
291 write!(f, "UNIQUE ({fields})")
292 }
293 } else if let Some(predicate) = self.predicate() {
294 write!(f, "({fields}) WHERE {predicate}")
295 } else {
296 write!(f, "({fields})")
297 }
298 }
299}
300
301impl MacroNode for Index {
302 fn as_any(&self) -> &dyn std::any::Any {
303 self
304 }
305}
306
307impl ValidateNode for Index {
308 fn validate(&self) -> Result<(), ErrorTree> {
309 let mut errs = ErrorTree::new();
310 validate_source_name(
311 &mut errs,
312 "index",
313 self.name(),
314 icydb_schema::IndexSourceKey::try_new,
315 );
316 errs.result()
317 }
318}
319
320impl VisitableNode for Index {
321 fn route_key(&self) -> String {
322 self.joined_key_items()
323 }
324}
325
326#[cfg(test)]
331mod tests {
332 use crate::node::index::{Index, IndexExpression, IndexKeyItem, IndexKeyItemsRef};
333
334 #[test]
335 fn index_with_predicate_reports_conditional_shape() {
336 let index = Index::new_with_predicate(
337 "idx_user__email",
338 &["email"],
339 false,
340 Some("active = true"),
341 None,
342 );
343
344 assert_eq!(index.predicate(), Some("active = true"));
345 assert_eq!(index.to_string(), "(email) WHERE active = true");
346 }
347
348 #[test]
349 fn index_without_predicate_preserves_unconditional_shape() {
350 let index = Index::new("uidx_user__email", &["email"], true);
351
352 assert_eq!(index.predicate(), None);
353 assert_eq!(index.to_string(), "UNIQUE (email)");
354 }
355
356 #[test]
357 fn index_with_explicit_key_items_exposes_expression_items() {
358 static KEY_ITEMS: [IndexKeyItem; 2] = [
359 IndexKeyItem::Field("tenant_id"),
360 IndexKeyItem::Expression(IndexExpression::Lower("email")),
361 ];
362 let index = Index::new_with_key_items(
363 "idx_user__tenant_id__lower_email",
364 &["tenant_id"],
365 &KEY_ITEMS,
366 false,
367 );
368
369 assert!(index.has_expression_key_items());
370 assert_eq!(index.to_string(), "(tenant_id, LOWER(email))");
371 std::assert_matches!(
372 index.key_items(),
373 IndexKeyItemsRef::Items(items)
374 if items == KEY_ITEMS.as_slice()
375 );
376 }
377}