pgevolve_core/ir/
default_expr.rs1use serde::{Deserialize, Serialize};
13
14use crate::identifier::QualifiedName;
15use crate::ir::difference::Difference;
16use crate::ir::eq::Equiv;
17
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum DefaultExpr {
22 Literal(LiteralValue),
24 Sequence(QualifiedName),
26 Expr(NormalizedExpr),
28}
29
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum LiteralValue {
37 Bool(bool),
39 Integer(i64),
41 Float(f64),
43 Text(String),
45 Bytea(Vec<u8>),
47 Null,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
58pub struct NormalizedExpr {
59 pub canonical_text: String,
62 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 self.canonical_text.cmp(&other.canonical_text)
78 }
79}
80
81impl NormalizedExpr {
82 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 pub fn from_canonical_text(text: impl Into<String>) -> Self {
99 Self::from_text(text)
100 }
101}
102
103impl Equiv for DefaultExpr {
104 fn differences(&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.differences(&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}