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