1use serde::{Deserialize, Serialize};
4
5use crate::identifier::{Identifier, QualifiedName};
6use crate::ir::default_expr::NormalizedExpr;
7use crate::ir::eq::DiffMacro;
8
9#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
13#[serde(tag = "kind", rename_all = "snake_case")]
14pub enum IndexParent {
15 Table(QualifiedName),
17 Mv(QualifiedName),
19}
20
21impl IndexParent {
22 pub const fn qname(&self) -> &QualifiedName {
24 match self {
25 Self::Table(q) | Self::Mv(q) => q,
26 }
27 }
28
29 pub const fn is_mv(&self) -> bool {
31 matches!(self, Self::Mv(_))
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DiffMacro)]
37pub struct Index {
38 pub qname: QualifiedName,
40 #[diff(via_debug)]
42 pub on: IndexParent,
43 #[diff(via_debug)]
45 pub method: IndexMethod,
46 #[diff(via_debug)]
48 pub columns: Vec<IndexColumn>,
49 #[diff(via_debug)]
51 pub include: Vec<Identifier>,
52 pub unique: bool,
54 pub nulls_not_distinct: bool,
56 #[diff(via_debug)]
58 pub predicate: Option<NormalizedExpr>,
59 #[diff(via_debug)]
61 pub tablespace: Option<Identifier>,
62 #[diff(via_debug)]
64 pub comment: Option<String>,
65 #[diff(via_debug)]
68 pub storage: crate::ir::reloptions::IndexStorageOptions,
69}
70
71impl Index {
72 #[must_use]
77 pub fn structurally_eq(&self, other: &Self) -> bool {
78 self.qname == other.qname
79 && self.on == other.on
80 && self.method == other.method
81 && self.columns == other.columns
82 && self.include == other.include
83 && self.unique == other.unique
84 && self.nulls_not_distinct == other.nulls_not_distinct
85 && self.predicate == other.predicate
86 && self.tablespace == other.tablespace
87 && self.comment == other.comment
88 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94pub struct IndexColumn {
95 pub expr: IndexColumnExpr,
97 pub collation: Option<QualifiedName>,
99 pub opclass: Option<QualifiedName>,
101 pub sort_order: SortOrder,
103 pub nulls_order: NullsOrder,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(rename_all = "snake_case")]
110pub enum IndexColumnExpr {
111 Column(Identifier),
113 Expression(NormalizedExpr),
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
119#[serde(rename_all = "snake_case")]
120pub enum IndexMethod {
121 BTree,
123 Hash,
125 Gin,
127 Gist,
129 Brin,
131 Spgist,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
137#[serde(rename_all = "snake_case")]
138pub enum SortOrder {
139 Asc,
141 Desc,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
147#[serde(rename_all = "snake_case")]
148pub enum NullsOrder {
149 NullsFirst,
151 NullsLast,
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::ir::eq::Diff;
159
160 fn id(s: &str) -> Identifier {
161 Identifier::from_unquoted(s).unwrap()
162 }
163
164 fn qn(schema: &str, name: &str) -> QualifiedName {
165 QualifiedName::new(id(schema), id(name))
166 }
167
168 fn col(name: &str) -> IndexColumn {
169 IndexColumn {
170 expr: IndexColumnExpr::Column(id(name)),
171 collation: None,
172 opclass: None,
173 sort_order: SortOrder::Asc,
174 nulls_order: NullsOrder::NullsLast,
175 }
176 }
177
178 fn base() -> Index {
179 Index {
180 qname: qn("app", "users_email_idx"),
181 on: IndexParent::Table(qn("app", "users")),
182 method: IndexMethod::BTree,
183 columns: vec![col("email")],
184 include: vec![],
185 unique: true,
186 nulls_not_distinct: false,
187 predicate: None,
188 tablespace: None,
189 comment: None,
190 storage: crate::ir::reloptions::IndexStorageOptions::default(),
191 }
192 }
193
194 #[test]
195 fn equal_indexes_have_no_diff() {
196 assert!(base().canonical_eq(&base()));
197 }
198
199 #[test]
200 fn unique_change_diffs() {
201 let mut b = base();
202 b.unique = false;
203 assert!(base().diff(&b).iter().any(|x| x.path == "unique"));
204 }
205
206 #[test]
207 fn include_columns_diff() {
208 let mut b = base();
209 b.include = vec![id("name")];
210 assert!(base().diff(&b).iter().any(|x| x.path == "include"));
211 }
212
213 #[test]
214 fn predicate_change_diffs() {
215 let mut b = base();
216 b.predicate = Some(NormalizedExpr::from_text("deleted_at is null"));
217 assert!(base().diff(&b).iter().any(|x| x.path == "predicate"));
218 }
219
220 #[test]
221 fn opclass_change_diffs() {
222 let mut b = base();
223 b.columns[0].opclass = Some(qn("pg_catalog", "text_pattern_ops"));
224 assert!(base().diff(&b).iter().any(|x| x.path == "columns"));
225 }
226
227 #[test]
228 fn column_order_matters() {
229 let a = Index {
230 columns: vec![col("a"), col("b")],
231 ..base()
232 };
233 let b = Index {
234 columns: vec![col("b"), col("a")],
235 ..base()
236 };
237 assert!(!a.canonical_eq(&b));
238 }
239
240 #[test]
241 fn storage_change_diffs() {
242 let mut b = base();
243 b.storage = crate::ir::reloptions::IndexStorageOptions {
244 fillfactor: Some(70),
245 ..Default::default()
246 };
247 assert!(base().diff(&b).iter().any(|x| x.path == "storage"));
248 }
249
250 #[test]
251 fn index_can_target_a_materialized_view() {
252 let mv_idx = Index {
253 qname: qn("app", "mv_email_idx"),
254 on: IndexParent::Mv(qn("app", "users_mv")),
255 method: IndexMethod::BTree,
256 columns: vec![col("email")],
257 include: vec![],
258 unique: true,
259 nulls_not_distinct: false,
260 predicate: None,
261 tablespace: None,
262 comment: None,
263 storage: crate::ir::reloptions::IndexStorageOptions::default(),
264 };
265 assert!(mv_idx.on.is_mv());
266 assert_eq!(mv_idx.on.qname().to_string(), "app.users_mv");
267 assert!(mv_idx.canonical_eq(&mv_idx));
268
269 let tbl_idx = base();
271 assert!(!tbl_idx.on.is_mv());
272 assert_eq!(tbl_idx.on.qname().to_string(), "app.users");
273
274 let tbl_idx_same_name = Index {
276 qname: qn("app", "mv_email_idx"),
277 on: IndexParent::Table(qn("app", "users_mv")),
278 ..mv_idx.clone()
279 };
280 assert!(!mv_idx.canonical_eq(&tbl_idx_same_name));
281 let diffs = mv_idx.diff(&tbl_idx_same_name);
282 assert!(diffs.iter().any(|d| d.path == "on"));
283 }
284}