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