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 /// A change that cannot be performed in-place.
353 ///
354 /// Emitted by the differ when it detects a structural difference that has
355 /// no safe automatic migration path (e.g., changing a table's `PARTITION BY`
356 /// clause). The ordering phase converts this into a [`PlanError`] so the
357 /// plan never reaches execution.
358 ///
359 /// [`PlanError`]: crate::plan::error::PlanError
360 UnsupportedDiff {
361 /// Human-readable explanation of what changed and why it cannot be
362 /// performed in-place.
363 reason: String,
364 },
365}
366
367// Silence the unused-import lint — OwnerObjectKind and GrantTarget are used in
368// the variants above but Rust's lint fires on the `use` line when all uses are
369// inside the enum definition.
370const _: () = {
371 let _ = core::mem::size_of::<OwnerObjectKind>();
372 let _ = core::mem::size_of::<GrantTarget>();
373};
374
375/// A structural change to a single user-defined type.
376#[allow(clippy::large_enum_variant)]
377#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
378#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
379pub enum UserTypeChange {
380 /// Create a new user-defined type.
381 Create(UserType),
382 /// Drop a user-defined type by qualified name.
383 Drop(QualifiedName),
384
385 /// Add a new label to an existing enum type.
386 EnumAddValue {
387 /// Qualified name of the enum type.
388 qname: QualifiedName,
389 /// The new label string.
390 value: String,
391 /// If `Some`, the new value is placed immediately before this label.
392 before: Option<String>,
393 /// If `Some`, the new value is placed immediately after this label.
394 after: Option<String>,
395 },
396 /// Rename an existing enum label.
397 EnumRenameValue {
398 /// Qualified name of the enum type.
399 qname: QualifiedName,
400 /// The existing label to rename.
401 from: String,
402 /// The new label name.
403 to: String,
404 },
405
406 /// Add a CHECK constraint to a domain.
407 DomainAddCheck {
408 /// Qualified name of the domain type.
409 qname: QualifiedName,
410 /// The constraint to add.
411 constraint: DomainCheck,
412 },
413 /// Drop a named CHECK constraint from a domain.
414 DomainDropCheck {
415 /// Qualified name of the domain type.
416 qname: QualifiedName,
417 /// The constraint name to drop.
418 name: Identifier,
419 },
420 /// Set (or clear) the DEFAULT expression on a domain.
421 DomainSetDefault {
422 /// Qualified name of the domain type.
423 qname: QualifiedName,
424 /// New default expression (`None` clears the default).
425 default: Option<NormalizedExpr>,
426 },
427 /// Toggle the `NOT NULL` constraint on a domain.
428 DomainSetNotNull {
429 /// Qualified name of the domain type.
430 qname: QualifiedName,
431 /// `true` means NOT NULL (i.e., `nullable = false`).
432 not_null: bool,
433 },
434
435 /// Add a new attribute to a composite type.
436 CompositeAddAttribute {
437 /// Qualified name of the composite type.
438 qname: QualifiedName,
439 /// The attribute to add.
440 attribute: CompositeAttribute,
441 },
442 /// Drop an attribute from a composite type.
443 CompositeDropAttribute {
444 /// Qualified name of the composite type.
445 qname: QualifiedName,
446 /// The attribute name to drop.
447 name: Identifier,
448 },
449 /// Change the type of an existing composite attribute.
450 CompositeAlterAttributeType {
451 /// Qualified name of the composite type.
452 qname: QualifiedName,
453 /// The attribute name whose type is being changed.
454 attribute: Identifier,
455 /// The new column type.
456 new_type: ColumnType,
457 },
458
459 /// Set (or clear) the `COMMENT ON TYPE` for this type.
460 SetComment {
461 /// Qualified name of the type.
462 qname: QualifiedName,
463 /// New comment (`None` clears the comment).
464 comment: Option<String>,
465 },
466
467 /// Emitted when the requested change cannot be done in place via `ALTER`.
468 ///
469 /// T8's cascade walker appends `ReplaceBody` for any views/MVs that depend
470 /// on the type so they are recreated after the type is rebuilt. T9's SQL
471 /// emitter expands this single entry into `DROP TYPE … CASCADE` plus
472 /// `CREATE TYPE`, then the recreated dependents follow in topological
473 /// order. The planner never emits this entry as one raw SQL step.
474 ReplaceWithCascade {
475 /// The desired type (source).
476 source: UserType,
477 /// The existing type (catalog / live database).
478 catalog: UserType,
479 },
480}
481
482/// A structural change to a single view.
483#[allow(clippy::large_enum_variant)]
484#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
485#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
486pub enum ViewChange {
487 /// Create a new view.
488 Create(View),
489 /// Drop an existing view.
490 Drop(QualifiedName),
491 /// Replace the SELECT body of a view.
492 ///
493 /// `compatible` is `true` when Postgres's `CREATE OR REPLACE VIEW` rules
494 /// are satisfied (same column names and types at the same indexes, with
495 /// new columns only appended at the end). When `compatible` is `false`,
496 /// the planner must `DROP` then re-`CREATE` the view (and rebuild its
497 /// dependents).
498 ReplaceBody {
499 /// The view as it should exist (source SQL).
500 source: View,
501 /// The view as it currently exists (live catalog).
502 catalog: View,
503 /// Whether `CREATE OR REPLACE VIEW` can be used (`true`) or a
504 /// `DROP` + `CREATE` cycle is required (`false`).
505 compatible: bool,
506 },
507 /// Change `WITH (security_barrier = ..., security_invoker = ...)` reloptions.
508 SetReloption {
509 /// View qname.
510 qname: QualifiedName,
511 /// Desired `security_barrier` value (`None` clears the option).
512 security_barrier: Option<bool>,
513 /// Desired `security_invoker` value (`None` clears the option).
514 security_invoker: Option<bool>,
515 },
516 /// Set (or clear) the view-level `COMMENT ON VIEW`.
517 SetComment {
518 /// View qname.
519 qname: QualifiedName,
520 /// New comment (`None` clears the comment).
521 comment: Option<String>,
522 },
523 /// Set (or clear) a `COMMENT ON COLUMN view.col`.
524 SetColumnComment {
525 /// View qname.
526 qname: QualifiedName,
527 /// Column name.
528 column: Identifier,
529 /// New comment (`None` clears the comment).
530 comment: Option<String>,
531 },
532}
533
534/// A structural change to a single materialized view.
535#[allow(clippy::large_enum_variant)]
536#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
537#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
538pub enum MvChange {
539 /// Create a new materialized view.
540 Create(MaterializedView),
541 /// Drop an existing materialized view.
542 ///
543 /// Unlike `ViewChange::Drop`, this is classified as `Safe` because
544 /// materialized views are derived data that can be recreated by refreshing.
545 Drop(QualifiedName),
546 /// Replace the SELECT body of a materialized view.
547 ///
548 /// Materialized views do not support `CREATE OR REPLACE`; the planner must
549 /// always `DROP` then `CREATE` the MV (and rebuild any indexes on it).
550 ReplaceBody {
551 /// The MV as it should exist (source SQL).
552 source: MaterializedView,
553 /// The MV as it currently exists (live catalog).
554 catalog: MaterializedView,
555 },
556 /// Set (or clear) the MV-level `COMMENT ON MATERIALIZED VIEW`.
557 SetComment {
558 /// MV qname.
559 qname: QualifiedName,
560 /// New comment (`None` clears the comment).
561 comment: Option<String>,
562 },
563 /// Set (or clear) a `COMMENT ON COLUMN mv.col`.
564 SetColumnComment {
565 /// MV qname.
566 qname: QualifiedName,
567 /// Column name.
568 column: Identifier,
569 /// New comment (`None` clears the comment).
570 comment: Option<String>,
571 },
572}
573
574/// A structural change to a single user-defined function.
575#[allow(clippy::large_enum_variant)]
576#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
577#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
578pub enum FunctionChange {
579 /// Create a new function.
580 Create(Function),
581 /// Drop an existing function by qualified name and normalized arg types.
582 Drop {
583 /// Qualified name of the function.
584 qname: QualifiedName,
585 /// Normalized argument types (IN/INOUT/VARIADIC only) used to
586 /// disambiguate overloads in the DROP FUNCTION statement.
587 args: NormalizedArgTypes,
588 },
589 /// Replace the function body and/or attributes using `CREATE OR REPLACE
590 /// FUNCTION`. Only valid when `function_can_or_replace` returns `true`.
591 CreateOrReplace(Function),
592 /// The function's return type or language changed in a way that PG's
593 /// `CREATE OR REPLACE FUNCTION` rejects. The planner must emit
594 /// `DROP FUNCTION … CASCADE` followed by `CREATE FUNCTION`.
595 ReplaceWithCascade {
596 /// The desired function (source).
597 source: Function,
598 /// The existing function (catalog / live database).
599 catalog: Function,
600 },
601 /// Set (or clear) `COMMENT ON FUNCTION`.
602 SetComment {
603 /// Qualified name of the function.
604 qname: QualifiedName,
605 /// Normalized argument types for disambiguation.
606 args: NormalizedArgTypes,
607 /// New comment (`None` clears the comment).
608 comment: Option<String>,
609 },
610}
611
612/// A structural change to a single user-defined procedure.
613#[allow(clippy::large_enum_variant)]
614#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
615#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
616pub enum ProcedureChange {
617 /// Create a new procedure.
618 Create(Procedure),
619 /// Drop an existing procedure by qualified name.
620 Drop(QualifiedName),
621 /// Replace the procedure body and/or attributes using `CREATE OR REPLACE
622 /// PROCEDURE`.
623 CreateOrReplace(Procedure),
624 /// Set (or clear) `COMMENT ON PROCEDURE`.
625 SetComment {
626 /// Qualified name of the procedure.
627 qname: QualifiedName,
628 /// New comment (`None` clears the comment).
629 comment: Option<String>,
630 },
631}
632
633/// Change to one extension. Pair-by-name semantics.
634#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
635#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
636pub enum ExtensionChange {
637 /// Install a new extension.
638 Create(Extension),
639 /// Drop an extension by name. Emits `DROP EXTENSION ... CASCADE`.
640 Drop(Identifier),
641 /// Bump extension version: `ALTER EXTENSION ... UPDATE TO 'v'`.
642 AlterUpdate {
643 /// Extension name.
644 name: Identifier,
645 /// New version to update to.
646 to_version: String,
647 },
648 /// Schema-changing replace (DROP CASCADE + CREATE).
649 ReplaceWithCascade(Extension),
650 /// Change the `COMMENT ON EXTENSION` text.
651 CommentOn {
652 /// Extension name.
653 name: Identifier,
654 /// New comment value (`None` clears the comment).
655 comment: Option<String>,
656 },
657}
658
659/// Change to one trigger. Pair-by-qname semantics; any structural
660/// difference emits `Replace`.
661#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
662#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
663pub enum TriggerChange {
664 /// Install a new trigger.
665 Create(Trigger),
666 /// Drop a trigger by qname (needs the table for `DROP TRIGGER name ON table`).
667 Drop {
668 /// Qualified name of the trigger.
669 qname: QualifiedName,
670 /// Owning table (needed for `DROP TRIGGER name ON table`).
671 table: QualifiedName,
672 },
673 /// Any structural change: drop + create.
674 Replace(Trigger),
675 /// Change the `COMMENT ON TRIGGER` text.
676 CommentOn {
677 /// Qualified name of the trigger.
678 qname: QualifiedName,
679 /// Owning table (needed for `COMMENT ON TRIGGER name ON table`).
680 table: QualifiedName,
681 /// New comment value (`None` clears the comment).
682 comment: Option<String>,
683 },
684}
685
686/// A partition-membership change on a child table.
687///
688/// These changes are emitted by the differ when the `partition_of` field of a
689/// table changes. Wiring to SQL steps lands in PART9.
690#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
691#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
692pub enum TableChange {
693 /// Attach a child table to a parent partitioned table.
694 ///
695 /// Corresponds to `ALTER TABLE <parent> ATTACH PARTITION <child> <bounds>`.
696 AttachPartition {
697 /// The partitioned parent table.
698 parent: QualifiedName,
699 /// The child table being attached.
700 child: QualifiedName,
701 /// The partition bounds clause.
702 bounds: PartitionBounds,
703 },
704 /// Detach a child table from a parent partitioned table.
705 ///
706 /// Corresponds to `ALTER TABLE <parent> DETACH PARTITION <child>`.
707 DetachPartition {
708 /// The partitioned parent table.
709 parent: QualifiedName,
710 /// The child table being detached.
711 child: QualifiedName,
712 },
713}
714
715#[cfg(test)]
716mod tests {
717 use super::*;
718 use crate::identifier::Identifier;
719 use crate::ir::column::Column;
720 use crate::ir::column_type::ColumnType;
721
722 fn id(s: &str) -> Identifier {
723 Identifier::from_unquoted(s).unwrap()
724 }
725
726 fn qn(schema: &str, name: &str) -> QualifiedName {
727 QualifiedName::new(id(schema), id(name))
728 }
729
730 fn table_users() -> Table {
731 Table {
732 qname: qn("app", "users"),
733 columns: vec![Column {
734 name: id("id"),
735 ty: ColumnType::BigInt,
736 nullable: false,
737 default: None,
738 identity: None,
739 generated: None,
740 collation: None,
741 storage: None,
742 compression: None,
743 comment: None,
744 }],
745 constraints: vec![],
746 partition_by: None,
747 partition_of: None,
748 comment: None,
749 owner: None,
750 grants: vec![],
751 rls_enabled: false,
752 rls_forced: false,
753 policies: vec![],
754 storage: crate::ir::reloptions::TableStorageOptions::default(),
755 }
756 }
757
758 #[test]
759 fn create_table_entry_serde_round_trip() {
760 let entry = ChangeEntry {
761 change: Change::CreateTable(table_users()),
762 destructiveness: Destructiveness::Safe,
763 };
764 let json = serde_json::to_string(&entry).unwrap();
765 let back: ChangeEntry = serde_json::from_str(&json).unwrap();
766 assert_eq!(entry, back);
767 }
768
769 #[test]
770 fn drop_table_entry_serde_round_trip() {
771 let entry = ChangeEntry {
772 change: Change::DropTable {
773 qname: qn("app", "users"),
774 row_count_estimate: None,
775 },
776 destructiveness: Destructiveness::RequiresApprovalAndDataLossWarning {
777 reason: "drops table app.users".into(),
778 },
779 };
780 let json = serde_json::to_string(&entry).unwrap();
781 let back: ChangeEntry = serde_json::from_str(&json).unwrap();
782 assert_eq!(entry, back);
783 }
784
785 #[test]
786 fn equal_create_table_changes_compare_equal() {
787 let a = Change::CreateTable(table_users());
788 let b = Change::CreateTable(table_users());
789 assert_eq!(a, b);
790 }
791}