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