Skip to main content

pgevolve_core/ir/
default_expr.rs

1//! `DefaultExpr` — column default expression.
2//!
3//! Three kinds:
4//! - [`DefaultExpr::Literal`]: a typed literal (numbers, strings, bytea, NULL, etc.).
5//! - [`DefaultExpr::Sequence`]: `nextval('seq')` references — recognized as desugared
6//!   identity / `SERIAL` sources.
7//! - [`DefaultExpr::Expr`]: any other expression, stored as a [`NormalizedExpr`].
8//!
9//! Real AST normalization (cast stripping, paren folding, commutative-operand sorting)
10//! lands in phase 2 once `pg_query` is wired in. Phase 1 ships the structural types.
11
12use serde::{Deserialize, Serialize};
13
14use crate::identifier::QualifiedName;
15use crate::ir::difference::Difference;
16use crate::ir::eq::Diff;
17
18/// A column-default expression.
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum DefaultExpr {
22    /// A literal value.
23    Literal(LiteralValue),
24    /// A reference to a sequence (e.g., `nextval('app.seq1'::regclass)`).
25    Sequence(QualifiedName),
26    /// Any other expression.
27    Expr(NormalizedExpr),
28}
29
30/// A typed literal default value.
31///
32/// Note: `Float(f64)` precludes deriving `Eq` and `Hash` on this type;
33/// equality is `PartialEq` only.
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum LiteralValue {
37    /// Boolean literal.
38    Bool(bool),
39    /// Integer literal (covers smallint/int/bigint).
40    Integer(i64),
41    /// Floating-point literal.
42    Float(f64),
43    /// Text-like literal (text, varchar, char, etc.).
44    Text(String),
45    /// Bytea literal.
46    Bytea(Vec<u8>),
47    /// SQL `NULL`.
48    Null,
49}
50
51/// A normalized SQL expression — its canonical text plus a hash of the canonical AST.
52///
53/// The `ast_hash` is a BLAKE3 hash of `canonical_text` (the canonical bytes after
54/// keyword lowercasing, paren folding, cast stripping, and commutative-operand
55/// sorting). Two expressions hash-equal iff their canonical texts match exactly,
56/// so equivalence is decided byte-wise on the canonical form.
57#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
58pub struct NormalizedExpr {
59    /// Canonical textual form (lowercased keywords, sorted commutative operands,
60    /// stripped redundant casts).
61    pub canonical_text: String,
62    /// BLAKE3 hash of `canonical_text` as bytes.
63    pub ast_hash: [u8; 32],
64}
65
66impl PartialOrd for NormalizedExpr {
67    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
68        Some(self.cmp(other))
69    }
70}
71
72impl Ord for NormalizedExpr {
73    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
74        // Ordering by canonical_text is stable and deterministic: two
75        // NormalizedExprs with the same canonical_text are equal by
76        // construction (ast_hash is derived from it).
77        self.canonical_text.cmp(&other.canonical_text)
78    }
79}
80
81impl NormalizedExpr {
82    /// Construct from already-canonical text. The hash is computed from the text.
83    ///
84    /// This constructor does NOT run any normalization passes — it assumes the
85    /// caller has already produced canonical form. Source-side construction goes
86    /// through `pgevolve_core::parse::normalize_expr::from_pg_node` which deparses
87    /// and normalizes a `pg_query` node.
88    pub fn from_text(canonical_text: impl Into<String>) -> Self {
89        let canonical_text = canonical_text.into();
90        let ast_hash = blake3::hash(canonical_text.as_bytes()).into();
91        Self {
92            canonical_text,
93            ast_hash,
94        }
95    }
96
97    /// Alias for [`Self::from_text`], matching the phase-2 plan vocabulary.
98    pub fn from_canonical_text(text: impl Into<String>) -> Self {
99        Self::from_text(text)
100    }
101}
102
103impl Diff for DefaultExpr {
104    fn diff(&self, other: &Self) -> Vec<Difference> {
105        if self == other {
106            Vec::new()
107        } else {
108            vec![Difference::new("", display(self), display(other))]
109        }
110    }
111}
112
113fn display(d: &DefaultExpr) -> String {
114    match d {
115        DefaultExpr::Literal(LiteralValue::Bool(b)) => b.to_string(),
116        DefaultExpr::Literal(LiteralValue::Integer(i)) => i.to_string(),
117        DefaultExpr::Literal(LiteralValue::Float(f)) => f.to_string(),
118        DefaultExpr::Literal(LiteralValue::Text(t)) => format!("'{}'", t.replace('\'', "''")),
119        DefaultExpr::Literal(LiteralValue::Bytea(b)) => format!("\\x{}", hex(b)),
120        DefaultExpr::Literal(LiteralValue::Null) => "NULL".into(),
121        DefaultExpr::Sequence(q) => format!("nextval('{}')", q.render_sql()),
122        DefaultExpr::Expr(e) => e.canonical_text.clone(),
123    }
124}
125
126fn hex(bytes: &[u8]) -> String {
127    let mut s = String::with_capacity(bytes.len() * 2);
128    for b in bytes {
129        use std::fmt::Write as _;
130        let _ = write!(s, "{b:02x}");
131    }
132    s
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::identifier::Identifier;
139
140    #[test]
141    fn equal_text_literals_canonical_eq() {
142        let a = DefaultExpr::Literal(LiteralValue::Text("foo".into()));
143        let b = DefaultExpr::Literal(LiteralValue::Text("foo".into()));
144        assert!(a.canonical_eq(&b));
145    }
146
147    #[test]
148    fn different_text_literals_diff() {
149        let a = DefaultExpr::Literal(LiteralValue::Text("foo".into()));
150        let b = DefaultExpr::Literal(LiteralValue::Text("bar".into()));
151        let d = a.diff(&b);
152        assert_eq!(d.len(), 1);
153    }
154
155    #[test]
156    fn sequence_differs_from_literal() {
157        let q = QualifiedName::new(
158            Identifier::from_unquoted("app").unwrap(),
159            Identifier::from_unquoted("seq1").unwrap(),
160        );
161        let a = DefaultExpr::Sequence(q);
162        let b = DefaultExpr::Literal(LiteralValue::Integer(1));
163        assert!(!a.canonical_eq(&b));
164    }
165
166    #[test]
167    fn integer_and_text_literals_distinct() {
168        let a = DefaultExpr::Literal(LiteralValue::Integer(1));
169        let b = DefaultExpr::Literal(LiteralValue::Text("1".into()));
170        assert!(!a.canonical_eq(&b));
171    }
172
173    #[test]
174    fn null_literals_equal() {
175        let a = DefaultExpr::Literal(LiteralValue::Null);
176        let b = DefaultExpr::Literal(LiteralValue::Null);
177        assert!(a.canonical_eq(&b));
178    }
179
180    #[test]
181    fn normalized_expr_round_trips() {
182        let e = NormalizedExpr::from_text("now()");
183        let json = serde_json::to_string(&e).unwrap();
184        let back: NormalizedExpr = serde_json::from_str(&json).unwrap();
185        assert_eq!(e, back);
186    }
187}