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