Skip to main content

pgevolve_core/ir/
column.rs

1//! `Column` and column-level features (identity, generated, default, collation).
2
3use serde::{Deserialize, Serialize};
4
5use crate::identifier::{Identifier, QualifiedName};
6use crate::ir::column_type::ColumnType;
7use crate::ir::default_expr::DefaultExpr;
8use crate::ir::difference::Difference;
9use crate::ir::eq::{Equiv, field_difference};
10
11/// A table column.
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub struct Column {
14    /// Column name.
15    pub name: Identifier,
16    /// Canonical column type.
17    pub ty: ColumnType,
18    /// `NOT NULL` lives here, not in `Constraint`.
19    pub nullable: bool,
20    /// Optional column default.
21    pub default: Option<DefaultExpr>,
22    /// Identity specification (`GENERATED ALWAYS|BY DEFAULT AS IDENTITY`).
23    pub identity: Option<Identity>,
24    /// Generated-column specification (`GENERATED ALWAYS AS (expr) STORED`).
25    pub generated: Option<Generated>,
26    /// Optional collation.
27    pub collation: Option<QualifiedName>,
28    /// Per-column TOAST storage strategy. `None` means "PG type default".
29    pub storage: Option<StorageKind>,
30    /// Per-column TOAST compression codec. `None` means "cluster default".
31    pub compression: Option<Compression>,
32    /// Optional comment.
33    pub comment: Option<String>,
34}
35
36impl Equiv for Column {
37    fn differences(&self, other: &Self) -> Vec<Difference> {
38        let Self {
39            name: _,
40            ty: _,
41            nullable: _,
42            default: _,
43            identity: _,
44            generated: _,
45            collation: _,
46            storage: _,
47            compression: _,
48            comment: _,
49        } = self;
50        let mut out = Vec::new();
51        out.extend(field_difference("name", &self.name, &other.name));
52        out.extend(field_difference(
53            "ty",
54            &format!("{:?}", self.ty),
55            &format!("{:?}", other.ty),
56        ));
57        out.extend(field_difference(
58            "nullable",
59            &self.nullable,
60            &other.nullable,
61        ));
62        out.extend(field_difference(
63            "default",
64            &format!("{:?}", self.default),
65            &format!("{:?}", other.default),
66        ));
67        out.extend(field_difference(
68            "identity",
69            &format!("{:?}", self.identity),
70            &format!("{:?}", other.identity),
71        ));
72        out.extend(field_difference(
73            "generated",
74            &format!("{:?}", self.generated),
75            &format!("{:?}", other.generated),
76        ));
77        out.extend(field_difference(
78            "collation",
79            &format!("{:?}", self.collation),
80            &format!("{:?}", other.collation),
81        ));
82        out.extend(field_difference(
83            "storage",
84            &format!("{:?}", self.storage),
85            &format!("{:?}", other.storage),
86        ));
87        out.extend(field_difference(
88            "compression",
89            &format!("{:?}", self.compression),
90            &format!("{:?}", other.compression),
91        ));
92        out.extend(field_difference(
93            "comment",
94            &format!("{:?}", self.comment),
95            &format!("{:?}", other.comment),
96        ));
97        out
98    }
99}
100
101/// `GENERATED ... AS IDENTITY` specification.
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103pub struct Identity {
104    /// `ALWAYS` or `BY DEFAULT`.
105    pub kind: IdentityKind,
106    /// Backing sequence options.
107    pub sequence: SequenceOptions,
108}
109
110/// Identity kind.
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113pub enum IdentityKind {
114    /// `GENERATED ALWAYS AS IDENTITY`.
115    Always,
116    /// `GENERATED BY DEFAULT AS IDENTITY`.
117    ByDefault,
118}
119
120/// `GENERATED ALWAYS AS (expr) STORED` specification.
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122pub struct Generated {
123    /// Stored vs virtual (PG only supports stored as of v17).
124    pub kind: GeneratedKind,
125    /// Generation expression.
126    pub expression: crate::ir::default_expr::NormalizedExpr,
127}
128
129/// Generated-column kind.
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
131#[serde(rename_all = "snake_case")]
132pub enum GeneratedKind {
133    /// `STORED`.
134    Stored,
135}
136
137/// Per-column TOAST storage strategy.
138///
139/// Mirrors Postgres `pg_attribute.attstorage` (`p|e|x|m`). The semantic
140/// effective value depends on the column's data type; see
141/// [`crate::ir::canon::filter_pg_defaults`] for how an explicit
142/// `Some(default)` is stripped to `None` so source and catalog round-trip equally.
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
144#[serde(rename_all = "snake_case")]
145pub enum StorageKind {
146    /// `PLAIN` — inline only, never `TOASTed` or compressed.
147    Plain,
148    /// `EXTERNAL` — out-of-line allowed, compression forbidden.
149    External,
150    /// `EXTENDED` — out-of-line and compression allowed (default for most variable-width types).
151    Extended,
152    /// `MAIN` — compression allowed inline, out-of-line only as a last resort.
153    Main,
154}
155
156/// Per-column TOAST compression codec.
157///
158/// Mirrors the codec character stored in Postgres `pg_attribute.attcompression`.
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
160#[serde(rename_all = "snake_case")]
161pub enum Compression {
162    /// PGLZ — Postgres's historical lz-family codec; the default for clusters
163    /// shipped with `default_toast_compression = 'pglz'`.
164    Pglz,
165    /// LZ4 — added in PG 14; faster compression/decompression at lower ratio.
166    Lz4,
167}
168
169/// Sequence options that back an identity column.
170#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
171pub struct SequenceOptions {
172    /// Start value.
173    pub start: i64,
174    /// Increment.
175    pub increment: i64,
176    /// Min value (`None` = type minimum).
177    pub min_value: Option<i64>,
178    /// Max value (`None` = type maximum).
179    pub max_value: Option<i64>,
180    /// Cache size.
181    pub cache: i64,
182    /// Whether the sequence cycles.
183    pub cycle: bool,
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use crate::ir::eq::Equiv;
190
191    fn id(s: &str) -> Identifier {
192        Identifier::from_unquoted(s).unwrap()
193    }
194
195    fn base() -> Column {
196        Column {
197            name: id("email"),
198            ty: ColumnType::Text,
199            nullable: false,
200            default: None,
201            identity: None,
202            generated: None,
203            collation: None,
204            storage: None,
205            compression: None,
206            comment: None,
207        }
208    }
209
210    #[test]
211    fn equal_columns_have_no_diff() {
212        assert!(base().canonical_eq(&base()));
213    }
214
215    #[test]
216    fn type_change_diffs() {
217        let mut b = base();
218        b.ty = ColumnType::Varchar { len: Some(255) };
219        assert!(base().differences(&b).iter().any(|x| x.path == "ty"));
220    }
221
222    #[test]
223    fn nullability_change_diffs() {
224        let mut b = base();
225        b.nullable = true;
226        assert!(base().differences(&b).iter().any(|x| x.path == "nullable"));
227    }
228
229    #[test]
230    fn identity_change_diffs() {
231        let mut b = base();
232        b.identity = Some(Identity {
233            kind: IdentityKind::ByDefault,
234            sequence: SequenceOptions {
235                start: 1,
236                increment: 1,
237                min_value: None,
238                max_value: None,
239                cache: 1,
240                cycle: false,
241            },
242        });
243        assert!(base().differences(&b).iter().any(|x| x.path == "identity"));
244    }
245
246    #[test]
247    fn storage_change_diffs() {
248        let mut b = base();
249        b.storage = Some(StorageKind::External);
250        assert!(base().differences(&b).iter().any(|x| x.path == "storage"));
251    }
252
253    #[test]
254    fn compression_change_diffs() {
255        let mut b = base();
256        b.compression = Some(Compression::Lz4);
257        assert!(
258            base()
259                .differences(&b)
260                .iter()
261                .any(|x| x.path == "compression")
262        );
263    }
264}