Skip to main content

pgevolve_core/diff/
table_op.rs

1//! `TableOp` — per-table column / constraint operations.
2//!
3//! Carried inside [`Change::AlterTable`](super::change::Change::AlterTable).
4
5use serde::{Deserialize, Serialize};
6
7use crate::identifier::Identifier;
8use crate::ir::column::{Column, Compression, Generated, Identity, StorageKind};
9use crate::ir::column_type::ColumnType;
10use crate::ir::constraint::Constraint;
11use crate::ir::default_expr::{DefaultExpr, NormalizedExpr};
12
13use super::destructiveness::Destructiveness;
14
15/// One table-level op paired with its destructiveness classification.
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17pub struct TableOpEntry {
18    /// The table operation.
19    pub op: TableOp,
20    /// Risk classification.
21    pub destructiveness: Destructiveness,
22}
23
24/// One column / constraint / comment operation on a table.
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
27pub enum TableOp {
28    /// Add a column.
29    AddColumn(Column),
30    /// Drop a column.
31    DropColumn {
32        /// Column name.
33        name: Identifier,
34        /// Whether the column is on a populated table; the planner refines this
35        /// from `pg_class.reltuples`. Differ leaves `false` if unknown.
36        is_populated: bool,
37    },
38    /// Change a column's data type. Carries an optional `USING` clause; v0.1
39    /// emits `None` and leaves the rewrite pass to decide.
40    AlterColumnType {
41        /// Column name.
42        name: Identifier,
43        /// Existing type in the target.
44        from: ColumnType,
45        /// Desired type in the source.
46        to: ColumnType,
47        /// Optional `USING` expression.
48        using: Option<NormalizedExpr>,
49    },
50    /// Toggle a column's `NOT NULL`-ness.
51    SetColumnNullable {
52        /// Column name.
53        name: Identifier,
54        /// Target nullability (`true` = nullable, `false` = `NOT NULL`).
55        nullable: bool,
56    },
57    /// Set or clear a column's `DEFAULT` expression.
58    SetColumnDefault {
59        /// Column name.
60        name: Identifier,
61        /// New default (`None` = drop the default).
62        default: Option<DefaultExpr>,
63    },
64    /// Set or clear a column's identity specification.
65    SetColumnIdentity {
66        /// Column name.
67        name: Identifier,
68        /// New identity (`None` = drop identity).
69        identity: Option<Identity>,
70    },
71    /// Set or clear a column's generated-column expression.
72    SetColumnGenerated {
73        /// Column name.
74        name: Identifier,
75        /// New generated spec (`None` = drop generated-ness).
76        generated: Option<Generated>,
77    },
78    /// Set or clear a column-level comment.
79    SetColumnComment {
80        /// Column name.
81        name: Identifier,
82        /// New comment (`None` clears).
83        comment: Option<String>,
84    },
85    /// Change a column's TOAST storage strategy.
86    SetColumnStorage {
87        /// Column name.
88        name: Identifier,
89        /// Previous storage. Caller resolves `Column.storage = None` to
90        /// the type default before emitting, so both sides are explicit.
91        /// Carried so the lint rule (Stage 6) can detect downgrades
92        /// without needing to re-derive the previous state.
93        from: StorageKind,
94        /// New storage. Same resolution rule as `from`.
95        to: StorageKind,
96    },
97    /// Change a column's TOAST compression codec.
98    SetColumnCompression {
99        /// Column name.
100        name: Identifier,
101        /// New compression. `None` means "use cluster default" — emits
102        /// `SET COMPRESSION DEFAULT` at render time.
103        compression: Option<Compression>,
104    },
105
106    /// Add a table constraint.
107    AddConstraint(Constraint),
108    /// Drop a table constraint by name.
109    DropConstraint {
110        /// Constraint name (no schema — constraint names live in the table's namespace).
111        name: Identifier,
112    },
113    /// Set or clear a constraint comment.
114    SetConstraintComment {
115        /// Constraint name.
116        name: Identifier,
117        /// New comment.
118        comment: Option<String>,
119    },
120
121    /// Set or clear a table-level comment.
122    SetTableComment {
123        /// New comment (`None` clears).
124        comment: Option<String>,
125    },
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    fn id(s: &str) -> Identifier {
133        Identifier::from_unquoted(s).unwrap()
134    }
135
136    #[test]
137    fn add_column_serde_round_trip() {
138        let op = TableOp::AddColumn(Column {
139            name: id("email"),
140            ty: ColumnType::Text,
141            nullable: true,
142            default: None,
143            identity: None,
144            generated: None,
145            collation: None,
146            storage: None,
147            compression: None,
148            comment: None,
149        });
150        let entry = TableOpEntry {
151            op,
152            destructiveness: Destructiveness::Safe,
153        };
154        let json = serde_json::to_string(&entry).unwrap();
155        let back: TableOpEntry = serde_json::from_str(&json).unwrap();
156        assert_eq!(entry, back);
157    }
158
159    #[test]
160    fn drop_column_serde_round_trip() {
161        let entry = TableOpEntry {
162            op: TableOp::DropColumn {
163                name: id("email"),
164                is_populated: false,
165            },
166            destructiveness: Destructiveness::RequiresApprovalAndDataLossWarning {
167                reason: "drops column email".into(),
168            },
169        };
170        let json = serde_json::to_string(&entry).unwrap();
171        let back: TableOpEntry = serde_json::from_str(&json).unwrap();
172        assert_eq!(entry, back);
173    }
174
175    #[test]
176    fn alter_column_type_serde_round_trip() {
177        let entry = TableOpEntry {
178            op: TableOp::AlterColumnType {
179                name: id("count"),
180                from: ColumnType::Integer,
181                to: ColumnType::BigInt,
182                using: None,
183            },
184            destructiveness: Destructiveness::Safe,
185        };
186        let json = serde_json::to_string(&entry).unwrap();
187        let back: TableOpEntry = serde_json::from_str(&json).unwrap();
188        assert_eq!(entry, back);
189    }
190
191    #[test]
192    fn set_table_comment_serde_round_trip() {
193        let entry = TableOpEntry {
194            op: TableOp::SetTableComment {
195                comment: Some("the users table".into()),
196            },
197            destructiveness: Destructiveness::Safe,
198        };
199        let json = serde_json::to_string(&entry).unwrap();
200        let back: TableOpEntry = serde_json::from_str(&json).unwrap();
201        assert_eq!(entry, back);
202    }
203}