1pub mod change;
15pub mod changeset;
16pub mod cluster;
17pub mod columns;
18pub mod constraints;
19pub mod default_privileges;
20pub mod destructiveness;
21pub mod extensions;
22pub mod grants;
23pub mod indexes;
24pub mod owner_op;
25pub mod policies;
26pub mod publications;
27pub mod reloptions;
28pub mod routines;
29pub mod schemas;
30pub mod sequence_op;
31pub mod sequences;
32pub mod table_op;
33pub mod tables;
34pub mod triggers;
35pub mod types;
36pub mod views;
37
38pub use change::{
39 Change, ChangeEntry, ExtensionChange, FunctionChange, MvChange, ProcedureChange, TableChange,
40 TriggerChange, UserTypeChange, ViewChange,
41};
42pub use changeset::{ChangeSet, RevokeWithOwnerObservation, UnmanagedGrantObservation};
43pub use cluster::{ClusterChange, ClusterChangeEntry, ClusterChangeSet, diff_cluster};
44pub use destructiveness::Destructiveness;
45pub use owner_op::{AlterObjectOwner, OwnerObjectKind};
46pub use routines::{diff_functions, diff_procedures};
47pub use sequence_op::{SequenceOp, SequenceOpEntry};
48pub use table_op::{TableOp, TableOpEntry};
49
50use crate::catalog::DriftReport;
51use crate::ir::catalog::Catalog;
52
53pub fn diff(target: &Catalog, source: &Catalog, drift: &DriftReport) -> ChangeSet {
63 let mut out = ChangeSet::new();
64
65 let managed_roles = grants::collect_managed_roles(source);
69
70 for (table_qname, constraint_name) in &drift.pending_validation {
73 out.push(
74 Change::ValidateConstraint {
75 table: table_qname.clone(),
76 constraint: constraint_name.clone(),
77 },
78 Destructiveness::Safe,
79 );
80 }
81 for qname in &drift.invalid_indexes {
82 out.push(
83 Change::RecreateIndex {
84 qname: qname.clone(),
85 },
86 Destructiveness::Safe,
87 );
88 }
89
90 schemas::diff_schemas(target, source, &mut out, &managed_roles);
91 extensions::diff_extensions(&target.extensions, &source.extensions, &mut out);
92 tables::diff_tables(target, source, &mut out, &managed_roles);
93 indexes::diff_indexes(target, source, &mut out);
94 sequences::diff_sequences(target, source, &mut out, &managed_roles);
95 views::diff_views(&target.views, &source.views, &mut out, &managed_roles);
96 views::diff_materialized_views(
97 &target.materialized_views,
98 &source.materialized_views,
99 &mut out,
100 &managed_roles,
101 );
102 types::diff_user_types(&target.types, &source.types, &mut out, &managed_roles);
103 routines::diff_functions(
104 &target.functions,
105 &source.functions,
106 &mut out,
107 &managed_roles,
108 );
109 routines::diff_procedures(
110 &target.procedures,
111 &source.procedures,
112 &mut out,
113 &managed_roles,
114 );
115 triggers::diff_triggers(&target.triggers, &source.triggers, &mut out);
116 publications::diff_publications(target, source, &mut out);
117
118 let dp_changes = default_privileges::diff_default_privileges(
120 &target.default_privileges,
121 &source.default_privileges,
122 &managed_roles,
123 );
124 for dp in dp_changes {
125 out.push(
126 Change::AlterDefaultPrivileges {
127 target_role: dp.target_role,
128 schema: dp.schema,
129 object_type: dp.object_type,
130 is_grant: dp.is_grant,
131 grant: dp.grant,
132 },
133 Destructiveness::Safe,
134 );
135 }
136
137 out
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::catalog::DriftReport;
144 use crate::identifier::{Identifier, QualifiedName};
145 use crate::ir::column::Column;
146 use crate::ir::column_type::ColumnType;
147 use crate::ir::constraint::{
148 Constraint, ConstraintKind, Deferrable, FkMatchType, ForeignKey, ReferentialAction,
149 };
150 use crate::ir::index::{
151 Index, IndexColumn, IndexColumnExpr, IndexMethod, IndexParent, NullsOrder, SortOrder,
152 };
153 use crate::ir::schema::Schema;
154 use crate::ir::sequence::Sequence;
155 use crate::ir::table::Table;
156
157 fn id(s: &str) -> Identifier {
158 Identifier::from_unquoted(s).unwrap()
159 }
160
161 fn qn(schema: &str, name: &str) -> QualifiedName {
162 QualifiedName::new(id(schema), id(name))
163 }
164
165 fn col(name: &str, ty: ColumnType, nullable: bool) -> Column {
166 Column {
167 name: id(name),
168 ty,
169 nullable,
170 default: None,
171 identity: None,
172 generated: None,
173 collation: None,
174 storage: None,
175 compression: None,
176 comment: None,
177 }
178 }
179
180 fn catalog_empty() -> Catalog {
181 Catalog::empty()
182 }
183
184 fn catalog_with_one_table() -> Catalog {
185 let mut c = Catalog::empty();
186 c.schemas.push(Schema::new(id("app")));
187 c.tables.push(Table {
188 qname: qn("app", "users"),
189 columns: vec![col("id", ColumnType::BigInt, false)],
190 constraints: vec![Constraint {
191 qname: qn("app", "users_pkey"),
192 kind: ConstraintKind::PrimaryKey {
193 columns: vec![id("id")],
194 include: vec![],
195 },
196 deferrable: Deferrable::NotDeferrable,
197 comment: None,
198 }],
199 partition_by: None,
200 partition_of: None,
201 comment: Some("user accounts".into()),
202 owner: None,
203 grants: vec![],
204 rls_enabled: false,
205 rls_forced: false,
206 policies: vec![],
207 storage: crate::ir::reloptions::TableStorageOptions::default(),
208 });
209 c
210 }
211
212 fn catalog_with_indexes_and_fks() -> Catalog {
213 let mut c = Catalog::empty();
214 c.schemas.push(Schema::new(id("app")));
215 c.tables.push(Table {
216 qname: qn("app", "orgs"),
217 columns: vec![col("id", ColumnType::BigInt, false)],
218 constraints: vec![Constraint {
219 qname: qn("app", "orgs_pkey"),
220 kind: ConstraintKind::PrimaryKey {
221 columns: vec![id("id")],
222 include: vec![],
223 },
224 deferrable: Deferrable::NotDeferrable,
225 comment: None,
226 }],
227 partition_by: None,
228 partition_of: None,
229 comment: None,
230 owner: None,
231 grants: vec![],
232 rls_enabled: false,
233 rls_forced: false,
234 policies: vec![],
235 storage: crate::ir::reloptions::TableStorageOptions::default(),
236 });
237 c.tables.push(Table {
238 qname: qn("app", "users"),
239 columns: vec![
240 col("id", ColumnType::BigInt, false),
241 col("org_id", ColumnType::BigInt, false),
242 col("email", ColumnType::Varchar { len: Some(255) }, true),
243 ],
244 constraints: vec![
245 Constraint {
246 qname: qn("app", "users_pkey"),
247 kind: ConstraintKind::PrimaryKey {
248 columns: vec![id("id")],
249 include: vec![],
250 },
251 deferrable: Deferrable::NotDeferrable,
252 comment: None,
253 },
254 Constraint {
255 qname: qn("app", "users_org_fkey"),
256 kind: ConstraintKind::ForeignKey(ForeignKey {
257 columns: vec![id("org_id")],
258 referenced_table: qn("app", "orgs"),
259 referenced_columns: vec![id("id")],
260 on_update: ReferentialAction::NoAction,
261 on_delete: ReferentialAction::Cascade,
262 match_type: FkMatchType::Simple,
263 }),
264 deferrable: Deferrable::NotDeferrable,
265 comment: None,
266 },
267 ],
268 partition_by: None,
269 partition_of: None,
270 comment: None,
271 owner: None,
272 grants: vec![],
273 rls_enabled: false,
274 rls_forced: false,
275 policies: vec![],
276 storage: crate::ir::reloptions::TableStorageOptions::default(),
277 });
278 c.indexes.push(Index {
279 qname: qn("app", "users_email_idx"),
280 on: IndexParent::Table(qn("app", "users")),
281 method: IndexMethod::BTree,
282 columns: vec![IndexColumn {
283 expr: IndexColumnExpr::Column(id("email")),
284 collation: None,
285 opclass: None,
286 sort_order: SortOrder::Asc,
287 nulls_order: NullsOrder::NullsLast,
288 }],
289 include: vec![],
290 unique: true,
291 nulls_not_distinct: false,
292 predicate: None,
293 tablespace: None,
294 comment: None,
295 storage: crate::ir::reloptions::IndexStorageOptions::default(),
296 });
297 c.sequences.push(Sequence {
298 qname: qn("app", "global_counter"),
299 data_type: ColumnType::BigInt,
300 start: 1,
301 increment: 1,
302 min_value: None,
303 max_value: None,
304 cache: 1,
305 cycle: false,
306 owned_by: None,
307 comment: None,
308 owner: None,
309 grants: vec![],
310 });
311 c
312 }
313
314 #[test]
315 fn diff_against_empty_self_is_empty() {
316 let c = Catalog::empty();
317 assert!(diff(&c, &c, &DriftReport::default()).is_empty());
318 }
319
320 #[test]
321 fn diff_against_single_table_self_is_empty() {
322 assert!(
323 diff(
324 &catalog_with_one_table(),
325 &catalog_with_one_table(),
326 &DriftReport::default()
327 )
328 .is_empty()
329 );
330 }
331
332 #[test]
335 fn diff_against_self_is_empty() {
336 let catalogs = vec![
337 catalog_empty(),
338 catalog_with_one_table(),
339 catalog_with_indexes_and_fks(),
340 ];
341 for c in &catalogs {
342 assert!(
343 diff(c, c, &DriftReport::default()).is_empty(),
344 "diff(c, c) was not empty for catalog: {c:?}"
345 );
346 }
347 }
348}