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