1use serde::{Deserialize, Serialize};
4
5use crate::identifier::{Identifier, QualifiedName};
6use crate::ir::default_expr::NormalizedExpr;
7use crate::ir::difference::Difference;
8use crate::ir::eq::{Equiv, field_difference};
9
10#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
14#[serde(tag = "kind", rename_all = "snake_case")]
15pub enum IndexParent {
16 Table(QualifiedName),
18 Mv(QualifiedName),
20}
21
22impl IndexParent {
23 pub const fn qname(&self) -> &QualifiedName {
25 match self {
26 Self::Table(q) | Self::Mv(q) => q,
27 }
28 }
29
30 pub const fn is_mv(&self) -> bool {
32 matches!(self, Self::Mv(_))
33 }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct Index {
39 pub qname: QualifiedName,
41 pub on: IndexParent,
43 pub method: IndexMethod,
45 pub columns: Vec<IndexColumn>,
47 pub include: Vec<Identifier>,
49 pub unique: bool,
51 pub nulls_not_distinct: bool,
53 pub predicate: Option<NormalizedExpr>,
55 pub tablespace: Option<Identifier>,
57 pub comment: Option<String>,
59 pub storage: crate::ir::reloptions::IndexStorageOptions,
62}
63
64impl Equiv for Index {
65 fn differences(&self, other: &Self) -> Vec<Difference> {
66 let Self {
67 qname: _,
68 on: _,
69 method: _,
70 columns: _,
71 include: _,
72 unique: _,
73 nulls_not_distinct: _,
74 predicate: _,
75 tablespace: _,
76 comment: _,
77 storage: _,
78 } = self;
79 let mut out = Vec::new();
80 out.extend(field_difference("qname", &self.qname, &other.qname));
81 out.extend(field_difference(
82 "on",
83 &format!("{:?}", self.on),
84 &format!("{:?}", other.on),
85 ));
86 out.extend(field_difference(
87 "method",
88 &format!("{:?}", self.method),
89 &format!("{:?}", other.method),
90 ));
91 out.extend(field_difference(
92 "columns",
93 &format!("{:?}", self.columns),
94 &format!("{:?}", other.columns),
95 ));
96 out.extend(field_difference(
97 "include",
98 &format!("{:?}", self.include),
99 &format!("{:?}", other.include),
100 ));
101 out.extend(field_difference("unique", &self.unique, &other.unique));
102 out.extend(field_difference(
103 "nulls_not_distinct",
104 &self.nulls_not_distinct,
105 &other.nulls_not_distinct,
106 ));
107 out.extend(field_difference(
108 "predicate",
109 &format!("{:?}", self.predicate),
110 &format!("{:?}", other.predicate),
111 ));
112 out.extend(field_difference(
113 "tablespace",
114 &format!("{:?}", self.tablespace),
115 &format!("{:?}", other.tablespace),
116 ));
117 out.extend(field_difference(
118 "comment",
119 &format!("{:?}", self.comment),
120 &format!("{:?}", other.comment),
121 ));
122 out.extend(field_difference(
123 "storage",
124 &format!("{:?}", self.storage),
125 &format!("{:?}", other.storage),
126 ));
127 out
128 }
129}
130
131impl Index {
132 #[must_use]
137 pub fn structurally_eq(&self, other: &Self) -> bool {
138 self.qname == other.qname
139 && self.on == other.on
140 && self.method == other.method
141 && self.columns == other.columns
142 && self.include == other.include
143 && self.unique == other.unique
144 && self.nulls_not_distinct == other.nulls_not_distinct
145 && self.predicate == other.predicate
146 && self.tablespace == other.tablespace
147 && self.comment == other.comment
148 }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
154pub struct IndexColumn {
155 pub expr: IndexColumnExpr,
157 pub collation: Option<QualifiedName>,
159 pub opclass: Option<QualifiedName>,
161 pub sort_order: SortOrder,
163 pub nulls_order: NullsOrder,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
169#[serde(rename_all = "snake_case")]
170pub enum IndexColumnExpr {
171 Column(Identifier),
173 Expression(NormalizedExpr),
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
179#[serde(rename_all = "snake_case")]
180pub enum IndexMethod {
181 BTree,
183 Hash,
185 Gin,
187 Gist,
189 Brin,
191 Spgist,
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
197#[serde(rename_all = "snake_case")]
198pub enum SortOrder {
199 Asc,
201 Desc,
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
207#[serde(rename_all = "snake_case")]
208pub enum NullsOrder {
209 NullsFirst,
211 NullsLast,
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use crate::ir::eq::Equiv;
219
220 fn id(s: &str) -> Identifier {
221 Identifier::from_unquoted(s).unwrap()
222 }
223
224 fn qn(schema: &str, name: &str) -> QualifiedName {
225 QualifiedName::new(id(schema), id(name))
226 }
227
228 fn col(name: &str) -> IndexColumn {
229 IndexColumn {
230 expr: IndexColumnExpr::Column(id(name)),
231 collation: None,
232 opclass: None,
233 sort_order: SortOrder::Asc,
234 nulls_order: NullsOrder::NullsLast,
235 }
236 }
237
238 fn base() -> Index {
239 Index {
240 qname: qn("app", "users_email_idx"),
241 on: IndexParent::Table(qn("app", "users")),
242 method: IndexMethod::BTree,
243 columns: vec![col("email")],
244 include: vec![],
245 unique: true,
246 nulls_not_distinct: false,
247 predicate: None,
248 tablespace: None,
249 comment: None,
250 storage: crate::ir::reloptions::IndexStorageOptions::default(),
251 }
252 }
253
254 #[test]
255 fn equal_indexes_have_no_diff() {
256 assert!(base().canonical_eq(&base()));
257 }
258
259 #[test]
260 fn unique_change_diffs() {
261 let mut b = base();
262 b.unique = false;
263 assert!(base().differences(&b).iter().any(|x| x.path == "unique"));
264 }
265
266 #[test]
267 fn include_columns_diff() {
268 let mut b = base();
269 b.include = vec![id("name")];
270 assert!(base().differences(&b).iter().any(|x| x.path == "include"));
271 }
272
273 #[test]
274 fn predicate_change_diffs() {
275 let mut b = base();
276 b.predicate = Some(NormalizedExpr::from_text("deleted_at is null"));
277 assert!(base().differences(&b).iter().any(|x| x.path == "predicate"));
278 }
279
280 #[test]
281 fn opclass_change_diffs() {
282 let mut b = base();
283 b.columns[0].opclass = Some(qn("pg_catalog", "text_pattern_ops"));
284 assert!(base().differences(&b).iter().any(|x| x.path == "columns"));
285 }
286
287 #[test]
288 fn column_order_matters() {
289 let a = Index {
290 columns: vec![col("a"), col("b")],
291 ..base()
292 };
293 let b = Index {
294 columns: vec![col("b"), col("a")],
295 ..base()
296 };
297 assert!(!a.canonical_eq(&b));
298 }
299
300 #[test]
301 fn storage_change_diffs() {
302 let mut b = base();
303 b.storage = crate::ir::reloptions::IndexStorageOptions {
304 fillfactor: Some(70),
305 ..Default::default()
306 };
307 assert!(base().differences(&b).iter().any(|x| x.path == "storage"));
308 }
309
310 #[test]
311 fn index_can_target_a_materialized_view() {
312 let mv_idx = Index {
313 qname: qn("app", "mv_email_idx"),
314 on: IndexParent::Mv(qn("app", "users_mv")),
315 method: IndexMethod::BTree,
316 columns: vec![col("email")],
317 include: vec![],
318 unique: true,
319 nulls_not_distinct: false,
320 predicate: None,
321 tablespace: None,
322 comment: None,
323 storage: crate::ir::reloptions::IndexStorageOptions::default(),
324 };
325 assert!(mv_idx.on.is_mv());
326 assert_eq!(mv_idx.on.qname().to_string(), "app.users_mv");
327 assert!(mv_idx.canonical_eq(&mv_idx));
328
329 let tbl_idx = base();
331 assert!(!tbl_idx.on.is_mv());
332 assert_eq!(tbl_idx.on.qname().to_string(), "app.users");
333
334 let tbl_idx_same_name = Index {
336 qname: qn("app", "mv_email_idx"),
337 on: IndexParent::Table(qn("app", "users_mv")),
338 ..mv_idx.clone()
339 };
340 assert!(!mv_idx.canonical_eq(&tbl_idx_same_name));
341 let diffs = mv_idx.differences(&tbl_idx_same_name);
342 assert!(diffs.iter().any(|d| d.path == "on"));
343 }
344}