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