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