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