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