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