pgevolve_core/diff/change.rs
1//! `Change` — one entry in a [`ChangeSet`](super::changeset::ChangeSet).
2//!
3//! A `Change` describes a structural difference between a target catalog and
4//! a source catalog at the level of a top-level object (schema, table, index,
5//! sequence). Per-column / per-constraint operations live inside the
6//! [`AlterTable`](Change::AlterTable) variant as a list of
7//! [`TableOpEntry`]; per-field sequence updates
8//! live inside [`AlterSequence`](Change::AlterSequence) as
9//! [`SequenceOpEntry`].
10
11use serde::{Deserialize, Serialize};
12
13use crate::identifier::{Identifier, QualifiedName};
14use crate::ir::column_type::ColumnType;
15use crate::ir::default_expr::NormalizedExpr;
16use crate::ir::default_privileges::DefaultPrivObjectType;
17use crate::ir::extension::Extension;
18use crate::ir::function::{Function, NormalizedArgTypes};
19use crate::ir::grant::{Grant, GrantTarget};
20use crate::ir::index::Index;
21use crate::ir::partition::PartitionBounds;
22use crate::ir::procedure::Procedure;
23use crate::ir::schema::Schema;
24use crate::ir::sequence::Sequence;
25use crate::ir::table::Table;
26use crate::ir::trigger::Trigger;
27use crate::ir::user_type::{CompositeAttribute, DomainCheck, UserType};
28use crate::ir::view::{MaterializedView, View};
29
30use super::destructiveness::Destructiveness;
31use super::owner_op::{AlterObjectOwner, OwnerObjectKind};
32use super::sequence_op::SequenceOpEntry;
33use super::table_op::TableOpEntry;
34
35/// A change paired with its destructiveness classification.
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37pub struct ChangeEntry {
38 /// The structural change.
39 pub change: Change,
40 /// Risk classification for this change.
41 pub destructiveness: Destructiveness,
42}
43
44/// One structural change between two catalogs.
45///
46/// `Change` is intentionally not boxed: the enum's footprint is dominated by
47/// `CreateTable` / `CreateIndex` / `ReplaceIndex` payloads, but `ChangeSet`
48/// only stores at most one of each per object, so `Vec` overhead is the
49/// dominant cost and boxing would just add an extra allocation per entry.
50#[allow(clippy::large_enum_variant)]
51#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
52#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
53pub enum Change {
54 /// Add a schema.
55 CreateSchema(Schema),
56 /// Drop a schema by name.
57 DropSchema(Identifier),
58 /// Update schema metadata (only `comment` in v0.1).
59 AlterSchema {
60 /// Schema name.
61 name: Identifier,
62 /// New comment value (`None` clears the comment).
63 comment: Option<String>,
64 },
65
66 /// Add a table.
67 CreateTable(Table),
68 /// Drop a table.
69 DropTable {
70 /// Table qname.
71 qname: QualifiedName,
72 /// Best-effort estimate of row count, populated by the planner from
73 /// `pg_class.reltuples`. The differ leaves this `None`.
74 row_count_estimate: Option<i64>,
75 },
76 /// Alter a table — column / constraint / comment operations.
77 AlterTable {
78 /// Target table qname.
79 qname: QualifiedName,
80 /// Per-table operations. Order is the planner's job; the differ emits
81 /// these in arbitrary order.
82 ops: Vec<TableOpEntry>,
83 },
84
85 /// Create an index.
86 CreateIndex(Index),
87 /// Drop an index.
88 DropIndex(QualifiedName),
89 /// Replace an index (DROP + CREATE) when a property change requires it.
90 ReplaceIndex {
91 /// The index as it exists in the target.
92 from: Index,
93 /// The index as it should exist in the source.
94 to: Index,
95 },
96
97 /// Create a sequence.
98 CreateSequence(Sequence),
99 /// Drop a sequence.
100 DropSequence(QualifiedName),
101 /// Alter a sequence — per-field operations.
102 AlterSequence {
103 /// Target sequence qname.
104 qname: QualifiedName,
105 /// Per-sequence operations.
106 ops: Vec<SequenceOpEntry>,
107 },
108
109 /// A constraint exists in the catalog but is `NOT VALID`
110 /// (`pg_constraint.convalidated = false`). Planner emits
111 /// `VALIDATE CONSTRAINT`. Non-destructive.
112 ValidateConstraint {
113 /// Qualified name of the table owning the constraint.
114 table: QualifiedName,
115 /// Constraint name.
116 constraint: Identifier,
117 },
118 /// An index exists in the catalog but is `INVALID`
119 /// (`pg_index.indisvalid = false`). Planner emits `DROP INDEX + CREATE
120 /// INDEX` to rebuild it. Non-destructive.
121 RecreateIndex {
122 /// Qualified name of the invalid index.
123 qname: QualifiedName,
124 },
125
126 /// A view-level change.
127 View(ViewChange),
128 /// A materialized-view-level change.
129 Mv(MvChange),
130 /// A user-defined type change (enum, domain, composite).
131 UserType(UserTypeChange),
132 /// A user-defined function change.
133 Function(FunctionChange),
134 /// A user-defined procedure change.
135 Procedure(ProcedureChange),
136 /// An extension change.
137 Extension(ExtensionChange),
138 /// A trigger change.
139 Trigger(TriggerChange),
140 /// A partition-membership change (ATTACH / DETACH PARTITION).
141 Table(TableChange),
142 /// Grant an object-level privilege on a grantable object (non-column).
143 ///
144 /// Emitted when a grant appears in source but not in the target catalog.
145 GrantObjectPrivilege {
146 /// Qualified name of the grantable object (schema, table, sequence,
147 /// view, function, procedure, or type).
148 qname: QualifiedName,
149 /// Which kind of object this is (drives the SQL keyword in the renderer).
150 kind: OwnerObjectKind,
151 /// Argument signature for routines (e.g., `"(int, text)"`).
152 /// Empty string for non-routine object kinds.
153 #[serde(default)]
154 signature: String,
155 /// The full grant to apply.
156 grant: Grant,
157 },
158 /// Revoke an object-level privilege from a grantable object (non-column).
159 ///
160 /// Only emitted for managed grantees (see [`super::grants::diff_grants`]).
161 RevokeObjectPrivilege {
162 /// Qualified name of the grantable object.
163 qname: QualifiedName,
164 /// Which kind of object this is.
165 kind: OwnerObjectKind,
166 /// Argument signature for routines (e.g., `"(int, text)"`).
167 /// Empty string for non-routine object kinds.
168 #[serde(default)]
169 signature: String,
170 /// The full grant to revoke.
171 grant: Grant,
172 },
173 /// Grant a column-level privilege on a table, view, or materialized view.
174 ///
175 /// Emitted when the grant's `columns` field is `Some(_)`.
176 GrantColumnPrivilege {
177 /// Qualified name of the table / view / materialized view.
178 qname: QualifiedName,
179 /// The full grant (including the `columns` list).
180 grant: Grant,
181 },
182 /// Revoke a column-level privilege from a table, view, or materialized view.
183 ///
184 /// Only emitted for managed grantees.
185 RevokeColumnPrivilege {
186 /// Qualified name of the table / view / materialized view.
187 qname: QualifiedName,
188 /// The full grant (including the `columns` list).
189 grant: Grant,
190 },
191 /// Change the owner of a grantable object.
192 ///
193 /// Emitted when the source declares an owner (`owner: Some(_)`) and the
194 /// target owner differs. When the source has `owner: None`, ownership is
195 /// unmanaged and no change is emitted.
196 AlterObjectOwner(AlterObjectOwner),
197 /// Add or remove a default privilege for a `(FOR ROLE, IN SCHEMA?,
198 /// object-type)` key.
199 AlterDefaultPrivileges {
200 /// `FOR ROLE x` — the grantor role.
201 target_role: Identifier,
202 /// `IN SCHEMA y` — scope. `None` = global.
203 schema: Option<Identifier>,
204 /// Object-type discriminant.
205 object_type: DefaultPrivObjectType,
206 /// `true` = GRANT step, `false` = REVOKE step.
207 is_grant: bool,
208 /// The grantee and privilege being adjusted.
209 grant: Grant,
210 },
211
212 /// Create a new policy on the named table.
213 CreatePolicy {
214 /// Table the policy belongs to.
215 table: QualifiedName,
216 /// The policy to create.
217 policy: crate::ir::policy::Policy,
218 },
219 /// Drop a policy from the named table.
220 DropPolicy {
221 /// Table the policy belongs to.
222 table: QualifiedName,
223 /// Name of the policy to drop.
224 name: Identifier,
225 },
226 /// Alter a policy's roles / USING / WITH CHECK.
227 ///
228 /// Note: PG rejects `ALTER POLICY` when the command kind changes — the
229 /// differ emits `DropPolicy` + `CreatePolicy` in that case instead.
230 AlterPolicy {
231 /// Table the policy belongs to.
232 table: QualifiedName,
233 /// The desired policy state.
234 policy: crate::ir::policy::Policy,
235 },
236 /// Toggle a table's `ROW LEVEL SECURITY`.
237 SetTableRowSecurity {
238 /// Qualified name of the table.
239 qname: QualifiedName,
240 /// `true` = `ENABLE ROW LEVEL SECURITY`, `false` = `DISABLE`.
241 enable: bool,
242 },
243 /// Toggle a table's `FORCE ROW LEVEL SECURITY`.
244 SetTableForceRowSecurity {
245 /// Qualified name of the table.
246 qname: QualifiedName,
247 /// `true` = `FORCE ROW LEVEL SECURITY`, `false` = `NO FORCE`.
248 force: bool,
249 },
250
251 /// Set table storage reloptions. `options` carries the sparse delta — only
252 /// the fields whose source value differs from the catalog.
253 ///
254 /// No `Reset*` variant: the lenient policy treats source `None` as "skip".
255 SetTableStorage {
256 /// Qualified name of the table.
257 qname: QualifiedName,
258 /// Sparse delta of options to apply.
259 options: crate::ir::reloptions::TableStorageOptions,
260 },
261 /// Set index storage reloptions. Sparse delta.
262 ///
263 /// No `Reset*` variant: the lenient policy treats source `None` as "skip".
264 SetIndexStorage {
265 /// Qualified name of the index.
266 qname: QualifiedName,
267 /// Sparse delta of options to apply.
268 options: crate::ir::reloptions::IndexStorageOptions,
269 },
270 /// Set materialized view storage reloptions. Sparse delta.
271 ///
272 /// No `Reset*` variant: the lenient policy treats source `None` as "skip".
273 SetMaterializedViewStorage {
274 /// Qualified name of the materialized view.
275 qname: QualifiedName,
276 /// Sparse delta of options to apply.
277 options: crate::ir::reloptions::TableStorageOptions,
278 },
279
280 /// `CREATE PUBLICATION ...`
281 CreatePublication(crate::ir::publication::Publication),
282 /// `DROP PUBLICATION ...` — destructive.
283 DropPublication {
284 /// Publication name.
285 name: crate::identifier::Identifier,
286 },
287 /// `DROP PUBLICATION old; CREATE PUBLICATION new;` — destructive; used
288 /// when the publication's scope mode switches (`AllTables` ↔ `Selective`).
289 ReplacePublication {
290 /// The publication as it exists in the target.
291 from: crate::ir::publication::Publication,
292 /// The publication as it should exist in the source.
293 to: crate::ir::publication::Publication,
294 },
295 /// `ALTER PUBLICATION p ADD TABLE x [(cols)] [WHERE (filter)]`
296 AlterPublicationAddTable {
297 /// Publication name.
298 publication: crate::identifier::Identifier,
299 /// The table entry to add.
300 table: crate::ir::publication::PublishedTable,
301 },
302 /// `ALTER PUBLICATION p DROP TABLE x`
303 AlterPublicationDropTable {
304 /// Publication name.
305 publication: crate::identifier::Identifier,
306 /// Qualified name of the table to drop.
307 qname: crate::identifier::QualifiedName,
308 },
309 /// `ALTER PUBLICATION p SET TABLE x (cols) WHERE (filter)`
310 AlterPublicationSetTable {
311 /// Publication name.
312 publication: crate::identifier::Identifier,
313 /// The desired table entry state.
314 table: crate::ir::publication::PublishedTable,
315 },
316 /// `ALTER PUBLICATION p ADD TABLES IN SCHEMA s` (PG15+)
317 AlterPublicationAddSchema {
318 /// Publication name.
319 publication: crate::identifier::Identifier,
320 /// Schema to add.
321 schema: crate::identifier::Identifier,
322 },
323 /// `ALTER PUBLICATION p DROP TABLES IN SCHEMA s` (PG15+)
324 AlterPublicationDropSchema {
325 /// Publication name.
326 publication: crate::identifier::Identifier,
327 /// Schema to drop.
328 schema: crate::identifier::Identifier,
329 },
330 /// `ALTER PUBLICATION p SET (publish = '...')`
331 AlterPublicationSetPublish {
332 /// Publication name.
333 publication: crate::identifier::Identifier,
334 /// Desired publish-kinds bitset.
335 kinds: crate::ir::publication::PublishKinds,
336 },
337 /// `ALTER PUBLICATION p SET (publish_via_partition_root = ...)`
338 AlterPublicationSetViaRoot {
339 /// Publication name.
340 publication: crate::identifier::Identifier,
341 /// Desired value.
342 value: bool,
343 },
344 /// `COMMENT ON PUBLICATION p IS '...'`
345 CommentOnPublication {
346 /// Publication name.
347 name: crate::identifier::Identifier,
348 /// New comment value (`None` clears the comment).
349 comment: Option<String>,
350 },
351
352 /// `CREATE SUBSCRIPTION ...`
353 CreateSubscription(crate::ir::subscription::Subscription),
354 /// `DROP SUBSCRIPTION ...` — destructive.
355 DropSubscription {
356 /// Subscription name.
357 name: crate::identifier::Identifier,
358 },
359 /// `ALTER SUBSCRIPTION s CONNECTION '...'`
360 AlterSubscriptionConnection {
361 /// Subscription name.
362 name: crate::identifier::Identifier,
363 /// New connection string (may contain `${VAR}` placeholders).
364 new_connection: String,
365 },
366 /// `ALTER SUBSCRIPTION s ADD PUBLICATION p`
367 AlterSubscriptionAddPublication {
368 /// Subscription name.
369 name: crate::identifier::Identifier,
370 /// Publication to add.
371 publication: crate::identifier::Identifier,
372 },
373 /// `ALTER SUBSCRIPTION s DROP PUBLICATION p`
374 AlterSubscriptionDropPublication {
375 /// Subscription name.
376 name: crate::identifier::Identifier,
377 /// Publication to drop.
378 publication: crate::identifier::Identifier,
379 },
380 /// Reserved: pgevolve never emits this (granular ADD/DROP only), but
381 /// the parser accepts source `ALTER SUBSCRIPTION s SET PUBLICATION …` for
382 /// normalizing into the IR's publications field. The variant exists so
383 /// `kind_name` / `parse_kind_name` round-trip every legal `StepKind` name.
384 AlterSubscriptionSetPublication {
385 /// Subscription name.
386 name: crate::identifier::Identifier,
387 /// Publication list.
388 publications: Vec<crate::identifier::Identifier>,
389 },
390 /// `ALTER SUBSCRIPTION s SET (option = value, ...)` — sparse-delta.
391 ///
392 /// `create_slot` and `copy_data` are NEVER included (CREATE-only PG options).
393 AlterSubscriptionSetOptions {
394 /// Subscription name.
395 name: crate::identifier::Identifier,
396 /// Sparse options delta — only changed fields are `Some`.
397 options: crate::ir::subscription::SubscriptionOptions,
398 },
399 /// `COMMENT ON SUBSCRIPTION s IS '...'`
400 CommentOnSubscription {
401 /// Subscription name.
402 name: crate::identifier::Identifier,
403 /// New comment value (`None` clears the comment).
404 comment: Option<String>,
405 },
406
407 /// A change that cannot be performed in-place.
408 ///
409 /// Emitted by the differ when it detects a structural difference that has
410 /// no safe automatic migration path (e.g., changing a table's `PARTITION BY`
411 /// clause). The ordering phase converts this into a [`PlanError`] so the
412 /// plan never reaches execution.
413 ///
414 /// [`PlanError`]: crate::plan::error::PlanError
415 UnsupportedDiff {
416 /// Human-readable explanation of what changed and why it cannot be
417 /// performed in-place.
418 reason: String,
419 },
420}
421
422// Silence the unused-import lint — OwnerObjectKind and GrantTarget are used in
423// the variants above but Rust's lint fires on the `use` line when all uses are
424// inside the enum definition.
425const _: () = {
426 let _ = core::mem::size_of::<OwnerObjectKind>();
427 let _ = core::mem::size_of::<GrantTarget>();
428};
429
430/// A structural change to a single user-defined type.
431#[allow(clippy::large_enum_variant)]
432#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
433#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
434pub enum UserTypeChange {
435 /// Create a new user-defined type.
436 Create(UserType),
437 /// Drop a user-defined type by qualified name.
438 Drop(QualifiedName),
439
440 /// Add a new label to an existing enum type.
441 EnumAddValue {
442 /// Qualified name of the enum type.
443 qname: QualifiedName,
444 /// The new label string.
445 value: String,
446 /// If `Some`, the new value is placed immediately before this label.
447 before: Option<String>,
448 /// If `Some`, the new value is placed immediately after this label.
449 after: Option<String>,
450 },
451 /// Rename an existing enum label.
452 EnumRenameValue {
453 /// Qualified name of the enum type.
454 qname: QualifiedName,
455 /// The existing label to rename.
456 from: String,
457 /// The new label name.
458 to: String,
459 },
460
461 /// Add a CHECK constraint to a domain.
462 DomainAddCheck {
463 /// Qualified name of the domain type.
464 qname: QualifiedName,
465 /// The constraint to add.
466 constraint: DomainCheck,
467 },
468 /// Drop a named CHECK constraint from a domain.
469 DomainDropCheck {
470 /// Qualified name of the domain type.
471 qname: QualifiedName,
472 /// The constraint name to drop.
473 name: Identifier,
474 },
475 /// Set (or clear) the DEFAULT expression on a domain.
476 DomainSetDefault {
477 /// Qualified name of the domain type.
478 qname: QualifiedName,
479 /// New default expression (`None` clears the default).
480 default: Option<NormalizedExpr>,
481 },
482 /// Toggle the `NOT NULL` constraint on a domain.
483 DomainSetNotNull {
484 /// Qualified name of the domain type.
485 qname: QualifiedName,
486 /// `true` means NOT NULL (i.e., `nullable = false`).
487 not_null: bool,
488 },
489
490 /// Add a new attribute to a composite type.
491 CompositeAddAttribute {
492 /// Qualified name of the composite type.
493 qname: QualifiedName,
494 /// The attribute to add.
495 attribute: CompositeAttribute,
496 },
497 /// Drop an attribute from a composite type.
498 CompositeDropAttribute {
499 /// Qualified name of the composite type.
500 qname: QualifiedName,
501 /// The attribute name to drop.
502 name: Identifier,
503 },
504 /// Change the type of an existing composite attribute.
505 CompositeAlterAttributeType {
506 /// Qualified name of the composite type.
507 qname: QualifiedName,
508 /// The attribute name whose type is being changed.
509 attribute: Identifier,
510 /// The new column type.
511 new_type: ColumnType,
512 },
513
514 /// Set (or clear) the `COMMENT ON TYPE` for this type.
515 SetComment {
516 /// Qualified name of the type.
517 qname: QualifiedName,
518 /// New comment (`None` clears the comment).
519 comment: Option<String>,
520 },
521
522 /// Emitted when the requested change cannot be done in place via `ALTER`.
523 ///
524 /// T8's cascade walker appends `ReplaceBody` for any views/MVs that depend
525 /// on the type so they are recreated after the type is rebuilt. T9's SQL
526 /// emitter expands this single entry into `DROP TYPE … CASCADE` plus
527 /// `CREATE TYPE`, then the recreated dependents follow in topological
528 /// order. The planner never emits this entry as one raw SQL step.
529 ReplaceWithCascade {
530 /// The desired type (source).
531 source: UserType,
532 /// The existing type (catalog / live database).
533 catalog: UserType,
534 },
535}
536
537/// A structural change to a single view.
538#[allow(clippy::large_enum_variant)]
539#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
540#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
541pub enum ViewChange {
542 /// Create a new view.
543 Create(View),
544 /// Drop an existing view.
545 Drop(QualifiedName),
546 /// Replace the SELECT body of a view.
547 ///
548 /// `compatible` is `true` when Postgres's `CREATE OR REPLACE VIEW` rules
549 /// are satisfied (same column names and types at the same indexes, with
550 /// new columns only appended at the end). When `compatible` is `false`,
551 /// the planner must `DROP` then re-`CREATE` the view (and rebuild its
552 /// dependents).
553 ReplaceBody {
554 /// The view as it should exist (source SQL).
555 source: View,
556 /// The view as it currently exists (live catalog).
557 catalog: View,
558 /// Whether `CREATE OR REPLACE VIEW` can be used (`true`) or a
559 /// `DROP` + `CREATE` cycle is required (`false`).
560 compatible: bool,
561 },
562 /// Change `WITH (security_barrier = ..., security_invoker = ...)` reloptions.
563 SetReloption {
564 /// View qname.
565 qname: QualifiedName,
566 /// Desired `security_barrier` value (`None` clears the option).
567 security_barrier: Option<bool>,
568 /// Desired `security_invoker` value (`None` clears the option).
569 security_invoker: Option<bool>,
570 },
571 /// Set (or clear) the view-level `COMMENT ON VIEW`.
572 SetComment {
573 /// View qname.
574 qname: QualifiedName,
575 /// New comment (`None` clears the comment).
576 comment: Option<String>,
577 },
578 /// Set (or clear) a `COMMENT ON COLUMN view.col`.
579 SetColumnComment {
580 /// View qname.
581 qname: QualifiedName,
582 /// Column name.
583 column: Identifier,
584 /// New comment (`None` clears the comment).
585 comment: Option<String>,
586 },
587}
588
589/// A structural change to a single materialized view.
590#[allow(clippy::large_enum_variant)]
591#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
592#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
593pub enum MvChange {
594 /// Create a new materialized view.
595 Create(MaterializedView),
596 /// Drop an existing materialized view.
597 ///
598 /// Unlike `ViewChange::Drop`, this is classified as `Safe` because
599 /// materialized views are derived data that can be recreated by refreshing.
600 Drop(QualifiedName),
601 /// Replace the SELECT body of a materialized view.
602 ///
603 /// Materialized views do not support `CREATE OR REPLACE`; the planner must
604 /// always `DROP` then `CREATE` the MV (and rebuild any indexes on it).
605 ReplaceBody {
606 /// The MV as it should exist (source SQL).
607 source: MaterializedView,
608 /// The MV as it currently exists (live catalog).
609 catalog: MaterializedView,
610 },
611 /// Set (or clear) the MV-level `COMMENT ON MATERIALIZED VIEW`.
612 SetComment {
613 /// MV qname.
614 qname: QualifiedName,
615 /// New comment (`None` clears the comment).
616 comment: Option<String>,
617 },
618 /// Set (or clear) a `COMMENT ON COLUMN mv.col`.
619 SetColumnComment {
620 /// MV qname.
621 qname: QualifiedName,
622 /// Column name.
623 column: Identifier,
624 /// New comment (`None` clears the comment).
625 comment: Option<String>,
626 },
627}
628
629/// A structural change to a single user-defined function.
630#[allow(clippy::large_enum_variant)]
631#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
632#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
633pub enum FunctionChange {
634 /// Create a new function.
635 Create(Function),
636 /// Drop an existing function by qualified name and normalized arg types.
637 Drop {
638 /// Qualified name of the function.
639 qname: QualifiedName,
640 /// Normalized argument types (IN/INOUT/VARIADIC only) used to
641 /// disambiguate overloads in the DROP FUNCTION statement.
642 args: NormalizedArgTypes,
643 },
644 /// Replace the function body and/or attributes using `CREATE OR REPLACE
645 /// FUNCTION`. Only valid when `function_can_or_replace` returns `true`.
646 CreateOrReplace(Function),
647 /// The function's return type or language changed in a way that PG's
648 /// `CREATE OR REPLACE FUNCTION` rejects. The planner must emit
649 /// `DROP FUNCTION … CASCADE` followed by `CREATE FUNCTION`.
650 ReplaceWithCascade {
651 /// The desired function (source).
652 source: Function,
653 /// The existing function (catalog / live database).
654 catalog: Function,
655 },
656 /// Set (or clear) `COMMENT ON FUNCTION`.
657 SetComment {
658 /// Qualified name of the function.
659 qname: QualifiedName,
660 /// Normalized argument types for disambiguation.
661 args: NormalizedArgTypes,
662 /// New comment (`None` clears the comment).
663 comment: Option<String>,
664 },
665}
666
667/// A structural change to a single user-defined procedure.
668#[allow(clippy::large_enum_variant)]
669#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
670#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
671pub enum ProcedureChange {
672 /// Create a new procedure.
673 Create(Procedure),
674 /// Drop an existing procedure by qualified name.
675 Drop(QualifiedName),
676 /// Replace the procedure body and/or attributes using `CREATE OR REPLACE
677 /// PROCEDURE`.
678 CreateOrReplace(Procedure),
679 /// Set (or clear) `COMMENT ON PROCEDURE`.
680 SetComment {
681 /// Qualified name of the procedure.
682 qname: QualifiedName,
683 /// New comment (`None` clears the comment).
684 comment: Option<String>,
685 },
686}
687
688/// Change to one extension. Pair-by-name semantics.
689#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
690#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
691pub enum ExtensionChange {
692 /// Install a new extension.
693 Create(Extension),
694 /// Drop an extension by name. Emits `DROP EXTENSION ... CASCADE`.
695 Drop(Identifier),
696 /// Bump extension version: `ALTER EXTENSION ... UPDATE TO 'v'`.
697 AlterUpdate {
698 /// Extension name.
699 name: Identifier,
700 /// New version to update to.
701 to_version: String,
702 },
703 /// Schema-changing replace (DROP CASCADE + CREATE).
704 ReplaceWithCascade(Extension),
705 /// Change the `COMMENT ON EXTENSION` text.
706 CommentOn {
707 /// Extension name.
708 name: Identifier,
709 /// New comment value (`None` clears the comment).
710 comment: Option<String>,
711 },
712}
713
714/// Change to one trigger. Pair-by-qname semantics; any structural
715/// difference emits `Replace`.
716#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
717#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
718pub enum TriggerChange {
719 /// Install a new trigger.
720 Create(Trigger),
721 /// Drop a trigger by qname (needs the table for `DROP TRIGGER name ON table`).
722 Drop {
723 /// Qualified name of the trigger.
724 qname: QualifiedName,
725 /// Owning table (needed for `DROP TRIGGER name ON table`).
726 table: QualifiedName,
727 },
728 /// Any structural change: drop + create.
729 Replace(Trigger),
730 /// Change the `COMMENT ON TRIGGER` text.
731 CommentOn {
732 /// Qualified name of the trigger.
733 qname: QualifiedName,
734 /// Owning table (needed for `COMMENT ON TRIGGER name ON table`).
735 table: QualifiedName,
736 /// New comment value (`None` clears the comment).
737 comment: Option<String>,
738 },
739}
740
741/// A partition-membership change on a child table.
742///
743/// These changes are emitted by the differ when the `partition_of` field of a
744/// table changes. Wiring to SQL steps lands in PART9.
745#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
746#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
747pub enum TableChange {
748 /// Attach a child table to a parent partitioned table.
749 ///
750 /// Corresponds to `ALTER TABLE <parent> ATTACH PARTITION <child> <bounds>`.
751 AttachPartition {
752 /// The partitioned parent table.
753 parent: QualifiedName,
754 /// The child table being attached.
755 child: QualifiedName,
756 /// The partition bounds clause.
757 bounds: PartitionBounds,
758 },
759 /// Detach a child table from a parent partitioned table.
760 ///
761 /// Corresponds to `ALTER TABLE <parent> DETACH PARTITION <child>`.
762 DetachPartition {
763 /// The partitioned parent table.
764 parent: QualifiedName,
765 /// The child table being detached.
766 child: QualifiedName,
767 },
768}
769
770#[cfg(test)]
771mod tests {
772 use super::*;
773 use crate::identifier::Identifier;
774 use crate::ir::column::Column;
775 use crate::ir::column_type::ColumnType;
776
777 fn id(s: &str) -> Identifier {
778 Identifier::from_unquoted(s).unwrap()
779 }
780
781 fn qn(schema: &str, name: &str) -> QualifiedName {
782 QualifiedName::new(id(schema), id(name))
783 }
784
785 fn table_users() -> Table {
786 Table {
787 qname: qn("app", "users"),
788 columns: vec![Column {
789 name: id("id"),
790 ty: ColumnType::BigInt,
791 nullable: false,
792 default: None,
793 identity: None,
794 generated: None,
795 collation: None,
796 storage: None,
797 compression: None,
798 comment: None,
799 }],
800 constraints: vec![],
801 partition_by: None,
802 partition_of: None,
803 comment: None,
804 owner: None,
805 grants: vec![],
806 rls_enabled: false,
807 rls_forced: false,
808 policies: vec![],
809 storage: crate::ir::reloptions::TableStorageOptions::default(),
810 }
811 }
812
813 #[test]
814 fn create_table_entry_serde_round_trip() {
815 let entry = ChangeEntry {
816 change: Change::CreateTable(table_users()),
817 destructiveness: Destructiveness::Safe,
818 };
819 let json = serde_json::to_string(&entry).unwrap();
820 let back: ChangeEntry = serde_json::from_str(&json).unwrap();
821 assert_eq!(entry, back);
822 }
823
824 #[test]
825 fn drop_table_entry_serde_round_trip() {
826 let entry = ChangeEntry {
827 change: Change::DropTable {
828 qname: qn("app", "users"),
829 row_count_estimate: None,
830 },
831 destructiveness: Destructiveness::RequiresApprovalAndDataLossWarning {
832 reason: "drops table app.users".into(),
833 },
834 };
835 let json = serde_json::to_string(&entry).unwrap();
836 let back: ChangeEntry = serde_json::from_str(&json).unwrap();
837 assert_eq!(entry, back);
838 }
839
840 #[test]
841 fn equal_create_table_changes_compare_equal() {
842 let a = Change::CreateTable(table_users());
843 let b = Change::CreateTable(table_users());
844 assert_eq!(a, b);
845 }
846}