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