Skip to main content

pg2sqlite_core/transform/
expr_map.rs

1/// PostgreSQL expression → SQLite expression conversion.
2use crate::diagnostics::warning::{self, Severity, Warning};
3use crate::ir::Expr;
4
5/// Convert a PG expression to a SQLite-compatible expression.
6/// Returns None if the expression should be dropped entirely.
7pub fn map_expr(expr: &Expr, object: &str, warnings: &mut Vec<Warning>) -> Option<Expr> {
8    match expr {
9        // Literals pass through
10        Expr::IntegerLiteral(_) | Expr::FloatLiteral(_) | Expr::StringLiteral(_) | Expr::Null => {
11            Some(expr.clone())
12        }
13
14        // Boolean → 0/1
15        Expr::BooleanLiteral(b) => Some(Expr::IntegerLiteral(if *b { 1 } else { 0 })),
16
17        // Column references pass through
18        Expr::ColumnRef(_) => Some(expr.clone()),
19
20        // CURRENT_TIMESTAMP passes through
21        Expr::CurrentTimestamp => Some(Expr::CurrentTimestamp),
22
23        // nextval('seq') → removed
24        Expr::NextVal(seq) => {
25            warnings.push(
26                Warning::new(
27                    warning::NEXTVAL_REMOVED,
28                    Severity::Lossy,
29                    format!("nextval('{seq}') default removed"),
30                )
31                .with_object(object),
32            );
33            None
34        }
35
36        // Function calls
37        Expr::FunctionCall { name, args } => map_function_call(name, args, object, warnings),
38
39        // Cast → strip the cast, keep the inner expression
40        Expr::Cast {
41            expr: inner,
42            type_name,
43        } => {
44            warnings.push(
45                Warning::new(
46                    warning::CAST_REMOVED,
47                    Severity::Info,
48                    format!("cast to {type_name} removed"),
49                )
50                .with_object(object),
51            );
52            map_expr(inner, object, warnings)
53        }
54
55        // Binary operations — recursively convert both sides
56        Expr::BinaryOp { left, op, right } => {
57            let left = map_expr(left, object, warnings)?;
58            let right = map_expr(right, object, warnings)?;
59            Some(Expr::BinaryOp {
60                left: std::boxed::Box::new(left),
61                op: op.clone(),
62                right: std::boxed::Box::new(right),
63            })
64        }
65
66        // Unary operations
67        Expr::UnaryOp { op, expr: inner } => {
68            let mapped = map_expr(inner, object, warnings)?;
69            Some(Expr::UnaryOp {
70                op: op.clone(),
71                expr: std::boxed::Box::new(mapped),
72            })
73        }
74
75        // IS NULL / IS NOT NULL
76        Expr::IsNull {
77            expr: inner,
78            negated,
79        } => {
80            let mapped = map_expr(inner, object, warnings)?;
81            Some(Expr::IsNull {
82                expr: std::boxed::Box::new(mapped),
83                negated: *negated,
84            })
85        }
86
87        // IN list
88        Expr::InList {
89            expr: inner,
90            list,
91            negated,
92        } => {
93            let mapped_expr = map_expr(inner, object, warnings)?;
94            let mapped_list: Vec<Expr> = list
95                .iter()
96                .filter_map(|e| map_expr(e, object, warnings))
97                .collect();
98            Some(Expr::InList {
99                expr: std::boxed::Box::new(mapped_expr),
100                list: mapped_list,
101                negated: *negated,
102            })
103        }
104
105        // BETWEEN
106        Expr::Between {
107            expr: inner,
108            low,
109            high,
110            negated,
111        } => {
112            let mapped = map_expr(inner, object, warnings)?;
113            let mapped_low = map_expr(low, object, warnings)?;
114            let mapped_high = map_expr(high, object, warnings)?;
115            Some(Expr::Between {
116                expr: std::boxed::Box::new(mapped),
117                low: std::boxed::Box::new(mapped_low),
118                high: std::boxed::Box::new(mapped_high),
119                negated: *negated,
120            })
121        }
122
123        // Nested expressions
124        Expr::Nested(inner) => {
125            let mapped = map_expr(inner, object, warnings)?;
126            Some(Expr::Nested(std::boxed::Box::new(mapped)))
127        }
128
129        // Raw SQL — pass through (best effort)
130        Expr::Raw(_) => Some(expr.clone()),
131    }
132}
133
134fn map_function_call(
135    name: &str,
136    args: &[Expr],
137    object: &str,
138    warnings: &mut Vec<Warning>,
139) -> Option<Expr> {
140    match name {
141        // now() → CURRENT_TIMESTAMP
142        "now" => Some(Expr::CurrentTimestamp),
143
144        // lower(), upper(), length(), abs(), max(), min() — SQLite-compatible
145        "lower" | "upper" | "length" | "abs" | "max" | "min" | "coalesce" | "nullif" | "typeof"
146        | "trim" | "ltrim" | "rtrim" | "replace" | "substr" | "instr" | "hex" | "quote"
147        | "round" | "random" | "unicode" | "zeroblob" | "total" | "sum" | "avg" | "count"
148        | "group_concat" => {
149            let mapped_args: Vec<Expr> = args
150                .iter()
151                .filter_map(|a| map_expr(a, object, warnings))
152                .collect();
153            Some(Expr::FunctionCall {
154                name: name.to_string(),
155                args: mapped_args,
156            })
157        }
158
159        // PG-specific functions → drop with warning
160        _ => {
161            warnings.push(
162                Warning::new(
163                    warning::DEFAULT_UNSUPPORTED,
164                    Severity::Unsupported,
165                    format!("unsupported function '{name}()' in expression"),
166                )
167                .with_object(object),
168            );
169            None
170        }
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_literal_passthrough() {
180        let mut w = Vec::new();
181        let expr = Expr::IntegerLiteral(42);
182        assert_eq!(
183            map_expr(&expr, "t.c", &mut w),
184            Some(Expr::IntegerLiteral(42))
185        );
186        assert!(w.is_empty());
187    }
188
189    #[test]
190    fn test_boolean_to_integer() {
191        let mut w = Vec::new();
192        assert_eq!(
193            map_expr(&Expr::BooleanLiteral(true), "t.c", &mut w),
194            Some(Expr::IntegerLiteral(1))
195        );
196        assert_eq!(
197            map_expr(&Expr::BooleanLiteral(false), "t.c", &mut w),
198            Some(Expr::IntegerLiteral(0))
199        );
200    }
201
202    #[test]
203    fn test_now_to_current_timestamp() {
204        let mut w = Vec::new();
205        let expr = Expr::FunctionCall {
206            name: "now".to_string(),
207            args: vec![],
208        };
209        assert_eq!(map_expr(&expr, "t.c", &mut w), Some(Expr::CurrentTimestamp));
210    }
211
212    #[test]
213    fn test_nextval_removed() {
214        let mut w = Vec::new();
215        let expr = Expr::NextVal("users_id_seq".to_string());
216        assert_eq!(map_expr(&expr, "t.id", &mut w), None);
217        assert_eq!(w[0].code, warning::NEXTVAL_REMOVED);
218    }
219
220    #[test]
221    fn test_cast_stripped() {
222        let mut w = Vec::new();
223        let expr = Expr::Cast {
224            expr: std::boxed::Box::new(Expr::IntegerLiteral(42)),
225            type_name: "integer".to_string(),
226        };
227        assert_eq!(
228            map_expr(&expr, "t.c", &mut w),
229            Some(Expr::IntegerLiteral(42))
230        );
231        assert_eq!(w[0].code, warning::CAST_REMOVED);
232    }
233
234    #[test]
235    fn test_unsupported_function() {
236        let mut w = Vec::new();
237        let expr = Expr::FunctionCall {
238            name: "gen_random_uuid".to_string(),
239            args: vec![],
240        };
241        assert_eq!(map_expr(&expr, "t.c", &mut w), None);
242        assert_eq!(w[0].code, warning::DEFAULT_UNSUPPORTED);
243    }
244
245    #[test]
246    fn test_compatible_function_passthrough() {
247        let mut w = Vec::new();
248        let expr = Expr::FunctionCall {
249            name: "lower".to_string(),
250            args: vec![Expr::ColumnRef("name".to_string())],
251        };
252        let result = map_expr(&expr, "t.c", &mut w);
253        assert!(result.is_some());
254        assert!(w.is_empty());
255    }
256}