Skip to main content

pg2sqlite_core/ir/
expr.rs

1//! Expression types for default values, CHECK constraints, and WHERE clauses.
2
3/// An expression node in the IR.
4#[derive(Debug, Clone, PartialEq)]
5pub enum Expr {
6    /// Integer literal (e.g., `42`)
7    IntegerLiteral(i64),
8    /// Float literal (e.g., `3.14`)
9    FloatLiteral(f64),
10    /// String literal (e.g., `'hello'`)
11    StringLiteral(String),
12    /// Boolean literal (`true`/`false`)
13    BooleanLiteral(bool),
14    /// NULL literal
15    Null,
16    /// Column reference (e.g., `status`)
17    ColumnRef(String),
18    /// Function call (e.g., `now()`, `lower(col)`)
19    FunctionCall { name: String, args: Vec<Expr> },
20    /// Type cast (e.g., `value::integer`, `CAST(value AS integer)`)
21    Cast {
22        expr: std::boxed::Box<Expr>,
23        type_name: String,
24    },
25    /// Binary operation (e.g., `a + b`, `a AND b`)
26    BinaryOp {
27        left: std::boxed::Box<Expr>,
28        op: String,
29        right: std::boxed::Box<Expr>,
30    },
31    /// Unary operation (e.g., `NOT a`, `-x`)
32    UnaryOp {
33        op: String,
34        expr: std::boxed::Box<Expr>,
35    },
36    /// IS NULL / IS NOT NULL
37    IsNull {
38        expr: std::boxed::Box<Expr>,
39        negated: bool,
40    },
41    /// IN list (e.g., `col IN ('a', 'b', 'c')`)
42    InList {
43        expr: std::boxed::Box<Expr>,
44        list: Vec<Expr>,
45        negated: bool,
46    },
47    /// BETWEEN (e.g., `col BETWEEN 1 AND 10`)
48    Between {
49        expr: std::boxed::Box<Expr>,
50        low: std::boxed::Box<Expr>,
51        high: std::boxed::Box<Expr>,
52        negated: bool,
53    },
54    /// Parenthesized expression
55    Nested(std::boxed::Box<Expr>),
56    /// nextval('sequence_name') — PG-specific, removed during transform
57    NextVal(String),
58    /// CURRENT_TIMESTAMP (SQLite built-in)
59    CurrentTimestamp,
60    /// Raw SQL string for expressions that can't be decomposed further
61    Raw(String),
62}
63
64impl Expr {
65    /// Render this expression as a SQL string.
66    pub fn to_sql(&self) -> String {
67        match self {
68            Expr::IntegerLiteral(n) => n.to_string(),
69            Expr::FloatLiteral(n) => n.to_string(),
70            Expr::StringLiteral(s) => format!("'{}'", s.replace('\'', "''")),
71            Expr::BooleanLiteral(b) => {
72                if *b {
73                    "1".to_string()
74                } else {
75                    "0".to_string()
76                }
77            }
78            Expr::Null => "NULL".to_string(),
79            Expr::ColumnRef(name) => {
80                // Quote each dot-separated identifier part to prevent injection
81                name.split('.')
82                    .map(|part| {
83                        let ident = super::Ident::new(part);
84                        ident.to_sql()
85                    })
86                    .collect::<Vec<_>>()
87                    .join(".")
88            }
89            Expr::FunctionCall { name, args } => {
90                let args_str: Vec<String> = args.iter().map(|a| a.to_sql()).collect();
91                format!("{name}({})", args_str.join(", "))
92            }
93            Expr::Cast { expr, type_name } => {
94                format!("CAST({} AS {type_name})", expr.to_sql())
95            }
96            Expr::BinaryOp { left, op, right } => {
97                format!("{} {op} {}", left.to_sql(), right.to_sql())
98            }
99            Expr::UnaryOp { op, expr } => {
100                format!("{op} {}", expr.to_sql())
101            }
102            Expr::IsNull { expr, negated } => {
103                if *negated {
104                    format!("{} IS NOT NULL", expr.to_sql())
105                } else {
106                    format!("{} IS NULL", expr.to_sql())
107                }
108            }
109            Expr::InList {
110                expr,
111                list,
112                negated,
113            } => {
114                let items: Vec<String> = list.iter().map(|e| e.to_sql()).collect();
115                let not = if *negated { "NOT " } else { "" };
116                format!("{} {not}IN ({})", expr.to_sql(), items.join(", "))
117            }
118            Expr::Between {
119                expr,
120                low,
121                high,
122                negated,
123            } => {
124                let not = if *negated { "NOT " } else { "" };
125                format!(
126                    "{} {not}BETWEEN {} AND {}",
127                    expr.to_sql(),
128                    low.to_sql(),
129                    high.to_sql()
130                )
131            }
132            Expr::Nested(inner) => format!("({})", inner.to_sql()),
133            Expr::NextVal(seq) => format!("nextval('{seq}')"),
134            Expr::CurrentTimestamp => "CURRENT_TIMESTAMP".to_string(),
135            Expr::Raw(sql) => sql.clone(),
136        }
137    }
138}