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