Skip to main content

polyglot_sql/dialects/
materialize.rs

1//! Materialize Dialect
2//!
3//! Materialize-specific transformations based on sqlglot patterns.
4//! Materialize is PostgreSQL-compatible with streaming SQL extensions.
5
6use super::{DialectImpl, DialectType};
7use crate::error::Result;
8use crate::expressions::{AggFunc, Case, Cast, Expression, Function, VarArgFunc};
9use crate::generator::GeneratorConfig;
10use crate::tokens::TokenizerConfig;
11
12/// Materialize dialect (PostgreSQL-compatible streaming database)
13pub struct MaterializeDialect;
14
15impl DialectImpl for MaterializeDialect {
16    fn dialect_type(&self) -> DialectType {
17        DialectType::Materialize
18    }
19
20    fn tokenizer_config(&self) -> TokenizerConfig {
21        let mut config = TokenizerConfig::default();
22        // Materialize uses double quotes for identifiers (PostgreSQL-style)
23        config.identifiers.insert('"', '"');
24        // PostgreSQL-style nested comments supported
25        config.nested_comments = true;
26        config
27    }
28
29    fn generator_config(&self) -> GeneratorConfig {
30        use crate::generator::IdentifierQuoteStyle;
31        GeneratorConfig {
32            identifier_quote: '"',
33            identifier_quote_style: IdentifierQuoteStyle::DOUBLE_QUOTE,
34            dialect: Some(DialectType::Materialize),
35            single_string_interval: true,
36            ..Default::default()
37        }
38    }
39
40    fn transform_expr(&self, expr: Expression) -> Result<Expression> {
41        match expr {
42            // IFNULL -> COALESCE in Materialize
43            Expression::IfNull(f) => Ok(Expression::Coalesce(Box::new(VarArgFunc {
44                original_name: None,
45                expressions: vec![f.this, f.expression],
46            }))),
47
48            // NVL -> COALESCE in Materialize
49            Expression::Nvl(f) => Ok(Expression::Coalesce(Box::new(VarArgFunc {
50                original_name: None,
51                expressions: vec![f.this, f.expression],
52            }))),
53
54            // Coalesce with original_name (e.g., IFNULL parsed as Coalesce) -> clear original_name
55            Expression::Coalesce(mut f) => {
56                f.original_name = None;
57                Ok(Expression::Coalesce(f))
58            }
59
60            // TryCast -> not directly supported, use CAST
61            Expression::TryCast(c) => Ok(Expression::Cast(c)),
62
63            // SafeCast -> CAST in Materialize
64            Expression::SafeCast(c) => Ok(Expression::Cast(c)),
65
66            // ILIKE is native in Materialize (PostgreSQL-style)
67            Expression::ILike(op) => Ok(Expression::ILike(op)),
68
69            // CountIf -> SUM(CASE WHEN condition THEN 1 ELSE 0 END)
70            Expression::CountIf(f) => {
71                let case_expr = Expression::Case(Box::new(Case {
72                    operand: None,
73                    whens: vec![(f.this.clone(), Expression::number(1))],
74                    else_: Some(Expression::number(0)),
75                    comments: Vec::new(),
76                }));
77                Ok(Expression::Sum(Box::new(AggFunc {
78                    ignore_nulls: None,
79                    having_max: None,
80                    this: case_expr,
81                    distinct: f.distinct,
82                    filter: f.filter,
83                    order_by: Vec::new(),
84                    name: None,
85                    limit: None,
86                })))
87            }
88
89            // RAND -> RANDOM in Materialize (PostgreSQL-style)
90            Expression::Rand(r) => {
91                let _ = r.seed;
92                Ok(Expression::Random(crate::expressions::Random))
93            }
94
95            // Generic function transformations
96            Expression::Function(f) => self.transform_function(*f),
97
98            // Generic aggregate function transformations
99            Expression::AggregateFunction(f) => self.transform_aggregate_function(f),
100
101            // Cast transformations
102            Expression::Cast(c) => self.transform_cast(*c),
103
104            // Pass through everything else
105            _ => Ok(expr),
106        }
107    }
108}
109
110impl MaterializeDialect {
111    fn transform_function(&self, f: Function) -> Result<Expression> {
112        let name_upper = f.name.to_uppercase();
113        match name_upper.as_str() {
114            // IFNULL -> COALESCE
115            "IFNULL" if f.args.len() == 2 => Ok(Expression::Coalesce(Box::new(VarArgFunc {
116                original_name: None,
117                expressions: f.args,
118            }))),
119
120            // NVL -> COALESCE
121            "NVL" if f.args.len() == 2 => Ok(Expression::Coalesce(Box::new(VarArgFunc {
122                original_name: None,
123                expressions: f.args,
124            }))),
125
126            // ISNULL -> COALESCE
127            "ISNULL" if f.args.len() == 2 => Ok(Expression::Coalesce(Box::new(VarArgFunc {
128                original_name: None,
129                expressions: f.args,
130            }))),
131
132            // NOW is native in Materialize
133            "NOW" => Ok(Expression::CurrentTimestamp(
134                crate::expressions::CurrentTimestamp {
135                    precision: None,
136                    sysdate: false,
137                },
138            )),
139
140            // GETDATE -> NOW
141            "GETDATE" => Ok(Expression::CurrentTimestamp(
142                crate::expressions::CurrentTimestamp {
143                    precision: None,
144                    sysdate: false,
145                },
146            )),
147
148            // RAND -> RANDOM
149            "RAND" => Ok(Expression::Random(crate::expressions::Random)),
150
151            // STRING_AGG is native in Materialize (PostgreSQL-style)
152            "STRING_AGG" => Ok(Expression::Function(Box::new(f))),
153
154            // GROUP_CONCAT -> STRING_AGG
155            "GROUP_CONCAT" if !f.args.is_empty() => Ok(Expression::Function(Box::new(
156                Function::new("STRING_AGG".to_string(), f.args),
157            ))),
158
159            // LISTAGG -> STRING_AGG
160            "LISTAGG" if !f.args.is_empty() => Ok(Expression::Function(Box::new(Function::new(
161                "STRING_AGG".to_string(),
162                f.args,
163            )))),
164
165            // SUBSTR -> SUBSTRING
166            "SUBSTR" => Ok(Expression::Function(Box::new(Function::new(
167                "SUBSTRING".to_string(),
168                f.args,
169            )))),
170
171            // LENGTH is native in Materialize
172            "LENGTH" => Ok(Expression::Function(Box::new(f))),
173
174            // LEN -> LENGTH
175            "LEN" if f.args.len() == 1 => Ok(Expression::Function(Box::new(Function::new(
176                "LENGTH".to_string(),
177                f.args,
178            )))),
179
180            // CHARINDEX -> STRPOS (with swapped args)
181            "CHARINDEX" if f.args.len() >= 2 => {
182                let mut args = f.args;
183                let substring = args.remove(0);
184                let string = args.remove(0);
185                Ok(Expression::Function(Box::new(Function::new(
186                    "STRPOS".to_string(),
187                    vec![string, substring],
188                ))))
189            }
190
191            // INSTR -> STRPOS
192            "INSTR" if f.args.len() >= 2 => Ok(Expression::Function(Box::new(Function::new(
193                "STRPOS".to_string(),
194                f.args,
195            )))),
196
197            // LOCATE -> STRPOS (with swapped args)
198            "LOCATE" if f.args.len() >= 2 => {
199                let mut args = f.args;
200                let substring = args.remove(0);
201                let string = args.remove(0);
202                Ok(Expression::Function(Box::new(Function::new(
203                    "STRPOS".to_string(),
204                    vec![string, substring],
205                ))))
206            }
207
208            // STRPOS is native in Materialize
209            "STRPOS" => Ok(Expression::Function(Box::new(f))),
210
211            // ARRAY_LENGTH is native in Materialize
212            "ARRAY_LENGTH" => Ok(Expression::Function(Box::new(f))),
213
214            // SIZE -> ARRAY_LENGTH
215            "SIZE" if f.args.len() == 1 => Ok(Expression::Function(Box::new(Function::new(
216                "ARRAY_LENGTH".to_string(),
217                f.args,
218            )))),
219
220            // CARDINALITY is native in Materialize
221            "CARDINALITY" => Ok(Expression::Function(Box::new(f))),
222
223            // TO_CHAR is native in Materialize
224            "TO_CHAR" => Ok(Expression::Function(Box::new(f))),
225
226            // DATE_FORMAT -> TO_CHAR
227            "DATE_FORMAT" if f.args.len() >= 2 => Ok(Expression::Function(Box::new(
228                Function::new("TO_CHAR".to_string(), f.args),
229            ))),
230
231            // strftime -> TO_CHAR
232            "STRFTIME" if f.args.len() >= 2 => {
233                let mut args = f.args;
234                let format = args.remove(0);
235                let date = args.remove(0);
236                Ok(Expression::Function(Box::new(Function::new(
237                    "TO_CHAR".to_string(),
238                    vec![date, format],
239                ))))
240            }
241
242            // JSON_EXTRACT_PATH_TEXT is native in Materialize
243            "JSON_EXTRACT_PATH_TEXT" => Ok(Expression::Function(Box::new(f))),
244
245            // GET_JSON_OBJECT -> JSON_EXTRACT_PATH_TEXT
246            "GET_JSON_OBJECT" if f.args.len() == 2 => Ok(Expression::Function(Box::new(
247                Function::new("JSON_EXTRACT_PATH_TEXT".to_string(), f.args),
248            ))),
249
250            // JSON_EXTRACT -> JSON_EXTRACT_PATH_TEXT
251            "JSON_EXTRACT" if f.args.len() >= 2 => Ok(Expression::Function(Box::new(
252                Function::new("JSON_EXTRACT_PATH_TEXT".to_string(), f.args),
253            ))),
254
255            // Pass through everything else
256            _ => Ok(Expression::Function(Box::new(f))),
257        }
258    }
259
260    fn transform_aggregate_function(
261        &self,
262        f: Box<crate::expressions::AggregateFunction>,
263    ) -> Result<Expression> {
264        let name_upper = f.name.to_uppercase();
265        match name_upper.as_str() {
266            // COUNT_IF -> SUM(CASE WHEN...)
267            "COUNT_IF" if !f.args.is_empty() => {
268                let condition = f.args.into_iter().next().unwrap();
269                let case_expr = Expression::Case(Box::new(Case {
270                    operand: None,
271                    whens: vec![(condition, Expression::number(1))],
272                    else_: Some(Expression::number(0)),
273                    comments: Vec::new(),
274                }));
275                Ok(Expression::Sum(Box::new(AggFunc {
276                    ignore_nulls: None,
277                    having_max: None,
278                    this: case_expr,
279                    distinct: f.distinct,
280                    filter: f.filter,
281                    order_by: Vec::new(),
282                    name: None,
283                    limit: None,
284                })))
285            }
286
287            // Pass through everything else
288            _ => Ok(Expression::AggregateFunction(f)),
289        }
290    }
291
292    fn transform_cast(&self, c: Cast) -> Result<Expression> {
293        // Materialize type mappings are handled in the generator
294        Ok(Expression::Cast(Box::new(c)))
295    }
296}