icydb_schema/node/
index.rs1use crate::prelude::*;
2use std::{
3 fmt::{self, Display},
4 ops::Not,
5};
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
15pub enum IndexKeyItem {
16 Field(&'static str),
17 Expression(&'static str),
18}
19
20impl IndexKeyItem {
21 #[must_use]
23 pub const fn text(&self) -> &'static str {
24 match self {
25 Self::Field(field) | Self::Expression(field) => field,
26 }
27 }
28}
29
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37pub enum IndexKeyItemsRef {
38 Fields(&'static [&'static str]),
39 Items(&'static [IndexKeyItem]),
40}
41
42#[derive(Clone, Debug, Serialize)]
47pub struct Index {
48 fields: &'static [&'static str],
49
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 key_items: Option<&'static [IndexKeyItem]>,
52
53 #[serde(default, skip_serializing_if = "Not::not")]
54 unique: bool,
55
56 #[serde(default, skip_serializing_if = "Option::is_none")]
59 predicate: Option<&'static str>,
60}
61
62impl Index {
63 #[must_use]
65 pub const fn new(fields: &'static [&'static str], unique: bool) -> Self {
66 Self::new_with_key_items_and_predicate(fields, None, unique, None)
67 }
68
69 #[must_use]
71 pub const fn new_with_predicate(
72 fields: &'static [&'static str],
73 unique: bool,
74 predicate: Option<&'static str>,
75 ) -> Self {
76 Self::new_with_key_items_and_predicate(fields, None, unique, predicate)
77 }
78
79 #[must_use]
81 pub const fn new_with_key_items(
82 fields: &'static [&'static str],
83 key_items: &'static [IndexKeyItem],
84 unique: bool,
85 ) -> Self {
86 Self::new_with_key_items_and_predicate(fields, Some(key_items), unique, None)
87 }
88
89 #[must_use]
91 pub const fn new_with_key_items_and_predicate(
92 fields: &'static [&'static str],
93 key_items: Option<&'static [IndexKeyItem]>,
94 unique: bool,
95 predicate: Option<&'static str>,
96 ) -> Self {
97 Self {
98 fields,
99 key_items,
100 unique,
101 predicate,
102 }
103 }
104
105 #[must_use]
107 pub const fn fields(&self) -> &'static [&'static str] {
108 self.fields
109 }
110
111 #[must_use]
113 pub const fn key_items(&self) -> IndexKeyItemsRef {
114 if let Some(items) = self.key_items {
115 IndexKeyItemsRef::Items(items)
116 } else {
117 IndexKeyItemsRef::Fields(self.fields)
118 }
119 }
120
121 #[must_use]
123 pub const fn has_expression_key_items(&self) -> bool {
124 let Some(items) = self.key_items else {
125 return false;
126 };
127
128 let mut index = 0usize;
129 while index < items.len() {
130 if matches!(items[index], IndexKeyItem::Expression(_)) {
131 return true;
132 }
133 index = index.saturating_add(1);
134 }
135
136 false
137 }
138
139 #[must_use]
141 pub const fn is_unique(&self) -> bool {
142 self.unique
143 }
144
145 #[must_use]
150 pub const fn predicate(&self) -> Option<&'static str> {
151 self.predicate
152 }
153
154 #[must_use]
155 pub fn is_prefix_of(&self, other: &Self) -> bool {
156 self.fields().len() < other.fields().len() && other.fields().starts_with(self.fields())
157 }
158
159 fn joined_key_items(&self) -> String {
160 match self.key_items() {
161 IndexKeyItemsRef::Fields(fields) => fields.join(", "),
162 IndexKeyItemsRef::Items(items) => items
163 .iter()
164 .map(IndexKeyItem::text)
165 .collect::<Vec<_>>()
166 .join(", "),
167 }
168 }
169}
170
171impl Display for Index {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 let fields = self.joined_key_items();
174
175 if self.is_unique() {
176 if let Some(predicate) = self.predicate() {
177 write!(f, "UNIQUE ({fields}) WHERE {predicate}")
178 } else {
179 write!(f, "UNIQUE ({fields})")
180 }
181 } else if let Some(predicate) = self.predicate() {
182 write!(f, "({fields}) WHERE {predicate}")
183 } else {
184 write!(f, "({fields})")
185 }
186 }
187}
188
189impl MacroNode for Index {
190 fn as_any(&self) -> &dyn std::any::Any {
191 self
192 }
193}
194
195impl ValidateNode for Index {}
196
197impl VisitableNode for Index {
198 fn route_key(&self) -> String {
199 self.joined_key_items()
200 }
201}
202
203#[cfg(test)]
208mod tests {
209 use crate::node::index::{Index, IndexKeyItem, IndexKeyItemsRef};
210
211 #[test]
212 fn index_with_predicate_reports_conditional_shape() {
213 let index = Index::new_with_predicate(&["email"], false, Some("active = true"));
214
215 assert_eq!(index.predicate(), Some("active = true"));
216 assert_eq!(index.to_string(), "(email) WHERE active = true");
217 }
218
219 #[test]
220 fn index_without_predicate_preserves_legacy_shape() {
221 let index = Index::new(&["email"], true);
222
223 assert_eq!(index.predicate(), None);
224 assert_eq!(index.to_string(), "UNIQUE (email)");
225 }
226
227 #[test]
228 fn index_with_explicit_key_items_exposes_expression_items() {
229 static KEY_ITEMS: [IndexKeyItem; 2] = [
230 IndexKeyItem::Field("tenant_id"),
231 IndexKeyItem::Expression("LOWER(email)"),
232 ];
233 let index = Index::new_with_key_items(&["tenant_id"], &KEY_ITEMS, false);
234
235 assert!(index.has_expression_key_items());
236 assert_eq!(index.to_string(), "(tenant_id, LOWER(email))");
237 assert!(matches!(
238 index.key_items(),
239 IndexKeyItemsRef::Items(items)
240 if items == KEY_ITEMS.as_slice()
241 ));
242 }
243}