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 source_key: &'static str,
112 name: &'static str,
113 fields: &'static [&'static str],
114
115 #[serde(skip_serializing_if = "Option::is_none")]
116 key_items: Option<&'static [IndexKeyItem]>,
117
118 #[serde(skip_serializing_if = "Not::not")]
119 unique: bool,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
124 predicate: Option<&'static str>,
125
126 #[serde(skip)]
127 predicate_expression: Option<SourceExpressionResolver>,
128}
129
130impl Index {
131 #[must_use]
133 pub const fn new(
134 source_key: &'static str,
135 name: &'static str,
136 fields: &'static [&'static str],
137 unique: bool,
138 ) -> Self {
139 Self::new_with_key_items_and_predicate(source_key, name, fields, None, unique, None, None)
140 }
141
142 #[must_use]
144 pub const fn new_with_predicate(
145 source_key: &'static str,
146 name: &'static str,
147 fields: &'static [&'static str],
148 unique: bool,
149 predicate: Option<&'static str>,
150 predicate_expression: Option<SourceExpressionResolver>,
151 ) -> Self {
152 Self::new_with_key_items_and_predicate(
153 source_key,
154 name,
155 fields,
156 None,
157 unique,
158 predicate,
159 predicate_expression,
160 )
161 }
162
163 #[must_use]
165 pub const fn new_with_key_items(
166 source_key: &'static str,
167 name: &'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(
173 source_key,
174 name,
175 fields,
176 Some(key_items),
177 unique,
178 None,
179 None,
180 )
181 }
182
183 #[must_use]
185 pub const fn new_with_key_items_and_predicate(
186 source_key: &'static str,
187 name: &'static str,
188 fields: &'static [&'static str],
189 key_items: Option<&'static [IndexKeyItem]>,
190 unique: bool,
191 predicate: Option<&'static str>,
192 predicate_expression: Option<SourceExpressionResolver>,
193 ) -> Self {
194 Self {
195 source_key,
196 name,
197 fields,
198 key_items,
199 unique,
200 predicate,
201 predicate_expression,
202 }
203 }
204
205 #[must_use]
207 pub const fn source_key(&self) -> &'static str {
208 self.source_key
209 }
210
211 #[must_use]
213 pub const fn name(&self) -> &'static str {
214 self.name
215 }
216
217 #[must_use]
219 pub const fn fields(&self) -> &'static [&'static str] {
220 self.fields
221 }
222
223 #[must_use]
225 pub const fn key_items(&self) -> IndexKeyItemsRef {
226 if let Some(items) = self.key_items {
227 IndexKeyItemsRef::Items(items)
228 } else {
229 IndexKeyItemsRef::Fields(self.fields)
230 }
231 }
232
233 #[must_use]
235 pub const fn has_expression_key_items(&self) -> bool {
236 let Some(items) = self.key_items else {
237 return false;
238 };
239
240 let mut index = 0usize;
241 while index < items.len() {
242 if matches!(items[index], IndexKeyItem::Expression(_)) {
243 return true;
244 }
245 index = index.saturating_add(1);
246 }
247
248 false
249 }
250
251 #[must_use]
253 pub const fn is_unique(&self) -> bool {
254 self.unique
255 }
256
257 #[must_use]
262 pub const fn predicate(&self) -> Option<&'static str> {
263 self.predicate
264 }
265
266 pub fn source_predicate(
274 &self,
275 schema: &Schema,
276 ) -> Result<Option<SourceCheckExpr>, SchemaContractError> {
277 match (self.predicate, self.predicate_expression) {
278 (None, None) => Ok(None),
279 (Some(_), Some(resolve)) => resolve(schema).map(Some),
280 (None, Some(_)) | (Some(_), None) => Err(SchemaContractError::InvalidExpression),
281 }
282 }
283
284 #[must_use]
285 pub fn is_prefix_of(&self, other: &Self) -> bool {
286 self.fields().len() < other.fields().len() && other.fields().starts_with(self.fields())
287 }
288
289 fn joined_key_items(&self) -> String {
290 match self.key_items() {
291 IndexKeyItemsRef::Fields(fields) => fields.join(", "),
292 IndexKeyItemsRef::Items(items) => {
293 let mut joined = String::new();
294
295 for item in items {
296 if !joined.is_empty() {
297 joined.push_str(", ");
298 }
299 joined.push_str(item.canonical_text().as_str());
300 }
301
302 joined
303 }
304 }
305 }
306}
307
308impl Display for Index {
309 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310 let fields = self.joined_key_items();
311
312 if self.is_unique() {
313 if let Some(predicate) = self.predicate() {
314 write!(f, "UNIQUE ({fields}) WHERE {predicate}")
315 } else {
316 write!(f, "UNIQUE ({fields})")
317 }
318 } else if let Some(predicate) = self.predicate() {
319 write!(f, "({fields}) WHERE {predicate}")
320 } else {
321 write!(f, "({fields})")
322 }
323 }
324}
325
326impl MacroNode for Index {
327 fn as_any(&self) -> &dyn std::any::Any {
328 self
329 }
330}
331
332impl ValidateNode for Index {
333 fn validate(&self) -> Result<(), ErrorTree> {
334 let mut errs = ErrorTree::new();
335 validate_source_key(
336 &mut errs,
337 "index",
338 self.source_key(),
339 icydb_schema::IndexSourceKey::try_new,
340 );
341 errs.result()
342 }
343}
344
345impl VisitableNode for Index {
346 fn route_key(&self) -> String {
347 self.joined_key_items()
348 }
349}
350
351#[cfg(test)]
356mod tests {
357 use crate::node::index::{Index, IndexExpression, IndexKeyItem, IndexKeyItemsRef};
358
359 #[test]
360 fn index_with_predicate_reports_conditional_shape() {
361 let index = Index::new_with_predicate(
362 "email_active",
363 "idx_user__email",
364 &["email"],
365 false,
366 Some("active = true"),
367 None,
368 );
369
370 assert_eq!(index.predicate(), Some("active = true"));
371 assert_eq!(index.to_string(), "(email) WHERE active = true");
372 }
373
374 #[test]
375 fn index_without_predicate_preserves_unconditional_shape() {
376 let index = Index::new("email", "uidx_user__email", &["email"], true);
377
378 assert_eq!(index.predicate(), None);
379 assert_eq!(index.to_string(), "UNIQUE (email)");
380 }
381
382 #[test]
383 fn index_with_explicit_key_items_exposes_expression_items() {
384 static KEY_ITEMS: [IndexKeyItem; 2] = [
385 IndexKeyItem::Field("tenant_id"),
386 IndexKeyItem::Expression(IndexExpression::Lower("email")),
387 ];
388 let index = Index::new_with_key_items(
389 "tenant_lower_email",
390 "idx_user__tenant_id__lower_email",
391 &["tenant_id"],
392 &KEY_ITEMS,
393 false,
394 );
395
396 assert!(index.has_expression_key_items());
397 assert_eq!(index.to_string(), "(tenant_id, LOWER(email))");
398 std::assert_matches!(
399 index.key_items(),
400 IndexKeyItemsRef::Items(items)
401 if items == KEY_ITEMS.as_slice()
402 );
403 }
404}