polyglot_sql/dialects/mod.rs
1//! SQL Dialect System
2//!
3//! This module implements the dialect abstraction layer that enables SQL transpilation
4//! between 30+ database engines. Each dialect encapsulates three concerns:
5//!
6//! - **Tokenization**: Dialect-specific lexing rules (e.g., BigQuery uses backtick quoting,
7//! MySQL uses backtick for identifiers, TSQL uses square brackets).
8//! - **Generation**: How AST nodes are rendered back to SQL text, including identifier quoting
9//! style, function name casing, and syntax variations.
10//! - **Transformation**: AST-level rewrites that convert dialect-specific constructs to/from
11//! a normalized form (e.g., Snowflake `SQUARE(x)` becomes `POWER(x, 2)`).
12//!
13//! The primary entry point is [`Dialect::get`], which returns a configured [`Dialect`] instance
14//! for a given [`DialectType`]. From there, callers can [`parse`](Dialect::parse),
15//! [`generate`](Dialect::generate), [`transform`](Dialect::transform), or
16//! [`transpile`](Dialect::transpile) to another dialect in a single call.
17//!
18//! Each concrete dialect (e.g., `PostgresDialect`, `BigQueryDialect`) implements the
19//! [`DialectImpl`] trait, which provides configuration hooks and expression-level transforms.
20//! Dialect modules live in submodules of this module and are re-exported here.
21
22mod generic; // Always compiled
23
24#[cfg(feature = "dialect-athena")]
25mod athena;
26#[cfg(feature = "dialect-bigquery")]
27mod bigquery;
28#[cfg(feature = "dialect-clickhouse")]
29mod clickhouse;
30#[cfg(feature = "dialect-cockroachdb")]
31mod cockroachdb;
32#[cfg(feature = "dialect-databricks")]
33mod databricks;
34#[cfg(feature = "dialect-datafusion")]
35mod datafusion;
36#[cfg(feature = "dialect-doris")]
37mod doris;
38#[cfg(feature = "dialect-dremio")]
39mod dremio;
40#[cfg(feature = "dialect-drill")]
41mod drill;
42#[cfg(feature = "dialect-druid")]
43mod druid;
44#[cfg(feature = "dialect-duckdb")]
45mod duckdb;
46#[cfg(feature = "dialect-dune")]
47mod dune;
48#[cfg(feature = "dialect-exasol")]
49mod exasol;
50#[cfg(feature = "dialect-fabric")]
51mod fabric;
52#[cfg(feature = "dialect-hive")]
53mod hive;
54#[cfg(feature = "dialect-materialize")]
55mod materialize;
56#[cfg(feature = "dialect-mysql")]
57mod mysql;
58#[cfg(feature = "dialect-oracle")]
59mod oracle;
60#[cfg(feature = "dialect-postgresql")]
61mod postgres;
62#[cfg(feature = "dialect-presto")]
63mod presto;
64#[cfg(feature = "dialect-redshift")]
65mod redshift;
66#[cfg(feature = "dialect-risingwave")]
67mod risingwave;
68#[cfg(feature = "dialect-singlestore")]
69mod singlestore;
70#[cfg(feature = "dialect-snowflake")]
71mod snowflake;
72#[cfg(feature = "dialect-solr")]
73mod solr;
74#[cfg(feature = "dialect-spark")]
75mod spark;
76#[cfg(feature = "dialect-sqlite")]
77mod sqlite;
78#[cfg(feature = "dialect-starrocks")]
79mod starrocks;
80#[cfg(feature = "dialect-tableau")]
81mod tableau;
82#[cfg(feature = "dialect-teradata")]
83mod teradata;
84#[cfg(feature = "dialect-tidb")]
85mod tidb;
86#[cfg(feature = "dialect-trino")]
87mod trino;
88#[cfg(feature = "dialect-tsql")]
89mod tsql;
90
91pub use generic::GenericDialect; // Always available
92
93#[cfg(feature = "dialect-athena")]
94pub use athena::AthenaDialect;
95#[cfg(feature = "dialect-bigquery")]
96pub use bigquery::BigQueryDialect;
97#[cfg(feature = "dialect-clickhouse")]
98pub use clickhouse::ClickHouseDialect;
99#[cfg(feature = "dialect-cockroachdb")]
100pub use cockroachdb::CockroachDBDialect;
101#[cfg(feature = "dialect-databricks")]
102pub use databricks::DatabricksDialect;
103#[cfg(feature = "dialect-datafusion")]
104pub use datafusion::DataFusionDialect;
105#[cfg(feature = "dialect-doris")]
106pub use doris::DorisDialect;
107#[cfg(feature = "dialect-dremio")]
108pub use dremio::DremioDialect;
109#[cfg(feature = "dialect-drill")]
110pub use drill::DrillDialect;
111#[cfg(feature = "dialect-druid")]
112pub use druid::DruidDialect;
113#[cfg(feature = "dialect-duckdb")]
114pub use duckdb::DuckDBDialect;
115#[cfg(feature = "dialect-dune")]
116pub use dune::DuneDialect;
117#[cfg(feature = "dialect-exasol")]
118pub use exasol::ExasolDialect;
119#[cfg(feature = "dialect-fabric")]
120pub use fabric::FabricDialect;
121#[cfg(feature = "dialect-hive")]
122pub use hive::HiveDialect;
123#[cfg(feature = "dialect-materialize")]
124pub use materialize::MaterializeDialect;
125#[cfg(feature = "dialect-mysql")]
126pub use mysql::MySQLDialect;
127#[cfg(feature = "dialect-oracle")]
128pub use oracle::OracleDialect;
129#[cfg(feature = "dialect-postgresql")]
130pub use postgres::PostgresDialect;
131#[cfg(feature = "dialect-presto")]
132pub use presto::PrestoDialect;
133#[cfg(feature = "dialect-redshift")]
134pub use redshift::RedshiftDialect;
135#[cfg(feature = "dialect-risingwave")]
136pub use risingwave::RisingWaveDialect;
137#[cfg(feature = "dialect-singlestore")]
138pub use singlestore::SingleStoreDialect;
139#[cfg(feature = "dialect-snowflake")]
140pub use snowflake::SnowflakeDialect;
141#[cfg(feature = "dialect-solr")]
142pub use solr::SolrDialect;
143#[cfg(feature = "dialect-spark")]
144pub use spark::SparkDialect;
145#[cfg(feature = "dialect-sqlite")]
146pub use sqlite::SQLiteDialect;
147#[cfg(feature = "dialect-starrocks")]
148pub use starrocks::StarRocksDialect;
149#[cfg(feature = "dialect-tableau")]
150pub use tableau::TableauDialect;
151#[cfg(feature = "dialect-teradata")]
152pub use teradata::TeradataDialect;
153#[cfg(feature = "dialect-tidb")]
154pub use tidb::TiDBDialect;
155#[cfg(feature = "dialect-trino")]
156pub use trino::TrinoDialect;
157#[cfg(feature = "dialect-tsql")]
158pub use tsql::TSQLDialect;
159
160use crate::error::Result;
161#[cfg(feature = "transpile")]
162use crate::expressions::{Cast, ColumnConstraint, Function, Identifier, Literal};
163use crate::expressions::{DataType, Expression};
164#[cfg(any(
165 feature = "transpile",
166 feature = "ast-tools",
167 feature = "generate",
168 feature = "semantic"
169))]
170use crate::expressions::{From, FunctionBody, Join, Null, OrderBy, OutputClause, TableRef, With};
171#[cfg(feature = "transpile")]
172use crate::generator::UnsupportedLevel;
173#[cfg(feature = "generate")]
174use crate::generator::{Generator, GeneratorConfig};
175#[cfg(feature = "transpile")]
176use crate::guard::enforce_generate_ast;
177use crate::guard::{enforce_input, ComplexityGuardOptions};
178use crate::parser::Parser;
179#[cfg(feature = "transpile")]
180use crate::tokens::TokenType;
181use crate::tokens::{Token, Tokenizer, TokenizerConfig};
182#[cfg(feature = "transpile")]
183use crate::traversal::ExpressionWalk;
184use serde::{Deserialize, Serialize};
185use std::collections::HashMap;
186use std::sync::{Arc, LazyLock, RwLock};
187
188/// Enumeration of all supported SQL dialects.
189///
190/// Each variant corresponds to a specific SQL database engine or query language.
191/// The `Generic` variant represents standard SQL with no dialect-specific behavior,
192/// and is used as the default when no dialect is specified.
193///
194/// Dialect names are case-insensitive when parsed from strings via [`FromStr`].
195/// Some dialects accept aliases (e.g., "mssql" and "sqlserver" both resolve to [`TSQL`](DialectType::TSQL)).
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
197#[serde(rename_all = "lowercase")]
198pub enum DialectType {
199 /// Standard SQL with no dialect-specific behavior (default).
200 Generic,
201 /// PostgreSQL -- advanced open-source relational database.
202 PostgreSQL,
203 /// MySQL -- widely-used open-source relational database (also accepts "mysql").
204 MySQL,
205 /// Google BigQuery -- serverless cloud data warehouse with unique syntax (backtick quoting, STRUCT types, QUALIFY).
206 BigQuery,
207 /// Snowflake -- cloud data platform with QUALIFY clause, FLATTEN, and variant types.
208 Snowflake,
209 /// DuckDB -- in-process analytical database with modern SQL extensions.
210 DuckDB,
211 /// SQLite -- lightweight embedded relational database.
212 SQLite,
213 /// Apache Hive -- data warehouse on Hadoop with HiveQL syntax.
214 Hive,
215 /// Apache Spark SQL -- distributed query engine (also accepts "spark2").
216 Spark,
217 /// Trino -- distributed SQL query engine (formerly PrestoSQL).
218 Trino,
219 /// PrestoDB -- distributed SQL query engine for big data.
220 Presto,
221 /// Amazon Redshift -- cloud data warehouse based on PostgreSQL.
222 Redshift,
223 /// Transact-SQL (T-SQL) -- Microsoft SQL Server and Azure SQL (also accepts "mssql", "sqlserver").
224 TSQL,
225 /// Oracle Database -- commercial relational database with PL/SQL extensions.
226 Oracle,
227 /// ClickHouse -- column-oriented OLAP database for real-time analytics.
228 ClickHouse,
229 /// Databricks SQL -- Spark-based lakehouse platform with QUALIFY support.
230 Databricks,
231 /// Amazon Athena -- serverless query service (hybrid Trino/Hive engine).
232 Athena,
233 /// Teradata -- enterprise data warehouse with proprietary SQL extensions.
234 Teradata,
235 /// Apache Doris -- real-time analytical database (MySQL-compatible).
236 Doris,
237 /// StarRocks -- sub-second OLAP database (MySQL-compatible).
238 StarRocks,
239 /// Materialize -- streaming SQL database built on differential dataflow.
240 Materialize,
241 /// RisingWave -- distributed streaming database with PostgreSQL compatibility.
242 RisingWave,
243 /// SingleStore (formerly MemSQL) -- distributed SQL database (also accepts "memsql").
244 SingleStore,
245 /// CockroachDB -- distributed SQL database with PostgreSQL compatibility (also accepts "cockroach").
246 CockroachDB,
247 /// TiDB -- distributed HTAP database with MySQL compatibility.
248 TiDB,
249 /// Apache Druid -- real-time analytics database.
250 Druid,
251 /// Apache Solr -- search platform with SQL interface.
252 Solr,
253 /// Tableau -- data visualization platform with its own SQL dialect.
254 Tableau,
255 /// Dune Analytics -- blockchain analytics SQL engine.
256 Dune,
257 /// Microsoft Fabric -- unified analytics platform (T-SQL based).
258 Fabric,
259 /// Apache Drill -- schema-free SQL query engine for big data.
260 Drill,
261 /// Dremio -- data lakehouse platform with Arrow-based query engine.
262 Dremio,
263 /// Exasol -- in-memory analytic database.
264 Exasol,
265 /// Apache DataFusion -- Arrow-based query engine with modern SQL extensions.
266 DataFusion,
267}
268
269impl Default for DialectType {
270 fn default() -> Self {
271 DialectType::Generic
272 }
273}
274
275impl std::fmt::Display for DialectType {
276 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
277 match self {
278 DialectType::Generic => write!(f, "generic"),
279 DialectType::PostgreSQL => write!(f, "postgresql"),
280 DialectType::MySQL => write!(f, "mysql"),
281 DialectType::BigQuery => write!(f, "bigquery"),
282 DialectType::Snowflake => write!(f, "snowflake"),
283 DialectType::DuckDB => write!(f, "duckdb"),
284 DialectType::SQLite => write!(f, "sqlite"),
285 DialectType::Hive => write!(f, "hive"),
286 DialectType::Spark => write!(f, "spark"),
287 DialectType::Trino => write!(f, "trino"),
288 DialectType::Presto => write!(f, "presto"),
289 DialectType::Redshift => write!(f, "redshift"),
290 DialectType::TSQL => write!(f, "tsql"),
291 DialectType::Oracle => write!(f, "oracle"),
292 DialectType::ClickHouse => write!(f, "clickhouse"),
293 DialectType::Databricks => write!(f, "databricks"),
294 DialectType::Athena => write!(f, "athena"),
295 DialectType::Teradata => write!(f, "teradata"),
296 DialectType::Doris => write!(f, "doris"),
297 DialectType::StarRocks => write!(f, "starrocks"),
298 DialectType::Materialize => write!(f, "materialize"),
299 DialectType::RisingWave => write!(f, "risingwave"),
300 DialectType::SingleStore => write!(f, "singlestore"),
301 DialectType::CockroachDB => write!(f, "cockroachdb"),
302 DialectType::TiDB => write!(f, "tidb"),
303 DialectType::Druid => write!(f, "druid"),
304 DialectType::Solr => write!(f, "solr"),
305 DialectType::Tableau => write!(f, "tableau"),
306 DialectType::Dune => write!(f, "dune"),
307 DialectType::Fabric => write!(f, "fabric"),
308 DialectType::Drill => write!(f, "drill"),
309 DialectType::Dremio => write!(f, "dremio"),
310 DialectType::Exasol => write!(f, "exasol"),
311 DialectType::DataFusion => write!(f, "datafusion"),
312 }
313 }
314}
315
316impl std::str::FromStr for DialectType {
317 type Err = crate::error::Error;
318
319 fn from_str(s: &str) -> Result<Self> {
320 match s.to_ascii_lowercase().as_str() {
321 "generic" | "" => Ok(DialectType::Generic),
322 "postgres" | "postgresql" => Ok(DialectType::PostgreSQL),
323 "mysql" => Ok(DialectType::MySQL),
324 "bigquery" => Ok(DialectType::BigQuery),
325 "snowflake" => Ok(DialectType::Snowflake),
326 "duckdb" => Ok(DialectType::DuckDB),
327 "sqlite" => Ok(DialectType::SQLite),
328 "hive" => Ok(DialectType::Hive),
329 "spark" | "spark2" => Ok(DialectType::Spark),
330 "trino" => Ok(DialectType::Trino),
331 "presto" => Ok(DialectType::Presto),
332 "redshift" => Ok(DialectType::Redshift),
333 "tsql" | "mssql" | "sqlserver" => Ok(DialectType::TSQL),
334 "oracle" => Ok(DialectType::Oracle),
335 "clickhouse" => Ok(DialectType::ClickHouse),
336 "databricks" => Ok(DialectType::Databricks),
337 "athena" => Ok(DialectType::Athena),
338 "teradata" => Ok(DialectType::Teradata),
339 "doris" => Ok(DialectType::Doris),
340 "starrocks" => Ok(DialectType::StarRocks),
341 "materialize" => Ok(DialectType::Materialize),
342 "risingwave" => Ok(DialectType::RisingWave),
343 "singlestore" | "memsql" => Ok(DialectType::SingleStore),
344 "cockroachdb" | "cockroach" => Ok(DialectType::CockroachDB),
345 "tidb" => Ok(DialectType::TiDB),
346 "druid" => Ok(DialectType::Druid),
347 "solr" => Ok(DialectType::Solr),
348 "tableau" => Ok(DialectType::Tableau),
349 "dune" => Ok(DialectType::Dune),
350 "fabric" => Ok(DialectType::Fabric),
351 "drill" => Ok(DialectType::Drill),
352 "dremio" => Ok(DialectType::Dremio),
353 "exasol" => Ok(DialectType::Exasol),
354 "datafusion" | "arrow-datafusion" | "arrow_datafusion" => Ok(DialectType::DataFusion),
355 _ => Err(crate::error::Error::parse(
356 format!("Unknown dialect: {}", s),
357 0,
358 0,
359 0,
360 0,
361 )),
362 }
363 }
364}
365
366/// Trait that each concrete SQL dialect must implement.
367///
368/// `DialectImpl` provides the configuration hooks and per-expression transform logic
369/// that distinguish one dialect from another. Implementors supply:
370///
371/// - A [`DialectType`] identifier.
372/// - Optional overrides for tokenizer and generator configuration (defaults to generic SQL).
373/// - An expression-level transform function ([`transform_expr`](DialectImpl::transform_expr))
374/// that rewrites individual AST nodes for this dialect (e.g., converting `NVL` to `COALESCE`).
375/// - An optional preprocessing step ([`preprocess`](DialectImpl::preprocess)) for whole-tree
376/// rewrites that must run before the recursive per-node transform (e.g., eliminating QUALIFY).
377///
378/// The default implementations are no-ops, so a minimal dialect only needs to provide
379/// [`dialect_type`](DialectImpl::dialect_type) and override the methods that differ from
380/// standard SQL.
381pub trait DialectImpl {
382 /// Returns the [`DialectType`] that identifies this dialect.
383 fn dialect_type(&self) -> DialectType;
384
385 /// Returns the tokenizer configuration for this dialect.
386 ///
387 /// Override to customize identifier quoting characters, string escape rules,
388 /// comment styles, and other lexing behavior.
389 fn tokenizer_config(&self) -> TokenizerConfig {
390 TokenizerConfig::default()
391 }
392
393 /// Returns the generator configuration for this dialect.
394 ///
395 /// Override to customize identifier quoting style, function name casing,
396 /// keyword casing, and other SQL generation behavior.
397 #[cfg(feature = "generate")]
398 fn generator_config(&self) -> GeneratorConfig {
399 GeneratorConfig::default()
400 }
401
402 /// Returns a generator configuration tailored to a specific expression.
403 ///
404 /// Override this for hybrid dialects like Athena that route to different SQL engines
405 /// based on expression type (e.g., Hive-style generation for DDL, Trino-style for DML).
406 /// The default delegates to [`generator_config`](DialectImpl::generator_config).
407 #[cfg(feature = "generate")]
408 fn generator_config_for_expr(&self, _expr: &Expression) -> GeneratorConfig {
409 self.generator_config()
410 }
411
412 /// Transforms a single expression node for this dialect, without recursing into children.
413 ///
414 /// This is the per-node rewrite hook invoked by [`transform_recursive`]. Return the
415 /// expression unchanged if no dialect-specific rewrite is needed. Transformations
416 /// typically include function renaming, operator substitution, and type mapping.
417 #[cfg(feature = "transpile")]
418 fn transform_expr(&self, expr: Expression) -> Result<Expression> {
419 Ok(expr)
420 }
421
422 /// Applies whole-tree preprocessing transforms before the recursive per-node pass.
423 ///
424 /// Override this to apply structural rewrites that must see the entire tree at once,
425 /// such as `eliminate_qualify`, `eliminate_distinct_on`, `ensure_bools`, or
426 /// `explode_projection_to_unnest`. The default is a no-op pass-through.
427 #[cfg(feature = "transpile")]
428 fn preprocess(&self, expr: Expression) -> Result<Expression> {
429 Ok(expr)
430 }
431}
432
433/// Recursively transforms a [`DataType`](crate::expressions::DataType), handling nested
434/// parametric types such as `ARRAY<INT>`, `STRUCT<a INT, b TEXT>`, and `MAP<STRING, INT>`.
435///
436/// The outer type is first passed through `transform_fn` as an `Expression::DataType`,
437/// and then nested element/field types are recursed into. This ensures that dialect-level
438/// type mappings (e.g., `INT` to `INTEGER`) propagate into complex nested types.
439#[cfg(any(
440 feature = "transpile",
441 feature = "ast-tools",
442 feature = "generate",
443 feature = "semantic"
444))]
445fn transform_data_type_recursive<F>(
446 dt: crate::expressions::DataType,
447 transform_fn: &F,
448) -> Result<crate::expressions::DataType>
449where
450 F: Fn(Expression) -> Result<Expression>,
451{
452 use crate::expressions::DataType;
453 // First, transform the outermost type through the expression system
454 let dt_expr = transform_fn(Expression::DataType(dt))?;
455 let dt = match dt_expr {
456 Expression::DataType(d) => d,
457 _ => {
458 return Ok(match dt_expr {
459 _ => DataType::Custom {
460 name: "UNKNOWN".to_string(),
461 },
462 })
463 }
464 };
465 // Then recurse into nested types
466 match dt {
467 DataType::Array {
468 element_type,
469 dimension,
470 } => {
471 let inner = transform_data_type_recursive(*element_type, transform_fn)?;
472 Ok(DataType::Array {
473 element_type: Box::new(inner),
474 dimension,
475 })
476 }
477 DataType::List { element_type } => {
478 let inner = transform_data_type_recursive(*element_type, transform_fn)?;
479 Ok(DataType::List {
480 element_type: Box::new(inner),
481 })
482 }
483 DataType::Struct { fields, nested } => {
484 let mut new_fields = Vec::new();
485 for mut field in fields {
486 field.data_type = transform_data_type_recursive(field.data_type, transform_fn)?;
487 new_fields.push(field);
488 }
489 Ok(DataType::Struct {
490 fields: new_fields,
491 nested,
492 })
493 }
494 DataType::Map {
495 key_type,
496 value_type,
497 } => {
498 let k = transform_data_type_recursive(*key_type, transform_fn)?;
499 let v = transform_data_type_recursive(*value_type, transform_fn)?;
500 Ok(DataType::Map {
501 key_type: Box::new(k),
502 value_type: Box::new(v),
503 })
504 }
505 other => Ok(other),
506 }
507}
508
509/// Convert DuckDB C-style format strings to Presto C-style format strings.
510/// DuckDB and Presto both use C-style % directives but with different specifiers for some cases.
511#[cfg(feature = "transpile")]
512fn duckdb_to_presto_format(fmt: &str) -> String {
513 // Order matters: handle longer patterns first to avoid partial replacements
514 let mut result = fmt.to_string();
515 // First pass: mark multi-char patterns with placeholders
516 result = result.replace("%-m", "\x01NOPADM\x01");
517 result = result.replace("%-d", "\x01NOPADD\x01");
518 result = result.replace("%-I", "\x01NOPADI\x01");
519 result = result.replace("%-H", "\x01NOPADH\x01");
520 result = result.replace("%H:%M:%S", "\x01HMS\x01");
521 result = result.replace("%Y-%m-%d", "\x01YMD\x01");
522 // Now convert individual specifiers
523 result = result.replace("%M", "%i");
524 result = result.replace("%S", "%s");
525 // Restore multi-char patterns with Presto equivalents
526 result = result.replace("\x01NOPADM\x01", "%c");
527 result = result.replace("\x01NOPADD\x01", "%e");
528 result = result.replace("\x01NOPADI\x01", "%l");
529 result = result.replace("\x01NOPADH\x01", "%k");
530 result = result.replace("\x01HMS\x01", "%T");
531 result = result.replace("\x01YMD\x01", "%Y-%m-%d");
532 result
533}
534
535/// Convert DuckDB C-style format strings to BigQuery format strings.
536/// BigQuery uses a mix of strftime-like directives.
537#[cfg(feature = "transpile")]
538fn duckdb_to_bigquery_format(fmt: &str) -> String {
539 let mut result = fmt.to_string();
540 // Handle longer patterns first
541 result = result.replace("%-d", "%e");
542 result = result.replace("%Y-%m-%d %H:%M:%S", "%F %T");
543 result = result.replace("%Y-%m-%d", "%F");
544 result = result.replace("%H:%M:%S", "%T");
545 result
546}
547
548#[cfg(feature = "transpile")]
549fn presto_to_java_format(fmt: &str) -> String {
550 fmt.replace("%Y", "yyyy")
551 .replace("%m", "MM")
552 .replace("%d", "dd")
553 .replace("%H", "HH")
554 .replace("%i", "mm")
555 .replace("%S", "ss")
556 .replace("%s", "ss")
557 .replace("%y", "yy")
558 .replace("%T", "HH:mm:ss")
559 .replace("%F", "yyyy-MM-dd")
560 .replace("%M", "MMMM")
561}
562
563#[cfg(feature = "transpile")]
564fn normalize_presto_format(fmt: &str) -> String {
565 fmt.replace("%H:%i:%S", "%T").replace("%H:%i:%s", "%T")
566}
567
568#[cfg(feature = "transpile")]
569fn presto_to_duckdb_format(fmt: &str) -> String {
570 fmt.replace("%i", "%M")
571 .replace("%s", "%S")
572 .replace("%T", "%H:%M:%S")
573}
574
575#[cfg(feature = "transpile")]
576fn presto_to_bigquery_format(fmt: &str) -> String {
577 fmt.replace("%Y-%m-%d", "%F")
578 .replace("%H:%i:%S", "%T")
579 .replace("%H:%i:%s", "%T")
580 .replace("%i", "%M")
581 .replace("%s", "%S")
582}
583
584#[cfg(feature = "transpile")]
585fn is_default_presto_timestamp_format(fmt: &str) -> bool {
586 let normalized = normalize_presto_format(fmt);
587 normalized == "%Y-%m-%d %T"
588 || normalized == "%Y-%m-%d %H:%i:%S"
589 || fmt == "%Y-%m-%d %H:%i:%S"
590 || fmt == "%Y-%m-%d %T"
591}
592
593#[cfg(feature = "transpile")]
594fn is_default_presto_date_format(fmt: &str) -> bool {
595 fmt == "%Y-%m-%d" || fmt == "%F"
596}
597
598#[cfg(any(
599 feature = "transpile",
600 feature = "ast-tools",
601 feature = "generate",
602 feature = "semantic"
603))]
604#[derive(Debug)]
605enum TransformTask {
606 Visit(Expression),
607 Finish(FinishTask),
608}
609
610#[cfg(any(
611 feature = "transpile",
612 feature = "ast-tools",
613 feature = "generate",
614 feature = "semantic"
615))]
616#[derive(Debug)]
617enum FinishTask {
618 Unary(Expression),
619 Binary(Expression),
620 CastLike(Expression),
621 List(Expression, usize),
622 From(crate::expressions::From, usize),
623 Select(SelectFrame),
624 SetOp(Expression),
625}
626
627#[cfg(any(
628 feature = "transpile",
629 feature = "ast-tools",
630 feature = "generate",
631 feature = "semantic"
632))]
633#[derive(Debug)]
634struct SelectFrame {
635 select: Box<crate::expressions::Select>,
636 expr_count: usize,
637 from_present: bool,
638 where_present: bool,
639 group_by_count: usize,
640 having_present: bool,
641 qualify_present: bool,
642}
643
644#[cfg(any(
645 feature = "transpile",
646 feature = "ast-tools",
647 feature = "generate",
648 feature = "semantic"
649))]
650fn transform_pop_result(results: &mut Vec<Expression>) -> Result<Expression> {
651 results
652 .pop()
653 .ok_or_else(|| crate::error::Error::Internal("transform stack underflow".to_string()))
654}
655
656#[cfg(any(
657 feature = "transpile",
658 feature = "ast-tools",
659 feature = "generate",
660 feature = "semantic"
661))]
662fn transform_pop_results(results: &mut Vec<Expression>, count: usize) -> Result<Vec<Expression>> {
663 if results.len() < count {
664 return Err(crate::error::Error::Internal(
665 "transform result stack underflow".to_string(),
666 ));
667 }
668 Ok(results.split_off(results.len() - count))
669}
670
671/// Applies a transform function bottom-up through an entire expression tree.
672///
673/// The public entrypoint uses an explicit task stack for the recursion-heavy shapes
674/// that dominate deeply nested SQL (nested SELECT/FROM/SUBQUERY chains, set-operation
675/// trees, and common binary/unary expression chains). Less common shapes currently
676/// reuse the reference recursive implementation so semantics stay identical while
677/// the hot path avoids stack growth.
678#[cfg(any(
679 feature = "transpile",
680 feature = "ast-tools",
681 feature = "generate",
682 feature = "semantic"
683))]
684pub fn transform_recursive<F>(expr: Expression, transform_fn: &F) -> Result<Expression>
685where
686 F: Fn(Expression) -> Result<Expression>,
687{
688 #[cfg(feature = "stacker")]
689 {
690 let red_zone = if cfg!(debug_assertions) {
691 4 * 1024 * 1024
692 } else {
693 1024 * 1024
694 };
695 stacker::maybe_grow(red_zone, 8 * 1024 * 1024, move || {
696 transform_recursive_inner(expr, transform_fn)
697 })
698 }
699 #[cfg(not(feature = "stacker"))]
700 {
701 transform_recursive_inner(expr, transform_fn)
702 }
703}
704
705#[cfg(any(
706 feature = "transpile",
707 feature = "ast-tools",
708 feature = "generate",
709 feature = "semantic"
710))]
711fn transform_recursive_inner<F>(expr: Expression, transform_fn: &F) -> Result<Expression>
712where
713 F: Fn(Expression) -> Result<Expression>,
714{
715 let mut tasks = vec![TransformTask::Visit(expr)];
716 let mut results = Vec::new();
717
718 while let Some(task) = tasks.pop() {
719 match task {
720 TransformTask::Visit(expr) => {
721 if matches!(
722 &expr,
723 Expression::Literal(_)
724 | Expression::Boolean(_)
725 | Expression::Null(_)
726 | Expression::Identifier(_)
727 | Expression::Star(_)
728 | Expression::Parameter(_)
729 | Expression::Placeholder(_)
730 | Expression::SessionParameter(_)
731 ) {
732 results.push(transform_fn(expr)?);
733 continue;
734 }
735
736 match expr {
737 Expression::Alias(mut alias) => {
738 let child = std::mem::replace(&mut alias.this, Expression::Null(Null));
739 tasks.push(TransformTask::Finish(FinishTask::Unary(Expression::Alias(
740 alias,
741 ))));
742 tasks.push(TransformTask::Visit(child));
743 }
744 Expression::Paren(mut paren) => {
745 let child = std::mem::replace(&mut paren.this, Expression::Null(Null));
746 tasks.push(TransformTask::Finish(FinishTask::Unary(Expression::Paren(
747 paren,
748 ))));
749 tasks.push(TransformTask::Visit(child));
750 }
751 Expression::Not(mut not) => {
752 let child = std::mem::replace(&mut not.this, Expression::Null(Null));
753 tasks.push(TransformTask::Finish(FinishTask::Unary(Expression::Not(
754 not,
755 ))));
756 tasks.push(TransformTask::Visit(child));
757 }
758 Expression::Neg(mut neg) => {
759 let child = std::mem::replace(&mut neg.this, Expression::Null(Null));
760 tasks.push(TransformTask::Finish(FinishTask::Unary(Expression::Neg(
761 neg,
762 ))));
763 tasks.push(TransformTask::Visit(child));
764 }
765 Expression::IsNull(mut expr) => {
766 let child = std::mem::replace(&mut expr.this, Expression::Null(Null));
767 tasks.push(TransformTask::Finish(FinishTask::Unary(
768 Expression::IsNull(expr),
769 )));
770 tasks.push(TransformTask::Visit(child));
771 }
772 Expression::IsTrue(mut expr) => {
773 let child = std::mem::replace(&mut expr.this, Expression::Null(Null));
774 tasks.push(TransformTask::Finish(FinishTask::Unary(
775 Expression::IsTrue(expr),
776 )));
777 tasks.push(TransformTask::Visit(child));
778 }
779 Expression::IsFalse(mut expr) => {
780 let child = std::mem::replace(&mut expr.this, Expression::Null(Null));
781 tasks.push(TransformTask::Finish(FinishTask::Unary(
782 Expression::IsFalse(expr),
783 )));
784 tasks.push(TransformTask::Visit(child));
785 }
786 Expression::Subquery(mut subquery) => {
787 let child = std::mem::replace(&mut subquery.this, Expression::Null(Null));
788 tasks.push(TransformTask::Finish(FinishTask::Unary(
789 Expression::Subquery(subquery),
790 )));
791 tasks.push(TransformTask::Visit(child));
792 }
793 Expression::Exists(mut exists) => {
794 let child = std::mem::replace(&mut exists.this, Expression::Null(Null));
795 tasks.push(TransformTask::Finish(FinishTask::Unary(
796 Expression::Exists(exists),
797 )));
798 tasks.push(TransformTask::Visit(child));
799 }
800 Expression::TableArgument(mut arg) => {
801 let child = std::mem::replace(&mut arg.this, Expression::Null(Null));
802 tasks.push(TransformTask::Finish(FinishTask::Unary(
803 Expression::TableArgument(arg),
804 )));
805 tasks.push(TransformTask::Visit(child));
806 }
807 Expression::And(mut op) => {
808 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
809 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
810 tasks.push(TransformTask::Finish(FinishTask::Binary(Expression::And(
811 op,
812 ))));
813 tasks.push(TransformTask::Visit(right));
814 tasks.push(TransformTask::Visit(left));
815 }
816 Expression::Or(mut op) => {
817 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
818 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
819 tasks.push(TransformTask::Finish(FinishTask::Binary(Expression::Or(
820 op,
821 ))));
822 tasks.push(TransformTask::Visit(right));
823 tasks.push(TransformTask::Visit(left));
824 }
825 Expression::Add(mut op) => {
826 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
827 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
828 tasks.push(TransformTask::Finish(FinishTask::Binary(Expression::Add(
829 op,
830 ))));
831 tasks.push(TransformTask::Visit(right));
832 tasks.push(TransformTask::Visit(left));
833 }
834 Expression::Sub(mut op) => {
835 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
836 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
837 tasks.push(TransformTask::Finish(FinishTask::Binary(Expression::Sub(
838 op,
839 ))));
840 tasks.push(TransformTask::Visit(right));
841 tasks.push(TransformTask::Visit(left));
842 }
843 Expression::Mul(mut op) => {
844 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
845 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
846 tasks.push(TransformTask::Finish(FinishTask::Binary(Expression::Mul(
847 op,
848 ))));
849 tasks.push(TransformTask::Visit(right));
850 tasks.push(TransformTask::Visit(left));
851 }
852 Expression::Div(mut op) => {
853 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
854 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
855 tasks.push(TransformTask::Finish(FinishTask::Binary(Expression::Div(
856 op,
857 ))));
858 tasks.push(TransformTask::Visit(right));
859 tasks.push(TransformTask::Visit(left));
860 }
861 Expression::Eq(mut op) => {
862 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
863 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
864 tasks.push(TransformTask::Finish(FinishTask::Binary(Expression::Eq(
865 op,
866 ))));
867 tasks.push(TransformTask::Visit(right));
868 tasks.push(TransformTask::Visit(left));
869 }
870 Expression::Lt(mut op) => {
871 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
872 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
873 tasks.push(TransformTask::Finish(FinishTask::Binary(Expression::Lt(
874 op,
875 ))));
876 tasks.push(TransformTask::Visit(right));
877 tasks.push(TransformTask::Visit(left));
878 }
879 Expression::Gt(mut op) => {
880 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
881 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
882 tasks.push(TransformTask::Finish(FinishTask::Binary(Expression::Gt(
883 op,
884 ))));
885 tasks.push(TransformTask::Visit(right));
886 tasks.push(TransformTask::Visit(left));
887 }
888 Expression::Neq(mut op) => {
889 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
890 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
891 tasks.push(TransformTask::Finish(FinishTask::Binary(Expression::Neq(
892 op,
893 ))));
894 tasks.push(TransformTask::Visit(right));
895 tasks.push(TransformTask::Visit(left));
896 }
897 Expression::Lte(mut op) => {
898 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
899 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
900 tasks.push(TransformTask::Finish(FinishTask::Binary(Expression::Lte(
901 op,
902 ))));
903 tasks.push(TransformTask::Visit(right));
904 tasks.push(TransformTask::Visit(left));
905 }
906 Expression::Gte(mut op) => {
907 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
908 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
909 tasks.push(TransformTask::Finish(FinishTask::Binary(Expression::Gte(
910 op,
911 ))));
912 tasks.push(TransformTask::Visit(right));
913 tasks.push(TransformTask::Visit(left));
914 }
915 Expression::Mod(mut op) => {
916 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
917 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
918 tasks.push(TransformTask::Finish(FinishTask::Binary(Expression::Mod(
919 op,
920 ))));
921 tasks.push(TransformTask::Visit(right));
922 tasks.push(TransformTask::Visit(left));
923 }
924 Expression::Concat(mut op) => {
925 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
926 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
927 tasks.push(TransformTask::Finish(FinishTask::Binary(
928 Expression::Concat(op),
929 )));
930 tasks.push(TransformTask::Visit(right));
931 tasks.push(TransformTask::Visit(left));
932 }
933 Expression::BitwiseAnd(mut op) => {
934 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
935 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
936 tasks.push(TransformTask::Finish(FinishTask::Binary(
937 Expression::BitwiseAnd(op),
938 )));
939 tasks.push(TransformTask::Visit(right));
940 tasks.push(TransformTask::Visit(left));
941 }
942 Expression::BitwiseOr(mut op) => {
943 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
944 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
945 tasks.push(TransformTask::Finish(FinishTask::Binary(
946 Expression::BitwiseOr(op),
947 )));
948 tasks.push(TransformTask::Visit(right));
949 tasks.push(TransformTask::Visit(left));
950 }
951 Expression::BitwiseXor(mut op) => {
952 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
953 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
954 tasks.push(TransformTask::Finish(FinishTask::Binary(
955 Expression::BitwiseXor(op),
956 )));
957 tasks.push(TransformTask::Visit(right));
958 tasks.push(TransformTask::Visit(left));
959 }
960 Expression::Is(mut op) => {
961 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
962 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
963 tasks.push(TransformTask::Finish(FinishTask::Binary(Expression::Is(
964 op,
965 ))));
966 tasks.push(TransformTask::Visit(right));
967 tasks.push(TransformTask::Visit(left));
968 }
969 Expression::MemberOf(mut op) => {
970 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
971 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
972 tasks.push(TransformTask::Finish(FinishTask::Binary(
973 Expression::MemberOf(op),
974 )));
975 tasks.push(TransformTask::Visit(right));
976 tasks.push(TransformTask::Visit(left));
977 }
978 Expression::ArrayContainsAll(mut op) => {
979 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
980 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
981 tasks.push(TransformTask::Finish(FinishTask::Binary(
982 Expression::ArrayContainsAll(op),
983 )));
984 tasks.push(TransformTask::Visit(right));
985 tasks.push(TransformTask::Visit(left));
986 }
987 Expression::ArrayContainedBy(mut op) => {
988 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
989 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
990 tasks.push(TransformTask::Finish(FinishTask::Binary(
991 Expression::ArrayContainedBy(op),
992 )));
993 tasks.push(TransformTask::Visit(right));
994 tasks.push(TransformTask::Visit(left));
995 }
996 Expression::ArrayOverlaps(mut op) => {
997 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
998 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
999 tasks.push(TransformTask::Finish(FinishTask::Binary(
1000 Expression::ArrayOverlaps(op),
1001 )));
1002 tasks.push(TransformTask::Visit(right));
1003 tasks.push(TransformTask::Visit(left));
1004 }
1005 Expression::TsMatch(mut op) => {
1006 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
1007 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
1008 tasks.push(TransformTask::Finish(FinishTask::Binary(
1009 Expression::TsMatch(op),
1010 )));
1011 tasks.push(TransformTask::Visit(right));
1012 tasks.push(TransformTask::Visit(left));
1013 }
1014 Expression::Adjacent(mut op) => {
1015 let right = std::mem::replace(&mut op.right, Expression::Null(Null));
1016 let left = std::mem::replace(&mut op.left, Expression::Null(Null));
1017 tasks.push(TransformTask::Finish(FinishTask::Binary(
1018 Expression::Adjacent(op),
1019 )));
1020 tasks.push(TransformTask::Visit(right));
1021 tasks.push(TransformTask::Visit(left));
1022 }
1023 Expression::Like(mut like) => {
1024 let right = std::mem::replace(&mut like.right, Expression::Null(Null));
1025 let left = std::mem::replace(&mut like.left, Expression::Null(Null));
1026 tasks.push(TransformTask::Finish(FinishTask::Binary(Expression::Like(
1027 like,
1028 ))));
1029 tasks.push(TransformTask::Visit(right));
1030 tasks.push(TransformTask::Visit(left));
1031 }
1032 Expression::ILike(mut like) => {
1033 let right = std::mem::replace(&mut like.right, Expression::Null(Null));
1034 let left = std::mem::replace(&mut like.left, Expression::Null(Null));
1035 tasks.push(TransformTask::Finish(FinishTask::Binary(
1036 Expression::ILike(like),
1037 )));
1038 tasks.push(TransformTask::Visit(right));
1039 tasks.push(TransformTask::Visit(left));
1040 }
1041 Expression::Cast(mut cast) => {
1042 let child = std::mem::replace(&mut cast.this, Expression::Null(Null));
1043 tasks.push(TransformTask::Finish(FinishTask::CastLike(
1044 Expression::Cast(cast),
1045 )));
1046 tasks.push(TransformTask::Visit(child));
1047 }
1048 Expression::TryCast(mut cast) => {
1049 let child = std::mem::replace(&mut cast.this, Expression::Null(Null));
1050 tasks.push(TransformTask::Finish(FinishTask::CastLike(
1051 Expression::TryCast(cast),
1052 )));
1053 tasks.push(TransformTask::Visit(child));
1054 }
1055 Expression::SafeCast(mut cast) => {
1056 let child = std::mem::replace(&mut cast.this, Expression::Null(Null));
1057 tasks.push(TransformTask::Finish(FinishTask::CastLike(
1058 Expression::SafeCast(cast),
1059 )));
1060 tasks.push(TransformTask::Visit(child));
1061 }
1062 Expression::Function(mut function) => {
1063 let args = std::mem::take(&mut function.args);
1064 let count = args.len();
1065 tasks.push(TransformTask::Finish(FinishTask::List(
1066 Expression::Function(function),
1067 count,
1068 )));
1069 for child in args.into_iter().rev() {
1070 tasks.push(TransformTask::Visit(child));
1071 }
1072 }
1073 Expression::Array(mut array) => {
1074 let expressions = std::mem::take(&mut array.expressions);
1075 let count = expressions.len();
1076 tasks.push(TransformTask::Finish(FinishTask::List(
1077 Expression::Array(array),
1078 count,
1079 )));
1080 for child in expressions.into_iter().rev() {
1081 tasks.push(TransformTask::Visit(child));
1082 }
1083 }
1084 Expression::Tuple(mut tuple) => {
1085 let expressions = std::mem::take(&mut tuple.expressions);
1086 let count = expressions.len();
1087 tasks.push(TransformTask::Finish(FinishTask::List(
1088 Expression::Tuple(tuple),
1089 count,
1090 )));
1091 for child in expressions.into_iter().rev() {
1092 tasks.push(TransformTask::Visit(child));
1093 }
1094 }
1095 Expression::ArrayFunc(mut array) => {
1096 let expressions = std::mem::take(&mut array.expressions);
1097 let count = expressions.len();
1098 tasks.push(TransformTask::Finish(FinishTask::List(
1099 Expression::ArrayFunc(array),
1100 count,
1101 )));
1102 for child in expressions.into_iter().rev() {
1103 tasks.push(TransformTask::Visit(child));
1104 }
1105 }
1106 Expression::Coalesce(mut func) => {
1107 let expressions = std::mem::take(&mut func.expressions);
1108 let count = expressions.len();
1109 tasks.push(TransformTask::Finish(FinishTask::List(
1110 Expression::Coalesce(func),
1111 count,
1112 )));
1113 for child in expressions.into_iter().rev() {
1114 tasks.push(TransformTask::Visit(child));
1115 }
1116 }
1117 Expression::Greatest(mut func) => {
1118 let expressions = std::mem::take(&mut func.expressions);
1119 let count = expressions.len();
1120 tasks.push(TransformTask::Finish(FinishTask::List(
1121 Expression::Greatest(func),
1122 count,
1123 )));
1124 for child in expressions.into_iter().rev() {
1125 tasks.push(TransformTask::Visit(child));
1126 }
1127 }
1128 Expression::Least(mut func) => {
1129 let expressions = std::mem::take(&mut func.expressions);
1130 let count = expressions.len();
1131 tasks.push(TransformTask::Finish(FinishTask::List(
1132 Expression::Least(func),
1133 count,
1134 )));
1135 for child in expressions.into_iter().rev() {
1136 tasks.push(TransformTask::Visit(child));
1137 }
1138 }
1139 Expression::ArrayConcat(mut func) => {
1140 let expressions = std::mem::take(&mut func.expressions);
1141 let count = expressions.len();
1142 tasks.push(TransformTask::Finish(FinishTask::List(
1143 Expression::ArrayConcat(func),
1144 count,
1145 )));
1146 for child in expressions.into_iter().rev() {
1147 tasks.push(TransformTask::Visit(child));
1148 }
1149 }
1150 Expression::ArrayIntersect(mut func) => {
1151 let expressions = std::mem::take(&mut func.expressions);
1152 let count = expressions.len();
1153 tasks.push(TransformTask::Finish(FinishTask::List(
1154 Expression::ArrayIntersect(func),
1155 count,
1156 )));
1157 for child in expressions.into_iter().rev() {
1158 tasks.push(TransformTask::Visit(child));
1159 }
1160 }
1161 Expression::ArrayZip(mut func) => {
1162 let expressions = std::mem::take(&mut func.expressions);
1163 let count = expressions.len();
1164 tasks.push(TransformTask::Finish(FinishTask::List(
1165 Expression::ArrayZip(func),
1166 count,
1167 )));
1168 for child in expressions.into_iter().rev() {
1169 tasks.push(TransformTask::Visit(child));
1170 }
1171 }
1172 Expression::MapConcat(mut func) => {
1173 let expressions = std::mem::take(&mut func.expressions);
1174 let count = expressions.len();
1175 tasks.push(TransformTask::Finish(FinishTask::List(
1176 Expression::MapConcat(func),
1177 count,
1178 )));
1179 for child in expressions.into_iter().rev() {
1180 tasks.push(TransformTask::Visit(child));
1181 }
1182 }
1183 Expression::JsonArray(mut func) => {
1184 let expressions = std::mem::take(&mut func.expressions);
1185 let count = expressions.len();
1186 tasks.push(TransformTask::Finish(FinishTask::List(
1187 Expression::JsonArray(func),
1188 count,
1189 )));
1190 for child in expressions.into_iter().rev() {
1191 tasks.push(TransformTask::Visit(child));
1192 }
1193 }
1194 Expression::From(mut from) => {
1195 let expressions = std::mem::take(&mut from.expressions);
1196 let count = expressions.len();
1197 tasks.push(TransformTask::Finish(FinishTask::From(*from, count)));
1198 for child in expressions.into_iter().rev() {
1199 tasks.push(TransformTask::Visit(child));
1200 }
1201 }
1202 Expression::Select(mut select) => {
1203 let expressions = std::mem::take(&mut select.expressions);
1204 let expr_count = expressions.len();
1205
1206 let from_info = select.from.take().map(|mut from| {
1207 let children = std::mem::take(&mut from.expressions);
1208 (from, children)
1209 });
1210 let from_present = from_info.is_some();
1211
1212 let where_child = select.where_clause.as_mut().map(|where_clause| {
1213 std::mem::replace(&mut where_clause.this, Expression::Null(Null))
1214 });
1215 let where_present = where_child.is_some();
1216
1217 let group_expressions = select
1218 .group_by
1219 .as_mut()
1220 .map(|group_by| std::mem::take(&mut group_by.expressions))
1221 .unwrap_or_default();
1222 let group_by_count = group_expressions.len();
1223
1224 let having_child = select.having.as_mut().map(|having| {
1225 std::mem::replace(&mut having.this, Expression::Null(Null))
1226 });
1227 let having_present = having_child.is_some();
1228
1229 let qualify_child = select.qualify.as_mut().map(|qualify| {
1230 std::mem::replace(&mut qualify.this, Expression::Null(Null))
1231 });
1232 let qualify_present = qualify_child.is_some();
1233
1234 tasks.push(TransformTask::Finish(FinishTask::Select(SelectFrame {
1235 select,
1236 expr_count,
1237 from_present,
1238 where_present,
1239 group_by_count,
1240 having_present,
1241 qualify_present,
1242 })));
1243
1244 if let Some(child) = qualify_child {
1245 tasks.push(TransformTask::Visit(child));
1246 }
1247 if let Some(child) = having_child {
1248 tasks.push(TransformTask::Visit(child));
1249 }
1250 for child in group_expressions.into_iter().rev() {
1251 tasks.push(TransformTask::Visit(child));
1252 }
1253 if let Some(child) = where_child {
1254 tasks.push(TransformTask::Visit(child));
1255 }
1256 if let Some((from, children)) = from_info {
1257 tasks.push(TransformTask::Finish(FinishTask::From(
1258 from,
1259 children.len(),
1260 )));
1261 for child in children.into_iter().rev() {
1262 tasks.push(TransformTask::Visit(child));
1263 }
1264 }
1265 for child in expressions.into_iter().rev() {
1266 tasks.push(TransformTask::Visit(child));
1267 }
1268 }
1269 Expression::Union(mut union) => {
1270 let right = std::mem::replace(&mut union.right, Expression::Null(Null));
1271 let left = std::mem::replace(&mut union.left, Expression::Null(Null));
1272 tasks.push(TransformTask::Finish(FinishTask::SetOp(Expression::Union(
1273 union,
1274 ))));
1275 tasks.push(TransformTask::Visit(right));
1276 tasks.push(TransformTask::Visit(left));
1277 }
1278 Expression::Intersect(mut intersect) => {
1279 let right = std::mem::replace(&mut intersect.right, Expression::Null(Null));
1280 let left = std::mem::replace(&mut intersect.left, Expression::Null(Null));
1281 tasks.push(TransformTask::Finish(FinishTask::SetOp(
1282 Expression::Intersect(intersect),
1283 )));
1284 tasks.push(TransformTask::Visit(right));
1285 tasks.push(TransformTask::Visit(left));
1286 }
1287 Expression::Except(mut except) => {
1288 let right = std::mem::replace(&mut except.right, Expression::Null(Null));
1289 let left = std::mem::replace(&mut except.left, Expression::Null(Null));
1290 tasks.push(TransformTask::Finish(FinishTask::SetOp(
1291 Expression::Except(except),
1292 )));
1293 tasks.push(TransformTask::Visit(right));
1294 tasks.push(TransformTask::Visit(left));
1295 }
1296 other => {
1297 results.push(transform_recursive_reference(other, transform_fn)?);
1298 }
1299 }
1300 }
1301 TransformTask::Finish(finish) => match finish {
1302 FinishTask::Unary(expr) => {
1303 let child = transform_pop_result(&mut results)?;
1304 let rebuilt = match expr {
1305 Expression::Alias(mut alias) => {
1306 alias.this = child;
1307 Expression::Alias(alias)
1308 }
1309 Expression::Paren(mut paren) => {
1310 paren.this = child;
1311 Expression::Paren(paren)
1312 }
1313 Expression::Not(mut not) => {
1314 not.this = child;
1315 Expression::Not(not)
1316 }
1317 Expression::Neg(mut neg) => {
1318 neg.this = child;
1319 Expression::Neg(neg)
1320 }
1321 Expression::IsNull(mut expr) => {
1322 expr.this = child;
1323 Expression::IsNull(expr)
1324 }
1325 Expression::IsTrue(mut expr) => {
1326 expr.this = child;
1327 Expression::IsTrue(expr)
1328 }
1329 Expression::IsFalse(mut expr) => {
1330 expr.this = child;
1331 Expression::IsFalse(expr)
1332 }
1333 Expression::Subquery(mut subquery) => {
1334 subquery.this = child;
1335 Expression::Subquery(subquery)
1336 }
1337 Expression::Exists(mut exists) => {
1338 exists.this = child;
1339 Expression::Exists(exists)
1340 }
1341 Expression::TableArgument(mut arg) => {
1342 arg.this = child;
1343 Expression::TableArgument(arg)
1344 }
1345 _ => {
1346 return Err(crate::error::Error::Internal(
1347 "unexpected unary transform task".to_string(),
1348 ));
1349 }
1350 };
1351 results.push(transform_fn(rebuilt)?);
1352 }
1353 FinishTask::Binary(expr) => {
1354 let mut children = transform_pop_results(&mut results, 2)?.into_iter();
1355 let left = children.next().expect("left child");
1356 let right = children.next().expect("right child");
1357 let rebuilt = match expr {
1358 Expression::And(mut op) => {
1359 op.left = left;
1360 op.right = right;
1361 Expression::And(op)
1362 }
1363 Expression::Or(mut op) => {
1364 op.left = left;
1365 op.right = right;
1366 Expression::Or(op)
1367 }
1368 Expression::Add(mut op) => {
1369 op.left = left;
1370 op.right = right;
1371 Expression::Add(op)
1372 }
1373 Expression::Sub(mut op) => {
1374 op.left = left;
1375 op.right = right;
1376 Expression::Sub(op)
1377 }
1378 Expression::Mul(mut op) => {
1379 op.left = left;
1380 op.right = right;
1381 Expression::Mul(op)
1382 }
1383 Expression::Div(mut op) => {
1384 op.left = left;
1385 op.right = right;
1386 Expression::Div(op)
1387 }
1388 Expression::Eq(mut op) => {
1389 op.left = left;
1390 op.right = right;
1391 Expression::Eq(op)
1392 }
1393 Expression::Lt(mut op) => {
1394 op.left = left;
1395 op.right = right;
1396 Expression::Lt(op)
1397 }
1398 Expression::Gt(mut op) => {
1399 op.left = left;
1400 op.right = right;
1401 Expression::Gt(op)
1402 }
1403 Expression::Neq(mut op) => {
1404 op.left = left;
1405 op.right = right;
1406 Expression::Neq(op)
1407 }
1408 Expression::Lte(mut op) => {
1409 op.left = left;
1410 op.right = right;
1411 Expression::Lte(op)
1412 }
1413 Expression::Gte(mut op) => {
1414 op.left = left;
1415 op.right = right;
1416 Expression::Gte(op)
1417 }
1418 Expression::Mod(mut op) => {
1419 op.left = left;
1420 op.right = right;
1421 Expression::Mod(op)
1422 }
1423 Expression::Concat(mut op) => {
1424 op.left = left;
1425 op.right = right;
1426 Expression::Concat(op)
1427 }
1428 Expression::BitwiseAnd(mut op) => {
1429 op.left = left;
1430 op.right = right;
1431 Expression::BitwiseAnd(op)
1432 }
1433 Expression::BitwiseOr(mut op) => {
1434 op.left = left;
1435 op.right = right;
1436 Expression::BitwiseOr(op)
1437 }
1438 Expression::BitwiseXor(mut op) => {
1439 op.left = left;
1440 op.right = right;
1441 Expression::BitwiseXor(op)
1442 }
1443 Expression::Is(mut op) => {
1444 op.left = left;
1445 op.right = right;
1446 Expression::Is(op)
1447 }
1448 Expression::MemberOf(mut op) => {
1449 op.left = left;
1450 op.right = right;
1451 Expression::MemberOf(op)
1452 }
1453 Expression::ArrayContainsAll(mut op) => {
1454 op.left = left;
1455 op.right = right;
1456 Expression::ArrayContainsAll(op)
1457 }
1458 Expression::ArrayContainedBy(mut op) => {
1459 op.left = left;
1460 op.right = right;
1461 Expression::ArrayContainedBy(op)
1462 }
1463 Expression::ArrayOverlaps(mut op) => {
1464 op.left = left;
1465 op.right = right;
1466 Expression::ArrayOverlaps(op)
1467 }
1468 Expression::TsMatch(mut op) => {
1469 op.left = left;
1470 op.right = right;
1471 Expression::TsMatch(op)
1472 }
1473 Expression::Adjacent(mut op) => {
1474 op.left = left;
1475 op.right = right;
1476 Expression::Adjacent(op)
1477 }
1478 Expression::Like(mut like) => {
1479 like.left = left;
1480 like.right = right;
1481 Expression::Like(like)
1482 }
1483 Expression::ILike(mut like) => {
1484 like.left = left;
1485 like.right = right;
1486 Expression::ILike(like)
1487 }
1488 _ => {
1489 return Err(crate::error::Error::Internal(
1490 "unexpected binary transform task".to_string(),
1491 ));
1492 }
1493 };
1494 results.push(transform_fn(rebuilt)?);
1495 }
1496 FinishTask::CastLike(expr) => {
1497 let child = transform_pop_result(&mut results)?;
1498 let rebuilt = match expr {
1499 Expression::Cast(mut cast) => {
1500 cast.this = child;
1501 cast.to = transform_data_type_recursive(cast.to, transform_fn)?;
1502 Expression::Cast(cast)
1503 }
1504 Expression::TryCast(mut cast) => {
1505 cast.this = child;
1506 cast.to = transform_data_type_recursive(cast.to, transform_fn)?;
1507 Expression::TryCast(cast)
1508 }
1509 Expression::SafeCast(mut cast) => {
1510 cast.this = child;
1511 cast.to = transform_data_type_recursive(cast.to, transform_fn)?;
1512 Expression::SafeCast(cast)
1513 }
1514 _ => {
1515 return Err(crate::error::Error::Internal(
1516 "unexpected cast transform task".to_string(),
1517 ));
1518 }
1519 };
1520 results.push(transform_fn(rebuilt)?);
1521 }
1522 FinishTask::List(expr, count) => {
1523 let children = transform_pop_results(&mut results, count)?;
1524 let rebuilt = match expr {
1525 Expression::Function(mut function) => {
1526 function.args = children;
1527 Expression::Function(function)
1528 }
1529 Expression::Array(mut array) => {
1530 array.expressions = children;
1531 Expression::Array(array)
1532 }
1533 Expression::Tuple(mut tuple) => {
1534 tuple.expressions = children;
1535 Expression::Tuple(tuple)
1536 }
1537 Expression::ArrayFunc(mut array) => {
1538 array.expressions = children;
1539 Expression::ArrayFunc(array)
1540 }
1541 Expression::Coalesce(mut func) => {
1542 func.expressions = children;
1543 Expression::Coalesce(func)
1544 }
1545 Expression::Greatest(mut func) => {
1546 func.expressions = children;
1547 Expression::Greatest(func)
1548 }
1549 Expression::Least(mut func) => {
1550 func.expressions = children;
1551 Expression::Least(func)
1552 }
1553 Expression::ArrayConcat(mut func) => {
1554 func.expressions = children;
1555 Expression::ArrayConcat(func)
1556 }
1557 Expression::ArrayIntersect(mut func) => {
1558 func.expressions = children;
1559 Expression::ArrayIntersect(func)
1560 }
1561 Expression::ArrayZip(mut func) => {
1562 func.expressions = children;
1563 Expression::ArrayZip(func)
1564 }
1565 Expression::MapConcat(mut func) => {
1566 func.expressions = children;
1567 Expression::MapConcat(func)
1568 }
1569 Expression::JsonArray(mut func) => {
1570 func.expressions = children;
1571 Expression::JsonArray(func)
1572 }
1573 _ => {
1574 return Err(crate::error::Error::Internal(
1575 "unexpected list transform task".to_string(),
1576 ));
1577 }
1578 };
1579 results.push(transform_fn(rebuilt)?);
1580 }
1581 FinishTask::From(mut from, count) => {
1582 from.expressions = transform_pop_results(&mut results, count)?;
1583 results.push(transform_fn(Expression::From(Box::new(from)))?);
1584 }
1585 FinishTask::Select(frame) => {
1586 let mut select = *frame.select;
1587
1588 if frame.qualify_present {
1589 if let Some(ref mut qualify) = select.qualify {
1590 qualify.this = transform_pop_result(&mut results)?;
1591 }
1592 }
1593 if frame.having_present {
1594 if let Some(ref mut having) = select.having {
1595 having.this = transform_pop_result(&mut results)?;
1596 }
1597 }
1598 if frame.group_by_count > 0 {
1599 if let Some(ref mut group_by) = select.group_by {
1600 group_by.expressions =
1601 transform_pop_results(&mut results, frame.group_by_count)?;
1602 }
1603 }
1604 if frame.where_present {
1605 if let Some(ref mut where_clause) = select.where_clause {
1606 where_clause.this = transform_pop_result(&mut results)?;
1607 }
1608 }
1609 if frame.from_present {
1610 match transform_pop_result(&mut results)? {
1611 Expression::From(from) => {
1612 select.from = Some(*from);
1613 }
1614 _ => {
1615 return Err(crate::error::Error::Internal(
1616 "expected FROM expression result".to_string(),
1617 ));
1618 }
1619 }
1620 }
1621 select.expressions = transform_pop_results(&mut results, frame.expr_count)?;
1622
1623 select.joins = select
1624 .joins
1625 .into_iter()
1626 .map(|mut join| {
1627 join.this = transform_recursive(join.this, transform_fn)?;
1628 if let Some(on) = join.on.take() {
1629 join.on = Some(transform_recursive(on, transform_fn)?);
1630 }
1631 match transform_fn(Expression::Join(Box::new(join)))? {
1632 Expression::Join(j) => Ok(*j),
1633 _ => Err(crate::error::Error::parse(
1634 "Join transformation returned non-join expression",
1635 0,
1636 0,
1637 0,
1638 0,
1639 )),
1640 }
1641 })
1642 .collect::<Result<Vec<_>>>()?;
1643
1644 select.lateral_views = select
1645 .lateral_views
1646 .into_iter()
1647 .map(|mut lv| {
1648 lv.this = transform_recursive(lv.this, transform_fn)?;
1649 Ok(lv)
1650 })
1651 .collect::<Result<Vec<_>>>()?;
1652
1653 if let Some(mut with) = select.with.take() {
1654 with.ctes = with
1655 .ctes
1656 .into_iter()
1657 .map(|mut cte| {
1658 let original = cte.this.clone();
1659 cte.this =
1660 transform_recursive(cte.this, transform_fn).unwrap_or(original);
1661 cte
1662 })
1663 .collect();
1664 select.with = Some(with);
1665 }
1666
1667 if let Some(mut order) = select.order_by.take() {
1668 order.expressions = order
1669 .expressions
1670 .into_iter()
1671 .map(|o| {
1672 let mut o = o;
1673 let original = o.this.clone();
1674 o.this =
1675 transform_recursive(o.this, transform_fn).unwrap_or(original);
1676 match transform_fn(Expression::Ordered(Box::new(o.clone()))) {
1677 Ok(Expression::Ordered(transformed)) => *transformed,
1678 Ok(_) | Err(_) => o,
1679 }
1680 })
1681 .collect();
1682 select.order_by = Some(order);
1683 }
1684
1685 if let Some(ref mut windows) = select.windows {
1686 for nw in windows.iter_mut() {
1687 nw.spec.order_by = std::mem::take(&mut nw.spec.order_by)
1688 .into_iter()
1689 .map(|o| {
1690 let mut o = o;
1691 let original = o.this.clone();
1692 o.this = transform_recursive(o.this, transform_fn)
1693 .unwrap_or(original);
1694 match transform_fn(Expression::Ordered(Box::new(o.clone()))) {
1695 Ok(Expression::Ordered(transformed)) => *transformed,
1696 Ok(_) | Err(_) => o,
1697 }
1698 })
1699 .collect();
1700 }
1701 }
1702
1703 results.push(transform_fn(Expression::Select(Box::new(select)))?);
1704 }
1705 FinishTask::SetOp(expr) => {
1706 let mut children = transform_pop_results(&mut results, 2)?.into_iter();
1707 let left = children.next().expect("left child");
1708 let right = children.next().expect("right child");
1709
1710 let rebuilt = match expr {
1711 Expression::Union(mut union) => {
1712 union.left = left;
1713 union.right = right;
1714 if let Some(mut order) = union.order_by.take() {
1715 order.expressions = order
1716 .expressions
1717 .into_iter()
1718 .map(|o| {
1719 let mut o = o;
1720 let original = o.this.clone();
1721 o.this = transform_recursive(o.this, transform_fn)
1722 .unwrap_or(original);
1723 match transform_fn(Expression::Ordered(Box::new(o.clone())))
1724 {
1725 Ok(Expression::Ordered(transformed)) => *transformed,
1726 Ok(_) | Err(_) => o,
1727 }
1728 })
1729 .collect();
1730 union.order_by = Some(order);
1731 }
1732 if let Some(mut with) = union.with.take() {
1733 with.ctes = with
1734 .ctes
1735 .into_iter()
1736 .map(|mut cte| {
1737 let original = cte.this.clone();
1738 cte.this = transform_recursive(cte.this, transform_fn)
1739 .unwrap_or(original);
1740 cte
1741 })
1742 .collect();
1743 union.with = Some(with);
1744 }
1745 Expression::Union(union)
1746 }
1747 Expression::Intersect(mut intersect) => {
1748 intersect.left = left;
1749 intersect.right = right;
1750 if let Some(mut order) = intersect.order_by.take() {
1751 order.expressions = order
1752 .expressions
1753 .into_iter()
1754 .map(|o| {
1755 let mut o = o;
1756 let original = o.this.clone();
1757 o.this = transform_recursive(o.this, transform_fn)
1758 .unwrap_or(original);
1759 match transform_fn(Expression::Ordered(Box::new(o.clone())))
1760 {
1761 Ok(Expression::Ordered(transformed)) => *transformed,
1762 Ok(_) | Err(_) => o,
1763 }
1764 })
1765 .collect();
1766 intersect.order_by = Some(order);
1767 }
1768 if let Some(mut with) = intersect.with.take() {
1769 with.ctes = with
1770 .ctes
1771 .into_iter()
1772 .map(|mut cte| {
1773 let original = cte.this.clone();
1774 cte.this = transform_recursive(cte.this, transform_fn)
1775 .unwrap_or(original);
1776 cte
1777 })
1778 .collect();
1779 intersect.with = Some(with);
1780 }
1781 Expression::Intersect(intersect)
1782 }
1783 Expression::Except(mut except) => {
1784 except.left = left;
1785 except.right = right;
1786 if let Some(mut order) = except.order_by.take() {
1787 order.expressions = order
1788 .expressions
1789 .into_iter()
1790 .map(|o| {
1791 let mut o = o;
1792 let original = o.this.clone();
1793 o.this = transform_recursive(o.this, transform_fn)
1794 .unwrap_or(original);
1795 match transform_fn(Expression::Ordered(Box::new(o.clone())))
1796 {
1797 Ok(Expression::Ordered(transformed)) => *transformed,
1798 Ok(_) | Err(_) => o,
1799 }
1800 })
1801 .collect();
1802 except.order_by = Some(order);
1803 }
1804 if let Some(mut with) = except.with.take() {
1805 with.ctes = with
1806 .ctes
1807 .into_iter()
1808 .map(|mut cte| {
1809 let original = cte.this.clone();
1810 cte.this = transform_recursive(cte.this, transform_fn)
1811 .unwrap_or(original);
1812 cte
1813 })
1814 .collect();
1815 except.with = Some(with);
1816 }
1817 Expression::Except(except)
1818 }
1819 _ => {
1820 return Err(crate::error::Error::Internal(
1821 "unexpected set-op transform task".to_string(),
1822 ));
1823 }
1824 };
1825 results.push(transform_fn(rebuilt)?);
1826 }
1827 },
1828 }
1829 }
1830
1831 match results.len() {
1832 1 => Ok(results.pop().expect("single transform result")),
1833 _ => Err(crate::error::Error::Internal(
1834 "unexpected transform result stack size".to_string(),
1835 )),
1836 }
1837}
1838
1839#[cfg(any(
1840 feature = "transpile",
1841 feature = "ast-tools",
1842 feature = "generate",
1843 feature = "semantic"
1844))]
1845fn transform_table_ref_recursive<F>(table: TableRef, transform_fn: &F) -> Result<TableRef>
1846where
1847 F: Fn(Expression) -> Result<Expression>,
1848{
1849 match transform_recursive(Expression::Table(Box::new(table)), transform_fn)? {
1850 Expression::Table(table) => Ok(*table),
1851 _ => Err(crate::error::Error::parse(
1852 "TableRef transformation returned non-table expression",
1853 0,
1854 0,
1855 0,
1856 0,
1857 )),
1858 }
1859}
1860
1861#[cfg(any(
1862 feature = "transpile",
1863 feature = "ast-tools",
1864 feature = "generate",
1865 feature = "semantic"
1866))]
1867fn transform_from_recursive<F>(from: From, transform_fn: &F) -> Result<From>
1868where
1869 F: Fn(Expression) -> Result<Expression>,
1870{
1871 match transform_recursive(Expression::From(Box::new(from)), transform_fn)? {
1872 Expression::From(from) => Ok(*from),
1873 _ => Err(crate::error::Error::parse(
1874 "FROM transformation returned non-FROM expression",
1875 0,
1876 0,
1877 0,
1878 0,
1879 )),
1880 }
1881}
1882
1883#[cfg(any(
1884 feature = "transpile",
1885 feature = "ast-tools",
1886 feature = "generate",
1887 feature = "semantic"
1888))]
1889fn transform_join_recursive<F>(mut join: Join, transform_fn: &F) -> Result<Join>
1890where
1891 F: Fn(Expression) -> Result<Expression>,
1892{
1893 join.this = transform_recursive(join.this, transform_fn)?;
1894 if let Some(on) = join.on.take() {
1895 join.on = Some(transform_recursive(on, transform_fn)?);
1896 }
1897 if let Some(match_condition) = join.match_condition.take() {
1898 join.match_condition = Some(transform_recursive(match_condition, transform_fn)?);
1899 }
1900 join.pivots = join
1901 .pivots
1902 .into_iter()
1903 .map(|pivot| transform_recursive(pivot, transform_fn))
1904 .collect::<Result<Vec<_>>>()?;
1905
1906 match transform_fn(Expression::Join(Box::new(join)))? {
1907 Expression::Join(join) => Ok(*join),
1908 _ => Err(crate::error::Error::parse(
1909 "Join transformation returned non-join expression",
1910 0,
1911 0,
1912 0,
1913 0,
1914 )),
1915 }
1916}
1917
1918#[cfg(any(
1919 feature = "transpile",
1920 feature = "ast-tools",
1921 feature = "generate",
1922 feature = "semantic"
1923))]
1924fn transform_output_clause_recursive<F>(
1925 mut output: OutputClause,
1926 transform_fn: &F,
1927) -> Result<OutputClause>
1928where
1929 F: Fn(Expression) -> Result<Expression>,
1930{
1931 output.columns = output
1932 .columns
1933 .into_iter()
1934 .map(|column| transform_recursive(column, transform_fn))
1935 .collect::<Result<Vec<_>>>()?;
1936 if let Some(into_table) = output.into_table.take() {
1937 output.into_table = Some(transform_recursive(into_table, transform_fn)?);
1938 }
1939 Ok(output)
1940}
1941
1942#[cfg(any(
1943 feature = "transpile",
1944 feature = "ast-tools",
1945 feature = "generate",
1946 feature = "semantic"
1947))]
1948fn transform_with_recursive<F>(mut with: With, transform_fn: &F) -> Result<With>
1949where
1950 F: Fn(Expression) -> Result<Expression>,
1951{
1952 with.ctes = with
1953 .ctes
1954 .into_iter()
1955 .map(|mut cte| {
1956 cte.this = transform_recursive(cte.this, transform_fn)?;
1957 Ok(cte)
1958 })
1959 .collect::<Result<Vec<_>>>()?;
1960 if let Some(search) = with.search.take() {
1961 with.search = Some(Box::new(transform_recursive(*search, transform_fn)?));
1962 }
1963 Ok(with)
1964}
1965
1966#[cfg(any(
1967 feature = "transpile",
1968 feature = "ast-tools",
1969 feature = "generate",
1970 feature = "semantic"
1971))]
1972fn transform_order_by_recursive<F>(mut order: OrderBy, transform_fn: &F) -> Result<OrderBy>
1973where
1974 F: Fn(Expression) -> Result<Expression>,
1975{
1976 order.expressions = order
1977 .expressions
1978 .into_iter()
1979 .map(|mut ordered| {
1980 let original = ordered.this.clone();
1981 ordered.this = transform_recursive(ordered.this, transform_fn).unwrap_or(original);
1982 match transform_fn(Expression::Ordered(Box::new(ordered.clone()))) {
1983 Ok(Expression::Ordered(transformed)) => Ok(*transformed),
1984 Ok(_) | Err(_) => Ok(ordered),
1985 }
1986 })
1987 .collect::<Result<Vec<_>>>()?;
1988 Ok(order)
1989}
1990
1991#[cfg(any(
1992 feature = "transpile",
1993 feature = "ast-tools",
1994 feature = "generate",
1995 feature = "semantic"
1996))]
1997fn transform_recursive_reference<F>(expr: Expression, transform_fn: &F) -> Result<Expression>
1998where
1999 F: Fn(Expression) -> Result<Expression>,
2000{
2001 use crate::expressions::BinaryOp;
2002
2003 // Helper macro to recurse into AggFunc-based expressions (this, filter, order_by, having_max, limit).
2004 macro_rules! recurse_agg {
2005 ($variant:ident, $f:expr) => {{
2006 let mut f = $f;
2007 f.this = transform_recursive(f.this, transform_fn)?;
2008 if let Some(filter) = f.filter.take() {
2009 f.filter = Some(transform_recursive(filter, transform_fn)?);
2010 }
2011 for ord in &mut f.order_by {
2012 ord.this = transform_recursive(
2013 std::mem::replace(&mut ord.this, Expression::Null(crate::expressions::Null)),
2014 transform_fn,
2015 )?;
2016 }
2017 if let Some((ref mut expr, _)) = f.having_max {
2018 *expr = Box::new(transform_recursive(
2019 std::mem::replace(expr.as_mut(), Expression::Null(crate::expressions::Null)),
2020 transform_fn,
2021 )?);
2022 }
2023 if let Some(limit) = f.limit.take() {
2024 f.limit = Some(Box::new(transform_recursive(*limit, transform_fn)?));
2025 }
2026 Expression::$variant(f)
2027 }};
2028 }
2029
2030 // Helper macro to transform binary ops with Box<BinaryOp>
2031 macro_rules! transform_binary {
2032 ($variant:ident, $op:expr) => {{
2033 let left = transform_recursive($op.left, transform_fn)?;
2034 let right = transform_recursive($op.right, transform_fn)?;
2035 Expression::$variant(Box::new(BinaryOp {
2036 left,
2037 right,
2038 left_comments: $op.left_comments,
2039 operator_comments: $op.operator_comments,
2040 trailing_comments: $op.trailing_comments,
2041 inferred_type: $op.inferred_type,
2042 }))
2043 }};
2044 }
2045
2046 // Fast path: leaf nodes never need child traversal, apply transform directly
2047 if matches!(
2048 &expr,
2049 Expression::Literal(_)
2050 | Expression::Boolean(_)
2051 | Expression::Null(_)
2052 | Expression::Identifier(_)
2053 | Expression::Star(_)
2054 | Expression::Parameter(_)
2055 | Expression::Placeholder(_)
2056 | Expression::SessionParameter(_)
2057 ) {
2058 return transform_fn(expr);
2059 }
2060
2061 // First recursively transform children, then apply the transform function
2062 let expr = match expr {
2063 Expression::Select(mut select) => {
2064 select.expressions = select
2065 .expressions
2066 .into_iter()
2067 .map(|e| transform_recursive(e, transform_fn))
2068 .collect::<Result<Vec<_>>>()?;
2069
2070 // Transform FROM clause
2071 if let Some(mut from) = select.from.take() {
2072 from.expressions = from
2073 .expressions
2074 .into_iter()
2075 .map(|e| transform_recursive(e, transform_fn))
2076 .collect::<Result<Vec<_>>>()?;
2077 select.from = Some(from);
2078 }
2079
2080 // Transform JOINs - important for CROSS APPLY / LATERAL transformations
2081 select.joins = select
2082 .joins
2083 .into_iter()
2084 .map(|mut join| {
2085 join.this = transform_recursive(join.this, transform_fn)?;
2086 if let Some(on) = join.on.take() {
2087 join.on = Some(transform_recursive(on, transform_fn)?);
2088 }
2089 // Wrap join in Expression::Join to allow transform_fn to transform it
2090 match transform_fn(Expression::Join(Box::new(join)))? {
2091 Expression::Join(j) => Ok(*j),
2092 _ => Err(crate::error::Error::parse(
2093 "Join transformation returned non-join expression",
2094 0,
2095 0,
2096 0,
2097 0,
2098 )),
2099 }
2100 })
2101 .collect::<Result<Vec<_>>>()?;
2102
2103 // Transform LATERAL VIEW expressions (Hive/Spark)
2104 select.lateral_views = select
2105 .lateral_views
2106 .into_iter()
2107 .map(|mut lv| {
2108 lv.this = transform_recursive(lv.this, transform_fn)?;
2109 Ok(lv)
2110 })
2111 .collect::<Result<Vec<_>>>()?;
2112
2113 // Transform WHERE clause
2114 if let Some(mut where_clause) = select.where_clause.take() {
2115 where_clause.this = transform_recursive(where_clause.this, transform_fn)?;
2116 select.where_clause = Some(where_clause);
2117 }
2118
2119 // Transform GROUP BY
2120 if let Some(mut group_by) = select.group_by.take() {
2121 group_by.expressions = group_by
2122 .expressions
2123 .into_iter()
2124 .map(|e| transform_recursive(e, transform_fn))
2125 .collect::<Result<Vec<_>>>()?;
2126 select.group_by = Some(group_by);
2127 }
2128
2129 // Transform HAVING
2130 if let Some(mut having) = select.having.take() {
2131 having.this = transform_recursive(having.this, transform_fn)?;
2132 select.having = Some(having);
2133 }
2134
2135 // Transform WITH (CTEs)
2136 if let Some(mut with) = select.with.take() {
2137 with.ctes = with
2138 .ctes
2139 .into_iter()
2140 .map(|mut cte| {
2141 let original = cte.this.clone();
2142 cte.this = transform_recursive(cte.this, transform_fn).unwrap_or(original);
2143 cte
2144 })
2145 .collect();
2146 select.with = Some(with);
2147 }
2148
2149 // Transform ORDER BY
2150 if let Some(mut order) = select.order_by.take() {
2151 order.expressions = order
2152 .expressions
2153 .into_iter()
2154 .map(|o| {
2155 let mut o = o;
2156 let original = o.this.clone();
2157 o.this = transform_recursive(o.this, transform_fn).unwrap_or(original);
2158 // Also apply transform to the Ordered wrapper itself (for NULLS FIRST etc.)
2159 match transform_fn(Expression::Ordered(Box::new(o.clone()))) {
2160 Ok(Expression::Ordered(transformed)) => *transformed,
2161 Ok(_) | Err(_) => o,
2162 }
2163 })
2164 .collect();
2165 select.order_by = Some(order);
2166 }
2167
2168 // Transform WINDOW clause order_by
2169 if let Some(ref mut windows) = select.windows {
2170 for nw in windows.iter_mut() {
2171 nw.spec.order_by = std::mem::take(&mut nw.spec.order_by)
2172 .into_iter()
2173 .map(|o| {
2174 let mut o = o;
2175 let original = o.this.clone();
2176 o.this = transform_recursive(o.this, transform_fn).unwrap_or(original);
2177 match transform_fn(Expression::Ordered(Box::new(o.clone()))) {
2178 Ok(Expression::Ordered(transformed)) => *transformed,
2179 Ok(_) | Err(_) => o,
2180 }
2181 })
2182 .collect();
2183 }
2184 }
2185
2186 // Transform QUALIFY
2187 if let Some(mut qual) = select.qualify.take() {
2188 qual.this = transform_recursive(qual.this, transform_fn)?;
2189 select.qualify = Some(qual);
2190 }
2191
2192 Expression::Select(select)
2193 }
2194 Expression::Function(mut f) => {
2195 f.args = f
2196 .args
2197 .into_iter()
2198 .map(|e| transform_recursive(e, transform_fn))
2199 .collect::<Result<Vec<_>>>()?;
2200 Expression::Function(f)
2201 }
2202 Expression::AggregateFunction(mut f) => {
2203 f.args = f
2204 .args
2205 .into_iter()
2206 .map(|e| transform_recursive(e, transform_fn))
2207 .collect::<Result<Vec<_>>>()?;
2208 if let Some(filter) = f.filter {
2209 f.filter = Some(transform_recursive(filter, transform_fn)?);
2210 }
2211 Expression::AggregateFunction(f)
2212 }
2213 Expression::WindowFunction(mut wf) => {
2214 wf.this = transform_recursive(wf.this, transform_fn)?;
2215 wf.over.partition_by = wf
2216 .over
2217 .partition_by
2218 .into_iter()
2219 .map(|e| transform_recursive(e, transform_fn))
2220 .collect::<Result<Vec<_>>>()?;
2221 // Transform order_by items through Expression::Ordered wrapper
2222 wf.over.order_by = wf
2223 .over
2224 .order_by
2225 .into_iter()
2226 .map(|o| {
2227 let mut o = o;
2228 o.this = transform_recursive(o.this, transform_fn)?;
2229 match transform_fn(Expression::Ordered(Box::new(o)))? {
2230 Expression::Ordered(transformed) => Ok(*transformed),
2231 _ => Err(crate::error::Error::parse(
2232 "Ordered transformation returned non-Ordered expression",
2233 0,
2234 0,
2235 0,
2236 0,
2237 )),
2238 }
2239 })
2240 .collect::<Result<Vec<_>>>()?;
2241 Expression::WindowFunction(wf)
2242 }
2243 Expression::Alias(mut a) => {
2244 a.this = transform_recursive(a.this, transform_fn)?;
2245 Expression::Alias(a)
2246 }
2247 Expression::Cast(mut c) => {
2248 c.this = transform_recursive(c.this, transform_fn)?;
2249 // Also transform the target data type (recursively for nested types like ARRAY<INT>, STRUCT<a INT>)
2250 c.to = transform_data_type_recursive(c.to, transform_fn)?;
2251 Expression::Cast(c)
2252 }
2253 Expression::And(op) => transform_binary!(And, *op),
2254 Expression::Or(op) => transform_binary!(Or, *op),
2255 Expression::Add(op) => transform_binary!(Add, *op),
2256 Expression::Sub(op) => transform_binary!(Sub, *op),
2257 Expression::Mul(op) => transform_binary!(Mul, *op),
2258 Expression::Div(op) => transform_binary!(Div, *op),
2259 Expression::Eq(op) => transform_binary!(Eq, *op),
2260 Expression::Lt(op) => transform_binary!(Lt, *op),
2261 Expression::Gt(op) => transform_binary!(Gt, *op),
2262 Expression::Paren(mut p) => {
2263 p.this = transform_recursive(p.this, transform_fn)?;
2264 Expression::Paren(p)
2265 }
2266 Expression::Coalesce(mut f) => {
2267 f.expressions = f
2268 .expressions
2269 .into_iter()
2270 .map(|e| transform_recursive(e, transform_fn))
2271 .collect::<Result<Vec<_>>>()?;
2272 Expression::Coalesce(f)
2273 }
2274 Expression::IfNull(mut f) => {
2275 f.this = transform_recursive(f.this, transform_fn)?;
2276 f.expression = transform_recursive(f.expression, transform_fn)?;
2277 Expression::IfNull(f)
2278 }
2279 Expression::Nvl(mut f) => {
2280 f.this = transform_recursive(f.this, transform_fn)?;
2281 f.expression = transform_recursive(f.expression, transform_fn)?;
2282 Expression::Nvl(f)
2283 }
2284 Expression::In(mut i) => {
2285 i.this = transform_recursive(i.this, transform_fn)?;
2286 i.expressions = i
2287 .expressions
2288 .into_iter()
2289 .map(|e| transform_recursive(e, transform_fn))
2290 .collect::<Result<Vec<_>>>()?;
2291 if let Some(query) = i.query {
2292 i.query = Some(transform_recursive(query, transform_fn)?);
2293 }
2294 Expression::In(i)
2295 }
2296 Expression::Not(mut n) => {
2297 n.this = transform_recursive(n.this, transform_fn)?;
2298 Expression::Not(n)
2299 }
2300 Expression::ArraySlice(mut s) => {
2301 s.this = transform_recursive(s.this, transform_fn)?;
2302 if let Some(start) = s.start {
2303 s.start = Some(transform_recursive(start, transform_fn)?);
2304 }
2305 if let Some(end) = s.end {
2306 s.end = Some(transform_recursive(end, transform_fn)?);
2307 }
2308 Expression::ArraySlice(s)
2309 }
2310 Expression::Subscript(mut s) => {
2311 s.this = transform_recursive(s.this, transform_fn)?;
2312 s.index = transform_recursive(s.index, transform_fn)?;
2313 Expression::Subscript(s)
2314 }
2315 Expression::Array(mut a) => {
2316 a.expressions = a
2317 .expressions
2318 .into_iter()
2319 .map(|e| transform_recursive(e, transform_fn))
2320 .collect::<Result<Vec<_>>>()?;
2321 Expression::Array(a)
2322 }
2323 Expression::Struct(mut s) => {
2324 let mut new_fields = Vec::new();
2325 for (name, expr) in s.fields {
2326 let transformed = transform_recursive(expr, transform_fn)?;
2327 new_fields.push((name, transformed));
2328 }
2329 s.fields = new_fields;
2330 Expression::Struct(s)
2331 }
2332 Expression::NamedArgument(mut na) => {
2333 na.value = transform_recursive(na.value, transform_fn)?;
2334 Expression::NamedArgument(na)
2335 }
2336 Expression::MapFunc(mut m) => {
2337 m.keys = m
2338 .keys
2339 .into_iter()
2340 .map(|e| transform_recursive(e, transform_fn))
2341 .collect::<Result<Vec<_>>>()?;
2342 m.values = m
2343 .values
2344 .into_iter()
2345 .map(|e| transform_recursive(e, transform_fn))
2346 .collect::<Result<Vec<_>>>()?;
2347 Expression::MapFunc(m)
2348 }
2349 Expression::ArrayFunc(mut a) => {
2350 a.expressions = a
2351 .expressions
2352 .into_iter()
2353 .map(|e| transform_recursive(e, transform_fn))
2354 .collect::<Result<Vec<_>>>()?;
2355 Expression::ArrayFunc(a)
2356 }
2357 Expression::Lambda(mut l) => {
2358 l.body = transform_recursive(l.body, transform_fn)?;
2359 Expression::Lambda(l)
2360 }
2361 Expression::JsonExtract(mut f) => {
2362 f.this = transform_recursive(f.this, transform_fn)?;
2363 f.path = transform_recursive(f.path, transform_fn)?;
2364 Expression::JsonExtract(f)
2365 }
2366 Expression::JsonExtractScalar(mut f) => {
2367 f.this = transform_recursive(f.this, transform_fn)?;
2368 f.path = transform_recursive(f.path, transform_fn)?;
2369 Expression::JsonExtractScalar(f)
2370 }
2371
2372 // ===== UnaryFunc-based expressions =====
2373 // These all have a single `this: Expression` child
2374 Expression::Length(mut f) => {
2375 f.this = transform_recursive(f.this, transform_fn)?;
2376 Expression::Length(f)
2377 }
2378 Expression::Upper(mut f) => {
2379 f.this = transform_recursive(f.this, transform_fn)?;
2380 Expression::Upper(f)
2381 }
2382 Expression::Lower(mut f) => {
2383 f.this = transform_recursive(f.this, transform_fn)?;
2384 Expression::Lower(f)
2385 }
2386 Expression::LTrim(mut f) => {
2387 f.this = transform_recursive(f.this, transform_fn)?;
2388 Expression::LTrim(f)
2389 }
2390 Expression::RTrim(mut f) => {
2391 f.this = transform_recursive(f.this, transform_fn)?;
2392 Expression::RTrim(f)
2393 }
2394 Expression::Reverse(mut f) => {
2395 f.this = transform_recursive(f.this, transform_fn)?;
2396 Expression::Reverse(f)
2397 }
2398 Expression::Abs(mut f) => {
2399 f.this = transform_recursive(f.this, transform_fn)?;
2400 Expression::Abs(f)
2401 }
2402 Expression::Ceil(mut f) => {
2403 f.this = transform_recursive(f.this, transform_fn)?;
2404 Expression::Ceil(f)
2405 }
2406 Expression::Floor(mut f) => {
2407 f.this = transform_recursive(f.this, transform_fn)?;
2408 Expression::Floor(f)
2409 }
2410 Expression::Sign(mut f) => {
2411 f.this = transform_recursive(f.this, transform_fn)?;
2412 Expression::Sign(f)
2413 }
2414 Expression::Sqrt(mut f) => {
2415 f.this = transform_recursive(f.this, transform_fn)?;
2416 Expression::Sqrt(f)
2417 }
2418 Expression::Cbrt(mut f) => {
2419 f.this = transform_recursive(f.this, transform_fn)?;
2420 Expression::Cbrt(f)
2421 }
2422 Expression::Ln(mut f) => {
2423 f.this = transform_recursive(f.this, transform_fn)?;
2424 Expression::Ln(f)
2425 }
2426 Expression::Log(mut f) => {
2427 f.this = transform_recursive(f.this, transform_fn)?;
2428 if let Some(base) = f.base {
2429 f.base = Some(transform_recursive(base, transform_fn)?);
2430 }
2431 Expression::Log(f)
2432 }
2433 Expression::Exp(mut f) => {
2434 f.this = transform_recursive(f.this, transform_fn)?;
2435 Expression::Exp(f)
2436 }
2437 Expression::Date(mut f) => {
2438 f.this = transform_recursive(f.this, transform_fn)?;
2439 Expression::Date(f)
2440 }
2441 Expression::Stddev(f) => recurse_agg!(Stddev, f),
2442 Expression::StddevSamp(f) => recurse_agg!(StddevSamp, f),
2443 Expression::Variance(f) => recurse_agg!(Variance, f),
2444
2445 // ===== BinaryFunc-based expressions =====
2446 Expression::ModFunc(mut f) => {
2447 f.this = transform_recursive(f.this, transform_fn)?;
2448 f.expression = transform_recursive(f.expression, transform_fn)?;
2449 Expression::ModFunc(f)
2450 }
2451 Expression::Power(mut f) => {
2452 f.this = transform_recursive(f.this, transform_fn)?;
2453 f.expression = transform_recursive(f.expression, transform_fn)?;
2454 Expression::Power(f)
2455 }
2456 Expression::MapFromArrays(mut f) => {
2457 f.this = transform_recursive(f.this, transform_fn)?;
2458 f.expression = transform_recursive(f.expression, transform_fn)?;
2459 Expression::MapFromArrays(f)
2460 }
2461 Expression::ElementAt(mut f) => {
2462 f.this = transform_recursive(f.this, transform_fn)?;
2463 f.expression = transform_recursive(f.expression, transform_fn)?;
2464 Expression::ElementAt(f)
2465 }
2466 Expression::MapContainsKey(mut f) => {
2467 f.this = transform_recursive(f.this, transform_fn)?;
2468 f.expression = transform_recursive(f.expression, transform_fn)?;
2469 Expression::MapContainsKey(f)
2470 }
2471 Expression::Left(mut f) => {
2472 f.this = transform_recursive(f.this, transform_fn)?;
2473 f.length = transform_recursive(f.length, transform_fn)?;
2474 Expression::Left(f)
2475 }
2476 Expression::Right(mut f) => {
2477 f.this = transform_recursive(f.this, transform_fn)?;
2478 f.length = transform_recursive(f.length, transform_fn)?;
2479 Expression::Right(f)
2480 }
2481 Expression::Repeat(mut f) => {
2482 f.this = transform_recursive(f.this, transform_fn)?;
2483 f.times = transform_recursive(f.times, transform_fn)?;
2484 Expression::Repeat(f)
2485 }
2486
2487 // ===== Complex function expressions =====
2488 Expression::Substring(mut f) => {
2489 f.this = transform_recursive(f.this, transform_fn)?;
2490 f.start = transform_recursive(f.start, transform_fn)?;
2491 if let Some(len) = f.length {
2492 f.length = Some(transform_recursive(len, transform_fn)?);
2493 }
2494 Expression::Substring(f)
2495 }
2496 Expression::Replace(mut f) => {
2497 f.this = transform_recursive(f.this, transform_fn)?;
2498 f.old = transform_recursive(f.old, transform_fn)?;
2499 f.new = transform_recursive(f.new, transform_fn)?;
2500 Expression::Replace(f)
2501 }
2502 Expression::ConcatWs(mut f) => {
2503 f.separator = transform_recursive(f.separator, transform_fn)?;
2504 f.expressions = f
2505 .expressions
2506 .into_iter()
2507 .map(|e| transform_recursive(e, transform_fn))
2508 .collect::<Result<Vec<_>>>()?;
2509 Expression::ConcatWs(f)
2510 }
2511 Expression::Trim(mut f) => {
2512 f.this = transform_recursive(f.this, transform_fn)?;
2513 if let Some(chars) = f.characters {
2514 f.characters = Some(transform_recursive(chars, transform_fn)?);
2515 }
2516 Expression::Trim(f)
2517 }
2518 Expression::Split(mut f) => {
2519 f.this = transform_recursive(f.this, transform_fn)?;
2520 f.delimiter = transform_recursive(f.delimiter, transform_fn)?;
2521 Expression::Split(f)
2522 }
2523 Expression::Lpad(mut f) => {
2524 f.this = transform_recursive(f.this, transform_fn)?;
2525 f.length = transform_recursive(f.length, transform_fn)?;
2526 if let Some(fill) = f.fill {
2527 f.fill = Some(transform_recursive(fill, transform_fn)?);
2528 }
2529 Expression::Lpad(f)
2530 }
2531 Expression::Rpad(mut f) => {
2532 f.this = transform_recursive(f.this, transform_fn)?;
2533 f.length = transform_recursive(f.length, transform_fn)?;
2534 if let Some(fill) = f.fill {
2535 f.fill = Some(transform_recursive(fill, transform_fn)?);
2536 }
2537 Expression::Rpad(f)
2538 }
2539
2540 // ===== Conditional expressions =====
2541 Expression::Case(mut c) => {
2542 if let Some(operand) = c.operand {
2543 c.operand = Some(transform_recursive(operand, transform_fn)?);
2544 }
2545 c.whens = c
2546 .whens
2547 .into_iter()
2548 .map(|(cond, then)| {
2549 let new_cond = transform_recursive(cond.clone(), transform_fn).unwrap_or(cond);
2550 let new_then = transform_recursive(then.clone(), transform_fn).unwrap_or(then);
2551 (new_cond, new_then)
2552 })
2553 .collect();
2554 if let Some(else_expr) = c.else_ {
2555 c.else_ = Some(transform_recursive(else_expr, transform_fn)?);
2556 }
2557 Expression::Case(c)
2558 }
2559 Expression::IfFunc(mut f) => {
2560 f.condition = transform_recursive(f.condition, transform_fn)?;
2561 f.true_value = transform_recursive(f.true_value, transform_fn)?;
2562 if let Some(false_val) = f.false_value {
2563 f.false_value = Some(transform_recursive(false_val, transform_fn)?);
2564 }
2565 Expression::IfFunc(f)
2566 }
2567
2568 // ===== Date/Time expressions =====
2569 Expression::DateAdd(mut f) => {
2570 f.this = transform_recursive(f.this, transform_fn)?;
2571 f.interval = transform_recursive(f.interval, transform_fn)?;
2572 Expression::DateAdd(f)
2573 }
2574 Expression::DateSub(mut f) => {
2575 f.this = transform_recursive(f.this, transform_fn)?;
2576 f.interval = transform_recursive(f.interval, transform_fn)?;
2577 Expression::DateSub(f)
2578 }
2579 Expression::DateDiff(mut f) => {
2580 f.this = transform_recursive(f.this, transform_fn)?;
2581 f.expression = transform_recursive(f.expression, transform_fn)?;
2582 Expression::DateDiff(f)
2583 }
2584 Expression::DateTrunc(mut f) => {
2585 f.this = transform_recursive(f.this, transform_fn)?;
2586 Expression::DateTrunc(f)
2587 }
2588 Expression::Extract(mut f) => {
2589 f.this = transform_recursive(f.this, transform_fn)?;
2590 Expression::Extract(f)
2591 }
2592
2593 // ===== JSON expressions =====
2594 Expression::JsonObject(mut f) => {
2595 f.pairs = f
2596 .pairs
2597 .into_iter()
2598 .map(|(k, v)| {
2599 let new_k = transform_recursive(k, transform_fn)?;
2600 let new_v = transform_recursive(v, transform_fn)?;
2601 Ok((new_k, new_v))
2602 })
2603 .collect::<Result<Vec<_>>>()?;
2604 Expression::JsonObject(f)
2605 }
2606
2607 // ===== Subquery expressions =====
2608 Expression::Subquery(mut s) => {
2609 s.this = transform_recursive(s.this, transform_fn)?;
2610 Expression::Subquery(s)
2611 }
2612 Expression::Exists(mut e) => {
2613 e.this = transform_recursive(e.this, transform_fn)?;
2614 Expression::Exists(e)
2615 }
2616 Expression::Describe(mut d) => {
2617 d.target = transform_recursive(d.target, transform_fn)?;
2618 Expression::Describe(d)
2619 }
2620
2621 // ===== Set operations =====
2622 Expression::Union(mut u) => {
2623 let left = std::mem::replace(&mut u.left, Expression::Null(Null));
2624 u.left = transform_recursive(left, transform_fn)?;
2625 let right = std::mem::replace(&mut u.right, Expression::Null(Null));
2626 u.right = transform_recursive(right, transform_fn)?;
2627 if let Some(mut order) = u.order_by.take() {
2628 order.expressions = order
2629 .expressions
2630 .into_iter()
2631 .map(|o| {
2632 let mut o = o;
2633 let original = o.this.clone();
2634 o.this = transform_recursive(o.this, transform_fn).unwrap_or(original);
2635 match transform_fn(Expression::Ordered(Box::new(o.clone()))) {
2636 Ok(Expression::Ordered(transformed)) => *transformed,
2637 Ok(_) | Err(_) => o,
2638 }
2639 })
2640 .collect();
2641 u.order_by = Some(order);
2642 }
2643 if let Some(mut with) = u.with.take() {
2644 with.ctes = with
2645 .ctes
2646 .into_iter()
2647 .map(|mut cte| {
2648 let original = cte.this.clone();
2649 cte.this = transform_recursive(cte.this, transform_fn).unwrap_or(original);
2650 cte
2651 })
2652 .collect();
2653 u.with = Some(with);
2654 }
2655 Expression::Union(u)
2656 }
2657 Expression::Intersect(mut i) => {
2658 let left = std::mem::replace(&mut i.left, Expression::Null(Null));
2659 i.left = transform_recursive(left, transform_fn)?;
2660 let right = std::mem::replace(&mut i.right, Expression::Null(Null));
2661 i.right = transform_recursive(right, transform_fn)?;
2662 if let Some(mut order) = i.order_by.take() {
2663 order.expressions = order
2664 .expressions
2665 .into_iter()
2666 .map(|o| {
2667 let mut o = o;
2668 let original = o.this.clone();
2669 o.this = transform_recursive(o.this, transform_fn).unwrap_or(original);
2670 match transform_fn(Expression::Ordered(Box::new(o.clone()))) {
2671 Ok(Expression::Ordered(transformed)) => *transformed,
2672 Ok(_) | Err(_) => o,
2673 }
2674 })
2675 .collect();
2676 i.order_by = Some(order);
2677 }
2678 if let Some(mut with) = i.with.take() {
2679 with.ctes = with
2680 .ctes
2681 .into_iter()
2682 .map(|mut cte| {
2683 let original = cte.this.clone();
2684 cte.this = transform_recursive(cte.this, transform_fn).unwrap_or(original);
2685 cte
2686 })
2687 .collect();
2688 i.with = Some(with);
2689 }
2690 Expression::Intersect(i)
2691 }
2692 Expression::Except(mut e) => {
2693 let left = std::mem::replace(&mut e.left, Expression::Null(Null));
2694 e.left = transform_recursive(left, transform_fn)?;
2695 let right = std::mem::replace(&mut e.right, Expression::Null(Null));
2696 e.right = transform_recursive(right, transform_fn)?;
2697 if let Some(mut order) = e.order_by.take() {
2698 order.expressions = order
2699 .expressions
2700 .into_iter()
2701 .map(|o| {
2702 let mut o = o;
2703 let original = o.this.clone();
2704 o.this = transform_recursive(o.this, transform_fn).unwrap_or(original);
2705 match transform_fn(Expression::Ordered(Box::new(o.clone()))) {
2706 Ok(Expression::Ordered(transformed)) => *transformed,
2707 Ok(_) | Err(_) => o,
2708 }
2709 })
2710 .collect();
2711 e.order_by = Some(order);
2712 }
2713 if let Some(mut with) = e.with.take() {
2714 with.ctes = with
2715 .ctes
2716 .into_iter()
2717 .map(|mut cte| {
2718 let original = cte.this.clone();
2719 cte.this = transform_recursive(cte.this, transform_fn).unwrap_or(original);
2720 cte
2721 })
2722 .collect();
2723 e.with = Some(with);
2724 }
2725 Expression::Except(e)
2726 }
2727
2728 // ===== DML expressions =====
2729 Expression::Insert(mut ins) => {
2730 // Transform VALUES clause expressions
2731 let mut new_values = Vec::new();
2732 for row in ins.values {
2733 let mut new_row = Vec::new();
2734 for e in row {
2735 new_row.push(transform_recursive(e, transform_fn)?);
2736 }
2737 new_values.push(new_row);
2738 }
2739 ins.values = new_values;
2740
2741 // Transform query (for INSERT ... SELECT)
2742 if let Some(query) = ins.query {
2743 ins.query = Some(transform_recursive(query, transform_fn)?);
2744 }
2745
2746 // Transform RETURNING clause
2747 let mut new_returning = Vec::new();
2748 for e in ins.returning {
2749 new_returning.push(transform_recursive(e, transform_fn)?);
2750 }
2751 ins.returning = new_returning;
2752
2753 // Transform ON CONFLICT clause
2754 if let Some(on_conflict) = ins.on_conflict {
2755 ins.on_conflict = Some(Box::new(transform_recursive(*on_conflict, transform_fn)?));
2756 }
2757
2758 Expression::Insert(ins)
2759 }
2760 Expression::Update(mut upd) => {
2761 upd.table = transform_table_ref_recursive(upd.table, transform_fn)?;
2762 upd.extra_tables = upd
2763 .extra_tables
2764 .into_iter()
2765 .map(|table| transform_table_ref_recursive(table, transform_fn))
2766 .collect::<Result<Vec<_>>>()?;
2767 upd.table_joins = upd
2768 .table_joins
2769 .into_iter()
2770 .map(|join| transform_join_recursive(join, transform_fn))
2771 .collect::<Result<Vec<_>>>()?;
2772 upd.set = upd
2773 .set
2774 .into_iter()
2775 .map(|(id, val)| {
2776 let new_val = transform_recursive(val.clone(), transform_fn).unwrap_or(val);
2777 (id, new_val)
2778 })
2779 .collect();
2780 if let Some(from_clause) = upd.from_clause.take() {
2781 upd.from_clause = Some(transform_from_recursive(from_clause, transform_fn)?);
2782 }
2783 upd.from_joins = upd
2784 .from_joins
2785 .into_iter()
2786 .map(|join| transform_join_recursive(join, transform_fn))
2787 .collect::<Result<Vec<_>>>()?;
2788 if let Some(mut where_clause) = upd.where_clause.take() {
2789 where_clause.this = transform_recursive(where_clause.this, transform_fn)?;
2790 upd.where_clause = Some(where_clause);
2791 }
2792 upd.returning = upd
2793 .returning
2794 .into_iter()
2795 .map(|expr| transform_recursive(expr, transform_fn))
2796 .collect::<Result<Vec<_>>>()?;
2797 if let Some(output) = upd.output.take() {
2798 upd.output = Some(transform_output_clause_recursive(output, transform_fn)?);
2799 }
2800 if let Some(with) = upd.with.take() {
2801 upd.with = Some(transform_with_recursive(with, transform_fn)?);
2802 }
2803 if let Some(limit) = upd.limit.take() {
2804 upd.limit = Some(transform_recursive(limit, transform_fn)?);
2805 }
2806 if let Some(order_by) = upd.order_by.take() {
2807 upd.order_by = Some(transform_order_by_recursive(order_by, transform_fn)?);
2808 }
2809 Expression::Update(upd)
2810 }
2811 Expression::Delete(mut del) => {
2812 del.table = transform_table_ref_recursive(del.table, transform_fn)?;
2813 del.using = del
2814 .using
2815 .into_iter()
2816 .map(|table| transform_table_ref_recursive(table, transform_fn))
2817 .collect::<Result<Vec<_>>>()?;
2818 if let Some(mut where_clause) = del.where_clause.take() {
2819 where_clause.this = transform_recursive(where_clause.this, transform_fn)?;
2820 del.where_clause = Some(where_clause);
2821 }
2822 if let Some(output) = del.output.take() {
2823 del.output = Some(transform_output_clause_recursive(output, transform_fn)?);
2824 }
2825 if let Some(with) = del.with.take() {
2826 del.with = Some(transform_with_recursive(with, transform_fn)?);
2827 }
2828 if let Some(limit) = del.limit.take() {
2829 del.limit = Some(transform_recursive(limit, transform_fn)?);
2830 }
2831 if let Some(order_by) = del.order_by.take() {
2832 del.order_by = Some(transform_order_by_recursive(order_by, transform_fn)?);
2833 }
2834 del.returning = del
2835 .returning
2836 .into_iter()
2837 .map(|expr| transform_recursive(expr, transform_fn))
2838 .collect::<Result<Vec<_>>>()?;
2839 del.tables = del
2840 .tables
2841 .into_iter()
2842 .map(|table| transform_table_ref_recursive(table, transform_fn))
2843 .collect::<Result<Vec<_>>>()?;
2844 del.joins = del
2845 .joins
2846 .into_iter()
2847 .map(|join| transform_join_recursive(join, transform_fn))
2848 .collect::<Result<Vec<_>>>()?;
2849 Expression::Delete(del)
2850 }
2851
2852 // ===== CTE expressions =====
2853 Expression::With(mut w) => {
2854 w.ctes = w
2855 .ctes
2856 .into_iter()
2857 .map(|mut cte| {
2858 let original = cte.this.clone();
2859 cte.this = transform_recursive(cte.this, transform_fn).unwrap_or(original);
2860 cte
2861 })
2862 .collect();
2863 Expression::With(w)
2864 }
2865 Expression::Cte(mut c) => {
2866 c.this = transform_recursive(c.this, transform_fn)?;
2867 Expression::Cte(c)
2868 }
2869
2870 // ===== Order expressions =====
2871 Expression::Ordered(mut o) => {
2872 o.this = transform_recursive(o.this, transform_fn)?;
2873 Expression::Ordered(o)
2874 }
2875
2876 // ===== Negation =====
2877 Expression::Neg(mut n) => {
2878 n.this = transform_recursive(n.this, transform_fn)?;
2879 Expression::Neg(n)
2880 }
2881
2882 // ===== Between =====
2883 Expression::Between(mut b) => {
2884 b.this = transform_recursive(b.this, transform_fn)?;
2885 b.low = transform_recursive(b.low, transform_fn)?;
2886 b.high = transform_recursive(b.high, transform_fn)?;
2887 Expression::Between(b)
2888 }
2889 Expression::IsNull(mut i) => {
2890 i.this = transform_recursive(i.this, transform_fn)?;
2891 Expression::IsNull(i)
2892 }
2893 Expression::IsTrue(mut i) => {
2894 i.this = transform_recursive(i.this, transform_fn)?;
2895 Expression::IsTrue(i)
2896 }
2897 Expression::IsFalse(mut i) => {
2898 i.this = transform_recursive(i.this, transform_fn)?;
2899 Expression::IsFalse(i)
2900 }
2901
2902 // ===== Like expressions =====
2903 Expression::Like(mut l) => {
2904 l.left = transform_recursive(l.left, transform_fn)?;
2905 l.right = transform_recursive(l.right, transform_fn)?;
2906 Expression::Like(l)
2907 }
2908 Expression::ILike(mut l) => {
2909 l.left = transform_recursive(l.left, transform_fn)?;
2910 l.right = transform_recursive(l.right, transform_fn)?;
2911 Expression::ILike(l)
2912 }
2913
2914 // ===== Additional binary ops not covered by macro =====
2915 Expression::Neq(op) => transform_binary!(Neq, *op),
2916 Expression::Lte(op) => transform_binary!(Lte, *op),
2917 Expression::Gte(op) => transform_binary!(Gte, *op),
2918 Expression::Mod(op) => transform_binary!(Mod, *op),
2919 Expression::Concat(op) => transform_binary!(Concat, *op),
2920 Expression::BitwiseAnd(op) => transform_binary!(BitwiseAnd, *op),
2921 Expression::BitwiseOr(op) => transform_binary!(BitwiseOr, *op),
2922 Expression::BitwiseXor(op) => transform_binary!(BitwiseXor, *op),
2923 Expression::Is(op) => transform_binary!(Is, *op),
2924
2925 // ===== TryCast / SafeCast =====
2926 Expression::TryCast(mut c) => {
2927 c.this = transform_recursive(c.this, transform_fn)?;
2928 c.to = transform_data_type_recursive(c.to, transform_fn)?;
2929 Expression::TryCast(c)
2930 }
2931 Expression::SafeCast(mut c) => {
2932 c.this = transform_recursive(c.this, transform_fn)?;
2933 c.to = transform_data_type_recursive(c.to, transform_fn)?;
2934 Expression::SafeCast(c)
2935 }
2936
2937 // ===== Misc =====
2938 Expression::Unnest(mut f) => {
2939 f.this = transform_recursive(f.this, transform_fn)?;
2940 f.expressions = f
2941 .expressions
2942 .into_iter()
2943 .map(|e| transform_recursive(e, transform_fn))
2944 .collect::<Result<Vec<_>>>()?;
2945 Expression::Unnest(f)
2946 }
2947 Expression::Explode(mut f) => {
2948 f.this = transform_recursive(f.this, transform_fn)?;
2949 Expression::Explode(f)
2950 }
2951 Expression::GroupConcat(mut f) => {
2952 f.this = transform_recursive(f.this, transform_fn)?;
2953 Expression::GroupConcat(f)
2954 }
2955 Expression::StringAgg(mut f) => {
2956 f.this = transform_recursive(f.this, transform_fn)?;
2957 if let Some(order_by) = f.order_by.take() {
2958 f.order_by = Some(
2959 order_by
2960 .into_iter()
2961 .map(|mut ordered| {
2962 let original = ordered.this.clone();
2963 ordered.this =
2964 transform_recursive(ordered.this, transform_fn).unwrap_or(original);
2965 match transform_fn(Expression::Ordered(Box::new(ordered.clone()))) {
2966 Ok(Expression::Ordered(transformed)) => Ok(*transformed),
2967 Ok(_) | Err(_) => Ok(ordered),
2968 }
2969 })
2970 .collect::<Result<Vec<_>>>()?,
2971 );
2972 }
2973 Expression::StringAgg(f)
2974 }
2975 Expression::ListAgg(mut f) => {
2976 f.this = transform_recursive(f.this, transform_fn)?;
2977 Expression::ListAgg(f)
2978 }
2979 Expression::ArrayAgg(mut f) => {
2980 f.this = transform_recursive(f.this, transform_fn)?;
2981 Expression::ArrayAgg(f)
2982 }
2983 Expression::ParseJson(mut f) => {
2984 f.this = transform_recursive(f.this, transform_fn)?;
2985 Expression::ParseJson(f)
2986 }
2987 Expression::ToJson(mut f) => {
2988 f.this = transform_recursive(f.this, transform_fn)?;
2989 Expression::ToJson(f)
2990 }
2991 Expression::JSONExtract(mut e) => {
2992 e.this = Box::new(transform_recursive(*e.this, transform_fn)?);
2993 e.expression = Box::new(transform_recursive(*e.expression, transform_fn)?);
2994 Expression::JSONExtract(e)
2995 }
2996 Expression::JSONExtractScalar(mut e) => {
2997 e.this = Box::new(transform_recursive(*e.this, transform_fn)?);
2998 e.expression = Box::new(transform_recursive(*e.expression, transform_fn)?);
2999 Expression::JSONExtractScalar(e)
3000 }
3001
3002 // StrToTime: recurse into this
3003 Expression::StrToTime(mut e) => {
3004 e.this = Box::new(transform_recursive(*e.this, transform_fn)?);
3005 Expression::StrToTime(e)
3006 }
3007
3008 // UnixToTime: recurse into this
3009 Expression::UnixToTime(mut e) => {
3010 e.this = Box::new(transform_recursive(*e.this, transform_fn)?);
3011 Expression::UnixToTime(e)
3012 }
3013
3014 // CreateTable: recurse into column defaults, on_update expressions, and data types
3015 Expression::CreateTable(mut ct) => {
3016 for col in &mut ct.columns {
3017 if let Some(default_expr) = col.default.take() {
3018 col.default = Some(transform_recursive(default_expr, transform_fn)?);
3019 }
3020 if let Some(on_update_expr) = col.on_update.take() {
3021 col.on_update = Some(transform_recursive(on_update_expr, transform_fn)?);
3022 }
3023 // Note: Column data type transformations (INT -> INT64 for BigQuery, etc.)
3024 // are NOT applied here because per-dialect transforms are designed for CAST/expression
3025 // contexts and may not produce correct results for DDL column definitions.
3026 // The DDL type mappings would need dedicated handling per source/target pair.
3027 }
3028 if let Some(as_select) = ct.as_select.take() {
3029 ct.as_select = Some(transform_recursive(as_select, transform_fn)?);
3030 }
3031 Expression::CreateTable(ct)
3032 }
3033
3034 // CreateView: recurse into the view body query
3035 Expression::CreateView(mut cv) => {
3036 cv.query = transform_recursive(cv.query, transform_fn)?;
3037 Expression::CreateView(cv)
3038 }
3039
3040 // CreateTask: recurse into the task body
3041 Expression::CreateTask(mut ct) => {
3042 ct.body = transform_recursive(ct.body, transform_fn)?;
3043 Expression::CreateTask(ct)
3044 }
3045
3046 // Prepare: recurse into the prepared statement body
3047 Expression::Prepare(mut prepare) => {
3048 prepare.statement = transform_recursive(prepare.statement, transform_fn)?;
3049 Expression::Prepare(prepare)
3050 }
3051
3052 // Execute: recurse into procedure/prepared name and argument values
3053 Expression::Execute(mut execute) => {
3054 execute.this = transform_recursive(execute.this, transform_fn)?;
3055 execute.arguments = execute
3056 .arguments
3057 .into_iter()
3058 .map(|argument| transform_recursive(argument, transform_fn))
3059 .collect::<Result<Vec<_>>>()?;
3060 execute.parameters = execute
3061 .parameters
3062 .into_iter()
3063 .map(|mut parameter| {
3064 parameter.value = transform_recursive(parameter.value, transform_fn)?;
3065 Ok(parameter)
3066 })
3067 .collect::<Result<Vec<_>>>()?;
3068 Expression::Execute(execute)
3069 }
3070
3071 // CreateProcedure: recurse into body expressions
3072 Expression::CreateProcedure(mut cp) => {
3073 if let Some(body) = cp.body.take() {
3074 cp.body = Some(match body {
3075 FunctionBody::Expression(expr) => {
3076 FunctionBody::Expression(transform_recursive(expr, transform_fn)?)
3077 }
3078 FunctionBody::Return(expr) => {
3079 FunctionBody::Return(transform_recursive(expr, transform_fn)?)
3080 }
3081 FunctionBody::Statements(stmts) => {
3082 let transformed_stmts = stmts
3083 .into_iter()
3084 .map(|s| transform_recursive(s, transform_fn))
3085 .collect::<Result<Vec<_>>>()?;
3086 FunctionBody::Statements(transformed_stmts)
3087 }
3088 other => other,
3089 });
3090 }
3091 Expression::CreateProcedure(cp)
3092 }
3093
3094 // CreateFunction: recurse into body expressions
3095 Expression::CreateFunction(mut cf) => {
3096 if let Some(body) = cf.body.take() {
3097 cf.body = Some(match body {
3098 FunctionBody::Expression(expr) => {
3099 FunctionBody::Expression(transform_recursive(expr, transform_fn)?)
3100 }
3101 FunctionBody::Return(expr) => {
3102 FunctionBody::Return(transform_recursive(expr, transform_fn)?)
3103 }
3104 FunctionBody::Statements(stmts) => {
3105 let transformed_stmts = stmts
3106 .into_iter()
3107 .map(|s| transform_recursive(s, transform_fn))
3108 .collect::<Result<Vec<_>>>()?;
3109 FunctionBody::Statements(transformed_stmts)
3110 }
3111 other => other,
3112 });
3113 }
3114 Expression::CreateFunction(cf)
3115 }
3116
3117 // MemberOf: recurse into left and right operands
3118 Expression::MemberOf(op) => transform_binary!(MemberOf, *op),
3119 // ArrayContainsAll (@>): recurse into left and right operands
3120 Expression::ArrayContainsAll(op) => transform_binary!(ArrayContainsAll, *op),
3121 // ArrayContainedBy (<@): recurse into left and right operands
3122 Expression::ArrayContainedBy(op) => transform_binary!(ArrayContainedBy, *op),
3123 // ArrayOverlaps (&&): recurse into left and right operands
3124 Expression::ArrayOverlaps(op) => transform_binary!(ArrayOverlaps, *op),
3125 // TsMatch (@@): recurse into left and right operands
3126 Expression::TsMatch(op) => transform_binary!(TsMatch, *op),
3127 // Adjacent (-|-): recurse into left and right operands
3128 Expression::Adjacent(op) => transform_binary!(Adjacent, *op),
3129
3130 // Table: recurse into when (HistoricalData) and changes fields
3131 Expression::Table(mut t) => {
3132 if let Some(when) = t.when.take() {
3133 let transformed =
3134 transform_recursive(Expression::HistoricalData(when), transform_fn)?;
3135 if let Expression::HistoricalData(hd) = transformed {
3136 t.when = Some(hd);
3137 }
3138 }
3139 if let Some(changes) = t.changes.take() {
3140 let transformed = transform_recursive(Expression::Changes(changes), transform_fn)?;
3141 if let Expression::Changes(c) = transformed {
3142 t.changes = Some(c);
3143 }
3144 }
3145 Expression::Table(t)
3146 }
3147
3148 // HistoricalData (Snowflake time travel): recurse into expression
3149 Expression::HistoricalData(mut hd) => {
3150 *hd.expression = transform_recursive(*hd.expression, transform_fn)?;
3151 Expression::HistoricalData(hd)
3152 }
3153
3154 // Changes (Snowflake CHANGES clause): recurse into at_before and end
3155 Expression::Changes(mut c) => {
3156 if let Some(at_before) = c.at_before.take() {
3157 c.at_before = Some(Box::new(transform_recursive(*at_before, transform_fn)?));
3158 }
3159 if let Some(end) = c.end.take() {
3160 c.end = Some(Box::new(transform_recursive(*end, transform_fn)?));
3161 }
3162 Expression::Changes(c)
3163 }
3164
3165 // TableArgument: TABLE(expr) or MODEL(expr)
3166 Expression::TableArgument(mut ta) => {
3167 ta.this = transform_recursive(ta.this, transform_fn)?;
3168 Expression::TableArgument(ta)
3169 }
3170
3171 // JoinedTable: (tbl1 JOIN tbl2 ON ...) - recurse into left and join tables
3172 Expression::JoinedTable(mut jt) => {
3173 jt.left = transform_recursive(jt.left, transform_fn)?;
3174 jt.joins = jt
3175 .joins
3176 .into_iter()
3177 .map(|mut join| {
3178 join.this = transform_recursive(join.this, transform_fn)?;
3179 if let Some(on) = join.on.take() {
3180 join.on = Some(transform_recursive(on, transform_fn)?);
3181 }
3182 match transform_fn(Expression::Join(Box::new(join)))? {
3183 Expression::Join(j) => Ok(*j),
3184 _ => Err(crate::error::Error::parse(
3185 "Join transformation returned non-join expression",
3186 0,
3187 0,
3188 0,
3189 0,
3190 )),
3191 }
3192 })
3193 .collect::<Result<Vec<_>>>()?;
3194 jt.lateral_views = jt
3195 .lateral_views
3196 .into_iter()
3197 .map(|mut lv| {
3198 lv.this = transform_recursive(lv.this, transform_fn)?;
3199 Ok(lv)
3200 })
3201 .collect::<Result<Vec<_>>>()?;
3202 Expression::JoinedTable(jt)
3203 }
3204
3205 // Lateral: LATERAL func() - recurse into the function expression
3206 Expression::Lateral(mut lat) => {
3207 *lat.this = transform_recursive(*lat.this, transform_fn)?;
3208 Expression::Lateral(lat)
3209 }
3210
3211 // WithinGroup: recurse into order_by items (for NULLS FIRST/LAST etc.)
3212 // but NOT into wg.this - the inner function is handled by StringAggConvert/GroupConcatConvert
3213 // as a unit together with the WithinGroup wrapper
3214 Expression::WithinGroup(mut wg) => {
3215 wg.order_by = wg
3216 .order_by
3217 .into_iter()
3218 .map(|mut o| {
3219 let original = o.this.clone();
3220 o.this = transform_recursive(o.this, transform_fn).unwrap_or(original);
3221 match transform_fn(Expression::Ordered(Box::new(o.clone()))) {
3222 Ok(Expression::Ordered(transformed)) => *transformed,
3223 Ok(_) | Err(_) => o,
3224 }
3225 })
3226 .collect();
3227 Expression::WithinGroup(wg)
3228 }
3229
3230 // Filter: recurse into both the aggregate and the filter condition
3231 Expression::Filter(mut f) => {
3232 f.this = Box::new(transform_recursive(*f.this, transform_fn)?);
3233 f.expression = Box::new(transform_recursive(*f.expression, transform_fn)?);
3234 Expression::Filter(f)
3235 }
3236
3237 // Aggregate functions (AggFunc-based): recurse into the aggregate argument,
3238 // filter, order_by, having_max, and limit.
3239 // Stddev, StddevSamp, Variance, and ArrayAgg are handled earlier in this match.
3240 Expression::Sum(f) => recurse_agg!(Sum, f),
3241 Expression::Avg(f) => recurse_agg!(Avg, f),
3242 Expression::Min(f) => recurse_agg!(Min, f),
3243 Expression::Max(f) => recurse_agg!(Max, f),
3244 Expression::CountIf(f) => recurse_agg!(CountIf, f),
3245 Expression::StddevPop(f) => recurse_agg!(StddevPop, f),
3246 Expression::VarPop(f) => recurse_agg!(VarPop, f),
3247 Expression::VarSamp(f) => recurse_agg!(VarSamp, f),
3248 Expression::Median(f) => recurse_agg!(Median, f),
3249 Expression::Mode(f) => recurse_agg!(Mode, f),
3250 Expression::First(f) => recurse_agg!(First, f),
3251 Expression::Last(f) => recurse_agg!(Last, f),
3252 Expression::AnyValue(f) => recurse_agg!(AnyValue, f),
3253 Expression::ApproxDistinct(f) => recurse_agg!(ApproxDistinct, f),
3254 Expression::ApproxCountDistinct(f) => recurse_agg!(ApproxCountDistinct, f),
3255 Expression::LogicalAnd(f) => recurse_agg!(LogicalAnd, f),
3256 Expression::LogicalOr(f) => recurse_agg!(LogicalOr, f),
3257 Expression::Skewness(f) => recurse_agg!(Skewness, f),
3258 Expression::ArrayConcatAgg(f) => recurse_agg!(ArrayConcatAgg, f),
3259 Expression::ArrayUniqueAgg(f) => recurse_agg!(ArrayUniqueAgg, f),
3260 Expression::BoolXorAgg(f) => recurse_agg!(BoolXorAgg, f),
3261 Expression::BitwiseOrAgg(f) => recurse_agg!(BitwiseOrAgg, f),
3262 Expression::BitwiseAndAgg(f) => recurse_agg!(BitwiseAndAgg, f),
3263 Expression::BitwiseXorAgg(f) => recurse_agg!(BitwiseXorAgg, f),
3264
3265 // Count has its own struct with an Option<Expression> `this` field
3266 Expression::Count(mut c) => {
3267 if let Some(this) = c.this.take() {
3268 c.this = Some(transform_recursive(this, transform_fn)?);
3269 }
3270 if let Some(filter) = c.filter.take() {
3271 c.filter = Some(transform_recursive(filter, transform_fn)?);
3272 }
3273 Expression::Count(c)
3274 }
3275
3276 Expression::PipeOperator(mut pipe) => {
3277 pipe.this = transform_recursive(pipe.this, transform_fn)?;
3278 pipe.expression = transform_recursive(pipe.expression, transform_fn)?;
3279 Expression::PipeOperator(pipe)
3280 }
3281
3282 // ArrayExcept/ArrayContains/ArrayDistinct: recurse into children
3283 Expression::ArrayExcept(mut f) => {
3284 f.this = transform_recursive(f.this, transform_fn)?;
3285 f.expression = transform_recursive(f.expression, transform_fn)?;
3286 Expression::ArrayExcept(f)
3287 }
3288 Expression::ArrayContains(mut f) => {
3289 f.this = transform_recursive(f.this, transform_fn)?;
3290 f.expression = transform_recursive(f.expression, transform_fn)?;
3291 Expression::ArrayContains(f)
3292 }
3293 Expression::ArrayDistinct(mut f) => {
3294 f.this = transform_recursive(f.this, transform_fn)?;
3295 Expression::ArrayDistinct(f)
3296 }
3297 Expression::ArrayPosition(mut f) => {
3298 f.this = transform_recursive(f.this, transform_fn)?;
3299 f.expression = transform_recursive(f.expression, transform_fn)?;
3300 Expression::ArrayPosition(f)
3301 }
3302
3303 // Pass through leaf nodes unchanged
3304 other => other,
3305 };
3306
3307 // Then apply the transform function
3308 transform_fn(expr)
3309}
3310
3311/// Returns the tokenizer config, generator config, and expression transform closure
3312/// for a built-in dialect type. This is the shared implementation used by both
3313/// `Dialect::get()` and custom dialect construction.
3314// ---------------------------------------------------------------------------
3315// Cached dialect configurations
3316// ---------------------------------------------------------------------------
3317
3318/// Pre-computed tokenizer + generator configs for a dialect, cached via `LazyLock`.
3319/// Transform closures are cheap (unit-struct method calls) and created fresh each time.
3320struct CachedDialectConfig {
3321 tokenizer_config: TokenizerConfig,
3322 #[cfg(feature = "generate")]
3323 generator_config: Arc<GeneratorConfig>,
3324}
3325
3326struct DialectConfigs {
3327 tokenizer_config: TokenizerConfig,
3328 #[cfg(feature = "generate")]
3329 generator_config: Arc<GeneratorConfig>,
3330 #[cfg(feature = "transpile")]
3331 transformer: Box<dyn Fn(Expression) -> Result<Expression> + Send + Sync>,
3332}
3333
3334/// Declare a per-dialect `LazyLock<CachedDialectConfig>` static.
3335macro_rules! cached_dialect {
3336 ($static_name:ident, $dialect_struct:expr, $feature:literal) => {
3337 #[cfg(feature = $feature)]
3338 static $static_name: LazyLock<CachedDialectConfig> = LazyLock::new(|| {
3339 let d = $dialect_struct;
3340 CachedDialectConfig {
3341 tokenizer_config: d.tokenizer_config(),
3342 #[cfg(feature = "generate")]
3343 generator_config: Arc::new(d.generator_config()),
3344 }
3345 });
3346 };
3347}
3348
3349static CACHED_GENERIC: LazyLock<CachedDialectConfig> = LazyLock::new(|| {
3350 let d = GenericDialect;
3351 CachedDialectConfig {
3352 tokenizer_config: d.tokenizer_config(),
3353 #[cfg(feature = "generate")]
3354 generator_config: Arc::new(d.generator_config()),
3355 }
3356});
3357
3358cached_dialect!(CACHED_POSTGRESQL, PostgresDialect, "dialect-postgresql");
3359cached_dialect!(CACHED_MYSQL, MySQLDialect, "dialect-mysql");
3360cached_dialect!(CACHED_BIGQUERY, BigQueryDialect, "dialect-bigquery");
3361cached_dialect!(CACHED_SNOWFLAKE, SnowflakeDialect, "dialect-snowflake");
3362cached_dialect!(CACHED_DUCKDB, DuckDBDialect, "dialect-duckdb");
3363cached_dialect!(CACHED_TSQL, TSQLDialect, "dialect-tsql");
3364cached_dialect!(CACHED_ORACLE, OracleDialect, "dialect-oracle");
3365cached_dialect!(CACHED_HIVE, HiveDialect, "dialect-hive");
3366cached_dialect!(CACHED_SPARK, SparkDialect, "dialect-spark");
3367cached_dialect!(CACHED_SQLITE, SQLiteDialect, "dialect-sqlite");
3368cached_dialect!(CACHED_PRESTO, PrestoDialect, "dialect-presto");
3369cached_dialect!(CACHED_TRINO, TrinoDialect, "dialect-trino");
3370cached_dialect!(CACHED_REDSHIFT, RedshiftDialect, "dialect-redshift");
3371cached_dialect!(CACHED_CLICKHOUSE, ClickHouseDialect, "dialect-clickhouse");
3372cached_dialect!(CACHED_DATABRICKS, DatabricksDialect, "dialect-databricks");
3373cached_dialect!(CACHED_ATHENA, AthenaDialect, "dialect-athena");
3374cached_dialect!(CACHED_TERADATA, TeradataDialect, "dialect-teradata");
3375cached_dialect!(CACHED_DORIS, DorisDialect, "dialect-doris");
3376cached_dialect!(CACHED_STARROCKS, StarRocksDialect, "dialect-starrocks");
3377cached_dialect!(
3378 CACHED_MATERIALIZE,
3379 MaterializeDialect,
3380 "dialect-materialize"
3381);
3382cached_dialect!(CACHED_RISINGWAVE, RisingWaveDialect, "dialect-risingwave");
3383cached_dialect!(
3384 CACHED_SINGLESTORE,
3385 SingleStoreDialect,
3386 "dialect-singlestore"
3387);
3388cached_dialect!(
3389 CACHED_COCKROACHDB,
3390 CockroachDBDialect,
3391 "dialect-cockroachdb"
3392);
3393cached_dialect!(CACHED_TIDB, TiDBDialect, "dialect-tidb");
3394cached_dialect!(CACHED_DRUID, DruidDialect, "dialect-druid");
3395cached_dialect!(CACHED_SOLR, SolrDialect, "dialect-solr");
3396cached_dialect!(CACHED_TABLEAU, TableauDialect, "dialect-tableau");
3397cached_dialect!(CACHED_DUNE, DuneDialect, "dialect-dune");
3398cached_dialect!(CACHED_FABRIC, FabricDialect, "dialect-fabric");
3399cached_dialect!(CACHED_DRILL, DrillDialect, "dialect-drill");
3400cached_dialect!(CACHED_DREMIO, DremioDialect, "dialect-dremio");
3401cached_dialect!(CACHED_EXASOL, ExasolDialect, "dialect-exasol");
3402cached_dialect!(CACHED_DATAFUSION, DataFusionDialect, "dialect-datafusion");
3403
3404fn configs_for_dialect_type(dt: DialectType) -> DialectConfigs {
3405 /// Clone configs from a cached static and pair with a fresh transform closure.
3406 macro_rules! from_cache {
3407 ($cache:expr, $dialect_struct:expr) => {{
3408 let c = &*$cache;
3409 DialectConfigs {
3410 tokenizer_config: c.tokenizer_config.clone(),
3411 #[cfg(feature = "generate")]
3412 generator_config: c.generator_config.clone(),
3413 #[cfg(feature = "transpile")]
3414 transformer: Box::new(move |e| $dialect_struct.transform_expr(e)),
3415 }
3416 }};
3417 }
3418 match dt {
3419 #[cfg(feature = "dialect-postgresql")]
3420 DialectType::PostgreSQL => from_cache!(CACHED_POSTGRESQL, PostgresDialect),
3421 #[cfg(feature = "dialect-mysql")]
3422 DialectType::MySQL => from_cache!(CACHED_MYSQL, MySQLDialect),
3423 #[cfg(feature = "dialect-bigquery")]
3424 DialectType::BigQuery => from_cache!(CACHED_BIGQUERY, BigQueryDialect),
3425 #[cfg(feature = "dialect-snowflake")]
3426 DialectType::Snowflake => from_cache!(CACHED_SNOWFLAKE, SnowflakeDialect),
3427 #[cfg(feature = "dialect-duckdb")]
3428 DialectType::DuckDB => from_cache!(CACHED_DUCKDB, DuckDBDialect),
3429 #[cfg(feature = "dialect-tsql")]
3430 DialectType::TSQL => from_cache!(CACHED_TSQL, TSQLDialect),
3431 #[cfg(feature = "dialect-oracle")]
3432 DialectType::Oracle => from_cache!(CACHED_ORACLE, OracleDialect),
3433 #[cfg(feature = "dialect-hive")]
3434 DialectType::Hive => from_cache!(CACHED_HIVE, HiveDialect),
3435 #[cfg(feature = "dialect-spark")]
3436 DialectType::Spark => from_cache!(CACHED_SPARK, SparkDialect),
3437 #[cfg(feature = "dialect-sqlite")]
3438 DialectType::SQLite => from_cache!(CACHED_SQLITE, SQLiteDialect),
3439 #[cfg(feature = "dialect-presto")]
3440 DialectType::Presto => from_cache!(CACHED_PRESTO, PrestoDialect),
3441 #[cfg(feature = "dialect-trino")]
3442 DialectType::Trino => from_cache!(CACHED_TRINO, TrinoDialect),
3443 #[cfg(feature = "dialect-redshift")]
3444 DialectType::Redshift => from_cache!(CACHED_REDSHIFT, RedshiftDialect),
3445 #[cfg(feature = "dialect-clickhouse")]
3446 DialectType::ClickHouse => from_cache!(CACHED_CLICKHOUSE, ClickHouseDialect),
3447 #[cfg(feature = "dialect-databricks")]
3448 DialectType::Databricks => from_cache!(CACHED_DATABRICKS, DatabricksDialect),
3449 #[cfg(feature = "dialect-athena")]
3450 DialectType::Athena => from_cache!(CACHED_ATHENA, AthenaDialect),
3451 #[cfg(feature = "dialect-teradata")]
3452 DialectType::Teradata => from_cache!(CACHED_TERADATA, TeradataDialect),
3453 #[cfg(feature = "dialect-doris")]
3454 DialectType::Doris => from_cache!(CACHED_DORIS, DorisDialect),
3455 #[cfg(feature = "dialect-starrocks")]
3456 DialectType::StarRocks => from_cache!(CACHED_STARROCKS, StarRocksDialect),
3457 #[cfg(feature = "dialect-materialize")]
3458 DialectType::Materialize => from_cache!(CACHED_MATERIALIZE, MaterializeDialect),
3459 #[cfg(feature = "dialect-risingwave")]
3460 DialectType::RisingWave => from_cache!(CACHED_RISINGWAVE, RisingWaveDialect),
3461 #[cfg(feature = "dialect-singlestore")]
3462 DialectType::SingleStore => from_cache!(CACHED_SINGLESTORE, SingleStoreDialect),
3463 #[cfg(feature = "dialect-cockroachdb")]
3464 DialectType::CockroachDB => from_cache!(CACHED_COCKROACHDB, CockroachDBDialect),
3465 #[cfg(feature = "dialect-tidb")]
3466 DialectType::TiDB => from_cache!(CACHED_TIDB, TiDBDialect),
3467 #[cfg(feature = "dialect-druid")]
3468 DialectType::Druid => from_cache!(CACHED_DRUID, DruidDialect),
3469 #[cfg(feature = "dialect-solr")]
3470 DialectType::Solr => from_cache!(CACHED_SOLR, SolrDialect),
3471 #[cfg(feature = "dialect-tableau")]
3472 DialectType::Tableau => from_cache!(CACHED_TABLEAU, TableauDialect),
3473 #[cfg(feature = "dialect-dune")]
3474 DialectType::Dune => from_cache!(CACHED_DUNE, DuneDialect),
3475 #[cfg(feature = "dialect-fabric")]
3476 DialectType::Fabric => from_cache!(CACHED_FABRIC, FabricDialect),
3477 #[cfg(feature = "dialect-drill")]
3478 DialectType::Drill => from_cache!(CACHED_DRILL, DrillDialect),
3479 #[cfg(feature = "dialect-dremio")]
3480 DialectType::Dremio => from_cache!(CACHED_DREMIO, DremioDialect),
3481 #[cfg(feature = "dialect-exasol")]
3482 DialectType::Exasol => from_cache!(CACHED_EXASOL, ExasolDialect),
3483 #[cfg(feature = "dialect-datafusion")]
3484 DialectType::DataFusion => from_cache!(CACHED_DATAFUSION, DataFusionDialect),
3485 _ => from_cache!(CACHED_GENERIC, GenericDialect),
3486 }
3487}
3488
3489// ---------------------------------------------------------------------------
3490// Custom dialect registry
3491// ---------------------------------------------------------------------------
3492
3493static CUSTOM_DIALECT_REGISTRY: LazyLock<RwLock<HashMap<String, Arc<CustomDialectConfig>>>> =
3494 LazyLock::new(|| RwLock::new(HashMap::new()));
3495
3496struct CustomDialectConfig {
3497 name: String,
3498 base_dialect: DialectType,
3499 tokenizer_config: TokenizerConfig,
3500 #[cfg(feature = "generate")]
3501 generator_config: GeneratorConfig,
3502 #[cfg(feature = "transpile")]
3503 transform: Option<Arc<dyn Fn(Expression) -> Result<Expression> + Send + Sync>>,
3504 #[cfg(feature = "transpile")]
3505 preprocess: Option<Arc<dyn Fn(Expression) -> Result<Expression> + Send + Sync>>,
3506}
3507
3508/// Fluent builder for creating and registering custom SQL dialects.
3509///
3510/// A custom dialect is based on an existing built-in dialect and allows selective
3511/// overrides of tokenizer configuration, generator configuration, and expression
3512/// transforms.
3513///
3514/// # Example
3515///
3516/// ```rust,ignore
3517/// use polyglot_sql::dialects::{CustomDialectBuilder, DialectType, Dialect};
3518/// use polyglot_sql::generator::NormalizeFunctions;
3519///
3520/// CustomDialectBuilder::new("my_postgres")
3521/// .based_on(DialectType::PostgreSQL)
3522/// .generator_config_modifier(|gc| {
3523/// gc.normalize_functions = NormalizeFunctions::Lower;
3524/// })
3525/// .register()
3526/// .unwrap();
3527///
3528/// let d = Dialect::get_by_name("my_postgres").unwrap();
3529/// let exprs = d.parse("SELECT COUNT(*)").unwrap();
3530/// let sql = d.generate(&exprs[0]).unwrap();
3531/// assert_eq!(sql, "select count(*)");
3532///
3533/// polyglot_sql::unregister_custom_dialect("my_postgres");
3534/// ```
3535pub struct CustomDialectBuilder {
3536 name: String,
3537 base_dialect: DialectType,
3538 tokenizer_modifier: Option<Box<dyn FnOnce(&mut TokenizerConfig)>>,
3539 #[cfg(feature = "generate")]
3540 generator_modifier: Option<Box<dyn FnOnce(&mut GeneratorConfig)>>,
3541 #[cfg(feature = "transpile")]
3542 transform: Option<Arc<dyn Fn(Expression) -> Result<Expression> + Send + Sync>>,
3543 #[cfg(feature = "transpile")]
3544 preprocess: Option<Arc<dyn Fn(Expression) -> Result<Expression> + Send + Sync>>,
3545}
3546
3547impl CustomDialectBuilder {
3548 /// Create a new builder with the given name. Defaults to `Generic` as the base dialect.
3549 pub fn new(name: impl Into<String>) -> Self {
3550 Self {
3551 name: name.into(),
3552 base_dialect: DialectType::Generic,
3553 tokenizer_modifier: None,
3554 #[cfg(feature = "generate")]
3555 generator_modifier: None,
3556 #[cfg(feature = "transpile")]
3557 transform: None,
3558 #[cfg(feature = "transpile")]
3559 preprocess: None,
3560 }
3561 }
3562
3563 /// Set the base built-in dialect to inherit configuration from.
3564 pub fn based_on(mut self, dialect: DialectType) -> Self {
3565 self.base_dialect = dialect;
3566 self
3567 }
3568
3569 /// Provide a closure that modifies the tokenizer configuration inherited from the base dialect.
3570 pub fn tokenizer_config_modifier<F>(mut self, f: F) -> Self
3571 where
3572 F: FnOnce(&mut TokenizerConfig) + 'static,
3573 {
3574 self.tokenizer_modifier = Some(Box::new(f));
3575 self
3576 }
3577
3578 /// Provide a closure that modifies the generator configuration inherited from the base dialect.
3579 #[cfg(feature = "generate")]
3580 pub fn generator_config_modifier<F>(mut self, f: F) -> Self
3581 where
3582 F: FnOnce(&mut GeneratorConfig) + 'static,
3583 {
3584 self.generator_modifier = Some(Box::new(f));
3585 self
3586 }
3587
3588 /// Set a custom per-node expression transform function.
3589 ///
3590 /// This replaces the base dialect's transform. It is called on every expression
3591 /// node during the recursive transform pass.
3592 #[cfg(feature = "transpile")]
3593 pub fn transform_fn<F>(mut self, f: F) -> Self
3594 where
3595 F: Fn(Expression) -> Result<Expression> + Send + Sync + 'static,
3596 {
3597 self.transform = Some(Arc::new(f));
3598 self
3599 }
3600
3601 /// Set a custom whole-tree preprocessing function.
3602 ///
3603 /// This replaces the base dialect's built-in preprocessing. It is called once
3604 /// on the entire expression tree before the recursive per-node transform.
3605 #[cfg(feature = "transpile")]
3606 pub fn preprocess_fn<F>(mut self, f: F) -> Self
3607 where
3608 F: Fn(Expression) -> Result<Expression> + Send + Sync + 'static,
3609 {
3610 self.preprocess = Some(Arc::new(f));
3611 self
3612 }
3613
3614 /// Build the custom dialect configuration and register it in the global registry.
3615 ///
3616 /// Returns an error if:
3617 /// - The name collides with a built-in dialect name
3618 /// - A custom dialect with the same name is already registered
3619 pub fn register(self) -> Result<()> {
3620 // Reject names that collide with built-in dialects
3621 if DialectType::from_str(&self.name).is_ok() {
3622 return Err(crate::error::Error::parse(
3623 format!(
3624 "Cannot register custom dialect '{}': name collides with built-in dialect",
3625 self.name
3626 ),
3627 0,
3628 0,
3629 0,
3630 0,
3631 ));
3632 }
3633
3634 // Get base configs
3635 let base_configs = configs_for_dialect_type(self.base_dialect);
3636 let mut tok_config = base_configs.tokenizer_config;
3637 #[cfg(feature = "generate")]
3638 let mut gen_config = (*base_configs.generator_config).clone();
3639
3640 // Apply modifiers
3641 if let Some(tok_mod) = self.tokenizer_modifier {
3642 tok_mod(&mut tok_config);
3643 }
3644 #[cfg(feature = "generate")]
3645 if let Some(gen_mod) = self.generator_modifier {
3646 gen_mod(&mut gen_config);
3647 }
3648
3649 let config = CustomDialectConfig {
3650 name: self.name.clone(),
3651 base_dialect: self.base_dialect,
3652 tokenizer_config: tok_config,
3653 #[cfg(feature = "generate")]
3654 generator_config: gen_config,
3655 #[cfg(feature = "transpile")]
3656 transform: self.transform,
3657 #[cfg(feature = "transpile")]
3658 preprocess: self.preprocess,
3659 };
3660
3661 register_custom_dialect(config)
3662 }
3663}
3664
3665use std::str::FromStr;
3666
3667fn register_custom_dialect(config: CustomDialectConfig) -> Result<()> {
3668 let mut registry = CUSTOM_DIALECT_REGISTRY.write().map_err(|e| {
3669 crate::error::Error::parse(format!("Registry lock poisoned: {}", e), 0, 0, 0, 0)
3670 })?;
3671
3672 if registry.contains_key(&config.name) {
3673 return Err(crate::error::Error::parse(
3674 format!("Custom dialect '{}' is already registered", config.name),
3675 0,
3676 0,
3677 0,
3678 0,
3679 ));
3680 }
3681
3682 registry.insert(config.name.clone(), Arc::new(config));
3683 Ok(())
3684}
3685
3686/// Remove a custom dialect from the global registry.
3687///
3688/// Returns `true` if a dialect with that name was found and removed,
3689/// `false` if no such custom dialect existed.
3690pub fn unregister_custom_dialect(name: &str) -> bool {
3691 if let Ok(mut registry) = CUSTOM_DIALECT_REGISTRY.write() {
3692 registry.remove(name).is_some()
3693 } else {
3694 false
3695 }
3696}
3697
3698fn get_custom_dialect_config(name: &str) -> Option<Arc<CustomDialectConfig>> {
3699 CUSTOM_DIALECT_REGISTRY
3700 .read()
3701 .ok()
3702 .and_then(|registry| registry.get(name).cloned())
3703}
3704
3705/// Main entry point for dialect-specific SQL operations.
3706///
3707/// A `Dialect` bundles together a tokenizer, generator configuration, and expression
3708/// transformer for a specific SQL database engine. It is the high-level API through
3709/// which callers parse, generate, transform, and transpile SQL.
3710///
3711/// # Usage
3712///
3713/// ```rust,ignore
3714/// use polyglot_sql::dialects::{Dialect, DialectType};
3715///
3716/// // Parse PostgreSQL SQL into an AST
3717/// let pg = Dialect::get(DialectType::PostgreSQL);
3718/// let exprs = pg.parse("SELECT id, name FROM users WHERE active")?;
3719///
3720/// // Transpile from PostgreSQL to BigQuery
3721/// let results = pg.transpile("SELECT NOW()", DialectType::BigQuery)?;
3722/// assert_eq!(results[0], "SELECT CURRENT_TIMESTAMP()");
3723/// ```
3724///
3725/// Obtain an instance via [`Dialect::get`] or [`Dialect::get_by_name`].
3726/// The struct is `Send + Sync` safe so it can be shared across threads.
3727pub struct Dialect {
3728 dialect_type: DialectType,
3729 tokenizer: Tokenizer,
3730 #[cfg(feature = "generate")]
3731 generator_config: Arc<GeneratorConfig>,
3732 #[cfg(feature = "transpile")]
3733 transformer: Box<dyn Fn(Expression) -> Result<Expression> + Send + Sync>,
3734 /// Optional function to get expression-specific generator config (for hybrid dialects like Athena).
3735 #[cfg(feature = "generate")]
3736 generator_config_for_expr: Option<Box<dyn Fn(&Expression) -> GeneratorConfig + Send + Sync>>,
3737 /// Optional custom preprocessing function (overrides built-in preprocess for custom dialects).
3738 #[cfg(feature = "transpile")]
3739 custom_preprocess: Option<Box<dyn Fn(Expression) -> Result<Expression> + Send + Sync>>,
3740}
3741
3742/// Options for [`Dialect::transpile_with`].
3743///
3744/// Use [`TranspileOptions::default`] for defaults, then tweak the fields you need.
3745/// The struct is marked `#[non_exhaustive]` so new fields can be added without
3746/// breaking the API.
3747///
3748/// The struct derives `Serialize`/`Deserialize` using camelCase field names so
3749/// it can be round-tripped over JSON bridges (C FFI, WASM) without mapping.
3750#[cfg(feature = "transpile")]
3751#[derive(Debug, Clone, Serialize, Deserialize)]
3752#[serde(rename_all = "camelCase", default)]
3753#[non_exhaustive]
3754pub struct TranspileOptions {
3755 /// Whether to pretty-print the output SQL.
3756 pub pretty: bool,
3757 /// How unsupported target-dialect constructs should be handled.
3758 ///
3759 /// The default is [`UnsupportedLevel::Warn`], which preserves the current
3760 /// compatibility behavior and continues transpilation.
3761 pub unsupported_level: UnsupportedLevel,
3762 /// Maximum number of unsupported diagnostics to include in raised errors.
3763 pub max_unsupported: usize,
3764 /// Complexity guard limits used while parsing, transforming, and generating.
3765 pub complexity_guard: ComplexityGuardOptions,
3766}
3767
3768#[cfg(feature = "transpile")]
3769impl Default for TranspileOptions {
3770 fn default() -> Self {
3771 Self {
3772 pretty: false,
3773 unsupported_level: UnsupportedLevel::Warn,
3774 max_unsupported: 3,
3775 complexity_guard: ComplexityGuardOptions::default(),
3776 }
3777 }
3778}
3779
3780#[cfg(feature = "transpile")]
3781impl TranspileOptions {
3782 /// Construct options with pretty-printing enabled.
3783 pub fn pretty() -> Self {
3784 Self {
3785 pretty: true,
3786 ..Default::default()
3787 }
3788 }
3789
3790 /// Construct options that raise when known unsupported constructs remain.
3791 pub fn strict() -> Self {
3792 Self {
3793 unsupported_level: UnsupportedLevel::Raise,
3794 ..Default::default()
3795 }
3796 }
3797
3798 /// Set how unsupported target-dialect constructs should be handled.
3799 pub fn with_unsupported_level(mut self, level: UnsupportedLevel) -> Self {
3800 self.unsupported_level = level;
3801 self
3802 }
3803
3804 /// Set the maximum number of unsupported diagnostics to include in raised errors.
3805 pub fn with_max_unsupported(mut self, max: usize) -> Self {
3806 self.max_unsupported = max;
3807 self
3808 }
3809
3810 /// Set complexity guard limits for parse/transpile/generate recursion-heavy paths.
3811 pub fn with_complexity_guard(mut self, guard: ComplexityGuardOptions) -> Self {
3812 self.complexity_guard = guard;
3813 self
3814 }
3815}
3816
3817/// A value that can be used as the target dialect in [`Dialect::transpile`] /
3818/// [`Dialect::transpile_with`].
3819///
3820/// Implemented for [`DialectType`] (built-in dialect enum) and `&Dialect` (any
3821/// dialect handle, including custom ones). End users do not normally need to
3822/// implement this trait themselves.
3823#[cfg(feature = "transpile")]
3824pub trait TranspileTarget {
3825 /// Invoke `f` with a reference to the resolved target dialect.
3826 fn with_dialect<R>(self, f: impl FnOnce(&Dialect) -> R) -> R;
3827}
3828
3829#[cfg(feature = "transpile")]
3830impl TranspileTarget for DialectType {
3831 fn with_dialect<R>(self, f: impl FnOnce(&Dialect) -> R) -> R {
3832 f(&Dialect::get(self))
3833 }
3834}
3835
3836#[cfg(feature = "transpile")]
3837impl TranspileTarget for &Dialect {
3838 fn with_dialect<R>(self, f: impl FnOnce(&Dialect) -> R) -> R {
3839 f(self)
3840 }
3841}
3842
3843impl Dialect {
3844 /// Creates a fully configured [`Dialect`] instance for the given [`DialectType`].
3845 ///
3846 /// This is the primary constructor. It initializes the tokenizer, generator config,
3847 /// and expression transformer based on the dialect's [`DialectImpl`] implementation.
3848 /// For hybrid dialects like Athena, it also sets up expression-specific generator
3849 /// config routing.
3850 pub fn get(dialect_type: DialectType) -> Self {
3851 let configs = configs_for_dialect_type(dialect_type);
3852 let tokenizer_config = configs.tokenizer_config;
3853 #[cfg(feature = "generate")]
3854 let generator_config = configs.generator_config;
3855 #[cfg(feature = "transpile")]
3856 let transformer = configs.transformer;
3857
3858 // Set up expression-specific generator config for hybrid dialects
3859 #[cfg(feature = "generate")]
3860 let generator_config_for_expr: Option<
3861 Box<dyn Fn(&Expression) -> GeneratorConfig + Send + Sync>,
3862 > = match dialect_type {
3863 #[cfg(feature = "dialect-athena")]
3864 DialectType::Athena => Some(Box::new(|expr| {
3865 AthenaDialect.generator_config_for_expr(expr)
3866 })),
3867 _ => None,
3868 };
3869
3870 Self {
3871 dialect_type,
3872 tokenizer: Tokenizer::new(tokenizer_config),
3873 #[cfg(feature = "generate")]
3874 generator_config,
3875 #[cfg(feature = "transpile")]
3876 transformer,
3877 #[cfg(feature = "generate")]
3878 generator_config_for_expr,
3879 #[cfg(feature = "transpile")]
3880 custom_preprocess: None,
3881 }
3882 }
3883
3884 /// Look up a dialect by string name.
3885 ///
3886 /// Checks built-in dialect names first (via [`DialectType::from_str`]), then
3887 /// falls back to the custom dialect registry. Returns `None` if no dialect
3888 /// with the given name exists.
3889 pub fn get_by_name(name: &str) -> Option<Self> {
3890 // Try built-in first
3891 if let Ok(dt) = DialectType::from_str(name) {
3892 return Some(Self::get(dt));
3893 }
3894
3895 // Try custom registry
3896 let config = get_custom_dialect_config(name)?;
3897 Some(Self::from_custom_config(&config))
3898 }
3899
3900 /// Construct a `Dialect` from a custom dialect configuration.
3901 fn from_custom_config(config: &CustomDialectConfig) -> Self {
3902 // Build the transformer: use custom if provided, else use base dialect's
3903 #[cfg(feature = "transpile")]
3904 let transformer: Box<dyn Fn(Expression) -> Result<Expression> + Send + Sync> =
3905 if let Some(ref custom_transform) = config.transform {
3906 let t = Arc::clone(custom_transform);
3907 Box::new(move |e| t(e))
3908 } else {
3909 configs_for_dialect_type(config.base_dialect).transformer
3910 };
3911
3912 // Build the custom preprocess: use custom if provided
3913 #[cfg(feature = "transpile")]
3914 let custom_preprocess: Option<
3915 Box<dyn Fn(Expression) -> Result<Expression> + Send + Sync>,
3916 > = config.preprocess.as_ref().map(|p| {
3917 let p = Arc::clone(p);
3918 Box::new(move |e: Expression| p(e))
3919 as Box<dyn Fn(Expression) -> Result<Expression> + Send + Sync>
3920 });
3921
3922 Self {
3923 dialect_type: config.base_dialect,
3924 tokenizer: Tokenizer::new(config.tokenizer_config.clone()),
3925 #[cfg(feature = "generate")]
3926 generator_config: Arc::new(config.generator_config.clone()),
3927 #[cfg(feature = "transpile")]
3928 transformer,
3929 #[cfg(feature = "generate")]
3930 generator_config_for_expr: None,
3931 #[cfg(feature = "transpile")]
3932 custom_preprocess,
3933 }
3934 }
3935
3936 /// Get the dialect type
3937 pub fn dialect_type(&self) -> DialectType {
3938 self.dialect_type
3939 }
3940
3941 /// Get the generator configuration
3942 #[cfg(feature = "generate")]
3943 pub fn generator_config(&self) -> &GeneratorConfig {
3944 &self.generator_config
3945 }
3946
3947 /// Parses a SQL string into a list of [`Expression`] AST nodes.
3948 ///
3949 /// The input may contain multiple semicolon-separated statements; each one
3950 /// produces a separate element in the returned vector. Tokenization uses
3951 /// this dialect's configured tokenizer, and parsing uses the dialect-aware parser.
3952 pub fn parse(&self, sql: &str) -> Result<Vec<Expression>> {
3953 self.parse_with_guard(sql, self.default_complexity_guard())
3954 }
3955
3956 fn parse_with_guard(
3957 &self,
3958 sql: &str,
3959 complexity_guard: ComplexityGuardOptions,
3960 ) -> Result<Vec<Expression>> {
3961 enforce_input(sql, &complexity_guard)?;
3962 let tokens = self.tokenizer.tokenize(sql)?;
3963 let config = crate::parser::ParserConfig {
3964 dialect: Some(self.dialect_type),
3965 complexity_guard,
3966 ..Default::default()
3967 };
3968 let mut parser = Parser::with_source(tokens, config, sql.to_string());
3969 parser.parse()
3970 }
3971
3972 fn default_complexity_guard(&self) -> ComplexityGuardOptions {
3973 let mut guard = ComplexityGuardOptions::default();
3974 if matches!(self.dialect_type, DialectType::ClickHouse) {
3975 guard.max_ast_depth = Some(4_096);
3976 guard.max_function_call_depth = Some(512);
3977 }
3978 guard
3979 }
3980
3981 #[cfg(feature = "transpile")]
3982 fn default_transpile_complexity_guard(
3983 &self,
3984 target_dialect: &Dialect,
3985 guard: ComplexityGuardOptions,
3986 ) -> ComplexityGuardOptions {
3987 if guard != ComplexityGuardOptions::default() {
3988 return guard;
3989 }
3990
3991 if matches!(self.dialect_type, DialectType::ClickHouse)
3992 || matches!(target_dialect.dialect_type, DialectType::ClickHouse)
3993 {
3994 let mut guard = guard;
3995 guard.max_ast_depth = Some(4_096);
3996 guard.max_function_call_depth = Some(512);
3997 guard
3998 } else {
3999 guard
4000 }
4001 }
4002
4003 /// Parse a standalone SQL data type using this dialect's tokenizer and parser.
4004 ///
4005 /// This accepts type strings such as `DECIMAL(10, 2)`, `INT[]`, or
4006 /// `STRUCT(a INT, b VARCHAR)` without requiring a surrounding statement.
4007 pub fn parse_data_type(&self, sql: &str) -> Result<DataType> {
4008 let complexity_guard = self.default_complexity_guard();
4009 enforce_input(sql, &complexity_guard)?;
4010 let tokens = self.tokenizer.tokenize(sql)?;
4011 let config = crate::parser::ParserConfig {
4012 dialect: Some(self.dialect_type),
4013 complexity_guard,
4014 ..Default::default()
4015 };
4016 let mut parser = Parser::with_source(tokens, config, sql.to_string());
4017 parser.parse_standalone_data_type()
4018 }
4019
4020 /// Tokenize SQL using this dialect's tokenizer configuration.
4021 pub fn tokenize(&self, sql: &str) -> Result<Vec<Token>> {
4022 self.tokenizer.tokenize(sql)
4023 }
4024
4025 /// Get the generator config for a specific expression (supports hybrid dialects).
4026 /// Returns an owned `GeneratorConfig` suitable for mutation before generation.
4027 #[cfg(feature = "generate")]
4028 fn get_config_for_expr(&self, expr: &Expression) -> GeneratorConfig {
4029 if let Some(ref config_fn) = self.generator_config_for_expr {
4030 config_fn(expr)
4031 } else {
4032 (*self.generator_config).clone()
4033 }
4034 }
4035
4036 /// Generates a SQL string from an [`Expression`] AST node.
4037 ///
4038 /// The output uses this dialect's generator configuration for identifier quoting,
4039 /// keyword casing, function name normalization, and syntax style. The result is
4040 /// a single-line (non-pretty) SQL string.
4041 #[cfg(feature = "generate")]
4042 pub fn generate(&self, expr: &Expression) -> Result<String> {
4043 // Fast path: when no per-expression config override, share the Arc cheaply.
4044 if self.generator_config_for_expr.is_none() {
4045 let mut generator = Generator::with_arc_config(self.generator_config.clone());
4046 return generator.generate(expr);
4047 }
4048 let config = self.get_config_for_expr(expr);
4049 let mut generator = Generator::with_config(config);
4050 generator.generate(expr)
4051 }
4052
4053 /// Generate SQL from an expression with pretty printing enabled
4054 #[cfg(feature = "generate")]
4055 pub fn generate_pretty(&self, expr: &Expression) -> Result<String> {
4056 let mut config = self.get_config_for_expr(expr);
4057 config.pretty = true;
4058 let mut generator = Generator::with_config(config);
4059 generator.generate(expr)
4060 }
4061
4062 /// Generate SQL from an expression with source dialect info (for transpilation)
4063 #[cfg(feature = "generate")]
4064 pub fn generate_with_source(&self, expr: &Expression, source: DialectType) -> Result<String> {
4065 let mut config = self.get_config_for_expr(expr);
4066 config.source_dialect = Some(source);
4067 let mut generator = Generator::with_config(config);
4068 generator.generate(expr)
4069 }
4070
4071 /// Generate SQL from an expression with pretty printing and source dialect info
4072 #[cfg(feature = "generate")]
4073 pub fn generate_pretty_with_source(
4074 &self,
4075 expr: &Expression,
4076 source: DialectType,
4077 ) -> Result<String> {
4078 let mut config = self.get_config_for_expr(expr);
4079 config.pretty = true;
4080 config.source_dialect = Some(source);
4081 let mut generator = Generator::with_config(config);
4082 generator.generate(expr)
4083 }
4084
4085 /// Generate SQL from an expression with source dialect and transpile options.
4086 #[cfg(all(feature = "generate", feature = "transpile"))]
4087 fn generate_with_transpile_options(
4088 &self,
4089 expr: &Expression,
4090 source: DialectType,
4091 opts: &TranspileOptions,
4092 ) -> Result<String> {
4093 let mut config = self.get_config_for_expr(expr);
4094 config.source_dialect = Some(source);
4095 config.pretty = opts.pretty;
4096 config.unsupported_level = opts.unsupported_level;
4097 config.max_unsupported = opts.max_unsupported.max(1);
4098 config.complexity_guard = opts.complexity_guard;
4099 let mut generator = Generator::with_config(config);
4100 generator.generate(expr)
4101 }
4102
4103 /// Generate SQL from an expression with forced identifier quoting (identify=True)
4104 #[cfg(feature = "generate")]
4105 pub fn generate_with_identify(&self, expr: &Expression) -> Result<String> {
4106 let mut config = self.get_config_for_expr(expr);
4107 config.always_quote_identifiers = true;
4108 let mut generator = Generator::with_config(config);
4109 generator.generate(expr)
4110 }
4111
4112 /// Generate SQL from an expression with pretty printing and forced identifier quoting
4113 #[cfg(feature = "generate")]
4114 pub fn generate_pretty_with_identify(&self, expr: &Expression) -> Result<String> {
4115 let mut config = (*self.generator_config).clone();
4116 config.pretty = true;
4117 config.always_quote_identifiers = true;
4118 let mut generator = Generator::with_config(config);
4119 generator.generate(expr)
4120 }
4121
4122 /// Generate SQL from an expression with caller-specified config overrides
4123 #[cfg(feature = "generate")]
4124 pub fn generate_with_overrides(
4125 &self,
4126 expr: &Expression,
4127 overrides: impl FnOnce(&mut GeneratorConfig),
4128 ) -> Result<String> {
4129 let mut config = self.get_config_for_expr(expr);
4130 overrides(&mut config);
4131 let mut generator = Generator::with_config(config);
4132 generator.generate(expr)
4133 }
4134
4135 /// Transforms an expression tree to conform to this dialect's syntax and semantics.
4136 ///
4137 /// The transformation proceeds in two phases:
4138 /// 1. **Preprocessing** -- whole-tree structural rewrites such as eliminating QUALIFY,
4139 /// ensuring boolean predicates, or converting DISTINCT ON to a window-function pattern.
4140 /// 2. **Recursive per-node transform** -- a bottom-up pass via [`transform_recursive`]
4141 /// that applies this dialect's [`DialectImpl::transform_expr`] to every node.
4142 ///
4143 /// This method is used both during transpilation (to rewrite an AST for a target dialect)
4144 /// and for identity transforms (normalizing SQL within the same dialect).
4145 #[cfg(feature = "transpile")]
4146 pub fn transform(&self, expr: Expression) -> Result<Expression> {
4147 self.transform_with_guard(expr, self.default_complexity_guard())
4148 }
4149
4150 #[cfg(feature = "transpile")]
4151 fn transform_with_guard(
4152 &self,
4153 expr: Expression,
4154 complexity_guard: ComplexityGuardOptions,
4155 ) -> Result<Expression> {
4156 enforce_generate_ast(&expr, &complexity_guard)?;
4157 // Apply preprocessing transforms based on dialect
4158 let preprocessed = self.preprocess(expr)?;
4159 // Then apply recursive transformation
4160 transform_recursive(preprocessed, &self.transformer)
4161 }
4162
4163 /// Apply dialect-specific preprocessing transforms
4164 #[cfg(feature = "transpile")]
4165 fn preprocess(&self, expr: Expression) -> Result<Expression> {
4166 // If a custom preprocess function is set, use it instead of the built-in logic
4167 if let Some(ref custom_preprocess) = self.custom_preprocess {
4168 return custom_preprocess(expr);
4169 }
4170
4171 #[cfg(any(
4172 feature = "dialect-mysql",
4173 feature = "dialect-postgresql",
4174 feature = "dialect-bigquery",
4175 feature = "dialect-snowflake",
4176 feature = "dialect-tsql",
4177 feature = "dialect-spark",
4178 feature = "dialect-databricks",
4179 feature = "dialect-hive",
4180 feature = "dialect-sqlite",
4181 feature = "dialect-trino",
4182 feature = "dialect-presto",
4183 feature = "dialect-duckdb",
4184 feature = "dialect-redshift",
4185 feature = "dialect-starrocks",
4186 feature = "dialect-oracle",
4187 feature = "dialect-clickhouse",
4188 ))]
4189 use crate::transforms;
4190
4191 match self.dialect_type {
4192 // MySQL doesn't support QUALIFY, DISTINCT ON, FULL OUTER JOIN
4193 // MySQL doesn't natively support GENERATE_DATE_ARRAY (expand to recursive CTE)
4194 #[cfg(feature = "dialect-mysql")]
4195 DialectType::MySQL => {
4196 let expr = transforms::eliminate_qualify(expr)?;
4197 let expr = transforms::eliminate_full_outer_join(expr)?;
4198 let expr = transforms::eliminate_semi_and_anti_joins(expr)?;
4199 let expr = transforms::unnest_generate_date_array_using_recursive_cte(expr)?;
4200 Ok(expr)
4201 }
4202 // PostgreSQL doesn't support QUALIFY
4203 // PostgreSQL: UNNEST(GENERATE_SERIES) -> subquery wrapping
4204 // PostgreSQL: Normalize SET ... TO to SET ... = in CREATE FUNCTION
4205 #[cfg(feature = "dialect-postgresql")]
4206 DialectType::PostgreSQL => {
4207 let expr = transforms::eliminate_qualify(expr)?;
4208 let expr = transforms::eliminate_semi_and_anti_joins(expr)?;
4209 let expr = transforms::unwrap_unnest_generate_series_for_postgres(expr)?;
4210 // Normalize SET ... TO to SET ... = in CREATE FUNCTION
4211 // Only normalize when sqlglot would fully parse (no body) —
4212 // sqlglot falls back to Command for complex function bodies,
4213 // preserving the original text including TO.
4214 let expr = if let Expression::CreateFunction(mut cf) = expr {
4215 if cf.body.is_none() {
4216 for opt in &mut cf.set_options {
4217 if let crate::expressions::FunctionSetValue::Value { use_to, .. } =
4218 &mut opt.value
4219 {
4220 *use_to = false;
4221 }
4222 }
4223 }
4224 Expression::CreateFunction(cf)
4225 } else {
4226 expr
4227 };
4228 Ok(expr)
4229 }
4230 // BigQuery doesn't support DISTINCT ON or CTE column aliases
4231 #[cfg(feature = "dialect-bigquery")]
4232 DialectType::BigQuery => {
4233 let expr = transforms::eliminate_semi_and_anti_joins(expr)?;
4234 let expr = transforms::pushdown_cte_column_names(expr)?;
4235 let expr = transforms::explode_projection_to_unnest(expr, DialectType::BigQuery)?;
4236 Ok(expr)
4237 }
4238 // Snowflake
4239 #[cfg(feature = "dialect-snowflake")]
4240 DialectType::Snowflake => {
4241 let expr = transforms::eliminate_semi_and_anti_joins(expr)?;
4242 let expr = transforms::eliminate_window_clause(expr)?;
4243 let expr = transforms::snowflake_flatten_projection_to_unnest(expr)?;
4244 Ok(expr)
4245 }
4246 // TSQL doesn't support QUALIFY
4247 // TSQL requires boolean expressions in WHERE/HAVING (no implicit truthiness)
4248 // TSQL doesn't support CTEs in subqueries (hoist to top level)
4249 // NOTE: no_limit_order_by_union is handled in cross_dialect_normalize (not preprocess)
4250 // to avoid breaking TSQL identity tests where ORDER BY on UNION is valid
4251 #[cfg(feature = "dialect-tsql")]
4252 DialectType::TSQL => {
4253 let expr = transforms::eliminate_qualify(expr)?;
4254 let expr = transforms::eliminate_semi_and_anti_joins(expr)?;
4255 let expr = transforms::ensure_bools(expr)?;
4256 let expr = transforms::unnest_generate_date_array_using_recursive_cte(expr)?;
4257 let expr = transforms::move_ctes_to_top_level(expr)?;
4258 let expr = transforms::qualify_derived_table_outputs(expr)?;
4259 Ok(expr)
4260 }
4261 // Spark doesn't support QUALIFY (but Databricks does)
4262 // Spark doesn't support CTEs in subqueries (hoist to top level)
4263 #[cfg(feature = "dialect-spark")]
4264 DialectType::Spark => {
4265 let expr = transforms::eliminate_qualify(expr)?;
4266 let expr = transforms::add_auto_table_alias(expr)?;
4267 let expr = transforms::simplify_nested_paren_values(expr)?;
4268 let expr = transforms::move_ctes_to_top_level(expr)?;
4269 Ok(expr)
4270 }
4271 // Databricks supports QUALIFY natively
4272 // Databricks doesn't support CTEs in subqueries (hoist to top level)
4273 #[cfg(feature = "dialect-databricks")]
4274 DialectType::Databricks => {
4275 let expr = transforms::add_auto_table_alias(expr)?;
4276 let expr = transforms::simplify_nested_paren_values(expr)?;
4277 let expr = transforms::move_ctes_to_top_level(expr)?;
4278 Ok(expr)
4279 }
4280 // Hive doesn't support QUALIFY or CTEs in subqueries
4281 #[cfg(feature = "dialect-hive")]
4282 DialectType::Hive => {
4283 let expr = transforms::eliminate_qualify(expr)?;
4284 let expr = transforms::move_ctes_to_top_level(expr)?;
4285 Ok(expr)
4286 }
4287 // SQLite doesn't support QUALIFY
4288 #[cfg(feature = "dialect-sqlite")]
4289 DialectType::SQLite => {
4290 let expr = transforms::eliminate_qualify(expr)?;
4291 Ok(expr)
4292 }
4293 // Trino doesn't support QUALIFY
4294 #[cfg(feature = "dialect-trino")]
4295 DialectType::Trino => {
4296 let expr = transforms::eliminate_qualify(expr)?;
4297 let expr = transforms::explode_projection_to_unnest(expr, DialectType::Trino)?;
4298 Ok(expr)
4299 }
4300 // Presto doesn't support QUALIFY or WINDOW clause
4301 #[cfg(feature = "dialect-presto")]
4302 DialectType::Presto => {
4303 let expr = transforms::eliminate_qualify(expr)?;
4304 let expr = transforms::eliminate_window_clause(expr)?;
4305 let expr = transforms::explode_projection_to_unnest(expr, DialectType::Presto)?;
4306 Ok(expr)
4307 }
4308 // DuckDB supports QUALIFY - no elimination needed
4309 // Expand POSEXPLODE to GENERATE_SUBSCRIPTS + UNNEST
4310 // Expand LIKE ANY / ILIKE ANY to OR chains (DuckDB doesn't support quantifiers)
4311 #[cfg(feature = "dialect-duckdb")]
4312 DialectType::DuckDB => {
4313 let expr = transforms::expand_posexplode_duckdb(expr)?;
4314 let expr = transforms::expand_like_any(expr)?;
4315 Ok(expr)
4316 }
4317 // Redshift doesn't support QUALIFY, WINDOW clause, or GENERATE_DATE_ARRAY
4318 #[cfg(feature = "dialect-redshift")]
4319 DialectType::Redshift => {
4320 let expr = transforms::eliminate_qualify(expr)?;
4321 let expr = transforms::eliminate_window_clause(expr)?;
4322 let expr = transforms::unnest_generate_date_array_using_recursive_cte(expr)?;
4323 Ok(expr)
4324 }
4325 // StarRocks doesn't support BETWEEN in DELETE statements or QUALIFY
4326 #[cfg(feature = "dialect-starrocks")]
4327 DialectType::StarRocks => {
4328 let expr = transforms::eliminate_qualify(expr)?;
4329 let expr = transforms::expand_between_in_delete(expr)?;
4330 let expr = transforms::eliminate_distinct_on_for_dialect(
4331 expr,
4332 Some(DialectType::StarRocks),
4333 Some(DialectType::StarRocks),
4334 )?;
4335 let expr = transforms::unnest_generate_date_array_using_recursive_cte(expr)?;
4336 Ok(expr)
4337 }
4338 // DataFusion supports QUALIFY and semi/anti joins natively
4339 #[cfg(feature = "dialect-datafusion")]
4340 DialectType::DataFusion => Ok(expr),
4341 // Oracle doesn't support QUALIFY
4342 #[cfg(feature = "dialect-oracle")]
4343 DialectType::Oracle => {
4344 let expr = transforms::eliminate_qualify(expr)?;
4345 Ok(expr)
4346 }
4347 // Drill - no special preprocessing needed
4348 #[cfg(feature = "dialect-drill")]
4349 DialectType::Drill => Ok(expr),
4350 // Teradata - no special preprocessing needed
4351 #[cfg(feature = "dialect-teradata")]
4352 DialectType::Teradata => Ok(expr),
4353 // ClickHouse doesn't support ORDER BY/LIMIT directly on UNION
4354 #[cfg(feature = "dialect-clickhouse")]
4355 DialectType::ClickHouse => {
4356 let expr = transforms::no_limit_order_by_union(expr)?;
4357 Ok(expr)
4358 }
4359 // Other dialects - no preprocessing
4360 _ => Ok(expr),
4361 }
4362 }
4363
4364 /// Transpile SQL from this dialect to the given target dialect.
4365 ///
4366 /// The target may be specified as either a built-in [`DialectType`] enum variant
4367 /// or as a reference to a [`Dialect`] handle (built-in or custom). Both work:
4368 ///
4369 /// ```rust,ignore
4370 /// let pg = Dialect::get(DialectType::PostgreSQL);
4371 /// pg.transpile("SELECT NOW()", DialectType::BigQuery)?; // enum
4372 /// pg.transpile("SELECT NOW()", &custom_dialect)?; // handle
4373 /// ```
4374 ///
4375 /// For pretty-printing or other options, use [`transpile_with`](Self::transpile_with).
4376 #[cfg(feature = "transpile")]
4377 pub fn transpile<T: TranspileTarget>(&self, sql: &str, target: T) -> Result<Vec<String>> {
4378 self.transpile_with(sql, target, TranspileOptions::default())
4379 }
4380
4381 /// Transpile SQL with configurable [`TranspileOptions`] (e.g. pretty-printing).
4382 #[cfg(feature = "transpile")]
4383 pub fn transpile_with<T: TranspileTarget>(
4384 &self,
4385 sql: &str,
4386 target: T,
4387 opts: TranspileOptions,
4388 ) -> Result<Vec<String>> {
4389 target.with_dialect(|td| self.transpile_inner(sql, td, &opts))
4390 }
4391
4392 #[cfg(feature = "transpile")]
4393 fn transpile_inner(
4394 &self,
4395 sql: &str,
4396 target_dialect: &Dialect,
4397 opts: &TranspileOptions,
4398 ) -> Result<Vec<String>> {
4399 let mut effective_opts = opts.clone();
4400 effective_opts.complexity_guard =
4401 self.default_transpile_complexity_guard(target_dialect, opts.complexity_guard);
4402 let opts = &effective_opts;
4403 let target = target_dialect.dialect_type;
4404 if matches!(self.dialect_type, DialectType::PostgreSQL)
4405 && matches!(target, DialectType::SQLite)
4406 {
4407 self.reject_pgvector_distance_operators_for_sqlite(sql)?;
4408 }
4409 let expressions = self.parse_with_guard(sql, opts.complexity_guard)?;
4410 let generic_identity =
4411 self.dialect_type == DialectType::Generic && target == DialectType::Generic;
4412
4413 if generic_identity {
4414 return expressions
4415 .into_iter()
4416 .map(|expr| {
4417 Self::reject_strict_unsupported(&expr, self.dialect_type, target, opts)?;
4418 target_dialect.generate_with_transpile_options(&expr, self.dialect_type, opts)
4419 })
4420 .collect();
4421 }
4422
4423 expressions
4424 .into_iter()
4425 .map(|expr| {
4426 // DuckDB source: normalize VARCHAR/CHAR to TEXT (DuckDB doesn't support
4427 // VARCHAR length constraints). This emulates Python sqlglot's DuckDB parser
4428 // where VARCHAR_LENGTH = None and VARCHAR maps to TEXT.
4429 let expr = if matches!(self.dialect_type, DialectType::DuckDB) {
4430 use crate::expressions::DataType as DT;
4431 transform_recursive(expr, &|e| match e {
4432 Expression::DataType(DT::VarChar { .. }) => {
4433 Ok(Expression::DataType(DT::Text))
4434 }
4435 Expression::DataType(DT::Char { .. }) => Ok(Expression::DataType(DT::Text)),
4436 _ => Ok(e),
4437 })?
4438 } else {
4439 expr
4440 };
4441
4442 // When source and target differ, first normalize the source dialect's
4443 // AST constructs to standard SQL, so that the target dialect can handle them.
4444 // This handles cases like Snowflake's SQUARE -> POWER, DIV0 -> CASE, etc.
4445 let normalized =
4446 if self.dialect_type != target && self.dialect_type != DialectType::Generic {
4447 self.transform_with_guard(expr, opts.complexity_guard)?
4448 } else {
4449 expr
4450 };
4451
4452 // For TSQL source targeting non-TSQL: unwrap ISNULL(JSON_QUERY(...), JSON_VALUE(...))
4453 // to just JSON_QUERY(...) so cross_dialect_normalize can convert it cleanly.
4454 // The TSQL read transform wraps JsonQuery in ISNULL for identity, but for
4455 // cross-dialect transpilation we need the unwrapped JSON_QUERY.
4456 let normalized =
4457 if matches!(self.dialect_type, DialectType::TSQL | DialectType::Fabric)
4458 && !matches!(target, DialectType::TSQL | DialectType::Fabric)
4459 {
4460 transform_recursive(normalized, &|e| {
4461 if let Expression::Function(ref f) = e {
4462 if f.name.eq_ignore_ascii_case("ISNULL") && f.args.len() == 2 {
4463 // Check if first arg is JSON_QUERY and second is JSON_VALUE
4464 if let (
4465 Expression::Function(ref jq),
4466 Expression::Function(ref jv),
4467 ) = (&f.args[0], &f.args[1])
4468 {
4469 if jq.name.eq_ignore_ascii_case("JSON_QUERY")
4470 && jv.name.eq_ignore_ascii_case("JSON_VALUE")
4471 {
4472 // Unwrap: return just JSON_QUERY(...)
4473 return Ok(f.args[0].clone());
4474 }
4475 }
4476 }
4477 }
4478 Ok(e)
4479 })?
4480 } else {
4481 normalized
4482 };
4483
4484 // Snowflake source to non-Snowflake target: CURRENT_TIME -> LOCALTIME
4485 // Snowflake's CURRENT_TIME is equivalent to LOCALTIME in other dialects.
4486 // Python sqlglot parses Snowflake's CURRENT_TIME as Localtime expression.
4487 let normalized = if matches!(self.dialect_type, DialectType::Snowflake)
4488 && !matches!(target, DialectType::Snowflake)
4489 {
4490 transform_recursive(normalized, &|e| {
4491 if let Expression::Function(ref f) = e {
4492 if f.name.eq_ignore_ascii_case("CURRENT_TIME") {
4493 return Ok(Expression::Localtime(Box::new(
4494 crate::expressions::Localtime { this: None },
4495 )));
4496 }
4497 }
4498 Ok(e)
4499 })?
4500 } else {
4501 normalized
4502 };
4503
4504 // Snowflake source to DuckDB target: REPEAT(' ', n) -> REPEAT(' ', CAST(n AS BIGINT))
4505 // Snowflake's SPACE(n) is converted to REPEAT(' ', n) by the Snowflake source
4506 // transform. DuckDB requires the count argument to be BIGINT.
4507 let normalized = if matches!(self.dialect_type, DialectType::Snowflake)
4508 && matches!(target, DialectType::DuckDB)
4509 {
4510 transform_recursive(normalized, &|e| {
4511 if let Expression::Function(ref f) = e {
4512 if f.name.eq_ignore_ascii_case("REPEAT") && f.args.len() == 2 {
4513 // Check if first arg is space string literal
4514 if let Expression::Literal(ref lit) = f.args[0] {
4515 if let crate::expressions::Literal::String(ref s) = lit.as_ref()
4516 {
4517 if s == " " {
4518 // Wrap second arg in CAST(... AS BIGINT) if not already
4519 if !matches!(f.args[1], Expression::Cast(_)) {
4520 let mut new_args = f.args.clone();
4521 new_args[1] = Expression::Cast(Box::new(
4522 crate::expressions::Cast {
4523 this: new_args[1].clone(),
4524 to: crate::expressions::DataType::BigInt {
4525 length: None,
4526 },
4527 trailing_comments: Vec::new(),
4528 double_colon_syntax: false,
4529 format: None,
4530 default: None,
4531 inferred_type: None,
4532 },
4533 ));
4534 return Ok(Expression::Function(Box::new(
4535 crate::expressions::Function {
4536 name: f.name.clone(),
4537 args: new_args,
4538 distinct: f.distinct,
4539 trailing_comments: f
4540 .trailing_comments
4541 .clone(),
4542 use_bracket_syntax: f.use_bracket_syntax,
4543 no_parens: f.no_parens,
4544 quoted: f.quoted,
4545 span: None,
4546 inferred_type: None,
4547 },
4548 )));
4549 }
4550 }
4551 }
4552 }
4553 }
4554 }
4555 Ok(e)
4556 })?
4557 } else {
4558 normalized
4559 };
4560
4561 // Propagate struct field names in arrays (for BigQuery source to non-BigQuery target)
4562 // BigQuery->BigQuery should NOT propagate names (BigQuery handles implicit inheritance)
4563 let normalized = if matches!(self.dialect_type, DialectType::BigQuery)
4564 && !matches!(target, DialectType::BigQuery)
4565 {
4566 crate::transforms::propagate_struct_field_names(normalized)?
4567 } else {
4568 normalized
4569 };
4570
4571 // Snowflake source to DuckDB target: RANDOM()/RANDOM(seed) -> scaled RANDOM()
4572 // Snowflake RANDOM() returns integer in [-2^63, 2^63-1], DuckDB RANDOM() returns float [0, 1)
4573 // Skip RANDOM inside UNIFORM/NORMAL/ZIPF/RANDSTR generator args since those
4574 // functions handle their generator args differently (as float seeds).
4575 let normalized = if matches!(self.dialect_type, DialectType::Snowflake)
4576 && matches!(target, DialectType::DuckDB)
4577 {
4578 fn make_scaled_random() -> Expression {
4579 let lower =
4580 Expression::Literal(Box::new(crate::expressions::Literal::Number(
4581 "-9.223372036854776E+18".to_string(),
4582 )));
4583 let upper =
4584 Expression::Literal(Box::new(crate::expressions::Literal::Number(
4585 "9.223372036854776e+18".to_string(),
4586 )));
4587 let random_call = Expression::Random(crate::expressions::Random);
4588 let range_size = Expression::Paren(Box::new(crate::expressions::Paren {
4589 this: Expression::Sub(Box::new(crate::expressions::BinaryOp {
4590 left: upper,
4591 right: lower.clone(),
4592 left_comments: vec![],
4593 operator_comments: vec![],
4594 trailing_comments: vec![],
4595 inferred_type: None,
4596 })),
4597 trailing_comments: vec![],
4598 }));
4599 let scaled = Expression::Mul(Box::new(crate::expressions::BinaryOp {
4600 left: random_call,
4601 right: range_size,
4602 left_comments: vec![],
4603 operator_comments: vec![],
4604 trailing_comments: vec![],
4605 inferred_type: None,
4606 }));
4607 let shifted = Expression::Add(Box::new(crate::expressions::BinaryOp {
4608 left: lower,
4609 right: scaled,
4610 left_comments: vec![],
4611 operator_comments: vec![],
4612 trailing_comments: vec![],
4613 inferred_type: None,
4614 }));
4615 Expression::Cast(Box::new(crate::expressions::Cast {
4616 this: shifted,
4617 to: crate::expressions::DataType::BigInt { length: None },
4618 trailing_comments: vec![],
4619 double_colon_syntax: false,
4620 format: None,
4621 default: None,
4622 inferred_type: None,
4623 }))
4624 }
4625
4626 // Pre-process: protect seeded RANDOM(seed) inside UNIFORM/NORMAL/ZIPF/RANDSTR
4627 // by converting Rand{seed: Some(s)} to Function{name:"RANDOM", args:[s]}.
4628 // This prevents transform_recursive (which is bottom-up) from expanding
4629 // seeded RANDOM into make_scaled_random() and losing the seed value.
4630 // Unseeded RANDOM()/Rand{seed:None} is left as-is so it gets expanded
4631 // and then un-expanded back to Expression::Random by the code below.
4632 let normalized = transform_recursive(normalized, &|e| {
4633 if let Expression::Function(ref f) = e {
4634 let n = f.name.to_ascii_uppercase();
4635 if n == "UNIFORM" || n == "NORMAL" || n == "ZIPF" || n == "RANDSTR" {
4636 if let Expression::Function(mut f) = e {
4637 for arg in f.args.iter_mut() {
4638 if let Expression::Rand(ref r) = arg {
4639 if r.lower.is_none() && r.upper.is_none() {
4640 if let Some(ref seed) = r.seed {
4641 // Convert Rand{seed: Some(s)} to Function("RANDOM", [s])
4642 // so it won't be expanded by the RANDOM expansion below
4643 *arg = Expression::Function(Box::new(
4644 crate::expressions::Function::new(
4645 "RANDOM".to_string(),
4646 vec![*seed.clone()],
4647 ),
4648 ));
4649 }
4650 }
4651 }
4652 }
4653 return Ok(Expression::Function(f));
4654 }
4655 }
4656 }
4657 Ok(e)
4658 })?;
4659
4660 // transform_recursive processes bottom-up, so RANDOM() (unseeded) inside
4661 // generator functions (UNIFORM, NORMAL, ZIPF) gets expanded before
4662 // we see the parent. We detect this and undo the expansion by replacing
4663 // the expanded pattern back with Expression::Random.
4664 // Seeded RANDOM(seed) was already protected above as Function("RANDOM", [seed]).
4665 // Note: RANDSTR is NOT included here — it needs the expanded form for unseeded
4666 // RANDOM() since the DuckDB handler uses the expanded SQL as-is in the hash.
4667 transform_recursive(normalized, &|e| {
4668 if let Expression::Function(ref f) = e {
4669 let n = f.name.to_ascii_uppercase();
4670 if n == "UNIFORM" || n == "NORMAL" || n == "ZIPF" {
4671 if let Expression::Function(mut f) = e {
4672 for arg in f.args.iter_mut() {
4673 // Detect expanded RANDOM pattern: CAST(-9.22... + RANDOM() * (...) AS BIGINT)
4674 if let Expression::Cast(ref cast) = arg {
4675 if matches!(
4676 cast.to,
4677 crate::expressions::DataType::BigInt { .. }
4678 ) {
4679 if let Expression::Add(ref add) = cast.this {
4680 if let Expression::Literal(ref lit) = add.left {
4681 if let crate::expressions::Literal::Number(
4682 ref num,
4683 ) = lit.as_ref()
4684 {
4685 if num == "-9.223372036854776E+18" {
4686 *arg = Expression::Random(
4687 crate::expressions::Random,
4688 );
4689 }
4690 }
4691 }
4692 }
4693 }
4694 }
4695 }
4696 return Ok(Expression::Function(f));
4697 }
4698 return Ok(e);
4699 }
4700 }
4701 match e {
4702 Expression::Random(_) => Ok(make_scaled_random()),
4703 // Rand(seed) with no bounds: drop seed and expand
4704 // (DuckDB RANDOM doesn't support seeds)
4705 Expression::Rand(ref r) if r.lower.is_none() && r.upper.is_none() => {
4706 Ok(make_scaled_random())
4707 }
4708 _ => Ok(e),
4709 }
4710 })?
4711 } else {
4712 normalized
4713 };
4714
4715 // Apply cross-dialect semantic normalizations
4716 let normalized =
4717 Self::cross_dialect_normalize(normalized, self.dialect_type, target)?;
4718
4719 let normalized =
4720 if matches!(
4721 self.dialect_type,
4722 DialectType::PostgreSQL | DialectType::CockroachDB
4723 ) && !matches!(target, DialectType::PostgreSQL | DialectType::CockroachDB)
4724 {
4725 Self::normalize_postgres_type_function_casts(normalized)?
4726 } else {
4727 normalized
4728 };
4729
4730 let normalized = if matches!(self.dialect_type, DialectType::SQLite)
4731 && !matches!(target, DialectType::SQLite)
4732 {
4733 Self::normalize_sqlite_double_quoted_defaults(normalized)?
4734 } else {
4735 normalized
4736 };
4737
4738 let normalized = if matches!(self.dialect_type, DialectType::PostgreSQL)
4739 && matches!(target, DialectType::SQLite)
4740 {
4741 Self::normalize_postgres_to_sqlite_types(normalized)?
4742 } else {
4743 normalized
4744 };
4745
4746 let normalized = if matches!(self.dialect_type, DialectType::PostgreSQL)
4747 && matches!(target, DialectType::Fabric)
4748 {
4749 Self::normalize_postgres_to_fabric_decimal_types(normalized)?
4750 } else {
4751 normalized
4752 };
4753
4754 // For DuckDB target from BigQuery source: wrap UNNEST of struct arrays in
4755 // (SELECT UNNEST(..., max_depth => 2)) subquery
4756 // Must run BEFORE unnest_alias_to_column_alias since it changes alias structure
4757 let normalized = if matches!(self.dialect_type, DialectType::BigQuery)
4758 && matches!(target, DialectType::DuckDB)
4759 {
4760 crate::transforms::wrap_duckdb_unnest_struct(normalized)?
4761 } else {
4762 normalized
4763 };
4764
4765 // Convert BigQuery UNNEST aliases to column-alias format for DuckDB/Presto/Spark
4766 // UNNEST(arr) AS x -> UNNEST(arr) AS _t0(x)
4767 let normalized = if matches!(self.dialect_type, DialectType::BigQuery)
4768 && matches!(
4769 target,
4770 DialectType::DuckDB
4771 | DialectType::Presto
4772 | DialectType::Trino
4773 | DialectType::Athena
4774 | DialectType::Spark
4775 | DialectType::Databricks
4776 ) {
4777 crate::transforms::unnest_alias_to_column_alias(normalized)?
4778 } else if matches!(self.dialect_type, DialectType::BigQuery)
4779 && matches!(target, DialectType::BigQuery | DialectType::Redshift)
4780 {
4781 // For BigQuery/Redshift targets: move UNNEST FROM items to CROSS JOINs
4782 // but don't convert alias format (no _t0 wrapper)
4783 let result = crate::transforms::unnest_from_to_cross_join(normalized)?;
4784 // For Redshift: strip UNNEST when arg is a column reference path
4785 if matches!(target, DialectType::Redshift) {
4786 crate::transforms::strip_unnest_column_refs(result)?
4787 } else {
4788 result
4789 }
4790 } else {
4791 normalized
4792 };
4793
4794 // For Presto/Trino targets from PostgreSQL/Redshift source:
4795 // Wrap UNNEST aliases from GENERATE_SERIES conversion: AS s -> AS _u(s)
4796 let normalized = if matches!(
4797 self.dialect_type,
4798 DialectType::PostgreSQL | DialectType::Redshift
4799 ) && matches!(
4800 target,
4801 DialectType::Presto | DialectType::Trino | DialectType::Athena
4802 ) {
4803 crate::transforms::wrap_unnest_join_aliases(normalized)?
4804 } else {
4805 normalized
4806 };
4807
4808 // Eliminate DISTINCT ON with target-dialect awareness
4809 // This must happen after source transform (which may produce DISTINCT ON)
4810 // and before target transform, with knowledge of the target dialect's NULL ordering behavior
4811 let normalized = crate::transforms::eliminate_distinct_on_for_dialect(
4812 normalized,
4813 Some(target),
4814 Some(self.dialect_type),
4815 )?;
4816
4817 // GENERATE_DATE_ARRAY in UNNEST -> Snowflake ARRAY_GENERATE_RANGE + DATEADD
4818 let normalized = if matches!(target, DialectType::Snowflake) {
4819 Self::transform_generate_date_array_snowflake(normalized)?
4820 } else {
4821 normalized
4822 };
4823
4824 // CROSS JOIN UNNEST -> LATERAL VIEW EXPLODE/INLINE for Spark/Hive/Databricks
4825 let normalized = if matches!(
4826 target,
4827 DialectType::Spark | DialectType::Databricks | DialectType::Hive
4828 ) {
4829 crate::transforms::unnest_to_explode_select(normalized)?
4830 } else {
4831 normalized
4832 };
4833
4834 // Wrap UNION with ORDER BY/LIMIT in a subquery for dialects that require it
4835 let normalized = if matches!(target, DialectType::ClickHouse | DialectType::TSQL) {
4836 crate::transforms::no_limit_order_by_union(normalized)?
4837 } else {
4838 normalized
4839 };
4840
4841 // TSQL: Convert COUNT(*) -> COUNT_BIG(*) when source is not TSQL/Fabric
4842 // Python sqlglot does this in the TSQL generator, but we can't do it there
4843 // because it would break TSQL -> TSQL identity
4844 let normalized = if matches!(target, DialectType::TSQL | DialectType::Fabric)
4845 && !matches!(self.dialect_type, DialectType::TSQL | DialectType::Fabric)
4846 {
4847 transform_recursive(normalized, &|e| {
4848 if let Expression::Count(ref c) = e {
4849 // Build COUNT_BIG(...) as an AggregateFunction
4850 let args = if c.star {
4851 vec![Expression::Star(crate::expressions::Star {
4852 table: None,
4853 except: None,
4854 replace: None,
4855 rename: None,
4856 trailing_comments: Vec::new(),
4857 span: None,
4858 })]
4859 } else if let Some(ref this) = c.this {
4860 vec![this.clone()]
4861 } else {
4862 vec![]
4863 };
4864 Ok(Expression::AggregateFunction(Box::new(
4865 crate::expressions::AggregateFunction {
4866 name: "COUNT_BIG".to_string(),
4867 args,
4868 distinct: c.distinct,
4869 filter: c.filter.clone(),
4870 order_by: Vec::new(),
4871 limit: None,
4872 ignore_nulls: None,
4873 inferred_type: None,
4874 },
4875 )))
4876 } else {
4877 Ok(e)
4878 }
4879 })?
4880 } else {
4881 normalized
4882 };
4883
4884 // T-SQL/Fabric do not have a scalar boolean type. Keep predicate
4885 // contexts intact, but materialize boolean-valued expressions used
4886 // as values before target transforms add ORDER BY null sort keys.
4887 let normalized = if matches!(target, DialectType::TSQL | DialectType::Fabric)
4888 && !matches!(self.dialect_type, DialectType::TSQL | DialectType::Fabric)
4889 {
4890 Self::rewrite_boolean_values_for_tsql(normalized)?
4891 } else {
4892 normalized
4893 };
4894
4895 let transformed =
4896 target_dialect.transform_with_guard(normalized, opts.complexity_guard)?;
4897
4898 // T-SQL and Fabric do not support aggregate FILTER clauses. Rewrite any
4899 // remaining filters after target transforms so special aggregate rewrites
4900 // (for example BOOL_OR/BOOL_AND) can consume their filters first.
4901 let transformed = if matches!(target, DialectType::TSQL | DialectType::Fabric) {
4902 Self::rewrite_aggregate_filters_for_tsql(transformed)?
4903 } else {
4904 transformed
4905 };
4906
4907 // DuckDB target: when FROM is RANGE(n), replace SEQ's ROW_NUMBER pattern with `range`
4908 let transformed = if matches!(target, DialectType::DuckDB) {
4909 Self::seq_rownum_to_range(transformed)?
4910 } else {
4911 transformed
4912 };
4913
4914 Self::reject_strict_unsupported(&transformed, self.dialect_type, target, opts)?;
4915
4916 let mut sql = target_dialect.generate_with_transpile_options(
4917 &transformed,
4918 self.dialect_type,
4919 opts,
4920 )?;
4921
4922 // Align a known Snowflake pretty-print edge case with Python sqlglot output.
4923 if opts.pretty && target == DialectType::Snowflake {
4924 sql = Self::normalize_snowflake_pretty(sql);
4925 }
4926
4927 Ok(sql)
4928 })
4929 .collect()
4930 }
4931}
4932
4933// Transpile-only methods: cross-dialect normalization and helpers
4934#[cfg(feature = "transpile")]
4935impl Dialect {
4936 fn reject_strict_unsupported(
4937 expr: &Expression,
4938 source: DialectType,
4939 target: DialectType,
4940 opts: &TranspileOptions,
4941 ) -> Result<()> {
4942 if !matches!(
4943 opts.unsupported_level,
4944 UnsupportedLevel::Raise | UnsupportedLevel::Immediate
4945 ) {
4946 return Ok(());
4947 }
4948
4949 let mut diagnostics = Vec::new();
4950
4951 for node in expr.dfs() {
4952 if matches!(target, DialectType::Fabric | DialectType::Hive)
4953 && Self::node_has_recursive_with(node)
4954 {
4955 Self::push_unsupported_diagnostic(&mut diagnostics, "recursive CTEs");
4956 }
4957
4958 if matches!(target, DialectType::TSQL | DialectType::Fabric)
4959 && Self::node_has_lateral(node)
4960 {
4961 Self::push_unsupported_diagnostic(&mut diagnostics, "LATERAL joins and subqueries");
4962 }
4963
4964 if !Self::target_supports_remaining_unnest(target) && Self::node_is_unnest(node) {
4965 Self::push_unsupported_diagnostic(&mut diagnostics, "UNNEST");
4966 }
4967
4968 if !Self::target_supports_remaining_explode(target) && Self::node_is_explode(node) {
4969 Self::push_unsupported_diagnostic(&mut diagnostics, "EXPLODE");
4970 }
4971
4972 if Self::target_lacks_array_agg(target) && Self::node_is_array_agg(node) {
4973 Self::push_unsupported_diagnostic(&mut diagnostics, "ARRAY_AGG");
4974 }
4975
4976 if matches!(target, DialectType::TSQL | DialectType::Fabric)
4977 && Self::node_is_regex_predicate(node)
4978 {
4979 Self::push_unsupported_diagnostic(
4980 &mut diagnostics,
4981 "regular expression predicates",
4982 );
4983 }
4984
4985 if matches!(target, DialectType::TSQL | DialectType::Fabric)
4986 && Self::node_is_non_subquery_any(node)
4987 {
4988 Self::push_unsupported_diagnostic(
4989 &mut diagnostics,
4990 "ANY over non-subquery expressions",
4991 );
4992 }
4993
4994 if matches!(source, DialectType::PostgreSQL | DialectType::CockroachDB)
4995 && !matches!(target, DialectType::PostgreSQL | DialectType::CockroachDB)
4996 {
4997 if Self::node_is_function_named(node, "JSONB_BUILD_OBJECT") {
4998 Self::push_unsupported_diagnostic(
4999 &mut diagnostics,
5000 "PostgreSQL JSONB_BUILD_OBJECT",
5001 );
5002 }
5003 if Self::node_is_function_named(node, "TO_TSVECTOR") {
5004 Self::push_unsupported_diagnostic(&mut diagnostics, "PostgreSQL TO_TSVECTOR");
5005 }
5006 if matches!(target, DialectType::TSQL | DialectType::Fabric)
5007 && Self::node_is_postgres_type_function_cast(node)
5008 {
5009 Self::push_unsupported_diagnostic(
5010 &mut diagnostics,
5011 "PostgreSQL type-name function casts",
5012 );
5013 }
5014 }
5015
5016 if opts.unsupported_level == UnsupportedLevel::Immediate && !diagnostics.is_empty() {
5017 break;
5018 }
5019 }
5020
5021 if diagnostics.is_empty() {
5022 return Ok(());
5023 }
5024
5025 let limit = if opts.unsupported_level == UnsupportedLevel::Immediate {
5026 1
5027 } else {
5028 opts.max_unsupported.max(1)
5029 };
5030 let mut messages = diagnostics.iter().take(limit).cloned().collect::<Vec<_>>();
5031 if diagnostics.len() > limit {
5032 messages.push(format!("... and {} more", diagnostics.len() - limit));
5033 }
5034
5035 Err(crate::error::Error::unsupported(
5036 messages.join("; "),
5037 target.to_string(),
5038 ))
5039 }
5040
5041 fn push_unsupported_diagnostic(diagnostics: &mut Vec<String>, message: &str) {
5042 if !diagnostics.iter().any(|existing| existing == message) {
5043 diagnostics.push(message.to_string());
5044 }
5045 }
5046
5047 fn node_has_recursive_with(expr: &Expression) -> bool {
5048 fn recursive(with: &Option<With>) -> bool {
5049 with.as_ref().is_some_and(|with| with.recursive)
5050 }
5051
5052 match expr {
5053 Expression::With(with) => with.recursive,
5054 Expression::Select(select) => recursive(&select.with),
5055 Expression::Union(union) => recursive(&union.with),
5056 Expression::Intersect(intersect) => recursive(&intersect.with),
5057 Expression::Except(except) => recursive(&except.with),
5058 Expression::Pivot(pivot) => recursive(&pivot.with),
5059 Expression::Insert(insert) => recursive(&insert.with),
5060 Expression::Update(update) => recursive(&update.with),
5061 Expression::Delete(delete) => recursive(&delete.with),
5062 _ => false,
5063 }
5064 }
5065
5066 fn node_has_lateral(expr: &Expression) -> bool {
5067 fn join_has_lateral(join: &Join) -> bool {
5068 matches!(
5069 join.kind,
5070 crate::expressions::JoinKind::Lateral | crate::expressions::JoinKind::LeftLateral
5071 ) || Dialect::node_has_lateral(&join.this)
5072 || join.on.as_ref().is_some_and(Dialect::node_has_lateral)
5073 || join
5074 .match_condition
5075 .as_ref()
5076 .is_some_and(Dialect::node_has_lateral)
5077 || join.pivots.iter().any(Dialect::node_has_lateral)
5078 }
5079
5080 fn joins_have_lateral(joins: &[Join]) -> bool {
5081 joins.iter().any(join_has_lateral)
5082 }
5083
5084 match expr {
5085 Expression::Subquery(subquery) => {
5086 subquery.lateral || Dialect::node_has_lateral(&subquery.this)
5087 }
5088 Expression::Lateral(_) | Expression::LateralView(_) => true,
5089 Expression::Join(join) => join_has_lateral(join),
5090 Expression::Select(select) => {
5091 !select.lateral_views.is_empty()
5092 || joins_have_lateral(&select.joins)
5093 || select
5094 .from
5095 .as_ref()
5096 .is_some_and(|from| from.expressions.iter().any(Dialect::node_has_lateral))
5097 }
5098 Expression::JoinedTable(joined) => {
5099 !joined.lateral_views.is_empty()
5100 || Dialect::node_has_lateral(&joined.left)
5101 || joins_have_lateral(&joined.joins)
5102 }
5103 Expression::Update(update) => {
5104 joins_have_lateral(&update.table_joins) || joins_have_lateral(&update.from_joins)
5105 }
5106 _ => false,
5107 }
5108 }
5109
5110 fn target_supports_remaining_unnest(target: DialectType) -> bool {
5111 matches!(
5112 target,
5113 DialectType::PostgreSQL
5114 | DialectType::BigQuery
5115 | DialectType::DuckDB
5116 | DialectType::Presto
5117 | DialectType::Trino
5118 | DialectType::Athena
5119 )
5120 }
5121
5122 fn target_supports_remaining_explode(target: DialectType) -> bool {
5123 matches!(
5124 target,
5125 DialectType::Spark | DialectType::Databricks | DialectType::Hive
5126 )
5127 }
5128
5129 fn target_lacks_array_agg(target: DialectType) -> bool {
5130 matches!(
5131 target,
5132 DialectType::Fabric
5133 | DialectType::TSQL
5134 | DialectType::MySQL
5135 | DialectType::SQLite
5136 | DialectType::Oracle
5137 )
5138 }
5139
5140 fn node_is_unnest(expr: &Expression) -> bool {
5141 matches!(expr, Expression::Unnest(_)) || Self::node_is_function_named(expr, "UNNEST")
5142 }
5143
5144 fn node_is_explode(expr: &Expression) -> bool {
5145 matches!(expr, Expression::Explode(_) | Expression::ExplodeOuter(_))
5146 || Self::node_is_function_named(expr, "EXPLODE")
5147 || Self::node_is_function_named(expr, "EXPLODE_OUTER")
5148 }
5149
5150 fn node_is_array_agg(expr: &Expression) -> bool {
5151 matches!(expr, Expression::ArrayAgg(_)) || Self::node_is_function_named(expr, "ARRAY_AGG")
5152 }
5153
5154 fn node_is_regex_predicate(expr: &Expression) -> bool {
5155 matches!(
5156 expr,
5157 Expression::SimilarTo(_) | Expression::RegexpLike(_) | Expression::RegexpILike(_)
5158 ) || Self::node_is_function_named(expr, "REGEXP_LIKE")
5159 || Self::node_is_function_named(expr, "REGEXP_I_LIKE")
5160 || Self::node_is_function_named(expr, "REGEXP_ILIKE")
5161 }
5162
5163 fn node_is_non_subquery_any(expr: &Expression) -> bool {
5164 matches!(
5165 expr,
5166 Expression::Any(q) if !Self::quantified_rhs_is_subquery(&q.subquery)
5167 )
5168 }
5169
5170 fn quantified_rhs_is_subquery(expr: &Expression) -> bool {
5171 match expr {
5172 Expression::Select(_) | Expression::Subquery(_) => true,
5173 Expression::Paren(paren) => Self::quantified_rhs_is_subquery(&paren.this),
5174 _ => false,
5175 }
5176 }
5177
5178 fn node_is_function_named(expr: &Expression, name: &str) -> bool {
5179 match expr {
5180 Expression::Function(function) => function.name.eq_ignore_ascii_case(name),
5181 Expression::AggregateFunction(function) => function.name.eq_ignore_ascii_case(name),
5182 _ => false,
5183 }
5184 }
5185
5186 fn normalize_postgres_type_function_casts(expr: Expression) -> Result<Expression> {
5187 transform_recursive(expr, &|e| match e {
5188 Expression::Function(function) => {
5189 let mut function = *function;
5190 if function.args.len() == 1
5191 && !function.distinct
5192 && !function.quoted
5193 && !function.use_bracket_syntax
5194 && !function.name.contains('.')
5195 {
5196 if let Some(to) = Self::postgres_type_function_data_type(&function.name) {
5197 let this = function.args.remove(0);
5198 return Ok(Expression::Cast(Box::new(Cast {
5199 this,
5200 to,
5201 trailing_comments: function.trailing_comments,
5202 double_colon_syntax: false,
5203 format: None,
5204 default: None,
5205 inferred_type: function.inferred_type,
5206 })));
5207 }
5208 }
5209 Ok(Expression::Function(Box::new(function)))
5210 }
5211 _ => Ok(e),
5212 })
5213 }
5214
5215 fn node_is_postgres_type_function_cast(expr: &Expression) -> bool {
5216 matches!(
5217 expr,
5218 Expression::Function(function)
5219 if !function.quoted
5220 && !function.use_bracket_syntax
5221 && !function.name.contains('.')
5222 && Self::postgres_type_function_data_type(&function.name).is_some()
5223 )
5224 }
5225
5226 fn postgres_type_function_data_type(name: &str) -> Option<DataType> {
5227 match name.to_ascii_uppercase().as_str() {
5228 "NUMERIC" | "DECIMAL" | "DEC" => Some(DataType::Decimal {
5229 precision: None,
5230 scale: None,
5231 }),
5232 "INT2" | "SMALLINT" => Some(DataType::SmallInt { length: None }),
5233 "INT4" | "INT" => Some(DataType::Int {
5234 length: None,
5235 integer_spelling: false,
5236 }),
5237 "INTEGER" => Some(DataType::Int {
5238 length: None,
5239 integer_spelling: true,
5240 }),
5241 "INT8" | "BIGINT" => Some(DataType::BigInt { length: None }),
5242 "FLOAT4" | "REAL" => Some(DataType::Float {
5243 precision: None,
5244 scale: None,
5245 real_spelling: true,
5246 }),
5247 "FLOAT8" => Some(DataType::Double {
5248 precision: None,
5249 scale: None,
5250 }),
5251 "BOOL" | "BOOLEAN" => Some(DataType::Boolean),
5252 "TEXT" => Some(DataType::Text),
5253 "VARCHAR" => Some(DataType::VarChar {
5254 length: None,
5255 parenthesized_length: false,
5256 }),
5257 "UUID" => Some(DataType::Uuid),
5258 _ => None,
5259 }
5260 }
5261
5262 fn rewrite_boolean_values_for_tsql(expr: Expression) -> Result<Expression> {
5263 match expr {
5264 Expression::Select(select) => Self::rewrite_boolean_values_in_tsql_select(select),
5265 Expression::Subquery(mut subquery) => {
5266 subquery.this = Self::rewrite_boolean_values_for_tsql(subquery.this)?;
5267 Ok(Expression::Subquery(subquery))
5268 }
5269 Expression::Union(mut union) => {
5270 let left = std::mem::replace(&mut union.left, Expression::null());
5271 let right = std::mem::replace(&mut union.right, Expression::null());
5272 union.left = Self::rewrite_boolean_values_for_tsql(left)?;
5273 union.right = Self::rewrite_boolean_values_for_tsql(right)?;
5274 if let Some(mut with) = union.with.take() {
5275 with.ctes = with
5276 .ctes
5277 .into_iter()
5278 .map(|mut cte| {
5279 cte.this = Self::rewrite_boolean_values_for_tsql(cte.this)?;
5280 Ok(cte)
5281 })
5282 .collect::<Result<Vec<_>>>()?;
5283 union.with = Some(with);
5284 }
5285 Ok(Expression::Union(union))
5286 }
5287 Expression::Intersect(mut intersect) => {
5288 let left = std::mem::replace(&mut intersect.left, Expression::null());
5289 let right = std::mem::replace(&mut intersect.right, Expression::null());
5290 intersect.left = Self::rewrite_boolean_values_for_tsql(left)?;
5291 intersect.right = Self::rewrite_boolean_values_for_tsql(right)?;
5292 Ok(Expression::Intersect(intersect))
5293 }
5294 Expression::Except(mut except) => {
5295 let left = std::mem::replace(&mut except.left, Expression::null());
5296 let right = std::mem::replace(&mut except.right, Expression::null());
5297 except.left = Self::rewrite_boolean_values_for_tsql(left)?;
5298 except.right = Self::rewrite_boolean_values_for_tsql(right)?;
5299 Ok(Expression::Except(except))
5300 }
5301 other => Self::rewrite_tsql_boolean_embedded_queries(other),
5302 }
5303 }
5304
5305 fn rewrite_boolean_values_in_tsql_select(
5306 mut select: Box<crate::expressions::Select>,
5307 ) -> Result<Expression> {
5308 if let Some(mut with) = select.with.take() {
5309 with.ctes = with
5310 .ctes
5311 .into_iter()
5312 .map(|mut cte| {
5313 cte.this = Self::rewrite_boolean_values_for_tsql(cte.this)?;
5314 Ok(cte)
5315 })
5316 .collect::<Result<Vec<_>>>()?;
5317 select.with = Some(with);
5318 }
5319
5320 select.expressions = select
5321 .expressions
5322 .into_iter()
5323 .map(Self::rewrite_tsql_boolean_scalar_value)
5324 .collect::<Result<Vec<_>>>()?;
5325
5326 if let Some(mut from) = select.from.take() {
5327 from.expressions = from
5328 .expressions
5329 .into_iter()
5330 .map(Self::rewrite_tsql_boolean_embedded_queries)
5331 .collect::<Result<Vec<_>>>()?;
5332 select.from = Some(from);
5333 }
5334
5335 select.joins = select
5336 .joins
5337 .into_iter()
5338 .map(|mut join| {
5339 join.this = Self::rewrite_tsql_boolean_embedded_queries(join.this)?;
5340 if let Some(on) = join.on.take() {
5341 join.on = Some(Self::rewrite_tsql_boolean_predicate_context(on)?);
5342 }
5343 if let Some(match_condition) = join.match_condition.take() {
5344 join.match_condition = Some(Self::rewrite_tsql_boolean_predicate_context(
5345 match_condition,
5346 )?);
5347 }
5348 join.pivots = join
5349 .pivots
5350 .into_iter()
5351 .map(Self::rewrite_tsql_boolean_embedded_queries)
5352 .collect::<Result<Vec<_>>>()?;
5353 Ok(join)
5354 })
5355 .collect::<Result<Vec<_>>>()?;
5356
5357 select.lateral_views = select
5358 .lateral_views
5359 .into_iter()
5360 .map(|mut lateral_view| {
5361 lateral_view.this = Self::rewrite_tsql_boolean_embedded_queries(lateral_view.this)?;
5362 Ok(lateral_view)
5363 })
5364 .collect::<Result<Vec<_>>>()?;
5365
5366 if let Some(prewhere) = select.prewhere.take() {
5367 select.prewhere = Some(Self::rewrite_tsql_boolean_predicate_context(prewhere)?);
5368 }
5369
5370 if let Some(mut where_clause) = select.where_clause.take() {
5371 where_clause.this = Self::rewrite_tsql_boolean_predicate_context(where_clause.this)?;
5372 select.where_clause = Some(where_clause);
5373 }
5374
5375 if let Some(mut group_by) = select.group_by.take() {
5376 group_by.expressions = group_by
5377 .expressions
5378 .into_iter()
5379 .map(Self::rewrite_tsql_boolean_scalar_value)
5380 .collect::<Result<Vec<_>>>()?;
5381 select.group_by = Some(group_by);
5382 }
5383
5384 if let Some(mut having) = select.having.take() {
5385 having.this = Self::rewrite_tsql_boolean_predicate_context(having.this)?;
5386 select.having = Some(having);
5387 }
5388
5389 if let Some(mut qualify) = select.qualify.take() {
5390 qualify.this = Self::rewrite_tsql_boolean_predicate_context(qualify.this)?;
5391 select.qualify = Some(qualify);
5392 }
5393
5394 if let Some(mut order_by) = select.order_by.take() {
5395 order_by.expressions = Self::rewrite_tsql_boolean_ordered_values(order_by.expressions)?;
5396 select.order_by = Some(order_by);
5397 }
5398
5399 if let Some(mut distribute_by) = select.distribute_by.take() {
5400 distribute_by.expressions = distribute_by
5401 .expressions
5402 .into_iter()
5403 .map(Self::rewrite_tsql_boolean_scalar_value)
5404 .collect::<Result<Vec<_>>>()?;
5405 select.distribute_by = Some(distribute_by);
5406 }
5407
5408 if let Some(mut cluster_by) = select.cluster_by.take() {
5409 cluster_by.expressions =
5410 Self::rewrite_tsql_boolean_ordered_values(cluster_by.expressions)?;
5411 select.cluster_by = Some(cluster_by);
5412 }
5413
5414 if let Some(mut sort_by) = select.sort_by.take() {
5415 sort_by.expressions = Self::rewrite_tsql_boolean_ordered_values(sort_by.expressions)?;
5416 select.sort_by = Some(sort_by);
5417 }
5418
5419 if let Some(limit_by) = select.limit_by.take() {
5420 select.limit_by = Some(
5421 limit_by
5422 .into_iter()
5423 .map(Self::rewrite_tsql_boolean_scalar_value)
5424 .collect::<Result<Vec<_>>>()?,
5425 );
5426 }
5427
5428 if let Some(distinct_on) = select.distinct_on.take() {
5429 select.distinct_on = Some(
5430 distinct_on
5431 .into_iter()
5432 .map(Self::rewrite_tsql_boolean_scalar_value)
5433 .collect::<Result<Vec<_>>>()?,
5434 );
5435 }
5436
5437 if let Some(mut sample) = select.sample.take() {
5438 sample.size = Self::rewrite_tsql_boolean_embedded_queries(sample.size)?;
5439 if let Some(offset) = sample.offset.take() {
5440 sample.offset = Some(Self::rewrite_tsql_boolean_embedded_queries(offset)?);
5441 }
5442 if let Some(bucket_numerator) = sample.bucket_numerator.take() {
5443 sample.bucket_numerator = Some(Box::new(
5444 Self::rewrite_tsql_boolean_embedded_queries(*bucket_numerator)?,
5445 ));
5446 }
5447 if let Some(bucket_denominator) = sample.bucket_denominator.take() {
5448 sample.bucket_denominator = Some(Box::new(
5449 Self::rewrite_tsql_boolean_embedded_queries(*bucket_denominator)?,
5450 ));
5451 }
5452 if let Some(bucket_field) = sample.bucket_field.take() {
5453 sample.bucket_field = Some(Box::new(Self::rewrite_tsql_boolean_embedded_queries(
5454 *bucket_field,
5455 )?));
5456 }
5457 select.sample = Some(sample);
5458 }
5459
5460 if let Some(settings) = select.settings.take() {
5461 select.settings = Some(
5462 settings
5463 .into_iter()
5464 .map(Self::rewrite_tsql_boolean_embedded_queries)
5465 .collect::<Result<Vec<_>>>()?,
5466 );
5467 }
5468
5469 if let Some(format) = select.format.take() {
5470 select.format = Some(Self::rewrite_tsql_boolean_embedded_queries(format)?);
5471 }
5472
5473 if let Some(mut windows) = select.windows.take() {
5474 for window in windows.iter_mut() {
5475 Self::rewrite_tsql_boolean_over_values(&mut window.spec)?;
5476 }
5477 select.windows = Some(windows);
5478 }
5479
5480 Ok(Expression::Select(select))
5481 }
5482
5483 fn rewrite_tsql_boolean_scalar_value(expr: Expression) -> Result<Expression> {
5484 if Self::is_tsql_boolean_value_expression(&expr) {
5485 return Ok(Self::tsql_boolean_value_case(expr));
5486 }
5487
5488 match expr {
5489 Expression::Alias(mut alias) => {
5490 alias.this = Self::rewrite_tsql_boolean_scalar_value(alias.this)?;
5491 Ok(Expression::Alias(alias))
5492 }
5493 Expression::Paren(mut paren) => {
5494 paren.this = Self::rewrite_tsql_boolean_scalar_value(paren.this)?;
5495 Ok(Expression::Paren(paren))
5496 }
5497 Expression::Cast(mut cast) => {
5498 cast.this = Self::rewrite_tsql_boolean_scalar_value(cast.this)?;
5499 if let Some(format) = cast.format.take() {
5500 cast.format = Some(Box::new(Self::rewrite_tsql_boolean_embedded_queries(
5501 *format,
5502 )?));
5503 }
5504 if let Some(default) = cast.default.take() {
5505 cast.default =
5506 Some(Box::new(Self::rewrite_tsql_boolean_scalar_value(*default)?));
5507 }
5508 Ok(Expression::Cast(cast))
5509 }
5510 Expression::TryCast(mut cast) => {
5511 cast.this = Self::rewrite_tsql_boolean_scalar_value(cast.this)?;
5512 if let Some(format) = cast.format.take() {
5513 cast.format = Some(Box::new(Self::rewrite_tsql_boolean_embedded_queries(
5514 *format,
5515 )?));
5516 }
5517 if let Some(default) = cast.default.take() {
5518 cast.default =
5519 Some(Box::new(Self::rewrite_tsql_boolean_scalar_value(*default)?));
5520 }
5521 Ok(Expression::TryCast(cast))
5522 }
5523 Expression::SafeCast(mut cast) => {
5524 cast.this = Self::rewrite_tsql_boolean_scalar_value(cast.this)?;
5525 if let Some(format) = cast.format.take() {
5526 cast.format = Some(Box::new(Self::rewrite_tsql_boolean_embedded_queries(
5527 *format,
5528 )?));
5529 }
5530 if let Some(default) = cast.default.take() {
5531 cast.default =
5532 Some(Box::new(Self::rewrite_tsql_boolean_scalar_value(*default)?));
5533 }
5534 Ok(Expression::SafeCast(cast))
5535 }
5536 Expression::Case(mut case) => {
5537 if let Some(operand) = case.operand.take() {
5538 case.operand = Some(Self::rewrite_tsql_boolean_scalar_value(operand)?);
5539 }
5540 case.whens = case
5541 .whens
5542 .into_iter()
5543 .map(|(condition, result)| {
5544 Ok((
5545 Self::rewrite_tsql_boolean_predicate_context(condition)?,
5546 Self::rewrite_tsql_boolean_scalar_value(result)?,
5547 ))
5548 })
5549 .collect::<Result<Vec<_>>>()?;
5550 if let Some(else_) = case.else_.take() {
5551 case.else_ = Some(Self::rewrite_tsql_boolean_scalar_value(else_)?);
5552 }
5553 Ok(Expression::Case(case))
5554 }
5555 Expression::IfFunc(mut if_func) => {
5556 if_func.condition =
5557 Self::rewrite_tsql_boolean_predicate_context(if_func.condition)?;
5558 if_func.true_value = Self::rewrite_tsql_boolean_scalar_value(if_func.true_value)?;
5559 if let Some(false_value) = if_func.false_value.take() {
5560 if_func.false_value =
5561 Some(Self::rewrite_tsql_boolean_scalar_value(false_value)?);
5562 }
5563 Ok(Expression::IfFunc(if_func))
5564 }
5565 Expression::WindowFunction(mut window_function) => {
5566 window_function.this =
5567 Self::rewrite_tsql_boolean_embedded_queries(window_function.this)?;
5568 Self::rewrite_tsql_boolean_over_values(&mut window_function.over)?;
5569 if let Some(mut keep) = window_function.keep.take() {
5570 keep.order_by = Self::rewrite_tsql_boolean_ordered_values(keep.order_by)?;
5571 window_function.keep = Some(keep);
5572 }
5573 Ok(Expression::WindowFunction(window_function))
5574 }
5575 Expression::WithinGroup(mut within_group) => {
5576 within_group.this = Self::rewrite_tsql_boolean_embedded_queries(within_group.this)?;
5577 within_group.order_by =
5578 Self::rewrite_tsql_boolean_ordered_values(within_group.order_by)?;
5579 Ok(Expression::WithinGroup(within_group))
5580 }
5581 Expression::Subquery(mut subquery) => {
5582 subquery.this = Self::rewrite_boolean_values_for_tsql(subquery.this)?;
5583 Ok(Expression::Subquery(subquery))
5584 }
5585 Expression::Select(select) => Self::rewrite_boolean_values_in_tsql_select(select),
5586 other => Self::rewrite_tsql_boolean_embedded_queries(other),
5587 }
5588 }
5589
5590 fn rewrite_tsql_boolean_predicate_context(expr: Expression) -> Result<Expression> {
5591 Self::rewrite_tsql_boolean_embedded_queries(expr)
5592 }
5593
5594 fn rewrite_tsql_boolean_embedded_queries(expr: Expression) -> Result<Expression> {
5595 transform_recursive(expr, &|e| match e {
5596 Expression::Select(select) => Self::rewrite_boolean_values_in_tsql_select(select),
5597 Expression::Subquery(mut subquery) => {
5598 subquery.this = Self::rewrite_boolean_values_for_tsql(subquery.this)?;
5599 Ok(Expression::Subquery(subquery))
5600 }
5601 Expression::Union(_) | Expression::Intersect(_) | Expression::Except(_) => {
5602 Self::rewrite_boolean_values_for_tsql(e)
5603 }
5604 other => Ok(other),
5605 })
5606 }
5607
5608 fn rewrite_tsql_boolean_ordered_values(
5609 ordered: Vec<crate::expressions::Ordered>,
5610 ) -> Result<Vec<crate::expressions::Ordered>> {
5611 ordered
5612 .into_iter()
5613 .map(|mut ordered| {
5614 ordered.this = Self::rewrite_tsql_boolean_scalar_value(ordered.this)?;
5615 if let Some(with_fill) = ordered.with_fill.take() {
5616 ordered.with_fill = Some(Box::new(
5617 Self::rewrite_tsql_boolean_with_fill_values(*with_fill)?,
5618 ));
5619 }
5620 Ok(ordered)
5621 })
5622 .collect()
5623 }
5624
5625 fn rewrite_tsql_boolean_with_fill_values(
5626 mut with_fill: crate::expressions::WithFill,
5627 ) -> Result<crate::expressions::WithFill> {
5628 if let Some(from) = with_fill.from_.take() {
5629 with_fill.from_ = Some(Box::new(Self::rewrite_tsql_boolean_scalar_value(*from)?));
5630 }
5631 if let Some(to) = with_fill.to.take() {
5632 with_fill.to = Some(Box::new(Self::rewrite_tsql_boolean_scalar_value(*to)?));
5633 }
5634 if let Some(step) = with_fill.step.take() {
5635 with_fill.step = Some(Box::new(Self::rewrite_tsql_boolean_scalar_value(*step)?));
5636 }
5637 if let Some(staleness) = with_fill.staleness.take() {
5638 with_fill.staleness = Some(Box::new(Self::rewrite_tsql_boolean_scalar_value(
5639 *staleness,
5640 )?));
5641 }
5642 if let Some(interpolate) = with_fill.interpolate.take() {
5643 with_fill.interpolate = Some(Box::new(Self::rewrite_tsql_boolean_scalar_value(
5644 *interpolate,
5645 )?));
5646 }
5647 Ok(with_fill)
5648 }
5649
5650 fn rewrite_tsql_boolean_over_values(over: &mut crate::expressions::Over) -> Result<()> {
5651 over.partition_by = std::mem::take(&mut over.partition_by)
5652 .into_iter()
5653 .map(Self::rewrite_tsql_boolean_scalar_value)
5654 .collect::<Result<Vec<_>>>()?;
5655 over.order_by =
5656 Self::rewrite_tsql_boolean_ordered_values(std::mem::take(&mut over.order_by))?;
5657 Ok(())
5658 }
5659
5660 fn is_tsql_boolean_value_expression(expr: &Expression) -> bool {
5661 match expr {
5662 Expression::Paren(paren) => Self::is_tsql_boolean_value_expression(&paren.this),
5663 Expression::Eq(_)
5664 | Expression::Neq(_)
5665 | Expression::Lt(_)
5666 | Expression::Lte(_)
5667 | Expression::Gt(_)
5668 | Expression::Gte(_)
5669 | Expression::Is(_)
5670 | Expression::IsNull(_)
5671 | Expression::IsTrue(_)
5672 | Expression::IsFalse(_)
5673 | Expression::Like(_)
5674 | Expression::ILike(_)
5675 | Expression::SimilarTo(_)
5676 | Expression::Glob(_)
5677 | Expression::RegexpLike(_)
5678 | Expression::In(_)
5679 | Expression::Between(_)
5680 | Expression::Exists(_)
5681 | Expression::And(_)
5682 | Expression::Or(_)
5683 | Expression::Not(_)
5684 | Expression::Any(_)
5685 | Expression::All(_)
5686 | Expression::EqualNull(_) => true,
5687 _ => false,
5688 }
5689 }
5690
5691 fn tsql_boolean_value_case(predicate: Expression) -> Expression {
5692 Expression::Case(Box::new(crate::expressions::Case {
5693 operand: None,
5694 whens: vec![
5695 (predicate.clone(), Expression::number(1)),
5696 (
5697 Expression::Not(Box::new(crate::expressions::UnaryOp {
5698 this: predicate,
5699 inferred_type: None,
5700 })),
5701 Expression::number(0),
5702 ),
5703 ],
5704 else_: None,
5705 comments: Vec::new(),
5706 inferred_type: None,
5707 }))
5708 }
5709
5710 fn rewrite_aggregate_filters_for_tsql(expr: Expression) -> Result<Expression> {
5711 transform_recursive(expr, &|e| Self::rewrite_aggregate_filter_for_tsql(e))
5712 }
5713
5714 fn rewrite_aggregate_filter_for_tsql(expr: Expression) -> Result<Expression> {
5715 macro_rules! rewrite_agg_filter {
5716 ($variant:ident, $agg:expr) => {{
5717 let mut agg = $agg;
5718 if let Some(filter) = agg.filter.take() {
5719 let this = std::mem::replace(&mut agg.this, Expression::null());
5720 agg.this = Self::conditional_aggregate_value_for_tsql(filter, this);
5721 }
5722 Ok(Expression::$variant(agg))
5723 }};
5724 }
5725
5726 match expr {
5727 Expression::Filter(filter) => {
5728 let condition = match *filter.expression {
5729 Expression::Where(where_) => where_.this,
5730 other => other,
5731 };
5732 Ok(Self::push_filter_into_tsql_aggregate(
5733 *filter.this,
5734 condition,
5735 ))
5736 }
5737 Expression::AggregateFunction(mut agg) => {
5738 if let Some(filter) = agg.filter.take() {
5739 Self::rewrite_generic_aggregate_filter_for_tsql(&mut agg, filter);
5740 }
5741 Ok(Expression::AggregateFunction(agg))
5742 }
5743 Expression::Count(mut count) => {
5744 if let Some(filter) = count.filter.take() {
5745 let value = if count.star {
5746 Expression::number(1)
5747 } else {
5748 count.this.take().unwrap_or_else(|| Expression::number(1))
5749 };
5750 count.star = false;
5751 count.this = Some(Self::conditional_aggregate_value_for_tsql(filter, value));
5752 }
5753 Ok(Expression::Count(count))
5754 }
5755 Expression::Sum(agg) => rewrite_agg_filter!(Sum, agg),
5756 Expression::Avg(agg) => rewrite_agg_filter!(Avg, agg),
5757 Expression::Min(agg) => rewrite_agg_filter!(Min, agg),
5758 Expression::Max(agg) => rewrite_agg_filter!(Max, agg),
5759 Expression::ArrayAgg(agg) => rewrite_agg_filter!(ArrayAgg, agg),
5760 Expression::CountIf(agg) => Ok(Expression::CountIf(agg)),
5761 Expression::Stddev(agg) => rewrite_agg_filter!(Stddev, agg),
5762 Expression::StddevPop(agg) => rewrite_agg_filter!(StddevPop, agg),
5763 Expression::StddevSamp(agg) => rewrite_agg_filter!(StddevSamp, agg),
5764 Expression::Variance(agg) => rewrite_agg_filter!(Variance, agg),
5765 Expression::VarPop(agg) => rewrite_agg_filter!(VarPop, agg),
5766 Expression::VarSamp(agg) => rewrite_agg_filter!(VarSamp, agg),
5767 Expression::Median(agg) => rewrite_agg_filter!(Median, agg),
5768 Expression::Mode(agg) => rewrite_agg_filter!(Mode, agg),
5769 Expression::First(agg) => rewrite_agg_filter!(First, agg),
5770 Expression::Last(agg) => rewrite_agg_filter!(Last, agg),
5771 Expression::AnyValue(agg) => rewrite_agg_filter!(AnyValue, agg),
5772 Expression::ApproxDistinct(agg) => rewrite_agg_filter!(ApproxDistinct, agg),
5773 Expression::ApproxCountDistinct(agg) => {
5774 rewrite_agg_filter!(ApproxCountDistinct, agg)
5775 }
5776 Expression::LogicalAnd(agg) => rewrite_agg_filter!(LogicalAnd, agg),
5777 Expression::LogicalOr(agg) => rewrite_agg_filter!(LogicalOr, agg),
5778 Expression::Skewness(agg) => rewrite_agg_filter!(Skewness, agg),
5779 Expression::ArrayConcatAgg(agg) => rewrite_agg_filter!(ArrayConcatAgg, agg),
5780 Expression::ArrayUniqueAgg(agg) => rewrite_agg_filter!(ArrayUniqueAgg, agg),
5781 Expression::BoolXorAgg(agg) => rewrite_agg_filter!(BoolXorAgg, agg),
5782 Expression::BitwiseAndAgg(agg) => rewrite_agg_filter!(BitwiseAndAgg, agg),
5783 Expression::BitwiseOrAgg(agg) => rewrite_agg_filter!(BitwiseOrAgg, agg),
5784 Expression::BitwiseXorAgg(agg) => rewrite_agg_filter!(BitwiseXorAgg, agg),
5785 Expression::StringAgg(mut agg) => {
5786 if let Some(filter) = agg.filter.take() {
5787 let this = std::mem::replace(&mut agg.this, Expression::null());
5788 agg.this = Self::conditional_aggregate_value_for_tsql(filter, this);
5789 }
5790 Ok(Expression::StringAgg(agg))
5791 }
5792 Expression::GroupConcat(mut agg) => {
5793 if let Some(filter) = agg.filter.take() {
5794 let this = std::mem::replace(&mut agg.this, Expression::null());
5795 agg.this = Self::conditional_aggregate_value_for_tsql(filter, this);
5796 }
5797 Ok(Expression::GroupConcat(agg))
5798 }
5799 Expression::ListAgg(mut agg) => {
5800 if let Some(filter) = agg.filter.take() {
5801 let this = std::mem::replace(&mut agg.this, Expression::null());
5802 agg.this = Self::conditional_aggregate_value_for_tsql(filter, this);
5803 }
5804 Ok(Expression::ListAgg(agg))
5805 }
5806 Expression::WithinGroup(mut within_group) => {
5807 within_group.this = Self::rewrite_aggregate_filters_for_tsql(within_group.this)?;
5808 Ok(Expression::WithinGroup(within_group))
5809 }
5810 other => Ok(other),
5811 }
5812 }
5813
5814 fn push_filter_into_tsql_aggregate(expr: Expression, filter: Expression) -> Expression {
5815 macro_rules! push_agg_filter {
5816 ($variant:ident, $agg:expr) => {{
5817 let mut agg = $agg;
5818 let this = std::mem::replace(&mut agg.this, Expression::null());
5819 agg.this = Self::conditional_aggregate_value_for_tsql(filter, this);
5820 agg.filter = None;
5821 Expression::$variant(agg)
5822 }};
5823 }
5824
5825 match expr {
5826 Expression::AggregateFunction(mut agg) => {
5827 Self::rewrite_generic_aggregate_filter_for_tsql(&mut agg, filter);
5828 Expression::AggregateFunction(agg)
5829 }
5830 Expression::Count(mut count) => {
5831 let value = if count.star {
5832 Expression::number(1)
5833 } else {
5834 count.this.take().unwrap_or_else(|| Expression::number(1))
5835 };
5836 count.star = false;
5837 count.filter = None;
5838 count.this = Some(Self::conditional_aggregate_value_for_tsql(filter, value));
5839 Expression::Count(count)
5840 }
5841 Expression::Sum(agg) => push_agg_filter!(Sum, agg),
5842 Expression::Avg(agg) => push_agg_filter!(Avg, agg),
5843 Expression::Min(agg) => push_agg_filter!(Min, agg),
5844 Expression::Max(agg) => push_agg_filter!(Max, agg),
5845 Expression::ArrayAgg(agg) => push_agg_filter!(ArrayAgg, agg),
5846 Expression::CountIf(mut agg) => {
5847 agg.filter = Some(filter);
5848 Expression::CountIf(agg)
5849 }
5850 Expression::Stddev(agg) => push_agg_filter!(Stddev, agg),
5851 Expression::StddevPop(agg) => push_agg_filter!(StddevPop, agg),
5852 Expression::StddevSamp(agg) => push_agg_filter!(StddevSamp, agg),
5853 Expression::Variance(agg) => push_agg_filter!(Variance, agg),
5854 Expression::VarPop(agg) => push_agg_filter!(VarPop, agg),
5855 Expression::VarSamp(agg) => push_agg_filter!(VarSamp, agg),
5856 Expression::Median(agg) => push_agg_filter!(Median, agg),
5857 Expression::Mode(agg) => push_agg_filter!(Mode, agg),
5858 Expression::First(agg) => push_agg_filter!(First, agg),
5859 Expression::Last(agg) => push_agg_filter!(Last, agg),
5860 Expression::AnyValue(agg) => push_agg_filter!(AnyValue, agg),
5861 Expression::ApproxDistinct(agg) => push_agg_filter!(ApproxDistinct, agg),
5862 Expression::ApproxCountDistinct(agg) => {
5863 push_agg_filter!(ApproxCountDistinct, agg)
5864 }
5865 Expression::LogicalAnd(agg) => push_agg_filter!(LogicalAnd, agg),
5866 Expression::LogicalOr(agg) => push_agg_filter!(LogicalOr, agg),
5867 Expression::Skewness(agg) => push_agg_filter!(Skewness, agg),
5868 Expression::ArrayConcatAgg(agg) => push_agg_filter!(ArrayConcatAgg, agg),
5869 Expression::ArrayUniqueAgg(agg) => push_agg_filter!(ArrayUniqueAgg, agg),
5870 Expression::BoolXorAgg(agg) => push_agg_filter!(BoolXorAgg, agg),
5871 Expression::BitwiseAndAgg(agg) => push_agg_filter!(BitwiseAndAgg, agg),
5872 Expression::BitwiseOrAgg(agg) => push_agg_filter!(BitwiseOrAgg, agg),
5873 Expression::BitwiseXorAgg(agg) => push_agg_filter!(BitwiseXorAgg, agg),
5874 Expression::StringAgg(mut agg) => {
5875 let this = std::mem::replace(&mut agg.this, Expression::null());
5876 agg.this = Self::conditional_aggregate_value_for_tsql(filter, this);
5877 agg.filter = None;
5878 Expression::StringAgg(agg)
5879 }
5880 Expression::GroupConcat(mut agg) => {
5881 let this = std::mem::replace(&mut agg.this, Expression::null());
5882 agg.this = Self::conditional_aggregate_value_for_tsql(filter, this);
5883 agg.filter = None;
5884 Expression::GroupConcat(agg)
5885 }
5886 Expression::ListAgg(mut agg) => {
5887 let this = std::mem::replace(&mut agg.this, Expression::null());
5888 agg.this = Self::conditional_aggregate_value_for_tsql(filter, this);
5889 agg.filter = None;
5890 Expression::ListAgg(agg)
5891 }
5892 Expression::WithinGroup(mut within_group) => {
5893 within_group.this =
5894 Self::push_filter_into_tsql_aggregate(within_group.this, filter);
5895 Expression::WithinGroup(within_group)
5896 }
5897 other => Expression::Filter(Box::new(crate::expressions::Filter {
5898 this: Box::new(other),
5899 expression: Box::new(filter),
5900 })),
5901 }
5902 }
5903
5904 fn rewrite_generic_aggregate_filter_for_tsql(
5905 agg: &mut crate::expressions::AggregateFunction,
5906 filter: Expression,
5907 ) {
5908 let is_count =
5909 agg.name.eq_ignore_ascii_case("COUNT") || agg.name.eq_ignore_ascii_case("COUNT_BIG");
5910 let is_count_star = is_count
5911 && (agg.args.is_empty()
5912 || (agg.args.len() == 1 && matches!(agg.args[0], Expression::Star(_))));
5913
5914 if is_count_star {
5915 agg.args = vec![Self::conditional_aggregate_value_for_tsql(
5916 filter,
5917 Expression::number(1),
5918 )];
5919 } else if !agg.args.is_empty() {
5920 agg.args = agg
5921 .args
5922 .drain(..)
5923 .map(|arg| Self::conditional_aggregate_value_for_tsql(filter.clone(), arg))
5924 .collect();
5925 } else {
5926 agg.filter = Some(filter);
5927 }
5928 }
5929
5930 fn conditional_aggregate_value_for_tsql(filter: Expression, value: Expression) -> Expression {
5931 Expression::Case(Box::new(crate::expressions::Case {
5932 operand: None,
5933 whens: vec![(filter, value)],
5934 else_: None,
5935 comments: Vec::new(),
5936 inferred_type: None,
5937 }))
5938 }
5939
5940 fn reject_pgvector_distance_operators_for_sqlite(&self, sql: &str) -> Result<()> {
5941 let tokens = self.tokenize(sql)?;
5942 for (i, token) in tokens.iter().enumerate() {
5943 if token.token_type == TokenType::NullsafeEq {
5944 return Err(crate::error::Error::unsupported(
5945 "PostgreSQL pgvector cosine distance operator <=>",
5946 "SQLite",
5947 ));
5948 }
5949 if token.token_type == TokenType::Lt
5950 && tokens
5951 .get(i + 1)
5952 .is_some_and(|token| token.token_type == TokenType::Tilde)
5953 && tokens
5954 .get(i + 2)
5955 .is_some_and(|token| token.token_type == TokenType::Gt)
5956 {
5957 return Err(crate::error::Error::unsupported(
5958 "PostgreSQL pgvector Hamming distance operator <~>",
5959 "SQLite",
5960 ));
5961 }
5962 }
5963 Ok(())
5964 }
5965
5966 fn normalize_sqlite_double_quoted_defaults(expr: Expression) -> Result<Expression> {
5967 fn normalize_default_expr(expr: Expression) -> Result<Expression> {
5968 transform_recursive(expr, &|e| match e {
5969 Expression::Column(col)
5970 if col.table.is_none() && col.name.quoted && !col.join_mark =>
5971 {
5972 Ok(Expression::Literal(Box::new(Literal::String(
5973 col.name.name,
5974 ))))
5975 }
5976 Expression::Identifier(id) if id.quoted => {
5977 Ok(Expression::Literal(Box::new(Literal::String(id.name))))
5978 }
5979 _ => Ok(e),
5980 })
5981 }
5982
5983 fn normalize_column_default(col: &mut crate::expressions::ColumnDef) -> Result<()> {
5984 if let Some(default) = col.default.take() {
5985 col.default = Some(normalize_default_expr(default)?);
5986 }
5987
5988 for constraint in &mut col.constraints {
5989 if let ColumnConstraint::Default(default) = constraint {
5990 *default = normalize_default_expr(default.clone())?;
5991 }
5992 }
5993
5994 Ok(())
5995 }
5996
5997 transform_recursive(expr, &|e| match e {
5998 Expression::CreateTable(mut ct) => {
5999 for column in &mut ct.columns {
6000 normalize_column_default(column)?;
6001 }
6002 Ok(Expression::CreateTable(ct))
6003 }
6004 Expression::ColumnDef(mut col) => {
6005 normalize_column_default(&mut col)?;
6006 Ok(Expression::ColumnDef(col))
6007 }
6008 _ => Ok(e),
6009 })
6010 }
6011
6012 fn normalize_postgres_to_sqlite_types(expr: Expression) -> Result<Expression> {
6013 fn sqlite_type(dt: crate::expressions::DataType) -> crate::expressions::DataType {
6014 use crate::expressions::DataType;
6015
6016 match dt {
6017 DataType::Bit { .. } => DataType::Int {
6018 length: None,
6019 integer_spelling: true,
6020 },
6021 DataType::TextWithLength { .. } => DataType::Text,
6022 DataType::VarChar { .. } => DataType::Text,
6023 DataType::Char { .. } => DataType::Text,
6024 DataType::Timestamp { timezone: true, .. } => DataType::Text,
6025 DataType::Custom { name } => {
6026 let base = name
6027 .split_once('(')
6028 .map_or(name.as_str(), |(base, _)| base)
6029 .trim();
6030 if base.eq_ignore_ascii_case("TSVECTOR")
6031 || base.eq_ignore_ascii_case("TIMESTAMPTZ")
6032 || base.eq_ignore_ascii_case("TIMESTAMP WITH TIME ZONE")
6033 || base.eq_ignore_ascii_case("NVARCHAR")
6034 || base.eq_ignore_ascii_case("NCHAR")
6035 {
6036 DataType::Text
6037 } else {
6038 DataType::Custom { name }
6039 }
6040 }
6041 _ => dt,
6042 }
6043 }
6044
6045 transform_recursive(expr, &|e| match e {
6046 Expression::DataType(dt) => Ok(Expression::DataType(sqlite_type(dt))),
6047 Expression::CreateTable(mut ct) => {
6048 for column in &mut ct.columns {
6049 column.data_type = sqlite_type(column.data_type.clone());
6050 }
6051 Ok(Expression::CreateTable(ct))
6052 }
6053 _ => Ok(e),
6054 })
6055 }
6056
6057 fn normalize_postgres_to_fabric_decimal_types(expr: Expression) -> Result<Expression> {
6058 fn fabric_decimal_type(dt: crate::expressions::DataType) -> crate::expressions::DataType {
6059 use crate::expressions::DataType;
6060
6061 match dt {
6062 DataType::Decimal {
6063 precision: None,
6064 scale: None,
6065 } => DataType::Decimal {
6066 precision: Some(38),
6067 scale: Some(10),
6068 },
6069 _ => dt,
6070 }
6071 }
6072
6073 transform_recursive(expr, &|e| match e {
6074 Expression::DataType(dt) => Ok(Expression::DataType(fabric_decimal_type(dt))),
6075 Expression::CreateTable(mut ct) => {
6076 for column in &mut ct.columns {
6077 column.data_type = fabric_decimal_type(column.data_type.clone());
6078 }
6079 Ok(Expression::CreateTable(ct))
6080 }
6081 Expression::ColumnDef(mut col) => {
6082 col.data_type = fabric_decimal_type(col.data_type);
6083 Ok(Expression::ColumnDef(col))
6084 }
6085 _ => Ok(e),
6086 })
6087 }
6088
6089 /// For DuckDB target: when FROM clause contains RANGE(n), replace
6090 /// `(ROW_NUMBER() OVER (ORDER BY 1 NULLS FIRST) - 1)` with `range` in select expressions.
6091 /// This handles SEQ1/2/4/8 → RANGE transpilation from Snowflake.
6092 fn seq_rownum_to_range(expr: Expression) -> Result<Expression> {
6093 if let Expression::Select(mut select) = expr {
6094 // Check if FROM contains a RANGE function
6095 let has_range_from = if let Some(ref from) = select.from {
6096 from.expressions.iter().any(|e| {
6097 // Check for direct RANGE(...) or aliased RANGE(...)
6098 match e {
6099 Expression::Function(f) => f.name.eq_ignore_ascii_case("RANGE"),
6100 Expression::Alias(a) => {
6101 matches!(&a.this, Expression::Function(f) if f.name.eq_ignore_ascii_case("RANGE"))
6102 }
6103 _ => false,
6104 }
6105 })
6106 } else {
6107 false
6108 };
6109
6110 if has_range_from {
6111 // Replace the ROW_NUMBER pattern in select expressions
6112 select.expressions = select
6113 .expressions
6114 .into_iter()
6115 .map(|e| Self::replace_rownum_with_range(e))
6116 .collect();
6117 }
6118
6119 Ok(Expression::Select(select))
6120 } else {
6121 Ok(expr)
6122 }
6123 }
6124
6125 /// Replace `(ROW_NUMBER() OVER (...) - 1)` with `range` column reference
6126 fn replace_rownum_with_range(expr: Expression) -> Expression {
6127 match expr {
6128 // Match: (ROW_NUMBER() OVER (...) - 1) % N → range % N
6129 Expression::Mod(op) => {
6130 let new_left = Self::try_replace_rownum_paren(&op.left);
6131 Expression::Mod(Box::new(crate::expressions::BinaryOp {
6132 left: new_left,
6133 right: op.right,
6134 left_comments: op.left_comments,
6135 operator_comments: op.operator_comments,
6136 trailing_comments: op.trailing_comments,
6137 inferred_type: op.inferred_type,
6138 }))
6139 }
6140 // Match: (CASE WHEN (ROW...) % N >= ... THEN ... ELSE ... END)
6141 Expression::Paren(p) => {
6142 let inner = Self::replace_rownum_with_range(p.this);
6143 Expression::Paren(Box::new(crate::expressions::Paren {
6144 this: inner,
6145 trailing_comments: p.trailing_comments,
6146 }))
6147 }
6148 Expression::Case(mut c) => {
6149 // Replace ROW_NUMBER in WHEN conditions and THEN expressions
6150 c.whens = c
6151 .whens
6152 .into_iter()
6153 .map(|(cond, then)| {
6154 (
6155 Self::replace_rownum_with_range(cond),
6156 Self::replace_rownum_with_range(then),
6157 )
6158 })
6159 .collect();
6160 if let Some(else_) = c.else_ {
6161 c.else_ = Some(Self::replace_rownum_with_range(else_));
6162 }
6163 Expression::Case(c)
6164 }
6165 Expression::Gte(op) => Expression::Gte(Box::new(crate::expressions::BinaryOp {
6166 left: Self::replace_rownum_with_range(op.left),
6167 right: op.right,
6168 left_comments: op.left_comments,
6169 operator_comments: op.operator_comments,
6170 trailing_comments: op.trailing_comments,
6171 inferred_type: op.inferred_type,
6172 })),
6173 Expression::Sub(op) => Expression::Sub(Box::new(crate::expressions::BinaryOp {
6174 left: Self::replace_rownum_with_range(op.left),
6175 right: op.right,
6176 left_comments: op.left_comments,
6177 operator_comments: op.operator_comments,
6178 trailing_comments: op.trailing_comments,
6179 inferred_type: op.inferred_type,
6180 })),
6181 Expression::Alias(mut a) => {
6182 a.this = Self::replace_rownum_with_range(a.this);
6183 Expression::Alias(a)
6184 }
6185 other => other,
6186 }
6187 }
6188
6189 /// Check if an expression is `(ROW_NUMBER() OVER (...) - 1)` and replace with `range`
6190 fn try_replace_rownum_paren(expr: &Expression) -> Expression {
6191 if let Expression::Paren(ref p) = expr {
6192 if let Expression::Sub(ref sub) = p.this {
6193 if let Expression::WindowFunction(ref wf) = sub.left {
6194 if let Expression::Function(ref f) = wf.this {
6195 if f.name.eq_ignore_ascii_case("ROW_NUMBER") {
6196 if let Expression::Literal(ref lit) = sub.right {
6197 if let crate::expressions::Literal::Number(ref n) = lit.as_ref() {
6198 if n == "1" {
6199 return Expression::column("range");
6200 }
6201 }
6202 }
6203 }
6204 }
6205 }
6206 }
6207 }
6208 expr.clone()
6209 }
6210
6211 /// Transform BigQuery GENERATE_DATE_ARRAY in UNNEST for Snowflake target.
6212 /// Converts:
6213 /// SELECT ..., alias, ... FROM t CROSS JOIN UNNEST(GENERATE_DATE_ARRAY(start, end, INTERVAL '1' unit)) AS alias
6214 /// To:
6215 /// SELECT ..., DATEADD(unit, CAST(alias AS INT), CAST(start AS DATE)) AS alias, ...
6216 /// FROM t, LATERAL FLATTEN(INPUT => ARRAY_GENERATE_RANGE(0, DATEDIFF(unit, start, end) + 1)) AS _t0(seq, key, path, index, alias, this)
6217 fn transform_generate_date_array_snowflake(expr: Expression) -> Result<Expression> {
6218 use crate::expressions::*;
6219 transform_recursive(expr, &|e| {
6220 // Handle ARRAY_SIZE(GENERATE_DATE_ARRAY(...)) -> ARRAY_SIZE((SELECT ARRAY_AGG(*) FROM subquery))
6221 if let Expression::ArraySize(ref af) = e {
6222 if let Expression::Function(ref f) = af.this {
6223 if f.name.eq_ignore_ascii_case("GENERATE_DATE_ARRAY") && f.args.len() >= 2 {
6224 let result = Self::convert_array_size_gda_snowflake(f)?;
6225 return Ok(result);
6226 }
6227 }
6228 }
6229
6230 let Expression::Select(mut sel) = e else {
6231 return Ok(e);
6232 };
6233
6234 // Find joins with UNNEST containing GenerateSeries (from GENERATE_DATE_ARRAY conversion)
6235 let mut gda_info: Option<(String, Expression, Expression, String)> = None; // (alias_name, start_expr, end_expr, unit)
6236 let mut gda_join_idx: Option<usize> = None;
6237
6238 for (idx, join) in sel.joins.iter().enumerate() {
6239 // The join.this may be:
6240 // 1. Unnest(UnnestFunc { alias: Some("mnth"), ... })
6241 // 2. Alias(Alias { this: Unnest(UnnestFunc { alias: None, ... }), alias: "mnth", ... })
6242 let (unnest_ref, alias_name) = match &join.this {
6243 Expression::Unnest(ref unnest) => {
6244 let alias = unnest.alias.as_ref().map(|id| id.name.clone());
6245 (Some(unnest.as_ref()), alias)
6246 }
6247 Expression::Alias(ref a) => {
6248 if let Expression::Unnest(ref unnest) = a.this {
6249 (Some(unnest.as_ref()), Some(a.alias.name.clone()))
6250 } else {
6251 (None, None)
6252 }
6253 }
6254 _ => (None, None),
6255 };
6256
6257 if let (Some(unnest), Some(alias)) = (unnest_ref, alias_name) {
6258 // Check the main expression (this) of the UNNEST for GENERATE_DATE_ARRAY function
6259 if let Expression::Function(ref f) = unnest.this {
6260 if f.name.eq_ignore_ascii_case("GENERATE_DATE_ARRAY") && f.args.len() >= 2 {
6261 let start_expr = f.args[0].clone();
6262 let end_expr = f.args[1].clone();
6263 let step = f.args.get(2).cloned();
6264
6265 // Extract unit from step interval
6266 let unit = if let Some(Expression::Interval(ref iv)) = step {
6267 if let Some(IntervalUnitSpec::Simple { ref unit, .. }) = iv.unit {
6268 Some(format!("{:?}", unit).to_ascii_uppercase())
6269 } else if let Some(ref this) = iv.this {
6270 // The interval may be stored as a string like "1 MONTH"
6271 if let Expression::Literal(lit) = this {
6272 if let Literal::String(ref s) = lit.as_ref() {
6273 let parts: Vec<&str> = s.split_whitespace().collect();
6274 if parts.len() == 2 {
6275 Some(parts[1].to_ascii_uppercase())
6276 } else if parts.len() == 1 {
6277 // Single word like "MONTH" or just "1"
6278 let upper = parts[0].to_ascii_uppercase();
6279 if matches!(
6280 upper.as_str(),
6281 "YEAR"
6282 | "QUARTER"
6283 | "MONTH"
6284 | "WEEK"
6285 | "DAY"
6286 | "HOUR"
6287 | "MINUTE"
6288 | "SECOND"
6289 ) {
6290 Some(upper)
6291 } else {
6292 None
6293 }
6294 } else {
6295 None
6296 }
6297 } else {
6298 None
6299 }
6300 } else {
6301 None
6302 }
6303 } else {
6304 None
6305 }
6306 } else {
6307 None
6308 };
6309
6310 if let Some(unit_str) = unit {
6311 gda_info = Some((alias, start_expr, end_expr, unit_str));
6312 gda_join_idx = Some(idx);
6313 }
6314 }
6315 }
6316 }
6317 if gda_info.is_some() {
6318 break;
6319 }
6320 }
6321
6322 let Some((alias_name, start_expr, end_expr, unit_str)) = gda_info else {
6323 // Also check FROM clause for UNNEST(GENERATE_DATE_ARRAY(...)) patterns
6324 // This handles Generic->Snowflake where GENERATE_DATE_ARRAY is in FROM, not in JOIN
6325 let result = Self::try_transform_from_gda_snowflake(sel);
6326 return result;
6327 };
6328 let join_idx = gda_join_idx.unwrap();
6329
6330 // Build ARRAY_GENERATE_RANGE(0, DATEDIFF(unit, start, end) + 1)
6331 // ARRAY_GENERATE_RANGE uses exclusive end, and we need DATEDIFF + 1 values
6332 // (inclusive date range), so the exclusive end is DATEDIFF + 1.
6333 let datediff = Expression::Function(Box::new(Function::new(
6334 "DATEDIFF".to_string(),
6335 vec![
6336 Expression::boxed_column(Column {
6337 name: Identifier::new(&unit_str),
6338 table: None,
6339 join_mark: false,
6340 trailing_comments: vec![],
6341 span: None,
6342 inferred_type: None,
6343 }),
6344 start_expr.clone(),
6345 end_expr.clone(),
6346 ],
6347 )));
6348 let datediff_plus_one = Expression::Add(Box::new(BinaryOp {
6349 left: datediff,
6350 right: Expression::Literal(Box::new(Literal::Number("1".to_string()))),
6351 left_comments: vec![],
6352 operator_comments: vec![],
6353 trailing_comments: vec![],
6354 inferred_type: None,
6355 }));
6356
6357 let array_gen_range = Expression::Function(Box::new(Function::new(
6358 "ARRAY_GENERATE_RANGE".to_string(),
6359 vec![
6360 Expression::Literal(Box::new(Literal::Number("0".to_string()))),
6361 datediff_plus_one,
6362 ],
6363 )));
6364
6365 // Build FLATTEN(INPUT => ARRAY_GENERATE_RANGE(...))
6366 let flatten_input = Expression::NamedArgument(Box::new(NamedArgument {
6367 name: Identifier::new("INPUT"),
6368 value: array_gen_range,
6369 separator: crate::expressions::NamedArgSeparator::DArrow,
6370 }));
6371 let flatten = Expression::Function(Box::new(Function::new(
6372 "FLATTEN".to_string(),
6373 vec![flatten_input],
6374 )));
6375
6376 // Build LATERAL FLATTEN(...) AS _t0(seq, key, path, index, alias, this)
6377 let alias_table = Alias {
6378 this: flatten,
6379 alias: Identifier::new("_t0"),
6380 column_aliases: vec![
6381 Identifier::new("seq"),
6382 Identifier::new("key"),
6383 Identifier::new("path"),
6384 Identifier::new("index"),
6385 Identifier::new(&alias_name),
6386 Identifier::new("this"),
6387 ],
6388 alias_explicit_as: false,
6389 alias_keyword: None,
6390 pre_alias_comments: vec![],
6391 trailing_comments: vec![],
6392 inferred_type: None,
6393 };
6394 let lateral_expr = Expression::Lateral(Box::new(Lateral {
6395 this: Box::new(Expression::Alias(Box::new(alias_table))),
6396 view: None,
6397 outer: None,
6398 alias: None,
6399 alias_quoted: false,
6400 cross_apply: None,
6401 ordinality: None,
6402 column_aliases: vec![],
6403 }));
6404
6405 // Remove the original join and add to FROM expressions
6406 sel.joins.remove(join_idx);
6407 if let Some(ref mut from) = sel.from {
6408 from.expressions.push(lateral_expr);
6409 }
6410
6411 // Build DATEADD(unit, CAST(alias AS INT), CAST(start AS DATE))
6412 let dateadd_expr = Expression::Function(Box::new(Function::new(
6413 "DATEADD".to_string(),
6414 vec![
6415 Expression::boxed_column(Column {
6416 name: Identifier::new(&unit_str),
6417 table: None,
6418 join_mark: false,
6419 trailing_comments: vec![],
6420 span: None,
6421 inferred_type: None,
6422 }),
6423 Expression::Cast(Box::new(Cast {
6424 this: Expression::boxed_column(Column {
6425 name: Identifier::new(&alias_name),
6426 table: None,
6427 join_mark: false,
6428 trailing_comments: vec![],
6429 span: None,
6430 inferred_type: None,
6431 }),
6432 to: DataType::Int {
6433 length: None,
6434 integer_spelling: false,
6435 },
6436 trailing_comments: vec![],
6437 double_colon_syntax: false,
6438 format: None,
6439 default: None,
6440 inferred_type: None,
6441 })),
6442 Expression::Cast(Box::new(Cast {
6443 this: start_expr.clone(),
6444 to: DataType::Date,
6445 trailing_comments: vec![],
6446 double_colon_syntax: false,
6447 format: None,
6448 default: None,
6449 inferred_type: None,
6450 })),
6451 ],
6452 )));
6453
6454 // Replace references to the alias in the SELECT list
6455 let new_exprs: Vec<Expression> = sel
6456 .expressions
6457 .iter()
6458 .map(|expr| Self::replace_column_ref_with_dateadd(expr, &alias_name, &dateadd_expr))
6459 .collect();
6460 sel.expressions = new_exprs;
6461
6462 Ok(Expression::Select(sel))
6463 })
6464 }
6465
6466 /// Helper: replace column references to `alias_name` with dateadd expression
6467 fn replace_column_ref_with_dateadd(
6468 expr: &Expression,
6469 alias_name: &str,
6470 dateadd: &Expression,
6471 ) -> Expression {
6472 use crate::expressions::*;
6473 match expr {
6474 Expression::Column(c) if c.name.name == alias_name && c.table.is_none() => {
6475 // Plain column reference -> DATEADD(...) AS alias_name
6476 Expression::Alias(Box::new(Alias {
6477 this: dateadd.clone(),
6478 alias: Identifier::new(alias_name),
6479 column_aliases: vec![],
6480 alias_explicit_as: false,
6481 alias_keyword: None,
6482 pre_alias_comments: vec![],
6483 trailing_comments: vec![],
6484 inferred_type: None,
6485 }))
6486 }
6487 Expression::Alias(a) => {
6488 // Check if the inner expression references the alias
6489 let new_this = Self::replace_column_ref_inner(&a.this, alias_name, dateadd);
6490 Expression::Alias(Box::new(Alias {
6491 this: new_this,
6492 alias: a.alias.clone(),
6493 column_aliases: a.column_aliases.clone(),
6494 alias_explicit_as: false,
6495 alias_keyword: None,
6496 pre_alias_comments: a.pre_alias_comments.clone(),
6497 trailing_comments: a.trailing_comments.clone(),
6498 inferred_type: None,
6499 }))
6500 }
6501 _ => expr.clone(),
6502 }
6503 }
6504
6505 /// Helper: replace column references in inner expression (not top-level)
6506 fn replace_column_ref_inner(
6507 expr: &Expression,
6508 alias_name: &str,
6509 dateadd: &Expression,
6510 ) -> Expression {
6511 use crate::expressions::*;
6512 match expr {
6513 Expression::Column(c) if c.name.name == alias_name && c.table.is_none() => {
6514 dateadd.clone()
6515 }
6516 Expression::Add(op) => {
6517 let left = Self::replace_column_ref_inner(&op.left, alias_name, dateadd);
6518 let right = Self::replace_column_ref_inner(&op.right, alias_name, dateadd);
6519 Expression::Add(Box::new(BinaryOp {
6520 left,
6521 right,
6522 left_comments: op.left_comments.clone(),
6523 operator_comments: op.operator_comments.clone(),
6524 trailing_comments: op.trailing_comments.clone(),
6525 inferred_type: None,
6526 }))
6527 }
6528 Expression::Sub(op) => {
6529 let left = Self::replace_column_ref_inner(&op.left, alias_name, dateadd);
6530 let right = Self::replace_column_ref_inner(&op.right, alias_name, dateadd);
6531 Expression::Sub(Box::new(BinaryOp {
6532 left,
6533 right,
6534 left_comments: op.left_comments.clone(),
6535 operator_comments: op.operator_comments.clone(),
6536 trailing_comments: op.trailing_comments.clone(),
6537 inferred_type: None,
6538 }))
6539 }
6540 Expression::Mul(op) => {
6541 let left = Self::replace_column_ref_inner(&op.left, alias_name, dateadd);
6542 let right = Self::replace_column_ref_inner(&op.right, alias_name, dateadd);
6543 Expression::Mul(Box::new(BinaryOp {
6544 left,
6545 right,
6546 left_comments: op.left_comments.clone(),
6547 operator_comments: op.operator_comments.clone(),
6548 trailing_comments: op.trailing_comments.clone(),
6549 inferred_type: None,
6550 }))
6551 }
6552 _ => expr.clone(),
6553 }
6554 }
6555
6556 /// Handle UNNEST(GENERATE_DATE_ARRAY(...)) in FROM clause for Snowflake target.
6557 /// Converts to a subquery with DATEADD + TABLE(FLATTEN(ARRAY_GENERATE_RANGE(...))).
6558 fn try_transform_from_gda_snowflake(
6559 mut sel: Box<crate::expressions::Select>,
6560 ) -> Result<Expression> {
6561 use crate::expressions::*;
6562
6563 // Extract GDA info from FROM clause
6564 let mut gda_info: Option<(
6565 usize,
6566 String,
6567 Expression,
6568 Expression,
6569 String,
6570 Option<(String, Vec<Identifier>)>,
6571 )> = None; // (from_idx, col_name, start, end, unit, outer_alias)
6572
6573 if let Some(ref from) = sel.from {
6574 for (idx, table_expr) in from.expressions.iter().enumerate() {
6575 // Pattern 1: UNNEST(GENERATE_DATE_ARRAY(...))
6576 // Pattern 2: Alias(UNNEST(GENERATE_DATE_ARRAY(...))) AS _q(date_week)
6577 let (unnest_opt, outer_alias_info) = match table_expr {
6578 Expression::Unnest(ref unnest) => (Some(unnest.as_ref()), None),
6579 Expression::Alias(ref a) => {
6580 if let Expression::Unnest(ref unnest) = a.this {
6581 let alias_info = (a.alias.name.clone(), a.column_aliases.clone());
6582 (Some(unnest.as_ref()), Some(alias_info))
6583 } else {
6584 (None, None)
6585 }
6586 }
6587 _ => (None, None),
6588 };
6589
6590 if let Some(unnest) = unnest_opt {
6591 // Check for GENERATE_DATE_ARRAY function
6592 let func_opt = match &unnest.this {
6593 Expression::Function(ref f)
6594 if f.name.eq_ignore_ascii_case("GENERATE_DATE_ARRAY")
6595 && f.args.len() >= 2 =>
6596 {
6597 Some(f)
6598 }
6599 // Also check for GenerateSeries (from earlier normalization)
6600 _ => None,
6601 };
6602
6603 if let Some(f) = func_opt {
6604 let start_expr = f.args[0].clone();
6605 let end_expr = f.args[1].clone();
6606 let step = f.args.get(2).cloned();
6607
6608 // Extract unit and column name
6609 let unit = Self::extract_interval_unit_str(&step);
6610 let col_name = outer_alias_info
6611 .as_ref()
6612 .and_then(|(_, cols)| cols.first().map(|id| id.name.clone()))
6613 .unwrap_or_else(|| "value".to_string());
6614
6615 if let Some(unit_str) = unit {
6616 gda_info = Some((
6617 idx,
6618 col_name,
6619 start_expr,
6620 end_expr,
6621 unit_str,
6622 outer_alias_info,
6623 ));
6624 break;
6625 }
6626 }
6627 }
6628 }
6629 }
6630
6631 let Some((from_idx, col_name, start_expr, end_expr, unit_str, outer_alias_info)) = gda_info
6632 else {
6633 return Ok(Expression::Select(sel));
6634 };
6635
6636 // Build the Snowflake subquery:
6637 // (SELECT DATEADD(unit, CAST(col_name AS INT), CAST(start AS DATE)) AS col_name
6638 // FROM TABLE(FLATTEN(INPUT => ARRAY_GENERATE_RANGE(0, DATEDIFF(unit, start, end) + 1))) AS _t0(seq, key, path, index, col_name, this))
6639
6640 // DATEDIFF(unit, start, end)
6641 let datediff = Expression::Function(Box::new(Function::new(
6642 "DATEDIFF".to_string(),
6643 vec![
6644 Expression::boxed_column(Column {
6645 name: Identifier::new(&unit_str),
6646 table: None,
6647 join_mark: false,
6648 trailing_comments: vec![],
6649 span: None,
6650 inferred_type: None,
6651 }),
6652 start_expr.clone(),
6653 end_expr.clone(),
6654 ],
6655 )));
6656 // DATEDIFF(...) + 1
6657 let datediff_plus_one = Expression::Add(Box::new(BinaryOp {
6658 left: datediff,
6659 right: Expression::Literal(Box::new(Literal::Number("1".to_string()))),
6660 left_comments: vec![],
6661 operator_comments: vec![],
6662 trailing_comments: vec![],
6663 inferred_type: None,
6664 }));
6665
6666 let array_gen_range = Expression::Function(Box::new(Function::new(
6667 "ARRAY_GENERATE_RANGE".to_string(),
6668 vec![
6669 Expression::Literal(Box::new(Literal::Number("0".to_string()))),
6670 datediff_plus_one,
6671 ],
6672 )));
6673
6674 // TABLE(FLATTEN(INPUT => ...))
6675 let flatten_input = Expression::NamedArgument(Box::new(NamedArgument {
6676 name: Identifier::new("INPUT"),
6677 value: array_gen_range,
6678 separator: crate::expressions::NamedArgSeparator::DArrow,
6679 }));
6680 let flatten = Expression::Function(Box::new(Function::new(
6681 "FLATTEN".to_string(),
6682 vec![flatten_input],
6683 )));
6684
6685 // Determine alias name for the table: use outer alias or _t0
6686 let table_alias_name = outer_alias_info
6687 .as_ref()
6688 .map(|(name, _)| name.clone())
6689 .unwrap_or_else(|| "_t0".to_string());
6690
6691 // TABLE(FLATTEN(...)) AS _t0(seq, key, path, index, col_name, this)
6692 let table_func =
6693 Expression::Function(Box::new(Function::new("TABLE".to_string(), vec![flatten])));
6694 let flatten_aliased = Expression::Alias(Box::new(Alias {
6695 this: table_func,
6696 alias: Identifier::new(&table_alias_name),
6697 column_aliases: vec![
6698 Identifier::new("seq"),
6699 Identifier::new("key"),
6700 Identifier::new("path"),
6701 Identifier::new("index"),
6702 Identifier::new(&col_name),
6703 Identifier::new("this"),
6704 ],
6705 alias_explicit_as: false,
6706 alias_keyword: None,
6707 pre_alias_comments: vec![],
6708 trailing_comments: vec![],
6709 inferred_type: None,
6710 }));
6711
6712 // SELECT DATEADD(unit, CAST(col_name AS INT), CAST(start AS DATE)) AS col_name
6713 let dateadd_expr = Expression::Function(Box::new(Function::new(
6714 "DATEADD".to_string(),
6715 vec![
6716 Expression::boxed_column(Column {
6717 name: Identifier::new(&unit_str),
6718 table: None,
6719 join_mark: false,
6720 trailing_comments: vec![],
6721 span: None,
6722 inferred_type: None,
6723 }),
6724 Expression::Cast(Box::new(Cast {
6725 this: Expression::boxed_column(Column {
6726 name: Identifier::new(&col_name),
6727 table: None,
6728 join_mark: false,
6729 trailing_comments: vec![],
6730 span: None,
6731 inferred_type: None,
6732 }),
6733 to: DataType::Int {
6734 length: None,
6735 integer_spelling: false,
6736 },
6737 trailing_comments: vec![],
6738 double_colon_syntax: false,
6739 format: None,
6740 default: None,
6741 inferred_type: None,
6742 })),
6743 // Use start_expr directly - it's already been normalized (DATE literal -> CAST)
6744 start_expr.clone(),
6745 ],
6746 )));
6747 let dateadd_aliased = Expression::Alias(Box::new(Alias {
6748 this: dateadd_expr,
6749 alias: Identifier::new(&col_name),
6750 column_aliases: vec![],
6751 alias_explicit_as: false,
6752 alias_keyword: None,
6753 pre_alias_comments: vec![],
6754 trailing_comments: vec![],
6755 inferred_type: None,
6756 }));
6757
6758 // Build inner SELECT
6759 let mut inner_select = Select::new();
6760 inner_select.expressions = vec![dateadd_aliased];
6761 inner_select.from = Some(From {
6762 expressions: vec![flatten_aliased],
6763 });
6764
6765 let inner_select_expr = Expression::Select(Box::new(inner_select));
6766 let subquery = Expression::Subquery(Box::new(Subquery {
6767 this: inner_select_expr,
6768 alias: None,
6769 column_aliases: vec![],
6770 alias_explicit_as: false,
6771 alias_keyword: None,
6772 order_by: None,
6773 limit: None,
6774 offset: None,
6775 distribute_by: None,
6776 sort_by: None,
6777 cluster_by: None,
6778 lateral: false,
6779 modifiers_inside: false,
6780 trailing_comments: vec![],
6781 inferred_type: None,
6782 }));
6783
6784 // If there was an outer alias (e.g., AS _q(date_week)), wrap with alias
6785 let replacement = if let Some((alias_name, col_aliases)) = outer_alias_info {
6786 Expression::Alias(Box::new(Alias {
6787 this: subquery,
6788 alias: Identifier::new(&alias_name),
6789 column_aliases: col_aliases,
6790 alias_explicit_as: false,
6791 alias_keyword: None,
6792 pre_alias_comments: vec![],
6793 trailing_comments: vec![],
6794 inferred_type: None,
6795 }))
6796 } else {
6797 subquery
6798 };
6799
6800 // Replace the FROM expression
6801 if let Some(ref mut from) = sel.from {
6802 from.expressions[from_idx] = replacement;
6803 }
6804
6805 Ok(Expression::Select(sel))
6806 }
6807
6808 /// Convert ARRAY_SIZE(GENERATE_DATE_ARRAY(start, end, step)) for Snowflake.
6809 /// Produces: ARRAY_SIZE((SELECT ARRAY_AGG(*) FROM (SELECT DATEADD(unit, CAST(value AS INT), start) AS value
6810 /// FROM TABLE(FLATTEN(INPUT => ARRAY_GENERATE_RANGE(0, DATEDIFF(unit, start, end) + 1))) AS _t0(...))))
6811 fn convert_array_size_gda_snowflake(f: &crate::expressions::Function) -> Result<Expression> {
6812 use crate::expressions::*;
6813
6814 let start_expr = f.args[0].clone();
6815 let end_expr = f.args[1].clone();
6816 let step = f.args.get(2).cloned();
6817 let unit_str = Self::extract_interval_unit_str(&step).unwrap_or_else(|| "DAY".to_string());
6818 let col_name = "value";
6819
6820 // Build the inner subquery: same as try_transform_from_gda_snowflake
6821 let datediff = Expression::Function(Box::new(Function::new(
6822 "DATEDIFF".to_string(),
6823 vec![
6824 Expression::boxed_column(Column {
6825 name: Identifier::new(&unit_str),
6826 table: None,
6827 join_mark: false,
6828 trailing_comments: vec![],
6829 span: None,
6830 inferred_type: None,
6831 }),
6832 start_expr.clone(),
6833 end_expr.clone(),
6834 ],
6835 )));
6836 // DATEDIFF(...) + 1
6837 let datediff_plus_one = Expression::Add(Box::new(BinaryOp {
6838 left: datediff,
6839 right: Expression::Literal(Box::new(Literal::Number("1".to_string()))),
6840 left_comments: vec![],
6841 operator_comments: vec![],
6842 trailing_comments: vec![],
6843 inferred_type: None,
6844 }));
6845
6846 let array_gen_range = Expression::Function(Box::new(Function::new(
6847 "ARRAY_GENERATE_RANGE".to_string(),
6848 vec![
6849 Expression::Literal(Box::new(Literal::Number("0".to_string()))),
6850 datediff_plus_one,
6851 ],
6852 )));
6853
6854 let flatten_input = Expression::NamedArgument(Box::new(NamedArgument {
6855 name: Identifier::new("INPUT"),
6856 value: array_gen_range,
6857 separator: crate::expressions::NamedArgSeparator::DArrow,
6858 }));
6859 let flatten = Expression::Function(Box::new(Function::new(
6860 "FLATTEN".to_string(),
6861 vec![flatten_input],
6862 )));
6863
6864 let table_func =
6865 Expression::Function(Box::new(Function::new("TABLE".to_string(), vec![flatten])));
6866 let flatten_aliased = Expression::Alias(Box::new(Alias {
6867 this: table_func,
6868 alias: Identifier::new("_t0"),
6869 column_aliases: vec![
6870 Identifier::new("seq"),
6871 Identifier::new("key"),
6872 Identifier::new("path"),
6873 Identifier::new("index"),
6874 Identifier::new(col_name),
6875 Identifier::new("this"),
6876 ],
6877 alias_explicit_as: false,
6878 alias_keyword: None,
6879 pre_alias_comments: vec![],
6880 trailing_comments: vec![],
6881 inferred_type: None,
6882 }));
6883
6884 let dateadd_expr = Expression::Function(Box::new(Function::new(
6885 "DATEADD".to_string(),
6886 vec![
6887 Expression::boxed_column(Column {
6888 name: Identifier::new(&unit_str),
6889 table: None,
6890 join_mark: false,
6891 trailing_comments: vec![],
6892 span: None,
6893 inferred_type: None,
6894 }),
6895 Expression::Cast(Box::new(Cast {
6896 this: Expression::boxed_column(Column {
6897 name: Identifier::new(col_name),
6898 table: None,
6899 join_mark: false,
6900 trailing_comments: vec![],
6901 span: None,
6902 inferred_type: None,
6903 }),
6904 to: DataType::Int {
6905 length: None,
6906 integer_spelling: false,
6907 },
6908 trailing_comments: vec![],
6909 double_colon_syntax: false,
6910 format: None,
6911 default: None,
6912 inferred_type: None,
6913 })),
6914 start_expr.clone(),
6915 ],
6916 )));
6917 let dateadd_aliased = Expression::Alias(Box::new(Alias {
6918 this: dateadd_expr,
6919 alias: Identifier::new(col_name),
6920 column_aliases: vec![],
6921 alias_explicit_as: false,
6922 alias_keyword: None,
6923 pre_alias_comments: vec![],
6924 trailing_comments: vec![],
6925 inferred_type: None,
6926 }));
6927
6928 // Inner SELECT: SELECT DATEADD(...) AS value FROM TABLE(FLATTEN(...)) AS _t0(...)
6929 let mut inner_select = Select::new();
6930 inner_select.expressions = vec![dateadd_aliased];
6931 inner_select.from = Some(From {
6932 expressions: vec![flatten_aliased],
6933 });
6934
6935 // Wrap in subquery for the inner part
6936 let inner_subquery = Expression::Subquery(Box::new(Subquery {
6937 this: Expression::Select(Box::new(inner_select)),
6938 alias: None,
6939 column_aliases: vec![],
6940 alias_explicit_as: false,
6941 alias_keyword: None,
6942 order_by: None,
6943 limit: None,
6944 offset: None,
6945 distribute_by: None,
6946 sort_by: None,
6947 cluster_by: None,
6948 lateral: false,
6949 modifiers_inside: false,
6950 trailing_comments: vec![],
6951 inferred_type: None,
6952 }));
6953
6954 // Outer: SELECT ARRAY_AGG(*) FROM (inner_subquery)
6955 let star = Expression::Star(Star {
6956 table: None,
6957 except: None,
6958 replace: None,
6959 rename: None,
6960 trailing_comments: vec![],
6961 span: None,
6962 });
6963 let array_agg = Expression::ArrayAgg(Box::new(AggFunc {
6964 this: star,
6965 distinct: false,
6966 filter: None,
6967 order_by: vec![],
6968 name: Some("ARRAY_AGG".to_string()),
6969 ignore_nulls: None,
6970 having_max: None,
6971 limit: None,
6972 inferred_type: None,
6973 }));
6974
6975 let mut outer_select = Select::new();
6976 outer_select.expressions = vec![array_agg];
6977 outer_select.from = Some(From {
6978 expressions: vec![inner_subquery],
6979 });
6980
6981 // Wrap in a subquery
6982 let outer_subquery = Expression::Subquery(Box::new(Subquery {
6983 this: Expression::Select(Box::new(outer_select)),
6984 alias: None,
6985 column_aliases: vec![],
6986 alias_explicit_as: false,
6987 alias_keyword: None,
6988 order_by: None,
6989 limit: None,
6990 offset: None,
6991 distribute_by: None,
6992 sort_by: None,
6993 cluster_by: None,
6994 lateral: false,
6995 modifiers_inside: false,
6996 trailing_comments: vec![],
6997 inferred_type: None,
6998 }));
6999
7000 // ARRAY_SIZE(subquery)
7001 Ok(Expression::ArraySize(Box::new(UnaryFunc::new(
7002 outer_subquery,
7003 ))))
7004 }
7005
7006 /// Extract interval unit string from an optional step expression.
7007 fn extract_interval_unit_str(step: &Option<Expression>) -> Option<String> {
7008 use crate::expressions::*;
7009 if let Some(Expression::Interval(ref iv)) = step {
7010 if let Some(IntervalUnitSpec::Simple { ref unit, .. }) = iv.unit {
7011 return Some(format!("{:?}", unit).to_ascii_uppercase());
7012 }
7013 if let Some(ref this) = iv.this {
7014 if let Expression::Literal(lit) = this {
7015 if let Literal::String(ref s) = lit.as_ref() {
7016 let parts: Vec<&str> = s.split_whitespace().collect();
7017 if parts.len() == 2 {
7018 return Some(parts[1].to_ascii_uppercase());
7019 } else if parts.len() == 1 {
7020 let upper = parts[0].to_ascii_uppercase();
7021 if matches!(
7022 upper.as_str(),
7023 "YEAR"
7024 | "QUARTER"
7025 | "MONTH"
7026 | "WEEK"
7027 | "DAY"
7028 | "HOUR"
7029 | "MINUTE"
7030 | "SECOND"
7031 ) {
7032 return Some(upper);
7033 }
7034 }
7035 }
7036 }
7037 }
7038 }
7039 // Default to DAY if no step or no interval
7040 if step.is_none() {
7041 return Some("DAY".to_string());
7042 }
7043 None
7044 }
7045
7046 fn normalize_snowflake_pretty(mut sql: String) -> String {
7047 if sql.contains("LATERAL IFF(_u.pos = _u_2.pos_2, _u_2.entity, NULL) AS datasource(SEQ, KEY, PATH, INDEX, VALUE, THIS)")
7048 && sql.contains("ARRAY_GENERATE_RANGE(0, (GREATEST(ARRAY_SIZE(INPUT => PARSE_JSON(flags))) - 1) + 1)")
7049 {
7050 sql = sql.replace(
7051 "AND uc.user_id <> ALL (SELECT DISTINCT\n _id\n FROM users, LATERAL IFF(_u.pos = _u_2.pos_2, _u_2.entity, NULL) AS datasource(SEQ, KEY, PATH, INDEX, VALUE, THIS)\n WHERE\n GET_PATH(datasource.value, 'name') = 'something')",
7052 "AND uc.user_id <> ALL (\n SELECT DISTINCT\n _id\n FROM users, LATERAL IFF(_u.pos = _u_2.pos_2, _u_2.entity, NULL) AS datasource(SEQ, KEY, PATH, INDEX, VALUE, THIS)\n WHERE\n GET_PATH(datasource.value, 'name') = 'something'\n )",
7053 );
7054
7055 sql = sql.replace(
7056 "CROSS JOIN TABLE(FLATTEN(INPUT => ARRAY_GENERATE_RANGE(0, (GREATEST(ARRAY_SIZE(INPUT => PARSE_JSON(flags))) - 1) + 1))) AS _u(seq, key, path, index, pos, this)",
7057 "CROSS JOIN TABLE(FLATTEN(INPUT => ARRAY_GENERATE_RANGE(0, (\n GREATEST(ARRAY_SIZE(INPUT => PARSE_JSON(flags))) - 1\n) + 1))) AS _u(seq, key, path, index, pos, this)",
7058 );
7059
7060 sql = sql.replace(
7061 "OR (_u.pos > (ARRAY_SIZE(INPUT => PARSE_JSON(flags)) - 1)\n AND _u_2.pos_2 = (ARRAY_SIZE(INPUT => PARSE_JSON(flags)) - 1))",
7062 "OR (\n _u.pos > (\n ARRAY_SIZE(INPUT => PARSE_JSON(flags)) - 1\n )\n AND _u_2.pos_2 = (\n ARRAY_SIZE(INPUT => PARSE_JSON(flags)) - 1\n )\n )",
7063 );
7064 }
7065
7066 sql
7067 }
7068
7069 /// Apply cross-dialect semantic normalizations that depend on knowing both source and target.
7070 /// This handles cases where the same syntax has different semantics across dialects.
7071 fn cross_dialect_normalize(
7072 expr: Expression,
7073 source: DialectType,
7074 target: DialectType,
7075 ) -> Result<Expression> {
7076 use crate::expressions::{
7077 AggFunc, BinaryOp, Case, Cast, ConvertTimezone, DataType, DateTimeField, DateTruncFunc,
7078 Function, Identifier, IsNull, Literal, Null, Paren,
7079 };
7080
7081 // Helper to tag which kind of transform to apply
7082 #[derive(Debug)]
7083 enum Action {
7084 None,
7085 GreatestLeastNull,
7086 ArrayGenerateRange,
7087 Div0TypedDivision,
7088 ArrayAggCollectList,
7089 ArrayAggWithinGroupFilter,
7090 ArrayAggFilter,
7091 CastTimestampToDatetime,
7092 DateTruncWrapCast,
7093 ToDateToCast,
7094 ConvertTimezoneToExpr,
7095 SetToVariable,
7096 RegexpReplaceSnowflakeToDuckDB,
7097 BigQueryFunctionNormalize,
7098 BigQuerySafeDivide,
7099 BigQueryCastType,
7100 BigQueryToHexBare, // _BQ_TO_HEX(x) with no LOWER/UPPER wrapper
7101 BigQueryToHexLower, // LOWER(_BQ_TO_HEX(x))
7102 BigQueryToHexUpper, // UPPER(_BQ_TO_HEX(x))
7103 BigQueryLastDayStripUnit, // LAST_DAY(date, MONTH) -> LAST_DAY(date)
7104 BigQueryCastFormat, // CAST(x AS type FORMAT 'fmt') -> PARSE_DATE/PARSE_TIMESTAMP etc.
7105 BigQueryAnyValueHaving, // ANY_VALUE(x HAVING MAX/MIN y) -> ARG_MAX_NULL/ARG_MIN_NULL for DuckDB
7106 BigQueryApproxQuantiles, // APPROX_QUANTILES(x, n) -> APPROX_QUANTILE(x, [quantiles]) for DuckDB
7107 GenericFunctionNormalize, // Cross-dialect function renaming (non-BigQuery sources)
7108 RegexpLikeToDuckDB, // RegexpLike -> REGEXP_MATCHES for DuckDB target
7109 EpochConvert, // Expression::Epoch -> target-specific epoch function
7110 EpochMsConvert, // Expression::EpochMs -> target-specific epoch ms function
7111 TSQLTypeNormalize, // TSQL types (MONEY, SMALLMONEY, REAL, DATETIME2) -> standard types
7112 MySQLSafeDivide, // MySQL a/b -> a / NULLIF(b, 0) with optional CAST
7113 NullsOrdering, // Add NULLS FIRST/LAST for ORDER BY
7114 AlterTableRenameStripSchema, // ALTER TABLE db.t1 RENAME TO db.t2 -> ALTER TABLE db.t1 RENAME TO t2
7115 StringAggConvert, // STRING_AGG/WITHIN GROUP -> target-specific aggregate
7116 GroupConcatConvert, // GROUP_CONCAT -> target-specific aggregate
7117 TempTableHash, // TSQL #table -> temp table normalization
7118 ArrayLengthConvert, // CARDINALITY/ARRAY_LENGTH/ARRAY_SIZE -> target-specific
7119 DatePartUnquote, // DATE_PART('month', x) -> DATE_PART(month, x) for Snowflake target
7120 NvlClearOriginal, // Clear NVL original_name for cross-dialect transpilation
7121 HiveCastToTryCast, // Hive/Spark CAST -> TRY_CAST for targets that support it
7122 XorExpand, // MySQL XOR -> (a AND NOT b) OR (NOT a AND b) for non-XOR targets
7123 CastTimestampStripTz, // CAST(x AS TIMESTAMP WITH TIME ZONE) -> CAST(x AS TIMESTAMP) for Hive/Spark
7124 JsonExtractToGetJsonObject, // JSON_EXTRACT/JSON_EXTRACT_SCALAR -> GET_JSON_OBJECT for Hive/Spark
7125 JsonExtractScalarToGetJsonObject, // JSON_EXTRACT_SCALAR -> GET_JSON_OBJECT for Hive/Spark
7126 JsonQueryValueConvert, // JsonQuery/JsonValue -> target-specific (ISNULL wrapper for TSQL, GET_JSON_OBJECT for Spark, etc.)
7127 JsonLiteralToJsonParse, // JSON 'x' -> JSON_PARSE('x') for Presto, PARSE_JSON for Snowflake; also DuckDB CAST(x AS JSON)
7128 DuckDBCastJsonToVariant, // DuckDB CAST(x AS JSON) -> CAST(x AS VARIANT) for Snowflake
7129 DuckDBTryCastJsonToTryJsonParse, // DuckDB TRY_CAST(x AS JSON) -> TRY(JSON_PARSE(x)) for Trino/Presto/Athena
7130 DuckDBJsonFuncToJsonParse, // DuckDB json(x) -> JSON_PARSE(x) for Trino/Presto/Athena
7131 DuckDBJsonValidToIsJson, // DuckDB json_valid(x) -> x IS JSON for Trino/Presto/Athena
7132 ArraySyntaxConvert, // ARRAY[x] -> ARRAY(x) for Spark, [x] for BigQuery/DuckDB
7133 AtTimeZoneConvert, // AT TIME ZONE -> AT_TIMEZONE (Presto) / FROM_UTC_TIMESTAMP (Spark)
7134 DayOfWeekConvert, // DAY_OF_WEEK -> dialect-specific
7135 MaxByMinByConvert, // MAX_BY/MIN_BY -> argMax/argMin for ClickHouse
7136 ArrayAggToCollectList, // ARRAY_AGG(x ORDER BY ...) -> COLLECT_LIST(x) for Hive/Spark
7137 ArrayAggToGroupConcat, // ARRAY_AGG(x) -> GROUP_CONCAT(x) for MySQL-like targets
7138 ElementAtConvert, // ELEMENT_AT(arr, idx) -> arr[idx] for PostgreSQL, arr[SAFE_ORDINAL(idx)] for BigQuery
7139 CurrentUserParens, // CURRENT_USER -> CURRENT_USER() for Snowflake
7140 CastToJsonForSpark, // CAST(x AS JSON) -> TO_JSON(x) for Spark
7141 CastJsonToFromJson, // CAST(JSON_PARSE(literal) AS ARRAY/MAP) -> FROM_JSON(literal, type_string)
7142 ToJsonConvert, // TO_JSON(x) -> JSON_FORMAT(CAST(x AS JSON)) for Presto etc.
7143 ArrayAggNullFilter, // ARRAY_AGG(x) FILTER(WHERE cond) -> add AND NOT x IS NULL for DuckDB
7144 ArrayAggIgnoreNullsDuckDB, // ARRAY_AGG(x IGNORE NULLS ORDER BY ...) -> ARRAY_AGG(x ORDER BY a NULLS FIRST, ...) for DuckDB
7145 BigQueryPercentileContToDuckDB, // PERCENTILE_CONT(x, frac RESPECT NULLS) -> QUANTILE_CONT(x, frac) for DuckDB
7146 BigQueryArraySelectAsStructToSnowflake, // ARRAY(SELECT AS STRUCT ...) -> (SELECT ARRAY_AGG(OBJECT_CONSTRUCT(...)))
7147 CountDistinctMultiArg, // COUNT(DISTINCT a, b) -> COUNT(DISTINCT CASE WHEN ... END)
7148 VarianceToClickHouse, // Expression::Variance -> varSamp for ClickHouse
7149 StddevToClickHouse, // Expression::Stddev -> stddevSamp for ClickHouse
7150 ApproxQuantileConvert, // Expression::ApproxQuantile -> APPROX_PERCENTILE for Snowflake
7151 ArrayIndexConvert, // array[1] -> array[0] for BigQuery (1-based to 0-based)
7152 DollarParamConvert, // $foo -> @foo for BigQuery
7153 TablesampleReservoir, // TABLESAMPLE (n ROWS) -> TABLESAMPLE RESERVOIR (n ROWS) for DuckDB
7154 BitAggFloatCast, // BIT_OR/BIT_AND/BIT_XOR float arg -> CAST(ROUND(CAST(arg)) AS INT) for DuckDB
7155 BitAggSnowflakeRename, // BIT_OR -> BITORAGG, BIT_AND -> BITANDAGG etc. for Snowflake
7156 StrftimeCastTimestamp, // CAST TIMESTAMP -> TIMESTAMP_NTZ for Spark in STRFTIME
7157 AnyValueIgnoreNulls, // ANY_VALUE(x) -> ANY_VALUE(x) IGNORE NULLS for Spark
7158 CreateTableStripComment, // Strip COMMENT column constraint, USING, PARTITIONED BY for DuckDB
7159 EscapeStringNormalize, // e'Hello\nworld' literal newline -> \n
7160 AnyToExists, // PostgreSQL x <op> ANY(array) -> EXISTS(array, x -> ...)
7161 ArrayConcatBracketConvert, // [1,2] -> ARRAY[1,2] for PostgreSQL in ARRAY_CAT
7162 SnowflakeIntervalFormat, // INTERVAL '2' HOUR -> INTERVAL '2 HOUR' for Snowflake
7163 AlterTableToSpRename, // ALTER TABLE RENAME -> EXEC sp_rename for TSQL
7164 StraightJoinCase, // STRAIGHT_JOIN -> straight_join for DuckDB
7165 RespectNullsConvert, // RESPECT NULLS window function handling
7166 MysqlNullsOrdering, // MySQL doesn't support NULLS ordering
7167 MysqlNullsLastRewrite, // Add CASE WHEN to ORDER BY for DuckDB -> MySQL (NULLS LAST simulation)
7168 BigQueryNullsOrdering, // BigQuery doesn't support NULLS FIRST/LAST - strip
7169 SnowflakeFloatProtect, // Protect FLOAT from being converted to DOUBLE by Snowflake target transform
7170 JsonToGetPath, // JSON arrow -> GET_PATH/PARSE_JSON for Snowflake
7171 FilterToIff, // FILTER(WHERE) -> IFF wrapping for Snowflake
7172 AggFilterToIff, // AggFunc.filter -> IFF wrapping for Snowflake (e.g., AVG(x) FILTER(WHERE cond))
7173 StructToRow, // DuckDB struct -> Presto ROW / BigQuery STRUCT
7174 SparkStructConvert, // Spark STRUCT(x AS col1, ...) -> ROW/DuckDB struct
7175 DecimalDefaultPrecision, // DECIMAL -> DECIMAL(18, 3) for Snowflake in BIT agg
7176 ApproxCountDistinctToApproxDistinct, // APPROX_COUNT_DISTINCT -> APPROX_DISTINCT for Presto/Trino
7177 CollectListToArrayAgg, // COLLECT_LIST -> ARRAY_AGG for Presto/DuckDB
7178 CollectSetConvert, // COLLECT_SET -> SET_AGG/ARRAY_AGG(DISTINCT)/ARRAY_UNIQUE_AGG
7179 PercentileConvert, // PERCENTILE -> QUANTILE/APPROX_PERCENTILE
7180 CorrIsnanWrap, // CORR(a,b) -> CASE WHEN ISNAN(CORR(a,b)) THEN NULL ELSE CORR(a,b) END
7181 TruncToDateTrunc, // TRUNC(ts, unit) -> DATE_TRUNC(unit, ts)
7182 ArrayContainsConvert, // ARRAY_CONTAINS -> CONTAINS/target-specific
7183 StrPositionExpand, // StrPosition with position -> complex STRPOS expansion for Presto/DuckDB
7184 TablesampleSnowflakeStrip, // Strip method and PERCENT for Snowflake target
7185 FirstToAnyValue, // FIRST(col) IGNORE NULLS -> ANY_VALUE(col) for DuckDB
7186 MonthsBetweenConvert, // Expression::MonthsBetween -> target-specific
7187 CurrentUserSparkParens, // CURRENT_USER -> CURRENT_USER() for Spark
7188 SparkDateFuncCast, // MONTH/YEAR/DAY('str') -> MONTH/YEAR/DAY(CAST('str' AS DATE)) from Spark
7189 MapFromArraysConvert, // Expression::MapFromArrays -> MAP/OBJECT_CONSTRUCT/MAP_FROM_ARRAYS
7190 AddMonthsConvert, // Expression::AddMonths -> target-specific DATEADD/DATE_ADD
7191 PercentileContConvert, // PERCENTILE_CONT/DISC WITHIN GROUP -> APPROX_PERCENTILE/PERCENTILE_APPROX
7192 GenerateSeriesConvert, // GENERATE_SERIES -> SEQUENCE/UNNEST(SEQUENCE)/EXPLODE(SEQUENCE)
7193 ConcatCoalesceWrap, // CONCAT(a, b) -> CONCAT(COALESCE(CAST(a), ''), ...) for Presto/ClickHouse
7194 PipeConcatToConcat, // a || b -> CONCAT(CAST(a), CAST(b)) for Presto
7195 DivFuncConvert, // DIV(a, b) -> a // b for DuckDB, CAST for BigQuery
7196 JsonObjectAggConvert, // JSON_OBJECT_AGG -> JSON_GROUP_OBJECT for DuckDB
7197 JsonbExistsConvert, // JSONB_EXISTS -> JSON_EXISTS for DuckDB
7198 DateBinConvert, // DATE_BIN -> TIME_BUCKET for DuckDB
7199 MysqlCastCharToText, // MySQL CAST(x AS CHAR) -> CAST(x AS TEXT/VARCHAR/STRING) for targets
7200 SparkCastVarcharToString, // Spark CAST(x AS VARCHAR/CHAR) -> CAST(x AS STRING) for Spark targets
7201 JsonExtractToArrow, // JSON_EXTRACT(x, path) -> x -> path for SQLite/DuckDB
7202 JsonExtractToTsql, // JSON_EXTRACT/JSON_EXTRACT_SCALAR -> ISNULL(JSON_QUERY, JSON_VALUE) for TSQL
7203 JsonExtractToClickHouse, // JSON_EXTRACT/JSON_EXTRACT_SCALAR -> JSONExtractString for ClickHouse
7204 JsonExtractScalarConvert, // JSON_EXTRACT_SCALAR -> target-specific (PostgreSQL, Snowflake, SQLite)
7205 JsonPathNormalize, // Normalize JSON path format (brackets, wildcards, quotes) for various dialects
7206 MinMaxToLeastGreatest, // Multi-arg MIN(a,b,c) -> LEAST(a,b,c), MAX(a,b,c) -> GREATEST(a,b,c)
7207 ClickHouseUniqToApproxCountDistinct, // uniq(x) -> APPROX_COUNT_DISTINCT(x) for non-ClickHouse targets
7208 ClickHouseAnyToAnyValue, // any(x) -> ANY_VALUE(x) for non-ClickHouse targets
7209 OracleVarchar2ToVarchar, // VARCHAR2(N CHAR/BYTE) -> VARCHAR(N) for non-Oracle targets
7210 Nvl2Expand, // NVL2(a, b, c) -> CASE WHEN NOT a IS NULL THEN b ELSE c END
7211 IfnullToCoalesce, // IFNULL(a, b) -> COALESCE(a, b)
7212 IsAsciiConvert, // IS_ASCII(x) -> dialect-specific ASCII check
7213 StrPositionConvert, // STR_POSITION(haystack, needle[, pos]) -> dialect-specific
7214 DecodeSimplify, // DECODE with null-safe -> simple = comparison
7215 ArraySumConvert, // ARRAY_SUM -> target-specific
7216 ArraySizeConvert, // ARRAY_SIZE -> target-specific
7217 ArrayAnyConvert, // ARRAY_ANY -> target-specific
7218 CastTimestamptzToFunc, // CAST(x AS TIMESTAMPTZ) -> TIMESTAMP(x) for MySQL/StarRocks
7219 TsOrDsToDateConvert, // TS_OR_DS_TO_DATE(x[, fmt]) -> dialect-specific
7220 TsOrDsToDateStrConvert, // TS_OR_DS_TO_DATE_STR(x) -> SUBSTRING(CAST(x AS type), 1, 10)
7221 DateStrToDateConvert, // DATE_STR_TO_DATE(x) -> CAST(x AS DATE)
7222 TimeStrToDateConvert, // TIME_STR_TO_DATE(x) -> CAST(x AS DATE)
7223 TimeStrToTimeConvert, // TIME_STR_TO_TIME(x) -> CAST(x AS TIMESTAMP)
7224 DateToDateStrConvert, // DATE_TO_DATE_STR(x) -> CAST(x AS TEXT/VARCHAR/STRING)
7225 DateToDiConvert, // DATE_TO_DI(x) -> dialect-specific (CAST date to YYYYMMDD integer)
7226 DiToDateConvert, // DI_TO_DATE(x) -> dialect-specific (integer YYYYMMDD to date)
7227 TsOrDiToDiConvert, // TS_OR_DI_TO_DI(x) -> dialect-specific
7228 UnixToStrConvert, // UNIX_TO_STR(x, fmt) -> dialect-specific
7229 UnixToTimeConvert, // UNIX_TO_TIME(x) -> dialect-specific
7230 UnixToTimeStrConvert, // UNIX_TO_TIME_STR(x) -> dialect-specific
7231 TimeToUnixConvert, // TIME_TO_UNIX(x) -> dialect-specific
7232 TimeToStrConvert, // TIME_TO_STR(x, fmt) -> dialect-specific
7233 StrToUnixConvert, // STR_TO_UNIX(x, fmt) -> dialect-specific
7234 DateTruncSwapArgs, // DATE_TRUNC('unit', x) -> DATE_TRUNC(x, unit) / TRUNC(x, unit)
7235 TimestampTruncConvert, // TIMESTAMP_TRUNC(x, UNIT[, tz]) -> dialect-specific
7236 StrToDateConvert, // STR_TO_DATE(x, fmt) from Generic -> CAST(StrToTime(x,fmt) AS DATE)
7237 TsOrDsAddConvert, // TS_OR_DS_ADD(x, n, 'UNIT') from Generic -> DATE_ADD per dialect
7238 DateFromUnixDateConvert, // DATE_FROM_UNIX_DATE(n) -> DATEADD(DAY, n, '1970-01-01')
7239 TimeStrToUnixConvert, // TIME_STR_TO_UNIX(x) -> dialect-specific
7240 TimeToTimeStrConvert, // TIME_TO_TIME_STR(x) -> CAST(x AS type)
7241 CreateTableLikeToCtas, // CREATE TABLE a LIKE b -> CREATE TABLE a AS SELECT * FROM b LIMIT 0
7242 CreateTableLikeToSelectInto, // CREATE TABLE a LIKE b -> SELECT TOP 0 * INTO a FROM b AS temp
7243 CreateTableLikeToAs, // CREATE TABLE a LIKE b -> CREATE TABLE a AS b (ClickHouse)
7244 ArrayRemoveConvert, // ARRAY_REMOVE(arr, target) -> LIST_FILTER/arrayFilter/ARRAY subquery
7245 ArrayReverseConvert, // ARRAY_REVERSE(x) -> arrayReverse(x) for ClickHouse
7246 JsonKeysConvert, // JSON_KEYS -> JSON_OBJECT_KEYS/OBJECT_KEYS
7247 ParseJsonStrip, // PARSE_JSON(x) -> x (strip wrapper)
7248 ArraySizeDrill, // ARRAY_SIZE -> REPEATED_COUNT for Drill
7249 WeekOfYearToWeekIso, // WEEKOFYEAR -> WEEKISO for Snowflake cross-dialect
7250 RegexpSubstrSnowflakeToDuckDB, // REGEXP_SUBSTR(s, p, ...) -> REGEXP_EXTRACT variants for DuckDB
7251 RegexpSubstrSnowflakeIdentity, // REGEXP_SUBSTR/REGEXP_SUBSTR_ALL strip trailing group=0 for Snowflake identity
7252 RegexpSubstrAllSnowflakeToDuckDB, // REGEXP_SUBSTR_ALL(s, p, ...) -> REGEXP_EXTRACT_ALL variants for DuckDB
7253 RegexpCountSnowflakeToDuckDB, // REGEXP_COUNT(s, p, ...) -> LENGTH(REGEXP_EXTRACT_ALL(...)) for DuckDB
7254 RegexpInstrSnowflakeToDuckDB, // REGEXP_INSTR(s, p, ...) -> complex CASE expression for DuckDB
7255 RegexpReplacePositionSnowflakeToDuckDB, // REGEXP_REPLACE(s, p, r, pos, occ) -> DuckDB form
7256 RlikeSnowflakeToDuckDB, // RLIKE(a, b[, flags]) -> REGEXP_FULL_MATCH(a, b[, flags]) for DuckDB
7257 RegexpExtractAllToSnowflake, // BigQuery REGEXP_EXTRACT_ALL -> REGEXP_SUBSTR_ALL for Snowflake
7258 ArrayExceptConvert, // ARRAY_EXCEPT -> DuckDB complex CASE / Snowflake ARRAY_EXCEPT / Presto ARRAY_EXCEPT
7259 ArrayPositionSnowflakeSwap, // ARRAY_POSITION(arr, elem) -> ARRAY_POSITION(elem, arr) for Snowflake
7260 RegexpLikeExasolAnchor, // RegexpLike -> Exasol REGEXP_LIKE with .*pattern.* anchoring
7261 ArrayDistinctConvert, // ARRAY_DISTINCT -> DuckDB LIST_DISTINCT with NULL-aware CASE
7262 ArrayDistinctClickHouse, // ARRAY_DISTINCT -> arrayDistinct for ClickHouse
7263 ArrayContainsDuckDBConvert, // ARRAY_CONTAINS -> DuckDB CASE with NULL-aware check
7264 SnowflakeWindowFrameStrip, // Strip default ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING for Snowflake target
7265 SnowflakeWindowFrameAdd, // Add default ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING for non-Snowflake target
7266 SnowflakeArrayPositionToDuckDB, // ARRAY_POSITION(val, arr) -> ARRAY_POSITION(arr, val) - 1 for DuckDB
7267 }
7268
7269 // Handle SELECT INTO -> CREATE TABLE AS for DuckDB/Snowflake/etc.
7270 let expr = if matches!(source, DialectType::TSQL | DialectType::Fabric) {
7271 Self::transform_select_into(expr, source, target)
7272 } else {
7273 expr
7274 };
7275
7276 // Strip OFFSET ROWS for non-TSQL/Oracle targets
7277 let expr = if !matches!(
7278 target,
7279 DialectType::TSQL | DialectType::Oracle | DialectType::Fabric
7280 ) {
7281 if let Expression::Select(mut select) = expr {
7282 if let Some(ref mut offset) = select.offset {
7283 offset.rows = None;
7284 }
7285 Expression::Select(select)
7286 } else {
7287 expr
7288 }
7289 } else {
7290 expr
7291 };
7292
7293 // Oracle: LIMIT -> FETCH FIRST, OFFSET -> OFFSET ROWS
7294 let expr = if matches!(target, DialectType::Oracle) {
7295 if let Expression::Select(mut select) = expr {
7296 if let Some(limit) = select.limit.take() {
7297 // Convert LIMIT to FETCH FIRST n ROWS ONLY
7298 select.fetch = Some(crate::expressions::Fetch {
7299 direction: "FIRST".to_string(),
7300 count: Some(limit.this),
7301 percent: false,
7302 rows: true,
7303 with_ties: false,
7304 });
7305 }
7306 // Add ROWS to OFFSET if present
7307 if let Some(ref mut offset) = select.offset {
7308 offset.rows = Some(true);
7309 }
7310 Expression::Select(select)
7311 } else {
7312 expr
7313 }
7314 } else {
7315 expr
7316 };
7317
7318 // Handle CreateTable WITH properties transformation before recursive transforms
7319 let expr = if let Expression::CreateTable(mut ct) = expr {
7320 Self::transform_create_table_properties(&mut ct, source, target);
7321
7322 // Handle Hive-style PARTITIONED BY (col_name type, ...) -> target-specific
7323 // When the PARTITIONED BY clause contains column definitions, merge them into the
7324 // main column list and adjust the PARTITIONED BY clause for the target dialect.
7325 if matches!(
7326 source,
7327 DialectType::Hive | DialectType::Spark | DialectType::Databricks
7328 ) {
7329 let mut partition_col_names: Vec<String> = Vec::new();
7330 let mut partition_col_defs: Vec<crate::expressions::ColumnDef> = Vec::new();
7331 let mut has_col_def_partitions = false;
7332
7333 // Check if any PARTITIONED BY property contains ColumnDef expressions
7334 for prop in &ct.properties {
7335 if let Expression::PartitionedByProperty(ref pbp) = prop {
7336 if let Expression::Tuple(ref tuple) = *pbp.this {
7337 for expr in &tuple.expressions {
7338 if let Expression::ColumnDef(ref cd) = expr {
7339 has_col_def_partitions = true;
7340 partition_col_names.push(cd.name.name.clone());
7341 partition_col_defs.push(*cd.clone());
7342 }
7343 }
7344 }
7345 }
7346 }
7347
7348 if has_col_def_partitions && !matches!(target, DialectType::Hive) {
7349 // Merge partition columns into main column list
7350 for cd in partition_col_defs {
7351 ct.columns.push(cd);
7352 }
7353
7354 // Replace PARTITIONED BY property with column-name-only version
7355 ct.properties
7356 .retain(|p| !matches!(p, Expression::PartitionedByProperty(_)));
7357
7358 if matches!(
7359 target,
7360 DialectType::Presto | DialectType::Trino | DialectType::Athena
7361 ) {
7362 // Presto: WITH (PARTITIONED_BY=ARRAY['y', 'z'])
7363 let array_elements: Vec<String> = partition_col_names
7364 .iter()
7365 .map(|n| format!("'{}'", n))
7366 .collect();
7367 let array_value = format!("ARRAY[{}]", array_elements.join(", "));
7368 ct.with_properties
7369 .push(("PARTITIONED_BY".to_string(), array_value));
7370 } else if matches!(target, DialectType::Spark | DialectType::Databricks) {
7371 // Spark: PARTITIONED BY (y, z) - just column names
7372 let name_exprs: Vec<Expression> = partition_col_names
7373 .iter()
7374 .map(|n| {
7375 Expression::Column(Box::new(crate::expressions::Column {
7376 name: crate::expressions::Identifier::new(n.clone()),
7377 table: None,
7378 join_mark: false,
7379 trailing_comments: Vec::new(),
7380 span: None,
7381 inferred_type: None,
7382 }))
7383 })
7384 .collect();
7385 ct.properties.insert(
7386 0,
7387 Expression::PartitionedByProperty(Box::new(
7388 crate::expressions::PartitionedByProperty {
7389 this: Box::new(Expression::Tuple(Box::new(
7390 crate::expressions::Tuple {
7391 expressions: name_exprs,
7392 },
7393 ))),
7394 },
7395 )),
7396 );
7397 }
7398 // For DuckDB and other targets, just drop the PARTITIONED BY (already retained above)
7399 }
7400
7401 // Note: Non-ColumnDef partitions (e.g., function expressions like MONTHS(y))
7402 // are handled by transform_create_table_properties which runs first
7403 }
7404
7405 // Strip LOCATION property for Presto/Trino (not supported)
7406 if matches!(
7407 target,
7408 DialectType::Presto | DialectType::Trino | DialectType::Athena
7409 ) {
7410 ct.properties
7411 .retain(|p| !matches!(p, Expression::LocationProperty(_)));
7412 }
7413
7414 // Strip table-level constraints for Spark/Hive/Databricks
7415 // Keep PRIMARY KEY and LIKE constraints but strip TSQL-specific modifiers; remove all others
7416 if matches!(
7417 target,
7418 DialectType::Spark | DialectType::Databricks | DialectType::Hive
7419 ) {
7420 ct.constraints.retain(|c| {
7421 matches!(
7422 c,
7423 crate::expressions::TableConstraint::PrimaryKey { .. }
7424 | crate::expressions::TableConstraint::Like { .. }
7425 )
7426 });
7427 for constraint in &mut ct.constraints {
7428 if let crate::expressions::TableConstraint::PrimaryKey {
7429 columns,
7430 modifiers,
7431 ..
7432 } = constraint
7433 {
7434 // Strip ASC/DESC from column names
7435 for col in columns.iter_mut() {
7436 if col.name.ends_with(" ASC") {
7437 col.name = col.name[..col.name.len() - 4].to_string();
7438 } else if col.name.ends_with(" DESC") {
7439 col.name = col.name[..col.name.len() - 5].to_string();
7440 }
7441 }
7442 // Strip TSQL-specific modifiers
7443 modifiers.clustered = None;
7444 modifiers.with_options.clear();
7445 modifiers.on_filegroup = None;
7446 }
7447 }
7448 }
7449
7450 // Databricks: IDENTITY columns with INT/INTEGER -> BIGINT
7451 if matches!(target, DialectType::Databricks) {
7452 for col in &mut ct.columns {
7453 if col.auto_increment {
7454 if matches!(col.data_type, crate::expressions::DataType::Int { .. }) {
7455 col.data_type = crate::expressions::DataType::BigInt { length: None };
7456 }
7457 }
7458 }
7459 }
7460
7461 // Spark/Databricks: INTEGER -> INT in column definitions
7462 // Python sqlglot always outputs INT for Spark/Databricks
7463 if matches!(target, DialectType::Spark | DialectType::Databricks) {
7464 for col in &mut ct.columns {
7465 if let crate::expressions::DataType::Int {
7466 integer_spelling, ..
7467 } = &mut col.data_type
7468 {
7469 *integer_spelling = false;
7470 }
7471 }
7472 }
7473
7474 // Strip explicit NULL constraints for Hive/Spark (B INTEGER NULL -> B INTEGER)
7475 if matches!(target, DialectType::Hive | DialectType::Spark) {
7476 for col in &mut ct.columns {
7477 // If nullable is explicitly true (NULL), change to None (omit it)
7478 if col.nullable == Some(true) {
7479 col.nullable = None;
7480 }
7481 // Also remove from constraints if stored there
7482 col.constraints
7483 .retain(|c| !matches!(c, crate::expressions::ColumnConstraint::Null));
7484 }
7485 }
7486
7487 // Strip TSQL ON filegroup for non-TSQL/Fabric targets
7488 if ct.on_property.is_some()
7489 && !matches!(target, DialectType::TSQL | DialectType::Fabric)
7490 {
7491 ct.on_property = None;
7492 }
7493
7494 // Snowflake: strip ARRAY type parameters (ARRAY<INT> -> ARRAY, ARRAY<ARRAY<INT>> -> ARRAY)
7495 // Snowflake doesn't support typed arrays in DDL
7496 if matches!(target, DialectType::Snowflake) {
7497 fn strip_array_type_params(dt: &mut crate::expressions::DataType) {
7498 if let crate::expressions::DataType::Array { .. } = dt {
7499 *dt = crate::expressions::DataType::Custom {
7500 name: "ARRAY".to_string(),
7501 };
7502 }
7503 }
7504 for col in &mut ct.columns {
7505 strip_array_type_params(&mut col.data_type);
7506 }
7507 }
7508
7509 // PostgreSQL target: ensure IDENTITY columns have NOT NULL
7510 // If NOT NULL was explicit in source (present in constraint_order), preserve original order.
7511 // If NOT NULL was not explicit, add it after IDENTITY (GENERATED BY DEFAULT AS IDENTITY NOT NULL).
7512 if matches!(target, DialectType::PostgreSQL) {
7513 for col in &mut ct.columns {
7514 if col.auto_increment && !col.constraint_order.is_empty() {
7515 use crate::expressions::ConstraintType;
7516 let has_explicit_not_null = col
7517 .constraint_order
7518 .iter()
7519 .any(|ct| *ct == ConstraintType::NotNull);
7520
7521 if has_explicit_not_null {
7522 // Source had explicit NOT NULL - preserve original order
7523 // Just ensure nullable is set
7524 if col.nullable != Some(false) {
7525 col.nullable = Some(false);
7526 }
7527 } else {
7528 // Source didn't have explicit NOT NULL - build order with
7529 // AutoIncrement + NotNull first, then remaining constraints
7530 let mut new_order = Vec::new();
7531 // Put AutoIncrement (IDENTITY) first, followed by synthetic NotNull
7532 new_order.push(ConstraintType::AutoIncrement);
7533 new_order.push(ConstraintType::NotNull);
7534 // Add remaining constraints in original order (except AutoIncrement)
7535 for ct_type in &col.constraint_order {
7536 if *ct_type != ConstraintType::AutoIncrement {
7537 new_order.push(ct_type.clone());
7538 }
7539 }
7540 col.constraint_order = new_order;
7541 col.nullable = Some(false);
7542 }
7543 }
7544 }
7545 }
7546
7547 Expression::CreateTable(ct)
7548 } else {
7549 expr
7550 };
7551
7552 // Handle CreateView column stripping for Presto/Trino target
7553 let expr = if let Expression::CreateView(mut cv) = expr {
7554 // Presto/Trino: drop column list when view has a SELECT body
7555 if matches!(target, DialectType::Presto | DialectType::Trino) && !cv.columns.is_empty()
7556 {
7557 if !matches!(&cv.query, Expression::Null(_)) {
7558 cv.columns.clear();
7559 }
7560 }
7561 Expression::CreateView(cv)
7562 } else {
7563 expr
7564 };
7565
7566 // Wrap bare VALUES in CTE bodies with SELECT * FROM (...) AS _values for generic/non-Presto targets
7567 let expr = if !matches!(
7568 target,
7569 DialectType::Presto | DialectType::Trino | DialectType::Athena
7570 ) {
7571 if let Expression::Select(mut select) = expr {
7572 if let Some(ref mut with) = select.with {
7573 for cte in &mut with.ctes {
7574 if let Expression::Values(ref vals) = cte.this {
7575 // Build: SELECT * FROM (VALUES ...) AS _values
7576 let values_subquery =
7577 Expression::Subquery(Box::new(crate::expressions::Subquery {
7578 this: Expression::Values(vals.clone()),
7579 alias: Some(Identifier::new("_values".to_string())),
7580 column_aliases: Vec::new(),
7581 alias_explicit_as: false,
7582 alias_keyword: None,
7583 order_by: None,
7584 limit: None,
7585 offset: None,
7586 distribute_by: None,
7587 sort_by: None,
7588 cluster_by: None,
7589 lateral: false,
7590 modifiers_inside: false,
7591 trailing_comments: Vec::new(),
7592 inferred_type: None,
7593 }));
7594 let mut new_select = crate::expressions::Select::new();
7595 new_select.expressions =
7596 vec![Expression::Star(crate::expressions::Star {
7597 table: None,
7598 except: None,
7599 replace: None,
7600 rename: None,
7601 trailing_comments: Vec::new(),
7602 span: None,
7603 })];
7604 new_select.from = Some(crate::expressions::From {
7605 expressions: vec![values_subquery],
7606 });
7607 cte.this = Expression::Select(Box::new(new_select));
7608 }
7609 }
7610 }
7611 Expression::Select(select)
7612 } else {
7613 expr
7614 }
7615 } else {
7616 expr
7617 };
7618
7619 // PostgreSQL CREATE INDEX: add NULLS FIRST to index columns that don't have nulls ordering
7620 let expr = if matches!(target, DialectType::PostgreSQL) {
7621 if let Expression::CreateIndex(mut ci) = expr {
7622 for col in &mut ci.columns {
7623 if col.nulls_first.is_none() {
7624 col.nulls_first = Some(true);
7625 }
7626 }
7627 Expression::CreateIndex(ci)
7628 } else {
7629 expr
7630 }
7631 } else {
7632 expr
7633 };
7634
7635 transform_recursive(expr, &|e| {
7636 // BigQuery CAST(ARRAY[STRUCT(...)] AS STRUCT_TYPE[]) -> DuckDB: convert unnamed Structs to ROW()
7637 // This converts auto-named struct literals {'_0': x, '_1': y} inside typed arrays to ROW(x, y)
7638 if matches!(source, DialectType::BigQuery) && matches!(target, DialectType::DuckDB) {
7639 if let Expression::Cast(ref c) = e {
7640 // Check if this is a CAST of an array to a struct array type
7641 let is_struct_array_cast =
7642 matches!(&c.to, crate::expressions::DataType::Array { .. });
7643 if is_struct_array_cast {
7644 let has_auto_named_structs = match &c.this {
7645 Expression::Array(arr) => arr.expressions.iter().any(|elem| {
7646 if let Expression::Struct(s) = elem {
7647 s.fields.iter().all(|(name, _)| {
7648 name.as_ref().map_or(true, |n| {
7649 n.starts_with('_') && n[1..].parse::<usize>().is_ok()
7650 })
7651 })
7652 } else {
7653 false
7654 }
7655 }),
7656 Expression::ArrayFunc(arr) => arr.expressions.iter().any(|elem| {
7657 if let Expression::Struct(s) = elem {
7658 s.fields.iter().all(|(name, _)| {
7659 name.as_ref().map_or(true, |n| {
7660 n.starts_with('_') && n[1..].parse::<usize>().is_ok()
7661 })
7662 })
7663 } else {
7664 false
7665 }
7666 }),
7667 _ => false,
7668 };
7669 if has_auto_named_structs {
7670 let convert_struct_to_row = |elem: Expression| -> Expression {
7671 if let Expression::Struct(s) = elem {
7672 let row_args: Vec<Expression> =
7673 s.fields.into_iter().map(|(_, v)| v).collect();
7674 Expression::Function(Box::new(Function::new(
7675 "ROW".to_string(),
7676 row_args,
7677 )))
7678 } else {
7679 elem
7680 }
7681 };
7682 let mut c_clone = c.as_ref().clone();
7683 match &mut c_clone.this {
7684 Expression::Array(arr) => {
7685 arr.expressions = arr
7686 .expressions
7687 .drain(..)
7688 .map(convert_struct_to_row)
7689 .collect();
7690 }
7691 Expression::ArrayFunc(arr) => {
7692 arr.expressions = arr
7693 .expressions
7694 .drain(..)
7695 .map(convert_struct_to_row)
7696 .collect();
7697 }
7698 _ => {}
7699 }
7700 return Ok(Expression::Cast(Box::new(c_clone)));
7701 }
7702 }
7703 }
7704 }
7705
7706 // BigQuery SELECT AS STRUCT -> DuckDB struct literal {'key': value, ...}
7707 if matches!(source, DialectType::BigQuery) && matches!(target, DialectType::DuckDB) {
7708 if let Expression::Select(ref sel) = e {
7709 if sel.kind.as_deref() == Some("STRUCT") {
7710 let mut fields = Vec::new();
7711 for expr in &sel.expressions {
7712 match expr {
7713 Expression::Alias(a) => {
7714 fields.push((Some(a.alias.name.clone()), a.this.clone()));
7715 }
7716 Expression::Column(c) => {
7717 fields.push((Some(c.name.name.clone()), expr.clone()));
7718 }
7719 _ => {
7720 fields.push((None, expr.clone()));
7721 }
7722 }
7723 }
7724 let struct_lit =
7725 Expression::Struct(Box::new(crate::expressions::Struct { fields }));
7726 let mut new_select = sel.as_ref().clone();
7727 new_select.kind = None;
7728 new_select.expressions = vec![struct_lit];
7729 return Ok(Expression::Select(Box::new(new_select)));
7730 }
7731 }
7732 }
7733
7734 // Convert @variable -> ${variable} for Spark/Hive/Databricks
7735 if matches!(source, DialectType::TSQL | DialectType::Fabric)
7736 && matches!(
7737 target,
7738 DialectType::Spark | DialectType::Databricks | DialectType::Hive
7739 )
7740 {
7741 if let Expression::Parameter(ref p) = e {
7742 if p.style == crate::expressions::ParameterStyle::At {
7743 if let Some(ref name) = p.name {
7744 return Ok(Expression::Parameter(Box::new(
7745 crate::expressions::Parameter {
7746 name: Some(name.clone()),
7747 index: p.index,
7748 style: crate::expressions::ParameterStyle::DollarBrace,
7749 quoted: p.quoted,
7750 string_quoted: p.string_quoted,
7751 expression: None,
7752 },
7753 )));
7754 }
7755 }
7756 }
7757 // Also handle Column("@x") -> Parameter("x", DollarBrace) for TSQL vars
7758 if let Expression::Column(ref col) = e {
7759 if col.name.name.starts_with('@') && col.table.is_none() {
7760 let var_name = col.name.name.trim_start_matches('@').to_string();
7761 return Ok(Expression::Parameter(Box::new(
7762 crate::expressions::Parameter {
7763 name: Some(var_name),
7764 index: None,
7765 style: crate::expressions::ParameterStyle::DollarBrace,
7766 quoted: false,
7767 string_quoted: false,
7768 expression: None,
7769 },
7770 )));
7771 }
7772 }
7773 }
7774
7775 // Convert @variable -> variable in SET statements for Spark/Databricks
7776 if matches!(source, DialectType::TSQL | DialectType::Fabric)
7777 && matches!(target, DialectType::Spark | DialectType::Databricks)
7778 {
7779 if let Expression::SetStatement(ref s) = e {
7780 let mut new_items = s.items.clone();
7781 let mut changed = false;
7782 for item in &mut new_items {
7783 // Strip @ from the SET name (Parameter style)
7784 if let Expression::Parameter(ref p) = item.name {
7785 if p.style == crate::expressions::ParameterStyle::At {
7786 if let Some(ref name) = p.name {
7787 item.name = Expression::Identifier(Identifier::new(name));
7788 changed = true;
7789 }
7790 }
7791 }
7792 // Strip @ from the SET name (Identifier style - SET parser)
7793 if let Expression::Identifier(ref id) = item.name {
7794 if id.name.starts_with('@') {
7795 let var_name = id.name.trim_start_matches('@').to_string();
7796 item.name = Expression::Identifier(Identifier::new(&var_name));
7797 changed = true;
7798 }
7799 }
7800 // Strip @ from the SET name (Column style - alternative parsing)
7801 if let Expression::Column(ref col) = item.name {
7802 if col.name.name.starts_with('@') && col.table.is_none() {
7803 let var_name = col.name.name.trim_start_matches('@').to_string();
7804 item.name = Expression::Identifier(Identifier::new(&var_name));
7805 changed = true;
7806 }
7807 }
7808 }
7809 if changed {
7810 let mut new_set = (**s).clone();
7811 new_set.items = new_items;
7812 return Ok(Expression::SetStatement(Box::new(new_set)));
7813 }
7814 }
7815 }
7816
7817 // Strip NOLOCK hint for non-TSQL targets
7818 if matches!(source, DialectType::TSQL | DialectType::Fabric)
7819 && !matches!(target, DialectType::TSQL | DialectType::Fabric)
7820 {
7821 if let Expression::Table(ref tr) = e {
7822 if !tr.hints.is_empty() {
7823 let mut new_tr = tr.clone();
7824 new_tr.hints.clear();
7825 return Ok(Expression::Table(new_tr));
7826 }
7827 }
7828 }
7829
7830 // Snowflake: TRUE IS TRUE -> TRUE, FALSE IS FALSE -> FALSE
7831 // Snowflake simplifies IS TRUE/IS FALSE on boolean literals
7832 if matches!(target, DialectType::Snowflake) {
7833 if let Expression::IsTrue(ref itf) = e {
7834 if let Expression::Boolean(ref b) = itf.this {
7835 if !itf.not {
7836 return Ok(Expression::Boolean(crate::expressions::BooleanLiteral {
7837 value: b.value,
7838 }));
7839 } else {
7840 return Ok(Expression::Boolean(crate::expressions::BooleanLiteral {
7841 value: !b.value,
7842 }));
7843 }
7844 }
7845 }
7846 if let Expression::IsFalse(ref itf) = e {
7847 if let Expression::Boolean(ref b) = itf.this {
7848 if !itf.not {
7849 return Ok(Expression::Boolean(crate::expressions::BooleanLiteral {
7850 value: !b.value,
7851 }));
7852 } else {
7853 return Ok(Expression::Boolean(crate::expressions::BooleanLiteral {
7854 value: b.value,
7855 }));
7856 }
7857 }
7858 }
7859 }
7860
7861 // BigQuery: split dotted backtick identifiers in table names
7862 // e.g., `a.b.c` -> "a"."b"."c" when source is BigQuery and target is not BigQuery
7863 if matches!(source, DialectType::BigQuery) && !matches!(target, DialectType::BigQuery) {
7864 if let Expression::CreateTable(ref ct) = e {
7865 let mut changed = false;
7866 let mut new_ct = ct.clone();
7867 // Split the table name
7868 if ct.name.schema.is_none() && ct.name.name.name.contains('.') {
7869 let parts: Vec<&str> = ct.name.name.name.split('.').collect();
7870 // Use quoted identifiers when the original was quoted (backtick in BigQuery)
7871 let was_quoted = ct.name.name.quoted;
7872 let mk_id = |s: &str| {
7873 if was_quoted {
7874 Identifier::quoted(s)
7875 } else {
7876 Identifier::new(s)
7877 }
7878 };
7879 if parts.len() == 3 {
7880 new_ct.name.catalog = Some(mk_id(parts[0]));
7881 new_ct.name.schema = Some(mk_id(parts[1]));
7882 new_ct.name.name = mk_id(parts[2]);
7883 changed = true;
7884 } else if parts.len() == 2 {
7885 new_ct.name.schema = Some(mk_id(parts[0]));
7886 new_ct.name.name = mk_id(parts[1]);
7887 changed = true;
7888 }
7889 }
7890 // Split the clone source name
7891 if let Some(ref clone_src) = ct.clone_source {
7892 if clone_src.schema.is_none() && clone_src.name.name.contains('.') {
7893 let parts: Vec<&str> = clone_src.name.name.split('.').collect();
7894 let was_quoted = clone_src.name.quoted;
7895 let mk_id = |s: &str| {
7896 if was_quoted {
7897 Identifier::quoted(s)
7898 } else {
7899 Identifier::new(s)
7900 }
7901 };
7902 let mut new_src = clone_src.clone();
7903 if parts.len() == 3 {
7904 new_src.catalog = Some(mk_id(parts[0]));
7905 new_src.schema = Some(mk_id(parts[1]));
7906 new_src.name = mk_id(parts[2]);
7907 new_ct.clone_source = Some(new_src);
7908 changed = true;
7909 } else if parts.len() == 2 {
7910 new_src.schema = Some(mk_id(parts[0]));
7911 new_src.name = mk_id(parts[1]);
7912 new_ct.clone_source = Some(new_src);
7913 changed = true;
7914 }
7915 }
7916 }
7917 if changed {
7918 return Ok(Expression::CreateTable(new_ct));
7919 }
7920 }
7921 }
7922
7923 // BigQuery array subscript: a[1], b[OFFSET(1)], c[ORDINAL(1)], d[SAFE_OFFSET(1)], e[SAFE_ORDINAL(1)]
7924 // -> DuckDB/Presto: convert 0-based to 1-based, handle SAFE_* -> ELEMENT_AT for Presto
7925 if matches!(source, DialectType::BigQuery)
7926 && matches!(
7927 target,
7928 DialectType::DuckDB
7929 | DialectType::Presto
7930 | DialectType::Trino
7931 | DialectType::Athena
7932 )
7933 {
7934 if let Expression::Subscript(ref sub) = e {
7935 let (new_index, is_safe) = match &sub.index {
7936 // a[1] -> a[1+1] = a[2] (plain index is 0-based in BQ)
7937 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)) => {
7938 let Literal::Number(n) = lit.as_ref() else {
7939 unreachable!()
7940 };
7941 if let Ok(val) = n.parse::<i64>() {
7942 (
7943 Some(Expression::Literal(Box::new(Literal::Number(
7944 (val + 1).to_string(),
7945 )))),
7946 false,
7947 )
7948 } else {
7949 (None, false)
7950 }
7951 }
7952 // OFFSET(n) -> n+1 (0-based)
7953 Expression::Function(ref f)
7954 if f.name.eq_ignore_ascii_case("OFFSET") && f.args.len() == 1 =>
7955 {
7956 if let Expression::Literal(lit) = &f.args[0] {
7957 if let Literal::Number(n) = lit.as_ref() {
7958 if let Ok(val) = n.parse::<i64>() {
7959 (
7960 Some(Expression::Literal(Box::new(Literal::Number(
7961 (val + 1).to_string(),
7962 )))),
7963 false,
7964 )
7965 } else {
7966 (
7967 Some(Expression::Add(Box::new(
7968 crate::expressions::BinaryOp::new(
7969 f.args[0].clone(),
7970 Expression::number(1),
7971 ),
7972 ))),
7973 false,
7974 )
7975 }
7976 } else {
7977 (None, false)
7978 }
7979 } else {
7980 (
7981 Some(Expression::Add(Box::new(
7982 crate::expressions::BinaryOp::new(
7983 f.args[0].clone(),
7984 Expression::number(1),
7985 ),
7986 ))),
7987 false,
7988 )
7989 }
7990 }
7991 // ORDINAL(n) -> n (already 1-based)
7992 Expression::Function(ref f)
7993 if f.name.eq_ignore_ascii_case("ORDINAL") && f.args.len() == 1 =>
7994 {
7995 (Some(f.args[0].clone()), false)
7996 }
7997 // SAFE_OFFSET(n) -> n+1 (0-based, safe)
7998 Expression::Function(ref f)
7999 if f.name.eq_ignore_ascii_case("SAFE_OFFSET") && f.args.len() == 1 =>
8000 {
8001 if let Expression::Literal(lit) = &f.args[0] {
8002 if let Literal::Number(n) = lit.as_ref() {
8003 if let Ok(val) = n.parse::<i64>() {
8004 (
8005 Some(Expression::Literal(Box::new(Literal::Number(
8006 (val + 1).to_string(),
8007 )))),
8008 true,
8009 )
8010 } else {
8011 (
8012 Some(Expression::Add(Box::new(
8013 crate::expressions::BinaryOp::new(
8014 f.args[0].clone(),
8015 Expression::number(1),
8016 ),
8017 ))),
8018 true,
8019 )
8020 }
8021 } else {
8022 (None, false)
8023 }
8024 } else {
8025 (
8026 Some(Expression::Add(Box::new(
8027 crate::expressions::BinaryOp::new(
8028 f.args[0].clone(),
8029 Expression::number(1),
8030 ),
8031 ))),
8032 true,
8033 )
8034 }
8035 }
8036 // SAFE_ORDINAL(n) -> n (already 1-based, safe)
8037 Expression::Function(ref f)
8038 if f.name.eq_ignore_ascii_case("SAFE_ORDINAL") && f.args.len() == 1 =>
8039 {
8040 (Some(f.args[0].clone()), true)
8041 }
8042 _ => (None, false),
8043 };
8044 if let Some(idx) = new_index {
8045 if is_safe
8046 && matches!(
8047 target,
8048 DialectType::Presto | DialectType::Trino | DialectType::Athena
8049 )
8050 {
8051 // Presto: SAFE_OFFSET/SAFE_ORDINAL -> ELEMENT_AT(arr, idx)
8052 return Ok(Expression::Function(Box::new(Function::new(
8053 "ELEMENT_AT".to_string(),
8054 vec![sub.this.clone(), idx],
8055 ))));
8056 } else {
8057 // DuckDB or non-safe: just use subscript with converted index
8058 return Ok(Expression::Subscript(Box::new(
8059 crate::expressions::Subscript {
8060 this: sub.this.clone(),
8061 index: idx,
8062 },
8063 )));
8064 }
8065 }
8066 }
8067 }
8068
8069 // BigQuery LENGTH(x) -> DuckDB CASE TYPEOF(x) WHEN 'BLOB' THEN OCTET_LENGTH(...) ELSE LENGTH(...) END
8070 if matches!(source, DialectType::BigQuery) && matches!(target, DialectType::DuckDB) {
8071 if let Expression::Length(ref uf) = e {
8072 let arg = uf.this.clone();
8073 let typeof_func = Expression::Function(Box::new(Function::new(
8074 "TYPEOF".to_string(),
8075 vec![arg.clone()],
8076 )));
8077 let blob_cast = Expression::Cast(Box::new(Cast {
8078 this: arg.clone(),
8079 to: DataType::VarBinary { length: None },
8080 trailing_comments: vec![],
8081 double_colon_syntax: false,
8082 format: None,
8083 default: None,
8084 inferred_type: None,
8085 }));
8086 let octet_length = Expression::Function(Box::new(Function::new(
8087 "OCTET_LENGTH".to_string(),
8088 vec![blob_cast],
8089 )));
8090 let text_cast = Expression::Cast(Box::new(Cast {
8091 this: arg,
8092 to: DataType::Text,
8093 trailing_comments: vec![],
8094 double_colon_syntax: false,
8095 format: None,
8096 default: None,
8097 inferred_type: None,
8098 }));
8099 let length_text = Expression::Length(Box::new(crate::expressions::UnaryFunc {
8100 this: text_cast,
8101 original_name: None,
8102 inferred_type: None,
8103 }));
8104 return Ok(Expression::Case(Box::new(Case {
8105 operand: Some(typeof_func),
8106 whens: vec![(
8107 Expression::Literal(Box::new(Literal::String("BLOB".to_string()))),
8108 octet_length,
8109 )],
8110 else_: Some(length_text),
8111 comments: Vec::new(),
8112 inferred_type: None,
8113 })));
8114 }
8115 }
8116
8117 // BigQuery UNNEST alias handling (only for non-BigQuery sources):
8118 // UNNEST(...) AS x -> UNNEST(...) (drop unused table alias)
8119 // UNNEST(...) AS x(y) -> UNNEST(...) AS y (use column alias as main alias)
8120 if matches!(target, DialectType::BigQuery) && !matches!(source, DialectType::BigQuery) {
8121 if let Expression::Alias(ref a) = e {
8122 if matches!(&a.this, Expression::Unnest(_)) {
8123 if a.column_aliases.is_empty() {
8124 // Drop the entire alias, return just the UNNEST expression
8125 return Ok(a.this.clone());
8126 } else {
8127 // Use first column alias as the main alias
8128 let mut new_alias = a.as_ref().clone();
8129 new_alias.alias = a.column_aliases[0].clone();
8130 new_alias.column_aliases.clear();
8131 return Ok(Expression::Alias(Box::new(new_alias)));
8132 }
8133 }
8134 }
8135 }
8136
8137 // BigQuery IN UNNEST(expr) -> IN (SELECT UNNEST/EXPLODE(expr)) for non-BigQuery targets
8138 if matches!(source, DialectType::BigQuery) && !matches!(target, DialectType::BigQuery) {
8139 if let Expression::In(ref in_expr) = e {
8140 if let Some(ref unnest_inner) = in_expr.unnest {
8141 // Build the function call for the target dialect
8142 let func_expr = if matches!(
8143 target,
8144 DialectType::Hive | DialectType::Spark | DialectType::Databricks
8145 ) {
8146 // Use EXPLODE for Hive/Spark
8147 Expression::Function(Box::new(Function::new(
8148 "EXPLODE".to_string(),
8149 vec![*unnest_inner.clone()],
8150 )))
8151 } else {
8152 // Use UNNEST for Presto/Trino/DuckDB/etc.
8153 Expression::Unnest(Box::new(crate::expressions::UnnestFunc {
8154 this: *unnest_inner.clone(),
8155 expressions: Vec::new(),
8156 with_ordinality: false,
8157 alias: None,
8158 offset_alias: None,
8159 }))
8160 };
8161
8162 // Wrap in SELECT
8163 let mut inner_select = crate::expressions::Select::new();
8164 inner_select.expressions = vec![func_expr];
8165
8166 let subquery_expr = Expression::Select(Box::new(inner_select));
8167
8168 return Ok(Expression::In(Box::new(crate::expressions::In {
8169 this: in_expr.this.clone(),
8170 expressions: Vec::new(),
8171 query: Some(subquery_expr),
8172 not: in_expr.not,
8173 global: in_expr.global,
8174 unnest: None,
8175 is_field: false,
8176 })));
8177 }
8178 }
8179 }
8180
8181 // SQLite: GENERATE_SERIES AS t(i) -> (SELECT value AS i FROM GENERATE_SERIES(...)) AS t
8182 // This handles the subquery wrapping for RANGE -> GENERATE_SERIES in FROM context
8183 if matches!(target, DialectType::SQLite) && matches!(source, DialectType::DuckDB) {
8184 if let Expression::Alias(ref a) = e {
8185 if let Expression::Function(ref f) = a.this {
8186 if f.name.eq_ignore_ascii_case("GENERATE_SERIES")
8187 && !a.column_aliases.is_empty()
8188 {
8189 // Build: (SELECT value AS col_alias FROM GENERATE_SERIES(start, end)) AS table_alias
8190 let col_alias = a.column_aliases[0].clone();
8191 let mut inner_select = crate::expressions::Select::new();
8192 inner_select.expressions =
8193 vec![Expression::Alias(Box::new(crate::expressions::Alias::new(
8194 Expression::Identifier(Identifier::new("value".to_string())),
8195 col_alias,
8196 )))];
8197 inner_select.from = Some(crate::expressions::From {
8198 expressions: vec![a.this.clone()],
8199 });
8200 let subquery =
8201 Expression::Subquery(Box::new(crate::expressions::Subquery {
8202 this: Expression::Select(Box::new(inner_select)),
8203 alias: Some(a.alias.clone()),
8204 column_aliases: Vec::new(),
8205 alias_explicit_as: false,
8206 alias_keyword: None,
8207 order_by: None,
8208 limit: None,
8209 offset: None,
8210 lateral: false,
8211 modifiers_inside: false,
8212 trailing_comments: Vec::new(),
8213 distribute_by: None,
8214 sort_by: None,
8215 cluster_by: None,
8216 inferred_type: None,
8217 }));
8218 return Ok(subquery);
8219 }
8220 }
8221 }
8222 }
8223
8224 // BigQuery implicit UNNEST: comma-join on array path -> CROSS JOIN UNNEST
8225 // e.g., SELECT results FROM Coordinates, Coordinates.position AS results
8226 // -> SELECT results FROM Coordinates CROSS JOIN UNNEST(Coordinates.position) AS results
8227 if matches!(source, DialectType::BigQuery) {
8228 if let Expression::Select(ref s) = e {
8229 if let Some(ref from) = s.from {
8230 if from.expressions.len() >= 2 {
8231 // Collect table names from first expression
8232 let first_tables: Vec<String> = from
8233 .expressions
8234 .iter()
8235 .take(1)
8236 .filter_map(|expr| {
8237 if let Expression::Table(t) = expr {
8238 Some(t.name.name.to_ascii_lowercase())
8239 } else {
8240 None
8241 }
8242 })
8243 .collect();
8244
8245 // Check if any subsequent FROM expressions are schema-qualified with a matching table name
8246 // or have a dotted name matching a table
8247 let mut needs_rewrite = false;
8248 for expr in from.expressions.iter().skip(1) {
8249 if let Expression::Table(t) = expr {
8250 if let Some(ref schema) = t.schema {
8251 if first_tables.contains(&schema.name.to_ascii_lowercase())
8252 {
8253 needs_rewrite = true;
8254 break;
8255 }
8256 }
8257 // Also check dotted names in quoted identifiers (e.g., `Coordinates.position`)
8258 if t.schema.is_none() && t.name.name.contains('.') {
8259 let parts: Vec<&str> = t.name.name.split('.').collect();
8260 if parts.len() >= 2
8261 && first_tables.contains(&parts[0].to_ascii_lowercase())
8262 {
8263 needs_rewrite = true;
8264 break;
8265 }
8266 }
8267 }
8268 }
8269
8270 if needs_rewrite {
8271 let mut new_select = s.clone();
8272 let mut new_from_exprs = vec![from.expressions[0].clone()];
8273 let mut new_joins = s.joins.clone();
8274
8275 for expr in from.expressions.iter().skip(1) {
8276 if let Expression::Table(ref t) = expr {
8277 if let Some(ref schema) = t.schema {
8278 if first_tables
8279 .contains(&schema.name.to_ascii_lowercase())
8280 {
8281 // This is an array path reference, convert to CROSS JOIN UNNEST
8282 let col_expr = Expression::Column(Box::new(
8283 crate::expressions::Column {
8284 name: t.name.clone(),
8285 table: Some(schema.clone()),
8286 join_mark: false,
8287 trailing_comments: vec![],
8288 span: None,
8289 inferred_type: None,
8290 },
8291 ));
8292 let unnest_expr = Expression::Unnest(Box::new(
8293 crate::expressions::UnnestFunc {
8294 this: col_expr,
8295 expressions: Vec::new(),
8296 with_ordinality: false,
8297 alias: None,
8298 offset_alias: None,
8299 },
8300 ));
8301 let join_this = if let Some(ref alias) = t.alias {
8302 if matches!(
8303 target,
8304 DialectType::Presto
8305 | DialectType::Trino
8306 | DialectType::Athena
8307 ) {
8308 // Presto: UNNEST(x) AS _t0(results)
8309 Expression::Alias(Box::new(
8310 crate::expressions::Alias {
8311 this: unnest_expr,
8312 alias: Identifier::new("_t0"),
8313 column_aliases: vec![alias.clone()],
8314 alias_explicit_as: false,
8315 alias_keyword: None,
8316 pre_alias_comments: vec![],
8317 trailing_comments: vec![],
8318 inferred_type: None,
8319 },
8320 ))
8321 } else {
8322 // BigQuery: UNNEST(x) AS results
8323 Expression::Alias(Box::new(
8324 crate::expressions::Alias {
8325 this: unnest_expr,
8326 alias: alias.clone(),
8327 column_aliases: vec![],
8328 alias_explicit_as: false,
8329 alias_keyword: None,
8330 pre_alias_comments: vec![],
8331 trailing_comments: vec![],
8332 inferred_type: None,
8333 },
8334 ))
8335 }
8336 } else {
8337 unnest_expr
8338 };
8339 new_joins.push(crate::expressions::Join {
8340 kind: crate::expressions::JoinKind::Cross,
8341 this: join_this,
8342 on: None,
8343 using: Vec::new(),
8344 use_inner_keyword: false,
8345 use_outer_keyword: false,
8346 deferred_condition: false,
8347 join_hint: None,
8348 match_condition: None,
8349 pivots: Vec::new(),
8350 comments: Vec::new(),
8351 nesting_group: 0,
8352 directed: false,
8353 });
8354 } else {
8355 new_from_exprs.push(expr.clone());
8356 }
8357 } else if t.schema.is_none() && t.name.name.contains('.') {
8358 // Dotted name in quoted identifier: `Coordinates.position`
8359 let parts: Vec<&str> = t.name.name.split('.').collect();
8360 if parts.len() >= 2
8361 && first_tables
8362 .contains(&parts[0].to_ascii_lowercase())
8363 {
8364 let join_this =
8365 if matches!(target, DialectType::BigQuery) {
8366 // BigQuery: keep as single quoted identifier, just convert comma -> CROSS JOIN
8367 Expression::Table(t.clone())
8368 } else {
8369 // Other targets: split into "schema"."name"
8370 let mut new_t = t.clone();
8371 new_t.schema =
8372 Some(Identifier::quoted(parts[0]));
8373 new_t.name = Identifier::quoted(parts[1]);
8374 Expression::Table(new_t)
8375 };
8376 new_joins.push(crate::expressions::Join {
8377 kind: crate::expressions::JoinKind::Cross,
8378 this: join_this,
8379 on: None,
8380 using: Vec::new(),
8381 use_inner_keyword: false,
8382 use_outer_keyword: false,
8383 deferred_condition: false,
8384 join_hint: None,
8385 match_condition: None,
8386 pivots: Vec::new(),
8387 comments: Vec::new(),
8388 nesting_group: 0,
8389 directed: false,
8390 });
8391 } else {
8392 new_from_exprs.push(expr.clone());
8393 }
8394 } else {
8395 new_from_exprs.push(expr.clone());
8396 }
8397 } else {
8398 new_from_exprs.push(expr.clone());
8399 }
8400 }
8401
8402 new_select.from = Some(crate::expressions::From {
8403 expressions: new_from_exprs,
8404 ..from.clone()
8405 });
8406 new_select.joins = new_joins;
8407 return Ok(Expression::Select(new_select));
8408 }
8409 }
8410 }
8411 }
8412 }
8413
8414 // CROSS JOIN UNNEST -> LATERAL VIEW EXPLODE for Hive/Spark
8415 if matches!(
8416 target,
8417 DialectType::Hive | DialectType::Spark | DialectType::Databricks
8418 ) {
8419 if let Expression::Select(ref s) = e {
8420 // Check if any joins are CROSS JOIN with UNNEST/EXPLODE
8421 let is_unnest_or_explode_expr = |expr: &Expression| -> bool {
8422 matches!(expr, Expression::Unnest(_))
8423 || matches!(expr, Expression::Function(f) if f.name.eq_ignore_ascii_case("EXPLODE"))
8424 };
8425 let has_unnest_join = s.joins.iter().any(|j| {
8426 j.kind == crate::expressions::JoinKind::Cross && (
8427 matches!(&j.this, Expression::Alias(a) if is_unnest_or_explode_expr(&a.this))
8428 || is_unnest_or_explode_expr(&j.this)
8429 )
8430 });
8431 if has_unnest_join {
8432 let mut select = s.clone();
8433 let mut new_joins = Vec::new();
8434 for join in select.joins.drain(..) {
8435 if join.kind == crate::expressions::JoinKind::Cross {
8436 // Extract the UNNEST/EXPLODE from the join
8437 let (func_expr, table_alias, col_aliases) = match &join.this {
8438 Expression::Alias(a) => {
8439 let ta = if a.alias.is_empty() {
8440 None
8441 } else {
8442 Some(a.alias.clone())
8443 };
8444 let cas = a.column_aliases.clone();
8445 match &a.this {
8446 Expression::Unnest(u) => {
8447 // Multi-arg UNNEST(y, z) -> INLINE(ARRAYS_ZIP(y, z))
8448 if !u.expressions.is_empty() {
8449 let mut all_args = vec![u.this.clone()];
8450 all_args.extend(u.expressions.clone());
8451 let arrays_zip =
8452 Expression::Function(Box::new(
8453 crate::expressions::Function::new(
8454 "ARRAYS_ZIP".to_string(),
8455 all_args,
8456 ),
8457 ));
8458 let inline = Expression::Function(Box::new(
8459 crate::expressions::Function::new(
8460 "INLINE".to_string(),
8461 vec![arrays_zip],
8462 ),
8463 ));
8464 (Some(inline), ta, a.column_aliases.clone())
8465 } else {
8466 // Convert UNNEST(x) to EXPLODE(x) or POSEXPLODE(x)
8467 let func_name = if u.with_ordinality {
8468 "POSEXPLODE"
8469 } else {
8470 "EXPLODE"
8471 };
8472 let explode = Expression::Function(Box::new(
8473 crate::expressions::Function::new(
8474 func_name.to_string(),
8475 vec![u.this.clone()],
8476 ),
8477 ));
8478 // For POSEXPLODE, add 'pos' to column aliases
8479 let cas = if u.with_ordinality {
8480 let mut pos_aliases =
8481 vec![Identifier::new(
8482 "pos".to_string(),
8483 )];
8484 pos_aliases
8485 .extend(a.column_aliases.clone());
8486 pos_aliases
8487 } else {
8488 a.column_aliases.clone()
8489 };
8490 (Some(explode), ta, cas)
8491 }
8492 }
8493 Expression::Function(f)
8494 if f.name.eq_ignore_ascii_case("EXPLODE") =>
8495 {
8496 (Some(Expression::Function(f.clone())), ta, cas)
8497 }
8498 _ => (None, None, Vec::new()),
8499 }
8500 }
8501 Expression::Unnest(u) => {
8502 let func_name = if u.with_ordinality {
8503 "POSEXPLODE"
8504 } else {
8505 "EXPLODE"
8506 };
8507 let explode = Expression::Function(Box::new(
8508 crate::expressions::Function::new(
8509 func_name.to_string(),
8510 vec![u.this.clone()],
8511 ),
8512 ));
8513 let ta = u.alias.clone();
8514 let col_aliases = if u.with_ordinality {
8515 vec![Identifier::new("pos".to_string())]
8516 } else {
8517 Vec::new()
8518 };
8519 (Some(explode), ta, col_aliases)
8520 }
8521 _ => (None, None, Vec::new()),
8522 };
8523 if let Some(func) = func_expr {
8524 select.lateral_views.push(crate::expressions::LateralView {
8525 this: func,
8526 table_alias,
8527 column_aliases: col_aliases,
8528 outer: false,
8529 });
8530 } else {
8531 new_joins.push(join);
8532 }
8533 } else {
8534 new_joins.push(join);
8535 }
8536 }
8537 select.joins = new_joins;
8538 return Ok(Expression::Select(select));
8539 }
8540 }
8541 }
8542
8543 // UNNEST expansion: DuckDB SELECT UNNEST(arr) in SELECT list -> expanded query
8544 // for BigQuery, Presto/Trino, Snowflake
8545 if matches!(source, DialectType::DuckDB | DialectType::PostgreSQL)
8546 && matches!(
8547 target,
8548 DialectType::BigQuery
8549 | DialectType::Presto
8550 | DialectType::Trino
8551 | DialectType::Snowflake
8552 )
8553 {
8554 if let Expression::Select(ref s) = e {
8555 // Check if any SELECT expressions contain UNNEST
8556 // Note: UNNEST can appear as Expression::Unnest OR Expression::Function("UNNEST")
8557 let has_unnest_in_select = s.expressions.iter().any(|expr| {
8558 fn contains_unnest(e: &Expression) -> bool {
8559 match e {
8560 Expression::Unnest(_) => true,
8561 Expression::Function(f)
8562 if f.name.eq_ignore_ascii_case("UNNEST") =>
8563 {
8564 true
8565 }
8566 Expression::Alias(a) => contains_unnest(&a.this),
8567 Expression::Add(op)
8568 | Expression::Sub(op)
8569 | Expression::Mul(op)
8570 | Expression::Div(op) => {
8571 contains_unnest(&op.left) || contains_unnest(&op.right)
8572 }
8573 _ => false,
8574 }
8575 }
8576 contains_unnest(expr)
8577 });
8578
8579 if has_unnest_in_select {
8580 let rewritten = Self::rewrite_unnest_expansion(s, target);
8581 if let Some(new_select) = rewritten {
8582 return Ok(Expression::Select(Box::new(new_select)));
8583 }
8584 }
8585 }
8586 }
8587
8588 // BigQuery -> PostgreSQL: convert escape sequences in string literals to actual characters
8589 // BigQuery '\n' -> PostgreSQL literal newline in string
8590 if matches!(source, DialectType::BigQuery) && matches!(target, DialectType::PostgreSQL)
8591 {
8592 if let Expression::Literal(ref lit) = e {
8593 if let Literal::String(ref s) = lit.as_ref() {
8594 if s.contains("\\n")
8595 || s.contains("\\t")
8596 || s.contains("\\r")
8597 || s.contains("\\\\")
8598 {
8599 let converted = s
8600 .replace("\\n", "\n")
8601 .replace("\\t", "\t")
8602 .replace("\\r", "\r")
8603 .replace("\\\\", "\\");
8604 return Ok(Expression::Literal(Box::new(Literal::String(converted))));
8605 }
8606 }
8607 }
8608 }
8609
8610 // Cross-dialect: convert Literal::Timestamp to target-specific CAST form
8611 // when source != target (identity tests keep the Literal::Timestamp for native handling)
8612 if source != target {
8613 if let Expression::Literal(ref lit) = e {
8614 if let Literal::Timestamp(ref s) = lit.as_ref() {
8615 let s = s.clone();
8616 // MySQL: TIMESTAMP handling depends on source dialect
8617 // BigQuery TIMESTAMP is timezone-aware -> TIMESTAMP() function in MySQL
8618 // Other sources' TIMESTAMP is non-timezone -> CAST('x' AS DATETIME) in MySQL
8619 if matches!(target, DialectType::MySQL) {
8620 if matches!(source, DialectType::BigQuery) {
8621 // BigQuery TIMESTAMP is timezone-aware -> MySQL TIMESTAMP() function
8622 return Ok(Expression::Function(Box::new(Function::new(
8623 "TIMESTAMP".to_string(),
8624 vec![Expression::Literal(Box::new(Literal::String(s)))],
8625 ))));
8626 } else {
8627 // Non-timezone TIMESTAMP -> CAST('x' AS DATETIME) in MySQL
8628 return Ok(Expression::Cast(Box::new(Cast {
8629 this: Expression::Literal(Box::new(Literal::String(s))),
8630 to: DataType::Custom {
8631 name: "DATETIME".to_string(),
8632 },
8633 trailing_comments: Vec::new(),
8634 double_colon_syntax: false,
8635 format: None,
8636 default: None,
8637 inferred_type: None,
8638 })));
8639 }
8640 }
8641 let dt = match target {
8642 DialectType::BigQuery | DialectType::StarRocks => DataType::Custom {
8643 name: "DATETIME".to_string(),
8644 },
8645 DialectType::Snowflake => {
8646 // BigQuery TIMESTAMP is timezone-aware -> use TIMESTAMPTZ for Snowflake
8647 if matches!(source, DialectType::BigQuery) {
8648 DataType::Custom {
8649 name: "TIMESTAMPTZ".to_string(),
8650 }
8651 } else if matches!(
8652 source,
8653 DialectType::PostgreSQL
8654 | DialectType::Redshift
8655 | DialectType::Snowflake
8656 ) {
8657 DataType::Timestamp {
8658 precision: None,
8659 timezone: false,
8660 }
8661 } else {
8662 DataType::Custom {
8663 name: "TIMESTAMPNTZ".to_string(),
8664 }
8665 }
8666 }
8667 DialectType::Spark | DialectType::Databricks => {
8668 // BigQuery TIMESTAMP is timezone-aware -> use plain TIMESTAMP for Spark/Databricks
8669 if matches!(source, DialectType::BigQuery) {
8670 DataType::Timestamp {
8671 precision: None,
8672 timezone: false,
8673 }
8674 } else {
8675 DataType::Custom {
8676 name: "TIMESTAMP_NTZ".to_string(),
8677 }
8678 }
8679 }
8680 DialectType::ClickHouse => DataType::Nullable {
8681 inner: Box::new(DataType::Custom {
8682 name: "DateTime".to_string(),
8683 }),
8684 },
8685 DialectType::TSQL | DialectType::Fabric => DataType::Custom {
8686 name: "DATETIME2".to_string(),
8687 },
8688 DialectType::DuckDB => {
8689 // DuckDB: use TIMESTAMPTZ when source is BigQuery (BQ TIMESTAMP is always UTC/tz-aware)
8690 // or when the timestamp string explicitly has timezone info
8691 if matches!(source, DialectType::BigQuery)
8692 || Self::timestamp_string_has_timezone(&s)
8693 {
8694 DataType::Custom {
8695 name: "TIMESTAMPTZ".to_string(),
8696 }
8697 } else {
8698 DataType::Timestamp {
8699 precision: None,
8700 timezone: false,
8701 }
8702 }
8703 }
8704 _ => DataType::Timestamp {
8705 precision: None,
8706 timezone: false,
8707 },
8708 };
8709 return Ok(Expression::Cast(Box::new(Cast {
8710 this: Expression::Literal(Box::new(Literal::String(s))),
8711 to: dt,
8712 trailing_comments: vec![],
8713 double_colon_syntax: false,
8714 format: None,
8715 default: None,
8716 inferred_type: None,
8717 })));
8718 }
8719 }
8720 }
8721
8722 // PostgreSQL DELETE requires explicit AS for table aliases
8723 if matches!(target, DialectType::PostgreSQL | DialectType::Redshift) {
8724 if let Expression::Delete(ref del) = e {
8725 if del.alias.is_some() && !del.alias_explicit_as {
8726 let mut new_del = del.clone();
8727 new_del.alias_explicit_as = true;
8728 return Ok(Expression::Delete(new_del));
8729 }
8730 }
8731 }
8732
8733 // UNION/INTERSECT/EXCEPT DISTINCT handling:
8734 // Some dialects require explicit DISTINCT (BigQuery, ClickHouse),
8735 // while others don't support it (Presto, Spark, DuckDB, etc.)
8736 {
8737 let needs_distinct =
8738 matches!(target, DialectType::BigQuery | DialectType::ClickHouse);
8739 let drop_distinct = matches!(
8740 target,
8741 DialectType::Presto
8742 | DialectType::Trino
8743 | DialectType::Athena
8744 | DialectType::Spark
8745 | DialectType::Databricks
8746 | DialectType::DuckDB
8747 | DialectType::Hive
8748 | DialectType::MySQL
8749 | DialectType::PostgreSQL
8750 | DialectType::SQLite
8751 | DialectType::TSQL
8752 | DialectType::Redshift
8753 | DialectType::Snowflake
8754 | DialectType::Oracle
8755 | DialectType::Teradata
8756 | DialectType::Drill
8757 | DialectType::Doris
8758 | DialectType::StarRocks
8759 );
8760 match &e {
8761 Expression::Union(u) if !u.all && needs_distinct && !u.distinct => {
8762 let mut new_u = (**u).clone();
8763 new_u.distinct = true;
8764 return Ok(Expression::Union(Box::new(new_u)));
8765 }
8766 Expression::Intersect(i) if !i.all && needs_distinct && !i.distinct => {
8767 let mut new_i = (**i).clone();
8768 new_i.distinct = true;
8769 return Ok(Expression::Intersect(Box::new(new_i)));
8770 }
8771 Expression::Except(ex) if !ex.all && needs_distinct && !ex.distinct => {
8772 let mut new_ex = (**ex).clone();
8773 new_ex.distinct = true;
8774 return Ok(Expression::Except(Box::new(new_ex)));
8775 }
8776 Expression::Union(u) if u.distinct && drop_distinct => {
8777 let mut new_u = (**u).clone();
8778 new_u.distinct = false;
8779 return Ok(Expression::Union(Box::new(new_u)));
8780 }
8781 Expression::Intersect(i) if i.distinct && drop_distinct => {
8782 let mut new_i = (**i).clone();
8783 new_i.distinct = false;
8784 return Ok(Expression::Intersect(Box::new(new_i)));
8785 }
8786 Expression::Except(ex) if ex.distinct && drop_distinct => {
8787 let mut new_ex = (**ex).clone();
8788 new_ex.distinct = false;
8789 return Ok(Expression::Except(Box::new(new_ex)));
8790 }
8791 _ => {}
8792 }
8793 }
8794
8795 // ClickHouse: MAP('a', '1') -> map('a', '1') (lowercase function name)
8796 if matches!(target, DialectType::ClickHouse) {
8797 if let Expression::Function(ref f) = e {
8798 if f.name.eq_ignore_ascii_case("MAP") && !f.args.is_empty() {
8799 let mut new_f = f.as_ref().clone();
8800 new_f.name = "map".to_string();
8801 return Ok(Expression::Function(Box::new(new_f)));
8802 }
8803 }
8804 }
8805
8806 // ClickHouse: INTERSECT ALL -> INTERSECT (ClickHouse doesn't support ALL on INTERSECT)
8807 if matches!(target, DialectType::ClickHouse) {
8808 if let Expression::Intersect(ref i) = e {
8809 if i.all {
8810 let mut new_i = (**i).clone();
8811 new_i.all = false;
8812 return Ok(Expression::Intersect(Box::new(new_i)));
8813 }
8814 }
8815 }
8816
8817 // Integer division: a / b -> CAST(a AS DOUBLE) / b for dialects that need it
8818 // Only from Generic source, to prevent double-wrapping
8819 if matches!(source, DialectType::Generic) {
8820 if let Expression::Div(ref op) = e {
8821 let cast_type = match target {
8822 DialectType::TSQL | DialectType::Fabric => Some(DataType::Float {
8823 precision: None,
8824 scale: None,
8825 real_spelling: false,
8826 }),
8827 DialectType::Drill
8828 | DialectType::Trino
8829 | DialectType::Athena
8830 | DialectType::Presto => Some(DataType::Double {
8831 precision: None,
8832 scale: None,
8833 }),
8834 DialectType::PostgreSQL
8835 | DialectType::Redshift
8836 | DialectType::Materialize
8837 | DialectType::Teradata
8838 | DialectType::RisingWave => Some(DataType::Double {
8839 precision: None,
8840 scale: None,
8841 }),
8842 _ => None,
8843 };
8844 if let Some(dt) = cast_type {
8845 let cast_left = Expression::Cast(Box::new(Cast {
8846 this: op.left.clone(),
8847 to: dt,
8848 double_colon_syntax: false,
8849 trailing_comments: Vec::new(),
8850 format: None,
8851 default: None,
8852 inferred_type: None,
8853 }));
8854 let new_op = crate::expressions::BinaryOp {
8855 left: cast_left,
8856 right: op.right.clone(),
8857 left_comments: op.left_comments.clone(),
8858 operator_comments: op.operator_comments.clone(),
8859 trailing_comments: op.trailing_comments.clone(),
8860 inferred_type: None,
8861 };
8862 return Ok(Expression::Div(Box::new(new_op)));
8863 }
8864 }
8865 }
8866
8867 // CREATE DATABASE -> CREATE SCHEMA for DuckDB target
8868 if matches!(target, DialectType::DuckDB) {
8869 if let Expression::CreateDatabase(db) = e {
8870 let mut schema = crate::expressions::CreateSchema::new(db.name.name.clone());
8871 schema.if_not_exists = db.if_not_exists;
8872 return Ok(Expression::CreateSchema(Box::new(schema)));
8873 }
8874 if let Expression::DropDatabase(db) = e {
8875 let mut schema = crate::expressions::DropSchema::new(db.name.name.clone());
8876 schema.if_exists = db.if_exists;
8877 return Ok(Expression::DropSchema(Box::new(schema)));
8878 }
8879 }
8880
8881 // Strip ClickHouse Nullable(...) wrapper for non-ClickHouse targets
8882 if matches!(source, DialectType::ClickHouse)
8883 && !matches!(target, DialectType::ClickHouse)
8884 {
8885 if let Expression::Cast(ref c) = e {
8886 if let DataType::Custom { ref name } = c.to {
8887 if name.len() >= 9
8888 && name[..9].eq_ignore_ascii_case("NULLABLE(")
8889 && name.ends_with(")")
8890 {
8891 let inner = &name[9..name.len() - 1]; // strip "Nullable(" and ")"
8892 let inner_upper = inner.to_ascii_uppercase();
8893 let new_dt = match inner_upper.as_str() {
8894 "DATETIME" | "DATETIME64" => DataType::Timestamp {
8895 precision: None,
8896 timezone: false,
8897 },
8898 "DATE" => DataType::Date,
8899 "INT64" | "BIGINT" => DataType::BigInt { length: None },
8900 "INT32" | "INT" | "INTEGER" => DataType::Int {
8901 length: None,
8902 integer_spelling: false,
8903 },
8904 "FLOAT64" | "DOUBLE" => DataType::Double {
8905 precision: None,
8906 scale: None,
8907 },
8908 "STRING" => DataType::Text,
8909 _ => DataType::Custom {
8910 name: inner.to_string(),
8911 },
8912 };
8913 let mut new_cast = c.clone();
8914 new_cast.to = new_dt;
8915 return Ok(Expression::Cast(new_cast));
8916 }
8917 }
8918 }
8919 }
8920
8921 // ARRAY_CONCAT_AGG -> Snowflake: ARRAY_FLATTEN(ARRAY_AGG(...))
8922 if matches!(target, DialectType::Snowflake) {
8923 if let Expression::ArrayConcatAgg(ref agg) = e {
8924 let mut agg_clone = agg.as_ref().clone();
8925 agg_clone.name = None; // Clear name so generator uses default "ARRAY_AGG"
8926 let array_agg = Expression::ArrayAgg(Box::new(agg_clone));
8927 let flatten = Expression::Function(Box::new(Function::new(
8928 "ARRAY_FLATTEN".to_string(),
8929 vec![array_agg],
8930 )));
8931 return Ok(flatten);
8932 }
8933 }
8934
8935 // ARRAY_CONCAT_AGG -> others: keep as function for cross-dialect
8936 if !matches!(target, DialectType::BigQuery | DialectType::Snowflake) {
8937 if let Expression::ArrayConcatAgg(agg) = e {
8938 let arg = agg.this;
8939 return Ok(Expression::Function(Box::new(Function::new(
8940 "ARRAY_CONCAT_AGG".to_string(),
8941 vec![arg],
8942 ))));
8943 }
8944 }
8945
8946 // Determine what action to take by inspecting e immutably
8947 let action = {
8948 let source_propagates_nulls =
8949 matches!(source, DialectType::Snowflake | DialectType::BigQuery);
8950 let target_ignores_nulls =
8951 matches!(target, DialectType::DuckDB | DialectType::PostgreSQL);
8952
8953 match &e {
8954 Expression::Function(f) => {
8955 let name = f.name.to_ascii_uppercase();
8956 // DuckDB json(x) is a synonym for CAST(x AS JSON) — parses a string.
8957 // Map to JSON_PARSE(x) for Trino/Presto/Athena to preserve semantics.
8958 if name == "JSON"
8959 && f.args.len() == 1
8960 && matches!(source, DialectType::DuckDB)
8961 && matches!(
8962 target,
8963 DialectType::Presto | DialectType::Trino | DialectType::Athena
8964 )
8965 {
8966 Action::DuckDBJsonFuncToJsonParse
8967 // DuckDB json_valid(x) has no direct Trino equivalent; emit the
8968 // SQL:2016 `x IS JSON` predicate which has matching semantics.
8969 } else if name == "JSON_VALID"
8970 && f.args.len() == 1
8971 && matches!(source, DialectType::DuckDB)
8972 && matches!(
8973 target,
8974 DialectType::Presto | DialectType::Trino | DialectType::Athena
8975 )
8976 {
8977 Action::DuckDBJsonValidToIsJson
8978 // DATE_PART: strip quotes from first arg when target is Snowflake (source != Snowflake)
8979 } else if (name == "DATE_PART" || name == "DATEPART")
8980 && f.args.len() == 2
8981 && matches!(target, DialectType::Snowflake)
8982 && !matches!(source, DialectType::Snowflake)
8983 && matches!(
8984 &f.args[0],
8985 Expression::Literal(lit) if matches!(lit.as_ref(), crate::expressions::Literal::String(_))
8986 )
8987 {
8988 Action::DatePartUnquote
8989 } else if source_propagates_nulls
8990 && target_ignores_nulls
8991 && (name == "GREATEST" || name == "LEAST")
8992 && f.args.len() >= 2
8993 {
8994 Action::GreatestLeastNull
8995 } else if matches!(source, DialectType::Snowflake)
8996 && name == "ARRAY_GENERATE_RANGE"
8997 && f.args.len() >= 2
8998 {
8999 Action::ArrayGenerateRange
9000 } else if matches!(source, DialectType::Snowflake)
9001 && matches!(target, DialectType::DuckDB)
9002 && name == "DATE_TRUNC"
9003 && f.args.len() == 2
9004 {
9005 // Determine if DuckDB DATE_TRUNC needs CAST wrapping to preserve input type.
9006 // Logic based on Python sqlglot's input_type_preserved flag:
9007 // - DATE + non-date-unit (HOUR, MINUTE, etc.) -> wrap
9008 // - TIMESTAMP + date-unit (YEAR, QUARTER, MONTH, WEEK, DAY) -> wrap
9009 // - TIMESTAMPTZ/TIMESTAMPLTZ/TIME -> always wrap
9010 let unit_str = match &f.args[0] {
9011 Expression::Literal(lit) if matches!(lit.as_ref(), crate::expressions::Literal::String(_)) => {
9012 let crate::expressions::Literal::String(s) = lit.as_ref() else { unreachable!() };
9013 Some(s.to_ascii_uppercase())
9014 }
9015 _ => None,
9016 };
9017 let is_date_unit = unit_str.as_ref().map_or(false, |u| {
9018 matches!(u.as_str(), "YEAR" | "QUARTER" | "MONTH" | "WEEK" | "DAY")
9019 });
9020 match &f.args[1] {
9021 Expression::Cast(c) => match &c.to {
9022 DataType::Time { .. } => Action::DateTruncWrapCast,
9023 DataType::Custom { name }
9024 if name.eq_ignore_ascii_case("TIMESTAMPTZ")
9025 || name.eq_ignore_ascii_case("TIMESTAMPLTZ") =>
9026 {
9027 Action::DateTruncWrapCast
9028 }
9029 DataType::Timestamp { timezone: true, .. } => {
9030 Action::DateTruncWrapCast
9031 }
9032 DataType::Date if !is_date_unit => Action::DateTruncWrapCast,
9033 DataType::Timestamp {
9034 timezone: false, ..
9035 } if is_date_unit => Action::DateTruncWrapCast,
9036 _ => Action::None,
9037 },
9038 _ => Action::None,
9039 }
9040 } else if matches!(source, DialectType::Snowflake)
9041 && matches!(target, DialectType::DuckDB)
9042 && name == "TO_DATE"
9043 && f.args.len() == 1
9044 && !matches!(
9045 &f.args[0],
9046 Expression::Literal(lit) if matches!(lit.as_ref(), crate::expressions::Literal::String(_))
9047 )
9048 {
9049 Action::ToDateToCast
9050 } else if !matches!(source, DialectType::Redshift)
9051 && matches!(target, DialectType::Redshift)
9052 && name == "CONVERT_TIMEZONE"
9053 && (f.args.len() == 2 || f.args.len() == 3)
9054 {
9055 // Convert Function("CONVERT_TIMEZONE") to Expression::ConvertTimezone
9056 // so Redshift's transform_expr won't expand 2-arg to 3-arg with 'UTC'.
9057 // The Redshift parser adds 'UTC' as default source_tz, but when
9058 // transpiling from other dialects, we should preserve the original form.
9059 Action::ConvertTimezoneToExpr
9060 } else if matches!(source, DialectType::Snowflake)
9061 && matches!(target, DialectType::DuckDB)
9062 && name == "REGEXP_REPLACE"
9063 && f.args.len() == 4
9064 && !matches!(
9065 &f.args[3],
9066 Expression::Literal(lit) if matches!(lit.as_ref(), crate::expressions::Literal::String(_))
9067 )
9068 {
9069 // Snowflake REGEXP_REPLACE with position arg -> DuckDB needs 'g' flag
9070 Action::RegexpReplaceSnowflakeToDuckDB
9071 } else if matches!(source, DialectType::Snowflake)
9072 && matches!(target, DialectType::DuckDB)
9073 && name == "REGEXP_REPLACE"
9074 && f.args.len() == 5
9075 {
9076 // Snowflake REGEXP_REPLACE(s, p, r, pos, occ) -> DuckDB
9077 Action::RegexpReplacePositionSnowflakeToDuckDB
9078 } else if matches!(source, DialectType::Snowflake)
9079 && matches!(target, DialectType::DuckDB)
9080 && name == "REGEXP_SUBSTR"
9081 {
9082 // Snowflake REGEXP_SUBSTR -> DuckDB REGEXP_EXTRACT variants
9083 Action::RegexpSubstrSnowflakeToDuckDB
9084 } else if matches!(source, DialectType::Snowflake)
9085 && matches!(target, DialectType::Snowflake)
9086 && (name == "REGEXP_SUBSTR" || name == "REGEXP_SUBSTR_ALL")
9087 && f.args.len() == 6
9088 {
9089 // Snowflake identity: strip trailing group=0
9090 Action::RegexpSubstrSnowflakeIdentity
9091 } else if matches!(source, DialectType::Snowflake)
9092 && matches!(target, DialectType::DuckDB)
9093 && name == "REGEXP_SUBSTR_ALL"
9094 {
9095 // Snowflake REGEXP_SUBSTR_ALL -> DuckDB REGEXP_EXTRACT_ALL variants
9096 Action::RegexpSubstrAllSnowflakeToDuckDB
9097 } else if matches!(source, DialectType::Snowflake)
9098 && matches!(target, DialectType::DuckDB)
9099 && name == "REGEXP_COUNT"
9100 {
9101 // Snowflake REGEXP_COUNT -> DuckDB LENGTH(REGEXP_EXTRACT_ALL(...))
9102 Action::RegexpCountSnowflakeToDuckDB
9103 } else if matches!(source, DialectType::Snowflake)
9104 && matches!(target, DialectType::DuckDB)
9105 && name == "REGEXP_INSTR"
9106 {
9107 // Snowflake REGEXP_INSTR -> DuckDB complex CASE expression
9108 Action::RegexpInstrSnowflakeToDuckDB
9109 } else if matches!(source, DialectType::BigQuery)
9110 && matches!(target, DialectType::Snowflake)
9111 && name == "REGEXP_EXTRACT_ALL"
9112 {
9113 // BigQuery REGEXP_EXTRACT_ALL -> Snowflake REGEXP_SUBSTR_ALL
9114 Action::RegexpExtractAllToSnowflake
9115 } else if name == "_BQ_TO_HEX" {
9116 // Internal marker from TO_HEX conversion - bare (no LOWER/UPPER wrapper)
9117 Action::BigQueryToHexBare
9118 } else if matches!(source, DialectType::BigQuery)
9119 && !matches!(target, DialectType::BigQuery)
9120 {
9121 // BigQuery-specific functions that need to be converted to standard forms
9122 match name.as_str() {
9123 "TIMESTAMP_DIFF" | "DATETIME_DIFF" | "TIME_DIFF"
9124 | "DATE_DIFF"
9125 | "TIMESTAMP_ADD" | "TIMESTAMP_SUB"
9126 | "DATETIME_ADD" | "DATETIME_SUB"
9127 | "TIME_ADD" | "TIME_SUB"
9128 | "DATE_ADD" | "DATE_SUB"
9129 | "SAFE_DIVIDE"
9130 | "GENERATE_UUID"
9131 | "COUNTIF"
9132 | "EDIT_DISTANCE"
9133 | "TIMESTAMP_SECONDS" | "TIMESTAMP_MILLIS" | "TIMESTAMP_MICROS"
9134 | "TIMESTAMP_TRUNC" | "DATETIME_TRUNC" | "DATE_TRUNC"
9135 | "TO_HEX"
9136 | "TO_JSON_STRING"
9137 | "GENERATE_ARRAY" | "GENERATE_TIMESTAMP_ARRAY"
9138 | "DIV"
9139 | "UNIX_DATE" | "UNIX_SECONDS" | "UNIX_MILLIS" | "UNIX_MICROS"
9140 | "LAST_DAY"
9141 | "TIME" | "DATETIME" | "TIMESTAMP" | "STRING"
9142 | "REGEXP_CONTAINS"
9143 | "CONTAINS_SUBSTR"
9144 | "SAFE_ADD" | "SAFE_SUBTRACT" | "SAFE_MULTIPLY"
9145 | "SAFE_CAST"
9146 | "GENERATE_DATE_ARRAY"
9147 | "PARSE_DATE" | "PARSE_DATETIME" | "PARSE_TIMESTAMP"
9148 | "FORMAT_DATE" | "FORMAT_DATETIME" | "FORMAT_TIMESTAMP"
9149 | "ARRAY_CONCAT"
9150 | "JSON_QUERY" | "JSON_VALUE_ARRAY"
9151 | "INSTR"
9152 | "MD5" | "SHA1" | "SHA256" | "SHA512"
9153 | "GENERATE_UUID()" // just in case
9154 | "REGEXP_EXTRACT_ALL"
9155 | "REGEXP_EXTRACT"
9156 | "INT64"
9157 | "ARRAY_CONCAT_AGG"
9158 | "DATE_DIFF(" // just in case
9159 | "TO_HEX_MD5" // internal
9160 | "MOD"
9161 | "CONCAT"
9162 | "CURRENT_TIMESTAMP" | "CURRENT_DATE" | "CURRENT_DATETIME" | "CURRENT_TIME"
9163 | "STRUCT"
9164 | "ROUND"
9165 | "MAKE_INTERVAL"
9166 | "ARRAY_TO_STRING"
9167 | "PERCENTILE_CONT"
9168 => Action::BigQueryFunctionNormalize,
9169 "ARRAY" if matches!(target, DialectType::Snowflake)
9170 && f.args.len() == 1
9171 && matches!(&f.args[0], Expression::Select(s) if s.kind.as_deref() == Some("STRUCT"))
9172 => Action::BigQueryArraySelectAsStructToSnowflake,
9173 _ => Action::None,
9174 }
9175 } else if matches!(source, DialectType::BigQuery)
9176 && matches!(target, DialectType::BigQuery)
9177 {
9178 // BigQuery -> BigQuery normalizations
9179 match name.as_str() {
9180 "TIMESTAMP_DIFF"
9181 | "DATETIME_DIFF"
9182 | "TIME_DIFF"
9183 | "DATE_DIFF"
9184 | "DATE_ADD"
9185 | "TO_HEX"
9186 | "CURRENT_TIMESTAMP"
9187 | "CURRENT_DATE"
9188 | "CURRENT_TIME"
9189 | "CURRENT_DATETIME"
9190 | "GENERATE_DATE_ARRAY"
9191 | "INSTR"
9192 | "FORMAT_DATETIME"
9193 | "DATETIME"
9194 | "MAKE_INTERVAL" => Action::BigQueryFunctionNormalize,
9195 _ => Action::None,
9196 }
9197 } else {
9198 // Generic function normalization for non-BigQuery sources
9199 match name.as_str() {
9200 "ARBITRARY" | "AGGREGATE"
9201 | "REGEXP_MATCHES" | "REGEXP_FULL_MATCH"
9202 | "STRUCT_EXTRACT"
9203 | "LIST_FILTER" | "LIST_TRANSFORM" | "LIST_SORT" | "LIST_REVERSE_SORT"
9204 | "STRING_TO_ARRAY" | "STR_SPLIT" | "STR_SPLIT_REGEX" | "SPLIT_TO_ARRAY"
9205 | "SUBSTRINGINDEX"
9206 | "ARRAY_LENGTH" | "SIZE" | "CARDINALITY"
9207 | "UNICODE"
9208 | "XOR"
9209 | "ARRAY_REVERSE_SORT"
9210 | "ENCODE" | "DECODE"
9211 | "QUANTILE"
9212 | "EPOCH" | "EPOCH_MS"
9213 | "HASHBYTES"
9214 | "JSON_EXTRACT_PATH" | "JSON_EXTRACT_PATH_TEXT"
9215 | "APPROX_DISTINCT"
9216 | "DATE_PARSE" | "FORMAT_DATETIME"
9217 | "REGEXP_EXTRACT" | "REGEXP_SUBSTR" | "TO_DAYS"
9218 | "RLIKE"
9219 | "DATEDIFF" | "DATE_DIFF" | "MONTHS_BETWEEN"
9220 | "ADD_MONTHS" | "DATEADD" | "DATE_ADD" | "DATE_SUB" | "DATETRUNC"
9221 | "LAST_DAY" | "LAST_DAY_OF_MONTH" | "EOMONTH"
9222 | "ARRAY_CONSTRUCT" | "ARRAY_CAT" | "ARRAY_COMPACT"
9223 | "ARRAY_FILTER" | "FILTER" | "REDUCE" | "ARRAY_REVERSE"
9224 | "MAP" | "MAP_FROM_ENTRIES"
9225 | "COLLECT_LIST" | "COLLECT_SET"
9226 | "ISNAN" | "IS_NAN"
9227 | "TO_UTC_TIMESTAMP" | "FROM_UTC_TIMESTAMP"
9228 | "FORMAT_NUMBER"
9229 | "TOMONDAY" | "TOSTARTOFWEEK" | "TOSTARTOFMONTH" | "TOSTARTOFYEAR"
9230 | "ELEMENT_AT"
9231 | "EXPLODE" | "EXPLODE_OUTER" | "POSEXPLODE"
9232 | "SPLIT_PART"
9233 // GENERATE_SERIES: handled separately below
9234 | "JSON_EXTRACT" | "JSON_EXTRACT_SCALAR"
9235 | "JSON_QUERY" | "JSON_VALUE"
9236 | "JSON_SEARCH"
9237 | "JSON_EXTRACT_JSON" | "BSON_EXTRACT_BSON"
9238 | "TO_UNIX_TIMESTAMP" | "UNIX_TIMESTAMP"
9239 | "CURDATE" | "CURTIME"
9240 | "ARRAY_TO_STRING"
9241 | "ARRAY_SORT" | "SORT_ARRAY"
9242 | "LEFT" | "RIGHT"
9243 | "MAP_FROM_ARRAYS"
9244 | "LIKE" | "ILIKE"
9245 | "ARRAY_CONCAT" | "LIST_CONCAT"
9246 | "QUANTILE_CONT" | "QUANTILE_DISC"
9247 | "PERCENTILE_CONT" | "PERCENTILE_DISC"
9248 | "PERCENTILE_APPROX" | "APPROX_PERCENTILE"
9249 | "LOCATE" | "STRPOS" | "INSTR"
9250 | "CHAR"
9251 // CONCAT: handled separately for COALESCE wrapping
9252 | "ARRAY_JOIN"
9253 | "ARRAY_CONTAINS" | "HAS" | "CONTAINS"
9254 | "ISNULL"
9255 | "MONTHNAME"
9256 | "TO_TIMESTAMP"
9257 | "TO_DATE"
9258 | "TO_JSON"
9259 | "REGEXP_SPLIT"
9260 | "SPLIT"
9261 | "FORMATDATETIME"
9262 | "ARRAYJOIN"
9263 | "SPLITBYSTRING" | "SPLITBYREGEXP"
9264 | "NVL"
9265 | "TO_CHAR"
9266 | "DBMS_RANDOM.VALUE"
9267 | "REGEXP_LIKE"
9268 | "REPLICATE"
9269 | "LEN"
9270 | "COUNT_BIG"
9271 | "DATEFROMPARTS"
9272 | "DATETIMEFROMPARTS"
9273 | "CONVERT" | "TRY_CONVERT"
9274 | "STRFTIME" | "STRPTIME"
9275 | "DATE_FORMAT" | "FORMAT_DATE"
9276 | "PARSE_TIMESTAMP" | "PARSE_DATETIME" | "PARSE_DATE"
9277 | "FROM_ISO8601_TIMESTAMP" | "FROM_ISO8601_DATE"
9278 | "FROM_BASE64" | "TO_BASE64"
9279 | "GETDATE"
9280 | "TO_HEX" | "FROM_HEX" | "UNHEX" | "HEX"
9281 | "TO_UTF8" | "FROM_UTF8"
9282 | "STARTS_WITH" | "STARTSWITH"
9283 | "APPROX_COUNT_DISTINCT"
9284 | "JSON_FORMAT"
9285 | "SYSDATE"
9286 | "LOGICAL_OR" | "LOGICAL_AND"
9287 | "MONTHS_ADD"
9288 | "SCHEMA_NAME"
9289 | "STRTOL"
9290 | "EDITDIST3"
9291 | "FORMAT"
9292 | "LIST_CONTAINS" | "LIST_HAS"
9293 | "VARIANCE" | "STDDEV"
9294 | "ISINF"
9295 | "TO_UNIXTIME"
9296 | "FROM_UNIXTIME"
9297 | "DATEPART" | "DATE_PART"
9298 | "DATENAME"
9299 | "STRING_AGG"
9300 | "JSON_ARRAYAGG"
9301 | "APPROX_QUANTILE"
9302 | "MAKE_DATE"
9303 | "LIST_HAS_ANY" | "ARRAY_HAS_ANY"
9304 | "RANGE"
9305 | "TRY_ELEMENT_AT"
9306 | "STR_TO_MAP"
9307 | "STRING"
9308 | "STR_TO_TIME"
9309 | "CURRENT_SCHEMA"
9310 | "LTRIM" | "RTRIM"
9311 | "UUID"
9312 | "FARM_FINGERPRINT"
9313 | "JSON_KEYS"
9314 | "WEEKOFYEAR"
9315 | "CONCAT_WS"
9316 | "TRY_DIVIDE"
9317 | "ARRAY_SLICE"
9318 | "ARRAY_PREPEND"
9319 | "ARRAY_REMOVE"
9320 | "GENERATE_DATE_ARRAY"
9321 | "PARSE_JSON"
9322 | "JSON_REMOVE"
9323 | "JSON_SET"
9324 | "LEVENSHTEIN"
9325 | "CURRENT_VERSION"
9326 | "ARRAY_MAX"
9327 | "ARRAY_MIN"
9328 | "JAROWINKLER_SIMILARITY"
9329 | "CURRENT_SCHEMAS"
9330 | "TO_VARIANT"
9331 | "JSON_GROUP_ARRAY" | "JSON_GROUP_OBJECT"
9332 | "ARRAYS_OVERLAP" | "ARRAY_INTERSECTION"
9333 => Action::GenericFunctionNormalize,
9334 // Canonical date functions -> dialect-specific
9335 "TS_OR_DS_TO_DATE" => Action::TsOrDsToDateConvert,
9336 "TS_OR_DS_TO_DATE_STR" if f.args.len() == 1 => Action::TsOrDsToDateStrConvert,
9337 "DATE_STR_TO_DATE" if f.args.len() == 1 => Action::DateStrToDateConvert,
9338 "TIME_STR_TO_DATE" if f.args.len() == 1 => Action::TimeStrToDateConvert,
9339 "TIME_STR_TO_TIME" if f.args.len() <= 2 => Action::TimeStrToTimeConvert,
9340 "TIME_STR_TO_UNIX" if f.args.len() == 1 => Action::TimeStrToUnixConvert,
9341 "TIME_TO_TIME_STR" if f.args.len() == 1 => Action::TimeToTimeStrConvert,
9342 "DATE_TO_DATE_STR" if f.args.len() == 1 => Action::DateToDateStrConvert,
9343 "DATE_TO_DI" if f.args.len() == 1 => Action::DateToDiConvert,
9344 "DI_TO_DATE" if f.args.len() == 1 => Action::DiToDateConvert,
9345 "TS_OR_DI_TO_DI" if f.args.len() == 1 => Action::TsOrDiToDiConvert,
9346 "UNIX_TO_STR" if f.args.len() == 2 => Action::UnixToStrConvert,
9347 "UNIX_TO_TIME" if f.args.len() == 1 => Action::UnixToTimeConvert,
9348 "UNIX_TO_TIME_STR" if f.args.len() == 1 => Action::UnixToTimeStrConvert,
9349 "TIME_TO_UNIX" if f.args.len() == 1 => Action::TimeToUnixConvert,
9350 "TIME_TO_STR" if f.args.len() == 2 => Action::TimeToStrConvert,
9351 "STR_TO_UNIX" if f.args.len() == 2 => Action::StrToUnixConvert,
9352 // STR_TO_DATE(x, fmt) -> dialect-specific
9353 "STR_TO_DATE" if f.args.len() == 2
9354 && matches!(source, DialectType::Generic) => Action::StrToDateConvert,
9355 "STR_TO_DATE" => Action::GenericFunctionNormalize,
9356 // TS_OR_DS_ADD(x, n, 'UNIT') from Generic -> dialect-specific DATE_ADD
9357 "TS_OR_DS_ADD" if f.args.len() == 3
9358 && matches!(source, DialectType::Generic) => Action::TsOrDsAddConvert,
9359 // DATE_FROM_UNIX_DATE(n) -> DATEADD(DAY, n, '1970-01-01')
9360 "DATE_FROM_UNIX_DATE" if f.args.len() == 1 => Action::DateFromUnixDateConvert,
9361 // NVL2(a, b, c) -> CASE WHEN NOT a IS NULL THEN b [ELSE c] END
9362 "NVL2" if (f.args.len() == 2 || f.args.len() == 3) => Action::Nvl2Expand,
9363 // IFNULL(a, b) -> COALESCE(a, b) when coming from Generic source
9364 "IFNULL" if f.args.len() == 2 => Action::IfnullToCoalesce,
9365 // IS_ASCII(x) -> dialect-specific
9366 "IS_ASCII" if f.args.len() == 1 => Action::IsAsciiConvert,
9367 // STR_POSITION(haystack, needle[, pos[, occ]]) -> dialect-specific
9368 "STR_POSITION" => Action::StrPositionConvert,
9369 // ARRAY_SUM -> dialect-specific
9370 "ARRAY_SUM" => Action::ArraySumConvert,
9371 // ARRAY_SIZE -> dialect-specific (Drill only)
9372 "ARRAY_SIZE" if matches!(target, DialectType::Drill) => Action::ArraySizeConvert,
9373 // ARRAY_ANY -> dialect-specific
9374 "ARRAY_ANY" if f.args.len() == 2 => Action::ArrayAnyConvert,
9375 // Functions needing specific cross-dialect transforms
9376 "MAX_BY" | "MIN_BY" if matches!(target, DialectType::ClickHouse | DialectType::Spark | DialectType::Databricks | DialectType::DuckDB) => Action::MaxByMinByConvert,
9377 "STRUCT" if matches!(source, DialectType::Spark | DialectType::Databricks)
9378 && !matches!(target, DialectType::Spark | DialectType::Databricks | DialectType::Hive) => Action::SparkStructConvert,
9379 "ARRAY" if matches!(source, DialectType::BigQuery)
9380 && matches!(target, DialectType::Snowflake)
9381 && f.args.len() == 1
9382 && matches!(&f.args[0], Expression::Select(s) if s.kind.as_deref() == Some("STRUCT")) => Action::BigQueryArraySelectAsStructToSnowflake,
9383 "ARRAY" if matches!(target, DialectType::Presto | DialectType::Trino | DialectType::Athena | DialectType::BigQuery | DialectType::DuckDB | DialectType::Snowflake | DialectType::ClickHouse | DialectType::StarRocks) => Action::ArraySyntaxConvert,
9384 "TRUNC" if f.args.len() == 2 && matches!(&f.args[1], Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_))) && matches!(target, DialectType::Presto | DialectType::Trino | DialectType::ClickHouse) => Action::TruncToDateTrunc,
9385 "TRUNC" | "TRUNCATE" if f.args.len() <= 2 && !f.args.get(1).map_or(false, |a| matches!(a, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)))) => Action::GenericFunctionNormalize,
9386 // DATE_TRUNC('unit', x) from Generic source -> arg swap for BigQuery/Doris/Spark/MySQL
9387 "DATE_TRUNC" if f.args.len() == 2
9388 && matches!(source, DialectType::Generic)
9389 && matches!(target, DialectType::BigQuery | DialectType::Doris | DialectType::StarRocks
9390 | DialectType::Spark | DialectType::Databricks | DialectType::MySQL) => Action::DateTruncSwapArgs,
9391 // TIMESTAMP_TRUNC(x, UNIT) from Generic source -> convert to per-dialect
9392 "TIMESTAMP_TRUNC" if f.args.len() >= 2
9393 && matches!(source, DialectType::Generic) => Action::TimestampTruncConvert,
9394 "UNIFORM" if matches!(target, DialectType::Snowflake) => Action::GenericFunctionNormalize,
9395 // GENERATE_SERIES -> SEQUENCE/UNNEST/EXPLODE for target dialects
9396 "GENERATE_SERIES" if matches!(source, DialectType::PostgreSQL | DialectType::Redshift)
9397 && !matches!(target, DialectType::PostgreSQL | DialectType::Redshift | DialectType::TSQL | DialectType::Fabric) => Action::GenerateSeriesConvert,
9398 // GENERATE_SERIES with interval normalization for PG target
9399 "GENERATE_SERIES" if f.args.len() >= 3
9400 && matches!(source, DialectType::PostgreSQL | DialectType::Redshift)
9401 && matches!(target, DialectType::PostgreSQL | DialectType::Redshift) => Action::GenerateSeriesConvert,
9402 "GENERATE_SERIES" => Action::None, // passthrough for other cases
9403 // CONCAT(a, b) -> COALESCE wrapping for Presto/ClickHouse from PostgreSQL
9404 "CONCAT" if matches!(source, DialectType::PostgreSQL | DialectType::Redshift)
9405 && matches!(target, DialectType::Presto | DialectType::Trino | DialectType::ClickHouse) => Action::ConcatCoalesceWrap,
9406 "CONCAT" => Action::GenericFunctionNormalize,
9407 // DIV(a, b) -> target-specific integer division
9408 "DIV" if f.args.len() == 2
9409 && matches!(source, DialectType::PostgreSQL)
9410 && matches!(target, DialectType::DuckDB | DialectType::BigQuery | DialectType::SQLite) => Action::DivFuncConvert,
9411 // JSON_OBJECT_AGG/JSONB_OBJECT_AGG -> JSON_GROUP_OBJECT for DuckDB
9412 "JSON_OBJECT_AGG" | "JSONB_OBJECT_AGG" if f.args.len() == 2
9413 && matches!(target, DialectType::DuckDB) => Action::JsonObjectAggConvert,
9414 // JSONB_EXISTS -> JSON_EXISTS for DuckDB
9415 "JSONB_EXISTS" if f.args.len() == 2
9416 && matches!(target, DialectType::DuckDB) => Action::JsonbExistsConvert,
9417 // DATE_BIN -> TIME_BUCKET for DuckDB
9418 "DATE_BIN" if matches!(target, DialectType::DuckDB) => Action::DateBinConvert,
9419 // Multi-arg MIN(a,b,c) -> LEAST, MAX(a,b,c) -> GREATEST
9420 "MIN" | "MAX" if f.args.len() > 1 && !matches!(target, DialectType::SQLite) => Action::MinMaxToLeastGreatest,
9421 // ClickHouse uniq -> APPROX_COUNT_DISTINCT for other dialects
9422 "UNIQ" if matches!(source, DialectType::ClickHouse) && !matches!(target, DialectType::ClickHouse) => Action::ClickHouseUniqToApproxCountDistinct,
9423 // ClickHouse any -> ANY_VALUE for other dialects
9424 "ANY" if f.args.len() == 1 && matches!(source, DialectType::ClickHouse) && !matches!(target, DialectType::ClickHouse) => Action::ClickHouseAnyToAnyValue,
9425 _ => Action::None,
9426 }
9427 }
9428 }
9429 Expression::AggregateFunction(af) => {
9430 let name = af.name.to_ascii_uppercase();
9431 match name.as_str() {
9432 "ARBITRARY" | "AGGREGATE" => Action::GenericFunctionNormalize,
9433 "JSON_ARRAYAGG" => Action::GenericFunctionNormalize,
9434 // JSON_OBJECT_AGG/JSONB_OBJECT_AGG -> JSON_GROUP_OBJECT for DuckDB
9435 "JSON_OBJECT_AGG" | "JSONB_OBJECT_AGG"
9436 if matches!(target, DialectType::DuckDB) =>
9437 {
9438 Action::JsonObjectAggConvert
9439 }
9440 "ARRAY_AGG"
9441 if matches!(
9442 target,
9443 DialectType::Hive
9444 | DialectType::Spark
9445 | DialectType::Databricks
9446 ) =>
9447 {
9448 Action::ArrayAggToCollectList
9449 }
9450 "MAX_BY" | "MIN_BY"
9451 if matches!(
9452 target,
9453 DialectType::ClickHouse
9454 | DialectType::Spark
9455 | DialectType::Databricks
9456 | DialectType::DuckDB
9457 ) =>
9458 {
9459 Action::MaxByMinByConvert
9460 }
9461 "COLLECT_LIST"
9462 if matches!(
9463 target,
9464 DialectType::Presto | DialectType::Trino | DialectType::DuckDB
9465 ) =>
9466 {
9467 Action::CollectListToArrayAgg
9468 }
9469 "COLLECT_SET"
9470 if matches!(
9471 target,
9472 DialectType::Presto
9473 | DialectType::Trino
9474 | DialectType::Snowflake
9475 | DialectType::DuckDB
9476 ) =>
9477 {
9478 Action::CollectSetConvert
9479 }
9480 "PERCENTILE"
9481 if matches!(
9482 target,
9483 DialectType::DuckDB | DialectType::Presto | DialectType::Trino
9484 ) =>
9485 {
9486 Action::PercentileConvert
9487 }
9488 // CORR -> CASE WHEN ISNAN(CORR(a,b)) THEN NULL ELSE CORR(a,b) END for DuckDB
9489 "CORR"
9490 if matches!(target, DialectType::DuckDB)
9491 && matches!(source, DialectType::Snowflake) =>
9492 {
9493 Action::CorrIsnanWrap
9494 }
9495 // BigQuery APPROX_QUANTILES(x, n) -> APPROX_QUANTILE(x, [quantiles]) for DuckDB
9496 "APPROX_QUANTILES"
9497 if matches!(source, DialectType::BigQuery)
9498 && matches!(target, DialectType::DuckDB) =>
9499 {
9500 Action::BigQueryApproxQuantiles
9501 }
9502 // BigQuery PERCENTILE_CONT(x, frac RESPECT NULLS) -> QUANTILE_CONT(x, frac) for DuckDB
9503 "PERCENTILE_CONT"
9504 if matches!(source, DialectType::BigQuery)
9505 && matches!(target, DialectType::DuckDB)
9506 && af.args.len() >= 2 =>
9507 {
9508 Action::BigQueryPercentileContToDuckDB
9509 }
9510 _ => Action::None,
9511 }
9512 }
9513 Expression::JSONArrayAgg(_) => match target {
9514 DialectType::PostgreSQL => Action::GenericFunctionNormalize,
9515 _ => Action::None,
9516 },
9517 Expression::ToNumber(tn) => {
9518 // TO_NUMBER(x) with 1 arg -> CAST(x AS DOUBLE) for most targets
9519 if tn.format.is_none() && tn.precision.is_none() && tn.scale.is_none() {
9520 match target {
9521 DialectType::Oracle
9522 | DialectType::Snowflake
9523 | DialectType::Teradata => Action::None,
9524 _ => Action::GenericFunctionNormalize,
9525 }
9526 } else {
9527 Action::None
9528 }
9529 }
9530 Expression::Nvl2(_) => {
9531 // NVL2(a, b, c) -> CASE WHEN NOT a IS NULL THEN b ELSE c END for most dialects
9532 // Keep as NVL2 for dialects that support it natively
9533 match target {
9534 DialectType::Oracle
9535 | DialectType::Snowflake
9536 | DialectType::Teradata
9537 | DialectType::Spark
9538 | DialectType::Databricks
9539 | DialectType::Redshift => Action::None,
9540 _ => Action::Nvl2Expand,
9541 }
9542 }
9543 Expression::Decode(_) | Expression::DecodeCase(_) => {
9544 // DECODE(a, b, c[, d, e[, ...]]) -> CASE WHEN with null-safe comparisons
9545 // Keep as DECODE for Oracle/Snowflake
9546 match target {
9547 DialectType::Oracle | DialectType::Snowflake => Action::None,
9548 _ => Action::DecodeSimplify,
9549 }
9550 }
9551 Expression::Coalesce(ref cf) => {
9552 // IFNULL(a, b) -> COALESCE(a, b): clear original_name for cross-dialect
9553 // BigQuery keeps IFNULL natively when source is also BigQuery
9554 if cf.original_name.as_deref() == Some("IFNULL")
9555 && !(matches!(source, DialectType::BigQuery)
9556 && matches!(target, DialectType::BigQuery))
9557 {
9558 Action::IfnullToCoalesce
9559 } else {
9560 Action::None
9561 }
9562 }
9563 Expression::IfFunc(if_func) => {
9564 if matches!(source, DialectType::Snowflake)
9565 && matches!(
9566 target,
9567 DialectType::Presto | DialectType::Trino | DialectType::SQLite
9568 )
9569 && matches!(if_func.false_value, Some(Expression::Div(_)))
9570 {
9571 Action::Div0TypedDivision
9572 } else {
9573 Action::None
9574 }
9575 }
9576 Expression::ToJson(_) => match target {
9577 DialectType::Presto | DialectType::Trino => Action::ToJsonConvert,
9578 DialectType::BigQuery => Action::ToJsonConvert,
9579 DialectType::DuckDB => Action::ToJsonConvert,
9580 _ => Action::None,
9581 },
9582 Expression::ArrayAgg(ref agg) => {
9583 if matches!(target, DialectType::MySQL | DialectType::SingleStore) {
9584 Action::ArrayAggToGroupConcat
9585 } else if matches!(
9586 target,
9587 DialectType::Hive | DialectType::Spark | DialectType::Databricks
9588 ) {
9589 // Any source -> Hive/Spark: convert ARRAY_AGG to COLLECT_LIST
9590 Action::ArrayAggToCollectList
9591 } else if matches!(
9592 source,
9593 DialectType::Spark | DialectType::Databricks | DialectType::Hive
9594 ) && matches!(target, DialectType::DuckDB)
9595 && agg.filter.is_some()
9596 {
9597 // Spark/Hive ARRAY_AGG excludes NULLs, DuckDB includes them
9598 // Need to add NOT x IS NULL to existing filter
9599 Action::ArrayAggNullFilter
9600 } else if matches!(target, DialectType::DuckDB)
9601 && agg.ignore_nulls == Some(true)
9602 && !agg.order_by.is_empty()
9603 {
9604 // BigQuery ARRAY_AGG(x IGNORE NULLS ORDER BY ...) -> DuckDB ARRAY_AGG(x ORDER BY a NULLS FIRST, ...)
9605 Action::ArrayAggIgnoreNullsDuckDB
9606 } else if !matches!(source, DialectType::Snowflake) {
9607 Action::None
9608 } else if matches!(target, DialectType::Spark | DialectType::Databricks) {
9609 let is_array_agg = agg.name.as_deref().map_or(false, |n| n.eq_ignore_ascii_case("ARRAY_AGG"))
9610 || agg.name.is_none();
9611 if is_array_agg {
9612 Action::ArrayAggCollectList
9613 } else {
9614 Action::None
9615 }
9616 } else if matches!(
9617 target,
9618 DialectType::DuckDB | DialectType::Presto | DialectType::Trino
9619 ) && agg.filter.is_none()
9620 {
9621 Action::ArrayAggFilter
9622 } else {
9623 Action::None
9624 }
9625 }
9626 Expression::WithinGroup(wg) => {
9627 if matches!(source, DialectType::Snowflake)
9628 && matches!(
9629 target,
9630 DialectType::DuckDB | DialectType::Presto | DialectType::Trino
9631 )
9632 && matches!(wg.this, Expression::ArrayAgg(_))
9633 {
9634 Action::ArrayAggWithinGroupFilter
9635 } else if matches!(&wg.this, Expression::AggregateFunction(af) if af.name.eq_ignore_ascii_case("STRING_AGG"))
9636 || matches!(&wg.this, Expression::Function(f) if f.name.eq_ignore_ascii_case("STRING_AGG"))
9637 || matches!(&wg.this, Expression::StringAgg(_))
9638 {
9639 Action::StringAggConvert
9640 } else if matches!(
9641 target,
9642 DialectType::Presto
9643 | DialectType::Trino
9644 | DialectType::Athena
9645 | DialectType::Spark
9646 | DialectType::Databricks
9647 ) && (matches!(&wg.this, Expression::Function(f) if f.name.eq_ignore_ascii_case("PERCENTILE_CONT") || f.name.eq_ignore_ascii_case("PERCENTILE_DISC"))
9648 || matches!(&wg.this, Expression::AggregateFunction(af) if af.name.eq_ignore_ascii_case("PERCENTILE_CONT") || af.name.eq_ignore_ascii_case("PERCENTILE_DISC"))
9649 || matches!(&wg.this, Expression::PercentileCont(_)))
9650 {
9651 Action::PercentileContConvert
9652 } else {
9653 Action::None
9654 }
9655 }
9656 // For BigQuery: CAST(x AS TIMESTAMP) -> CAST(x AS DATETIME)
9657 // because BigQuery's TIMESTAMP is really TIMESTAMPTZ, and
9658 // DATETIME is the timezone-unaware type
9659 Expression::Cast(ref c) => {
9660 if c.format.is_some()
9661 && (matches!(source, DialectType::BigQuery)
9662 || matches!(source, DialectType::Teradata))
9663 {
9664 Action::BigQueryCastFormat
9665 } else if matches!(target, DialectType::BigQuery)
9666 && !matches!(source, DialectType::BigQuery)
9667 && matches!(
9668 c.to,
9669 DataType::Timestamp {
9670 timezone: false,
9671 ..
9672 }
9673 )
9674 {
9675 Action::CastTimestampToDatetime
9676 } else if matches!(target, DialectType::MySQL | DialectType::StarRocks)
9677 && !matches!(source, DialectType::MySQL | DialectType::StarRocks)
9678 && matches!(
9679 c.to,
9680 DataType::Timestamp {
9681 timezone: false,
9682 ..
9683 }
9684 )
9685 {
9686 // Generic/other -> MySQL/StarRocks: CAST(x AS TIMESTAMP) -> CAST(x AS DATETIME)
9687 // but MySQL-native CAST(x AS TIMESTAMP) stays as TIMESTAMP(x) via transform_cast
9688 Action::CastTimestampToDatetime
9689 } else if matches!(
9690 source,
9691 DialectType::Hive | DialectType::Spark | DialectType::Databricks
9692 ) && matches!(
9693 target,
9694 DialectType::Presto
9695 | DialectType::Trino
9696 | DialectType::Athena
9697 | DialectType::DuckDB
9698 | DialectType::Snowflake
9699 | DialectType::BigQuery
9700 | DialectType::Databricks
9701 | DialectType::TSQL
9702 ) {
9703 Action::HiveCastToTryCast
9704 } else if matches!(c.to, DataType::Timestamp { timezone: true, .. })
9705 && matches!(target, DialectType::MySQL | DialectType::StarRocks)
9706 {
9707 // CAST(x AS TIMESTAMPTZ) -> TIMESTAMP(x) function for MySQL/StarRocks
9708 Action::CastTimestamptzToFunc
9709 } else if matches!(c.to, DataType::Timestamp { timezone: true, .. })
9710 && matches!(
9711 target,
9712 DialectType::Hive
9713 | DialectType::Spark
9714 | DialectType::Databricks
9715 | DialectType::BigQuery
9716 )
9717 {
9718 // CAST(x AS TIMESTAMP WITH TIME ZONE) -> CAST(x AS TIMESTAMP) for Hive/Spark/BigQuery
9719 Action::CastTimestampStripTz
9720 } else if matches!(&c.to, DataType::Json)
9721 && matches!(source, DialectType::DuckDB)
9722 && matches!(target, DialectType::Snowflake)
9723 {
9724 Action::DuckDBCastJsonToVariant
9725 } else if matches!(&c.to, DataType::Json)
9726 && matches!(&c.this, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)))
9727 && matches!(
9728 target,
9729 DialectType::Presto
9730 | DialectType::Trino
9731 | DialectType::Athena
9732 | DialectType::Snowflake
9733 )
9734 {
9735 // CAST('x' AS JSON) -> JSON_PARSE('x') for Presto, PARSE_JSON for Snowflake
9736 // Only when the input is a string literal (JSON 'value' syntax)
9737 Action::JsonLiteralToJsonParse
9738 } else if matches!(&c.to, DataType::Json)
9739 && matches!(source, DialectType::DuckDB)
9740 && matches!(
9741 target,
9742 DialectType::Presto | DialectType::Trino | DialectType::Athena
9743 )
9744 {
9745 // DuckDB's CAST(x AS JSON) parses the string value into a JSON value.
9746 // Trino/Presto/Athena's CAST(x AS JSON) instead wraps the value as a
9747 // JSON string (no parsing) — different semantics. Use JSON_PARSE(x)
9748 // in the target to preserve DuckDB's parse semantics.
9749 Action::JsonLiteralToJsonParse
9750 } else if matches!(&c.to, DataType::Json | DataType::JsonB)
9751 && matches!(target, DialectType::Spark | DialectType::Databricks)
9752 {
9753 // CAST(x AS JSON) -> TO_JSON(x) for Spark
9754 Action::CastToJsonForSpark
9755 } else if (matches!(
9756 &c.to,
9757 DataType::Array { .. } | DataType::Map { .. } | DataType::Struct { .. }
9758 )) && matches!(
9759 target,
9760 DialectType::Spark | DialectType::Databricks
9761 ) && (matches!(&c.this, Expression::ParseJson(_))
9762 || matches!(
9763 &c.this,
9764 Expression::Function(f)
9765 if f.name.eq_ignore_ascii_case("JSON_EXTRACT")
9766 || f.name.eq_ignore_ascii_case("JSON_EXTRACT_SCALAR")
9767 || f.name.eq_ignore_ascii_case("GET_JSON_OBJECT")
9768 ))
9769 {
9770 // CAST(JSON_PARSE(...) AS ARRAY/MAP) or CAST(JSON_EXTRACT/GET_JSON_OBJECT(...) AS ARRAY/MAP)
9771 // -> FROM_JSON(..., type_string) for Spark
9772 Action::CastJsonToFromJson
9773 } else if matches!(target, DialectType::Spark | DialectType::Databricks)
9774 && matches!(
9775 c.to,
9776 DataType::Timestamp {
9777 timezone: false,
9778 ..
9779 }
9780 )
9781 && matches!(source, DialectType::DuckDB)
9782 {
9783 Action::StrftimeCastTimestamp
9784 } else if matches!(source, DialectType::DuckDB)
9785 && matches!(
9786 c.to,
9787 DataType::Decimal {
9788 precision: None,
9789 ..
9790 }
9791 )
9792 {
9793 Action::DecimalDefaultPrecision
9794 } else if matches!(source, DialectType::MySQL | DialectType::SingleStore)
9795 && matches!(c.to, DataType::Char { length: None })
9796 && !matches!(target, DialectType::MySQL | DialectType::SingleStore)
9797 {
9798 // MySQL CAST(x AS CHAR) was originally TEXT - convert to target text type
9799 Action::MysqlCastCharToText
9800 } else if matches!(
9801 source,
9802 DialectType::Spark | DialectType::Databricks | DialectType::Hive
9803 ) && matches!(
9804 target,
9805 DialectType::Spark | DialectType::Databricks | DialectType::Hive
9806 ) && Self::has_varchar_char_type(&c.to)
9807 {
9808 // Spark parses VARCHAR(n)/CHAR(n) as TEXT, so normalize back to STRING
9809 Action::SparkCastVarcharToString
9810 } else {
9811 Action::None
9812 }
9813 }
9814 Expression::SafeCast(ref c) => {
9815 if c.format.is_some()
9816 && matches!(source, DialectType::BigQuery)
9817 && !matches!(target, DialectType::BigQuery)
9818 {
9819 Action::BigQueryCastFormat
9820 } else {
9821 Action::None
9822 }
9823 }
9824 Expression::TryCast(ref c) => {
9825 if matches!(&c.to, DataType::Json)
9826 && matches!(source, DialectType::DuckDB)
9827 && matches!(
9828 target,
9829 DialectType::Presto | DialectType::Trino | DialectType::Athena
9830 )
9831 {
9832 // DuckDB's TRY_CAST(x AS JSON) tries to parse x as JSON, returning
9833 // NULL on parse failure. Trino/Presto/Athena's TRY_CAST(x AS JSON)
9834 // wraps the value as a JSON string (no parse). Emit TRY(JSON_PARSE(x))
9835 // to preserve DuckDB's parse-or-null semantics.
9836 Action::DuckDBTryCastJsonToTryJsonParse
9837 } else {
9838 Action::None
9839 }
9840 }
9841 Expression::JSONArray(ref ja)
9842 if matches!(target, DialectType::Snowflake)
9843 && ja.null_handling.is_none()
9844 && ja.return_type.is_none()
9845 && ja.strict.is_none() =>
9846 {
9847 Action::GenericFunctionNormalize
9848 }
9849 Expression::JsonArray(_) if matches!(target, DialectType::Snowflake) => {
9850 Action::GenericFunctionNormalize
9851 }
9852 // For DuckDB: DATE_TRUNC should preserve the input type
9853 Expression::DateTrunc(_) | Expression::TimestampTrunc(_) => {
9854 if matches!(source, DialectType::Snowflake)
9855 && matches!(target, DialectType::DuckDB)
9856 {
9857 Action::DateTruncWrapCast
9858 } else {
9859 Action::None
9860 }
9861 }
9862 // For DuckDB: SET a = 1 -> SET VARIABLE a = 1
9863 Expression::SetStatement(s) => {
9864 if matches!(target, DialectType::DuckDB)
9865 && !matches!(source, DialectType::TSQL | DialectType::Fabric)
9866 && s.items.iter().any(|item| item.kind.is_none())
9867 {
9868 Action::SetToVariable
9869 } else {
9870 Action::None
9871 }
9872 }
9873 // Cross-dialect NULL ordering normalization.
9874 // When nulls_first is not specified, fill in the source dialect's implied
9875 // default so the target generator can correctly add/strip NULLS FIRST/LAST.
9876 Expression::Ordered(o) => {
9877 // MySQL doesn't support NULLS FIRST/LAST - strip or rewrite
9878 if matches!(target, DialectType::MySQL) && o.nulls_first.is_some() {
9879 Action::MysqlNullsOrdering
9880 } else {
9881 // Skip targets that don't support NULLS FIRST/LAST syntax unless
9882 // the generator can preserve semantics with a CASE sort key.
9883 let target_rewrites_nulls =
9884 matches!(target, DialectType::TSQL | DialectType::Fabric);
9885 let target_supports_nulls = !matches!(
9886 target,
9887 DialectType::MySQL
9888 | DialectType::TSQL
9889 | DialectType::Fabric
9890 | DialectType::StarRocks
9891 | DialectType::Doris
9892 );
9893 if o.nulls_first.is_none()
9894 && source != target
9895 && (target_supports_nulls || target_rewrites_nulls)
9896 {
9897 Action::NullsOrdering
9898 } else {
9899 Action::None
9900 }
9901 }
9902 }
9903 // BigQuery data types: convert INT64, BYTES, NUMERIC etc. to standard types
9904 Expression::DataType(dt) => {
9905 if matches!(source, DialectType::BigQuery)
9906 && !matches!(target, DialectType::BigQuery)
9907 {
9908 match dt {
9909 DataType::Custom { ref name }
9910 if name.eq_ignore_ascii_case("INT64")
9911 || name.eq_ignore_ascii_case("FLOAT64")
9912 || name.eq_ignore_ascii_case("BOOL")
9913 || name.eq_ignore_ascii_case("BYTES")
9914 || name.eq_ignore_ascii_case("NUMERIC")
9915 || name.eq_ignore_ascii_case("STRING")
9916 || name.eq_ignore_ascii_case("DATETIME") =>
9917 {
9918 Action::BigQueryCastType
9919 }
9920 _ => Action::None,
9921 }
9922 } else if matches!(source, DialectType::TSQL) {
9923 // For TSQL source -> any target (including TSQL itself for REAL)
9924 match dt {
9925 // REAL -> FLOAT even for TSQL->TSQL
9926 DataType::Custom { ref name }
9927 if name.eq_ignore_ascii_case("REAL") =>
9928 {
9929 Action::TSQLTypeNormalize
9930 }
9931 DataType::Float {
9932 real_spelling: true,
9933 ..
9934 } => Action::TSQLTypeNormalize,
9935 // Other TSQL type normalizations only for non-TSQL targets
9936 DataType::Custom { ref name }
9937 if !matches!(target, DialectType::TSQL)
9938 && (name.eq_ignore_ascii_case("MONEY")
9939 || name.eq_ignore_ascii_case("SMALLMONEY")
9940 || name.eq_ignore_ascii_case("DATETIME2")
9941 || name.eq_ignore_ascii_case("IMAGE")
9942 || name.eq_ignore_ascii_case("BIT")
9943 || name.eq_ignore_ascii_case("ROWVERSION")
9944 || name.eq_ignore_ascii_case("UNIQUEIDENTIFIER")
9945 || name.eq_ignore_ascii_case("DATETIMEOFFSET")
9946 || (name.len() >= 7 && name[..7].eq_ignore_ascii_case("NUMERIC"))
9947 || (name.len() >= 10 && name[..10].eq_ignore_ascii_case("DATETIME2("))
9948 || (name.len() >= 5 && name[..5].eq_ignore_ascii_case("TIME("))) =>
9949 {
9950 Action::TSQLTypeNormalize
9951 }
9952 DataType::Float {
9953 precision: Some(_), ..
9954 } if !matches!(target, DialectType::TSQL) => {
9955 Action::TSQLTypeNormalize
9956 }
9957 DataType::TinyInt { .. }
9958 if !matches!(target, DialectType::TSQL) =>
9959 {
9960 Action::TSQLTypeNormalize
9961 }
9962 // INTEGER -> INT for Databricks/Spark targets
9963 DataType::Int {
9964 integer_spelling: true,
9965 ..
9966 } if matches!(
9967 target,
9968 DialectType::Databricks | DialectType::Spark
9969 ) =>
9970 {
9971 Action::TSQLTypeNormalize
9972 }
9973 _ => Action::None,
9974 }
9975 } else if (matches!(source, DialectType::Oracle)
9976 || matches!(source, DialectType::Generic))
9977 && !matches!(target, DialectType::Oracle)
9978 {
9979 match dt {
9980 DataType::Custom { ref name }
9981 if (name.len() >= 9 && name[..9].eq_ignore_ascii_case("VARCHAR2("))
9982 || (name.len() >= 10 && name[..10].eq_ignore_ascii_case("NVARCHAR2("))
9983 || name.eq_ignore_ascii_case("VARCHAR2")
9984 || name.eq_ignore_ascii_case("NVARCHAR2") =>
9985 {
9986 Action::OracleVarchar2ToVarchar
9987 }
9988 _ => Action::None,
9989 }
9990 } else if matches!(target, DialectType::Snowflake)
9991 && !matches!(source, DialectType::Snowflake)
9992 {
9993 // When target is Snowflake but source is NOT Snowflake,
9994 // protect FLOAT from being converted to DOUBLE by Snowflake's transform.
9995 // Snowflake treats FLOAT=DOUBLE internally, but non-Snowflake sources
9996 // should keep their FLOAT spelling.
9997 match dt {
9998 DataType::Float { .. } => Action::SnowflakeFloatProtect,
9999 _ => Action::None,
10000 }
10001 } else {
10002 Action::None
10003 }
10004 }
10005 // LOWER patterns from BigQuery TO_HEX conversions:
10006 // - LOWER(LOWER(HEX(x))) from non-BQ targets: flatten
10007 // - LOWER(Function("TO_HEX")) for BQ->BQ: strip LOWER
10008 Expression::Lower(uf) => {
10009 if matches!(source, DialectType::BigQuery) {
10010 match &uf.this {
10011 Expression::Lower(_) => Action::BigQueryToHexLower,
10012 Expression::Function(f)
10013 if f.name == "TO_HEX"
10014 && matches!(target, DialectType::BigQuery) =>
10015 {
10016 // BQ->BQ: LOWER(TO_HEX(x)) -> TO_HEX(x)
10017 Action::BigQueryToHexLower
10018 }
10019 _ => Action::None,
10020 }
10021 } else {
10022 Action::None
10023 }
10024 }
10025 // UPPER patterns from BigQuery TO_HEX conversions:
10026 // - UPPER(LOWER(HEX(x))) from non-BQ targets: extract inner
10027 // - UPPER(Function("TO_HEX")) for BQ->BQ: keep as UPPER(TO_HEX(x))
10028 Expression::Upper(uf) => {
10029 if matches!(source, DialectType::BigQuery) {
10030 match &uf.this {
10031 Expression::Lower(_) => Action::BigQueryToHexUpper,
10032 _ => Action::None,
10033 }
10034 } else {
10035 Action::None
10036 }
10037 }
10038 // BigQuery LAST_DAY(date, unit) -> strip unit for non-BigQuery targets
10039 // Snowflake supports LAST_DAY with unit, so keep it there
10040 Expression::LastDay(ld) => {
10041 if matches!(source, DialectType::BigQuery)
10042 && !matches!(target, DialectType::BigQuery | DialectType::Snowflake)
10043 && ld.unit.is_some()
10044 {
10045 Action::BigQueryLastDayStripUnit
10046 } else {
10047 Action::None
10048 }
10049 }
10050 // BigQuery SafeDivide expressions (already parsed as SafeDivide)
10051 Expression::SafeDivide(_) => {
10052 if matches!(source, DialectType::BigQuery)
10053 && !matches!(target, DialectType::BigQuery)
10054 {
10055 Action::BigQuerySafeDivide
10056 } else {
10057 Action::None
10058 }
10059 }
10060 // BigQuery ANY_VALUE(x HAVING MAX/MIN y) -> ARG_MAX_NULL/ARG_MIN_NULL for DuckDB
10061 // ANY_VALUE(x) -> ANY_VALUE(x) IGNORE NULLS for Spark
10062 Expression::AnyValue(ref agg) => {
10063 if matches!(source, DialectType::BigQuery)
10064 && matches!(target, DialectType::DuckDB)
10065 && agg.having_max.is_some()
10066 {
10067 Action::BigQueryAnyValueHaving
10068 } else if matches!(target, DialectType::Spark | DialectType::Databricks)
10069 && !matches!(source, DialectType::Spark | DialectType::Databricks)
10070 && agg.ignore_nulls.is_none()
10071 {
10072 Action::AnyValueIgnoreNulls
10073 } else {
10074 Action::None
10075 }
10076 }
10077 Expression::Any(ref q) => {
10078 if matches!(source, DialectType::PostgreSQL)
10079 && matches!(
10080 target,
10081 DialectType::Spark | DialectType::Databricks | DialectType::Hive
10082 )
10083 && q.op.is_some()
10084 && !matches!(
10085 q.subquery,
10086 Expression::Select(_) | Expression::Subquery(_)
10087 )
10088 {
10089 Action::AnyToExists
10090 } else {
10091 Action::None
10092 }
10093 }
10094 // BigQuery APPROX_QUANTILES(x, n) -> APPROX_QUANTILE(x, [quantiles]) for DuckDB
10095 // Snowflake RLIKE does full-string match; DuckDB REGEXP_FULL_MATCH also does full-string match
10096 Expression::RegexpLike(_)
10097 if matches!(source, DialectType::Snowflake)
10098 && matches!(target, DialectType::DuckDB) =>
10099 {
10100 Action::RlikeSnowflakeToDuckDB
10101 }
10102 // RegexpLike from non-DuckDB/non-Snowflake sources -> REGEXP_MATCHES for DuckDB target
10103 Expression::RegexpLike(_)
10104 if !matches!(source, DialectType::DuckDB)
10105 && matches!(target, DialectType::DuckDB) =>
10106 {
10107 Action::RegexpLikeToDuckDB
10108 }
10109 // RegexpLike -> Exasol: anchor pattern with .*...*
10110 Expression::RegexpLike(_)
10111 if matches!(target, DialectType::Exasol) =>
10112 {
10113 Action::RegexpLikeExasolAnchor
10114 }
10115 // Safe-division source -> non-safe target: NULLIF wrapping and/or CAST
10116 // Safe-division dialects: MySQL, DuckDB, SingleStore, TiDB, ClickHouse, Doris
10117 Expression::Div(ref op)
10118 if matches!(
10119 source,
10120 DialectType::MySQL
10121 | DialectType::DuckDB
10122 | DialectType::SingleStore
10123 | DialectType::TiDB
10124 | DialectType::ClickHouse
10125 | DialectType::Doris
10126 ) && matches!(
10127 target,
10128 DialectType::PostgreSQL
10129 | DialectType::Redshift
10130 | DialectType::Drill
10131 | DialectType::Trino
10132 | DialectType::Presto
10133 | DialectType::Athena
10134 | DialectType::TSQL
10135 | DialectType::Teradata
10136 | DialectType::SQLite
10137 | DialectType::BigQuery
10138 | DialectType::Snowflake
10139 | DialectType::Databricks
10140 | DialectType::Oracle
10141 | DialectType::Materialize
10142 | DialectType::RisingWave
10143 ) =>
10144 {
10145 // Only wrap if RHS is not already NULLIF
10146 if !matches!(&op.right, Expression::Function(f) if f.name.eq_ignore_ascii_case("NULLIF"))
10147 {
10148 Action::MySQLSafeDivide
10149 } else {
10150 Action::None
10151 }
10152 }
10153 // ALTER TABLE ... RENAME TO <schema>.<table> -> strip schema for most targets
10154 // For TSQL/Fabric, convert to sp_rename instead
10155 Expression::AlterTable(ref at) if !at.actions.is_empty() => {
10156 if let Some(crate::expressions::AlterTableAction::RenameTable(
10157 ref new_tbl,
10158 )) = at.actions.first()
10159 {
10160 if matches!(target, DialectType::TSQL | DialectType::Fabric) {
10161 // TSQL: ALTER TABLE RENAME -> EXEC sp_rename
10162 Action::AlterTableToSpRename
10163 } else if new_tbl.schema.is_some()
10164 && matches!(
10165 target,
10166 DialectType::BigQuery
10167 | DialectType::Doris
10168 | DialectType::StarRocks
10169 | DialectType::DuckDB
10170 | DialectType::PostgreSQL
10171 | DialectType::Redshift
10172 )
10173 {
10174 Action::AlterTableRenameStripSchema
10175 } else {
10176 Action::None
10177 }
10178 } else {
10179 Action::None
10180 }
10181 }
10182 // EPOCH(x) expression -> target-specific epoch conversion
10183 Expression::Epoch(_) if !matches!(target, DialectType::DuckDB) => {
10184 Action::EpochConvert
10185 }
10186 // EPOCH_MS(x) expression -> target-specific epoch ms conversion
10187 Expression::EpochMs(_) if !matches!(target, DialectType::DuckDB) => {
10188 Action::EpochMsConvert
10189 }
10190 // STRING_AGG -> GROUP_CONCAT for MySQL/SQLite
10191 Expression::StringAgg(_) => {
10192 if matches!(
10193 target,
10194 DialectType::MySQL
10195 | DialectType::SingleStore
10196 | DialectType::Doris
10197 | DialectType::StarRocks
10198 | DialectType::SQLite
10199 ) {
10200 Action::StringAggConvert
10201 } else if matches!(target, DialectType::Spark | DialectType::Databricks) {
10202 Action::StringAggConvert
10203 } else {
10204 Action::None
10205 }
10206 }
10207 Expression::CombinedParameterizedAgg(_) => Action::GenericFunctionNormalize,
10208 // GROUP_CONCAT -> STRING_AGG for PostgreSQL/Presto/etc.
10209 // Also handles GROUP_CONCAT normalization for MySQL/SQLite targets
10210 Expression::GroupConcat(_) => Action::GroupConcatConvert,
10211 // CARDINALITY/ARRAY_LENGTH/ARRAY_SIZE -> target-specific array length
10212 // DuckDB CARDINALITY -> keep as CARDINALITY for DuckDB target (used for maps)
10213 Expression::Cardinality(_)
10214 if matches!(source, DialectType::DuckDB)
10215 && matches!(target, DialectType::DuckDB) =>
10216 {
10217 Action::None
10218 }
10219 Expression::Cardinality(_) | Expression::ArrayLength(_) => {
10220 Action::ArrayLengthConvert
10221 }
10222 Expression::ArraySize(_) => {
10223 if matches!(target, DialectType::Drill) {
10224 Action::ArraySizeDrill
10225 } else {
10226 Action::ArrayLengthConvert
10227 }
10228 }
10229 // ARRAY_REMOVE(arr, target) -> LIST_FILTER/arrayFilter/ARRAY subquery
10230 Expression::ArrayRemove(_) => match target {
10231 DialectType::DuckDB | DialectType::ClickHouse | DialectType::BigQuery => {
10232 Action::ArrayRemoveConvert
10233 }
10234 _ => Action::None,
10235 },
10236 // ARRAY_REVERSE(x) -> arrayReverse for ClickHouse
10237 Expression::ArrayReverse(_) => match target {
10238 DialectType::ClickHouse => Action::ArrayReverseConvert,
10239 _ => Action::None,
10240 },
10241 // JSON_KEYS(x) -> JSON_OBJECT_KEYS/OBJECT_KEYS for Spark/Databricks/Snowflake
10242 Expression::JsonKeys(_) => match target {
10243 DialectType::Spark | DialectType::Databricks | DialectType::Snowflake => {
10244 Action::JsonKeysConvert
10245 }
10246 _ => Action::None,
10247 },
10248 // PARSE_JSON(x) -> strip for SQLite/Doris/MySQL/StarRocks
10249 Expression::ParseJson(_) => match target {
10250 DialectType::SQLite
10251 | DialectType::Doris
10252 | DialectType::MySQL
10253 | DialectType::StarRocks => Action::ParseJsonStrip,
10254 _ => Action::None,
10255 },
10256 // WeekOfYear -> WEEKISO for Snowflake (cross-dialect only)
10257 Expression::WeekOfYear(_)
10258 if matches!(target, DialectType::Snowflake)
10259 && !matches!(source, DialectType::Snowflake) =>
10260 {
10261 Action::WeekOfYearToWeekIso
10262 }
10263 // NVL: clear original_name so generator uses dialect-specific function names
10264 Expression::Nvl(f) if f.original_name.is_some() => Action::NvlClearOriginal,
10265 // XOR: expand for dialects that don't support the XOR keyword
10266 Expression::Xor(_) => {
10267 let target_supports_xor = matches!(
10268 target,
10269 DialectType::MySQL
10270 | DialectType::SingleStore
10271 | DialectType::Doris
10272 | DialectType::StarRocks
10273 );
10274 if !target_supports_xor {
10275 Action::XorExpand
10276 } else {
10277 Action::None
10278 }
10279 }
10280 // TSQL #table -> temp table normalization (CREATE TABLE)
10281 Expression::CreateTable(ct)
10282 if matches!(source, DialectType::TSQL | DialectType::Fabric)
10283 && !matches!(target, DialectType::TSQL | DialectType::Fabric)
10284 && ct.name.name.name.starts_with('#') =>
10285 {
10286 Action::TempTableHash
10287 }
10288 // TSQL #table -> strip # from table references in SELECT/etc.
10289 Expression::Table(tr)
10290 if matches!(source, DialectType::TSQL | DialectType::Fabric)
10291 && !matches!(target, DialectType::TSQL | DialectType::Fabric)
10292 && tr.name.name.starts_with('#') =>
10293 {
10294 Action::TempTableHash
10295 }
10296 // TSQL #table -> strip # from DROP TABLE names
10297 Expression::DropTable(ref dt)
10298 if matches!(source, DialectType::TSQL | DialectType::Fabric)
10299 && !matches!(target, DialectType::TSQL | DialectType::Fabric)
10300 && dt.names.iter().any(|n| n.name.name.starts_with('#')) =>
10301 {
10302 Action::TempTableHash
10303 }
10304 // JSON_EXTRACT -> ISNULL(JSON_QUERY, JSON_VALUE) for TSQL
10305 Expression::JsonExtract(_)
10306 if matches!(target, DialectType::TSQL | DialectType::Fabric) =>
10307 {
10308 Action::JsonExtractToTsql
10309 }
10310 // JSON_EXTRACT_SCALAR -> ISNULL(JSON_QUERY, JSON_VALUE) for TSQL
10311 Expression::JsonExtractScalar(_)
10312 if matches!(target, DialectType::TSQL | DialectType::Fabric) =>
10313 {
10314 Action::JsonExtractToTsql
10315 }
10316 // JSON_EXTRACT -> JSONExtractString for ClickHouse
10317 Expression::JsonExtract(_) if matches!(target, DialectType::ClickHouse) => {
10318 Action::JsonExtractToClickHouse
10319 }
10320 // JSON_EXTRACT_SCALAR -> JSONExtractString for ClickHouse
10321 Expression::JsonExtractScalar(_)
10322 if matches!(target, DialectType::ClickHouse) =>
10323 {
10324 Action::JsonExtractToClickHouse
10325 }
10326 // JSON_EXTRACT -> arrow syntax for SQLite/DuckDB
10327 Expression::JsonExtract(ref f)
10328 if !f.arrow_syntax
10329 && matches!(target, DialectType::SQLite | DialectType::DuckDB) =>
10330 {
10331 Action::JsonExtractToArrow
10332 }
10333 // JSON_EXTRACT with JSONPath -> JSON_EXTRACT_PATH for PostgreSQL (non-PG sources only)
10334 Expression::JsonExtract(ref f)
10335 if matches!(target, DialectType::PostgreSQL | DialectType::Redshift)
10336 && !matches!(
10337 source,
10338 DialectType::PostgreSQL
10339 | DialectType::Redshift
10340 | DialectType::Materialize
10341 )
10342 && matches!(&f.path, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(s) if s.starts_with('$'))) =>
10343 {
10344 Action::JsonExtractToGetJsonObject
10345 }
10346 // JSON_EXTRACT -> GET_JSON_OBJECT for Hive/Spark
10347 Expression::JsonExtract(_)
10348 if matches!(
10349 target,
10350 DialectType::Hive | DialectType::Spark | DialectType::Databricks
10351 ) =>
10352 {
10353 Action::JsonExtractToGetJsonObject
10354 }
10355 // JSON_EXTRACT_SCALAR -> target-specific for PostgreSQL, Snowflake, SQLite
10356 // Skip if already in arrow/hash_arrow syntax (same-dialect identity case)
10357 Expression::JsonExtractScalar(ref f)
10358 if !f.arrow_syntax
10359 && !f.hash_arrow_syntax
10360 && matches!(
10361 target,
10362 DialectType::PostgreSQL
10363 | DialectType::Redshift
10364 | DialectType::Snowflake
10365 | DialectType::SQLite
10366 | DialectType::DuckDB
10367 ) =>
10368 {
10369 Action::JsonExtractScalarConvert
10370 }
10371 // JSON_EXTRACT_SCALAR -> GET_JSON_OBJECT for Hive/Spark
10372 Expression::JsonExtractScalar(_)
10373 if matches!(
10374 target,
10375 DialectType::Hive | DialectType::Spark | DialectType::Databricks
10376 ) =>
10377 {
10378 Action::JsonExtractScalarToGetJsonObject
10379 }
10380 // JSON_EXTRACT path normalization for BigQuery, MySQL (bracket/wildcard handling)
10381 Expression::JsonExtract(ref f)
10382 if !f.arrow_syntax
10383 && matches!(target, DialectType::BigQuery | DialectType::MySQL) =>
10384 {
10385 Action::JsonPathNormalize
10386 }
10387 // JsonQuery (parsed JSON_QUERY) -> target-specific
10388 Expression::JsonQuery(_) => Action::JsonQueryValueConvert,
10389 // JsonValue (parsed JSON_VALUE) -> target-specific
10390 Expression::JsonValue(_) => Action::JsonQueryValueConvert,
10391 // AT TIME ZONE -> AT_TIMEZONE for Presto, FROM_UTC_TIMESTAMP for Spark,
10392 // TIMESTAMP(DATETIME(...)) for BigQuery, CONVERT_TIMEZONE for Snowflake
10393 Expression::AtTimeZone(_)
10394 if matches!(
10395 target,
10396 DialectType::Presto
10397 | DialectType::Trino
10398 | DialectType::Athena
10399 | DialectType::Spark
10400 | DialectType::Databricks
10401 | DialectType::BigQuery
10402 | DialectType::Snowflake
10403 ) =>
10404 {
10405 Action::AtTimeZoneConvert
10406 }
10407 // DAY_OF_WEEK -> dialect-specific
10408 Expression::DayOfWeek(_)
10409 if matches!(
10410 target,
10411 DialectType::DuckDB | DialectType::Spark | DialectType::Databricks
10412 ) =>
10413 {
10414 Action::DayOfWeekConvert
10415 }
10416 // CURRENT_USER -> CURRENT_USER() for Snowflake
10417 Expression::CurrentUser(_) if matches!(target, DialectType::Snowflake) => {
10418 Action::CurrentUserParens
10419 }
10420 // ELEMENT_AT(arr, idx) -> arr[idx] for PostgreSQL, arr[SAFE_ORDINAL(idx)] for BigQuery
10421 Expression::ElementAt(_)
10422 if matches!(target, DialectType::PostgreSQL | DialectType::BigQuery) =>
10423 {
10424 Action::ElementAtConvert
10425 }
10426 // ARRAY[...] (ArrayFunc bracket_notation=false) -> convert for target dialect
10427 Expression::ArrayFunc(ref arr)
10428 if !arr.bracket_notation
10429 && matches!(
10430 target,
10431 DialectType::Spark
10432 | DialectType::Databricks
10433 | DialectType::Hive
10434 | DialectType::BigQuery
10435 | DialectType::DuckDB
10436 | DialectType::Snowflake
10437 | DialectType::Presto
10438 | DialectType::Trino
10439 | DialectType::Athena
10440 | DialectType::ClickHouse
10441 | DialectType::StarRocks
10442 ) =>
10443 {
10444 Action::ArraySyntaxConvert
10445 }
10446 // VARIANCE expression -> varSamp for ClickHouse
10447 Expression::Variance(_) if matches!(target, DialectType::ClickHouse) => {
10448 Action::VarianceToClickHouse
10449 }
10450 // STDDEV expression -> stddevSamp for ClickHouse
10451 Expression::Stddev(_) if matches!(target, DialectType::ClickHouse) => {
10452 Action::StddevToClickHouse
10453 }
10454 // ApproxQuantile -> APPROX_PERCENTILE for Snowflake
10455 Expression::ApproxQuantile(_) if matches!(target, DialectType::Snowflake) => {
10456 Action::ApproxQuantileConvert
10457 }
10458 // MonthsBetween -> target-specific
10459 Expression::MonthsBetween(_)
10460 if !matches!(
10461 target,
10462 DialectType::Spark | DialectType::Databricks | DialectType::Hive
10463 ) =>
10464 {
10465 Action::MonthsBetweenConvert
10466 }
10467 // AddMonths -> target-specific DATEADD/DATE_ADD
10468 Expression::AddMonths(_) => Action::AddMonthsConvert,
10469 // MapFromArrays -> target-specific (MAP, OBJECT_CONSTRUCT, MAP_FROM_ARRAYS)
10470 Expression::MapFromArrays(_)
10471 if !matches!(target, DialectType::Spark | DialectType::Databricks) =>
10472 {
10473 Action::MapFromArraysConvert
10474 }
10475 // CURRENT_USER -> CURRENT_USER() for Spark
10476 Expression::CurrentUser(_)
10477 if matches!(target, DialectType::Spark | DialectType::Databricks) =>
10478 {
10479 Action::CurrentUserSparkParens
10480 }
10481 // MONTH/YEAR/DAY('string') from Spark -> cast string to DATE for DuckDB/Presto
10482 Expression::Month(ref f) | Expression::Year(ref f) | Expression::Day(ref f)
10483 if matches!(
10484 source,
10485 DialectType::Spark | DialectType::Databricks | DialectType::Hive
10486 ) && matches!(&f.this, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)))
10487 && matches!(
10488 target,
10489 DialectType::DuckDB
10490 | DialectType::Presto
10491 | DialectType::Trino
10492 | DialectType::Athena
10493 | DialectType::PostgreSQL
10494 | DialectType::Redshift
10495 ) =>
10496 {
10497 Action::SparkDateFuncCast
10498 }
10499 // $parameter -> @parameter for BigQuery
10500 Expression::Parameter(ref p)
10501 if matches!(target, DialectType::BigQuery)
10502 && matches!(source, DialectType::DuckDB)
10503 && (p.style == crate::expressions::ParameterStyle::Dollar
10504 || p.style == crate::expressions::ParameterStyle::DoubleDollar) =>
10505 {
10506 Action::DollarParamConvert
10507 }
10508 // EscapeString literal: normalize literal newlines to \n
10509 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::EscapeString(ref s) if s.contains('\n') || s.contains('\r') || s.contains('\t'))
10510 =>
10511 {
10512 Action::EscapeStringNormalize
10513 }
10514 // straight_join: keep lowercase for DuckDB, quote for MySQL
10515 Expression::Column(ref col)
10516 if col.name.name == "STRAIGHT_JOIN"
10517 && col.table.is_none()
10518 && matches!(source, DialectType::DuckDB)
10519 && matches!(target, DialectType::DuckDB | DialectType::MySQL) =>
10520 {
10521 Action::StraightJoinCase
10522 }
10523 // DATE and TIMESTAMP literal type conversions are now handled in the generator directly
10524 // Snowflake INTERVAL format: INTERVAL '2' HOUR -> INTERVAL '2 HOUR'
10525 Expression::Interval(ref iv)
10526 if matches!(
10527 target,
10528 DialectType::Snowflake
10529 | DialectType::PostgreSQL
10530 | DialectType::Redshift
10531 ) && iv.unit.is_some()
10532 && iv.this.as_ref().map_or(false, |t| matches!(t, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)))) =>
10533 {
10534 Action::SnowflakeIntervalFormat
10535 }
10536 // TABLESAMPLE -> TABLESAMPLE RESERVOIR for DuckDB target
10537 Expression::TableSample(ref ts) if matches!(target, DialectType::DuckDB) => {
10538 if let Some(ref sample) = ts.sample {
10539 if !sample.explicit_method {
10540 Action::TablesampleReservoir
10541 } else {
10542 Action::None
10543 }
10544 } else {
10545 Action::None
10546 }
10547 }
10548 // TABLESAMPLE from non-Snowflake source to Snowflake: strip method and PERCENT
10549 // Handles both Expression::TableSample wrapper and Expression::Table with table_sample
10550 Expression::TableSample(ref ts)
10551 if matches!(target, DialectType::Snowflake)
10552 && !matches!(source, DialectType::Snowflake)
10553 && ts.sample.is_some() =>
10554 {
10555 if let Some(ref sample) = ts.sample {
10556 if !sample.explicit_method {
10557 Action::TablesampleSnowflakeStrip
10558 } else {
10559 Action::None
10560 }
10561 } else {
10562 Action::None
10563 }
10564 }
10565 Expression::Table(ref t)
10566 if matches!(target, DialectType::Snowflake)
10567 && !matches!(source, DialectType::Snowflake)
10568 && t.table_sample.is_some() =>
10569 {
10570 if let Some(ref sample) = t.table_sample {
10571 if !sample.explicit_method {
10572 Action::TablesampleSnowflakeStrip
10573 } else {
10574 Action::None
10575 }
10576 } else {
10577 Action::None
10578 }
10579 }
10580 // ALTER TABLE RENAME -> EXEC sp_rename for TSQL
10581 Expression::AlterTable(ref at)
10582 if matches!(target, DialectType::TSQL | DialectType::Fabric)
10583 && !at.actions.is_empty()
10584 && matches!(
10585 at.actions.first(),
10586 Some(crate::expressions::AlterTableAction::RenameTable(_))
10587 ) =>
10588 {
10589 Action::AlterTableToSpRename
10590 }
10591 // Subscript index: 1-based to 0-based for BigQuery/Hive/Spark
10592 Expression::Subscript(ref sub)
10593 if matches!(
10594 target,
10595 DialectType::BigQuery
10596 | DialectType::Hive
10597 | DialectType::Spark
10598 | DialectType::Databricks
10599 ) && matches!(
10600 source,
10601 DialectType::DuckDB
10602 | DialectType::PostgreSQL
10603 | DialectType::Presto
10604 | DialectType::Trino
10605 | DialectType::Redshift
10606 | DialectType::ClickHouse
10607 ) && matches!(&sub.index, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(ref n) if n.parse::<i64>().unwrap_or(0) > 0)) =>
10608 {
10609 Action::ArrayIndexConvert
10610 }
10611 // ANY_VALUE IGNORE NULLS detection moved to the AnyValue arm above
10612 // MysqlNullsOrdering for Ordered is now handled in the Ordered arm above
10613 // RESPECT NULLS handling for SQLite (strip it, add NULLS LAST to ORDER BY)
10614 // and for MySQL (rewrite ORDER BY with CASE WHEN for null ordering)
10615 Expression::WindowFunction(ref wf) => {
10616 // BigQuery doesn't support NULLS FIRST/LAST in window function ORDER BY
10617 // EXCEPT for ROW_NUMBER which keeps NULLS LAST
10618 let is_row_number = matches!(wf.this, Expression::RowNumber(_));
10619 if matches!(target, DialectType::BigQuery)
10620 && !is_row_number
10621 && !wf.over.order_by.is_empty()
10622 && wf.over.order_by.iter().any(|o| o.nulls_first.is_some())
10623 {
10624 Action::BigQueryNullsOrdering
10625 // DuckDB -> MySQL: Add CASE WHEN for NULLS LAST simulation in window ORDER BY
10626 // But NOT when frame is RANGE/GROUPS, since adding CASE WHEN would break value-based frames
10627 } else {
10628 let source_nulls_last = matches!(source, DialectType::DuckDB);
10629 let has_range_frame = wf.over.frame.as_ref().map_or(false, |f| {
10630 matches!(
10631 f.kind,
10632 crate::expressions::WindowFrameKind::Range
10633 | crate::expressions::WindowFrameKind::Groups
10634 )
10635 });
10636 if source_nulls_last
10637 && matches!(target, DialectType::MySQL)
10638 && !wf.over.order_by.is_empty()
10639 && wf.over.order_by.iter().any(|o| !o.desc)
10640 && !has_range_frame
10641 {
10642 Action::MysqlNullsLastRewrite
10643 } else {
10644 // Check for Snowflake window frame handling for FIRST_VALUE/LAST_VALUE/NTH_VALUE
10645 let is_ranking_window_func = matches!(
10646 &wf.this,
10647 Expression::FirstValue(_)
10648 | Expression::LastValue(_)
10649 | Expression::NthValue(_)
10650 );
10651 let has_full_unbounded_frame = wf.over.frame.as_ref().map_or(false, |f| {
10652 matches!(f.kind, crate::expressions::WindowFrameKind::Rows)
10653 && matches!(f.start, crate::expressions::WindowFrameBound::UnboundedPreceding)
10654 && matches!(f.end, Some(crate::expressions::WindowFrameBound::UnboundedFollowing))
10655 && f.exclude.is_none()
10656 });
10657 if is_ranking_window_func && matches!(source, DialectType::Snowflake) {
10658 if has_full_unbounded_frame && matches!(target, DialectType::Snowflake) {
10659 // Strip the default frame for Snowflake target
10660 Action::SnowflakeWindowFrameStrip
10661 } else if !has_full_unbounded_frame && wf.over.frame.is_none() && !matches!(target, DialectType::Snowflake) {
10662 // Add default frame for non-Snowflake target
10663 Action::SnowflakeWindowFrameAdd
10664 } else {
10665 match &wf.this {
10666 Expression::FirstValue(ref vf)
10667 | Expression::LastValue(ref vf)
10668 if vf.ignore_nulls == Some(false) =>
10669 {
10670 match target {
10671 DialectType::SQLite => Action::RespectNullsConvert,
10672 _ => Action::None,
10673 }
10674 }
10675 _ => Action::None,
10676 }
10677 }
10678 } else {
10679 match &wf.this {
10680 Expression::FirstValue(ref vf)
10681 | Expression::LastValue(ref vf)
10682 if vf.ignore_nulls == Some(false) =>
10683 {
10684 // RESPECT NULLS
10685 match target {
10686 DialectType::SQLite | DialectType::PostgreSQL => {
10687 Action::RespectNullsConvert
10688 }
10689 _ => Action::None,
10690 }
10691 }
10692 _ => Action::None,
10693 }
10694 }
10695 }
10696 }
10697 }
10698 // CREATE TABLE a LIKE b -> dialect-specific transformations
10699 Expression::CreateTable(ref ct)
10700 if ct.columns.is_empty()
10701 && ct.constraints.iter().any(|c| {
10702 matches!(c, crate::expressions::TableConstraint::Like { .. })
10703 })
10704 && matches!(
10705 target,
10706 DialectType::DuckDB | DialectType::SQLite | DialectType::Drill
10707 ) =>
10708 {
10709 Action::CreateTableLikeToCtas
10710 }
10711 Expression::CreateTable(ref ct)
10712 if ct.columns.is_empty()
10713 && ct.constraints.iter().any(|c| {
10714 matches!(c, crate::expressions::TableConstraint::Like { .. })
10715 })
10716 && matches!(target, DialectType::TSQL | DialectType::Fabric) =>
10717 {
10718 Action::CreateTableLikeToSelectInto
10719 }
10720 Expression::CreateTable(ref ct)
10721 if ct.columns.is_empty()
10722 && ct.constraints.iter().any(|c| {
10723 matches!(c, crate::expressions::TableConstraint::Like { .. })
10724 })
10725 && matches!(target, DialectType::ClickHouse) =>
10726 {
10727 Action::CreateTableLikeToAs
10728 }
10729 // CREATE TABLE: strip COMMENT column constraint, USING, PARTITIONED BY for DuckDB
10730 Expression::CreateTable(ref ct)
10731 if matches!(target, DialectType::DuckDB)
10732 && matches!(
10733 source,
10734 DialectType::DuckDB
10735 | DialectType::Spark
10736 | DialectType::Databricks
10737 | DialectType::Hive
10738 ) =>
10739 {
10740 let has_comment = ct.columns.iter().any(|c| {
10741 c.comment.is_some()
10742 || c.constraints.iter().any(|con| {
10743 matches!(con, crate::expressions::ColumnConstraint::Comment(_))
10744 })
10745 });
10746 let has_props = !ct.properties.is_empty();
10747 if has_comment || has_props {
10748 Action::CreateTableStripComment
10749 } else {
10750 Action::None
10751 }
10752 }
10753 // Array conversion: Expression::Array -> Expression::ArrayFunc for PostgreSQL
10754 Expression::Array(_)
10755 if matches!(target, DialectType::PostgreSQL | DialectType::Redshift) =>
10756 {
10757 Action::ArrayConcatBracketConvert
10758 }
10759 // ArrayFunc (bracket notation) -> Function("ARRAY") for Redshift (from BigQuery source)
10760 Expression::ArrayFunc(ref arr)
10761 if arr.bracket_notation
10762 && matches!(source, DialectType::BigQuery)
10763 && matches!(target, DialectType::Redshift) =>
10764 {
10765 Action::ArrayConcatBracketConvert
10766 }
10767 // BIT_OR/BIT_AND/BIT_XOR: float/decimal arg cast for DuckDB, or rename for Snowflake
10768 Expression::BitwiseOrAgg(ref f)
10769 | Expression::BitwiseAndAgg(ref f)
10770 | Expression::BitwiseXorAgg(ref f) => {
10771 if matches!(target, DialectType::DuckDB) {
10772 // Check if the arg is CAST(val AS FLOAT/DOUBLE/DECIMAL/REAL)
10773 if let Expression::Cast(ref c) = f.this {
10774 match &c.to {
10775 DataType::Float { .. }
10776 | DataType::Double { .. }
10777 | DataType::Decimal { .. } => Action::BitAggFloatCast,
10778 DataType::Custom { ref name }
10779 if name.eq_ignore_ascii_case("REAL") =>
10780 {
10781 Action::BitAggFloatCast
10782 }
10783 _ => Action::None,
10784 }
10785 } else {
10786 Action::None
10787 }
10788 } else if matches!(target, DialectType::Snowflake) {
10789 Action::BitAggSnowflakeRename
10790 } else {
10791 Action::None
10792 }
10793 }
10794 // FILTER -> IFF for Snowflake (aggregate functions with FILTER clause)
10795 Expression::Filter(ref _f) if matches!(target, DialectType::Snowflake) => {
10796 Action::FilterToIff
10797 }
10798 // AggFunc.filter -> IFF wrapping for Snowflake (e.g., AVG(x) FILTER(WHERE cond))
10799 Expression::Avg(ref f)
10800 | Expression::Sum(ref f)
10801 | Expression::Min(ref f)
10802 | Expression::Max(ref f)
10803 | Expression::CountIf(ref f)
10804 | Expression::Stddev(ref f)
10805 | Expression::StddevPop(ref f)
10806 | Expression::StddevSamp(ref f)
10807 | Expression::Variance(ref f)
10808 | Expression::VarPop(ref f)
10809 | Expression::VarSamp(ref f)
10810 | Expression::Median(ref f)
10811 | Expression::Mode(ref f)
10812 | Expression::First(ref f)
10813 | Expression::Last(ref f)
10814 | Expression::ApproxDistinct(ref f)
10815 if f.filter.is_some() && matches!(target, DialectType::Snowflake) =>
10816 {
10817 Action::AggFilterToIff
10818 }
10819 Expression::Count(ref c)
10820 if c.filter.is_some() && matches!(target, DialectType::Snowflake) =>
10821 {
10822 Action::AggFilterToIff
10823 }
10824 // COUNT(DISTINCT a, b) -> COUNT(DISTINCT CASE WHEN ... END) for dialects that don't support multi-arg DISTINCT
10825 Expression::Count(ref c)
10826 if c.distinct
10827 && matches!(&c.this, Some(Expression::Tuple(_)))
10828 && matches!(
10829 target,
10830 DialectType::Presto
10831 | DialectType::Trino
10832 | DialectType::DuckDB
10833 | DialectType::PostgreSQL
10834 ) =>
10835 {
10836 Action::CountDistinctMultiArg
10837 }
10838 // JSON arrow -> GET_PATH/PARSE_JSON for Snowflake
10839 Expression::JsonExtract(_) if matches!(target, DialectType::Snowflake) => {
10840 Action::JsonToGetPath
10841 }
10842 // DuckDB struct/dict -> BigQuery STRUCT / Presto ROW
10843 Expression::Struct(_)
10844 if matches!(
10845 target,
10846 DialectType::BigQuery | DialectType::Presto | DialectType::Trino
10847 ) && matches!(source, DialectType::DuckDB) =>
10848 {
10849 Action::StructToRow
10850 }
10851 // DuckDB curly-brace dict {'key': value} -> BigQuery STRUCT / Presto ROW
10852 Expression::MapFunc(ref m)
10853 if m.curly_brace_syntax
10854 && matches!(
10855 target,
10856 DialectType::BigQuery | DialectType::Presto | DialectType::Trino
10857 )
10858 && matches!(source, DialectType::DuckDB) =>
10859 {
10860 Action::StructToRow
10861 }
10862 // APPROX_COUNT_DISTINCT -> APPROX_DISTINCT for Presto/Trino
10863 Expression::ApproxCountDistinct(_)
10864 if matches!(
10865 target,
10866 DialectType::Presto | DialectType::Trino | DialectType::Athena
10867 ) =>
10868 {
10869 Action::ApproxCountDistinctToApproxDistinct
10870 }
10871 // ARRAY_CONTAINS(arr, val) -> CONTAINS(arr, val) for Presto, ARRAY_CONTAINS(CAST(val AS VARIANT), arr) for Snowflake
10872 Expression::ArrayContains(_)
10873 if matches!(
10874 target,
10875 DialectType::Presto | DialectType::Trino | DialectType::Snowflake
10876 ) && !(matches!(source, DialectType::Snowflake) && matches!(target, DialectType::Snowflake)) =>
10877 {
10878 Action::ArrayContainsConvert
10879 }
10880 // ARRAY_CONTAINS -> DuckDB NULL-aware CASE (from Snowflake source with check_null semantics)
10881 Expression::ArrayContains(_)
10882 if matches!(target, DialectType::DuckDB)
10883 && matches!(source, DialectType::Snowflake) =>
10884 {
10885 Action::ArrayContainsDuckDBConvert
10886 }
10887 // ARRAY_EXCEPT -> target-specific conversion
10888 Expression::ArrayExcept(_)
10889 if matches!(
10890 target,
10891 DialectType::DuckDB | DialectType::Snowflake | DialectType::Presto | DialectType::Trino | DialectType::Athena
10892 ) =>
10893 {
10894 Action::ArrayExceptConvert
10895 }
10896 // ARRAY_POSITION -> swap args for Snowflake target (only when source is not Snowflake)
10897 Expression::ArrayPosition(_)
10898 if matches!(target, DialectType::Snowflake)
10899 && !matches!(source, DialectType::Snowflake) =>
10900 {
10901 Action::ArrayPositionSnowflakeSwap
10902 }
10903 // ARRAY_POSITION(val, arr) -> ARRAY_POSITION(arr, val) - 1 for DuckDB from Snowflake source
10904 Expression::ArrayPosition(_)
10905 if matches!(target, DialectType::DuckDB)
10906 && matches!(source, DialectType::Snowflake) =>
10907 {
10908 Action::SnowflakeArrayPositionToDuckDB
10909 }
10910 // ARRAY_DISTINCT -> arrayDistinct for ClickHouse
10911 Expression::ArrayDistinct(_)
10912 if matches!(target, DialectType::ClickHouse) =>
10913 {
10914 Action::ArrayDistinctClickHouse
10915 }
10916 // ARRAY_DISTINCT -> DuckDB LIST_DISTINCT with NULL-aware CASE
10917 Expression::ArrayDistinct(_)
10918 if matches!(target, DialectType::DuckDB)
10919 && matches!(source, DialectType::Snowflake) =>
10920 {
10921 Action::ArrayDistinctConvert
10922 }
10923 // StrPosition with position -> complex expansion for Presto/DuckDB
10924 // STRPOS doesn't support a position arg in these dialects
10925 Expression::StrPosition(ref sp)
10926 if sp.position.is_some()
10927 && matches!(
10928 target,
10929 DialectType::Presto
10930 | DialectType::Trino
10931 | DialectType::Athena
10932 | DialectType::DuckDB
10933 ) =>
10934 {
10935 Action::StrPositionExpand
10936 }
10937 // FIRST(col) IGNORE NULLS -> ANY_VALUE(col) for DuckDB
10938 Expression::First(ref f)
10939 if f.ignore_nulls == Some(true)
10940 && matches!(target, DialectType::DuckDB) =>
10941 {
10942 Action::FirstToAnyValue
10943 }
10944 // BEGIN -> START TRANSACTION for Presto/Trino
10945 Expression::Command(ref cmd)
10946 if cmd.this.eq_ignore_ascii_case("BEGIN")
10947 && matches!(
10948 target,
10949 DialectType::Presto | DialectType::Trino | DialectType::Athena
10950 ) =>
10951 {
10952 // Handled inline below
10953 Action::None // We'll handle it directly
10954 }
10955 // Note: PostgreSQL ^ is now parsed as Power directly (not BitwiseXor).
10956 // PostgreSQL # is parsed as BitwiseXor (which is correct).
10957 // a || b (Concat operator) -> CONCAT function for Presto/Trino
10958 Expression::Concat(ref _op)
10959 if matches!(source, DialectType::PostgreSQL | DialectType::Redshift)
10960 && matches!(target, DialectType::Presto | DialectType::Trino) =>
10961 {
10962 Action::PipeConcatToConcat
10963 }
10964 _ => Action::None,
10965 }
10966 };
10967
10968 match action {
10969 Action::None => {
10970 // Handle inline transforms that don't need a dedicated action
10971 if matches!(target, DialectType::TSQL | DialectType::Fabric) {
10972 if let Some(rewritten) = Self::rewrite_tsql_interval_arithmetic(&e) {
10973 return Ok(rewritten);
10974 }
10975 }
10976
10977 // BETWEEN SYMMETRIC/ASYMMETRIC expansion for non-PostgreSQL/Dremio targets
10978 if let Expression::Between(ref b) = e {
10979 if let Some(sym) = b.symmetric {
10980 let keeps_symmetric =
10981 matches!(target, DialectType::PostgreSQL | DialectType::Dremio);
10982 if !keeps_symmetric {
10983 if sym {
10984 // SYMMETRIC: expand to (x BETWEEN a AND b OR x BETWEEN b AND a)
10985 let b = if let Expression::Between(b) = e {
10986 *b
10987 } else {
10988 unreachable!()
10989 };
10990 let between1 = Expression::Between(Box::new(
10991 crate::expressions::Between {
10992 this: b.this.clone(),
10993 low: b.low.clone(),
10994 high: b.high.clone(),
10995 not: b.not,
10996 symmetric: None,
10997 },
10998 ));
10999 let between2 = Expression::Between(Box::new(
11000 crate::expressions::Between {
11001 this: b.this,
11002 low: b.high,
11003 high: b.low,
11004 not: b.not,
11005 symmetric: None,
11006 },
11007 ));
11008 return Ok(Expression::Paren(Box::new(
11009 crate::expressions::Paren {
11010 this: Expression::Or(Box::new(
11011 crate::expressions::BinaryOp::new(
11012 between1, between2,
11013 ),
11014 )),
11015 trailing_comments: vec![],
11016 },
11017 )));
11018 } else {
11019 // ASYMMETRIC: strip qualifier, keep as regular BETWEEN
11020 let b = if let Expression::Between(b) = e {
11021 *b
11022 } else {
11023 unreachable!()
11024 };
11025 return Ok(Expression::Between(Box::new(
11026 crate::expressions::Between {
11027 this: b.this,
11028 low: b.low,
11029 high: b.high,
11030 not: b.not,
11031 symmetric: None,
11032 },
11033 )));
11034 }
11035 }
11036 }
11037 }
11038
11039 // ILIKE -> LOWER(x) LIKE LOWER(y) for StarRocks/Doris
11040 if let Expression::ILike(ref _like) = e {
11041 if matches!(target, DialectType::StarRocks | DialectType::Doris) {
11042 let like = if let Expression::ILike(l) = e {
11043 *l
11044 } else {
11045 unreachable!()
11046 };
11047 let lower_left = Expression::Function(Box::new(Function::new(
11048 "LOWER".to_string(),
11049 vec![like.left],
11050 )));
11051 let lower_right = Expression::Function(Box::new(Function::new(
11052 "LOWER".to_string(),
11053 vec![like.right],
11054 )));
11055 return Ok(Expression::Like(Box::new(crate::expressions::LikeOp {
11056 left: lower_left,
11057 right: lower_right,
11058 escape: like.escape,
11059 quantifier: like.quantifier,
11060 inferred_type: None,
11061 })));
11062 }
11063 }
11064
11065 // Oracle DBMS_RANDOM.VALUE() -> RANDOM() for PostgreSQL, RAND() for others
11066 if let Expression::MethodCall(ref mc) = e {
11067 if matches!(source, DialectType::Oracle)
11068 && mc.method.name.eq_ignore_ascii_case("VALUE")
11069 && mc.args.is_empty()
11070 {
11071 let is_dbms_random = match &mc.this {
11072 Expression::Identifier(id) => {
11073 id.name.eq_ignore_ascii_case("DBMS_RANDOM")
11074 }
11075 Expression::Column(col) => {
11076 col.table.is_none()
11077 && col.name.name.eq_ignore_ascii_case("DBMS_RANDOM")
11078 }
11079 _ => false,
11080 };
11081 if is_dbms_random {
11082 let func_name = match target {
11083 DialectType::PostgreSQL
11084 | DialectType::Redshift
11085 | DialectType::DuckDB
11086 | DialectType::SQLite => "RANDOM",
11087 DialectType::Oracle => "DBMS_RANDOM.VALUE",
11088 _ => "RAND",
11089 };
11090 return Ok(Expression::Function(Box::new(Function::new(
11091 func_name.to_string(),
11092 vec![],
11093 ))));
11094 }
11095 }
11096 }
11097 // TRIM without explicit position -> add BOTH for ClickHouse
11098 if let Expression::Trim(ref trim) = e {
11099 if matches!(target, DialectType::ClickHouse)
11100 && trim.sql_standard_syntax
11101 && trim.characters.is_some()
11102 && !trim.position_explicit
11103 {
11104 let mut new_trim = (**trim).clone();
11105 new_trim.position_explicit = true;
11106 return Ok(Expression::Trim(Box::new(new_trim)));
11107 }
11108 }
11109 // BEGIN -> START TRANSACTION for Presto/Trino
11110 if let Expression::Transaction(ref txn) = e {
11111 if matches!(
11112 target,
11113 DialectType::Presto | DialectType::Trino | DialectType::Athena
11114 ) {
11115 // Convert BEGIN to START TRANSACTION by setting mark to "START"
11116 let mut txn = txn.clone();
11117 txn.mark = Some(Box::new(Expression::Identifier(Identifier::new(
11118 "START".to_string(),
11119 ))));
11120 return Ok(Expression::Transaction(Box::new(*txn)));
11121 }
11122 }
11123 // IS TRUE/FALSE -> simplified forms for Presto/Trino
11124 if matches!(
11125 target,
11126 DialectType::Presto | DialectType::Trino | DialectType::Athena
11127 ) {
11128 match &e {
11129 Expression::IsTrue(itf) if !itf.not => {
11130 // x IS TRUE -> x
11131 return Ok(itf.this.clone());
11132 }
11133 Expression::IsTrue(itf) if itf.not => {
11134 // x IS NOT TRUE -> NOT x
11135 return Ok(Expression::Not(Box::new(
11136 crate::expressions::UnaryOp {
11137 this: itf.this.clone(),
11138 inferred_type: None,
11139 },
11140 )));
11141 }
11142 Expression::IsFalse(itf) if !itf.not => {
11143 // x IS FALSE -> NOT x
11144 return Ok(Expression::Not(Box::new(
11145 crate::expressions::UnaryOp {
11146 this: itf.this.clone(),
11147 inferred_type: None,
11148 },
11149 )));
11150 }
11151 Expression::IsFalse(itf) if itf.not => {
11152 // x IS NOT FALSE -> NOT NOT x
11153 let not_x =
11154 Expression::Not(Box::new(crate::expressions::UnaryOp {
11155 this: itf.this.clone(),
11156 inferred_type: None,
11157 }));
11158 return Ok(Expression::Not(Box::new(
11159 crate::expressions::UnaryOp {
11160 this: not_x,
11161 inferred_type: None,
11162 },
11163 )));
11164 }
11165 _ => {}
11166 }
11167 }
11168 // x IS NOT FALSE -> NOT x IS FALSE for Redshift
11169 if matches!(target, DialectType::Redshift) {
11170 if let Expression::IsFalse(ref itf) = e {
11171 if itf.not {
11172 return Ok(Expression::Not(Box::new(
11173 crate::expressions::UnaryOp {
11174 this: Expression::IsFalse(Box::new(
11175 crate::expressions::IsTrueFalse {
11176 this: itf.this.clone(),
11177 not: false,
11178 },
11179 )),
11180 inferred_type: None,
11181 },
11182 )));
11183 }
11184 }
11185 }
11186 // REGEXP_REPLACE: add 'g' flag when source defaults to global replacement
11187 // Snowflake default is global, PostgreSQL/DuckDB default is first-match-only
11188 if let Expression::Function(ref f) = e {
11189 if f.name.eq_ignore_ascii_case("REGEXP_REPLACE")
11190 && matches!(source, DialectType::Snowflake)
11191 && matches!(target, DialectType::PostgreSQL | DialectType::DuckDB)
11192 {
11193 if f.args.len() == 3 {
11194 let mut args = f.args.clone();
11195 args.push(Expression::string("g"));
11196 return Ok(Expression::Function(Box::new(Function::new(
11197 "REGEXP_REPLACE".to_string(),
11198 args,
11199 ))));
11200 } else if f.args.len() == 4 {
11201 // 4th arg might be position, add 'g' as 5th
11202 let mut args = f.args.clone();
11203 args.push(Expression::string("g"));
11204 return Ok(Expression::Function(Box::new(Function::new(
11205 "REGEXP_REPLACE".to_string(),
11206 args,
11207 ))));
11208 }
11209 }
11210 }
11211 Ok(e)
11212 }
11213
11214 Action::GreatestLeastNull => {
11215 let f = if let Expression::Function(f) = e {
11216 *f
11217 } else {
11218 unreachable!("action only triggered for Function expressions")
11219 };
11220 let mut null_checks: Vec<Expression> = f
11221 .args
11222 .iter()
11223 .map(|a| {
11224 Expression::IsNull(Box::new(IsNull {
11225 this: a.clone(),
11226 not: false,
11227 postfix_form: false,
11228 }))
11229 })
11230 .collect();
11231 let condition = if null_checks.len() == 1 {
11232 null_checks.remove(0)
11233 } else {
11234 let first = null_checks.remove(0);
11235 null_checks.into_iter().fold(first, |acc, check| {
11236 Expression::Or(Box::new(BinaryOp::new(acc, check)))
11237 })
11238 };
11239 Ok(Expression::Case(Box::new(Case {
11240 operand: None,
11241 whens: vec![(condition, Expression::Null(Null))],
11242 else_: Some(Expression::Function(Box::new(Function::new(
11243 f.name, f.args,
11244 )))),
11245 comments: Vec::new(),
11246 inferred_type: None,
11247 })))
11248 }
11249
11250 Action::ArrayGenerateRange => {
11251 let f = if let Expression::Function(f) = e {
11252 *f
11253 } else {
11254 unreachable!("action only triggered for Function expressions")
11255 };
11256 let start = f.args[0].clone();
11257 let end = f.args[1].clone();
11258 let step = f.args.get(2).cloned();
11259
11260 // Helper: compute end - 1 for converting exclusive→inclusive end.
11261 // When end is a literal number, simplify to a computed literal.
11262 fn exclusive_to_inclusive_end(end: &Expression) -> Expression {
11263 // Try to simplify literal numbers
11264 match end {
11265 Expression::Literal(lit)
11266 if matches!(lit.as_ref(), Literal::Number(_)) =>
11267 {
11268 let Literal::Number(n) = lit.as_ref() else {
11269 unreachable!()
11270 };
11271 if let Ok(val) = n.parse::<i64>() {
11272 return Expression::number(val - 1);
11273 }
11274 }
11275 Expression::Neg(u) => {
11276 if let Expression::Literal(lit) = &u.this {
11277 if let Literal::Number(n) = lit.as_ref() {
11278 if let Ok(val) = n.parse::<i64>() {
11279 return Expression::number(-val - 1);
11280 }
11281 }
11282 }
11283 }
11284 _ => {}
11285 }
11286 // Non-literal: produce end - 1 expression
11287 Expression::Sub(Box::new(BinaryOp::new(end.clone(), Expression::number(1))))
11288 }
11289
11290 match target {
11291 // Snowflake ARRAY_GENERATE_RANGE and DuckDB RANGE both use exclusive end,
11292 // so no adjustment needed — just rename the function.
11293 DialectType::Snowflake => {
11294 let mut args = vec![start, end];
11295 if let Some(s) = step {
11296 args.push(s);
11297 }
11298 Ok(Expression::Function(Box::new(Function::new(
11299 "ARRAY_GENERATE_RANGE".to_string(),
11300 args,
11301 ))))
11302 }
11303 DialectType::DuckDB => {
11304 let mut args = vec![start, end];
11305 if let Some(s) = step {
11306 args.push(s);
11307 }
11308 Ok(Expression::Function(Box::new(Function::new(
11309 "RANGE".to_string(),
11310 args,
11311 ))))
11312 }
11313 // These dialects use inclusive end, so convert exclusive→inclusive.
11314 // Presto/Trino: simplify literal numbers (3 → 2).
11315 DialectType::Presto | DialectType::Trino => {
11316 let end_inclusive = exclusive_to_inclusive_end(&end);
11317 let mut args = vec![start, end_inclusive];
11318 if let Some(s) = step {
11319 args.push(s);
11320 }
11321 Ok(Expression::Function(Box::new(Function::new(
11322 "SEQUENCE".to_string(),
11323 args,
11324 ))))
11325 }
11326 // PostgreSQL, Redshift, BigQuery: keep as end - 1 expression form.
11327 DialectType::PostgreSQL | DialectType::Redshift => {
11328 let end_minus_1 = Expression::Sub(Box::new(BinaryOp::new(
11329 end.clone(),
11330 Expression::number(1),
11331 )));
11332 let mut args = vec![start, end_minus_1];
11333 if let Some(s) = step {
11334 args.push(s);
11335 }
11336 Ok(Expression::Function(Box::new(Function::new(
11337 "GENERATE_SERIES".to_string(),
11338 args,
11339 ))))
11340 }
11341 DialectType::BigQuery => {
11342 let end_minus_1 = Expression::Sub(Box::new(BinaryOp::new(
11343 end.clone(),
11344 Expression::number(1),
11345 )));
11346 let mut args = vec![start, end_minus_1];
11347 if let Some(s) = step {
11348 args.push(s);
11349 }
11350 Ok(Expression::Function(Box::new(Function::new(
11351 "GENERATE_ARRAY".to_string(),
11352 args,
11353 ))))
11354 }
11355 _ => Ok(Expression::Function(Box::new(Function::new(
11356 f.name, f.args,
11357 )))),
11358 }
11359 }
11360
11361 Action::Div0TypedDivision => {
11362 let if_func = if let Expression::IfFunc(f) = e {
11363 *f
11364 } else {
11365 unreachable!("action only triggered for IfFunc expressions")
11366 };
11367 if let Some(Expression::Div(div)) = if_func.false_value {
11368 let cast_type = if matches!(target, DialectType::SQLite) {
11369 DataType::Float {
11370 precision: None,
11371 scale: None,
11372 real_spelling: true,
11373 }
11374 } else {
11375 DataType::Double {
11376 precision: None,
11377 scale: None,
11378 }
11379 };
11380 let casted_left = Expression::Cast(Box::new(Cast {
11381 this: div.left,
11382 to: cast_type,
11383 trailing_comments: vec![],
11384 double_colon_syntax: false,
11385 format: None,
11386 default: None,
11387 inferred_type: None,
11388 }));
11389 Ok(Expression::IfFunc(Box::new(crate::expressions::IfFunc {
11390 condition: if_func.condition,
11391 true_value: if_func.true_value,
11392 false_value: Some(Expression::Div(Box::new(BinaryOp::new(
11393 casted_left,
11394 div.right,
11395 )))),
11396 original_name: if_func.original_name,
11397 inferred_type: None,
11398 })))
11399 } else {
11400 // Not actually a Div, reconstruct
11401 Ok(Expression::IfFunc(Box::new(if_func)))
11402 }
11403 }
11404
11405 Action::ArrayAggCollectList => {
11406 let agg = if let Expression::ArrayAgg(a) = e {
11407 *a
11408 } else {
11409 unreachable!("action only triggered for ArrayAgg expressions")
11410 };
11411 Ok(Expression::ArrayAgg(Box::new(AggFunc {
11412 name: Some("COLLECT_LIST".to_string()),
11413 ..agg
11414 })))
11415 }
11416
11417 Action::ArrayAggToGroupConcat => {
11418 let agg = if let Expression::ArrayAgg(a) = e {
11419 *a
11420 } else {
11421 unreachable!("action only triggered for ArrayAgg expressions")
11422 };
11423 Ok(Expression::ArrayAgg(Box::new(AggFunc {
11424 name: Some("GROUP_CONCAT".to_string()),
11425 ..agg
11426 })))
11427 }
11428
11429 Action::ArrayAggWithinGroupFilter => {
11430 let wg = if let Expression::WithinGroup(w) = e {
11431 *w
11432 } else {
11433 unreachable!("action only triggered for WithinGroup expressions")
11434 };
11435 if let Expression::ArrayAgg(inner_agg) = wg.this {
11436 let col = inner_agg.this.clone();
11437 let filter = Expression::IsNull(Box::new(IsNull {
11438 this: col,
11439 not: true,
11440 postfix_form: false,
11441 }));
11442 // For DuckDB, add explicit NULLS FIRST for DESC ordering
11443 let order_by = if matches!(target, DialectType::DuckDB) {
11444 wg.order_by
11445 .into_iter()
11446 .map(|mut o| {
11447 if o.desc && o.nulls_first.is_none() {
11448 o.nulls_first = Some(true);
11449 }
11450 o
11451 })
11452 .collect()
11453 } else {
11454 wg.order_by
11455 };
11456 Ok(Expression::ArrayAgg(Box::new(AggFunc {
11457 this: inner_agg.this,
11458 distinct: inner_agg.distinct,
11459 filter: Some(filter),
11460 order_by,
11461 name: inner_agg.name,
11462 ignore_nulls: inner_agg.ignore_nulls,
11463 having_max: inner_agg.having_max,
11464 limit: inner_agg.limit,
11465 inferred_type: None,
11466 })))
11467 } else {
11468 Ok(Expression::WithinGroup(Box::new(wg)))
11469 }
11470 }
11471
11472 Action::ArrayAggFilter => {
11473 let agg = if let Expression::ArrayAgg(a) = e {
11474 *a
11475 } else {
11476 unreachable!("action only triggered for ArrayAgg expressions")
11477 };
11478 let col = agg.this.clone();
11479 let filter = Expression::IsNull(Box::new(IsNull {
11480 this: col,
11481 not: true,
11482 postfix_form: false,
11483 }));
11484 Ok(Expression::ArrayAgg(Box::new(AggFunc {
11485 filter: Some(filter),
11486 ..agg
11487 })))
11488 }
11489
11490 Action::ArrayAggNullFilter => {
11491 // ARRAY_AGG(x) FILTER(WHERE cond) -> ARRAY_AGG(x) FILTER(WHERE cond AND NOT x IS NULL)
11492 // For source dialects that exclude NULLs (Spark/Hive) targeting DuckDB which includes them
11493 let agg = if let Expression::ArrayAgg(a) = e {
11494 *a
11495 } else {
11496 unreachable!("action only triggered for ArrayAgg expressions")
11497 };
11498 let col = agg.this.clone();
11499 let not_null = Expression::IsNull(Box::new(IsNull {
11500 this: col,
11501 not: true,
11502 postfix_form: true, // Use "NOT x IS NULL" form (prefix NOT)
11503 }));
11504 let new_filter = if let Some(existing_filter) = agg.filter {
11505 // AND the NOT IS NULL with existing filter
11506 Expression::And(Box::new(crate::expressions::BinaryOp::new(
11507 existing_filter,
11508 not_null,
11509 )))
11510 } else {
11511 not_null
11512 };
11513 Ok(Expression::ArrayAgg(Box::new(AggFunc {
11514 filter: Some(new_filter),
11515 ..agg
11516 })))
11517 }
11518
11519 Action::BigQueryArraySelectAsStructToSnowflake => {
11520 // ARRAY(SELECT AS STRUCT x1 AS x1, x2 AS x2 FROM t)
11521 // -> (SELECT ARRAY_AGG(OBJECT_CONSTRUCT('x1', x1, 'x2', x2)) FROM t)
11522 if let Expression::Function(mut f) = e {
11523 let is_match = f.args.len() == 1
11524 && matches!(&f.args[0], Expression::Select(s) if s.kind.as_deref() == Some("STRUCT"));
11525 if is_match {
11526 let inner_select = match f.args.remove(0) {
11527 Expression::Select(s) => *s,
11528 _ => unreachable!(
11529 "argument already verified to be a Select expression"
11530 ),
11531 };
11532 // Build OBJECT_CONSTRUCT args from SELECT expressions
11533 let mut oc_args = Vec::new();
11534 for expr in &inner_select.expressions {
11535 match expr {
11536 Expression::Alias(a) => {
11537 let key = Expression::Literal(Box::new(Literal::String(
11538 a.alias.name.clone(),
11539 )));
11540 let value = a.this.clone();
11541 oc_args.push(key);
11542 oc_args.push(value);
11543 }
11544 Expression::Column(c) => {
11545 let key = Expression::Literal(Box::new(Literal::String(
11546 c.name.name.clone(),
11547 )));
11548 oc_args.push(key);
11549 oc_args.push(expr.clone());
11550 }
11551 _ => {
11552 oc_args.push(expr.clone());
11553 }
11554 }
11555 }
11556 let object_construct = Expression::Function(Box::new(Function::new(
11557 "OBJECT_CONSTRUCT".to_string(),
11558 oc_args,
11559 )));
11560 let array_agg = Expression::Function(Box::new(Function::new(
11561 "ARRAY_AGG".to_string(),
11562 vec![object_construct],
11563 )));
11564 let mut new_select = crate::expressions::Select::new();
11565 new_select.expressions = vec![array_agg];
11566 new_select.from = inner_select.from.clone();
11567 new_select.where_clause = inner_select.where_clause.clone();
11568 new_select.group_by = inner_select.group_by.clone();
11569 new_select.having = inner_select.having.clone();
11570 new_select.joins = inner_select.joins.clone();
11571 Ok(Expression::Subquery(Box::new(
11572 crate::expressions::Subquery {
11573 this: Expression::Select(Box::new(new_select)),
11574 alias: None,
11575 column_aliases: Vec::new(),
11576 alias_explicit_as: false,
11577 alias_keyword: None,
11578 order_by: None,
11579 limit: None,
11580 offset: None,
11581 distribute_by: None,
11582 sort_by: None,
11583 cluster_by: None,
11584 lateral: false,
11585 modifiers_inside: false,
11586 trailing_comments: Vec::new(),
11587 inferred_type: None,
11588 },
11589 )))
11590 } else {
11591 Ok(Expression::Function(f))
11592 }
11593 } else {
11594 Ok(e)
11595 }
11596 }
11597
11598 Action::BigQueryPercentileContToDuckDB => {
11599 // PERCENTILE_CONT(x, frac [RESPECT NULLS]) -> QUANTILE_CONT(x, frac) for DuckDB
11600 if let Expression::AggregateFunction(mut af) = e {
11601 af.name = "QUANTILE_CONT".to_string();
11602 af.ignore_nulls = None; // Strip RESPECT/IGNORE NULLS
11603 // Keep only first 2 args
11604 if af.args.len() > 2 {
11605 af.args.truncate(2);
11606 }
11607 Ok(Expression::AggregateFunction(af))
11608 } else {
11609 Ok(e)
11610 }
11611 }
11612
11613 Action::ArrayAggIgnoreNullsDuckDB => {
11614 // ARRAY_AGG(x IGNORE NULLS ORDER BY a, b DESC) -> ARRAY_AGG(x ORDER BY a NULLS FIRST, b DESC)
11615 // Strip IGNORE NULLS, add NULLS FIRST to first ORDER BY column
11616 let mut agg = if let Expression::ArrayAgg(a) = e {
11617 *a
11618 } else {
11619 unreachable!("action only triggered for ArrayAgg expressions")
11620 };
11621 agg.ignore_nulls = None; // Strip IGNORE NULLS
11622 if !agg.order_by.is_empty() {
11623 agg.order_by[0].nulls_first = Some(true);
11624 }
11625 Ok(Expression::ArrayAgg(Box::new(agg)))
11626 }
11627
11628 Action::CountDistinctMultiArg => {
11629 // COUNT(DISTINCT a, b) -> COUNT(DISTINCT CASE WHEN a IS NULL THEN NULL WHEN b IS NULL THEN NULL ELSE (a, b) END)
11630 if let Expression::Count(c) = e {
11631 if let Some(Expression::Tuple(t)) = c.this {
11632 let args = t.expressions;
11633 // Build CASE expression:
11634 // WHEN a IS NULL THEN NULL WHEN b IS NULL THEN NULL ELSE (a, b) END
11635 let mut whens = Vec::new();
11636 for arg in &args {
11637 whens.push((
11638 Expression::IsNull(Box::new(IsNull {
11639 this: arg.clone(),
11640 not: false,
11641 postfix_form: false,
11642 })),
11643 Expression::Null(crate::expressions::Null),
11644 ));
11645 }
11646 // Build the tuple for ELSE
11647 let tuple_expr =
11648 Expression::Tuple(Box::new(crate::expressions::Tuple {
11649 expressions: args,
11650 }));
11651 let case_expr = Expression::Case(Box::new(crate::expressions::Case {
11652 operand: None,
11653 whens,
11654 else_: Some(tuple_expr),
11655 comments: Vec::new(),
11656 inferred_type: None,
11657 }));
11658 Ok(Expression::Count(Box::new(crate::expressions::CountFunc {
11659 this: Some(case_expr),
11660 star: false,
11661 distinct: true,
11662 filter: c.filter,
11663 ignore_nulls: c.ignore_nulls,
11664 original_name: c.original_name,
11665 inferred_type: None,
11666 })))
11667 } else {
11668 Ok(Expression::Count(c))
11669 }
11670 } else {
11671 Ok(e)
11672 }
11673 }
11674
11675 Action::CastTimestampToDatetime => {
11676 let c = if let Expression::Cast(c) = e {
11677 *c
11678 } else {
11679 unreachable!("action only triggered for Cast expressions")
11680 };
11681 Ok(Expression::Cast(Box::new(Cast {
11682 to: DataType::Custom {
11683 name: "DATETIME".to_string(),
11684 },
11685 ..c
11686 })))
11687 }
11688
11689 Action::CastTimestampStripTz => {
11690 // CAST(x AS TIMESTAMP(n) WITH TIME ZONE) -> CAST(x AS TIMESTAMP) for Hive/Spark/BigQuery
11691 let c = if let Expression::Cast(c) = e {
11692 *c
11693 } else {
11694 unreachable!("action only triggered for Cast expressions")
11695 };
11696 Ok(Expression::Cast(Box::new(Cast {
11697 to: DataType::Timestamp {
11698 precision: None,
11699 timezone: false,
11700 },
11701 ..c
11702 })))
11703 }
11704
11705 Action::CastTimestamptzToFunc => {
11706 // CAST(x AS TIMESTAMPTZ) -> TIMESTAMP(x) function for MySQL/StarRocks
11707 let c = if let Expression::Cast(c) = e {
11708 *c
11709 } else {
11710 unreachable!("action only triggered for Cast expressions")
11711 };
11712 Ok(Expression::Function(Box::new(Function::new(
11713 "TIMESTAMP".to_string(),
11714 vec![c.this],
11715 ))))
11716 }
11717
11718 Action::ToDateToCast => {
11719 // Convert TO_DATE(x) -> CAST(x AS DATE) for DuckDB
11720 if let Expression::Function(f) = e {
11721 let arg = f.args.into_iter().next().unwrap();
11722 Ok(Expression::Cast(Box::new(Cast {
11723 this: arg,
11724 to: DataType::Date,
11725 double_colon_syntax: false,
11726 trailing_comments: vec![],
11727 format: None,
11728 default: None,
11729 inferred_type: None,
11730 })))
11731 } else {
11732 Ok(e)
11733 }
11734 }
11735 Action::DateTruncWrapCast => {
11736 // Handle both Expression::DateTrunc/TimestampTrunc and
11737 // Expression::Function("DATE_TRUNC", [unit, expr])
11738 match e {
11739 Expression::DateTrunc(d) | Expression::TimestampTrunc(d) => {
11740 let input_type = match &d.this {
11741 Expression::Cast(c) => Some(c.to.clone()),
11742 _ => None,
11743 };
11744 if let Some(cast_type) = input_type {
11745 let is_time = matches!(cast_type, DataType::Time { .. });
11746 if is_time {
11747 let date_expr = Expression::Cast(Box::new(Cast {
11748 this: Expression::Literal(Box::new(
11749 crate::expressions::Literal::String(
11750 "1970-01-01".to_string(),
11751 ),
11752 )),
11753 to: DataType::Date,
11754 double_colon_syntax: false,
11755 trailing_comments: vec![],
11756 format: None,
11757 default: None,
11758 inferred_type: None,
11759 }));
11760 let add_expr =
11761 Expression::Add(Box::new(BinaryOp::new(date_expr, d.this)));
11762 let inner = Expression::DateTrunc(Box::new(DateTruncFunc {
11763 this: add_expr,
11764 unit: d.unit,
11765 }));
11766 Ok(Expression::Cast(Box::new(Cast {
11767 this: inner,
11768 to: cast_type,
11769 double_colon_syntax: false,
11770 trailing_comments: vec![],
11771 format: None,
11772 default: None,
11773 inferred_type: None,
11774 })))
11775 } else {
11776 let inner = Expression::DateTrunc(Box::new(*d));
11777 Ok(Expression::Cast(Box::new(Cast {
11778 this: inner,
11779 to: cast_type,
11780 double_colon_syntax: false,
11781 trailing_comments: vec![],
11782 format: None,
11783 default: None,
11784 inferred_type: None,
11785 })))
11786 }
11787 } else {
11788 Ok(Expression::DateTrunc(d))
11789 }
11790 }
11791 Expression::Function(f) if f.args.len() == 2 => {
11792 // Function-based DATE_TRUNC(unit, expr)
11793 let input_type = match &f.args[1] {
11794 Expression::Cast(c) => Some(c.to.clone()),
11795 _ => None,
11796 };
11797 if let Some(cast_type) = input_type {
11798 let is_time = matches!(cast_type, DataType::Time { .. });
11799 if is_time {
11800 let date_expr = Expression::Cast(Box::new(Cast {
11801 this: Expression::Literal(Box::new(
11802 crate::expressions::Literal::String(
11803 "1970-01-01".to_string(),
11804 ),
11805 )),
11806 to: DataType::Date,
11807 double_colon_syntax: false,
11808 trailing_comments: vec![],
11809 format: None,
11810 default: None,
11811 inferred_type: None,
11812 }));
11813 let mut args = f.args;
11814 let unit_arg = args.remove(0);
11815 let time_expr = args.remove(0);
11816 let add_expr = Expression::Add(Box::new(BinaryOp::new(
11817 date_expr, time_expr,
11818 )));
11819 let inner = Expression::Function(Box::new(Function::new(
11820 "DATE_TRUNC".to_string(),
11821 vec![unit_arg, add_expr],
11822 )));
11823 Ok(Expression::Cast(Box::new(Cast {
11824 this: inner,
11825 to: cast_type,
11826 double_colon_syntax: false,
11827 trailing_comments: vec![],
11828 format: None,
11829 default: None,
11830 inferred_type: None,
11831 })))
11832 } else {
11833 // Wrap the function in CAST
11834 Ok(Expression::Cast(Box::new(Cast {
11835 this: Expression::Function(f),
11836 to: cast_type,
11837 double_colon_syntax: false,
11838 trailing_comments: vec![],
11839 format: None,
11840 default: None,
11841 inferred_type: None,
11842 })))
11843 }
11844 } else {
11845 Ok(Expression::Function(f))
11846 }
11847 }
11848 other => Ok(other),
11849 }
11850 }
11851
11852 Action::RegexpReplaceSnowflakeToDuckDB => {
11853 // Snowflake REGEXP_REPLACE(s, p, r, position) -> REGEXP_REPLACE(s, p, r, 'g')
11854 if let Expression::Function(f) = e {
11855 let mut args = f.args;
11856 let subject = args.remove(0);
11857 let pattern = args.remove(0);
11858 let replacement = args.remove(0);
11859 Ok(Expression::Function(Box::new(Function::new(
11860 "REGEXP_REPLACE".to_string(),
11861 vec![
11862 subject,
11863 pattern,
11864 replacement,
11865 Expression::Literal(Box::new(crate::expressions::Literal::String(
11866 "g".to_string(),
11867 ))),
11868 ],
11869 ))))
11870 } else {
11871 Ok(e)
11872 }
11873 }
11874
11875 Action::RegexpReplacePositionSnowflakeToDuckDB => {
11876 // Snowflake REGEXP_REPLACE(s, p, r, pos, occ) -> DuckDB form
11877 // pos=1, occ=1 -> REGEXP_REPLACE(s, p, r) (single replace, no 'g')
11878 // pos>1, occ=0 -> SUBSTRING(s, 1, pos-1) || REGEXP_REPLACE(SUBSTRING(s, pos), p, r, 'g')
11879 // pos>1, occ=1 -> SUBSTRING(s, 1, pos-1) || REGEXP_REPLACE(SUBSTRING(s, pos), p, r)
11880 // pos=1, occ=0 -> REGEXP_REPLACE(s, p, r, 'g') (replace all)
11881 if let Expression::Function(f) = e {
11882 let mut args = f.args;
11883 let subject = args.remove(0);
11884 let pattern = args.remove(0);
11885 let replacement = args.remove(0);
11886 let position = args.remove(0);
11887 let occurrence = args.remove(0);
11888
11889 let is_pos_1 = matches!(&position, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(n) if n == "1"));
11890 let is_occ_0 = matches!(&occurrence, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(n) if n == "0"));
11891 let is_occ_1 = matches!(&occurrence, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(n) if n == "1"));
11892
11893 if is_pos_1 && is_occ_1 {
11894 // REGEXP_REPLACE(s, p, r) - single replace, no flags
11895 Ok(Expression::Function(Box::new(Function::new(
11896 "REGEXP_REPLACE".to_string(),
11897 vec![subject, pattern, replacement],
11898 ))))
11899 } else if is_pos_1 && is_occ_0 {
11900 // REGEXP_REPLACE(s, p, r, 'g') - global replace
11901 Ok(Expression::Function(Box::new(Function::new(
11902 "REGEXP_REPLACE".to_string(),
11903 vec![
11904 subject,
11905 pattern,
11906 replacement,
11907 Expression::Literal(Box::new(Literal::String("g".to_string()))),
11908 ],
11909 ))))
11910 } else {
11911 // pos>1: SUBSTRING(s, 1, pos-1) || REGEXP_REPLACE(SUBSTRING(s, pos), p, r[, 'g'])
11912 // Pre-compute pos-1 when position is a numeric literal
11913 let pos_minus_1 = if let Expression::Literal(ref lit) = position {
11914 if let Literal::Number(ref n) = lit.as_ref() {
11915 if let Ok(val) = n.parse::<i64>() {
11916 Expression::number(val - 1)
11917 } else {
11918 Expression::Sub(Box::new(BinaryOp::new(
11919 position.clone(),
11920 Expression::number(1),
11921 )))
11922 }
11923 } else {
11924 position.clone()
11925 }
11926 } else {
11927 Expression::Sub(Box::new(BinaryOp::new(
11928 position.clone(),
11929 Expression::number(1),
11930 )))
11931 };
11932 let prefix = Expression::Function(Box::new(Function::new(
11933 "SUBSTRING".to_string(),
11934 vec![subject.clone(), Expression::number(1), pos_minus_1],
11935 )));
11936 let suffix_subject = Expression::Function(Box::new(Function::new(
11937 "SUBSTRING".to_string(),
11938 vec![subject, position],
11939 )));
11940 let mut replace_args = vec![suffix_subject, pattern, replacement];
11941 if is_occ_0 {
11942 replace_args.push(Expression::Literal(Box::new(Literal::String(
11943 "g".to_string(),
11944 ))));
11945 }
11946 let replace_expr = Expression::Function(Box::new(Function::new(
11947 "REGEXP_REPLACE".to_string(),
11948 replace_args,
11949 )));
11950 Ok(Expression::DPipe(Box::new(crate::expressions::DPipe {
11951 this: Box::new(prefix),
11952 expression: Box::new(replace_expr),
11953 safe: None,
11954 })))
11955 }
11956 } else {
11957 Ok(e)
11958 }
11959 }
11960
11961 Action::RegexpSubstrSnowflakeToDuckDB => {
11962 // Snowflake REGEXP_SUBSTR -> DuckDB REGEXP_EXTRACT variants
11963 if let Expression::Function(f) = e {
11964 let mut args = f.args;
11965 let arg_count = args.len();
11966 match arg_count {
11967 // REGEXP_SUBSTR(s, p) -> REGEXP_EXTRACT(s, p)
11968 0..=2 => Ok(Expression::Function(Box::new(Function::new(
11969 "REGEXP_EXTRACT".to_string(),
11970 args,
11971 )))),
11972 // REGEXP_SUBSTR(s, p, pos) -> REGEXP_EXTRACT(NULLIF(SUBSTRING(s, pos), ''), p)
11973 3 => {
11974 let subject = args.remove(0);
11975 let pattern = args.remove(0);
11976 let position = args.remove(0);
11977 let is_pos_1 = matches!(&position, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(n) if n == "1"));
11978 if is_pos_1 {
11979 Ok(Expression::Function(Box::new(Function::new(
11980 "REGEXP_EXTRACT".to_string(),
11981 vec![subject, pattern],
11982 ))))
11983 } else {
11984 let substring_expr =
11985 Expression::Function(Box::new(Function::new(
11986 "SUBSTRING".to_string(),
11987 vec![subject, position],
11988 )));
11989 let nullif_expr =
11990 Expression::Function(Box::new(Function::new(
11991 "NULLIF".to_string(),
11992 vec![
11993 substring_expr,
11994 Expression::Literal(Box::new(Literal::String(
11995 String::new(),
11996 ))),
11997 ],
11998 )));
11999 Ok(Expression::Function(Box::new(Function::new(
12000 "REGEXP_EXTRACT".to_string(),
12001 vec![nullif_expr, pattern],
12002 ))))
12003 }
12004 }
12005 // REGEXP_SUBSTR(s, p, pos, occ) -> depends on pos and occ
12006 4 => {
12007 let subject = args.remove(0);
12008 let pattern = args.remove(0);
12009 let position = args.remove(0);
12010 let occurrence = args.remove(0);
12011 let is_pos_1 = matches!(&position, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(n) if n == "1"));
12012 let is_occ_1 = matches!(&occurrence, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(n) if n == "1"));
12013
12014 let effective_subject = if is_pos_1 {
12015 subject
12016 } else {
12017 let substring_expr =
12018 Expression::Function(Box::new(Function::new(
12019 "SUBSTRING".to_string(),
12020 vec![subject, position],
12021 )));
12022 Expression::Function(Box::new(Function::new(
12023 "NULLIF".to_string(),
12024 vec![
12025 substring_expr,
12026 Expression::Literal(Box::new(Literal::String(
12027 String::new(),
12028 ))),
12029 ],
12030 )))
12031 };
12032
12033 if is_occ_1 {
12034 Ok(Expression::Function(Box::new(Function::new(
12035 "REGEXP_EXTRACT".to_string(),
12036 vec![effective_subject, pattern],
12037 ))))
12038 } else {
12039 // ARRAY_EXTRACT(REGEXP_EXTRACT_ALL(s, p), occ)
12040 let extract_all =
12041 Expression::Function(Box::new(Function::new(
12042 "REGEXP_EXTRACT_ALL".to_string(),
12043 vec![effective_subject, pattern],
12044 )));
12045 Ok(Expression::Function(Box::new(Function::new(
12046 "ARRAY_EXTRACT".to_string(),
12047 vec![extract_all, occurrence],
12048 ))))
12049 }
12050 }
12051 // REGEXP_SUBSTR(s, p, 1, 1, 'e') -> REGEXP_EXTRACT(s, p)
12052 5 => {
12053 let subject = args.remove(0);
12054 let pattern = args.remove(0);
12055 let _position = args.remove(0);
12056 let _occurrence = args.remove(0);
12057 let _flags = args.remove(0);
12058 // Strip 'e' flag, convert to REGEXP_EXTRACT
12059 Ok(Expression::Function(Box::new(Function::new(
12060 "REGEXP_EXTRACT".to_string(),
12061 vec![subject, pattern],
12062 ))))
12063 }
12064 // REGEXP_SUBSTR(s, p, 1, 1, 'e', group) -> REGEXP_EXTRACT(s, p[, group])
12065 _ => {
12066 let subject = args.remove(0);
12067 let pattern = args.remove(0);
12068 let _position = args.remove(0);
12069 let _occurrence = args.remove(0);
12070 let _flags = args.remove(0);
12071 let group = args.remove(0);
12072 let is_group_0 = matches!(&group, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(n) if n == "0"));
12073 if is_group_0 {
12074 // Strip group=0 (default)
12075 Ok(Expression::Function(Box::new(Function::new(
12076 "REGEXP_EXTRACT".to_string(),
12077 vec![subject, pattern],
12078 ))))
12079 } else {
12080 Ok(Expression::Function(Box::new(Function::new(
12081 "REGEXP_EXTRACT".to_string(),
12082 vec![subject, pattern, group],
12083 ))))
12084 }
12085 }
12086 }
12087 } else {
12088 Ok(e)
12089 }
12090 }
12091
12092 Action::RegexpSubstrSnowflakeIdentity => {
12093 // Snowflake→Snowflake: REGEXP_SUBSTR/REGEXP_SUBSTR_ALL with 6 args
12094 // Strip trailing group=0
12095 if let Expression::Function(f) = e {
12096 let func_name = f.name.clone();
12097 let mut args = f.args;
12098 if args.len() == 6 {
12099 let is_group_0 = matches!(&args[5], Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(n) if n == "0"));
12100 if is_group_0 {
12101 args.truncate(5);
12102 }
12103 }
12104 Ok(Expression::Function(Box::new(Function::new(
12105 func_name, args,
12106 ))))
12107 } else {
12108 Ok(e)
12109 }
12110 }
12111
12112 Action::RegexpSubstrAllSnowflakeToDuckDB => {
12113 // Snowflake REGEXP_SUBSTR_ALL -> DuckDB REGEXP_EXTRACT_ALL variants
12114 if let Expression::Function(f) = e {
12115 let mut args = f.args;
12116 let arg_count = args.len();
12117 match arg_count {
12118 // REGEXP_SUBSTR_ALL(s, p) -> REGEXP_EXTRACT_ALL(s, p)
12119 0..=2 => Ok(Expression::Function(Box::new(Function::new(
12120 "REGEXP_EXTRACT_ALL".to_string(),
12121 args,
12122 )))),
12123 // REGEXP_SUBSTR_ALL(s, p, pos) -> REGEXP_EXTRACT_ALL(SUBSTRING(s, pos), p)
12124 3 => {
12125 let subject = args.remove(0);
12126 let pattern = args.remove(0);
12127 let position = args.remove(0);
12128 let is_pos_1 = matches!(&position, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(n) if n == "1"));
12129 if is_pos_1 {
12130 Ok(Expression::Function(Box::new(Function::new(
12131 "REGEXP_EXTRACT_ALL".to_string(),
12132 vec![subject, pattern],
12133 ))))
12134 } else {
12135 let substring_expr =
12136 Expression::Function(Box::new(Function::new(
12137 "SUBSTRING".to_string(),
12138 vec![subject, position],
12139 )));
12140 Ok(Expression::Function(Box::new(Function::new(
12141 "REGEXP_EXTRACT_ALL".to_string(),
12142 vec![substring_expr, pattern],
12143 ))))
12144 }
12145 }
12146 // REGEXP_SUBSTR_ALL(s, p, 1, occ) -> REGEXP_EXTRACT_ALL(s, p)[occ:]
12147 4 => {
12148 let subject = args.remove(0);
12149 let pattern = args.remove(0);
12150 let position = args.remove(0);
12151 let occurrence = args.remove(0);
12152 let is_pos_1 = matches!(&position, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(n) if n == "1"));
12153 let is_occ_1 = matches!(&occurrence, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(n) if n == "1"));
12154
12155 let effective_subject = if is_pos_1 {
12156 subject
12157 } else {
12158 Expression::Function(Box::new(Function::new(
12159 "SUBSTRING".to_string(),
12160 vec![subject, position],
12161 )))
12162 };
12163
12164 if is_occ_1 {
12165 Ok(Expression::Function(Box::new(Function::new(
12166 "REGEXP_EXTRACT_ALL".to_string(),
12167 vec![effective_subject, pattern],
12168 ))))
12169 } else {
12170 // REGEXP_EXTRACT_ALL(s, p)[occ:]
12171 let extract_all =
12172 Expression::Function(Box::new(Function::new(
12173 "REGEXP_EXTRACT_ALL".to_string(),
12174 vec![effective_subject, pattern],
12175 )));
12176 Ok(Expression::ArraySlice(Box::new(
12177 crate::expressions::ArraySlice {
12178 this: extract_all,
12179 start: Some(occurrence),
12180 end: None,
12181 },
12182 )))
12183 }
12184 }
12185 // REGEXP_SUBSTR_ALL(s, p, 1, 1, 'e') -> REGEXP_EXTRACT_ALL(s, p)
12186 5 => {
12187 let subject = args.remove(0);
12188 let pattern = args.remove(0);
12189 let _position = args.remove(0);
12190 let _occurrence = args.remove(0);
12191 let _flags = args.remove(0);
12192 Ok(Expression::Function(Box::new(Function::new(
12193 "REGEXP_EXTRACT_ALL".to_string(),
12194 vec![subject, pattern],
12195 ))))
12196 }
12197 // REGEXP_SUBSTR_ALL(s, p, 1, 1, 'e', 0) -> REGEXP_EXTRACT_ALL(s, p)
12198 _ => {
12199 let subject = args.remove(0);
12200 let pattern = args.remove(0);
12201 let _position = args.remove(0);
12202 let _occurrence = args.remove(0);
12203 let _flags = args.remove(0);
12204 let group = args.remove(0);
12205 let is_group_0 = matches!(&group, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(n) if n == "0"));
12206 if is_group_0 {
12207 Ok(Expression::Function(Box::new(Function::new(
12208 "REGEXP_EXTRACT_ALL".to_string(),
12209 vec![subject, pattern],
12210 ))))
12211 } else {
12212 Ok(Expression::Function(Box::new(Function::new(
12213 "REGEXP_EXTRACT_ALL".to_string(),
12214 vec![subject, pattern, group],
12215 ))))
12216 }
12217 }
12218 }
12219 } else {
12220 Ok(e)
12221 }
12222 }
12223
12224 Action::RegexpCountSnowflakeToDuckDB => {
12225 // Snowflake REGEXP_COUNT(s, p[, pos[, flags]]) ->
12226 // DuckDB: CASE WHEN p = '' THEN 0 ELSE LENGTH(REGEXP_EXTRACT_ALL(s, p)) END
12227 if let Expression::Function(f) = e {
12228 let mut args = f.args;
12229 let arg_count = args.len();
12230 let subject = args.remove(0);
12231 let pattern = args.remove(0);
12232
12233 // Handle position arg
12234 let effective_subject = if arg_count >= 3 {
12235 let position = args.remove(0);
12236 Expression::Function(Box::new(Function::new(
12237 "SUBSTRING".to_string(),
12238 vec![subject, position],
12239 )))
12240 } else {
12241 subject
12242 };
12243
12244 // Handle flags arg -> embed as (?flags) prefix in pattern
12245 let effective_pattern = if arg_count >= 4 {
12246 let flags = args.remove(0);
12247 match &flags {
12248 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(f_str) if !f_str.is_empty()) =>
12249 {
12250 let Literal::String(f_str) = lit.as_ref() else {
12251 unreachable!()
12252 };
12253 // Always use concatenation: '(?flags)' || pattern
12254 let prefix = Expression::Literal(Box::new(Literal::String(
12255 format!("(?{})", f_str),
12256 )));
12257 Expression::DPipe(Box::new(crate::expressions::DPipe {
12258 this: Box::new(prefix),
12259 expression: Box::new(pattern.clone()),
12260 safe: None,
12261 }))
12262 }
12263 _ => pattern.clone(),
12264 }
12265 } else {
12266 pattern.clone()
12267 };
12268
12269 // Build: CASE WHEN p = '' THEN 0 ELSE LENGTH(REGEXP_EXTRACT_ALL(s, p)) END
12270 let extract_all = Expression::Function(Box::new(Function::new(
12271 "REGEXP_EXTRACT_ALL".to_string(),
12272 vec![effective_subject, effective_pattern.clone()],
12273 )));
12274 let length_expr =
12275 Expression::Length(Box::new(crate::expressions::UnaryFunc {
12276 this: extract_all,
12277 original_name: None,
12278 inferred_type: None,
12279 }));
12280 let condition = Expression::Eq(Box::new(BinaryOp::new(
12281 effective_pattern,
12282 Expression::Literal(Box::new(Literal::String(String::new()))),
12283 )));
12284 Ok(Expression::Case(Box::new(Case {
12285 operand: None,
12286 whens: vec![(condition, Expression::number(0))],
12287 else_: Some(length_expr),
12288 comments: vec![],
12289 inferred_type: None,
12290 })))
12291 } else {
12292 Ok(e)
12293 }
12294 }
12295
12296 Action::RegexpInstrSnowflakeToDuckDB => {
12297 // Snowflake REGEXP_INSTR(s, p[, pos[, occ[, option[, flags[, group]]]]]) ->
12298 // DuckDB: CASE WHEN s IS NULL OR p IS NULL [OR ...] THEN NULL
12299 // WHEN p = '' THEN 0
12300 // WHEN LENGTH(REGEXP_EXTRACT_ALL(eff_s, eff_p)) < occ THEN 0
12301 // ELSE 1 + COALESCE(LIST_SUM(LIST_TRANSFORM(STRING_SPLIT_REGEX(eff_s, eff_p)[1:occ], x -> LENGTH(x))), 0)
12302 // + COALESCE(LIST_SUM(LIST_TRANSFORM(REGEXP_EXTRACT_ALL(eff_s, eff_p)[1:occ - 1], x -> LENGTH(x))), 0)
12303 // + pos_offset
12304 // END
12305 if let Expression::Function(f) = e {
12306 let mut args = f.args;
12307 let subject = args.remove(0);
12308 let pattern = if !args.is_empty() {
12309 args.remove(0)
12310 } else {
12311 Expression::Literal(Box::new(Literal::String(String::new())))
12312 };
12313
12314 // Collect all original args for NULL checks
12315 let position = if !args.is_empty() {
12316 Some(args.remove(0))
12317 } else {
12318 None
12319 };
12320 let occurrence = if !args.is_empty() {
12321 Some(args.remove(0))
12322 } else {
12323 None
12324 };
12325 let option = if !args.is_empty() {
12326 Some(args.remove(0))
12327 } else {
12328 None
12329 };
12330 let flags = if !args.is_empty() {
12331 Some(args.remove(0))
12332 } else {
12333 None
12334 };
12335 let _group = if !args.is_empty() {
12336 Some(args.remove(0))
12337 } else {
12338 None
12339 };
12340
12341 let is_pos_1 = position.as_ref().map_or(true, |p| matches!(p, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(n) if n == "1")));
12342 let occurrence_expr = occurrence.clone().unwrap_or(Expression::number(1));
12343
12344 // Build NULL check: subject IS NULL OR pattern IS NULL [OR pos IS NULL ...]
12345 let mut null_checks: Vec<Expression> = vec![
12346 Expression::Is(Box::new(BinaryOp::new(
12347 subject.clone(),
12348 Expression::Null(Null),
12349 ))),
12350 Expression::Is(Box::new(BinaryOp::new(
12351 pattern.clone(),
12352 Expression::Null(Null),
12353 ))),
12354 ];
12355 // Add NULL checks for all provided optional args
12356 for opt_arg in [&position, &occurrence, &option, &flags].iter() {
12357 if let Some(arg) = opt_arg {
12358 null_checks.push(Expression::Is(Box::new(BinaryOp::new(
12359 (*arg).clone(),
12360 Expression::Null(Null),
12361 ))));
12362 }
12363 }
12364 // Chain with OR
12365 let null_condition = null_checks
12366 .into_iter()
12367 .reduce(|a, b| Expression::Or(Box::new(BinaryOp::new(a, b))))
12368 .unwrap();
12369
12370 // Effective subject (apply position offset)
12371 let effective_subject = if is_pos_1 {
12372 subject.clone()
12373 } else {
12374 let pos = position.clone().unwrap_or(Expression::number(1));
12375 Expression::Function(Box::new(Function::new(
12376 "SUBSTRING".to_string(),
12377 vec![subject.clone(), pos],
12378 )))
12379 };
12380
12381 // Effective pattern (apply flags if present)
12382 let effective_pattern = if let Some(ref fl) = flags {
12383 if let Expression::Literal(lit) = fl {
12384 if let Literal::String(f_str) = lit.as_ref() {
12385 if !f_str.is_empty() {
12386 let prefix = Expression::Literal(Box::new(
12387 Literal::String(format!("(?{})", f_str)),
12388 ));
12389 Expression::DPipe(Box::new(crate::expressions::DPipe {
12390 this: Box::new(prefix),
12391 expression: Box::new(pattern.clone()),
12392 safe: None,
12393 }))
12394 } else {
12395 pattern.clone()
12396 }
12397 } else {
12398 fl.clone()
12399 }
12400 } else {
12401 pattern.clone()
12402 }
12403 } else {
12404 pattern.clone()
12405 };
12406
12407 // WHEN pattern = '' THEN 0
12408 let empty_pattern_check = Expression::Eq(Box::new(BinaryOp::new(
12409 effective_pattern.clone(),
12410 Expression::Literal(Box::new(Literal::String(String::new()))),
12411 )));
12412
12413 // WHEN LENGTH(REGEXP_EXTRACT_ALL(eff_s, eff_p)) < occ THEN 0
12414 let match_count_check = Expression::Lt(Box::new(BinaryOp::new(
12415 Expression::Length(Box::new(crate::expressions::UnaryFunc {
12416 this: Expression::Function(Box::new(Function::new(
12417 "REGEXP_EXTRACT_ALL".to_string(),
12418 vec![effective_subject.clone(), effective_pattern.clone()],
12419 ))),
12420 original_name: None,
12421 inferred_type: None,
12422 })),
12423 occurrence_expr.clone(),
12424 )));
12425
12426 // Helper: build LENGTH lambda for LIST_TRANSFORM
12427 let make_len_lambda = || {
12428 Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
12429 parameters: vec![crate::expressions::Identifier::new("x")],
12430 body: Expression::Length(Box::new(crate::expressions::UnaryFunc {
12431 this: Expression::Identifier(
12432 crate::expressions::Identifier::new("x"),
12433 ),
12434 original_name: None,
12435 inferred_type: None,
12436 })),
12437 colon: false,
12438 parameter_types: vec![],
12439 }))
12440 };
12441
12442 // COALESCE(LIST_SUM(LIST_TRANSFORM(STRING_SPLIT_REGEX(s, p)[1:occ], x -> LENGTH(x))), 0)
12443 let split_sliced =
12444 Expression::ArraySlice(Box::new(crate::expressions::ArraySlice {
12445 this: Expression::Function(Box::new(Function::new(
12446 "STRING_SPLIT_REGEX".to_string(),
12447 vec![effective_subject.clone(), effective_pattern.clone()],
12448 ))),
12449 start: Some(Expression::number(1)),
12450 end: Some(occurrence_expr.clone()),
12451 }));
12452 let split_sum = Expression::Function(Box::new(Function::new(
12453 "COALESCE".to_string(),
12454 vec![
12455 Expression::Function(Box::new(Function::new(
12456 "LIST_SUM".to_string(),
12457 vec![Expression::Function(Box::new(Function::new(
12458 "LIST_TRANSFORM".to_string(),
12459 vec![split_sliced, make_len_lambda()],
12460 )))],
12461 ))),
12462 Expression::number(0),
12463 ],
12464 )));
12465
12466 // COALESCE(LIST_SUM(LIST_TRANSFORM(REGEXP_EXTRACT_ALL(s, p)[1:occ - 1], x -> LENGTH(x))), 0)
12467 let extract_sliced =
12468 Expression::ArraySlice(Box::new(crate::expressions::ArraySlice {
12469 this: Expression::Function(Box::new(Function::new(
12470 "REGEXP_EXTRACT_ALL".to_string(),
12471 vec![effective_subject.clone(), effective_pattern.clone()],
12472 ))),
12473 start: Some(Expression::number(1)),
12474 end: Some(Expression::Sub(Box::new(BinaryOp::new(
12475 occurrence_expr.clone(),
12476 Expression::number(1),
12477 )))),
12478 }));
12479 let extract_sum = Expression::Function(Box::new(Function::new(
12480 "COALESCE".to_string(),
12481 vec![
12482 Expression::Function(Box::new(Function::new(
12483 "LIST_SUM".to_string(),
12484 vec![Expression::Function(Box::new(Function::new(
12485 "LIST_TRANSFORM".to_string(),
12486 vec![extract_sliced, make_len_lambda()],
12487 )))],
12488 ))),
12489 Expression::number(0),
12490 ],
12491 )));
12492
12493 // Position offset: pos - 1 when pos > 1, else 0
12494 let pos_offset: Expression = if !is_pos_1 {
12495 let pos = position.clone().unwrap_or(Expression::number(1));
12496 Expression::Sub(Box::new(BinaryOp::new(pos, Expression::number(1))))
12497 } else {
12498 Expression::number(0)
12499 };
12500
12501 // ELSE: 1 + split_sum + extract_sum + pos_offset
12502 let else_expr = Expression::Add(Box::new(BinaryOp::new(
12503 Expression::Add(Box::new(BinaryOp::new(
12504 Expression::Add(Box::new(BinaryOp::new(
12505 Expression::number(1),
12506 split_sum,
12507 ))),
12508 extract_sum,
12509 ))),
12510 pos_offset,
12511 )));
12512
12513 Ok(Expression::Case(Box::new(Case {
12514 operand: None,
12515 whens: vec![
12516 (null_condition, Expression::Null(Null)),
12517 (empty_pattern_check, Expression::number(0)),
12518 (match_count_check, Expression::number(0)),
12519 ],
12520 else_: Some(else_expr),
12521 comments: vec![],
12522 inferred_type: None,
12523 })))
12524 } else {
12525 Ok(e)
12526 }
12527 }
12528
12529 Action::RlikeSnowflakeToDuckDB => {
12530 // Snowflake RLIKE(a, b[, flags]) -> DuckDB REGEXP_FULL_MATCH(a, b[, flags])
12531 // Both do full-string matching, so no anchoring needed
12532 let (subject, pattern, flags) = match e {
12533 Expression::RegexpLike(ref rl) => {
12534 (rl.this.clone(), rl.pattern.clone(), rl.flags.clone())
12535 }
12536 Expression::Function(ref f) if f.args.len() >= 2 => {
12537 let s = f.args[0].clone();
12538 let p = f.args[1].clone();
12539 let fl = f.args.get(2).cloned();
12540 (s, p, fl)
12541 }
12542 _ => return Ok(e),
12543 };
12544
12545 let mut result_args = vec![subject, pattern];
12546 if let Some(fl) = flags {
12547 result_args.push(fl);
12548 }
12549 Ok(Expression::Function(Box::new(Function::new(
12550 "REGEXP_FULL_MATCH".to_string(),
12551 result_args,
12552 ))))
12553 }
12554
12555 Action::RegexpExtractAllToSnowflake => {
12556 // BigQuery REGEXP_EXTRACT_ALL(s, p) -> Snowflake REGEXP_SUBSTR_ALL(s, p)
12557 // With capture group: REGEXP_SUBSTR_ALL(s, p, 1, 1, 'c', 1)
12558 if let Expression::Function(f) = e {
12559 let mut args = f.args;
12560 if args.len() >= 2 {
12561 let str_expr = args.remove(0);
12562 let pattern = args.remove(0);
12563
12564 let has_groups = match &pattern {
12565 Expression::Literal(lit)
12566 if matches!(lit.as_ref(), Literal::String(_)) =>
12567 {
12568 let Literal::String(s) = lit.as_ref() else {
12569 unreachable!()
12570 };
12571 s.contains('(') && s.contains(')')
12572 }
12573 _ => false,
12574 };
12575
12576 if has_groups {
12577 Ok(Expression::Function(Box::new(Function::new(
12578 "REGEXP_SUBSTR_ALL".to_string(),
12579 vec![
12580 str_expr,
12581 pattern,
12582 Expression::number(1),
12583 Expression::number(1),
12584 Expression::Literal(Box::new(Literal::String(
12585 "c".to_string(),
12586 ))),
12587 Expression::number(1),
12588 ],
12589 ))))
12590 } else {
12591 Ok(Expression::Function(Box::new(Function::new(
12592 "REGEXP_SUBSTR_ALL".to_string(),
12593 vec![str_expr, pattern],
12594 ))))
12595 }
12596 } else {
12597 Ok(Expression::Function(Box::new(Function::new(
12598 "REGEXP_SUBSTR_ALL".to_string(),
12599 args,
12600 ))))
12601 }
12602 } else {
12603 Ok(e)
12604 }
12605 }
12606
12607 Action::SetToVariable => {
12608 // For DuckDB: SET a = 1 -> SET VARIABLE a = 1
12609 if let Expression::SetStatement(mut s) = e {
12610 for item in &mut s.items {
12611 if item.kind.is_none() {
12612 // Check if name already has VARIABLE prefix (from DuckDB source parsing)
12613 let already_variable = match &item.name {
12614 Expression::Identifier(id) => id.name.starts_with("VARIABLE "),
12615 _ => false,
12616 };
12617 if already_variable {
12618 // Extract the actual name and set kind
12619 if let Expression::Identifier(ref mut id) = item.name {
12620 let actual_name = id.name["VARIABLE ".len()..].to_string();
12621 id.name = actual_name;
12622 }
12623 }
12624 item.kind = Some("VARIABLE".to_string());
12625 }
12626 }
12627 Ok(Expression::SetStatement(s))
12628 } else {
12629 Ok(e)
12630 }
12631 }
12632
12633 Action::ConvertTimezoneToExpr => {
12634 // Convert Function("CONVERT_TIMEZONE", args) to Expression::ConvertTimezone
12635 // This prevents Redshift's transform_expr from expanding 2-arg to 3-arg with 'UTC'
12636 if let Expression::Function(f) = e {
12637 if f.args.len() == 2 {
12638 let mut args = f.args;
12639 let target_tz = args.remove(0);
12640 let timestamp = args.remove(0);
12641 Ok(Expression::ConvertTimezone(Box::new(ConvertTimezone {
12642 source_tz: None,
12643 target_tz: Some(Box::new(target_tz)),
12644 timestamp: Some(Box::new(timestamp)),
12645 options: vec![],
12646 })))
12647 } else if f.args.len() == 3 {
12648 let mut args = f.args;
12649 let source_tz = args.remove(0);
12650 let target_tz = args.remove(0);
12651 let timestamp = args.remove(0);
12652 Ok(Expression::ConvertTimezone(Box::new(ConvertTimezone {
12653 source_tz: Some(Box::new(source_tz)),
12654 target_tz: Some(Box::new(target_tz)),
12655 timestamp: Some(Box::new(timestamp)),
12656 options: vec![],
12657 })))
12658 } else {
12659 Ok(Expression::Function(f))
12660 }
12661 } else {
12662 Ok(e)
12663 }
12664 }
12665
12666 Action::BigQueryCastType => {
12667 // Convert BigQuery types to standard SQL types
12668 if let Expression::DataType(dt) = e {
12669 match dt {
12670 DataType::Custom { ref name } if name.eq_ignore_ascii_case("INT64") => {
12671 Ok(Expression::DataType(DataType::BigInt { length: None }))
12672 }
12673 DataType::Custom { ref name }
12674 if name.eq_ignore_ascii_case("FLOAT64") =>
12675 {
12676 Ok(Expression::DataType(DataType::Double {
12677 precision: None,
12678 scale: None,
12679 }))
12680 }
12681 DataType::Custom { ref name } if name.eq_ignore_ascii_case("BOOL") => {
12682 Ok(Expression::DataType(DataType::Boolean))
12683 }
12684 DataType::Custom { ref name } if name.eq_ignore_ascii_case("BYTES") => {
12685 Ok(Expression::DataType(DataType::VarBinary { length: None }))
12686 }
12687 DataType::Custom { ref name }
12688 if name.eq_ignore_ascii_case("NUMERIC") =>
12689 {
12690 // For DuckDB target, use Custom("DECIMAL") to avoid DuckDB's
12691 // default precision (18, 3) being added to bare DECIMAL
12692 if matches!(target, DialectType::DuckDB) {
12693 Ok(Expression::DataType(DataType::Custom {
12694 name: "DECIMAL".to_string(),
12695 }))
12696 } else {
12697 Ok(Expression::DataType(DataType::Decimal {
12698 precision: None,
12699 scale: None,
12700 }))
12701 }
12702 }
12703 DataType::Custom { ref name }
12704 if name.eq_ignore_ascii_case("STRING") =>
12705 {
12706 Ok(Expression::DataType(DataType::String { length: None }))
12707 }
12708 DataType::Custom { ref name }
12709 if name.eq_ignore_ascii_case("DATETIME") =>
12710 {
12711 Ok(Expression::DataType(DataType::Timestamp {
12712 precision: None,
12713 timezone: false,
12714 }))
12715 }
12716 _ => Ok(Expression::DataType(dt)),
12717 }
12718 } else {
12719 Ok(e)
12720 }
12721 }
12722
12723 Action::BigQuerySafeDivide => {
12724 // Convert SafeDivide expression to IF/CASE form for most targets
12725 if let Expression::SafeDivide(sd) = e {
12726 let x = *sd.this;
12727 let y = *sd.expression;
12728 // Wrap x and y in parens if they're complex expressions
12729 let y_ref = match &y {
12730 Expression::Column(_)
12731 | Expression::Literal(_)
12732 | Expression::Identifier(_) => y.clone(),
12733 _ => Expression::Paren(Box::new(Paren {
12734 this: y.clone(),
12735 trailing_comments: vec![],
12736 })),
12737 };
12738 let x_ref = match &x {
12739 Expression::Column(_)
12740 | Expression::Literal(_)
12741 | Expression::Identifier(_) => x.clone(),
12742 _ => Expression::Paren(Box::new(Paren {
12743 this: x.clone(),
12744 trailing_comments: vec![],
12745 })),
12746 };
12747 let condition = Expression::Neq(Box::new(BinaryOp::new(
12748 y_ref.clone(),
12749 Expression::number(0),
12750 )));
12751 let div_expr = Expression::Div(Box::new(BinaryOp::new(x_ref, y_ref)));
12752
12753 if matches!(target, DialectType::Spark | DialectType::Databricks) {
12754 Ok(Expression::Function(Box::new(Function::new(
12755 "TRY_DIVIDE".to_string(),
12756 vec![x, y],
12757 ))))
12758 } else if matches!(target, DialectType::Presto | DialectType::Trino) {
12759 // Presto/Trino: IF(y <> 0, CAST(x AS DOUBLE) / y, NULL)
12760 let cast_x = Expression::Cast(Box::new(Cast {
12761 this: match &x {
12762 Expression::Column(_)
12763 | Expression::Literal(_)
12764 | Expression::Identifier(_) => x,
12765 _ => Expression::Paren(Box::new(Paren {
12766 this: x,
12767 trailing_comments: vec![],
12768 })),
12769 },
12770 to: DataType::Double {
12771 precision: None,
12772 scale: None,
12773 },
12774 trailing_comments: vec![],
12775 double_colon_syntax: false,
12776 format: None,
12777 default: None,
12778 inferred_type: None,
12779 }));
12780 let cast_div = Expression::Div(Box::new(BinaryOp::new(
12781 cast_x,
12782 match &y {
12783 Expression::Column(_)
12784 | Expression::Literal(_)
12785 | Expression::Identifier(_) => y,
12786 _ => Expression::Paren(Box::new(Paren {
12787 this: y,
12788 trailing_comments: vec![],
12789 })),
12790 },
12791 )));
12792 Ok(Expression::IfFunc(Box::new(crate::expressions::IfFunc {
12793 condition,
12794 true_value: cast_div,
12795 false_value: Some(Expression::Null(Null)),
12796 original_name: None,
12797 inferred_type: None,
12798 })))
12799 } else if matches!(target, DialectType::PostgreSQL) {
12800 // PostgreSQL: CASE WHEN y <> 0 THEN CAST(x AS DOUBLE PRECISION) / y ELSE NULL END
12801 let cast_x = Expression::Cast(Box::new(Cast {
12802 this: match &x {
12803 Expression::Column(_)
12804 | Expression::Literal(_)
12805 | Expression::Identifier(_) => x,
12806 _ => Expression::Paren(Box::new(Paren {
12807 this: x,
12808 trailing_comments: vec![],
12809 })),
12810 },
12811 to: DataType::Custom {
12812 name: "DOUBLE PRECISION".to_string(),
12813 },
12814 trailing_comments: vec![],
12815 double_colon_syntax: false,
12816 format: None,
12817 default: None,
12818 inferred_type: None,
12819 }));
12820 let y_paren = match &y {
12821 Expression::Column(_)
12822 | Expression::Literal(_)
12823 | Expression::Identifier(_) => y,
12824 _ => Expression::Paren(Box::new(Paren {
12825 this: y,
12826 trailing_comments: vec![],
12827 })),
12828 };
12829 let cast_div =
12830 Expression::Div(Box::new(BinaryOp::new(cast_x, y_paren)));
12831 Ok(Expression::Case(Box::new(Case {
12832 operand: None,
12833 whens: vec![(condition, cast_div)],
12834 else_: Some(Expression::Null(Null)),
12835 comments: Vec::new(),
12836 inferred_type: None,
12837 })))
12838 } else if matches!(target, DialectType::DuckDB) {
12839 // DuckDB: CASE WHEN y <> 0 THEN x / y ELSE NULL END
12840 Ok(Expression::Case(Box::new(Case {
12841 operand: None,
12842 whens: vec![(condition, div_expr)],
12843 else_: Some(Expression::Null(Null)),
12844 comments: Vec::new(),
12845 inferred_type: None,
12846 })))
12847 } else if matches!(target, DialectType::Snowflake) {
12848 // Snowflake: IFF(y <> 0, x / y, NULL)
12849 Ok(Expression::IfFunc(Box::new(crate::expressions::IfFunc {
12850 condition,
12851 true_value: div_expr,
12852 false_value: Some(Expression::Null(Null)),
12853 original_name: Some("IFF".to_string()),
12854 inferred_type: None,
12855 })))
12856 } else {
12857 // All others: IF(y <> 0, x / y, NULL)
12858 Ok(Expression::IfFunc(Box::new(crate::expressions::IfFunc {
12859 condition,
12860 true_value: div_expr,
12861 false_value: Some(Expression::Null(Null)),
12862 original_name: None,
12863 inferred_type: None,
12864 })))
12865 }
12866 } else {
12867 Ok(e)
12868 }
12869 }
12870
12871 Action::BigQueryLastDayStripUnit => {
12872 if let Expression::LastDay(mut ld) = e {
12873 ld.unit = None; // Strip the unit (MONTH is default)
12874 match target {
12875 DialectType::PostgreSQL => {
12876 // LAST_DAY(date) -> CAST(DATE_TRUNC('MONTH', date) + INTERVAL '1 MONTH' - INTERVAL '1 DAY' AS DATE)
12877 let date_trunc = Expression::Function(Box::new(Function::new(
12878 "DATE_TRUNC".to_string(),
12879 vec![
12880 Expression::Literal(Box::new(
12881 crate::expressions::Literal::String(
12882 "MONTH".to_string(),
12883 ),
12884 )),
12885 ld.this.clone(),
12886 ],
12887 )));
12888 let plus_month =
12889 Expression::Add(Box::new(crate::expressions::BinaryOp::new(
12890 date_trunc,
12891 Expression::Interval(Box::new(
12892 crate::expressions::Interval {
12893 this: Some(Expression::Literal(Box::new(
12894 crate::expressions::Literal::String(
12895 "1 MONTH".to_string(),
12896 ),
12897 ))),
12898 unit: None,
12899 },
12900 )),
12901 )));
12902 let minus_day =
12903 Expression::Sub(Box::new(crate::expressions::BinaryOp::new(
12904 plus_month,
12905 Expression::Interval(Box::new(
12906 crate::expressions::Interval {
12907 this: Some(Expression::Literal(Box::new(
12908 crate::expressions::Literal::String(
12909 "1 DAY".to_string(),
12910 ),
12911 ))),
12912 unit: None,
12913 },
12914 )),
12915 )));
12916 Ok(Expression::Cast(Box::new(Cast {
12917 this: minus_day,
12918 to: DataType::Date,
12919 trailing_comments: vec![],
12920 double_colon_syntax: false,
12921 format: None,
12922 default: None,
12923 inferred_type: None,
12924 })))
12925 }
12926 DialectType::Presto => {
12927 // LAST_DAY(date) -> LAST_DAY_OF_MONTH(date)
12928 Ok(Expression::Function(Box::new(Function::new(
12929 "LAST_DAY_OF_MONTH".to_string(),
12930 vec![ld.this],
12931 ))))
12932 }
12933 DialectType::ClickHouse => {
12934 // ClickHouse LAST_DAY(CAST(x AS Nullable(DATE)))
12935 // Need to wrap the DATE type in Nullable
12936 let nullable_date = match ld.this {
12937 Expression::Cast(mut c) => {
12938 c.to = DataType::Nullable {
12939 inner: Box::new(DataType::Date),
12940 };
12941 Expression::Cast(c)
12942 }
12943 other => other,
12944 };
12945 ld.this = nullable_date;
12946 Ok(Expression::LastDay(ld))
12947 }
12948 _ => Ok(Expression::LastDay(ld)),
12949 }
12950 } else {
12951 Ok(e)
12952 }
12953 }
12954
12955 Action::BigQueryCastFormat => {
12956 // CAST(x AS DATE FORMAT 'fmt') -> PARSE_DATE('%m/%d/%Y', x) for BigQuery
12957 // CAST(x AS TIMESTAMP FORMAT 'fmt') -> PARSE_TIMESTAMP(...) for BigQuery
12958 // SAFE_CAST(x AS DATE FORMAT 'fmt') -> CAST(TRY_STRPTIME(x, ...) AS DATE) for DuckDB
12959 let (this, to, format_expr, is_safe) = match e {
12960 Expression::Cast(ref c) if c.format.is_some() => (
12961 c.this.clone(),
12962 c.to.clone(),
12963 c.format.as_ref().unwrap().as_ref().clone(),
12964 false,
12965 ),
12966 Expression::SafeCast(ref c) if c.format.is_some() => (
12967 c.this.clone(),
12968 c.to.clone(),
12969 c.format.as_ref().unwrap().as_ref().clone(),
12970 true,
12971 ),
12972 _ => return Ok(e),
12973 };
12974 // For CAST(x AS STRING FORMAT ...) when target is BigQuery, keep as-is
12975 if matches!(target, DialectType::BigQuery) {
12976 match &to {
12977 DataType::String { .. } | DataType::VarChar { .. } | DataType::Text => {
12978 // CAST(x AS STRING FORMAT 'fmt') stays as CAST expression for BigQuery
12979 return Ok(e);
12980 }
12981 _ => {}
12982 }
12983 }
12984 // Extract timezone from format if AT TIME ZONE is present
12985 let (actual_format_expr, timezone) = match &format_expr {
12986 Expression::AtTimeZone(ref atz) => {
12987 (atz.this.clone(), Some(atz.zone.clone()))
12988 }
12989 _ => (format_expr.clone(), None),
12990 };
12991 let strftime_fmt = Self::bq_cast_format_to_strftime(&actual_format_expr);
12992 match target {
12993 DialectType::BigQuery => {
12994 // CAST(x AS DATE FORMAT 'fmt') -> PARSE_DATE(strftime_fmt, x)
12995 // CAST(x AS TIMESTAMP FORMAT 'fmt' AT TIME ZONE 'tz') -> PARSE_TIMESTAMP(strftime_fmt, x, tz)
12996 let func_name = match &to {
12997 DataType::Date => "PARSE_DATE",
12998 DataType::Timestamp { .. } => "PARSE_TIMESTAMP",
12999 DataType::Time { .. } => "PARSE_TIMESTAMP",
13000 _ => "PARSE_TIMESTAMP",
13001 };
13002 let mut func_args = vec![strftime_fmt, this];
13003 if let Some(tz) = timezone {
13004 func_args.push(tz);
13005 }
13006 Ok(Expression::Function(Box::new(Function::new(
13007 func_name.to_string(),
13008 func_args,
13009 ))))
13010 }
13011 DialectType::DuckDB => {
13012 // SAFE_CAST(x AS DATE FORMAT 'fmt') -> CAST(TRY_STRPTIME(x, fmt) AS DATE)
13013 // CAST(x AS DATE FORMAT 'fmt') -> CAST(STRPTIME(x, fmt) AS DATE)
13014 let duck_fmt = Self::bq_format_to_duckdb(&strftime_fmt);
13015 let parse_fn_name = if is_safe { "TRY_STRPTIME" } else { "STRPTIME" };
13016 let parse_call = Expression::Function(Box::new(Function::new(
13017 parse_fn_name.to_string(),
13018 vec![this, duck_fmt],
13019 )));
13020 Ok(Expression::Cast(Box::new(Cast {
13021 this: parse_call,
13022 to,
13023 trailing_comments: vec![],
13024 double_colon_syntax: false,
13025 format: None,
13026 default: None,
13027 inferred_type: None,
13028 })))
13029 }
13030 _ => Ok(e),
13031 }
13032 }
13033
13034 Action::BigQueryFunctionNormalize => {
13035 Self::normalize_bigquery_function(e, source, target)
13036 }
13037
13038 Action::BigQueryToHexBare => {
13039 // Not used anymore - handled directly in normalize_bigquery_function
13040 Ok(e)
13041 }
13042
13043 Action::BigQueryToHexLower => {
13044 if let Expression::Lower(uf) = e {
13045 match uf.this {
13046 // BQ->BQ: LOWER(TO_HEX(x)) -> TO_HEX(x)
13047 Expression::Function(f)
13048 if matches!(target, DialectType::BigQuery)
13049 && f.name == "TO_HEX" =>
13050 {
13051 Ok(Expression::Function(f))
13052 }
13053 // LOWER(LOWER(HEX/TO_HEX(x))) patterns
13054 Expression::Lower(inner_uf) => {
13055 if matches!(target, DialectType::BigQuery) {
13056 // BQ->BQ: extract TO_HEX
13057 if let Expression::Function(f) = inner_uf.this {
13058 Ok(Expression::Function(Box::new(Function::new(
13059 "TO_HEX".to_string(),
13060 f.args,
13061 ))))
13062 } else {
13063 Ok(Expression::Lower(inner_uf))
13064 }
13065 } else {
13066 // Flatten: LOWER(LOWER(x)) -> LOWER(x)
13067 Ok(Expression::Lower(inner_uf))
13068 }
13069 }
13070 other => {
13071 Ok(Expression::Lower(Box::new(crate::expressions::UnaryFunc {
13072 this: other,
13073 original_name: None,
13074 inferred_type: None,
13075 })))
13076 }
13077 }
13078 } else {
13079 Ok(e)
13080 }
13081 }
13082
13083 Action::BigQueryToHexUpper => {
13084 // UPPER(LOWER(HEX(x))) -> HEX(x) (UPPER cancels LOWER, HEX is already uppercase)
13085 // UPPER(LOWER(TO_HEX(x))) -> TO_HEX(x) for Presto/Trino
13086 if let Expression::Upper(uf) = e {
13087 if let Expression::Lower(inner_uf) = uf.this {
13088 // For BQ->BQ: UPPER(TO_HEX(x)) should stay as UPPER(TO_HEX(x))
13089 if matches!(target, DialectType::BigQuery) {
13090 // Restore TO_HEX name in inner function
13091 if let Expression::Function(f) = inner_uf.this {
13092 let restored = Expression::Function(Box::new(Function::new(
13093 "TO_HEX".to_string(),
13094 f.args,
13095 )));
13096 Ok(Expression::Upper(Box::new(
13097 crate::expressions::UnaryFunc::new(restored),
13098 )))
13099 } else {
13100 Ok(Expression::Upper(inner_uf))
13101 }
13102 } else {
13103 // Extract the inner HEX/TO_HEX function (UPPER(LOWER(x)) = x when HEX is uppercase)
13104 Ok(inner_uf.this)
13105 }
13106 } else {
13107 Ok(Expression::Upper(uf))
13108 }
13109 } else {
13110 Ok(e)
13111 }
13112 }
13113
13114 Action::BigQueryAnyValueHaving => {
13115 // ANY_VALUE(x HAVING MAX y) -> ARG_MAX_NULL(x, y)
13116 // ANY_VALUE(x HAVING MIN y) -> ARG_MIN_NULL(x, y)
13117 if let Expression::AnyValue(agg) = e {
13118 if let Some((having_expr, is_max)) = agg.having_max {
13119 let func_name = if is_max {
13120 "ARG_MAX_NULL"
13121 } else {
13122 "ARG_MIN_NULL"
13123 };
13124 Ok(Expression::Function(Box::new(Function::new(
13125 func_name.to_string(),
13126 vec![agg.this, *having_expr],
13127 ))))
13128 } else {
13129 Ok(Expression::AnyValue(agg))
13130 }
13131 } else {
13132 Ok(e)
13133 }
13134 }
13135
13136 Action::BigQueryApproxQuantiles => {
13137 // APPROX_QUANTILES(x, n) -> APPROX_QUANTILE(x, [0, 1/n, 2/n, ..., 1])
13138 // APPROX_QUANTILES(DISTINCT x, n) -> APPROX_QUANTILE(DISTINCT x, [0, 1/n, ..., 1])
13139 if let Expression::AggregateFunction(agg) = e {
13140 if agg.args.len() >= 2 {
13141 let x_expr = agg.args[0].clone();
13142 let n_expr = &agg.args[1];
13143
13144 // Extract the numeric value from n_expr
13145 let n = match n_expr {
13146 Expression::Literal(lit)
13147 if matches!(
13148 lit.as_ref(),
13149 crate::expressions::Literal::Number(_)
13150 ) =>
13151 {
13152 let crate::expressions::Literal::Number(s) = lit.as_ref()
13153 else {
13154 unreachable!()
13155 };
13156 s.parse::<usize>().unwrap_or(2)
13157 }
13158 _ => 2,
13159 };
13160
13161 // Generate quantile array: [0, 1/n, 2/n, ..., 1]
13162 let mut quantiles = Vec::new();
13163 for i in 0..=n {
13164 let q = i as f64 / n as f64;
13165 // Format nicely: 0 -> 0, 0.25 -> 0.25, 1 -> 1
13166 if q == 0.0 {
13167 quantiles.push(Expression::number(0));
13168 } else if q == 1.0 {
13169 quantiles.push(Expression::number(1));
13170 } else {
13171 quantiles.push(Expression::Literal(Box::new(
13172 crate::expressions::Literal::Number(format!("{}", q)),
13173 )));
13174 }
13175 }
13176
13177 let array_expr =
13178 Expression::Array(Box::new(crate::expressions::Array {
13179 expressions: quantiles,
13180 }));
13181
13182 // Preserve DISTINCT modifier
13183 let mut new_func = Function::new(
13184 "APPROX_QUANTILE".to_string(),
13185 vec![x_expr, array_expr],
13186 );
13187 new_func.distinct = agg.distinct;
13188 Ok(Expression::Function(Box::new(new_func)))
13189 } else {
13190 Ok(Expression::AggregateFunction(agg))
13191 }
13192 } else {
13193 Ok(e)
13194 }
13195 }
13196
13197 Action::GenericFunctionNormalize => {
13198 // Helper closure to convert ARBITRARY to target-specific function
13199 fn convert_arbitrary(arg: Expression, target: DialectType) -> Expression {
13200 let name = match target {
13201 DialectType::ClickHouse => "any",
13202 DialectType::TSQL | DialectType::SQLite => "MAX",
13203 DialectType::Hive => "FIRST",
13204 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
13205 "ARBITRARY"
13206 }
13207 _ => "ANY_VALUE",
13208 };
13209 Expression::Function(Box::new(Function::new(name.to_string(), vec![arg])))
13210 }
13211
13212 if let Expression::Function(f) = e {
13213 let name = f.name.to_ascii_uppercase();
13214 match name.as_str() {
13215 "ARBITRARY" if f.args.len() == 1 => {
13216 let arg = f.args.into_iter().next().unwrap();
13217 Ok(convert_arbitrary(arg, target))
13218 }
13219 "TO_NUMBER" if f.args.len() == 1 => {
13220 let arg = f.args.into_iter().next().unwrap();
13221 match target {
13222 DialectType::Oracle | DialectType::Snowflake => {
13223 Ok(Expression::Function(Box::new(Function::new(
13224 "TO_NUMBER".to_string(),
13225 vec![arg],
13226 ))))
13227 }
13228 _ => Ok(Expression::Cast(Box::new(crate::expressions::Cast {
13229 this: arg,
13230 to: crate::expressions::DataType::Double {
13231 precision: None,
13232 scale: None,
13233 },
13234 double_colon_syntax: false,
13235 trailing_comments: Vec::new(),
13236 format: None,
13237 default: None,
13238 inferred_type: None,
13239 }))),
13240 }
13241 }
13242 "AGGREGATE" if f.args.len() >= 3 => match target {
13243 DialectType::DuckDB
13244 | DialectType::Hive
13245 | DialectType::Presto
13246 | DialectType::Trino => Ok(Expression::Function(Box::new(
13247 Function::new("REDUCE".to_string(), f.args),
13248 ))),
13249 _ => Ok(Expression::Function(f)),
13250 },
13251 // REGEXP_MATCHES(x, y) -> RegexpLike for most targets, keep as-is for DuckDB
13252 "REGEXP_MATCHES" if f.args.len() >= 2 => {
13253 if matches!(target, DialectType::DuckDB) {
13254 Ok(Expression::Function(f))
13255 } else {
13256 let mut args = f.args;
13257 let this = args.remove(0);
13258 let pattern = args.remove(0);
13259 let flags = if args.is_empty() {
13260 None
13261 } else {
13262 Some(args.remove(0))
13263 };
13264 Ok(Expression::RegexpLike(Box::new(
13265 crate::expressions::RegexpFunc {
13266 this,
13267 pattern,
13268 flags,
13269 },
13270 )))
13271 }
13272 }
13273 // REGEXP_FULL_MATCH (Hive REGEXP) -> RegexpLike
13274 "REGEXP_FULL_MATCH" if f.args.len() >= 2 => {
13275 if matches!(target, DialectType::DuckDB) {
13276 Ok(Expression::Function(f))
13277 } else {
13278 let mut args = f.args;
13279 let this = args.remove(0);
13280 let pattern = args.remove(0);
13281 let flags = if args.is_empty() {
13282 None
13283 } else {
13284 Some(args.remove(0))
13285 };
13286 Ok(Expression::RegexpLike(Box::new(
13287 crate::expressions::RegexpFunc {
13288 this,
13289 pattern,
13290 flags,
13291 },
13292 )))
13293 }
13294 }
13295 // STRUCT_EXTRACT(x, 'field') -> x.field (StructExtract expression)
13296 "STRUCT_EXTRACT" if f.args.len() == 2 => {
13297 let mut args = f.args;
13298 let this = args.remove(0);
13299 let field_expr = args.remove(0);
13300 // Extract string literal to get field name
13301 let field_name = match &field_expr {
13302 Expression::Literal(lit)
13303 if matches!(
13304 lit.as_ref(),
13305 crate::expressions::Literal::String(_)
13306 ) =>
13307 {
13308 let crate::expressions::Literal::String(s) = lit.as_ref()
13309 else {
13310 unreachable!()
13311 };
13312 s.clone()
13313 }
13314 Expression::Identifier(id) => id.name.clone(),
13315 _ => {
13316 return Ok(Expression::Function(Box::new(Function::new(
13317 "STRUCT_EXTRACT".to_string(),
13318 vec![this, field_expr],
13319 ))))
13320 }
13321 };
13322 Ok(Expression::StructExtract(Box::new(
13323 crate::expressions::StructExtractFunc {
13324 this,
13325 field: crate::expressions::Identifier::new(field_name),
13326 },
13327 )))
13328 }
13329 // LIST_FILTER([4,5,6], x -> x > 4) -> FILTER(ARRAY(4,5,6), x -> x > 4)
13330 "LIST_FILTER" if f.args.len() == 2 => {
13331 let name = match target {
13332 DialectType::DuckDB => "LIST_FILTER",
13333 _ => "FILTER",
13334 };
13335 Ok(Expression::Function(Box::new(Function::new(
13336 name.to_string(),
13337 f.args,
13338 ))))
13339 }
13340 // LIST_TRANSFORM(x, y -> y + 1) -> TRANSFORM(x, y -> y + 1)
13341 "LIST_TRANSFORM" if f.args.len() == 2 => {
13342 let name = match target {
13343 DialectType::DuckDB => "LIST_TRANSFORM",
13344 _ => "TRANSFORM",
13345 };
13346 Ok(Expression::Function(Box::new(Function::new(
13347 name.to_string(),
13348 f.args,
13349 ))))
13350 }
13351 // LIST_SORT(x) -> LIST_SORT(x) for DuckDB, ARRAY_SORT(x) for Presto/Trino, SORT_ARRAY(x) for others
13352 "LIST_SORT" if f.args.len() >= 1 => {
13353 let name = match target {
13354 DialectType::DuckDB => "LIST_SORT",
13355 DialectType::Presto | DialectType::Trino => "ARRAY_SORT",
13356 _ => "SORT_ARRAY",
13357 };
13358 Ok(Expression::Function(Box::new(Function::new(
13359 name.to_string(),
13360 f.args,
13361 ))))
13362 }
13363 // LIST_REVERSE_SORT(x) -> SORT_ARRAY(x, FALSE) for Spark/Hive, ARRAY_SORT(x, lambda) for Presto
13364 "LIST_REVERSE_SORT" if f.args.len() >= 1 => {
13365 match target {
13366 DialectType::DuckDB => Ok(Expression::Function(Box::new(
13367 Function::new("ARRAY_REVERSE_SORT".to_string(), f.args),
13368 ))),
13369 DialectType::Spark
13370 | DialectType::Databricks
13371 | DialectType::Hive => {
13372 let mut args = f.args;
13373 args.push(Expression::Identifier(
13374 crate::expressions::Identifier::new("FALSE"),
13375 ));
13376 Ok(Expression::Function(Box::new(Function::new(
13377 "SORT_ARRAY".to_string(),
13378 args,
13379 ))))
13380 }
13381 DialectType::Presto
13382 | DialectType::Trino
13383 | DialectType::Athena => {
13384 // ARRAY_SORT(x, (a, b) -> CASE WHEN a < b THEN 1 WHEN a > b THEN -1 ELSE 0 END)
13385 let arr = f.args.into_iter().next().unwrap();
13386 let lambda = Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
13387 parameters: vec![
13388 crate::expressions::Identifier::new("a"),
13389 crate::expressions::Identifier::new("b"),
13390 ],
13391 body: Expression::Case(Box::new(Case {
13392 operand: None,
13393 whens: vec![
13394 (
13395 Expression::Lt(Box::new(BinaryOp::new(
13396 Expression::Identifier(crate::expressions::Identifier::new("a")),
13397 Expression::Identifier(crate::expressions::Identifier::new("b")),
13398 ))),
13399 Expression::number(1),
13400 ),
13401 (
13402 Expression::Gt(Box::new(BinaryOp::new(
13403 Expression::Identifier(crate::expressions::Identifier::new("a")),
13404 Expression::Identifier(crate::expressions::Identifier::new("b")),
13405 ))),
13406 Expression::Literal(Box::new(Literal::Number("-1".to_string()))),
13407 ),
13408 ],
13409 else_: Some(Expression::number(0)),
13410 comments: Vec::new(),
13411 inferred_type: None,
13412 })),
13413 colon: false,
13414 parameter_types: Vec::new(),
13415 }));
13416 Ok(Expression::Function(Box::new(Function::new(
13417 "ARRAY_SORT".to_string(),
13418 vec![arr, lambda],
13419 ))))
13420 }
13421 _ => Ok(Expression::Function(Box::new(Function::new(
13422 "LIST_REVERSE_SORT".to_string(),
13423 f.args,
13424 )))),
13425 }
13426 }
13427 // SPLIT_TO_ARRAY(x) with 1 arg -> add default ',' separator and rename
13428 "SPLIT_TO_ARRAY" if f.args.len() == 1 => {
13429 let mut args = f.args;
13430 args.push(Expression::string(","));
13431 let name = match target {
13432 DialectType::DuckDB => "STR_SPLIT",
13433 DialectType::Presto | DialectType::Trino => "SPLIT",
13434 DialectType::Spark
13435 | DialectType::Databricks
13436 | DialectType::Hive => "SPLIT",
13437 DialectType::PostgreSQL => "STRING_TO_ARRAY",
13438 DialectType::Redshift => "SPLIT_TO_ARRAY",
13439 _ => "SPLIT",
13440 };
13441 Ok(Expression::Function(Box::new(Function::new(
13442 name.to_string(),
13443 args,
13444 ))))
13445 }
13446 // SPLIT_TO_ARRAY(x, sep) with 2 args -> rename based on target
13447 "SPLIT_TO_ARRAY" if f.args.len() == 2 => {
13448 let name = match target {
13449 DialectType::DuckDB => "STR_SPLIT",
13450 DialectType::Presto | DialectType::Trino => "SPLIT",
13451 DialectType::Spark
13452 | DialectType::Databricks
13453 | DialectType::Hive => "SPLIT",
13454 DialectType::PostgreSQL => "STRING_TO_ARRAY",
13455 DialectType::Redshift => "SPLIT_TO_ARRAY",
13456 _ => "SPLIT",
13457 };
13458 Ok(Expression::Function(Box::new(Function::new(
13459 name.to_string(),
13460 f.args,
13461 ))))
13462 }
13463 // STRING_TO_ARRAY/STR_SPLIT -> target-specific split function
13464 "STRING_TO_ARRAY" | "STR_SPLIT" if f.args.len() >= 2 => {
13465 let name = match target {
13466 DialectType::DuckDB => "STR_SPLIT",
13467 DialectType::Presto | DialectType::Trino => "SPLIT",
13468 DialectType::Spark
13469 | DialectType::Databricks
13470 | DialectType::Hive => "SPLIT",
13471 DialectType::Doris | DialectType::StarRocks => {
13472 "SPLIT_BY_STRING"
13473 }
13474 DialectType::PostgreSQL | DialectType::Redshift => {
13475 "STRING_TO_ARRAY"
13476 }
13477 _ => "SPLIT",
13478 };
13479 // For Spark/Hive, SPLIT uses regex - need to escape literal with \Q...\E
13480 if matches!(
13481 target,
13482 DialectType::Spark
13483 | DialectType::Databricks
13484 | DialectType::Hive
13485 ) {
13486 let mut args = f.args;
13487 let x = args.remove(0);
13488 let sep = args.remove(0);
13489 // Wrap separator in CONCAT('\\Q', sep, '\\E')
13490 let escaped_sep =
13491 Expression::Function(Box::new(Function::new(
13492 "CONCAT".to_string(),
13493 vec![
13494 Expression::string("\\Q"),
13495 sep,
13496 Expression::string("\\E"),
13497 ],
13498 )));
13499 Ok(Expression::Function(Box::new(Function::new(
13500 name.to_string(),
13501 vec![x, escaped_sep],
13502 ))))
13503 } else {
13504 Ok(Expression::Function(Box::new(Function::new(
13505 name.to_string(),
13506 f.args,
13507 ))))
13508 }
13509 }
13510 // STR_SPLIT_REGEX(x, 'a') / REGEXP_SPLIT(x, 'a') -> target-specific regex split
13511 "STR_SPLIT_REGEX" | "REGEXP_SPLIT" if f.args.len() == 2 => {
13512 let name = match target {
13513 DialectType::DuckDB => "STR_SPLIT_REGEX",
13514 DialectType::Presto | DialectType::Trino => "REGEXP_SPLIT",
13515 DialectType::Spark
13516 | DialectType::Databricks
13517 | DialectType::Hive => "SPLIT",
13518 _ => "REGEXP_SPLIT",
13519 };
13520 Ok(Expression::Function(Box::new(Function::new(
13521 name.to_string(),
13522 f.args,
13523 ))))
13524 }
13525 // SPLIT(str, delim) from Snowflake -> DuckDB with CASE wrapper
13526 "SPLIT"
13527 if f.args.len() == 2
13528 && matches!(source, DialectType::Snowflake)
13529 && matches!(target, DialectType::DuckDB) =>
13530 {
13531 let mut args = f.args;
13532 let str_arg = args.remove(0);
13533 let delim_arg = args.remove(0);
13534
13535 // STR_SPLIT(str, delim) as the base
13536 let base_func = Expression::Function(Box::new(Function::new(
13537 "STR_SPLIT".to_string(),
13538 vec![str_arg.clone(), delim_arg.clone()],
13539 )));
13540
13541 // [str] - array with single element
13542 let array_with_input =
13543 Expression::Array(Box::new(crate::expressions::Array {
13544 expressions: vec![str_arg],
13545 }));
13546
13547 // CASE
13548 // WHEN delim IS NULL THEN NULL
13549 // WHEN delim = '' THEN [str]
13550 // ELSE STR_SPLIT(str, delim)
13551 // END
13552 Ok(Expression::Case(Box::new(Case {
13553 operand: None,
13554 whens: vec![
13555 (
13556 Expression::Is(Box::new(BinaryOp {
13557 left: delim_arg.clone(),
13558 right: Expression::Null(Null),
13559 left_comments: vec![],
13560 operator_comments: vec![],
13561 trailing_comments: vec![],
13562 inferred_type: None,
13563 })),
13564 Expression::Null(Null),
13565 ),
13566 (
13567 Expression::Eq(Box::new(BinaryOp {
13568 left: delim_arg,
13569 right: Expression::string(""),
13570 left_comments: vec![],
13571 operator_comments: vec![],
13572 trailing_comments: vec![],
13573 inferred_type: None,
13574 })),
13575 array_with_input,
13576 ),
13577 ],
13578 else_: Some(base_func),
13579 comments: vec![],
13580 inferred_type: None,
13581 })))
13582 }
13583 // SPLIT(x, sep) from Presto/StarRocks/Doris -> target-specific split with regex escaping for Hive/Spark
13584 "SPLIT"
13585 if f.args.len() == 2
13586 && matches!(
13587 source,
13588 DialectType::Presto
13589 | DialectType::Trino
13590 | DialectType::Athena
13591 | DialectType::StarRocks
13592 | DialectType::Doris
13593 )
13594 && matches!(
13595 target,
13596 DialectType::Spark
13597 | DialectType::Databricks
13598 | DialectType::Hive
13599 ) =>
13600 {
13601 // Presto/StarRocks SPLIT is literal, Hive/Spark SPLIT is regex
13602 let mut args = f.args;
13603 let x = args.remove(0);
13604 let sep = args.remove(0);
13605 let escaped_sep = Expression::Function(Box::new(Function::new(
13606 "CONCAT".to_string(),
13607 vec![Expression::string("\\Q"), sep, Expression::string("\\E")],
13608 )));
13609 Ok(Expression::Function(Box::new(Function::new(
13610 "SPLIT".to_string(),
13611 vec![x, escaped_sep],
13612 ))))
13613 }
13614 // SUBSTRINGINDEX -> SUBSTRING_INDEX (ClickHouse camelCase to standard)
13615 // For ClickHouse target, preserve original name to maintain camelCase
13616 "SUBSTRINGINDEX" => {
13617 let name = if matches!(target, DialectType::ClickHouse) {
13618 f.name.clone()
13619 } else {
13620 "SUBSTRING_INDEX".to_string()
13621 };
13622 Ok(Expression::Function(Box::new(Function::new(name, f.args))))
13623 }
13624 // ARRAY_LENGTH/SIZE/CARDINALITY -> target-specific array length function
13625 "ARRAY_LENGTH" | "SIZE" | "CARDINALITY" => {
13626 // DuckDB source CARDINALITY -> DuckDB target: keep as CARDINALITY (used for maps)
13627 if name == "CARDINALITY"
13628 && matches!(source, DialectType::DuckDB)
13629 && matches!(target, DialectType::DuckDB)
13630 {
13631 return Ok(Expression::Function(f));
13632 }
13633 // Get the array argument (first arg, drop dimension args)
13634 let mut args = f.args;
13635 let arr = if args.is_empty() {
13636 return Ok(Expression::Function(Box::new(Function::new(
13637 name.to_string(),
13638 args,
13639 ))));
13640 } else {
13641 args.remove(0)
13642 };
13643 let name =
13644 match target {
13645 DialectType::Spark
13646 | DialectType::Databricks
13647 | DialectType::Hive => "SIZE",
13648 DialectType::Presto | DialectType::Trino => "CARDINALITY",
13649 DialectType::BigQuery => "ARRAY_LENGTH",
13650 DialectType::DuckDB => {
13651 // DuckDB: use ARRAY_LENGTH with all args
13652 let mut all_args = vec![arr];
13653 all_args.extend(args);
13654 return Ok(Expression::Function(Box::new(
13655 Function::new("ARRAY_LENGTH".to_string(), all_args),
13656 )));
13657 }
13658 DialectType::PostgreSQL | DialectType::Redshift => {
13659 // Keep ARRAY_LENGTH with dimension arg
13660 let mut all_args = vec![arr];
13661 all_args.extend(args);
13662 return Ok(Expression::Function(Box::new(
13663 Function::new("ARRAY_LENGTH".to_string(), all_args),
13664 )));
13665 }
13666 DialectType::ClickHouse => "LENGTH",
13667 _ => "ARRAY_LENGTH",
13668 };
13669 Ok(Expression::Function(Box::new(Function::new(
13670 name.to_string(),
13671 vec![arr],
13672 ))))
13673 }
13674 // TO_VARIANT(x) -> CAST(x AS VARIANT) for DuckDB
13675 "TO_VARIANT" if f.args.len() == 1 => match target {
13676 DialectType::DuckDB => {
13677 let arg = f.args.into_iter().next().unwrap();
13678 Ok(Expression::Cast(Box::new(Cast {
13679 this: arg,
13680 to: DataType::Custom {
13681 name: "VARIANT".to_string(),
13682 },
13683 double_colon_syntax: false,
13684 trailing_comments: Vec::new(),
13685 format: None,
13686 default: None,
13687 inferred_type: None,
13688 })))
13689 }
13690 _ => Ok(Expression::Function(f)),
13691 },
13692 // JSON_GROUP_ARRAY(x) -> JSON_AGG(x) for PostgreSQL
13693 "JSON_GROUP_ARRAY" if f.args.len() == 1 => match target {
13694 DialectType::PostgreSQL => Ok(Expression::Function(Box::new(
13695 Function::new("JSON_AGG".to_string(), f.args),
13696 ))),
13697 _ => Ok(Expression::Function(f)),
13698 },
13699 // JSON_GROUP_OBJECT(key, value) -> JSON_OBJECT_AGG(key, value) for PostgreSQL
13700 "JSON_GROUP_OBJECT" if f.args.len() == 2 => match target {
13701 DialectType::PostgreSQL => Ok(Expression::Function(Box::new(
13702 Function::new("JSON_OBJECT_AGG".to_string(), f.args),
13703 ))),
13704 _ => Ok(Expression::Function(f)),
13705 },
13706 // UNICODE(x) -> target-specific codepoint function
13707 "UNICODE" if f.args.len() == 1 => {
13708 match target {
13709 DialectType::SQLite | DialectType::DuckDB => {
13710 Ok(Expression::Function(Box::new(Function::new(
13711 "UNICODE".to_string(),
13712 f.args,
13713 ))))
13714 }
13715 DialectType::Oracle => {
13716 // ASCII(UNISTR(x))
13717 let inner = Expression::Function(Box::new(Function::new(
13718 "UNISTR".to_string(),
13719 f.args,
13720 )));
13721 Ok(Expression::Function(Box::new(Function::new(
13722 "ASCII".to_string(),
13723 vec![inner],
13724 ))))
13725 }
13726 DialectType::MySQL => {
13727 // ORD(CONVERT(x USING utf32))
13728 let arg = f.args.into_iter().next().unwrap();
13729 let convert_expr = Expression::ConvertToCharset(Box::new(
13730 crate::expressions::ConvertToCharset {
13731 this: Box::new(arg),
13732 dest: Some(Box::new(Expression::Identifier(
13733 crate::expressions::Identifier::new("utf32"),
13734 ))),
13735 source: None,
13736 },
13737 ));
13738 Ok(Expression::Function(Box::new(Function::new(
13739 "ORD".to_string(),
13740 vec![convert_expr],
13741 ))))
13742 }
13743 _ => Ok(Expression::Function(Box::new(Function::new(
13744 "ASCII".to_string(),
13745 f.args,
13746 )))),
13747 }
13748 }
13749 // XOR(a, b, ...) -> a XOR b XOR ... for MySQL, BITWISE_XOR for Presto/Trino, # for PostgreSQL, ^ for BigQuery
13750 "XOR" if f.args.len() >= 2 => {
13751 match target {
13752 DialectType::ClickHouse => {
13753 // ClickHouse: keep as xor() function with lowercase name
13754 Ok(Expression::Function(Box::new(Function::new(
13755 "xor".to_string(),
13756 f.args,
13757 ))))
13758 }
13759 DialectType::Presto | DialectType::Trino => {
13760 if f.args.len() == 2 {
13761 Ok(Expression::Function(Box::new(Function::new(
13762 "BITWISE_XOR".to_string(),
13763 f.args,
13764 ))))
13765 } else {
13766 // Nest: BITWISE_XOR(BITWISE_XOR(a, b), c)
13767 let mut args = f.args;
13768 let first = args.remove(0);
13769 let second = args.remove(0);
13770 let mut result =
13771 Expression::Function(Box::new(Function::new(
13772 "BITWISE_XOR".to_string(),
13773 vec![first, second],
13774 )));
13775 for arg in args {
13776 result =
13777 Expression::Function(Box::new(Function::new(
13778 "BITWISE_XOR".to_string(),
13779 vec![result, arg],
13780 )));
13781 }
13782 Ok(result)
13783 }
13784 }
13785 DialectType::MySQL
13786 | DialectType::SingleStore
13787 | DialectType::Doris
13788 | DialectType::StarRocks => {
13789 // Convert XOR(a, b, c) -> Expression::Xor with expressions list
13790 let args = f.args;
13791 Ok(Expression::Xor(Box::new(crate::expressions::Xor {
13792 this: None,
13793 expression: None,
13794 expressions: args,
13795 })))
13796 }
13797 DialectType::PostgreSQL | DialectType::Redshift => {
13798 // PostgreSQL: a # b (hash operator for XOR)
13799 let mut args = f.args;
13800 let first = args.remove(0);
13801 let second = args.remove(0);
13802 let mut result = Expression::BitwiseXor(Box::new(
13803 BinaryOp::new(first, second),
13804 ));
13805 for arg in args {
13806 result = Expression::BitwiseXor(Box::new(
13807 BinaryOp::new(result, arg),
13808 ));
13809 }
13810 Ok(result)
13811 }
13812 DialectType::DuckDB => {
13813 // DuckDB: keep as XOR function (DuckDB ^ is Power, not XOR)
13814 Ok(Expression::Function(Box::new(Function::new(
13815 "XOR".to_string(),
13816 f.args,
13817 ))))
13818 }
13819 DialectType::BigQuery => {
13820 // BigQuery: a ^ b (caret operator for XOR)
13821 let mut args = f.args;
13822 let first = args.remove(0);
13823 let second = args.remove(0);
13824 let mut result = Expression::BitwiseXor(Box::new(
13825 BinaryOp::new(first, second),
13826 ));
13827 for arg in args {
13828 result = Expression::BitwiseXor(Box::new(
13829 BinaryOp::new(result, arg),
13830 ));
13831 }
13832 Ok(result)
13833 }
13834 _ => Ok(Expression::Function(Box::new(Function::new(
13835 "XOR".to_string(),
13836 f.args,
13837 )))),
13838 }
13839 }
13840 // ARRAY_REVERSE_SORT(x) -> SORT_ARRAY(x, FALSE) for Spark/Hive, ARRAY_SORT(x, lambda) for Presto
13841 "ARRAY_REVERSE_SORT" if f.args.len() >= 1 => {
13842 match target {
13843 DialectType::Spark
13844 | DialectType::Databricks
13845 | DialectType::Hive => {
13846 let mut args = f.args;
13847 args.push(Expression::Identifier(
13848 crate::expressions::Identifier::new("FALSE"),
13849 ));
13850 Ok(Expression::Function(Box::new(Function::new(
13851 "SORT_ARRAY".to_string(),
13852 args,
13853 ))))
13854 }
13855 DialectType::Presto
13856 | DialectType::Trino
13857 | DialectType::Athena => {
13858 // ARRAY_SORT(x, (a, b) -> CASE WHEN a < b THEN 1 WHEN a > b THEN -1 ELSE 0 END)
13859 let arr = f.args.into_iter().next().unwrap();
13860 let lambda = Expression::Lambda(Box::new(
13861 crate::expressions::LambdaExpr {
13862 parameters: vec![
13863 Identifier::new("a"),
13864 Identifier::new("b"),
13865 ],
13866 colon: false,
13867 parameter_types: Vec::new(),
13868 body: Expression::Case(Box::new(Case {
13869 operand: None,
13870 whens: vec![
13871 (
13872 Expression::Lt(Box::new(
13873 BinaryOp::new(
13874 Expression::Identifier(
13875 Identifier::new("a"),
13876 ),
13877 Expression::Identifier(
13878 Identifier::new("b"),
13879 ),
13880 ),
13881 )),
13882 Expression::number(1),
13883 ),
13884 (
13885 Expression::Gt(Box::new(
13886 BinaryOp::new(
13887 Expression::Identifier(
13888 Identifier::new("a"),
13889 ),
13890 Expression::Identifier(
13891 Identifier::new("b"),
13892 ),
13893 ),
13894 )),
13895 Expression::Neg(Box::new(
13896 crate::expressions::UnaryOp {
13897 this: Expression::number(1),
13898 inferred_type: None,
13899 },
13900 )),
13901 ),
13902 ],
13903 else_: Some(Expression::number(0)),
13904 comments: Vec::new(),
13905 inferred_type: None,
13906 })),
13907 },
13908 ));
13909 Ok(Expression::Function(Box::new(Function::new(
13910 "ARRAY_SORT".to_string(),
13911 vec![arr, lambda],
13912 ))))
13913 }
13914 _ => Ok(Expression::Function(Box::new(Function::new(
13915 "ARRAY_REVERSE_SORT".to_string(),
13916 f.args,
13917 )))),
13918 }
13919 }
13920 // ENCODE(x) -> ENCODE(x, 'utf-8') for Spark/Hive, TO_UTF8(x) for Presto
13921 "ENCODE" if f.args.len() == 1 => match target {
13922 DialectType::Spark
13923 | DialectType::Databricks
13924 | DialectType::Hive => {
13925 let mut args = f.args;
13926 args.push(Expression::string("utf-8"));
13927 Ok(Expression::Function(Box::new(Function::new(
13928 "ENCODE".to_string(),
13929 args,
13930 ))))
13931 }
13932 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
13933 Ok(Expression::Function(Box::new(Function::new(
13934 "TO_UTF8".to_string(),
13935 f.args,
13936 ))))
13937 }
13938 _ => Ok(Expression::Function(Box::new(Function::new(
13939 "ENCODE".to_string(),
13940 f.args,
13941 )))),
13942 },
13943 // DECODE(x) -> DECODE(x, 'utf-8') for Spark/Hive, FROM_UTF8(x) for Presto
13944 "DECODE" if f.args.len() == 1 => match target {
13945 DialectType::Spark
13946 | DialectType::Databricks
13947 | DialectType::Hive => {
13948 let mut args = f.args;
13949 args.push(Expression::string("utf-8"));
13950 Ok(Expression::Function(Box::new(Function::new(
13951 "DECODE".to_string(),
13952 args,
13953 ))))
13954 }
13955 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
13956 Ok(Expression::Function(Box::new(Function::new(
13957 "FROM_UTF8".to_string(),
13958 f.args,
13959 ))))
13960 }
13961 _ => Ok(Expression::Function(Box::new(Function::new(
13962 "DECODE".to_string(),
13963 f.args,
13964 )))),
13965 },
13966 // QUANTILE(x, p) -> PERCENTILE(x, p) for Spark/Hive
13967 "QUANTILE" if f.args.len() == 2 => {
13968 let name = match target {
13969 DialectType::Spark
13970 | DialectType::Databricks
13971 | DialectType::Hive => "PERCENTILE",
13972 DialectType::Presto | DialectType::Trino => "APPROX_PERCENTILE",
13973 DialectType::BigQuery => "PERCENTILE_CONT",
13974 _ => "QUANTILE",
13975 };
13976 Ok(Expression::Function(Box::new(Function::new(
13977 name.to_string(),
13978 f.args,
13979 ))))
13980 }
13981 // QUANTILE_CONT(x, q) -> PERCENTILE_CONT(q) WITHIN GROUP (ORDER BY x) for PostgreSQL/Snowflake
13982 "QUANTILE_CONT" if f.args.len() == 2 => {
13983 let mut args = f.args;
13984 let column = args.remove(0);
13985 let quantile = args.remove(0);
13986 match target {
13987 DialectType::DuckDB => {
13988 Ok(Expression::Function(Box::new(Function::new(
13989 "QUANTILE_CONT".to_string(),
13990 vec![column, quantile],
13991 ))))
13992 }
13993 DialectType::PostgreSQL
13994 | DialectType::Redshift
13995 | DialectType::Snowflake => {
13996 // PERCENTILE_CONT(q) WITHIN GROUP (ORDER BY x)
13997 let inner = Expression::PercentileCont(Box::new(
13998 crate::expressions::PercentileFunc {
13999 this: column.clone(),
14000 percentile: quantile,
14001 order_by: None,
14002 filter: None,
14003 },
14004 ));
14005 Ok(Expression::WithinGroup(Box::new(
14006 crate::expressions::WithinGroup {
14007 this: inner,
14008 order_by: vec![crate::expressions::Ordered {
14009 this: column,
14010 desc: false,
14011 nulls_first: None,
14012 explicit_asc: false,
14013 with_fill: None,
14014 }],
14015 },
14016 )))
14017 }
14018 _ => Ok(Expression::Function(Box::new(Function::new(
14019 "QUANTILE_CONT".to_string(),
14020 vec![column, quantile],
14021 )))),
14022 }
14023 }
14024 // QUANTILE_DISC(x, q) -> PERCENTILE_DISC(q) WITHIN GROUP (ORDER BY x) for PostgreSQL/Snowflake
14025 "QUANTILE_DISC" if f.args.len() == 2 => {
14026 let mut args = f.args;
14027 let column = args.remove(0);
14028 let quantile = args.remove(0);
14029 match target {
14030 DialectType::DuckDB => {
14031 Ok(Expression::Function(Box::new(Function::new(
14032 "QUANTILE_DISC".to_string(),
14033 vec![column, quantile],
14034 ))))
14035 }
14036 DialectType::PostgreSQL
14037 | DialectType::Redshift
14038 | DialectType::Snowflake => {
14039 // PERCENTILE_DISC(q) WITHIN GROUP (ORDER BY x)
14040 let inner = Expression::PercentileDisc(Box::new(
14041 crate::expressions::PercentileFunc {
14042 this: column.clone(),
14043 percentile: quantile,
14044 order_by: None,
14045 filter: None,
14046 },
14047 ));
14048 Ok(Expression::WithinGroup(Box::new(
14049 crate::expressions::WithinGroup {
14050 this: inner,
14051 order_by: vec![crate::expressions::Ordered {
14052 this: column,
14053 desc: false,
14054 nulls_first: None,
14055 explicit_asc: false,
14056 with_fill: None,
14057 }],
14058 },
14059 )))
14060 }
14061 _ => Ok(Expression::Function(Box::new(Function::new(
14062 "QUANTILE_DISC".to_string(),
14063 vec![column, quantile],
14064 )))),
14065 }
14066 }
14067 // PERCENTILE_APPROX(x, p) / APPROX_PERCENTILE(x, p) -> target-specific
14068 "PERCENTILE_APPROX" | "APPROX_PERCENTILE" if f.args.len() >= 2 => {
14069 let name = match target {
14070 DialectType::Presto
14071 | DialectType::Trino
14072 | DialectType::Athena => "APPROX_PERCENTILE",
14073 DialectType::Spark
14074 | DialectType::Databricks
14075 | DialectType::Hive => "PERCENTILE_APPROX",
14076 DialectType::DuckDB => "APPROX_QUANTILE",
14077 DialectType::PostgreSQL | DialectType::Redshift => {
14078 "PERCENTILE_CONT"
14079 }
14080 _ => &f.name,
14081 };
14082 Ok(Expression::Function(Box::new(Function::new(
14083 name.to_string(),
14084 f.args,
14085 ))))
14086 }
14087 // EPOCH(x) -> UNIX_TIMESTAMP(x) for Spark/Hive
14088 "EPOCH" if f.args.len() == 1 => {
14089 let name = match target {
14090 DialectType::Spark
14091 | DialectType::Databricks
14092 | DialectType::Hive => "UNIX_TIMESTAMP",
14093 DialectType::Presto | DialectType::Trino => "TO_UNIXTIME",
14094 _ => "EPOCH",
14095 };
14096 Ok(Expression::Function(Box::new(Function::new(
14097 name.to_string(),
14098 f.args,
14099 ))))
14100 }
14101 // EPOCH_MS(x) -> target-specific epoch milliseconds conversion
14102 "EPOCH_MS" if f.args.len() == 1 => {
14103 match target {
14104 DialectType::Spark | DialectType::Databricks => {
14105 Ok(Expression::Function(Box::new(Function::new(
14106 "TIMESTAMP_MILLIS".to_string(),
14107 f.args,
14108 ))))
14109 }
14110 DialectType::Hive => {
14111 // Hive: FROM_UNIXTIME(x / 1000)
14112 let arg = f.args.into_iter().next().unwrap();
14113 let div_expr = Expression::Div(Box::new(
14114 crate::expressions::BinaryOp::new(
14115 arg,
14116 Expression::number(1000),
14117 ),
14118 ));
14119 Ok(Expression::Function(Box::new(Function::new(
14120 "FROM_UNIXTIME".to_string(),
14121 vec![div_expr],
14122 ))))
14123 }
14124 DialectType::Presto | DialectType::Trino => {
14125 Ok(Expression::Function(Box::new(Function::new(
14126 "FROM_UNIXTIME".to_string(),
14127 vec![Expression::Div(Box::new(
14128 crate::expressions::BinaryOp::new(
14129 f.args.into_iter().next().unwrap(),
14130 Expression::number(1000),
14131 ),
14132 ))],
14133 ))))
14134 }
14135 _ => Ok(Expression::Function(Box::new(Function::new(
14136 "EPOCH_MS".to_string(),
14137 f.args,
14138 )))),
14139 }
14140 }
14141 // HASHBYTES('algorithm', x) -> target-specific hash function
14142 "HASHBYTES" if f.args.len() == 2 => {
14143 // Keep HASHBYTES as-is for TSQL target
14144 if matches!(target, DialectType::TSQL) {
14145 return Ok(Expression::Function(f));
14146 }
14147 let algo_expr = &f.args[0];
14148 let algo = match algo_expr {
14149 Expression::Literal(lit)
14150 if matches!(
14151 lit.as_ref(),
14152 crate::expressions::Literal::String(_)
14153 ) =>
14154 {
14155 let crate::expressions::Literal::String(s) = lit.as_ref()
14156 else {
14157 unreachable!()
14158 };
14159 s.to_ascii_uppercase()
14160 }
14161 _ => return Ok(Expression::Function(f)),
14162 };
14163 let data_arg = f.args.into_iter().nth(1).unwrap();
14164 match algo.as_str() {
14165 "SHA1" => {
14166 let name = match target {
14167 DialectType::Spark | DialectType::Databricks => "SHA",
14168 DialectType::Hive => "SHA1",
14169 _ => "SHA1",
14170 };
14171 Ok(Expression::Function(Box::new(Function::new(
14172 name.to_string(),
14173 vec![data_arg],
14174 ))))
14175 }
14176 "SHA2_256" => {
14177 Ok(Expression::Function(Box::new(Function::new(
14178 "SHA2".to_string(),
14179 vec![data_arg, Expression::number(256)],
14180 ))))
14181 }
14182 "SHA2_512" => {
14183 Ok(Expression::Function(Box::new(Function::new(
14184 "SHA2".to_string(),
14185 vec![data_arg, Expression::number(512)],
14186 ))))
14187 }
14188 "MD5" => Ok(Expression::Function(Box::new(Function::new(
14189 "MD5".to_string(),
14190 vec![data_arg],
14191 )))),
14192 _ => Ok(Expression::Function(Box::new(Function::new(
14193 "HASHBYTES".to_string(),
14194 vec![Expression::string(&algo), data_arg],
14195 )))),
14196 }
14197 }
14198 // JSON_EXTRACT_PATH(json, key1, key2, ...) -> target-specific JSON extraction
14199 "JSON_EXTRACT_PATH" | "JSON_EXTRACT_PATH_TEXT" if f.args.len() >= 2 => {
14200 let is_text = name == "JSON_EXTRACT_PATH_TEXT";
14201 let mut args = f.args;
14202 let json_expr = args.remove(0);
14203 // Build JSON path from remaining keys: $.key1.key2 or $.key1[0]
14204 let mut json_path = "$".to_string();
14205 for a in &args {
14206 match a {
14207 Expression::Literal(lit)
14208 if matches!(
14209 lit.as_ref(),
14210 crate::expressions::Literal::String(_)
14211 ) =>
14212 {
14213 let crate::expressions::Literal::String(s) =
14214 lit.as_ref()
14215 else {
14216 unreachable!()
14217 };
14218 // Numeric string keys become array indices: [0]
14219 if s.chars().all(|c| c.is_ascii_digit()) {
14220 json_path.push('[');
14221 json_path.push_str(s);
14222 json_path.push(']');
14223 } else {
14224 json_path.push('.');
14225 json_path.push_str(s);
14226 }
14227 }
14228 _ => {
14229 json_path.push_str(".?");
14230 }
14231 }
14232 }
14233 match target {
14234 DialectType::Spark
14235 | DialectType::Databricks
14236 | DialectType::Hive => {
14237 Ok(Expression::Function(Box::new(Function::new(
14238 "GET_JSON_OBJECT".to_string(),
14239 vec![json_expr, Expression::string(&json_path)],
14240 ))))
14241 }
14242 DialectType::Presto | DialectType::Trino => {
14243 let func_name = if is_text {
14244 "JSON_EXTRACT_SCALAR"
14245 } else {
14246 "JSON_EXTRACT"
14247 };
14248 Ok(Expression::Function(Box::new(Function::new(
14249 func_name.to_string(),
14250 vec![json_expr, Expression::string(&json_path)],
14251 ))))
14252 }
14253 DialectType::BigQuery | DialectType::MySQL => {
14254 let func_name = if is_text {
14255 "JSON_EXTRACT_SCALAR"
14256 } else {
14257 "JSON_EXTRACT"
14258 };
14259 Ok(Expression::Function(Box::new(Function::new(
14260 func_name.to_string(),
14261 vec![json_expr, Expression::string(&json_path)],
14262 ))))
14263 }
14264 DialectType::PostgreSQL | DialectType::Materialize => {
14265 // Keep as JSON_EXTRACT_PATH_TEXT / JSON_EXTRACT_PATH for PostgreSQL/Materialize
14266 let func_name = if is_text {
14267 "JSON_EXTRACT_PATH_TEXT"
14268 } else {
14269 "JSON_EXTRACT_PATH"
14270 };
14271 let mut new_args = vec![json_expr];
14272 new_args.extend(args);
14273 Ok(Expression::Function(Box::new(Function::new(
14274 func_name.to_string(),
14275 new_args,
14276 ))))
14277 }
14278 DialectType::DuckDB | DialectType::SQLite => {
14279 // Use -> for JSON_EXTRACT_PATH, ->> for JSON_EXTRACT_PATH_TEXT
14280 if is_text {
14281 Ok(Expression::JsonExtractScalar(Box::new(
14282 crate::expressions::JsonExtractFunc {
14283 this: json_expr,
14284 path: Expression::string(&json_path),
14285 returning: None,
14286 arrow_syntax: true,
14287 hash_arrow_syntax: false,
14288 wrapper_option: None,
14289 quotes_option: None,
14290 on_scalar_string: false,
14291 on_error: None,
14292 },
14293 )))
14294 } else {
14295 Ok(Expression::JsonExtract(Box::new(
14296 crate::expressions::JsonExtractFunc {
14297 this: json_expr,
14298 path: Expression::string(&json_path),
14299 returning: None,
14300 arrow_syntax: true,
14301 hash_arrow_syntax: false,
14302 wrapper_option: None,
14303 quotes_option: None,
14304 on_scalar_string: false,
14305 on_error: None,
14306 },
14307 )))
14308 }
14309 }
14310 DialectType::Redshift => {
14311 // Keep as JSON_EXTRACT_PATH_TEXT for Redshift
14312 let mut new_args = vec![json_expr];
14313 new_args.extend(args);
14314 Ok(Expression::Function(Box::new(Function::new(
14315 "JSON_EXTRACT_PATH_TEXT".to_string(),
14316 new_args,
14317 ))))
14318 }
14319 DialectType::TSQL => {
14320 // ISNULL(JSON_QUERY(json, '$.path'), JSON_VALUE(json, '$.path'))
14321 let jq = Expression::Function(Box::new(Function::new(
14322 "JSON_QUERY".to_string(),
14323 vec![json_expr.clone(), Expression::string(&json_path)],
14324 )));
14325 let jv = Expression::Function(Box::new(Function::new(
14326 "JSON_VALUE".to_string(),
14327 vec![json_expr, Expression::string(&json_path)],
14328 )));
14329 Ok(Expression::Function(Box::new(Function::new(
14330 "ISNULL".to_string(),
14331 vec![jq, jv],
14332 ))))
14333 }
14334 DialectType::ClickHouse => {
14335 let func_name = if is_text {
14336 "JSONExtractString"
14337 } else {
14338 "JSONExtractRaw"
14339 };
14340 let mut new_args = vec![json_expr];
14341 new_args.extend(args);
14342 Ok(Expression::Function(Box::new(Function::new(
14343 func_name.to_string(),
14344 new_args,
14345 ))))
14346 }
14347 _ => {
14348 let func_name = if is_text {
14349 "JSON_EXTRACT_SCALAR"
14350 } else {
14351 "JSON_EXTRACT"
14352 };
14353 Ok(Expression::Function(Box::new(Function::new(
14354 func_name.to_string(),
14355 vec![json_expr, Expression::string(&json_path)],
14356 ))))
14357 }
14358 }
14359 }
14360 // APPROX_DISTINCT(x) -> APPROX_COUNT_DISTINCT(x) for Spark/Hive/BigQuery
14361 "APPROX_DISTINCT" if f.args.len() >= 1 => {
14362 let name = match target {
14363 DialectType::Spark
14364 | DialectType::Databricks
14365 | DialectType::Hive
14366 | DialectType::BigQuery => "APPROX_COUNT_DISTINCT",
14367 _ => "APPROX_DISTINCT",
14368 };
14369 let mut args = f.args;
14370 // Hive doesn't support the accuracy parameter
14371 if name == "APPROX_COUNT_DISTINCT"
14372 && matches!(target, DialectType::Hive)
14373 {
14374 args.truncate(1);
14375 }
14376 Ok(Expression::Function(Box::new(Function::new(
14377 name.to_string(),
14378 args,
14379 ))))
14380 }
14381 // REGEXP_EXTRACT(x, pattern) - normalize default group index
14382 "REGEXP_EXTRACT" if f.args.len() == 2 => {
14383 // Determine source default group index
14384 let source_default = match source {
14385 DialectType::Presto
14386 | DialectType::Trino
14387 | DialectType::DuckDB => 0,
14388 _ => 1, // Hive/Spark/Databricks default = 1
14389 };
14390 // Determine target default group index
14391 let target_default = match target {
14392 DialectType::Presto
14393 | DialectType::Trino
14394 | DialectType::DuckDB
14395 | DialectType::BigQuery => 0,
14396 DialectType::Snowflake => {
14397 // Snowflake uses REGEXP_SUBSTR
14398 return Ok(Expression::Function(Box::new(Function::new(
14399 "REGEXP_SUBSTR".to_string(),
14400 f.args,
14401 ))));
14402 }
14403 _ => 1, // Hive/Spark/Databricks default = 1
14404 };
14405 if source_default != target_default {
14406 let mut args = f.args;
14407 args.push(Expression::number(source_default));
14408 Ok(Expression::Function(Box::new(Function::new(
14409 "REGEXP_EXTRACT".to_string(),
14410 args,
14411 ))))
14412 } else {
14413 Ok(Expression::Function(Box::new(Function::new(
14414 "REGEXP_EXTRACT".to_string(),
14415 f.args,
14416 ))))
14417 }
14418 }
14419 // RLIKE(str, pattern) -> RegexpLike expression (generates as target-specific form)
14420 "RLIKE" if f.args.len() == 2 => {
14421 let mut args = f.args;
14422 let str_expr = args.remove(0);
14423 let pattern = args.remove(0);
14424 match target {
14425 DialectType::DuckDB => {
14426 // REGEXP_MATCHES(str, pattern)
14427 Ok(Expression::Function(Box::new(Function::new(
14428 "REGEXP_MATCHES".to_string(),
14429 vec![str_expr, pattern],
14430 ))))
14431 }
14432 _ => {
14433 // Convert to RegexpLike which generates as RLIKE/~/REGEXP_LIKE per dialect
14434 Ok(Expression::RegexpLike(Box::new(
14435 crate::expressions::RegexpFunc {
14436 this: str_expr,
14437 pattern,
14438 flags: None,
14439 },
14440 )))
14441 }
14442 }
14443 }
14444 // EOMONTH(date[, month_offset]) -> target-specific
14445 "EOMONTH" if f.args.len() >= 1 => {
14446 let mut args = f.args;
14447 let date_arg = args.remove(0);
14448 let month_offset = if !args.is_empty() {
14449 Some(args.remove(0))
14450 } else {
14451 None
14452 };
14453
14454 // Helper: wrap date in CAST to DATE
14455 let cast_to_date = |e: Expression| -> Expression {
14456 Expression::Cast(Box::new(Cast {
14457 this: e,
14458 to: DataType::Date,
14459 trailing_comments: vec![],
14460 double_colon_syntax: false,
14461 format: None,
14462 default: None,
14463 inferred_type: None,
14464 }))
14465 };
14466
14467 match target {
14468 DialectType::TSQL | DialectType::Fabric => {
14469 // TSQL: EOMONTH(CAST(date AS DATE)) or EOMONTH(DATEADD(MONTH, offset, CAST(date AS DATE)))
14470 let date = cast_to_date(date_arg);
14471 let date = if let Some(offset) = month_offset {
14472 Expression::Function(Box::new(Function::new(
14473 "DATEADD".to_string(),
14474 vec![
14475 Expression::Identifier(Identifier::new(
14476 "MONTH",
14477 )),
14478 offset,
14479 date,
14480 ],
14481 )))
14482 } else {
14483 date
14484 };
14485 Ok(Expression::Function(Box::new(Function::new(
14486 "EOMONTH".to_string(),
14487 vec![date],
14488 ))))
14489 }
14490 DialectType::Presto
14491 | DialectType::Trino
14492 | DialectType::Athena => {
14493 // Presto: LAST_DAY_OF_MONTH(CAST(CAST(date AS TIMESTAMP) AS DATE))
14494 // or with offset: LAST_DAY_OF_MONTH(DATE_ADD('MONTH', offset, CAST(CAST(date AS TIMESTAMP) AS DATE)))
14495 let cast_ts = Expression::Cast(Box::new(Cast {
14496 this: date_arg,
14497 to: DataType::Timestamp {
14498 timezone: false,
14499 precision: None,
14500 },
14501 trailing_comments: vec![],
14502 double_colon_syntax: false,
14503 format: None,
14504 default: None,
14505 inferred_type: None,
14506 }));
14507 let date = cast_to_date(cast_ts);
14508 let date = if let Some(offset) = month_offset {
14509 Expression::Function(Box::new(Function::new(
14510 "DATE_ADD".to_string(),
14511 vec![Expression::string("MONTH"), offset, date],
14512 )))
14513 } else {
14514 date
14515 };
14516 Ok(Expression::Function(Box::new(Function::new(
14517 "LAST_DAY_OF_MONTH".to_string(),
14518 vec![date],
14519 ))))
14520 }
14521 DialectType::PostgreSQL => {
14522 // PostgreSQL: CAST(DATE_TRUNC('MONTH', CAST(date AS DATE) [+ INTERVAL 'offset MONTH']) + INTERVAL '1 MONTH' - INTERVAL '1 DAY' AS DATE)
14523 let date = cast_to_date(date_arg);
14524 let date = if let Some(offset) = month_offset {
14525 let interval_str = format!(
14526 "{} MONTH",
14527 Self::expr_to_string_static(&offset)
14528 );
14529 Expression::Add(Box::new(
14530 crate::expressions::BinaryOp::new(
14531 date,
14532 Expression::Interval(Box::new(
14533 crate::expressions::Interval {
14534 this: Some(Expression::string(
14535 &interval_str,
14536 )),
14537 unit: None,
14538 },
14539 )),
14540 ),
14541 ))
14542 } else {
14543 date
14544 };
14545 let truncated =
14546 Expression::Function(Box::new(Function::new(
14547 "DATE_TRUNC".to_string(),
14548 vec![Expression::string("MONTH"), date],
14549 )));
14550 let plus_month = Expression::Add(Box::new(
14551 crate::expressions::BinaryOp::new(
14552 truncated,
14553 Expression::Interval(Box::new(
14554 crate::expressions::Interval {
14555 this: Some(Expression::string("1 MONTH")),
14556 unit: None,
14557 },
14558 )),
14559 ),
14560 ));
14561 let minus_day = Expression::Sub(Box::new(
14562 crate::expressions::BinaryOp::new(
14563 plus_month,
14564 Expression::Interval(Box::new(
14565 crate::expressions::Interval {
14566 this: Some(Expression::string("1 DAY")),
14567 unit: None,
14568 },
14569 )),
14570 ),
14571 ));
14572 Ok(Expression::Cast(Box::new(Cast {
14573 this: minus_day,
14574 to: DataType::Date,
14575 trailing_comments: vec![],
14576 double_colon_syntax: false,
14577 format: None,
14578 default: None,
14579 inferred_type: None,
14580 })))
14581 }
14582 DialectType::DuckDB => {
14583 // DuckDB: LAST_DAY(CAST(date AS DATE) [+ INTERVAL (offset) MONTH])
14584 let date = cast_to_date(date_arg);
14585 let date = if let Some(offset) = month_offset {
14586 // Wrap negative numbers in parentheses for DuckDB INTERVAL
14587 let interval_val =
14588 if matches!(&offset, Expression::Neg(_)) {
14589 Expression::Paren(Box::new(
14590 crate::expressions::Paren {
14591 this: offset,
14592 trailing_comments: Vec::new(),
14593 },
14594 ))
14595 } else {
14596 offset
14597 };
14598 Expression::Add(Box::new(crate::expressions::BinaryOp::new(
14599 date,
14600 Expression::Interval(Box::new(crate::expressions::Interval {
14601 this: Some(interval_val),
14602 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
14603 unit: crate::expressions::IntervalUnit::Month,
14604 use_plural: false,
14605 }),
14606 })),
14607 )))
14608 } else {
14609 date
14610 };
14611 Ok(Expression::Function(Box::new(Function::new(
14612 "LAST_DAY".to_string(),
14613 vec![date],
14614 ))))
14615 }
14616 DialectType::Snowflake | DialectType::Redshift => {
14617 // Snowflake/Redshift: LAST_DAY(TO_DATE(date) or CAST(date AS DATE))
14618 // With offset: LAST_DAY(DATEADD(MONTH, offset, TO_DATE(date)))
14619 let date = if matches!(target, DialectType::Snowflake) {
14620 Expression::Function(Box::new(Function::new(
14621 "TO_DATE".to_string(),
14622 vec![date_arg],
14623 )))
14624 } else {
14625 cast_to_date(date_arg)
14626 };
14627 let date = if let Some(offset) = month_offset {
14628 Expression::Function(Box::new(Function::new(
14629 "DATEADD".to_string(),
14630 vec![
14631 Expression::Identifier(Identifier::new(
14632 "MONTH",
14633 )),
14634 offset,
14635 date,
14636 ],
14637 )))
14638 } else {
14639 date
14640 };
14641 Ok(Expression::Function(Box::new(Function::new(
14642 "LAST_DAY".to_string(),
14643 vec![date],
14644 ))))
14645 }
14646 DialectType::Spark | DialectType::Databricks => {
14647 // Spark: LAST_DAY(TO_DATE(date))
14648 // With offset: LAST_DAY(ADD_MONTHS(TO_DATE(date), offset))
14649 let date = Expression::Function(Box::new(Function::new(
14650 "TO_DATE".to_string(),
14651 vec![date_arg],
14652 )));
14653 let date = if let Some(offset) = month_offset {
14654 Expression::Function(Box::new(Function::new(
14655 "ADD_MONTHS".to_string(),
14656 vec![date, offset],
14657 )))
14658 } else {
14659 date
14660 };
14661 Ok(Expression::Function(Box::new(Function::new(
14662 "LAST_DAY".to_string(),
14663 vec![date],
14664 ))))
14665 }
14666 DialectType::MySQL => {
14667 // MySQL: LAST_DAY(DATE(date)) - no offset
14668 // With offset: LAST_DAY(DATE_ADD(date, INTERVAL offset MONTH)) - no DATE() wrapper
14669 let date = if let Some(offset) = month_offset {
14670 let iu = crate::expressions::IntervalUnit::Month;
14671 Expression::DateAdd(Box::new(
14672 crate::expressions::DateAddFunc {
14673 this: date_arg,
14674 interval: offset,
14675 unit: iu,
14676 },
14677 ))
14678 } else {
14679 Expression::Function(Box::new(Function::new(
14680 "DATE".to_string(),
14681 vec![date_arg],
14682 )))
14683 };
14684 Ok(Expression::Function(Box::new(Function::new(
14685 "LAST_DAY".to_string(),
14686 vec![date],
14687 ))))
14688 }
14689 DialectType::BigQuery => {
14690 // BigQuery: LAST_DAY(CAST(date AS DATE))
14691 // With offset: LAST_DAY(DATE_ADD(CAST(date AS DATE), INTERVAL offset MONTH))
14692 let date = cast_to_date(date_arg);
14693 let date = if let Some(offset) = month_offset {
14694 let interval = Expression::Interval(Box::new(crate::expressions::Interval {
14695 this: Some(offset),
14696 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
14697 unit: crate::expressions::IntervalUnit::Month,
14698 use_plural: false,
14699 }),
14700 }));
14701 Expression::Function(Box::new(Function::new(
14702 "DATE_ADD".to_string(),
14703 vec![date, interval],
14704 )))
14705 } else {
14706 date
14707 };
14708 Ok(Expression::Function(Box::new(Function::new(
14709 "LAST_DAY".to_string(),
14710 vec![date],
14711 ))))
14712 }
14713 DialectType::ClickHouse => {
14714 // ClickHouse: LAST_DAY(CAST(date AS Nullable(DATE)))
14715 let date = Expression::Cast(Box::new(Cast {
14716 this: date_arg,
14717 to: DataType::Nullable {
14718 inner: Box::new(DataType::Date),
14719 },
14720 trailing_comments: vec![],
14721 double_colon_syntax: false,
14722 format: None,
14723 default: None,
14724 inferred_type: None,
14725 }));
14726 let date = if let Some(offset) = month_offset {
14727 Expression::Function(Box::new(Function::new(
14728 "DATE_ADD".to_string(),
14729 vec![
14730 Expression::Identifier(Identifier::new(
14731 "MONTH",
14732 )),
14733 offset,
14734 date,
14735 ],
14736 )))
14737 } else {
14738 date
14739 };
14740 Ok(Expression::Function(Box::new(Function::new(
14741 "LAST_DAY".to_string(),
14742 vec![date],
14743 ))))
14744 }
14745 DialectType::Hive => {
14746 // Hive: LAST_DAY(date)
14747 let date = if let Some(offset) = month_offset {
14748 Expression::Function(Box::new(Function::new(
14749 "ADD_MONTHS".to_string(),
14750 vec![date_arg, offset],
14751 )))
14752 } else {
14753 date_arg
14754 };
14755 Ok(Expression::Function(Box::new(Function::new(
14756 "LAST_DAY".to_string(),
14757 vec![date],
14758 ))))
14759 }
14760 _ => {
14761 // Default: LAST_DAY(date)
14762 let date = if let Some(offset) = month_offset {
14763 let unit =
14764 Expression::Identifier(Identifier::new("MONTH"));
14765 Expression::Function(Box::new(Function::new(
14766 "DATEADD".to_string(),
14767 vec![unit, offset, date_arg],
14768 )))
14769 } else {
14770 date_arg
14771 };
14772 Ok(Expression::Function(Box::new(Function::new(
14773 "LAST_DAY".to_string(),
14774 vec![date],
14775 ))))
14776 }
14777 }
14778 }
14779 // LAST_DAY(x) / LAST_DAY_OF_MONTH(x) -> target-specific
14780 "LAST_DAY" | "LAST_DAY_OF_MONTH"
14781 if !matches!(source, DialectType::BigQuery)
14782 && f.args.len() >= 1 =>
14783 {
14784 let first_arg = f.args.into_iter().next().unwrap();
14785 match target {
14786 DialectType::TSQL | DialectType::Fabric => {
14787 Ok(Expression::Function(Box::new(Function::new(
14788 "EOMONTH".to_string(),
14789 vec![first_arg],
14790 ))))
14791 }
14792 DialectType::Presto
14793 | DialectType::Trino
14794 | DialectType::Athena => {
14795 Ok(Expression::Function(Box::new(Function::new(
14796 "LAST_DAY_OF_MONTH".to_string(),
14797 vec![first_arg],
14798 ))))
14799 }
14800 _ => Ok(Expression::Function(Box::new(Function::new(
14801 "LAST_DAY".to_string(),
14802 vec![first_arg],
14803 )))),
14804 }
14805 }
14806 // BigQuery PARSE_DATETIME(format, value) -> target-specific parsing calls.
14807 "PARSE_DATETIME"
14808 if matches!(source, DialectType::BigQuery) && f.args.len() == 2 =>
14809 {
14810 fn expand_bigquery_datetime_format(expr: Expression) -> Expression {
14811 match expr {
14812 Expression::Literal(lit) => match lit.as_ref() {
14813 Literal::String(s) => Expression::string(
14814 s.replace("%F", "%Y-%m-%d")
14815 .replace("%T", "%H:%M:%S"),
14816 ),
14817 _ => Expression::Literal(lit),
14818 },
14819 other => other,
14820 }
14821 }
14822
14823 let mut args = f.args;
14824 let format = expand_bigquery_datetime_format(args.remove(0));
14825 let value = args.remove(0);
14826 match target {
14827 DialectType::DuckDB => {
14828 let value_with_year = Expression::Concat(Box::new(
14829 crate::expressions::BinaryOp::new(
14830 Expression::string("1970 "),
14831 value,
14832 ),
14833 ));
14834 let format_with_year = Expression::Concat(Box::new(
14835 crate::expressions::BinaryOp::new(
14836 Expression::string("%Y "),
14837 format,
14838 ),
14839 ));
14840 Ok(Expression::Function(Box::new(Function::new(
14841 "STRPTIME".to_string(),
14842 vec![value_with_year, format_with_year],
14843 ))))
14844 }
14845 DialectType::Snowflake => {
14846 Ok(Expression::Function(Box::new(Function::new(
14847 "PARSE_DATETIME".to_string(),
14848 vec![value, format],
14849 ))))
14850 }
14851 _ => Ok(Expression::Function(Box::new(Function::new(
14852 "PARSE_DATETIME".to_string(),
14853 vec![format, value],
14854 )))),
14855 }
14856 }
14857 // Presto/Trino ISO-8601 helpers become casts outside Presto-family targets.
14858 "FROM_ISO8601_TIMESTAMP"
14859 if matches!(
14860 source,
14861 DialectType::Presto | DialectType::Trino | DialectType::Athena
14862 ) && f.args.len() == 1
14863 && !matches!(
14864 target,
14865 DialectType::Presto
14866 | DialectType::Trino
14867 | DialectType::Athena
14868 ) =>
14869 {
14870 Ok(Expression::Cast(Box::new(crate::expressions::Cast {
14871 this: f.args.into_iter().next().unwrap(),
14872 to: DataType::Timestamp {
14873 precision: None,
14874 timezone: matches!(
14875 target,
14876 DialectType::DuckDB | DialectType::Snowflake
14877 ),
14878 },
14879 trailing_comments: Vec::new(),
14880 double_colon_syntax: false,
14881 format: None,
14882 default: None,
14883 inferred_type: None,
14884 })))
14885 }
14886 "FROM_ISO8601_DATE"
14887 if matches!(
14888 source,
14889 DialectType::Presto | DialectType::Trino | DialectType::Athena
14890 ) && f.args.len() == 1
14891 && !matches!(
14892 target,
14893 DialectType::Presto
14894 | DialectType::Trino
14895 | DialectType::Athena
14896 ) =>
14897 {
14898 Ok(Expression::Cast(Box::new(crate::expressions::Cast {
14899 this: f.args.into_iter().next().unwrap(),
14900 to: DataType::Date,
14901 trailing_comments: Vec::new(),
14902 double_colon_syntax: false,
14903 format: None,
14904 default: None,
14905 inferred_type: None,
14906 })))
14907 }
14908 // MAP(keys_array, vals_array) from Presto (2-arg form) -> target-specific
14909 "MAP"
14910 if f.args.len() == 2
14911 && matches!(
14912 source,
14913 DialectType::Presto
14914 | DialectType::Trino
14915 | DialectType::Athena
14916 ) =>
14917 {
14918 let keys_arg = f.args[0].clone();
14919 let vals_arg = f.args[1].clone();
14920
14921 // Helper: extract array elements from Array/ArrayFunc/Function("ARRAY") expressions
14922 fn extract_array_elements(
14923 expr: &Expression,
14924 ) -> Option<&Vec<Expression>> {
14925 match expr {
14926 Expression::Array(arr) => Some(&arr.expressions),
14927 Expression::ArrayFunc(arr) => Some(&arr.expressions),
14928 Expression::Function(f)
14929 if f.name.eq_ignore_ascii_case("ARRAY") =>
14930 {
14931 Some(&f.args)
14932 }
14933 _ => None,
14934 }
14935 }
14936
14937 match target {
14938 DialectType::Spark | DialectType::Databricks => {
14939 // Presto MAP(keys, vals) -> Spark MAP_FROM_ARRAYS(keys, vals)
14940 Ok(Expression::Function(Box::new(Function::new(
14941 "MAP_FROM_ARRAYS".to_string(),
14942 f.args,
14943 ))))
14944 }
14945 DialectType::Hive => {
14946 // Presto MAP(ARRAY[k1,k2], ARRAY[v1,v2]) -> Hive MAP(k1, v1, k2, v2)
14947 if let (Some(keys), Some(vals)) = (
14948 extract_array_elements(&keys_arg),
14949 extract_array_elements(&vals_arg),
14950 ) {
14951 if keys.len() == vals.len() {
14952 let mut interleaved = Vec::new();
14953 for (k, v) in keys.iter().zip(vals.iter()) {
14954 interleaved.push(k.clone());
14955 interleaved.push(v.clone());
14956 }
14957 Ok(Expression::Function(Box::new(Function::new(
14958 "MAP".to_string(),
14959 interleaved,
14960 ))))
14961 } else {
14962 Ok(Expression::Function(Box::new(Function::new(
14963 "MAP".to_string(),
14964 f.args,
14965 ))))
14966 }
14967 } else {
14968 Ok(Expression::Function(Box::new(Function::new(
14969 "MAP".to_string(),
14970 f.args,
14971 ))))
14972 }
14973 }
14974 DialectType::Snowflake => {
14975 // Presto MAP(ARRAY[k1,k2], ARRAY[v1,v2]) -> Snowflake OBJECT_CONSTRUCT(k1, v1, k2, v2)
14976 if let (Some(keys), Some(vals)) = (
14977 extract_array_elements(&keys_arg),
14978 extract_array_elements(&vals_arg),
14979 ) {
14980 if keys.len() == vals.len() {
14981 let mut interleaved = Vec::new();
14982 for (k, v) in keys.iter().zip(vals.iter()) {
14983 interleaved.push(k.clone());
14984 interleaved.push(v.clone());
14985 }
14986 Ok(Expression::Function(Box::new(Function::new(
14987 "OBJECT_CONSTRUCT".to_string(),
14988 interleaved,
14989 ))))
14990 } else {
14991 Ok(Expression::Function(Box::new(Function::new(
14992 "MAP".to_string(),
14993 f.args,
14994 ))))
14995 }
14996 } else {
14997 Ok(Expression::Function(Box::new(Function::new(
14998 "MAP".to_string(),
14999 f.args,
15000 ))))
15001 }
15002 }
15003 _ => Ok(Expression::Function(f)),
15004 }
15005 }
15006 // MAP() with 0 args from Spark -> MAP(ARRAY[], ARRAY[]) for Presto/Trino
15007 "MAP"
15008 if f.args.is_empty()
15009 && matches!(
15010 source,
15011 DialectType::Hive
15012 | DialectType::Spark
15013 | DialectType::Databricks
15014 )
15015 && matches!(
15016 target,
15017 DialectType::Presto
15018 | DialectType::Trino
15019 | DialectType::Athena
15020 ) =>
15021 {
15022 let empty_keys =
15023 Expression::Array(Box::new(crate::expressions::Array {
15024 expressions: vec![],
15025 }));
15026 let empty_vals =
15027 Expression::Array(Box::new(crate::expressions::Array {
15028 expressions: vec![],
15029 }));
15030 Ok(Expression::Function(Box::new(Function::new(
15031 "MAP".to_string(),
15032 vec![empty_keys, empty_vals],
15033 ))))
15034 }
15035 // MAP(k1, v1, k2, v2, ...) from Hive/Spark -> target-specific
15036 "MAP"
15037 if f.args.len() >= 2
15038 && f.args.len() % 2 == 0
15039 && matches!(
15040 source,
15041 DialectType::Hive
15042 | DialectType::Spark
15043 | DialectType::Databricks
15044 | DialectType::ClickHouse
15045 | DialectType::StarRocks
15046 ) =>
15047 {
15048 let args = f.args;
15049 match target {
15050 DialectType::DuckDB => {
15051 // MAP([k1, k2], [v1, v2])
15052 let mut keys = Vec::new();
15053 let mut vals = Vec::new();
15054 for (i, arg) in args.into_iter().enumerate() {
15055 if i % 2 == 0 {
15056 keys.push(arg);
15057 } else {
15058 vals.push(arg);
15059 }
15060 }
15061 let keys_arr = Expression::Array(Box::new(
15062 crate::expressions::Array { expressions: keys },
15063 ));
15064 let vals_arr = Expression::Array(Box::new(
15065 crate::expressions::Array { expressions: vals },
15066 ));
15067 Ok(Expression::Function(Box::new(Function::new(
15068 "MAP".to_string(),
15069 vec![keys_arr, vals_arr],
15070 ))))
15071 }
15072 DialectType::Presto | DialectType::Trino => {
15073 // MAP(ARRAY[k1, k2], ARRAY[v1, v2])
15074 let mut keys = Vec::new();
15075 let mut vals = Vec::new();
15076 for (i, arg) in args.into_iter().enumerate() {
15077 if i % 2 == 0 {
15078 keys.push(arg);
15079 } else {
15080 vals.push(arg);
15081 }
15082 }
15083 let keys_arr = Expression::Array(Box::new(
15084 crate::expressions::Array { expressions: keys },
15085 ));
15086 let vals_arr = Expression::Array(Box::new(
15087 crate::expressions::Array { expressions: vals },
15088 ));
15089 Ok(Expression::Function(Box::new(Function::new(
15090 "MAP".to_string(),
15091 vec![keys_arr, vals_arr],
15092 ))))
15093 }
15094 DialectType::Snowflake => Ok(Expression::Function(Box::new(
15095 Function::new("OBJECT_CONSTRUCT".to_string(), args),
15096 ))),
15097 DialectType::ClickHouse => Ok(Expression::Function(Box::new(
15098 Function::new("map".to_string(), args),
15099 ))),
15100 _ => Ok(Expression::Function(Box::new(Function::new(
15101 "MAP".to_string(),
15102 args,
15103 )))),
15104 }
15105 }
15106 // COLLECT_LIST(x) -> ARRAY_AGG(x) for most targets
15107 "COLLECT_LIST" if f.args.len() >= 1 => {
15108 let name = match target {
15109 DialectType::Spark
15110 | DialectType::Databricks
15111 | DialectType::Hive => "COLLECT_LIST",
15112 DialectType::DuckDB
15113 | DialectType::PostgreSQL
15114 | DialectType::Redshift
15115 | DialectType::Snowflake
15116 | DialectType::BigQuery => "ARRAY_AGG",
15117 DialectType::Presto | DialectType::Trino => "ARRAY_AGG",
15118 _ => "ARRAY_AGG",
15119 };
15120 Ok(Expression::Function(Box::new(Function::new(
15121 name.to_string(),
15122 f.args,
15123 ))))
15124 }
15125 // COLLECT_SET(x) -> target-specific distinct array aggregation
15126 "COLLECT_SET" if f.args.len() >= 1 => {
15127 let name = match target {
15128 DialectType::Spark
15129 | DialectType::Databricks
15130 | DialectType::Hive => "COLLECT_SET",
15131 DialectType::Presto
15132 | DialectType::Trino
15133 | DialectType::Athena => "SET_AGG",
15134 DialectType::Snowflake => "ARRAY_UNIQUE_AGG",
15135 _ => "ARRAY_AGG",
15136 };
15137 Ok(Expression::Function(Box::new(Function::new(
15138 name.to_string(),
15139 f.args,
15140 ))))
15141 }
15142 // ISNAN(x) / IS_NAN(x) - normalize
15143 "ISNAN" | "IS_NAN" => {
15144 let name = match target {
15145 DialectType::Spark
15146 | DialectType::Databricks
15147 | DialectType::Hive => "ISNAN",
15148 DialectType::Presto
15149 | DialectType::Trino
15150 | DialectType::Athena => "IS_NAN",
15151 DialectType::BigQuery
15152 | DialectType::PostgreSQL
15153 | DialectType::Redshift => "IS_NAN",
15154 DialectType::ClickHouse => "IS_NAN",
15155 _ => "ISNAN",
15156 };
15157 Ok(Expression::Function(Box::new(Function::new(
15158 name.to_string(),
15159 f.args,
15160 ))))
15161 }
15162 // SPLIT_PART(str, delim, index) -> target-specific
15163 "SPLIT_PART" if f.args.len() == 3 => {
15164 match target {
15165 DialectType::Spark | DialectType::Databricks => {
15166 // Keep as SPLIT_PART (Spark 3.4+)
15167 Ok(Expression::Function(Box::new(Function::new(
15168 "SPLIT_PART".to_string(),
15169 f.args,
15170 ))))
15171 }
15172 DialectType::DuckDB
15173 if matches!(source, DialectType::Snowflake) =>
15174 {
15175 // Snowflake SPLIT_PART -> DuckDB with CASE wrapper:
15176 // - part_index 0 treated as 1
15177 // - empty delimiter: return whole string if index 1 or -1, else ''
15178 let mut args = f.args;
15179 let str_arg = args.remove(0);
15180 let delim_arg = args.remove(0);
15181 let idx_arg = args.remove(0);
15182
15183 // (CASE WHEN idx = 0 THEN 1 ELSE idx END)
15184 let adjusted_idx = Expression::Paren(Box::new(Paren {
15185 this: Expression::Case(Box::new(Case {
15186 operand: None,
15187 whens: vec![(
15188 Expression::Eq(Box::new(BinaryOp {
15189 left: idx_arg.clone(),
15190 right: Expression::number(0),
15191 left_comments: vec![],
15192 operator_comments: vec![],
15193 trailing_comments: vec![],
15194 inferred_type: None,
15195 })),
15196 Expression::number(1),
15197 )],
15198 else_: Some(idx_arg.clone()),
15199 comments: vec![],
15200 inferred_type: None,
15201 })),
15202 trailing_comments: vec![],
15203 }));
15204
15205 // SPLIT_PART(str, delim, adjusted_idx)
15206 let base_func =
15207 Expression::Function(Box::new(Function::new(
15208 "SPLIT_PART".to_string(),
15209 vec![
15210 str_arg.clone(),
15211 delim_arg.clone(),
15212 adjusted_idx.clone(),
15213 ],
15214 )));
15215
15216 // (CASE WHEN adjusted_idx = 1 OR adjusted_idx = -1 THEN str ELSE '' END)
15217 let empty_delim_case = Expression::Paren(Box::new(Paren {
15218 this: Expression::Case(Box::new(Case {
15219 operand: None,
15220 whens: vec![(
15221 Expression::Or(Box::new(BinaryOp {
15222 left: Expression::Eq(Box::new(BinaryOp {
15223 left: adjusted_idx.clone(),
15224 right: Expression::number(1),
15225 left_comments: vec![],
15226 operator_comments: vec![],
15227 trailing_comments: vec![],
15228 inferred_type: None,
15229 })),
15230 right: Expression::Eq(Box::new(BinaryOp {
15231 left: adjusted_idx,
15232 right: Expression::number(-1),
15233 left_comments: vec![],
15234 operator_comments: vec![],
15235 trailing_comments: vec![],
15236 inferred_type: None,
15237 })),
15238 left_comments: vec![],
15239 operator_comments: vec![],
15240 trailing_comments: vec![],
15241 inferred_type: None,
15242 })),
15243 str_arg,
15244 )],
15245 else_: Some(Expression::string("")),
15246 comments: vec![],
15247 inferred_type: None,
15248 })),
15249 trailing_comments: vec![],
15250 }));
15251
15252 // CASE WHEN delim = '' THEN (empty case) ELSE SPLIT_PART(...) END
15253 Ok(Expression::Case(Box::new(Case {
15254 operand: None,
15255 whens: vec![(
15256 Expression::Eq(Box::new(BinaryOp {
15257 left: delim_arg,
15258 right: Expression::string(""),
15259 left_comments: vec![],
15260 operator_comments: vec![],
15261 trailing_comments: vec![],
15262 inferred_type: None,
15263 })),
15264 empty_delim_case,
15265 )],
15266 else_: Some(base_func),
15267 comments: vec![],
15268 inferred_type: None,
15269 })))
15270 }
15271 DialectType::DuckDB
15272 | DialectType::PostgreSQL
15273 | DialectType::Snowflake
15274 | DialectType::Redshift
15275 | DialectType::Trino
15276 | DialectType::Presto => Ok(Expression::Function(Box::new(
15277 Function::new("SPLIT_PART".to_string(), f.args),
15278 ))),
15279 DialectType::Hive => {
15280 // SPLIT(str, delim)[index]
15281 // Complex conversion, just keep as-is for now
15282 Ok(Expression::Function(Box::new(Function::new(
15283 "SPLIT_PART".to_string(),
15284 f.args,
15285 ))))
15286 }
15287 _ => Ok(Expression::Function(Box::new(Function::new(
15288 "SPLIT_PART".to_string(),
15289 f.args,
15290 )))),
15291 }
15292 }
15293 // JSON_EXTRACT(json, path) -> target-specific JSON extraction
15294 "JSON_EXTRACT" | "JSON_EXTRACT_SCALAR" if f.args.len() == 2 => {
15295 let is_scalar = name == "JSON_EXTRACT_SCALAR";
15296 match target {
15297 DialectType::Spark
15298 | DialectType::Databricks
15299 | DialectType::Hive => {
15300 let mut args = f.args;
15301 // Spark/Hive don't support Presto's TRY(expr) wrapper form here.
15302 // Mirror sqlglot by unwrapping TRY(expr) to expr before GET_JSON_OBJECT.
15303 if let Some(Expression::Function(inner)) = args.first() {
15304 if inner.name.eq_ignore_ascii_case("TRY")
15305 && inner.args.len() == 1
15306 {
15307 let mut inner_args = inner.args.clone();
15308 args[0] = inner_args.remove(0);
15309 }
15310 }
15311 Ok(Expression::Function(Box::new(Function::new(
15312 "GET_JSON_OBJECT".to_string(),
15313 args,
15314 ))))
15315 }
15316 DialectType::DuckDB | DialectType::SQLite => {
15317 // json -> path syntax
15318 let mut args = f.args;
15319 let json_expr = args.remove(0);
15320 let path = args.remove(0);
15321 Ok(Expression::JsonExtract(Box::new(
15322 crate::expressions::JsonExtractFunc {
15323 this: json_expr,
15324 path,
15325 returning: None,
15326 arrow_syntax: true,
15327 hash_arrow_syntax: false,
15328 wrapper_option: None,
15329 quotes_option: None,
15330 on_scalar_string: false,
15331 on_error: None,
15332 },
15333 )))
15334 }
15335 DialectType::TSQL => {
15336 let func_name = if is_scalar {
15337 "JSON_VALUE"
15338 } else {
15339 "JSON_QUERY"
15340 };
15341 Ok(Expression::Function(Box::new(Function::new(
15342 func_name.to_string(),
15343 f.args,
15344 ))))
15345 }
15346 DialectType::PostgreSQL | DialectType::Redshift => {
15347 let func_name = if is_scalar {
15348 "JSON_EXTRACT_PATH_TEXT"
15349 } else {
15350 "JSON_EXTRACT_PATH"
15351 };
15352 Ok(Expression::Function(Box::new(Function::new(
15353 func_name.to_string(),
15354 f.args,
15355 ))))
15356 }
15357 _ => Ok(Expression::Function(Box::new(Function::new(
15358 name.to_string(),
15359 f.args,
15360 )))),
15361 }
15362 }
15363 // MySQL JSON_SEARCH(json_doc, mode, search[, escape_char[, path]]) -> DuckDB json_tree-based lookup
15364 "JSON_SEARCH"
15365 if matches!(target, DialectType::DuckDB)
15366 && (3..=5).contains(&f.args.len()) =>
15367 {
15368 let args = &f.args;
15369
15370 // Only rewrite deterministic modes and NULL/no escape-char variant.
15371 let mode = match &args[1] {
15372 Expression::Literal(lit)
15373 if matches!(
15374 lit.as_ref(),
15375 crate::expressions::Literal::String(_)
15376 ) =>
15377 {
15378 let crate::expressions::Literal::String(s) = lit.as_ref()
15379 else {
15380 unreachable!()
15381 };
15382 s.to_ascii_lowercase()
15383 }
15384 _ => return Ok(Expression::Function(f)),
15385 };
15386 if mode != "one" && mode != "all" {
15387 return Ok(Expression::Function(f));
15388 }
15389 if args.len() >= 4 && !matches!(&args[3], Expression::Null(_)) {
15390 return Ok(Expression::Function(f));
15391 }
15392
15393 let json_doc_sql = match Generator::sql(&args[0]) {
15394 Ok(sql) => sql,
15395 Err(_) => return Ok(Expression::Function(f)),
15396 };
15397 let search_sql = match Generator::sql(&args[2]) {
15398 Ok(sql) => sql,
15399 Err(_) => return Ok(Expression::Function(f)),
15400 };
15401 let path_sql = if args.len() == 5 {
15402 match Generator::sql(&args[4]) {
15403 Ok(sql) => sql,
15404 Err(_) => return Ok(Expression::Function(f)),
15405 }
15406 } else {
15407 "'$'".to_string()
15408 };
15409
15410 let rewrite_sql = if mode == "all" {
15411 format!(
15412 "(SELECT TO_JSON(LIST(__jt.fullkey)) FROM json_tree({}, {}) AS __jt WHERE __jt.atom = TO_JSON({}))",
15413 json_doc_sql, path_sql, search_sql
15414 )
15415 } else {
15416 format!(
15417 "(SELECT TO_JSON(__jt.fullkey) FROM json_tree({}, {}) AS __jt WHERE __jt.atom = TO_JSON({}) ORDER BY __jt.id LIMIT 1)",
15418 json_doc_sql, path_sql, search_sql
15419 )
15420 };
15421
15422 Ok(Expression::Raw(crate::expressions::Raw {
15423 sql: rewrite_sql,
15424 }))
15425 }
15426 // SingleStore JSON_EXTRACT_JSON(json, key1, key2, ...) -> JSON_EXTRACT(json, '$.key1.key2' or '$.key1[key2]')
15427 // BSON_EXTRACT_BSON(json, key1, ...) -> JSONB_EXTRACT(json, '$.key1')
15428 "JSON_EXTRACT_JSON" | "BSON_EXTRACT_BSON"
15429 if f.args.len() >= 2
15430 && matches!(source, DialectType::SingleStore) =>
15431 {
15432 let is_bson = name == "BSON_EXTRACT_BSON";
15433 let mut args = f.args;
15434 let json_expr = args.remove(0);
15435
15436 // Build JSONPath from remaining arguments
15437 let mut path = String::from("$");
15438 for arg in &args {
15439 if let Expression::Literal(lit) = arg {
15440 if let crate::expressions::Literal::String(s) = lit.as_ref()
15441 {
15442 // Check if it's a numeric string (array index)
15443 if s.parse::<i64>().is_ok() {
15444 path.push('[');
15445 path.push_str(s);
15446 path.push(']');
15447 } else {
15448 path.push('.');
15449 path.push_str(s);
15450 }
15451 }
15452 }
15453 }
15454
15455 let target_func = if is_bson {
15456 "JSONB_EXTRACT"
15457 } else {
15458 "JSON_EXTRACT"
15459 };
15460 Ok(Expression::Function(Box::new(Function::new(
15461 target_func.to_string(),
15462 vec![json_expr, Expression::string(&path)],
15463 ))))
15464 }
15465 // ARRAY_SUM(lambda, array) from Doris -> ClickHouse arraySum
15466 "ARRAY_SUM" if matches!(target, DialectType::ClickHouse) => {
15467 Ok(Expression::Function(Box::new(Function {
15468 name: "arraySum".to_string(),
15469 args: f.args,
15470 distinct: f.distinct,
15471 trailing_comments: f.trailing_comments,
15472 use_bracket_syntax: f.use_bracket_syntax,
15473 no_parens: f.no_parens,
15474 quoted: f.quoted,
15475 span: None,
15476 inferred_type: None,
15477 })))
15478 }
15479 // TSQL JSON_QUERY/JSON_VALUE -> target-specific
15480 // Note: For TSQL->TSQL, JsonQuery stays as Expression::JsonQuery (source transform not called)
15481 // and is handled by JsonQueryValueConvert action. This handles the case where
15482 // TSQL read transform converted JsonQuery to Function("JSON_QUERY") for cross-dialect.
15483 "JSON_QUERY" | "JSON_VALUE"
15484 if f.args.len() == 2
15485 && matches!(
15486 source,
15487 DialectType::TSQL | DialectType::Fabric
15488 ) =>
15489 {
15490 match target {
15491 DialectType::Spark
15492 | DialectType::Databricks
15493 | DialectType::Hive => Ok(Expression::Function(Box::new(
15494 Function::new("GET_JSON_OBJECT".to_string(), f.args),
15495 ))),
15496 _ => Ok(Expression::Function(Box::new(Function::new(
15497 name.to_string(),
15498 f.args,
15499 )))),
15500 }
15501 }
15502 // UNIX_TIMESTAMP(x) -> TO_UNIXTIME(x) for Presto
15503 "UNIX_TIMESTAMP" if f.args.len() == 1 => {
15504 let arg = f.args.into_iter().next().unwrap();
15505 let is_hive_source = matches!(
15506 source,
15507 DialectType::Hive
15508 | DialectType::Spark
15509 | DialectType::Databricks
15510 );
15511 match target {
15512 DialectType::DuckDB if is_hive_source => {
15513 // DuckDB: EPOCH(STRPTIME(x, '%Y-%m-%d %H:%M:%S'))
15514 let strptime =
15515 Expression::Function(Box::new(Function::new(
15516 "STRPTIME".to_string(),
15517 vec![arg, Expression::string("%Y-%m-%d %H:%M:%S")],
15518 )));
15519 Ok(Expression::Function(Box::new(Function::new(
15520 "EPOCH".to_string(),
15521 vec![strptime],
15522 ))))
15523 }
15524 DialectType::Presto | DialectType::Trino if is_hive_source => {
15525 // Presto: TO_UNIXTIME(COALESCE(TRY(DATE_PARSE(CAST(x AS VARCHAR), '%Y-%m-%d %T')), PARSE_DATETIME(DATE_FORMAT(x, '%Y-%m-%d %T'), 'yyyy-MM-dd HH:mm:ss')))
15526 let cast_varchar =
15527 Expression::Cast(Box::new(crate::expressions::Cast {
15528 this: arg.clone(),
15529 to: DataType::VarChar {
15530 length: None,
15531 parenthesized_length: false,
15532 },
15533 trailing_comments: vec![],
15534 double_colon_syntax: false,
15535 format: None,
15536 default: None,
15537 inferred_type: None,
15538 }));
15539 let date_parse =
15540 Expression::Function(Box::new(Function::new(
15541 "DATE_PARSE".to_string(),
15542 vec![
15543 cast_varchar,
15544 Expression::string("%Y-%m-%d %T"),
15545 ],
15546 )));
15547 let try_expr = Expression::Function(Box::new(
15548 Function::new("TRY".to_string(), vec![date_parse]),
15549 ));
15550 let date_format =
15551 Expression::Function(Box::new(Function::new(
15552 "DATE_FORMAT".to_string(),
15553 vec![arg, Expression::string("%Y-%m-%d %T")],
15554 )));
15555 let parse_datetime =
15556 Expression::Function(Box::new(Function::new(
15557 "PARSE_DATETIME".to_string(),
15558 vec![
15559 date_format,
15560 Expression::string("yyyy-MM-dd HH:mm:ss"),
15561 ],
15562 )));
15563 let coalesce =
15564 Expression::Function(Box::new(Function::new(
15565 "COALESCE".to_string(),
15566 vec![try_expr, parse_datetime],
15567 )));
15568 Ok(Expression::Function(Box::new(Function::new(
15569 "TO_UNIXTIME".to_string(),
15570 vec![coalesce],
15571 ))))
15572 }
15573 DialectType::Presto | DialectType::Trino => {
15574 Ok(Expression::Function(Box::new(Function::new(
15575 "TO_UNIXTIME".to_string(),
15576 vec![arg],
15577 ))))
15578 }
15579 _ => Ok(Expression::Function(Box::new(Function::new(
15580 "UNIX_TIMESTAMP".to_string(),
15581 vec![arg],
15582 )))),
15583 }
15584 }
15585 // TO_UNIX_TIMESTAMP(x) -> UNIX_TIMESTAMP(x) for Spark/Hive
15586 "TO_UNIX_TIMESTAMP" if f.args.len() >= 1 => match target {
15587 DialectType::Spark
15588 | DialectType::Databricks
15589 | DialectType::Hive => Ok(Expression::Function(Box::new(
15590 Function::new("UNIX_TIMESTAMP".to_string(), f.args),
15591 ))),
15592 _ => Ok(Expression::Function(Box::new(Function::new(
15593 "TO_UNIX_TIMESTAMP".to_string(),
15594 f.args,
15595 )))),
15596 },
15597 // CURDATE() -> CURRENT_DATE
15598 "CURDATE" => {
15599 Ok(Expression::CurrentDate(crate::expressions::CurrentDate))
15600 }
15601 // CURTIME() -> CURRENT_TIME
15602 "CURTIME" => {
15603 Ok(Expression::CurrentTime(crate::expressions::CurrentTime {
15604 precision: None,
15605 }))
15606 }
15607 // ARRAY_SORT(x) or ARRAY_SORT(x, lambda) -> SORT_ARRAY(x) for Hive, LIST_SORT for DuckDB
15608 "ARRAY_SORT" if f.args.len() >= 1 => {
15609 match target {
15610 DialectType::Hive => {
15611 let mut args = f.args;
15612 args.truncate(1); // Drop lambda comparator
15613 Ok(Expression::Function(Box::new(Function::new(
15614 "SORT_ARRAY".to_string(),
15615 args,
15616 ))))
15617 }
15618 DialectType::DuckDB
15619 if matches!(source, DialectType::Snowflake) =>
15620 {
15621 // Snowflake ARRAY_SORT(arr[, asc_bool[, nulls_first_bool]]) -> DuckDB LIST_SORT(arr[, 'ASC'/'DESC'[, 'NULLS FIRST']])
15622 let mut args_iter = f.args.into_iter();
15623 let arr = args_iter.next().unwrap();
15624 let asc_arg = args_iter.next();
15625 let nulls_first_arg = args_iter.next();
15626
15627 let is_asc_bool = asc_arg
15628 .as_ref()
15629 .map(|a| matches!(a, Expression::Boolean(_)))
15630 .unwrap_or(false);
15631 let is_nf_bool = nulls_first_arg
15632 .as_ref()
15633 .map(|a| matches!(a, Expression::Boolean(_)))
15634 .unwrap_or(false);
15635
15636 // No boolean args: pass through as-is
15637 if !is_asc_bool && !is_nf_bool {
15638 let mut result_args = vec![arr];
15639 if let Some(asc) = asc_arg {
15640 result_args.push(asc);
15641 if let Some(nf) = nulls_first_arg {
15642 result_args.push(nf);
15643 }
15644 }
15645 Ok(Expression::Function(Box::new(Function::new(
15646 "LIST_SORT".to_string(),
15647 result_args,
15648 ))))
15649 } else {
15650 // Has boolean args: convert to DuckDB LIST_SORT format
15651 let descending = matches!(&asc_arg, Some(Expression::Boolean(b)) if !b.value);
15652
15653 // Snowflake defaults: nulls_first = TRUE for DESC, FALSE for ASC
15654 let nulls_are_first = match &nulls_first_arg {
15655 Some(Expression::Boolean(b)) => b.value,
15656 None if is_asc_bool => descending, // Snowflake default
15657 _ => false,
15658 };
15659 let nulls_first_sql = if nulls_are_first {
15660 Some(Expression::string("NULLS FIRST"))
15661 } else {
15662 None
15663 };
15664
15665 if !is_asc_bool {
15666 // asc is non-boolean expression, nulls_first is boolean
15667 let mut result_args = vec![arr];
15668 if let Some(asc) = asc_arg {
15669 result_args.push(asc);
15670 }
15671 if let Some(nf) = nulls_first_sql {
15672 result_args.push(nf);
15673 }
15674 Ok(Expression::Function(Box::new(Function::new(
15675 "LIST_SORT".to_string(),
15676 result_args,
15677 ))))
15678 } else {
15679 if !descending && !nulls_are_first {
15680 // ASC, NULLS LAST (default) -> LIST_SORT(arr)
15681 Ok(Expression::Function(Box::new(
15682 Function::new(
15683 "LIST_SORT".to_string(),
15684 vec![arr],
15685 ),
15686 )))
15687 } else if descending && !nulls_are_first {
15688 // DESC, NULLS LAST -> ARRAY_REVERSE_SORT(arr)
15689 Ok(Expression::Function(Box::new(
15690 Function::new(
15691 "ARRAY_REVERSE_SORT".to_string(),
15692 vec![arr],
15693 ),
15694 )))
15695 } else {
15696 // NULLS FIRST -> LIST_SORT(arr, 'ASC'/'DESC', 'NULLS FIRST')
15697 let order_str =
15698 if descending { "DESC" } else { "ASC" };
15699 Ok(Expression::Function(Box::new(
15700 Function::new(
15701 "LIST_SORT".to_string(),
15702 vec![
15703 arr,
15704 Expression::string(order_str),
15705 Expression::string("NULLS FIRST"),
15706 ],
15707 ),
15708 )))
15709 }
15710 }
15711 }
15712 }
15713 DialectType::DuckDB => {
15714 // Non-Snowflake source: ARRAY_SORT(x, lambda) -> ARRAY_SORT(x) (drop comparator)
15715 let mut args = f.args;
15716 args.truncate(1); // Drop lambda comparator for DuckDB
15717 Ok(Expression::Function(Box::new(Function::new(
15718 "ARRAY_SORT".to_string(),
15719 args,
15720 ))))
15721 }
15722 _ => Ok(Expression::Function(f)),
15723 }
15724 }
15725 // SORT_ARRAY(x) -> LIST_SORT(x) for DuckDB, ARRAY_SORT(x) for Presto/Trino, keep for Hive/Spark
15726 "SORT_ARRAY" if f.args.len() == 1 => match target {
15727 DialectType::Hive
15728 | DialectType::Spark
15729 | DialectType::Databricks => Ok(Expression::Function(f)),
15730 DialectType::DuckDB => Ok(Expression::Function(Box::new(
15731 Function::new("LIST_SORT".to_string(), f.args),
15732 ))),
15733 _ => Ok(Expression::Function(Box::new(Function::new(
15734 "ARRAY_SORT".to_string(),
15735 f.args,
15736 )))),
15737 },
15738 // SORT_ARRAY(x, FALSE) -> ARRAY_REVERSE_SORT(x) for DuckDB, ARRAY_SORT(x, lambda) for Presto
15739 "SORT_ARRAY" if f.args.len() == 2 => {
15740 let is_desc =
15741 matches!(&f.args[1], Expression::Boolean(b) if !b.value);
15742 if is_desc {
15743 match target {
15744 DialectType::DuckDB => {
15745 Ok(Expression::Function(Box::new(Function::new(
15746 "ARRAY_REVERSE_SORT".to_string(),
15747 vec![f.args.into_iter().next().unwrap()],
15748 ))))
15749 }
15750 DialectType::Presto | DialectType::Trino => {
15751 let arr_arg = f.args.into_iter().next().unwrap();
15752 let a = Expression::Column(Box::new(
15753 crate::expressions::Column {
15754 name: crate::expressions::Identifier::new("a"),
15755 table: None,
15756 join_mark: false,
15757 trailing_comments: Vec::new(),
15758 span: None,
15759 inferred_type: None,
15760 },
15761 ));
15762 let b = Expression::Column(Box::new(
15763 crate::expressions::Column {
15764 name: crate::expressions::Identifier::new("b"),
15765 table: None,
15766 join_mark: false,
15767 trailing_comments: Vec::new(),
15768 span: None,
15769 inferred_type: None,
15770 },
15771 ));
15772 let case_expr = Expression::Case(Box::new(
15773 crate::expressions::Case {
15774 operand: None,
15775 whens: vec![
15776 (
15777 Expression::Lt(Box::new(
15778 BinaryOp::new(a.clone(), b.clone()),
15779 )),
15780 Expression::Literal(Box::new(
15781 Literal::Number("1".to_string()),
15782 )),
15783 ),
15784 (
15785 Expression::Gt(Box::new(
15786 BinaryOp::new(a.clone(), b.clone()),
15787 )),
15788 Expression::Literal(Box::new(
15789 Literal::Number("-1".to_string()),
15790 )),
15791 ),
15792 ],
15793 else_: Some(Expression::Literal(Box::new(
15794 Literal::Number("0".to_string()),
15795 ))),
15796 comments: Vec::new(),
15797 inferred_type: None,
15798 },
15799 ));
15800 let lambda = Expression::Lambda(Box::new(
15801 crate::expressions::LambdaExpr {
15802 parameters: vec![
15803 crate::expressions::Identifier::new("a"),
15804 crate::expressions::Identifier::new("b"),
15805 ],
15806 body: case_expr,
15807 colon: false,
15808 parameter_types: Vec::new(),
15809 },
15810 ));
15811 Ok(Expression::Function(Box::new(Function::new(
15812 "ARRAY_SORT".to_string(),
15813 vec![arr_arg, lambda],
15814 ))))
15815 }
15816 _ => Ok(Expression::Function(f)),
15817 }
15818 } else {
15819 // SORT_ARRAY(x, TRUE) -> LIST_SORT(x) for DuckDB, ARRAY_SORT(x) for others
15820 match target {
15821 DialectType::Hive => Ok(Expression::Function(f)),
15822 DialectType::DuckDB => {
15823 Ok(Expression::Function(Box::new(Function::new(
15824 "LIST_SORT".to_string(),
15825 vec![f.args.into_iter().next().unwrap()],
15826 ))))
15827 }
15828 _ => Ok(Expression::Function(Box::new(Function::new(
15829 "ARRAY_SORT".to_string(),
15830 vec![f.args.into_iter().next().unwrap()],
15831 )))),
15832 }
15833 }
15834 }
15835 // LEFT(x, n), RIGHT(x, n) -> SUBSTRING for targets without LEFT/RIGHT
15836 "LEFT" if f.args.len() == 2 => {
15837 match target {
15838 DialectType::Hive
15839 | DialectType::Presto
15840 | DialectType::Trino
15841 | DialectType::Athena => {
15842 let x = f.args[0].clone();
15843 let n = f.args[1].clone();
15844 Ok(Expression::Function(Box::new(Function::new(
15845 "SUBSTRING".to_string(),
15846 vec![x, Expression::number(1), n],
15847 ))))
15848 }
15849 DialectType::Spark | DialectType::Databricks
15850 if matches!(
15851 source,
15852 DialectType::TSQL | DialectType::Fabric
15853 ) =>
15854 {
15855 // TSQL LEFT(x, n) -> LEFT(CAST(x AS STRING), n) for Spark
15856 let x = f.args[0].clone();
15857 let n = f.args[1].clone();
15858 let cast_x = Expression::Cast(Box::new(Cast {
15859 this: x,
15860 to: DataType::VarChar {
15861 length: None,
15862 parenthesized_length: false,
15863 },
15864 double_colon_syntax: false,
15865 trailing_comments: Vec::new(),
15866 format: None,
15867 default: None,
15868 inferred_type: None,
15869 }));
15870 Ok(Expression::Function(Box::new(Function::new(
15871 "LEFT".to_string(),
15872 vec![cast_x, n],
15873 ))))
15874 }
15875 _ => Ok(Expression::Function(f)),
15876 }
15877 }
15878 "RIGHT" if f.args.len() == 2 => {
15879 match target {
15880 DialectType::Hive
15881 | DialectType::Presto
15882 | DialectType::Trino
15883 | DialectType::Athena => {
15884 let x = f.args[0].clone();
15885 let n = f.args[1].clone();
15886 // SUBSTRING(x, LENGTH(x) - (n - 1))
15887 let len_x = Expression::Function(Box::new(Function::new(
15888 "LENGTH".to_string(),
15889 vec![x.clone()],
15890 )));
15891 let n_minus_1 = Expression::Sub(Box::new(
15892 crate::expressions::BinaryOp::new(
15893 n,
15894 Expression::number(1),
15895 ),
15896 ));
15897 let n_minus_1_paren = Expression::Paren(Box::new(
15898 crate::expressions::Paren {
15899 this: n_minus_1,
15900 trailing_comments: Vec::new(),
15901 },
15902 ));
15903 let offset = Expression::Sub(Box::new(
15904 crate::expressions::BinaryOp::new(
15905 len_x,
15906 n_minus_1_paren,
15907 ),
15908 ));
15909 Ok(Expression::Function(Box::new(Function::new(
15910 "SUBSTRING".to_string(),
15911 vec![x, offset],
15912 ))))
15913 }
15914 DialectType::Spark | DialectType::Databricks
15915 if matches!(
15916 source,
15917 DialectType::TSQL | DialectType::Fabric
15918 ) =>
15919 {
15920 // TSQL RIGHT(x, n) -> RIGHT(CAST(x AS STRING), n) for Spark
15921 let x = f.args[0].clone();
15922 let n = f.args[1].clone();
15923 let cast_x = Expression::Cast(Box::new(Cast {
15924 this: x,
15925 to: DataType::VarChar {
15926 length: None,
15927 parenthesized_length: false,
15928 },
15929 double_colon_syntax: false,
15930 trailing_comments: Vec::new(),
15931 format: None,
15932 default: None,
15933 inferred_type: None,
15934 }));
15935 Ok(Expression::Function(Box::new(Function::new(
15936 "RIGHT".to_string(),
15937 vec![cast_x, n],
15938 ))))
15939 }
15940 _ => Ok(Expression::Function(f)),
15941 }
15942 }
15943 // MAP_FROM_ARRAYS(keys, vals) -> target-specific map construction
15944 "MAP_FROM_ARRAYS" if f.args.len() == 2 => match target {
15945 DialectType::Snowflake => Ok(Expression::Function(Box::new(
15946 Function::new("OBJECT_CONSTRUCT".to_string(), f.args),
15947 ))),
15948 DialectType::Spark | DialectType::Databricks => {
15949 Ok(Expression::Function(Box::new(Function::new(
15950 "MAP_FROM_ARRAYS".to_string(),
15951 f.args,
15952 ))))
15953 }
15954 _ => Ok(Expression::Function(Box::new(Function::new(
15955 "MAP".to_string(),
15956 f.args,
15957 )))),
15958 },
15959 // LIKE(foo, 'pat') -> foo LIKE 'pat'; LIKE(foo, 'pat', '!') -> foo LIKE 'pat' ESCAPE '!'
15960 // SQLite uses LIKE(pattern, string[, escape]) with args in reverse order
15961 "LIKE" if f.args.len() >= 2 => {
15962 let (this, pattern) = if matches!(source, DialectType::SQLite) {
15963 // SQLite: LIKE(pattern, string) -> string LIKE pattern
15964 (f.args[1].clone(), f.args[0].clone())
15965 } else {
15966 // Standard: LIKE(string, pattern) -> string LIKE pattern
15967 (f.args[0].clone(), f.args[1].clone())
15968 };
15969 let escape = if f.args.len() >= 3 {
15970 Some(f.args[2].clone())
15971 } else {
15972 None
15973 };
15974 Ok(Expression::Like(Box::new(crate::expressions::LikeOp {
15975 left: this,
15976 right: pattern,
15977 escape,
15978 quantifier: None,
15979 inferred_type: None,
15980 })))
15981 }
15982 // ILIKE(foo, 'pat') -> foo ILIKE 'pat'
15983 "ILIKE" if f.args.len() >= 2 => {
15984 let this = f.args[0].clone();
15985 let pattern = f.args[1].clone();
15986 let escape = if f.args.len() >= 3 {
15987 Some(f.args[2].clone())
15988 } else {
15989 None
15990 };
15991 Ok(Expression::ILike(Box::new(crate::expressions::LikeOp {
15992 left: this,
15993 right: pattern,
15994 escape,
15995 quantifier: None,
15996 inferred_type: None,
15997 })))
15998 }
15999 // CHAR(n) -> CHR(n) for non-MySQL/non-TSQL targets
16000 "CHAR" if f.args.len() == 1 => match target {
16001 DialectType::MySQL
16002 | DialectType::SingleStore
16003 | DialectType::TSQL => Ok(Expression::Function(f)),
16004 _ => Ok(Expression::Function(Box::new(Function::new(
16005 "CHR".to_string(),
16006 f.args,
16007 )))),
16008 },
16009 // CONCAT(a, b) -> a || b for PostgreSQL
16010 "CONCAT"
16011 if f.args.len() == 2
16012 && matches!(target, DialectType::PostgreSQL)
16013 && matches!(
16014 source,
16015 DialectType::ClickHouse | DialectType::MySQL
16016 ) =>
16017 {
16018 let mut args = f.args;
16019 let right = args.pop().unwrap();
16020 let left = args.pop().unwrap();
16021 Ok(Expression::DPipe(Box::new(crate::expressions::DPipe {
16022 this: Box::new(left),
16023 expression: Box::new(right),
16024 safe: None,
16025 })))
16026 }
16027 // ARRAY_TO_STRING(arr, delim) -> target-specific
16028 "ARRAY_TO_STRING"
16029 if f.args.len() == 2
16030 && matches!(target, DialectType::DuckDB)
16031 && matches!(source, DialectType::Snowflake) =>
16032 {
16033 let mut args = f.args;
16034 let arr = args.remove(0);
16035 let sep = args.remove(0);
16036 // sep IS NULL
16037 let sep_is_null = Expression::IsNull(Box::new(IsNull {
16038 this: sep.clone(),
16039 not: false,
16040 postfix_form: false,
16041 }));
16042 // COALESCE(CAST(x AS TEXT), '')
16043 let cast_x = Expression::Cast(Box::new(Cast {
16044 this: Expression::Identifier(Identifier::new("x")),
16045 to: DataType::Text,
16046 trailing_comments: Vec::new(),
16047 double_colon_syntax: false,
16048 format: None,
16049 default: None,
16050 inferred_type: None,
16051 }));
16052 let coalesce = Expression::Coalesce(Box::new(
16053 crate::expressions::VarArgFunc {
16054 original_name: None,
16055 expressions: vec![
16056 cast_x,
16057 Expression::Literal(Box::new(Literal::String(
16058 String::new(),
16059 ))),
16060 ],
16061 inferred_type: None,
16062 },
16063 ));
16064 let lambda =
16065 Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
16066 parameters: vec![Identifier::new("x")],
16067 body: coalesce,
16068 colon: false,
16069 parameter_types: Vec::new(),
16070 }));
16071 let list_transform = Expression::Function(Box::new(Function::new(
16072 "LIST_TRANSFORM".to_string(),
16073 vec![arr, lambda],
16074 )));
16075 let array_to_string =
16076 Expression::Function(Box::new(Function::new(
16077 "ARRAY_TO_STRING".to_string(),
16078 vec![list_transform, sep],
16079 )));
16080 Ok(Expression::Case(Box::new(Case {
16081 operand: None,
16082 whens: vec![(sep_is_null, Expression::Null(Null))],
16083 else_: Some(array_to_string),
16084 comments: Vec::new(),
16085 inferred_type: None,
16086 })))
16087 }
16088 "ARRAY_TO_STRING" if f.args.len() >= 2 => match target {
16089 DialectType::Presto | DialectType::Trino => {
16090 Ok(Expression::Function(Box::new(Function::new(
16091 "ARRAY_JOIN".to_string(),
16092 f.args,
16093 ))))
16094 }
16095 DialectType::TSQL => Ok(Expression::Function(Box::new(
16096 Function::new("STRING_AGG".to_string(), f.args),
16097 ))),
16098 _ => Ok(Expression::Function(f)),
16099 },
16100 // ARRAY_CONCAT / LIST_CONCAT -> target-specific
16101 "ARRAY_CONCAT" | "LIST_CONCAT" if f.args.len() == 2 => match target {
16102 DialectType::Spark
16103 | DialectType::Databricks
16104 | DialectType::Hive => Ok(Expression::Function(Box::new(
16105 Function::new("CONCAT".to_string(), f.args),
16106 ))),
16107 DialectType::Snowflake => Ok(Expression::Function(Box::new(
16108 Function::new("ARRAY_CAT".to_string(), f.args),
16109 ))),
16110 DialectType::Redshift => Ok(Expression::Function(Box::new(
16111 Function::new("ARRAY_CONCAT".to_string(), f.args),
16112 ))),
16113 DialectType::PostgreSQL => Ok(Expression::Function(Box::new(
16114 Function::new("ARRAY_CAT".to_string(), f.args),
16115 ))),
16116 DialectType::DuckDB => Ok(Expression::Function(Box::new(
16117 Function::new("LIST_CONCAT".to_string(), f.args),
16118 ))),
16119 DialectType::Presto | DialectType::Trino => {
16120 Ok(Expression::Function(Box::new(Function::new(
16121 "CONCAT".to_string(),
16122 f.args,
16123 ))))
16124 }
16125 DialectType::BigQuery => Ok(Expression::Function(Box::new(
16126 Function::new("ARRAY_CONCAT".to_string(), f.args),
16127 ))),
16128 _ => Ok(Expression::Function(f)),
16129 },
16130 // ARRAY_CONTAINS(arr, x) / HAS(arr, x) / CONTAINS(arr, x) normalization
16131 "HAS" if f.args.len() == 2 => match target {
16132 DialectType::Spark
16133 | DialectType::Databricks
16134 | DialectType::Hive => Ok(Expression::Function(Box::new(
16135 Function::new("ARRAY_CONTAINS".to_string(), f.args),
16136 ))),
16137 DialectType::Presto | DialectType::Trino => {
16138 Ok(Expression::Function(Box::new(Function::new(
16139 "CONTAINS".to_string(),
16140 f.args,
16141 ))))
16142 }
16143 _ => Ok(Expression::Function(f)),
16144 },
16145 // NVL(a, b, c, d) -> COALESCE(a, b, c, d) - NVL should keep all args
16146 "NVL" if f.args.len() > 2 => Ok(Expression::Function(Box::new(
16147 Function::new("COALESCE".to_string(), f.args),
16148 ))),
16149 // ISNULL(x) in MySQL -> (x IS NULL)
16150 "ISNULL"
16151 if f.args.len() == 1
16152 && matches!(source, DialectType::MySQL)
16153 && matches!(target, DialectType::MySQL) =>
16154 {
16155 let arg = f.args.into_iter().next().unwrap();
16156 Ok(Expression::Paren(Box::new(crate::expressions::Paren {
16157 this: Expression::IsNull(Box::new(
16158 crate::expressions::IsNull {
16159 this: arg,
16160 not: false,
16161 postfix_form: false,
16162 },
16163 )),
16164 trailing_comments: Vec::new(),
16165 })))
16166 }
16167 // MONTHNAME(x) -> DATE_FORMAT(x, '%M') for MySQL -> MySQL
16168 "MONTHNAME"
16169 if f.args.len() == 1 && matches!(target, DialectType::MySQL) =>
16170 {
16171 let arg = f.args.into_iter().next().unwrap();
16172 Ok(Expression::Function(Box::new(Function::new(
16173 "DATE_FORMAT".to_string(),
16174 vec![arg, Expression::string("%M")],
16175 ))))
16176 }
16177 // ClickHouse splitByString('s', x) -> DuckDB STR_SPLIT(x, 's') / Hive SPLIT(x, CONCAT('\\Q', 's', '\\E'))
16178 "SPLITBYSTRING" if f.args.len() == 2 => {
16179 let sep = f.args[0].clone();
16180 let str_arg = f.args[1].clone();
16181 match target {
16182 DialectType::DuckDB => Ok(Expression::Function(Box::new(
16183 Function::new("STR_SPLIT".to_string(), vec![str_arg, sep]),
16184 ))),
16185 DialectType::Doris => {
16186 Ok(Expression::Function(Box::new(Function::new(
16187 "SPLIT_BY_STRING".to_string(),
16188 vec![str_arg, sep],
16189 ))))
16190 }
16191 DialectType::Hive
16192 | DialectType::Spark
16193 | DialectType::Databricks => {
16194 // SPLIT(x, CONCAT('\\Q', sep, '\\E'))
16195 let escaped =
16196 Expression::Function(Box::new(Function::new(
16197 "CONCAT".to_string(),
16198 vec![
16199 Expression::string("\\Q"),
16200 sep,
16201 Expression::string("\\E"),
16202 ],
16203 )));
16204 Ok(Expression::Function(Box::new(Function::new(
16205 "SPLIT".to_string(),
16206 vec![str_arg, escaped],
16207 ))))
16208 }
16209 _ => Ok(Expression::Function(f)),
16210 }
16211 }
16212 // ClickHouse splitByRegexp('pattern', x) -> DuckDB STR_SPLIT_REGEX(x, 'pattern')
16213 "SPLITBYREGEXP" if f.args.len() == 2 => {
16214 let sep = f.args[0].clone();
16215 let str_arg = f.args[1].clone();
16216 match target {
16217 DialectType::DuckDB => {
16218 Ok(Expression::Function(Box::new(Function::new(
16219 "STR_SPLIT_REGEX".to_string(),
16220 vec![str_arg, sep],
16221 ))))
16222 }
16223 DialectType::Hive
16224 | DialectType::Spark
16225 | DialectType::Databricks => {
16226 Ok(Expression::Function(Box::new(Function::new(
16227 "SPLIT".to_string(),
16228 vec![str_arg, sep],
16229 ))))
16230 }
16231 _ => Ok(Expression::Function(f)),
16232 }
16233 }
16234 // ClickHouse toMonday(x) -> DATE_TRUNC('WEEK', x) / DATE_TRUNC(x, 'WEEK') for Doris
16235 "TOMONDAY" => {
16236 if f.args.len() == 1 {
16237 let arg = f.args.into_iter().next().unwrap();
16238 match target {
16239 DialectType::Doris => {
16240 Ok(Expression::Function(Box::new(Function::new(
16241 "DATE_TRUNC".to_string(),
16242 vec![arg, Expression::string("WEEK")],
16243 ))))
16244 }
16245 _ => Ok(Expression::Function(Box::new(Function::new(
16246 "DATE_TRUNC".to_string(),
16247 vec![Expression::string("WEEK"), arg],
16248 )))),
16249 }
16250 } else {
16251 Ok(Expression::Function(f))
16252 }
16253 }
16254 // COLLECT_LIST with FILTER(WHERE x IS NOT NULL) for targets that need it
16255 "COLLECT_LIST" if f.args.len() == 1 => match target {
16256 DialectType::Spark
16257 | DialectType::Databricks
16258 | DialectType::Hive => Ok(Expression::Function(f)),
16259 _ => Ok(Expression::Function(Box::new(Function::new(
16260 "ARRAY_AGG".to_string(),
16261 f.args,
16262 )))),
16263 },
16264 // TO_CHAR(x) with 1 arg -> CAST(x AS STRING) for Doris
16265 "TO_CHAR"
16266 if f.args.len() == 1 && matches!(target, DialectType::Doris) =>
16267 {
16268 let arg = f.args.into_iter().next().unwrap();
16269 Ok(Expression::Cast(Box::new(crate::expressions::Cast {
16270 this: arg,
16271 to: DataType::Custom {
16272 name: "STRING".to_string(),
16273 },
16274 double_colon_syntax: false,
16275 trailing_comments: Vec::new(),
16276 format: None,
16277 default: None,
16278 inferred_type: None,
16279 })))
16280 }
16281 // DBMS_RANDOM.VALUE() -> RANDOM() for PostgreSQL
16282 "DBMS_RANDOM.VALUE" if f.args.is_empty() => match target {
16283 DialectType::PostgreSQL => Ok(Expression::Function(Box::new(
16284 Function::new("RANDOM".to_string(), vec![]),
16285 ))),
16286 _ => Ok(Expression::Function(f)),
16287 },
16288 // ClickHouse formatDateTime -> target-specific
16289 "FORMATDATETIME" if f.args.len() >= 2 => match target {
16290 DialectType::MySQL => Ok(Expression::Function(Box::new(
16291 Function::new("DATE_FORMAT".to_string(), f.args),
16292 ))),
16293 _ => Ok(Expression::Function(f)),
16294 },
16295 // REPLICATE('x', n) -> REPEAT('x', n) for non-TSQL targets
16296 "REPLICATE" if f.args.len() == 2 => match target {
16297 DialectType::TSQL => Ok(Expression::Function(f)),
16298 _ => Ok(Expression::Function(Box::new(Function::new(
16299 "REPEAT".to_string(),
16300 f.args,
16301 )))),
16302 },
16303 // LEN(x) -> LENGTH(x) for non-TSQL targets
16304 // No CAST needed when arg is already a string literal
16305 "LEN" if f.args.len() == 1 => {
16306 match target {
16307 DialectType::TSQL => Ok(Expression::Function(f)),
16308 DialectType::Spark | DialectType::Databricks => {
16309 let arg = f.args.into_iter().next().unwrap();
16310 // Don't wrap string literals with CAST - they're already strings
16311 let is_string = matches!(
16312 &arg,
16313 Expression::Literal(lit) if matches!(lit.as_ref(), crate::expressions::Literal::String(_))
16314 );
16315 let final_arg = if is_string {
16316 arg
16317 } else {
16318 Expression::Cast(Box::new(Cast {
16319 this: arg,
16320 to: DataType::VarChar {
16321 length: None,
16322 parenthesized_length: false,
16323 },
16324 double_colon_syntax: false,
16325 trailing_comments: Vec::new(),
16326 format: None,
16327 default: None,
16328 inferred_type: None,
16329 }))
16330 };
16331 Ok(Expression::Function(Box::new(Function::new(
16332 "LENGTH".to_string(),
16333 vec![final_arg],
16334 ))))
16335 }
16336 _ => {
16337 let arg = f.args.into_iter().next().unwrap();
16338 Ok(Expression::Function(Box::new(Function::new(
16339 "LENGTH".to_string(),
16340 vec![arg],
16341 ))))
16342 }
16343 }
16344 }
16345 // COUNT_BIG(x) -> COUNT(x) for non-TSQL targets
16346 "COUNT_BIG" if f.args.len() == 1 => match target {
16347 DialectType::TSQL => Ok(Expression::Function(f)),
16348 _ => Ok(Expression::Function(Box::new(Function::new(
16349 "COUNT".to_string(),
16350 f.args,
16351 )))),
16352 },
16353 // DATEFROMPARTS(y, m, d) -> MAKE_DATE(y, m, d) for non-TSQL targets
16354 "DATEFROMPARTS" if f.args.len() == 3 => match target {
16355 DialectType::TSQL => Ok(Expression::Function(f)),
16356 _ => Ok(Expression::Function(Box::new(Function::new(
16357 "MAKE_DATE".to_string(),
16358 f.args,
16359 )))),
16360 },
16361 // REGEXP_LIKE(str, pattern) -> RegexpLike expression (target-specific output)
16362 "REGEXP_LIKE" if f.args.len() >= 2 => {
16363 let str_expr = f.args[0].clone();
16364 let pattern = f.args[1].clone();
16365 let flags = if f.args.len() >= 3 {
16366 Some(f.args[2].clone())
16367 } else {
16368 None
16369 };
16370 match target {
16371 DialectType::DuckDB => {
16372 let mut new_args = vec![str_expr, pattern];
16373 if let Some(fl) = flags {
16374 new_args.push(fl);
16375 }
16376 Ok(Expression::Function(Box::new(Function::new(
16377 "REGEXP_MATCHES".to_string(),
16378 new_args,
16379 ))))
16380 }
16381 _ => Ok(Expression::RegexpLike(Box::new(
16382 crate::expressions::RegexpFunc {
16383 this: str_expr,
16384 pattern,
16385 flags,
16386 },
16387 ))),
16388 }
16389 }
16390 // ClickHouse arrayJoin -> UNNEST for PostgreSQL
16391 "ARRAYJOIN" if f.args.len() == 1 => match target {
16392 DialectType::PostgreSQL => Ok(Expression::Function(Box::new(
16393 Function::new("UNNEST".to_string(), f.args),
16394 ))),
16395 _ => Ok(Expression::Function(f)),
16396 },
16397 // DATETIMEFROMPARTS(y, m, d, h, mi, s, ms) -> MAKE_TIMESTAMP / TIMESTAMP_FROM_PARTS
16398 "DATETIMEFROMPARTS" if f.args.len() == 7 => {
16399 match target {
16400 DialectType::TSQL => Ok(Expression::Function(f)),
16401 DialectType::DuckDB => {
16402 // MAKE_TIMESTAMP(y, m, d, h, mi, s + (ms / 1000.0))
16403 let mut args = f.args;
16404 let ms = args.pop().unwrap();
16405 let s = args.pop().unwrap();
16406 // s + (ms / 1000.0)
16407 let ms_frac = Expression::Div(Box::new(BinaryOp::new(
16408 ms,
16409 Expression::Literal(Box::new(
16410 crate::expressions::Literal::Number(
16411 "1000.0".to_string(),
16412 ),
16413 )),
16414 )));
16415 let s_with_ms = Expression::Add(Box::new(BinaryOp::new(
16416 s,
16417 Expression::Paren(Box::new(Paren {
16418 this: ms_frac,
16419 trailing_comments: vec![],
16420 })),
16421 )));
16422 args.push(s_with_ms);
16423 Ok(Expression::Function(Box::new(Function::new(
16424 "MAKE_TIMESTAMP".to_string(),
16425 args,
16426 ))))
16427 }
16428 DialectType::Snowflake => {
16429 // TIMESTAMP_FROM_PARTS(y, m, d, h, mi, s, ms * 1000000)
16430 let mut args = f.args;
16431 let ms = args.pop().unwrap();
16432 // ms * 1000000
16433 let ns = Expression::Mul(Box::new(BinaryOp::new(
16434 ms,
16435 Expression::number(1000000),
16436 )));
16437 args.push(ns);
16438 Ok(Expression::Function(Box::new(Function::new(
16439 "TIMESTAMP_FROM_PARTS".to_string(),
16440 args,
16441 ))))
16442 }
16443 _ => {
16444 // Default: keep function name for other targets
16445 Ok(Expression::Function(Box::new(Function::new(
16446 "DATETIMEFROMPARTS".to_string(),
16447 f.args,
16448 ))))
16449 }
16450 }
16451 }
16452 // CONVERT(type, expr [, style]) -> CAST(expr AS type) for non-TSQL targets
16453 // TRY_CONVERT(type, expr [, style]) -> TRY_CAST(expr AS type) for non-TSQL targets
16454 "CONVERT" | "TRY_CONVERT" if f.args.len() >= 2 => {
16455 let is_try = name == "TRY_CONVERT";
16456 let type_expr = f.args[0].clone();
16457 let value_expr = f.args[1].clone();
16458 let style = if f.args.len() >= 3 {
16459 Some(&f.args[2])
16460 } else {
16461 None
16462 };
16463
16464 // For TSQL->TSQL, normalize types and preserve CONVERT/TRY_CONVERT
16465 if matches!(target, DialectType::TSQL) {
16466 let normalized_type = match &type_expr {
16467 Expression::DataType(dt) => {
16468 let new_dt = match dt {
16469 DataType::Int { .. } => DataType::Custom {
16470 name: "INTEGER".to_string(),
16471 },
16472 _ => dt.clone(),
16473 };
16474 Expression::DataType(new_dt)
16475 }
16476 Expression::Identifier(id) => {
16477 if id.name.eq_ignore_ascii_case("INT") {
16478 Expression::Identifier(
16479 crate::expressions::Identifier::new("INTEGER"),
16480 )
16481 } else {
16482 let upper = id.name.to_ascii_uppercase();
16483 Expression::Identifier(
16484 crate::expressions::Identifier::new(upper),
16485 )
16486 }
16487 }
16488 Expression::Column(col) => {
16489 if col.name.name.eq_ignore_ascii_case("INT") {
16490 Expression::Identifier(
16491 crate::expressions::Identifier::new("INTEGER"),
16492 )
16493 } else {
16494 let upper = col.name.name.to_ascii_uppercase();
16495 Expression::Identifier(
16496 crate::expressions::Identifier::new(upper),
16497 )
16498 }
16499 }
16500 _ => type_expr.clone(),
16501 };
16502 let func_name = if is_try { "TRY_CONVERT" } else { "CONVERT" };
16503 let mut new_args = vec![normalized_type, value_expr];
16504 if let Some(s) = style {
16505 new_args.push(s.clone());
16506 }
16507 return Ok(Expression::Function(Box::new(Function::new(
16508 func_name.to_string(),
16509 new_args,
16510 ))));
16511 }
16512
16513 // For other targets: CONVERT(type, expr) -> CAST(expr AS type)
16514 fn expr_to_datatype(e: &Expression) -> Option<DataType> {
16515 match e {
16516 Expression::DataType(dt) => {
16517 // Convert NVARCHAR/NCHAR Custom types to standard VarChar/Char
16518 match dt {
16519 DataType::Custom { name }
16520 if name.starts_with("NVARCHAR(")
16521 || name.starts_with("NCHAR(") =>
16522 {
16523 // Extract the length from "NVARCHAR(200)" or "NCHAR(40)"
16524 let inner = &name[name.find('(').unwrap() + 1
16525 ..name.len() - 1];
16526 if inner.eq_ignore_ascii_case("MAX") {
16527 Some(DataType::Text)
16528 } else if let Ok(len) = inner.parse::<u32>() {
16529 if name.starts_with("NCHAR") {
16530 Some(DataType::Char {
16531 length: Some(len),
16532 })
16533 } else {
16534 Some(DataType::VarChar {
16535 length: Some(len),
16536 parenthesized_length: false,
16537 })
16538 }
16539 } else {
16540 Some(dt.clone())
16541 }
16542 }
16543 DataType::Custom { name } if name == "NVARCHAR" => {
16544 Some(DataType::VarChar {
16545 length: None,
16546 parenthesized_length: false,
16547 })
16548 }
16549 DataType::Custom { name } if name == "NCHAR" => {
16550 Some(DataType::Char { length: None })
16551 }
16552 DataType::Custom { name }
16553 if name == "NVARCHAR(MAX)"
16554 || name == "VARCHAR(MAX)" =>
16555 {
16556 Some(DataType::Text)
16557 }
16558 _ => Some(dt.clone()),
16559 }
16560 }
16561 Expression::Identifier(id) => {
16562 let name = id.name.to_ascii_uppercase();
16563 match name.as_str() {
16564 "INT" | "INTEGER" => Some(DataType::Int {
16565 length: None,
16566 integer_spelling: false,
16567 }),
16568 "BIGINT" => Some(DataType::BigInt { length: None }),
16569 "SMALLINT" => {
16570 Some(DataType::SmallInt { length: None })
16571 }
16572 "TINYINT" => {
16573 Some(DataType::TinyInt { length: None })
16574 }
16575 "FLOAT" => Some(DataType::Float {
16576 precision: None,
16577 scale: None,
16578 real_spelling: false,
16579 }),
16580 "REAL" => Some(DataType::Float {
16581 precision: None,
16582 scale: None,
16583 real_spelling: true,
16584 }),
16585 "DATETIME" | "DATETIME2" => {
16586 Some(DataType::Timestamp {
16587 timezone: false,
16588 precision: None,
16589 })
16590 }
16591 "DATE" => Some(DataType::Date),
16592 "BIT" => Some(DataType::Boolean),
16593 "TEXT" => Some(DataType::Text),
16594 "NUMERIC" => Some(DataType::Decimal {
16595 precision: None,
16596 scale: None,
16597 }),
16598 "MONEY" => Some(DataType::Decimal {
16599 precision: Some(15),
16600 scale: Some(4),
16601 }),
16602 "SMALLMONEY" => Some(DataType::Decimal {
16603 precision: Some(6),
16604 scale: Some(4),
16605 }),
16606 "VARCHAR" => Some(DataType::VarChar {
16607 length: None,
16608 parenthesized_length: false,
16609 }),
16610 "NVARCHAR" => Some(DataType::VarChar {
16611 length: None,
16612 parenthesized_length: false,
16613 }),
16614 "CHAR" => Some(DataType::Char { length: None }),
16615 "NCHAR" => Some(DataType::Char { length: None }),
16616 _ => Some(DataType::Custom { name }),
16617 }
16618 }
16619 Expression::Column(col) => {
16620 let name = col.name.name.to_ascii_uppercase();
16621 match name.as_str() {
16622 "INT" | "INTEGER" => Some(DataType::Int {
16623 length: None,
16624 integer_spelling: false,
16625 }),
16626 "BIGINT" => Some(DataType::BigInt { length: None }),
16627 "FLOAT" => Some(DataType::Float {
16628 precision: None,
16629 scale: None,
16630 real_spelling: false,
16631 }),
16632 "DATETIME" | "DATETIME2" => {
16633 Some(DataType::Timestamp {
16634 timezone: false,
16635 precision: None,
16636 })
16637 }
16638 "DATE" => Some(DataType::Date),
16639 "NUMERIC" => Some(DataType::Decimal {
16640 precision: None,
16641 scale: None,
16642 }),
16643 "VARCHAR" => Some(DataType::VarChar {
16644 length: None,
16645 parenthesized_length: false,
16646 }),
16647 "NVARCHAR" => Some(DataType::VarChar {
16648 length: None,
16649 parenthesized_length: false,
16650 }),
16651 "CHAR" => Some(DataType::Char { length: None }),
16652 "NCHAR" => Some(DataType::Char { length: None }),
16653 _ => Some(DataType::Custom { name }),
16654 }
16655 }
16656 // NVARCHAR(200) parsed as Function("NVARCHAR", [200])
16657 Expression::Function(f) => {
16658 let fname = f.name.to_ascii_uppercase();
16659 match fname.as_str() {
16660 "VARCHAR" | "NVARCHAR" => {
16661 let len = f.args.first().and_then(|a| {
16662 if let Expression::Literal(lit) = a
16663 {
16664 if let crate::expressions::Literal::Number(n) = lit.as_ref() {
16665 n.parse::<u32>().ok()
16666 } else { None }
16667 } else if let Expression::Identifier(id) = a
16668 {
16669 if id.name.eq_ignore_ascii_case("MAX") {
16670 None
16671 } else {
16672 None
16673 }
16674 } else {
16675 None
16676 }
16677 });
16678 // Check for VARCHAR(MAX) -> TEXT
16679 let is_max = f.args.first().map_or(false, |a| {
16680 matches!(a, Expression::Identifier(id) if id.name.eq_ignore_ascii_case("MAX"))
16681 || matches!(a, Expression::Column(col) if col.name.name.eq_ignore_ascii_case("MAX"))
16682 });
16683 if is_max {
16684 Some(DataType::Text)
16685 } else {
16686 Some(DataType::VarChar {
16687 length: len,
16688 parenthesized_length: false,
16689 })
16690 }
16691 }
16692 "NCHAR" | "CHAR" => {
16693 let len = f.args.first().and_then(|a| {
16694 if let Expression::Literal(lit) = a
16695 {
16696 if let crate::expressions::Literal::Number(n) = lit.as_ref() {
16697 n.parse::<u32>().ok()
16698 } else { None }
16699 } else {
16700 None
16701 }
16702 });
16703 Some(DataType::Char { length: len })
16704 }
16705 "NUMERIC" | "DECIMAL" => {
16706 let precision = f.args.first().and_then(|a| {
16707 if let Expression::Literal(lit) = a
16708 {
16709 if let crate::expressions::Literal::Number(n) = lit.as_ref() {
16710 n.parse::<u32>().ok()
16711 } else { None }
16712 } else {
16713 None
16714 }
16715 });
16716 let scale = f.args.get(1).and_then(|a| {
16717 if let Expression::Literal(lit) = a
16718 {
16719 if let crate::expressions::Literal::Number(n) = lit.as_ref() {
16720 n.parse::<u32>().ok()
16721 } else { None }
16722 } else {
16723 None
16724 }
16725 });
16726 Some(DataType::Decimal { precision, scale })
16727 }
16728 _ => None,
16729 }
16730 }
16731 _ => None,
16732 }
16733 }
16734
16735 if let Some(mut dt) = expr_to_datatype(&type_expr) {
16736 // For TSQL source: VARCHAR/CHAR without length defaults to 30
16737 let is_tsql_source =
16738 matches!(source, DialectType::TSQL | DialectType::Fabric);
16739 if is_tsql_source {
16740 match &dt {
16741 DataType::VarChar { length: None, .. } => {
16742 dt = DataType::VarChar {
16743 length: Some(30),
16744 parenthesized_length: false,
16745 };
16746 }
16747 DataType::Char { length: None } => {
16748 dt = DataType::Char { length: Some(30) };
16749 }
16750 _ => {}
16751 }
16752 }
16753
16754 // Determine if this is a string type
16755 let is_string_type = matches!(
16756 dt,
16757 DataType::VarChar { .. }
16758 | DataType::Char { .. }
16759 | DataType::Text
16760 ) || matches!(&dt, DataType::Custom { name } if name == "NVARCHAR" || name == "NCHAR"
16761 || name.starts_with("NVARCHAR(") || name.starts_with("NCHAR(")
16762 || name.starts_with("VARCHAR(") || name == "VARCHAR"
16763 || name == "STRING");
16764
16765 // Determine if this is a date/time type
16766 let is_datetime_type = matches!(
16767 dt,
16768 DataType::Timestamp { .. } | DataType::Date
16769 ) || matches!(&dt, DataType::Custom { name } if name == "DATETIME"
16770 || name == "DATETIME2" || name == "SMALLDATETIME");
16771
16772 // Check for date conversion with style
16773 if style.is_some() {
16774 let style_num = style.and_then(|s| {
16775 if let Expression::Literal(lit) = s {
16776 if let crate::expressions::Literal::Number(n) =
16777 lit.as_ref()
16778 {
16779 n.parse::<u32>().ok()
16780 } else {
16781 None
16782 }
16783 } else {
16784 None
16785 }
16786 });
16787
16788 // TSQL CONVERT date styles (Java format)
16789 let format_str = style_num.and_then(|n| match n {
16790 101 => Some("MM/dd/yyyy"),
16791 102 => Some("yyyy.MM.dd"),
16792 103 => Some("dd/MM/yyyy"),
16793 104 => Some("dd.MM.yyyy"),
16794 105 => Some("dd-MM-yyyy"),
16795 108 => Some("HH:mm:ss"),
16796 110 => Some("MM-dd-yyyy"),
16797 112 => Some("yyyyMMdd"),
16798 120 | 20 => Some("yyyy-MM-dd HH:mm:ss"),
16799 121 | 21 => Some("yyyy-MM-dd HH:mm:ss.SSSSSS"),
16800 126 | 127 => Some("yyyy-MM-dd'T'HH:mm:ss.SSS"),
16801 _ => None,
16802 });
16803
16804 // Non-string, non-datetime types with style: just CAST, ignore the style
16805 if !is_string_type && !is_datetime_type {
16806 let cast_expr = if is_try {
16807 Expression::TryCast(Box::new(
16808 crate::expressions::Cast {
16809 this: value_expr,
16810 to: dt,
16811 trailing_comments: Vec::new(),
16812 double_colon_syntax: false,
16813 format: None,
16814 default: None,
16815 inferred_type: None,
16816 },
16817 ))
16818 } else {
16819 Expression::Cast(Box::new(
16820 crate::expressions::Cast {
16821 this: value_expr,
16822 to: dt,
16823 trailing_comments: Vec::new(),
16824 double_colon_syntax: false,
16825 format: None,
16826 default: None,
16827 inferred_type: None,
16828 },
16829 ))
16830 };
16831 return Ok(cast_expr);
16832 }
16833
16834 if let Some(java_fmt) = format_str {
16835 let c_fmt = java_fmt
16836 .replace("yyyy", "%Y")
16837 .replace("MM", "%m")
16838 .replace("dd", "%d")
16839 .replace("HH", "%H")
16840 .replace("mm", "%M")
16841 .replace("ss", "%S")
16842 .replace("SSSSSS", "%f")
16843 .replace("SSS", "%f")
16844 .replace("'T'", "T");
16845
16846 // For datetime target types: style is the INPUT format for parsing strings -> dates
16847 if is_datetime_type {
16848 match target {
16849 DialectType::DuckDB => {
16850 return Ok(Expression::Function(Box::new(
16851 Function::new(
16852 "STRPTIME".to_string(),
16853 vec![
16854 value_expr,
16855 Expression::string(&c_fmt),
16856 ],
16857 ),
16858 )));
16859 }
16860 DialectType::Spark
16861 | DialectType::Databricks => {
16862 // CONVERT(DATETIME, x, style) -> TO_TIMESTAMP(x, fmt)
16863 // CONVERT(DATE, x, style) -> TO_DATE(x, fmt)
16864 let func_name =
16865 if matches!(dt, DataType::Date) {
16866 "TO_DATE"
16867 } else {
16868 "TO_TIMESTAMP"
16869 };
16870 return Ok(Expression::Function(Box::new(
16871 Function::new(
16872 func_name.to_string(),
16873 vec![
16874 value_expr,
16875 Expression::string(java_fmt),
16876 ],
16877 ),
16878 )));
16879 }
16880 DialectType::Hive => {
16881 return Ok(Expression::Function(Box::new(
16882 Function::new(
16883 "TO_TIMESTAMP".to_string(),
16884 vec![
16885 value_expr,
16886 Expression::string(java_fmt),
16887 ],
16888 ),
16889 )));
16890 }
16891 _ => {
16892 return Ok(Expression::Cast(Box::new(
16893 crate::expressions::Cast {
16894 this: value_expr,
16895 to: dt,
16896 trailing_comments: Vec::new(),
16897 double_colon_syntax: false,
16898 format: None,
16899 default: None,
16900 inferred_type: None,
16901 },
16902 )));
16903 }
16904 }
16905 }
16906
16907 // For string target types: style is the OUTPUT format for dates -> strings
16908 match target {
16909 DialectType::DuckDB => Ok(Expression::Function(
16910 Box::new(Function::new(
16911 "STRPTIME".to_string(),
16912 vec![
16913 value_expr,
16914 Expression::string(&c_fmt),
16915 ],
16916 )),
16917 )),
16918 DialectType::Spark | DialectType::Databricks => {
16919 // For string target types with style: CAST(DATE_FORMAT(x, fmt) AS type)
16920 // Determine the target string type
16921 let string_dt = match &dt {
16922 DataType::VarChar {
16923 length: Some(l),
16924 ..
16925 } => DataType::VarChar {
16926 length: Some(*l),
16927 parenthesized_length: false,
16928 },
16929 DataType::Text => DataType::Custom {
16930 name: "STRING".to_string(),
16931 },
16932 _ => DataType::Custom {
16933 name: "STRING".to_string(),
16934 },
16935 };
16936 let date_format_expr = Expression::Function(
16937 Box::new(Function::new(
16938 "DATE_FORMAT".to_string(),
16939 vec![
16940 value_expr,
16941 Expression::string(java_fmt),
16942 ],
16943 )),
16944 );
16945 let cast_expr = if is_try {
16946 Expression::TryCast(Box::new(
16947 crate::expressions::Cast {
16948 this: date_format_expr,
16949 to: string_dt,
16950 trailing_comments: Vec::new(),
16951 double_colon_syntax: false,
16952 format: None,
16953 default: None,
16954 inferred_type: None,
16955 },
16956 ))
16957 } else {
16958 Expression::Cast(Box::new(
16959 crate::expressions::Cast {
16960 this: date_format_expr,
16961 to: string_dt,
16962 trailing_comments: Vec::new(),
16963 double_colon_syntax: false,
16964 format: None,
16965 default: None,
16966 inferred_type: None,
16967 },
16968 ))
16969 };
16970 Ok(cast_expr)
16971 }
16972 DialectType::MySQL | DialectType::SingleStore => {
16973 // For MySQL: CAST(DATE_FORMAT(x, mysql_fmt) AS CHAR(n))
16974 let mysql_fmt = java_fmt
16975 .replace("yyyy", "%Y")
16976 .replace("MM", "%m")
16977 .replace("dd", "%d")
16978 .replace("HH:mm:ss.SSSSSS", "%T")
16979 .replace("HH:mm:ss", "%T")
16980 .replace("HH", "%H")
16981 .replace("mm", "%i")
16982 .replace("ss", "%S");
16983 let date_format_expr = Expression::Function(
16984 Box::new(Function::new(
16985 "DATE_FORMAT".to_string(),
16986 vec![
16987 value_expr,
16988 Expression::string(&mysql_fmt),
16989 ],
16990 )),
16991 );
16992 // MySQL uses CHAR for string casts
16993 let mysql_dt = match &dt {
16994 DataType::VarChar { length, .. } => {
16995 DataType::Char { length: *length }
16996 }
16997 _ => dt,
16998 };
16999 Ok(Expression::Cast(Box::new(
17000 crate::expressions::Cast {
17001 this: date_format_expr,
17002 to: mysql_dt,
17003 trailing_comments: Vec::new(),
17004 double_colon_syntax: false,
17005 format: None,
17006 default: None,
17007 inferred_type: None,
17008 },
17009 )))
17010 }
17011 DialectType::Hive => {
17012 let func_name = "TO_TIMESTAMP";
17013 Ok(Expression::Function(Box::new(
17014 Function::new(
17015 func_name.to_string(),
17016 vec![
17017 value_expr,
17018 Expression::string(java_fmt),
17019 ],
17020 ),
17021 )))
17022 }
17023 _ => Ok(Expression::Cast(Box::new(
17024 crate::expressions::Cast {
17025 this: value_expr,
17026 to: dt,
17027 trailing_comments: Vec::new(),
17028 double_colon_syntax: false,
17029 format: None,
17030 default: None,
17031 inferred_type: None,
17032 },
17033 ))),
17034 }
17035 } else {
17036 // Unknown style, just CAST
17037 let cast_expr = if is_try {
17038 Expression::TryCast(Box::new(
17039 crate::expressions::Cast {
17040 this: value_expr,
17041 to: dt,
17042 trailing_comments: Vec::new(),
17043 double_colon_syntax: false,
17044 format: None,
17045 default: None,
17046 inferred_type: None,
17047 },
17048 ))
17049 } else {
17050 Expression::Cast(Box::new(
17051 crate::expressions::Cast {
17052 this: value_expr,
17053 to: dt,
17054 trailing_comments: Vec::new(),
17055 double_colon_syntax: false,
17056 format: None,
17057 default: None,
17058 inferred_type: None,
17059 },
17060 ))
17061 };
17062 Ok(cast_expr)
17063 }
17064 } else {
17065 // No style - simple CAST
17066 let final_dt = if matches!(
17067 target,
17068 DialectType::MySQL | DialectType::SingleStore
17069 ) {
17070 match &dt {
17071 DataType::Int { .. }
17072 | DataType::BigInt { .. }
17073 | DataType::SmallInt { .. }
17074 | DataType::TinyInt { .. } => DataType::Custom {
17075 name: "SIGNED".to_string(),
17076 },
17077 DataType::VarChar { length, .. } => {
17078 DataType::Char { length: *length }
17079 }
17080 _ => dt,
17081 }
17082 } else {
17083 dt
17084 };
17085 let cast_expr = if is_try {
17086 Expression::TryCast(Box::new(
17087 crate::expressions::Cast {
17088 this: value_expr,
17089 to: final_dt,
17090 trailing_comments: Vec::new(),
17091 double_colon_syntax: false,
17092 format: None,
17093 default: None,
17094 inferred_type: None,
17095 },
17096 ))
17097 } else {
17098 Expression::Cast(Box::new(crate::expressions::Cast {
17099 this: value_expr,
17100 to: final_dt,
17101 trailing_comments: Vec::new(),
17102 double_colon_syntax: false,
17103 format: None,
17104 default: None,
17105 inferred_type: None,
17106 }))
17107 };
17108 Ok(cast_expr)
17109 }
17110 } else {
17111 // Can't convert type expression - keep as CONVERT/TRY_CONVERT function
17112 Ok(Expression::Function(f))
17113 }
17114 }
17115 // STRFTIME(val, fmt) from DuckDB / STRFTIME(fmt, val) from SQLite -> target-specific
17116 "STRFTIME" if f.args.len() == 2 => {
17117 // SQLite uses STRFTIME(fmt, val); DuckDB uses STRFTIME(val, fmt)
17118 let (val, fmt_expr) = if matches!(source, DialectType::SQLite) {
17119 // SQLite: args[0] = format, args[1] = value
17120 (f.args[1].clone(), &f.args[0])
17121 } else {
17122 // DuckDB and others: args[0] = value, args[1] = format
17123 (f.args[0].clone(), &f.args[1])
17124 };
17125
17126 // Helper to convert C-style format to Java-style
17127 fn c_to_java_format(fmt: &str) -> String {
17128 fmt.replace("%Y", "yyyy")
17129 .replace("%m", "MM")
17130 .replace("%d", "dd")
17131 .replace("%H", "HH")
17132 .replace("%M", "mm")
17133 .replace("%S", "ss")
17134 .replace("%f", "SSSSSS")
17135 .replace("%y", "yy")
17136 .replace("%-m", "M")
17137 .replace("%-d", "d")
17138 .replace("%-H", "H")
17139 .replace("%-I", "h")
17140 .replace("%I", "hh")
17141 .replace("%p", "a")
17142 .replace("%j", "DDD")
17143 .replace("%a", "EEE")
17144 .replace("%b", "MMM")
17145 .replace("%F", "yyyy-MM-dd")
17146 .replace("%T", "HH:mm:ss")
17147 }
17148
17149 // Helper: recursively convert format strings within expressions (handles CONCAT)
17150 fn convert_fmt_expr(
17151 expr: &Expression,
17152 converter: &dyn Fn(&str) -> String,
17153 ) -> Expression {
17154 match expr {
17155 Expression::Literal(lit)
17156 if matches!(
17157 lit.as_ref(),
17158 crate::expressions::Literal::String(_)
17159 ) =>
17160 {
17161 let crate::expressions::Literal::String(s) =
17162 lit.as_ref()
17163 else {
17164 unreachable!()
17165 };
17166 Expression::string(&converter(s))
17167 }
17168 Expression::Function(func)
17169 if func.name.eq_ignore_ascii_case("CONCAT") =>
17170 {
17171 let new_args: Vec<Expression> = func
17172 .args
17173 .iter()
17174 .map(|a| convert_fmt_expr(a, converter))
17175 .collect();
17176 Expression::Function(Box::new(Function::new(
17177 "CONCAT".to_string(),
17178 new_args,
17179 )))
17180 }
17181 other => other.clone(),
17182 }
17183 }
17184
17185 match target {
17186 DialectType::DuckDB => {
17187 if matches!(source, DialectType::SQLite) {
17188 // SQLite STRFTIME(fmt, val) -> DuckDB STRFTIME(CAST(val AS TIMESTAMP), fmt)
17189 let cast_val = Expression::Cast(Box::new(Cast {
17190 this: val,
17191 to: crate::expressions::DataType::Timestamp {
17192 precision: None,
17193 timezone: false,
17194 },
17195 trailing_comments: Vec::new(),
17196 double_colon_syntax: false,
17197 format: None,
17198 default: None,
17199 inferred_type: None,
17200 }));
17201 Ok(Expression::Function(Box::new(Function::new(
17202 "STRFTIME".to_string(),
17203 vec![cast_val, fmt_expr.clone()],
17204 ))))
17205 } else {
17206 Ok(Expression::Function(f))
17207 }
17208 }
17209 DialectType::Spark
17210 | DialectType::Databricks
17211 | DialectType::Hive => {
17212 // STRFTIME(val, fmt) -> DATE_FORMAT(val, java_fmt)
17213 let converted_fmt =
17214 convert_fmt_expr(fmt_expr, &c_to_java_format);
17215 Ok(Expression::Function(Box::new(Function::new(
17216 "DATE_FORMAT".to_string(),
17217 vec![val, converted_fmt],
17218 ))))
17219 }
17220 DialectType::TSQL | DialectType::Fabric => {
17221 // STRFTIME(val, fmt) -> FORMAT(val, java_fmt)
17222 let converted_fmt =
17223 convert_fmt_expr(fmt_expr, &c_to_java_format);
17224 Ok(Expression::Function(Box::new(Function::new(
17225 "FORMAT".to_string(),
17226 vec![val, converted_fmt],
17227 ))))
17228 }
17229 DialectType::Presto
17230 | DialectType::Trino
17231 | DialectType::Athena => {
17232 // STRFTIME(val, fmt) -> DATE_FORMAT(val, presto_fmt) (convert DuckDB format to Presto)
17233 if let Expression::Literal(lit) = fmt_expr {
17234 if let crate::expressions::Literal::String(s) =
17235 lit.as_ref()
17236 {
17237 let presto_fmt = duckdb_to_presto_format(s);
17238 Ok(Expression::Function(Box::new(Function::new(
17239 "DATE_FORMAT".to_string(),
17240 vec![val, Expression::string(&presto_fmt)],
17241 ))))
17242 } else {
17243 Ok(Expression::Function(Box::new(Function::new(
17244 "DATE_FORMAT".to_string(),
17245 vec![val, fmt_expr.clone()],
17246 ))))
17247 }
17248 } else {
17249 Ok(Expression::Function(Box::new(Function::new(
17250 "DATE_FORMAT".to_string(),
17251 vec![val, fmt_expr.clone()],
17252 ))))
17253 }
17254 }
17255 DialectType::BigQuery => {
17256 // STRFTIME(val, fmt) -> FORMAT_DATE(bq_fmt, val) - note reversed arg order
17257 if let Expression::Literal(lit) = fmt_expr {
17258 if let crate::expressions::Literal::String(s) =
17259 lit.as_ref()
17260 {
17261 let bq_fmt = duckdb_to_bigquery_format(s);
17262 Ok(Expression::Function(Box::new(Function::new(
17263 "FORMAT_DATE".to_string(),
17264 vec![Expression::string(&bq_fmt), val],
17265 ))))
17266 } else {
17267 Ok(Expression::Function(Box::new(Function::new(
17268 "FORMAT_DATE".to_string(),
17269 vec![fmt_expr.clone(), val],
17270 ))))
17271 }
17272 } else {
17273 Ok(Expression::Function(Box::new(Function::new(
17274 "FORMAT_DATE".to_string(),
17275 vec![fmt_expr.clone(), val],
17276 ))))
17277 }
17278 }
17279 DialectType::PostgreSQL | DialectType::Redshift => {
17280 // STRFTIME(val, fmt) -> TO_CHAR(val, pg_fmt)
17281 if let Expression::Literal(lit) = fmt_expr {
17282 if let crate::expressions::Literal::String(s) =
17283 lit.as_ref()
17284 {
17285 let pg_fmt = s
17286 .replace("%Y", "YYYY")
17287 .replace("%m", "MM")
17288 .replace("%d", "DD")
17289 .replace("%H", "HH24")
17290 .replace("%M", "MI")
17291 .replace("%S", "SS")
17292 .replace("%y", "YY")
17293 .replace("%-m", "FMMM")
17294 .replace("%-d", "FMDD")
17295 .replace("%-H", "FMHH24")
17296 .replace("%-I", "FMHH12")
17297 .replace("%p", "AM")
17298 .replace("%F", "YYYY-MM-DD")
17299 .replace("%T", "HH24:MI:SS");
17300 Ok(Expression::Function(Box::new(Function::new(
17301 "TO_CHAR".to_string(),
17302 vec![val, Expression::string(&pg_fmt)],
17303 ))))
17304 } else {
17305 Ok(Expression::Function(Box::new(Function::new(
17306 "TO_CHAR".to_string(),
17307 vec![val, fmt_expr.clone()],
17308 ))))
17309 }
17310 } else {
17311 Ok(Expression::Function(Box::new(Function::new(
17312 "TO_CHAR".to_string(),
17313 vec![val, fmt_expr.clone()],
17314 ))))
17315 }
17316 }
17317 _ => Ok(Expression::Function(f)),
17318 }
17319 }
17320 // STRPTIME(val, fmt) from DuckDB -> target-specific date parse function
17321 "STRPTIME" if f.args.len() == 2 => {
17322 let val = f.args[0].clone();
17323 let fmt_expr = &f.args[1];
17324
17325 fn c_to_java_format_parse(fmt: &str) -> String {
17326 fmt.replace("%Y", "yyyy")
17327 .replace("%m", "MM")
17328 .replace("%d", "dd")
17329 .replace("%H", "HH")
17330 .replace("%M", "mm")
17331 .replace("%S", "ss")
17332 .replace("%f", "SSSSSS")
17333 .replace("%y", "yy")
17334 .replace("%-m", "M")
17335 .replace("%-d", "d")
17336 .replace("%-H", "H")
17337 .replace("%-I", "h")
17338 .replace("%I", "hh")
17339 .replace("%p", "a")
17340 .replace("%F", "yyyy-MM-dd")
17341 .replace("%T", "HH:mm:ss")
17342 }
17343
17344 match target {
17345 DialectType::DuckDB => Ok(Expression::Function(f)),
17346 DialectType::Spark | DialectType::Databricks => {
17347 // STRPTIME(val, fmt) -> TO_TIMESTAMP(val, java_fmt)
17348 if let Expression::Literal(lit) = fmt_expr {
17349 if let crate::expressions::Literal::String(s) =
17350 lit.as_ref()
17351 {
17352 let java_fmt = c_to_java_format_parse(s);
17353 Ok(Expression::Function(Box::new(Function::new(
17354 "TO_TIMESTAMP".to_string(),
17355 vec![val, Expression::string(&java_fmt)],
17356 ))))
17357 } else {
17358 Ok(Expression::Function(Box::new(Function::new(
17359 "TO_TIMESTAMP".to_string(),
17360 vec![val, fmt_expr.clone()],
17361 ))))
17362 }
17363 } else {
17364 Ok(Expression::Function(Box::new(Function::new(
17365 "TO_TIMESTAMP".to_string(),
17366 vec![val, fmt_expr.clone()],
17367 ))))
17368 }
17369 }
17370 DialectType::Hive => {
17371 // STRPTIME(val, fmt) -> CAST(FROM_UNIXTIME(UNIX_TIMESTAMP(val, java_fmt)) AS TIMESTAMP)
17372 if let Expression::Literal(lit) = fmt_expr {
17373 if let crate::expressions::Literal::String(s) =
17374 lit.as_ref()
17375 {
17376 let java_fmt = c_to_java_format_parse(s);
17377 let unix_ts =
17378 Expression::Function(Box::new(Function::new(
17379 "UNIX_TIMESTAMP".to_string(),
17380 vec![val, Expression::string(&java_fmt)],
17381 )));
17382 let from_unix =
17383 Expression::Function(Box::new(Function::new(
17384 "FROM_UNIXTIME".to_string(),
17385 vec![unix_ts],
17386 )));
17387 Ok(Expression::Cast(Box::new(
17388 crate::expressions::Cast {
17389 this: from_unix,
17390 to: DataType::Timestamp {
17391 timezone: false,
17392 precision: None,
17393 },
17394 trailing_comments: Vec::new(),
17395 double_colon_syntax: false,
17396 format: None,
17397 default: None,
17398 inferred_type: None,
17399 },
17400 )))
17401 } else {
17402 Ok(Expression::Function(f))
17403 }
17404 } else {
17405 Ok(Expression::Function(f))
17406 }
17407 }
17408 DialectType::Presto
17409 | DialectType::Trino
17410 | DialectType::Athena => {
17411 // STRPTIME(val, fmt) -> DATE_PARSE(val, presto_fmt) (convert DuckDB format to Presto)
17412 if let Expression::Literal(lit) = fmt_expr {
17413 if let crate::expressions::Literal::String(s) =
17414 lit.as_ref()
17415 {
17416 let presto_fmt = duckdb_to_presto_format(s);
17417 Ok(Expression::Function(Box::new(Function::new(
17418 "DATE_PARSE".to_string(),
17419 vec![val, Expression::string(&presto_fmt)],
17420 ))))
17421 } else {
17422 Ok(Expression::Function(Box::new(Function::new(
17423 "DATE_PARSE".to_string(),
17424 vec![val, fmt_expr.clone()],
17425 ))))
17426 }
17427 } else {
17428 Ok(Expression::Function(Box::new(Function::new(
17429 "DATE_PARSE".to_string(),
17430 vec![val, fmt_expr.clone()],
17431 ))))
17432 }
17433 }
17434 DialectType::BigQuery => {
17435 // STRPTIME(val, fmt) -> PARSE_TIMESTAMP(bq_fmt, val) - note reversed arg order
17436 if let Expression::Literal(lit) = fmt_expr {
17437 if let crate::expressions::Literal::String(s) =
17438 lit.as_ref()
17439 {
17440 let bq_fmt = duckdb_to_bigquery_format(s);
17441 Ok(Expression::Function(Box::new(Function::new(
17442 "PARSE_TIMESTAMP".to_string(),
17443 vec![Expression::string(&bq_fmt), val],
17444 ))))
17445 } else {
17446 Ok(Expression::Function(Box::new(Function::new(
17447 "PARSE_TIMESTAMP".to_string(),
17448 vec![fmt_expr.clone(), val],
17449 ))))
17450 }
17451 } else {
17452 Ok(Expression::Function(Box::new(Function::new(
17453 "PARSE_TIMESTAMP".to_string(),
17454 vec![fmt_expr.clone(), val],
17455 ))))
17456 }
17457 }
17458 _ => Ok(Expression::Function(f)),
17459 }
17460 }
17461 // DATE_FORMAT(val, fmt) from Presto source (C-style format) -> target-specific
17462 "DATE_FORMAT"
17463 if f.args.len() >= 2
17464 && matches!(
17465 source,
17466 DialectType::Presto
17467 | DialectType::Trino
17468 | DialectType::Athena
17469 ) =>
17470 {
17471 let val = f.args[0].clone();
17472 let fmt_expr = &f.args[1];
17473
17474 match target {
17475 DialectType::Presto
17476 | DialectType::Trino
17477 | DialectType::Athena => {
17478 // Presto -> Presto: normalize format (e.g., %H:%i:%S -> %T)
17479 if let Expression::Literal(lit) = fmt_expr {
17480 if let crate::expressions::Literal::String(s) =
17481 lit.as_ref()
17482 {
17483 let normalized = normalize_presto_format(s);
17484 Ok(Expression::Function(Box::new(Function::new(
17485 "DATE_FORMAT".to_string(),
17486 vec![val, Expression::string(&normalized)],
17487 ))))
17488 } else {
17489 Ok(Expression::Function(f))
17490 }
17491 } else {
17492 Ok(Expression::Function(f))
17493 }
17494 }
17495 DialectType::Hive
17496 | DialectType::Spark
17497 | DialectType::Databricks => {
17498 // Convert Presto C-style to Java-style format
17499 if let Expression::Literal(lit) = fmt_expr {
17500 if let crate::expressions::Literal::String(s) =
17501 lit.as_ref()
17502 {
17503 let java_fmt = presto_to_java_format(s);
17504 Ok(Expression::Function(Box::new(Function::new(
17505 "DATE_FORMAT".to_string(),
17506 vec![val, Expression::string(&java_fmt)],
17507 ))))
17508 } else {
17509 Ok(Expression::Function(f))
17510 }
17511 } else {
17512 Ok(Expression::Function(f))
17513 }
17514 }
17515 DialectType::DuckDB => {
17516 // Convert to STRFTIME(val, duckdb_fmt)
17517 if let Expression::Literal(lit) = fmt_expr {
17518 if let crate::expressions::Literal::String(s) =
17519 lit.as_ref()
17520 {
17521 let duckdb_fmt = presto_to_duckdb_format(s);
17522 Ok(Expression::Function(Box::new(Function::new(
17523 "STRFTIME".to_string(),
17524 vec![val, Expression::string(&duckdb_fmt)],
17525 ))))
17526 } else {
17527 Ok(Expression::Function(Box::new(Function::new(
17528 "STRFTIME".to_string(),
17529 vec![val, fmt_expr.clone()],
17530 ))))
17531 }
17532 } else {
17533 Ok(Expression::Function(Box::new(Function::new(
17534 "STRFTIME".to_string(),
17535 vec![val, fmt_expr.clone()],
17536 ))))
17537 }
17538 }
17539 DialectType::BigQuery => {
17540 // Convert to FORMAT_DATE(bq_fmt, val) - reversed args
17541 if let Expression::Literal(lit) = fmt_expr {
17542 if let crate::expressions::Literal::String(s) =
17543 lit.as_ref()
17544 {
17545 let bq_fmt = presto_to_bigquery_format(s);
17546 Ok(Expression::Function(Box::new(Function::new(
17547 "FORMAT_DATE".to_string(),
17548 vec![Expression::string(&bq_fmt), val],
17549 ))))
17550 } else {
17551 Ok(Expression::Function(Box::new(Function::new(
17552 "FORMAT_DATE".to_string(),
17553 vec![fmt_expr.clone(), val],
17554 ))))
17555 }
17556 } else {
17557 Ok(Expression::Function(Box::new(Function::new(
17558 "FORMAT_DATE".to_string(),
17559 vec![fmt_expr.clone(), val],
17560 ))))
17561 }
17562 }
17563 _ => Ok(Expression::Function(f)),
17564 }
17565 }
17566 // DATE_PARSE(val, fmt) from Presto source -> target-specific parse function
17567 "DATE_PARSE"
17568 if f.args.len() >= 2
17569 && matches!(
17570 source,
17571 DialectType::Presto
17572 | DialectType::Trino
17573 | DialectType::Athena
17574 ) =>
17575 {
17576 let val = f.args[0].clone();
17577 let fmt_expr = &f.args[1];
17578
17579 match target {
17580 DialectType::Presto
17581 | DialectType::Trino
17582 | DialectType::Athena => {
17583 // Presto -> Presto: normalize format
17584 if let Expression::Literal(lit) = fmt_expr {
17585 if let crate::expressions::Literal::String(s) =
17586 lit.as_ref()
17587 {
17588 let normalized = normalize_presto_format(s);
17589 Ok(Expression::Function(Box::new(Function::new(
17590 "DATE_PARSE".to_string(),
17591 vec![val, Expression::string(&normalized)],
17592 ))))
17593 } else {
17594 Ok(Expression::Function(f))
17595 }
17596 } else {
17597 Ok(Expression::Function(f))
17598 }
17599 }
17600 DialectType::Hive => {
17601 // Presto -> Hive: if default format, just CAST(x AS TIMESTAMP)
17602 if let Expression::Literal(lit) = fmt_expr {
17603 if let crate::expressions::Literal::String(s) =
17604 lit.as_ref()
17605 {
17606 if is_default_presto_timestamp_format(s)
17607 || is_default_presto_date_format(s)
17608 {
17609 Ok(Expression::Cast(Box::new(
17610 crate::expressions::Cast {
17611 this: val,
17612 to: DataType::Timestamp {
17613 timezone: false,
17614 precision: None,
17615 },
17616 trailing_comments: Vec::new(),
17617 double_colon_syntax: false,
17618 format: None,
17619 default: None,
17620 inferred_type: None,
17621 },
17622 )))
17623 } else {
17624 let java_fmt = presto_to_java_format(s);
17625 Ok(Expression::Function(Box::new(
17626 Function::new(
17627 "TO_TIMESTAMP".to_string(),
17628 vec![
17629 val,
17630 Expression::string(&java_fmt),
17631 ],
17632 ),
17633 )))
17634 }
17635 } else {
17636 Ok(Expression::Function(f))
17637 }
17638 } else {
17639 Ok(Expression::Function(f))
17640 }
17641 }
17642 DialectType::Spark | DialectType::Databricks => {
17643 // Presto -> Spark: TO_TIMESTAMP(val, java_fmt)
17644 if let Expression::Literal(lit) = fmt_expr {
17645 if let crate::expressions::Literal::String(s) =
17646 lit.as_ref()
17647 {
17648 let java_fmt = presto_to_java_format(s);
17649 Ok(Expression::Function(Box::new(Function::new(
17650 "TO_TIMESTAMP".to_string(),
17651 vec![val, Expression::string(&java_fmt)],
17652 ))))
17653 } else {
17654 Ok(Expression::Function(f))
17655 }
17656 } else {
17657 Ok(Expression::Function(f))
17658 }
17659 }
17660 DialectType::DuckDB => {
17661 // Presto -> DuckDB: STRPTIME(val, duckdb_fmt)
17662 if let Expression::Literal(lit) = fmt_expr {
17663 if let crate::expressions::Literal::String(s) =
17664 lit.as_ref()
17665 {
17666 let duckdb_fmt = presto_to_duckdb_format(s);
17667 Ok(Expression::Function(Box::new(Function::new(
17668 "STRPTIME".to_string(),
17669 vec![val, Expression::string(&duckdb_fmt)],
17670 ))))
17671 } else {
17672 Ok(Expression::Function(Box::new(Function::new(
17673 "STRPTIME".to_string(),
17674 vec![val, fmt_expr.clone()],
17675 ))))
17676 }
17677 } else {
17678 Ok(Expression::Function(Box::new(Function::new(
17679 "STRPTIME".to_string(),
17680 vec![val, fmt_expr.clone()],
17681 ))))
17682 }
17683 }
17684 _ => Ok(Expression::Function(f)),
17685 }
17686 }
17687 // FROM_BASE64(x) / TO_BASE64(x) from Presto -> Hive-specific renames
17688 "FROM_BASE64"
17689 if f.args.len() == 1 && matches!(target, DialectType::Hive) =>
17690 {
17691 Ok(Expression::Function(Box::new(Function::new(
17692 "UNBASE64".to_string(),
17693 f.args,
17694 ))))
17695 }
17696 "TO_BASE64"
17697 if f.args.len() == 1 && matches!(target, DialectType::Hive) =>
17698 {
17699 Ok(Expression::Function(Box::new(Function::new(
17700 "BASE64".to_string(),
17701 f.args,
17702 ))))
17703 }
17704 // FROM_UNIXTIME(x) -> CAST(FROM_UNIXTIME(x) AS TIMESTAMP) for Spark
17705 "FROM_UNIXTIME"
17706 if f.args.len() == 1
17707 && matches!(
17708 source,
17709 DialectType::Presto
17710 | DialectType::Trino
17711 | DialectType::Athena
17712 )
17713 && matches!(
17714 target,
17715 DialectType::Spark | DialectType::Databricks
17716 ) =>
17717 {
17718 // Wrap FROM_UNIXTIME(x) in CAST(... AS TIMESTAMP)
17719 let from_unix = Expression::Function(Box::new(Function::new(
17720 "FROM_UNIXTIME".to_string(),
17721 f.args,
17722 )));
17723 Ok(Expression::Cast(Box::new(crate::expressions::Cast {
17724 this: from_unix,
17725 to: DataType::Timestamp {
17726 timezone: false,
17727 precision: None,
17728 },
17729 trailing_comments: Vec::new(),
17730 double_colon_syntax: false,
17731 format: None,
17732 default: None,
17733 inferred_type: None,
17734 })))
17735 }
17736 // DATE_FORMAT(val, fmt) from Hive/Spark/MySQL -> target-specific format function
17737 "DATE_FORMAT"
17738 if f.args.len() >= 2
17739 && !matches!(
17740 target,
17741 DialectType::Hive
17742 | DialectType::Spark
17743 | DialectType::Databricks
17744 | DialectType::MySQL
17745 | DialectType::SingleStore
17746 ) =>
17747 {
17748 let val = f.args[0].clone();
17749 let fmt_expr = &f.args[1];
17750 let is_hive_source = matches!(
17751 source,
17752 DialectType::Hive
17753 | DialectType::Spark
17754 | DialectType::Databricks
17755 );
17756
17757 fn java_to_c_format(fmt: &str) -> String {
17758 // Replace Java patterns with C strftime patterns.
17759 // Uses multi-pass to handle patterns that conflict.
17760 // First pass: replace multi-char patterns (longer first)
17761 let result = fmt
17762 .replace("yyyy", "%Y")
17763 .replace("SSSSSS", "%f")
17764 .replace("EEEE", "%W")
17765 .replace("MM", "%m")
17766 .replace("dd", "%d")
17767 .replace("HH", "%H")
17768 .replace("mm", "%M")
17769 .replace("ss", "%S")
17770 .replace("yy", "%y");
17771 // Second pass: handle single-char timezone patterns
17772 // z -> %Z (timezone name), Z -> %z (timezone offset)
17773 // Must be careful not to replace 'z'/'Z' inside already-replaced %Y, %M etc.
17774 let mut out = String::new();
17775 let chars: Vec<char> = result.chars().collect();
17776 let mut i = 0;
17777 while i < chars.len() {
17778 if chars[i] == '%' && i + 1 < chars.len() {
17779 // Already a format specifier, skip both chars
17780 out.push(chars[i]);
17781 out.push(chars[i + 1]);
17782 i += 2;
17783 } else if chars[i] == 'z' {
17784 out.push_str("%Z");
17785 i += 1;
17786 } else if chars[i] == 'Z' {
17787 out.push_str("%z");
17788 i += 1;
17789 } else {
17790 out.push(chars[i]);
17791 i += 1;
17792 }
17793 }
17794 out
17795 }
17796
17797 fn java_to_presto_format(fmt: &str) -> String {
17798 // Presto uses %T for HH:MM:SS
17799 let c_fmt = java_to_c_format(fmt);
17800 c_fmt.replace("%H:%M:%S", "%T")
17801 }
17802
17803 fn java_to_bq_format(fmt: &str) -> String {
17804 // BigQuery uses %F for yyyy-MM-dd and %T for HH:mm:ss
17805 let c_fmt = java_to_c_format(fmt);
17806 c_fmt.replace("%Y-%m-%d", "%F").replace("%H:%M:%S", "%T")
17807 }
17808
17809 // For Hive source, CAST string literals to appropriate type
17810 let cast_val = if is_hive_source {
17811 match &val {
17812 Expression::Literal(lit)
17813 if matches!(
17814 lit.as_ref(),
17815 crate::expressions::Literal::String(_)
17816 ) =>
17817 {
17818 match target {
17819 DialectType::DuckDB
17820 | DialectType::Presto
17821 | DialectType::Trino
17822 | DialectType::Athena => {
17823 Self::ensure_cast_timestamp(val.clone())
17824 }
17825 DialectType::BigQuery => {
17826 // BigQuery: CAST(val AS DATETIME)
17827 Expression::Cast(Box::new(
17828 crate::expressions::Cast {
17829 this: val.clone(),
17830 to: DataType::Custom {
17831 name: "DATETIME".to_string(),
17832 },
17833 trailing_comments: vec![],
17834 double_colon_syntax: false,
17835 format: None,
17836 default: None,
17837 inferred_type: None,
17838 },
17839 ))
17840 }
17841 _ => val.clone(),
17842 }
17843 }
17844 // For CAST(x AS DATE) or DATE literal, Presto needs CAST(CAST(x AS DATE) AS TIMESTAMP)
17845 Expression::Cast(c)
17846 if matches!(c.to, DataType::Date)
17847 && matches!(
17848 target,
17849 DialectType::Presto
17850 | DialectType::Trino
17851 | DialectType::Athena
17852 ) =>
17853 {
17854 Expression::Cast(Box::new(crate::expressions::Cast {
17855 this: val.clone(),
17856 to: DataType::Timestamp {
17857 timezone: false,
17858 precision: None,
17859 },
17860 trailing_comments: vec![],
17861 double_colon_syntax: false,
17862 format: None,
17863 default: None,
17864 inferred_type: None,
17865 }))
17866 }
17867 Expression::Literal(lit)
17868 if matches!(
17869 lit.as_ref(),
17870 crate::expressions::Literal::Date(_)
17871 ) && matches!(
17872 target,
17873 DialectType::Presto
17874 | DialectType::Trino
17875 | DialectType::Athena
17876 ) =>
17877 {
17878 // DATE 'x' -> CAST(CAST('x' AS DATE) AS TIMESTAMP)
17879 let cast_date = Self::date_literal_to_cast(val.clone());
17880 Expression::Cast(Box::new(crate::expressions::Cast {
17881 this: cast_date,
17882 to: DataType::Timestamp {
17883 timezone: false,
17884 precision: None,
17885 },
17886 trailing_comments: vec![],
17887 double_colon_syntax: false,
17888 format: None,
17889 default: None,
17890 inferred_type: None,
17891 }))
17892 }
17893 _ => val.clone(),
17894 }
17895 } else {
17896 val.clone()
17897 };
17898
17899 match target {
17900 DialectType::DuckDB => {
17901 if let Expression::Literal(lit) = fmt_expr {
17902 if let crate::expressions::Literal::String(s) =
17903 lit.as_ref()
17904 {
17905 let c_fmt = if is_hive_source {
17906 java_to_c_format(s)
17907 } else {
17908 s.clone()
17909 };
17910 Ok(Expression::Function(Box::new(Function::new(
17911 "STRFTIME".to_string(),
17912 vec![cast_val, Expression::string(&c_fmt)],
17913 ))))
17914 } else {
17915 Ok(Expression::Function(Box::new(Function::new(
17916 "STRFTIME".to_string(),
17917 vec![cast_val, fmt_expr.clone()],
17918 ))))
17919 }
17920 } else {
17921 Ok(Expression::Function(Box::new(Function::new(
17922 "STRFTIME".to_string(),
17923 vec![cast_val, fmt_expr.clone()],
17924 ))))
17925 }
17926 }
17927 DialectType::Presto
17928 | DialectType::Trino
17929 | DialectType::Athena => {
17930 if is_hive_source {
17931 if let Expression::Literal(lit) = fmt_expr {
17932 if let crate::expressions::Literal::String(s) =
17933 lit.as_ref()
17934 {
17935 let p_fmt = java_to_presto_format(s);
17936 Ok(Expression::Function(Box::new(
17937 Function::new(
17938 "DATE_FORMAT".to_string(),
17939 vec![
17940 cast_val,
17941 Expression::string(&p_fmt),
17942 ],
17943 ),
17944 )))
17945 } else {
17946 Ok(Expression::Function(Box::new(
17947 Function::new(
17948 "DATE_FORMAT".to_string(),
17949 vec![cast_val, fmt_expr.clone()],
17950 ),
17951 )))
17952 }
17953 } else {
17954 Ok(Expression::Function(Box::new(Function::new(
17955 "DATE_FORMAT".to_string(),
17956 vec![cast_val, fmt_expr.clone()],
17957 ))))
17958 }
17959 } else {
17960 Ok(Expression::Function(Box::new(Function::new(
17961 "DATE_FORMAT".to_string(),
17962 f.args,
17963 ))))
17964 }
17965 }
17966 DialectType::BigQuery => {
17967 // DATE_FORMAT(val, fmt) -> FORMAT_DATE(fmt, val)
17968 if let Expression::Literal(lit) = fmt_expr {
17969 if let crate::expressions::Literal::String(s) =
17970 lit.as_ref()
17971 {
17972 let bq_fmt = if is_hive_source {
17973 java_to_bq_format(s)
17974 } else {
17975 java_to_c_format(s)
17976 };
17977 Ok(Expression::Function(Box::new(Function::new(
17978 "FORMAT_DATE".to_string(),
17979 vec![Expression::string(&bq_fmt), cast_val],
17980 ))))
17981 } else {
17982 Ok(Expression::Function(Box::new(Function::new(
17983 "FORMAT_DATE".to_string(),
17984 vec![fmt_expr.clone(), cast_val],
17985 ))))
17986 }
17987 } else {
17988 Ok(Expression::Function(Box::new(Function::new(
17989 "FORMAT_DATE".to_string(),
17990 vec![fmt_expr.clone(), cast_val],
17991 ))))
17992 }
17993 }
17994 DialectType::PostgreSQL | DialectType::Redshift => {
17995 if let Expression::Literal(lit) = fmt_expr {
17996 if let crate::expressions::Literal::String(s) =
17997 lit.as_ref()
17998 {
17999 let pg_fmt = s
18000 .replace("yyyy", "YYYY")
18001 .replace("MM", "MM")
18002 .replace("dd", "DD")
18003 .replace("HH", "HH24")
18004 .replace("mm", "MI")
18005 .replace("ss", "SS")
18006 .replace("yy", "YY");
18007 Ok(Expression::Function(Box::new(Function::new(
18008 "TO_CHAR".to_string(),
18009 vec![val, Expression::string(&pg_fmt)],
18010 ))))
18011 } else {
18012 Ok(Expression::Function(Box::new(Function::new(
18013 "TO_CHAR".to_string(),
18014 vec![val, fmt_expr.clone()],
18015 ))))
18016 }
18017 } else {
18018 Ok(Expression::Function(Box::new(Function::new(
18019 "TO_CHAR".to_string(),
18020 vec![val, fmt_expr.clone()],
18021 ))))
18022 }
18023 }
18024 _ => Ok(Expression::Function(f)),
18025 }
18026 }
18027 // DATEDIFF(unit, start, end) - 3-arg form
18028 // SQLite uses DATEDIFF(date1, date2, unit_string) instead
18029 "DATEDIFF" if f.args.len() == 3 => {
18030 let mut args = f.args;
18031 // SQLite source: args = (date1, date2, unit_string)
18032 // Standard source: args = (unit, start, end)
18033 let (_arg0, arg1, arg2, unit_str) =
18034 if matches!(source, DialectType::SQLite) {
18035 let date1 = args.remove(0);
18036 let date2 = args.remove(0);
18037 let unit_expr = args.remove(0);
18038 let unit_s = Self::get_unit_str_static(&unit_expr);
18039
18040 // For SQLite target, generate JULIANDAY arithmetic directly
18041 if matches!(target, DialectType::SQLite) {
18042 let jd_first = Expression::Function(Box::new(
18043 Function::new("JULIANDAY".to_string(), vec![date1]),
18044 ));
18045 let jd_second = Expression::Function(Box::new(
18046 Function::new("JULIANDAY".to_string(), vec![date2]),
18047 ));
18048 let diff = Expression::Sub(Box::new(
18049 crate::expressions::BinaryOp::new(
18050 jd_first, jd_second,
18051 ),
18052 ));
18053 let paren_diff = Expression::Paren(Box::new(
18054 crate::expressions::Paren {
18055 this: diff,
18056 trailing_comments: Vec::new(),
18057 },
18058 ));
18059 let adjusted = match unit_s.as_str() {
18060 "HOUR" => Expression::Mul(Box::new(
18061 crate::expressions::BinaryOp::new(
18062 paren_diff,
18063 Expression::Literal(Box::new(
18064 Literal::Number("24.0".to_string()),
18065 )),
18066 ),
18067 )),
18068 "MINUTE" => Expression::Mul(Box::new(
18069 crate::expressions::BinaryOp::new(
18070 paren_diff,
18071 Expression::Literal(Box::new(
18072 Literal::Number("1440.0".to_string()),
18073 )),
18074 ),
18075 )),
18076 "SECOND" => Expression::Mul(Box::new(
18077 crate::expressions::BinaryOp::new(
18078 paren_diff,
18079 Expression::Literal(Box::new(
18080 Literal::Number("86400.0".to_string()),
18081 )),
18082 ),
18083 )),
18084 "MONTH" => Expression::Div(Box::new(
18085 crate::expressions::BinaryOp::new(
18086 paren_diff,
18087 Expression::Literal(Box::new(
18088 Literal::Number("30.0".to_string()),
18089 )),
18090 ),
18091 )),
18092 "YEAR" => Expression::Div(Box::new(
18093 crate::expressions::BinaryOp::new(
18094 paren_diff,
18095 Expression::Literal(Box::new(
18096 Literal::Number("365.0".to_string()),
18097 )),
18098 ),
18099 )),
18100 _ => paren_diff,
18101 };
18102 return Ok(Expression::Cast(Box::new(Cast {
18103 this: adjusted,
18104 to: DataType::Int {
18105 length: None,
18106 integer_spelling: true,
18107 },
18108 trailing_comments: vec![],
18109 double_colon_syntax: false,
18110 format: None,
18111 default: None,
18112 inferred_type: None,
18113 })));
18114 }
18115
18116 // For other targets, remap to standard (unit, start, end) form
18117 let unit_ident =
18118 Expression::Identifier(Identifier::new(&unit_s));
18119 (unit_ident, date1, date2, unit_s)
18120 } else {
18121 let arg0 = args.remove(0);
18122 let arg1 = args.remove(0);
18123 let arg2 = args.remove(0);
18124 let unit_s = Self::get_unit_str_static(&arg0);
18125 (arg0, arg1, arg2, unit_s)
18126 };
18127
18128 // For Hive/Spark source, string literal dates need to be cast
18129 // Note: Databricks is excluded - it handles string args like standard SQL
18130 let is_hive_spark =
18131 matches!(source, DialectType::Hive | DialectType::Spark);
18132
18133 match target {
18134 DialectType::Snowflake => {
18135 let unit =
18136 Expression::Identifier(Identifier::new(&unit_str));
18137 // Use ensure_to_date_preserved to add TO_DATE with a marker
18138 // that prevents the Snowflake TO_DATE handler from converting it to CAST
18139 let d1 = if is_hive_spark {
18140 Self::ensure_to_date_preserved(arg1)
18141 } else {
18142 arg1
18143 };
18144 let d2 = if is_hive_spark {
18145 Self::ensure_to_date_preserved(arg2)
18146 } else {
18147 arg2
18148 };
18149 Ok(Expression::Function(Box::new(Function::new(
18150 "DATEDIFF".to_string(),
18151 vec![unit, d1, d2],
18152 ))))
18153 }
18154 DialectType::Redshift => {
18155 let unit =
18156 Expression::Identifier(Identifier::new(&unit_str));
18157 let d1 = if is_hive_spark {
18158 Self::ensure_cast_date(arg1)
18159 } else {
18160 arg1
18161 };
18162 let d2 = if is_hive_spark {
18163 Self::ensure_cast_date(arg2)
18164 } else {
18165 arg2
18166 };
18167 Ok(Expression::Function(Box::new(Function::new(
18168 "DATEDIFF".to_string(),
18169 vec![unit, d1, d2],
18170 ))))
18171 }
18172 DialectType::TSQL => {
18173 let unit =
18174 Expression::Identifier(Identifier::new(&unit_str));
18175 Ok(Expression::Function(Box::new(Function::new(
18176 "DATEDIFF".to_string(),
18177 vec![unit, arg1, arg2],
18178 ))))
18179 }
18180 DialectType::DuckDB => {
18181 let is_redshift_tsql = matches!(
18182 source,
18183 DialectType::Redshift | DialectType::TSQL
18184 );
18185 if is_hive_spark {
18186 // For Hive/Spark source, CAST string args to DATE and emit DATE_DIFF directly
18187 let d1 = Self::ensure_cast_date(arg1);
18188 let d2 = Self::ensure_cast_date(arg2);
18189 Ok(Expression::Function(Box::new(Function::new(
18190 "DATE_DIFF".to_string(),
18191 vec![Expression::string(&unit_str), d1, d2],
18192 ))))
18193 } else if matches!(source, DialectType::Snowflake) {
18194 // For Snowflake source: special handling per unit
18195 match unit_str.as_str() {
18196 "NANOSECOND" => {
18197 // DATEDIFF(NANOSECOND, start, end) -> EPOCH_NS(CAST(end AS TIMESTAMP_NS)) - EPOCH_NS(CAST(start AS TIMESTAMP_NS))
18198 fn cast_to_timestamp_ns(
18199 expr: Expression,
18200 ) -> Expression
18201 {
18202 Expression::Cast(Box::new(Cast {
18203 this: expr,
18204 to: DataType::Custom {
18205 name: "TIMESTAMP_NS".to_string(),
18206 },
18207 trailing_comments: vec![],
18208 double_colon_syntax: false,
18209 format: None,
18210 default: None,
18211 inferred_type: None,
18212 }))
18213 }
18214 let epoch_end = Expression::Function(Box::new(
18215 Function::new(
18216 "EPOCH_NS".to_string(),
18217 vec![cast_to_timestamp_ns(arg2)],
18218 ),
18219 ));
18220 let epoch_start = Expression::Function(
18221 Box::new(Function::new(
18222 "EPOCH_NS".to_string(),
18223 vec![cast_to_timestamp_ns(arg1)],
18224 )),
18225 );
18226 Ok(Expression::Sub(Box::new(BinaryOp::new(
18227 epoch_end,
18228 epoch_start,
18229 ))))
18230 }
18231 "WEEK" => {
18232 // DATE_DIFF('WEEK', DATE_TRUNC('WEEK', CAST(x AS DATE)), DATE_TRUNC('WEEK', CAST(y AS DATE)))
18233 let d1 = Self::force_cast_date(arg1);
18234 let d2 = Self::force_cast_date(arg2);
18235 let dt1 = Expression::Function(Box::new(
18236 Function::new(
18237 "DATE_TRUNC".to_string(),
18238 vec![Expression::string("WEEK"), d1],
18239 ),
18240 ));
18241 let dt2 = Expression::Function(Box::new(
18242 Function::new(
18243 "DATE_TRUNC".to_string(),
18244 vec![Expression::string("WEEK"), d2],
18245 ),
18246 ));
18247 Ok(Expression::Function(Box::new(
18248 Function::new(
18249 "DATE_DIFF".to_string(),
18250 vec![
18251 Expression::string(&unit_str),
18252 dt1,
18253 dt2,
18254 ],
18255 ),
18256 )))
18257 }
18258 _ => {
18259 // YEAR, MONTH, QUARTER, DAY, etc.: CAST to DATE
18260 let d1 = Self::force_cast_date(arg1);
18261 let d2 = Self::force_cast_date(arg2);
18262 Ok(Expression::Function(Box::new(
18263 Function::new(
18264 "DATE_DIFF".to_string(),
18265 vec![
18266 Expression::string(&unit_str),
18267 d1,
18268 d2,
18269 ],
18270 ),
18271 )))
18272 }
18273 }
18274 } else if is_redshift_tsql {
18275 // For Redshift/TSQL source, CAST args to TIMESTAMP (always)
18276 let d1 = Self::force_cast_timestamp(arg1);
18277 let d2 = Self::force_cast_timestamp(arg2);
18278 Ok(Expression::Function(Box::new(Function::new(
18279 "DATE_DIFF".to_string(),
18280 vec![Expression::string(&unit_str), d1, d2],
18281 ))))
18282 } else {
18283 // Keep as DATEDIFF so DuckDB's transform_datediff handles
18284 // DATE_TRUNC for WEEK, CAST for string literals, etc.
18285 let unit =
18286 Expression::Identifier(Identifier::new(&unit_str));
18287 Ok(Expression::Function(Box::new(Function::new(
18288 "DATEDIFF".to_string(),
18289 vec![unit, arg1, arg2],
18290 ))))
18291 }
18292 }
18293 DialectType::BigQuery => {
18294 let is_redshift_tsql = matches!(
18295 source,
18296 DialectType::Redshift
18297 | DialectType::TSQL
18298 | DialectType::Snowflake
18299 );
18300 let cast_d1 = if is_hive_spark {
18301 Self::ensure_cast_date(arg1)
18302 } else if is_redshift_tsql {
18303 Self::force_cast_datetime(arg1)
18304 } else {
18305 Self::ensure_cast_datetime(arg1)
18306 };
18307 let cast_d2 = if is_hive_spark {
18308 Self::ensure_cast_date(arg2)
18309 } else if is_redshift_tsql {
18310 Self::force_cast_datetime(arg2)
18311 } else {
18312 Self::ensure_cast_datetime(arg2)
18313 };
18314 let unit =
18315 Expression::Identifier(Identifier::new(&unit_str));
18316 Ok(Expression::Function(Box::new(Function::new(
18317 "DATE_DIFF".to_string(),
18318 vec![cast_d2, cast_d1, unit],
18319 ))))
18320 }
18321 DialectType::Presto
18322 | DialectType::Trino
18323 | DialectType::Athena => {
18324 // For Hive/Spark source, string literals need double-cast: CAST(CAST(x AS TIMESTAMP) AS DATE)
18325 // For Redshift/TSQL source, args need CAST to TIMESTAMP (always)
18326 let is_redshift_tsql = matches!(
18327 source,
18328 DialectType::Redshift
18329 | DialectType::TSQL
18330 | DialectType::Snowflake
18331 );
18332 let d1 = if is_hive_spark {
18333 Self::double_cast_timestamp_date(arg1)
18334 } else if is_redshift_tsql {
18335 Self::force_cast_timestamp(arg1)
18336 } else {
18337 arg1
18338 };
18339 let d2 = if is_hive_spark {
18340 Self::double_cast_timestamp_date(arg2)
18341 } else if is_redshift_tsql {
18342 Self::force_cast_timestamp(arg2)
18343 } else {
18344 arg2
18345 };
18346 Ok(Expression::Function(Box::new(Function::new(
18347 "DATE_DIFF".to_string(),
18348 vec![Expression::string(&unit_str), d1, d2],
18349 ))))
18350 }
18351 DialectType::Hive => match unit_str.as_str() {
18352 "MONTH" => Ok(Expression::Cast(Box::new(Cast {
18353 this: Expression::Function(Box::new(Function::new(
18354 "MONTHS_BETWEEN".to_string(),
18355 vec![arg2, arg1],
18356 ))),
18357 to: DataType::Int {
18358 length: None,
18359 integer_spelling: false,
18360 },
18361 trailing_comments: vec![],
18362 double_colon_syntax: false,
18363 format: None,
18364 default: None,
18365 inferred_type: None,
18366 }))),
18367 "WEEK" => Ok(Expression::Cast(Box::new(Cast {
18368 this: Expression::Div(Box::new(
18369 crate::expressions::BinaryOp::new(
18370 Expression::Function(Box::new(Function::new(
18371 "DATEDIFF".to_string(),
18372 vec![arg2, arg1],
18373 ))),
18374 Expression::number(7),
18375 ),
18376 )),
18377 to: DataType::Int {
18378 length: None,
18379 integer_spelling: false,
18380 },
18381 trailing_comments: vec![],
18382 double_colon_syntax: false,
18383 format: None,
18384 default: None,
18385 inferred_type: None,
18386 }))),
18387 _ => Ok(Expression::Function(Box::new(Function::new(
18388 "DATEDIFF".to_string(),
18389 vec![arg2, arg1],
18390 )))),
18391 },
18392 DialectType::Spark | DialectType::Databricks => {
18393 let unit =
18394 Expression::Identifier(Identifier::new(&unit_str));
18395 Ok(Expression::Function(Box::new(Function::new(
18396 "DATEDIFF".to_string(),
18397 vec![unit, arg1, arg2],
18398 ))))
18399 }
18400 _ => {
18401 // For Hive/Spark source targeting PostgreSQL etc., cast string literals to DATE
18402 let d1 = if is_hive_spark {
18403 Self::ensure_cast_date(arg1)
18404 } else {
18405 arg1
18406 };
18407 let d2 = if is_hive_spark {
18408 Self::ensure_cast_date(arg2)
18409 } else {
18410 arg2
18411 };
18412 let unit =
18413 Expression::Identifier(Identifier::new(&unit_str));
18414 Ok(Expression::Function(Box::new(Function::new(
18415 "DATEDIFF".to_string(),
18416 vec![unit, d1, d2],
18417 ))))
18418 }
18419 }
18420 }
18421 // DATEDIFF(end, start) - 2-arg form from Hive/MySQL
18422 "DATEDIFF" if f.args.len() == 2 => {
18423 let mut args = f.args;
18424 let arg0 = args.remove(0);
18425 let arg1 = args.remove(0);
18426
18427 // Helper: unwrap TO_DATE(x) -> x (extracts inner arg)
18428 // Also recognizes TryCast/Cast to DATE that may have been produced by
18429 // cross-dialect TO_DATE -> TRY_CAST conversion
18430 let unwrap_to_date = |e: Expression| -> (Expression, bool) {
18431 if let Expression::Function(ref f) = e {
18432 if f.name.eq_ignore_ascii_case("TO_DATE")
18433 && f.args.len() == 1
18434 {
18435 return (f.args[0].clone(), true);
18436 }
18437 }
18438 // Also recognize TryCast(x, Date) as an already-converted TO_DATE
18439 if let Expression::TryCast(ref c) = e {
18440 if matches!(c.to, DataType::Date) {
18441 return (e, true); // Already properly cast, return as-is
18442 }
18443 }
18444 (e, false)
18445 };
18446
18447 match target {
18448 DialectType::DuckDB => {
18449 // For Hive source, always CAST to DATE
18450 // If arg is TO_DATE(x) or TRY_CAST(x AS DATE), use it directly
18451 let cast_d0 = if matches!(
18452 source,
18453 DialectType::Hive
18454 | DialectType::Spark
18455 | DialectType::Databricks
18456 ) {
18457 let (inner, was_to_date) = unwrap_to_date(arg1);
18458 if was_to_date {
18459 // Already a date expression, use directly
18460 if matches!(&inner, Expression::TryCast(_)) {
18461 inner // Already TRY_CAST(x AS DATE)
18462 } else {
18463 Self::try_cast_date(inner)
18464 }
18465 } else {
18466 Self::force_cast_date(inner)
18467 }
18468 } else {
18469 Self::ensure_cast_date(arg1)
18470 };
18471 let cast_d1 = if matches!(
18472 source,
18473 DialectType::Hive
18474 | DialectType::Spark
18475 | DialectType::Databricks
18476 ) {
18477 let (inner, was_to_date) = unwrap_to_date(arg0);
18478 if was_to_date {
18479 if matches!(&inner, Expression::TryCast(_)) {
18480 inner
18481 } else {
18482 Self::try_cast_date(inner)
18483 }
18484 } else {
18485 Self::force_cast_date(inner)
18486 }
18487 } else {
18488 Self::ensure_cast_date(arg0)
18489 };
18490 Ok(Expression::Function(Box::new(Function::new(
18491 "DATE_DIFF".to_string(),
18492 vec![Expression::string("DAY"), cast_d0, cast_d1],
18493 ))))
18494 }
18495 DialectType::Presto
18496 | DialectType::Trino
18497 | DialectType::Athena => {
18498 // For Hive/Spark source, apply double_cast_timestamp_date
18499 // For other sources (MySQL etc.), just swap args without casting
18500 if matches!(
18501 source,
18502 DialectType::Hive
18503 | DialectType::Spark
18504 | DialectType::Databricks
18505 ) {
18506 let cast_fn = |e: Expression| -> Expression {
18507 let (inner, was_to_date) = unwrap_to_date(e);
18508 if was_to_date {
18509 let first_cast =
18510 Self::double_cast_timestamp_date(inner);
18511 Self::double_cast_timestamp_date(first_cast)
18512 } else {
18513 Self::double_cast_timestamp_date(inner)
18514 }
18515 };
18516 Ok(Expression::Function(Box::new(Function::new(
18517 "DATE_DIFF".to_string(),
18518 vec![
18519 Expression::string("DAY"),
18520 cast_fn(arg1),
18521 cast_fn(arg0),
18522 ],
18523 ))))
18524 } else {
18525 Ok(Expression::Function(Box::new(Function::new(
18526 "DATE_DIFF".to_string(),
18527 vec![Expression::string("DAY"), arg1, arg0],
18528 ))))
18529 }
18530 }
18531 DialectType::Redshift => {
18532 let unit = Expression::Identifier(Identifier::new("DAY"));
18533 Ok(Expression::Function(Box::new(Function::new(
18534 "DATEDIFF".to_string(),
18535 vec![unit, arg1, arg0],
18536 ))))
18537 }
18538 _ => Ok(Expression::Function(Box::new(Function::new(
18539 "DATEDIFF".to_string(),
18540 vec![arg0, arg1],
18541 )))),
18542 }
18543 }
18544 // DATE_DIFF(unit, start, end) - 3-arg with string unit (ClickHouse/DuckDB style)
18545 "DATE_DIFF" if f.args.len() == 3 => {
18546 let mut args = f.args;
18547 let arg0 = args.remove(0);
18548 let arg1 = args.remove(0);
18549 let arg2 = args.remove(0);
18550 let unit_str = Self::get_unit_str_static(&arg0);
18551
18552 match target {
18553 DialectType::DuckDB => {
18554 // DuckDB: DATE_DIFF('UNIT', start, end)
18555 Ok(Expression::Function(Box::new(Function::new(
18556 "DATE_DIFF".to_string(),
18557 vec![Expression::string(&unit_str), arg1, arg2],
18558 ))))
18559 }
18560 DialectType::Presto
18561 | DialectType::Trino
18562 | DialectType::Athena => {
18563 Ok(Expression::Function(Box::new(Function::new(
18564 "DATE_DIFF".to_string(),
18565 vec![Expression::string(&unit_str), arg1, arg2],
18566 ))))
18567 }
18568 DialectType::ClickHouse => {
18569 // ClickHouse: DATE_DIFF(UNIT, start, end) - identifier unit
18570 let unit =
18571 Expression::Identifier(Identifier::new(&unit_str));
18572 Ok(Expression::Function(Box::new(Function::new(
18573 "DATE_DIFF".to_string(),
18574 vec![unit, arg1, arg2],
18575 ))))
18576 }
18577 DialectType::Snowflake | DialectType::Redshift => {
18578 let unit =
18579 Expression::Identifier(Identifier::new(&unit_str));
18580 Ok(Expression::Function(Box::new(Function::new(
18581 "DATEDIFF".to_string(),
18582 vec![unit, arg1, arg2],
18583 ))))
18584 }
18585 _ => {
18586 let unit =
18587 Expression::Identifier(Identifier::new(&unit_str));
18588 Ok(Expression::Function(Box::new(Function::new(
18589 "DATEDIFF".to_string(),
18590 vec![unit, arg1, arg2],
18591 ))))
18592 }
18593 }
18594 }
18595 // DATEADD(unit, val, date) - 3-arg form
18596 "DATEADD" if f.args.len() == 3 => {
18597 let mut args = f.args;
18598 let arg0 = args.remove(0);
18599 let arg1 = args.remove(0);
18600 let arg2 = args.remove(0);
18601 let unit_str = Self::get_unit_str_static(&arg0);
18602
18603 // Normalize TSQL unit abbreviations to standard names
18604 let unit_str = match unit_str.as_str() {
18605 "YY" | "YYYY" => "YEAR".to_string(),
18606 "QQ" | "Q" => "QUARTER".to_string(),
18607 "MM" | "M" => "MONTH".to_string(),
18608 "WK" | "WW" => "WEEK".to_string(),
18609 "DD" | "D" | "DY" => "DAY".to_string(),
18610 "HH" => "HOUR".to_string(),
18611 "MI" | "N" => "MINUTE".to_string(),
18612 "SS" | "S" => "SECOND".to_string(),
18613 "MS" => "MILLISECOND".to_string(),
18614 "MCS" | "US" => "MICROSECOND".to_string(),
18615 _ => unit_str,
18616 };
18617 match target {
18618 DialectType::Snowflake => {
18619 let unit =
18620 Expression::Identifier(Identifier::new(&unit_str));
18621 // Cast string literal to TIMESTAMP, but not for Snowflake source
18622 // (Snowflake natively accepts string literals in DATEADD)
18623 let arg2 = if matches!(
18624 &arg2,
18625 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_))
18626 ) && !matches!(source, DialectType::Snowflake)
18627 {
18628 Expression::Cast(Box::new(Cast {
18629 this: arg2,
18630 to: DataType::Timestamp {
18631 precision: None,
18632 timezone: false,
18633 },
18634 trailing_comments: Vec::new(),
18635 double_colon_syntax: false,
18636 format: None,
18637 default: None,
18638 inferred_type: None,
18639 }))
18640 } else {
18641 arg2
18642 };
18643 Ok(Expression::Function(Box::new(Function::new(
18644 "DATEADD".to_string(),
18645 vec![unit, arg1, arg2],
18646 ))))
18647 }
18648 DialectType::TSQL => {
18649 let unit =
18650 Expression::Identifier(Identifier::new(&unit_str));
18651 // Cast string literal to DATETIME2, but not when source is Spark/Databricks family
18652 let arg2 = if matches!(
18653 &arg2,
18654 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_))
18655 ) && !matches!(
18656 source,
18657 DialectType::Spark
18658 | DialectType::Databricks
18659 | DialectType::Hive
18660 ) {
18661 Expression::Cast(Box::new(Cast {
18662 this: arg2,
18663 to: DataType::Custom {
18664 name: "DATETIME2".to_string(),
18665 },
18666 trailing_comments: Vec::new(),
18667 double_colon_syntax: false,
18668 format: None,
18669 default: None,
18670 inferred_type: None,
18671 }))
18672 } else {
18673 arg2
18674 };
18675 Ok(Expression::Function(Box::new(Function::new(
18676 "DATEADD".to_string(),
18677 vec![unit, arg1, arg2],
18678 ))))
18679 }
18680 DialectType::Redshift => {
18681 let unit =
18682 Expression::Identifier(Identifier::new(&unit_str));
18683 Ok(Expression::Function(Box::new(Function::new(
18684 "DATEADD".to_string(),
18685 vec![unit, arg1, arg2],
18686 ))))
18687 }
18688 DialectType::Databricks => {
18689 let unit =
18690 Expression::Identifier(Identifier::new(&unit_str));
18691 // Sources with native DATEADD (TSQL, Databricks, Snowflake) -> DATEADD
18692 // Other sources (Redshift TsOrDsAdd, etc.) -> DATE_ADD
18693 let func_name = if matches!(
18694 source,
18695 DialectType::TSQL
18696 | DialectType::Fabric
18697 | DialectType::Databricks
18698 | DialectType::Snowflake
18699 ) {
18700 "DATEADD"
18701 } else {
18702 "DATE_ADD"
18703 };
18704 Ok(Expression::Function(Box::new(Function::new(
18705 func_name.to_string(),
18706 vec![unit, arg1, arg2],
18707 ))))
18708 }
18709 DialectType::DuckDB => {
18710 // Special handling for NANOSECOND from Snowflake
18711 if unit_str == "NANOSECOND"
18712 && matches!(source, DialectType::Snowflake)
18713 {
18714 // DATEADD(NANOSECOND, offset, ts) -> MAKE_TIMESTAMP_NS(EPOCH_NS(CAST(ts AS TIMESTAMP_NS)) + offset)
18715 let cast_ts = Expression::Cast(Box::new(Cast {
18716 this: arg2,
18717 to: DataType::Custom {
18718 name: "TIMESTAMP_NS".to_string(),
18719 },
18720 trailing_comments: vec![],
18721 double_colon_syntax: false,
18722 format: None,
18723 default: None,
18724 inferred_type: None,
18725 }));
18726 let epoch_ns =
18727 Expression::Function(Box::new(Function::new(
18728 "EPOCH_NS".to_string(),
18729 vec![cast_ts],
18730 )));
18731 let sum = Expression::Add(Box::new(BinaryOp::new(
18732 epoch_ns, arg1,
18733 )));
18734 Ok(Expression::Function(Box::new(Function::new(
18735 "MAKE_TIMESTAMP_NS".to_string(),
18736 vec![sum],
18737 ))))
18738 } else {
18739 // DuckDB: convert to date + INTERVAL syntax with CAST
18740 let iu = Self::parse_interval_unit_static(&unit_str);
18741 let interval = Expression::Interval(Box::new(crate::expressions::Interval {
18742 this: Some(arg1),
18743 unit: Some(crate::expressions::IntervalUnitSpec::Simple { unit: iu, use_plural: false }),
18744 }));
18745 // Cast string literal to TIMESTAMP
18746 let arg2 = if matches!(
18747 &arg2,
18748 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_))
18749 ) {
18750 Expression::Cast(Box::new(Cast {
18751 this: arg2,
18752 to: DataType::Timestamp {
18753 precision: None,
18754 timezone: false,
18755 },
18756 trailing_comments: Vec::new(),
18757 double_colon_syntax: false,
18758 format: None,
18759 default: None,
18760 inferred_type: None,
18761 }))
18762 } else {
18763 arg2
18764 };
18765 Ok(Expression::Add(Box::new(
18766 crate::expressions::BinaryOp::new(arg2, interval),
18767 )))
18768 }
18769 }
18770 DialectType::Spark => {
18771 // For TSQL source: convert to ADD_MONTHS/DATE_ADD(date, val)
18772 // For other sources: keep 3-arg DATE_ADD(UNIT, val, date) form
18773 if matches!(source, DialectType::TSQL | DialectType::Fabric)
18774 {
18775 fn multiply_expr_spark(
18776 expr: Expression,
18777 factor: i64,
18778 ) -> Expression
18779 {
18780 if let Expression::Literal(lit) = &expr {
18781 if let crate::expressions::Literal::Number(n) =
18782 lit.as_ref()
18783 {
18784 if let Ok(val) = n.parse::<i64>() {
18785 return Expression::Literal(Box::new(
18786 crate::expressions::Literal::Number(
18787 (val * factor).to_string(),
18788 ),
18789 ));
18790 }
18791 }
18792 }
18793 Expression::Mul(Box::new(
18794 crate::expressions::BinaryOp::new(
18795 expr,
18796 Expression::Literal(Box::new(
18797 crate::expressions::Literal::Number(
18798 factor.to_string(),
18799 ),
18800 )),
18801 ),
18802 ))
18803 }
18804 let normalized_unit = match unit_str.as_str() {
18805 "YEAR" | "YY" | "YYYY" => "YEAR",
18806 "QUARTER" | "QQ" | "Q" => "QUARTER",
18807 "MONTH" | "MM" | "M" => "MONTH",
18808 "WEEK" | "WK" | "WW" => "WEEK",
18809 "DAY" | "DD" | "D" | "DY" => "DAY",
18810 _ => &unit_str,
18811 };
18812 match normalized_unit {
18813 "YEAR" => {
18814 let months = multiply_expr_spark(arg1, 12);
18815 Ok(Expression::Function(Box::new(
18816 Function::new(
18817 "ADD_MONTHS".to_string(),
18818 vec![arg2, months],
18819 ),
18820 )))
18821 }
18822 "QUARTER" => {
18823 let months = multiply_expr_spark(arg1, 3);
18824 Ok(Expression::Function(Box::new(
18825 Function::new(
18826 "ADD_MONTHS".to_string(),
18827 vec![arg2, months],
18828 ),
18829 )))
18830 }
18831 "MONTH" => Ok(Expression::Function(Box::new(
18832 Function::new(
18833 "ADD_MONTHS".to_string(),
18834 vec![arg2, arg1],
18835 ),
18836 ))),
18837 "WEEK" => {
18838 let days = multiply_expr_spark(arg1, 7);
18839 Ok(Expression::Function(Box::new(
18840 Function::new(
18841 "DATE_ADD".to_string(),
18842 vec![arg2, days],
18843 ),
18844 )))
18845 }
18846 "DAY" => Ok(Expression::Function(Box::new(
18847 Function::new(
18848 "DATE_ADD".to_string(),
18849 vec![arg2, arg1],
18850 ),
18851 ))),
18852 _ => {
18853 let unit = Expression::Identifier(
18854 Identifier::new(&unit_str),
18855 );
18856 Ok(Expression::Function(Box::new(
18857 Function::new(
18858 "DATE_ADD".to_string(),
18859 vec![unit, arg1, arg2],
18860 ),
18861 )))
18862 }
18863 }
18864 } else {
18865 // Non-TSQL source: keep 3-arg DATE_ADD(UNIT, val, date)
18866 let unit =
18867 Expression::Identifier(Identifier::new(&unit_str));
18868 Ok(Expression::Function(Box::new(Function::new(
18869 "DATE_ADD".to_string(),
18870 vec![unit, arg1, arg2],
18871 ))))
18872 }
18873 }
18874 DialectType::Hive => match unit_str.as_str() {
18875 "MONTH" => {
18876 Ok(Expression::Function(Box::new(Function::new(
18877 "ADD_MONTHS".to_string(),
18878 vec![arg2, arg1],
18879 ))))
18880 }
18881 _ => Ok(Expression::Function(Box::new(Function::new(
18882 "DATE_ADD".to_string(),
18883 vec![arg2, arg1],
18884 )))),
18885 },
18886 DialectType::Presto
18887 | DialectType::Trino
18888 | DialectType::Athena => {
18889 // Cast string literal date to TIMESTAMP
18890 let arg2 = if matches!(
18891 &arg2,
18892 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_))
18893 ) {
18894 Expression::Cast(Box::new(Cast {
18895 this: arg2,
18896 to: DataType::Timestamp {
18897 precision: None,
18898 timezone: false,
18899 },
18900 trailing_comments: Vec::new(),
18901 double_colon_syntax: false,
18902 format: None,
18903 default: None,
18904 inferred_type: None,
18905 }))
18906 } else {
18907 arg2
18908 };
18909 Ok(Expression::Function(Box::new(Function::new(
18910 "DATE_ADD".to_string(),
18911 vec![Expression::string(&unit_str), arg1, arg2],
18912 ))))
18913 }
18914 DialectType::MySQL => {
18915 let iu = Self::parse_interval_unit_static(&unit_str);
18916 Ok(Expression::DateAdd(Box::new(
18917 crate::expressions::DateAddFunc {
18918 this: arg2,
18919 interval: arg1,
18920 unit: iu,
18921 },
18922 )))
18923 }
18924 DialectType::PostgreSQL => {
18925 // Cast string literal date to TIMESTAMP
18926 let arg2 = if matches!(
18927 &arg2,
18928 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_))
18929 ) {
18930 Expression::Cast(Box::new(Cast {
18931 this: arg2,
18932 to: DataType::Timestamp {
18933 precision: None,
18934 timezone: false,
18935 },
18936 trailing_comments: Vec::new(),
18937 double_colon_syntax: false,
18938 format: None,
18939 default: None,
18940 inferred_type: None,
18941 }))
18942 } else {
18943 arg2
18944 };
18945 let interval = Expression::Interval(Box::new(
18946 crate::expressions::Interval {
18947 this: Some(Expression::string(&format!(
18948 "{} {}",
18949 Self::expr_to_string_static(&arg1),
18950 unit_str
18951 ))),
18952 unit: None,
18953 },
18954 ));
18955 Ok(Expression::Add(Box::new(
18956 crate::expressions::BinaryOp::new(arg2, interval),
18957 )))
18958 }
18959 DialectType::BigQuery => {
18960 let iu = Self::parse_interval_unit_static(&unit_str);
18961 let interval = Expression::Interval(Box::new(
18962 crate::expressions::Interval {
18963 this: Some(arg1),
18964 unit: Some(
18965 crate::expressions::IntervalUnitSpec::Simple {
18966 unit: iu,
18967 use_plural: false,
18968 },
18969 ),
18970 },
18971 ));
18972 // Non-TSQL sources: CAST string literal to DATETIME
18973 let arg2 = if !matches!(
18974 source,
18975 DialectType::TSQL | DialectType::Fabric
18976 ) && matches!(
18977 &arg2,
18978 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_))
18979 ) {
18980 Expression::Cast(Box::new(Cast {
18981 this: arg2,
18982 to: DataType::Custom {
18983 name: "DATETIME".to_string(),
18984 },
18985 trailing_comments: Vec::new(),
18986 double_colon_syntax: false,
18987 format: None,
18988 default: None,
18989 inferred_type: None,
18990 }))
18991 } else {
18992 arg2
18993 };
18994 Ok(Expression::Function(Box::new(Function::new(
18995 "DATE_ADD".to_string(),
18996 vec![arg2, interval],
18997 ))))
18998 }
18999 _ => {
19000 let unit =
19001 Expression::Identifier(Identifier::new(&unit_str));
19002 Ok(Expression::Function(Box::new(Function::new(
19003 "DATEADD".to_string(),
19004 vec![unit, arg1, arg2],
19005 ))))
19006 }
19007 }
19008 }
19009 // DATE_ADD - 3-arg: either (unit, val, date) from Presto/ClickHouse
19010 // or (date, val, 'UNIT') from Generic canonical form
19011 "DATE_ADD" if f.args.len() == 3 => {
19012 let mut args = f.args;
19013 let arg0 = args.remove(0);
19014 let arg1 = args.remove(0);
19015 let arg2 = args.remove(0);
19016 // Detect Generic canonical form: DATE_ADD(date, amount, 'UNIT')
19017 // where arg2 is a string literal matching a unit name
19018 let arg2_unit = match &arg2 {
19019 Expression::Literal(lit)
19020 if matches!(lit.as_ref(), Literal::String(_)) =>
19021 {
19022 let Literal::String(s) = lit.as_ref() else {
19023 unreachable!()
19024 };
19025 let u = s.to_ascii_uppercase();
19026 if matches!(
19027 u.as_str(),
19028 "DAY"
19029 | "MONTH"
19030 | "YEAR"
19031 | "HOUR"
19032 | "MINUTE"
19033 | "SECOND"
19034 | "WEEK"
19035 | "QUARTER"
19036 | "MILLISECOND"
19037 | "MICROSECOND"
19038 ) {
19039 Some(u)
19040 } else {
19041 None
19042 }
19043 }
19044 _ => None,
19045 };
19046 // Reorder: if arg2 is the unit, swap to (unit, val, date) form
19047 let (unit_str, val, date) = if let Some(u) = arg2_unit {
19048 (u, arg1, arg0)
19049 } else {
19050 (Self::get_unit_str_static(&arg0), arg1, arg2)
19051 };
19052 // Alias for backward compat with the rest of the match
19053 let arg1 = val;
19054 let arg2 = date;
19055
19056 match target {
19057 DialectType::Presto
19058 | DialectType::Trino
19059 | DialectType::Athena => {
19060 Ok(Expression::Function(Box::new(Function::new(
19061 "DATE_ADD".to_string(),
19062 vec![Expression::string(&unit_str), arg1, arg2],
19063 ))))
19064 }
19065 DialectType::DuckDB => {
19066 let iu = Self::parse_interval_unit_static(&unit_str);
19067 let interval = Expression::Interval(Box::new(
19068 crate::expressions::Interval {
19069 this: Some(arg1),
19070 unit: Some(
19071 crate::expressions::IntervalUnitSpec::Simple {
19072 unit: iu,
19073 use_plural: false,
19074 },
19075 ),
19076 },
19077 ));
19078 Ok(Expression::Add(Box::new(
19079 crate::expressions::BinaryOp::new(arg2, interval),
19080 )))
19081 }
19082 DialectType::PostgreSQL
19083 | DialectType::Materialize
19084 | DialectType::RisingWave => {
19085 // PostgreSQL: x + INTERVAL '1 DAY'
19086 let amount_str = Self::expr_to_string_static(&arg1);
19087 let interval = Expression::Interval(Box::new(
19088 crate::expressions::Interval {
19089 this: Some(Expression::string(&format!(
19090 "{} {}",
19091 amount_str, unit_str
19092 ))),
19093 unit: None,
19094 },
19095 ));
19096 Ok(Expression::Add(Box::new(
19097 crate::expressions::BinaryOp::new(arg2, interval),
19098 )))
19099 }
19100 DialectType::Snowflake
19101 | DialectType::TSQL
19102 | DialectType::Redshift => {
19103 let unit =
19104 Expression::Identifier(Identifier::new(&unit_str));
19105 Ok(Expression::Function(Box::new(Function::new(
19106 "DATEADD".to_string(),
19107 vec![unit, arg1, arg2],
19108 ))))
19109 }
19110 DialectType::BigQuery
19111 | DialectType::MySQL
19112 | DialectType::Doris
19113 | DialectType::StarRocks
19114 | DialectType::Drill => {
19115 // DATE_ADD(date, INTERVAL amount UNIT)
19116 let iu = Self::parse_interval_unit_static(&unit_str);
19117 let interval = Expression::Interval(Box::new(
19118 crate::expressions::Interval {
19119 this: Some(arg1),
19120 unit: Some(
19121 crate::expressions::IntervalUnitSpec::Simple {
19122 unit: iu,
19123 use_plural: false,
19124 },
19125 ),
19126 },
19127 ));
19128 Ok(Expression::Function(Box::new(Function::new(
19129 "DATE_ADD".to_string(),
19130 vec![arg2, interval],
19131 ))))
19132 }
19133 DialectType::SQLite => {
19134 // SQLite: DATE(x, '1 DAY')
19135 // Build the string '1 DAY' from amount and unit
19136 let amount_str = match &arg1 {
19137 Expression::Literal(lit)
19138 if matches!(lit.as_ref(), Literal::Number(_)) =>
19139 {
19140 let Literal::Number(n) = lit.as_ref() else {
19141 unreachable!()
19142 };
19143 n.clone()
19144 }
19145 _ => "1".to_string(),
19146 };
19147 Ok(Expression::Function(Box::new(Function::new(
19148 "DATE".to_string(),
19149 vec![
19150 arg2,
19151 Expression::string(format!(
19152 "{} {}",
19153 amount_str, unit_str
19154 )),
19155 ],
19156 ))))
19157 }
19158 DialectType::Dremio => {
19159 // Dremio: DATE_ADD(date, amount) - drops unit
19160 Ok(Expression::Function(Box::new(Function::new(
19161 "DATE_ADD".to_string(),
19162 vec![arg2, arg1],
19163 ))))
19164 }
19165 DialectType::Spark => {
19166 // Spark: DATE_ADD(date, val) for DAY, or DATEADD(UNIT, val, date)
19167 if unit_str == "DAY" {
19168 Ok(Expression::Function(Box::new(Function::new(
19169 "DATE_ADD".to_string(),
19170 vec![arg2, arg1],
19171 ))))
19172 } else {
19173 let unit =
19174 Expression::Identifier(Identifier::new(&unit_str));
19175 Ok(Expression::Function(Box::new(Function::new(
19176 "DATE_ADD".to_string(),
19177 vec![unit, arg1, arg2],
19178 ))))
19179 }
19180 }
19181 DialectType::Databricks => {
19182 let unit =
19183 Expression::Identifier(Identifier::new(&unit_str));
19184 Ok(Expression::Function(Box::new(Function::new(
19185 "DATE_ADD".to_string(),
19186 vec![unit, arg1, arg2],
19187 ))))
19188 }
19189 DialectType::Hive => {
19190 // Hive: DATE_ADD(date, val) for DAY
19191 Ok(Expression::Function(Box::new(Function::new(
19192 "DATE_ADD".to_string(),
19193 vec![arg2, arg1],
19194 ))))
19195 }
19196 _ => {
19197 let unit =
19198 Expression::Identifier(Identifier::new(&unit_str));
19199 Ok(Expression::Function(Box::new(Function::new(
19200 "DATE_ADD".to_string(),
19201 vec![unit, arg1, arg2],
19202 ))))
19203 }
19204 }
19205 }
19206 // DATE_ADD(date, days) - 2-arg Hive/Spark/Generic form (add days)
19207 "DATE_ADD"
19208 if f.args.len() == 2
19209 && matches!(
19210 source,
19211 DialectType::Hive
19212 | DialectType::Spark
19213 | DialectType::Databricks
19214 | DialectType::Generic
19215 ) =>
19216 {
19217 let mut args = f.args;
19218 let date = args.remove(0);
19219 let days = args.remove(0);
19220 match target {
19221 DialectType::Hive | DialectType::Spark => {
19222 // Keep as DATE_ADD(date, days) for Hive/Spark
19223 Ok(Expression::Function(Box::new(Function::new(
19224 "DATE_ADD".to_string(),
19225 vec![date, days],
19226 ))))
19227 }
19228 DialectType::Databricks => Ok(Expression::Function(Box::new(
19229 Function::new("DATE_ADD".to_string(), vec![date, days]),
19230 ))),
19231 DialectType::DuckDB => {
19232 // DuckDB: CAST(date AS DATE) + INTERVAL days DAY
19233 let cast_date = Self::ensure_cast_date(date);
19234 // Wrap complex expressions (like Mul from DATE_SUB negation) in Paren
19235 let interval_val = if matches!(
19236 days,
19237 Expression::Mul(_)
19238 | Expression::Sub(_)
19239 | Expression::Add(_)
19240 ) {
19241 Expression::Paren(Box::new(crate::expressions::Paren {
19242 this: days,
19243 trailing_comments: vec![],
19244 }))
19245 } else {
19246 days
19247 };
19248 let interval = Expression::Interval(Box::new(
19249 crate::expressions::Interval {
19250 this: Some(interval_val),
19251 unit: Some(
19252 crate::expressions::IntervalUnitSpec::Simple {
19253 unit: crate::expressions::IntervalUnit::Day,
19254 use_plural: false,
19255 },
19256 ),
19257 },
19258 ));
19259 Ok(Expression::Add(Box::new(
19260 crate::expressions::BinaryOp::new(cast_date, interval),
19261 )))
19262 }
19263 DialectType::Snowflake => {
19264 // For Hive source with string literal date, use CAST(CAST(date AS TIMESTAMP) AS DATE)
19265 let cast_date = if matches!(
19266 source,
19267 DialectType::Hive
19268 | DialectType::Spark
19269 | DialectType::Databricks
19270 ) {
19271 if matches!(
19272 date,
19273 Expression::Literal(ref lit) if matches!(lit.as_ref(), Literal::String(_))
19274 ) {
19275 Self::double_cast_timestamp_date(date)
19276 } else {
19277 date
19278 }
19279 } else {
19280 date
19281 };
19282 Ok(Expression::Function(Box::new(Function::new(
19283 "DATEADD".to_string(),
19284 vec![
19285 Expression::Identifier(Identifier::new("DAY")),
19286 days,
19287 cast_date,
19288 ],
19289 ))))
19290 }
19291 DialectType::Redshift => {
19292 Ok(Expression::Function(Box::new(Function::new(
19293 "DATEADD".to_string(),
19294 vec![
19295 Expression::Identifier(Identifier::new("DAY")),
19296 days,
19297 date,
19298 ],
19299 ))))
19300 }
19301 DialectType::TSQL | DialectType::Fabric => {
19302 // For Hive source with string literal date, use CAST(CAST(date AS DATETIME2) AS DATE)
19303 // But Databricks DATE_ADD doesn't need this wrapping for TSQL
19304 let cast_date = if matches!(
19305 source,
19306 DialectType::Hive
19307 | DialectType::Spark
19308 | DialectType::Databricks
19309 ) {
19310 if matches!(
19311 date,
19312 Expression::Literal(ref lit) if matches!(lit.as_ref(), Literal::String(_))
19313 ) {
19314 Self::double_cast_datetime2_date(date)
19315 } else {
19316 date
19317 }
19318 } else {
19319 date
19320 };
19321 Ok(Expression::Function(Box::new(Function::new(
19322 "DATEADD".to_string(),
19323 vec![
19324 Expression::Identifier(Identifier::new("DAY")),
19325 days,
19326 cast_date,
19327 ],
19328 ))))
19329 }
19330 DialectType::Presto
19331 | DialectType::Trino
19332 | DialectType::Athena => {
19333 // For Hive source with string literal date, use CAST(CAST(date AS TIMESTAMP) AS DATE)
19334 let cast_date = if matches!(
19335 source,
19336 DialectType::Hive
19337 | DialectType::Spark
19338 | DialectType::Databricks
19339 ) {
19340 if matches!(
19341 date,
19342 Expression::Literal(ref lit) if matches!(lit.as_ref(), Literal::String(_))
19343 ) {
19344 Self::double_cast_timestamp_date(date)
19345 } else {
19346 date
19347 }
19348 } else {
19349 date
19350 };
19351 Ok(Expression::Function(Box::new(Function::new(
19352 "DATE_ADD".to_string(),
19353 vec![Expression::string("DAY"), days, cast_date],
19354 ))))
19355 }
19356 DialectType::BigQuery => {
19357 // For Hive/Spark source, wrap date in CAST(CAST(date AS DATETIME) AS DATE)
19358 let cast_date = if matches!(
19359 source,
19360 DialectType::Hive
19361 | DialectType::Spark
19362 | DialectType::Databricks
19363 ) {
19364 Self::double_cast_datetime_date(date)
19365 } else {
19366 date
19367 };
19368 // Wrap complex expressions in Paren for interval
19369 let interval_val = if matches!(
19370 days,
19371 Expression::Mul(_)
19372 | Expression::Sub(_)
19373 | Expression::Add(_)
19374 ) {
19375 Expression::Paren(Box::new(crate::expressions::Paren {
19376 this: days,
19377 trailing_comments: vec![],
19378 }))
19379 } else {
19380 days
19381 };
19382 let interval = Expression::Interval(Box::new(
19383 crate::expressions::Interval {
19384 this: Some(interval_val),
19385 unit: Some(
19386 crate::expressions::IntervalUnitSpec::Simple {
19387 unit: crate::expressions::IntervalUnit::Day,
19388 use_plural: false,
19389 },
19390 ),
19391 },
19392 ));
19393 Ok(Expression::Function(Box::new(Function::new(
19394 "DATE_ADD".to_string(),
19395 vec![cast_date, interval],
19396 ))))
19397 }
19398 DialectType::MySQL => {
19399 let iu = crate::expressions::IntervalUnit::Day;
19400 Ok(Expression::DateAdd(Box::new(
19401 crate::expressions::DateAddFunc {
19402 this: date,
19403 interval: days,
19404 unit: iu,
19405 },
19406 )))
19407 }
19408 DialectType::PostgreSQL => {
19409 let interval = Expression::Interval(Box::new(
19410 crate::expressions::Interval {
19411 this: Some(Expression::string(&format!(
19412 "{} DAY",
19413 Self::expr_to_string_static(&days)
19414 ))),
19415 unit: None,
19416 },
19417 ));
19418 Ok(Expression::Add(Box::new(
19419 crate::expressions::BinaryOp::new(date, interval),
19420 )))
19421 }
19422 DialectType::Doris
19423 | DialectType::StarRocks
19424 | DialectType::Drill => {
19425 // DATE_ADD(date, INTERVAL days DAY)
19426 let interval = Expression::Interval(Box::new(
19427 crate::expressions::Interval {
19428 this: Some(days),
19429 unit: Some(
19430 crate::expressions::IntervalUnitSpec::Simple {
19431 unit: crate::expressions::IntervalUnit::Day,
19432 use_plural: false,
19433 },
19434 ),
19435 },
19436 ));
19437 Ok(Expression::Function(Box::new(Function::new(
19438 "DATE_ADD".to_string(),
19439 vec![date, interval],
19440 ))))
19441 }
19442 _ => Ok(Expression::Function(Box::new(Function::new(
19443 "DATE_ADD".to_string(),
19444 vec![date, days],
19445 )))),
19446 }
19447 }
19448 // DATE_ADD(date, INTERVAL val UNIT) - MySQL 2-arg form with INTERVAL as 2nd arg
19449 "DATE_ADD"
19450 if f.args.len() == 2
19451 && matches!(
19452 source,
19453 DialectType::MySQL | DialectType::SingleStore
19454 )
19455 && matches!(&f.args[1], Expression::Interval(_)) =>
19456 {
19457 let mut args = f.args;
19458 let date = args.remove(0);
19459 let interval_expr = args.remove(0);
19460 let (val, unit) = Self::extract_interval_parts(&interval_expr)
19461 .unwrap_or_else(|| {
19462 (
19463 interval_expr.clone(),
19464 crate::expressions::IntervalUnit::Day,
19465 )
19466 });
19467 let unit_str = Self::interval_unit_to_string(&unit);
19468 let is_literal = matches!(&val,
19469 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_) | Literal::String(_))
19470 );
19471
19472 match target {
19473 DialectType::MySQL | DialectType::SingleStore => {
19474 // Keep as DATE_ADD(date, INTERVAL val UNIT)
19475 Ok(Expression::Function(Box::new(Function::new(
19476 "DATE_ADD".to_string(),
19477 vec![date, interval_expr],
19478 ))))
19479 }
19480 DialectType::PostgreSQL => {
19481 if is_literal {
19482 // Literal: date + INTERVAL 'val UNIT'
19483 let interval = Expression::Interval(Box::new(
19484 crate::expressions::Interval {
19485 this: Some(Expression::Literal(Box::new(
19486 Literal::String(format!(
19487 "{} {}",
19488 Self::expr_to_string(&val),
19489 unit_str
19490 )),
19491 ))),
19492 unit: None,
19493 },
19494 ));
19495 Ok(Expression::Add(Box::new(
19496 crate::expressions::BinaryOp::new(date, interval),
19497 )))
19498 } else {
19499 // Non-literal (column ref): date + INTERVAL '1 UNIT' * val
19500 let interval_one = Expression::Interval(Box::new(
19501 crate::expressions::Interval {
19502 this: Some(Expression::Literal(Box::new(
19503 Literal::String(format!("1 {}", unit_str)),
19504 ))),
19505 unit: None,
19506 },
19507 ));
19508 let mul = Expression::Mul(Box::new(
19509 crate::expressions::BinaryOp::new(
19510 interval_one,
19511 val,
19512 ),
19513 ));
19514 Ok(Expression::Add(Box::new(
19515 crate::expressions::BinaryOp::new(date, mul),
19516 )))
19517 }
19518 }
19519 _ => {
19520 // Default: keep as DATE_ADD(date, interval)
19521 Ok(Expression::Function(Box::new(Function::new(
19522 "DATE_ADD".to_string(),
19523 vec![date, interval_expr],
19524 ))))
19525 }
19526 }
19527 }
19528 // DATE_SUB(date, days) - 2-arg Hive/Spark form (subtract days)
19529 "DATE_SUB"
19530 if f.args.len() == 2
19531 && matches!(
19532 source,
19533 DialectType::Hive
19534 | DialectType::Spark
19535 | DialectType::Databricks
19536 ) =>
19537 {
19538 let mut args = f.args;
19539 let date = args.remove(0);
19540 let days = args.remove(0);
19541 // Helper to create days * -1
19542 let make_neg_days = |d: Expression| -> Expression {
19543 Expression::Mul(Box::new(crate::expressions::BinaryOp::new(
19544 d,
19545 Expression::Literal(Box::new(Literal::Number(
19546 "-1".to_string(),
19547 ))),
19548 )))
19549 };
19550 let is_string_literal = matches!(date, Expression::Literal(ref lit) if matches!(lit.as_ref(), Literal::String(_)));
19551 match target {
19552 DialectType::Hive
19553 | DialectType::Spark
19554 | DialectType::Databricks => {
19555 // Keep as DATE_SUB(date, days) for Hive/Spark
19556 Ok(Expression::Function(Box::new(Function::new(
19557 "DATE_SUB".to_string(),
19558 vec![date, days],
19559 ))))
19560 }
19561 DialectType::DuckDB => {
19562 let cast_date = Self::ensure_cast_date(date);
19563 let neg = make_neg_days(days);
19564 let interval = Expression::Interval(Box::new(
19565 crate::expressions::Interval {
19566 this: Some(Expression::Paren(Box::new(
19567 crate::expressions::Paren {
19568 this: neg,
19569 trailing_comments: vec![],
19570 },
19571 ))),
19572 unit: Some(
19573 crate::expressions::IntervalUnitSpec::Simple {
19574 unit: crate::expressions::IntervalUnit::Day,
19575 use_plural: false,
19576 },
19577 ),
19578 },
19579 ));
19580 Ok(Expression::Add(Box::new(
19581 crate::expressions::BinaryOp::new(cast_date, interval),
19582 )))
19583 }
19584 DialectType::Snowflake => {
19585 let cast_date = if is_string_literal {
19586 Self::double_cast_timestamp_date(date)
19587 } else {
19588 date
19589 };
19590 let neg = make_neg_days(days);
19591 Ok(Expression::Function(Box::new(Function::new(
19592 "DATEADD".to_string(),
19593 vec![
19594 Expression::Identifier(Identifier::new("DAY")),
19595 neg,
19596 cast_date,
19597 ],
19598 ))))
19599 }
19600 DialectType::Redshift => {
19601 let neg = make_neg_days(days);
19602 Ok(Expression::Function(Box::new(Function::new(
19603 "DATEADD".to_string(),
19604 vec![
19605 Expression::Identifier(Identifier::new("DAY")),
19606 neg,
19607 date,
19608 ],
19609 ))))
19610 }
19611 DialectType::TSQL | DialectType::Fabric => {
19612 let cast_date = if is_string_literal {
19613 Self::double_cast_datetime2_date(date)
19614 } else {
19615 date
19616 };
19617 let neg = make_neg_days(days);
19618 Ok(Expression::Function(Box::new(Function::new(
19619 "DATEADD".to_string(),
19620 vec![
19621 Expression::Identifier(Identifier::new("DAY")),
19622 neg,
19623 cast_date,
19624 ],
19625 ))))
19626 }
19627 DialectType::Presto
19628 | DialectType::Trino
19629 | DialectType::Athena => {
19630 let cast_date = if is_string_literal {
19631 Self::double_cast_timestamp_date(date)
19632 } else {
19633 date
19634 };
19635 let neg = make_neg_days(days);
19636 Ok(Expression::Function(Box::new(Function::new(
19637 "DATE_ADD".to_string(),
19638 vec![Expression::string("DAY"), neg, cast_date],
19639 ))))
19640 }
19641 DialectType::BigQuery => {
19642 let cast_date = if is_string_literal {
19643 Self::double_cast_datetime_date(date)
19644 } else {
19645 date
19646 };
19647 let neg = make_neg_days(days);
19648 let interval = Expression::Interval(Box::new(
19649 crate::expressions::Interval {
19650 this: Some(Expression::Paren(Box::new(
19651 crate::expressions::Paren {
19652 this: neg,
19653 trailing_comments: vec![],
19654 },
19655 ))),
19656 unit: Some(
19657 crate::expressions::IntervalUnitSpec::Simple {
19658 unit: crate::expressions::IntervalUnit::Day,
19659 use_plural: false,
19660 },
19661 ),
19662 },
19663 ));
19664 Ok(Expression::Function(Box::new(Function::new(
19665 "DATE_ADD".to_string(),
19666 vec![cast_date, interval],
19667 ))))
19668 }
19669 _ => Ok(Expression::Function(Box::new(Function::new(
19670 "DATE_SUB".to_string(),
19671 vec![date, days],
19672 )))),
19673 }
19674 }
19675 // ADD_MONTHS(date, val) -> target-specific
19676 "ADD_MONTHS" if f.args.len() == 2 => {
19677 let mut args = f.args;
19678 let date = args.remove(0);
19679 let val = args.remove(0);
19680 match target {
19681 DialectType::TSQL => {
19682 let cast_date = Self::ensure_cast_datetime2(date);
19683 Ok(Expression::Function(Box::new(Function::new(
19684 "DATEADD".to_string(),
19685 vec![
19686 Expression::Identifier(Identifier::new("MONTH")),
19687 val,
19688 cast_date,
19689 ],
19690 ))))
19691 }
19692 DialectType::DuckDB => {
19693 let interval = Expression::Interval(Box::new(
19694 crate::expressions::Interval {
19695 this: Some(val),
19696 unit: Some(
19697 crate::expressions::IntervalUnitSpec::Simple {
19698 unit:
19699 crate::expressions::IntervalUnit::Month,
19700 use_plural: false,
19701 },
19702 ),
19703 },
19704 ));
19705 Ok(Expression::Add(Box::new(
19706 crate::expressions::BinaryOp::new(date, interval),
19707 )))
19708 }
19709 DialectType::Snowflake => {
19710 // Keep ADD_MONTHS when source is Snowflake
19711 if matches!(source, DialectType::Snowflake) {
19712 Ok(Expression::Function(Box::new(Function::new(
19713 "ADD_MONTHS".to_string(),
19714 vec![date, val],
19715 ))))
19716 } else {
19717 Ok(Expression::Function(Box::new(Function::new(
19718 "DATEADD".to_string(),
19719 vec![
19720 Expression::Identifier(Identifier::new(
19721 "MONTH",
19722 )),
19723 val,
19724 date,
19725 ],
19726 ))))
19727 }
19728 }
19729 DialectType::Redshift => {
19730 Ok(Expression::Function(Box::new(Function::new(
19731 "DATEADD".to_string(),
19732 vec![
19733 Expression::Identifier(Identifier::new("MONTH")),
19734 val,
19735 date,
19736 ],
19737 ))))
19738 }
19739 DialectType::Presto
19740 | DialectType::Trino
19741 | DialectType::Athena => {
19742 Ok(Expression::Function(Box::new(Function::new(
19743 "DATE_ADD".to_string(),
19744 vec![Expression::string("MONTH"), val, date],
19745 ))))
19746 }
19747 DialectType::BigQuery => {
19748 let interval = Expression::Interval(Box::new(
19749 crate::expressions::Interval {
19750 this: Some(val),
19751 unit: Some(
19752 crate::expressions::IntervalUnitSpec::Simple {
19753 unit:
19754 crate::expressions::IntervalUnit::Month,
19755 use_plural: false,
19756 },
19757 ),
19758 },
19759 ));
19760 Ok(Expression::Function(Box::new(Function::new(
19761 "DATE_ADD".to_string(),
19762 vec![date, interval],
19763 ))))
19764 }
19765 _ => Ok(Expression::Function(Box::new(Function::new(
19766 "ADD_MONTHS".to_string(),
19767 vec![date, val],
19768 )))),
19769 }
19770 }
19771 // DATETRUNC(unit, date) - TSQL form -> DATE_TRUNC for other targets
19772 "DATETRUNC" if f.args.len() == 2 => {
19773 let mut args = f.args;
19774 let arg0 = args.remove(0);
19775 let arg1 = args.remove(0);
19776 let unit_str = Self::get_unit_str_static(&arg0);
19777 match target {
19778 DialectType::TSQL | DialectType::Fabric => {
19779 // Keep as DATETRUNC for TSQL - the target handler will uppercase the unit
19780 Ok(Expression::Function(Box::new(Function::new(
19781 "DATETRUNC".to_string(),
19782 vec![
19783 Expression::Identifier(Identifier::new(&unit_str)),
19784 arg1,
19785 ],
19786 ))))
19787 }
19788 DialectType::DuckDB => {
19789 // DuckDB: DATE_TRUNC('UNIT', expr) with CAST for string literals
19790 let date = Self::ensure_cast_timestamp(arg1);
19791 Ok(Expression::Function(Box::new(Function::new(
19792 "DATE_TRUNC".to_string(),
19793 vec![Expression::string(&unit_str), date],
19794 ))))
19795 }
19796 DialectType::ClickHouse => {
19797 // ClickHouse: dateTrunc('UNIT', expr)
19798 Ok(Expression::Function(Box::new(Function::new(
19799 "dateTrunc".to_string(),
19800 vec![Expression::string(&unit_str), arg1],
19801 ))))
19802 }
19803 _ => {
19804 // Standard: DATE_TRUNC('UNIT', expr)
19805 let unit = Expression::string(&unit_str);
19806 Ok(Expression::Function(Box::new(Function::new(
19807 "DATE_TRUNC".to_string(),
19808 vec![unit, arg1],
19809 ))))
19810 }
19811 }
19812 }
19813 // GETDATE() -> CURRENT_TIMESTAMP for non-TSQL targets
19814 "GETDATE" if f.args.is_empty() => match target {
19815 DialectType::TSQL => Ok(Expression::Function(f)),
19816 DialectType::Redshift => Ok(Expression::Function(Box::new(
19817 Function::new("GETDATE".to_string(), vec![]),
19818 ))),
19819 _ => Ok(Expression::CurrentTimestamp(
19820 crate::expressions::CurrentTimestamp {
19821 precision: None,
19822 sysdate: false,
19823 },
19824 )),
19825 },
19826 // TO_HEX(x) / HEX(x) -> target-specific hex function
19827 "TO_HEX" | "HEX" if f.args.len() == 1 => {
19828 let name = match target {
19829 DialectType::Presto | DialectType::Trino => "TO_HEX",
19830 DialectType::Spark
19831 | DialectType::Databricks
19832 | DialectType::Hive => "HEX",
19833 DialectType::DuckDB
19834 | DialectType::PostgreSQL
19835 | DialectType::Redshift => "TO_HEX",
19836 _ => &f.name,
19837 };
19838 Ok(Expression::Function(Box::new(Function::new(
19839 name.to_string(),
19840 f.args,
19841 ))))
19842 }
19843 // FROM_HEX(x) / UNHEX(x) -> target-specific hex decode function
19844 "FROM_HEX" | "UNHEX" if f.args.len() == 1 => {
19845 match target {
19846 DialectType::BigQuery => {
19847 // BigQuery: UNHEX(x) -> FROM_HEX(x)
19848 // Special case: UNHEX(MD5(x)) -> FROM_HEX(TO_HEX(MD5(x)))
19849 // because BigQuery MD5 returns BYTES, not hex string
19850 let arg = &f.args[0];
19851 let wrapped_arg = match arg {
19852 Expression::Function(inner_f)
19853 if inner_f.name.eq_ignore_ascii_case("MD5")
19854 || inner_f
19855 .name
19856 .eq_ignore_ascii_case("SHA1")
19857 || inner_f
19858 .name
19859 .eq_ignore_ascii_case("SHA256")
19860 || inner_f
19861 .name
19862 .eq_ignore_ascii_case("SHA512") =>
19863 {
19864 // Wrap hash function in TO_HEX for BigQuery
19865 Expression::Function(Box::new(Function::new(
19866 "TO_HEX".to_string(),
19867 vec![arg.clone()],
19868 )))
19869 }
19870 _ => f.args.into_iter().next().unwrap(),
19871 };
19872 Ok(Expression::Function(Box::new(Function::new(
19873 "FROM_HEX".to_string(),
19874 vec![wrapped_arg],
19875 ))))
19876 }
19877 _ => {
19878 let name = match target {
19879 DialectType::Presto | DialectType::Trino => "FROM_HEX",
19880 DialectType::Spark
19881 | DialectType::Databricks
19882 | DialectType::Hive => "UNHEX",
19883 _ => &f.name,
19884 };
19885 Ok(Expression::Function(Box::new(Function::new(
19886 name.to_string(),
19887 f.args,
19888 ))))
19889 }
19890 }
19891 }
19892 // TO_UTF8(x) -> ENCODE(x, 'utf-8') for Spark
19893 "TO_UTF8" if f.args.len() == 1 => match target {
19894 DialectType::Spark | DialectType::Databricks => {
19895 let mut args = f.args;
19896 args.push(Expression::string("utf-8"));
19897 Ok(Expression::Function(Box::new(Function::new(
19898 "ENCODE".to_string(),
19899 args,
19900 ))))
19901 }
19902 _ => Ok(Expression::Function(f)),
19903 },
19904 // FROM_UTF8(x) -> DECODE(x, 'utf-8') for Spark
19905 "FROM_UTF8" if f.args.len() == 1 => match target {
19906 DialectType::Spark | DialectType::Databricks => {
19907 let mut args = f.args;
19908 args.push(Expression::string("utf-8"));
19909 Ok(Expression::Function(Box::new(Function::new(
19910 "DECODE".to_string(),
19911 args,
19912 ))))
19913 }
19914 _ => Ok(Expression::Function(f)),
19915 },
19916 // STARTS_WITH(x, y) / STARTSWITH(x, y) -> target-specific
19917 "STARTS_WITH" | "STARTSWITH" if f.args.len() == 2 => {
19918 let name = match target {
19919 DialectType::Spark | DialectType::Databricks => "STARTSWITH",
19920 DialectType::Presto | DialectType::Trino => "STARTS_WITH",
19921 DialectType::PostgreSQL | DialectType::Redshift => {
19922 "STARTS_WITH"
19923 }
19924 _ => &f.name,
19925 };
19926 Ok(Expression::Function(Box::new(Function::new(
19927 name.to_string(),
19928 f.args,
19929 ))))
19930 }
19931 // APPROX_COUNT_DISTINCT(x) <-> APPROX_DISTINCT(x)
19932 "APPROX_COUNT_DISTINCT" if f.args.len() >= 1 => {
19933 let name = match target {
19934 DialectType::Presto
19935 | DialectType::Trino
19936 | DialectType::Athena => "APPROX_DISTINCT",
19937 _ => "APPROX_COUNT_DISTINCT",
19938 };
19939 Ok(Expression::Function(Box::new(Function::new(
19940 name.to_string(),
19941 f.args,
19942 ))))
19943 }
19944 // JSON_EXTRACT -> GET_JSON_OBJECT for Spark/Hive
19945 "JSON_EXTRACT"
19946 if f.args.len() == 2
19947 && !matches!(source, DialectType::BigQuery)
19948 && matches!(
19949 target,
19950 DialectType::Spark
19951 | DialectType::Databricks
19952 | DialectType::Hive
19953 ) =>
19954 {
19955 Ok(Expression::Function(Box::new(Function::new(
19956 "GET_JSON_OBJECT".to_string(),
19957 f.args,
19958 ))))
19959 }
19960 // JSON_EXTRACT(x, path) -> x -> path for SQLite (arrow syntax)
19961 "JSON_EXTRACT"
19962 if f.args.len() == 2 && matches!(target, DialectType::SQLite) =>
19963 {
19964 let mut args = f.args;
19965 let path = args.remove(1);
19966 let this = args.remove(0);
19967 Ok(Expression::JsonExtract(Box::new(
19968 crate::expressions::JsonExtractFunc {
19969 this,
19970 path,
19971 returning: None,
19972 arrow_syntax: true,
19973 hash_arrow_syntax: false,
19974 wrapper_option: None,
19975 quotes_option: None,
19976 on_scalar_string: false,
19977 on_error: None,
19978 },
19979 )))
19980 }
19981 // JSON_FORMAT(x) -> TO_JSON(x) for Spark, TO_JSON_STRING for BigQuery, CAST(TO_JSON(x) AS TEXT) for DuckDB
19982 "JSON_FORMAT" if f.args.len() == 1 => {
19983 match target {
19984 DialectType::Spark | DialectType::Databricks => {
19985 // Presto JSON_FORMAT(JSON '...') needs Spark's string-unquoting flow:
19986 // REGEXP_EXTRACT(TO_JSON(FROM_JSON('[...]', SCHEMA_OF_JSON('[...]'))), '^.(.*).$', 1)
19987 if matches!(
19988 source,
19989 DialectType::Presto
19990 | DialectType::Trino
19991 | DialectType::Athena
19992 ) {
19993 if let Some(Expression::ParseJson(pj)) = f.args.first()
19994 {
19995 if let Expression::Literal(lit) = &pj.this {
19996 if let Literal::String(s) = lit.as_ref() {
19997 let wrapped =
19998 Expression::Literal(Box::new(
19999 Literal::String(format!("[{}]", s)),
20000 ));
20001 let schema_of_json = Expression::Function(
20002 Box::new(Function::new(
20003 "SCHEMA_OF_JSON".to_string(),
20004 vec![wrapped.clone()],
20005 )),
20006 );
20007 let from_json = Expression::Function(
20008 Box::new(Function::new(
20009 "FROM_JSON".to_string(),
20010 vec![wrapped, schema_of_json],
20011 )),
20012 );
20013 let to_json = Expression::Function(
20014 Box::new(Function::new(
20015 "TO_JSON".to_string(),
20016 vec![from_json],
20017 )),
20018 );
20019 return Ok(Expression::Function(Box::new(
20020 Function::new(
20021 "REGEXP_EXTRACT".to_string(),
20022 vec![
20023 to_json,
20024 Expression::Literal(Box::new(
20025 Literal::String(
20026 "^.(.*).$".to_string(),
20027 ),
20028 )),
20029 Expression::Literal(Box::new(
20030 Literal::Number(
20031 "1".to_string(),
20032 ),
20033 )),
20034 ],
20035 ),
20036 )));
20037 }
20038 }
20039 }
20040 }
20041
20042 // Strip inner CAST(... AS JSON) or TO_JSON() if present
20043 // The CastToJsonForSpark may have already converted CAST(x AS JSON) to TO_JSON(x)
20044 let mut args = f.args;
20045 if let Some(Expression::Cast(ref c)) = args.first() {
20046 if matches!(&c.to, DataType::Json | DataType::JsonB) {
20047 args = vec![c.this.clone()];
20048 }
20049 } else if let Some(Expression::Function(ref inner_f)) =
20050 args.first()
20051 {
20052 if inner_f.name.eq_ignore_ascii_case("TO_JSON")
20053 && inner_f.args.len() == 1
20054 {
20055 // Already TO_JSON(x) from CastToJsonForSpark, just use the inner arg
20056 args = inner_f.args.clone();
20057 }
20058 }
20059 Ok(Expression::Function(Box::new(Function::new(
20060 "TO_JSON".to_string(),
20061 args,
20062 ))))
20063 }
20064 DialectType::BigQuery => Ok(Expression::Function(Box::new(
20065 Function::new("TO_JSON_STRING".to_string(), f.args),
20066 ))),
20067 DialectType::DuckDB => {
20068 // CAST(TO_JSON(x) AS TEXT)
20069 let to_json = Expression::Function(Box::new(
20070 Function::new("TO_JSON".to_string(), f.args),
20071 ));
20072 Ok(Expression::Cast(Box::new(Cast {
20073 this: to_json,
20074 to: DataType::Text,
20075 trailing_comments: Vec::new(),
20076 double_colon_syntax: false,
20077 format: None,
20078 default: None,
20079 inferred_type: None,
20080 })))
20081 }
20082 _ => Ok(Expression::Function(f)),
20083 }
20084 }
20085 // SYSDATE -> CURRENT_TIMESTAMP for non-Oracle/Redshift/Snowflake targets
20086 "SYSDATE" if f.args.is_empty() => {
20087 match target {
20088 DialectType::Oracle | DialectType::Redshift => {
20089 Ok(Expression::Function(f))
20090 }
20091 DialectType::Snowflake => {
20092 // Snowflake uses SYSDATE() with parens
20093 let mut f = *f;
20094 f.no_parens = false;
20095 Ok(Expression::Function(Box::new(f)))
20096 }
20097 DialectType::DuckDB => {
20098 // DuckDB: SYSDATE() -> CURRENT_TIMESTAMP AT TIME ZONE 'UTC'
20099 Ok(Expression::AtTimeZone(Box::new(
20100 crate::expressions::AtTimeZone {
20101 this: Expression::CurrentTimestamp(
20102 crate::expressions::CurrentTimestamp {
20103 precision: None,
20104 sysdate: false,
20105 },
20106 ),
20107 zone: Expression::Literal(Box::new(
20108 Literal::String("UTC".to_string()),
20109 )),
20110 },
20111 )))
20112 }
20113 _ => Ok(Expression::CurrentTimestamp(
20114 crate::expressions::CurrentTimestamp {
20115 precision: None,
20116 sysdate: true,
20117 },
20118 )),
20119 }
20120 }
20121 // LOGICAL_OR(x) -> BOOL_OR(x)
20122 "LOGICAL_OR" if f.args.len() == 1 => {
20123 let name = match target {
20124 DialectType::Spark | DialectType::Databricks => "BOOL_OR",
20125 _ => &f.name,
20126 };
20127 Ok(Expression::Function(Box::new(Function::new(
20128 name.to_string(),
20129 f.args,
20130 ))))
20131 }
20132 // LOGICAL_AND(x) -> BOOL_AND(x)
20133 "LOGICAL_AND" if f.args.len() == 1 => {
20134 let name = match target {
20135 DialectType::Spark | DialectType::Databricks => "BOOL_AND",
20136 _ => &f.name,
20137 };
20138 Ok(Expression::Function(Box::new(Function::new(
20139 name.to_string(),
20140 f.args,
20141 ))))
20142 }
20143 // MONTHS_ADD(d, n) -> ADD_MONTHS(d, n) for Oracle
20144 "MONTHS_ADD" if f.args.len() == 2 => match target {
20145 DialectType::Oracle => Ok(Expression::Function(Box::new(
20146 Function::new("ADD_MONTHS".to_string(), f.args),
20147 ))),
20148 _ => Ok(Expression::Function(f)),
20149 },
20150 // ARRAY_JOIN(arr, sep[, null_replacement]) -> target-specific
20151 "ARRAY_JOIN" if f.args.len() >= 2 => {
20152 match target {
20153 DialectType::Spark | DialectType::Databricks => {
20154 // Keep as ARRAY_JOIN for Spark (it supports null_replacement)
20155 Ok(Expression::Function(f))
20156 }
20157 DialectType::Hive => {
20158 // ARRAY_JOIN(arr, sep[, null_rep]) -> CONCAT_WS(sep, arr) (drop null_replacement)
20159 let mut args = f.args;
20160 let arr = args.remove(0);
20161 let sep = args.remove(0);
20162 // Drop any remaining args (null_replacement)
20163 Ok(Expression::Function(Box::new(Function::new(
20164 "CONCAT_WS".to_string(),
20165 vec![sep, arr],
20166 ))))
20167 }
20168 DialectType::Presto | DialectType::Trino => {
20169 Ok(Expression::Function(f))
20170 }
20171 _ => Ok(Expression::Function(f)),
20172 }
20173 }
20174 // LOCATE(substr, str, pos) 3-arg -> target-specific
20175 // For Presto/DuckDB: STRPOS doesn't support 3-arg, need complex expansion
20176 "LOCATE"
20177 if f.args.len() == 3
20178 && matches!(
20179 target,
20180 DialectType::Presto
20181 | DialectType::Trino
20182 | DialectType::Athena
20183 | DialectType::DuckDB
20184 ) =>
20185 {
20186 let mut args = f.args;
20187 let substr = args.remove(0);
20188 let string = args.remove(0);
20189 let pos = args.remove(0);
20190 // STRPOS(SUBSTRING(string, pos), substr)
20191 let substring_call = Expression::Function(Box::new(Function::new(
20192 "SUBSTRING".to_string(),
20193 vec![string.clone(), pos.clone()],
20194 )));
20195 let strpos_call = Expression::Function(Box::new(Function::new(
20196 "STRPOS".to_string(),
20197 vec![substring_call, substr.clone()],
20198 )));
20199 // STRPOS(...) + pos - 1
20200 let pos_adjusted =
20201 Expression::Sub(Box::new(crate::expressions::BinaryOp::new(
20202 Expression::Add(Box::new(
20203 crate::expressions::BinaryOp::new(
20204 strpos_call.clone(),
20205 pos.clone(),
20206 ),
20207 )),
20208 Expression::number(1),
20209 )));
20210 // STRPOS(...) = 0
20211 let is_zero =
20212 Expression::Eq(Box::new(crate::expressions::BinaryOp::new(
20213 strpos_call.clone(),
20214 Expression::number(0),
20215 )));
20216
20217 match target {
20218 DialectType::Presto
20219 | DialectType::Trino
20220 | DialectType::Athena => {
20221 // IF(STRPOS(...) = 0, 0, STRPOS(...) + pos - 1)
20222 Ok(Expression::Function(Box::new(Function::new(
20223 "IF".to_string(),
20224 vec![is_zero, Expression::number(0), pos_adjusted],
20225 ))))
20226 }
20227 DialectType::DuckDB => {
20228 // CASE WHEN STRPOS(...) = 0 THEN 0 ELSE STRPOS(...) + pos - 1 END
20229 Ok(Expression::Case(Box::new(crate::expressions::Case {
20230 operand: None,
20231 whens: vec![(is_zero, Expression::number(0))],
20232 else_: Some(pos_adjusted),
20233 comments: Vec::new(),
20234 inferred_type: None,
20235 })))
20236 }
20237 _ => Ok(Expression::Function(Box::new(Function::new(
20238 "LOCATE".to_string(),
20239 vec![substr, string, pos],
20240 )))),
20241 }
20242 }
20243 // STRPOS(haystack, needle, occurrence) 3-arg -> INSTR(haystack, needle, 1, occurrence)
20244 "STRPOS"
20245 if f.args.len() == 3
20246 && matches!(
20247 target,
20248 DialectType::BigQuery
20249 | DialectType::Oracle
20250 | DialectType::Teradata
20251 ) =>
20252 {
20253 let mut args = f.args;
20254 let haystack = args.remove(0);
20255 let needle = args.remove(0);
20256 let occurrence = args.remove(0);
20257 Ok(Expression::Function(Box::new(Function::new(
20258 "INSTR".to_string(),
20259 vec![haystack, needle, Expression::number(1), occurrence],
20260 ))))
20261 }
20262 // SCHEMA_NAME(id) -> target-specific
20263 "SCHEMA_NAME" if f.args.len() <= 1 => match target {
20264 DialectType::MySQL | DialectType::SingleStore => {
20265 Ok(Expression::Function(Box::new(Function::new(
20266 "SCHEMA".to_string(),
20267 vec![],
20268 ))))
20269 }
20270 DialectType::PostgreSQL => Ok(Expression::CurrentSchema(Box::new(
20271 crate::expressions::CurrentSchema { this: None },
20272 ))),
20273 DialectType::SQLite => Ok(Expression::string("main")),
20274 _ => Ok(Expression::Function(f)),
20275 },
20276 // STRTOL(str, base) -> FROM_BASE(str, base) for Trino/Presto
20277 "STRTOL" if f.args.len() == 2 => match target {
20278 DialectType::Presto | DialectType::Trino => {
20279 Ok(Expression::Function(Box::new(Function::new(
20280 "FROM_BASE".to_string(),
20281 f.args,
20282 ))))
20283 }
20284 _ => Ok(Expression::Function(f)),
20285 },
20286 // EDITDIST3(a, b) -> LEVENSHTEIN(a, b) for Spark
20287 "EDITDIST3" if f.args.len() == 2 => match target {
20288 DialectType::Spark | DialectType::Databricks => {
20289 Ok(Expression::Function(Box::new(Function::new(
20290 "LEVENSHTEIN".to_string(),
20291 f.args,
20292 ))))
20293 }
20294 _ => Ok(Expression::Function(f)),
20295 },
20296 // FORMAT(num, decimals) from MySQL -> DuckDB FORMAT('{:,.Xf}', num)
20297 "FORMAT"
20298 if f.args.len() == 2
20299 && matches!(
20300 source,
20301 DialectType::MySQL | DialectType::SingleStore
20302 )
20303 && matches!(target, DialectType::DuckDB) =>
20304 {
20305 let mut args = f.args;
20306 let num_expr = args.remove(0);
20307 let decimals_expr = args.remove(0);
20308 // Extract decimal count
20309 let dec_count = match &decimals_expr {
20310 Expression::Literal(lit)
20311 if matches!(lit.as_ref(), Literal::Number(_)) =>
20312 {
20313 let Literal::Number(n) = lit.as_ref() else {
20314 unreachable!()
20315 };
20316 n.clone()
20317 }
20318 _ => "0".to_string(),
20319 };
20320 let fmt_str = format!("{{:,.{}f}}", dec_count);
20321 Ok(Expression::Function(Box::new(Function::new(
20322 "FORMAT".to_string(),
20323 vec![Expression::string(&fmt_str), num_expr],
20324 ))))
20325 }
20326 // FORMAT(x, fmt) from TSQL -> DATE_FORMAT for Spark, or expand short codes
20327 "FORMAT"
20328 if f.args.len() == 2
20329 && matches!(
20330 source,
20331 DialectType::TSQL | DialectType::Fabric
20332 ) =>
20333 {
20334 let val_expr = f.args[0].clone();
20335 let fmt_expr = f.args[1].clone();
20336 // Expand unambiguous .NET single-char date format shortcodes to full patterns.
20337 // Only expand shortcodes that are NOT also valid numeric format specifiers.
20338 // Ambiguous: d, D, f, F, g, G (used for both dates and numbers)
20339 // Unambiguous date: m/M (Month day), t/T (Time), y/Y (Year month)
20340 let (expanded_fmt, is_shortcode) = match &fmt_expr {
20341 Expression::Literal(lit)
20342 if matches!(
20343 lit.as_ref(),
20344 crate::expressions::Literal::String(_)
20345 ) =>
20346 {
20347 let crate::expressions::Literal::String(s) = lit.as_ref()
20348 else {
20349 unreachable!()
20350 };
20351 match s.as_str() {
20352 "m" | "M" => (Expression::string("MMMM d"), true),
20353 "t" => (Expression::string("h:mm tt"), true),
20354 "T" => (Expression::string("h:mm:ss tt"), true),
20355 "y" | "Y" => (Expression::string("MMMM yyyy"), true),
20356 _ => (fmt_expr.clone(), false),
20357 }
20358 }
20359 _ => (fmt_expr.clone(), false),
20360 };
20361 // Check if the format looks like a date format
20362 let is_date_format = is_shortcode
20363 || match &expanded_fmt {
20364 Expression::Literal(lit)
20365 if matches!(
20366 lit.as_ref(),
20367 crate::expressions::Literal::String(_)
20368 ) =>
20369 {
20370 let crate::expressions::Literal::String(s) =
20371 lit.as_ref()
20372 else {
20373 unreachable!()
20374 };
20375 // Date formats typically contain yyyy, MM, dd, MMMM, HH, etc.
20376 s.contains("yyyy")
20377 || s.contains("YYYY")
20378 || s.contains("MM")
20379 || s.contains("dd")
20380 || s.contains("MMMM")
20381 || s.contains("HH")
20382 || s.contains("hh")
20383 || s.contains("ss")
20384 }
20385 _ => false,
20386 };
20387 match target {
20388 DialectType::Spark | DialectType::Databricks => {
20389 let func_name = if is_date_format {
20390 "DATE_FORMAT"
20391 } else {
20392 "FORMAT_NUMBER"
20393 };
20394 Ok(Expression::Function(Box::new(Function::new(
20395 func_name.to_string(),
20396 vec![val_expr, expanded_fmt],
20397 ))))
20398 }
20399 _ => {
20400 // For TSQL and other targets, expand shortcodes but keep FORMAT
20401 if is_shortcode {
20402 Ok(Expression::Function(Box::new(Function::new(
20403 "FORMAT".to_string(),
20404 vec![val_expr, expanded_fmt],
20405 ))))
20406 } else {
20407 Ok(Expression::Function(f))
20408 }
20409 }
20410 }
20411 }
20412 // FORMAT('%s', x) from Trino/Presto -> target-specific
20413 "FORMAT"
20414 if f.args.len() >= 2
20415 && matches!(
20416 source,
20417 DialectType::Trino
20418 | DialectType::Presto
20419 | DialectType::Athena
20420 ) =>
20421 {
20422 let fmt_expr = f.args[0].clone();
20423 let value_args: Vec<Expression> = f.args[1..].to_vec();
20424 match target {
20425 // DuckDB: replace %s with {} in format string
20426 DialectType::DuckDB => {
20427 let new_fmt = match &fmt_expr {
20428 Expression::Literal(lit)
20429 if matches!(lit.as_ref(), Literal::String(_)) =>
20430 {
20431 let Literal::String(s) = lit.as_ref() else {
20432 unreachable!()
20433 };
20434 Expression::Literal(Box::new(Literal::String(
20435 s.replace("%s", "{}"),
20436 )))
20437 }
20438 _ => fmt_expr,
20439 };
20440 let mut args = vec![new_fmt];
20441 args.extend(value_args);
20442 Ok(Expression::Function(Box::new(Function::new(
20443 "FORMAT".to_string(),
20444 args,
20445 ))))
20446 }
20447 // Snowflake: FORMAT('%s', x) -> TO_CHAR(x) when just %s
20448 DialectType::Snowflake => match &fmt_expr {
20449 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(s) if s == "%s" && value_args.len() == 1) =>
20450 {
20451 let Literal::String(_) = lit.as_ref() else {
20452 unreachable!()
20453 };
20454 Ok(Expression::Function(Box::new(Function::new(
20455 "TO_CHAR".to_string(),
20456 value_args,
20457 ))))
20458 }
20459 _ => Ok(Expression::Function(f)),
20460 },
20461 // Default: keep FORMAT as-is
20462 _ => Ok(Expression::Function(f)),
20463 }
20464 }
20465 // LIST_CONTAINS / LIST_HAS / ARRAY_CONTAINS -> target-specific
20466 "LIST_CONTAINS" | "LIST_HAS" | "ARRAY_CONTAINS"
20467 if f.args.len() == 2 =>
20468 {
20469 // When coming from Snowflake source: ARRAY_CONTAINS(value, array)
20470 // args[0]=value, args[1]=array. For DuckDB target, swap and add NULL-aware CASE.
20471 if matches!(target, DialectType::DuckDB)
20472 && matches!(source, DialectType::Snowflake)
20473 && f.name.eq_ignore_ascii_case("ARRAY_CONTAINS")
20474 {
20475 let value = f.args[0].clone();
20476 let array = f.args[1].clone();
20477
20478 // value IS NULL
20479 let value_is_null =
20480 Expression::IsNull(Box::new(crate::expressions::IsNull {
20481 this: value.clone(),
20482 not: false,
20483 postfix_form: false,
20484 }));
20485
20486 // ARRAY_LENGTH(array)
20487 let array_length =
20488 Expression::Function(Box::new(Function::new(
20489 "ARRAY_LENGTH".to_string(),
20490 vec![array.clone()],
20491 )));
20492 // LIST_COUNT(array)
20493 let list_count = Expression::Function(Box::new(Function::new(
20494 "LIST_COUNT".to_string(),
20495 vec![array.clone()],
20496 )));
20497 // ARRAY_LENGTH(array) <> LIST_COUNT(array)
20498 let neq =
20499 Expression::Neq(Box::new(crate::expressions::BinaryOp {
20500 left: array_length,
20501 right: list_count,
20502 left_comments: vec![],
20503 operator_comments: vec![],
20504 trailing_comments: vec![],
20505 inferred_type: None,
20506 }));
20507 // NULLIF(ARRAY_LENGTH(array) <> LIST_COUNT(array), FALSE)
20508 let nullif =
20509 Expression::Nullif(Box::new(crate::expressions::Nullif {
20510 this: Box::new(neq),
20511 expression: Box::new(Expression::Boolean(
20512 crate::expressions::BooleanLiteral { value: false },
20513 )),
20514 }));
20515
20516 // ARRAY_CONTAINS(array, value) - DuckDB syntax: array first, value second
20517 let array_contains =
20518 Expression::Function(Box::new(Function::new(
20519 "ARRAY_CONTAINS".to_string(),
20520 vec![array, value],
20521 )));
20522
20523 // CASE WHEN value IS NULL THEN NULLIF(...) ELSE ARRAY_CONTAINS(array, value) END
20524 return Ok(Expression::Case(Box::new(Case {
20525 operand: None,
20526 whens: vec![(value_is_null, nullif)],
20527 else_: Some(array_contains),
20528 comments: Vec::new(),
20529 inferred_type: None,
20530 })));
20531 }
20532 match target {
20533 DialectType::PostgreSQL | DialectType::Redshift => {
20534 // CASE WHEN needle IS NULL THEN NULL ELSE COALESCE(needle = ANY(arr), FALSE) END
20535 let arr = f.args[0].clone();
20536 let needle = f.args[1].clone();
20537 // Convert [] to ARRAY[] for PostgreSQL
20538 let pg_arr = match arr {
20539 Expression::Array(a) => Expression::ArrayFunc(
20540 Box::new(crate::expressions::ArrayConstructor {
20541 expressions: a.expressions,
20542 bracket_notation: false,
20543 use_list_keyword: false,
20544 }),
20545 ),
20546 _ => arr,
20547 };
20548 // needle = ANY(arr) using the Any quantified expression
20549 let any_expr = Expression::Any(Box::new(
20550 crate::expressions::QuantifiedExpr {
20551 this: needle.clone(),
20552 subquery: pg_arr,
20553 op: Some(crate::expressions::QuantifiedOp::Eq),
20554 },
20555 ));
20556 let coalesce = Expression::Coalesce(Box::new(
20557 crate::expressions::VarArgFunc {
20558 expressions: vec![
20559 any_expr,
20560 Expression::Boolean(
20561 crate::expressions::BooleanLiteral {
20562 value: false,
20563 },
20564 ),
20565 ],
20566 original_name: None,
20567 inferred_type: None,
20568 },
20569 ));
20570 let is_null_check = Expression::IsNull(Box::new(
20571 crate::expressions::IsNull {
20572 this: needle,
20573 not: false,
20574 postfix_form: false,
20575 },
20576 ));
20577 Ok(Expression::Case(Box::new(Case {
20578 operand: None,
20579 whens: vec![(
20580 is_null_check,
20581 Expression::Null(crate::expressions::Null),
20582 )],
20583 else_: Some(coalesce),
20584 comments: Vec::new(),
20585 inferred_type: None,
20586 })))
20587 }
20588 _ => Ok(Expression::Function(Box::new(Function::new(
20589 "ARRAY_CONTAINS".to_string(),
20590 f.args,
20591 )))),
20592 }
20593 }
20594 // LIST_HAS_ANY / ARRAY_HAS_ANY -> target-specific overlap operator
20595 "LIST_HAS_ANY" | "ARRAY_HAS_ANY" if f.args.len() == 2 => {
20596 match target {
20597 DialectType::PostgreSQL | DialectType::Redshift => {
20598 // arr1 && arr2 with ARRAY[] syntax
20599 let mut args = f.args;
20600 let arr1 = args.remove(0);
20601 let arr2 = args.remove(0);
20602 let pg_arr1 = match arr1 {
20603 Expression::Array(a) => Expression::ArrayFunc(
20604 Box::new(crate::expressions::ArrayConstructor {
20605 expressions: a.expressions,
20606 bracket_notation: false,
20607 use_list_keyword: false,
20608 }),
20609 ),
20610 _ => arr1,
20611 };
20612 let pg_arr2 = match arr2 {
20613 Expression::Array(a) => Expression::ArrayFunc(
20614 Box::new(crate::expressions::ArrayConstructor {
20615 expressions: a.expressions,
20616 bracket_notation: false,
20617 use_list_keyword: false,
20618 }),
20619 ),
20620 _ => arr2,
20621 };
20622 Ok(Expression::ArrayOverlaps(Box::new(BinaryOp::new(
20623 pg_arr1, pg_arr2,
20624 ))))
20625 }
20626 DialectType::DuckDB => {
20627 // DuckDB: arr1 && arr2 (native support)
20628 let mut args = f.args;
20629 let arr1 = args.remove(0);
20630 let arr2 = args.remove(0);
20631 Ok(Expression::ArrayOverlaps(Box::new(BinaryOp::new(
20632 arr1, arr2,
20633 ))))
20634 }
20635 _ => Ok(Expression::Function(Box::new(Function::new(
20636 "LIST_HAS_ANY".to_string(),
20637 f.args,
20638 )))),
20639 }
20640 }
20641 // APPROX_QUANTILE(x, q) -> target-specific
20642 "APPROX_QUANTILE" if f.args.len() == 2 => match target {
20643 DialectType::Snowflake => Ok(Expression::Function(Box::new(
20644 Function::new("APPROX_PERCENTILE".to_string(), f.args),
20645 ))),
20646 DialectType::DuckDB => Ok(Expression::Function(f)),
20647 _ => Ok(Expression::Function(f)),
20648 },
20649 // MAKE_DATE(y, m, d) -> DATE(y, m, d) for BigQuery
20650 "MAKE_DATE" if f.args.len() == 3 => match target {
20651 DialectType::BigQuery => Ok(Expression::Function(Box::new(
20652 Function::new("DATE".to_string(), f.args),
20653 ))),
20654 _ => Ok(Expression::Function(f)),
20655 },
20656 // RANGE(start, end[, step]) -> target-specific
20657 "RANGE"
20658 if f.args.len() >= 2 && !matches!(target, DialectType::DuckDB) =>
20659 {
20660 let start = f.args[0].clone();
20661 let end = f.args[1].clone();
20662 let step = f.args.get(2).cloned();
20663 match target {
20664 // Snowflake ARRAY_GENERATE_RANGE uses exclusive end (same as DuckDB RANGE),
20665 // so just rename without adjusting the end argument.
20666 DialectType::Snowflake => {
20667 let mut args = vec![start, end];
20668 if let Some(s) = step {
20669 args.push(s);
20670 }
20671 Ok(Expression::Function(Box::new(Function::new(
20672 "ARRAY_GENERATE_RANGE".to_string(),
20673 args,
20674 ))))
20675 }
20676 DialectType::Spark | DialectType::Databricks => {
20677 // RANGE(start, end) -> SEQUENCE(start, end-1)
20678 // RANGE(start, end, step) -> SEQUENCE(start, end-step, step) when step constant
20679 // RANGE(start, start) -> ARRAY() (empty)
20680 // RANGE(start, end, 0) -> ARRAY() (empty)
20681 // When end is variable: IF((end - 1) <= start, ARRAY(), SEQUENCE(start, (end - 1)))
20682
20683 // Check for constant args
20684 fn extract_i64(e: &Expression) -> Option<i64> {
20685 match e {
20686 Expression::Literal(lit)
20687 if matches!(
20688 lit.as_ref(),
20689 Literal::Number(_)
20690 ) =>
20691 {
20692 let Literal::Number(n) = lit.as_ref() else {
20693 unreachable!()
20694 };
20695 n.parse::<i64>().ok()
20696 }
20697 Expression::Neg(u) => {
20698 if let Expression::Literal(lit) = &u.this {
20699 if let Literal::Number(n) = lit.as_ref() {
20700 n.parse::<i64>().ok().map(|v| -v)
20701 } else {
20702 None
20703 }
20704 } else {
20705 None
20706 }
20707 }
20708 _ => None,
20709 }
20710 }
20711 let start_val = extract_i64(&start);
20712 let end_val = extract_i64(&end);
20713 let step_val = step.as_ref().and_then(|s| extract_i64(s));
20714
20715 // Check for RANGE(x, x) or RANGE(x, y, 0) -> empty array
20716 if step_val == Some(0) {
20717 return Ok(Expression::Function(Box::new(
20718 Function::new("ARRAY".to_string(), vec![]),
20719 )));
20720 }
20721 if let (Some(s), Some(e_val)) = (start_val, end_val) {
20722 if s == e_val {
20723 return Ok(Expression::Function(Box::new(
20724 Function::new("ARRAY".to_string(), vec![]),
20725 )));
20726 }
20727 }
20728
20729 if let (Some(_s_val), Some(e_val)) = (start_val, end_val) {
20730 // All constants - compute new end = end - step (if step provided) or end - 1
20731 match step_val {
20732 Some(st) if st < 0 => {
20733 // Negative step: SEQUENCE(start, end - step, step)
20734 let new_end = e_val - st; // end - step (= end + |step|)
20735 let mut args =
20736 vec![start, Expression::number(new_end)];
20737 if let Some(s) = step {
20738 args.push(s);
20739 }
20740 Ok(Expression::Function(Box::new(
20741 Function::new("SEQUENCE".to_string(), args),
20742 )))
20743 }
20744 Some(st) => {
20745 let new_end = e_val - st;
20746 let mut args =
20747 vec![start, Expression::number(new_end)];
20748 if let Some(s) = step {
20749 args.push(s);
20750 }
20751 Ok(Expression::Function(Box::new(
20752 Function::new("SEQUENCE".to_string(), args),
20753 )))
20754 }
20755 None => {
20756 // No step: SEQUENCE(start, end - 1)
20757 let new_end = e_val - 1;
20758 Ok(Expression::Function(Box::new(
20759 Function::new(
20760 "SEQUENCE".to_string(),
20761 vec![
20762 start,
20763 Expression::number(new_end),
20764 ],
20765 ),
20766 )))
20767 }
20768 }
20769 } else {
20770 // Variable end: IF((end - 1) < start, ARRAY(), SEQUENCE(start, (end - 1)))
20771 let end_m1 = Expression::Sub(Box::new(BinaryOp::new(
20772 end.clone(),
20773 Expression::number(1),
20774 )));
20775 let cond = Expression::Lt(Box::new(BinaryOp::new(
20776 Expression::Paren(Box::new(Paren {
20777 this: end_m1.clone(),
20778 trailing_comments: Vec::new(),
20779 })),
20780 start.clone(),
20781 )));
20782 let empty = Expression::Function(Box::new(
20783 Function::new("ARRAY".to_string(), vec![]),
20784 ));
20785 let mut seq_args = vec![
20786 start,
20787 Expression::Paren(Box::new(Paren {
20788 this: end_m1,
20789 trailing_comments: Vec::new(),
20790 })),
20791 ];
20792 if let Some(s) = step {
20793 seq_args.push(s);
20794 }
20795 let seq = Expression::Function(Box::new(
20796 Function::new("SEQUENCE".to_string(), seq_args),
20797 ));
20798 Ok(Expression::IfFunc(Box::new(
20799 crate::expressions::IfFunc {
20800 condition: cond,
20801 true_value: empty,
20802 false_value: Some(seq),
20803 original_name: None,
20804 inferred_type: None,
20805 },
20806 )))
20807 }
20808 }
20809 DialectType::SQLite => {
20810 // RANGE(start, end) -> GENERATE_SERIES(start, end)
20811 // The subquery wrapping is handled at the Alias level
20812 let mut args = vec![start, end];
20813 if let Some(s) = step {
20814 args.push(s);
20815 }
20816 Ok(Expression::Function(Box::new(Function::new(
20817 "GENERATE_SERIES".to_string(),
20818 args,
20819 ))))
20820 }
20821 _ => Ok(Expression::Function(f)),
20822 }
20823 }
20824 // ARRAY_REVERSE_SORT -> target-specific
20825 // (handled above as well, but also need DuckDB self-normalization)
20826 // MAP_FROM_ARRAYS(keys, values) -> target-specific map construction
20827 "MAP_FROM_ARRAYS" if f.args.len() == 2 => match target {
20828 DialectType::Snowflake => Ok(Expression::Function(Box::new(
20829 Function::new("OBJECT_CONSTRUCT".to_string(), f.args),
20830 ))),
20831 DialectType::Spark | DialectType::Databricks => {
20832 Ok(Expression::Function(Box::new(Function::new(
20833 "MAP_FROM_ARRAYS".to_string(),
20834 f.args,
20835 ))))
20836 }
20837 _ => Ok(Expression::Function(Box::new(Function::new(
20838 "MAP".to_string(),
20839 f.args,
20840 )))),
20841 },
20842 // VARIANCE(x) -> varSamp(x) for ClickHouse
20843 "VARIANCE" if f.args.len() == 1 => match target {
20844 DialectType::ClickHouse => Ok(Expression::Function(Box::new(
20845 Function::new("varSamp".to_string(), f.args),
20846 ))),
20847 _ => Ok(Expression::Function(f)),
20848 },
20849 // STDDEV(x) -> stddevSamp(x) for ClickHouse
20850 "STDDEV" if f.args.len() == 1 => match target {
20851 DialectType::ClickHouse => Ok(Expression::Function(Box::new(
20852 Function::new("stddevSamp".to_string(), f.args),
20853 ))),
20854 _ => Ok(Expression::Function(f)),
20855 },
20856 // ISINF(x) -> IS_INF(x) for BigQuery
20857 "ISINF" if f.args.len() == 1 => match target {
20858 DialectType::BigQuery => Ok(Expression::Function(Box::new(
20859 Function::new("IS_INF".to_string(), f.args),
20860 ))),
20861 _ => Ok(Expression::Function(f)),
20862 },
20863 // CONTAINS(arr, x) -> ARRAY_CONTAINS(arr, x) for Spark/Hive
20864 "CONTAINS" if f.args.len() == 2 => match target {
20865 DialectType::Spark
20866 | DialectType::Databricks
20867 | DialectType::Hive => Ok(Expression::Function(Box::new(
20868 Function::new("ARRAY_CONTAINS".to_string(), f.args),
20869 ))),
20870 _ => Ok(Expression::Function(f)),
20871 },
20872 // ARRAY_CONTAINS(arr, x) -> CONTAINS(arr, x) for Presto
20873 "ARRAY_CONTAINS" if f.args.len() == 2 => match target {
20874 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
20875 Ok(Expression::Function(Box::new(Function::new(
20876 "CONTAINS".to_string(),
20877 f.args,
20878 ))))
20879 }
20880 DialectType::DuckDB => Ok(Expression::Function(Box::new(
20881 Function::new("ARRAY_CONTAINS".to_string(), f.args),
20882 ))),
20883 _ => Ok(Expression::Function(f)),
20884 },
20885 // TO_UNIXTIME(x) -> UNIX_TIMESTAMP(x) for Hive/Spark
20886 "TO_UNIXTIME" if f.args.len() == 1 => match target {
20887 DialectType::Hive
20888 | DialectType::Spark
20889 | DialectType::Databricks => Ok(Expression::Function(Box::new(
20890 Function::new("UNIX_TIMESTAMP".to_string(), f.args),
20891 ))),
20892 _ => Ok(Expression::Function(f)),
20893 },
20894 // FROM_UNIXTIME(x) -> target-specific
20895 "FROM_UNIXTIME" if f.args.len() == 1 => {
20896 match target {
20897 DialectType::Hive
20898 | DialectType::Spark
20899 | DialectType::Databricks
20900 | DialectType::Presto
20901 | DialectType::Trino => Ok(Expression::Function(f)),
20902 DialectType::DuckDB => {
20903 // DuckDB: TO_TIMESTAMP(x)
20904 let arg = f.args.into_iter().next().unwrap();
20905 Ok(Expression::Function(Box::new(Function::new(
20906 "TO_TIMESTAMP".to_string(),
20907 vec![arg],
20908 ))))
20909 }
20910 DialectType::PostgreSQL => {
20911 // PG: TO_TIMESTAMP(col)
20912 let arg = f.args.into_iter().next().unwrap();
20913 Ok(Expression::Function(Box::new(Function::new(
20914 "TO_TIMESTAMP".to_string(),
20915 vec![arg],
20916 ))))
20917 }
20918 DialectType::Redshift => {
20919 // Redshift: (TIMESTAMP 'epoch' + col * INTERVAL '1 SECOND')
20920 let arg = f.args.into_iter().next().unwrap();
20921 let epoch_ts = Expression::Literal(Box::new(
20922 Literal::Timestamp("epoch".to_string()),
20923 ));
20924 let interval = Expression::Interval(Box::new(
20925 crate::expressions::Interval {
20926 this: Some(Expression::string("1 SECOND")),
20927 unit: None,
20928 },
20929 ));
20930 let mul =
20931 Expression::Mul(Box::new(BinaryOp::new(arg, interval)));
20932 let add =
20933 Expression::Add(Box::new(BinaryOp::new(epoch_ts, mul)));
20934 Ok(Expression::Paren(Box::new(crate::expressions::Paren {
20935 this: add,
20936 trailing_comments: Vec::new(),
20937 })))
20938 }
20939 _ => Ok(Expression::Function(f)),
20940 }
20941 }
20942 // FROM_UNIXTIME(x, fmt) with 2 args from Hive/Spark -> target-specific
20943 "FROM_UNIXTIME"
20944 if f.args.len() == 2
20945 && matches!(
20946 source,
20947 DialectType::Hive
20948 | DialectType::Spark
20949 | DialectType::Databricks
20950 ) =>
20951 {
20952 let mut args = f.args;
20953 let unix_ts = args.remove(0);
20954 let fmt_expr = args.remove(0);
20955 match target {
20956 DialectType::DuckDB => {
20957 // DuckDB: STRFTIME(TO_TIMESTAMP(x), c_fmt)
20958 let to_ts = Expression::Function(Box::new(Function::new(
20959 "TO_TIMESTAMP".to_string(),
20960 vec![unix_ts],
20961 )));
20962 if let Expression::Literal(lit) = &fmt_expr {
20963 if let crate::expressions::Literal::String(s) =
20964 lit.as_ref()
20965 {
20966 let c_fmt = Self::hive_format_to_c_format(s);
20967 Ok(Expression::Function(Box::new(Function::new(
20968 "STRFTIME".to_string(),
20969 vec![to_ts, Expression::string(&c_fmt)],
20970 ))))
20971 } else {
20972 Ok(Expression::Function(Box::new(Function::new(
20973 "STRFTIME".to_string(),
20974 vec![to_ts, fmt_expr],
20975 ))))
20976 }
20977 } else {
20978 Ok(Expression::Function(Box::new(Function::new(
20979 "STRFTIME".to_string(),
20980 vec![to_ts, fmt_expr],
20981 ))))
20982 }
20983 }
20984 DialectType::Presto
20985 | DialectType::Trino
20986 | DialectType::Athena => {
20987 // Presto: DATE_FORMAT(FROM_UNIXTIME(x), presto_fmt)
20988 let from_unix =
20989 Expression::Function(Box::new(Function::new(
20990 "FROM_UNIXTIME".to_string(),
20991 vec![unix_ts],
20992 )));
20993 if let Expression::Literal(lit) = &fmt_expr {
20994 if let crate::expressions::Literal::String(s) =
20995 lit.as_ref()
20996 {
20997 let p_fmt = Self::hive_format_to_presto_format(s);
20998 Ok(Expression::Function(Box::new(Function::new(
20999 "DATE_FORMAT".to_string(),
21000 vec![from_unix, Expression::string(&p_fmt)],
21001 ))))
21002 } else {
21003 Ok(Expression::Function(Box::new(Function::new(
21004 "DATE_FORMAT".to_string(),
21005 vec![from_unix, fmt_expr],
21006 ))))
21007 }
21008 } else {
21009 Ok(Expression::Function(Box::new(Function::new(
21010 "DATE_FORMAT".to_string(),
21011 vec![from_unix, fmt_expr],
21012 ))))
21013 }
21014 }
21015 _ => {
21016 // Keep as FROM_UNIXTIME(x, fmt) for other targets
21017 Ok(Expression::Function(Box::new(Function::new(
21018 "FROM_UNIXTIME".to_string(),
21019 vec![unix_ts, fmt_expr],
21020 ))))
21021 }
21022 }
21023 }
21024 // DATEPART(unit, expr) -> EXTRACT(unit FROM expr) for Spark
21025 "DATEPART" | "DATE_PART" if f.args.len() == 2 => {
21026 let unit_str = Self::get_unit_str_static(&f.args[0]);
21027 // Get the raw unit text preserving original case
21028 let raw_unit = match &f.args[0] {
21029 Expression::Identifier(id) => id.name.clone(),
21030 Expression::Var(v) => v.this.clone(),
21031 Expression::Literal(lit)
21032 if matches!(
21033 lit.as_ref(),
21034 crate::expressions::Literal::String(_)
21035 ) =>
21036 {
21037 let crate::expressions::Literal::String(s) = lit.as_ref()
21038 else {
21039 unreachable!()
21040 };
21041 s.clone()
21042 }
21043 Expression::Column(col) => col.name.name.clone(),
21044 _ => unit_str.clone(),
21045 };
21046 match target {
21047 DialectType::TSQL | DialectType::Fabric => {
21048 // Preserve original case of unit for TSQL
21049 let unit_name = match unit_str.as_str() {
21050 "YY" | "YYYY" => "YEAR".to_string(),
21051 "QQ" | "Q" => "QUARTER".to_string(),
21052 "MM" | "M" => "MONTH".to_string(),
21053 "WK" | "WW" => "WEEK".to_string(),
21054 "DD" | "D" | "DY" => "DAY".to_string(),
21055 "HH" => "HOUR".to_string(),
21056 "MI" | "N" => "MINUTE".to_string(),
21057 "SS" | "S" => "SECOND".to_string(),
21058 _ => raw_unit.clone(), // preserve original case
21059 };
21060 let mut args = f.args;
21061 args[0] =
21062 Expression::Identifier(Identifier::new(&unit_name));
21063 Ok(Expression::Function(Box::new(Function::new(
21064 "DATEPART".to_string(),
21065 args,
21066 ))))
21067 }
21068 DialectType::Spark | DialectType::Databricks => {
21069 // DATEPART(unit, expr) -> EXTRACT(unit FROM expr)
21070 // Preserve original case for non-abbreviation units
21071 let unit = match unit_str.as_str() {
21072 "YY" | "YYYY" => "YEAR".to_string(),
21073 "QQ" | "Q" => "QUARTER".to_string(),
21074 "MM" | "M" => "MONTH".to_string(),
21075 "WK" | "WW" => "WEEK".to_string(),
21076 "DD" | "D" | "DY" => "DAY".to_string(),
21077 "HH" => "HOUR".to_string(),
21078 "MI" | "N" => "MINUTE".to_string(),
21079 "SS" | "S" => "SECOND".to_string(),
21080 _ => raw_unit, // preserve original case
21081 };
21082 Ok(Expression::Extract(Box::new(
21083 crate::expressions::ExtractFunc {
21084 this: f.args[1].clone(),
21085 field: crate::expressions::DateTimeField::Custom(
21086 unit,
21087 ),
21088 },
21089 )))
21090 }
21091 _ => Ok(Expression::Function(Box::new(Function::new(
21092 "DATE_PART".to_string(),
21093 f.args,
21094 )))),
21095 }
21096 }
21097 // DATENAME(mm, date) -> FORMAT(CAST(date AS DATETIME2), 'MMMM') for TSQL
21098 // DATENAME(dw, date) -> FORMAT(CAST(date AS DATETIME2), 'dddd') for TSQL
21099 // DATENAME(mm, date) -> DATE_FORMAT(CAST(date AS TIMESTAMP), 'MMMM') for Spark
21100 // DATENAME(dw, date) -> DATE_FORMAT(CAST(date AS TIMESTAMP), 'EEEE') for Spark
21101 "DATENAME" if f.args.len() == 2 => {
21102 let unit_str = Self::get_unit_str_static(&f.args[0]);
21103 let date_expr = f.args[1].clone();
21104 match unit_str.as_str() {
21105 "MM" | "M" | "MONTH" => match target {
21106 DialectType::TSQL => {
21107 let cast_date = Expression::Cast(Box::new(
21108 crate::expressions::Cast {
21109 this: date_expr,
21110 to: DataType::Custom {
21111 name: "DATETIME2".to_string(),
21112 },
21113 trailing_comments: Vec::new(),
21114 double_colon_syntax: false,
21115 format: None,
21116 default: None,
21117 inferred_type: None,
21118 },
21119 ));
21120 Ok(Expression::Function(Box::new(Function::new(
21121 "FORMAT".to_string(),
21122 vec![cast_date, Expression::string("MMMM")],
21123 ))))
21124 }
21125 DialectType::Spark | DialectType::Databricks => {
21126 let cast_date = Expression::Cast(Box::new(
21127 crate::expressions::Cast {
21128 this: date_expr,
21129 to: DataType::Timestamp {
21130 timezone: false,
21131 precision: None,
21132 },
21133 trailing_comments: Vec::new(),
21134 double_colon_syntax: false,
21135 format: None,
21136 default: None,
21137 inferred_type: None,
21138 },
21139 ));
21140 Ok(Expression::Function(Box::new(Function::new(
21141 "DATE_FORMAT".to_string(),
21142 vec![cast_date, Expression::string("MMMM")],
21143 ))))
21144 }
21145 _ => Ok(Expression::Function(f)),
21146 },
21147 "DW" | "WEEKDAY" => match target {
21148 DialectType::TSQL => {
21149 let cast_date = Expression::Cast(Box::new(
21150 crate::expressions::Cast {
21151 this: date_expr,
21152 to: DataType::Custom {
21153 name: "DATETIME2".to_string(),
21154 },
21155 trailing_comments: Vec::new(),
21156 double_colon_syntax: false,
21157 format: None,
21158 default: None,
21159 inferred_type: None,
21160 },
21161 ));
21162 Ok(Expression::Function(Box::new(Function::new(
21163 "FORMAT".to_string(),
21164 vec![cast_date, Expression::string("dddd")],
21165 ))))
21166 }
21167 DialectType::Spark | DialectType::Databricks => {
21168 let cast_date = Expression::Cast(Box::new(
21169 crate::expressions::Cast {
21170 this: date_expr,
21171 to: DataType::Timestamp {
21172 timezone: false,
21173 precision: None,
21174 },
21175 trailing_comments: Vec::new(),
21176 double_colon_syntax: false,
21177 format: None,
21178 default: None,
21179 inferred_type: None,
21180 },
21181 ));
21182 Ok(Expression::Function(Box::new(Function::new(
21183 "DATE_FORMAT".to_string(),
21184 vec![cast_date, Expression::string("EEEE")],
21185 ))))
21186 }
21187 _ => Ok(Expression::Function(f)),
21188 },
21189 _ => Ok(Expression::Function(f)),
21190 }
21191 }
21192 // STRING_AGG(x, sep) without WITHIN GROUP -> target-specific
21193 "STRING_AGG" if f.args.len() >= 2 => {
21194 let x = f.args[0].clone();
21195 let sep = f.args[1].clone();
21196 match target {
21197 DialectType::MySQL
21198 | DialectType::SingleStore
21199 | DialectType::Doris
21200 | DialectType::StarRocks => Ok(Expression::GroupConcat(
21201 Box::new(crate::expressions::GroupConcatFunc {
21202 this: x,
21203 separator: Some(sep),
21204 order_by: None,
21205 distinct: false,
21206 filter: None,
21207 limit: None,
21208 inferred_type: None,
21209 }),
21210 )),
21211 DialectType::SQLite => Ok(Expression::GroupConcat(Box::new(
21212 crate::expressions::GroupConcatFunc {
21213 this: x,
21214 separator: Some(sep),
21215 order_by: None,
21216 distinct: false,
21217 filter: None,
21218 limit: None,
21219 inferred_type: None,
21220 },
21221 ))),
21222 DialectType::PostgreSQL | DialectType::Redshift => {
21223 Ok(Expression::StringAgg(Box::new(
21224 crate::expressions::StringAggFunc {
21225 this: x,
21226 separator: Some(sep),
21227 order_by: None,
21228 distinct: false,
21229 filter: None,
21230 limit: None,
21231 inferred_type: None,
21232 },
21233 )))
21234 }
21235 _ => Ok(Expression::Function(f)),
21236 }
21237 }
21238 "TRY_DIVIDE" if f.args.len() == 2 => {
21239 let mut args = f.args;
21240 let x = args.remove(0);
21241 let y = args.remove(0);
21242 match target {
21243 DialectType::Spark | DialectType::Databricks => {
21244 Ok(Expression::Function(Box::new(Function::new(
21245 "TRY_DIVIDE".to_string(),
21246 vec![x, y],
21247 ))))
21248 }
21249 DialectType::Snowflake => {
21250 let y_ref = match &y {
21251 Expression::Column(_)
21252 | Expression::Literal(_)
21253 | Expression::Identifier(_) => y.clone(),
21254 _ => Expression::Paren(Box::new(Paren {
21255 this: y.clone(),
21256 trailing_comments: vec![],
21257 })),
21258 };
21259 let x_ref = match &x {
21260 Expression::Column(_)
21261 | Expression::Literal(_)
21262 | Expression::Identifier(_) => x.clone(),
21263 _ => Expression::Paren(Box::new(Paren {
21264 this: x.clone(),
21265 trailing_comments: vec![],
21266 })),
21267 };
21268 let condition = Expression::Neq(Box::new(
21269 crate::expressions::BinaryOp::new(
21270 y_ref.clone(),
21271 Expression::number(0),
21272 ),
21273 ));
21274 let div_expr = Expression::Div(Box::new(
21275 crate::expressions::BinaryOp::new(x_ref, y_ref),
21276 ));
21277 Ok(Expression::IfFunc(Box::new(
21278 crate::expressions::IfFunc {
21279 condition,
21280 true_value: div_expr,
21281 false_value: Some(Expression::Null(Null)),
21282 original_name: Some("IFF".to_string()),
21283 inferred_type: None,
21284 },
21285 )))
21286 }
21287 DialectType::DuckDB => {
21288 let y_ref = match &y {
21289 Expression::Column(_)
21290 | Expression::Literal(_)
21291 | Expression::Identifier(_) => y.clone(),
21292 _ => Expression::Paren(Box::new(Paren {
21293 this: y.clone(),
21294 trailing_comments: vec![],
21295 })),
21296 };
21297 let x_ref = match &x {
21298 Expression::Column(_)
21299 | Expression::Literal(_)
21300 | Expression::Identifier(_) => x.clone(),
21301 _ => Expression::Paren(Box::new(Paren {
21302 this: x.clone(),
21303 trailing_comments: vec![],
21304 })),
21305 };
21306 let condition = Expression::Neq(Box::new(
21307 crate::expressions::BinaryOp::new(
21308 y_ref.clone(),
21309 Expression::number(0),
21310 ),
21311 ));
21312 let div_expr = Expression::Div(Box::new(
21313 crate::expressions::BinaryOp::new(x_ref, y_ref),
21314 ));
21315 Ok(Expression::Case(Box::new(Case {
21316 operand: None,
21317 whens: vec![(condition, div_expr)],
21318 else_: Some(Expression::Null(Null)),
21319 comments: Vec::new(),
21320 inferred_type: None,
21321 })))
21322 }
21323 _ => Ok(Expression::Function(Box::new(Function::new(
21324 "TRY_DIVIDE".to_string(),
21325 vec![x, y],
21326 )))),
21327 }
21328 }
21329 // JSON_ARRAYAGG -> JSON_AGG for PostgreSQL
21330 "JSON_ARRAYAGG" => match target {
21331 DialectType::PostgreSQL => {
21332 Ok(Expression::Function(Box::new(Function {
21333 name: "JSON_AGG".to_string(),
21334 ..(*f)
21335 })))
21336 }
21337 _ => Ok(Expression::Function(f)),
21338 },
21339 // SCHEMA_NAME(id) -> CURRENT_SCHEMA for PostgreSQL, 'main' for SQLite
21340 "SCHEMA_NAME" => match target {
21341 DialectType::PostgreSQL => Ok(Expression::CurrentSchema(Box::new(
21342 crate::expressions::CurrentSchema { this: None },
21343 ))),
21344 DialectType::SQLite => Ok(Expression::string("main")),
21345 _ => Ok(Expression::Function(f)),
21346 },
21347 // TO_TIMESTAMP(x, fmt) 2-arg from Spark/Hive: convert Java format to target format
21348 "TO_TIMESTAMP"
21349 if f.args.len() == 2
21350 && matches!(
21351 source,
21352 DialectType::Spark
21353 | DialectType::Databricks
21354 | DialectType::Hive
21355 )
21356 && matches!(target, DialectType::DuckDB) =>
21357 {
21358 let mut args = f.args;
21359 let val = args.remove(0);
21360 let fmt_expr = args.remove(0);
21361 if let Expression::Literal(ref lit) = fmt_expr {
21362 if let Literal::String(ref s) = lit.as_ref() {
21363 // Convert Java/Spark format to C strptime format
21364 fn java_to_c_fmt(fmt: &str) -> String {
21365 let result = fmt
21366 .replace("yyyy", "%Y")
21367 .replace("SSSSSS", "%f")
21368 .replace("EEEE", "%W")
21369 .replace("MM", "%m")
21370 .replace("dd", "%d")
21371 .replace("HH", "%H")
21372 .replace("mm", "%M")
21373 .replace("ss", "%S")
21374 .replace("yy", "%y");
21375 let mut out = String::new();
21376 let chars: Vec<char> = result.chars().collect();
21377 let mut i = 0;
21378 while i < chars.len() {
21379 if chars[i] == '%' && i + 1 < chars.len() {
21380 out.push(chars[i]);
21381 out.push(chars[i + 1]);
21382 i += 2;
21383 } else if chars[i] == 'z' {
21384 out.push_str("%Z");
21385 i += 1;
21386 } else if chars[i] == 'Z' {
21387 out.push_str("%z");
21388 i += 1;
21389 } else {
21390 out.push(chars[i]);
21391 i += 1;
21392 }
21393 }
21394 out
21395 }
21396 let c_fmt = java_to_c_fmt(s);
21397 Ok(Expression::Function(Box::new(Function::new(
21398 "STRPTIME".to_string(),
21399 vec![val, Expression::string(&c_fmt)],
21400 ))))
21401 } else {
21402 Ok(Expression::Function(Box::new(Function::new(
21403 "STRPTIME".to_string(),
21404 vec![val, fmt_expr],
21405 ))))
21406 }
21407 } else {
21408 Ok(Expression::Function(Box::new(Function::new(
21409 "STRPTIME".to_string(),
21410 vec![val, fmt_expr],
21411 ))))
21412 }
21413 }
21414 // TO_DATE(x) 1-arg from Doris: date conversion
21415 "TO_DATE"
21416 if f.args.len() == 1
21417 && matches!(
21418 source,
21419 DialectType::Doris | DialectType::StarRocks
21420 ) =>
21421 {
21422 let arg = f.args.into_iter().next().unwrap();
21423 match target {
21424 DialectType::Oracle
21425 | DialectType::DuckDB
21426 | DialectType::TSQL => {
21427 // CAST(x AS DATE)
21428 Ok(Expression::Cast(Box::new(Cast {
21429 this: arg,
21430 to: DataType::Date,
21431 double_colon_syntax: false,
21432 trailing_comments: vec![],
21433 format: None,
21434 default: None,
21435 inferred_type: None,
21436 })))
21437 }
21438 DialectType::MySQL | DialectType::SingleStore => {
21439 // DATE(x)
21440 Ok(Expression::Function(Box::new(Function::new(
21441 "DATE".to_string(),
21442 vec![arg],
21443 ))))
21444 }
21445 _ => {
21446 // Default: keep as TO_DATE(x) (Spark, PostgreSQL, etc.)
21447 Ok(Expression::Function(Box::new(Function::new(
21448 "TO_DATE".to_string(),
21449 vec![arg],
21450 ))))
21451 }
21452 }
21453 }
21454 // TO_DATE(x) 1-arg from Spark/Hive: safe date conversion
21455 "TO_DATE"
21456 if f.args.len() == 1
21457 && matches!(
21458 source,
21459 DialectType::Spark
21460 | DialectType::Databricks
21461 | DialectType::Hive
21462 ) =>
21463 {
21464 let arg = f.args.into_iter().next().unwrap();
21465 match target {
21466 DialectType::DuckDB => {
21467 // Spark TO_DATE is safe -> TRY_CAST(x AS DATE)
21468 Ok(Expression::TryCast(Box::new(Cast {
21469 this: arg,
21470 to: DataType::Date,
21471 double_colon_syntax: false,
21472 trailing_comments: vec![],
21473 format: None,
21474 default: None,
21475 inferred_type: None,
21476 })))
21477 }
21478 DialectType::Presto
21479 | DialectType::Trino
21480 | DialectType::Athena => {
21481 // CAST(CAST(x AS TIMESTAMP) AS DATE)
21482 Ok(Self::double_cast_timestamp_date(arg))
21483 }
21484 DialectType::Snowflake => {
21485 // Spark's TO_DATE is safe -> TRY_TO_DATE(x, 'yyyy-mm-DD')
21486 // The default Spark format 'yyyy-MM-dd' maps to Snowflake 'yyyy-mm-DD'
21487 Ok(Expression::Function(Box::new(Function::new(
21488 "TRY_TO_DATE".to_string(),
21489 vec![arg, Expression::string("yyyy-mm-DD")],
21490 ))))
21491 }
21492 _ => {
21493 // Default: keep as TO_DATE(x)
21494 Ok(Expression::Function(Box::new(Function::new(
21495 "TO_DATE".to_string(),
21496 vec![arg],
21497 ))))
21498 }
21499 }
21500 }
21501 // TO_DATE(x, fmt) 2-arg from Spark/Hive: format-based date conversion
21502 "TO_DATE"
21503 if f.args.len() == 2
21504 && matches!(
21505 source,
21506 DialectType::Spark
21507 | DialectType::Databricks
21508 | DialectType::Hive
21509 ) =>
21510 {
21511 let mut args = f.args;
21512 let val = args.remove(0);
21513 let fmt_expr = args.remove(0);
21514 let is_default_format = matches!(&fmt_expr, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(s) if s == "yyyy-MM-dd"));
21515
21516 if is_default_format {
21517 // Default format: same as 1-arg form
21518 match target {
21519 DialectType::DuckDB => {
21520 Ok(Expression::TryCast(Box::new(Cast {
21521 this: val,
21522 to: DataType::Date,
21523 double_colon_syntax: false,
21524 trailing_comments: vec![],
21525 format: None,
21526 default: None,
21527 inferred_type: None,
21528 })))
21529 }
21530 DialectType::Presto
21531 | DialectType::Trino
21532 | DialectType::Athena => {
21533 Ok(Self::double_cast_timestamp_date(val))
21534 }
21535 DialectType::Snowflake => {
21536 // TRY_TO_DATE(x, format) with Snowflake format mapping
21537 let sf_fmt = "yyyy-MM-dd"
21538 .replace("yyyy", "yyyy")
21539 .replace("MM", "mm")
21540 .replace("dd", "DD");
21541 Ok(Expression::Function(Box::new(Function::new(
21542 "TRY_TO_DATE".to_string(),
21543 vec![val, Expression::string(&sf_fmt)],
21544 ))))
21545 }
21546 _ => Ok(Expression::Function(Box::new(Function::new(
21547 "TO_DATE".to_string(),
21548 vec![val],
21549 )))),
21550 }
21551 } else {
21552 // Non-default format: use format-based parsing
21553 if let Expression::Literal(ref lit) = fmt_expr {
21554 if let Literal::String(ref s) = lit.as_ref() {
21555 match target {
21556 DialectType::DuckDB => {
21557 // CAST(CAST(TRY_STRPTIME(x, c_fmt) AS TIMESTAMP) AS DATE)
21558 fn java_to_c_fmt_todate(fmt: &str) -> String {
21559 let result = fmt
21560 .replace("yyyy", "%Y")
21561 .replace("SSSSSS", "%f")
21562 .replace("EEEE", "%W")
21563 .replace("MM", "%m")
21564 .replace("dd", "%d")
21565 .replace("HH", "%H")
21566 .replace("mm", "%M")
21567 .replace("ss", "%S")
21568 .replace("yy", "%y");
21569 let mut out = String::new();
21570 let chars: Vec<char> =
21571 result.chars().collect();
21572 let mut i = 0;
21573 while i < chars.len() {
21574 if chars[i] == '%'
21575 && i + 1 < chars.len()
21576 {
21577 out.push(chars[i]);
21578 out.push(chars[i + 1]);
21579 i += 2;
21580 } else if chars[i] == 'z' {
21581 out.push_str("%Z");
21582 i += 1;
21583 } else if chars[i] == 'Z' {
21584 out.push_str("%z");
21585 i += 1;
21586 } else {
21587 out.push(chars[i]);
21588 i += 1;
21589 }
21590 }
21591 out
21592 }
21593 let c_fmt = java_to_c_fmt_todate(s);
21594 // CAST(CAST(TRY_STRPTIME(x, fmt) AS TIMESTAMP) AS DATE)
21595 let try_strptime = Expression::Function(
21596 Box::new(Function::new(
21597 "TRY_STRPTIME".to_string(),
21598 vec![val, Expression::string(&c_fmt)],
21599 )),
21600 );
21601 let cast_ts =
21602 Expression::Cast(Box::new(Cast {
21603 this: try_strptime,
21604 to: DataType::Timestamp {
21605 precision: None,
21606 timezone: false,
21607 },
21608 double_colon_syntax: false,
21609 trailing_comments: vec![],
21610 format: None,
21611 default: None,
21612 inferred_type: None,
21613 }));
21614 Ok(Expression::Cast(Box::new(Cast {
21615 this: cast_ts,
21616 to: DataType::Date,
21617 double_colon_syntax: false,
21618 trailing_comments: vec![],
21619 format: None,
21620 default: None,
21621 inferred_type: None,
21622 })))
21623 }
21624 DialectType::Presto
21625 | DialectType::Trino
21626 | DialectType::Athena => {
21627 // CAST(DATE_PARSE(x, presto_fmt) AS DATE)
21628 let p_fmt = s
21629 .replace("yyyy", "%Y")
21630 .replace("SSSSSS", "%f")
21631 .replace("MM", "%m")
21632 .replace("dd", "%d")
21633 .replace("HH", "%H")
21634 .replace("mm", "%M")
21635 .replace("ss", "%S")
21636 .replace("yy", "%y");
21637 let date_parse = Expression::Function(
21638 Box::new(Function::new(
21639 "DATE_PARSE".to_string(),
21640 vec![val, Expression::string(&p_fmt)],
21641 )),
21642 );
21643 Ok(Expression::Cast(Box::new(Cast {
21644 this: date_parse,
21645 to: DataType::Date,
21646 double_colon_syntax: false,
21647 trailing_comments: vec![],
21648 format: None,
21649 default: None,
21650 inferred_type: None,
21651 })))
21652 }
21653 DialectType::Snowflake => {
21654 // TRY_TO_DATE(x, snowflake_fmt)
21655 Ok(Expression::Function(Box::new(
21656 Function::new(
21657 "TRY_TO_DATE".to_string(),
21658 vec![val, Expression::string(s)],
21659 ),
21660 )))
21661 }
21662 _ => Ok(Expression::Function(Box::new(
21663 Function::new(
21664 "TO_DATE".to_string(),
21665 vec![val, fmt_expr],
21666 ),
21667 ))),
21668 }
21669 } else {
21670 Ok(Expression::Function(Box::new(Function::new(
21671 "TO_DATE".to_string(),
21672 vec![val, fmt_expr],
21673 ))))
21674 }
21675 } else {
21676 Ok(Expression::Function(Box::new(Function::new(
21677 "TO_DATE".to_string(),
21678 vec![val, fmt_expr],
21679 ))))
21680 }
21681 }
21682 }
21683 // TO_TIMESTAMP(x) 1-arg: epoch conversion
21684 "TO_TIMESTAMP"
21685 if f.args.len() == 1
21686 && matches!(source, DialectType::DuckDB)
21687 && matches!(
21688 target,
21689 DialectType::BigQuery
21690 | DialectType::Presto
21691 | DialectType::Trino
21692 | DialectType::Hive
21693 | DialectType::Spark
21694 | DialectType::Databricks
21695 | DialectType::Athena
21696 ) =>
21697 {
21698 let arg = f.args.into_iter().next().unwrap();
21699 let func_name = match target {
21700 DialectType::BigQuery => "TIMESTAMP_SECONDS",
21701 DialectType::Presto
21702 | DialectType::Trino
21703 | DialectType::Athena
21704 | DialectType::Hive
21705 | DialectType::Spark
21706 | DialectType::Databricks => "FROM_UNIXTIME",
21707 _ => "TO_TIMESTAMP",
21708 };
21709 Ok(Expression::Function(Box::new(Function::new(
21710 func_name.to_string(),
21711 vec![arg],
21712 ))))
21713 }
21714 // CONCAT(x) single-arg: -> CONCAT(COALESCE(x, '')) for Spark
21715 "CONCAT" if f.args.len() == 1 => {
21716 let arg = f.args.into_iter().next().unwrap();
21717 match target {
21718 DialectType::Presto
21719 | DialectType::Trino
21720 | DialectType::Athena => {
21721 // CONCAT(a) -> CAST(a AS VARCHAR)
21722 Ok(Expression::Cast(Box::new(Cast {
21723 this: arg,
21724 to: DataType::VarChar {
21725 length: None,
21726 parenthesized_length: false,
21727 },
21728 trailing_comments: vec![],
21729 double_colon_syntax: false,
21730 format: None,
21731 default: None,
21732 inferred_type: None,
21733 })))
21734 }
21735 DialectType::TSQL => {
21736 // CONCAT(a) -> a
21737 Ok(arg)
21738 }
21739 DialectType::DuckDB => {
21740 // Keep CONCAT(a) for DuckDB (native support)
21741 Ok(Expression::Function(Box::new(Function::new(
21742 "CONCAT".to_string(),
21743 vec![arg],
21744 ))))
21745 }
21746 DialectType::Spark | DialectType::Databricks => {
21747 let coalesced = Expression::Coalesce(Box::new(
21748 crate::expressions::VarArgFunc {
21749 expressions: vec![arg, Expression::string("")],
21750 original_name: None,
21751 inferred_type: None,
21752 },
21753 ));
21754 Ok(Expression::Function(Box::new(Function::new(
21755 "CONCAT".to_string(),
21756 vec![coalesced],
21757 ))))
21758 }
21759 _ => Ok(Expression::Function(Box::new(Function::new(
21760 "CONCAT".to_string(),
21761 vec![arg],
21762 )))),
21763 }
21764 }
21765 // REGEXP_EXTRACT(a, p) 2-arg: BigQuery default group is 0 (no 3rd arg needed)
21766 "REGEXP_EXTRACT"
21767 if f.args.len() == 3 && matches!(target, DialectType::BigQuery) =>
21768 {
21769 // If group_index is 0, drop it
21770 let drop_group = match &f.args[2] {
21771 Expression::Literal(lit)
21772 if matches!(lit.as_ref(), Literal::Number(_)) =>
21773 {
21774 let Literal::Number(n) = lit.as_ref() else {
21775 unreachable!()
21776 };
21777 n == "0"
21778 }
21779 _ => false,
21780 };
21781 if drop_group {
21782 let mut args = f.args;
21783 args.truncate(2);
21784 Ok(Expression::Function(Box::new(Function::new(
21785 "REGEXP_EXTRACT".to_string(),
21786 args,
21787 ))))
21788 } else {
21789 Ok(Expression::Function(f))
21790 }
21791 }
21792 // REGEXP_EXTRACT(a, pattern, group, flags) 4-arg -> REGEXP_SUBSTR for Snowflake
21793 "REGEXP_EXTRACT"
21794 if f.args.len() == 4
21795 && matches!(target, DialectType::Snowflake) =>
21796 {
21797 // REGEXP_EXTRACT(a, 'pattern', 2, 'i') -> REGEXP_SUBSTR(a, 'pattern', 1, 1, 'i', 2)
21798 let mut args = f.args;
21799 let this = args.remove(0);
21800 let pattern = args.remove(0);
21801 let group = args.remove(0);
21802 let flags = args.remove(0);
21803 Ok(Expression::Function(Box::new(Function::new(
21804 "REGEXP_SUBSTR".to_string(),
21805 vec![
21806 this,
21807 pattern,
21808 Expression::number(1),
21809 Expression::number(1),
21810 flags,
21811 group,
21812 ],
21813 ))))
21814 }
21815 // REGEXP_SUBSTR(a, pattern, position) 3-arg -> REGEXP_EXTRACT(SUBSTRING(a, pos), pattern)
21816 "REGEXP_SUBSTR"
21817 if f.args.len() == 3
21818 && matches!(
21819 target,
21820 DialectType::DuckDB
21821 | DialectType::Presto
21822 | DialectType::Trino
21823 | DialectType::Spark
21824 | DialectType::Databricks
21825 ) =>
21826 {
21827 let mut args = f.args;
21828 let this = args.remove(0);
21829 let pattern = args.remove(0);
21830 let position = args.remove(0);
21831 // Wrap subject in SUBSTRING(this, position) to apply the offset
21832 let substring_expr = Expression::Function(Box::new(Function::new(
21833 "SUBSTRING".to_string(),
21834 vec![this, position],
21835 )));
21836 let target_name = match target {
21837 DialectType::DuckDB => "REGEXP_EXTRACT",
21838 _ => "REGEXP_EXTRACT",
21839 };
21840 Ok(Expression::Function(Box::new(Function::new(
21841 target_name.to_string(),
21842 vec![substring_expr, pattern],
21843 ))))
21844 }
21845 // TO_DAYS(x) -> (DATEDIFF(x, '0000-01-01') + 1) or target-specific
21846 "TO_DAYS" if f.args.len() == 1 => {
21847 let x = f.args.into_iter().next().unwrap();
21848 let epoch = Expression::string("0000-01-01");
21849 // Build the final target-specific expression directly
21850 let datediff_expr = match target {
21851 DialectType::MySQL | DialectType::SingleStore => {
21852 // MySQL: (DATEDIFF(x, '0000-01-01') + 1)
21853 Expression::Function(Box::new(Function::new(
21854 "DATEDIFF".to_string(),
21855 vec![x, epoch],
21856 )))
21857 }
21858 DialectType::DuckDB => {
21859 // DuckDB: (DATE_DIFF('DAY', CAST('0000-01-01' AS DATE), CAST(x AS DATE)) + 1)
21860 let cast_epoch = Expression::Cast(Box::new(Cast {
21861 this: epoch,
21862 to: DataType::Date,
21863 trailing_comments: Vec::new(),
21864 double_colon_syntax: false,
21865 format: None,
21866 default: None,
21867 inferred_type: None,
21868 }));
21869 let cast_x = Expression::Cast(Box::new(Cast {
21870 this: x,
21871 to: DataType::Date,
21872 trailing_comments: Vec::new(),
21873 double_colon_syntax: false,
21874 format: None,
21875 default: None,
21876 inferred_type: None,
21877 }));
21878 Expression::Function(Box::new(Function::new(
21879 "DATE_DIFF".to_string(),
21880 vec![Expression::string("DAY"), cast_epoch, cast_x],
21881 )))
21882 }
21883 DialectType::Presto
21884 | DialectType::Trino
21885 | DialectType::Athena => {
21886 // Presto: (DATE_DIFF('DAY', CAST(CAST('0000-01-01' AS TIMESTAMP) AS DATE), CAST(CAST(x AS TIMESTAMP) AS DATE)) + 1)
21887 let cast_epoch = Self::double_cast_timestamp_date(epoch);
21888 let cast_x = Self::double_cast_timestamp_date(x);
21889 Expression::Function(Box::new(Function::new(
21890 "DATE_DIFF".to_string(),
21891 vec![Expression::string("DAY"), cast_epoch, cast_x],
21892 )))
21893 }
21894 _ => {
21895 // Default: (DATEDIFF(x, '0000-01-01') + 1)
21896 Expression::Function(Box::new(Function::new(
21897 "DATEDIFF".to_string(),
21898 vec![x, epoch],
21899 )))
21900 }
21901 };
21902 let add_one = Expression::Add(Box::new(BinaryOp::new(
21903 datediff_expr,
21904 Expression::number(1),
21905 )));
21906 Ok(Expression::Paren(Box::new(crate::expressions::Paren {
21907 this: add_one,
21908 trailing_comments: Vec::new(),
21909 })))
21910 }
21911 // STR_TO_DATE(x, format) -> DATE_PARSE / STRPTIME / TO_DATE etc.
21912 "STR_TO_DATE"
21913 if f.args.len() == 2
21914 && matches!(
21915 target,
21916 DialectType::Presto | DialectType::Trino
21917 ) =>
21918 {
21919 let mut args = f.args;
21920 let x = args.remove(0);
21921 let format_expr = args.remove(0);
21922 // Check if the format contains time components
21923 let has_time = if let Expression::Literal(ref lit) = format_expr {
21924 if let Literal::String(ref fmt) = lit.as_ref() {
21925 fmt.contains("%H")
21926 || fmt.contains("%T")
21927 || fmt.contains("%M")
21928 || fmt.contains("%S")
21929 || fmt.contains("%I")
21930 || fmt.contains("%p")
21931 } else {
21932 false
21933 }
21934 } else {
21935 false
21936 };
21937 let date_parse = Expression::Function(Box::new(Function::new(
21938 "DATE_PARSE".to_string(),
21939 vec![x, format_expr],
21940 )));
21941 if has_time {
21942 // Has time components: just DATE_PARSE
21943 Ok(date_parse)
21944 } else {
21945 // Date-only: CAST(DATE_PARSE(...) AS DATE)
21946 Ok(Expression::Cast(Box::new(Cast {
21947 this: date_parse,
21948 to: DataType::Date,
21949 trailing_comments: Vec::new(),
21950 double_colon_syntax: false,
21951 format: None,
21952 default: None,
21953 inferred_type: None,
21954 })))
21955 }
21956 }
21957 "STR_TO_DATE"
21958 if f.args.len() == 2
21959 && matches!(
21960 target,
21961 DialectType::PostgreSQL | DialectType::Redshift
21962 ) =>
21963 {
21964 let mut args = f.args;
21965 let x = args.remove(0);
21966 let fmt = args.remove(0);
21967 let pg_fmt = match fmt {
21968 Expression::Literal(lit)
21969 if matches!(lit.as_ref(), Literal::String(_)) =>
21970 {
21971 let Literal::String(s) = lit.as_ref() else {
21972 unreachable!()
21973 };
21974 Expression::string(
21975 &s.replace("%Y", "YYYY")
21976 .replace("%m", "MM")
21977 .replace("%d", "DD")
21978 .replace("%H", "HH24")
21979 .replace("%M", "MI")
21980 .replace("%S", "SS"),
21981 )
21982 }
21983 other => other,
21984 };
21985 let to_date = Expression::Function(Box::new(Function::new(
21986 "TO_DATE".to_string(),
21987 vec![x, pg_fmt],
21988 )));
21989 Ok(Expression::Cast(Box::new(Cast {
21990 this: to_date,
21991 to: DataType::Timestamp {
21992 timezone: false,
21993 precision: None,
21994 },
21995 trailing_comments: Vec::new(),
21996 double_colon_syntax: false,
21997 format: None,
21998 default: None,
21999 inferred_type: None,
22000 })))
22001 }
22002 // RANGE(start, end) -> GENERATE_SERIES for SQLite
22003 "RANGE"
22004 if (f.args.len() == 1 || f.args.len() == 2)
22005 && matches!(target, DialectType::SQLite) =>
22006 {
22007 if f.args.len() == 2 {
22008 // RANGE(start, end) -> (SELECT value AS col_alias FROM GENERATE_SERIES(start, end))
22009 // For SQLite, RANGE is exclusive on end, GENERATE_SERIES is inclusive
22010 let mut args = f.args;
22011 let start = args.remove(0);
22012 let end = args.remove(0);
22013 Ok(Expression::Function(Box::new(Function::new(
22014 "GENERATE_SERIES".to_string(),
22015 vec![start, end],
22016 ))))
22017 } else {
22018 Ok(Expression::Function(f))
22019 }
22020 }
22021 // UNIFORM(low, high[, seed]) -> UNIFORM(low, high, RANDOM([seed])) for Snowflake
22022 // When source is Snowflake, keep as-is (args already in correct form)
22023 "UNIFORM"
22024 if matches!(target, DialectType::Snowflake)
22025 && (f.args.len() == 2 || f.args.len() == 3) =>
22026 {
22027 if matches!(source, DialectType::Snowflake) {
22028 // Snowflake -> Snowflake: keep as-is
22029 Ok(Expression::Function(f))
22030 } else {
22031 let mut args = f.args;
22032 let low = args.remove(0);
22033 let high = args.remove(0);
22034 let random = if !args.is_empty() {
22035 let seed = args.remove(0);
22036 Expression::Function(Box::new(Function::new(
22037 "RANDOM".to_string(),
22038 vec![seed],
22039 )))
22040 } else {
22041 Expression::Function(Box::new(Function::new(
22042 "RANDOM".to_string(),
22043 vec![],
22044 )))
22045 };
22046 Ok(Expression::Function(Box::new(Function::new(
22047 "UNIFORM".to_string(),
22048 vec![low, high, random],
22049 ))))
22050 }
22051 }
22052 // TO_UTC_TIMESTAMP(ts, tz) -> target-specific UTC conversion
22053 "TO_UTC_TIMESTAMP" if f.args.len() == 2 => {
22054 let mut args = f.args;
22055 let ts_arg = args.remove(0);
22056 let tz_arg = args.remove(0);
22057 // Cast string literal to TIMESTAMP for all targets
22058 let ts_cast = if matches!(&ts_arg, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)))
22059 {
22060 Expression::Cast(Box::new(Cast {
22061 this: ts_arg,
22062 to: DataType::Timestamp {
22063 timezone: false,
22064 precision: None,
22065 },
22066 trailing_comments: vec![],
22067 double_colon_syntax: false,
22068 format: None,
22069 default: None,
22070 inferred_type: None,
22071 }))
22072 } else {
22073 ts_arg
22074 };
22075 match target {
22076 DialectType::Spark | DialectType::Databricks => {
22077 Ok(Expression::Function(Box::new(Function::new(
22078 "TO_UTC_TIMESTAMP".to_string(),
22079 vec![ts_cast, tz_arg],
22080 ))))
22081 }
22082 DialectType::Snowflake => {
22083 // CONVERT_TIMEZONE(tz, 'UTC', CAST(ts AS TIMESTAMP))
22084 Ok(Expression::Function(Box::new(Function::new(
22085 "CONVERT_TIMEZONE".to_string(),
22086 vec![tz_arg, Expression::string("UTC"), ts_cast],
22087 ))))
22088 }
22089 DialectType::Presto
22090 | DialectType::Trino
22091 | DialectType::Athena => {
22092 // WITH_TIMEZONE(CAST(ts AS TIMESTAMP), tz) AT TIME ZONE 'UTC'
22093 let wtz = Expression::Function(Box::new(Function::new(
22094 "WITH_TIMEZONE".to_string(),
22095 vec![ts_cast, tz_arg],
22096 )));
22097 Ok(Expression::AtTimeZone(Box::new(
22098 crate::expressions::AtTimeZone {
22099 this: wtz,
22100 zone: Expression::string("UTC"),
22101 },
22102 )))
22103 }
22104 DialectType::BigQuery => {
22105 // DATETIME(TIMESTAMP(CAST(ts AS DATETIME), tz), 'UTC')
22106 let cast_dt = Expression::Cast(Box::new(Cast {
22107 this: if let Expression::Cast(c) = ts_cast {
22108 c.this
22109 } else {
22110 ts_cast.clone()
22111 },
22112 to: DataType::Custom {
22113 name: "DATETIME".to_string(),
22114 },
22115 trailing_comments: vec![],
22116 double_colon_syntax: false,
22117 format: None,
22118 default: None,
22119 inferred_type: None,
22120 }));
22121 let ts_func =
22122 Expression::Function(Box::new(Function::new(
22123 "TIMESTAMP".to_string(),
22124 vec![cast_dt, tz_arg],
22125 )));
22126 Ok(Expression::Function(Box::new(Function::new(
22127 "DATETIME".to_string(),
22128 vec![ts_func, Expression::string("UTC")],
22129 ))))
22130 }
22131 _ => {
22132 // DuckDB, PostgreSQL, Redshift: CAST(ts AS TIMESTAMP) AT TIME ZONE tz AT TIME ZONE 'UTC'
22133 let atz1 = Expression::AtTimeZone(Box::new(
22134 crate::expressions::AtTimeZone {
22135 this: ts_cast,
22136 zone: tz_arg,
22137 },
22138 ));
22139 Ok(Expression::AtTimeZone(Box::new(
22140 crate::expressions::AtTimeZone {
22141 this: atz1,
22142 zone: Expression::string("UTC"),
22143 },
22144 )))
22145 }
22146 }
22147 }
22148 // FROM_UTC_TIMESTAMP(ts, tz) -> target-specific UTC conversion
22149 "FROM_UTC_TIMESTAMP" if f.args.len() == 2 => {
22150 let mut args = f.args;
22151 let ts_arg = args.remove(0);
22152 let tz_arg = args.remove(0);
22153 // Cast string literal to TIMESTAMP
22154 let ts_cast = if matches!(&ts_arg, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)))
22155 {
22156 Expression::Cast(Box::new(Cast {
22157 this: ts_arg,
22158 to: DataType::Timestamp {
22159 timezone: false,
22160 precision: None,
22161 },
22162 trailing_comments: vec![],
22163 double_colon_syntax: false,
22164 format: None,
22165 default: None,
22166 inferred_type: None,
22167 }))
22168 } else {
22169 ts_arg
22170 };
22171 match target {
22172 DialectType::Spark | DialectType::Databricks => {
22173 Ok(Expression::Function(Box::new(Function::new(
22174 "FROM_UTC_TIMESTAMP".to_string(),
22175 vec![ts_cast, tz_arg],
22176 ))))
22177 }
22178 DialectType::Presto
22179 | DialectType::Trino
22180 | DialectType::Athena => {
22181 // AT_TIMEZONE(CAST(ts AS TIMESTAMP), tz)
22182 Ok(Expression::Function(Box::new(Function::new(
22183 "AT_TIMEZONE".to_string(),
22184 vec![ts_cast, tz_arg],
22185 ))))
22186 }
22187 DialectType::Snowflake => {
22188 // CONVERT_TIMEZONE('UTC', tz, CAST(ts AS TIMESTAMP))
22189 Ok(Expression::Function(Box::new(Function::new(
22190 "CONVERT_TIMEZONE".to_string(),
22191 vec![Expression::string("UTC"), tz_arg, ts_cast],
22192 ))))
22193 }
22194 _ => {
22195 // DuckDB, PostgreSQL, Redshift: CAST(ts AS TIMESTAMP) AT TIME ZONE tz
22196 Ok(Expression::AtTimeZone(Box::new(
22197 crate::expressions::AtTimeZone {
22198 this: ts_cast,
22199 zone: tz_arg,
22200 },
22201 )))
22202 }
22203 }
22204 }
22205 // MAP_FROM_ARRAYS(keys, values) -> target-specific map construction
22206 "MAP_FROM_ARRAYS" if f.args.len() == 2 => {
22207 let name = match target {
22208 DialectType::Snowflake => "OBJECT_CONSTRUCT",
22209 _ => "MAP",
22210 };
22211 Ok(Expression::Function(Box::new(Function::new(
22212 name.to_string(),
22213 f.args,
22214 ))))
22215 }
22216 // STR_TO_MAP(s, pair_delim, kv_delim) -> SPLIT_TO_MAP for Presto
22217 "STR_TO_MAP" if f.args.len() >= 1 => match target {
22218 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
22219 Ok(Expression::Function(Box::new(Function::new(
22220 "SPLIT_TO_MAP".to_string(),
22221 f.args,
22222 ))))
22223 }
22224 _ => Ok(Expression::Function(f)),
22225 },
22226 // TIME_TO_STR(x, fmt) -> Expression::TimeToStr for proper generation
22227 "TIME_TO_STR" if f.args.len() == 2 => {
22228 let mut args = f.args;
22229 let this = args.remove(0);
22230 let fmt_expr = args.remove(0);
22231 let format = if let Expression::Literal(lit) = fmt_expr {
22232 if let Literal::String(s) = lit.as_ref() {
22233 s.clone()
22234 } else {
22235 String::new()
22236 }
22237 } else {
22238 "%Y-%m-%d %H:%M:%S".to_string()
22239 };
22240 Ok(Expression::TimeToStr(Box::new(
22241 crate::expressions::TimeToStr {
22242 this: Box::new(this),
22243 format,
22244 culture: None,
22245 zone: None,
22246 },
22247 )))
22248 }
22249 // STR_TO_TIME(x, fmt) -> Expression::StrToTime for proper generation
22250 "STR_TO_TIME" if f.args.len() == 2 => {
22251 let mut args = f.args;
22252 let this = args.remove(0);
22253 let fmt_expr = args.remove(0);
22254 let format = if let Expression::Literal(lit) = fmt_expr {
22255 if let Literal::String(s) = lit.as_ref() {
22256 s.clone()
22257 } else {
22258 String::new()
22259 }
22260 } else {
22261 "%Y-%m-%d %H:%M:%S".to_string()
22262 };
22263 Ok(Expression::StrToTime(Box::new(
22264 crate::expressions::StrToTime {
22265 this: Box::new(this),
22266 format,
22267 zone: None,
22268 safe: None,
22269 target_type: None,
22270 },
22271 )))
22272 }
22273 // STR_TO_UNIX(x, fmt) -> Expression::StrToUnix for proper generation
22274 "STR_TO_UNIX" if f.args.len() >= 1 => {
22275 let mut args = f.args;
22276 let this = args.remove(0);
22277 let format = if !args.is_empty() {
22278 if let Expression::Literal(lit) = args.remove(0) {
22279 if let Literal::String(s) = lit.as_ref() {
22280 Some(s.clone())
22281 } else {
22282 None
22283 }
22284 } else {
22285 None
22286 }
22287 } else {
22288 None
22289 };
22290 Ok(Expression::StrToUnix(Box::new(
22291 crate::expressions::StrToUnix {
22292 this: Some(Box::new(this)),
22293 format,
22294 },
22295 )))
22296 }
22297 // TIME_TO_UNIX(x) -> Expression::TimeToUnix for proper generation
22298 "TIME_TO_UNIX" if f.args.len() == 1 => {
22299 let mut args = f.args;
22300 let this = args.remove(0);
22301 Ok(Expression::TimeToUnix(Box::new(
22302 crate::expressions::UnaryFunc {
22303 this,
22304 original_name: None,
22305 inferred_type: None,
22306 },
22307 )))
22308 }
22309 // UNIX_TO_STR(x, fmt) -> Expression::UnixToStr for proper generation
22310 "UNIX_TO_STR" if f.args.len() >= 1 => {
22311 let mut args = f.args;
22312 let this = args.remove(0);
22313 let format = if !args.is_empty() {
22314 if let Expression::Literal(lit) = args.remove(0) {
22315 if let Literal::String(s) = lit.as_ref() {
22316 Some(s.clone())
22317 } else {
22318 None
22319 }
22320 } else {
22321 None
22322 }
22323 } else {
22324 None
22325 };
22326 Ok(Expression::UnixToStr(Box::new(
22327 crate::expressions::UnixToStr {
22328 this: Box::new(this),
22329 format,
22330 },
22331 )))
22332 }
22333 // UNIX_TO_TIME(x) -> Expression::UnixToTime for proper generation
22334 "UNIX_TO_TIME" if f.args.len() == 1 => {
22335 let mut args = f.args;
22336 let this = args.remove(0);
22337 Ok(Expression::UnixToTime(Box::new(
22338 crate::expressions::UnixToTime {
22339 this: Box::new(this),
22340 scale: None,
22341 zone: None,
22342 hours: None,
22343 minutes: None,
22344 format: None,
22345 target_type: None,
22346 },
22347 )))
22348 }
22349 // TIME_STR_TO_DATE(x) -> Expression::TimeStrToDate for proper generation
22350 "TIME_STR_TO_DATE" if f.args.len() == 1 => {
22351 let mut args = f.args;
22352 let this = args.remove(0);
22353 Ok(Expression::TimeStrToDate(Box::new(
22354 crate::expressions::UnaryFunc {
22355 this,
22356 original_name: None,
22357 inferred_type: None,
22358 },
22359 )))
22360 }
22361 // TIME_STR_TO_TIME(x) -> Expression::TimeStrToTime for proper generation
22362 "TIME_STR_TO_TIME" if f.args.len() == 1 => {
22363 let mut args = f.args;
22364 let this = args.remove(0);
22365 Ok(Expression::TimeStrToTime(Box::new(
22366 crate::expressions::TimeStrToTime {
22367 this: Box::new(this),
22368 zone: None,
22369 },
22370 )))
22371 }
22372 // MONTHS_BETWEEN(end, start) -> DuckDB complex expansion
22373 "MONTHS_BETWEEN" if f.args.len() == 2 => {
22374 match target {
22375 DialectType::DuckDB => {
22376 let mut args = f.args;
22377 let end_date = args.remove(0);
22378 let start_date = args.remove(0);
22379 let cast_end = Self::ensure_cast_date(end_date);
22380 let cast_start = Self::ensure_cast_date(start_date);
22381 // DATE_DIFF('MONTH', start, end) + CASE WHEN DAY(end) = DAY(LAST_DAY(end)) AND DAY(start) = DAY(LAST_DAY(start)) THEN 0 ELSE (DAY(end) - DAY(start)) / 31.0 END
22382 let dd = Expression::Function(Box::new(Function::new(
22383 "DATE_DIFF".to_string(),
22384 vec![
22385 Expression::string("MONTH"),
22386 cast_start.clone(),
22387 cast_end.clone(),
22388 ],
22389 )));
22390 let day_end =
22391 Expression::Function(Box::new(Function::new(
22392 "DAY".to_string(),
22393 vec![cast_end.clone()],
22394 )));
22395 let day_start =
22396 Expression::Function(Box::new(Function::new(
22397 "DAY".to_string(),
22398 vec![cast_start.clone()],
22399 )));
22400 let last_day_end =
22401 Expression::Function(Box::new(Function::new(
22402 "LAST_DAY".to_string(),
22403 vec![cast_end.clone()],
22404 )));
22405 let last_day_start =
22406 Expression::Function(Box::new(Function::new(
22407 "LAST_DAY".to_string(),
22408 vec![cast_start.clone()],
22409 )));
22410 let day_last_end = Expression::Function(Box::new(
22411 Function::new("DAY".to_string(), vec![last_day_end]),
22412 ));
22413 let day_last_start = Expression::Function(Box::new(
22414 Function::new("DAY".to_string(), vec![last_day_start]),
22415 ));
22416 let cond1 = Expression::Eq(Box::new(BinaryOp::new(
22417 day_end.clone(),
22418 day_last_end,
22419 )));
22420 let cond2 = Expression::Eq(Box::new(BinaryOp::new(
22421 day_start.clone(),
22422 day_last_start,
22423 )));
22424 let both_cond =
22425 Expression::And(Box::new(BinaryOp::new(cond1, cond2)));
22426 let day_diff = Expression::Sub(Box::new(BinaryOp::new(
22427 day_end, day_start,
22428 )));
22429 let day_diff_paren = Expression::Paren(Box::new(
22430 crate::expressions::Paren {
22431 this: day_diff,
22432 trailing_comments: Vec::new(),
22433 },
22434 ));
22435 let frac = Expression::Div(Box::new(BinaryOp::new(
22436 day_diff_paren,
22437 Expression::Literal(Box::new(Literal::Number(
22438 "31.0".to_string(),
22439 ))),
22440 )));
22441 let case_expr = Expression::Case(Box::new(Case {
22442 operand: None,
22443 whens: vec![(both_cond, Expression::number(0))],
22444 else_: Some(frac),
22445 comments: Vec::new(),
22446 inferred_type: None,
22447 }));
22448 Ok(Expression::Add(Box::new(BinaryOp::new(dd, case_expr))))
22449 }
22450 DialectType::Snowflake | DialectType::Redshift => {
22451 let mut args = f.args;
22452 let end_date = args.remove(0);
22453 let start_date = args.remove(0);
22454 let unit = Expression::Identifier(Identifier::new("MONTH"));
22455 Ok(Expression::Function(Box::new(Function::new(
22456 "DATEDIFF".to_string(),
22457 vec![unit, start_date, end_date],
22458 ))))
22459 }
22460 DialectType::Presto
22461 | DialectType::Trino
22462 | DialectType::Athena => {
22463 let mut args = f.args;
22464 let end_date = args.remove(0);
22465 let start_date = args.remove(0);
22466 Ok(Expression::Function(Box::new(Function::new(
22467 "DATE_DIFF".to_string(),
22468 vec![Expression::string("MONTH"), start_date, end_date],
22469 ))))
22470 }
22471 _ => Ok(Expression::Function(f)),
22472 }
22473 }
22474 // MONTHS_BETWEEN(end, start, roundOff) - 3-arg form (Spark-specific)
22475 // Drop the roundOff arg for non-Spark targets, keep it for Spark
22476 "MONTHS_BETWEEN" if f.args.len() == 3 => {
22477 match target {
22478 DialectType::Spark | DialectType::Databricks => {
22479 Ok(Expression::Function(f))
22480 }
22481 _ => {
22482 // Drop the 3rd arg and delegate to the 2-arg logic
22483 let mut args = f.args;
22484 let end_date = args.remove(0);
22485 let start_date = args.remove(0);
22486 // Re-create as 2-arg and process
22487 let f2 = Function::new(
22488 "MONTHS_BETWEEN".to_string(),
22489 vec![end_date, start_date],
22490 );
22491 let e2 = Expression::Function(Box::new(f2));
22492 Self::cross_dialect_normalize(e2, source, target)
22493 }
22494 }
22495 }
22496 // TO_TIMESTAMP(x) with 1 arg -> CAST(x AS TIMESTAMP) for most targets
22497 "TO_TIMESTAMP"
22498 if f.args.len() == 1
22499 && matches!(
22500 source,
22501 DialectType::Spark
22502 | DialectType::Databricks
22503 | DialectType::Hive
22504 ) =>
22505 {
22506 let arg = f.args.into_iter().next().unwrap();
22507 Ok(Expression::Cast(Box::new(Cast {
22508 this: arg,
22509 to: DataType::Timestamp {
22510 timezone: false,
22511 precision: None,
22512 },
22513 trailing_comments: vec![],
22514 double_colon_syntax: false,
22515 format: None,
22516 default: None,
22517 inferred_type: None,
22518 })))
22519 }
22520 // STRING(x) -> CAST(x AS STRING) for Spark target
22521 "STRING"
22522 if f.args.len() == 1
22523 && matches!(
22524 source,
22525 DialectType::Spark | DialectType::Databricks
22526 ) =>
22527 {
22528 let arg = f.args.into_iter().next().unwrap();
22529 let dt = match target {
22530 DialectType::Spark
22531 | DialectType::Databricks
22532 | DialectType::Hive => DataType::Custom {
22533 name: "STRING".to_string(),
22534 },
22535 _ => DataType::Text,
22536 };
22537 Ok(Expression::Cast(Box::new(Cast {
22538 this: arg,
22539 to: dt,
22540 trailing_comments: vec![],
22541 double_colon_syntax: false,
22542 format: None,
22543 default: None,
22544 inferred_type: None,
22545 })))
22546 }
22547 // LOGICAL_OR(x) -> BOOL_OR(x) for Spark target
22548 "LOGICAL_OR" if f.args.len() == 1 => {
22549 let name = match target {
22550 DialectType::Spark | DialectType::Databricks => "BOOL_OR",
22551 _ => "LOGICAL_OR",
22552 };
22553 Ok(Expression::Function(Box::new(Function::new(
22554 name.to_string(),
22555 f.args,
22556 ))))
22557 }
22558 // SPLIT(x, pattern) from Spark -> STR_SPLIT_REGEX for DuckDB, REGEXP_SPLIT for Presto
22559 "SPLIT"
22560 if f.args.len() == 2
22561 && matches!(
22562 source,
22563 DialectType::Spark
22564 | DialectType::Databricks
22565 | DialectType::Hive
22566 ) =>
22567 {
22568 let name = match target {
22569 DialectType::DuckDB => "STR_SPLIT_REGEX",
22570 DialectType::Presto
22571 | DialectType::Trino
22572 | DialectType::Athena => "REGEXP_SPLIT",
22573 DialectType::Spark
22574 | DialectType::Databricks
22575 | DialectType::Hive => "SPLIT",
22576 _ => "SPLIT",
22577 };
22578 Ok(Expression::Function(Box::new(Function::new(
22579 name.to_string(),
22580 f.args,
22581 ))))
22582 }
22583 // TRY_ELEMENT_AT -> ELEMENT_AT for Presto, array[idx] for DuckDB
22584 "TRY_ELEMENT_AT" if f.args.len() == 2 => match target {
22585 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
22586 Ok(Expression::Function(Box::new(Function::new(
22587 "ELEMENT_AT".to_string(),
22588 f.args,
22589 ))))
22590 }
22591 DialectType::DuckDB => {
22592 let mut args = f.args;
22593 let arr = args.remove(0);
22594 let idx = args.remove(0);
22595 Ok(Expression::Subscript(Box::new(
22596 crate::expressions::Subscript {
22597 this: arr,
22598 index: idx,
22599 },
22600 )))
22601 }
22602 _ => Ok(Expression::Function(f)),
22603 },
22604 // ARRAY_FILTER(arr, lambda) -> FILTER for Hive/Spark/Presto, LIST_FILTER for DuckDB
22605 "ARRAY_FILTER" if f.args.len() == 2 => {
22606 let name = match target {
22607 DialectType::DuckDB => "LIST_FILTER",
22608 DialectType::StarRocks => "ARRAY_FILTER",
22609 _ => "FILTER",
22610 };
22611 Ok(Expression::Function(Box::new(Function::new(
22612 name.to_string(),
22613 f.args,
22614 ))))
22615 }
22616 // FILTER(arr, lambda) -> ARRAY_FILTER for StarRocks, LIST_FILTER for DuckDB
22617 "FILTER" if f.args.len() == 2 => {
22618 let name = match target {
22619 DialectType::DuckDB => "LIST_FILTER",
22620 DialectType::StarRocks => "ARRAY_FILTER",
22621 _ => "FILTER",
22622 };
22623 Ok(Expression::Function(Box::new(Function::new(
22624 name.to_string(),
22625 f.args,
22626 ))))
22627 }
22628 // REDUCE(arr, init, lambda1, lambda2) -> AGGREGATE for Spark
22629 "REDUCE" if f.args.len() >= 3 => {
22630 let name = match target {
22631 DialectType::Spark | DialectType::Databricks => "AGGREGATE",
22632 _ => "REDUCE",
22633 };
22634 Ok(Expression::Function(Box::new(Function::new(
22635 name.to_string(),
22636 f.args,
22637 ))))
22638 }
22639 // CURRENT_SCHEMA() -> dialect-specific
22640 "CURRENT_SCHEMA" => {
22641 match target {
22642 DialectType::PostgreSQL => {
22643 // PostgreSQL: CURRENT_SCHEMA (no parens)
22644 Ok(Expression::Function(Box::new(Function {
22645 name: "CURRENT_SCHEMA".to_string(),
22646 args: vec![],
22647 distinct: false,
22648 trailing_comments: vec![],
22649 use_bracket_syntax: false,
22650 no_parens: true,
22651 quoted: false,
22652 span: None,
22653 inferred_type: None,
22654 })))
22655 }
22656 DialectType::MySQL
22657 | DialectType::Doris
22658 | DialectType::StarRocks => Ok(Expression::Function(Box::new(
22659 Function::new("SCHEMA".to_string(), vec![]),
22660 ))),
22661 DialectType::TSQL => Ok(Expression::Function(Box::new(
22662 Function::new("SCHEMA_NAME".to_string(), vec![]),
22663 ))),
22664 DialectType::SQLite => Ok(Expression::Literal(Box::new(
22665 Literal::String("main".to_string()),
22666 ))),
22667 _ => Ok(Expression::Function(f)),
22668 }
22669 }
22670 // LTRIM(str, chars) 2-arg -> TRIM(LEADING chars FROM str) for Spark/Hive/Databricks/ClickHouse
22671 "LTRIM" if f.args.len() == 2 => match target {
22672 DialectType::Spark
22673 | DialectType::Hive
22674 | DialectType::Databricks
22675 | DialectType::ClickHouse => {
22676 let mut args = f.args;
22677 let str_expr = args.remove(0);
22678 let chars = args.remove(0);
22679 Ok(Expression::Trim(Box::new(crate::expressions::TrimFunc {
22680 this: str_expr,
22681 characters: Some(chars),
22682 position: crate::expressions::TrimPosition::Leading,
22683 sql_standard_syntax: true,
22684 position_explicit: true,
22685 })))
22686 }
22687 _ => Ok(Expression::Function(f)),
22688 },
22689 // RTRIM(str, chars) 2-arg -> TRIM(TRAILING chars FROM str) for Spark/Hive/Databricks/ClickHouse
22690 "RTRIM" if f.args.len() == 2 => match target {
22691 DialectType::Spark
22692 | DialectType::Hive
22693 | DialectType::Databricks
22694 | DialectType::ClickHouse => {
22695 let mut args = f.args;
22696 let str_expr = args.remove(0);
22697 let chars = args.remove(0);
22698 Ok(Expression::Trim(Box::new(crate::expressions::TrimFunc {
22699 this: str_expr,
22700 characters: Some(chars),
22701 position: crate::expressions::TrimPosition::Trailing,
22702 sql_standard_syntax: true,
22703 position_explicit: true,
22704 })))
22705 }
22706 _ => Ok(Expression::Function(f)),
22707 },
22708 // ARRAY_REVERSE(x) -> arrayReverse(x) for ClickHouse
22709 "ARRAY_REVERSE" if f.args.len() == 1 => match target {
22710 DialectType::ClickHouse => {
22711 let mut new_f = *f;
22712 new_f.name = "arrayReverse".to_string();
22713 Ok(Expression::Function(Box::new(new_f)))
22714 }
22715 _ => Ok(Expression::Function(f)),
22716 },
22717 // UUID() -> NEWID() for TSQL
22718 "UUID" if f.args.is_empty() => match target {
22719 DialectType::TSQL | DialectType::Fabric => {
22720 Ok(Expression::Function(Box::new(Function::new(
22721 "NEWID".to_string(),
22722 vec![],
22723 ))))
22724 }
22725 _ => Ok(Expression::Function(f)),
22726 },
22727 // FARM_FINGERPRINT(x) -> farmFingerprint64(x) for ClickHouse, FARMFINGERPRINT64(x) for Redshift
22728 "FARM_FINGERPRINT" if f.args.len() == 1 => match target {
22729 DialectType::ClickHouse => {
22730 let mut new_f = *f;
22731 new_f.name = "farmFingerprint64".to_string();
22732 Ok(Expression::Function(Box::new(new_f)))
22733 }
22734 DialectType::Redshift => {
22735 let mut new_f = *f;
22736 new_f.name = "FARMFINGERPRINT64".to_string();
22737 Ok(Expression::Function(Box::new(new_f)))
22738 }
22739 _ => Ok(Expression::Function(f)),
22740 },
22741 // JSON_KEYS(x) -> JSON_OBJECT_KEYS(x) for Databricks/Spark, OBJECT_KEYS(x) for Snowflake
22742 "JSON_KEYS" => match target {
22743 DialectType::Databricks | DialectType::Spark => {
22744 let mut new_f = *f;
22745 new_f.name = "JSON_OBJECT_KEYS".to_string();
22746 Ok(Expression::Function(Box::new(new_f)))
22747 }
22748 DialectType::Snowflake => {
22749 let mut new_f = *f;
22750 new_f.name = "OBJECT_KEYS".to_string();
22751 Ok(Expression::Function(Box::new(new_f)))
22752 }
22753 _ => Ok(Expression::Function(f)),
22754 },
22755 // WEEKOFYEAR(x) -> WEEKISO(x) for Snowflake
22756 "WEEKOFYEAR" => match target {
22757 DialectType::Snowflake => {
22758 let mut new_f = *f;
22759 new_f.name = "WEEKISO".to_string();
22760 Ok(Expression::Function(Box::new(new_f)))
22761 }
22762 _ => Ok(Expression::Function(f)),
22763 },
22764 // FORMAT(fmt, args...) -> FORMAT_STRING(fmt, args...) for Databricks
22765 "FORMAT"
22766 if f.args.len() >= 2 && matches!(source, DialectType::Generic) =>
22767 {
22768 match target {
22769 DialectType::Databricks | DialectType::Spark => {
22770 let mut new_f = *f;
22771 new_f.name = "FORMAT_STRING".to_string();
22772 Ok(Expression::Function(Box::new(new_f)))
22773 }
22774 _ => Ok(Expression::Function(f)),
22775 }
22776 }
22777 // CONCAT_WS from Generic is null-propagating in SQLGlot fixtures.
22778 // Trino also requires non-separator arguments cast to VARCHAR.
22779 "CONCAT_WS" if f.args.len() >= 2 => {
22780 fn concat_ws_null_case(
22781 args: Vec<Expression>,
22782 else_expr: Expression,
22783 ) -> Expression {
22784 let mut null_checks = args.iter().cloned().map(|arg| {
22785 Expression::IsNull(Box::new(crate::expressions::IsNull {
22786 this: arg,
22787 not: false,
22788 postfix_form: false,
22789 }))
22790 });
22791 let first_null_check = null_checks
22792 .next()
22793 .expect("CONCAT_WS with >= 2 args must yield a null check");
22794 let null_check =
22795 null_checks.fold(first_null_check, |left, right| {
22796 Expression::Or(Box::new(BinaryOp {
22797 left,
22798 right,
22799 left_comments: Vec::new(),
22800 operator_comments: Vec::new(),
22801 trailing_comments: Vec::new(),
22802 inferred_type: None,
22803 }))
22804 });
22805 Expression::Case(Box::new(Case {
22806 operand: None,
22807 whens: vec![(null_check, Expression::Null(Null))],
22808 else_: Some(else_expr),
22809 comments: vec![],
22810 inferred_type: None,
22811 }))
22812 }
22813
22814 match target {
22815 DialectType::Trino
22816 if matches!(source, DialectType::Generic) =>
22817 {
22818 let original_args = f.args.clone();
22819 let mut args = f.args;
22820 let sep = args.remove(0);
22821 let cast_args: Vec<Expression> = args
22822 .into_iter()
22823 .map(|a| {
22824 Expression::Cast(Box::new(Cast {
22825 this: a,
22826 to: DataType::VarChar {
22827 length: None,
22828 parenthesized_length: false,
22829 },
22830 double_colon_syntax: false,
22831 trailing_comments: Vec::new(),
22832 format: None,
22833 default: None,
22834 inferred_type: None,
22835 }))
22836 })
22837 .collect();
22838 let mut new_args = vec![sep];
22839 new_args.extend(cast_args);
22840 let else_expr = Expression::Function(Box::new(
22841 Function::new("CONCAT_WS".to_string(), new_args),
22842 ));
22843 Ok(concat_ws_null_case(original_args, else_expr))
22844 }
22845 DialectType::Presto
22846 | DialectType::Trino
22847 | DialectType::Athena => {
22848 let mut args = f.args;
22849 let sep = args.remove(0);
22850 let cast_args: Vec<Expression> = args
22851 .into_iter()
22852 .map(|a| {
22853 Expression::Cast(Box::new(Cast {
22854 this: a,
22855 to: DataType::VarChar {
22856 length: None,
22857 parenthesized_length: false,
22858 },
22859 double_colon_syntax: false,
22860 trailing_comments: Vec::new(),
22861 format: None,
22862 default: None,
22863 inferred_type: None,
22864 }))
22865 })
22866 .collect();
22867 let mut new_args = vec![sep];
22868 new_args.extend(cast_args);
22869 Ok(Expression::Function(Box::new(Function::new(
22870 "CONCAT_WS".to_string(),
22871 new_args,
22872 ))))
22873 }
22874 DialectType::Spark
22875 | DialectType::Hive
22876 | DialectType::DuckDB
22877 if matches!(source, DialectType::Generic) =>
22878 {
22879 let args = f.args;
22880 let else_expr = Expression::Function(Box::new(
22881 Function::new("CONCAT_WS".to_string(), args.clone()),
22882 ));
22883 Ok(concat_ws_null_case(args, else_expr))
22884 }
22885 _ => Ok(Expression::Function(f)),
22886 }
22887 }
22888 // ARRAY_SLICE(x, start, end) -> SLICE(x, start, end) for Presto/Trino/Databricks, arraySlice for ClickHouse
22889 "ARRAY_SLICE" if f.args.len() >= 2 => match target {
22890 DialectType::DuckDB
22891 if f.args.len() == 3
22892 && matches!(source, DialectType::Snowflake) =>
22893 {
22894 // Snowflake ARRAY_SLICE (0-indexed, exclusive end)
22895 // -> DuckDB ARRAY_SLICE (1-indexed, inclusive end)
22896 let mut args = f.args;
22897 let arr = args.remove(0);
22898 let start = args.remove(0);
22899 let end = args.remove(0);
22900
22901 // CASE WHEN start >= 0 THEN start + 1 ELSE start END
22902 let adjusted_start = Expression::Case(Box::new(Case {
22903 operand: None,
22904 whens: vec![(
22905 Expression::Gte(Box::new(BinaryOp {
22906 left: start.clone(),
22907 right: Expression::number(0),
22908 left_comments: vec![],
22909 operator_comments: vec![],
22910 trailing_comments: vec![],
22911 inferred_type: None,
22912 })),
22913 Expression::Add(Box::new(BinaryOp {
22914 left: start.clone(),
22915 right: Expression::number(1),
22916 left_comments: vec![],
22917 operator_comments: vec![],
22918 trailing_comments: vec![],
22919 inferred_type: None,
22920 })),
22921 )],
22922 else_: Some(start),
22923 comments: vec![],
22924 inferred_type: None,
22925 }));
22926
22927 // CASE WHEN end < 0 THEN end - 1 ELSE end END
22928 let adjusted_end = Expression::Case(Box::new(Case {
22929 operand: None,
22930 whens: vec![(
22931 Expression::Lt(Box::new(BinaryOp {
22932 left: end.clone(),
22933 right: Expression::number(0),
22934 left_comments: vec![],
22935 operator_comments: vec![],
22936 trailing_comments: vec![],
22937 inferred_type: None,
22938 })),
22939 Expression::Sub(Box::new(BinaryOp {
22940 left: end.clone(),
22941 right: Expression::number(1),
22942 left_comments: vec![],
22943 operator_comments: vec![],
22944 trailing_comments: vec![],
22945 inferred_type: None,
22946 })),
22947 )],
22948 else_: Some(end),
22949 comments: vec![],
22950 inferred_type: None,
22951 }));
22952
22953 Ok(Expression::Function(Box::new(Function::new(
22954 "ARRAY_SLICE".to_string(),
22955 vec![arr, adjusted_start, adjusted_end],
22956 ))))
22957 }
22958 DialectType::Presto
22959 | DialectType::Trino
22960 | DialectType::Athena
22961 | DialectType::Databricks
22962 | DialectType::Spark => {
22963 let mut new_f = *f;
22964 new_f.name = "SLICE".to_string();
22965 Ok(Expression::Function(Box::new(new_f)))
22966 }
22967 DialectType::ClickHouse => {
22968 let mut new_f = *f;
22969 new_f.name = "arraySlice".to_string();
22970 Ok(Expression::Function(Box::new(new_f)))
22971 }
22972 _ => Ok(Expression::Function(f)),
22973 },
22974 // ARRAY_PREPEND(arr, x) -> LIST_PREPEND(x, arr) for DuckDB (swap args)
22975 "ARRAY_PREPEND" if f.args.len() == 2 => match target {
22976 DialectType::DuckDB => {
22977 let mut args = f.args;
22978 let arr = args.remove(0);
22979 let val = args.remove(0);
22980 Ok(Expression::Function(Box::new(Function::new(
22981 "LIST_PREPEND".to_string(),
22982 vec![val, arr],
22983 ))))
22984 }
22985 _ => Ok(Expression::Function(f)),
22986 },
22987 // ARRAY_REMOVE(arr, target) -> dialect-specific
22988 "ARRAY_REMOVE" if f.args.len() == 2 => {
22989 match target {
22990 DialectType::DuckDB => {
22991 let mut args = f.args;
22992 let arr = args.remove(0);
22993 let target_val = args.remove(0);
22994 let u_id = crate::expressions::Identifier::new("_u");
22995 // LIST_FILTER(arr, _u -> _u <> target)
22996 let lambda = Expression::Lambda(Box::new(
22997 crate::expressions::LambdaExpr {
22998 parameters: vec![u_id.clone()],
22999 body: Expression::Neq(Box::new(BinaryOp {
23000 left: Expression::Identifier(u_id),
23001 right: target_val,
23002 left_comments: Vec::new(),
23003 operator_comments: Vec::new(),
23004 trailing_comments: Vec::new(),
23005 inferred_type: None,
23006 })),
23007 colon: false,
23008 parameter_types: Vec::new(),
23009 },
23010 ));
23011 Ok(Expression::Function(Box::new(Function::new(
23012 "LIST_FILTER".to_string(),
23013 vec![arr, lambda],
23014 ))))
23015 }
23016 DialectType::ClickHouse => {
23017 let mut args = f.args;
23018 let arr = args.remove(0);
23019 let target_val = args.remove(0);
23020 let u_id = crate::expressions::Identifier::new("_u");
23021 // arrayFilter(_u -> _u <> target, arr)
23022 let lambda = Expression::Lambda(Box::new(
23023 crate::expressions::LambdaExpr {
23024 parameters: vec![u_id.clone()],
23025 body: Expression::Neq(Box::new(BinaryOp {
23026 left: Expression::Identifier(u_id),
23027 right: target_val,
23028 left_comments: Vec::new(),
23029 operator_comments: Vec::new(),
23030 trailing_comments: Vec::new(),
23031 inferred_type: None,
23032 })),
23033 colon: false,
23034 parameter_types: Vec::new(),
23035 },
23036 ));
23037 Ok(Expression::Function(Box::new(Function::new(
23038 "arrayFilter".to_string(),
23039 vec![lambda, arr],
23040 ))))
23041 }
23042 DialectType::BigQuery => {
23043 // ARRAY(SELECT _u FROM UNNEST(the_array) AS _u WHERE _u <> target)
23044 let mut args = f.args;
23045 let arr = args.remove(0);
23046 let target_val = args.remove(0);
23047 let u_id = crate::expressions::Identifier::new("_u");
23048 let u_col = Expression::Column(Box::new(
23049 crate::expressions::Column {
23050 name: u_id.clone(),
23051 table: None,
23052 join_mark: false,
23053 trailing_comments: Vec::new(),
23054 span: None,
23055 inferred_type: None,
23056 },
23057 ));
23058 // UNNEST(the_array) AS _u
23059 let unnest_expr = Expression::Unnest(Box::new(
23060 crate::expressions::UnnestFunc {
23061 this: arr,
23062 expressions: Vec::new(),
23063 with_ordinality: false,
23064 alias: None,
23065 offset_alias: None,
23066 },
23067 ));
23068 let aliased_unnest = Expression::Alias(Box::new(
23069 crate::expressions::Alias {
23070 this: unnest_expr,
23071 alias: u_id.clone(),
23072 column_aliases: Vec::new(),
23073 alias_explicit_as: false,
23074 alias_keyword: None,
23075 pre_alias_comments: Vec::new(),
23076 trailing_comments: Vec::new(),
23077 inferred_type: None,
23078 },
23079 ));
23080 // _u <> target
23081 let where_cond = Expression::Neq(Box::new(BinaryOp {
23082 left: u_col.clone(),
23083 right: target_val,
23084 left_comments: Vec::new(),
23085 operator_comments: Vec::new(),
23086 trailing_comments: Vec::new(),
23087 inferred_type: None,
23088 }));
23089 // SELECT _u FROM UNNEST(the_array) AS _u WHERE _u <> target
23090 let subquery = Expression::Select(Box::new(
23091 crate::expressions::Select::new()
23092 .column(u_col)
23093 .from(aliased_unnest)
23094 .where_(where_cond),
23095 ));
23096 // ARRAY(subquery) -- use ArrayFunc with subquery as single element
23097 Ok(Expression::ArrayFunc(Box::new(
23098 crate::expressions::ArrayConstructor {
23099 expressions: vec![subquery],
23100 bracket_notation: false,
23101 use_list_keyword: false,
23102 },
23103 )))
23104 }
23105 _ => Ok(Expression::Function(f)),
23106 }
23107 }
23108 // PARSE_JSON(str) -> remove for SQLite/Doris (just use the string literal)
23109 "PARSE_JSON" if f.args.len() == 1 => {
23110 match target {
23111 DialectType::SQLite
23112 | DialectType::Doris
23113 | DialectType::MySQL
23114 | DialectType::StarRocks => {
23115 // Strip PARSE_JSON, return the inner argument
23116 Ok(f.args.into_iter().next().unwrap())
23117 }
23118 _ => Ok(Expression::Function(f)),
23119 }
23120 }
23121 // JSON_REMOVE(PARSE_JSON(str), path...) -> for SQLite strip PARSE_JSON
23122 // This is handled by PARSE_JSON stripping above; JSON_REMOVE is passed through
23123 "JSON_REMOVE" => Ok(Expression::Function(f)),
23124 // JSON_SET(PARSE_JSON(str), path, PARSE_JSON(val)) -> for SQLite strip PARSE_JSON
23125 // This is handled by PARSE_JSON stripping above; JSON_SET is passed through
23126 "JSON_SET" => Ok(Expression::Function(f)),
23127 // DECODE(x, search1, result1, ..., default) -> CASE WHEN
23128 // Behavior per search value type:
23129 // NULL literal -> CASE WHEN x IS NULL THEN result
23130 // Literal (number, string, bool) -> CASE WHEN x = literal THEN result
23131 // Non-literal (column, expr) -> CASE WHEN x = search OR (x IS NULL AND search IS NULL) THEN result
23132 "DECODE" if f.args.len() >= 3 => {
23133 // Keep as DECODE for targets that support it natively
23134 let keep_as_decode = matches!(
23135 target,
23136 DialectType::Oracle
23137 | DialectType::Snowflake
23138 | DialectType::Redshift
23139 | DialectType::Teradata
23140 | DialectType::Spark
23141 | DialectType::Databricks
23142 );
23143 if keep_as_decode {
23144 return Ok(Expression::Function(f));
23145 }
23146
23147 let mut args = f.args;
23148 let this_expr = args.remove(0);
23149 let mut pairs = Vec::new();
23150 let mut default = None;
23151 let mut i = 0;
23152 while i + 1 < args.len() {
23153 pairs.push((args[i].clone(), args[i + 1].clone()));
23154 i += 2;
23155 }
23156 if i < args.len() {
23157 default = Some(args[i].clone());
23158 }
23159 // Helper: check if expression is a literal value
23160 fn is_literal(e: &Expression) -> bool {
23161 matches!(
23162 e,
23163 Expression::Literal(_)
23164 | Expression::Boolean(_)
23165 | Expression::Neg(_)
23166 )
23167 }
23168 let whens: Vec<(Expression, Expression)> = pairs
23169 .into_iter()
23170 .map(|(search, result)| {
23171 if matches!(&search, Expression::Null(_)) {
23172 // NULL search -> IS NULL
23173 let condition = Expression::Is(Box::new(BinaryOp {
23174 left: this_expr.clone(),
23175 right: Expression::Null(crate::expressions::Null),
23176 left_comments: Vec::new(),
23177 operator_comments: Vec::new(),
23178 trailing_comments: Vec::new(),
23179 inferred_type: None,
23180 }));
23181 (condition, result)
23182 } else if is_literal(&search) {
23183 // Literal search -> simple equality
23184 let eq = Expression::Eq(Box::new(BinaryOp {
23185 left: this_expr.clone(),
23186 right: search,
23187 left_comments: Vec::new(),
23188 operator_comments: Vec::new(),
23189 trailing_comments: Vec::new(),
23190 inferred_type: None,
23191 }));
23192 (eq, result)
23193 } else {
23194 // Non-literal (column ref, expression) -> null-safe comparison
23195 let needs_paren = matches!(
23196 &search,
23197 Expression::Eq(_)
23198 | Expression::Neq(_)
23199 | Expression::Gt(_)
23200 | Expression::Gte(_)
23201 | Expression::Lt(_)
23202 | Expression::Lte(_)
23203 );
23204 let search_for_eq = if needs_paren {
23205 Expression::Paren(Box::new(
23206 crate::expressions::Paren {
23207 this: search.clone(),
23208 trailing_comments: Vec::new(),
23209 },
23210 ))
23211 } else {
23212 search.clone()
23213 };
23214 let eq = Expression::Eq(Box::new(BinaryOp {
23215 left: this_expr.clone(),
23216 right: search_for_eq,
23217 left_comments: Vec::new(),
23218 operator_comments: Vec::new(),
23219 trailing_comments: Vec::new(),
23220 inferred_type: None,
23221 }));
23222 let search_for_null = if needs_paren {
23223 Expression::Paren(Box::new(
23224 crate::expressions::Paren {
23225 this: search.clone(),
23226 trailing_comments: Vec::new(),
23227 },
23228 ))
23229 } else {
23230 search.clone()
23231 };
23232 let x_is_null = Expression::Is(Box::new(BinaryOp {
23233 left: this_expr.clone(),
23234 right: Expression::Null(crate::expressions::Null),
23235 left_comments: Vec::new(),
23236 operator_comments: Vec::new(),
23237 trailing_comments: Vec::new(),
23238 inferred_type: None,
23239 }));
23240 let s_is_null = Expression::Is(Box::new(BinaryOp {
23241 left: search_for_null,
23242 right: Expression::Null(crate::expressions::Null),
23243 left_comments: Vec::new(),
23244 operator_comments: Vec::new(),
23245 trailing_comments: Vec::new(),
23246 inferred_type: None,
23247 }));
23248 let both_null = Expression::And(Box::new(BinaryOp {
23249 left: x_is_null,
23250 right: s_is_null,
23251 left_comments: Vec::new(),
23252 operator_comments: Vec::new(),
23253 trailing_comments: Vec::new(),
23254 inferred_type: None,
23255 }));
23256 let condition = Expression::Or(Box::new(BinaryOp {
23257 left: eq,
23258 right: Expression::Paren(Box::new(
23259 crate::expressions::Paren {
23260 this: both_null,
23261 trailing_comments: Vec::new(),
23262 },
23263 )),
23264 left_comments: Vec::new(),
23265 operator_comments: Vec::new(),
23266 trailing_comments: Vec::new(),
23267 inferred_type: None,
23268 }));
23269 (condition, result)
23270 }
23271 })
23272 .collect();
23273 Ok(Expression::Case(Box::new(Case {
23274 operand: None,
23275 whens,
23276 else_: default,
23277 comments: Vec::new(),
23278 inferred_type: None,
23279 })))
23280 }
23281 // LEVENSHTEIN(a, b, ...) -> dialect-specific
23282 "LEVENSHTEIN" => {
23283 match target {
23284 DialectType::BigQuery => {
23285 let mut new_f = *f;
23286 new_f.name = "EDIT_DISTANCE".to_string();
23287 Ok(Expression::Function(Box::new(new_f)))
23288 }
23289 DialectType::Drill => {
23290 let mut new_f = *f;
23291 new_f.name = "LEVENSHTEIN_DISTANCE".to_string();
23292 Ok(Expression::Function(Box::new(new_f)))
23293 }
23294 DialectType::PostgreSQL if f.args.len() == 6 => {
23295 // PostgreSQL: LEVENSHTEIN(src, tgt, ins, del, sub, max_d) -> LEVENSHTEIN_LESS_EQUAL
23296 // 2 args: basic, 5 args: with costs, 6 args: with costs + max_distance
23297 let mut new_f = *f;
23298 new_f.name = "LEVENSHTEIN_LESS_EQUAL".to_string();
23299 Ok(Expression::Function(Box::new(new_f)))
23300 }
23301 _ => Ok(Expression::Function(f)),
23302 }
23303 }
23304 // ARRAY_MAX(x) -> arrayMax(x) for ClickHouse, LIST_MAX(x) for DuckDB
23305 "ARRAY_MAX" => {
23306 let name = match target {
23307 DialectType::ClickHouse => "arrayMax",
23308 DialectType::DuckDB => "LIST_MAX",
23309 _ => "ARRAY_MAX",
23310 };
23311 let mut new_f = *f;
23312 new_f.name = name.to_string();
23313 Ok(Expression::Function(Box::new(new_f)))
23314 }
23315 // ARRAY_MIN(x) -> arrayMin(x) for ClickHouse, LIST_MIN(x) for DuckDB
23316 "ARRAY_MIN" => {
23317 let name = match target {
23318 DialectType::ClickHouse => "arrayMin",
23319 DialectType::DuckDB => "LIST_MIN",
23320 _ => "ARRAY_MIN",
23321 };
23322 let mut new_f = *f;
23323 new_f.name = name.to_string();
23324 Ok(Expression::Function(Box::new(new_f)))
23325 }
23326 // JAROWINKLER_SIMILARITY(a, b) -> jaroWinklerSimilarity(UPPER(a), UPPER(b)) for ClickHouse
23327 // -> JARO_WINKLER_SIMILARITY(UPPER(a), UPPER(b)) for DuckDB
23328 "JAROWINKLER_SIMILARITY" if f.args.len() == 2 => {
23329 let mut args = f.args;
23330 let b = args.pop().unwrap();
23331 let a = args.pop().unwrap();
23332 match target {
23333 DialectType::ClickHouse => {
23334 let upper_a = Expression::Upper(Box::new(
23335 crate::expressions::UnaryFunc::new(a),
23336 ));
23337 let upper_b = Expression::Upper(Box::new(
23338 crate::expressions::UnaryFunc::new(b),
23339 ));
23340 Ok(Expression::Function(Box::new(Function::new(
23341 "jaroWinklerSimilarity".to_string(),
23342 vec![upper_a, upper_b],
23343 ))))
23344 }
23345 DialectType::DuckDB => {
23346 let upper_a = Expression::Upper(Box::new(
23347 crate::expressions::UnaryFunc::new(a),
23348 ));
23349 let upper_b = Expression::Upper(Box::new(
23350 crate::expressions::UnaryFunc::new(b),
23351 ));
23352 let score = Expression::Function(Box::new(Function::new(
23353 "JARO_WINKLER_SIMILARITY".to_string(),
23354 vec![upper_a, upper_b],
23355 )));
23356 let scaled = Expression::Mul(Box::new(BinaryOp {
23357 left: score,
23358 right: Expression::number(100),
23359 left_comments: Vec::new(),
23360 operator_comments: Vec::new(),
23361 trailing_comments: Vec::new(),
23362 inferred_type: None,
23363 }));
23364 Ok(Expression::Cast(Box::new(Cast {
23365 this: scaled,
23366 to: DataType::Int {
23367 length: None,
23368 integer_spelling: false,
23369 },
23370 trailing_comments: Vec::new(),
23371 double_colon_syntax: false,
23372 format: None,
23373 default: None,
23374 inferred_type: None,
23375 })))
23376 }
23377 _ => Ok(Expression::Function(Box::new(Function::new(
23378 "JAROWINKLER_SIMILARITY".to_string(),
23379 vec![a, b],
23380 )))),
23381 }
23382 }
23383 // CURRENT_SCHEMAS(x) -> CURRENT_SCHEMAS() for Snowflake (drop arg)
23384 "CURRENT_SCHEMAS" => match target {
23385 DialectType::Snowflake => Ok(Expression::Function(Box::new(
23386 Function::new("CURRENT_SCHEMAS".to_string(), vec![]),
23387 ))),
23388 _ => Ok(Expression::Function(f)),
23389 },
23390 // TRUNC/TRUNCATE (numeric) -> dialect-specific
23391 "TRUNC" | "TRUNCATE" if f.args.len() <= 2 => {
23392 match target {
23393 DialectType::TSQL | DialectType::Fabric => {
23394 // ROUND(x, decimals, 1) - the 1 flag means truncation
23395 let mut args = f.args;
23396 let this = if args.is_empty() {
23397 return Ok(Expression::Function(Box::new(
23398 Function::new("TRUNC".to_string(), args),
23399 )));
23400 } else {
23401 args.remove(0)
23402 };
23403 let decimals = if args.is_empty() {
23404 Expression::Literal(Box::new(Literal::Number(
23405 "0".to_string(),
23406 )))
23407 } else {
23408 args.remove(0)
23409 };
23410 Ok(Expression::Function(Box::new(Function::new(
23411 "ROUND".to_string(),
23412 vec![
23413 this,
23414 decimals,
23415 Expression::Literal(Box::new(Literal::Number(
23416 "1".to_string(),
23417 ))),
23418 ],
23419 ))))
23420 }
23421 DialectType::Presto
23422 | DialectType::Trino
23423 | DialectType::Athena => {
23424 // TRUNCATE(x, decimals)
23425 let mut new_f = *f;
23426 new_f.name = "TRUNCATE".to_string();
23427 Ok(Expression::Function(Box::new(new_f)))
23428 }
23429 DialectType::MySQL
23430 | DialectType::SingleStore
23431 | DialectType::TiDB => {
23432 // TRUNCATE(x, decimals)
23433 let mut new_f = *f;
23434 new_f.name = "TRUNCATE".to_string();
23435 Ok(Expression::Function(Box::new(new_f)))
23436 }
23437 DialectType::DuckDB => {
23438 // DuckDB supports TRUNC(x, decimals) — preserve both args
23439 let mut args = f.args;
23440 // Snowflake fractions_supported: wrap non-INT decimals in CAST(... AS INT)
23441 if args.len() == 2
23442 && matches!(source, DialectType::Snowflake)
23443 {
23444 let decimals = args.remove(1);
23445 let is_int = matches!(&decimals, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)))
23446 || matches!(&decimals, Expression::Cast(c) if matches!(c.to, DataType::Int { .. } | DataType::SmallInt { .. } | DataType::BigInt { .. } | DataType::TinyInt { .. }));
23447 let wrapped = if !is_int {
23448 Expression::Cast(Box::new(
23449 crate::expressions::Cast {
23450 this: decimals,
23451 to: DataType::Int {
23452 length: None,
23453 integer_spelling: false,
23454 },
23455 double_colon_syntax: false,
23456 trailing_comments: Vec::new(),
23457 format: None,
23458 default: None,
23459 inferred_type: None,
23460 },
23461 ))
23462 } else {
23463 decimals
23464 };
23465 args.push(wrapped);
23466 }
23467 Ok(Expression::Function(Box::new(Function::new(
23468 "TRUNC".to_string(),
23469 args,
23470 ))))
23471 }
23472 DialectType::ClickHouse => {
23473 // trunc(x, decimals) - lowercase
23474 let mut new_f = *f;
23475 new_f.name = "trunc".to_string();
23476 Ok(Expression::Function(Box::new(new_f)))
23477 }
23478 DialectType::Spark | DialectType::Databricks => {
23479 // Spark: TRUNC is date-only; numeric TRUNC → CAST(x AS BIGINT)
23480 let this = f.args.into_iter().next().unwrap_or(
23481 Expression::Literal(Box::new(Literal::Number(
23482 "0".to_string(),
23483 ))),
23484 );
23485 Ok(Expression::Cast(Box::new(crate::expressions::Cast {
23486 this,
23487 to: crate::expressions::DataType::BigInt {
23488 length: None,
23489 },
23490 double_colon_syntax: false,
23491 trailing_comments: Vec::new(),
23492 format: None,
23493 default: None,
23494 inferred_type: None,
23495 })))
23496 }
23497 _ => {
23498 // TRUNC(x, decimals) for PostgreSQL, Oracle, Snowflake, etc.
23499 let mut new_f = *f;
23500 new_f.name = "TRUNC".to_string();
23501 Ok(Expression::Function(Box::new(new_f)))
23502 }
23503 }
23504 }
23505 // CURRENT_VERSION() -> VERSION() for most dialects
23506 "CURRENT_VERSION" => match target {
23507 DialectType::Snowflake
23508 | DialectType::Databricks
23509 | DialectType::StarRocks => Ok(Expression::Function(f)),
23510 DialectType::SQLite => {
23511 let mut new_f = *f;
23512 new_f.name = "SQLITE_VERSION".to_string();
23513 Ok(Expression::Function(Box::new(new_f)))
23514 }
23515 _ => {
23516 let mut new_f = *f;
23517 new_f.name = "VERSION".to_string();
23518 Ok(Expression::Function(Box::new(new_f)))
23519 }
23520 },
23521 // ARRAY_REVERSE(x) -> arrayReverse(x) for ClickHouse
23522 "ARRAY_REVERSE" => match target {
23523 DialectType::ClickHouse => {
23524 let mut new_f = *f;
23525 new_f.name = "arrayReverse".to_string();
23526 Ok(Expression::Function(Box::new(new_f)))
23527 }
23528 _ => Ok(Expression::Function(f)),
23529 },
23530 // GENERATE_DATE_ARRAY(start, end[, step]) -> target-specific
23531 "GENERATE_DATE_ARRAY" => {
23532 let mut args = f.args;
23533 if matches!(target, DialectType::BigQuery) {
23534 // BigQuery keeps GENERATE_DATE_ARRAY; add default interval if not present
23535 if args.len() == 2 {
23536 let default_interval = Expression::Interval(Box::new(
23537 crate::expressions::Interval {
23538 this: Some(Expression::Literal(Box::new(
23539 Literal::String("1".to_string()),
23540 ))),
23541 unit: Some(
23542 crate::expressions::IntervalUnitSpec::Simple {
23543 unit: crate::expressions::IntervalUnit::Day,
23544 use_plural: false,
23545 },
23546 ),
23547 },
23548 ));
23549 args.push(default_interval);
23550 }
23551 Ok(Expression::Function(Box::new(Function::new(
23552 "GENERATE_DATE_ARRAY".to_string(),
23553 args,
23554 ))))
23555 } else if matches!(target, DialectType::DuckDB) {
23556 // DuckDB: CAST(GENERATE_SERIES(start, end, step) AS DATE[])
23557 let start = args.get(0).cloned();
23558 let end = args.get(1).cloned();
23559 let step = args.get(2).cloned().or_else(|| {
23560 Some(Expression::Interval(Box::new(
23561 crate::expressions::Interval {
23562 this: Some(Expression::Literal(Box::new(
23563 Literal::String("1".to_string()),
23564 ))),
23565 unit: Some(
23566 crate::expressions::IntervalUnitSpec::Simple {
23567 unit: crate::expressions::IntervalUnit::Day,
23568 use_plural: false,
23569 },
23570 ),
23571 },
23572 )))
23573 });
23574 let gen_series = Expression::GenerateSeries(Box::new(
23575 crate::expressions::GenerateSeries {
23576 start: start.map(Box::new),
23577 end: end.map(Box::new),
23578 step: step.map(Box::new),
23579 is_end_exclusive: None,
23580 },
23581 ));
23582 Ok(Expression::Cast(Box::new(Cast {
23583 this: gen_series,
23584 to: DataType::Array {
23585 element_type: Box::new(DataType::Date),
23586 dimension: None,
23587 },
23588 trailing_comments: vec![],
23589 double_colon_syntax: false,
23590 format: None,
23591 default: None,
23592 inferred_type: None,
23593 })))
23594 } else if matches!(
23595 target,
23596 DialectType::Presto | DialectType::Trino | DialectType::Athena
23597 ) {
23598 // Presto/Trino: SEQUENCE(start, end, interval) with interval normalization
23599 let start = args.get(0).cloned();
23600 let end = args.get(1).cloned();
23601 let step = args.get(2).cloned().or_else(|| {
23602 Some(Expression::Interval(Box::new(
23603 crate::expressions::Interval {
23604 this: Some(Expression::Literal(Box::new(
23605 Literal::String("1".to_string()),
23606 ))),
23607 unit: Some(
23608 crate::expressions::IntervalUnitSpec::Simple {
23609 unit: crate::expressions::IntervalUnit::Day,
23610 use_plural: false,
23611 },
23612 ),
23613 },
23614 )))
23615 });
23616 let gen_series = Expression::GenerateSeries(Box::new(
23617 crate::expressions::GenerateSeries {
23618 start: start.map(Box::new),
23619 end: end.map(Box::new),
23620 step: step.map(Box::new),
23621 is_end_exclusive: None,
23622 },
23623 ));
23624 Ok(gen_series)
23625 } else if matches!(
23626 target,
23627 DialectType::Spark | DialectType::Databricks
23628 ) {
23629 // Spark/Databricks: SEQUENCE(start, end, step) - keep step as-is
23630 let start = args.get(0).cloned();
23631 let end = args.get(1).cloned();
23632 let step = args.get(2).cloned().or_else(|| {
23633 Some(Expression::Interval(Box::new(
23634 crate::expressions::Interval {
23635 this: Some(Expression::Literal(Box::new(
23636 Literal::String("1".to_string()),
23637 ))),
23638 unit: Some(
23639 crate::expressions::IntervalUnitSpec::Simple {
23640 unit: crate::expressions::IntervalUnit::Day,
23641 use_plural: false,
23642 },
23643 ),
23644 },
23645 )))
23646 });
23647 let gen_series = Expression::GenerateSeries(Box::new(
23648 crate::expressions::GenerateSeries {
23649 start: start.map(Box::new),
23650 end: end.map(Box::new),
23651 step: step.map(Box::new),
23652 is_end_exclusive: None,
23653 },
23654 ));
23655 Ok(gen_series)
23656 } else if matches!(target, DialectType::Snowflake) {
23657 // Snowflake: keep as GENERATE_DATE_ARRAY for later transform
23658 if args.len() == 2 {
23659 let default_interval = Expression::Interval(Box::new(
23660 crate::expressions::Interval {
23661 this: Some(Expression::Literal(Box::new(
23662 Literal::String("1".to_string()),
23663 ))),
23664 unit: Some(
23665 crate::expressions::IntervalUnitSpec::Simple {
23666 unit: crate::expressions::IntervalUnit::Day,
23667 use_plural: false,
23668 },
23669 ),
23670 },
23671 ));
23672 args.push(default_interval);
23673 }
23674 Ok(Expression::Function(Box::new(Function::new(
23675 "GENERATE_DATE_ARRAY".to_string(),
23676 args,
23677 ))))
23678 } else if matches!(
23679 target,
23680 DialectType::MySQL
23681 | DialectType::TSQL
23682 | DialectType::Fabric
23683 | DialectType::Redshift
23684 ) {
23685 // MySQL/TSQL/Redshift: keep as GENERATE_DATE_ARRAY for the preprocess
23686 // step (unnest_generate_date_array_using_recursive_cte) to convert to CTE
23687 Ok(Expression::Function(Box::new(Function::new(
23688 "GENERATE_DATE_ARRAY".to_string(),
23689 args,
23690 ))))
23691 } else {
23692 // PostgreSQL/others: convert to GenerateSeries
23693 let start = args.get(0).cloned();
23694 let end = args.get(1).cloned();
23695 let step = args.get(2).cloned().or_else(|| {
23696 Some(Expression::Interval(Box::new(
23697 crate::expressions::Interval {
23698 this: Some(Expression::Literal(Box::new(
23699 Literal::String("1".to_string()),
23700 ))),
23701 unit: Some(
23702 crate::expressions::IntervalUnitSpec::Simple {
23703 unit: crate::expressions::IntervalUnit::Day,
23704 use_plural: false,
23705 },
23706 ),
23707 },
23708 )))
23709 });
23710 Ok(Expression::GenerateSeries(Box::new(
23711 crate::expressions::GenerateSeries {
23712 start: start.map(Box::new),
23713 end: end.map(Box::new),
23714 step: step.map(Box::new),
23715 is_end_exclusive: None,
23716 },
23717 )))
23718 }
23719 }
23720 // ARRAYS_OVERLAP(arr1, arr2) from Snowflake -> DuckDB:
23721 // (arr1 && arr2) OR (ARRAY_LENGTH(arr1) <> LIST_COUNT(arr1) AND ARRAY_LENGTH(arr2) <> LIST_COUNT(arr2))
23722 "ARRAYS_OVERLAP"
23723 if f.args.len() == 2
23724 && matches!(source, DialectType::Snowflake)
23725 && matches!(target, DialectType::DuckDB) =>
23726 {
23727 let mut args = f.args;
23728 let arr1 = args.remove(0);
23729 let arr2 = args.remove(0);
23730
23731 // (arr1 && arr2)
23732 let overlap = Expression::Paren(Box::new(Paren {
23733 this: Expression::ArrayOverlaps(Box::new(BinaryOp {
23734 left: arr1.clone(),
23735 right: arr2.clone(),
23736 left_comments: vec![],
23737 operator_comments: vec![],
23738 trailing_comments: vec![],
23739 inferred_type: None,
23740 })),
23741 trailing_comments: vec![],
23742 }));
23743
23744 // ARRAY_LENGTH(arr1) <> LIST_COUNT(arr1)
23745 let arr1_has_null = Expression::Neq(Box::new(BinaryOp {
23746 left: Expression::Function(Box::new(Function::new(
23747 "ARRAY_LENGTH".to_string(),
23748 vec![arr1.clone()],
23749 ))),
23750 right: Expression::Function(Box::new(Function::new(
23751 "LIST_COUNT".to_string(),
23752 vec![arr1],
23753 ))),
23754 left_comments: vec![],
23755 operator_comments: vec![],
23756 trailing_comments: vec![],
23757 inferred_type: None,
23758 }));
23759
23760 // ARRAY_LENGTH(arr2) <> LIST_COUNT(arr2)
23761 let arr2_has_null = Expression::Neq(Box::new(BinaryOp {
23762 left: Expression::Function(Box::new(Function::new(
23763 "ARRAY_LENGTH".to_string(),
23764 vec![arr2.clone()],
23765 ))),
23766 right: Expression::Function(Box::new(Function::new(
23767 "LIST_COUNT".to_string(),
23768 vec![arr2],
23769 ))),
23770 left_comments: vec![],
23771 operator_comments: vec![],
23772 trailing_comments: vec![],
23773 inferred_type: None,
23774 }));
23775
23776 // (ARRAY_LENGTH(arr1) <> LIST_COUNT(arr1) AND ARRAY_LENGTH(arr2) <> LIST_COUNT(arr2))
23777 let null_check = Expression::Paren(Box::new(Paren {
23778 this: Expression::And(Box::new(BinaryOp {
23779 left: arr1_has_null,
23780 right: arr2_has_null,
23781 left_comments: vec![],
23782 operator_comments: vec![],
23783 trailing_comments: vec![],
23784 inferred_type: None,
23785 })),
23786 trailing_comments: vec![],
23787 }));
23788
23789 // (arr1 && arr2) OR (null_check)
23790 Ok(Expression::Or(Box::new(BinaryOp {
23791 left: overlap,
23792 right: null_check,
23793 left_comments: vec![],
23794 operator_comments: vec![],
23795 trailing_comments: vec![],
23796 inferred_type: None,
23797 })))
23798 }
23799 // ARRAY_INTERSECTION([1, 2], [2, 3]) from Snowflake -> DuckDB:
23800 // Bag semantics using LIST_TRANSFORM/LIST_FILTER with GENERATE_SERIES
23801 "ARRAY_INTERSECTION"
23802 if f.args.len() == 2
23803 && matches!(source, DialectType::Snowflake)
23804 && matches!(target, DialectType::DuckDB) =>
23805 {
23806 let mut args = f.args;
23807 let arr1 = args.remove(0);
23808 let arr2 = args.remove(0);
23809
23810 // Build: arr1 IS NULL
23811 let arr1_is_null = Expression::IsNull(Box::new(IsNull {
23812 this: arr1.clone(),
23813 not: false,
23814 postfix_form: false,
23815 }));
23816 let arr2_is_null = Expression::IsNull(Box::new(IsNull {
23817 this: arr2.clone(),
23818 not: false,
23819 postfix_form: false,
23820 }));
23821 let null_check = Expression::Or(Box::new(BinaryOp {
23822 left: arr1_is_null,
23823 right: arr2_is_null,
23824 left_comments: vec![],
23825 operator_comments: vec![],
23826 trailing_comments: vec![],
23827 inferred_type: None,
23828 }));
23829
23830 // GENERATE_SERIES(1, LENGTH(arr1))
23831 let gen_series = Expression::Function(Box::new(Function::new(
23832 "GENERATE_SERIES".to_string(),
23833 vec![
23834 Expression::number(1),
23835 Expression::Function(Box::new(Function::new(
23836 "LENGTH".to_string(),
23837 vec![arr1.clone()],
23838 ))),
23839 ],
23840 )));
23841
23842 // LIST_ZIP(arr1, GENERATE_SERIES(1, LENGTH(arr1)))
23843 let list_zip = Expression::Function(Box::new(Function::new(
23844 "LIST_ZIP".to_string(),
23845 vec![arr1.clone(), gen_series],
23846 )));
23847
23848 // pair[1] and pair[2]
23849 let pair_col = Expression::column("pair");
23850 let pair_1 = Expression::Subscript(Box::new(
23851 crate::expressions::Subscript {
23852 this: pair_col.clone(),
23853 index: Expression::number(1),
23854 },
23855 ));
23856 let pair_2 = Expression::Subscript(Box::new(
23857 crate::expressions::Subscript {
23858 this: pair_col.clone(),
23859 index: Expression::number(2),
23860 },
23861 ));
23862
23863 // arr1[1:pair[2]]
23864 let arr1_slice = Expression::ArraySlice(Box::new(
23865 crate::expressions::ArraySlice {
23866 this: arr1.clone(),
23867 start: Some(Expression::number(1)),
23868 end: Some(pair_2),
23869 },
23870 ));
23871
23872 // e IS NOT DISTINCT FROM pair[1]
23873 let e_col = Expression::column("e");
23874 let is_not_distinct = Expression::NullSafeEq(Box::new(BinaryOp {
23875 left: e_col.clone(),
23876 right: pair_1.clone(),
23877 left_comments: vec![],
23878 operator_comments: vec![],
23879 trailing_comments: vec![],
23880 inferred_type: None,
23881 }));
23882
23883 // e -> e IS NOT DISTINCT FROM pair[1]
23884 let inner_lambda1 =
23885 Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
23886 parameters: vec![crate::expressions::Identifier::new("e")],
23887 body: is_not_distinct,
23888 colon: false,
23889 parameter_types: vec![],
23890 }));
23891
23892 // LIST_FILTER(arr1[1:pair[2]], e -> e IS NOT DISTINCT FROM pair[1])
23893 let inner_filter1 = Expression::Function(Box::new(Function::new(
23894 "LIST_FILTER".to_string(),
23895 vec![arr1_slice, inner_lambda1],
23896 )));
23897
23898 // LENGTH(LIST_FILTER(arr1[1:pair[2]], ...))
23899 let len1 = Expression::Function(Box::new(Function::new(
23900 "LENGTH".to_string(),
23901 vec![inner_filter1],
23902 )));
23903
23904 // e -> e IS NOT DISTINCT FROM pair[1]
23905 let inner_lambda2 =
23906 Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
23907 parameters: vec![crate::expressions::Identifier::new("e")],
23908 body: Expression::NullSafeEq(Box::new(BinaryOp {
23909 left: e_col,
23910 right: pair_1.clone(),
23911 left_comments: vec![],
23912 operator_comments: vec![],
23913 trailing_comments: vec![],
23914 inferred_type: None,
23915 })),
23916 colon: false,
23917 parameter_types: vec![],
23918 }));
23919
23920 // LIST_FILTER(arr2, e -> e IS NOT DISTINCT FROM pair[1])
23921 let inner_filter2 = Expression::Function(Box::new(Function::new(
23922 "LIST_FILTER".to_string(),
23923 vec![arr2.clone(), inner_lambda2],
23924 )));
23925
23926 // LENGTH(LIST_FILTER(arr2, ...))
23927 let len2 = Expression::Function(Box::new(Function::new(
23928 "LENGTH".to_string(),
23929 vec![inner_filter2],
23930 )));
23931
23932 // LENGTH(...) <= LENGTH(...)
23933 let cond = Expression::Paren(Box::new(Paren {
23934 this: Expression::Lte(Box::new(BinaryOp {
23935 left: len1,
23936 right: len2,
23937 left_comments: vec![],
23938 operator_comments: vec![],
23939 trailing_comments: vec![],
23940 inferred_type: None,
23941 })),
23942 trailing_comments: vec![],
23943 }));
23944
23945 // pair -> (condition)
23946 let filter_lambda =
23947 Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
23948 parameters: vec![crate::expressions::Identifier::new(
23949 "pair",
23950 )],
23951 body: cond,
23952 colon: false,
23953 parameter_types: vec![],
23954 }));
23955
23956 // LIST_FILTER(LIST_ZIP(...), pair -> ...)
23957 let outer_filter = Expression::Function(Box::new(Function::new(
23958 "LIST_FILTER".to_string(),
23959 vec![list_zip, filter_lambda],
23960 )));
23961
23962 // pair -> pair[1]
23963 let transform_lambda =
23964 Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
23965 parameters: vec![crate::expressions::Identifier::new(
23966 "pair",
23967 )],
23968 body: pair_1,
23969 colon: false,
23970 parameter_types: vec![],
23971 }));
23972
23973 // LIST_TRANSFORM(LIST_FILTER(...), pair -> pair[1])
23974 let list_transform = Expression::Function(Box::new(Function::new(
23975 "LIST_TRANSFORM".to_string(),
23976 vec![outer_filter, transform_lambda],
23977 )));
23978
23979 // CASE WHEN arr1 IS NULL OR arr2 IS NULL THEN NULL
23980 // ELSE LIST_TRANSFORM(LIST_FILTER(...), pair -> pair[1])
23981 // END
23982 Ok(Expression::Case(Box::new(Case {
23983 operand: None,
23984 whens: vec![(null_check, Expression::Null(Null))],
23985 else_: Some(list_transform),
23986 comments: vec![],
23987 inferred_type: None,
23988 })))
23989 }
23990 // ARRAY_CONSTRUCT(args) -> Expression::Array for all targets
23991 "ARRAY_CONSTRUCT" => {
23992 if matches!(target, DialectType::Snowflake) {
23993 Ok(Expression::Function(f))
23994 } else {
23995 Ok(Expression::Array(Box::new(crate::expressions::Array {
23996 expressions: f.args,
23997 })))
23998 }
23999 }
24000 // ARRAY(args) function -> Expression::Array for DuckDB/Snowflake/Presto/Trino/Athena
24001 "ARRAY"
24002 if !f.args.iter().any(|a| {
24003 matches!(a, Expression::Select(_) | Expression::Subquery(_))
24004 }) =>
24005 {
24006 match target {
24007 DialectType::DuckDB
24008 | DialectType::Snowflake
24009 | DialectType::Presto
24010 | DialectType::Trino
24011 | DialectType::Athena => {
24012 Ok(Expression::Array(Box::new(crate::expressions::Array {
24013 expressions: f.args,
24014 })))
24015 }
24016 _ => Ok(Expression::Function(f)),
24017 }
24018 }
24019 _ => Ok(Expression::Function(f)),
24020 }
24021 } else if let Expression::AggregateFunction(mut af) = e {
24022 let name = af.name.to_ascii_uppercase();
24023 match name.as_str() {
24024 "ARBITRARY" if af.args.len() == 1 => {
24025 let arg = af.args.into_iter().next().unwrap();
24026 Ok(convert_arbitrary(arg, target))
24027 }
24028 "JSON_ARRAYAGG" => {
24029 match target {
24030 DialectType::PostgreSQL => {
24031 af.name = "JSON_AGG".to_string();
24032 // Add NULLS FIRST to ORDER BY items for PostgreSQL
24033 for ordered in af.order_by.iter_mut() {
24034 if ordered.nulls_first.is_none() {
24035 ordered.nulls_first = Some(true);
24036 }
24037 }
24038 Ok(Expression::AggregateFunction(af))
24039 }
24040 _ => Ok(Expression::AggregateFunction(af)),
24041 }
24042 }
24043 _ => Ok(Expression::AggregateFunction(af)),
24044 }
24045 } else if let Expression::JSONArrayAgg(ja) = e {
24046 // JSONArrayAgg -> JSON_AGG for PostgreSQL, JSON_ARRAYAGG for others
24047 match target {
24048 DialectType::PostgreSQL => {
24049 let mut order_by = Vec::new();
24050 if let Some(order_expr) = ja.order {
24051 if let Expression::OrderBy(ob) = *order_expr {
24052 for mut ordered in ob.expressions {
24053 if ordered.nulls_first.is_none() {
24054 ordered.nulls_first = Some(true);
24055 }
24056 order_by.push(ordered);
24057 }
24058 }
24059 }
24060 Ok(Expression::AggregateFunction(Box::new(
24061 crate::expressions::AggregateFunction {
24062 name: "JSON_AGG".to_string(),
24063 args: vec![*ja.this],
24064 distinct: false,
24065 filter: None,
24066 order_by,
24067 limit: None,
24068 ignore_nulls: None,
24069 inferred_type: None,
24070 },
24071 )))
24072 }
24073 _ => Ok(Expression::JSONArrayAgg(ja)),
24074 }
24075 } else if let Expression::JSONArray(ja) = e {
24076 match target {
24077 DialectType::Snowflake
24078 if ja.null_handling.is_none()
24079 && ja.return_type.is_none()
24080 && ja.strict.is_none() =>
24081 {
24082 let array_construct = Expression::ArrayFunc(Box::new(
24083 crate::expressions::ArrayConstructor {
24084 expressions: ja.expressions,
24085 bracket_notation: false,
24086 use_list_keyword: false,
24087 },
24088 ));
24089 Ok(Expression::Function(Box::new(Function::new(
24090 "TO_VARIANT".to_string(),
24091 vec![array_construct],
24092 ))))
24093 }
24094 _ => Ok(Expression::JSONArray(ja)),
24095 }
24096 } else if let Expression::JsonArray(f) = e {
24097 match target {
24098 DialectType::Snowflake => {
24099 let array_construct = Expression::ArrayFunc(Box::new(
24100 crate::expressions::ArrayConstructor {
24101 expressions: f.expressions,
24102 bracket_notation: false,
24103 use_list_keyword: false,
24104 },
24105 ));
24106 Ok(Expression::Function(Box::new(Function::new(
24107 "TO_VARIANT".to_string(),
24108 vec![array_construct],
24109 ))))
24110 }
24111 _ => Ok(Expression::JsonArray(f)),
24112 }
24113 } else if let Expression::CombinedParameterizedAgg(cpa) = e {
24114 let function_name = match cpa.this.as_ref() {
24115 Expression::Identifier(ident) => Some(ident.name.as_str()),
24116 _ => None,
24117 };
24118 match function_name {
24119 Some(name)
24120 if name.eq_ignore_ascii_case("groupConcat")
24121 && cpa.expressions.len() == 1 =>
24122 {
24123 match target {
24124 DialectType::MySQL | DialectType::SingleStore => {
24125 let this = cpa.expressions[0].clone();
24126 let separator = cpa.params.first().cloned();
24127 Ok(Expression::GroupConcat(Box::new(
24128 crate::expressions::GroupConcatFunc {
24129 this,
24130 separator,
24131 order_by: None,
24132 distinct: false,
24133 filter: None,
24134 limit: None,
24135 inferred_type: None,
24136 },
24137 )))
24138 }
24139 DialectType::DuckDB => Ok(Expression::ListAgg(Box::new({
24140 let this = cpa.expressions[0].clone();
24141 let separator = cpa.params.first().cloned();
24142 crate::expressions::ListAggFunc {
24143 this,
24144 separator,
24145 on_overflow: None,
24146 order_by: None,
24147 distinct: false,
24148 filter: None,
24149 inferred_type: None,
24150 }
24151 }))),
24152 _ => Ok(Expression::CombinedParameterizedAgg(cpa)),
24153 }
24154 }
24155 _ => Ok(Expression::CombinedParameterizedAgg(cpa)),
24156 }
24157 } else if let Expression::ToNumber(tn) = e {
24158 // TO_NUMBER(x) with no format/precision/scale -> CAST(x AS DOUBLE)
24159 let arg = *tn.this;
24160 Ok(Expression::Cast(Box::new(crate::expressions::Cast {
24161 this: arg,
24162 to: crate::expressions::DataType::Double {
24163 precision: None,
24164 scale: None,
24165 },
24166 double_colon_syntax: false,
24167 trailing_comments: Vec::new(),
24168 format: None,
24169 default: None,
24170 inferred_type: None,
24171 })))
24172 } else {
24173 Ok(e)
24174 }
24175 }
24176
24177 Action::RegexpLikeToDuckDB => {
24178 if let Expression::RegexpLike(f) = e {
24179 let mut args = vec![f.this, f.pattern];
24180 if let Some(flags) = f.flags {
24181 args.push(flags);
24182 }
24183 Ok(Expression::Function(Box::new(Function::new(
24184 "REGEXP_MATCHES".to_string(),
24185 args,
24186 ))))
24187 } else {
24188 Ok(e)
24189 }
24190 }
24191 Action::EpochConvert => {
24192 if let Expression::Epoch(f) = e {
24193 let arg = f.this;
24194 let name = match target {
24195 DialectType::Spark | DialectType::Databricks | DialectType::Hive => {
24196 "UNIX_TIMESTAMP"
24197 }
24198 DialectType::Presto | DialectType::Trino => "TO_UNIXTIME",
24199 DialectType::BigQuery => "TIME_TO_UNIX",
24200 _ => "EPOCH",
24201 };
24202 Ok(Expression::Function(Box::new(Function::new(
24203 name.to_string(),
24204 vec![arg],
24205 ))))
24206 } else {
24207 Ok(e)
24208 }
24209 }
24210 Action::EpochMsConvert => {
24211 use crate::expressions::{BinaryOp, Cast};
24212 if let Expression::EpochMs(f) = e {
24213 let arg = f.this;
24214 match target {
24215 DialectType::Spark | DialectType::Databricks => {
24216 Ok(Expression::Function(Box::new(Function::new(
24217 "TIMESTAMP_MILLIS".to_string(),
24218 vec![arg],
24219 ))))
24220 }
24221 DialectType::BigQuery => Ok(Expression::Function(Box::new(
24222 Function::new("TIMESTAMP_MILLIS".to_string(), vec![arg]),
24223 ))),
24224 DialectType::Presto | DialectType::Trino => {
24225 // FROM_UNIXTIME(CAST(x AS DOUBLE) / POW(10, 3))
24226 let cast_arg = Expression::Cast(Box::new(Cast {
24227 this: arg,
24228 to: DataType::Double {
24229 precision: None,
24230 scale: None,
24231 },
24232 trailing_comments: Vec::new(),
24233 double_colon_syntax: false,
24234 format: None,
24235 default: None,
24236 inferred_type: None,
24237 }));
24238 let div = Expression::Div(Box::new(BinaryOp::new(
24239 cast_arg,
24240 Expression::Function(Box::new(Function::new(
24241 "POW".to_string(),
24242 vec![Expression::number(10), Expression::number(3)],
24243 ))),
24244 )));
24245 Ok(Expression::Function(Box::new(Function::new(
24246 "FROM_UNIXTIME".to_string(),
24247 vec![div],
24248 ))))
24249 }
24250 DialectType::MySQL => {
24251 // FROM_UNIXTIME(x / POWER(10, 3))
24252 let div = Expression::Div(Box::new(BinaryOp::new(
24253 arg,
24254 Expression::Function(Box::new(Function::new(
24255 "POWER".to_string(),
24256 vec![Expression::number(10), Expression::number(3)],
24257 ))),
24258 )));
24259 Ok(Expression::Function(Box::new(Function::new(
24260 "FROM_UNIXTIME".to_string(),
24261 vec![div],
24262 ))))
24263 }
24264 DialectType::PostgreSQL | DialectType::Redshift => {
24265 // TO_TIMESTAMP(CAST(x AS DOUBLE PRECISION) / POWER(10, 3))
24266 let cast_arg = Expression::Cast(Box::new(Cast {
24267 this: arg,
24268 to: DataType::Custom {
24269 name: "DOUBLE PRECISION".to_string(),
24270 },
24271 trailing_comments: Vec::new(),
24272 double_colon_syntax: false,
24273 format: None,
24274 default: None,
24275 inferred_type: None,
24276 }));
24277 let div = Expression::Div(Box::new(BinaryOp::new(
24278 cast_arg,
24279 Expression::Function(Box::new(Function::new(
24280 "POWER".to_string(),
24281 vec![Expression::number(10), Expression::number(3)],
24282 ))),
24283 )));
24284 Ok(Expression::Function(Box::new(Function::new(
24285 "TO_TIMESTAMP".to_string(),
24286 vec![div],
24287 ))))
24288 }
24289 DialectType::ClickHouse => {
24290 // fromUnixTimestamp64Milli(CAST(x AS Nullable(Int64)))
24291 let cast_arg = Expression::Cast(Box::new(Cast {
24292 this: arg,
24293 to: DataType::Nullable {
24294 inner: Box::new(DataType::BigInt { length: None }),
24295 },
24296 trailing_comments: Vec::new(),
24297 double_colon_syntax: false,
24298 format: None,
24299 default: None,
24300 inferred_type: None,
24301 }));
24302 Ok(Expression::Function(Box::new(Function::new(
24303 "fromUnixTimestamp64Milli".to_string(),
24304 vec![cast_arg],
24305 ))))
24306 }
24307 _ => Ok(Expression::Function(Box::new(Function::new(
24308 "EPOCH_MS".to_string(),
24309 vec![arg],
24310 )))),
24311 }
24312 } else {
24313 Ok(e)
24314 }
24315 }
24316 Action::TSQLTypeNormalize => {
24317 if let Expression::DataType(dt) = e {
24318 let new_dt = match &dt {
24319 DataType::Custom { name } if name.eq_ignore_ascii_case("MONEY") => {
24320 DataType::Decimal {
24321 precision: Some(15),
24322 scale: Some(4),
24323 }
24324 }
24325 DataType::Custom { name }
24326 if name.eq_ignore_ascii_case("SMALLMONEY") =>
24327 {
24328 DataType::Decimal {
24329 precision: Some(6),
24330 scale: Some(4),
24331 }
24332 }
24333 DataType::Custom { name } if name.eq_ignore_ascii_case("DATETIME2") => {
24334 DataType::Timestamp {
24335 timezone: false,
24336 precision: None,
24337 }
24338 }
24339 DataType::Custom { name } if name.eq_ignore_ascii_case("REAL") => {
24340 DataType::Float {
24341 precision: None,
24342 scale: None,
24343 real_spelling: false,
24344 }
24345 }
24346 DataType::Float {
24347 real_spelling: true,
24348 ..
24349 } => DataType::Float {
24350 precision: None,
24351 scale: None,
24352 real_spelling: false,
24353 },
24354 DataType::Custom { name } if name.eq_ignore_ascii_case("IMAGE") => {
24355 DataType::Custom {
24356 name: "BLOB".to_string(),
24357 }
24358 }
24359 DataType::Custom { name } if name.eq_ignore_ascii_case("BIT") => {
24360 DataType::Boolean
24361 }
24362 DataType::Custom { name }
24363 if name.eq_ignore_ascii_case("ROWVERSION") =>
24364 {
24365 DataType::Custom {
24366 name: "BINARY".to_string(),
24367 }
24368 }
24369 DataType::Custom { name }
24370 if name.eq_ignore_ascii_case("UNIQUEIDENTIFIER") =>
24371 {
24372 match target {
24373 DialectType::Spark
24374 | DialectType::Databricks
24375 | DialectType::Hive => DataType::Custom {
24376 name: "STRING".to_string(),
24377 },
24378 _ => DataType::VarChar {
24379 length: Some(36),
24380 parenthesized_length: true,
24381 },
24382 }
24383 }
24384 DataType::Custom { name }
24385 if name.eq_ignore_ascii_case("DATETIMEOFFSET") =>
24386 {
24387 match target {
24388 DialectType::Spark
24389 | DialectType::Databricks
24390 | DialectType::Hive => DataType::Timestamp {
24391 timezone: false,
24392 precision: None,
24393 },
24394 _ => DataType::Timestamp {
24395 timezone: true,
24396 precision: None,
24397 },
24398 }
24399 }
24400 DataType::Custom { ref name }
24401 if name.len() >= 10
24402 && name[..10].eq_ignore_ascii_case("DATETIME2(") =>
24403 {
24404 // DATETIME2(n) -> TIMESTAMP
24405 DataType::Timestamp {
24406 timezone: false,
24407 precision: None,
24408 }
24409 }
24410 DataType::Custom { ref name }
24411 if name.len() >= 5 && name[..5].eq_ignore_ascii_case("TIME(") =>
24412 {
24413 // TIME(n) -> TIMESTAMP for Spark, keep as TIME for others
24414 match target {
24415 DialectType::Spark
24416 | DialectType::Databricks
24417 | DialectType::Hive => DataType::Timestamp {
24418 timezone: false,
24419 precision: None,
24420 },
24421 _ => return Ok(Expression::DataType(dt)),
24422 }
24423 }
24424 DataType::Custom { ref name }
24425 if name.len() >= 7 && name[..7].eq_ignore_ascii_case("NUMERIC") =>
24426 {
24427 // Parse NUMERIC(p,s) back to Decimal(p,s)
24428 let upper = name.to_ascii_uppercase();
24429 if let Some(inner) = upper
24430 .strip_prefix("NUMERIC(")
24431 .and_then(|s| s.strip_suffix(')'))
24432 {
24433 let parts: Vec<&str> = inner.split(',').collect();
24434 let precision =
24435 parts.first().and_then(|s| s.trim().parse::<u32>().ok());
24436 let scale =
24437 parts.get(1).and_then(|s| s.trim().parse::<u32>().ok());
24438 DataType::Decimal { precision, scale }
24439 } else if upper == "NUMERIC" {
24440 DataType::Decimal {
24441 precision: None,
24442 scale: None,
24443 }
24444 } else {
24445 return Ok(Expression::DataType(dt));
24446 }
24447 }
24448 DataType::Float {
24449 precision: Some(p), ..
24450 } => {
24451 // For Hive/Spark: FLOAT(1-32) -> FLOAT, FLOAT(33+) -> DOUBLE (IEEE 754 boundary)
24452 // For other targets: FLOAT(1-24) -> FLOAT, FLOAT(25+) -> DOUBLE (TSQL boundary)
24453 let boundary = match target {
24454 DialectType::Hive
24455 | DialectType::Spark
24456 | DialectType::Databricks => 32,
24457 _ => 24,
24458 };
24459 if *p <= boundary {
24460 DataType::Float {
24461 precision: None,
24462 scale: None,
24463 real_spelling: false,
24464 }
24465 } else {
24466 DataType::Double {
24467 precision: None,
24468 scale: None,
24469 }
24470 }
24471 }
24472 DataType::TinyInt { .. } => match target {
24473 DialectType::DuckDB => DataType::Custom {
24474 name: "UTINYINT".to_string(),
24475 },
24476 DialectType::Hive
24477 | DialectType::Spark
24478 | DialectType::Databricks => DataType::SmallInt { length: None },
24479 _ => return Ok(Expression::DataType(dt)),
24480 },
24481 // INTEGER -> INT for Spark/Databricks
24482 DataType::Int {
24483 length,
24484 integer_spelling: true,
24485 } => DataType::Int {
24486 length: *length,
24487 integer_spelling: false,
24488 },
24489 _ => return Ok(Expression::DataType(dt)),
24490 };
24491 Ok(Expression::DataType(new_dt))
24492 } else {
24493 Ok(e)
24494 }
24495 }
24496 Action::MySQLSafeDivide => {
24497 use crate::expressions::{BinaryOp, Cast};
24498 if let Expression::Div(op) = e {
24499 let left = op.left;
24500 let right = op.right;
24501 // For SQLite: CAST left as REAL but NO NULLIF wrapping
24502 if matches!(target, DialectType::SQLite) {
24503 let new_left = Expression::Cast(Box::new(Cast {
24504 this: left,
24505 to: DataType::Float {
24506 precision: None,
24507 scale: None,
24508 real_spelling: true,
24509 },
24510 trailing_comments: Vec::new(),
24511 double_colon_syntax: false,
24512 format: None,
24513 default: None,
24514 inferred_type: None,
24515 }));
24516 return Ok(Expression::Div(Box::new(BinaryOp::new(new_left, right))));
24517 }
24518 // Wrap right in NULLIF(right, 0)
24519 let nullif_right = Expression::Function(Box::new(Function::new(
24520 "NULLIF".to_string(),
24521 vec![right, Expression::number(0)],
24522 )));
24523 // For some dialects, also CAST the left side
24524 let new_left = match target {
24525 DialectType::PostgreSQL
24526 | DialectType::Redshift
24527 | DialectType::Teradata
24528 | DialectType::Materialize
24529 | DialectType::RisingWave => Expression::Cast(Box::new(Cast {
24530 this: left,
24531 to: DataType::Custom {
24532 name: "DOUBLE PRECISION".to_string(),
24533 },
24534 trailing_comments: Vec::new(),
24535 double_colon_syntax: false,
24536 format: None,
24537 default: None,
24538 inferred_type: None,
24539 })),
24540 DialectType::Drill
24541 | DialectType::Trino
24542 | DialectType::Presto
24543 | DialectType::Athena => Expression::Cast(Box::new(Cast {
24544 this: left,
24545 to: DataType::Double {
24546 precision: None,
24547 scale: None,
24548 },
24549 trailing_comments: Vec::new(),
24550 double_colon_syntax: false,
24551 format: None,
24552 default: None,
24553 inferred_type: None,
24554 })),
24555 DialectType::TSQL => Expression::Cast(Box::new(Cast {
24556 this: left,
24557 to: DataType::Float {
24558 precision: None,
24559 scale: None,
24560 real_spelling: false,
24561 },
24562 trailing_comments: Vec::new(),
24563 double_colon_syntax: false,
24564 format: None,
24565 default: None,
24566 inferred_type: None,
24567 })),
24568 _ => left,
24569 };
24570 Ok(Expression::Div(Box::new(BinaryOp::new(
24571 new_left,
24572 nullif_right,
24573 ))))
24574 } else {
24575 Ok(e)
24576 }
24577 }
24578 Action::AlterTableRenameStripSchema => {
24579 if let Expression::AlterTable(mut at) = e {
24580 if let Some(crate::expressions::AlterTableAction::RenameTable(
24581 ref mut new_tbl,
24582 )) = at.actions.first_mut()
24583 {
24584 new_tbl.schema = None;
24585 new_tbl.catalog = None;
24586 }
24587 Ok(Expression::AlterTable(at))
24588 } else {
24589 Ok(e)
24590 }
24591 }
24592 Action::NullsOrdering => {
24593 // Fill in the source dialect's implied null ordering default.
24594 // This makes implicit null ordering explicit so the target generator
24595 // can correctly strip or keep it.
24596 //
24597 // Dialect null ordering categories:
24598 // nulls_are_large (Oracle, PostgreSQL, Redshift, Snowflake):
24599 // ASC -> NULLS LAST, DESC -> NULLS FIRST
24600 // nulls_are_small (Spark, Hive, BigQuery, MySQL, Databricks, ClickHouse, etc.):
24601 // ASC -> NULLS FIRST, DESC -> NULLS LAST
24602 // nulls_are_last (DuckDB, Presto, Trino, Dremio, Athena):
24603 // NULLS LAST always (both ASC and DESC)
24604 if let Expression::Ordered(mut o) = e {
24605 let is_asc = !o.desc;
24606
24607 let is_source_nulls_large = matches!(
24608 source,
24609 DialectType::Oracle
24610 | DialectType::PostgreSQL
24611 | DialectType::Redshift
24612 | DialectType::Snowflake
24613 );
24614 let is_source_nulls_last = matches!(
24615 source,
24616 DialectType::DuckDB
24617 | DialectType::Presto
24618 | DialectType::Trino
24619 | DialectType::Dremio
24620 | DialectType::Athena
24621 | DialectType::ClickHouse
24622 | DialectType::Drill
24623 | DialectType::Exasol
24624 | DialectType::DataFusion
24625 );
24626
24627 // Determine target category to check if default matches
24628 let is_target_nulls_large = matches!(
24629 target,
24630 DialectType::Oracle
24631 | DialectType::PostgreSQL
24632 | DialectType::Redshift
24633 | DialectType::Snowflake
24634 );
24635 let is_target_nulls_last = matches!(
24636 target,
24637 DialectType::DuckDB
24638 | DialectType::Presto
24639 | DialectType::Trino
24640 | DialectType::Dremio
24641 | DialectType::Athena
24642 | DialectType::ClickHouse
24643 | DialectType::Drill
24644 | DialectType::Exasol
24645 | DialectType::DataFusion
24646 );
24647
24648 // Compute the implied nulls_first for source
24649 let source_nulls_first = if is_source_nulls_large {
24650 !is_asc // ASC -> NULLS LAST (false), DESC -> NULLS FIRST (true)
24651 } else if is_source_nulls_last {
24652 false // NULLS LAST always
24653 } else {
24654 is_asc // nulls_are_small: ASC -> NULLS FIRST (true), DESC -> NULLS LAST (false)
24655 };
24656
24657 // Compute the target's default
24658 let target_nulls_first = if is_target_nulls_large {
24659 !is_asc
24660 } else if is_target_nulls_last {
24661 false
24662 } else {
24663 is_asc
24664 };
24665
24666 // Only add explicit nulls ordering if source and target defaults differ
24667 if source_nulls_first != target_nulls_first {
24668 o.nulls_first = Some(source_nulls_first);
24669 }
24670 // If they match, leave nulls_first as None so the generator won't output it
24671
24672 Ok(Expression::Ordered(o))
24673 } else {
24674 Ok(e)
24675 }
24676 }
24677 Action::StringAggConvert => {
24678 match e {
24679 Expression::WithinGroup(wg) => {
24680 // STRING_AGG(x, sep) WITHIN GROUP (ORDER BY z) -> target-specific
24681 // Extract args and distinct flag from either Function, AggregateFunction, or StringAgg
24682 let (x_opt, sep_opt, distinct) = match wg.this {
24683 Expression::AggregateFunction(ref af)
24684 if af.name.eq_ignore_ascii_case("STRING_AGG")
24685 && af.args.len() >= 2 =>
24686 {
24687 (
24688 Some(af.args[0].clone()),
24689 Some(af.args[1].clone()),
24690 af.distinct,
24691 )
24692 }
24693 Expression::Function(ref f)
24694 if f.name.eq_ignore_ascii_case("STRING_AGG")
24695 && f.args.len() >= 2 =>
24696 {
24697 (Some(f.args[0].clone()), Some(f.args[1].clone()), false)
24698 }
24699 Expression::StringAgg(ref sa) => {
24700 (Some(sa.this.clone()), sa.separator.clone(), sa.distinct)
24701 }
24702 _ => (None, None, false),
24703 };
24704 if let (Some(x), Some(sep)) = (x_opt, sep_opt) {
24705 let order_by = wg.order_by;
24706
24707 match target {
24708 DialectType::TSQL | DialectType::Fabric => {
24709 // Keep as WithinGroup(StringAgg) for TSQL
24710 Ok(Expression::WithinGroup(Box::new(
24711 crate::expressions::WithinGroup {
24712 this: Expression::StringAgg(Box::new(
24713 crate::expressions::StringAggFunc {
24714 this: x,
24715 separator: Some(sep),
24716 order_by: None, // order_by goes in WithinGroup, not StringAgg
24717 distinct,
24718 filter: None,
24719 limit: None,
24720 inferred_type: None,
24721 },
24722 )),
24723 order_by,
24724 },
24725 )))
24726 }
24727 DialectType::MySQL
24728 | DialectType::SingleStore
24729 | DialectType::Doris
24730 | DialectType::StarRocks => {
24731 // GROUP_CONCAT(x ORDER BY z SEPARATOR sep)
24732 Ok(Expression::GroupConcat(Box::new(
24733 crate::expressions::GroupConcatFunc {
24734 this: x,
24735 separator: Some(sep),
24736 order_by: Some(order_by),
24737 distinct,
24738 filter: None,
24739 limit: None,
24740 inferred_type: None,
24741 },
24742 )))
24743 }
24744 DialectType::SQLite => {
24745 // GROUP_CONCAT(x, sep) - no ORDER BY support
24746 Ok(Expression::GroupConcat(Box::new(
24747 crate::expressions::GroupConcatFunc {
24748 this: x,
24749 separator: Some(sep),
24750 order_by: None,
24751 distinct,
24752 filter: None,
24753 limit: None,
24754 inferred_type: None,
24755 },
24756 )))
24757 }
24758 DialectType::PostgreSQL | DialectType::Redshift => {
24759 // STRING_AGG(x, sep ORDER BY z)
24760 Ok(Expression::StringAgg(Box::new(
24761 crate::expressions::StringAggFunc {
24762 this: x,
24763 separator: Some(sep),
24764 order_by: Some(order_by),
24765 distinct,
24766 filter: None,
24767 limit: None,
24768 inferred_type: None,
24769 },
24770 )))
24771 }
24772 _ => {
24773 // Default: keep as STRING_AGG(x, sep) with ORDER BY inside
24774 Ok(Expression::StringAgg(Box::new(
24775 crate::expressions::StringAggFunc {
24776 this: x,
24777 separator: Some(sep),
24778 order_by: Some(order_by),
24779 distinct,
24780 filter: None,
24781 limit: None,
24782 inferred_type: None,
24783 },
24784 )))
24785 }
24786 }
24787 } else {
24788 Ok(Expression::WithinGroup(wg))
24789 }
24790 }
24791 Expression::StringAgg(sa) => {
24792 match target {
24793 DialectType::MySQL
24794 | DialectType::SingleStore
24795 | DialectType::Doris
24796 | DialectType::StarRocks => {
24797 // STRING_AGG(x, sep) -> GROUP_CONCAT(x SEPARATOR sep)
24798 Ok(Expression::GroupConcat(Box::new(
24799 crate::expressions::GroupConcatFunc {
24800 this: sa.this,
24801 separator: sa.separator,
24802 order_by: sa.order_by,
24803 distinct: sa.distinct,
24804 filter: sa.filter,
24805 limit: None,
24806 inferred_type: None,
24807 },
24808 )))
24809 }
24810 DialectType::SQLite => {
24811 // STRING_AGG(x, sep) -> GROUP_CONCAT(x, sep)
24812 Ok(Expression::GroupConcat(Box::new(
24813 crate::expressions::GroupConcatFunc {
24814 this: sa.this,
24815 separator: sa.separator,
24816 order_by: None, // SQLite doesn't support ORDER BY in GROUP_CONCAT
24817 distinct: sa.distinct,
24818 filter: sa.filter,
24819 limit: None,
24820 inferred_type: None,
24821 },
24822 )))
24823 }
24824 DialectType::Spark | DialectType::Databricks => {
24825 // STRING_AGG(x, sep) -> LISTAGG(x, sep)
24826 Ok(Expression::ListAgg(Box::new(
24827 crate::expressions::ListAggFunc {
24828 this: sa.this,
24829 separator: sa.separator,
24830 on_overflow: None,
24831 order_by: sa.order_by,
24832 distinct: sa.distinct,
24833 filter: None,
24834 inferred_type: None,
24835 },
24836 )))
24837 }
24838 _ => Ok(Expression::StringAgg(sa)),
24839 }
24840 }
24841 _ => Ok(e),
24842 }
24843 }
24844 Action::GroupConcatConvert => {
24845 // Helper to expand CONCAT(a, b, c) -> a || b || c (for PostgreSQL/SQLite)
24846 // or CONCAT(a, b, c) -> a + b + c (for TSQL)
24847 fn expand_concat_to_dpipe(expr: Expression) -> Expression {
24848 if let Expression::Function(ref f) = expr {
24849 if f.name.eq_ignore_ascii_case("CONCAT") && f.args.len() > 1 {
24850 let mut result = f.args[0].clone();
24851 for arg in &f.args[1..] {
24852 result = Expression::Concat(Box::new(BinaryOp {
24853 left: result,
24854 right: arg.clone(),
24855 left_comments: vec![],
24856 operator_comments: vec![],
24857 trailing_comments: vec![],
24858 inferred_type: None,
24859 }));
24860 }
24861 return result;
24862 }
24863 }
24864 expr
24865 }
24866 fn expand_concat_to_plus(expr: Expression) -> Expression {
24867 if let Expression::Function(ref f) = expr {
24868 if f.name.eq_ignore_ascii_case("CONCAT") && f.args.len() > 1 {
24869 let mut result = f.args[0].clone();
24870 for arg in &f.args[1..] {
24871 result = Expression::Add(Box::new(BinaryOp {
24872 left: result,
24873 right: arg.clone(),
24874 left_comments: vec![],
24875 operator_comments: vec![],
24876 trailing_comments: vec![],
24877 inferred_type: None,
24878 }));
24879 }
24880 return result;
24881 }
24882 }
24883 expr
24884 }
24885 // Helper to wrap each arg in CAST(arg AS VARCHAR) for Presto/Trino CONCAT
24886 fn wrap_concat_args_in_varchar_cast(expr: Expression) -> Expression {
24887 if let Expression::Function(ref f) = expr {
24888 if f.name.eq_ignore_ascii_case("CONCAT") && f.args.len() > 1 {
24889 let new_args: Vec<Expression> = f
24890 .args
24891 .iter()
24892 .map(|arg| {
24893 Expression::Cast(Box::new(crate::expressions::Cast {
24894 this: arg.clone(),
24895 to: crate::expressions::DataType::VarChar {
24896 length: None,
24897 parenthesized_length: false,
24898 },
24899 trailing_comments: Vec::new(),
24900 double_colon_syntax: false,
24901 format: None,
24902 default: None,
24903 inferred_type: None,
24904 }))
24905 })
24906 .collect();
24907 return Expression::Function(Box::new(
24908 crate::expressions::Function::new(
24909 "CONCAT".to_string(),
24910 new_args,
24911 ),
24912 ));
24913 }
24914 }
24915 expr
24916 }
24917 if let Expression::GroupConcat(gc) = e {
24918 match target {
24919 DialectType::Presto => {
24920 // GROUP_CONCAT(x [, sep]) -> ARRAY_JOIN(ARRAY_AGG(x), sep)
24921 let sep = gc.separator.unwrap_or(Expression::string(","));
24922 // For multi-arg CONCAT, wrap each arg in CAST(... AS VARCHAR)
24923 let this = wrap_concat_args_in_varchar_cast(gc.this);
24924 let array_agg =
24925 Expression::ArrayAgg(Box::new(crate::expressions::AggFunc {
24926 this,
24927 distinct: gc.distinct,
24928 filter: gc.filter,
24929 order_by: gc.order_by.unwrap_or_default(),
24930 name: None,
24931 ignore_nulls: None,
24932 having_max: None,
24933 limit: None,
24934 inferred_type: None,
24935 }));
24936 Ok(Expression::ArrayJoin(Box::new(
24937 crate::expressions::ArrayJoinFunc {
24938 this: array_agg,
24939 separator: sep,
24940 null_replacement: None,
24941 },
24942 )))
24943 }
24944 DialectType::Trino => {
24945 // GROUP_CONCAT(x [, sep]) -> LISTAGG(x, sep)
24946 let sep = gc.separator.unwrap_or(Expression::string(","));
24947 // For multi-arg CONCAT, wrap each arg in CAST(... AS VARCHAR)
24948 let this = wrap_concat_args_in_varchar_cast(gc.this);
24949 Ok(Expression::ListAgg(Box::new(
24950 crate::expressions::ListAggFunc {
24951 this,
24952 separator: Some(sep),
24953 on_overflow: None,
24954 order_by: gc.order_by,
24955 distinct: gc.distinct,
24956 filter: gc.filter,
24957 inferred_type: None,
24958 },
24959 )))
24960 }
24961 DialectType::PostgreSQL
24962 | DialectType::Redshift
24963 | DialectType::Snowflake
24964 | DialectType::DuckDB
24965 | DialectType::Hive
24966 | DialectType::ClickHouse => {
24967 // GROUP_CONCAT(x [, sep]) -> STRING_AGG(x, sep)
24968 let sep = gc.separator.unwrap_or(Expression::string(","));
24969 // Expand CONCAT(a,b,c) -> a || b || c for || dialects
24970 let this = expand_concat_to_dpipe(gc.this);
24971 // For PostgreSQL, add NULLS LAST for DESC / NULLS FIRST for ASC
24972 let order_by = if target == DialectType::PostgreSQL {
24973 gc.order_by.map(|ords| {
24974 ords.into_iter()
24975 .map(|mut o| {
24976 if o.nulls_first.is_none() {
24977 if o.desc {
24978 o.nulls_first = Some(false);
24979 // NULLS LAST
24980 } else {
24981 o.nulls_first = Some(true);
24982 // NULLS FIRST
24983 }
24984 }
24985 o
24986 })
24987 .collect()
24988 })
24989 } else {
24990 gc.order_by
24991 };
24992 Ok(Expression::StringAgg(Box::new(
24993 crate::expressions::StringAggFunc {
24994 this,
24995 separator: Some(sep),
24996 order_by,
24997 distinct: gc.distinct,
24998 filter: gc.filter,
24999 limit: None,
25000 inferred_type: None,
25001 },
25002 )))
25003 }
25004 DialectType::TSQL => {
25005 // GROUP_CONCAT(x [, sep]) -> STRING_AGG(x, sep) WITHIN GROUP (ORDER BY ...)
25006 // TSQL doesn't support DISTINCT in STRING_AGG
25007 let sep = gc.separator.unwrap_or(Expression::string(","));
25008 // Expand CONCAT(a,b,c) -> a + b + c for TSQL
25009 let this = expand_concat_to_plus(gc.this);
25010 Ok(Expression::StringAgg(Box::new(
25011 crate::expressions::StringAggFunc {
25012 this,
25013 separator: Some(sep),
25014 order_by: gc.order_by,
25015 distinct: false, // TSQL doesn't support DISTINCT in STRING_AGG
25016 filter: gc.filter,
25017 limit: None,
25018 inferred_type: None,
25019 },
25020 )))
25021 }
25022 DialectType::SQLite => {
25023 // GROUP_CONCAT stays as GROUP_CONCAT but ORDER BY is removed
25024 // SQLite GROUP_CONCAT doesn't support ORDER BY
25025 // Expand CONCAT(a,b,c) -> a || b || c
25026 let this = expand_concat_to_dpipe(gc.this);
25027 Ok(Expression::GroupConcat(Box::new(
25028 crate::expressions::GroupConcatFunc {
25029 this,
25030 separator: gc.separator,
25031 order_by: None, // SQLite doesn't support ORDER BY in GROUP_CONCAT
25032 distinct: gc.distinct,
25033 filter: gc.filter,
25034 limit: None,
25035 inferred_type: None,
25036 },
25037 )))
25038 }
25039 DialectType::Spark | DialectType::Databricks => {
25040 // GROUP_CONCAT(x [, sep]) -> LISTAGG(x, sep)
25041 let sep = gc.separator.unwrap_or(Expression::string(","));
25042 Ok(Expression::ListAgg(Box::new(
25043 crate::expressions::ListAggFunc {
25044 this: gc.this,
25045 separator: Some(sep),
25046 on_overflow: None,
25047 order_by: gc.order_by,
25048 distinct: gc.distinct,
25049 filter: None,
25050 inferred_type: None,
25051 },
25052 )))
25053 }
25054 DialectType::MySQL
25055 | DialectType::SingleStore
25056 | DialectType::StarRocks => {
25057 // MySQL GROUP_CONCAT should have explicit SEPARATOR (default ',')
25058 if gc.separator.is_none() {
25059 let mut gc = gc;
25060 gc.separator = Some(Expression::string(","));
25061 Ok(Expression::GroupConcat(gc))
25062 } else {
25063 Ok(Expression::GroupConcat(gc))
25064 }
25065 }
25066 _ => Ok(Expression::GroupConcat(gc)),
25067 }
25068 } else {
25069 Ok(e)
25070 }
25071 }
25072 Action::TempTableHash => {
25073 match e {
25074 Expression::CreateTable(mut ct) => {
25075 // TSQL #table -> TEMPORARY TABLE with # stripped from name
25076 let name = &ct.name.name.name;
25077 if name.starts_with('#') {
25078 ct.name.name.name = name.trim_start_matches('#').to_string();
25079 }
25080 // Set temporary flag
25081 ct.temporary = true;
25082 Ok(Expression::CreateTable(ct))
25083 }
25084 Expression::Table(mut tr) => {
25085 // Strip # from table references
25086 let name = &tr.name.name;
25087 if name.starts_with('#') {
25088 tr.name.name = name.trim_start_matches('#').to_string();
25089 }
25090 Ok(Expression::Table(tr))
25091 }
25092 Expression::DropTable(mut dt) => {
25093 // Strip # from DROP TABLE names
25094 for table_ref in &mut dt.names {
25095 if table_ref.name.name.starts_with('#') {
25096 table_ref.name.name =
25097 table_ref.name.name.trim_start_matches('#').to_string();
25098 }
25099 }
25100 Ok(Expression::DropTable(dt))
25101 }
25102 _ => Ok(e),
25103 }
25104 }
25105 Action::NvlClearOriginal => {
25106 if let Expression::Nvl(mut f) = e {
25107 f.original_name = None;
25108 Ok(Expression::Nvl(f))
25109 } else {
25110 Ok(e)
25111 }
25112 }
25113 Action::HiveCastToTryCast => {
25114 // Convert Hive/Spark CAST to TRY_CAST for targets that support it
25115 if let Expression::Cast(mut c) = e {
25116 // For Spark/Hive -> DuckDB: TIMESTAMP -> TIMESTAMPTZ
25117 // (Spark's TIMESTAMP is always timezone-aware)
25118 if matches!(target, DialectType::DuckDB)
25119 && matches!(source, DialectType::Spark | DialectType::Databricks)
25120 && matches!(
25121 c.to,
25122 DataType::Timestamp {
25123 timezone: false,
25124 ..
25125 }
25126 )
25127 {
25128 c.to = DataType::Custom {
25129 name: "TIMESTAMPTZ".to_string(),
25130 };
25131 }
25132 // For Spark source -> Databricks: VARCHAR/CHAR -> STRING
25133 // Spark parses VARCHAR(n)/CHAR(n) as TEXT, normalize to STRING
25134 if matches!(target, DialectType::Databricks | DialectType::Spark)
25135 && matches!(
25136 source,
25137 DialectType::Spark | DialectType::Databricks | DialectType::Hive
25138 )
25139 && Self::has_varchar_char_type(&c.to)
25140 {
25141 c.to = Self::normalize_varchar_to_string(c.to);
25142 }
25143 Ok(Expression::TryCast(c))
25144 } else {
25145 Ok(e)
25146 }
25147 }
25148 Action::XorExpand => {
25149 // Expand XOR to (a AND NOT b) OR (NOT a AND b) for dialects without XOR keyword
25150 // Snowflake: use BOOLXOR(a, b) instead
25151 if let Expression::Xor(xor) = e {
25152 // Collect all XOR operands
25153 let mut operands = Vec::new();
25154 if let Some(this) = xor.this {
25155 operands.push(*this);
25156 }
25157 if let Some(expr) = xor.expression {
25158 operands.push(*expr);
25159 }
25160 operands.extend(xor.expressions);
25161
25162 // Snowflake: use BOOLXOR(a, b)
25163 if matches!(target, DialectType::Snowflake) && operands.len() == 2 {
25164 let a = operands.remove(0);
25165 let b = operands.remove(0);
25166 return Ok(Expression::Function(Box::new(Function::new(
25167 "BOOLXOR".to_string(),
25168 vec![a, b],
25169 ))));
25170 }
25171
25172 // Helper to build (a AND NOT b) OR (NOT a AND b)
25173 let make_xor = |a: Expression, b: Expression| -> Expression {
25174 let not_b = Expression::Not(Box::new(
25175 crate::expressions::UnaryOp::new(b.clone()),
25176 ));
25177 let not_a = Expression::Not(Box::new(
25178 crate::expressions::UnaryOp::new(a.clone()),
25179 ));
25180 let left_and = Expression::And(Box::new(BinaryOp {
25181 left: a,
25182 right: Expression::Paren(Box::new(Paren {
25183 this: not_b,
25184 trailing_comments: Vec::new(),
25185 })),
25186 left_comments: Vec::new(),
25187 operator_comments: Vec::new(),
25188 trailing_comments: Vec::new(),
25189 inferred_type: None,
25190 }));
25191 let right_and = Expression::And(Box::new(BinaryOp {
25192 left: Expression::Paren(Box::new(Paren {
25193 this: not_a,
25194 trailing_comments: Vec::new(),
25195 })),
25196 right: b,
25197 left_comments: Vec::new(),
25198 operator_comments: Vec::new(),
25199 trailing_comments: Vec::new(),
25200 inferred_type: None,
25201 }));
25202 Expression::Or(Box::new(BinaryOp {
25203 left: Expression::Paren(Box::new(Paren {
25204 this: left_and,
25205 trailing_comments: Vec::new(),
25206 })),
25207 right: Expression::Paren(Box::new(Paren {
25208 this: right_and,
25209 trailing_comments: Vec::new(),
25210 })),
25211 left_comments: Vec::new(),
25212 operator_comments: Vec::new(),
25213 trailing_comments: Vec::new(),
25214 inferred_type: None,
25215 }))
25216 };
25217
25218 if operands.len() >= 2 {
25219 let mut result = make_xor(operands.remove(0), operands.remove(0));
25220 for operand in operands {
25221 result = make_xor(result, operand);
25222 }
25223 Ok(result)
25224 } else if operands.len() == 1 {
25225 Ok(operands.remove(0))
25226 } else {
25227 // No operands - return FALSE (shouldn't happen)
25228 Ok(Expression::Boolean(crate::expressions::BooleanLiteral {
25229 value: false,
25230 }))
25231 }
25232 } else {
25233 Ok(e)
25234 }
25235 }
25236 Action::DatePartUnquote => {
25237 // DATE_PART('month', x) -> DATE_PART(month, x) for Snowflake target
25238 // Convert the quoted string first arg to a bare Column/Identifier
25239 if let Expression::Function(mut f) = e {
25240 if let Some(Expression::Literal(lit)) = f.args.first() {
25241 if let crate::expressions::Literal::String(s) = lit.as_ref() {
25242 let bare_name = s.to_ascii_lowercase();
25243 f.args[0] =
25244 Expression::Column(Box::new(crate::expressions::Column {
25245 name: Identifier::new(bare_name),
25246 table: None,
25247 join_mark: false,
25248 trailing_comments: Vec::new(),
25249 span: None,
25250 inferred_type: None,
25251 }));
25252 }
25253 }
25254 Ok(Expression::Function(f))
25255 } else {
25256 Ok(e)
25257 }
25258 }
25259 Action::ArrayLengthConvert => {
25260 // Extract the argument from the expression
25261 let arg = match e {
25262 Expression::Cardinality(ref f) => f.this.clone(),
25263 Expression::ArrayLength(ref f) => f.this.clone(),
25264 Expression::ArraySize(ref f) => f.this.clone(),
25265 _ => return Ok(e),
25266 };
25267 match target {
25268 DialectType::Spark | DialectType::Databricks | DialectType::Hive => {
25269 Ok(Expression::Function(Box::new(Function::new(
25270 "SIZE".to_string(),
25271 vec![arg],
25272 ))))
25273 }
25274 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
25275 Ok(Expression::Cardinality(Box::new(
25276 crate::expressions::UnaryFunc::new(arg),
25277 )))
25278 }
25279 DialectType::BigQuery => Ok(Expression::ArrayLength(Box::new(
25280 crate::expressions::UnaryFunc::new(arg),
25281 ))),
25282 DialectType::DuckDB => Ok(Expression::ArrayLength(Box::new(
25283 crate::expressions::UnaryFunc::new(arg),
25284 ))),
25285 DialectType::PostgreSQL | DialectType::Redshift => {
25286 // PostgreSQL ARRAY_LENGTH requires dimension arg
25287 Ok(Expression::Function(Box::new(Function::new(
25288 "ARRAY_LENGTH".to_string(),
25289 vec![arg, Expression::number(1)],
25290 ))))
25291 }
25292 DialectType::Snowflake => Ok(Expression::ArraySize(Box::new(
25293 crate::expressions::UnaryFunc::new(arg),
25294 ))),
25295 _ => Ok(e), // Keep original
25296 }
25297 }
25298
25299 Action::JsonExtractToArrow => {
25300 // JSON_EXTRACT(x, path) -> x -> path for SQLite/DuckDB (set arrow_syntax = true)
25301 if let Expression::JsonExtract(mut f) = e {
25302 f.arrow_syntax = true;
25303 // Transform path: convert bracket notation to dot notation
25304 // SQLite strips wildcards, DuckDB preserves them
25305 if let Expression::Literal(ref lit) = f.path {
25306 if let Literal::String(ref s) = lit.as_ref() {
25307 let mut transformed = s.clone();
25308 if matches!(target, DialectType::SQLite) {
25309 transformed = Self::strip_json_wildcards(&transformed);
25310 }
25311 transformed = Self::bracket_to_dot_notation(&transformed);
25312 if transformed != *s {
25313 f.path = Expression::string(&transformed);
25314 }
25315 }
25316 }
25317 Ok(Expression::JsonExtract(f))
25318 } else {
25319 Ok(e)
25320 }
25321 }
25322
25323 Action::JsonExtractToGetJsonObject => {
25324 if let Expression::JsonExtract(f) = e {
25325 if matches!(target, DialectType::PostgreSQL | DialectType::Redshift) {
25326 // JSON_EXTRACT(x, '$.key') -> JSON_EXTRACT_PATH(x, 'key') for PostgreSQL
25327 // Use proper decomposition that handles brackets
25328 let keys: Vec<Expression> = if let Expression::Literal(lit) = f.path {
25329 if let Literal::String(ref s) = lit.as_ref() {
25330 let parts = Self::decompose_json_path(s);
25331 parts.into_iter().map(|k| Expression::string(&k)).collect()
25332 } else {
25333 vec![]
25334 }
25335 } else {
25336 vec![f.path]
25337 };
25338 let func_name = if matches!(target, DialectType::Redshift) {
25339 "JSON_EXTRACT_PATH_TEXT"
25340 } else {
25341 "JSON_EXTRACT_PATH"
25342 };
25343 let mut args = vec![f.this];
25344 args.extend(keys);
25345 Ok(Expression::Function(Box::new(Function::new(
25346 func_name.to_string(),
25347 args,
25348 ))))
25349 } else {
25350 // GET_JSON_OBJECT(x, '$.path') for Hive/Spark
25351 // Convert bracket double quotes to single quotes
25352 let path = if let Expression::Literal(ref lit) = f.path {
25353 if let Literal::String(ref s) = lit.as_ref() {
25354 let normalized = Self::bracket_to_single_quotes(s);
25355 if normalized != *s {
25356 Expression::string(&normalized)
25357 } else {
25358 f.path.clone()
25359 }
25360 } else {
25361 f.path.clone()
25362 }
25363 } else {
25364 f.path.clone()
25365 };
25366 Ok(Expression::Function(Box::new(Function::new(
25367 "GET_JSON_OBJECT".to_string(),
25368 vec![f.this, path],
25369 ))))
25370 }
25371 } else {
25372 Ok(e)
25373 }
25374 }
25375
25376 Action::JsonExtractScalarToGetJsonObject => {
25377 // JSON_EXTRACT_SCALAR(x, '$.path') -> GET_JSON_OBJECT(x, '$.path') for Hive/Spark
25378 if let Expression::JsonExtractScalar(f) = e {
25379 Ok(Expression::Function(Box::new(Function::new(
25380 "GET_JSON_OBJECT".to_string(),
25381 vec![f.this, f.path],
25382 ))))
25383 } else {
25384 Ok(e)
25385 }
25386 }
25387
25388 Action::JsonExtractToTsql => {
25389 // JSON_EXTRACT/JSON_EXTRACT_SCALAR -> ISNULL(JSON_QUERY(x, path), JSON_VALUE(x, path)) for TSQL
25390 let (this, path) = match e {
25391 Expression::JsonExtract(f) => (f.this, f.path),
25392 Expression::JsonExtractScalar(f) => (f.this, f.path),
25393 _ => return Ok(e),
25394 };
25395 // Transform path: strip wildcards, convert bracket notation to dot notation
25396 let transformed_path = if let Expression::Literal(ref lit) = path {
25397 if let Literal::String(ref s) = lit.as_ref() {
25398 let stripped = Self::strip_json_wildcards(s);
25399 let dotted = Self::bracket_to_dot_notation(&stripped);
25400 Expression::string(&dotted)
25401 } else {
25402 path.clone()
25403 }
25404 } else {
25405 path
25406 };
25407 let json_query = Expression::Function(Box::new(Function::new(
25408 "JSON_QUERY".to_string(),
25409 vec![this.clone(), transformed_path.clone()],
25410 )));
25411 let json_value = Expression::Function(Box::new(Function::new(
25412 "JSON_VALUE".to_string(),
25413 vec![this, transformed_path],
25414 )));
25415 Ok(Expression::Function(Box::new(Function::new(
25416 "ISNULL".to_string(),
25417 vec![json_query, json_value],
25418 ))))
25419 }
25420
25421 Action::JsonExtractToClickHouse => {
25422 // JSON_EXTRACT/JSON_EXTRACT_SCALAR -> JSONExtractString(x, 'key1', idx, 'key2') for ClickHouse
25423 let (this, path) = match e {
25424 Expression::JsonExtract(f) => (f.this, f.path),
25425 Expression::JsonExtractScalar(f) => (f.this, f.path),
25426 _ => return Ok(e),
25427 };
25428 let args: Vec<Expression> = if let Expression::Literal(lit) = path {
25429 if let Literal::String(ref s) = lit.as_ref() {
25430 let parts = Self::decompose_json_path(s);
25431 let mut result = vec![this];
25432 for part in parts {
25433 // ClickHouse uses 1-based integer indices for array access
25434 if let Ok(idx) = part.parse::<i64>() {
25435 result.push(Expression::number(idx + 1));
25436 } else {
25437 result.push(Expression::string(&part));
25438 }
25439 }
25440 result
25441 } else {
25442 vec![]
25443 }
25444 } else {
25445 vec![this, path]
25446 };
25447 Ok(Expression::Function(Box::new(Function::new(
25448 "JSONExtractString".to_string(),
25449 args,
25450 ))))
25451 }
25452
25453 Action::JsonExtractScalarConvert => {
25454 // JSON_EXTRACT_SCALAR -> target-specific
25455 if let Expression::JsonExtractScalar(f) = e {
25456 match target {
25457 DialectType::PostgreSQL | DialectType::Redshift => {
25458 // JSON_EXTRACT_SCALAR(x, '$.path') -> JSON_EXTRACT_PATH_TEXT(x, 'key1', 'key2')
25459 let keys: Vec<Expression> = if let Expression::Literal(lit) = f.path
25460 {
25461 if let Literal::String(ref s) = lit.as_ref() {
25462 let parts = Self::decompose_json_path(s);
25463 parts.into_iter().map(|k| Expression::string(&k)).collect()
25464 } else {
25465 vec![]
25466 }
25467 } else {
25468 vec![f.path]
25469 };
25470 let mut args = vec![f.this];
25471 args.extend(keys);
25472 Ok(Expression::Function(Box::new(Function::new(
25473 "JSON_EXTRACT_PATH_TEXT".to_string(),
25474 args,
25475 ))))
25476 }
25477 DialectType::Snowflake => {
25478 // JSON_EXTRACT_SCALAR(x, '$.path') -> JSON_EXTRACT_PATH_TEXT(x, 'stripped_path')
25479 let stripped_path = if let Expression::Literal(ref lit) = f.path {
25480 if let Literal::String(ref s) = lit.as_ref() {
25481 let stripped = Self::strip_json_dollar_prefix(s);
25482 Expression::string(&stripped)
25483 } else {
25484 f.path.clone()
25485 }
25486 } else {
25487 f.path
25488 };
25489 Ok(Expression::Function(Box::new(Function::new(
25490 "JSON_EXTRACT_PATH_TEXT".to_string(),
25491 vec![f.this, stripped_path],
25492 ))))
25493 }
25494 DialectType::SQLite | DialectType::DuckDB => {
25495 // JSON_EXTRACT_SCALAR(x, '$.path') -> x ->> '$.path'
25496 Ok(Expression::JsonExtractScalar(Box::new(
25497 crate::expressions::JsonExtractFunc {
25498 this: f.this,
25499 path: f.path,
25500 returning: f.returning,
25501 arrow_syntax: true,
25502 hash_arrow_syntax: false,
25503 wrapper_option: None,
25504 quotes_option: None,
25505 on_scalar_string: false,
25506 on_error: None,
25507 },
25508 )))
25509 }
25510 _ => Ok(Expression::JsonExtractScalar(f)),
25511 }
25512 } else {
25513 Ok(e)
25514 }
25515 }
25516
25517 Action::JsonPathNormalize => {
25518 // Normalize JSON path format for BigQuery, MySQL, etc.
25519 if let Expression::JsonExtract(mut f) = e {
25520 if let Expression::Literal(ref lit) = f.path {
25521 if let Literal::String(ref s) = lit.as_ref() {
25522 let mut normalized = s.clone();
25523 // Convert bracket notation and handle wildcards per dialect
25524 match target {
25525 DialectType::BigQuery => {
25526 // BigQuery strips wildcards and uses single quotes in brackets
25527 normalized = Self::strip_json_wildcards(&normalized);
25528 normalized = Self::bracket_to_single_quotes(&normalized);
25529 }
25530 DialectType::MySQL => {
25531 // MySQL preserves wildcards, converts brackets to dot notation
25532 normalized = Self::bracket_to_dot_notation(&normalized);
25533 }
25534 _ => {}
25535 }
25536 if normalized != *s {
25537 f.path = Expression::string(&normalized);
25538 }
25539 }
25540 }
25541 Ok(Expression::JsonExtract(f))
25542 } else {
25543 Ok(e)
25544 }
25545 }
25546
25547 Action::JsonQueryValueConvert => {
25548 // JsonQuery/JsonValue -> target-specific
25549 let (f, is_query) = match e {
25550 Expression::JsonQuery(f) => (f, true),
25551 Expression::JsonValue(f) => (f, false),
25552 _ => return Ok(e),
25553 };
25554 match target {
25555 DialectType::TSQL | DialectType::Fabric => {
25556 // ISNULL(JSON_QUERY(...), JSON_VALUE(...))
25557 let json_query = Expression::Function(Box::new(Function::new(
25558 "JSON_QUERY".to_string(),
25559 vec![f.this.clone(), f.path.clone()],
25560 )));
25561 let json_value = Expression::Function(Box::new(Function::new(
25562 "JSON_VALUE".to_string(),
25563 vec![f.this, f.path],
25564 )));
25565 Ok(Expression::Function(Box::new(Function::new(
25566 "ISNULL".to_string(),
25567 vec![json_query, json_value],
25568 ))))
25569 }
25570 DialectType::Spark | DialectType::Databricks | DialectType::Hive => {
25571 Ok(Expression::Function(Box::new(Function::new(
25572 "GET_JSON_OBJECT".to_string(),
25573 vec![f.this, f.path],
25574 ))))
25575 }
25576 DialectType::PostgreSQL | DialectType::Redshift => {
25577 Ok(Expression::Function(Box::new(Function::new(
25578 "JSON_EXTRACT_PATH_TEXT".to_string(),
25579 vec![f.this, f.path],
25580 ))))
25581 }
25582 DialectType::DuckDB | DialectType::SQLite => {
25583 // json -> path arrow syntax
25584 Ok(Expression::JsonExtract(Box::new(
25585 crate::expressions::JsonExtractFunc {
25586 this: f.this,
25587 path: f.path,
25588 returning: f.returning,
25589 arrow_syntax: true,
25590 hash_arrow_syntax: false,
25591 wrapper_option: f.wrapper_option,
25592 quotes_option: f.quotes_option,
25593 on_scalar_string: f.on_scalar_string,
25594 on_error: f.on_error,
25595 },
25596 )))
25597 }
25598 DialectType::Snowflake => {
25599 // GET_PATH(PARSE_JSON(json), 'path')
25600 // Strip $. prefix from path
25601 // Only wrap in PARSE_JSON if not already a PARSE_JSON call or ParseJson expression
25602 let json_expr = match &f.this {
25603 Expression::Function(ref inner_f)
25604 if inner_f.name.eq_ignore_ascii_case("PARSE_JSON") =>
25605 {
25606 f.this
25607 }
25608 Expression::ParseJson(_) => {
25609 // Already a ParseJson expression, which generates as PARSE_JSON(...)
25610 f.this
25611 }
25612 _ => Expression::Function(Box::new(Function::new(
25613 "PARSE_JSON".to_string(),
25614 vec![f.this],
25615 ))),
25616 };
25617 let path_str = match &f.path {
25618 Expression::Literal(lit)
25619 if matches!(lit.as_ref(), Literal::String(_)) =>
25620 {
25621 let Literal::String(s) = lit.as_ref() else {
25622 unreachable!()
25623 };
25624 let stripped = s.strip_prefix("$.").unwrap_or(s);
25625 Expression::Literal(Box::new(Literal::String(
25626 stripped.to_string(),
25627 )))
25628 }
25629 other => other.clone(),
25630 };
25631 Ok(Expression::Function(Box::new(Function::new(
25632 "GET_PATH".to_string(),
25633 vec![json_expr, path_str],
25634 ))))
25635 }
25636 _ => {
25637 // Default: keep as JSON_QUERY/JSON_VALUE function
25638 let func_name = if is_query { "JSON_QUERY" } else { "JSON_VALUE" };
25639 Ok(Expression::Function(Box::new(Function::new(
25640 func_name.to_string(),
25641 vec![f.this, f.path],
25642 ))))
25643 }
25644 }
25645 }
25646
25647 Action::JsonLiteralToJsonParse => {
25648 // CAST('x' AS JSON) -> JSON_PARSE('x') for Presto, PARSE_JSON for Snowflake
25649 // Also DuckDB CAST(x AS JSON) -> JSON_PARSE(x) for Trino/Presto/Athena
25650 if let Expression::Cast(c) = e {
25651 let func_name = if matches!(target, DialectType::Snowflake) {
25652 "PARSE_JSON"
25653 } else {
25654 "JSON_PARSE"
25655 };
25656 Ok(Expression::Function(Box::new(Function::new(
25657 func_name.to_string(),
25658 vec![c.this],
25659 ))))
25660 } else {
25661 Ok(e)
25662 }
25663 }
25664
25665 Action::DuckDBCastJsonToVariant => {
25666 if let Expression::Cast(c) = e {
25667 Ok(Expression::Cast(Box::new(Cast {
25668 this: c.this,
25669 to: DataType::Custom {
25670 name: "VARIANT".to_string(),
25671 },
25672 trailing_comments: c.trailing_comments,
25673 double_colon_syntax: false,
25674 format: None,
25675 default: None,
25676 inferred_type: None,
25677 })))
25678 } else {
25679 Ok(e)
25680 }
25681 }
25682
25683 Action::DuckDBTryCastJsonToTryJsonParse => {
25684 // DuckDB TRY_CAST(x AS JSON) -> TRY(JSON_PARSE(x)) for Trino/Presto/Athena
25685 if let Expression::TryCast(c) = e {
25686 let json_parse = Expression::Function(Box::new(Function::new(
25687 "JSON_PARSE".to_string(),
25688 vec![c.this],
25689 )));
25690 Ok(Expression::Function(Box::new(Function::new(
25691 "TRY".to_string(),
25692 vec![json_parse],
25693 ))))
25694 } else {
25695 Ok(e)
25696 }
25697 }
25698
25699 Action::DuckDBJsonFuncToJsonParse => {
25700 // DuckDB json(x) -> JSON_PARSE(x) for Trino/Presto/Athena
25701 if let Expression::Function(f) = e {
25702 let args = f.args;
25703 Ok(Expression::Function(Box::new(Function::new(
25704 "JSON_PARSE".to_string(),
25705 args,
25706 ))))
25707 } else {
25708 Ok(e)
25709 }
25710 }
25711
25712 Action::DuckDBJsonValidToIsJson => {
25713 // DuckDB json_valid(x) -> x IS JSON (SQL:2016 predicate) for Trino/Presto/Athena
25714 if let Expression::Function(mut f) = e {
25715 let arg = f.args.remove(0);
25716 Ok(Expression::IsJson(Box::new(crate::expressions::IsJson {
25717 this: arg,
25718 json_type: None,
25719 unique_keys: None,
25720 negated: false,
25721 })))
25722 } else {
25723 Ok(e)
25724 }
25725 }
25726
25727 Action::AtTimeZoneConvert => {
25728 // AT TIME ZONE -> target-specific conversion
25729 if let Expression::AtTimeZone(atz) = e {
25730 match target {
25731 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
25732 Ok(Expression::Function(Box::new(Function::new(
25733 "AT_TIMEZONE".to_string(),
25734 vec![atz.this, atz.zone],
25735 ))))
25736 }
25737 DialectType::Spark | DialectType::Databricks => {
25738 Ok(Expression::Function(Box::new(Function::new(
25739 "FROM_UTC_TIMESTAMP".to_string(),
25740 vec![atz.this, atz.zone],
25741 ))))
25742 }
25743 DialectType::Snowflake => {
25744 // CONVERT_TIMEZONE('zone', expr)
25745 Ok(Expression::Function(Box::new(Function::new(
25746 "CONVERT_TIMEZONE".to_string(),
25747 vec![atz.zone, atz.this],
25748 ))))
25749 }
25750 DialectType::BigQuery => {
25751 // TIMESTAMP(DATETIME(expr, 'zone'))
25752 let datetime_call = Expression::Function(Box::new(Function::new(
25753 "DATETIME".to_string(),
25754 vec![atz.this, atz.zone],
25755 )));
25756 Ok(Expression::Function(Box::new(Function::new(
25757 "TIMESTAMP".to_string(),
25758 vec![datetime_call],
25759 ))))
25760 }
25761 _ => Ok(Expression::Function(Box::new(Function::new(
25762 "AT_TIMEZONE".to_string(),
25763 vec![atz.this, atz.zone],
25764 )))),
25765 }
25766 } else {
25767 Ok(e)
25768 }
25769 }
25770
25771 Action::DayOfWeekConvert => {
25772 // DAY_OF_WEEK -> ISODOW for DuckDB, ((DAYOFWEEK(x) % 7) + 1) for Spark
25773 if let Expression::DayOfWeek(f) = e {
25774 match target {
25775 DialectType::DuckDB => Ok(Expression::Function(Box::new(
25776 Function::new("ISODOW".to_string(), vec![f.this]),
25777 ))),
25778 DialectType::Spark | DialectType::Databricks => {
25779 // ((DAYOFWEEK(x) % 7) + 1)
25780 let dayofweek = Expression::Function(Box::new(Function::new(
25781 "DAYOFWEEK".to_string(),
25782 vec![f.this],
25783 )));
25784 let modulo = Expression::Mod(Box::new(BinaryOp {
25785 left: dayofweek,
25786 right: Expression::number(7),
25787 left_comments: Vec::new(),
25788 operator_comments: Vec::new(),
25789 trailing_comments: Vec::new(),
25790 inferred_type: None,
25791 }));
25792 let paren_mod = Expression::Paren(Box::new(Paren {
25793 this: modulo,
25794 trailing_comments: Vec::new(),
25795 }));
25796 let add_one = Expression::Add(Box::new(BinaryOp {
25797 left: paren_mod,
25798 right: Expression::number(1),
25799 left_comments: Vec::new(),
25800 operator_comments: Vec::new(),
25801 trailing_comments: Vec::new(),
25802 inferred_type: None,
25803 }));
25804 Ok(Expression::Paren(Box::new(Paren {
25805 this: add_one,
25806 trailing_comments: Vec::new(),
25807 })))
25808 }
25809 _ => Ok(Expression::DayOfWeek(f)),
25810 }
25811 } else {
25812 Ok(e)
25813 }
25814 }
25815
25816 Action::MaxByMinByConvert => {
25817 // MAX_BY -> argMax for ClickHouse, drop 3rd arg for Spark
25818 // MIN_BY -> argMin for ClickHouse, ARG_MIN for DuckDB, drop 3rd arg for Spark/ClickHouse
25819 // Handle both Expression::Function and Expression::AggregateFunction
25820 let (is_max, args) = match &e {
25821 Expression::Function(f) => {
25822 (f.name.eq_ignore_ascii_case("MAX_BY"), f.args.clone())
25823 }
25824 Expression::AggregateFunction(af) => {
25825 (af.name.eq_ignore_ascii_case("MAX_BY"), af.args.clone())
25826 }
25827 _ => return Ok(e),
25828 };
25829 match target {
25830 DialectType::ClickHouse => {
25831 let name = if is_max { "argMax" } else { "argMin" };
25832 let mut args = args;
25833 args.truncate(2);
25834 Ok(Expression::Function(Box::new(Function::new(
25835 name.to_string(),
25836 args,
25837 ))))
25838 }
25839 DialectType::DuckDB => {
25840 let name = if is_max { "ARG_MAX" } else { "ARG_MIN" };
25841 Ok(Expression::Function(Box::new(Function::new(
25842 name.to_string(),
25843 args,
25844 ))))
25845 }
25846 DialectType::Spark | DialectType::Databricks => {
25847 let mut args = args;
25848 args.truncate(2);
25849 let name = if is_max { "MAX_BY" } else { "MIN_BY" };
25850 Ok(Expression::Function(Box::new(Function::new(
25851 name.to_string(),
25852 args,
25853 ))))
25854 }
25855 _ => Ok(e),
25856 }
25857 }
25858
25859 Action::ElementAtConvert => {
25860 // ELEMENT_AT(arr, idx) -> arr[idx] for PostgreSQL, arr[SAFE_ORDINAL(idx)] for BigQuery
25861 let (arr, idx) = if let Expression::ElementAt(bf) = e {
25862 (bf.this, bf.expression)
25863 } else if let Expression::Function(ref f) = e {
25864 if f.args.len() >= 2 {
25865 if let Expression::Function(f) = e {
25866 let mut args = f.args;
25867 let arr = args.remove(0);
25868 let idx = args.remove(0);
25869 (arr, idx)
25870 } else {
25871 unreachable!("outer condition already matched Expression::Function")
25872 }
25873 } else {
25874 return Ok(e);
25875 }
25876 } else {
25877 return Ok(e);
25878 };
25879 match target {
25880 DialectType::PostgreSQL => {
25881 // Wrap array in parens for PostgreSQL: (ARRAY[1,2,3])[4]
25882 let arr_expr = Expression::Paren(Box::new(Paren {
25883 this: arr,
25884 trailing_comments: vec![],
25885 }));
25886 Ok(Expression::Subscript(Box::new(
25887 crate::expressions::Subscript {
25888 this: arr_expr,
25889 index: idx,
25890 },
25891 )))
25892 }
25893 DialectType::BigQuery => {
25894 // BigQuery: convert ARRAY[...] to bare [...] for subscript
25895 let arr_expr = match arr {
25896 Expression::ArrayFunc(af) => Expression::ArrayFunc(Box::new(
25897 crate::expressions::ArrayConstructor {
25898 expressions: af.expressions,
25899 bracket_notation: true,
25900 use_list_keyword: false,
25901 },
25902 )),
25903 other => other,
25904 };
25905 let safe_ordinal = Expression::Function(Box::new(Function::new(
25906 "SAFE_ORDINAL".to_string(),
25907 vec![idx],
25908 )));
25909 Ok(Expression::Subscript(Box::new(
25910 crate::expressions::Subscript {
25911 this: arr_expr,
25912 index: safe_ordinal,
25913 },
25914 )))
25915 }
25916 _ => Ok(Expression::Function(Box::new(Function::new(
25917 "ELEMENT_AT".to_string(),
25918 vec![arr, idx],
25919 )))),
25920 }
25921 }
25922
25923 Action::CurrentUserParens => {
25924 // CURRENT_USER -> CURRENT_USER() for Snowflake
25925 Ok(Expression::Function(Box::new(Function::new(
25926 "CURRENT_USER".to_string(),
25927 vec![],
25928 ))))
25929 }
25930
25931 Action::ArrayAggToCollectList => {
25932 // ARRAY_AGG(x ORDER BY ...) -> COLLECT_LIST(x) for Hive/Spark
25933 // Python sqlglot Hive.arrayagg_sql strips ORDER BY for simple cases
25934 // but preserves it when DISTINCT/IGNORE NULLS/LIMIT are present
25935 match e {
25936 Expression::AggregateFunction(mut af) => {
25937 let is_simple =
25938 !af.distinct && af.ignore_nulls.is_none() && af.limit.is_none();
25939 let args = if af.args.is_empty() {
25940 vec![]
25941 } else {
25942 vec![af.args[0].clone()]
25943 };
25944 af.name = "COLLECT_LIST".to_string();
25945 af.args = args;
25946 if is_simple {
25947 af.order_by = Vec::new();
25948 }
25949 Ok(Expression::AggregateFunction(af))
25950 }
25951 Expression::ArrayAgg(agg) => {
25952 let is_simple =
25953 !agg.distinct && agg.ignore_nulls.is_none() && agg.limit.is_none();
25954 Ok(Expression::AggregateFunction(Box::new(
25955 crate::expressions::AggregateFunction {
25956 name: "COLLECT_LIST".to_string(),
25957 args: vec![agg.this.clone()],
25958 distinct: agg.distinct,
25959 filter: agg.filter.clone(),
25960 order_by: if is_simple {
25961 Vec::new()
25962 } else {
25963 agg.order_by.clone()
25964 },
25965 limit: agg.limit.clone(),
25966 ignore_nulls: agg.ignore_nulls,
25967 inferred_type: None,
25968 },
25969 )))
25970 }
25971 _ => Ok(e),
25972 }
25973 }
25974
25975 Action::ArraySyntaxConvert => {
25976 match e {
25977 // ARRAY[1, 2] (ArrayFunc bracket_notation=false) -> set bracket_notation=true
25978 // so the generator uses dialect-specific output (ARRAY() for Spark, [] for BigQuery)
25979 Expression::ArrayFunc(arr) if !arr.bracket_notation => Ok(
25980 Expression::ArrayFunc(Box::new(crate::expressions::ArrayConstructor {
25981 expressions: arr.expressions,
25982 bracket_notation: true,
25983 use_list_keyword: false,
25984 })),
25985 ),
25986 // ARRAY(y) function style -> ArrayFunc for target dialect
25987 // bracket_notation=true for BigQuery/DuckDB/ClickHouse/StarRocks (output []), false for Presto (output ARRAY[])
25988 Expression::Function(f) if f.name.eq_ignore_ascii_case("ARRAY") => {
25989 let bracket = matches!(
25990 target,
25991 DialectType::BigQuery
25992 | DialectType::DuckDB
25993 | DialectType::Snowflake
25994 | DialectType::ClickHouse
25995 | DialectType::StarRocks
25996 );
25997 Ok(Expression::ArrayFunc(Box::new(
25998 crate::expressions::ArrayConstructor {
25999 expressions: f.args,
26000 bracket_notation: bracket,
26001 use_list_keyword: false,
26002 },
26003 )))
26004 }
26005 _ => Ok(e),
26006 }
26007 }
26008
26009 Action::CastToJsonForSpark => {
26010 // CAST(x AS JSON) -> TO_JSON(x) for Spark
26011 if let Expression::Cast(c) = e {
26012 Ok(Expression::Function(Box::new(Function::new(
26013 "TO_JSON".to_string(),
26014 vec![c.this],
26015 ))))
26016 } else {
26017 Ok(e)
26018 }
26019 }
26020
26021 Action::CastJsonToFromJson => {
26022 // CAST(ParseJson(literal) AS ARRAY/MAP/STRUCT) -> FROM_JSON(literal, type_string) for Spark
26023 if let Expression::Cast(c) = e {
26024 // Extract the string literal from ParseJson
26025 let literal_expr = if let Expression::ParseJson(pj) = c.this {
26026 pj.this
26027 } else {
26028 c.this
26029 };
26030 // Convert the target DataType to Spark's type string format
26031 let type_str = Self::data_type_to_spark_string(&c.to);
26032 Ok(Expression::Function(Box::new(Function::new(
26033 "FROM_JSON".to_string(),
26034 vec![
26035 literal_expr,
26036 Expression::Literal(Box::new(Literal::String(type_str))),
26037 ],
26038 ))))
26039 } else {
26040 Ok(e)
26041 }
26042 }
26043
26044 Action::ToJsonConvert => {
26045 // TO_JSON(x) -> target-specific conversion
26046 if let Expression::ToJson(f) = e {
26047 let arg = f.this;
26048 match target {
26049 DialectType::Presto | DialectType::Trino => {
26050 // JSON_FORMAT(CAST(x AS JSON))
26051 let cast_json = Expression::Cast(Box::new(Cast {
26052 this: arg,
26053 to: DataType::Custom {
26054 name: "JSON".to_string(),
26055 },
26056 trailing_comments: vec![],
26057 double_colon_syntax: false,
26058 format: None,
26059 default: None,
26060 inferred_type: None,
26061 }));
26062 Ok(Expression::Function(Box::new(Function::new(
26063 "JSON_FORMAT".to_string(),
26064 vec![cast_json],
26065 ))))
26066 }
26067 DialectType::BigQuery => Ok(Expression::Function(Box::new(
26068 Function::new("TO_JSON_STRING".to_string(), vec![arg]),
26069 ))),
26070 DialectType::DuckDB => {
26071 // CAST(TO_JSON(x) AS TEXT)
26072 let to_json =
26073 Expression::ToJson(Box::new(crate::expressions::UnaryFunc {
26074 this: arg,
26075 original_name: None,
26076 inferred_type: None,
26077 }));
26078 Ok(Expression::Cast(Box::new(Cast {
26079 this: to_json,
26080 to: DataType::Text,
26081 trailing_comments: vec![],
26082 double_colon_syntax: false,
26083 format: None,
26084 default: None,
26085 inferred_type: None,
26086 })))
26087 }
26088 _ => Ok(Expression::ToJson(Box::new(
26089 crate::expressions::UnaryFunc {
26090 this: arg,
26091 original_name: None,
26092 inferred_type: None,
26093 },
26094 ))),
26095 }
26096 } else {
26097 Ok(e)
26098 }
26099 }
26100
26101 Action::VarianceToClickHouse => {
26102 if let Expression::Variance(f) = e {
26103 Ok(Expression::Function(Box::new(Function::new(
26104 "varSamp".to_string(),
26105 vec![f.this],
26106 ))))
26107 } else {
26108 Ok(e)
26109 }
26110 }
26111
26112 Action::StddevToClickHouse => {
26113 if let Expression::Stddev(f) = e {
26114 Ok(Expression::Function(Box::new(Function::new(
26115 "stddevSamp".to_string(),
26116 vec![f.this],
26117 ))))
26118 } else {
26119 Ok(e)
26120 }
26121 }
26122
26123 Action::ApproxQuantileConvert => {
26124 if let Expression::ApproxQuantile(aq) = e {
26125 let mut args = vec![*aq.this];
26126 if let Some(q) = aq.quantile {
26127 args.push(*q);
26128 }
26129 Ok(Expression::Function(Box::new(Function::new(
26130 "APPROX_PERCENTILE".to_string(),
26131 args,
26132 ))))
26133 } else {
26134 Ok(e)
26135 }
26136 }
26137
26138 Action::DollarParamConvert => {
26139 if let Expression::Parameter(p) = e {
26140 Ok(Expression::Parameter(Box::new(
26141 crate::expressions::Parameter {
26142 name: p.name,
26143 index: p.index,
26144 style: crate::expressions::ParameterStyle::At,
26145 quoted: p.quoted,
26146 string_quoted: p.string_quoted,
26147 expression: p.expression,
26148 },
26149 )))
26150 } else {
26151 Ok(e)
26152 }
26153 }
26154
26155 Action::EscapeStringNormalize => {
26156 if let Expression::Literal(ref lit) = e {
26157 if let Literal::EscapeString(s) = lit.as_ref() {
26158 // Strip prefix (e.g., "e:" or "E:") if present from tokenizer
26159 let stripped = if s.starts_with("e:") || s.starts_with("E:") {
26160 s[2..].to_string()
26161 } else {
26162 s.clone()
26163 };
26164 let normalized = stripped
26165 .replace('\n', "\\n")
26166 .replace('\r', "\\r")
26167 .replace('\t', "\\t");
26168 match target {
26169 DialectType::BigQuery => {
26170 // BigQuery: e'...' -> CAST(b'...' AS STRING)
26171 // Use Raw for the b'...' part to avoid double-escaping
26172 let raw_sql = format!("CAST(b'{}' AS STRING)", normalized);
26173 Ok(Expression::Raw(crate::expressions::Raw { sql: raw_sql }))
26174 }
26175 _ => Ok(Expression::Literal(Box::new(Literal::EscapeString(
26176 normalized,
26177 )))),
26178 }
26179 } else {
26180 Ok(e)
26181 }
26182 } else {
26183 Ok(e)
26184 }
26185 }
26186
26187 Action::StraightJoinCase => {
26188 // straight_join: keep lowercase for DuckDB, quote for MySQL
26189 if let Expression::Column(col) = e {
26190 if col.name.name == "STRAIGHT_JOIN" {
26191 let mut new_col = col;
26192 new_col.name.name = "straight_join".to_string();
26193 if matches!(target, DialectType::MySQL) {
26194 // MySQL: needs quoting since it's a reserved keyword
26195 new_col.name.quoted = true;
26196 }
26197 Ok(Expression::Column(new_col))
26198 } else {
26199 Ok(Expression::Column(col))
26200 }
26201 } else {
26202 Ok(e)
26203 }
26204 }
26205
26206 Action::TablesampleReservoir => {
26207 // TABLESAMPLE -> TABLESAMPLE RESERVOIR for DuckDB
26208 if let Expression::TableSample(mut ts) = e {
26209 if let Some(ref mut sample) = ts.sample {
26210 sample.method = crate::expressions::SampleMethod::Reservoir;
26211 sample.explicit_method = true;
26212 }
26213 Ok(Expression::TableSample(ts))
26214 } else {
26215 Ok(e)
26216 }
26217 }
26218
26219 Action::TablesampleSnowflakeStrip => {
26220 // Strip method and PERCENT for Snowflake target from non-Snowflake source
26221 match e {
26222 Expression::TableSample(mut ts) => {
26223 if let Some(ref mut sample) = ts.sample {
26224 sample.suppress_method_output = true;
26225 sample.unit_after_size = false;
26226 sample.is_percent = false;
26227 }
26228 Ok(Expression::TableSample(ts))
26229 }
26230 Expression::Table(mut t) => {
26231 if let Some(ref mut sample) = t.table_sample {
26232 sample.suppress_method_output = true;
26233 sample.unit_after_size = false;
26234 sample.is_percent = false;
26235 }
26236 Ok(Expression::Table(t))
26237 }
26238 _ => Ok(e),
26239 }
26240 }
26241
26242 Action::FirstToAnyValue => {
26243 // FIRST(col) IGNORE NULLS -> ANY_VALUE(col) for DuckDB
26244 if let Expression::First(mut agg) = e {
26245 agg.ignore_nulls = None;
26246 agg.name = Some("ANY_VALUE".to_string());
26247 Ok(Expression::AnyValue(agg))
26248 } else {
26249 Ok(e)
26250 }
26251 }
26252
26253 Action::ArrayIndexConvert => {
26254 // Subscript index: 1-based to 0-based for BigQuery
26255 if let Expression::Subscript(mut sub) = e {
26256 if let Expression::Literal(ref lit) = sub.index {
26257 if let Literal::Number(ref n) = lit.as_ref() {
26258 if let Ok(val) = n.parse::<i64>() {
26259 sub.index = Expression::Literal(Box::new(Literal::Number(
26260 (val - 1).to_string(),
26261 )));
26262 }
26263 }
26264 }
26265 Ok(Expression::Subscript(sub))
26266 } else {
26267 Ok(e)
26268 }
26269 }
26270
26271 Action::AnyValueIgnoreNulls => {
26272 // ANY_VALUE(x) -> ANY_VALUE(x) IGNORE NULLS for Spark
26273 if let Expression::AnyValue(mut av) = e {
26274 if av.ignore_nulls.is_none() {
26275 av.ignore_nulls = Some(true);
26276 }
26277 Ok(Expression::AnyValue(av))
26278 } else {
26279 Ok(e)
26280 }
26281 }
26282
26283 Action::BigQueryNullsOrdering => {
26284 // BigQuery doesn't support NULLS FIRST/LAST in window function ORDER BY
26285 if let Expression::WindowFunction(mut wf) = e {
26286 for o in &mut wf.over.order_by {
26287 o.nulls_first = None;
26288 }
26289 Ok(Expression::WindowFunction(wf))
26290 } else if let Expression::Ordered(mut o) = e {
26291 o.nulls_first = None;
26292 Ok(Expression::Ordered(o))
26293 } else {
26294 Ok(e)
26295 }
26296 }
26297
26298 Action::SnowflakeFloatProtect => {
26299 // Convert DataType::Float to DataType::Custom("FLOAT") to prevent
26300 // Snowflake's target transform from converting it to DOUBLE.
26301 // Non-Snowflake sources should keep their FLOAT spelling.
26302 if let Expression::DataType(DataType::Float { .. }) = e {
26303 Ok(Expression::DataType(DataType::Custom {
26304 name: "FLOAT".to_string(),
26305 }))
26306 } else {
26307 Ok(e)
26308 }
26309 }
26310
26311 Action::MysqlNullsOrdering => {
26312 // MySQL doesn't support NULLS FIRST/LAST - strip or rewrite
26313 if let Expression::Ordered(mut o) = e {
26314 let nulls_last = o.nulls_first == Some(false);
26315 let desc = o.desc;
26316 // MySQL default: ASC -> NULLS LAST, DESC -> NULLS FIRST
26317 // If requested ordering matches default, just strip NULLS clause
26318 let matches_default = if desc {
26319 // DESC default is NULLS FIRST, so nulls_first=true matches
26320 o.nulls_first == Some(true)
26321 } else {
26322 // ASC default is NULLS LAST, so nulls_first=false matches
26323 nulls_last
26324 };
26325 if matches_default {
26326 o.nulls_first = None;
26327 Ok(Expression::Ordered(o))
26328 } else {
26329 // Need CASE WHEN x IS NULL THEN 0/1 ELSE 0/1 END, x
26330 // For ASC NULLS FIRST: ORDER BY CASE WHEN x IS NULL THEN 0 ELSE 1 END, x ASC
26331 // For DESC NULLS LAST: ORDER BY CASE WHEN x IS NULL THEN 1 ELSE 0 END, x DESC
26332 let null_val = if desc { 1 } else { 0 };
26333 let non_null_val = if desc { 0 } else { 1 };
26334 let _case_expr = Expression::Case(Box::new(Case {
26335 operand: None,
26336 whens: vec![(
26337 Expression::IsNull(Box::new(crate::expressions::IsNull {
26338 this: o.this.clone(),
26339 not: false,
26340 postfix_form: false,
26341 })),
26342 Expression::number(null_val),
26343 )],
26344 else_: Some(Expression::number(non_null_val)),
26345 comments: Vec::new(),
26346 inferred_type: None,
26347 }));
26348 o.nulls_first = None;
26349 // Return a tuple of [case_expr, ordered_expr]
26350 // We need to return both as part of the ORDER BY
26351 // But since transform_recursive processes individual expressions,
26352 // we can't easily add extra ORDER BY items here.
26353 // Instead, strip the nulls_first
26354 o.nulls_first = None;
26355 Ok(Expression::Ordered(o))
26356 }
26357 } else {
26358 Ok(e)
26359 }
26360 }
26361
26362 Action::MysqlNullsLastRewrite => {
26363 // DuckDB -> MySQL: Add CASE WHEN IS NULL THEN 1 ELSE 0 END to ORDER BY
26364 // to simulate NULLS LAST for ASC ordering
26365 if let Expression::WindowFunction(mut wf) = e {
26366 let mut new_order_by = Vec::new();
26367 for o in wf.over.order_by {
26368 if !o.desc {
26369 // ASC: DuckDB has NULLS LAST, MySQL has NULLS FIRST
26370 // Add CASE WHEN expr IS NULL THEN 1 ELSE 0 END before expr
26371 let case_expr = Expression::Case(Box::new(Case {
26372 operand: None,
26373 whens: vec![(
26374 Expression::IsNull(Box::new(crate::expressions::IsNull {
26375 this: o.this.clone(),
26376 not: false,
26377 postfix_form: false,
26378 })),
26379 Expression::Literal(Box::new(Literal::Number(
26380 "1".to_string(),
26381 ))),
26382 )],
26383 else_: Some(Expression::Literal(Box::new(Literal::Number(
26384 "0".to_string(),
26385 )))),
26386 comments: Vec::new(),
26387 inferred_type: None,
26388 }));
26389 new_order_by.push(crate::expressions::Ordered {
26390 this: case_expr,
26391 desc: false,
26392 nulls_first: None,
26393 explicit_asc: false,
26394 with_fill: None,
26395 });
26396 let mut ordered = o;
26397 ordered.nulls_first = None;
26398 new_order_by.push(ordered);
26399 } else {
26400 // DESC: DuckDB has NULLS LAST, MySQL also has NULLS LAST (NULLs smallest in DESC)
26401 // No change needed
26402 let mut ordered = o;
26403 ordered.nulls_first = None;
26404 new_order_by.push(ordered);
26405 }
26406 }
26407 wf.over.order_by = new_order_by;
26408 Ok(Expression::WindowFunction(wf))
26409 } else {
26410 Ok(e)
26411 }
26412 }
26413
26414 Action::RespectNullsConvert => {
26415 // RESPECT NULLS -> strip for SQLite (FIRST_VALUE(c) OVER (...))
26416 if let Expression::WindowFunction(mut wf) = e {
26417 match &mut wf.this {
26418 Expression::FirstValue(ref mut vf) => {
26419 if vf.ignore_nulls == Some(false) {
26420 vf.ignore_nulls = None;
26421 // For SQLite, we'd need to add NULLS LAST to ORDER BY in the OVER clause
26422 // but that's handled by the generator's NULLS ordering
26423 }
26424 }
26425 Expression::LastValue(ref mut vf) => {
26426 if vf.ignore_nulls == Some(false) {
26427 vf.ignore_nulls = None;
26428 }
26429 }
26430 _ => {}
26431 }
26432 Ok(Expression::WindowFunction(wf))
26433 } else {
26434 Ok(e)
26435 }
26436 }
26437
26438 Action::SnowflakeWindowFrameStrip => {
26439 // Strip the default ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
26440 // for FIRST_VALUE/LAST_VALUE/NTH_VALUE when targeting Snowflake
26441 if let Expression::WindowFunction(mut wf) = e {
26442 wf.over.frame = None;
26443 Ok(Expression::WindowFunction(wf))
26444 } else {
26445 Ok(e)
26446 }
26447 }
26448
26449 Action::SnowflakeWindowFrameAdd => {
26450 // Add default ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
26451 // for FIRST_VALUE/LAST_VALUE/NTH_VALUE when transpiling from Snowflake to non-Snowflake
26452 if let Expression::WindowFunction(mut wf) = e {
26453 wf.over.frame = Some(crate::expressions::WindowFrame {
26454 kind: crate::expressions::WindowFrameKind::Rows,
26455 start: crate::expressions::WindowFrameBound::UnboundedPreceding,
26456 end: Some(crate::expressions::WindowFrameBound::UnboundedFollowing),
26457 exclude: None,
26458 kind_text: None,
26459 start_side_text: None,
26460 end_side_text: None,
26461 });
26462 Ok(Expression::WindowFunction(wf))
26463 } else {
26464 Ok(e)
26465 }
26466 }
26467
26468 Action::CreateTableStripComment => {
26469 // Strip COMMENT column constraint, USING, PARTITIONED BY for DuckDB
26470 if let Expression::CreateTable(mut ct) = e {
26471 for col in &mut ct.columns {
26472 col.comment = None;
26473 col.constraints.retain(|c| {
26474 !matches!(c, crate::expressions::ColumnConstraint::Comment(_))
26475 });
26476 // Also remove Comment from constraint_order
26477 col.constraint_order.retain(|c| {
26478 !matches!(c, crate::expressions::ConstraintType::Comment)
26479 });
26480 }
26481 // Strip properties (USING, PARTITIONED BY, etc.)
26482 ct.properties.clear();
26483 Ok(Expression::CreateTable(ct))
26484 } else {
26485 Ok(e)
26486 }
26487 }
26488
26489 Action::AlterTableToSpRename => {
26490 // ALTER TABLE db.t1 RENAME TO db.t2 -> EXEC sp_rename 'db.t1', 't2'
26491 if let Expression::AlterTable(ref at) = e {
26492 if let Some(crate::expressions::AlterTableAction::RenameTable(
26493 ref new_tbl,
26494 )) = at.actions.first()
26495 {
26496 // Build the old table name using TSQL bracket quoting
26497 let old_name = if let Some(ref schema) = at.name.schema {
26498 if at.name.name.quoted || schema.quoted {
26499 format!("[{}].[{}]", schema.name, at.name.name.name)
26500 } else {
26501 format!("{}.{}", schema.name, at.name.name.name)
26502 }
26503 } else {
26504 if at.name.name.quoted {
26505 format!("[{}]", at.name.name.name)
26506 } else {
26507 at.name.name.name.clone()
26508 }
26509 };
26510 let new_name = new_tbl.name.name.clone();
26511 // EXEC sp_rename 'old_name', 'new_name'
26512 let sql = format!("EXEC sp_rename '{}', '{}'", old_name, new_name);
26513 Ok(Expression::Raw(crate::expressions::Raw { sql }))
26514 } else {
26515 Ok(e)
26516 }
26517 } else {
26518 Ok(e)
26519 }
26520 }
26521
26522 Action::SnowflakeIntervalFormat => {
26523 // INTERVAL '2' HOUR -> INTERVAL '2 HOUR' for Snowflake
26524 if let Expression::Interval(mut iv) = e {
26525 if let (Some(Expression::Literal(lit)), Some(ref unit_spec)) =
26526 (&iv.this, &iv.unit)
26527 {
26528 if let Literal::String(ref val) = lit.as_ref() {
26529 let unit_str = match unit_spec {
26530 crate::expressions::IntervalUnitSpec::Simple {
26531 unit, ..
26532 } => match unit {
26533 crate::expressions::IntervalUnit::Year => "YEAR",
26534 crate::expressions::IntervalUnit::Quarter => "QUARTER",
26535 crate::expressions::IntervalUnit::Month => "MONTH",
26536 crate::expressions::IntervalUnit::Week => "WEEK",
26537 crate::expressions::IntervalUnit::Day => "DAY",
26538 crate::expressions::IntervalUnit::Hour => "HOUR",
26539 crate::expressions::IntervalUnit::Minute => "MINUTE",
26540 crate::expressions::IntervalUnit::Second => "SECOND",
26541 crate::expressions::IntervalUnit::Millisecond => {
26542 "MILLISECOND"
26543 }
26544 crate::expressions::IntervalUnit::Microsecond => {
26545 "MICROSECOND"
26546 }
26547 crate::expressions::IntervalUnit::Nanosecond => {
26548 "NANOSECOND"
26549 }
26550 },
26551 _ => "",
26552 };
26553 if !unit_str.is_empty() {
26554 let combined = format!("{} {}", val, unit_str);
26555 iv.this = Some(Expression::Literal(Box::new(Literal::String(
26556 combined,
26557 ))));
26558 iv.unit = None;
26559 }
26560 }
26561 }
26562 Ok(Expression::Interval(iv))
26563 } else {
26564 Ok(e)
26565 }
26566 }
26567
26568 Action::ArrayConcatBracketConvert => {
26569 // Expression::Array/ArrayFunc -> target-specific
26570 // For PostgreSQL: Array -> ArrayFunc (bracket_notation: false)
26571 // For Redshift: Array/ArrayFunc -> Function("ARRAY", args) to produce ARRAY(1, 2) with parens
26572 match e {
26573 Expression::Array(arr) => {
26574 if matches!(target, DialectType::Redshift) {
26575 Ok(Expression::Function(Box::new(Function::new(
26576 "ARRAY".to_string(),
26577 arr.expressions,
26578 ))))
26579 } else {
26580 Ok(Expression::ArrayFunc(Box::new(
26581 crate::expressions::ArrayConstructor {
26582 expressions: arr.expressions,
26583 bracket_notation: false,
26584 use_list_keyword: false,
26585 },
26586 )))
26587 }
26588 }
26589 Expression::ArrayFunc(arr) => {
26590 // Only for Redshift: convert bracket-notation ArrayFunc to Function("ARRAY")
26591 if matches!(target, DialectType::Redshift) {
26592 Ok(Expression::Function(Box::new(Function::new(
26593 "ARRAY".to_string(),
26594 arr.expressions,
26595 ))))
26596 } else {
26597 Ok(Expression::ArrayFunc(arr))
26598 }
26599 }
26600 _ => Ok(e),
26601 }
26602 }
26603
26604 Action::BitAggFloatCast => {
26605 // BIT_OR/BIT_AND/BIT_XOR with float/decimal cast arg -> wrap with ROUND+INT cast for DuckDB
26606 // For FLOAT/DOUBLE/REAL: CAST(ROUND(CAST(val AS type)) AS INT)
26607 // For DECIMAL: CAST(CAST(val AS DECIMAL(p,s)) AS INT)
26608 let int_type = DataType::Int {
26609 length: None,
26610 integer_spelling: false,
26611 };
26612 let wrap_agg = |agg_this: Expression, int_dt: DataType| -> Expression {
26613 if let Expression::Cast(c) = agg_this {
26614 match &c.to {
26615 DataType::Float { .. }
26616 | DataType::Double { .. }
26617 | DataType::Custom { .. } => {
26618 // FLOAT/DOUBLE/REAL: CAST(ROUND(CAST(val AS type)) AS INT)
26619 // Change FLOAT to REAL (Float with real_spelling=true) for DuckDB generator
26620 let inner_type = match &c.to {
26621 DataType::Float {
26622 precision, scale, ..
26623 } => DataType::Float {
26624 precision: *precision,
26625 scale: *scale,
26626 real_spelling: true,
26627 },
26628 other => other.clone(),
26629 };
26630 let inner_cast =
26631 Expression::Cast(Box::new(crate::expressions::Cast {
26632 this: c.this.clone(),
26633 to: inner_type,
26634 trailing_comments: Vec::new(),
26635 double_colon_syntax: false,
26636 format: None,
26637 default: None,
26638 inferred_type: None,
26639 }));
26640 let rounded = Expression::Function(Box::new(Function::new(
26641 "ROUND".to_string(),
26642 vec![inner_cast],
26643 )));
26644 Expression::Cast(Box::new(crate::expressions::Cast {
26645 this: rounded,
26646 to: int_dt,
26647 trailing_comments: Vec::new(),
26648 double_colon_syntax: false,
26649 format: None,
26650 default: None,
26651 inferred_type: None,
26652 }))
26653 }
26654 DataType::Decimal { .. } => {
26655 // DECIMAL: CAST(CAST(val AS DECIMAL(p,s)) AS INT)
26656 Expression::Cast(Box::new(crate::expressions::Cast {
26657 this: Expression::Cast(c),
26658 to: int_dt,
26659 trailing_comments: Vec::new(),
26660 double_colon_syntax: false,
26661 format: None,
26662 default: None,
26663 inferred_type: None,
26664 }))
26665 }
26666 _ => Expression::Cast(c),
26667 }
26668 } else {
26669 agg_this
26670 }
26671 };
26672 match e {
26673 Expression::BitwiseOrAgg(mut f) => {
26674 f.this = wrap_agg(f.this, int_type);
26675 Ok(Expression::BitwiseOrAgg(f))
26676 }
26677 Expression::BitwiseAndAgg(mut f) => {
26678 let int_type = DataType::Int {
26679 length: None,
26680 integer_spelling: false,
26681 };
26682 f.this = wrap_agg(f.this, int_type);
26683 Ok(Expression::BitwiseAndAgg(f))
26684 }
26685 Expression::BitwiseXorAgg(mut f) => {
26686 let int_type = DataType::Int {
26687 length: None,
26688 integer_spelling: false,
26689 };
26690 f.this = wrap_agg(f.this, int_type);
26691 Ok(Expression::BitwiseXorAgg(f))
26692 }
26693 _ => Ok(e),
26694 }
26695 }
26696
26697 Action::BitAggSnowflakeRename => {
26698 // BIT_OR -> BITORAGG, BIT_AND -> BITANDAGG, BIT_XOR -> BITXORAGG for Snowflake
26699 match e {
26700 Expression::BitwiseOrAgg(f) => Ok(Expression::Function(Box::new(
26701 Function::new("BITORAGG".to_string(), vec![f.this]),
26702 ))),
26703 Expression::BitwiseAndAgg(f) => Ok(Expression::Function(Box::new(
26704 Function::new("BITANDAGG".to_string(), vec![f.this]),
26705 ))),
26706 Expression::BitwiseXorAgg(f) => Ok(Expression::Function(Box::new(
26707 Function::new("BITXORAGG".to_string(), vec![f.this]),
26708 ))),
26709 _ => Ok(e),
26710 }
26711 }
26712
26713 Action::StrftimeCastTimestamp => {
26714 // CAST(x AS TIMESTAMP) -> CAST(x AS TIMESTAMP_NTZ) for Spark
26715 if let Expression::Cast(mut c) = e {
26716 if matches!(
26717 c.to,
26718 DataType::Timestamp {
26719 timezone: false,
26720 ..
26721 }
26722 ) {
26723 c.to = DataType::Custom {
26724 name: "TIMESTAMP_NTZ".to_string(),
26725 };
26726 }
26727 Ok(Expression::Cast(c))
26728 } else {
26729 Ok(e)
26730 }
26731 }
26732
26733 Action::DecimalDefaultPrecision => {
26734 // DECIMAL without precision -> DECIMAL(18, 3) for Snowflake
26735 if let Expression::Cast(mut c) = e {
26736 if matches!(
26737 c.to,
26738 DataType::Decimal {
26739 precision: None,
26740 ..
26741 }
26742 ) {
26743 c.to = DataType::Decimal {
26744 precision: Some(18),
26745 scale: Some(3),
26746 };
26747 }
26748 Ok(Expression::Cast(c))
26749 } else {
26750 Ok(e)
26751 }
26752 }
26753
26754 Action::FilterToIff => {
26755 // FILTER(WHERE cond) -> rewrite aggregate: AGG(IFF(cond, val, NULL))
26756 if let Expression::Filter(f) = e {
26757 let condition = *f.expression;
26758 let agg = *f.this;
26759 // Strip WHERE from condition
26760 let cond = match condition {
26761 Expression::Where(w) => w.this,
26762 other => other,
26763 };
26764 // Extract the aggregate function and its argument
26765 // We want AVG(IFF(condition, x, NULL))
26766 match agg {
26767 Expression::Function(mut func) => {
26768 if !func.args.is_empty() {
26769 let orig_arg = func.args[0].clone();
26770 let iff_call = Expression::Function(Box::new(Function::new(
26771 "IFF".to_string(),
26772 vec![cond, orig_arg, Expression::Null(Null)],
26773 )));
26774 func.args[0] = iff_call;
26775 Ok(Expression::Function(func))
26776 } else {
26777 Ok(Expression::Filter(Box::new(crate::expressions::Filter {
26778 this: Box::new(Expression::Function(func)),
26779 expression: Box::new(cond),
26780 })))
26781 }
26782 }
26783 Expression::Avg(mut avg) => {
26784 let iff_call = Expression::Function(Box::new(Function::new(
26785 "IFF".to_string(),
26786 vec![cond, avg.this.clone(), Expression::Null(Null)],
26787 )));
26788 avg.this = iff_call;
26789 Ok(Expression::Avg(avg))
26790 }
26791 Expression::Sum(mut s) => {
26792 let iff_call = Expression::Function(Box::new(Function::new(
26793 "IFF".to_string(),
26794 vec![cond, s.this.clone(), Expression::Null(Null)],
26795 )));
26796 s.this = iff_call;
26797 Ok(Expression::Sum(s))
26798 }
26799 Expression::Count(mut c) => {
26800 if let Some(ref this_expr) = c.this {
26801 let iff_call = Expression::Function(Box::new(Function::new(
26802 "IFF".to_string(),
26803 vec![cond, this_expr.clone(), Expression::Null(Null)],
26804 )));
26805 c.this = Some(iff_call);
26806 }
26807 Ok(Expression::Count(c))
26808 }
26809 other => {
26810 // Fallback: keep as Filter
26811 Ok(Expression::Filter(Box::new(crate::expressions::Filter {
26812 this: Box::new(other),
26813 expression: Box::new(cond),
26814 })))
26815 }
26816 }
26817 } else {
26818 Ok(e)
26819 }
26820 }
26821
26822 Action::AggFilterToIff => {
26823 // AggFunc.filter -> IFF wrapping: AVG(x) FILTER(WHERE cond) -> AVG(IFF(cond, x, NULL))
26824 // Helper macro to handle the common AggFunc case
26825 macro_rules! handle_agg_filter_to_iff {
26826 ($variant:ident, $agg:expr) => {{
26827 let mut agg = $agg;
26828 if let Some(filter_cond) = agg.filter.take() {
26829 let iff_call = Expression::Function(Box::new(Function::new(
26830 "IFF".to_string(),
26831 vec![filter_cond, agg.this.clone(), Expression::Null(Null)],
26832 )));
26833 agg.this = iff_call;
26834 }
26835 Ok(Expression::$variant(agg))
26836 }};
26837 }
26838
26839 match e {
26840 Expression::Avg(agg) => handle_agg_filter_to_iff!(Avg, agg),
26841 Expression::Sum(agg) => handle_agg_filter_to_iff!(Sum, agg),
26842 Expression::Min(agg) => handle_agg_filter_to_iff!(Min, agg),
26843 Expression::Max(agg) => handle_agg_filter_to_iff!(Max, agg),
26844 Expression::ArrayAgg(agg) => handle_agg_filter_to_iff!(ArrayAgg, agg),
26845 Expression::CountIf(agg) => handle_agg_filter_to_iff!(CountIf, agg),
26846 Expression::Stddev(agg) => handle_agg_filter_to_iff!(Stddev, agg),
26847 Expression::StddevPop(agg) => handle_agg_filter_to_iff!(StddevPop, agg),
26848 Expression::StddevSamp(agg) => handle_agg_filter_to_iff!(StddevSamp, agg),
26849 Expression::Variance(agg) => handle_agg_filter_to_iff!(Variance, agg),
26850 Expression::VarPop(agg) => handle_agg_filter_to_iff!(VarPop, agg),
26851 Expression::VarSamp(agg) => handle_agg_filter_to_iff!(VarSamp, agg),
26852 Expression::Median(agg) => handle_agg_filter_to_iff!(Median, agg),
26853 Expression::Mode(agg) => handle_agg_filter_to_iff!(Mode, agg),
26854 Expression::First(agg) => handle_agg_filter_to_iff!(First, agg),
26855 Expression::Last(agg) => handle_agg_filter_to_iff!(Last, agg),
26856 Expression::AnyValue(agg) => handle_agg_filter_to_iff!(AnyValue, agg),
26857 Expression::ApproxDistinct(agg) => {
26858 handle_agg_filter_to_iff!(ApproxDistinct, agg)
26859 }
26860 Expression::Count(mut c) => {
26861 if let Some(filter_cond) = c.filter.take() {
26862 if let Some(ref this_expr) = c.this {
26863 let iff_call = Expression::Function(Box::new(Function::new(
26864 "IFF".to_string(),
26865 vec![
26866 filter_cond,
26867 this_expr.clone(),
26868 Expression::Null(Null),
26869 ],
26870 )));
26871 c.this = Some(iff_call);
26872 }
26873 }
26874 Ok(Expression::Count(c))
26875 }
26876 other => Ok(other),
26877 }
26878 }
26879
26880 Action::JsonToGetPath => {
26881 // JSON_EXTRACT(x, '$.key') -> GET_PATH(PARSE_JSON(x), 'key')
26882 if let Expression::JsonExtract(je) = e {
26883 // Convert to PARSE_JSON() wrapper:
26884 // - JSON(x) -> PARSE_JSON(x)
26885 // - PARSE_JSON(x) -> keep as-is
26886 // - anything else -> wrap in PARSE_JSON()
26887 let this = match &je.this {
26888 Expression::Function(f)
26889 if f.name.eq_ignore_ascii_case("JSON") && f.args.len() == 1 =>
26890 {
26891 Expression::Function(Box::new(Function::new(
26892 "PARSE_JSON".to_string(),
26893 f.args.clone(),
26894 )))
26895 }
26896 Expression::Function(f)
26897 if f.name.eq_ignore_ascii_case("PARSE_JSON") =>
26898 {
26899 je.this.clone()
26900 }
26901 // GET_PATH result is already JSON, don't wrap
26902 Expression::Function(f) if f.name.eq_ignore_ascii_case("GET_PATH") => {
26903 je.this.clone()
26904 }
26905 other => {
26906 // Wrap non-JSON expressions in PARSE_JSON()
26907 Expression::Function(Box::new(Function::new(
26908 "PARSE_JSON".to_string(),
26909 vec![other.clone()],
26910 )))
26911 }
26912 };
26913 // Convert path: extract key from JSONPath or strip $. prefix from string
26914 let path = match &je.path {
26915 Expression::JSONPath(jp) => {
26916 // Extract the key from JSONPath: $root.key -> 'key'
26917 let mut key_parts = Vec::new();
26918 for expr in &jp.expressions {
26919 match expr {
26920 Expression::JSONPathRoot(_) => {} // skip root
26921 Expression::JSONPathKey(k) => {
26922 if let Expression::Literal(lit) = &*k.this {
26923 if let Literal::String(s) = lit.as_ref() {
26924 key_parts.push(s.clone());
26925 }
26926 }
26927 }
26928 _ => {}
26929 }
26930 }
26931 if !key_parts.is_empty() {
26932 Expression::Literal(Box::new(Literal::String(
26933 key_parts.join("."),
26934 )))
26935 } else {
26936 je.path.clone()
26937 }
26938 }
26939 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(s) if s.starts_with("$.")) =>
26940 {
26941 let Literal::String(s) = lit.as_ref() else {
26942 unreachable!()
26943 };
26944 let stripped = Self::strip_json_wildcards(&s[2..].to_string());
26945 Expression::Literal(Box::new(Literal::String(stripped)))
26946 }
26947 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(s) if s.starts_with('$')) =>
26948 {
26949 let Literal::String(s) = lit.as_ref() else {
26950 unreachable!()
26951 };
26952 let stripped = Self::strip_json_wildcards(&s[1..].to_string());
26953 Expression::Literal(Box::new(Literal::String(stripped)))
26954 }
26955 _ => je.path.clone(),
26956 };
26957 Ok(Expression::Function(Box::new(Function::new(
26958 "GET_PATH".to_string(),
26959 vec![this, path],
26960 ))))
26961 } else {
26962 Ok(e)
26963 }
26964 }
26965
26966 Action::StructToRow => {
26967 // DuckDB struct/dict -> BigQuery STRUCT(value AS key, ...) / Presto ROW
26968 // Handles both Expression::Struct and Expression::MapFunc(curly_brace_syntax=true)
26969
26970 // Extract key-value pairs from either Struct or MapFunc
26971 let kv_pairs: Option<Vec<(String, Expression)>> = match &e {
26972 Expression::Struct(s) => Some(
26973 s.fields
26974 .iter()
26975 .map(|(opt_name, field_expr)| {
26976 if let Some(name) = opt_name {
26977 (name.clone(), field_expr.clone())
26978 } else if let Expression::NamedArgument(na) = field_expr {
26979 (na.name.name.clone(), na.value.clone())
26980 } else {
26981 (String::new(), field_expr.clone())
26982 }
26983 })
26984 .collect(),
26985 ),
26986 Expression::MapFunc(m) if m.curly_brace_syntax => Some(
26987 m.keys
26988 .iter()
26989 .zip(m.values.iter())
26990 .map(|(key, value)| {
26991 let key_name = match key {
26992 Expression::Literal(lit)
26993 if matches!(lit.as_ref(), Literal::String(_)) =>
26994 {
26995 let Literal::String(s) = lit.as_ref() else {
26996 unreachable!()
26997 };
26998 s.clone()
26999 }
27000 Expression::Identifier(id) => id.name.clone(),
27001 _ => String::new(),
27002 };
27003 (key_name, value.clone())
27004 })
27005 .collect(),
27006 ),
27007 _ => None,
27008 };
27009
27010 if let Some(pairs) = kv_pairs {
27011 let mut named_args = Vec::new();
27012 for (key_name, value) in pairs {
27013 if matches!(target, DialectType::BigQuery) && !key_name.is_empty() {
27014 named_args.push(Expression::Alias(Box::new(
27015 crate::expressions::Alias::new(
27016 value,
27017 Identifier::new(key_name),
27018 ),
27019 )));
27020 } else if matches!(target, DialectType::Presto | DialectType::Trino) {
27021 named_args.push(value);
27022 } else {
27023 named_args.push(value);
27024 }
27025 }
27026
27027 if matches!(target, DialectType::BigQuery) {
27028 Ok(Expression::Function(Box::new(Function::new(
27029 "STRUCT".to_string(),
27030 named_args,
27031 ))))
27032 } else if matches!(target, DialectType::Presto | DialectType::Trino) {
27033 // For Presto/Trino, infer types and wrap in CAST(ROW(...) AS ROW(name TYPE, ...))
27034 let row_func = Expression::Function(Box::new(Function::new(
27035 "ROW".to_string(),
27036 named_args,
27037 )));
27038
27039 // Try to infer types for each pair
27040 let kv_pairs_again: Option<Vec<(String, Expression)>> = match &e {
27041 Expression::Struct(s) => Some(
27042 s.fields
27043 .iter()
27044 .map(|(opt_name, field_expr)| {
27045 if let Some(name) = opt_name {
27046 (name.clone(), field_expr.clone())
27047 } else if let Expression::NamedArgument(na) = field_expr
27048 {
27049 (na.name.name.clone(), na.value.clone())
27050 } else {
27051 (String::new(), field_expr.clone())
27052 }
27053 })
27054 .collect(),
27055 ),
27056 Expression::MapFunc(m) if m.curly_brace_syntax => Some(
27057 m.keys
27058 .iter()
27059 .zip(m.values.iter())
27060 .map(|(key, value)| {
27061 let key_name = match key {
27062 Expression::Literal(lit)
27063 if matches!(
27064 lit.as_ref(),
27065 Literal::String(_)
27066 ) =>
27067 {
27068 let Literal::String(s) = lit.as_ref() else {
27069 unreachable!()
27070 };
27071 s.clone()
27072 }
27073 Expression::Identifier(id) => id.name.clone(),
27074 _ => String::new(),
27075 };
27076 (key_name, value.clone())
27077 })
27078 .collect(),
27079 ),
27080 _ => None,
27081 };
27082
27083 if let Some(pairs) = kv_pairs_again {
27084 // Infer types for all values
27085 let mut all_inferred = true;
27086 let mut fields = Vec::new();
27087 for (name, value) in &pairs {
27088 let inferred_type = match value {
27089 Expression::Literal(lit)
27090 if matches!(lit.as_ref(), Literal::Number(_)) =>
27091 {
27092 let Literal::Number(n) = lit.as_ref() else {
27093 unreachable!()
27094 };
27095 if n.contains('.') {
27096 Some(DataType::Double {
27097 precision: None,
27098 scale: None,
27099 })
27100 } else {
27101 Some(DataType::Int {
27102 length: None,
27103 integer_spelling: true,
27104 })
27105 }
27106 }
27107 Expression::Literal(lit)
27108 if matches!(lit.as_ref(), Literal::String(_)) =>
27109 {
27110 Some(DataType::VarChar {
27111 length: None,
27112 parenthesized_length: false,
27113 })
27114 }
27115 Expression::Boolean(_) => Some(DataType::Boolean),
27116 _ => None,
27117 };
27118 if let Some(dt) = inferred_type {
27119 fields.push(crate::expressions::StructField::new(
27120 name.clone(),
27121 dt,
27122 ));
27123 } else {
27124 all_inferred = false;
27125 break;
27126 }
27127 }
27128
27129 if all_inferred && !fields.is_empty() {
27130 let row_type = DataType::Struct {
27131 fields,
27132 nested: true,
27133 };
27134 Ok(Expression::Cast(Box::new(Cast {
27135 this: row_func,
27136 to: row_type,
27137 trailing_comments: Vec::new(),
27138 double_colon_syntax: false,
27139 format: None,
27140 default: None,
27141 inferred_type: None,
27142 })))
27143 } else {
27144 Ok(row_func)
27145 }
27146 } else {
27147 Ok(row_func)
27148 }
27149 } else {
27150 Ok(Expression::Function(Box::new(Function::new(
27151 "ROW".to_string(),
27152 named_args,
27153 ))))
27154 }
27155 } else {
27156 Ok(e)
27157 }
27158 }
27159
27160 Action::SparkStructConvert => {
27161 // Spark STRUCT(val AS name, ...) -> Presto CAST(ROW(...) AS ROW(name TYPE, ...))
27162 // or DuckDB {'name': val, ...}
27163 if let Expression::Function(f) = e {
27164 // Extract name-value pairs from aliased args
27165 let mut pairs: Vec<(String, Expression)> = Vec::new();
27166 for arg in &f.args {
27167 match arg {
27168 Expression::Alias(a) => {
27169 pairs.push((a.alias.name.clone(), a.this.clone()));
27170 }
27171 _ => {
27172 pairs.push((String::new(), arg.clone()));
27173 }
27174 }
27175 }
27176
27177 match target {
27178 DialectType::DuckDB => {
27179 // Convert to DuckDB struct literal {'name': value, ...}
27180 let mut keys = Vec::new();
27181 let mut values = Vec::new();
27182 for (name, value) in &pairs {
27183 keys.push(Expression::Literal(Box::new(Literal::String(
27184 name.clone(),
27185 ))));
27186 values.push(value.clone());
27187 }
27188 Ok(Expression::MapFunc(Box::new(
27189 crate::expressions::MapConstructor {
27190 keys,
27191 values,
27192 curly_brace_syntax: true,
27193 with_map_keyword: false,
27194 },
27195 )))
27196 }
27197 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
27198 // Convert to CAST(ROW(val1, val2) AS ROW(name1 TYPE1, name2 TYPE2))
27199 let row_args: Vec<Expression> =
27200 pairs.iter().map(|(_, v)| v.clone()).collect();
27201 let row_func = Expression::Function(Box::new(Function::new(
27202 "ROW".to_string(),
27203 row_args,
27204 )));
27205
27206 // Infer types
27207 let mut all_inferred = true;
27208 let mut fields = Vec::new();
27209 for (name, value) in &pairs {
27210 let inferred_type = match value {
27211 Expression::Literal(lit)
27212 if matches!(lit.as_ref(), Literal::Number(_)) =>
27213 {
27214 let Literal::Number(n) = lit.as_ref() else {
27215 unreachable!()
27216 };
27217 if n.contains('.') {
27218 Some(DataType::Double {
27219 precision: None,
27220 scale: None,
27221 })
27222 } else {
27223 Some(DataType::Int {
27224 length: None,
27225 integer_spelling: true,
27226 })
27227 }
27228 }
27229 Expression::Literal(lit)
27230 if matches!(lit.as_ref(), Literal::String(_)) =>
27231 {
27232 Some(DataType::VarChar {
27233 length: None,
27234 parenthesized_length: false,
27235 })
27236 }
27237 Expression::Boolean(_) => Some(DataType::Boolean),
27238 _ => None,
27239 };
27240 if let Some(dt) = inferred_type {
27241 fields.push(crate::expressions::StructField::new(
27242 name.clone(),
27243 dt,
27244 ));
27245 } else {
27246 all_inferred = false;
27247 break;
27248 }
27249 }
27250
27251 if all_inferred && !fields.is_empty() {
27252 let row_type = DataType::Struct {
27253 fields,
27254 nested: true,
27255 };
27256 Ok(Expression::Cast(Box::new(Cast {
27257 this: row_func,
27258 to: row_type,
27259 trailing_comments: Vec::new(),
27260 double_colon_syntax: false,
27261 format: None,
27262 default: None,
27263 inferred_type: None,
27264 })))
27265 } else {
27266 Ok(row_func)
27267 }
27268 }
27269 _ => Ok(Expression::Function(f)),
27270 }
27271 } else {
27272 Ok(e)
27273 }
27274 }
27275
27276 Action::ApproxCountDistinctToApproxDistinct => {
27277 // APPROX_COUNT_DISTINCT(x) -> APPROX_DISTINCT(x)
27278 if let Expression::ApproxCountDistinct(f) = e {
27279 Ok(Expression::ApproxDistinct(f))
27280 } else {
27281 Ok(e)
27282 }
27283 }
27284
27285 Action::CollectListToArrayAgg => {
27286 // COLLECT_LIST(x) -> ARRAY_AGG(x) FILTER(WHERE x IS NOT NULL)
27287 if let Expression::AggregateFunction(f) = e {
27288 let filter_expr = if !f.args.is_empty() {
27289 let arg = f.args[0].clone();
27290 Some(Expression::IsNull(Box::new(crate::expressions::IsNull {
27291 this: arg,
27292 not: true,
27293 postfix_form: false,
27294 })))
27295 } else {
27296 None
27297 };
27298 let agg = crate::expressions::AggFunc {
27299 this: if f.args.is_empty() {
27300 Expression::Null(crate::expressions::Null)
27301 } else {
27302 f.args[0].clone()
27303 },
27304 distinct: f.distinct,
27305 order_by: f.order_by.clone(),
27306 filter: filter_expr,
27307 ignore_nulls: None,
27308 name: None,
27309 having_max: None,
27310 limit: None,
27311 inferred_type: None,
27312 };
27313 Ok(Expression::ArrayAgg(Box::new(agg)))
27314 } else {
27315 Ok(e)
27316 }
27317 }
27318
27319 Action::CollectSetConvert => {
27320 // COLLECT_SET(x) -> target-specific
27321 if let Expression::AggregateFunction(f) = e {
27322 match target {
27323 DialectType::Presto => Ok(Expression::AggregateFunction(Box::new(
27324 crate::expressions::AggregateFunction {
27325 name: "SET_AGG".to_string(),
27326 args: f.args,
27327 distinct: false,
27328 order_by: f.order_by,
27329 filter: f.filter,
27330 limit: f.limit,
27331 ignore_nulls: f.ignore_nulls,
27332 inferred_type: None,
27333 },
27334 ))),
27335 DialectType::Snowflake => Ok(Expression::AggregateFunction(Box::new(
27336 crate::expressions::AggregateFunction {
27337 name: "ARRAY_UNIQUE_AGG".to_string(),
27338 args: f.args,
27339 distinct: false,
27340 order_by: f.order_by,
27341 filter: f.filter,
27342 limit: f.limit,
27343 ignore_nulls: f.ignore_nulls,
27344 inferred_type: None,
27345 },
27346 ))),
27347 DialectType::Trino | DialectType::DuckDB => {
27348 let agg = crate::expressions::AggFunc {
27349 this: if f.args.is_empty() {
27350 Expression::Null(crate::expressions::Null)
27351 } else {
27352 f.args[0].clone()
27353 },
27354 distinct: true,
27355 order_by: Vec::new(),
27356 filter: None,
27357 ignore_nulls: None,
27358 name: None,
27359 having_max: None,
27360 limit: None,
27361 inferred_type: None,
27362 };
27363 Ok(Expression::ArrayAgg(Box::new(agg)))
27364 }
27365 _ => Ok(Expression::AggregateFunction(f)),
27366 }
27367 } else {
27368 Ok(e)
27369 }
27370 }
27371
27372 Action::PercentileConvert => {
27373 // PERCENTILE(x, 0.5) -> QUANTILE(x, 0.5) / APPROX_PERCENTILE(x, 0.5)
27374 if let Expression::AggregateFunction(f) = e {
27375 let name = match target {
27376 DialectType::DuckDB => "QUANTILE",
27377 DialectType::Presto | DialectType::Trino => "APPROX_PERCENTILE",
27378 _ => "PERCENTILE",
27379 };
27380 Ok(Expression::AggregateFunction(Box::new(
27381 crate::expressions::AggregateFunction {
27382 name: name.to_string(),
27383 args: f.args,
27384 distinct: f.distinct,
27385 order_by: f.order_by,
27386 filter: f.filter,
27387 limit: f.limit,
27388 ignore_nulls: f.ignore_nulls,
27389 inferred_type: None,
27390 },
27391 )))
27392 } else {
27393 Ok(e)
27394 }
27395 }
27396
27397 Action::CorrIsnanWrap => {
27398 // CORR(a, b) -> CASE WHEN ISNAN(CORR(a, b)) THEN NULL ELSE CORR(a, b) END
27399 // The CORR expression could be AggregateFunction, WindowFunction, or Filter-wrapped
27400 let corr_clone = e.clone();
27401 let isnan = Expression::Function(Box::new(Function::new(
27402 "ISNAN".to_string(),
27403 vec![corr_clone.clone()],
27404 )));
27405 let case_expr = Expression::Case(Box::new(Case {
27406 operand: None,
27407 whens: vec![(isnan, Expression::Null(crate::expressions::Null))],
27408 else_: Some(corr_clone),
27409 comments: Vec::new(),
27410 inferred_type: None,
27411 }));
27412 Ok(case_expr)
27413 }
27414
27415 Action::TruncToDateTrunc => {
27416 // TRUNC(timestamp, 'MONTH') -> DATE_TRUNC('MONTH', timestamp)
27417 if let Expression::Function(f) = e {
27418 if f.args.len() == 2 {
27419 let timestamp = f.args[0].clone();
27420 let unit_expr = f.args[1].clone();
27421
27422 if matches!(target, DialectType::ClickHouse) {
27423 // For ClickHouse, produce Expression::DateTrunc which the generator
27424 // outputs as DATE_TRUNC(...) without going through the ClickHouse
27425 // target transform that would convert it to dateTrunc
27426 let unit_str = Self::get_unit_str_static(&unit_expr);
27427 let dt_field = match unit_str.as_str() {
27428 "YEAR" => DateTimeField::Year,
27429 "MONTH" => DateTimeField::Month,
27430 "DAY" => DateTimeField::Day,
27431 "HOUR" => DateTimeField::Hour,
27432 "MINUTE" => DateTimeField::Minute,
27433 "SECOND" => DateTimeField::Second,
27434 "WEEK" => DateTimeField::Week,
27435 "QUARTER" => DateTimeField::Quarter,
27436 _ => DateTimeField::Custom(unit_str),
27437 };
27438 Ok(Expression::DateTrunc(Box::new(
27439 crate::expressions::DateTruncFunc {
27440 this: timestamp,
27441 unit: dt_field,
27442 },
27443 )))
27444 } else {
27445 let new_args = vec![unit_expr, timestamp];
27446 Ok(Expression::Function(Box::new(Function::new(
27447 "DATE_TRUNC".to_string(),
27448 new_args,
27449 ))))
27450 }
27451 } else {
27452 Ok(Expression::Function(f))
27453 }
27454 } else {
27455 Ok(e)
27456 }
27457 }
27458
27459 Action::ArrayContainsConvert => {
27460 if let Expression::ArrayContains(f) = e {
27461 match target {
27462 DialectType::Presto | DialectType::Trino => {
27463 // ARRAY_CONTAINS(arr, val) -> CONTAINS(arr, val)
27464 Ok(Expression::Function(Box::new(Function::new(
27465 "CONTAINS".to_string(),
27466 vec![f.this, f.expression],
27467 ))))
27468 }
27469 DialectType::Snowflake => {
27470 // ARRAY_CONTAINS(arr, val) -> ARRAY_CONTAINS(CAST(val AS VARIANT), arr)
27471 let cast_val =
27472 Expression::Cast(Box::new(crate::expressions::Cast {
27473 this: f.expression,
27474 to: crate::expressions::DataType::Custom {
27475 name: "VARIANT".to_string(),
27476 },
27477 trailing_comments: Vec::new(),
27478 double_colon_syntax: false,
27479 format: None,
27480 default: None,
27481 inferred_type: None,
27482 }));
27483 Ok(Expression::Function(Box::new(Function::new(
27484 "ARRAY_CONTAINS".to_string(),
27485 vec![cast_val, f.this],
27486 ))))
27487 }
27488 _ => Ok(Expression::ArrayContains(f)),
27489 }
27490 } else {
27491 Ok(e)
27492 }
27493 }
27494
27495 Action::ArrayExceptConvert => {
27496 if let Expression::ArrayExcept(f) = e {
27497 let source_arr = f.this;
27498 let exclude_arr = f.expression;
27499 match target {
27500 DialectType::DuckDB if matches!(source, DialectType::Snowflake) => {
27501 // Snowflake ARRAY_EXCEPT -> DuckDB bag semantics:
27502 // CASE WHEN source IS NULL OR exclude IS NULL THEN NULL
27503 // ELSE LIST_TRANSFORM(LIST_FILTER(
27504 // LIST_ZIP(source, GENERATE_SERIES(1, LENGTH(source))),
27505 // pair -> (LENGTH(LIST_FILTER(source[1:pair[2]], e -> e IS NOT DISTINCT FROM pair[1]))
27506 // > LENGTH(LIST_FILTER(exclude, e -> e IS NOT DISTINCT FROM pair[1])))),
27507 // pair -> pair[1])
27508 // END
27509
27510 // Build null check
27511 let source_is_null =
27512 Expression::IsNull(Box::new(crate::expressions::IsNull {
27513 this: source_arr.clone(),
27514 not: false,
27515 postfix_form: false,
27516 }));
27517 let exclude_is_null =
27518 Expression::IsNull(Box::new(crate::expressions::IsNull {
27519 this: exclude_arr.clone(),
27520 not: false,
27521 postfix_form: false,
27522 }));
27523 let null_check =
27524 Expression::Or(Box::new(crate::expressions::BinaryOp {
27525 left: source_is_null,
27526 right: exclude_is_null,
27527 left_comments: vec![],
27528 operator_comments: vec![],
27529 trailing_comments: vec![],
27530 inferred_type: None,
27531 }));
27532
27533 // GENERATE_SERIES(1, LENGTH(source))
27534 let gen_series = Expression::Function(Box::new(Function::new(
27535 "GENERATE_SERIES".to_string(),
27536 vec![
27537 Expression::number(1),
27538 Expression::Function(Box::new(Function::new(
27539 "LENGTH".to_string(),
27540 vec![source_arr.clone()],
27541 ))),
27542 ],
27543 )));
27544
27545 // LIST_ZIP(source, GENERATE_SERIES(1, LENGTH(source)))
27546 let list_zip = Expression::Function(Box::new(Function::new(
27547 "LIST_ZIP".to_string(),
27548 vec![source_arr.clone(), gen_series],
27549 )));
27550
27551 // pair[1] and pair[2]
27552 let pair_col = Expression::column("pair");
27553 let pair_1 = Expression::Subscript(Box::new(
27554 crate::expressions::Subscript {
27555 this: pair_col.clone(),
27556 index: Expression::number(1),
27557 },
27558 ));
27559 let pair_2 = Expression::Subscript(Box::new(
27560 crate::expressions::Subscript {
27561 this: pair_col.clone(),
27562 index: Expression::number(2),
27563 },
27564 ));
27565
27566 // source[1:pair[2]]
27567 let source_slice = Expression::ArraySlice(Box::new(
27568 crate::expressions::ArraySlice {
27569 this: source_arr.clone(),
27570 start: Some(Expression::number(1)),
27571 end: Some(pair_2),
27572 },
27573 ));
27574
27575 let e_col = Expression::column("e");
27576
27577 // e -> e IS NOT DISTINCT FROM pair[1]
27578 let inner_lambda1 =
27579 Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
27580 parameters: vec![crate::expressions::Identifier::new("e")],
27581 body: Expression::NullSafeEq(Box::new(
27582 crate::expressions::BinaryOp {
27583 left: e_col.clone(),
27584 right: pair_1.clone(),
27585 left_comments: vec![],
27586 operator_comments: vec![],
27587 trailing_comments: vec![],
27588 inferred_type: None,
27589 },
27590 )),
27591 colon: false,
27592 parameter_types: vec![],
27593 }));
27594
27595 // LIST_FILTER(source[1:pair[2]], e -> e IS NOT DISTINCT FROM pair[1])
27596 let inner_filter1 = Expression::Function(Box::new(Function::new(
27597 "LIST_FILTER".to_string(),
27598 vec![source_slice, inner_lambda1],
27599 )));
27600
27601 // LENGTH(LIST_FILTER(source[1:pair[2]], ...))
27602 let len1 = Expression::Function(Box::new(Function::new(
27603 "LENGTH".to_string(),
27604 vec![inner_filter1],
27605 )));
27606
27607 // e -> e IS NOT DISTINCT FROM pair[1]
27608 let inner_lambda2 =
27609 Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
27610 parameters: vec![crate::expressions::Identifier::new("e")],
27611 body: Expression::NullSafeEq(Box::new(
27612 crate::expressions::BinaryOp {
27613 left: e_col,
27614 right: pair_1.clone(),
27615 left_comments: vec![],
27616 operator_comments: vec![],
27617 trailing_comments: vec![],
27618 inferred_type: None,
27619 },
27620 )),
27621 colon: false,
27622 parameter_types: vec![],
27623 }));
27624
27625 // LIST_FILTER(exclude, e -> e IS NOT DISTINCT FROM pair[1])
27626 let inner_filter2 = Expression::Function(Box::new(Function::new(
27627 "LIST_FILTER".to_string(),
27628 vec![exclude_arr.clone(), inner_lambda2],
27629 )));
27630
27631 // LENGTH(LIST_FILTER(exclude, ...))
27632 let len2 = Expression::Function(Box::new(Function::new(
27633 "LENGTH".to_string(),
27634 vec![inner_filter2],
27635 )));
27636
27637 // (LENGTH(...) > LENGTH(...))
27638 let cond = Expression::Paren(Box::new(Paren {
27639 this: Expression::Gt(Box::new(crate::expressions::BinaryOp {
27640 left: len1,
27641 right: len2,
27642 left_comments: vec![],
27643 operator_comments: vec![],
27644 trailing_comments: vec![],
27645 inferred_type: None,
27646 })),
27647 trailing_comments: vec![],
27648 }));
27649
27650 // pair -> (condition)
27651 let filter_lambda =
27652 Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
27653 parameters: vec![crate::expressions::Identifier::new(
27654 "pair",
27655 )],
27656 body: cond,
27657 colon: false,
27658 parameter_types: vec![],
27659 }));
27660
27661 // LIST_FILTER(LIST_ZIP(...), pair -> ...)
27662 let outer_filter = Expression::Function(Box::new(Function::new(
27663 "LIST_FILTER".to_string(),
27664 vec![list_zip, filter_lambda],
27665 )));
27666
27667 // pair -> pair[1]
27668 let transform_lambda =
27669 Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
27670 parameters: vec![crate::expressions::Identifier::new(
27671 "pair",
27672 )],
27673 body: pair_1,
27674 colon: false,
27675 parameter_types: vec![],
27676 }));
27677
27678 // LIST_TRANSFORM(LIST_FILTER(...), pair -> pair[1])
27679 let list_transform = Expression::Function(Box::new(Function::new(
27680 "LIST_TRANSFORM".to_string(),
27681 vec![outer_filter, transform_lambda],
27682 )));
27683
27684 Ok(Expression::Case(Box::new(Case {
27685 operand: None,
27686 whens: vec![(null_check, Expression::Null(Null))],
27687 else_: Some(list_transform),
27688 comments: Vec::new(),
27689 inferred_type: None,
27690 })))
27691 }
27692 DialectType::DuckDB => {
27693 // ARRAY_EXCEPT(source, exclude) -> set semantics for DuckDB:
27694 // CASE WHEN source IS NULL OR exclude IS NULL THEN NULL
27695 // ELSE LIST_FILTER(LIST_DISTINCT(source),
27696 // e -> LENGTH(LIST_FILTER(exclude, x -> x IS NOT DISTINCT FROM e)) = 0)
27697 // END
27698
27699 // Build: source IS NULL
27700 let source_is_null =
27701 Expression::IsNull(Box::new(crate::expressions::IsNull {
27702 this: source_arr.clone(),
27703 not: false,
27704 postfix_form: false,
27705 }));
27706 // Build: exclude IS NULL
27707 let exclude_is_null =
27708 Expression::IsNull(Box::new(crate::expressions::IsNull {
27709 this: exclude_arr.clone(),
27710 not: false,
27711 postfix_form: false,
27712 }));
27713 // source IS NULL OR exclude IS NULL
27714 let null_check =
27715 Expression::Or(Box::new(crate::expressions::BinaryOp {
27716 left: source_is_null,
27717 right: exclude_is_null,
27718 left_comments: vec![],
27719 operator_comments: vec![],
27720 trailing_comments: vec![],
27721 inferred_type: None,
27722 }));
27723
27724 // LIST_DISTINCT(source)
27725 let list_distinct = Expression::Function(Box::new(Function::new(
27726 "LIST_DISTINCT".to_string(),
27727 vec![source_arr.clone()],
27728 )));
27729
27730 // x IS NOT DISTINCT FROM e
27731 let x_col = Expression::column("x");
27732 let e_col = Expression::column("e");
27733 let is_not_distinct = Expression::NullSafeEq(Box::new(
27734 crate::expressions::BinaryOp {
27735 left: x_col,
27736 right: e_col.clone(),
27737 left_comments: vec![],
27738 operator_comments: vec![],
27739 trailing_comments: vec![],
27740 inferred_type: None,
27741 },
27742 ));
27743
27744 // x -> x IS NOT DISTINCT FROM e
27745 let inner_lambda =
27746 Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
27747 parameters: vec![crate::expressions::Identifier::new("x")],
27748 body: is_not_distinct,
27749 colon: false,
27750 parameter_types: vec![],
27751 }));
27752
27753 // LIST_FILTER(exclude, x -> x IS NOT DISTINCT FROM e)
27754 let inner_list_filter =
27755 Expression::Function(Box::new(Function::new(
27756 "LIST_FILTER".to_string(),
27757 vec![exclude_arr.clone(), inner_lambda],
27758 )));
27759
27760 // LENGTH(LIST_FILTER(exclude, x -> x IS NOT DISTINCT FROM e))
27761 let len_inner = Expression::Function(Box::new(Function::new(
27762 "LENGTH".to_string(),
27763 vec![inner_list_filter],
27764 )));
27765
27766 // LENGTH(...) = 0
27767 let eq_zero =
27768 Expression::Eq(Box::new(crate::expressions::BinaryOp {
27769 left: len_inner,
27770 right: Expression::number(0),
27771 left_comments: vec![],
27772 operator_comments: vec![],
27773 trailing_comments: vec![],
27774 inferred_type: None,
27775 }));
27776
27777 // e -> LENGTH(LIST_FILTER(...)) = 0
27778 let outer_lambda =
27779 Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
27780 parameters: vec![crate::expressions::Identifier::new("e")],
27781 body: eq_zero,
27782 colon: false,
27783 parameter_types: vec![],
27784 }));
27785
27786 // LIST_FILTER(LIST_DISTINCT(source), e -> ...)
27787 let outer_list_filter =
27788 Expression::Function(Box::new(Function::new(
27789 "LIST_FILTER".to_string(),
27790 vec![list_distinct, outer_lambda],
27791 )));
27792
27793 // CASE WHEN ... IS NULL ... THEN NULL ELSE LIST_FILTER(...) END
27794 Ok(Expression::Case(Box::new(Case {
27795 operand: None,
27796 whens: vec![(null_check, Expression::Null(Null))],
27797 else_: Some(outer_list_filter),
27798 comments: Vec::new(),
27799 inferred_type: None,
27800 })))
27801 }
27802 DialectType::Snowflake => {
27803 // Snowflake: ARRAY_EXCEPT(source, exclude) - keep as-is
27804 Ok(Expression::ArrayExcept(Box::new(
27805 crate::expressions::BinaryFunc {
27806 this: source_arr,
27807 expression: exclude_arr,
27808 original_name: None,
27809 inferred_type: None,
27810 },
27811 )))
27812 }
27813 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
27814 // Presto/Trino: ARRAY_EXCEPT(source, exclude) - keep function name, array syntax already converted
27815 Ok(Expression::Function(Box::new(Function::new(
27816 "ARRAY_EXCEPT".to_string(),
27817 vec![source_arr, exclude_arr],
27818 ))))
27819 }
27820 _ => Ok(Expression::ArrayExcept(Box::new(
27821 crate::expressions::BinaryFunc {
27822 this: source_arr,
27823 expression: exclude_arr,
27824 original_name: None,
27825 inferred_type: None,
27826 },
27827 ))),
27828 }
27829 } else {
27830 Ok(e)
27831 }
27832 }
27833
27834 Action::RegexpLikeExasolAnchor => {
27835 // RegexpLike -> Exasol: wrap pattern with .*...*
27836 // Exasol REGEXP_LIKE does full-string match, but RLIKE/REGEXP from other
27837 // dialects does partial match, so we need to anchor with .* on both sides
27838 if let Expression::RegexpLike(mut f) = e {
27839 match &f.pattern {
27840 Expression::Literal(lit)
27841 if matches!(lit.as_ref(), Literal::String(_)) =>
27842 {
27843 let Literal::String(s) = lit.as_ref() else {
27844 unreachable!()
27845 };
27846 // String literal: wrap with .*...*
27847 f.pattern = Expression::Literal(Box::new(Literal::String(
27848 format!(".*{}.*", s),
27849 )));
27850 }
27851 _ => {
27852 // Non-literal: wrap with CONCAT('.*', pattern, '.*')
27853 f.pattern =
27854 Expression::Paren(Box::new(crate::expressions::Paren {
27855 this: Expression::Concat(Box::new(
27856 crate::expressions::BinaryOp {
27857 left: Expression::Concat(Box::new(
27858 crate::expressions::BinaryOp {
27859 left: Expression::Literal(Box::new(
27860 Literal::String(".*".to_string()),
27861 )),
27862 right: f.pattern,
27863 left_comments: vec![],
27864 operator_comments: vec![],
27865 trailing_comments: vec![],
27866 inferred_type: None,
27867 },
27868 )),
27869 right: Expression::Literal(Box::new(
27870 Literal::String(".*".to_string()),
27871 )),
27872 left_comments: vec![],
27873 operator_comments: vec![],
27874 trailing_comments: vec![],
27875 inferred_type: None,
27876 },
27877 )),
27878 trailing_comments: vec![],
27879 }));
27880 }
27881 }
27882 Ok(Expression::RegexpLike(f))
27883 } else {
27884 Ok(e)
27885 }
27886 }
27887
27888 Action::ArrayPositionSnowflakeSwap => {
27889 // ARRAY_POSITION(arr, elem) -> ARRAY_POSITION(elem, arr) for Snowflake
27890 if let Expression::ArrayPosition(f) = e {
27891 Ok(Expression::ArrayPosition(Box::new(
27892 crate::expressions::BinaryFunc {
27893 this: f.expression,
27894 expression: f.this,
27895 original_name: f.original_name,
27896 inferred_type: f.inferred_type,
27897 },
27898 )))
27899 } else {
27900 Ok(e)
27901 }
27902 }
27903
27904 Action::SnowflakeArrayPositionToDuckDB => {
27905 // Snowflake ARRAY_POSITION(value, array) -> DuckDB ARRAY_POSITION(array, value) - 1
27906 // Snowflake uses 0-based indexing, DuckDB uses 1-based
27907 // The parser has this=value, expression=array (Snowflake order)
27908 if let Expression::ArrayPosition(f) = e {
27909 // Create ARRAY_POSITION(array, value) in standard order
27910 let standard_pos =
27911 Expression::ArrayPosition(Box::new(crate::expressions::BinaryFunc {
27912 this: f.expression, // array
27913 expression: f.this, // value
27914 original_name: f.original_name,
27915 inferred_type: f.inferred_type,
27916 }));
27917 // Subtract 1 for zero-based indexing
27918 Ok(Expression::Sub(Box::new(BinaryOp {
27919 left: standard_pos,
27920 right: Expression::number(1),
27921 left_comments: vec![],
27922 operator_comments: vec![],
27923 trailing_comments: vec![],
27924 inferred_type: None,
27925 })))
27926 } else {
27927 Ok(e)
27928 }
27929 }
27930
27931 Action::ArrayDistinctConvert => {
27932 // ARRAY_DISTINCT(arr) -> DuckDB NULL-aware CASE:
27933 // CASE WHEN ARRAY_LENGTH(arr) <> LIST_COUNT(arr)
27934 // THEN LIST_APPEND(LIST_DISTINCT(LIST_FILTER(arr, _u -> NOT _u IS NULL)), NULL)
27935 // ELSE LIST_DISTINCT(arr)
27936 // END
27937 if let Expression::ArrayDistinct(f) = e {
27938 let arr = f.this;
27939
27940 // ARRAY_LENGTH(arr)
27941 let array_length = Expression::Function(Box::new(Function::new(
27942 "ARRAY_LENGTH".to_string(),
27943 vec![arr.clone()],
27944 )));
27945 // LIST_COUNT(arr)
27946 let list_count = Expression::Function(Box::new(Function::new(
27947 "LIST_COUNT".to_string(),
27948 vec![arr.clone()],
27949 )));
27950 // ARRAY_LENGTH(arr) <> LIST_COUNT(arr)
27951 let neq = Expression::Neq(Box::new(crate::expressions::BinaryOp {
27952 left: array_length,
27953 right: list_count,
27954 left_comments: vec![],
27955 operator_comments: vec![],
27956 trailing_comments: vec![],
27957 inferred_type: None,
27958 }));
27959
27960 // _u column
27961 let u_col = Expression::column("_u");
27962 // NOT _u IS NULL
27963 let u_is_null = Expression::IsNull(Box::new(crate::expressions::IsNull {
27964 this: u_col.clone(),
27965 not: false,
27966 postfix_form: false,
27967 }));
27968 let not_u_is_null =
27969 Expression::Not(Box::new(crate::expressions::UnaryOp {
27970 this: u_is_null,
27971 inferred_type: None,
27972 }));
27973 // _u -> NOT _u IS NULL
27974 let filter_lambda =
27975 Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
27976 parameters: vec![crate::expressions::Identifier::new("_u")],
27977 body: not_u_is_null,
27978 colon: false,
27979 parameter_types: vec![],
27980 }));
27981 // LIST_FILTER(arr, _u -> NOT _u IS NULL)
27982 let list_filter = Expression::Function(Box::new(Function::new(
27983 "LIST_FILTER".to_string(),
27984 vec![arr.clone(), filter_lambda],
27985 )));
27986 // LIST_DISTINCT(LIST_FILTER(arr, ...))
27987 let list_distinct_filtered = Expression::Function(Box::new(Function::new(
27988 "LIST_DISTINCT".to_string(),
27989 vec![list_filter],
27990 )));
27991 // LIST_APPEND(LIST_DISTINCT(LIST_FILTER(...)), NULL)
27992 let list_append = Expression::Function(Box::new(Function::new(
27993 "LIST_APPEND".to_string(),
27994 vec![list_distinct_filtered, Expression::Null(Null)],
27995 )));
27996
27997 // LIST_DISTINCT(arr)
27998 let list_distinct = Expression::Function(Box::new(Function::new(
27999 "LIST_DISTINCT".to_string(),
28000 vec![arr],
28001 )));
28002
28003 // CASE WHEN neq THEN list_append ELSE list_distinct END
28004 Ok(Expression::Case(Box::new(Case {
28005 operand: None,
28006 whens: vec![(neq, list_append)],
28007 else_: Some(list_distinct),
28008 comments: Vec::new(),
28009 inferred_type: None,
28010 })))
28011 } else {
28012 Ok(e)
28013 }
28014 }
28015
28016 Action::ArrayDistinctClickHouse => {
28017 // ARRAY_DISTINCT(arr) -> arrayDistinct(arr) for ClickHouse
28018 if let Expression::ArrayDistinct(f) = e {
28019 Ok(Expression::Function(Box::new(Function::new(
28020 "arrayDistinct".to_string(),
28021 vec![f.this],
28022 ))))
28023 } else {
28024 Ok(e)
28025 }
28026 }
28027
28028 Action::ArrayContainsDuckDBConvert => {
28029 // Snowflake ARRAY_CONTAINS(value, array) -> DuckDB NULL-aware:
28030 // CASE WHEN value IS NULL
28031 // THEN NULLIF(ARRAY_LENGTH(array) <> LIST_COUNT(array), FALSE)
28032 // ELSE ARRAY_CONTAINS(array, value)
28033 // END
28034 // Note: In Rust AST from Snowflake parse, this=value (first arg), expression=array (second arg)
28035 if let Expression::ArrayContains(f) = e {
28036 let value = f.this;
28037 let array = f.expression;
28038
28039 // value IS NULL
28040 let value_is_null =
28041 Expression::IsNull(Box::new(crate::expressions::IsNull {
28042 this: value.clone(),
28043 not: false,
28044 postfix_form: false,
28045 }));
28046
28047 // ARRAY_LENGTH(array)
28048 let array_length = Expression::Function(Box::new(Function::new(
28049 "ARRAY_LENGTH".to_string(),
28050 vec![array.clone()],
28051 )));
28052 // LIST_COUNT(array)
28053 let list_count = Expression::Function(Box::new(Function::new(
28054 "LIST_COUNT".to_string(),
28055 vec![array.clone()],
28056 )));
28057 // ARRAY_LENGTH(array) <> LIST_COUNT(array)
28058 let neq = Expression::Neq(Box::new(crate::expressions::BinaryOp {
28059 left: array_length,
28060 right: list_count,
28061 left_comments: vec![],
28062 operator_comments: vec![],
28063 trailing_comments: vec![],
28064 inferred_type: None,
28065 }));
28066 // NULLIF(ARRAY_LENGTH(array) <> LIST_COUNT(array), FALSE)
28067 let nullif = Expression::Nullif(Box::new(crate::expressions::Nullif {
28068 this: Box::new(neq),
28069 expression: Box::new(Expression::Boolean(
28070 crate::expressions::BooleanLiteral { value: false },
28071 )),
28072 }));
28073
28074 // ARRAY_CONTAINS(array, value) - DuckDB syntax: array first, value second
28075 let array_contains = Expression::Function(Box::new(Function::new(
28076 "ARRAY_CONTAINS".to_string(),
28077 vec![array, value],
28078 )));
28079
28080 // CASE WHEN value IS NULL THEN NULLIF(...) ELSE ARRAY_CONTAINS(array, value) END
28081 Ok(Expression::Case(Box::new(Case {
28082 operand: None,
28083 whens: vec![(value_is_null, nullif)],
28084 else_: Some(array_contains),
28085 comments: Vec::new(),
28086 inferred_type: None,
28087 })))
28088 } else {
28089 Ok(e)
28090 }
28091 }
28092
28093 Action::StrPositionExpand => {
28094 // StrPosition with position arg -> complex STRPOS expansion for Presto/DuckDB
28095 // For Presto: IF(STRPOS(SUBSTRING(str, pos), substr) = 0, 0, STRPOS(SUBSTRING(str, pos), substr) + pos - 1)
28096 // For DuckDB: CASE WHEN STRPOS(SUBSTRING(str, pos), substr) = 0 THEN 0 ELSE STRPOS(SUBSTRING(str, pos), substr) + pos - 1 END
28097 if let Expression::StrPosition(sp) = e {
28098 let crate::expressions::StrPosition {
28099 this,
28100 substr,
28101 position,
28102 occurrence,
28103 } = *sp;
28104 let string = *this;
28105 let substr_expr = match substr {
28106 Some(s) => *s,
28107 None => Expression::Null(Null),
28108 };
28109 let pos = match position {
28110 Some(p) => *p,
28111 None => Expression::number(1),
28112 };
28113
28114 // SUBSTRING(string, pos)
28115 let substring_call = Expression::Function(Box::new(Function::new(
28116 "SUBSTRING".to_string(),
28117 vec![string.clone(), pos.clone()],
28118 )));
28119 // STRPOS(SUBSTRING(string, pos), substr)
28120 let strpos_call = Expression::Function(Box::new(Function::new(
28121 "STRPOS".to_string(),
28122 vec![substring_call, substr_expr.clone()],
28123 )));
28124 // STRPOS(...) + pos - 1
28125 let pos_adjusted =
28126 Expression::Sub(Box::new(crate::expressions::BinaryOp::new(
28127 Expression::Add(Box::new(crate::expressions::BinaryOp::new(
28128 strpos_call.clone(),
28129 pos.clone(),
28130 ))),
28131 Expression::number(1),
28132 )));
28133 // STRPOS(...) = 0
28134 let is_zero = Expression::Eq(Box::new(crate::expressions::BinaryOp::new(
28135 strpos_call.clone(),
28136 Expression::number(0),
28137 )));
28138
28139 match target {
28140 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
28141 // IF(STRPOS(SUBSTRING(str, pos), substr) = 0, 0, STRPOS(SUBSTRING(str, pos), substr) + pos - 1)
28142 Ok(Expression::Function(Box::new(Function::new(
28143 "IF".to_string(),
28144 vec![is_zero, Expression::number(0), pos_adjusted],
28145 ))))
28146 }
28147 DialectType::DuckDB => {
28148 // CASE WHEN STRPOS(SUBSTRING(str, pos), substr) = 0 THEN 0 ELSE STRPOS(SUBSTRING(str, pos), substr) + pos - 1 END
28149 Ok(Expression::Case(Box::new(Case {
28150 operand: None,
28151 whens: vec![(is_zero, Expression::number(0))],
28152 else_: Some(pos_adjusted),
28153 comments: Vec::new(),
28154 inferred_type: None,
28155 })))
28156 }
28157 _ => {
28158 // Reconstruct StrPosition
28159 Ok(Expression::StrPosition(Box::new(
28160 crate::expressions::StrPosition {
28161 this: Box::new(string),
28162 substr: Some(Box::new(substr_expr)),
28163 position: Some(Box::new(pos)),
28164 occurrence,
28165 },
28166 )))
28167 }
28168 }
28169 } else {
28170 Ok(e)
28171 }
28172 }
28173
28174 Action::MonthsBetweenConvert => {
28175 if let Expression::MonthsBetween(mb) = e {
28176 let crate::expressions::BinaryFunc {
28177 this: end_date,
28178 expression: start_date,
28179 ..
28180 } = *mb;
28181 match target {
28182 DialectType::DuckDB => {
28183 let cast_end = Self::ensure_cast_date(end_date);
28184 let cast_start = Self::ensure_cast_date(start_date);
28185 let dd = Expression::Function(Box::new(Function::new(
28186 "DATE_DIFF".to_string(),
28187 vec![
28188 Expression::string("MONTH"),
28189 cast_start.clone(),
28190 cast_end.clone(),
28191 ],
28192 )));
28193 let day_end = Expression::Function(Box::new(Function::new(
28194 "DAY".to_string(),
28195 vec![cast_end.clone()],
28196 )));
28197 let day_start = Expression::Function(Box::new(Function::new(
28198 "DAY".to_string(),
28199 vec![cast_start.clone()],
28200 )));
28201 let last_day_end = Expression::Function(Box::new(Function::new(
28202 "LAST_DAY".to_string(),
28203 vec![cast_end.clone()],
28204 )));
28205 let last_day_start = Expression::Function(Box::new(Function::new(
28206 "LAST_DAY".to_string(),
28207 vec![cast_start.clone()],
28208 )));
28209 let day_last_end = Expression::Function(Box::new(Function::new(
28210 "DAY".to_string(),
28211 vec![last_day_end],
28212 )));
28213 let day_last_start = Expression::Function(Box::new(Function::new(
28214 "DAY".to_string(),
28215 vec![last_day_start],
28216 )));
28217 let cond1 = Expression::Eq(Box::new(BinaryOp::new(
28218 day_end.clone(),
28219 day_last_end,
28220 )));
28221 let cond2 = Expression::Eq(Box::new(BinaryOp::new(
28222 day_start.clone(),
28223 day_last_start,
28224 )));
28225 let both_cond =
28226 Expression::And(Box::new(BinaryOp::new(cond1, cond2)));
28227 let day_diff =
28228 Expression::Sub(Box::new(BinaryOp::new(day_end, day_start)));
28229 let day_diff_paren =
28230 Expression::Paren(Box::new(crate::expressions::Paren {
28231 this: day_diff,
28232 trailing_comments: Vec::new(),
28233 }));
28234 let frac = Expression::Div(Box::new(BinaryOp::new(
28235 day_diff_paren,
28236 Expression::Literal(Box::new(Literal::Number(
28237 "31.0".to_string(),
28238 ))),
28239 )));
28240 let case_expr = Expression::Case(Box::new(Case {
28241 operand: None,
28242 whens: vec![(both_cond, Expression::number(0))],
28243 else_: Some(frac),
28244 comments: Vec::new(),
28245 inferred_type: None,
28246 }));
28247 Ok(Expression::Add(Box::new(BinaryOp::new(dd, case_expr))))
28248 }
28249 DialectType::Snowflake | DialectType::Redshift => {
28250 let unit = Expression::Identifier(Identifier::new("MONTH"));
28251 Ok(Expression::Function(Box::new(Function::new(
28252 "DATEDIFF".to_string(),
28253 vec![unit, start_date, end_date],
28254 ))))
28255 }
28256 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
28257 Ok(Expression::Function(Box::new(Function::new(
28258 "DATE_DIFF".to_string(),
28259 vec![Expression::string("MONTH"), start_date, end_date],
28260 ))))
28261 }
28262 _ => Ok(Expression::MonthsBetween(Box::new(
28263 crate::expressions::BinaryFunc {
28264 this: end_date,
28265 expression: start_date,
28266 original_name: None,
28267 inferred_type: None,
28268 },
28269 ))),
28270 }
28271 } else {
28272 Ok(e)
28273 }
28274 }
28275
28276 Action::AddMonthsConvert => {
28277 if let Expression::AddMonths(am) = e {
28278 let date = am.this;
28279 let val = am.expression;
28280 match target {
28281 DialectType::TSQL | DialectType::Fabric => {
28282 let cast_date = Self::ensure_cast_datetime2(date);
28283 Ok(Expression::Function(Box::new(Function::new(
28284 "DATEADD".to_string(),
28285 vec![
28286 Expression::Identifier(Identifier::new("MONTH")),
28287 val,
28288 cast_date,
28289 ],
28290 ))))
28291 }
28292 DialectType::DuckDB if matches!(source, DialectType::Snowflake) => {
28293 // DuckDB ADD_MONTHS from Snowflake: CASE WHEN LAST_DAY(date) = date THEN LAST_DAY(date + interval) ELSE date + interval END
28294 // Optionally wrapped in CAST(... AS type) if the input had a specific type
28295
28296 // Determine the cast type from the date expression
28297 let (cast_date, return_type) = match &date {
28298 Expression::Literal(lit)
28299 if matches!(lit.as_ref(), Literal::String(_)) =>
28300 {
28301 // String literal: CAST(str AS TIMESTAMP), no outer CAST
28302 (
28303 Expression::Cast(Box::new(Cast {
28304 this: date.clone(),
28305 to: DataType::Timestamp {
28306 precision: None,
28307 timezone: false,
28308 },
28309 trailing_comments: Vec::new(),
28310 double_colon_syntax: false,
28311 format: None,
28312 default: None,
28313 inferred_type: None,
28314 })),
28315 None,
28316 )
28317 }
28318 Expression::Cast(c) => {
28319 // Already cast (e.g., '2023-01-31'::DATE) - keep the cast, wrap result in CAST(... AS type)
28320 (date.clone(), Some(c.to.clone()))
28321 }
28322 _ => {
28323 // Expression or NULL::TYPE - keep as-is, check for cast type
28324 if let Expression::Cast(c) = &date {
28325 (date.clone(), Some(c.to.clone()))
28326 } else {
28327 (date.clone(), None)
28328 }
28329 }
28330 };
28331
28332 // Build the interval expression
28333 // For non-integer values (float, decimal, cast), use TO_MONTHS(CAST(ROUND(val) AS INT))
28334 // For integer values, use INTERVAL val MONTH
28335 let is_non_integer_val = match &val {
28336 Expression::Literal(lit)
28337 if matches!(lit.as_ref(), Literal::Number(_)) =>
28338 {
28339 let Literal::Number(n) = lit.as_ref() else {
28340 unreachable!()
28341 };
28342 n.contains('.')
28343 }
28344 Expression::Cast(_) => true, // e.g., 3.2::DECIMAL(10,2)
28345 Expression::Neg(n) => {
28346 if let Expression::Literal(lit) = &n.this {
28347 if let Literal::Number(s) = lit.as_ref() {
28348 s.contains('.')
28349 } else {
28350 false
28351 }
28352 } else {
28353 false
28354 }
28355 }
28356 _ => false,
28357 };
28358
28359 let add_interval = if is_non_integer_val {
28360 // TO_MONTHS(CAST(ROUND(val) AS INT))
28361 let round_val = Expression::Function(Box::new(Function::new(
28362 "ROUND".to_string(),
28363 vec![val.clone()],
28364 )));
28365 let cast_int = Expression::Cast(Box::new(Cast {
28366 this: round_val,
28367 to: DataType::Int {
28368 length: None,
28369 integer_spelling: false,
28370 },
28371 trailing_comments: Vec::new(),
28372 double_colon_syntax: false,
28373 format: None,
28374 default: None,
28375 inferred_type: None,
28376 }));
28377 Expression::Function(Box::new(Function::new(
28378 "TO_MONTHS".to_string(),
28379 vec![cast_int],
28380 )))
28381 } else {
28382 // INTERVAL val MONTH
28383 // For negative numbers, wrap in parens
28384 let interval_val = match &val {
28385 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(n) if n.starts_with('-')) =>
28386 {
28387 let Literal::Number(_) = lit.as_ref() else {
28388 unreachable!()
28389 };
28390 Expression::Paren(Box::new(Paren {
28391 this: val.clone(),
28392 trailing_comments: Vec::new(),
28393 }))
28394 }
28395 Expression::Neg(_) => Expression::Paren(Box::new(Paren {
28396 this: val.clone(),
28397 trailing_comments: Vec::new(),
28398 })),
28399 Expression::Null(_) => Expression::Paren(Box::new(Paren {
28400 this: val.clone(),
28401 trailing_comments: Vec::new(),
28402 })),
28403 _ => val.clone(),
28404 };
28405 Expression::Interval(Box::new(crate::expressions::Interval {
28406 this: Some(interval_val),
28407 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
28408 unit: crate::expressions::IntervalUnit::Month,
28409 use_plural: false,
28410 }),
28411 }))
28412 };
28413
28414 // Build: date + interval
28415 let date_plus_interval = Expression::Add(Box::new(BinaryOp::new(
28416 cast_date.clone(),
28417 add_interval.clone(),
28418 )));
28419
28420 // Build LAST_DAY(date)
28421 let last_day_date = Expression::Function(Box::new(Function::new(
28422 "LAST_DAY".to_string(),
28423 vec![cast_date.clone()],
28424 )));
28425
28426 // Build LAST_DAY(date + interval)
28427 let last_day_date_plus =
28428 Expression::Function(Box::new(Function::new(
28429 "LAST_DAY".to_string(),
28430 vec![date_plus_interval.clone()],
28431 )));
28432
28433 // Build: CASE WHEN LAST_DAY(date) = date THEN LAST_DAY(date + interval) ELSE date + interval END
28434 let case_expr = Expression::Case(Box::new(Case {
28435 operand: None,
28436 whens: vec![(
28437 Expression::Eq(Box::new(BinaryOp::new(
28438 last_day_date,
28439 cast_date.clone(),
28440 ))),
28441 last_day_date_plus,
28442 )],
28443 else_: Some(date_plus_interval),
28444 comments: Vec::new(),
28445 inferred_type: None,
28446 }));
28447
28448 // Wrap in CAST(... AS type) if needed
28449 if let Some(dt) = return_type {
28450 Ok(Expression::Cast(Box::new(Cast {
28451 this: case_expr,
28452 to: dt,
28453 trailing_comments: Vec::new(),
28454 double_colon_syntax: false,
28455 format: None,
28456 default: None,
28457 inferred_type: None,
28458 })))
28459 } else {
28460 Ok(case_expr)
28461 }
28462 }
28463 DialectType::DuckDB => {
28464 // Non-Snowflake source: simple date + INTERVAL
28465 let cast_date = if matches!(&date, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)))
28466 {
28467 Expression::Cast(Box::new(Cast {
28468 this: date,
28469 to: DataType::Timestamp {
28470 precision: None,
28471 timezone: false,
28472 },
28473 trailing_comments: Vec::new(),
28474 double_colon_syntax: false,
28475 format: None,
28476 default: None,
28477 inferred_type: None,
28478 }))
28479 } else {
28480 date
28481 };
28482 let interval =
28483 Expression::Interval(Box::new(crate::expressions::Interval {
28484 this: Some(val),
28485 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
28486 unit: crate::expressions::IntervalUnit::Month,
28487 use_plural: false,
28488 }),
28489 }));
28490 Ok(Expression::Add(Box::new(BinaryOp::new(
28491 cast_date, interval,
28492 ))))
28493 }
28494 DialectType::Snowflake => {
28495 // Keep ADD_MONTHS when source is also Snowflake
28496 if matches!(source, DialectType::Snowflake) {
28497 Ok(Expression::Function(Box::new(Function::new(
28498 "ADD_MONTHS".to_string(),
28499 vec![date, val],
28500 ))))
28501 } else {
28502 Ok(Expression::Function(Box::new(Function::new(
28503 "DATEADD".to_string(),
28504 vec![
28505 Expression::Identifier(Identifier::new("MONTH")),
28506 val,
28507 date,
28508 ],
28509 ))))
28510 }
28511 }
28512 DialectType::Redshift => {
28513 Ok(Expression::Function(Box::new(Function::new(
28514 "DATEADD".to_string(),
28515 vec![
28516 Expression::Identifier(Identifier::new("MONTH")),
28517 val,
28518 date,
28519 ],
28520 ))))
28521 }
28522 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
28523 let cast_date = if matches!(&date, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)))
28524 {
28525 Expression::Cast(Box::new(Cast {
28526 this: date,
28527 to: DataType::Timestamp {
28528 precision: None,
28529 timezone: false,
28530 },
28531 trailing_comments: Vec::new(),
28532 double_colon_syntax: false,
28533 format: None,
28534 default: None,
28535 inferred_type: None,
28536 }))
28537 } else {
28538 date
28539 };
28540 Ok(Expression::Function(Box::new(Function::new(
28541 "DATE_ADD".to_string(),
28542 vec![Expression::string("MONTH"), val, cast_date],
28543 ))))
28544 }
28545 DialectType::BigQuery => {
28546 let interval =
28547 Expression::Interval(Box::new(crate::expressions::Interval {
28548 this: Some(val),
28549 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
28550 unit: crate::expressions::IntervalUnit::Month,
28551 use_plural: false,
28552 }),
28553 }));
28554 let cast_date = if matches!(&date, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)))
28555 {
28556 Expression::Cast(Box::new(Cast {
28557 this: date,
28558 to: DataType::Custom {
28559 name: "DATETIME".to_string(),
28560 },
28561 trailing_comments: Vec::new(),
28562 double_colon_syntax: false,
28563 format: None,
28564 default: None,
28565 inferred_type: None,
28566 }))
28567 } else {
28568 date
28569 };
28570 Ok(Expression::Function(Box::new(Function::new(
28571 "DATE_ADD".to_string(),
28572 vec![cast_date, interval],
28573 ))))
28574 }
28575 DialectType::Spark | DialectType::Databricks | DialectType::Hive => {
28576 Ok(Expression::Function(Box::new(Function::new(
28577 "ADD_MONTHS".to_string(),
28578 vec![date, val],
28579 ))))
28580 }
28581 _ => {
28582 // Default: keep as AddMonths expression
28583 Ok(Expression::AddMonths(Box::new(
28584 crate::expressions::BinaryFunc {
28585 this: date,
28586 expression: val,
28587 original_name: None,
28588 inferred_type: None,
28589 },
28590 )))
28591 }
28592 }
28593 } else {
28594 Ok(e)
28595 }
28596 }
28597
28598 Action::PercentileContConvert => {
28599 // PERCENTILE_CONT(p) WITHIN GROUP (ORDER BY col) ->
28600 // Presto/Trino: APPROX_PERCENTILE(col, p)
28601 // Spark/Databricks: PERCENTILE_APPROX(col, p)
28602 if let Expression::WithinGroup(wg) = e {
28603 // Extract percentile value and order by column
28604 let (percentile, _is_disc) = match &wg.this {
28605 Expression::Function(f) => {
28606 let is_disc = f.name.eq_ignore_ascii_case("PERCENTILE_DISC");
28607 let pct = f.args.first().cloned().unwrap_or(Expression::Literal(
28608 Box::new(Literal::Number("0.5".to_string())),
28609 ));
28610 (pct, is_disc)
28611 }
28612 Expression::AggregateFunction(af) => {
28613 let is_disc = af.name.eq_ignore_ascii_case("PERCENTILE_DISC");
28614 let pct = af.args.first().cloned().unwrap_or(Expression::Literal(
28615 Box::new(Literal::Number("0.5".to_string())),
28616 ));
28617 (pct, is_disc)
28618 }
28619 Expression::PercentileCont(pc) => (pc.percentile.clone(), false),
28620 _ => return Ok(Expression::WithinGroup(wg)),
28621 };
28622 let col = wg.order_by.first().map(|o| o.this.clone()).unwrap_or(
28623 Expression::Literal(Box::new(Literal::Number("1".to_string()))),
28624 );
28625
28626 let func_name = match target {
28627 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
28628 "APPROX_PERCENTILE"
28629 }
28630 _ => "PERCENTILE_APPROX", // Spark, Databricks
28631 };
28632 Ok(Expression::Function(Box::new(Function::new(
28633 func_name.to_string(),
28634 vec![col, percentile],
28635 ))))
28636 } else {
28637 Ok(e)
28638 }
28639 }
28640
28641 Action::CurrentUserSparkParens => {
28642 // CURRENT_USER -> CURRENT_USER() for Spark
28643 if let Expression::CurrentUser(_) = e {
28644 Ok(Expression::Function(Box::new(Function::new(
28645 "CURRENT_USER".to_string(),
28646 vec![],
28647 ))))
28648 } else {
28649 Ok(e)
28650 }
28651 }
28652
28653 Action::SparkDateFuncCast => {
28654 // MONTH/YEAR/DAY('string') from Spark -> wrap arg in CAST to DATE
28655 let cast_arg = |arg: Expression| -> Expression {
28656 match target {
28657 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
28658 Self::double_cast_timestamp_date(arg)
28659 }
28660 _ => {
28661 // DuckDB, PostgreSQL, etc: CAST(arg AS DATE)
28662 Self::ensure_cast_date(arg)
28663 }
28664 }
28665 };
28666 match e {
28667 Expression::Month(f) => Ok(Expression::Month(Box::new(
28668 crate::expressions::UnaryFunc::new(cast_arg(f.this)),
28669 ))),
28670 Expression::Year(f) => Ok(Expression::Year(Box::new(
28671 crate::expressions::UnaryFunc::new(cast_arg(f.this)),
28672 ))),
28673 Expression::Day(f) => Ok(Expression::Day(Box::new(
28674 crate::expressions::UnaryFunc::new(cast_arg(f.this)),
28675 ))),
28676 other => Ok(other),
28677 }
28678 }
28679
28680 Action::MapFromArraysConvert => {
28681 // Expression::MapFromArrays -> target-specific
28682 if let Expression::MapFromArrays(mfa) = e {
28683 let keys = mfa.this;
28684 let values = mfa.expression;
28685 match target {
28686 DialectType::Snowflake => Ok(Expression::Function(Box::new(
28687 Function::new("OBJECT_CONSTRUCT".to_string(), vec![keys, values]),
28688 ))),
28689 _ => {
28690 // Hive, Presto, DuckDB, etc.: MAP(keys, values)
28691 Ok(Expression::Function(Box::new(Function::new(
28692 "MAP".to_string(),
28693 vec![keys, values],
28694 ))))
28695 }
28696 }
28697 } else {
28698 Ok(e)
28699 }
28700 }
28701
28702 Action::AnyToExists => {
28703 if let Expression::Any(q) = e {
28704 if let Some(op) = q.op.clone() {
28705 let lambda_param = crate::expressions::Identifier::new("x");
28706 let rhs = Expression::Identifier(lambda_param.clone());
28707 let body = match op {
28708 crate::expressions::QuantifiedOp::Eq => {
28709 Expression::Eq(Box::new(BinaryOp::new(q.this, rhs)))
28710 }
28711 crate::expressions::QuantifiedOp::Neq => {
28712 Expression::Neq(Box::new(BinaryOp::new(q.this, rhs)))
28713 }
28714 crate::expressions::QuantifiedOp::Lt => {
28715 Expression::Lt(Box::new(BinaryOp::new(q.this, rhs)))
28716 }
28717 crate::expressions::QuantifiedOp::Lte => {
28718 Expression::Lte(Box::new(BinaryOp::new(q.this, rhs)))
28719 }
28720 crate::expressions::QuantifiedOp::Gt => {
28721 Expression::Gt(Box::new(BinaryOp::new(q.this, rhs)))
28722 }
28723 crate::expressions::QuantifiedOp::Gte => {
28724 Expression::Gte(Box::new(BinaryOp::new(q.this, rhs)))
28725 }
28726 };
28727 let lambda =
28728 Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
28729 parameters: vec![lambda_param],
28730 body,
28731 colon: false,
28732 parameter_types: Vec::new(),
28733 }));
28734 Ok(Expression::Function(Box::new(Function::new(
28735 "EXISTS".to_string(),
28736 vec![q.subquery, lambda],
28737 ))))
28738 } else {
28739 Ok(Expression::Any(q))
28740 }
28741 } else {
28742 Ok(e)
28743 }
28744 }
28745
28746 Action::GenerateSeriesConvert => {
28747 // GENERATE_SERIES(start, end[, step]) -> SEQUENCE for Spark/Databricks/Hive, wrapped in UNNEST/EXPLODE
28748 // For DuckDB target: wrap in UNNEST(GENERATE_SERIES(...))
28749 // For PG/Redshift target: keep as GENERATE_SERIES but normalize interval string step
28750 if let Expression::Function(f) = e {
28751 if f.name.eq_ignore_ascii_case("GENERATE_SERIES") && f.args.len() >= 2 {
28752 let start = f.args[0].clone();
28753 let end = f.args[1].clone();
28754 let step = f.args.get(2).cloned();
28755
28756 // Normalize step: convert string interval like '1day' or ' 2 days ' to INTERVAL expression
28757 let step = step.map(|s| Self::normalize_interval_string(s, target));
28758
28759 // Helper: wrap CURRENT_TIMESTAMP in CAST(... AS TIMESTAMP) for Presto/Trino/Spark
28760 let maybe_cast_timestamp = |arg: Expression| -> Expression {
28761 if matches!(
28762 target,
28763 DialectType::Presto
28764 | DialectType::Trino
28765 | DialectType::Athena
28766 | DialectType::Spark
28767 | DialectType::Databricks
28768 | DialectType::Hive
28769 ) {
28770 match &arg {
28771 Expression::CurrentTimestamp(_) => {
28772 Expression::Cast(Box::new(Cast {
28773 this: arg,
28774 to: DataType::Timestamp {
28775 precision: None,
28776 timezone: false,
28777 },
28778 trailing_comments: Vec::new(),
28779 double_colon_syntax: false,
28780 format: None,
28781 default: None,
28782 inferred_type: None,
28783 }))
28784 }
28785 _ => arg,
28786 }
28787 } else {
28788 arg
28789 }
28790 };
28791
28792 let start = maybe_cast_timestamp(start);
28793 let end = maybe_cast_timestamp(end);
28794
28795 // For PostgreSQL/Redshift target, keep as GENERATE_SERIES
28796 if matches!(target, DialectType::PostgreSQL | DialectType::Redshift) {
28797 let mut gs_args = vec![start, end];
28798 if let Some(step) = step {
28799 gs_args.push(step);
28800 }
28801 return Ok(Expression::Function(Box::new(Function::new(
28802 "GENERATE_SERIES".to_string(),
28803 gs_args,
28804 ))));
28805 }
28806
28807 // For DuckDB target: wrap in UNNEST(GENERATE_SERIES(...))
28808 if matches!(target, DialectType::DuckDB) {
28809 let mut gs_args = vec![start, end];
28810 if let Some(step) = step {
28811 gs_args.push(step);
28812 }
28813 let gs = Expression::Function(Box::new(Function::new(
28814 "GENERATE_SERIES".to_string(),
28815 gs_args,
28816 )));
28817 return Ok(Expression::Function(Box::new(Function::new(
28818 "UNNEST".to_string(),
28819 vec![gs],
28820 ))));
28821 }
28822
28823 let mut seq_args = vec![start, end];
28824 if let Some(step) = step {
28825 seq_args.push(step);
28826 }
28827
28828 let seq = Expression::Function(Box::new(Function::new(
28829 "SEQUENCE".to_string(),
28830 seq_args,
28831 )));
28832
28833 match target {
28834 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
28835 // Wrap in UNNEST
28836 Ok(Expression::Function(Box::new(Function::new(
28837 "UNNEST".to_string(),
28838 vec![seq],
28839 ))))
28840 }
28841 DialectType::Spark
28842 | DialectType::Databricks
28843 | DialectType::Hive => {
28844 // Wrap in EXPLODE
28845 Ok(Expression::Function(Box::new(Function::new(
28846 "EXPLODE".to_string(),
28847 vec![seq],
28848 ))))
28849 }
28850 _ => {
28851 // Just SEQUENCE for others
28852 Ok(seq)
28853 }
28854 }
28855 } else {
28856 Ok(Expression::Function(f))
28857 }
28858 } else {
28859 Ok(e)
28860 }
28861 }
28862
28863 Action::ConcatCoalesceWrap => {
28864 // CONCAT(a, b) function -> CONCAT(COALESCE(CAST(a AS VARCHAR), ''), ...) for Presto
28865 // CONCAT(a, b) function -> CONCAT(COALESCE(a, ''), ...) for ClickHouse
28866 if let Expression::Function(f) = e {
28867 if f.name.eq_ignore_ascii_case("CONCAT") {
28868 let new_args: Vec<Expression> = f
28869 .args
28870 .into_iter()
28871 .map(|arg| {
28872 let cast_arg = if matches!(
28873 target,
28874 DialectType::Presto
28875 | DialectType::Trino
28876 | DialectType::Athena
28877 ) {
28878 Expression::Cast(Box::new(Cast {
28879 this: arg,
28880 to: DataType::VarChar {
28881 length: None,
28882 parenthesized_length: false,
28883 },
28884 trailing_comments: Vec::new(),
28885 double_colon_syntax: false,
28886 format: None,
28887 default: None,
28888 inferred_type: None,
28889 }))
28890 } else {
28891 arg
28892 };
28893 Expression::Function(Box::new(Function::new(
28894 "COALESCE".to_string(),
28895 vec![cast_arg, Expression::string("")],
28896 )))
28897 })
28898 .collect();
28899 Ok(Expression::Function(Box::new(Function::new(
28900 "CONCAT".to_string(),
28901 new_args,
28902 ))))
28903 } else {
28904 Ok(Expression::Function(f))
28905 }
28906 } else {
28907 Ok(e)
28908 }
28909 }
28910
28911 Action::PipeConcatToConcat => {
28912 // a || b (Concat operator) -> CONCAT(CAST(a AS VARCHAR), CAST(b AS VARCHAR)) for Presto/Trino
28913 if let Expression::Concat(op) = e {
28914 let cast_left = Expression::Cast(Box::new(Cast {
28915 this: op.left,
28916 to: DataType::VarChar {
28917 length: None,
28918 parenthesized_length: false,
28919 },
28920 trailing_comments: Vec::new(),
28921 double_colon_syntax: false,
28922 format: None,
28923 default: None,
28924 inferred_type: None,
28925 }));
28926 let cast_right = Expression::Cast(Box::new(Cast {
28927 this: op.right,
28928 to: DataType::VarChar {
28929 length: None,
28930 parenthesized_length: false,
28931 },
28932 trailing_comments: Vec::new(),
28933 double_colon_syntax: false,
28934 format: None,
28935 default: None,
28936 inferred_type: None,
28937 }));
28938 Ok(Expression::Function(Box::new(Function::new(
28939 "CONCAT".to_string(),
28940 vec![cast_left, cast_right],
28941 ))))
28942 } else {
28943 Ok(e)
28944 }
28945 }
28946
28947 Action::DivFuncConvert => {
28948 // DIV(a, b) -> target-specific integer division
28949 if let Expression::Function(f) = e {
28950 if f.name.eq_ignore_ascii_case("DIV") && f.args.len() == 2 {
28951 let a = f.args[0].clone();
28952 let b = f.args[1].clone();
28953 match target {
28954 DialectType::DuckDB => {
28955 // DIV(a, b) -> CAST(a // b AS DECIMAL)
28956 let int_div = Expression::IntDiv(Box::new(
28957 crate::expressions::BinaryFunc {
28958 this: a,
28959 expression: b,
28960 original_name: None,
28961 inferred_type: None,
28962 },
28963 ));
28964 Ok(Expression::Cast(Box::new(Cast {
28965 this: int_div,
28966 to: DataType::Decimal {
28967 precision: None,
28968 scale: None,
28969 },
28970 trailing_comments: Vec::new(),
28971 double_colon_syntax: false,
28972 format: None,
28973 default: None,
28974 inferred_type: None,
28975 })))
28976 }
28977 DialectType::BigQuery => {
28978 // DIV(a, b) -> CAST(DIV(a, b) AS NUMERIC)
28979 let div_func = Expression::Function(Box::new(Function::new(
28980 "DIV".to_string(),
28981 vec![a, b],
28982 )));
28983 Ok(Expression::Cast(Box::new(Cast {
28984 this: div_func,
28985 to: DataType::Custom {
28986 name: "NUMERIC".to_string(),
28987 },
28988 trailing_comments: Vec::new(),
28989 double_colon_syntax: false,
28990 format: None,
28991 default: None,
28992 inferred_type: None,
28993 })))
28994 }
28995 DialectType::SQLite => {
28996 // DIV(a, b) -> CAST(CAST(CAST(a AS REAL) / b AS INTEGER) AS REAL)
28997 let cast_a = Expression::Cast(Box::new(Cast {
28998 this: a,
28999 to: DataType::Custom {
29000 name: "REAL".to_string(),
29001 },
29002 trailing_comments: Vec::new(),
29003 double_colon_syntax: false,
29004 format: None,
29005 default: None,
29006 inferred_type: None,
29007 }));
29008 let div = Expression::Div(Box::new(BinaryOp::new(cast_a, b)));
29009 let cast_int = Expression::Cast(Box::new(Cast {
29010 this: div,
29011 to: DataType::Int {
29012 length: None,
29013 integer_spelling: true,
29014 },
29015 trailing_comments: Vec::new(),
29016 double_colon_syntax: false,
29017 format: None,
29018 default: None,
29019 inferred_type: None,
29020 }));
29021 Ok(Expression::Cast(Box::new(Cast {
29022 this: cast_int,
29023 to: DataType::Custom {
29024 name: "REAL".to_string(),
29025 },
29026 trailing_comments: Vec::new(),
29027 double_colon_syntax: false,
29028 format: None,
29029 default: None,
29030 inferred_type: None,
29031 })))
29032 }
29033 _ => Ok(Expression::Function(f)),
29034 }
29035 } else {
29036 Ok(Expression::Function(f))
29037 }
29038 } else {
29039 Ok(e)
29040 }
29041 }
29042
29043 Action::JsonObjectAggConvert => {
29044 // JSON_OBJECT_AGG/JSONB_OBJECT_AGG -> JSON_GROUP_OBJECT for DuckDB
29045 match e {
29046 Expression::Function(f) => Ok(Expression::Function(Box::new(
29047 Function::new("JSON_GROUP_OBJECT".to_string(), f.args),
29048 ))),
29049 Expression::AggregateFunction(af) => {
29050 // AggregateFunction stores all args in the `args` vec
29051 Ok(Expression::Function(Box::new(Function::new(
29052 "JSON_GROUP_OBJECT".to_string(),
29053 af.args,
29054 ))))
29055 }
29056 other => Ok(other),
29057 }
29058 }
29059
29060 Action::JsonbExistsConvert => {
29061 // JSONB_EXISTS('json', 'key') -> JSON_EXISTS('json', '$.key') for DuckDB
29062 if let Expression::Function(f) = e {
29063 if f.args.len() == 2 {
29064 let json_expr = f.args[0].clone();
29065 let key = match &f.args[1] {
29066 Expression::Literal(lit)
29067 if matches!(
29068 lit.as_ref(),
29069 crate::expressions::Literal::String(_)
29070 ) =>
29071 {
29072 let crate::expressions::Literal::String(s) = lit.as_ref()
29073 else {
29074 unreachable!()
29075 };
29076 format!("$.{}", s)
29077 }
29078 _ => return Ok(Expression::Function(f)),
29079 };
29080 Ok(Expression::Function(Box::new(Function::new(
29081 "JSON_EXISTS".to_string(),
29082 vec![json_expr, Expression::string(&key)],
29083 ))))
29084 } else {
29085 Ok(Expression::Function(f))
29086 }
29087 } else {
29088 Ok(e)
29089 }
29090 }
29091
29092 Action::DateBinConvert => {
29093 // DATE_BIN('interval', ts, origin) -> TIME_BUCKET('interval', ts, origin) for DuckDB
29094 if let Expression::Function(f) = e {
29095 Ok(Expression::Function(Box::new(Function::new(
29096 "TIME_BUCKET".to_string(),
29097 f.args,
29098 ))))
29099 } else {
29100 Ok(e)
29101 }
29102 }
29103
29104 Action::MysqlCastCharToText => {
29105 // MySQL CAST(x AS CHAR) was originally TEXT -> convert to target text type
29106 if let Expression::Cast(mut c) = e {
29107 c.to = DataType::Text;
29108 Ok(Expression::Cast(c))
29109 } else {
29110 Ok(e)
29111 }
29112 }
29113
29114 Action::SparkCastVarcharToString => {
29115 // Spark parses VARCHAR(n)/CHAR(n) as TEXT -> normalize to STRING
29116 match e {
29117 Expression::Cast(mut c) => {
29118 c.to = Self::normalize_varchar_to_string(c.to);
29119 Ok(Expression::Cast(c))
29120 }
29121 Expression::TryCast(mut c) => {
29122 c.to = Self::normalize_varchar_to_string(c.to);
29123 Ok(Expression::TryCast(c))
29124 }
29125 _ => Ok(e),
29126 }
29127 }
29128
29129 Action::MinMaxToLeastGreatest => {
29130 // Multi-arg MIN(a,b,c) -> LEAST(a,b,c), MAX(a,b,c) -> GREATEST(a,b,c)
29131 if let Expression::Function(f) = e {
29132 let new_name = if f.name.eq_ignore_ascii_case("MIN") {
29133 "LEAST"
29134 } else if f.name.eq_ignore_ascii_case("MAX") {
29135 "GREATEST"
29136 } else {
29137 return Ok(Expression::Function(f));
29138 };
29139 Ok(Expression::Function(Box::new(Function::new(
29140 new_name.to_string(),
29141 f.args,
29142 ))))
29143 } else {
29144 Ok(e)
29145 }
29146 }
29147
29148 Action::ClickHouseUniqToApproxCountDistinct => {
29149 // ClickHouse uniq(x) -> APPROX_COUNT_DISTINCT(x) for non-ClickHouse targets
29150 if let Expression::Function(f) = e {
29151 Ok(Expression::Function(Box::new(Function::new(
29152 "APPROX_COUNT_DISTINCT".to_string(),
29153 f.args,
29154 ))))
29155 } else {
29156 Ok(e)
29157 }
29158 }
29159
29160 Action::ClickHouseAnyToAnyValue => {
29161 // ClickHouse any(x) -> ANY_VALUE(x) for non-ClickHouse targets
29162 if let Expression::Function(f) = e {
29163 Ok(Expression::Function(Box::new(Function::new(
29164 "ANY_VALUE".to_string(),
29165 f.args,
29166 ))))
29167 } else {
29168 Ok(e)
29169 }
29170 }
29171
29172 Action::OracleVarchar2ToVarchar => {
29173 // Oracle VARCHAR2(N CHAR/BYTE) / NVARCHAR2(N) -> VarChar(N) for non-Oracle targets
29174 if let Expression::DataType(DataType::Custom { ref name }) = e {
29175 // Extract length from VARCHAR2(N ...) or NVARCHAR2(N ...)
29176 let starts_varchar2 =
29177 name.len() >= 9 && name[..9].eq_ignore_ascii_case("VARCHAR2(");
29178 let starts_nvarchar2 =
29179 name.len() >= 10 && name[..10].eq_ignore_ascii_case("NVARCHAR2(");
29180 let inner = if starts_varchar2 || starts_nvarchar2 {
29181 let start = if starts_nvarchar2 { 10 } else { 9 }; // skip "NVARCHAR2(" or "VARCHAR2("
29182 let end = name.len() - 1; // skip trailing ")"
29183 Some(&name[start..end])
29184 } else {
29185 Option::None
29186 };
29187 if let Some(inner_str) = inner {
29188 // Parse the number part, ignoring BYTE/CHAR qualifier
29189 let num_str = inner_str.split_whitespace().next().unwrap_or("");
29190 if let Ok(n) = num_str.parse::<u32>() {
29191 Ok(Expression::DataType(DataType::VarChar {
29192 length: Some(n),
29193 parenthesized_length: false,
29194 }))
29195 } else {
29196 Ok(e)
29197 }
29198 } else {
29199 // Plain VARCHAR2 / NVARCHAR2 without parens
29200 Ok(Expression::DataType(DataType::VarChar {
29201 length: Option::None,
29202 parenthesized_length: false,
29203 }))
29204 }
29205 } else {
29206 Ok(e)
29207 }
29208 }
29209
29210 Action::Nvl2Expand => {
29211 // NVL2(a, b[, c]) -> CASE WHEN NOT a IS NULL THEN b [ELSE c] END
29212 // But keep as NVL2 for dialects that support it natively
29213 let nvl2_native = matches!(
29214 target,
29215 DialectType::Oracle
29216 | DialectType::Snowflake
29217 | DialectType::Redshift
29218 | DialectType::Teradata
29219 | DialectType::Spark
29220 | DialectType::Databricks
29221 );
29222 let (a, b, c) = if let Expression::Nvl2(nvl2) = e {
29223 if nvl2_native {
29224 return Ok(Expression::Nvl2(nvl2));
29225 }
29226 (nvl2.this, nvl2.true_value, Some(nvl2.false_value))
29227 } else if let Expression::Function(f) = e {
29228 if nvl2_native {
29229 return Ok(Expression::Function(Box::new(Function::new(
29230 "NVL2".to_string(),
29231 f.args,
29232 ))));
29233 }
29234 if f.args.len() < 2 {
29235 return Ok(Expression::Function(f));
29236 }
29237 let mut args = f.args;
29238 let a = args.remove(0);
29239 let b = args.remove(0);
29240 let c = if !args.is_empty() {
29241 Some(args.remove(0))
29242 } else {
29243 Option::None
29244 };
29245 (a, b, c)
29246 } else {
29247 return Ok(e);
29248 };
29249 // Build: NOT (a IS NULL)
29250 let is_null = Expression::IsNull(Box::new(IsNull {
29251 this: a,
29252 not: false,
29253 postfix_form: false,
29254 }));
29255 let not_null = Expression::Not(Box::new(crate::expressions::UnaryOp {
29256 this: is_null,
29257 inferred_type: None,
29258 }));
29259 Ok(Expression::Case(Box::new(Case {
29260 operand: Option::None,
29261 whens: vec![(not_null, b)],
29262 else_: c,
29263 comments: Vec::new(),
29264 inferred_type: None,
29265 })))
29266 }
29267
29268 Action::IfnullToCoalesce => {
29269 // IFNULL(a, b) -> COALESCE(a, b): clear original_name to output COALESCE
29270 if let Expression::Coalesce(mut cf) = e {
29271 cf.original_name = Option::None;
29272 Ok(Expression::Coalesce(cf))
29273 } else if let Expression::Function(f) = e {
29274 Ok(Expression::Function(Box::new(Function::new(
29275 "COALESCE".to_string(),
29276 f.args,
29277 ))))
29278 } else {
29279 Ok(e)
29280 }
29281 }
29282
29283 Action::IsAsciiConvert => {
29284 // IS_ASCII(x) -> dialect-specific ASCII check
29285 if let Expression::Function(f) = e {
29286 let arg = f.args.into_iter().next().unwrap();
29287 match target {
29288 DialectType::MySQL | DialectType::SingleStore | DialectType::TiDB => {
29289 // REGEXP_LIKE(x, '^[[:ascii:]]*$')
29290 Ok(Expression::Function(Box::new(Function::new(
29291 "REGEXP_LIKE".to_string(),
29292 vec![
29293 arg,
29294 Expression::Literal(Box::new(Literal::String(
29295 "^[[:ascii:]]*$".to_string(),
29296 ))),
29297 ],
29298 ))))
29299 }
29300 DialectType::PostgreSQL
29301 | DialectType::Redshift
29302 | DialectType::Materialize
29303 | DialectType::RisingWave => {
29304 // (x ~ '^[[:ascii:]]*$')
29305 Ok(Expression::Paren(Box::new(Paren {
29306 this: Expression::RegexpLike(Box::new(
29307 crate::expressions::RegexpFunc {
29308 this: arg,
29309 pattern: Expression::Literal(Box::new(
29310 Literal::String("^[[:ascii:]]*$".to_string()),
29311 )),
29312 flags: Option::None,
29313 },
29314 )),
29315 trailing_comments: Vec::new(),
29316 })))
29317 }
29318 DialectType::SQLite => {
29319 // (NOT x GLOB CAST(x'2a5b5e012d7f5d2a' AS TEXT))
29320 let hex_lit = Expression::Literal(Box::new(Literal::HexString(
29321 "2a5b5e012d7f5d2a".to_string(),
29322 )));
29323 let cast_expr = Expression::Cast(Box::new(Cast {
29324 this: hex_lit,
29325 to: DataType::Text,
29326 trailing_comments: Vec::new(),
29327 double_colon_syntax: false,
29328 format: Option::None,
29329 default: Option::None,
29330 inferred_type: None,
29331 }));
29332 let glob = Expression::Glob(Box::new(BinaryOp {
29333 left: arg,
29334 right: cast_expr,
29335 left_comments: Vec::new(),
29336 operator_comments: Vec::new(),
29337 trailing_comments: Vec::new(),
29338 inferred_type: None,
29339 }));
29340 Ok(Expression::Paren(Box::new(Paren {
29341 this: Expression::Not(Box::new(crate::expressions::UnaryOp {
29342 this: glob,
29343 inferred_type: None,
29344 })),
29345 trailing_comments: Vec::new(),
29346 })))
29347 }
29348 DialectType::TSQL | DialectType::Fabric => {
29349 // (PATINDEX(CONVERT(VARCHAR(MAX), 0x255b5e002d7f5d25) COLLATE Latin1_General_BIN, x) = 0)
29350 let hex_lit = Expression::Literal(Box::new(Literal::HexNumber(
29351 "255b5e002d7f5d25".to_string(),
29352 )));
29353 let convert_expr = Expression::Convert(Box::new(
29354 crate::expressions::ConvertFunc {
29355 this: hex_lit,
29356 to: DataType::Text, // Text generates as VARCHAR(MAX) for TSQL
29357 style: None,
29358 },
29359 ));
29360 let collated = Expression::Collation(Box::new(
29361 crate::expressions::CollationExpr {
29362 this: convert_expr,
29363 collation: "Latin1_General_BIN".to_string(),
29364 quoted: false,
29365 double_quoted: false,
29366 },
29367 ));
29368 let patindex = Expression::Function(Box::new(Function::new(
29369 "PATINDEX".to_string(),
29370 vec![collated, arg],
29371 )));
29372 let zero =
29373 Expression::Literal(Box::new(Literal::Number("0".to_string())));
29374 let eq_zero = Expression::Eq(Box::new(BinaryOp {
29375 left: patindex,
29376 right: zero,
29377 left_comments: Vec::new(),
29378 operator_comments: Vec::new(),
29379 trailing_comments: Vec::new(),
29380 inferred_type: None,
29381 }));
29382 Ok(Expression::Paren(Box::new(Paren {
29383 this: eq_zero,
29384 trailing_comments: Vec::new(),
29385 })))
29386 }
29387 DialectType::Oracle => {
29388 // NVL(REGEXP_LIKE(x, '^[' || CHR(1) || '-' || CHR(127) || ']*$'), TRUE)
29389 // Build the pattern: '^[' || CHR(1) || '-' || CHR(127) || ']*$'
29390 let s1 = Expression::Literal(Box::new(Literal::String(
29391 "^[".to_string(),
29392 )));
29393 let chr1 = Expression::Function(Box::new(Function::new(
29394 "CHR".to_string(),
29395 vec![Expression::Literal(Box::new(Literal::Number(
29396 "1".to_string(),
29397 )))],
29398 )));
29399 let dash =
29400 Expression::Literal(Box::new(Literal::String("-".to_string())));
29401 let chr127 = Expression::Function(Box::new(Function::new(
29402 "CHR".to_string(),
29403 vec![Expression::Literal(Box::new(Literal::Number(
29404 "127".to_string(),
29405 )))],
29406 )));
29407 let s2 = Expression::Literal(Box::new(Literal::String(
29408 "]*$".to_string(),
29409 )));
29410 // Build: '^[' || CHR(1) || '-' || CHR(127) || ']*$'
29411 let concat1 =
29412 Expression::DPipe(Box::new(crate::expressions::DPipe {
29413 this: Box::new(s1),
29414 expression: Box::new(chr1),
29415 safe: None,
29416 }));
29417 let concat2 =
29418 Expression::DPipe(Box::new(crate::expressions::DPipe {
29419 this: Box::new(concat1),
29420 expression: Box::new(dash),
29421 safe: None,
29422 }));
29423 let concat3 =
29424 Expression::DPipe(Box::new(crate::expressions::DPipe {
29425 this: Box::new(concat2),
29426 expression: Box::new(chr127),
29427 safe: None,
29428 }));
29429 let concat4 =
29430 Expression::DPipe(Box::new(crate::expressions::DPipe {
29431 this: Box::new(concat3),
29432 expression: Box::new(s2),
29433 safe: None,
29434 }));
29435 let regexp_like = Expression::Function(Box::new(Function::new(
29436 "REGEXP_LIKE".to_string(),
29437 vec![arg, concat4],
29438 )));
29439 // Use Column("TRUE") to output literal TRUE keyword (not boolean 1/0)
29440 let true_expr =
29441 Expression::Column(Box::new(crate::expressions::Column {
29442 name: Identifier {
29443 name: "TRUE".to_string(),
29444 quoted: false,
29445 trailing_comments: Vec::new(),
29446 span: None,
29447 },
29448 table: None,
29449 join_mark: false,
29450 trailing_comments: Vec::new(),
29451 span: None,
29452 inferred_type: None,
29453 }));
29454 let nvl = Expression::Function(Box::new(Function::new(
29455 "NVL".to_string(),
29456 vec![regexp_like, true_expr],
29457 )));
29458 Ok(nvl)
29459 }
29460 _ => Ok(Expression::Function(Box::new(Function::new(
29461 "IS_ASCII".to_string(),
29462 vec![arg],
29463 )))),
29464 }
29465 } else {
29466 Ok(e)
29467 }
29468 }
29469
29470 Action::StrPositionConvert => {
29471 // STR_POSITION(haystack, needle[, position[, occurrence]]) -> dialect-specific
29472 if let Expression::Function(f) = e {
29473 if f.args.len() < 2 {
29474 return Ok(Expression::Function(f));
29475 }
29476 let mut args = f.args;
29477
29478 let haystack = args.remove(0);
29479 let needle = args.remove(0);
29480 let position = if !args.is_empty() {
29481 Some(args.remove(0))
29482 } else {
29483 Option::None
29484 };
29485 let occurrence = if !args.is_empty() {
29486 Some(args.remove(0))
29487 } else {
29488 Option::None
29489 };
29490
29491 // Helper to build: STRPOS/INSTR(SUBSTRING(haystack, pos), needle) expansion
29492 // Returns: CASE/IF WHEN func(SUBSTRING(haystack, pos), needle[, occ]) = 0 THEN 0 ELSE ... + pos - 1 END
29493 fn build_position_expansion(
29494 haystack: Expression,
29495 needle: Expression,
29496 pos: Expression,
29497 occurrence: Option<Expression>,
29498 inner_func: &str,
29499 wrapper: &str, // "CASE", "IF", "IIF"
29500 ) -> Expression {
29501 let substr = Expression::Function(Box::new(Function::new(
29502 "SUBSTRING".to_string(),
29503 vec![haystack, pos.clone()],
29504 )));
29505 let mut inner_args = vec![substr, needle];
29506 if let Some(occ) = occurrence {
29507 inner_args.push(occ);
29508 }
29509 let inner_call = Expression::Function(Box::new(Function::new(
29510 inner_func.to_string(),
29511 inner_args,
29512 )));
29513 let zero =
29514 Expression::Literal(Box::new(Literal::Number("0".to_string())));
29515 let one =
29516 Expression::Literal(Box::new(Literal::Number("1".to_string())));
29517 let eq_zero = Expression::Eq(Box::new(BinaryOp {
29518 left: inner_call.clone(),
29519 right: zero.clone(),
29520 left_comments: Vec::new(),
29521 operator_comments: Vec::new(),
29522 trailing_comments: Vec::new(),
29523 inferred_type: None,
29524 }));
29525 let add_pos = Expression::Add(Box::new(BinaryOp {
29526 left: inner_call,
29527 right: pos,
29528 left_comments: Vec::new(),
29529 operator_comments: Vec::new(),
29530 trailing_comments: Vec::new(),
29531 inferred_type: None,
29532 }));
29533 let sub_one = Expression::Sub(Box::new(BinaryOp {
29534 left: add_pos,
29535 right: one,
29536 left_comments: Vec::new(),
29537 operator_comments: Vec::new(),
29538 trailing_comments: Vec::new(),
29539 inferred_type: None,
29540 }));
29541
29542 match wrapper {
29543 "CASE" => Expression::Case(Box::new(Case {
29544 operand: Option::None,
29545 whens: vec![(eq_zero, zero)],
29546 else_: Some(sub_one),
29547 comments: Vec::new(),
29548 inferred_type: None,
29549 })),
29550 "IIF" => Expression::Function(Box::new(Function::new(
29551 "IIF".to_string(),
29552 vec![eq_zero, zero, sub_one],
29553 ))),
29554 _ => Expression::Function(Box::new(Function::new(
29555 "IF".to_string(),
29556 vec![eq_zero, zero, sub_one],
29557 ))),
29558 }
29559 }
29560
29561 match target {
29562 // STRPOS group: Athena, DuckDB, Presto, Trino, Drill
29563 DialectType::Athena
29564 | DialectType::DuckDB
29565 | DialectType::Presto
29566 | DialectType::Trino
29567 | DialectType::Drill => {
29568 if let Some(pos) = position {
29569 let wrapper = if matches!(target, DialectType::DuckDB) {
29570 "CASE"
29571 } else {
29572 "IF"
29573 };
29574 let result = build_position_expansion(
29575 haystack, needle, pos, occurrence, "STRPOS", wrapper,
29576 );
29577 if matches!(target, DialectType::Drill) {
29578 // Drill uses backtick-quoted `IF`
29579 if let Expression::Function(mut f) = result {
29580 f.name = "`IF`".to_string();
29581 Ok(Expression::Function(f))
29582 } else {
29583 Ok(result)
29584 }
29585 } else {
29586 Ok(result)
29587 }
29588 } else {
29589 Ok(Expression::Function(Box::new(Function::new(
29590 "STRPOS".to_string(),
29591 vec![haystack, needle],
29592 ))))
29593 }
29594 }
29595 // SQLite: IIF wrapper
29596 DialectType::SQLite => {
29597 if let Some(pos) = position {
29598 Ok(build_position_expansion(
29599 haystack, needle, pos, occurrence, "INSTR", "IIF",
29600 ))
29601 } else {
29602 Ok(Expression::Function(Box::new(Function::new(
29603 "INSTR".to_string(),
29604 vec![haystack, needle],
29605 ))))
29606 }
29607 }
29608 // INSTR group: Teradata, BigQuery, Oracle
29609 DialectType::Teradata | DialectType::BigQuery | DialectType::Oracle => {
29610 let mut a = vec![haystack, needle];
29611 if let Some(pos) = position {
29612 a.push(pos);
29613 }
29614 if let Some(occ) = occurrence {
29615 a.push(occ);
29616 }
29617 Ok(Expression::Function(Box::new(Function::new(
29618 "INSTR".to_string(),
29619 a,
29620 ))))
29621 }
29622 // CHARINDEX group: Snowflake, TSQL
29623 DialectType::Snowflake | DialectType::TSQL | DialectType::Fabric => {
29624 let mut a = vec![needle, haystack];
29625 if let Some(pos) = position {
29626 a.push(pos);
29627 }
29628 Ok(Expression::Function(Box::new(Function::new(
29629 "CHARINDEX".to_string(),
29630 a,
29631 ))))
29632 }
29633 // POSITION(needle IN haystack): PostgreSQL, Materialize, RisingWave, Redshift
29634 DialectType::PostgreSQL
29635 | DialectType::Materialize
29636 | DialectType::RisingWave
29637 | DialectType::Redshift => {
29638 if let Some(pos) = position {
29639 // Build: CASE WHEN POSITION(needle IN SUBSTRING(haystack FROM pos)) = 0 THEN 0
29640 // ELSE POSITION(...) + pos - 1 END
29641 let substr = Expression::Substring(Box::new(
29642 crate::expressions::SubstringFunc {
29643 this: haystack,
29644 start: pos.clone(),
29645 length: Option::None,
29646 from_for_syntax: true,
29647 },
29648 ));
29649 let pos_in = Expression::StrPosition(Box::new(
29650 crate::expressions::StrPosition {
29651 this: Box::new(substr),
29652 substr: Some(Box::new(needle)),
29653 position: Option::None,
29654 occurrence: Option::None,
29655 },
29656 ));
29657 let zero = Expression::Literal(Box::new(Literal::Number(
29658 "0".to_string(),
29659 )));
29660 let one = Expression::Literal(Box::new(Literal::Number(
29661 "1".to_string(),
29662 )));
29663 let eq_zero = Expression::Eq(Box::new(BinaryOp {
29664 left: pos_in.clone(),
29665 right: zero.clone(),
29666 left_comments: Vec::new(),
29667 operator_comments: Vec::new(),
29668 trailing_comments: Vec::new(),
29669 inferred_type: None,
29670 }));
29671 let add_pos = Expression::Add(Box::new(BinaryOp {
29672 left: pos_in,
29673 right: pos,
29674 left_comments: Vec::new(),
29675 operator_comments: Vec::new(),
29676 trailing_comments: Vec::new(),
29677 inferred_type: None,
29678 }));
29679 let sub_one = Expression::Sub(Box::new(BinaryOp {
29680 left: add_pos,
29681 right: one,
29682 left_comments: Vec::new(),
29683 operator_comments: Vec::new(),
29684 trailing_comments: Vec::new(),
29685 inferred_type: None,
29686 }));
29687 Ok(Expression::Case(Box::new(Case {
29688 operand: Option::None,
29689 whens: vec![(eq_zero, zero)],
29690 else_: Some(sub_one),
29691 comments: Vec::new(),
29692 inferred_type: None,
29693 })))
29694 } else {
29695 Ok(Expression::StrPosition(Box::new(
29696 crate::expressions::StrPosition {
29697 this: Box::new(haystack),
29698 substr: Some(Box::new(needle)),
29699 position: Option::None,
29700 occurrence: Option::None,
29701 },
29702 )))
29703 }
29704 }
29705 // LOCATE group: MySQL, Hive, Spark, Databricks, Doris
29706 DialectType::MySQL
29707 | DialectType::SingleStore
29708 | DialectType::TiDB
29709 | DialectType::Hive
29710 | DialectType::Spark
29711 | DialectType::Databricks
29712 | DialectType::Doris
29713 | DialectType::StarRocks => {
29714 let mut a = vec![needle, haystack];
29715 if let Some(pos) = position {
29716 a.push(pos);
29717 }
29718 Ok(Expression::Function(Box::new(Function::new(
29719 "LOCATE".to_string(),
29720 a,
29721 ))))
29722 }
29723 // ClickHouse: POSITION(haystack, needle[, position])
29724 DialectType::ClickHouse => {
29725 let mut a = vec![haystack, needle];
29726 if let Some(pos) = position {
29727 a.push(pos);
29728 }
29729 Ok(Expression::Function(Box::new(Function::new(
29730 "POSITION".to_string(),
29731 a,
29732 ))))
29733 }
29734 _ => {
29735 let mut a = vec![haystack, needle];
29736 if let Some(pos) = position {
29737 a.push(pos);
29738 }
29739 if let Some(occ) = occurrence {
29740 a.push(occ);
29741 }
29742 Ok(Expression::Function(Box::new(Function::new(
29743 "STR_POSITION".to_string(),
29744 a,
29745 ))))
29746 }
29747 }
29748 } else {
29749 Ok(e)
29750 }
29751 }
29752
29753 Action::ArraySumConvert => {
29754 // ARRAY_SUM(arr) -> dialect-specific
29755 if let Expression::Function(f) = e {
29756 let args = f.args;
29757 match target {
29758 DialectType::DuckDB => Ok(Expression::Function(Box::new(
29759 Function::new("LIST_SUM".to_string(), args),
29760 ))),
29761 DialectType::Spark | DialectType::Databricks => {
29762 // AGGREGATE(arr, 0, (acc, x) -> acc + x, acc -> acc)
29763 let arr = args.into_iter().next().unwrap();
29764 let zero =
29765 Expression::Literal(Box::new(Literal::Number("0".to_string())));
29766 let acc_id = Identifier::new("acc");
29767 let x_id = Identifier::new("x");
29768 let acc = Expression::Identifier(acc_id.clone());
29769 let x = Expression::Identifier(x_id.clone());
29770 let add = Expression::Add(Box::new(BinaryOp {
29771 left: acc.clone(),
29772 right: x,
29773 left_comments: Vec::new(),
29774 operator_comments: Vec::new(),
29775 trailing_comments: Vec::new(),
29776 inferred_type: None,
29777 }));
29778 let lambda1 =
29779 Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
29780 parameters: vec![acc_id.clone(), x_id],
29781 body: add,
29782 colon: false,
29783 parameter_types: Vec::new(),
29784 }));
29785 let lambda2 =
29786 Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
29787 parameters: vec![acc_id],
29788 body: acc,
29789 colon: false,
29790 parameter_types: Vec::new(),
29791 }));
29792 Ok(Expression::Function(Box::new(Function::new(
29793 "AGGREGATE".to_string(),
29794 vec![arr, zero, lambda1, lambda2],
29795 ))))
29796 }
29797 DialectType::Presto | DialectType::Athena => {
29798 // Presto/Athena keep ARRAY_SUM natively
29799 Ok(Expression::Function(Box::new(Function::new(
29800 "ARRAY_SUM".to_string(),
29801 args,
29802 ))))
29803 }
29804 DialectType::Trino => {
29805 // REDUCE(arr, 0, (acc, x) -> acc + x, acc -> acc)
29806 if args.len() == 1 {
29807 let arr = args.into_iter().next().unwrap();
29808 let zero = Expression::Literal(Box::new(Literal::Number(
29809 "0".to_string(),
29810 )));
29811 let acc_id = Identifier::new("acc");
29812 let x_id = Identifier::new("x");
29813 let acc = Expression::Identifier(acc_id.clone());
29814 let x = Expression::Identifier(x_id.clone());
29815 let add = Expression::Add(Box::new(BinaryOp {
29816 left: acc.clone(),
29817 right: x,
29818 left_comments: Vec::new(),
29819 operator_comments: Vec::new(),
29820 trailing_comments: Vec::new(),
29821 inferred_type: None,
29822 }));
29823 let lambda1 = Expression::Lambda(Box::new(
29824 crate::expressions::LambdaExpr {
29825 parameters: vec![acc_id.clone(), x_id],
29826 body: add,
29827 colon: false,
29828 parameter_types: Vec::new(),
29829 },
29830 ));
29831 let lambda2 = Expression::Lambda(Box::new(
29832 crate::expressions::LambdaExpr {
29833 parameters: vec![acc_id],
29834 body: acc,
29835 colon: false,
29836 parameter_types: Vec::new(),
29837 },
29838 ));
29839 Ok(Expression::Function(Box::new(Function::new(
29840 "REDUCE".to_string(),
29841 vec![arr, zero, lambda1, lambda2],
29842 ))))
29843 } else {
29844 Ok(Expression::Function(Box::new(Function::new(
29845 "ARRAY_SUM".to_string(),
29846 args,
29847 ))))
29848 }
29849 }
29850 DialectType::ClickHouse => {
29851 // arraySum(lambda, arr) or arraySum(arr)
29852 Ok(Expression::Function(Box::new(Function::new(
29853 "arraySum".to_string(),
29854 args,
29855 ))))
29856 }
29857 _ => Ok(Expression::Function(Box::new(Function::new(
29858 "ARRAY_SUM".to_string(),
29859 args,
29860 )))),
29861 }
29862 } else {
29863 Ok(e)
29864 }
29865 }
29866
29867 Action::ArraySizeConvert => {
29868 if let Expression::Function(f) = e {
29869 Ok(Expression::Function(Box::new(Function::new(
29870 "REPEATED_COUNT".to_string(),
29871 f.args,
29872 ))))
29873 } else {
29874 Ok(e)
29875 }
29876 }
29877
29878 Action::ArrayAnyConvert => {
29879 if let Expression::Function(f) = e {
29880 let mut args = f.args;
29881 if args.len() == 2 {
29882 let arr = args.remove(0);
29883 let lambda = args.remove(0);
29884
29885 // Extract lambda parameter name and body
29886 let (param_name, pred_body) =
29887 if let Expression::Lambda(ref lam) = lambda {
29888 let name = if let Some(p) = lam.parameters.first() {
29889 p.name.clone()
29890 } else {
29891 "x".to_string()
29892 };
29893 (name, lam.body.clone())
29894 } else {
29895 ("x".to_string(), lambda.clone())
29896 };
29897
29898 // Helper: build a function call Expression
29899 let make_func = |name: &str, args: Vec<Expression>| -> Expression {
29900 Expression::Function(Box::new(Function::new(
29901 name.to_string(),
29902 args,
29903 )))
29904 };
29905
29906 // Helper: build (len_func(arr) = 0 OR len_func(filter_expr) <> 0) wrapped in Paren
29907 let build_filter_pattern = |len_func: &str,
29908 len_args_extra: Vec<Expression>,
29909 filter_expr: Expression|
29910 -> Expression {
29911 // len_func(arr, ...extra) = 0
29912 let mut len_arr_args = vec![arr.clone()];
29913 len_arr_args.extend(len_args_extra.clone());
29914 let len_arr = make_func(len_func, len_arr_args);
29915 let eq_zero = Expression::Eq(Box::new(BinaryOp::new(
29916 len_arr,
29917 Expression::number(0),
29918 )));
29919
29920 // len_func(filter_expr, ...extra) <> 0
29921 let mut len_filter_args = vec![filter_expr];
29922 len_filter_args.extend(len_args_extra);
29923 let len_filter = make_func(len_func, len_filter_args);
29924 let neq_zero = Expression::Neq(Box::new(BinaryOp::new(
29925 len_filter,
29926 Expression::number(0),
29927 )));
29928
29929 // (eq_zero OR neq_zero)
29930 let or_expr =
29931 Expression::Or(Box::new(BinaryOp::new(eq_zero, neq_zero)));
29932 Expression::Paren(Box::new(Paren {
29933 this: or_expr,
29934 trailing_comments: Vec::new(),
29935 }))
29936 };
29937
29938 match target {
29939 DialectType::Trino | DialectType::Presto | DialectType::Athena => {
29940 Ok(make_func("ANY_MATCH", vec![arr, lambda]))
29941 }
29942 DialectType::ClickHouse => {
29943 // (LENGTH(arr) = 0 OR LENGTH(arrayFilter(x -> pred, arr)) <> 0)
29944 // ClickHouse arrayFilter takes lambda first, then array
29945 let filter_expr =
29946 make_func("arrayFilter", vec![lambda, arr.clone()]);
29947 Ok(build_filter_pattern("LENGTH", vec![], filter_expr))
29948 }
29949 DialectType::Databricks | DialectType::Spark => {
29950 // (SIZE(arr) = 0 OR SIZE(FILTER(arr, x -> pred)) <> 0)
29951 let filter_expr =
29952 make_func("FILTER", vec![arr.clone(), lambda]);
29953 Ok(build_filter_pattern("SIZE", vec![], filter_expr))
29954 }
29955 DialectType::DuckDB => {
29956 // (ARRAY_LENGTH(arr) = 0 OR ARRAY_LENGTH(LIST_FILTER(arr, x -> pred)) <> 0)
29957 let filter_expr =
29958 make_func("LIST_FILTER", vec![arr.clone(), lambda]);
29959 Ok(build_filter_pattern("ARRAY_LENGTH", vec![], filter_expr))
29960 }
29961 DialectType::Teradata => {
29962 // (CARDINALITY(arr) = 0 OR CARDINALITY(FILTER(arr, x -> pred)) <> 0)
29963 let filter_expr =
29964 make_func("FILTER", vec![arr.clone(), lambda]);
29965 Ok(build_filter_pattern("CARDINALITY", vec![], filter_expr))
29966 }
29967 DialectType::BigQuery => {
29968 // (ARRAY_LENGTH(arr) = 0 OR ARRAY_LENGTH(ARRAY(SELECT x FROM UNNEST(arr) AS x WHERE pred)) <> 0)
29969 // Build: SELECT x FROM UNNEST(arr) AS x WHERE pred
29970 let param_col = Expression::column(¶m_name);
29971 let unnest_expr = Expression::Unnest(Box::new(
29972 crate::expressions::UnnestFunc {
29973 this: arr.clone(),
29974 expressions: vec![],
29975 with_ordinality: false,
29976 alias: Some(Identifier::new(¶m_name)),
29977 offset_alias: None,
29978 },
29979 ));
29980 let mut sel = crate::expressions::Select::default();
29981 sel.expressions = vec![param_col];
29982 sel.from = Some(crate::expressions::From {
29983 expressions: vec![unnest_expr],
29984 });
29985 sel.where_clause =
29986 Some(crate::expressions::Where { this: pred_body });
29987 let array_subquery =
29988 make_func("ARRAY", vec![Expression::Select(Box::new(sel))]);
29989 Ok(build_filter_pattern("ARRAY_LENGTH", vec![], array_subquery))
29990 }
29991 DialectType::PostgreSQL => {
29992 // (ARRAY_LENGTH(arr, 1) = 0 OR ARRAY_LENGTH(ARRAY(SELECT x FROM UNNEST(arr) AS _t0(x) WHERE pred), 1) <> 0)
29993 // Build: SELECT x FROM UNNEST(arr) AS _t0(x) WHERE pred
29994 let param_col = Expression::column(¶m_name);
29995 // For PostgreSQL, UNNEST uses AS _t0(x) syntax - use TableAlias
29996 let unnest_with_alias =
29997 Expression::Alias(Box::new(crate::expressions::Alias {
29998 this: Expression::Unnest(Box::new(
29999 crate::expressions::UnnestFunc {
30000 this: arr.clone(),
30001 expressions: vec![],
30002 with_ordinality: false,
30003 alias: None,
30004 offset_alias: None,
30005 },
30006 )),
30007 alias: Identifier::new("_t0"),
30008 column_aliases: vec![Identifier::new(¶m_name)],
30009 alias_explicit_as: false,
30010 alias_keyword: None,
30011 pre_alias_comments: Vec::new(),
30012 trailing_comments: Vec::new(),
30013 inferred_type: None,
30014 }));
30015 let mut sel = crate::expressions::Select::default();
30016 sel.expressions = vec![param_col];
30017 sel.from = Some(crate::expressions::From {
30018 expressions: vec![unnest_with_alias],
30019 });
30020 sel.where_clause =
30021 Some(crate::expressions::Where { this: pred_body });
30022 let array_subquery =
30023 make_func("ARRAY", vec![Expression::Select(Box::new(sel))]);
30024 Ok(build_filter_pattern(
30025 "ARRAY_LENGTH",
30026 vec![Expression::number(1)],
30027 array_subquery,
30028 ))
30029 }
30030 _ => Ok(Expression::Function(Box::new(Function::new(
30031 "ARRAY_ANY".to_string(),
30032 vec![arr, lambda],
30033 )))),
30034 }
30035 } else {
30036 Ok(Expression::Function(Box::new(Function::new(
30037 "ARRAY_ANY".to_string(),
30038 args,
30039 ))))
30040 }
30041 } else {
30042 Ok(e)
30043 }
30044 }
30045
30046 Action::DecodeSimplify => {
30047 // DECODE(x, search1, result1, ..., default) -> CASE WHEN ... THEN result1 ... [ELSE default] END
30048 // For literal search values: CASE WHEN x = search THEN result
30049 // For NULL search: CASE WHEN x IS NULL THEN result
30050 // For non-literal (column, expr): CASE WHEN x = search OR (x IS NULL AND search IS NULL) THEN result
30051 fn is_decode_literal(e: &Expression) -> bool {
30052 matches!(
30053 e,
30054 Expression::Literal(_) | Expression::Boolean(_) | Expression::Neg(_)
30055 )
30056 }
30057
30058 let build_decode_case =
30059 |this_expr: Expression,
30060 pairs: Vec<(Expression, Expression)>,
30061 default: Option<Expression>| {
30062 let whens: Vec<(Expression, Expression)> = pairs
30063 .into_iter()
30064 .map(|(search, result)| {
30065 if matches!(&search, Expression::Null(_)) {
30066 // NULL search -> IS NULL
30067 let condition = Expression::Is(Box::new(BinaryOp {
30068 left: this_expr.clone(),
30069 right: Expression::Null(crate::expressions::Null),
30070 left_comments: Vec::new(),
30071 operator_comments: Vec::new(),
30072 trailing_comments: Vec::new(),
30073 inferred_type: None,
30074 }));
30075 (condition, result)
30076 } else if is_decode_literal(&search)
30077 || is_decode_literal(&this_expr)
30078 {
30079 // At least one side is a literal -> simple equality (no NULL check needed)
30080 let eq = Expression::Eq(Box::new(BinaryOp {
30081 left: this_expr.clone(),
30082 right: search,
30083 left_comments: Vec::new(),
30084 operator_comments: Vec::new(),
30085 trailing_comments: Vec::new(),
30086 inferred_type: None,
30087 }));
30088 (eq, result)
30089 } else {
30090 // Non-literal -> null-safe comparison
30091 let needs_paren = matches!(
30092 &search,
30093 Expression::Eq(_)
30094 | Expression::Neq(_)
30095 | Expression::Gt(_)
30096 | Expression::Gte(_)
30097 | Expression::Lt(_)
30098 | Expression::Lte(_)
30099 );
30100 let search_ref = if needs_paren {
30101 Expression::Paren(Box::new(crate::expressions::Paren {
30102 this: search.clone(),
30103 trailing_comments: Vec::new(),
30104 }))
30105 } else {
30106 search.clone()
30107 };
30108 // Build: x = search OR (x IS NULL AND search IS NULL)
30109 let eq = Expression::Eq(Box::new(BinaryOp {
30110 left: this_expr.clone(),
30111 right: search_ref,
30112 left_comments: Vec::new(),
30113 operator_comments: Vec::new(),
30114 trailing_comments: Vec::new(),
30115 inferred_type: None,
30116 }));
30117 let search_in_null = if needs_paren {
30118 Expression::Paren(Box::new(crate::expressions::Paren {
30119 this: search.clone(),
30120 trailing_comments: Vec::new(),
30121 }))
30122 } else {
30123 search.clone()
30124 };
30125 let x_is_null = Expression::Is(Box::new(BinaryOp {
30126 left: this_expr.clone(),
30127 right: Expression::Null(crate::expressions::Null),
30128 left_comments: Vec::new(),
30129 operator_comments: Vec::new(),
30130 trailing_comments: Vec::new(),
30131 inferred_type: None,
30132 }));
30133 let search_is_null = Expression::Is(Box::new(BinaryOp {
30134 left: search_in_null,
30135 right: Expression::Null(crate::expressions::Null),
30136 left_comments: Vec::new(),
30137 operator_comments: Vec::new(),
30138 trailing_comments: Vec::new(),
30139 inferred_type: None,
30140 }));
30141 let both_null = Expression::And(Box::new(BinaryOp {
30142 left: x_is_null,
30143 right: search_is_null,
30144 left_comments: Vec::new(),
30145 operator_comments: Vec::new(),
30146 trailing_comments: Vec::new(),
30147 inferred_type: None,
30148 }));
30149 let condition = Expression::Or(Box::new(BinaryOp {
30150 left: eq,
30151 right: Expression::Paren(Box::new(
30152 crate::expressions::Paren {
30153 this: both_null,
30154 trailing_comments: Vec::new(),
30155 },
30156 )),
30157 left_comments: Vec::new(),
30158 operator_comments: Vec::new(),
30159 trailing_comments: Vec::new(),
30160 inferred_type: None,
30161 }));
30162 (condition, result)
30163 }
30164 })
30165 .collect();
30166 Expression::Case(Box::new(Case {
30167 operand: None,
30168 whens,
30169 else_: default,
30170 comments: Vec::new(),
30171 inferred_type: None,
30172 }))
30173 };
30174
30175 if let Expression::Decode(decode) = e {
30176 Ok(build_decode_case(
30177 decode.this,
30178 decode.search_results,
30179 decode.default,
30180 ))
30181 } else if let Expression::DecodeCase(dc) = e {
30182 // DecodeCase has flat expressions: [x, s1, r1, s2, r2, ..., default?]
30183 let mut exprs = dc.expressions;
30184 if exprs.len() < 3 {
30185 return Ok(Expression::DecodeCase(Box::new(
30186 crate::expressions::DecodeCase { expressions: exprs },
30187 )));
30188 }
30189 let this_expr = exprs.remove(0);
30190 let mut pairs = Vec::new();
30191 let mut default = None;
30192 let mut i = 0;
30193 while i + 1 < exprs.len() {
30194 pairs.push((exprs[i].clone(), exprs[i + 1].clone()));
30195 i += 2;
30196 }
30197 if i < exprs.len() {
30198 // Odd remaining element is the default
30199 default = Some(exprs[i].clone());
30200 }
30201 Ok(build_decode_case(this_expr, pairs, default))
30202 } else {
30203 Ok(e)
30204 }
30205 }
30206
30207 Action::CreateTableLikeToCtas => {
30208 // CREATE TABLE a LIKE b -> CREATE TABLE a AS SELECT * FROM b LIMIT 0
30209 if let Expression::CreateTable(ct) = e {
30210 let like_source = ct.constraints.iter().find_map(|c| {
30211 if let crate::expressions::TableConstraint::Like { source, .. } = c {
30212 Some(source.clone())
30213 } else {
30214 None
30215 }
30216 });
30217 if let Some(source_table) = like_source {
30218 let mut new_ct = *ct;
30219 new_ct.constraints.clear();
30220 // Build: SELECT * FROM b LIMIT 0
30221 let select = Expression::Select(Box::new(crate::expressions::Select {
30222 expressions: vec![Expression::Star(crate::expressions::Star {
30223 table: None,
30224 except: None,
30225 replace: None,
30226 rename: None,
30227 trailing_comments: Vec::new(),
30228 span: None,
30229 })],
30230 from: Some(crate::expressions::From {
30231 expressions: vec![Expression::Table(Box::new(source_table))],
30232 }),
30233 limit: Some(crate::expressions::Limit {
30234 this: Expression::Literal(Box::new(Literal::Number(
30235 "0".to_string(),
30236 ))),
30237 percent: false,
30238 comments: Vec::new(),
30239 }),
30240 ..Default::default()
30241 }));
30242 new_ct.as_select = Some(select);
30243 Ok(Expression::CreateTable(Box::new(new_ct)))
30244 } else {
30245 Ok(Expression::CreateTable(ct))
30246 }
30247 } else {
30248 Ok(e)
30249 }
30250 }
30251
30252 Action::CreateTableLikeToSelectInto => {
30253 // CREATE TABLE a LIKE b -> SELECT TOP 0 * INTO a FROM b AS temp
30254 if let Expression::CreateTable(ct) = e {
30255 let like_source = ct.constraints.iter().find_map(|c| {
30256 if let crate::expressions::TableConstraint::Like { source, .. } = c {
30257 Some(source.clone())
30258 } else {
30259 None
30260 }
30261 });
30262 if let Some(source_table) = like_source {
30263 let mut aliased_source = source_table;
30264 aliased_source.alias = Some(Identifier::new("temp"));
30265 // Build: SELECT TOP 0 * INTO a FROM b AS temp
30266 let select = Expression::Select(Box::new(crate::expressions::Select {
30267 expressions: vec![Expression::Star(crate::expressions::Star {
30268 table: None,
30269 except: None,
30270 replace: None,
30271 rename: None,
30272 trailing_comments: Vec::new(),
30273 span: None,
30274 })],
30275 from: Some(crate::expressions::From {
30276 expressions: vec![Expression::Table(Box::new(aliased_source))],
30277 }),
30278 into: Some(crate::expressions::SelectInto {
30279 this: Expression::Table(Box::new(ct.name.clone())),
30280 temporary: false,
30281 unlogged: false,
30282 bulk_collect: false,
30283 expressions: Vec::new(),
30284 }),
30285 top: Some(crate::expressions::Top {
30286 this: Expression::Literal(Box::new(Literal::Number(
30287 "0".to_string(),
30288 ))),
30289 percent: false,
30290 with_ties: false,
30291 parenthesized: false,
30292 }),
30293 ..Default::default()
30294 }));
30295 Ok(select)
30296 } else {
30297 Ok(Expression::CreateTable(ct))
30298 }
30299 } else {
30300 Ok(e)
30301 }
30302 }
30303
30304 Action::CreateTableLikeToAs => {
30305 // CREATE TABLE a LIKE b -> CREATE TABLE a AS b (ClickHouse)
30306 if let Expression::CreateTable(ct) = e {
30307 let like_source = ct.constraints.iter().find_map(|c| {
30308 if let crate::expressions::TableConstraint::Like { source, .. } = c {
30309 Some(source.clone())
30310 } else {
30311 None
30312 }
30313 });
30314 if let Some(source_table) = like_source {
30315 let mut new_ct = *ct;
30316 new_ct.constraints.clear();
30317 // AS b (just a table reference, not a SELECT)
30318 new_ct.as_select = Some(Expression::Table(Box::new(source_table)));
30319 Ok(Expression::CreateTable(Box::new(new_ct)))
30320 } else {
30321 Ok(Expression::CreateTable(ct))
30322 }
30323 } else {
30324 Ok(e)
30325 }
30326 }
30327
30328 Action::TsOrDsToDateConvert => {
30329 // TS_OR_DS_TO_DATE(x[, fmt]) -> dialect-specific date conversion
30330 if let Expression::Function(f) = e {
30331 let mut args = f.args;
30332 let this = args.remove(0);
30333 let fmt = if !args.is_empty() {
30334 match &args[0] {
30335 Expression::Literal(lit)
30336 if matches!(lit.as_ref(), Literal::String(_)) =>
30337 {
30338 let Literal::String(s) = lit.as_ref() else {
30339 unreachable!()
30340 };
30341 Some(s.clone())
30342 }
30343 _ => None,
30344 }
30345 } else {
30346 None
30347 };
30348 Ok(Expression::TsOrDsToDate(Box::new(
30349 crate::expressions::TsOrDsToDate {
30350 this: Box::new(this),
30351 format: fmt,
30352 safe: None,
30353 },
30354 )))
30355 } else {
30356 Ok(e)
30357 }
30358 }
30359
30360 Action::TsOrDsToDateStrConvert => {
30361 // TS_OR_DS_TO_DATE_STR(x) -> SUBSTRING(CAST(x AS type), 1, 10)
30362 if let Expression::Function(f) = e {
30363 let arg = f.args.into_iter().next().unwrap();
30364 let str_type = match target {
30365 DialectType::DuckDB
30366 | DialectType::PostgreSQL
30367 | DialectType::Materialize => DataType::Text,
30368 DialectType::Hive | DialectType::Spark | DialectType::Databricks => {
30369 DataType::Custom {
30370 name: "STRING".to_string(),
30371 }
30372 }
30373 DialectType::Presto
30374 | DialectType::Trino
30375 | DialectType::Athena
30376 | DialectType::Drill => DataType::VarChar {
30377 length: None,
30378 parenthesized_length: false,
30379 },
30380 DialectType::MySQL | DialectType::Doris | DialectType::StarRocks => {
30381 DataType::Custom {
30382 name: "STRING".to_string(),
30383 }
30384 }
30385 _ => DataType::VarChar {
30386 length: None,
30387 parenthesized_length: false,
30388 },
30389 };
30390 let cast_expr = Expression::Cast(Box::new(Cast {
30391 this: arg,
30392 to: str_type,
30393 double_colon_syntax: false,
30394 trailing_comments: Vec::new(),
30395 format: None,
30396 default: None,
30397 inferred_type: None,
30398 }));
30399 Ok(Expression::Substring(Box::new(
30400 crate::expressions::SubstringFunc {
30401 this: cast_expr,
30402 start: Expression::number(1),
30403 length: Some(Expression::number(10)),
30404 from_for_syntax: false,
30405 },
30406 )))
30407 } else {
30408 Ok(e)
30409 }
30410 }
30411
30412 Action::DateStrToDateConvert => {
30413 // DATE_STR_TO_DATE(x) -> dialect-specific
30414 if let Expression::Function(f) = e {
30415 let arg = f.args.into_iter().next().unwrap();
30416 match target {
30417 DialectType::SQLite => {
30418 // SQLite: just the bare expression (dates are strings)
30419 Ok(arg)
30420 }
30421 _ => Ok(Expression::Cast(Box::new(Cast {
30422 this: arg,
30423 to: DataType::Date,
30424 double_colon_syntax: false,
30425 trailing_comments: Vec::new(),
30426 format: None,
30427 default: None,
30428 inferred_type: None,
30429 }))),
30430 }
30431 } else {
30432 Ok(e)
30433 }
30434 }
30435
30436 Action::TimeStrToDateConvert => {
30437 // TIME_STR_TO_DATE(x) -> dialect-specific
30438 if let Expression::Function(f) = e {
30439 let arg = f.args.into_iter().next().unwrap();
30440 match target {
30441 DialectType::Hive
30442 | DialectType::Doris
30443 | DialectType::StarRocks
30444 | DialectType::Snowflake => Ok(Expression::Function(Box::new(
30445 Function::new("TO_DATE".to_string(), vec![arg]),
30446 ))),
30447 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
30448 // Presto: CAST(x AS TIMESTAMP)
30449 Ok(Expression::Cast(Box::new(Cast {
30450 this: arg,
30451 to: DataType::Timestamp {
30452 timezone: false,
30453 precision: None,
30454 },
30455 double_colon_syntax: false,
30456 trailing_comments: Vec::new(),
30457 format: None,
30458 default: None,
30459 inferred_type: None,
30460 })))
30461 }
30462 _ => {
30463 // Default: CAST(x AS DATE)
30464 Ok(Expression::Cast(Box::new(Cast {
30465 this: arg,
30466 to: DataType::Date,
30467 double_colon_syntax: false,
30468 trailing_comments: Vec::new(),
30469 format: None,
30470 default: None,
30471 inferred_type: None,
30472 })))
30473 }
30474 }
30475 } else {
30476 Ok(e)
30477 }
30478 }
30479
30480 Action::TimeStrToTimeConvert => {
30481 // TIME_STR_TO_TIME(x[, zone]) -> dialect-specific CAST to timestamp type
30482 if let Expression::Function(f) = e {
30483 let mut args = f.args;
30484 let this = args.remove(0);
30485 let zone = if !args.is_empty() {
30486 match &args[0] {
30487 Expression::Literal(lit)
30488 if matches!(lit.as_ref(), Literal::String(_)) =>
30489 {
30490 let Literal::String(s) = lit.as_ref() else {
30491 unreachable!()
30492 };
30493 Some(s.clone())
30494 }
30495 _ => None,
30496 }
30497 } else {
30498 None
30499 };
30500 let has_zone = zone.is_some();
30501
30502 match target {
30503 DialectType::SQLite => {
30504 // SQLite: just the bare expression
30505 Ok(this)
30506 }
30507 DialectType::MySQL => {
30508 if has_zone {
30509 // MySQL with zone: TIMESTAMP(x)
30510 Ok(Expression::Function(Box::new(Function::new(
30511 "TIMESTAMP".to_string(),
30512 vec![this],
30513 ))))
30514 } else {
30515 // MySQL: CAST(x AS DATETIME) or with precision
30516 // Use DataType::Custom to avoid MySQL's transform_cast converting
30517 // CAST(x AS TIMESTAMP) -> TIMESTAMP(x)
30518 let precision = if let Expression::Literal(ref lit) = this {
30519 if let Literal::String(ref s) = lit.as_ref() {
30520 if let Some(dot_pos) = s.rfind('.') {
30521 let frac = &s[dot_pos + 1..];
30522 let digit_count = frac
30523 .chars()
30524 .take_while(|c| c.is_ascii_digit())
30525 .count();
30526 if digit_count > 0 {
30527 Some(digit_count)
30528 } else {
30529 None
30530 }
30531 } else {
30532 None
30533 }
30534 } else {
30535 None
30536 }
30537 } else {
30538 None
30539 };
30540 let type_name = match precision {
30541 Some(p) => format!("DATETIME({})", p),
30542 None => "DATETIME".to_string(),
30543 };
30544 Ok(Expression::Cast(Box::new(Cast {
30545 this,
30546 to: DataType::Custom { name: type_name },
30547 double_colon_syntax: false,
30548 trailing_comments: Vec::new(),
30549 format: None,
30550 default: None,
30551 inferred_type: None,
30552 })))
30553 }
30554 }
30555 DialectType::ClickHouse => {
30556 if has_zone {
30557 // ClickHouse with zone: CAST(x AS DateTime64(6, 'zone'))
30558 // We need to strip the timezone offset from the literal if present
30559 let clean_this = if let Expression::Literal(ref lit) = this {
30560 if let Literal::String(ref s) = lit.as_ref() {
30561 // Strip timezone offset like "-08:00" or "+00:00"
30562 let re_offset = s.rfind(|c: char| c == '+' || c == '-');
30563 if let Some(offset_pos) = re_offset {
30564 if offset_pos > 10 {
30565 // After the date part
30566 let trimmed = s[..offset_pos].to_string();
30567 Expression::Literal(Box::new(Literal::String(
30568 trimmed,
30569 )))
30570 } else {
30571 this.clone()
30572 }
30573 } else {
30574 this.clone()
30575 }
30576 } else {
30577 this.clone()
30578 }
30579 } else {
30580 this.clone()
30581 };
30582 let zone_str = zone.unwrap();
30583 // Build: CAST(x AS DateTime64(6, 'zone'))
30584 let type_name = format!("DateTime64(6, '{}')", zone_str);
30585 Ok(Expression::Cast(Box::new(Cast {
30586 this: clean_this,
30587 to: DataType::Custom { name: type_name },
30588 double_colon_syntax: false,
30589 trailing_comments: Vec::new(),
30590 format: None,
30591 default: None,
30592 inferred_type: None,
30593 })))
30594 } else {
30595 Ok(Expression::Cast(Box::new(Cast {
30596 this,
30597 to: DataType::Custom {
30598 name: "DateTime64(6)".to_string(),
30599 },
30600 double_colon_syntax: false,
30601 trailing_comments: Vec::new(),
30602 format: None,
30603 default: None,
30604 inferred_type: None,
30605 })))
30606 }
30607 }
30608 DialectType::BigQuery => {
30609 if has_zone {
30610 // BigQuery with zone: CAST(x AS TIMESTAMP)
30611 Ok(Expression::Cast(Box::new(Cast {
30612 this,
30613 to: DataType::Timestamp {
30614 timezone: false,
30615 precision: None,
30616 },
30617 double_colon_syntax: false,
30618 trailing_comments: Vec::new(),
30619 format: None,
30620 default: None,
30621 inferred_type: None,
30622 })))
30623 } else {
30624 // BigQuery: CAST(x AS DATETIME) - Timestamp{tz:false} renders as DATETIME for BigQuery
30625 Ok(Expression::Cast(Box::new(Cast {
30626 this,
30627 to: DataType::Custom {
30628 name: "DATETIME".to_string(),
30629 },
30630 double_colon_syntax: false,
30631 trailing_comments: Vec::new(),
30632 format: None,
30633 default: None,
30634 inferred_type: None,
30635 })))
30636 }
30637 }
30638 DialectType::Doris => {
30639 // Doris: CAST(x AS DATETIME)
30640 Ok(Expression::Cast(Box::new(Cast {
30641 this,
30642 to: DataType::Custom {
30643 name: "DATETIME".to_string(),
30644 },
30645 double_colon_syntax: false,
30646 trailing_comments: Vec::new(),
30647 format: None,
30648 default: None,
30649 inferred_type: None,
30650 })))
30651 }
30652 DialectType::TSQL | DialectType::Fabric => {
30653 if has_zone {
30654 // TSQL with zone: CAST(x AS DATETIMEOFFSET) AT TIME ZONE 'UTC'
30655 let cast_expr = Expression::Cast(Box::new(Cast {
30656 this,
30657 to: DataType::Custom {
30658 name: "DATETIMEOFFSET".to_string(),
30659 },
30660 double_colon_syntax: false,
30661 trailing_comments: Vec::new(),
30662 format: None,
30663 default: None,
30664 inferred_type: None,
30665 }));
30666 Ok(Expression::AtTimeZone(Box::new(
30667 crate::expressions::AtTimeZone {
30668 this: cast_expr,
30669 zone: Expression::Literal(Box::new(Literal::String(
30670 "UTC".to_string(),
30671 ))),
30672 },
30673 )))
30674 } else {
30675 // TSQL: CAST(x AS DATETIME2)
30676 Ok(Expression::Cast(Box::new(Cast {
30677 this,
30678 to: DataType::Custom {
30679 name: "DATETIME2".to_string(),
30680 },
30681 double_colon_syntax: false,
30682 trailing_comments: Vec::new(),
30683 format: None,
30684 default: None,
30685 inferred_type: None,
30686 })))
30687 }
30688 }
30689 DialectType::DuckDB => {
30690 if has_zone {
30691 // DuckDB with zone: CAST(x AS TIMESTAMPTZ)
30692 Ok(Expression::Cast(Box::new(Cast {
30693 this,
30694 to: DataType::Timestamp {
30695 timezone: true,
30696 precision: None,
30697 },
30698 double_colon_syntax: false,
30699 trailing_comments: Vec::new(),
30700 format: None,
30701 default: None,
30702 inferred_type: None,
30703 })))
30704 } else {
30705 // DuckDB: CAST(x AS TIMESTAMP)
30706 Ok(Expression::Cast(Box::new(Cast {
30707 this,
30708 to: DataType::Timestamp {
30709 timezone: false,
30710 precision: None,
30711 },
30712 double_colon_syntax: false,
30713 trailing_comments: Vec::new(),
30714 format: None,
30715 default: None,
30716 inferred_type: None,
30717 })))
30718 }
30719 }
30720 DialectType::PostgreSQL
30721 | DialectType::Materialize
30722 | DialectType::RisingWave => {
30723 if has_zone {
30724 // PostgreSQL with zone: CAST(x AS TIMESTAMPTZ)
30725 Ok(Expression::Cast(Box::new(Cast {
30726 this,
30727 to: DataType::Timestamp {
30728 timezone: true,
30729 precision: None,
30730 },
30731 double_colon_syntax: false,
30732 trailing_comments: Vec::new(),
30733 format: None,
30734 default: None,
30735 inferred_type: None,
30736 })))
30737 } else {
30738 // PostgreSQL: CAST(x AS TIMESTAMP)
30739 Ok(Expression::Cast(Box::new(Cast {
30740 this,
30741 to: DataType::Timestamp {
30742 timezone: false,
30743 precision: None,
30744 },
30745 double_colon_syntax: false,
30746 trailing_comments: Vec::new(),
30747 format: None,
30748 default: None,
30749 inferred_type: None,
30750 })))
30751 }
30752 }
30753 DialectType::Snowflake => {
30754 if has_zone {
30755 // Snowflake with zone: CAST(x AS TIMESTAMPTZ)
30756 Ok(Expression::Cast(Box::new(Cast {
30757 this,
30758 to: DataType::Timestamp {
30759 timezone: true,
30760 precision: None,
30761 },
30762 double_colon_syntax: false,
30763 trailing_comments: Vec::new(),
30764 format: None,
30765 default: None,
30766 inferred_type: None,
30767 })))
30768 } else {
30769 // Snowflake: CAST(x AS TIMESTAMP)
30770 Ok(Expression::Cast(Box::new(Cast {
30771 this,
30772 to: DataType::Timestamp {
30773 timezone: false,
30774 precision: None,
30775 },
30776 double_colon_syntax: false,
30777 trailing_comments: Vec::new(),
30778 format: None,
30779 default: None,
30780 inferred_type: None,
30781 })))
30782 }
30783 }
30784 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
30785 if has_zone {
30786 // Presto/Trino with zone: CAST(x AS TIMESTAMP WITH TIME ZONE)
30787 // Check for precision from sub-second digits
30788 let precision = if let Expression::Literal(ref lit) = this {
30789 if let Literal::String(ref s) = lit.as_ref() {
30790 if let Some(dot_pos) = s.rfind('.') {
30791 let frac = &s[dot_pos + 1..];
30792 let digit_count = frac
30793 .chars()
30794 .take_while(|c| c.is_ascii_digit())
30795 .count();
30796 if digit_count > 0
30797 && matches!(target, DialectType::Trino)
30798 {
30799 Some(digit_count as u32)
30800 } else {
30801 None
30802 }
30803 } else {
30804 None
30805 }
30806 } else {
30807 None
30808 }
30809 } else {
30810 None
30811 };
30812 let dt = if let Some(prec) = precision {
30813 DataType::Timestamp {
30814 timezone: true,
30815 precision: Some(prec),
30816 }
30817 } else {
30818 DataType::Timestamp {
30819 timezone: true,
30820 precision: None,
30821 }
30822 };
30823 Ok(Expression::Cast(Box::new(Cast {
30824 this,
30825 to: dt,
30826 double_colon_syntax: false,
30827 trailing_comments: Vec::new(),
30828 format: None,
30829 default: None,
30830 inferred_type: None,
30831 })))
30832 } else {
30833 // Check for sub-second precision for Trino
30834 let precision = if let Expression::Literal(ref lit) = this {
30835 if let Literal::String(ref s) = lit.as_ref() {
30836 if let Some(dot_pos) = s.rfind('.') {
30837 let frac = &s[dot_pos + 1..];
30838 let digit_count = frac
30839 .chars()
30840 .take_while(|c| c.is_ascii_digit())
30841 .count();
30842 if digit_count > 0
30843 && matches!(target, DialectType::Trino)
30844 {
30845 Some(digit_count as u32)
30846 } else {
30847 None
30848 }
30849 } else {
30850 None
30851 }
30852 } else {
30853 None
30854 }
30855 } else {
30856 None
30857 };
30858 let dt = DataType::Timestamp {
30859 timezone: false,
30860 precision,
30861 };
30862 Ok(Expression::Cast(Box::new(Cast {
30863 this,
30864 to: dt,
30865 double_colon_syntax: false,
30866 trailing_comments: Vec::new(),
30867 format: None,
30868 default: None,
30869 inferred_type: None,
30870 })))
30871 }
30872 }
30873 DialectType::Redshift => {
30874 if has_zone {
30875 // Redshift with zone: CAST(x AS TIMESTAMP WITH TIME ZONE)
30876 Ok(Expression::Cast(Box::new(Cast {
30877 this,
30878 to: DataType::Timestamp {
30879 timezone: true,
30880 precision: None,
30881 },
30882 double_colon_syntax: false,
30883 trailing_comments: Vec::new(),
30884 format: None,
30885 default: None,
30886 inferred_type: None,
30887 })))
30888 } else {
30889 // Redshift: CAST(x AS TIMESTAMP)
30890 Ok(Expression::Cast(Box::new(Cast {
30891 this,
30892 to: DataType::Timestamp {
30893 timezone: false,
30894 precision: None,
30895 },
30896 double_colon_syntax: false,
30897 trailing_comments: Vec::new(),
30898 format: None,
30899 default: None,
30900 inferred_type: None,
30901 })))
30902 }
30903 }
30904 _ => {
30905 // Default: CAST(x AS TIMESTAMP)
30906 Ok(Expression::Cast(Box::new(Cast {
30907 this,
30908 to: DataType::Timestamp {
30909 timezone: false,
30910 precision: None,
30911 },
30912 double_colon_syntax: false,
30913 trailing_comments: Vec::new(),
30914 format: None,
30915 default: None,
30916 inferred_type: None,
30917 })))
30918 }
30919 }
30920 } else {
30921 Ok(e)
30922 }
30923 }
30924
30925 Action::DateToDateStrConvert => {
30926 // DATE_TO_DATE_STR(x) -> CAST(x AS text_type) per dialect
30927 if let Expression::Function(f) = e {
30928 let arg = f.args.into_iter().next().unwrap();
30929 let str_type = match target {
30930 DialectType::DuckDB => DataType::Text,
30931 DialectType::Hive | DialectType::Spark | DialectType::Databricks => {
30932 DataType::Custom {
30933 name: "STRING".to_string(),
30934 }
30935 }
30936 DialectType::Presto
30937 | DialectType::Trino
30938 | DialectType::Athena
30939 | DialectType::Drill => DataType::VarChar {
30940 length: None,
30941 parenthesized_length: false,
30942 },
30943 _ => DataType::VarChar {
30944 length: None,
30945 parenthesized_length: false,
30946 },
30947 };
30948 Ok(Expression::Cast(Box::new(Cast {
30949 this: arg,
30950 to: str_type,
30951 double_colon_syntax: false,
30952 trailing_comments: Vec::new(),
30953 format: None,
30954 default: None,
30955 inferred_type: None,
30956 })))
30957 } else {
30958 Ok(e)
30959 }
30960 }
30961
30962 Action::DateToDiConvert => {
30963 // DATE_TO_DI(x) -> CAST(format_func(x, fmt) AS INT)
30964 if let Expression::Function(f) = e {
30965 let arg = f.args.into_iter().next().unwrap();
30966 let inner = match target {
30967 DialectType::DuckDB => {
30968 // STRFTIME(x, '%Y%m%d')
30969 Expression::Function(Box::new(Function::new(
30970 "STRFTIME".to_string(),
30971 vec![arg, Expression::string("%Y%m%d")],
30972 )))
30973 }
30974 DialectType::Hive | DialectType::Spark | DialectType::Databricks => {
30975 // DATE_FORMAT(x, 'yyyyMMdd')
30976 Expression::Function(Box::new(Function::new(
30977 "DATE_FORMAT".to_string(),
30978 vec![arg, Expression::string("yyyyMMdd")],
30979 )))
30980 }
30981 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
30982 // DATE_FORMAT(x, '%Y%m%d')
30983 Expression::Function(Box::new(Function::new(
30984 "DATE_FORMAT".to_string(),
30985 vec![arg, Expression::string("%Y%m%d")],
30986 )))
30987 }
30988 DialectType::Drill => {
30989 // TO_DATE(x, 'yyyyMMdd')
30990 Expression::Function(Box::new(Function::new(
30991 "TO_DATE".to_string(),
30992 vec![arg, Expression::string("yyyyMMdd")],
30993 )))
30994 }
30995 _ => {
30996 // Default: STRFTIME(x, '%Y%m%d')
30997 Expression::Function(Box::new(Function::new(
30998 "STRFTIME".to_string(),
30999 vec![arg, Expression::string("%Y%m%d")],
31000 )))
31001 }
31002 };
31003 // Use INT (not INTEGER) for Presto/Trino
31004 let int_type = match target {
31005 DialectType::Presto
31006 | DialectType::Trino
31007 | DialectType::Athena
31008 | DialectType::TSQL
31009 | DialectType::Fabric
31010 | DialectType::SQLite
31011 | DialectType::Redshift => DataType::Custom {
31012 name: "INT".to_string(),
31013 },
31014 _ => DataType::Int {
31015 length: None,
31016 integer_spelling: false,
31017 },
31018 };
31019 Ok(Expression::Cast(Box::new(Cast {
31020 this: inner,
31021 to: int_type,
31022 double_colon_syntax: false,
31023 trailing_comments: Vec::new(),
31024 format: None,
31025 default: None,
31026 inferred_type: None,
31027 })))
31028 } else {
31029 Ok(e)
31030 }
31031 }
31032
31033 Action::DiToDateConvert => {
31034 // DI_TO_DATE(x) -> dialect-specific integer-to-date conversion
31035 if let Expression::Function(f) = e {
31036 let arg = f.args.into_iter().next().unwrap();
31037 match target {
31038 DialectType::DuckDB => {
31039 // CAST(STRPTIME(CAST(x AS TEXT), '%Y%m%d') AS DATE)
31040 let cast_text = Expression::Cast(Box::new(Cast {
31041 this: arg,
31042 to: DataType::Text,
31043 double_colon_syntax: false,
31044 trailing_comments: Vec::new(),
31045 format: None,
31046 default: None,
31047 inferred_type: None,
31048 }));
31049 let strptime = Expression::Function(Box::new(Function::new(
31050 "STRPTIME".to_string(),
31051 vec![cast_text, Expression::string("%Y%m%d")],
31052 )));
31053 Ok(Expression::Cast(Box::new(Cast {
31054 this: strptime,
31055 to: DataType::Date,
31056 double_colon_syntax: false,
31057 trailing_comments: Vec::new(),
31058 format: None,
31059 default: None,
31060 inferred_type: None,
31061 })))
31062 }
31063 DialectType::Hive | DialectType::Spark | DialectType::Databricks => {
31064 // TO_DATE(CAST(x AS STRING), 'yyyyMMdd')
31065 let cast_str = Expression::Cast(Box::new(Cast {
31066 this: arg,
31067 to: DataType::Custom {
31068 name: "STRING".to_string(),
31069 },
31070 double_colon_syntax: false,
31071 trailing_comments: Vec::new(),
31072 format: None,
31073 default: None,
31074 inferred_type: None,
31075 }));
31076 Ok(Expression::Function(Box::new(Function::new(
31077 "TO_DATE".to_string(),
31078 vec![cast_str, Expression::string("yyyyMMdd")],
31079 ))))
31080 }
31081 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
31082 // CAST(DATE_PARSE(CAST(x AS VARCHAR), '%Y%m%d') AS DATE)
31083 let cast_varchar = Expression::Cast(Box::new(Cast {
31084 this: arg,
31085 to: DataType::VarChar {
31086 length: None,
31087 parenthesized_length: false,
31088 },
31089 double_colon_syntax: false,
31090 trailing_comments: Vec::new(),
31091 format: None,
31092 default: None,
31093 inferred_type: None,
31094 }));
31095 let date_parse = Expression::Function(Box::new(Function::new(
31096 "DATE_PARSE".to_string(),
31097 vec![cast_varchar, Expression::string("%Y%m%d")],
31098 )));
31099 Ok(Expression::Cast(Box::new(Cast {
31100 this: date_parse,
31101 to: DataType::Date,
31102 double_colon_syntax: false,
31103 trailing_comments: Vec::new(),
31104 format: None,
31105 default: None,
31106 inferred_type: None,
31107 })))
31108 }
31109 DialectType::Drill => {
31110 // TO_DATE(CAST(x AS VARCHAR), 'yyyyMMdd')
31111 let cast_varchar = Expression::Cast(Box::new(Cast {
31112 this: arg,
31113 to: DataType::VarChar {
31114 length: None,
31115 parenthesized_length: false,
31116 },
31117 double_colon_syntax: false,
31118 trailing_comments: Vec::new(),
31119 format: None,
31120 default: None,
31121 inferred_type: None,
31122 }));
31123 Ok(Expression::Function(Box::new(Function::new(
31124 "TO_DATE".to_string(),
31125 vec![cast_varchar, Expression::string("yyyyMMdd")],
31126 ))))
31127 }
31128 _ => Ok(Expression::Function(Box::new(Function::new(
31129 "DI_TO_DATE".to_string(),
31130 vec![arg],
31131 )))),
31132 }
31133 } else {
31134 Ok(e)
31135 }
31136 }
31137
31138 Action::TsOrDiToDiConvert => {
31139 // TS_OR_DI_TO_DI(x) -> CAST(SUBSTR(REPLACE(CAST(x AS type), '-', ''), 1, 8) AS INT)
31140 if let Expression::Function(f) = e {
31141 let arg = f.args.into_iter().next().unwrap();
31142 let str_type = match target {
31143 DialectType::DuckDB => DataType::Text,
31144 DialectType::Hive | DialectType::Spark | DialectType::Databricks => {
31145 DataType::Custom {
31146 name: "STRING".to_string(),
31147 }
31148 }
31149 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
31150 DataType::VarChar {
31151 length: None,
31152 parenthesized_length: false,
31153 }
31154 }
31155 _ => DataType::VarChar {
31156 length: None,
31157 parenthesized_length: false,
31158 },
31159 };
31160 let cast_str = Expression::Cast(Box::new(Cast {
31161 this: arg,
31162 to: str_type,
31163 double_colon_syntax: false,
31164 trailing_comments: Vec::new(),
31165 format: None,
31166 default: None,
31167 inferred_type: None,
31168 }));
31169 let replace_expr = Expression::Function(Box::new(Function::new(
31170 "REPLACE".to_string(),
31171 vec![cast_str, Expression::string("-"), Expression::string("")],
31172 )));
31173 let substr_name = match target {
31174 DialectType::DuckDB
31175 | DialectType::Hive
31176 | DialectType::Spark
31177 | DialectType::Databricks => "SUBSTR",
31178 _ => "SUBSTR",
31179 };
31180 let substr = Expression::Function(Box::new(Function::new(
31181 substr_name.to_string(),
31182 vec![replace_expr, Expression::number(1), Expression::number(8)],
31183 )));
31184 // Use INT (not INTEGER) for Presto/Trino etc.
31185 let int_type = match target {
31186 DialectType::Presto
31187 | DialectType::Trino
31188 | DialectType::Athena
31189 | DialectType::TSQL
31190 | DialectType::Fabric
31191 | DialectType::SQLite
31192 | DialectType::Redshift => DataType::Custom {
31193 name: "INT".to_string(),
31194 },
31195 _ => DataType::Int {
31196 length: None,
31197 integer_spelling: false,
31198 },
31199 };
31200 Ok(Expression::Cast(Box::new(Cast {
31201 this: substr,
31202 to: int_type,
31203 double_colon_syntax: false,
31204 trailing_comments: Vec::new(),
31205 format: None,
31206 default: None,
31207 inferred_type: None,
31208 })))
31209 } else {
31210 Ok(e)
31211 }
31212 }
31213
31214 Action::UnixToStrConvert => {
31215 // UNIX_TO_STR(x, fmt) -> convert to Expression::UnixToStr for generator
31216 if let Expression::Function(f) = e {
31217 let mut args = f.args;
31218 let this = args.remove(0);
31219 let fmt_expr = if !args.is_empty() {
31220 Some(args.remove(0))
31221 } else {
31222 None
31223 };
31224
31225 // Check if format is a string literal
31226 let fmt_str = fmt_expr.as_ref().and_then(|f| {
31227 if let Expression::Literal(lit) = f {
31228 if let Literal::String(s) = lit.as_ref() {
31229 Some(s.clone())
31230 } else {
31231 None
31232 }
31233 } else {
31234 None
31235 }
31236 });
31237
31238 if let Some(fmt_string) = fmt_str {
31239 // String literal format -> use UnixToStr expression (generator handles it)
31240 Ok(Expression::UnixToStr(Box::new(
31241 crate::expressions::UnixToStr {
31242 this: Box::new(this),
31243 format: Some(fmt_string),
31244 },
31245 )))
31246 } else if let Some(fmt_e) = fmt_expr {
31247 // Non-literal format (e.g., identifier `y`) -> build target expression directly
31248 match target {
31249 DialectType::DuckDB => {
31250 // STRFTIME(TO_TIMESTAMP(x), y)
31251 let to_ts = Expression::Function(Box::new(Function::new(
31252 "TO_TIMESTAMP".to_string(),
31253 vec![this],
31254 )));
31255 Ok(Expression::Function(Box::new(Function::new(
31256 "STRFTIME".to_string(),
31257 vec![to_ts, fmt_e],
31258 ))))
31259 }
31260 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
31261 // DATE_FORMAT(FROM_UNIXTIME(x), y)
31262 let from_unix = Expression::Function(Box::new(Function::new(
31263 "FROM_UNIXTIME".to_string(),
31264 vec![this],
31265 )));
31266 Ok(Expression::Function(Box::new(Function::new(
31267 "DATE_FORMAT".to_string(),
31268 vec![from_unix, fmt_e],
31269 ))))
31270 }
31271 DialectType::Hive
31272 | DialectType::Spark
31273 | DialectType::Databricks
31274 | DialectType::Doris
31275 | DialectType::StarRocks => {
31276 // FROM_UNIXTIME(x, y)
31277 Ok(Expression::Function(Box::new(Function::new(
31278 "FROM_UNIXTIME".to_string(),
31279 vec![this, fmt_e],
31280 ))))
31281 }
31282 _ => {
31283 // Default: keep as UNIX_TO_STR(x, y)
31284 Ok(Expression::Function(Box::new(Function::new(
31285 "UNIX_TO_STR".to_string(),
31286 vec![this, fmt_e],
31287 ))))
31288 }
31289 }
31290 } else {
31291 Ok(Expression::UnixToStr(Box::new(
31292 crate::expressions::UnixToStr {
31293 this: Box::new(this),
31294 format: None,
31295 },
31296 )))
31297 }
31298 } else {
31299 Ok(e)
31300 }
31301 }
31302
31303 Action::UnixToTimeConvert => {
31304 // UNIX_TO_TIME(x) -> convert to Expression::UnixToTime for generator
31305 if let Expression::Function(f) = e {
31306 let arg = f.args.into_iter().next().unwrap();
31307 Ok(Expression::UnixToTime(Box::new(
31308 crate::expressions::UnixToTime {
31309 this: Box::new(arg),
31310 scale: None,
31311 zone: None,
31312 hours: None,
31313 minutes: None,
31314 format: None,
31315 target_type: None,
31316 },
31317 )))
31318 } else {
31319 Ok(e)
31320 }
31321 }
31322
31323 Action::UnixToTimeStrConvert => {
31324 // UNIX_TO_TIME_STR(x) -> dialect-specific
31325 if let Expression::Function(f) = e {
31326 let arg = f.args.into_iter().next().unwrap();
31327 match target {
31328 DialectType::Hive | DialectType::Spark | DialectType::Databricks => {
31329 // FROM_UNIXTIME(x)
31330 Ok(Expression::Function(Box::new(Function::new(
31331 "FROM_UNIXTIME".to_string(),
31332 vec![arg],
31333 ))))
31334 }
31335 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
31336 // CAST(FROM_UNIXTIME(x) AS VARCHAR)
31337 let from_unix = Expression::Function(Box::new(Function::new(
31338 "FROM_UNIXTIME".to_string(),
31339 vec![arg],
31340 )));
31341 Ok(Expression::Cast(Box::new(Cast {
31342 this: from_unix,
31343 to: DataType::VarChar {
31344 length: None,
31345 parenthesized_length: false,
31346 },
31347 double_colon_syntax: false,
31348 trailing_comments: Vec::new(),
31349 format: None,
31350 default: None,
31351 inferred_type: None,
31352 })))
31353 }
31354 DialectType::DuckDB => {
31355 // CAST(TO_TIMESTAMP(x) AS TEXT)
31356 let to_ts = Expression::Function(Box::new(Function::new(
31357 "TO_TIMESTAMP".to_string(),
31358 vec![arg],
31359 )));
31360 Ok(Expression::Cast(Box::new(Cast {
31361 this: to_ts,
31362 to: DataType::Text,
31363 double_colon_syntax: false,
31364 trailing_comments: Vec::new(),
31365 format: None,
31366 default: None,
31367 inferred_type: None,
31368 })))
31369 }
31370 _ => Ok(Expression::Function(Box::new(Function::new(
31371 "UNIX_TO_TIME_STR".to_string(),
31372 vec![arg],
31373 )))),
31374 }
31375 } else {
31376 Ok(e)
31377 }
31378 }
31379
31380 Action::TimeToUnixConvert => {
31381 // TIME_TO_UNIX(x) -> convert to Expression::TimeToUnix for generator
31382 if let Expression::Function(f) = e {
31383 let arg = f.args.into_iter().next().unwrap();
31384 Ok(Expression::TimeToUnix(Box::new(
31385 crate::expressions::UnaryFunc {
31386 this: arg,
31387 original_name: None,
31388 inferred_type: None,
31389 },
31390 )))
31391 } else {
31392 Ok(e)
31393 }
31394 }
31395
31396 Action::TimeToStrConvert => {
31397 // TIME_TO_STR(x, fmt) -> convert to Expression::TimeToStr for generator
31398 if let Expression::Function(f) = e {
31399 let mut args = f.args;
31400 let this = args.remove(0);
31401 let fmt = match args.remove(0) {
31402 Expression::Literal(lit)
31403 if matches!(lit.as_ref(), Literal::String(_)) =>
31404 {
31405 let Literal::String(s) = lit.as_ref() else {
31406 unreachable!()
31407 };
31408 s.clone()
31409 }
31410 other => {
31411 return Ok(Expression::Function(Box::new(Function::new(
31412 "TIME_TO_STR".to_string(),
31413 vec![this, other],
31414 ))));
31415 }
31416 };
31417 Ok(Expression::TimeToStr(Box::new(
31418 crate::expressions::TimeToStr {
31419 this: Box::new(this),
31420 format: fmt,
31421 culture: None,
31422 zone: None,
31423 },
31424 )))
31425 } else {
31426 Ok(e)
31427 }
31428 }
31429
31430 Action::StrToUnixConvert => {
31431 // STR_TO_UNIX(x, fmt) -> convert to Expression::StrToUnix for generator
31432 if let Expression::Function(f) = e {
31433 let mut args = f.args;
31434 let this = args.remove(0);
31435 let fmt = match args.remove(0) {
31436 Expression::Literal(lit)
31437 if matches!(lit.as_ref(), Literal::String(_)) =>
31438 {
31439 let Literal::String(s) = lit.as_ref() else {
31440 unreachable!()
31441 };
31442 s.clone()
31443 }
31444 other => {
31445 return Ok(Expression::Function(Box::new(Function::new(
31446 "STR_TO_UNIX".to_string(),
31447 vec![this, other],
31448 ))));
31449 }
31450 };
31451 Ok(Expression::StrToUnix(Box::new(
31452 crate::expressions::StrToUnix {
31453 this: Some(Box::new(this)),
31454 format: Some(fmt),
31455 },
31456 )))
31457 } else {
31458 Ok(e)
31459 }
31460 }
31461
31462 Action::TimeStrToUnixConvert => {
31463 // TIME_STR_TO_UNIX(x) -> dialect-specific
31464 if let Expression::Function(f) = e {
31465 let arg = f.args.into_iter().next().unwrap();
31466 match target {
31467 DialectType::DuckDB => {
31468 // EPOCH(CAST(x AS TIMESTAMP))
31469 let cast_ts = Expression::Cast(Box::new(Cast {
31470 this: arg,
31471 to: DataType::Timestamp {
31472 timezone: false,
31473 precision: None,
31474 },
31475 double_colon_syntax: false,
31476 trailing_comments: Vec::new(),
31477 format: None,
31478 default: None,
31479 inferred_type: None,
31480 }));
31481 Ok(Expression::Function(Box::new(Function::new(
31482 "EPOCH".to_string(),
31483 vec![cast_ts],
31484 ))))
31485 }
31486 DialectType::Hive
31487 | DialectType::Doris
31488 | DialectType::StarRocks
31489 | DialectType::MySQL => {
31490 // UNIX_TIMESTAMP(x)
31491 Ok(Expression::Function(Box::new(Function::new(
31492 "UNIX_TIMESTAMP".to_string(),
31493 vec![arg],
31494 ))))
31495 }
31496 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
31497 // TO_UNIXTIME(DATE_PARSE(x, '%Y-%m-%d %T'))
31498 let date_parse = Expression::Function(Box::new(Function::new(
31499 "DATE_PARSE".to_string(),
31500 vec![arg, Expression::string("%Y-%m-%d %T")],
31501 )));
31502 Ok(Expression::Function(Box::new(Function::new(
31503 "TO_UNIXTIME".to_string(),
31504 vec![date_parse],
31505 ))))
31506 }
31507 _ => Ok(Expression::Function(Box::new(Function::new(
31508 "TIME_STR_TO_UNIX".to_string(),
31509 vec![arg],
31510 )))),
31511 }
31512 } else {
31513 Ok(e)
31514 }
31515 }
31516
31517 Action::TimeToTimeStrConvert => {
31518 // TIME_TO_TIME_STR(x) -> CAST(x AS str_type) per dialect
31519 if let Expression::Function(f) = e {
31520 let arg = f.args.into_iter().next().unwrap();
31521 let str_type = match target {
31522 DialectType::DuckDB => DataType::Text,
31523 DialectType::Hive
31524 | DialectType::Spark
31525 | DialectType::Databricks
31526 | DialectType::Doris
31527 | DialectType::StarRocks => DataType::Custom {
31528 name: "STRING".to_string(),
31529 },
31530 DialectType::Redshift => DataType::Custom {
31531 name: "VARCHAR(MAX)".to_string(),
31532 },
31533 _ => DataType::VarChar {
31534 length: None,
31535 parenthesized_length: false,
31536 },
31537 };
31538 Ok(Expression::Cast(Box::new(Cast {
31539 this: arg,
31540 to: str_type,
31541 double_colon_syntax: false,
31542 trailing_comments: Vec::new(),
31543 format: None,
31544 default: None,
31545 inferred_type: None,
31546 })))
31547 } else {
31548 Ok(e)
31549 }
31550 }
31551
31552 Action::DateTruncSwapArgs => {
31553 // DATE_TRUNC('unit', x) from Generic -> target-specific
31554 if let Expression::Function(f) = e {
31555 if f.args.len() == 2 {
31556 let unit_arg = f.args[0].clone();
31557 let expr_arg = f.args[1].clone();
31558 // Extract unit string from the first arg
31559 let unit_str = match &unit_arg {
31560 Expression::Literal(lit)
31561 if matches!(lit.as_ref(), Literal::String(_)) =>
31562 {
31563 let Literal::String(s) = lit.as_ref() else {
31564 unreachable!()
31565 };
31566 s.to_ascii_uppercase()
31567 }
31568 _ => return Ok(Expression::Function(f)),
31569 };
31570 match target {
31571 DialectType::BigQuery => {
31572 // BigQuery: DATE_TRUNC(x, UNIT) - unquoted unit
31573 let unit_ident =
31574 Expression::Column(Box::new(crate::expressions::Column {
31575 name: crate::expressions::Identifier::new(unit_str),
31576 table: None,
31577 join_mark: false,
31578 trailing_comments: Vec::new(),
31579 span: None,
31580 inferred_type: None,
31581 }));
31582 Ok(Expression::Function(Box::new(Function::new(
31583 "DATE_TRUNC".to_string(),
31584 vec![expr_arg, unit_ident],
31585 ))))
31586 }
31587 DialectType::Doris => {
31588 // Doris: DATE_TRUNC(x, 'UNIT')
31589 Ok(Expression::Function(Box::new(Function::new(
31590 "DATE_TRUNC".to_string(),
31591 vec![expr_arg, Expression::string(&unit_str)],
31592 ))))
31593 }
31594 DialectType::StarRocks => {
31595 // StarRocks: DATE_TRUNC('UNIT', x) - keep standard order
31596 Ok(Expression::Function(Box::new(Function::new(
31597 "DATE_TRUNC".to_string(),
31598 vec![Expression::string(&unit_str), expr_arg],
31599 ))))
31600 }
31601 DialectType::Spark | DialectType::Databricks => {
31602 // Spark: TRUNC(x, 'UNIT')
31603 Ok(Expression::Function(Box::new(Function::new(
31604 "TRUNC".to_string(),
31605 vec![expr_arg, Expression::string(&unit_str)],
31606 ))))
31607 }
31608 DialectType::MySQL => {
31609 // MySQL: complex expansion based on unit
31610 Self::date_trunc_to_mysql(&unit_str, &expr_arg)
31611 }
31612 _ => Ok(Expression::Function(f)),
31613 }
31614 } else {
31615 Ok(Expression::Function(f))
31616 }
31617 } else {
31618 Ok(e)
31619 }
31620 }
31621
31622 Action::TimestampTruncConvert => {
31623 // TIMESTAMP_TRUNC(x, UNIT[, tz]) from Generic -> target-specific
31624 if let Expression::Function(f) = e {
31625 if f.args.len() >= 2 {
31626 let expr_arg = f.args[0].clone();
31627 let unit_arg = f.args[1].clone();
31628 let tz_arg = if f.args.len() >= 3 {
31629 Some(f.args[2].clone())
31630 } else {
31631 None
31632 };
31633 // Extract unit string
31634 let unit_str = match &unit_arg {
31635 Expression::Literal(lit)
31636 if matches!(lit.as_ref(), Literal::String(_)) =>
31637 {
31638 let Literal::String(s) = lit.as_ref() else {
31639 unreachable!()
31640 };
31641 s.to_ascii_uppercase()
31642 }
31643 Expression::Column(c) => c.name.name.to_ascii_uppercase(),
31644 _ => {
31645 return Ok(Expression::Function(f));
31646 }
31647 };
31648 match target {
31649 DialectType::Spark | DialectType::Databricks => {
31650 // Spark: DATE_TRUNC('UNIT', x)
31651 Ok(Expression::Function(Box::new(Function::new(
31652 "DATE_TRUNC".to_string(),
31653 vec![Expression::string(&unit_str), expr_arg],
31654 ))))
31655 }
31656 DialectType::Doris | DialectType::StarRocks => {
31657 // Doris: DATE_TRUNC(x, 'UNIT')
31658 Ok(Expression::Function(Box::new(Function::new(
31659 "DATE_TRUNC".to_string(),
31660 vec![expr_arg, Expression::string(&unit_str)],
31661 ))))
31662 }
31663 DialectType::BigQuery => {
31664 // BigQuery: TIMESTAMP_TRUNC(x, UNIT) - keep but with unquoted unit
31665 let unit_ident =
31666 Expression::Column(Box::new(crate::expressions::Column {
31667 name: crate::expressions::Identifier::new(unit_str),
31668 table: None,
31669 join_mark: false,
31670 trailing_comments: Vec::new(),
31671 span: None,
31672 inferred_type: None,
31673 }));
31674 let mut args = vec![expr_arg, unit_ident];
31675 if let Some(tz) = tz_arg {
31676 args.push(tz);
31677 }
31678 Ok(Expression::Function(Box::new(Function::new(
31679 "TIMESTAMP_TRUNC".to_string(),
31680 args,
31681 ))))
31682 }
31683 DialectType::DuckDB => {
31684 // DuckDB with timezone: DATE_TRUNC('UNIT', x AT TIME ZONE 'tz') AT TIME ZONE 'tz'
31685 if let Some(tz) = tz_arg {
31686 let tz_str = match &tz {
31687 Expression::Literal(lit)
31688 if matches!(lit.as_ref(), Literal::String(_)) =>
31689 {
31690 let Literal::String(s) = lit.as_ref() else {
31691 unreachable!()
31692 };
31693 s.clone()
31694 }
31695 _ => "UTC".to_string(),
31696 };
31697 // x AT TIME ZONE 'tz'
31698 let at_tz = Expression::AtTimeZone(Box::new(
31699 crate::expressions::AtTimeZone {
31700 this: expr_arg,
31701 zone: Expression::string(&tz_str),
31702 },
31703 ));
31704 // DATE_TRUNC('UNIT', x AT TIME ZONE 'tz')
31705 let trunc = Expression::Function(Box::new(Function::new(
31706 "DATE_TRUNC".to_string(),
31707 vec![Expression::string(&unit_str), at_tz],
31708 )));
31709 // DATE_TRUNC(...) AT TIME ZONE 'tz'
31710 Ok(Expression::AtTimeZone(Box::new(
31711 crate::expressions::AtTimeZone {
31712 this: trunc,
31713 zone: Expression::string(&tz_str),
31714 },
31715 )))
31716 } else {
31717 Ok(Expression::Function(Box::new(Function::new(
31718 "DATE_TRUNC".to_string(),
31719 vec![Expression::string(&unit_str), expr_arg],
31720 ))))
31721 }
31722 }
31723 DialectType::Presto
31724 | DialectType::Trino
31725 | DialectType::Athena
31726 | DialectType::Snowflake => {
31727 // Presto/Snowflake: DATE_TRUNC('UNIT', x) - drop timezone
31728 Ok(Expression::Function(Box::new(Function::new(
31729 "DATE_TRUNC".to_string(),
31730 vec![Expression::string(&unit_str), expr_arg],
31731 ))))
31732 }
31733 _ => {
31734 // For most dialects: DATE_TRUNC('UNIT', x) + tz handling
31735 let mut args = vec![Expression::string(&unit_str), expr_arg];
31736 if let Some(tz) = tz_arg {
31737 args.push(tz);
31738 }
31739 Ok(Expression::Function(Box::new(Function::new(
31740 "DATE_TRUNC".to_string(),
31741 args,
31742 ))))
31743 }
31744 }
31745 } else {
31746 Ok(Expression::Function(f))
31747 }
31748 } else {
31749 Ok(e)
31750 }
31751 }
31752
31753 Action::StrToDateConvert => {
31754 // STR_TO_DATE(x, fmt) from Generic -> dialect-specific date parsing
31755 if let Expression::Function(f) = e {
31756 if f.args.len() == 2 {
31757 let mut args = f.args;
31758 let this = args.remove(0);
31759 let fmt_expr = args.remove(0);
31760 let fmt_str = match &fmt_expr {
31761 Expression::Literal(lit)
31762 if matches!(lit.as_ref(), Literal::String(_)) =>
31763 {
31764 let Literal::String(s) = lit.as_ref() else {
31765 unreachable!()
31766 };
31767 Some(s.clone())
31768 }
31769 _ => None,
31770 };
31771 let default_date = "%Y-%m-%d";
31772 let default_time = "%Y-%m-%d %H:%M:%S";
31773 let is_default = fmt_str
31774 .as_ref()
31775 .map_or(false, |f| f == default_date || f == default_time);
31776
31777 if is_default {
31778 // Default format: handle per-dialect
31779 match target {
31780 DialectType::MySQL
31781 | DialectType::Doris
31782 | DialectType::StarRocks => {
31783 // Keep STR_TO_DATE(x, fmt) as-is
31784 Ok(Expression::Function(Box::new(Function::new(
31785 "STR_TO_DATE".to_string(),
31786 vec![this, fmt_expr],
31787 ))))
31788 }
31789 DialectType::Hive => {
31790 // Hive: CAST(x AS DATE)
31791 Ok(Expression::Cast(Box::new(Cast {
31792 this,
31793 to: DataType::Date,
31794 double_colon_syntax: false,
31795 trailing_comments: Vec::new(),
31796 format: None,
31797 default: None,
31798 inferred_type: None,
31799 })))
31800 }
31801 DialectType::Presto
31802 | DialectType::Trino
31803 | DialectType::Athena => {
31804 // Presto: CAST(DATE_PARSE(x, '%Y-%m-%d') AS DATE)
31805 let date_parse =
31806 Expression::Function(Box::new(Function::new(
31807 "DATE_PARSE".to_string(),
31808 vec![this, fmt_expr],
31809 )));
31810 Ok(Expression::Cast(Box::new(Cast {
31811 this: date_parse,
31812 to: DataType::Date,
31813 double_colon_syntax: false,
31814 trailing_comments: Vec::new(),
31815 format: None,
31816 default: None,
31817 inferred_type: None,
31818 })))
31819 }
31820 _ => {
31821 // Others: TsOrDsToDate (delegates to generator)
31822 Ok(Expression::TsOrDsToDate(Box::new(
31823 crate::expressions::TsOrDsToDate {
31824 this: Box::new(this),
31825 format: None,
31826 safe: None,
31827 },
31828 )))
31829 }
31830 }
31831 } else if let Some(fmt) = fmt_str {
31832 match target {
31833 DialectType::Doris
31834 | DialectType::StarRocks
31835 | DialectType::MySQL => {
31836 // Keep STR_TO_DATE but with normalized format (%H:%M:%S -> %T, %-d -> %e)
31837 let mut normalized = fmt.clone();
31838 normalized = normalized.replace("%-d", "%e");
31839 normalized = normalized.replace("%-m", "%c");
31840 normalized = normalized.replace("%H:%M:%S", "%T");
31841 Ok(Expression::Function(Box::new(Function::new(
31842 "STR_TO_DATE".to_string(),
31843 vec![this, Expression::string(&normalized)],
31844 ))))
31845 }
31846 DialectType::Hive => {
31847 // Hive: CAST(FROM_UNIXTIME(UNIX_TIMESTAMP(x, java_fmt)) AS DATE)
31848 let java_fmt = crate::generator::Generator::strftime_to_java_format_static(&fmt);
31849 let unix_ts =
31850 Expression::Function(Box::new(Function::new(
31851 "UNIX_TIMESTAMP".to_string(),
31852 vec![this, Expression::string(&java_fmt)],
31853 )));
31854 let from_unix =
31855 Expression::Function(Box::new(Function::new(
31856 "FROM_UNIXTIME".to_string(),
31857 vec![unix_ts],
31858 )));
31859 Ok(Expression::Cast(Box::new(Cast {
31860 this: from_unix,
31861 to: DataType::Date,
31862 double_colon_syntax: false,
31863 trailing_comments: Vec::new(),
31864 format: None,
31865 default: None,
31866 inferred_type: None,
31867 })))
31868 }
31869 DialectType::Spark | DialectType::Databricks => {
31870 // Spark: TO_DATE(x, java_fmt)
31871 let java_fmt = crate::generator::Generator::strftime_to_java_format_static(&fmt);
31872 Ok(Expression::Function(Box::new(Function::new(
31873 "TO_DATE".to_string(),
31874 vec![this, Expression::string(&java_fmt)],
31875 ))))
31876 }
31877 DialectType::Drill => {
31878 // Drill: TO_DATE(x, java_fmt) with T quoted as 'T' in Java format
31879 // The generator's string literal escaping will double the quotes: 'T' -> ''T''
31880 let java_fmt = crate::generator::Generator::strftime_to_java_format_static(&fmt);
31881 let java_fmt = java_fmt.replace('T', "'T'");
31882 Ok(Expression::Function(Box::new(Function::new(
31883 "TO_DATE".to_string(),
31884 vec![this, Expression::string(&java_fmt)],
31885 ))))
31886 }
31887 _ => {
31888 // For other dialects: use TsOrDsToDate which delegates to generator
31889 Ok(Expression::TsOrDsToDate(Box::new(
31890 crate::expressions::TsOrDsToDate {
31891 this: Box::new(this),
31892 format: Some(fmt),
31893 safe: None,
31894 },
31895 )))
31896 }
31897 }
31898 } else {
31899 // Non-string format - keep as-is
31900 let mut new_args = Vec::new();
31901 new_args.push(this);
31902 new_args.push(fmt_expr);
31903 Ok(Expression::Function(Box::new(Function::new(
31904 "STR_TO_DATE".to_string(),
31905 new_args,
31906 ))))
31907 }
31908 } else {
31909 Ok(Expression::Function(f))
31910 }
31911 } else {
31912 Ok(e)
31913 }
31914 }
31915
31916 Action::TsOrDsAddConvert => {
31917 // TS_OR_DS_ADD(x, n, 'UNIT') from Generic -> dialect-specific DATE_ADD
31918 if let Expression::Function(f) = e {
31919 if f.args.len() == 3 {
31920 let mut args = f.args;
31921 let x = args.remove(0);
31922 let n = args.remove(0);
31923 let unit_expr = args.remove(0);
31924 let unit_str = match &unit_expr {
31925 Expression::Literal(lit)
31926 if matches!(lit.as_ref(), Literal::String(_)) =>
31927 {
31928 let Literal::String(s) = lit.as_ref() else {
31929 unreachable!()
31930 };
31931 s.to_ascii_uppercase()
31932 }
31933 _ => "DAY".to_string(),
31934 };
31935
31936 match target {
31937 DialectType::Hive
31938 | DialectType::Spark
31939 | DialectType::Databricks => {
31940 // DATE_ADD(x, n) - only supports DAY unit
31941 Ok(Expression::Function(Box::new(Function::new(
31942 "DATE_ADD".to_string(),
31943 vec![x, n],
31944 ))))
31945 }
31946 DialectType::MySQL => {
31947 // DATE_ADD(x, INTERVAL n UNIT)
31948 let iu = match unit_str.as_str() {
31949 "YEAR" => crate::expressions::IntervalUnit::Year,
31950 "QUARTER" => crate::expressions::IntervalUnit::Quarter,
31951 "MONTH" => crate::expressions::IntervalUnit::Month,
31952 "WEEK" => crate::expressions::IntervalUnit::Week,
31953 "HOUR" => crate::expressions::IntervalUnit::Hour,
31954 "MINUTE" => crate::expressions::IntervalUnit::Minute,
31955 "SECOND" => crate::expressions::IntervalUnit::Second,
31956 _ => crate::expressions::IntervalUnit::Day,
31957 };
31958 let interval = Expression::Interval(Box::new(
31959 crate::expressions::Interval {
31960 this: Some(n),
31961 unit: Some(
31962 crate::expressions::IntervalUnitSpec::Simple {
31963 unit: iu,
31964 use_plural: false,
31965 },
31966 ),
31967 },
31968 ));
31969 Ok(Expression::Function(Box::new(Function::new(
31970 "DATE_ADD".to_string(),
31971 vec![x, interval],
31972 ))))
31973 }
31974 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
31975 // DATE_ADD('UNIT', n, CAST(CAST(x AS TIMESTAMP) AS DATE))
31976 let cast_ts = Expression::Cast(Box::new(Cast {
31977 this: x,
31978 to: DataType::Timestamp {
31979 precision: None,
31980 timezone: false,
31981 },
31982 double_colon_syntax: false,
31983 trailing_comments: Vec::new(),
31984 format: None,
31985 default: None,
31986 inferred_type: None,
31987 }));
31988 let cast_date = Expression::Cast(Box::new(Cast {
31989 this: cast_ts,
31990 to: DataType::Date,
31991 double_colon_syntax: false,
31992 trailing_comments: Vec::new(),
31993 format: None,
31994 default: None,
31995 inferred_type: None,
31996 }));
31997 Ok(Expression::Function(Box::new(Function::new(
31998 "DATE_ADD".to_string(),
31999 vec![Expression::string(&unit_str), n, cast_date],
32000 ))))
32001 }
32002 DialectType::DuckDB => {
32003 // CAST(x AS DATE) + INTERVAL n UNIT
32004 let cast_date = Expression::Cast(Box::new(Cast {
32005 this: x,
32006 to: DataType::Date,
32007 double_colon_syntax: false,
32008 trailing_comments: Vec::new(),
32009 format: None,
32010 default: None,
32011 inferred_type: None,
32012 }));
32013 let iu = match unit_str.as_str() {
32014 "YEAR" => crate::expressions::IntervalUnit::Year,
32015 "QUARTER" => crate::expressions::IntervalUnit::Quarter,
32016 "MONTH" => crate::expressions::IntervalUnit::Month,
32017 "WEEK" => crate::expressions::IntervalUnit::Week,
32018 "HOUR" => crate::expressions::IntervalUnit::Hour,
32019 "MINUTE" => crate::expressions::IntervalUnit::Minute,
32020 "SECOND" => crate::expressions::IntervalUnit::Second,
32021 _ => crate::expressions::IntervalUnit::Day,
32022 };
32023 let interval = Expression::Interval(Box::new(
32024 crate::expressions::Interval {
32025 this: Some(n),
32026 unit: Some(
32027 crate::expressions::IntervalUnitSpec::Simple {
32028 unit: iu,
32029 use_plural: false,
32030 },
32031 ),
32032 },
32033 ));
32034 Ok(Expression::Add(Box::new(crate::expressions::BinaryOp {
32035 left: cast_date,
32036 right: interval,
32037 left_comments: Vec::new(),
32038 operator_comments: Vec::new(),
32039 trailing_comments: Vec::new(),
32040 inferred_type: None,
32041 })))
32042 }
32043 DialectType::Drill => {
32044 // DATE_ADD(CAST(x AS DATE), INTERVAL n UNIT)
32045 let cast_date = Expression::Cast(Box::new(Cast {
32046 this: x,
32047 to: DataType::Date,
32048 double_colon_syntax: false,
32049 trailing_comments: Vec::new(),
32050 format: None,
32051 default: None,
32052 inferred_type: None,
32053 }));
32054 let iu = match unit_str.as_str() {
32055 "YEAR" => crate::expressions::IntervalUnit::Year,
32056 "QUARTER" => crate::expressions::IntervalUnit::Quarter,
32057 "MONTH" => crate::expressions::IntervalUnit::Month,
32058 "WEEK" => crate::expressions::IntervalUnit::Week,
32059 "HOUR" => crate::expressions::IntervalUnit::Hour,
32060 "MINUTE" => crate::expressions::IntervalUnit::Minute,
32061 "SECOND" => crate::expressions::IntervalUnit::Second,
32062 _ => crate::expressions::IntervalUnit::Day,
32063 };
32064 let interval = Expression::Interval(Box::new(
32065 crate::expressions::Interval {
32066 this: Some(n),
32067 unit: Some(
32068 crate::expressions::IntervalUnitSpec::Simple {
32069 unit: iu,
32070 use_plural: false,
32071 },
32072 ),
32073 },
32074 ));
32075 Ok(Expression::Function(Box::new(Function::new(
32076 "DATE_ADD".to_string(),
32077 vec![cast_date, interval],
32078 ))))
32079 }
32080 _ => {
32081 // Default: keep as TS_OR_DS_ADD
32082 Ok(Expression::Function(Box::new(Function::new(
32083 "TS_OR_DS_ADD".to_string(),
32084 vec![x, n, unit_expr],
32085 ))))
32086 }
32087 }
32088 } else {
32089 Ok(Expression::Function(f))
32090 }
32091 } else {
32092 Ok(e)
32093 }
32094 }
32095
32096 Action::DateFromUnixDateConvert => {
32097 // DATE_FROM_UNIX_DATE(n) -> DATEADD(DAY, n, CAST('1970-01-01' AS DATE))
32098 if let Expression::Function(f) = e {
32099 // Keep as-is for dialects that support DATE_FROM_UNIX_DATE natively
32100 if matches!(
32101 target,
32102 DialectType::Spark | DialectType::Databricks | DialectType::BigQuery
32103 ) {
32104 return Ok(Expression::Function(Box::new(Function::new(
32105 "DATE_FROM_UNIX_DATE".to_string(),
32106 f.args,
32107 ))));
32108 }
32109 let n = f.args.into_iter().next().unwrap();
32110 let epoch_date = Expression::Cast(Box::new(Cast {
32111 this: Expression::string("1970-01-01"),
32112 to: DataType::Date,
32113 double_colon_syntax: false,
32114 trailing_comments: Vec::new(),
32115 format: None,
32116 default: None,
32117 inferred_type: None,
32118 }));
32119 match target {
32120 DialectType::DuckDB => {
32121 // CAST('1970-01-01' AS DATE) + INTERVAL n DAY
32122 let interval =
32123 Expression::Interval(Box::new(crate::expressions::Interval {
32124 this: Some(n),
32125 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
32126 unit: crate::expressions::IntervalUnit::Day,
32127 use_plural: false,
32128 }),
32129 }));
32130 Ok(Expression::Add(Box::new(
32131 crate::expressions::BinaryOp::new(epoch_date, interval),
32132 )))
32133 }
32134 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
32135 // DATE_ADD('DAY', n, CAST('1970-01-01' AS DATE))
32136 Ok(Expression::Function(Box::new(Function::new(
32137 "DATE_ADD".to_string(),
32138 vec![Expression::string("DAY"), n, epoch_date],
32139 ))))
32140 }
32141 DialectType::Snowflake | DialectType::Redshift | DialectType::TSQL => {
32142 // DATEADD(DAY, n, CAST('1970-01-01' AS DATE))
32143 Ok(Expression::Function(Box::new(Function::new(
32144 "DATEADD".to_string(),
32145 vec![
32146 Expression::Identifier(Identifier::new("DAY")),
32147 n,
32148 epoch_date,
32149 ],
32150 ))))
32151 }
32152 DialectType::BigQuery => {
32153 // DATE_ADD(CAST('1970-01-01' AS DATE), INTERVAL n DAY)
32154 let interval =
32155 Expression::Interval(Box::new(crate::expressions::Interval {
32156 this: Some(n),
32157 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
32158 unit: crate::expressions::IntervalUnit::Day,
32159 use_plural: false,
32160 }),
32161 }));
32162 Ok(Expression::Function(Box::new(Function::new(
32163 "DATE_ADD".to_string(),
32164 vec![epoch_date, interval],
32165 ))))
32166 }
32167 DialectType::MySQL
32168 | DialectType::Doris
32169 | DialectType::StarRocks
32170 | DialectType::Drill => {
32171 // DATE_ADD(CAST('1970-01-01' AS DATE), INTERVAL n DAY)
32172 let interval =
32173 Expression::Interval(Box::new(crate::expressions::Interval {
32174 this: Some(n),
32175 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
32176 unit: crate::expressions::IntervalUnit::Day,
32177 use_plural: false,
32178 }),
32179 }));
32180 Ok(Expression::Function(Box::new(Function::new(
32181 "DATE_ADD".to_string(),
32182 vec![epoch_date, interval],
32183 ))))
32184 }
32185 DialectType::Hive | DialectType::Spark | DialectType::Databricks => {
32186 // DATE_ADD(CAST('1970-01-01' AS DATE), n)
32187 Ok(Expression::Function(Box::new(Function::new(
32188 "DATE_ADD".to_string(),
32189 vec![epoch_date, n],
32190 ))))
32191 }
32192 DialectType::PostgreSQL
32193 | DialectType::Materialize
32194 | DialectType::RisingWave => {
32195 // CAST('1970-01-01' AS DATE) + INTERVAL 'n DAY'
32196 let n_str = match &n {
32197 Expression::Literal(lit)
32198 if matches!(lit.as_ref(), Literal::Number(_)) =>
32199 {
32200 let Literal::Number(s) = lit.as_ref() else {
32201 unreachable!()
32202 };
32203 s.clone()
32204 }
32205 _ => Self::expr_to_string_static(&n),
32206 };
32207 let interval =
32208 Expression::Interval(Box::new(crate::expressions::Interval {
32209 this: Some(Expression::string(&format!("{} DAY", n_str))),
32210 unit: None,
32211 }));
32212 Ok(Expression::Add(Box::new(
32213 crate::expressions::BinaryOp::new(epoch_date, interval),
32214 )))
32215 }
32216 _ => {
32217 // Default: keep as-is
32218 Ok(Expression::Function(Box::new(Function::new(
32219 "DATE_FROM_UNIX_DATE".to_string(),
32220 vec![n],
32221 ))))
32222 }
32223 }
32224 } else {
32225 Ok(e)
32226 }
32227 }
32228
32229 Action::ArrayRemoveConvert => {
32230 // ARRAY_REMOVE(arr, target) -> LIST_FILTER/arrayFilter
32231 if let Expression::ArrayRemove(bf) = e {
32232 let arr = bf.this;
32233 let target_val = bf.expression;
32234 match target {
32235 DialectType::DuckDB => {
32236 let u_id = crate::expressions::Identifier::new("_u");
32237 let lambda =
32238 Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
32239 parameters: vec![u_id.clone()],
32240 body: Expression::Neq(Box::new(BinaryOp {
32241 left: Expression::Identifier(u_id),
32242 right: target_val,
32243 left_comments: Vec::new(),
32244 operator_comments: Vec::new(),
32245 trailing_comments: Vec::new(),
32246 inferred_type: None,
32247 })),
32248 colon: false,
32249 parameter_types: Vec::new(),
32250 }));
32251 Ok(Expression::Function(Box::new(Function::new(
32252 "LIST_FILTER".to_string(),
32253 vec![arr, lambda],
32254 ))))
32255 }
32256 DialectType::ClickHouse => {
32257 let u_id = crate::expressions::Identifier::new("_u");
32258 let lambda =
32259 Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
32260 parameters: vec![u_id.clone()],
32261 body: Expression::Neq(Box::new(BinaryOp {
32262 left: Expression::Identifier(u_id),
32263 right: target_val,
32264 left_comments: Vec::new(),
32265 operator_comments: Vec::new(),
32266 trailing_comments: Vec::new(),
32267 inferred_type: None,
32268 })),
32269 colon: false,
32270 parameter_types: Vec::new(),
32271 }));
32272 Ok(Expression::Function(Box::new(Function::new(
32273 "arrayFilter".to_string(),
32274 vec![lambda, arr],
32275 ))))
32276 }
32277 DialectType::BigQuery => {
32278 // ARRAY(SELECT _u FROM UNNEST(the_array) AS _u WHERE _u <> target)
32279 let u_id = crate::expressions::Identifier::new("_u");
32280 let u_col =
32281 Expression::Column(Box::new(crate::expressions::Column {
32282 name: u_id.clone(),
32283 table: None,
32284 join_mark: false,
32285 trailing_comments: Vec::new(),
32286 span: None,
32287 inferred_type: None,
32288 }));
32289 let unnest_expr =
32290 Expression::Unnest(Box::new(crate::expressions::UnnestFunc {
32291 this: arr,
32292 expressions: Vec::new(),
32293 with_ordinality: false,
32294 alias: None,
32295 offset_alias: None,
32296 }));
32297 let aliased_unnest =
32298 Expression::Alias(Box::new(crate::expressions::Alias {
32299 this: unnest_expr,
32300 alias: u_id.clone(),
32301 column_aliases: Vec::new(),
32302 alias_explicit_as: false,
32303 alias_keyword: None,
32304 pre_alias_comments: Vec::new(),
32305 trailing_comments: Vec::new(),
32306 inferred_type: None,
32307 }));
32308 let where_cond = Expression::Neq(Box::new(BinaryOp {
32309 left: u_col.clone(),
32310 right: target_val,
32311 left_comments: Vec::new(),
32312 operator_comments: Vec::new(),
32313 trailing_comments: Vec::new(),
32314 inferred_type: None,
32315 }));
32316 let subquery = Expression::Select(Box::new(
32317 crate::expressions::Select::new()
32318 .column(u_col)
32319 .from(aliased_unnest)
32320 .where_(where_cond),
32321 ));
32322 Ok(Expression::ArrayFunc(Box::new(
32323 crate::expressions::ArrayConstructor {
32324 expressions: vec![subquery],
32325 bracket_notation: false,
32326 use_list_keyword: false,
32327 },
32328 )))
32329 }
32330 _ => Ok(Expression::ArrayRemove(Box::new(
32331 crate::expressions::BinaryFunc {
32332 original_name: None,
32333 this: arr,
32334 expression: target_val,
32335 inferred_type: None,
32336 },
32337 ))),
32338 }
32339 } else {
32340 Ok(e)
32341 }
32342 }
32343
32344 Action::ArrayReverseConvert => {
32345 // ARRAY_REVERSE(x) -> arrayReverse(x) for ClickHouse
32346 if let Expression::ArrayReverse(af) = e {
32347 Ok(Expression::Function(Box::new(Function::new(
32348 "arrayReverse".to_string(),
32349 vec![af.this],
32350 ))))
32351 } else {
32352 Ok(e)
32353 }
32354 }
32355
32356 Action::JsonKeysConvert => {
32357 // JSON_KEYS(x) -> JSON_OBJECT_KEYS/OBJECT_KEYS
32358 if let Expression::JsonKeys(uf) = e {
32359 match target {
32360 DialectType::Spark | DialectType::Databricks => {
32361 Ok(Expression::Function(Box::new(Function::new(
32362 "JSON_OBJECT_KEYS".to_string(),
32363 vec![uf.this],
32364 ))))
32365 }
32366 DialectType::Snowflake => Ok(Expression::Function(Box::new(
32367 Function::new("OBJECT_KEYS".to_string(), vec![uf.this]),
32368 ))),
32369 _ => Ok(Expression::JsonKeys(uf)),
32370 }
32371 } else {
32372 Ok(e)
32373 }
32374 }
32375
32376 Action::ParseJsonStrip => {
32377 // PARSE_JSON(x) -> x (strip wrapper for SQLite/Doris)
32378 if let Expression::ParseJson(uf) = e {
32379 Ok(uf.this)
32380 } else {
32381 Ok(e)
32382 }
32383 }
32384
32385 Action::ArraySizeDrill => {
32386 // ARRAY_SIZE(x) -> REPEATED_COUNT(x) for Drill
32387 if let Expression::ArraySize(uf) = e {
32388 Ok(Expression::Function(Box::new(Function::new(
32389 "REPEATED_COUNT".to_string(),
32390 vec![uf.this],
32391 ))))
32392 } else {
32393 Ok(e)
32394 }
32395 }
32396
32397 Action::WeekOfYearToWeekIso => {
32398 // WEEKOFYEAR(x) -> WEEKISO(x) for Snowflake (cross-dialect normalization)
32399 if let Expression::WeekOfYear(uf) = e {
32400 Ok(Expression::Function(Box::new(Function::new(
32401 "WEEKISO".to_string(),
32402 vec![uf.this],
32403 ))))
32404 } else {
32405 Ok(e)
32406 }
32407 }
32408 }
32409 })
32410 }
32411
32412 /// Convert DATE_TRUNC('unit', x) to MySQL-specific expansion
32413 fn date_trunc_to_mysql(unit: &str, expr: &Expression) -> Result<Expression> {
32414 use crate::expressions::Function;
32415 match unit {
32416 "DAY" => {
32417 // DATE(x)
32418 Ok(Expression::Function(Box::new(Function::new(
32419 "DATE".to_string(),
32420 vec![expr.clone()],
32421 ))))
32422 }
32423 "WEEK" => {
32424 // STR_TO_DATE(CONCAT(YEAR(x), ' ', WEEK(x, 1), ' 1'), '%Y %u %w')
32425 let year_x = Expression::Function(Box::new(Function::new(
32426 "YEAR".to_string(),
32427 vec![expr.clone()],
32428 )));
32429 let week_x = Expression::Function(Box::new(Function::new(
32430 "WEEK".to_string(),
32431 vec![expr.clone(), Expression::number(1)],
32432 )));
32433 let concat_args = vec![
32434 year_x,
32435 Expression::string(" "),
32436 week_x,
32437 Expression::string(" 1"),
32438 ];
32439 let concat = Expression::Function(Box::new(Function::new(
32440 "CONCAT".to_string(),
32441 concat_args,
32442 )));
32443 Ok(Expression::Function(Box::new(Function::new(
32444 "STR_TO_DATE".to_string(),
32445 vec![concat, Expression::string("%Y %u %w")],
32446 ))))
32447 }
32448 "MONTH" => {
32449 // STR_TO_DATE(CONCAT(YEAR(x), ' ', MONTH(x), ' 1'), '%Y %c %e')
32450 let year_x = Expression::Function(Box::new(Function::new(
32451 "YEAR".to_string(),
32452 vec![expr.clone()],
32453 )));
32454 let month_x = Expression::Function(Box::new(Function::new(
32455 "MONTH".to_string(),
32456 vec![expr.clone()],
32457 )));
32458 let concat_args = vec![
32459 year_x,
32460 Expression::string(" "),
32461 month_x,
32462 Expression::string(" 1"),
32463 ];
32464 let concat = Expression::Function(Box::new(Function::new(
32465 "CONCAT".to_string(),
32466 concat_args,
32467 )));
32468 Ok(Expression::Function(Box::new(Function::new(
32469 "STR_TO_DATE".to_string(),
32470 vec![concat, Expression::string("%Y %c %e")],
32471 ))))
32472 }
32473 "QUARTER" => {
32474 // STR_TO_DATE(CONCAT(YEAR(x), ' ', QUARTER(x) * 3 - 2, ' 1'), '%Y %c %e')
32475 let year_x = Expression::Function(Box::new(Function::new(
32476 "YEAR".to_string(),
32477 vec![expr.clone()],
32478 )));
32479 let quarter_x = Expression::Function(Box::new(Function::new(
32480 "QUARTER".to_string(),
32481 vec![expr.clone()],
32482 )));
32483 // QUARTER(x) * 3 - 2
32484 let mul = Expression::Mul(Box::new(crate::expressions::BinaryOp {
32485 left: quarter_x,
32486 right: Expression::number(3),
32487 left_comments: Vec::new(),
32488 operator_comments: Vec::new(),
32489 trailing_comments: Vec::new(),
32490 inferred_type: None,
32491 }));
32492 let sub = Expression::Sub(Box::new(crate::expressions::BinaryOp {
32493 left: mul,
32494 right: Expression::number(2),
32495 left_comments: Vec::new(),
32496 operator_comments: Vec::new(),
32497 trailing_comments: Vec::new(),
32498 inferred_type: None,
32499 }));
32500 let concat_args = vec![
32501 year_x,
32502 Expression::string(" "),
32503 sub,
32504 Expression::string(" 1"),
32505 ];
32506 let concat = Expression::Function(Box::new(Function::new(
32507 "CONCAT".to_string(),
32508 concat_args,
32509 )));
32510 Ok(Expression::Function(Box::new(Function::new(
32511 "STR_TO_DATE".to_string(),
32512 vec![concat, Expression::string("%Y %c %e")],
32513 ))))
32514 }
32515 "YEAR" => {
32516 // STR_TO_DATE(CONCAT(YEAR(x), ' 1 1'), '%Y %c %e')
32517 let year_x = Expression::Function(Box::new(Function::new(
32518 "YEAR".to_string(),
32519 vec![expr.clone()],
32520 )));
32521 let concat_args = vec![year_x, Expression::string(" 1 1")];
32522 let concat = Expression::Function(Box::new(Function::new(
32523 "CONCAT".to_string(),
32524 concat_args,
32525 )));
32526 Ok(Expression::Function(Box::new(Function::new(
32527 "STR_TO_DATE".to_string(),
32528 vec![concat, Expression::string("%Y %c %e")],
32529 ))))
32530 }
32531 _ => {
32532 // Unsupported unit -> keep as DATE_TRUNC
32533 Ok(Expression::Function(Box::new(Function::new(
32534 "DATE_TRUNC".to_string(),
32535 vec![Expression::string(unit), expr.clone()],
32536 ))))
32537 }
32538 }
32539 }
32540
32541 /// Check if a DataType is or contains VARCHAR/CHAR (for Spark VARCHAR->STRING normalization)
32542 fn has_varchar_char_type(dt: &crate::expressions::DataType) -> bool {
32543 use crate::expressions::DataType;
32544 match dt {
32545 DataType::VarChar { .. } | DataType::Char { .. } => true,
32546 DataType::Struct { fields, .. } => fields
32547 .iter()
32548 .any(|f| Self::has_varchar_char_type(&f.data_type)),
32549 _ => false,
32550 }
32551 }
32552
32553 /// Recursively normalize VARCHAR/CHAR to STRING in a DataType (for Spark)
32554 fn normalize_varchar_to_string(
32555 dt: crate::expressions::DataType,
32556 ) -> crate::expressions::DataType {
32557 use crate::expressions::DataType;
32558 match dt {
32559 DataType::VarChar { .. } | DataType::Char { .. } => DataType::Custom {
32560 name: "STRING".to_string(),
32561 },
32562 DataType::Struct { fields, nested } => {
32563 let fields = fields
32564 .into_iter()
32565 .map(|mut f| {
32566 f.data_type = Self::normalize_varchar_to_string(f.data_type);
32567 f
32568 })
32569 .collect();
32570 DataType::Struct { fields, nested }
32571 }
32572 other => other,
32573 }
32574 }
32575
32576 /// Normalize an interval string like '1day' or ' 2 days ' to proper INTERVAL expression
32577 fn normalize_interval_string(expr: Expression, target: DialectType) -> Expression {
32578 if let Expression::Literal(ref lit) = expr {
32579 if let crate::expressions::Literal::String(ref s) = lit.as_ref() {
32580 // Try to parse patterns like '1day', '1 day', '2 days', ' 2 days '
32581 let trimmed = s.trim();
32582
32583 // Find where digits end and unit text begins
32584 let digit_end = trimmed
32585 .find(|c: char| !c.is_ascii_digit())
32586 .unwrap_or(trimmed.len());
32587 if digit_end == 0 || digit_end == trimmed.len() {
32588 return expr;
32589 }
32590 let num = &trimmed[..digit_end];
32591 let unit_text = trimmed[digit_end..].trim().to_ascii_uppercase();
32592 if unit_text.is_empty() {
32593 return expr;
32594 }
32595
32596 let known_units = [
32597 "DAY", "DAYS", "HOUR", "HOURS", "MINUTE", "MINUTES", "SECOND", "SECONDS",
32598 "WEEK", "WEEKS", "MONTH", "MONTHS", "YEAR", "YEARS",
32599 ];
32600 if !known_units.contains(&unit_text.as_str()) {
32601 return expr;
32602 }
32603
32604 let unit_str = unit_text.clone();
32605 // Singularize
32606 let unit_singular = if unit_str.ends_with('S') && unit_str.len() > 3 {
32607 &unit_str[..unit_str.len() - 1]
32608 } else {
32609 &unit_str
32610 };
32611 let unit = unit_singular;
32612
32613 match target {
32614 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
32615 // INTERVAL '2' DAY
32616 let iu = match unit {
32617 "DAY" => crate::expressions::IntervalUnit::Day,
32618 "HOUR" => crate::expressions::IntervalUnit::Hour,
32619 "MINUTE" => crate::expressions::IntervalUnit::Minute,
32620 "SECOND" => crate::expressions::IntervalUnit::Second,
32621 "WEEK" => crate::expressions::IntervalUnit::Week,
32622 "MONTH" => crate::expressions::IntervalUnit::Month,
32623 "YEAR" => crate::expressions::IntervalUnit::Year,
32624 _ => return expr,
32625 };
32626 return Expression::Interval(Box::new(crate::expressions::Interval {
32627 this: Some(Expression::string(num)),
32628 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
32629 unit: iu,
32630 use_plural: false,
32631 }),
32632 }));
32633 }
32634 DialectType::PostgreSQL | DialectType::Redshift | DialectType::DuckDB => {
32635 // INTERVAL '2 DAYS'
32636 let plural = if num != "1" && !unit_str.ends_with('S') {
32637 format!("{} {}S", num, unit)
32638 } else if unit_str.ends_with('S') {
32639 format!("{} {}", num, unit_str)
32640 } else {
32641 format!("{} {}", num, unit)
32642 };
32643 return Expression::Interval(Box::new(crate::expressions::Interval {
32644 this: Some(Expression::string(&plural)),
32645 unit: None,
32646 }));
32647 }
32648 _ => {
32649 // Spark/Databricks/Hive: INTERVAL '1' DAY
32650 let iu = match unit {
32651 "DAY" => crate::expressions::IntervalUnit::Day,
32652 "HOUR" => crate::expressions::IntervalUnit::Hour,
32653 "MINUTE" => crate::expressions::IntervalUnit::Minute,
32654 "SECOND" => crate::expressions::IntervalUnit::Second,
32655 "WEEK" => crate::expressions::IntervalUnit::Week,
32656 "MONTH" => crate::expressions::IntervalUnit::Month,
32657 "YEAR" => crate::expressions::IntervalUnit::Year,
32658 _ => return expr,
32659 };
32660 return Expression::Interval(Box::new(crate::expressions::Interval {
32661 this: Some(Expression::string(num)),
32662 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
32663 unit: iu,
32664 use_plural: false,
32665 }),
32666 }));
32667 }
32668 }
32669 }
32670 }
32671 // If it's already an INTERVAL expression, pass through
32672 expr
32673 }
32674
32675 /// Rewrite SELECT expressions containing UNNEST into expanded form with CROSS JOINs.
32676 /// DuckDB: SELECT UNNEST(arr1), UNNEST(arr2) ->
32677 /// BigQuery: SELECT IF(pos = pos_2, col, NULL) AS col, ... FROM UNNEST(GENERATE_ARRAY(0, ...)) AS pos CROSS JOIN ...
32678 /// Presto: SELECT IF(_u.pos = _u_2.pos_2, _u_2.col) AS col, ... FROM UNNEST(SEQUENCE(1, ...)) AS _u(pos) CROSS JOIN ...
32679 fn rewrite_unnest_expansion(
32680 select: &crate::expressions::Select,
32681 target: DialectType,
32682 ) -> Option<crate::expressions::Select> {
32683 use crate::expressions::{
32684 Alias, BinaryOp, Column, From, Function, Identifier, Join, JoinKind, Literal,
32685 UnnestFunc,
32686 };
32687
32688 let index_offset: i64 = match target {
32689 DialectType::Presto | DialectType::Trino => 1,
32690 _ => 0, // BigQuery, Snowflake
32691 };
32692
32693 let if_func_name = match target {
32694 DialectType::Snowflake => "IFF",
32695 _ => "IF",
32696 };
32697
32698 let array_length_func = match target {
32699 DialectType::BigQuery => "ARRAY_LENGTH",
32700 DialectType::Presto | DialectType::Trino => "CARDINALITY",
32701 DialectType::Snowflake => "ARRAY_SIZE",
32702 _ => "ARRAY_LENGTH",
32703 };
32704
32705 let use_table_aliases = matches!(
32706 target,
32707 DialectType::Presto | DialectType::Trino | DialectType::Snowflake
32708 );
32709 let null_third_arg = matches!(target, DialectType::BigQuery | DialectType::Snowflake);
32710
32711 fn make_col(name: &str, table: Option<&str>) -> Expression {
32712 if let Some(tbl) = table {
32713 Expression::boxed_column(Column {
32714 name: Identifier::new(name.to_string()),
32715 table: Some(Identifier::new(tbl.to_string())),
32716 join_mark: false,
32717 trailing_comments: Vec::new(),
32718 span: None,
32719 inferred_type: None,
32720 })
32721 } else {
32722 Expression::Identifier(Identifier::new(name.to_string()))
32723 }
32724 }
32725
32726 fn make_join(this: Expression) -> Join {
32727 Join {
32728 this,
32729 on: None,
32730 using: Vec::new(),
32731 kind: JoinKind::Cross,
32732 use_inner_keyword: false,
32733 use_outer_keyword: false,
32734 deferred_condition: false,
32735 join_hint: None,
32736 match_condition: None,
32737 pivots: Vec::new(),
32738 comments: Vec::new(),
32739 nesting_group: 0,
32740 directed: false,
32741 }
32742 }
32743
32744 // Collect UNNEST info from SELECT expressions
32745 struct UnnestInfo {
32746 arr_expr: Expression,
32747 col_alias: String,
32748 pos_alias: String,
32749 source_alias: String,
32750 original_expr: Expression,
32751 has_outer_alias: Option<String>,
32752 }
32753
32754 let mut unnest_infos: Vec<UnnestInfo> = Vec::new();
32755 let mut col_counter = 0usize;
32756 let mut pos_counter = 1usize;
32757 let mut source_counter = 1usize;
32758
32759 fn extract_unnest_arg(expr: &Expression) -> Option<Expression> {
32760 match expr {
32761 Expression::Unnest(u) => Some(u.this.clone()),
32762 Expression::Function(f)
32763 if f.name.eq_ignore_ascii_case("UNNEST") && !f.args.is_empty() =>
32764 {
32765 Some(f.args[0].clone())
32766 }
32767 Expression::Alias(a) => extract_unnest_arg(&a.this),
32768 Expression::Add(op)
32769 | Expression::Sub(op)
32770 | Expression::Mul(op)
32771 | Expression::Div(op) => {
32772 extract_unnest_arg(&op.left).or_else(|| extract_unnest_arg(&op.right))
32773 }
32774 _ => None,
32775 }
32776 }
32777
32778 fn get_alias_name(expr: &Expression) -> Option<String> {
32779 if let Expression::Alias(a) = expr {
32780 Some(a.alias.name.clone())
32781 } else {
32782 None
32783 }
32784 }
32785
32786 for sel_expr in &select.expressions {
32787 if let Some(arr) = extract_unnest_arg(sel_expr) {
32788 col_counter += 1;
32789 pos_counter += 1;
32790 source_counter += 1;
32791
32792 let col_alias = if col_counter == 1 {
32793 "col".to_string()
32794 } else {
32795 format!("col_{}", col_counter)
32796 };
32797 let pos_alias = format!("pos_{}", pos_counter);
32798 let source_alias = format!("_u_{}", source_counter);
32799 let has_outer_alias = get_alias_name(sel_expr);
32800
32801 unnest_infos.push(UnnestInfo {
32802 arr_expr: arr,
32803 col_alias,
32804 pos_alias,
32805 source_alias,
32806 original_expr: sel_expr.clone(),
32807 has_outer_alias,
32808 });
32809 }
32810 }
32811
32812 if unnest_infos.is_empty() {
32813 return None;
32814 }
32815
32816 let series_alias = "pos".to_string();
32817 let series_source_alias = "_u".to_string();
32818 let tbl_ref = if use_table_aliases {
32819 Some(series_source_alias.as_str())
32820 } else {
32821 None
32822 };
32823
32824 // Build new SELECT expressions
32825 let mut new_select_exprs = Vec::new();
32826 for info in &unnest_infos {
32827 let actual_col_name = info.has_outer_alias.as_ref().unwrap_or(&info.col_alias);
32828 let src_ref = if use_table_aliases {
32829 Some(info.source_alias.as_str())
32830 } else {
32831 None
32832 };
32833
32834 let pos_col = make_col(&series_alias, tbl_ref);
32835 let unnest_pos_col = make_col(&info.pos_alias, src_ref);
32836 let col_ref = make_col(actual_col_name, src_ref);
32837
32838 let eq_cond = Expression::Eq(Box::new(BinaryOp::new(
32839 pos_col.clone(),
32840 unnest_pos_col.clone(),
32841 )));
32842 let mut if_args = vec![eq_cond, col_ref];
32843 if null_third_arg {
32844 if_args.push(Expression::Null(crate::expressions::Null));
32845 }
32846
32847 let if_expr =
32848 Expression::Function(Box::new(Function::new(if_func_name.to_string(), if_args)));
32849 let final_expr = Self::replace_unnest_with_if(&info.original_expr, &if_expr);
32850
32851 new_select_exprs.push(Expression::Alias(Box::new(Alias::new(
32852 final_expr,
32853 Identifier::new(actual_col_name.clone()),
32854 ))));
32855 }
32856
32857 // Build array size expressions for GREATEST
32858 let size_exprs: Vec<Expression> = unnest_infos
32859 .iter()
32860 .map(|info| {
32861 Expression::Function(Box::new(Function::new(
32862 array_length_func.to_string(),
32863 vec![info.arr_expr.clone()],
32864 )))
32865 })
32866 .collect();
32867
32868 let greatest =
32869 Expression::Function(Box::new(Function::new("GREATEST".to_string(), size_exprs)));
32870
32871 let series_end = if index_offset == 0 {
32872 Expression::Sub(Box::new(BinaryOp::new(
32873 greatest,
32874 Expression::Literal(Box::new(Literal::Number("1".to_string()))),
32875 )))
32876 } else {
32877 greatest
32878 };
32879
32880 // Build the position array source
32881 let series_unnest_expr = match target {
32882 DialectType::BigQuery => {
32883 let gen_array = Expression::Function(Box::new(Function::new(
32884 "GENERATE_ARRAY".to_string(),
32885 vec![
32886 Expression::Literal(Box::new(Literal::Number("0".to_string()))),
32887 series_end,
32888 ],
32889 )));
32890 Expression::Unnest(Box::new(UnnestFunc {
32891 this: gen_array,
32892 expressions: Vec::new(),
32893 with_ordinality: false,
32894 alias: None,
32895 offset_alias: None,
32896 }))
32897 }
32898 DialectType::Presto | DialectType::Trino => {
32899 let sequence = Expression::Function(Box::new(Function::new(
32900 "SEQUENCE".to_string(),
32901 vec![
32902 Expression::Literal(Box::new(Literal::Number("1".to_string()))),
32903 series_end,
32904 ],
32905 )));
32906 Expression::Unnest(Box::new(UnnestFunc {
32907 this: sequence,
32908 expressions: Vec::new(),
32909 with_ordinality: false,
32910 alias: None,
32911 offset_alias: None,
32912 }))
32913 }
32914 DialectType::Snowflake => {
32915 let range_end = Expression::Add(Box::new(BinaryOp::new(
32916 Expression::Paren(Box::new(crate::expressions::Paren {
32917 this: series_end,
32918 trailing_comments: Vec::new(),
32919 })),
32920 Expression::Literal(Box::new(Literal::Number("1".to_string()))),
32921 )));
32922 let gen_range = Expression::Function(Box::new(Function::new(
32923 "ARRAY_GENERATE_RANGE".to_string(),
32924 vec![
32925 Expression::Literal(Box::new(Literal::Number("0".to_string()))),
32926 range_end,
32927 ],
32928 )));
32929 let flatten_arg =
32930 Expression::NamedArgument(Box::new(crate::expressions::NamedArgument {
32931 name: Identifier::new("INPUT".to_string()),
32932 value: gen_range,
32933 separator: crate::expressions::NamedArgSeparator::DArrow,
32934 }));
32935 let flatten = Expression::Function(Box::new(Function::new(
32936 "FLATTEN".to_string(),
32937 vec![flatten_arg],
32938 )));
32939 Expression::Function(Box::new(Function::new("TABLE".to_string(), vec![flatten])))
32940 }
32941 _ => return None,
32942 };
32943
32944 // Build series alias expression
32945 let series_alias_expr = if use_table_aliases {
32946 let col_aliases = if matches!(target, DialectType::Snowflake) {
32947 vec![
32948 Identifier::new("seq".to_string()),
32949 Identifier::new("key".to_string()),
32950 Identifier::new("path".to_string()),
32951 Identifier::new("index".to_string()),
32952 Identifier::new(series_alias.clone()),
32953 Identifier::new("this".to_string()),
32954 ]
32955 } else {
32956 vec![Identifier::new(series_alias.clone())]
32957 };
32958 Expression::Alias(Box::new(Alias {
32959 this: series_unnest_expr,
32960 alias: Identifier::new(series_source_alias.clone()),
32961 column_aliases: col_aliases,
32962 alias_explicit_as: false,
32963 alias_keyword: None,
32964 pre_alias_comments: Vec::new(),
32965 trailing_comments: Vec::new(),
32966 inferred_type: None,
32967 }))
32968 } else {
32969 Expression::Alias(Box::new(Alias::new(
32970 series_unnest_expr,
32971 Identifier::new(series_alias.clone()),
32972 )))
32973 };
32974
32975 // Build CROSS JOINs for each UNNEST
32976 let mut joins = Vec::new();
32977 for info in &unnest_infos {
32978 let actual_col_name = info.has_outer_alias.as_ref().unwrap_or(&info.col_alias);
32979
32980 let unnest_join_expr = match target {
32981 DialectType::BigQuery => {
32982 // UNNEST([1,2,3]) AS col WITH OFFSET AS pos_2
32983 let unnest = UnnestFunc {
32984 this: info.arr_expr.clone(),
32985 expressions: Vec::new(),
32986 with_ordinality: true,
32987 alias: Some(Identifier::new(actual_col_name.clone())),
32988 offset_alias: Some(Identifier::new(info.pos_alias.clone())),
32989 };
32990 Expression::Unnest(Box::new(unnest))
32991 }
32992 DialectType::Presto | DialectType::Trino => {
32993 let unnest = UnnestFunc {
32994 this: info.arr_expr.clone(),
32995 expressions: Vec::new(),
32996 with_ordinality: true,
32997 alias: None,
32998 offset_alias: None,
32999 };
33000 Expression::Alias(Box::new(Alias {
33001 this: Expression::Unnest(Box::new(unnest)),
33002 alias: Identifier::new(info.source_alias.clone()),
33003 column_aliases: vec![
33004 Identifier::new(actual_col_name.clone()),
33005 Identifier::new(info.pos_alias.clone()),
33006 ],
33007 alias_explicit_as: false,
33008 alias_keyword: None,
33009 pre_alias_comments: Vec::new(),
33010 trailing_comments: Vec::new(),
33011 inferred_type: None,
33012 }))
33013 }
33014 DialectType::Snowflake => {
33015 let flatten_arg =
33016 Expression::NamedArgument(Box::new(crate::expressions::NamedArgument {
33017 name: Identifier::new("INPUT".to_string()),
33018 value: info.arr_expr.clone(),
33019 separator: crate::expressions::NamedArgSeparator::DArrow,
33020 }));
33021 let flatten = Expression::Function(Box::new(Function::new(
33022 "FLATTEN".to_string(),
33023 vec![flatten_arg],
33024 )));
33025 let table_fn = Expression::Function(Box::new(Function::new(
33026 "TABLE".to_string(),
33027 vec![flatten],
33028 )));
33029 Expression::Alias(Box::new(Alias {
33030 this: table_fn,
33031 alias: Identifier::new(info.source_alias.clone()),
33032 column_aliases: vec![
33033 Identifier::new("seq".to_string()),
33034 Identifier::new("key".to_string()),
33035 Identifier::new("path".to_string()),
33036 Identifier::new(info.pos_alias.clone()),
33037 Identifier::new(actual_col_name.clone()),
33038 Identifier::new("this".to_string()),
33039 ],
33040 alias_explicit_as: false,
33041 alias_keyword: None,
33042 pre_alias_comments: Vec::new(),
33043 trailing_comments: Vec::new(),
33044 inferred_type: None,
33045 }))
33046 }
33047 _ => return None,
33048 };
33049
33050 joins.push(make_join(unnest_join_expr));
33051 }
33052
33053 // Build WHERE clause
33054 let mut where_conditions: Vec<Expression> = Vec::new();
33055 for info in &unnest_infos {
33056 let src_ref = if use_table_aliases {
33057 Some(info.source_alias.as_str())
33058 } else {
33059 None
33060 };
33061 let pos_col = make_col(&series_alias, tbl_ref);
33062 let unnest_pos_col = make_col(&info.pos_alias, src_ref);
33063
33064 let arr_size = Expression::Function(Box::new(Function::new(
33065 array_length_func.to_string(),
33066 vec![info.arr_expr.clone()],
33067 )));
33068
33069 let size_ref = if index_offset == 0 {
33070 Expression::Paren(Box::new(crate::expressions::Paren {
33071 this: Expression::Sub(Box::new(BinaryOp::new(
33072 arr_size,
33073 Expression::Literal(Box::new(Literal::Number("1".to_string()))),
33074 ))),
33075 trailing_comments: Vec::new(),
33076 }))
33077 } else {
33078 arr_size
33079 };
33080
33081 let eq = Expression::Eq(Box::new(BinaryOp::new(
33082 pos_col.clone(),
33083 unnest_pos_col.clone(),
33084 )));
33085 let gt = Expression::Gt(Box::new(BinaryOp::new(pos_col, size_ref.clone())));
33086 let pos_eq_size = Expression::Eq(Box::new(BinaryOp::new(unnest_pos_col, size_ref)));
33087 let and_cond = Expression::And(Box::new(BinaryOp::new(gt, pos_eq_size)));
33088 let paren_and = Expression::Paren(Box::new(crate::expressions::Paren {
33089 this: and_cond,
33090 trailing_comments: Vec::new(),
33091 }));
33092 let or_cond = Expression::Or(Box::new(BinaryOp::new(eq, paren_and)));
33093
33094 where_conditions.push(or_cond);
33095 }
33096
33097 let where_expr = if where_conditions.len() == 1 {
33098 // Single condition: no parens needed
33099 where_conditions.into_iter().next().unwrap()
33100 } else {
33101 // Multiple conditions: wrap each OR in parens, then combine with AND
33102 let wrap = |e: Expression| {
33103 Expression::Paren(Box::new(crate::expressions::Paren {
33104 this: e,
33105 trailing_comments: Vec::new(),
33106 }))
33107 };
33108 let mut iter = where_conditions.into_iter();
33109 let first = wrap(iter.next().unwrap());
33110 let second = wrap(iter.next().unwrap());
33111 let mut combined = Expression::Paren(Box::new(crate::expressions::Paren {
33112 this: Expression::And(Box::new(BinaryOp::new(first, second))),
33113 trailing_comments: Vec::new(),
33114 }));
33115 for cond in iter {
33116 combined = Expression::And(Box::new(BinaryOp::new(combined, wrap(cond))));
33117 }
33118 combined
33119 };
33120
33121 // Build the new SELECT
33122 let mut new_select = select.clone();
33123 new_select.expressions = new_select_exprs;
33124
33125 if new_select.from.is_some() {
33126 let mut all_joins = vec![make_join(series_alias_expr)];
33127 all_joins.extend(joins);
33128 new_select.joins.extend(all_joins);
33129 } else {
33130 new_select.from = Some(From {
33131 expressions: vec![series_alias_expr],
33132 });
33133 new_select.joins.extend(joins);
33134 }
33135
33136 if let Some(ref existing_where) = new_select.where_clause {
33137 let combined = Expression::And(Box::new(BinaryOp::new(
33138 existing_where.this.clone(),
33139 where_expr,
33140 )));
33141 new_select.where_clause = Some(crate::expressions::Where { this: combined });
33142 } else {
33143 new_select.where_clause = Some(crate::expressions::Where { this: where_expr });
33144 }
33145
33146 Some(new_select)
33147 }
33148
33149 /// Helper to replace UNNEST(...) inside an expression with a replacement expression.
33150 fn replace_unnest_with_if(original: &Expression, replacement: &Expression) -> Expression {
33151 match original {
33152 Expression::Unnest(_) => replacement.clone(),
33153 Expression::Function(f) if f.name.eq_ignore_ascii_case("UNNEST") => replacement.clone(),
33154 Expression::Alias(a) => Self::replace_unnest_with_if(&a.this, replacement),
33155 Expression::Add(op) => {
33156 let left = Self::replace_unnest_with_if(&op.left, replacement);
33157 let right = Self::replace_unnest_with_if(&op.right, replacement);
33158 Expression::Add(Box::new(crate::expressions::BinaryOp::new(left, right)))
33159 }
33160 Expression::Sub(op) => {
33161 let left = Self::replace_unnest_with_if(&op.left, replacement);
33162 let right = Self::replace_unnest_with_if(&op.right, replacement);
33163 Expression::Sub(Box::new(crate::expressions::BinaryOp::new(left, right)))
33164 }
33165 Expression::Mul(op) => {
33166 let left = Self::replace_unnest_with_if(&op.left, replacement);
33167 let right = Self::replace_unnest_with_if(&op.right, replacement);
33168 Expression::Mul(Box::new(crate::expressions::BinaryOp::new(left, right)))
33169 }
33170 Expression::Div(op) => {
33171 let left = Self::replace_unnest_with_if(&op.left, replacement);
33172 let right = Self::replace_unnest_with_if(&op.right, replacement);
33173 Expression::Div(Box::new(crate::expressions::BinaryOp::new(left, right)))
33174 }
33175 _ => original.clone(),
33176 }
33177 }
33178
33179 /// Decompose a JSON path like `$.y[0].z` into individual parts: `["y", "0", "z"]`.
33180 /// Strips `$` prefix, handles bracket notation, quoted strings, and removes `[*]` wildcards.
33181 fn decompose_json_path(path: &str) -> Vec<String> {
33182 let mut parts = Vec::new();
33183 let path = if path.starts_with("$.") {
33184 &path[2..]
33185 } else if path.starts_with('$') {
33186 &path[1..]
33187 } else {
33188 path
33189 };
33190 if path.is_empty() {
33191 return parts;
33192 }
33193 let mut current = String::new();
33194 let chars: Vec<char> = path.chars().collect();
33195 let mut i = 0;
33196 while i < chars.len() {
33197 match chars[i] {
33198 '.' => {
33199 if !current.is_empty() {
33200 parts.push(current.clone());
33201 current.clear();
33202 }
33203 i += 1;
33204 }
33205 '[' => {
33206 if !current.is_empty() {
33207 parts.push(current.clone());
33208 current.clear();
33209 }
33210 i += 1;
33211 let mut bracket_content = String::new();
33212 while i < chars.len() && chars[i] != ']' {
33213 if chars[i] == '"' || chars[i] == '\'' {
33214 let quote = chars[i];
33215 i += 1;
33216 while i < chars.len() && chars[i] != quote {
33217 bracket_content.push(chars[i]);
33218 i += 1;
33219 }
33220 if i < chars.len() {
33221 i += 1;
33222 }
33223 } else {
33224 bracket_content.push(chars[i]);
33225 i += 1;
33226 }
33227 }
33228 if i < chars.len() {
33229 i += 1;
33230 }
33231 if bracket_content != "*" {
33232 parts.push(bracket_content);
33233 }
33234 }
33235 _ => {
33236 current.push(chars[i]);
33237 i += 1;
33238 }
33239 }
33240 }
33241 if !current.is_empty() {
33242 parts.push(current);
33243 }
33244 parts
33245 }
33246
33247 /// Strip `$` prefix from a JSON path, keeping the rest.
33248 /// `$.y[0].z` -> `y[0].z`, `$["a b"]` -> `["a b"]`
33249 fn strip_json_dollar_prefix(path: &str) -> String {
33250 if path.starts_with("$.") {
33251 path[2..].to_string()
33252 } else if path.starts_with('$') {
33253 path[1..].to_string()
33254 } else {
33255 path.to_string()
33256 }
33257 }
33258
33259 /// Strip `[*]` wildcards from a JSON path.
33260 /// `$.y[*]` -> `$.y`, `$.y[*].z` -> `$.y.z`
33261 fn strip_json_wildcards(path: &str) -> String {
33262 path.replace("[*]", "")
33263 .replace("..", ".") // Clean double dots from `$.y[*].z` -> `$.y..z`
33264 .trim_end_matches('.')
33265 .to_string()
33266 }
33267
33268 /// Convert bracket notation to dot notation for JSON paths.
33269 /// `$["a b"]` -> `$."a b"`, `$["key"]` -> `$.key`
33270 fn bracket_to_dot_notation(path: &str) -> String {
33271 let mut result = String::new();
33272 let chars: Vec<char> = path.chars().collect();
33273 let mut i = 0;
33274 while i < chars.len() {
33275 if chars[i] == '[' {
33276 // Read bracket content
33277 i += 1;
33278 let mut bracket_content = String::new();
33279 let mut is_quoted = false;
33280 let mut _quote_char = '"';
33281 while i < chars.len() && chars[i] != ']' {
33282 if chars[i] == '"' || chars[i] == '\'' {
33283 is_quoted = true;
33284 _quote_char = chars[i];
33285 i += 1;
33286 while i < chars.len() && chars[i] != _quote_char {
33287 bracket_content.push(chars[i]);
33288 i += 1;
33289 }
33290 if i < chars.len() {
33291 i += 1;
33292 }
33293 } else {
33294 bracket_content.push(chars[i]);
33295 i += 1;
33296 }
33297 }
33298 if i < chars.len() {
33299 i += 1;
33300 } // skip ]
33301 if bracket_content == "*" {
33302 // Keep wildcard as-is
33303 result.push_str("[*]");
33304 } else if is_quoted {
33305 // Quoted bracket -> dot notation with quotes
33306 result.push('.');
33307 result.push('"');
33308 result.push_str(&bracket_content);
33309 result.push('"');
33310 } else {
33311 // Numeric index -> keep as bracket
33312 result.push('[');
33313 result.push_str(&bracket_content);
33314 result.push(']');
33315 }
33316 } else {
33317 result.push(chars[i]);
33318 i += 1;
33319 }
33320 }
33321 result
33322 }
33323
33324 /// Convert JSON path bracket quoted strings to use single quotes instead of double quotes.
33325 /// `$["a b"]` -> `$['a b']`
33326 fn bracket_to_single_quotes(path: &str) -> String {
33327 let mut result = String::new();
33328 let chars: Vec<char> = path.chars().collect();
33329 let mut i = 0;
33330 while i < chars.len() {
33331 if chars[i] == '[' && i + 1 < chars.len() && chars[i + 1] == '"' {
33332 result.push('[');
33333 result.push('\'');
33334 i += 2; // skip [ and "
33335 while i < chars.len() && chars[i] != '"' {
33336 result.push(chars[i]);
33337 i += 1;
33338 }
33339 if i < chars.len() {
33340 i += 1;
33341 } // skip closing "
33342 result.push('\'');
33343 } else {
33344 result.push(chars[i]);
33345 i += 1;
33346 }
33347 }
33348 result
33349 }
33350
33351 /// Transform TSQL SELECT INTO -> CREATE TABLE AS for DuckDB/Snowflake
33352 /// or PostgreSQL #temp -> TEMPORARY.
33353 /// Also strips # from INSERT INTO #table for non-TSQL targets.
33354 fn transform_select_into(
33355 expr: Expression,
33356 _source: DialectType,
33357 target: DialectType,
33358 ) -> Expression {
33359 use crate::expressions::{CreateTable, Expression, TableRef};
33360
33361 // Handle INSERT INTO #temp -> INSERT INTO temp for non-TSQL targets
33362 if let Expression::Insert(ref insert) = expr {
33363 if insert.table.name.name.starts_with('#')
33364 && !matches!(target, DialectType::TSQL | DialectType::Fabric)
33365 {
33366 let mut new_insert = insert.clone();
33367 new_insert.table.name.name =
33368 insert.table.name.name.trim_start_matches('#').to_string();
33369 return Expression::Insert(new_insert);
33370 }
33371 return expr;
33372 }
33373
33374 if let Expression::Select(ref select) = expr {
33375 if let Some(ref into) = select.into {
33376 let table_name_raw = match &into.this {
33377 Expression::Table(tr) => tr.name.name.clone(),
33378 Expression::Identifier(id) => id.name.clone(),
33379 _ => String::new(),
33380 };
33381 let is_temp = table_name_raw.starts_with('#') || into.temporary;
33382 let clean_name = table_name_raw.trim_start_matches('#').to_string();
33383
33384 match target {
33385 DialectType::DuckDB | DialectType::Snowflake => {
33386 // SELECT INTO -> CREATE TABLE AS SELECT
33387 let mut new_select = select.clone();
33388 new_select.into = None;
33389 let ct = CreateTable {
33390 name: TableRef::new(clean_name),
33391 on_cluster: None,
33392 columns: Vec::new(),
33393 constraints: Vec::new(),
33394 if_not_exists: false,
33395 temporary: is_temp,
33396 or_replace: false,
33397 table_modifier: None,
33398 as_select: Some(Expression::Select(new_select)),
33399 as_select_parenthesized: false,
33400 on_commit: None,
33401 clone_source: None,
33402 clone_at_clause: None,
33403 shallow_clone: false,
33404 deep_clone: false,
33405 is_copy: false,
33406 leading_comments: Vec::new(),
33407 with_properties: Vec::new(),
33408 teradata_post_name_options: Vec::new(),
33409 with_data: None,
33410 with_statistics: None,
33411 teradata_indexes: Vec::new(),
33412 with_cte: None,
33413 properties: Vec::new(),
33414 partition_of: None,
33415 post_table_properties: Vec::new(),
33416 mysql_table_options: Vec::new(),
33417 inherits: Vec::new(),
33418 on_property: None,
33419 copy_grants: false,
33420 using_template: None,
33421 rollup: None,
33422 uuid: None,
33423 with_partition_columns: Vec::new(),
33424 with_connection: None,
33425 };
33426 return Expression::CreateTable(Box::new(ct));
33427 }
33428 DialectType::PostgreSQL | DialectType::Redshift => {
33429 // PostgreSQL: #foo -> INTO TEMPORARY foo
33430 if is_temp && !into.temporary {
33431 let mut new_select = select.clone();
33432 let mut new_into = into.clone();
33433 new_into.temporary = true;
33434 new_into.unlogged = false;
33435 new_into.this = Expression::Table(Box::new(TableRef::new(clean_name)));
33436 new_select.into = Some(new_into);
33437 Expression::Select(new_select)
33438 } else {
33439 expr
33440 }
33441 }
33442 _ => expr,
33443 }
33444 } else {
33445 expr
33446 }
33447 } else {
33448 expr
33449 }
33450 }
33451
33452 /// Transform CREATE TABLE WITH properties for cross-dialect transpilation.
33453 /// Handles FORMAT, PARTITIONED_BY, and other Presto WITH properties.
33454 fn transform_create_table_properties(
33455 ct: &mut crate::expressions::CreateTable,
33456 _source: DialectType,
33457 target: DialectType,
33458 ) {
33459 use crate::expressions::{
33460 BinaryOp, BooleanLiteral, Expression, FileFormatProperty, Identifier, Literal,
33461 Properties,
33462 };
33463
33464 // Helper to convert a raw property value string to the correct Expression
33465 let value_to_expr = |v: &str| -> Expression {
33466 let trimmed = v.trim();
33467 // Check if it's a quoted string (starts and ends with ')
33468 if trimmed.starts_with('\'') && trimmed.ends_with('\'') {
33469 Expression::Literal(Box::new(Literal::String(
33470 trimmed[1..trimmed.len() - 1].to_string(),
33471 )))
33472 }
33473 // Check if it's a number
33474 else if trimmed.parse::<i64>().is_ok() || trimmed.parse::<f64>().is_ok() {
33475 Expression::Literal(Box::new(Literal::Number(trimmed.to_string())))
33476 }
33477 // Check if it's ARRAY[...] or ARRAY(...)
33478 else if trimmed.len() >= 5 && trimmed[..5].eq_ignore_ascii_case("ARRAY") {
33479 // Convert ARRAY['y'] to ARRAY('y') for Hive/Spark
33480 let inner = trimmed
33481 .trim_start_matches(|c: char| c.is_alphabetic()) // Remove ARRAY
33482 .trim_start_matches('[')
33483 .trim_start_matches('(')
33484 .trim_end_matches(']')
33485 .trim_end_matches(')');
33486 let elements: Vec<Expression> = inner
33487 .split(',')
33488 .map(|e| {
33489 let elem = e.trim().trim_matches('\'');
33490 Expression::Literal(Box::new(Literal::String(elem.to_string())))
33491 })
33492 .collect();
33493 Expression::Function(Box::new(crate::expressions::Function::new(
33494 "ARRAY".to_string(),
33495 elements,
33496 )))
33497 }
33498 // Otherwise, just output as identifier (unquoted)
33499 else {
33500 Expression::Identifier(Identifier::new(trimmed.to_string()))
33501 }
33502 };
33503
33504 if ct.with_properties.is_empty() && ct.properties.is_empty() {
33505 return;
33506 }
33507
33508 // Handle Presto-style WITH properties
33509 if !ct.with_properties.is_empty() {
33510 // Extract FORMAT property and remaining properties
33511 let mut format_value: Option<String> = None;
33512 let mut partitioned_by: Option<String> = None;
33513 let mut other_props: Vec<(String, String)> = Vec::new();
33514
33515 for (key, value) in ct.with_properties.drain(..) {
33516 if key.eq_ignore_ascii_case("FORMAT") {
33517 // Strip surrounding quotes from value if present
33518 format_value = Some(value.trim_matches('\'').to_string());
33519 } else if key.eq_ignore_ascii_case("PARTITIONED_BY") {
33520 partitioned_by = Some(value);
33521 } else {
33522 other_props.push((key, value));
33523 }
33524 }
33525
33526 match target {
33527 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
33528 // Presto: keep WITH properties but lowercase 'format' key
33529 if let Some(fmt) = format_value {
33530 ct.with_properties
33531 .push(("format".to_string(), format!("'{}'", fmt)));
33532 }
33533 if let Some(part) = partitioned_by {
33534 // Convert (col1, col2) to ARRAY['col1', 'col2'] format
33535 let trimmed = part.trim();
33536 let inner = trimmed.trim_start_matches('(').trim_end_matches(')');
33537 // Also handle ARRAY['...'] format - keep as-is
33538 if trimmed.len() >= 5 && trimmed[..5].eq_ignore_ascii_case("ARRAY") {
33539 ct.with_properties
33540 .push(("PARTITIONED_BY".to_string(), part));
33541 } else {
33542 // Parse column names from the parenthesized list
33543 let cols: Vec<&str> = inner
33544 .split(',')
33545 .map(|c| c.trim().trim_matches('"').trim_matches('\''))
33546 .collect();
33547 let array_val = format!(
33548 "ARRAY[{}]",
33549 cols.iter()
33550 .map(|c| format!("'{}'", c))
33551 .collect::<Vec<_>>()
33552 .join(", ")
33553 );
33554 ct.with_properties
33555 .push(("PARTITIONED_BY".to_string(), array_val));
33556 }
33557 }
33558 ct.with_properties.extend(other_props);
33559 }
33560 DialectType::Hive => {
33561 // Hive: FORMAT -> STORED AS, other props -> TBLPROPERTIES
33562 if let Some(fmt) = format_value {
33563 ct.properties.push(Expression::FileFormatProperty(Box::new(
33564 FileFormatProperty {
33565 this: Some(Box::new(Expression::Identifier(Identifier::new(fmt)))),
33566 expressions: vec![],
33567 hive_format: Some(Box::new(Expression::Boolean(BooleanLiteral {
33568 value: true,
33569 }))),
33570 },
33571 )));
33572 }
33573 if let Some(_part) = partitioned_by {
33574 // PARTITIONED_BY handling is complex - move columns to partitioned by
33575 // For now, the partition columns are extracted from the column list
33576 Self::apply_partitioned_by(ct, &_part, target);
33577 }
33578 if !other_props.is_empty() {
33579 let eq_exprs: Vec<Expression> = other_props
33580 .into_iter()
33581 .map(|(k, v)| {
33582 Expression::Eq(Box::new(BinaryOp::new(
33583 Expression::Literal(Box::new(Literal::String(k))),
33584 value_to_expr(&v),
33585 )))
33586 })
33587 .collect();
33588 ct.properties
33589 .push(Expression::Properties(Box::new(Properties {
33590 expressions: eq_exprs,
33591 })));
33592 }
33593 }
33594 DialectType::Spark | DialectType::Databricks => {
33595 // Spark: FORMAT -> USING, other props -> TBLPROPERTIES
33596 if let Some(fmt) = format_value {
33597 ct.properties.push(Expression::FileFormatProperty(Box::new(
33598 FileFormatProperty {
33599 this: Some(Box::new(Expression::Identifier(Identifier::new(fmt)))),
33600 expressions: vec![],
33601 hive_format: None, // None means USING syntax
33602 },
33603 )));
33604 }
33605 if let Some(_part) = partitioned_by {
33606 Self::apply_partitioned_by(ct, &_part, target);
33607 }
33608 if !other_props.is_empty() {
33609 let eq_exprs: Vec<Expression> = other_props
33610 .into_iter()
33611 .map(|(k, v)| {
33612 Expression::Eq(Box::new(BinaryOp::new(
33613 Expression::Literal(Box::new(Literal::String(k))),
33614 value_to_expr(&v),
33615 )))
33616 })
33617 .collect();
33618 ct.properties
33619 .push(Expression::Properties(Box::new(Properties {
33620 expressions: eq_exprs,
33621 })));
33622 }
33623 }
33624 DialectType::DuckDB => {
33625 // DuckDB: strip all WITH properties (FORMAT, PARTITIONED_BY, etc.)
33626 // Keep nothing
33627 }
33628 _ => {
33629 // For other dialects, keep WITH properties as-is
33630 if let Some(fmt) = format_value {
33631 ct.with_properties
33632 .push(("FORMAT".to_string(), format!("'{}'", fmt)));
33633 }
33634 if let Some(part) = partitioned_by {
33635 ct.with_properties
33636 .push(("PARTITIONED_BY".to_string(), part));
33637 }
33638 ct.with_properties.extend(other_props);
33639 }
33640 }
33641 }
33642
33643 // Handle STORED AS 'PARQUET' (quoted format name) -> STORED AS PARQUET (unquoted)
33644 // and Hive STORED AS -> Presto WITH (format=...) conversion
33645 if !ct.properties.is_empty() {
33646 let is_presto_target = matches!(
33647 target,
33648 DialectType::Presto | DialectType::Trino | DialectType::Athena
33649 );
33650 let is_duckdb_target = matches!(target, DialectType::DuckDB);
33651
33652 if is_presto_target || is_duckdb_target {
33653 let mut new_properties = Vec::new();
33654 for prop in ct.properties.drain(..) {
33655 match &prop {
33656 Expression::FileFormatProperty(ffp) => {
33657 if is_presto_target {
33658 // Convert STORED AS/USING to WITH (format=...)
33659 if let Some(ref fmt_expr) = ffp.this {
33660 let fmt_str = match fmt_expr.as_ref() {
33661 Expression::Identifier(id) => id.name.clone(),
33662 Expression::Literal(lit)
33663 if matches!(lit.as_ref(), Literal::String(_)) =>
33664 {
33665 let Literal::String(s) = lit.as_ref() else {
33666 unreachable!()
33667 };
33668 s.clone()
33669 }
33670 _ => {
33671 new_properties.push(prop);
33672 continue;
33673 }
33674 };
33675 ct.with_properties
33676 .push(("format".to_string(), format!("'{}'", fmt_str)));
33677 }
33678 }
33679 // DuckDB: just strip file format properties
33680 }
33681 // Convert TBLPROPERTIES to WITH properties for Presto target
33682 Expression::Properties(props) if is_presto_target => {
33683 for expr in &props.expressions {
33684 if let Expression::Eq(eq) = expr {
33685 // Extract key and value from the Eq expression
33686 let key = match &eq.left {
33687 Expression::Literal(lit)
33688 if matches!(lit.as_ref(), Literal::String(_)) =>
33689 {
33690 let Literal::String(s) = lit.as_ref() else {
33691 unreachable!()
33692 };
33693 s.clone()
33694 }
33695 Expression::Identifier(id) => id.name.clone(),
33696 _ => continue,
33697 };
33698 let value = match &eq.right {
33699 Expression::Literal(lit)
33700 if matches!(lit.as_ref(), Literal::String(_)) =>
33701 {
33702 let Literal::String(s) = lit.as_ref() else {
33703 unreachable!()
33704 };
33705 format!("'{}'", s)
33706 }
33707 Expression::Literal(lit)
33708 if matches!(lit.as_ref(), Literal::Number(_)) =>
33709 {
33710 let Literal::Number(n) = lit.as_ref() else {
33711 unreachable!()
33712 };
33713 n.clone()
33714 }
33715 Expression::Identifier(id) => id.name.clone(),
33716 _ => continue,
33717 };
33718 ct.with_properties.push((key, value));
33719 }
33720 }
33721 }
33722 // Convert PartitionedByProperty for Presto target
33723 Expression::PartitionedByProperty(ref pbp) if is_presto_target => {
33724 // Check if it contains ColumnDef expressions (Hive-style with types)
33725 if let Expression::Tuple(ref tuple) = *pbp.this {
33726 let mut col_names: Vec<String> = Vec::new();
33727 let mut col_defs: Vec<crate::expressions::ColumnDef> = Vec::new();
33728 let mut has_col_defs = false;
33729 for expr in &tuple.expressions {
33730 if let Expression::ColumnDef(ref cd) = expr {
33731 has_col_defs = true;
33732 col_names.push(cd.name.name.clone());
33733 col_defs.push(*cd.clone());
33734 } else if let Expression::Column(ref col) = expr {
33735 col_names.push(col.name.name.clone());
33736 } else if let Expression::Identifier(ref id) = expr {
33737 col_names.push(id.name.clone());
33738 } else {
33739 // For function expressions like MONTHS(y), serialize to SQL
33740 let generic = Dialect::get(DialectType::Generic);
33741 if let Ok(sql) = generic.generate(expr) {
33742 col_names.push(sql);
33743 }
33744 }
33745 }
33746 if has_col_defs {
33747 // Merge partition column defs into the main column list
33748 for cd in col_defs {
33749 ct.columns.push(cd);
33750 }
33751 }
33752 if !col_names.is_empty() {
33753 // Add PARTITIONED_BY property
33754 let array_val = format!(
33755 "ARRAY[{}]",
33756 col_names
33757 .iter()
33758 .map(|n| format!("'{}'", n))
33759 .collect::<Vec<_>>()
33760 .join(", ")
33761 );
33762 ct.with_properties
33763 .push(("PARTITIONED_BY".to_string(), array_val));
33764 }
33765 }
33766 // Skip - don't keep in properties
33767 }
33768 _ => {
33769 if !is_duckdb_target {
33770 new_properties.push(prop);
33771 }
33772 }
33773 }
33774 }
33775 ct.properties = new_properties;
33776 } else {
33777 // For Hive/Spark targets, unquote format names in STORED AS
33778 for prop in &mut ct.properties {
33779 if let Expression::FileFormatProperty(ref mut ffp) = prop {
33780 if let Some(ref mut fmt_expr) = ffp.this {
33781 if let Expression::Literal(lit) = fmt_expr.as_ref() {
33782 if let Literal::String(s) = lit.as_ref() {
33783 // Convert STORED AS 'PARQUET' to STORED AS PARQUET (unquote)
33784 let unquoted = s.clone();
33785 *fmt_expr =
33786 Box::new(Expression::Identifier(Identifier::new(unquoted)));
33787 }
33788 }
33789 }
33790 }
33791 }
33792 }
33793 }
33794 }
33795
33796 /// Apply PARTITIONED_BY conversion: move partition columns from column list to PARTITIONED BY
33797 fn apply_partitioned_by(
33798 ct: &mut crate::expressions::CreateTable,
33799 partitioned_by_value: &str,
33800 target: DialectType,
33801 ) {
33802 use crate::expressions::{Column, Expression, Identifier, PartitionedByProperty, Tuple};
33803
33804 // Parse the ARRAY['col1', 'col2'] value to extract column names
33805 let mut col_names: Vec<String> = Vec::new();
33806 // The value looks like ARRAY['y', 'z'] or ARRAY('y', 'z')
33807 let inner = partitioned_by_value
33808 .trim()
33809 .trim_start_matches("ARRAY")
33810 .trim_start_matches('[')
33811 .trim_start_matches('(')
33812 .trim_end_matches(']')
33813 .trim_end_matches(')');
33814 for part in inner.split(',') {
33815 let col = part.trim().trim_matches('\'').trim_matches('"');
33816 if !col.is_empty() {
33817 col_names.push(col.to_string());
33818 }
33819 }
33820
33821 if col_names.is_empty() {
33822 return;
33823 }
33824
33825 if matches!(target, DialectType::Hive) {
33826 // Hive: PARTITIONED BY (col_name type, ...) - move columns out of column list
33827 let mut partition_col_defs = Vec::new();
33828 for col_name in &col_names {
33829 // Find and remove from columns
33830 if let Some(pos) = ct
33831 .columns
33832 .iter()
33833 .position(|c| c.name.name.eq_ignore_ascii_case(col_name))
33834 {
33835 let col_def = ct.columns.remove(pos);
33836 partition_col_defs.push(Expression::ColumnDef(Box::new(col_def)));
33837 }
33838 }
33839 if !partition_col_defs.is_empty() {
33840 ct.properties
33841 .push(Expression::PartitionedByProperty(Box::new(
33842 PartitionedByProperty {
33843 this: Box::new(Expression::Tuple(Box::new(Tuple {
33844 expressions: partition_col_defs,
33845 }))),
33846 },
33847 )));
33848 }
33849 } else if matches!(target, DialectType::Spark | DialectType::Databricks) {
33850 // Spark: PARTITIONED BY (col1, col2) - just column names, keep in column list
33851 // Use quoted identifiers to match the quoting style of the original column definitions
33852 let partition_exprs: Vec<Expression> = col_names
33853 .iter()
33854 .map(|name| {
33855 // Check if the column exists in the column list and use its quoting
33856 let is_quoted = ct
33857 .columns
33858 .iter()
33859 .any(|c| c.name.name.eq_ignore_ascii_case(name) && c.name.quoted);
33860 let ident = if is_quoted {
33861 Identifier::quoted(name.clone())
33862 } else {
33863 Identifier::new(name.clone())
33864 };
33865 Expression::boxed_column(Column {
33866 name: ident,
33867 table: None,
33868 join_mark: false,
33869 trailing_comments: Vec::new(),
33870 span: None,
33871 inferred_type: None,
33872 })
33873 })
33874 .collect();
33875 ct.properties
33876 .push(Expression::PartitionedByProperty(Box::new(
33877 PartitionedByProperty {
33878 this: Box::new(Expression::Tuple(Box::new(Tuple {
33879 expressions: partition_exprs,
33880 }))),
33881 },
33882 )));
33883 }
33884 // DuckDB: strip partitioned_by entirely (already handled)
33885 }
33886
33887 /// Convert a DataType to Spark's type string format (using angle brackets)
33888 fn data_type_to_spark_string(dt: &crate::expressions::DataType) -> String {
33889 use crate::expressions::DataType;
33890 match dt {
33891 DataType::Int { .. } => "INT".to_string(),
33892 DataType::BigInt { .. } => "BIGINT".to_string(),
33893 DataType::SmallInt { .. } => "SMALLINT".to_string(),
33894 DataType::TinyInt { .. } => "TINYINT".to_string(),
33895 DataType::Float { .. } => "FLOAT".to_string(),
33896 DataType::Double { .. } => "DOUBLE".to_string(),
33897 DataType::Decimal {
33898 precision: Some(p),
33899 scale: Some(s),
33900 } => format!("DECIMAL({}, {})", p, s),
33901 DataType::Decimal {
33902 precision: Some(p), ..
33903 } => format!("DECIMAL({})", p),
33904 DataType::Decimal { .. } => "DECIMAL".to_string(),
33905 DataType::VarChar { .. } | DataType::Text | DataType::String { .. } => {
33906 "STRING".to_string()
33907 }
33908 DataType::Char { .. } => "STRING".to_string(),
33909 DataType::Boolean => "BOOLEAN".to_string(),
33910 DataType::Date => "DATE".to_string(),
33911 DataType::Timestamp { .. } => "TIMESTAMP".to_string(),
33912 DataType::Json | DataType::JsonB => "STRING".to_string(),
33913 DataType::Binary { .. } => "BINARY".to_string(),
33914 DataType::Array { element_type, .. } => {
33915 format!("ARRAY<{}>", Self::data_type_to_spark_string(element_type))
33916 }
33917 DataType::Map {
33918 key_type,
33919 value_type,
33920 } => format!(
33921 "MAP<{}, {}>",
33922 Self::data_type_to_spark_string(key_type),
33923 Self::data_type_to_spark_string(value_type)
33924 ),
33925 DataType::Struct { fields, .. } => {
33926 let field_strs: Vec<String> = fields
33927 .iter()
33928 .map(|f| {
33929 if f.name.is_empty() {
33930 Self::data_type_to_spark_string(&f.data_type)
33931 } else {
33932 format!(
33933 "{}: {}",
33934 f.name,
33935 Self::data_type_to_spark_string(&f.data_type)
33936 )
33937 }
33938 })
33939 .collect();
33940 format!("STRUCT<{}>", field_strs.join(", "))
33941 }
33942 DataType::Custom { name } => name.clone(),
33943 _ => format!("{:?}", dt),
33944 }
33945 }
33946
33947 /// Extract value and unit from an Interval expression
33948 /// Returns (value_expression, IntervalUnit)
33949 fn extract_interval_parts(
33950 interval_expr: &Expression,
33951 ) -> Option<(Expression, crate::expressions::IntervalUnit)> {
33952 use crate::expressions::{DataType, IntervalUnit, IntervalUnitSpec, Literal};
33953
33954 fn unit_from_str(unit: &str) -> Option<IntervalUnit> {
33955 match unit.trim().to_ascii_uppercase().as_str() {
33956 "YEAR" | "YEARS" => Some(IntervalUnit::Year),
33957 "QUARTER" | "QUARTERS" => Some(IntervalUnit::Quarter),
33958 "MONTH" | "MONTHS" | "MON" | "MONS" | "MM" => Some(IntervalUnit::Month),
33959 "WEEK" | "WEEKS" | "ISOWEEK" => Some(IntervalUnit::Week),
33960 "DAY" | "DAYS" => Some(IntervalUnit::Day),
33961 "HOUR" | "HOURS" => Some(IntervalUnit::Hour),
33962 "MINUTE" | "MINUTES" => Some(IntervalUnit::Minute),
33963 "SECOND" | "SECONDS" => Some(IntervalUnit::Second),
33964 "MILLISECOND" | "MILLISECONDS" => Some(IntervalUnit::Millisecond),
33965 "MICROSECOND" | "MICROSECONDS" => Some(IntervalUnit::Microsecond),
33966 "NANOSECOND" | "NANOSECONDS" => Some(IntervalUnit::Nanosecond),
33967 _ => None,
33968 }
33969 }
33970
33971 fn parts_from_literal_string(s: &str) -> Option<(Expression, IntervalUnit)> {
33972 let mut parts = s.split_whitespace();
33973 let value = parts.next()?;
33974 let unit = unit_from_str(parts.next()?)?;
33975 Some((
33976 Expression::Literal(Box::new(Literal::String(value.to_string()))),
33977 unit,
33978 ))
33979 }
33980
33981 fn unit_from_spec(unit: &IntervalUnitSpec) -> Option<IntervalUnit> {
33982 match unit {
33983 IntervalUnitSpec::Simple { unit, .. } => Some(*unit),
33984 IntervalUnitSpec::Expr(expr) => match expr.as_ref() {
33985 Expression::Day(_) => Some(IntervalUnit::Day),
33986 Expression::Month(_) => Some(IntervalUnit::Month),
33987 Expression::Year(_) => Some(IntervalUnit::Year),
33988 Expression::Identifier(id) => unit_from_str(&id.name),
33989 Expression::Var(v) => unit_from_str(&v.this),
33990 Expression::Column(col) => unit_from_str(&col.name.name),
33991 _ => None,
33992 },
33993 _ => None,
33994 }
33995 }
33996
33997 match interval_expr {
33998 Expression::Interval(iv) => {
33999 let val = iv.this.clone().unwrap_or(Expression::number(0));
34000 if let Expression::Literal(lit) = &val {
34001 if let Literal::String(s) = lit.as_ref() {
34002 if let Some(parts) = parts_from_literal_string(s) {
34003 return Some(parts);
34004 }
34005 }
34006 }
34007 let unit = iv
34008 .unit
34009 .as_ref()
34010 .and_then(unit_from_spec)
34011 .unwrap_or(IntervalUnit::Day);
34012 Some((val, unit))
34013 }
34014 Expression::Cast(cast) if matches!(cast.to, DataType::Interval { .. }) => {
34015 if let Expression::Literal(lit) = &cast.this {
34016 if let Literal::String(s) = lit.as_ref() {
34017 if let Some(parts) = parts_from_literal_string(s) {
34018 return Some(parts);
34019 }
34020 }
34021 }
34022 let unit = match &cast.to {
34023 DataType::Interval {
34024 unit: Some(unit), ..
34025 } => unit_from_str(unit).unwrap_or(IntervalUnit::Day),
34026 _ => IntervalUnit::Day,
34027 };
34028 Some((cast.this.clone(), unit))
34029 }
34030 _ => None,
34031 }
34032 }
34033
34034 fn rewrite_tsql_interval_arithmetic(expr: &Expression) -> Option<Expression> {
34035 match expr {
34036 Expression::Add(op) => {
34037 Self::extract_interval_parts(&op.right)?;
34038 Some(Self::build_tsql_dateadd_from_interval(
34039 op.left.clone(),
34040 &op.right,
34041 false,
34042 ))
34043 }
34044 Expression::Sub(op) => {
34045 Self::extract_interval_parts(&op.right)?;
34046 Some(Self::build_tsql_dateadd_from_interval(
34047 op.left.clone(),
34048 &op.right,
34049 true,
34050 ))
34051 }
34052 _ => None,
34053 }
34054 }
34055
34056 fn build_tsql_dateadd_from_interval(
34057 date: Expression,
34058 interval: &Expression,
34059 subtract: bool,
34060 ) -> Expression {
34061 let (value, unit) = Self::extract_interval_parts(interval)
34062 .unwrap_or_else(|| (interval.clone(), crate::expressions::IntervalUnit::Day));
34063 let unit = Self::interval_unit_to_string(&unit);
34064 let amount = Self::tsql_dateadd_amount(value, subtract);
34065
34066 Expression::Function(Box::new(Function::new(
34067 "DATEADD".to_string(),
34068 vec![Expression::Identifier(Identifier::new(unit)), amount, date],
34069 )))
34070 }
34071
34072 fn tsql_dateadd_amount(value: Expression, negate: bool) -> Expression {
34073 use crate::expressions::{Parameter, ParameterStyle, UnaryOp};
34074
34075 fn numeric_literal_value(value: &Expression) -> Option<&str> {
34076 match value {
34077 Expression::Literal(lit) => match lit.as_ref() {
34078 crate::expressions::Literal::Number(n)
34079 | crate::expressions::Literal::String(n) => Some(n.as_str()),
34080 _ => None,
34081 },
34082 _ => None,
34083 }
34084 }
34085
34086 fn colon_parameter(value: &Expression) -> Option<Expression> {
34087 let Expression::Literal(lit) = value else {
34088 return None;
34089 };
34090 let crate::expressions::Literal::String(s) = lit.as_ref() else {
34091 return None;
34092 };
34093 let name = s.strip_prefix(':')?;
34094 if name.is_empty()
34095 || !name
34096 .chars()
34097 .all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
34098 {
34099 return None;
34100 }
34101
34102 Some(Expression::Parameter(Box::new(Parameter {
34103 name: if name.chars().all(|ch| ch.is_ascii_digit()) {
34104 None
34105 } else {
34106 Some(name.to_string())
34107 },
34108 index: name.parse::<u32>().ok(),
34109 style: ParameterStyle::Colon,
34110 quoted: false,
34111 string_quoted: false,
34112 expression: None,
34113 })))
34114 }
34115
34116 let value = colon_parameter(&value).unwrap_or(value);
34117
34118 if let Some(n) = numeric_literal_value(&value) {
34119 if let Ok(parsed) = n.parse::<f64>() {
34120 let normalized = if negate { -parsed } else { parsed };
34121 let rendered = if normalized.fract() == 0.0 {
34122 format!("{}", normalized as i64)
34123 } else {
34124 normalized.to_string()
34125 };
34126 return Expression::Literal(Box::new(crate::expressions::Literal::Number(
34127 rendered,
34128 )));
34129 }
34130 }
34131
34132 if !negate {
34133 return value;
34134 }
34135
34136 match value {
34137 Expression::Neg(op) => op.this,
34138 other => Expression::Neg(Box::new(UnaryOp {
34139 this: other,
34140 inferred_type: None,
34141 })),
34142 }
34143 }
34144
34145 /// Normalize BigQuery-specific functions to standard forms that target dialects can handle
34146 fn normalize_bigquery_function(
34147 e: Expression,
34148 source: DialectType,
34149 target: DialectType,
34150 ) -> Result<Expression> {
34151 use crate::expressions::{BinaryOp, Cast, DataType, Function, Identifier, Literal, Paren};
34152
34153 let f = if let Expression::Function(f) = e {
34154 *f
34155 } else {
34156 return Ok(e);
34157 };
34158 let name = f.name.to_ascii_uppercase();
34159 let mut args = f.args;
34160
34161 /// Helper to extract unit string from an identifier, column, or literal expression
34162 fn get_unit_str(expr: &Expression) -> String {
34163 match expr {
34164 Expression::Identifier(id) => id.name.to_ascii_uppercase(),
34165 Expression::Var(v) => v.this.to_ascii_uppercase(),
34166 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
34167 let Literal::String(s) = lit.as_ref() else {
34168 unreachable!()
34169 };
34170 s.to_ascii_uppercase()
34171 }
34172 Expression::Column(col) => col.name.name.to_ascii_uppercase(),
34173 // Handle WEEK(MONDAY), WEEK(SUNDAY) etc. which are parsed as Function("WEEK", [Column("MONDAY")])
34174 Expression::Function(f) => {
34175 let base = f.name.to_ascii_uppercase();
34176 if !f.args.is_empty() {
34177 // e.g., WEEK(MONDAY) -> "WEEK(MONDAY)"
34178 let inner = get_unit_str(&f.args[0]);
34179 format!("{}({})", base, inner)
34180 } else {
34181 base
34182 }
34183 }
34184 _ => "DAY".to_string(),
34185 }
34186 }
34187
34188 /// Parse unit string to IntervalUnit
34189 fn parse_interval_unit(s: &str) -> crate::expressions::IntervalUnit {
34190 match s {
34191 "YEAR" => crate::expressions::IntervalUnit::Year,
34192 "QUARTER" => crate::expressions::IntervalUnit::Quarter,
34193 "MONTH" => crate::expressions::IntervalUnit::Month,
34194 "WEEK" | "ISOWEEK" => crate::expressions::IntervalUnit::Week,
34195 "DAY" => crate::expressions::IntervalUnit::Day,
34196 "HOUR" => crate::expressions::IntervalUnit::Hour,
34197 "MINUTE" => crate::expressions::IntervalUnit::Minute,
34198 "SECOND" => crate::expressions::IntervalUnit::Second,
34199 "MILLISECOND" => crate::expressions::IntervalUnit::Millisecond,
34200 "MICROSECOND" => crate::expressions::IntervalUnit::Microsecond,
34201 _ if s.starts_with("WEEK(") => crate::expressions::IntervalUnit::Week,
34202 _ => crate::expressions::IntervalUnit::Day,
34203 }
34204 }
34205
34206 match name.as_str() {
34207 // TIMESTAMP_DIFF(date1, date2, unit) -> TIMESTAMPDIFF(unit, date2, date1)
34208 // (BigQuery: result = date1 - date2, Standard: result = end - start)
34209 "TIMESTAMP_DIFF" | "DATETIME_DIFF" | "TIME_DIFF" if args.len() == 3 => {
34210 let date1 = args.remove(0);
34211 let date2 = args.remove(0);
34212 let unit_expr = args.remove(0);
34213 let unit_str = get_unit_str(&unit_expr);
34214
34215 if matches!(target, DialectType::BigQuery) {
34216 // BigQuery -> BigQuery: just uppercase the unit
34217 let unit = Expression::Identifier(Identifier::new(unit_str.clone()));
34218 return Ok(Expression::Function(Box::new(Function::new(
34219 f.name,
34220 vec![date1, date2, unit],
34221 ))));
34222 }
34223
34224 // For Snowflake: use TimestampDiff expression so it generates TIMESTAMPDIFF
34225 // (Function("TIMESTAMPDIFF") would be converted to DATEDIFF by Snowflake's function normalization)
34226 if matches!(target, DialectType::Snowflake) {
34227 return Ok(Expression::TimestampDiff(Box::new(
34228 crate::expressions::TimestampDiff {
34229 this: Box::new(date2),
34230 expression: Box::new(date1),
34231 unit: Some(unit_str),
34232 },
34233 )));
34234 }
34235
34236 // For DuckDB: DATE_DIFF('UNIT', start, end) with proper CAST
34237 if matches!(target, DialectType::DuckDB) {
34238 let (cast_d1, cast_d2) = if name == "TIME_DIFF" {
34239 // CAST to TIME
34240 let cast_fn = |e: Expression| -> Expression {
34241 match e {
34242 Expression::Literal(lit)
34243 if matches!(lit.as_ref(), Literal::String(_)) =>
34244 {
34245 let Literal::String(s) = lit.as_ref() else {
34246 unreachable!()
34247 };
34248 Expression::Cast(Box::new(Cast {
34249 this: Expression::Literal(Box::new(Literal::String(
34250 s.clone(),
34251 ))),
34252 to: DataType::Custom {
34253 name: "TIME".to_string(),
34254 },
34255 trailing_comments: vec![],
34256 double_colon_syntax: false,
34257 format: None,
34258 default: None,
34259 inferred_type: None,
34260 }))
34261 }
34262 other => other,
34263 }
34264 };
34265 (cast_fn(date1), cast_fn(date2))
34266 } else if name == "DATETIME_DIFF" {
34267 // CAST to TIMESTAMP
34268 (
34269 Self::ensure_cast_timestamp(date1),
34270 Self::ensure_cast_timestamp(date2),
34271 )
34272 } else {
34273 // TIMESTAMP_DIFF: CAST to TIMESTAMPTZ
34274 (
34275 Self::ensure_cast_timestamptz(date1),
34276 Self::ensure_cast_timestamptz(date2),
34277 )
34278 };
34279 return Ok(Expression::Function(Box::new(Function::new(
34280 "DATE_DIFF".to_string(),
34281 vec![
34282 Expression::Literal(Box::new(Literal::String(unit_str))),
34283 cast_d2,
34284 cast_d1,
34285 ],
34286 ))));
34287 }
34288
34289 // Convert to standard TIMESTAMPDIFF(unit, start, end)
34290 let unit = Expression::Identifier(Identifier::new(unit_str));
34291 Ok(Expression::Function(Box::new(Function::new(
34292 "TIMESTAMPDIFF".to_string(),
34293 vec![unit, date2, date1],
34294 ))))
34295 }
34296
34297 // DATEDIFF(unit, start, end) -> target-specific form
34298 // Used by: Redshift, Snowflake, TSQL, Databricks, Spark
34299 "DATEDIFF" if args.len() == 3 => {
34300 let arg0 = args.remove(0);
34301 let arg1 = args.remove(0);
34302 let arg2 = args.remove(0);
34303 let unit_str = get_unit_str(&arg0);
34304
34305 // Redshift DATEDIFF(unit, start, end) order: result = end - start
34306 // Snowflake DATEDIFF(unit, start, end) order: result = end - start
34307 // TSQL DATEDIFF(unit, start, end) order: result = end - start
34308
34309 if matches!(target, DialectType::Snowflake) {
34310 // Snowflake: DATEDIFF(UNIT, start, end) - uppercase unit
34311 let unit = Expression::Identifier(Identifier::new(unit_str));
34312 return Ok(Expression::Function(Box::new(Function::new(
34313 "DATEDIFF".to_string(),
34314 vec![unit, arg1, arg2],
34315 ))));
34316 }
34317
34318 if matches!(target, DialectType::DuckDB) {
34319 // DuckDB: DATE_DIFF('UNIT', start, end) with CAST
34320 let cast_d1 = Self::ensure_cast_timestamp(arg1);
34321 let cast_d2 = Self::ensure_cast_timestamp(arg2);
34322 return Ok(Expression::Function(Box::new(Function::new(
34323 "DATE_DIFF".to_string(),
34324 vec![
34325 Expression::Literal(Box::new(Literal::String(unit_str))),
34326 cast_d1,
34327 cast_d2,
34328 ],
34329 ))));
34330 }
34331
34332 if matches!(target, DialectType::BigQuery) {
34333 // BigQuery: DATE_DIFF(end_date, start_date, UNIT) - reversed args, CAST to DATETIME
34334 let cast_d1 = Self::ensure_cast_datetime(arg1);
34335 let cast_d2 = Self::ensure_cast_datetime(arg2);
34336 let unit = Expression::Identifier(Identifier::new(unit_str));
34337 return Ok(Expression::Function(Box::new(Function::new(
34338 "DATE_DIFF".to_string(),
34339 vec![cast_d2, cast_d1, unit],
34340 ))));
34341 }
34342
34343 if matches!(target, DialectType::Spark | DialectType::Databricks) {
34344 // Spark/Databricks: DATEDIFF(UNIT, start, end) - uppercase unit
34345 let unit = Expression::Identifier(Identifier::new(unit_str));
34346 return Ok(Expression::Function(Box::new(Function::new(
34347 "DATEDIFF".to_string(),
34348 vec![unit, arg1, arg2],
34349 ))));
34350 }
34351
34352 if matches!(target, DialectType::Hive) {
34353 // Hive: DATEDIFF(end, start) for DAY only, use MONTHS_BETWEEN for MONTH
34354 match unit_str.as_str() {
34355 "MONTH" => {
34356 return Ok(Expression::Function(Box::new(Function::new(
34357 "CAST".to_string(),
34358 vec![Expression::Function(Box::new(Function::new(
34359 "MONTHS_BETWEEN".to_string(),
34360 vec![arg2, arg1],
34361 )))],
34362 ))));
34363 }
34364 "WEEK" => {
34365 return Ok(Expression::Cast(Box::new(Cast {
34366 this: Expression::Div(Box::new(crate::expressions::BinaryOp::new(
34367 Expression::Function(Box::new(Function::new(
34368 "DATEDIFF".to_string(),
34369 vec![arg2, arg1],
34370 ))),
34371 Expression::Literal(Box::new(Literal::Number("7".to_string()))),
34372 ))),
34373 to: DataType::Int {
34374 length: None,
34375 integer_spelling: false,
34376 },
34377 trailing_comments: vec![],
34378 double_colon_syntax: false,
34379 format: None,
34380 default: None,
34381 inferred_type: None,
34382 })));
34383 }
34384 _ => {
34385 // Default: DATEDIFF(end, start) for DAY
34386 return Ok(Expression::Function(Box::new(Function::new(
34387 "DATEDIFF".to_string(),
34388 vec![arg2, arg1],
34389 ))));
34390 }
34391 }
34392 }
34393
34394 if matches!(
34395 target,
34396 DialectType::Presto | DialectType::Trino | DialectType::Athena
34397 ) {
34398 // Presto/Trino: DATE_DIFF('UNIT', start, end)
34399 return Ok(Expression::Function(Box::new(Function::new(
34400 "DATE_DIFF".to_string(),
34401 vec![
34402 Expression::Literal(Box::new(Literal::String(unit_str))),
34403 arg1,
34404 arg2,
34405 ],
34406 ))));
34407 }
34408
34409 if matches!(target, DialectType::TSQL) {
34410 // TSQL: DATEDIFF(UNIT, start, CAST(end AS DATETIME2))
34411 let cast_d2 = Self::ensure_cast_datetime2(arg2);
34412 let unit = Expression::Identifier(Identifier::new(unit_str));
34413 return Ok(Expression::Function(Box::new(Function::new(
34414 "DATEDIFF".to_string(),
34415 vec![unit, arg1, cast_d2],
34416 ))));
34417 }
34418
34419 if matches!(target, DialectType::PostgreSQL) {
34420 // PostgreSQL doesn't have DATEDIFF - use date subtraction or EXTRACT
34421 // For now, use DATEDIFF (passthrough) with uppercased unit
34422 let unit = Expression::Identifier(Identifier::new(unit_str));
34423 return Ok(Expression::Function(Box::new(Function::new(
34424 "DATEDIFF".to_string(),
34425 vec![unit, arg1, arg2],
34426 ))));
34427 }
34428
34429 // Default: DATEDIFF(UNIT, start, end) with uppercase unit
34430 let unit = Expression::Identifier(Identifier::new(unit_str));
34431 Ok(Expression::Function(Box::new(Function::new(
34432 "DATEDIFF".to_string(),
34433 vec![unit, arg1, arg2],
34434 ))))
34435 }
34436
34437 // DATE_DIFF(date1, date2, unit) -> standard form
34438 "DATE_DIFF" if args.len() == 3 => {
34439 let date1 = args.remove(0);
34440 let date2 = args.remove(0);
34441 let unit_expr = args.remove(0);
34442 let unit_str = get_unit_str(&unit_expr);
34443
34444 if matches!(target, DialectType::BigQuery) {
34445 // BigQuery -> BigQuery: just uppercase the unit, normalize WEEK(SUNDAY) -> WEEK
34446 let norm_unit = if unit_str == "WEEK(SUNDAY)" {
34447 "WEEK".to_string()
34448 } else {
34449 unit_str
34450 };
34451 let norm_d1 = Self::date_literal_to_cast(date1);
34452 let norm_d2 = Self::date_literal_to_cast(date2);
34453 let unit = Expression::Identifier(Identifier::new(norm_unit));
34454 return Ok(Expression::Function(Box::new(Function::new(
34455 f.name,
34456 vec![norm_d1, norm_d2, unit],
34457 ))));
34458 }
34459
34460 if matches!(target, DialectType::MySQL) {
34461 // MySQL DATEDIFF only takes 2 args (date1, date2), returns day difference
34462 let norm_d1 = Self::date_literal_to_cast(date1);
34463 let norm_d2 = Self::date_literal_to_cast(date2);
34464 return Ok(Expression::Function(Box::new(Function::new(
34465 "DATEDIFF".to_string(),
34466 vec![norm_d1, norm_d2],
34467 ))));
34468 }
34469
34470 if matches!(target, DialectType::StarRocks) {
34471 // StarRocks: DATE_DIFF('UNIT', date1, date2) - unit as string, args NOT swapped
34472 let norm_d1 = Self::date_literal_to_cast(date1);
34473 let norm_d2 = Self::date_literal_to_cast(date2);
34474 return Ok(Expression::Function(Box::new(Function::new(
34475 "DATE_DIFF".to_string(),
34476 vec![
34477 Expression::Literal(Box::new(Literal::String(unit_str))),
34478 norm_d1,
34479 norm_d2,
34480 ],
34481 ))));
34482 }
34483
34484 if matches!(target, DialectType::DuckDB) {
34485 // DuckDB: DATE_DIFF('UNIT', date2, date1) with proper CAST for dates
34486 let norm_d1 = Self::ensure_cast_date(date1);
34487 let norm_d2 = Self::ensure_cast_date(date2);
34488
34489 // Handle WEEK variants: WEEK(MONDAY)/WEEK(SUNDAY)/ISOWEEK/WEEK
34490 let is_week_variant = unit_str == "WEEK"
34491 || unit_str.starts_with("WEEK(")
34492 || unit_str == "ISOWEEK";
34493 if is_week_variant {
34494 // For DuckDB, WEEK-based diffs use DATE_TRUNC approach
34495 // WEEK(MONDAY) / ISOWEEK: DATE_DIFF('WEEK', DATE_TRUNC('WEEK', d2), DATE_TRUNC('WEEK', d1))
34496 // WEEK / WEEK(SUNDAY): DATE_DIFF('WEEK', DATE_TRUNC('WEEK', d2 + INTERVAL '1' DAY), DATE_TRUNC('WEEK', d1 + INTERVAL '1' DAY))
34497 // WEEK(SATURDAY): DATE_DIFF('WEEK', DATE_TRUNC('WEEK', d2 + INTERVAL '-5' DAY), DATE_TRUNC('WEEK', d1 + INTERVAL '-5' DAY))
34498 let day_offset = if unit_str == "WEEK(MONDAY)" || unit_str == "ISOWEEK" {
34499 None // ISO weeks start on Monday, aligned with DATE_TRUNC('WEEK')
34500 } else if unit_str == "WEEK" || unit_str == "WEEK(SUNDAY)" {
34501 Some("1") // Shift Sunday to Monday alignment
34502 } else if unit_str == "WEEK(SATURDAY)" {
34503 Some("-5")
34504 } else if unit_str == "WEEK(TUESDAY)" {
34505 Some("-1")
34506 } else if unit_str == "WEEK(WEDNESDAY)" {
34507 Some("-2")
34508 } else if unit_str == "WEEK(THURSDAY)" {
34509 Some("-3")
34510 } else if unit_str == "WEEK(FRIDAY)" {
34511 Some("-4")
34512 } else {
34513 Some("1") // default to Sunday
34514 };
34515
34516 let make_trunc = |date: Expression, offset: Option<&str>| -> Expression {
34517 let shifted = if let Some(off) = offset {
34518 let interval =
34519 Expression::Interval(Box::new(crate::expressions::Interval {
34520 this: Some(Expression::Literal(Box::new(Literal::String(
34521 off.to_string(),
34522 )))),
34523 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
34524 unit: crate::expressions::IntervalUnit::Day,
34525 use_plural: false,
34526 }),
34527 }));
34528 Expression::Add(Box::new(crate::expressions::BinaryOp::new(
34529 date, interval,
34530 )))
34531 } else {
34532 date
34533 };
34534 Expression::Function(Box::new(Function::new(
34535 "DATE_TRUNC".to_string(),
34536 vec![
34537 Expression::Literal(Box::new(Literal::String(
34538 "WEEK".to_string(),
34539 ))),
34540 shifted,
34541 ],
34542 )))
34543 };
34544
34545 let trunc_d2 = make_trunc(norm_d2, day_offset);
34546 let trunc_d1 = make_trunc(norm_d1, day_offset);
34547 return Ok(Expression::Function(Box::new(Function::new(
34548 "DATE_DIFF".to_string(),
34549 vec![
34550 Expression::Literal(Box::new(Literal::String("WEEK".to_string()))),
34551 trunc_d2,
34552 trunc_d1,
34553 ],
34554 ))));
34555 }
34556
34557 return Ok(Expression::Function(Box::new(Function::new(
34558 "DATE_DIFF".to_string(),
34559 vec![
34560 Expression::Literal(Box::new(Literal::String(unit_str))),
34561 norm_d2,
34562 norm_d1,
34563 ],
34564 ))));
34565 }
34566
34567 // Default: DATEDIFF(unit, date2, date1)
34568 let unit = Expression::Identifier(Identifier::new(unit_str));
34569 Ok(Expression::Function(Box::new(Function::new(
34570 "DATEDIFF".to_string(),
34571 vec![unit, date2, date1],
34572 ))))
34573 }
34574
34575 // TIMESTAMP_ADD(ts, INTERVAL n UNIT) -> target-specific
34576 "TIMESTAMP_ADD" | "DATETIME_ADD" | "TIME_ADD" if args.len() == 2 => {
34577 let ts = args.remove(0);
34578 let interval_expr = args.remove(0);
34579 let (val, unit) =
34580 Self::extract_interval_parts(&interval_expr).unwrap_or_else(|| {
34581 (interval_expr.clone(), crate::expressions::IntervalUnit::Day)
34582 });
34583
34584 match target {
34585 DialectType::Snowflake => {
34586 // TIMESTAMPADD(UNIT, val, CAST(ts AS TIMESTAMPTZ))
34587 // Use TimestampAdd expression so Snowflake generates TIMESTAMPADD
34588 // (Function("TIMESTAMPADD") would be converted to DATEADD by Snowflake's function normalization)
34589 let unit_str = Self::interval_unit_to_string(&unit);
34590 let cast_ts = Self::maybe_cast_ts_to_tz(ts, &name);
34591 Ok(Expression::TimestampAdd(Box::new(
34592 crate::expressions::TimestampAdd {
34593 this: Box::new(val),
34594 expression: Box::new(cast_ts),
34595 unit: Some(unit_str.to_string()),
34596 },
34597 )))
34598 }
34599 DialectType::Spark | DialectType::Databricks => {
34600 if name == "DATETIME_ADD" && matches!(target, DialectType::Spark) {
34601 // Spark DATETIME_ADD: ts + INTERVAL val UNIT
34602 let interval =
34603 Expression::Interval(Box::new(crate::expressions::Interval {
34604 this: Some(val),
34605 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
34606 unit,
34607 use_plural: false,
34608 }),
34609 }));
34610 Ok(Expression::Add(Box::new(
34611 crate::expressions::BinaryOp::new(ts, interval),
34612 )))
34613 } else if name == "DATETIME_ADD"
34614 && matches!(target, DialectType::Databricks)
34615 {
34616 // Databricks DATETIME_ADD: TIMESTAMPADD(UNIT, val, ts)
34617 let unit_str = Self::interval_unit_to_string(&unit);
34618 Ok(Expression::Function(Box::new(Function::new(
34619 "TIMESTAMPADD".to_string(),
34620 vec![Expression::Identifier(Identifier::new(unit_str)), val, ts],
34621 ))))
34622 } else {
34623 // Presto-style: DATE_ADD('unit', val, CAST(ts AS TIMESTAMP))
34624 let unit_str = Self::interval_unit_to_string(&unit);
34625 let cast_ts =
34626 if name.starts_with("TIMESTAMP") || name.starts_with("DATETIME") {
34627 Self::maybe_cast_ts(ts)
34628 } else {
34629 ts
34630 };
34631 Ok(Expression::Function(Box::new(Function::new(
34632 "DATE_ADD".to_string(),
34633 vec![
34634 Expression::Identifier(Identifier::new(unit_str)),
34635 val,
34636 cast_ts,
34637 ],
34638 ))))
34639 }
34640 }
34641 DialectType::MySQL => {
34642 // DATE_ADD(TIMESTAMP(ts), INTERVAL val UNIT) for MySQL
34643 let mysql_ts = if name.starts_with("TIMESTAMP") {
34644 // Check if already wrapped in TIMESTAMP() function (from cross-dialect normalization)
34645 match &ts {
34646 Expression::Function(ref inner_f)
34647 if inner_f.name.eq_ignore_ascii_case("TIMESTAMP") =>
34648 {
34649 // Already wrapped, keep as-is
34650 ts
34651 }
34652 _ => {
34653 // Unwrap typed literals: TIMESTAMP '...' -> '...' for TIMESTAMP() wrapper
34654 let unwrapped = match ts {
34655 Expression::Literal(lit)
34656 if matches!(lit.as_ref(), Literal::Timestamp(_)) =>
34657 {
34658 let Literal::Timestamp(s) = lit.as_ref() else {
34659 unreachable!()
34660 };
34661 Expression::Literal(Box::new(Literal::String(
34662 s.clone(),
34663 )))
34664 }
34665 other => other,
34666 };
34667 Expression::Function(Box::new(Function::new(
34668 "TIMESTAMP".to_string(),
34669 vec![unwrapped],
34670 )))
34671 }
34672 }
34673 } else {
34674 ts
34675 };
34676 Ok(Expression::DateAdd(Box::new(
34677 crate::expressions::DateAddFunc {
34678 this: mysql_ts,
34679 interval: val,
34680 unit,
34681 },
34682 )))
34683 }
34684 _ => {
34685 // DuckDB and others use DateAdd expression (DuckDB converts to + INTERVAL)
34686 let cast_ts = if matches!(target, DialectType::DuckDB) {
34687 if name == "DATETIME_ADD" {
34688 Self::ensure_cast_timestamp(ts)
34689 } else if name.starts_with("TIMESTAMP") {
34690 Self::maybe_cast_ts_to_tz(ts, &name)
34691 } else {
34692 ts
34693 }
34694 } else {
34695 ts
34696 };
34697 Ok(Expression::DateAdd(Box::new(
34698 crate::expressions::DateAddFunc {
34699 this: cast_ts,
34700 interval: val,
34701 unit,
34702 },
34703 )))
34704 }
34705 }
34706 }
34707
34708 // TIMESTAMP_SUB(ts, INTERVAL n UNIT) -> target-specific
34709 "TIMESTAMP_SUB" | "DATETIME_SUB" | "TIME_SUB" if args.len() == 2 => {
34710 let ts = args.remove(0);
34711 let interval_expr = args.remove(0);
34712 let (val, unit) =
34713 Self::extract_interval_parts(&interval_expr).unwrap_or_else(|| {
34714 (interval_expr.clone(), crate::expressions::IntervalUnit::Day)
34715 });
34716
34717 match target {
34718 DialectType::Snowflake => {
34719 // TIMESTAMPADD(UNIT, val * -1, CAST(ts AS TIMESTAMPTZ))
34720 let unit_str = Self::interval_unit_to_string(&unit);
34721 let cast_ts = Self::maybe_cast_ts_to_tz(ts, &name);
34722 let neg_val = Expression::Mul(Box::new(crate::expressions::BinaryOp::new(
34723 val,
34724 Expression::Neg(Box::new(crate::expressions::UnaryOp {
34725 this: Expression::number(1),
34726 inferred_type: None,
34727 })),
34728 )));
34729 Ok(Expression::TimestampAdd(Box::new(
34730 crate::expressions::TimestampAdd {
34731 this: Box::new(neg_val),
34732 expression: Box::new(cast_ts),
34733 unit: Some(unit_str.to_string()),
34734 },
34735 )))
34736 }
34737 DialectType::Spark | DialectType::Databricks => {
34738 if (name == "DATETIME_SUB" && matches!(target, DialectType::Spark))
34739 || (name == "TIMESTAMP_SUB" && matches!(target, DialectType::Spark))
34740 {
34741 // Spark: ts - INTERVAL val UNIT
34742 let cast_ts = if name.starts_with("TIMESTAMP") {
34743 Self::maybe_cast_ts(ts)
34744 } else {
34745 ts
34746 };
34747 let interval =
34748 Expression::Interval(Box::new(crate::expressions::Interval {
34749 this: Some(val),
34750 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
34751 unit,
34752 use_plural: false,
34753 }),
34754 }));
34755 Ok(Expression::Sub(Box::new(
34756 crate::expressions::BinaryOp::new(cast_ts, interval),
34757 )))
34758 } else {
34759 // Databricks: TIMESTAMPADD(UNIT, val * -1, ts)
34760 let unit_str = Self::interval_unit_to_string(&unit);
34761 let neg_val =
34762 Expression::Mul(Box::new(crate::expressions::BinaryOp::new(
34763 val,
34764 Expression::Neg(Box::new(crate::expressions::UnaryOp {
34765 this: Expression::number(1),
34766 inferred_type: None,
34767 })),
34768 )));
34769 Ok(Expression::Function(Box::new(Function::new(
34770 "TIMESTAMPADD".to_string(),
34771 vec![
34772 Expression::Identifier(Identifier::new(unit_str)),
34773 neg_val,
34774 ts,
34775 ],
34776 ))))
34777 }
34778 }
34779 DialectType::MySQL => {
34780 let mysql_ts = if name.starts_with("TIMESTAMP") {
34781 // Check if already wrapped in TIMESTAMP() function (from cross-dialect normalization)
34782 match &ts {
34783 Expression::Function(ref inner_f)
34784 if inner_f.name.eq_ignore_ascii_case("TIMESTAMP") =>
34785 {
34786 // Already wrapped, keep as-is
34787 ts
34788 }
34789 _ => {
34790 let unwrapped = match ts {
34791 Expression::Literal(lit)
34792 if matches!(lit.as_ref(), Literal::Timestamp(_)) =>
34793 {
34794 let Literal::Timestamp(s) = lit.as_ref() else {
34795 unreachable!()
34796 };
34797 Expression::Literal(Box::new(Literal::String(
34798 s.clone(),
34799 )))
34800 }
34801 other => other,
34802 };
34803 Expression::Function(Box::new(Function::new(
34804 "TIMESTAMP".to_string(),
34805 vec![unwrapped],
34806 )))
34807 }
34808 }
34809 } else {
34810 ts
34811 };
34812 Ok(Expression::DateSub(Box::new(
34813 crate::expressions::DateAddFunc {
34814 this: mysql_ts,
34815 interval: val,
34816 unit,
34817 },
34818 )))
34819 }
34820 _ => {
34821 let cast_ts = if matches!(target, DialectType::DuckDB) {
34822 if name == "DATETIME_SUB" {
34823 Self::ensure_cast_timestamp(ts)
34824 } else if name.starts_with("TIMESTAMP") {
34825 Self::maybe_cast_ts_to_tz(ts, &name)
34826 } else {
34827 ts
34828 }
34829 } else {
34830 ts
34831 };
34832 Ok(Expression::DateSub(Box::new(
34833 crate::expressions::DateAddFunc {
34834 this: cast_ts,
34835 interval: val,
34836 unit,
34837 },
34838 )))
34839 }
34840 }
34841 }
34842
34843 // DATE_SUB(date, INTERVAL n UNIT) -> target-specific
34844 "DATE_SUB" if args.len() == 2 => {
34845 let date = args.remove(0);
34846 let interval_expr = args.remove(0);
34847 let (val, unit) =
34848 Self::extract_interval_parts(&interval_expr).unwrap_or_else(|| {
34849 (interval_expr.clone(), crate::expressions::IntervalUnit::Day)
34850 });
34851
34852 match target {
34853 DialectType::Databricks | DialectType::Spark => {
34854 // Databricks/Spark: DATE_ADD(date, -val)
34855 // Use DateAdd expression with negative val so it generates correctly
34856 // The generator will output DATE_ADD(date, INTERVAL -val DAY)
34857 // Then Databricks transform converts 2-arg DATE_ADD(date, interval) to DATEADD(DAY, interval, date)
34858 // Instead, we directly output as a simple negated DateSub
34859 Ok(Expression::DateSub(Box::new(
34860 crate::expressions::DateAddFunc {
34861 this: date,
34862 interval: val,
34863 unit,
34864 },
34865 )))
34866 }
34867 DialectType::DuckDB => {
34868 // DuckDB: CAST(date AS DATE) - INTERVAL 'val' UNIT
34869 let cast_date = Self::ensure_cast_date(date);
34870 let interval =
34871 Expression::Interval(Box::new(crate::expressions::Interval {
34872 this: Some(val),
34873 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
34874 unit,
34875 use_plural: false,
34876 }),
34877 }));
34878 Ok(Expression::Sub(Box::new(
34879 crate::expressions::BinaryOp::new(cast_date, interval),
34880 )))
34881 }
34882 DialectType::Snowflake => {
34883 // Snowflake: Let Snowflake's own DateSub -> DATEADD(UNIT, val * -1, date) handler work
34884 // Just ensure the date is cast properly
34885 let cast_date = Self::ensure_cast_date(date);
34886 Ok(Expression::DateSub(Box::new(
34887 crate::expressions::DateAddFunc {
34888 this: cast_date,
34889 interval: val,
34890 unit,
34891 },
34892 )))
34893 }
34894 DialectType::PostgreSQL => {
34895 // PostgreSQL: date - INTERVAL 'val UNIT'
34896 let unit_str = Self::interval_unit_to_string(&unit);
34897 let interval =
34898 Expression::Interval(Box::new(crate::expressions::Interval {
34899 this: Some(Expression::Literal(Box::new(Literal::String(
34900 format!("{} {}", Self::expr_to_string(&val), unit_str),
34901 )))),
34902 unit: None,
34903 }));
34904 Ok(Expression::Sub(Box::new(
34905 crate::expressions::BinaryOp::new(date, interval),
34906 )))
34907 }
34908 _ => Ok(Expression::DateSub(Box::new(
34909 crate::expressions::DateAddFunc {
34910 this: date,
34911 interval: val,
34912 unit,
34913 },
34914 ))),
34915 }
34916 }
34917
34918 // DATEADD(unit, val, date) -> target-specific form
34919 // Used by: Redshift, Snowflake, TSQL, ClickHouse
34920 "DATEADD" if args.len() == 3 => {
34921 let arg0 = args.remove(0);
34922 let arg1 = args.remove(0);
34923 let arg2 = args.remove(0);
34924 let unit_str = get_unit_str(&arg0);
34925
34926 if matches!(target, DialectType::Snowflake | DialectType::TSQL) {
34927 // Keep DATEADD(UNIT, val, date) with uppercased unit
34928 let unit = Expression::Identifier(Identifier::new(unit_str));
34929 // Only CAST to DATETIME2 for TSQL target when source is NOT Spark/Databricks family
34930 let date = if matches!(target, DialectType::TSQL)
34931 && !matches!(
34932 source,
34933 DialectType::Spark | DialectType::Databricks | DialectType::Hive
34934 ) {
34935 Self::ensure_cast_datetime2(arg2)
34936 } else {
34937 arg2
34938 };
34939 return Ok(Expression::Function(Box::new(Function::new(
34940 "DATEADD".to_string(),
34941 vec![unit, arg1, date],
34942 ))));
34943 }
34944
34945 if matches!(target, DialectType::DuckDB) {
34946 // DuckDB: date + INTERVAL 'val' UNIT
34947 let iu = parse_interval_unit(&unit_str);
34948 let interval = Expression::Interval(Box::new(crate::expressions::Interval {
34949 this: Some(arg1),
34950 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
34951 unit: iu,
34952 use_plural: false,
34953 }),
34954 }));
34955 let cast_date = Self::ensure_cast_timestamp(arg2);
34956 return Ok(Expression::Add(Box::new(
34957 crate::expressions::BinaryOp::new(cast_date, interval),
34958 )));
34959 }
34960
34961 if matches!(target, DialectType::BigQuery) {
34962 // BigQuery: DATE_ADD(date, INTERVAL val UNIT) or TIMESTAMP_ADD(ts, INTERVAL val UNIT)
34963 let iu = parse_interval_unit(&unit_str);
34964 let interval = Expression::Interval(Box::new(crate::expressions::Interval {
34965 this: Some(arg1),
34966 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
34967 unit: iu,
34968 use_plural: false,
34969 }),
34970 }));
34971 return Ok(Expression::Function(Box::new(Function::new(
34972 "DATE_ADD".to_string(),
34973 vec![arg2, interval],
34974 ))));
34975 }
34976
34977 if matches!(target, DialectType::Databricks) {
34978 // Databricks: keep DATEADD(UNIT, val, date) format
34979 let unit = Expression::Identifier(Identifier::new(unit_str));
34980 return Ok(Expression::Function(Box::new(Function::new(
34981 "DATEADD".to_string(),
34982 vec![unit, arg1, arg2],
34983 ))));
34984 }
34985
34986 if matches!(target, DialectType::Spark) {
34987 // Spark: convert month-based units to ADD_MONTHS, rest to DATE_ADD
34988 fn multiply_expr_dateadd(expr: Expression, factor: i64) -> Expression {
34989 if let Expression::Literal(lit) = &expr {
34990 if let crate::expressions::Literal::Number(n) = lit.as_ref() {
34991 if let Ok(val) = n.parse::<i64>() {
34992 return Expression::Literal(Box::new(
34993 crate::expressions::Literal::Number(
34994 (val * factor).to_string(),
34995 ),
34996 ));
34997 }
34998 }
34999 }
35000 Expression::Mul(Box::new(crate::expressions::BinaryOp::new(
35001 expr,
35002 Expression::Literal(Box::new(crate::expressions::Literal::Number(
35003 factor.to_string(),
35004 ))),
35005 )))
35006 }
35007 match unit_str.as_str() {
35008 "YEAR" => {
35009 let months = multiply_expr_dateadd(arg1, 12);
35010 return Ok(Expression::Function(Box::new(Function::new(
35011 "ADD_MONTHS".to_string(),
35012 vec![arg2, months],
35013 ))));
35014 }
35015 "QUARTER" => {
35016 let months = multiply_expr_dateadd(arg1, 3);
35017 return Ok(Expression::Function(Box::new(Function::new(
35018 "ADD_MONTHS".to_string(),
35019 vec![arg2, months],
35020 ))));
35021 }
35022 "MONTH" => {
35023 return Ok(Expression::Function(Box::new(Function::new(
35024 "ADD_MONTHS".to_string(),
35025 vec![arg2, arg1],
35026 ))));
35027 }
35028 "WEEK" => {
35029 let days = multiply_expr_dateadd(arg1, 7);
35030 return Ok(Expression::Function(Box::new(Function::new(
35031 "DATE_ADD".to_string(),
35032 vec![arg2, days],
35033 ))));
35034 }
35035 "DAY" => {
35036 return Ok(Expression::Function(Box::new(Function::new(
35037 "DATE_ADD".to_string(),
35038 vec![arg2, arg1],
35039 ))));
35040 }
35041 _ => {
35042 let unit = Expression::Identifier(Identifier::new(unit_str));
35043 return Ok(Expression::Function(Box::new(Function::new(
35044 "DATE_ADD".to_string(),
35045 vec![unit, arg1, arg2],
35046 ))));
35047 }
35048 }
35049 }
35050
35051 if matches!(target, DialectType::Hive) {
35052 // Hive: DATE_ADD(date, val) for DAY, or date + INTERVAL for others
35053 match unit_str.as_str() {
35054 "DAY" => {
35055 return Ok(Expression::Function(Box::new(Function::new(
35056 "DATE_ADD".to_string(),
35057 vec![arg2, arg1],
35058 ))));
35059 }
35060 "MONTH" => {
35061 return Ok(Expression::Function(Box::new(Function::new(
35062 "ADD_MONTHS".to_string(),
35063 vec![arg2, arg1],
35064 ))));
35065 }
35066 _ => {
35067 let iu = parse_interval_unit(&unit_str);
35068 let interval =
35069 Expression::Interval(Box::new(crate::expressions::Interval {
35070 this: Some(arg1),
35071 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
35072 unit: iu,
35073 use_plural: false,
35074 }),
35075 }));
35076 return Ok(Expression::Add(Box::new(
35077 crate::expressions::BinaryOp::new(arg2, interval),
35078 )));
35079 }
35080 }
35081 }
35082
35083 if matches!(target, DialectType::PostgreSQL) {
35084 // PostgreSQL: date + INTERVAL 'val UNIT'
35085 let interval = Expression::Interval(Box::new(crate::expressions::Interval {
35086 this: Some(Expression::Literal(Box::new(Literal::String(format!(
35087 "{} {}",
35088 Self::expr_to_string(&arg1),
35089 unit_str
35090 ))))),
35091 unit: None,
35092 }));
35093 return Ok(Expression::Add(Box::new(
35094 crate::expressions::BinaryOp::new(arg2, interval),
35095 )));
35096 }
35097
35098 if matches!(
35099 target,
35100 DialectType::Presto | DialectType::Trino | DialectType::Athena
35101 ) {
35102 // Presto/Trino: DATE_ADD('UNIT', val, date)
35103 return Ok(Expression::Function(Box::new(Function::new(
35104 "DATE_ADD".to_string(),
35105 vec![
35106 Expression::Literal(Box::new(Literal::String(unit_str))),
35107 arg1,
35108 arg2,
35109 ],
35110 ))));
35111 }
35112
35113 if matches!(target, DialectType::ClickHouse) {
35114 // ClickHouse: DATE_ADD(UNIT, val, date)
35115 let unit = Expression::Identifier(Identifier::new(unit_str));
35116 return Ok(Expression::Function(Box::new(Function::new(
35117 "DATE_ADD".to_string(),
35118 vec![unit, arg1, arg2],
35119 ))));
35120 }
35121
35122 // Default: keep DATEADD with uppercased unit
35123 let unit = Expression::Identifier(Identifier::new(unit_str));
35124 Ok(Expression::Function(Box::new(Function::new(
35125 "DATEADD".to_string(),
35126 vec![unit, arg1, arg2],
35127 ))))
35128 }
35129
35130 // DATE_ADD(unit, val, date) - 3 arg form from ClickHouse/Presto
35131 "DATE_ADD" if args.len() == 3 => {
35132 let arg0 = args.remove(0);
35133 let arg1 = args.remove(0);
35134 let arg2 = args.remove(0);
35135 let unit_str = get_unit_str(&arg0);
35136
35137 if matches!(
35138 target,
35139 DialectType::Presto | DialectType::Trino | DialectType::Athena
35140 ) {
35141 // Presto/Trino: DATE_ADD('UNIT', val, date)
35142 return Ok(Expression::Function(Box::new(Function::new(
35143 "DATE_ADD".to_string(),
35144 vec![
35145 Expression::Literal(Box::new(Literal::String(unit_str))),
35146 arg1,
35147 arg2,
35148 ],
35149 ))));
35150 }
35151
35152 if matches!(
35153 target,
35154 DialectType::Snowflake | DialectType::TSQL | DialectType::Redshift
35155 ) {
35156 // DATEADD(UNIT, val, date)
35157 let unit = Expression::Identifier(Identifier::new(unit_str));
35158 let date = if matches!(target, DialectType::TSQL) {
35159 Self::ensure_cast_datetime2(arg2)
35160 } else {
35161 arg2
35162 };
35163 return Ok(Expression::Function(Box::new(Function::new(
35164 "DATEADD".to_string(),
35165 vec![unit, arg1, date],
35166 ))));
35167 }
35168
35169 if matches!(target, DialectType::DuckDB) {
35170 // DuckDB: date + INTERVAL val UNIT
35171 let iu = parse_interval_unit(&unit_str);
35172 let interval = Expression::Interval(Box::new(crate::expressions::Interval {
35173 this: Some(arg1),
35174 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
35175 unit: iu,
35176 use_plural: false,
35177 }),
35178 }));
35179 return Ok(Expression::Add(Box::new(
35180 crate::expressions::BinaryOp::new(arg2, interval),
35181 )));
35182 }
35183
35184 if matches!(target, DialectType::Spark | DialectType::Databricks) {
35185 // Spark: DATE_ADD(UNIT, val, date) with uppercased unit
35186 let unit = Expression::Identifier(Identifier::new(unit_str));
35187 return Ok(Expression::Function(Box::new(Function::new(
35188 "DATE_ADD".to_string(),
35189 vec![unit, arg1, arg2],
35190 ))));
35191 }
35192
35193 // Default: DATE_ADD(UNIT, val, date)
35194 let unit = Expression::Identifier(Identifier::new(unit_str));
35195 Ok(Expression::Function(Box::new(Function::new(
35196 "DATE_ADD".to_string(),
35197 vec![unit, arg1, arg2],
35198 ))))
35199 }
35200
35201 // DATE_ADD(date, INTERVAL val UNIT) - 2 arg BigQuery form
35202 "DATE_ADD" if args.len() == 2 => {
35203 let date = args.remove(0);
35204 let interval_expr = args.remove(0);
35205 let (val, unit) =
35206 Self::extract_interval_parts(&interval_expr).unwrap_or_else(|| {
35207 (interval_expr.clone(), crate::expressions::IntervalUnit::Day)
35208 });
35209 let unit_str = Self::interval_unit_to_string(&unit);
35210
35211 match target {
35212 DialectType::DuckDB => {
35213 // DuckDB: CAST(date AS DATE) + INTERVAL 'val' UNIT
35214 let cast_date = Self::ensure_cast_date(date);
35215 let quoted_val = Self::quote_interval_val(&val);
35216 let interval =
35217 Expression::Interval(Box::new(crate::expressions::Interval {
35218 this: Some(quoted_val),
35219 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
35220 unit,
35221 use_plural: false,
35222 }),
35223 }));
35224 Ok(Expression::Add(Box::new(
35225 crate::expressions::BinaryOp::new(cast_date, interval),
35226 )))
35227 }
35228 DialectType::PostgreSQL => {
35229 // PostgreSQL: date + INTERVAL 'val UNIT'
35230 let interval =
35231 Expression::Interval(Box::new(crate::expressions::Interval {
35232 this: Some(Expression::Literal(Box::new(Literal::String(
35233 format!("{} {}", Self::expr_to_string(&val), unit_str),
35234 )))),
35235 unit: None,
35236 }));
35237 Ok(Expression::Add(Box::new(
35238 crate::expressions::BinaryOp::new(date, interval),
35239 )))
35240 }
35241 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
35242 // Presto: DATE_ADD('UNIT', CAST('val' AS BIGINT), date)
35243 let val_str = Self::expr_to_string(&val);
35244 Ok(Expression::Function(Box::new(Function::new(
35245 "DATE_ADD".to_string(),
35246 vec![
35247 Expression::Literal(Box::new(Literal::String(
35248 unit_str.to_string(),
35249 ))),
35250 Expression::Cast(Box::new(Cast {
35251 this: Expression::Literal(Box::new(Literal::String(val_str))),
35252 to: DataType::BigInt { length: None },
35253 trailing_comments: vec![],
35254 double_colon_syntax: false,
35255 format: None,
35256 default: None,
35257 inferred_type: None,
35258 })),
35259 date,
35260 ],
35261 ))))
35262 }
35263 DialectType::Spark | DialectType::Hive => {
35264 // Spark/Hive: DATE_ADD(date, val) for DAY
35265 match unit_str {
35266 "DAY" => Ok(Expression::Function(Box::new(Function::new(
35267 "DATE_ADD".to_string(),
35268 vec![date, val],
35269 )))),
35270 "MONTH" => Ok(Expression::Function(Box::new(Function::new(
35271 "ADD_MONTHS".to_string(),
35272 vec![date, val],
35273 )))),
35274 _ => {
35275 let iu = parse_interval_unit(&unit_str);
35276 let interval =
35277 Expression::Interval(Box::new(crate::expressions::Interval {
35278 this: Some(val),
35279 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
35280 unit: iu,
35281 use_plural: false,
35282 }),
35283 }));
35284 Ok(Expression::Function(Box::new(Function::new(
35285 "DATE_ADD".to_string(),
35286 vec![date, interval],
35287 ))))
35288 }
35289 }
35290 }
35291 DialectType::Snowflake => {
35292 // Snowflake: DATEADD(UNIT, 'val', CAST(date AS DATE))
35293 let cast_date = Self::ensure_cast_date(date);
35294 let val_str = Self::expr_to_string(&val);
35295 Ok(Expression::Function(Box::new(Function::new(
35296 "DATEADD".to_string(),
35297 vec![
35298 Expression::Identifier(Identifier::new(unit_str)),
35299 Expression::Literal(Box::new(Literal::String(val_str))),
35300 cast_date,
35301 ],
35302 ))))
35303 }
35304 DialectType::TSQL | DialectType::Fabric => {
35305 let cast_date = Self::ensure_cast_datetime2(date);
35306 Ok(Expression::Function(Box::new(Function::new(
35307 "DATEADD".to_string(),
35308 vec![
35309 Expression::Identifier(Identifier::new(unit_str)),
35310 val,
35311 cast_date,
35312 ],
35313 ))))
35314 }
35315 DialectType::Redshift => Ok(Expression::Function(Box::new(Function::new(
35316 "DATEADD".to_string(),
35317 vec![Expression::Identifier(Identifier::new(unit_str)), val, date],
35318 )))),
35319 DialectType::MySQL => {
35320 // MySQL: DATE_ADD(date, INTERVAL 'val' UNIT)
35321 let quoted_val = Self::quote_interval_val(&val);
35322 let iu = parse_interval_unit(&unit_str);
35323 let interval =
35324 Expression::Interval(Box::new(crate::expressions::Interval {
35325 this: Some(quoted_val),
35326 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
35327 unit: iu,
35328 use_plural: false,
35329 }),
35330 }));
35331 Ok(Expression::Function(Box::new(Function::new(
35332 "DATE_ADD".to_string(),
35333 vec![date, interval],
35334 ))))
35335 }
35336 DialectType::BigQuery => {
35337 // BigQuery: DATE_ADD(date, INTERVAL 'val' UNIT)
35338 let quoted_val = Self::quote_interval_val(&val);
35339 let iu = parse_interval_unit(&unit_str);
35340 let interval =
35341 Expression::Interval(Box::new(crate::expressions::Interval {
35342 this: Some(quoted_val),
35343 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
35344 unit: iu,
35345 use_plural: false,
35346 }),
35347 }));
35348 Ok(Expression::Function(Box::new(Function::new(
35349 "DATE_ADD".to_string(),
35350 vec![date, interval],
35351 ))))
35352 }
35353 DialectType::Databricks => Ok(Expression::Function(Box::new(Function::new(
35354 "DATEADD".to_string(),
35355 vec![Expression::Identifier(Identifier::new(unit_str)), val, date],
35356 )))),
35357 _ => {
35358 // Default: keep as DATE_ADD with decomposed interval
35359 Ok(Expression::DateAdd(Box::new(
35360 crate::expressions::DateAddFunc {
35361 this: date,
35362 interval: val,
35363 unit,
35364 },
35365 )))
35366 }
35367 }
35368 }
35369
35370 // ADD_MONTHS(date, val) -> target-specific form
35371 "ADD_MONTHS" if args.len() == 2 => {
35372 let date = args.remove(0);
35373 let val = args.remove(0);
35374
35375 if matches!(target, DialectType::TSQL) {
35376 // TSQL: DATEADD(MONTH, val, CAST(date AS DATETIME2))
35377 let cast_date = Self::ensure_cast_datetime2(date);
35378 return Ok(Expression::Function(Box::new(Function::new(
35379 "DATEADD".to_string(),
35380 vec![
35381 Expression::Identifier(Identifier::new("MONTH")),
35382 val,
35383 cast_date,
35384 ],
35385 ))));
35386 }
35387
35388 if matches!(target, DialectType::DuckDB) {
35389 // DuckDB: date + INTERVAL val MONTH
35390 let interval = Expression::Interval(Box::new(crate::expressions::Interval {
35391 this: Some(val),
35392 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
35393 unit: crate::expressions::IntervalUnit::Month,
35394 use_plural: false,
35395 }),
35396 }));
35397 return Ok(Expression::Add(Box::new(
35398 crate::expressions::BinaryOp::new(date, interval),
35399 )));
35400 }
35401
35402 if matches!(target, DialectType::Snowflake) {
35403 // Snowflake: keep ADD_MONTHS when source is also Snowflake, else DATEADD
35404 if matches!(source, DialectType::Snowflake) {
35405 return Ok(Expression::Function(Box::new(Function::new(
35406 "ADD_MONTHS".to_string(),
35407 vec![date, val],
35408 ))));
35409 }
35410 return Ok(Expression::Function(Box::new(Function::new(
35411 "DATEADD".to_string(),
35412 vec![Expression::Identifier(Identifier::new("MONTH")), val, date],
35413 ))));
35414 }
35415
35416 if matches!(target, DialectType::Spark | DialectType::Databricks) {
35417 // Spark: ADD_MONTHS(date, val) - keep as is
35418 return Ok(Expression::Function(Box::new(Function::new(
35419 "ADD_MONTHS".to_string(),
35420 vec![date, val],
35421 ))));
35422 }
35423
35424 if matches!(target, DialectType::Hive) {
35425 return Ok(Expression::Function(Box::new(Function::new(
35426 "ADD_MONTHS".to_string(),
35427 vec![date, val],
35428 ))));
35429 }
35430
35431 if matches!(
35432 target,
35433 DialectType::Presto | DialectType::Trino | DialectType::Athena
35434 ) {
35435 // Presto: DATE_ADD('MONTH', val, date)
35436 return Ok(Expression::Function(Box::new(Function::new(
35437 "DATE_ADD".to_string(),
35438 vec![
35439 Expression::Literal(Box::new(Literal::String("MONTH".to_string()))),
35440 val,
35441 date,
35442 ],
35443 ))));
35444 }
35445
35446 // Default: keep ADD_MONTHS
35447 Ok(Expression::Function(Box::new(Function::new(
35448 "ADD_MONTHS".to_string(),
35449 vec![date, val],
35450 ))))
35451 }
35452
35453 // SAFE_DIVIDE(x, y) -> target-specific form directly
35454 "SAFE_DIVIDE" if args.len() == 2 => {
35455 let x = args.remove(0);
35456 let y = args.remove(0);
35457 // Wrap x and y in parens if they're complex expressions
35458 let y_ref = match &y {
35459 Expression::Column(_) | Expression::Literal(_) | Expression::Identifier(_) => {
35460 y.clone()
35461 }
35462 _ => Expression::Paren(Box::new(Paren {
35463 this: y.clone(),
35464 trailing_comments: vec![],
35465 })),
35466 };
35467 let x_ref = match &x {
35468 Expression::Column(_) | Expression::Literal(_) | Expression::Identifier(_) => {
35469 x.clone()
35470 }
35471 _ => Expression::Paren(Box::new(Paren {
35472 this: x.clone(),
35473 trailing_comments: vec![],
35474 })),
35475 };
35476 let condition = Expression::Neq(Box::new(crate::expressions::BinaryOp::new(
35477 y_ref.clone(),
35478 Expression::number(0),
35479 )));
35480 let div_expr = Expression::Div(Box::new(crate::expressions::BinaryOp::new(
35481 x_ref.clone(),
35482 y_ref.clone(),
35483 )));
35484
35485 match target {
35486 DialectType::Spark | DialectType::Databricks => Ok(Expression::Function(
35487 Box::new(Function::new("TRY_DIVIDE".to_string(), vec![x, y])),
35488 )),
35489 DialectType::DuckDB | DialectType::PostgreSQL => {
35490 // CASE WHEN y <> 0 THEN x / y ELSE NULL END
35491 let result_div = if matches!(target, DialectType::PostgreSQL) {
35492 let cast_x = Expression::Cast(Box::new(Cast {
35493 this: x_ref,
35494 to: DataType::Custom {
35495 name: "DOUBLE PRECISION".to_string(),
35496 },
35497 trailing_comments: vec![],
35498 double_colon_syntax: false,
35499 format: None,
35500 default: None,
35501 inferred_type: None,
35502 }));
35503 Expression::Div(Box::new(crate::expressions::BinaryOp::new(
35504 cast_x, y_ref,
35505 )))
35506 } else {
35507 div_expr
35508 };
35509 Ok(Expression::Case(Box::new(crate::expressions::Case {
35510 operand: None,
35511 whens: vec![(condition, result_div)],
35512 else_: Some(Expression::Null(crate::expressions::Null)),
35513 comments: Vec::new(),
35514 inferred_type: None,
35515 })))
35516 }
35517 DialectType::Snowflake => {
35518 // IFF(y <> 0, x / y, NULL)
35519 Ok(Expression::IfFunc(Box::new(crate::expressions::IfFunc {
35520 condition,
35521 true_value: div_expr,
35522 false_value: Some(Expression::Null(crate::expressions::Null)),
35523 original_name: Some("IFF".to_string()),
35524 inferred_type: None,
35525 })))
35526 }
35527 DialectType::Presto | DialectType::Trino => {
35528 // IF(y <> 0, CAST(x AS DOUBLE) / y, NULL)
35529 let cast_x = Expression::Cast(Box::new(Cast {
35530 this: x_ref,
35531 to: DataType::Double {
35532 precision: None,
35533 scale: None,
35534 },
35535 trailing_comments: vec![],
35536 double_colon_syntax: false,
35537 format: None,
35538 default: None,
35539 inferred_type: None,
35540 }));
35541 let cast_div = Expression::Div(Box::new(
35542 crate::expressions::BinaryOp::new(cast_x, y_ref),
35543 ));
35544 Ok(Expression::IfFunc(Box::new(crate::expressions::IfFunc {
35545 condition,
35546 true_value: cast_div,
35547 false_value: Some(Expression::Null(crate::expressions::Null)),
35548 original_name: None,
35549 inferred_type: None,
35550 })))
35551 }
35552 _ => {
35553 // IF(y <> 0, x / y, NULL)
35554 Ok(Expression::IfFunc(Box::new(crate::expressions::IfFunc {
35555 condition,
35556 true_value: div_expr,
35557 false_value: Some(Expression::Null(crate::expressions::Null)),
35558 original_name: None,
35559 inferred_type: None,
35560 })))
35561 }
35562 }
35563 }
35564
35565 // GENERATE_UUID() -> UUID() with CAST to string
35566 "GENERATE_UUID" => {
35567 let uuid_expr = Expression::Uuid(Box::new(crate::expressions::Uuid {
35568 this: None,
35569 name: None,
35570 is_string: None,
35571 }));
35572 // Most targets need CAST(UUID() AS TEXT/VARCHAR/STRING)
35573 let cast_type = match target {
35574 DialectType::DuckDB => Some(DataType::Text),
35575 DialectType::Presto | DialectType::Trino => Some(DataType::VarChar {
35576 length: None,
35577 parenthesized_length: false,
35578 }),
35579 DialectType::Spark | DialectType::Databricks | DialectType::Hive => {
35580 Some(DataType::String { length: None })
35581 }
35582 _ => None,
35583 };
35584 if let Some(dt) = cast_type {
35585 Ok(Expression::Cast(Box::new(Cast {
35586 this: uuid_expr,
35587 to: dt,
35588 trailing_comments: vec![],
35589 double_colon_syntax: false,
35590 format: None,
35591 default: None,
35592 inferred_type: None,
35593 })))
35594 } else {
35595 Ok(uuid_expr)
35596 }
35597 }
35598
35599 // COUNTIF(x) -> CountIf expression
35600 "COUNTIF" if args.len() == 1 => {
35601 let arg = args.remove(0);
35602 Ok(Expression::CountIf(Box::new(crate::expressions::AggFunc {
35603 this: arg,
35604 distinct: false,
35605 filter: None,
35606 order_by: vec![],
35607 name: None,
35608 ignore_nulls: None,
35609 having_max: None,
35610 limit: None,
35611 inferred_type: None,
35612 })))
35613 }
35614
35615 // EDIT_DISTANCE(col1, col2, ...) -> Levenshtein expression
35616 "EDIT_DISTANCE" => {
35617 // Strip named arguments (max_distance => N) and pass as positional
35618 let mut positional_args: Vec<Expression> = vec![];
35619 for arg in args {
35620 match arg {
35621 Expression::NamedArgument(na) => {
35622 positional_args.push(na.value);
35623 }
35624 other => positional_args.push(other),
35625 }
35626 }
35627 if positional_args.len() >= 2 {
35628 let col1 = positional_args.remove(0);
35629 let col2 = positional_args.remove(0);
35630 let levenshtein = crate::expressions::BinaryFunc {
35631 this: col1,
35632 expression: col2,
35633 original_name: None,
35634 inferred_type: None,
35635 };
35636 // Pass extra args through a function wrapper with all args
35637 if !positional_args.is_empty() {
35638 let max_dist = positional_args.remove(0);
35639 // DuckDB: CASE WHEN LEVENSHTEIN(a, b) IS NULL OR max IS NULL THEN NULL ELSE LEAST(LEVENSHTEIN(a, b), max) END
35640 if matches!(target, DialectType::DuckDB) {
35641 let lev = Expression::Function(Box::new(Function::new(
35642 "LEVENSHTEIN".to_string(),
35643 vec![levenshtein.this, levenshtein.expression],
35644 )));
35645 let lev_is_null =
35646 Expression::IsNull(Box::new(crate::expressions::IsNull {
35647 this: lev.clone(),
35648 not: false,
35649 postfix_form: false,
35650 }));
35651 let max_is_null =
35652 Expression::IsNull(Box::new(crate::expressions::IsNull {
35653 this: max_dist.clone(),
35654 not: false,
35655 postfix_form: false,
35656 }));
35657 let null_check =
35658 Expression::Or(Box::new(crate::expressions::BinaryOp {
35659 left: lev_is_null,
35660 right: max_is_null,
35661 left_comments: Vec::new(),
35662 operator_comments: Vec::new(),
35663 trailing_comments: Vec::new(),
35664 inferred_type: None,
35665 }));
35666 let least =
35667 Expression::Least(Box::new(crate::expressions::VarArgFunc {
35668 expressions: vec![lev, max_dist],
35669 original_name: None,
35670 inferred_type: None,
35671 }));
35672 return Ok(Expression::Case(Box::new(crate::expressions::Case {
35673 operand: None,
35674 whens: vec![(
35675 null_check,
35676 Expression::Null(crate::expressions::Null),
35677 )],
35678 else_: Some(least),
35679 comments: Vec::new(),
35680 inferred_type: None,
35681 })));
35682 }
35683 let mut all_args = vec![levenshtein.this, levenshtein.expression, max_dist];
35684 all_args.extend(positional_args);
35685 // PostgreSQL: use LEVENSHTEIN_LESS_EQUAL when max_distance is provided
35686 let func_name = if matches!(target, DialectType::PostgreSQL) {
35687 "LEVENSHTEIN_LESS_EQUAL"
35688 } else {
35689 "LEVENSHTEIN"
35690 };
35691 return Ok(Expression::Function(Box::new(Function::new(
35692 func_name.to_string(),
35693 all_args,
35694 ))));
35695 }
35696 Ok(Expression::Levenshtein(Box::new(levenshtein)))
35697 } else {
35698 Ok(Expression::Function(Box::new(Function::new(
35699 "EDIT_DISTANCE".to_string(),
35700 positional_args,
35701 ))))
35702 }
35703 }
35704
35705 // TIMESTAMP_SECONDS(x) -> UnixToTime with scale 0
35706 "TIMESTAMP_SECONDS" if args.len() == 1 => {
35707 let arg = args.remove(0);
35708 Ok(Expression::UnixToTime(Box::new(
35709 crate::expressions::UnixToTime {
35710 this: Box::new(arg),
35711 scale: Some(0),
35712 zone: None,
35713 hours: None,
35714 minutes: None,
35715 format: None,
35716 target_type: None,
35717 },
35718 )))
35719 }
35720
35721 // TIMESTAMP_MILLIS(x) -> UnixToTime with scale 3
35722 "TIMESTAMP_MILLIS" if args.len() == 1 => {
35723 let arg = args.remove(0);
35724 Ok(Expression::UnixToTime(Box::new(
35725 crate::expressions::UnixToTime {
35726 this: Box::new(arg),
35727 scale: Some(3),
35728 zone: None,
35729 hours: None,
35730 minutes: None,
35731 format: None,
35732 target_type: None,
35733 },
35734 )))
35735 }
35736
35737 // TIMESTAMP_MICROS(x) -> UnixToTime with scale 6
35738 "TIMESTAMP_MICROS" if args.len() == 1 => {
35739 let arg = args.remove(0);
35740 Ok(Expression::UnixToTime(Box::new(
35741 crate::expressions::UnixToTime {
35742 this: Box::new(arg),
35743 scale: Some(6),
35744 zone: None,
35745 hours: None,
35746 minutes: None,
35747 format: None,
35748 target_type: None,
35749 },
35750 )))
35751 }
35752
35753 // DIV(x, y) -> IntDiv expression
35754 "DIV" if args.len() == 2 => {
35755 let x = args.remove(0);
35756 let y = args.remove(0);
35757 Ok(Expression::IntDiv(Box::new(
35758 crate::expressions::BinaryFunc {
35759 this: x,
35760 expression: y,
35761 original_name: None,
35762 inferred_type: None,
35763 },
35764 )))
35765 }
35766
35767 // TO_HEX(x) -> target-specific form
35768 "TO_HEX" if args.len() == 1 => {
35769 let arg = args.remove(0);
35770 // Check if inner function already returns hex string in certain targets
35771 let inner_returns_hex = matches!(&arg, Expression::Function(f) if matches!(f.name.as_str(), "MD5" | "SHA1" | "SHA256" | "SHA512"));
35772 if matches!(target, DialectType::BigQuery) {
35773 // BQ->BQ: keep as TO_HEX
35774 Ok(Expression::Function(Box::new(Function::new(
35775 "TO_HEX".to_string(),
35776 vec![arg],
35777 ))))
35778 } else if matches!(target, DialectType::DuckDB) && inner_returns_hex {
35779 // DuckDB: MD5/SHA already return hex strings, so TO_HEX is redundant
35780 Ok(arg)
35781 } else if matches!(target, DialectType::Snowflake) && inner_returns_hex {
35782 // Snowflake: TO_HEX(SHA1(x)) -> TO_CHAR(SHA1_BINARY(x))
35783 // TO_HEX(MD5(x)) -> TO_CHAR(MD5_BINARY(x))
35784 // TO_HEX(SHA256(x)) -> TO_CHAR(SHA2_BINARY(x, 256))
35785 // TO_HEX(SHA512(x)) -> TO_CHAR(SHA2_BINARY(x, 512))
35786 if let Expression::Function(ref inner_f) = arg {
35787 let inner_args = inner_f.args.clone();
35788 let binary_func = match inner_f.name.to_ascii_uppercase().as_str() {
35789 "SHA1" => Expression::Function(Box::new(Function::new(
35790 "SHA1_BINARY".to_string(),
35791 inner_args,
35792 ))),
35793 "MD5" => Expression::Function(Box::new(Function::new(
35794 "MD5_BINARY".to_string(),
35795 inner_args,
35796 ))),
35797 "SHA256" => {
35798 let mut a = inner_args;
35799 a.push(Expression::number(256));
35800 Expression::Function(Box::new(Function::new(
35801 "SHA2_BINARY".to_string(),
35802 a,
35803 )))
35804 }
35805 "SHA512" => {
35806 let mut a = inner_args;
35807 a.push(Expression::number(512));
35808 Expression::Function(Box::new(Function::new(
35809 "SHA2_BINARY".to_string(),
35810 a,
35811 )))
35812 }
35813 _ => arg.clone(),
35814 };
35815 Ok(Expression::Function(Box::new(Function::new(
35816 "TO_CHAR".to_string(),
35817 vec![binary_func],
35818 ))))
35819 } else {
35820 let inner = Expression::Function(Box::new(Function::new(
35821 "HEX".to_string(),
35822 vec![arg],
35823 )));
35824 Ok(Expression::Lower(Box::new(
35825 crate::expressions::UnaryFunc::new(inner),
35826 )))
35827 }
35828 } else if matches!(target, DialectType::Presto | DialectType::Trino) {
35829 let inner = Expression::Function(Box::new(Function::new(
35830 "TO_HEX".to_string(),
35831 vec![arg],
35832 )));
35833 Ok(Expression::Lower(Box::new(
35834 crate::expressions::UnaryFunc::new(inner),
35835 )))
35836 } else {
35837 let inner =
35838 Expression::Function(Box::new(Function::new("HEX".to_string(), vec![arg])));
35839 Ok(Expression::Lower(Box::new(
35840 crate::expressions::UnaryFunc::new(inner),
35841 )))
35842 }
35843 }
35844
35845 // LAST_DAY(date, unit) -> strip unit for most targets, or transform for PostgreSQL
35846 "LAST_DAY" if args.len() == 2 => {
35847 let date = args.remove(0);
35848 let _unit = args.remove(0); // Strip the unit (MONTH is default)
35849 Ok(Expression::Function(Box::new(Function::new(
35850 "LAST_DAY".to_string(),
35851 vec![date],
35852 ))))
35853 }
35854
35855 // GENERATE_ARRAY(start, end, step?) -> GenerateSeries expression
35856 "GENERATE_ARRAY" => {
35857 let start = args.get(0).cloned();
35858 let end = args.get(1).cloned();
35859 let step = args.get(2).cloned();
35860 Ok(Expression::GenerateSeries(Box::new(
35861 crate::expressions::GenerateSeries {
35862 start: start.map(Box::new),
35863 end: end.map(Box::new),
35864 step: step.map(Box::new),
35865 is_end_exclusive: None,
35866 },
35867 )))
35868 }
35869
35870 // GENERATE_TIMESTAMP_ARRAY(start, end, step) -> GenerateSeries expression
35871 "GENERATE_TIMESTAMP_ARRAY" => {
35872 let start = args.get(0).cloned();
35873 let end = args.get(1).cloned();
35874 let step = args.get(2).cloned();
35875
35876 if matches!(target, DialectType::DuckDB) {
35877 // DuckDB: GENERATE_SERIES(CAST(start AS TIMESTAMP), CAST(end AS TIMESTAMP), step)
35878 // Only cast string literals - leave columns/expressions as-is
35879 let maybe_cast_ts = |expr: Expression| -> Expression {
35880 if matches!(&expr, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)))
35881 {
35882 Expression::Cast(Box::new(Cast {
35883 this: expr,
35884 to: DataType::Timestamp {
35885 precision: None,
35886 timezone: false,
35887 },
35888 trailing_comments: vec![],
35889 double_colon_syntax: false,
35890 format: None,
35891 default: None,
35892 inferred_type: None,
35893 }))
35894 } else {
35895 expr
35896 }
35897 };
35898 let cast_start = start.map(maybe_cast_ts);
35899 let cast_end = end.map(maybe_cast_ts);
35900 Ok(Expression::GenerateSeries(Box::new(
35901 crate::expressions::GenerateSeries {
35902 start: cast_start.map(Box::new),
35903 end: cast_end.map(Box::new),
35904 step: step.map(Box::new),
35905 is_end_exclusive: None,
35906 },
35907 )))
35908 } else {
35909 Ok(Expression::GenerateSeries(Box::new(
35910 crate::expressions::GenerateSeries {
35911 start: start.map(Box::new),
35912 end: end.map(Box::new),
35913 step: step.map(Box::new),
35914 is_end_exclusive: None,
35915 },
35916 )))
35917 }
35918 }
35919
35920 // TO_JSON(x) -> target-specific (from Spark/Hive)
35921 "TO_JSON" => {
35922 match target {
35923 DialectType::Presto | DialectType::Trino => {
35924 // JSON_FORMAT(CAST(x AS JSON))
35925 let arg = args
35926 .into_iter()
35927 .next()
35928 .unwrap_or(Expression::Null(crate::expressions::Null));
35929 let cast_json = Expression::Cast(Box::new(Cast {
35930 this: arg,
35931 to: DataType::Custom {
35932 name: "JSON".to_string(),
35933 },
35934 trailing_comments: vec![],
35935 double_colon_syntax: false,
35936 format: None,
35937 default: None,
35938 inferred_type: None,
35939 }));
35940 Ok(Expression::Function(Box::new(Function::new(
35941 "JSON_FORMAT".to_string(),
35942 vec![cast_json],
35943 ))))
35944 }
35945 DialectType::BigQuery => Ok(Expression::Function(Box::new(Function::new(
35946 "TO_JSON_STRING".to_string(),
35947 args,
35948 )))),
35949 DialectType::DuckDB => {
35950 // CAST(TO_JSON(x) AS TEXT)
35951 let arg = args
35952 .into_iter()
35953 .next()
35954 .unwrap_or(Expression::Null(crate::expressions::Null));
35955 let to_json = Expression::Function(Box::new(Function::new(
35956 "TO_JSON".to_string(),
35957 vec![arg],
35958 )));
35959 Ok(Expression::Cast(Box::new(Cast {
35960 this: to_json,
35961 to: DataType::Text,
35962 trailing_comments: vec![],
35963 double_colon_syntax: false,
35964 format: None,
35965 default: None,
35966 inferred_type: None,
35967 })))
35968 }
35969 _ => Ok(Expression::Function(Box::new(Function::new(
35970 "TO_JSON".to_string(),
35971 args,
35972 )))),
35973 }
35974 }
35975
35976 // TO_JSON_STRING(x) -> target-specific
35977 "TO_JSON_STRING" => {
35978 match target {
35979 DialectType::Spark | DialectType::Databricks | DialectType::Hive => Ok(
35980 Expression::Function(Box::new(Function::new("TO_JSON".to_string(), args))),
35981 ),
35982 DialectType::Presto | DialectType::Trino => {
35983 // JSON_FORMAT(CAST(x AS JSON))
35984 let arg = args
35985 .into_iter()
35986 .next()
35987 .unwrap_or(Expression::Null(crate::expressions::Null));
35988 let cast_json = Expression::Cast(Box::new(Cast {
35989 this: arg,
35990 to: DataType::Custom {
35991 name: "JSON".to_string(),
35992 },
35993 trailing_comments: vec![],
35994 double_colon_syntax: false,
35995 format: None,
35996 default: None,
35997 inferred_type: None,
35998 }));
35999 Ok(Expression::Function(Box::new(Function::new(
36000 "JSON_FORMAT".to_string(),
36001 vec![cast_json],
36002 ))))
36003 }
36004 DialectType::DuckDB => {
36005 // CAST(TO_JSON(x) AS TEXT)
36006 let arg = args
36007 .into_iter()
36008 .next()
36009 .unwrap_or(Expression::Null(crate::expressions::Null));
36010 let to_json = Expression::Function(Box::new(Function::new(
36011 "TO_JSON".to_string(),
36012 vec![arg],
36013 )));
36014 Ok(Expression::Cast(Box::new(Cast {
36015 this: to_json,
36016 to: DataType::Text,
36017 trailing_comments: vec![],
36018 double_colon_syntax: false,
36019 format: None,
36020 default: None,
36021 inferred_type: None,
36022 })))
36023 }
36024 DialectType::Snowflake => {
36025 // TO_JSON(x)
36026 Ok(Expression::Function(Box::new(Function::new(
36027 "TO_JSON".to_string(),
36028 args,
36029 ))))
36030 }
36031 _ => Ok(Expression::Function(Box::new(Function::new(
36032 "TO_JSON_STRING".to_string(),
36033 args,
36034 )))),
36035 }
36036 }
36037
36038 // SAFE_ADD(x, y) -> SafeAdd expression
36039 "SAFE_ADD" if args.len() == 2 => {
36040 let x = args.remove(0);
36041 let y = args.remove(0);
36042 Ok(Expression::SafeAdd(Box::new(crate::expressions::SafeAdd {
36043 this: Box::new(x),
36044 expression: Box::new(y),
36045 })))
36046 }
36047
36048 // SAFE_SUBTRACT(x, y) -> SafeSubtract expression
36049 "SAFE_SUBTRACT" if args.len() == 2 => {
36050 let x = args.remove(0);
36051 let y = args.remove(0);
36052 Ok(Expression::SafeSubtract(Box::new(
36053 crate::expressions::SafeSubtract {
36054 this: Box::new(x),
36055 expression: Box::new(y),
36056 },
36057 )))
36058 }
36059
36060 // SAFE_MULTIPLY(x, y) -> SafeMultiply expression
36061 "SAFE_MULTIPLY" if args.len() == 2 => {
36062 let x = args.remove(0);
36063 let y = args.remove(0);
36064 Ok(Expression::SafeMultiply(Box::new(
36065 crate::expressions::SafeMultiply {
36066 this: Box::new(x),
36067 expression: Box::new(y),
36068 },
36069 )))
36070 }
36071
36072 // REGEXP_CONTAINS(str, pattern) -> RegexpLike expression
36073 "REGEXP_CONTAINS" if args.len() == 2 => {
36074 let str_expr = args.remove(0);
36075 let pattern = args.remove(0);
36076 Ok(Expression::RegexpLike(Box::new(
36077 crate::expressions::RegexpFunc {
36078 this: str_expr,
36079 pattern,
36080 flags: None,
36081 },
36082 )))
36083 }
36084
36085 // CONTAINS_SUBSTR(a, b) -> CONTAINS(LOWER(a), LOWER(b))
36086 "CONTAINS_SUBSTR" if args.len() == 2 => {
36087 let a = args.remove(0);
36088 let b = args.remove(0);
36089 let lower_a = Expression::Lower(Box::new(crate::expressions::UnaryFunc::new(a)));
36090 let lower_b = Expression::Lower(Box::new(crate::expressions::UnaryFunc::new(b)));
36091 Ok(Expression::Function(Box::new(Function::new(
36092 "CONTAINS".to_string(),
36093 vec![lower_a, lower_b],
36094 ))))
36095 }
36096
36097 // INT64(x) -> CAST(x AS BIGINT)
36098 "INT64" if args.len() == 1 => {
36099 let arg = args.remove(0);
36100 Ok(Expression::Cast(Box::new(Cast {
36101 this: arg,
36102 to: DataType::BigInt { length: None },
36103 trailing_comments: vec![],
36104 double_colon_syntax: false,
36105 format: None,
36106 default: None,
36107 inferred_type: None,
36108 })))
36109 }
36110
36111 // INSTR(str, substr) -> target-specific
36112 "INSTR" if args.len() >= 2 => {
36113 let str_expr = args.remove(0);
36114 let substr = args.remove(0);
36115 if matches!(target, DialectType::Snowflake) {
36116 // CHARINDEX(substr, str)
36117 Ok(Expression::Function(Box::new(Function::new(
36118 "CHARINDEX".to_string(),
36119 vec![substr, str_expr],
36120 ))))
36121 } else if matches!(target, DialectType::BigQuery) {
36122 // Keep as INSTR
36123 Ok(Expression::Function(Box::new(Function::new(
36124 "INSTR".to_string(),
36125 vec![str_expr, substr],
36126 ))))
36127 } else {
36128 // Default: keep as INSTR
36129 Ok(Expression::Function(Box::new(Function::new(
36130 "INSTR".to_string(),
36131 vec![str_expr, substr],
36132 ))))
36133 }
36134 }
36135
36136 // BigQuery DATE_TRUNC(expr, unit) -> DATE_TRUNC('unit', expr) for standard SQL
36137 "DATE_TRUNC" if args.len() == 2 => {
36138 let expr = args.remove(0);
36139 let unit_expr = args.remove(0);
36140 let unit_str = get_unit_str(&unit_expr);
36141
36142 match target {
36143 DialectType::DuckDB
36144 | DialectType::Snowflake
36145 | DialectType::PostgreSQL
36146 | DialectType::Presto
36147 | DialectType::Trino
36148 | DialectType::Databricks
36149 | DialectType::Spark
36150 | DialectType::Redshift
36151 | DialectType::ClickHouse
36152 | DialectType::TSQL => {
36153 // Standard: DATE_TRUNC('UNIT', expr)
36154 Ok(Expression::Function(Box::new(Function::new(
36155 "DATE_TRUNC".to_string(),
36156 vec![
36157 Expression::Literal(Box::new(Literal::String(unit_str))),
36158 expr,
36159 ],
36160 ))))
36161 }
36162 _ => {
36163 // Keep BigQuery arg order: DATE_TRUNC(expr, unit)
36164 Ok(Expression::Function(Box::new(Function::new(
36165 "DATE_TRUNC".to_string(),
36166 vec![expr, unit_expr],
36167 ))))
36168 }
36169 }
36170 }
36171
36172 // TIMESTAMP_TRUNC / DATETIME_TRUNC -> target-specific
36173 "TIMESTAMP_TRUNC" | "DATETIME_TRUNC" if args.len() >= 2 => {
36174 // TIMESTAMP_TRUNC(ts, unit) or TIMESTAMP_TRUNC(ts, unit, timezone)
36175 let ts = args.remove(0);
36176 let unit_expr = args.remove(0);
36177 let tz = if !args.is_empty() {
36178 Some(args.remove(0))
36179 } else {
36180 None
36181 };
36182 let unit_str = get_unit_str(&unit_expr);
36183
36184 match target {
36185 DialectType::DuckDB => {
36186 // DuckDB: DATE_TRUNC('UNIT', CAST(ts AS TIMESTAMPTZ))
36187 // With timezone: DATE_TRUNC('UNIT', ts AT TIME ZONE 'tz') AT TIME ZONE 'tz' (for DAY granularity)
36188 // Without timezone for MINUTE+ granularity: just DATE_TRUNC
36189 let is_coarse = matches!(
36190 unit_str.as_str(),
36191 "DAY" | "WEEK" | "MONTH" | "QUARTER" | "YEAR"
36192 );
36193 // For DATETIME_TRUNC, cast string args to TIMESTAMP
36194 let cast_ts = if name == "DATETIME_TRUNC" {
36195 match ts {
36196 Expression::Literal(ref lit)
36197 if matches!(lit.as_ref(), Literal::String(ref _s)) =>
36198 {
36199 Expression::Cast(Box::new(Cast {
36200 this: ts,
36201 to: DataType::Timestamp {
36202 precision: None,
36203 timezone: false,
36204 },
36205 trailing_comments: vec![],
36206 double_colon_syntax: false,
36207 format: None,
36208 default: None,
36209 inferred_type: None,
36210 }))
36211 }
36212 _ => Self::maybe_cast_ts_to_tz(ts, &name),
36213 }
36214 } else {
36215 Self::maybe_cast_ts_to_tz(ts, &name)
36216 };
36217
36218 if let Some(tz_arg) = tz {
36219 if is_coarse {
36220 // DATE_TRUNC('UNIT', ts AT TIME ZONE 'tz') AT TIME ZONE 'tz'
36221 let at_tz = Expression::AtTimeZone(Box::new(
36222 crate::expressions::AtTimeZone {
36223 this: cast_ts,
36224 zone: tz_arg.clone(),
36225 },
36226 ));
36227 let date_trunc = Expression::Function(Box::new(Function::new(
36228 "DATE_TRUNC".to_string(),
36229 vec![
36230 Expression::Literal(Box::new(Literal::String(unit_str))),
36231 at_tz,
36232 ],
36233 )));
36234 Ok(Expression::AtTimeZone(Box::new(
36235 crate::expressions::AtTimeZone {
36236 this: date_trunc,
36237 zone: tz_arg,
36238 },
36239 )))
36240 } else {
36241 // For MINUTE/HOUR: no AT TIME ZONE wrapper, just DATE_TRUNC('UNIT', ts)
36242 Ok(Expression::Function(Box::new(Function::new(
36243 "DATE_TRUNC".to_string(),
36244 vec![
36245 Expression::Literal(Box::new(Literal::String(unit_str))),
36246 cast_ts,
36247 ],
36248 ))))
36249 }
36250 } else {
36251 // No timezone: DATE_TRUNC('UNIT', CAST(ts AS TIMESTAMPTZ))
36252 Ok(Expression::Function(Box::new(Function::new(
36253 "DATE_TRUNC".to_string(),
36254 vec![
36255 Expression::Literal(Box::new(Literal::String(unit_str))),
36256 cast_ts,
36257 ],
36258 ))))
36259 }
36260 }
36261 DialectType::Databricks | DialectType::Spark => {
36262 // Databricks/Spark: DATE_TRUNC('UNIT', ts)
36263 Ok(Expression::Function(Box::new(Function::new(
36264 "DATE_TRUNC".to_string(),
36265 vec![Expression::Literal(Box::new(Literal::String(unit_str))), ts],
36266 ))))
36267 }
36268 _ => {
36269 // Default: keep as TIMESTAMP_TRUNC('UNIT', ts, [tz])
36270 let unit = Expression::Literal(Box::new(Literal::String(unit_str)));
36271 let mut date_trunc_args = vec![unit, ts];
36272 if let Some(tz_arg) = tz {
36273 date_trunc_args.push(tz_arg);
36274 }
36275 Ok(Expression::Function(Box::new(Function::new(
36276 "TIMESTAMP_TRUNC".to_string(),
36277 date_trunc_args,
36278 ))))
36279 }
36280 }
36281 }
36282
36283 // TIME(h, m, s) -> target-specific, TIME('string') -> CAST('string' AS TIME)
36284 "TIME" => {
36285 if args.len() == 3 {
36286 // TIME(h, m, s) constructor
36287 match target {
36288 DialectType::TSQL => {
36289 // TIMEFROMPARTS(h, m, s, 0, 0)
36290 args.push(Expression::number(0));
36291 args.push(Expression::number(0));
36292 Ok(Expression::Function(Box::new(Function::new(
36293 "TIMEFROMPARTS".to_string(),
36294 args,
36295 ))))
36296 }
36297 DialectType::MySQL => Ok(Expression::Function(Box::new(Function::new(
36298 "MAKETIME".to_string(),
36299 args,
36300 )))),
36301 DialectType::PostgreSQL => Ok(Expression::Function(Box::new(
36302 Function::new("MAKE_TIME".to_string(), args),
36303 ))),
36304 _ => Ok(Expression::Function(Box::new(Function::new(
36305 "TIME".to_string(),
36306 args,
36307 )))),
36308 }
36309 } else if args.len() == 1 {
36310 let arg = args.remove(0);
36311 if matches!(target, DialectType::Spark) {
36312 // Spark: CAST(x AS TIMESTAMP) (yes, TIMESTAMP not TIME)
36313 Ok(Expression::Cast(Box::new(Cast {
36314 this: arg,
36315 to: DataType::Timestamp {
36316 timezone: false,
36317 precision: None,
36318 },
36319 trailing_comments: vec![],
36320 double_colon_syntax: false,
36321 format: None,
36322 default: None,
36323 inferred_type: None,
36324 })))
36325 } else {
36326 // Most targets: CAST(x AS TIME)
36327 Ok(Expression::Cast(Box::new(Cast {
36328 this: arg,
36329 to: DataType::Time {
36330 precision: None,
36331 timezone: false,
36332 },
36333 trailing_comments: vec![],
36334 double_colon_syntax: false,
36335 format: None,
36336 default: None,
36337 inferred_type: None,
36338 })))
36339 }
36340 } else if args.len() == 2 {
36341 // TIME(expr, timezone) -> CAST(CAST(expr AS TIMESTAMPTZ) AT TIME ZONE tz AS TIME)
36342 let expr = args.remove(0);
36343 let tz = args.remove(0);
36344 let cast_tstz = Expression::Cast(Box::new(Cast {
36345 this: expr,
36346 to: DataType::Timestamp {
36347 timezone: true,
36348 precision: None,
36349 },
36350 trailing_comments: vec![],
36351 double_colon_syntax: false,
36352 format: None,
36353 default: None,
36354 inferred_type: None,
36355 }));
36356 let at_tz = Expression::AtTimeZone(Box::new(crate::expressions::AtTimeZone {
36357 this: cast_tstz,
36358 zone: tz,
36359 }));
36360 Ok(Expression::Cast(Box::new(Cast {
36361 this: at_tz,
36362 to: DataType::Time {
36363 precision: None,
36364 timezone: false,
36365 },
36366 trailing_comments: vec![],
36367 double_colon_syntax: false,
36368 format: None,
36369 default: None,
36370 inferred_type: None,
36371 })))
36372 } else {
36373 Ok(Expression::Function(Box::new(Function::new(
36374 "TIME".to_string(),
36375 args,
36376 ))))
36377 }
36378 }
36379
36380 // DATETIME('string') -> CAST('string' AS TIMESTAMP)
36381 // DATETIME('date', TIME 'time') -> CAST(CAST('date' AS DATE) + CAST('time' AS TIME) AS TIMESTAMP)
36382 // DATETIME('string', 'timezone') -> CAST(CAST('string' AS TIMESTAMPTZ) AT TIME ZONE tz AS TIMESTAMP)
36383 // DATETIME(y, m, d, h, min, s) -> target-specific
36384 "DATETIME" => {
36385 // For BigQuery target: keep DATETIME function but convert TIME literal to CAST
36386 if matches!(target, DialectType::BigQuery) {
36387 if args.len() == 2 {
36388 let has_time_literal = matches!(&args[1], Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Time(_)));
36389 if has_time_literal {
36390 let first = args.remove(0);
36391 let second = args.remove(0);
36392 let time_as_cast = match second {
36393 Expression::Literal(lit)
36394 if matches!(lit.as_ref(), Literal::Time(_)) =>
36395 {
36396 let Literal::Time(s) = lit.as_ref() else {
36397 unreachable!()
36398 };
36399 Expression::Cast(Box::new(Cast {
36400 this: Expression::Literal(Box::new(Literal::String(
36401 s.clone(),
36402 ))),
36403 to: DataType::Time {
36404 precision: None,
36405 timezone: false,
36406 },
36407 trailing_comments: vec![],
36408 double_colon_syntax: false,
36409 format: None,
36410 default: None,
36411 inferred_type: None,
36412 }))
36413 }
36414 other => other,
36415 };
36416 return Ok(Expression::Function(Box::new(Function::new(
36417 "DATETIME".to_string(),
36418 vec![first, time_as_cast],
36419 ))));
36420 }
36421 }
36422 return Ok(Expression::Function(Box::new(Function::new(
36423 "DATETIME".to_string(),
36424 args,
36425 ))));
36426 }
36427
36428 if args.len() == 1 {
36429 let arg = args.remove(0);
36430 Ok(Expression::Cast(Box::new(Cast {
36431 this: arg,
36432 to: DataType::Timestamp {
36433 timezone: false,
36434 precision: None,
36435 },
36436 trailing_comments: vec![],
36437 double_colon_syntax: false,
36438 format: None,
36439 default: None,
36440 inferred_type: None,
36441 })))
36442 } else if args.len() == 2 {
36443 let first = args.remove(0);
36444 let second = args.remove(0);
36445 // Check if second arg is a TIME literal
36446 let is_time_literal = matches!(&second, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Time(_)));
36447 if is_time_literal {
36448 // DATETIME('date', TIME 'time') -> CAST(CAST(date AS DATE) + CAST('time' AS TIME) AS TIMESTAMP)
36449 let cast_date = Expression::Cast(Box::new(Cast {
36450 this: first,
36451 to: DataType::Date,
36452 trailing_comments: vec![],
36453 double_colon_syntax: false,
36454 format: None,
36455 default: None,
36456 inferred_type: None,
36457 }));
36458 // Convert TIME 'x' literal to string 'x' so CAST produces CAST('x' AS TIME) not CAST(TIME 'x' AS TIME)
36459 let time_as_string = match second {
36460 Expression::Literal(lit)
36461 if matches!(lit.as_ref(), Literal::Time(_)) =>
36462 {
36463 let Literal::Time(s) = lit.as_ref() else {
36464 unreachable!()
36465 };
36466 Expression::Literal(Box::new(Literal::String(s.clone())))
36467 }
36468 other => other,
36469 };
36470 let cast_time = Expression::Cast(Box::new(Cast {
36471 this: time_as_string,
36472 to: DataType::Time {
36473 precision: None,
36474 timezone: false,
36475 },
36476 trailing_comments: vec![],
36477 double_colon_syntax: false,
36478 format: None,
36479 default: None,
36480 inferred_type: None,
36481 }));
36482 let add_expr =
36483 Expression::Add(Box::new(BinaryOp::new(cast_date, cast_time)));
36484 Ok(Expression::Cast(Box::new(Cast {
36485 this: add_expr,
36486 to: DataType::Timestamp {
36487 timezone: false,
36488 precision: None,
36489 },
36490 trailing_comments: vec![],
36491 double_colon_syntax: false,
36492 format: None,
36493 default: None,
36494 inferred_type: None,
36495 })))
36496 } else {
36497 // DATETIME('string', 'timezone')
36498 let cast_tstz = Expression::Cast(Box::new(Cast {
36499 this: first,
36500 to: DataType::Timestamp {
36501 timezone: true,
36502 precision: None,
36503 },
36504 trailing_comments: vec![],
36505 double_colon_syntax: false,
36506 format: None,
36507 default: None,
36508 inferred_type: None,
36509 }));
36510 let at_tz =
36511 Expression::AtTimeZone(Box::new(crate::expressions::AtTimeZone {
36512 this: cast_tstz,
36513 zone: second,
36514 }));
36515 Ok(Expression::Cast(Box::new(Cast {
36516 this: at_tz,
36517 to: DataType::Timestamp {
36518 timezone: false,
36519 precision: None,
36520 },
36521 trailing_comments: vec![],
36522 double_colon_syntax: false,
36523 format: None,
36524 default: None,
36525 inferred_type: None,
36526 })))
36527 }
36528 } else if args.len() >= 3 {
36529 // DATETIME(y, m, d, h, min, s) -> TIMESTAMP_FROM_PARTS for Snowflake
36530 // For other targets, use MAKE_TIMESTAMP or similar
36531 if matches!(target, DialectType::Snowflake) {
36532 Ok(Expression::Function(Box::new(Function::new(
36533 "TIMESTAMP_FROM_PARTS".to_string(),
36534 args,
36535 ))))
36536 } else {
36537 Ok(Expression::Function(Box::new(Function::new(
36538 "DATETIME".to_string(),
36539 args,
36540 ))))
36541 }
36542 } else {
36543 Ok(Expression::Function(Box::new(Function::new(
36544 "DATETIME".to_string(),
36545 args,
36546 ))))
36547 }
36548 }
36549
36550 // TIMESTAMP(x) -> CAST(x AS TIMESTAMP WITH TIME ZONE) for Presto
36551 // TIMESTAMP(x, tz) -> CAST(x AS TIMESTAMP) AT TIME ZONE tz for DuckDB
36552 "TIMESTAMP" => {
36553 if args.len() == 1 {
36554 let arg = args.remove(0);
36555 Ok(Expression::Cast(Box::new(Cast {
36556 this: arg,
36557 to: DataType::Timestamp {
36558 timezone: true,
36559 precision: None,
36560 },
36561 trailing_comments: vec![],
36562 double_colon_syntax: false,
36563 format: None,
36564 default: None,
36565 inferred_type: None,
36566 })))
36567 } else if args.len() == 2 {
36568 let arg = args.remove(0);
36569 let tz = args.remove(0);
36570 let cast_ts = Expression::Cast(Box::new(Cast {
36571 this: arg,
36572 to: DataType::Timestamp {
36573 timezone: false,
36574 precision: None,
36575 },
36576 trailing_comments: vec![],
36577 double_colon_syntax: false,
36578 format: None,
36579 default: None,
36580 inferred_type: None,
36581 }));
36582 if matches!(target, DialectType::Snowflake) {
36583 // CONVERT_TIMEZONE('tz', CAST(x AS TIMESTAMP))
36584 Ok(Expression::Function(Box::new(Function::new(
36585 "CONVERT_TIMEZONE".to_string(),
36586 vec![tz, cast_ts],
36587 ))))
36588 } else {
36589 Ok(Expression::AtTimeZone(Box::new(
36590 crate::expressions::AtTimeZone {
36591 this: cast_ts,
36592 zone: tz,
36593 },
36594 )))
36595 }
36596 } else {
36597 Ok(Expression::Function(Box::new(Function::new(
36598 "TIMESTAMP".to_string(),
36599 args,
36600 ))))
36601 }
36602 }
36603
36604 // STRING(x) -> CAST(x AS VARCHAR/TEXT)
36605 // STRING(x, tz) -> CAST(CAST(x AS TIMESTAMP) AT TIME ZONE 'UTC' AT TIME ZONE tz AS VARCHAR/TEXT)
36606 "STRING" => {
36607 if args.len() == 1 {
36608 let arg = args.remove(0);
36609 let cast_type = match target {
36610 DialectType::DuckDB => DataType::Text,
36611 _ => DataType::VarChar {
36612 length: None,
36613 parenthesized_length: false,
36614 },
36615 };
36616 Ok(Expression::Cast(Box::new(Cast {
36617 this: arg,
36618 to: cast_type,
36619 trailing_comments: vec![],
36620 double_colon_syntax: false,
36621 format: None,
36622 default: None,
36623 inferred_type: None,
36624 })))
36625 } else if args.len() == 2 {
36626 let arg = args.remove(0);
36627 let tz = args.remove(0);
36628 let cast_type = match target {
36629 DialectType::DuckDB => DataType::Text,
36630 _ => DataType::VarChar {
36631 length: None,
36632 parenthesized_length: false,
36633 },
36634 };
36635 if matches!(target, DialectType::Snowflake) {
36636 // STRING(x, tz) -> CAST(CONVERT_TIMEZONE('UTC', tz, x) AS VARCHAR)
36637 let convert_tz = Expression::Function(Box::new(Function::new(
36638 "CONVERT_TIMEZONE".to_string(),
36639 vec![
36640 Expression::Literal(Box::new(Literal::String("UTC".to_string()))),
36641 tz,
36642 arg,
36643 ],
36644 )));
36645 Ok(Expression::Cast(Box::new(Cast {
36646 this: convert_tz,
36647 to: cast_type,
36648 trailing_comments: vec![],
36649 double_colon_syntax: false,
36650 format: None,
36651 default: None,
36652 inferred_type: None,
36653 })))
36654 } else {
36655 // STRING(x, tz) -> CAST(CAST(x AS TIMESTAMP) AT TIME ZONE 'UTC' AT TIME ZONE tz AS TEXT/VARCHAR)
36656 let cast_ts = Expression::Cast(Box::new(Cast {
36657 this: arg,
36658 to: DataType::Timestamp {
36659 timezone: false,
36660 precision: None,
36661 },
36662 trailing_comments: vec![],
36663 double_colon_syntax: false,
36664 format: None,
36665 default: None,
36666 inferred_type: None,
36667 }));
36668 let at_utc =
36669 Expression::AtTimeZone(Box::new(crate::expressions::AtTimeZone {
36670 this: cast_ts,
36671 zone: Expression::Literal(Box::new(Literal::String(
36672 "UTC".to_string(),
36673 ))),
36674 }));
36675 let at_tz =
36676 Expression::AtTimeZone(Box::new(crate::expressions::AtTimeZone {
36677 this: at_utc,
36678 zone: tz,
36679 }));
36680 Ok(Expression::Cast(Box::new(Cast {
36681 this: at_tz,
36682 to: cast_type,
36683 trailing_comments: vec![],
36684 double_colon_syntax: false,
36685 format: None,
36686 default: None,
36687 inferred_type: None,
36688 })))
36689 }
36690 } else {
36691 Ok(Expression::Function(Box::new(Function::new(
36692 "STRING".to_string(),
36693 args,
36694 ))))
36695 }
36696 }
36697
36698 // UNIX_SECONDS, UNIX_MILLIS, UNIX_MICROS as functions (not expressions)
36699 "UNIX_SECONDS" if args.len() == 1 => {
36700 let ts = args.remove(0);
36701 match target {
36702 DialectType::DuckDB => {
36703 // CAST(EPOCH(CAST(ts AS TIMESTAMPTZ)) AS BIGINT)
36704 let cast_ts = Self::ensure_cast_timestamptz(ts);
36705 let epoch = Expression::Function(Box::new(Function::new(
36706 "EPOCH".to_string(),
36707 vec![cast_ts],
36708 )));
36709 Ok(Expression::Cast(Box::new(Cast {
36710 this: epoch,
36711 to: DataType::BigInt { length: None },
36712 trailing_comments: vec![],
36713 double_colon_syntax: false,
36714 format: None,
36715 default: None,
36716 inferred_type: None,
36717 })))
36718 }
36719 DialectType::Snowflake => {
36720 // TIMESTAMPDIFF(SECONDS, CAST('1970-01-01 00:00:00+00' AS TIMESTAMPTZ), ts)
36721 let epoch = Expression::Cast(Box::new(Cast {
36722 this: Expression::Literal(Box::new(Literal::String(
36723 "1970-01-01 00:00:00+00".to_string(),
36724 ))),
36725 to: DataType::Timestamp {
36726 timezone: true,
36727 precision: None,
36728 },
36729 trailing_comments: vec![],
36730 double_colon_syntax: false,
36731 format: None,
36732 default: None,
36733 inferred_type: None,
36734 }));
36735 Ok(Expression::TimestampDiff(Box::new(
36736 crate::expressions::TimestampDiff {
36737 this: Box::new(epoch),
36738 expression: Box::new(ts),
36739 unit: Some("SECONDS".to_string()),
36740 },
36741 )))
36742 }
36743 _ => Ok(Expression::Function(Box::new(Function::new(
36744 "UNIX_SECONDS".to_string(),
36745 vec![ts],
36746 )))),
36747 }
36748 }
36749
36750 "UNIX_MILLIS" if args.len() == 1 => {
36751 let ts = args.remove(0);
36752 match target {
36753 DialectType::DuckDB => {
36754 // EPOCH_MS(CAST(ts AS TIMESTAMPTZ))
36755 let cast_ts = Self::ensure_cast_timestamptz(ts);
36756 Ok(Expression::Function(Box::new(Function::new(
36757 "EPOCH_MS".to_string(),
36758 vec![cast_ts],
36759 ))))
36760 }
36761 _ => Ok(Expression::Function(Box::new(Function::new(
36762 "UNIX_MILLIS".to_string(),
36763 vec![ts],
36764 )))),
36765 }
36766 }
36767
36768 "UNIX_MICROS" if args.len() == 1 => {
36769 let ts = args.remove(0);
36770 match target {
36771 DialectType::DuckDB => {
36772 // EPOCH_US(CAST(ts AS TIMESTAMPTZ))
36773 let cast_ts = Self::ensure_cast_timestamptz(ts);
36774 Ok(Expression::Function(Box::new(Function::new(
36775 "EPOCH_US".to_string(),
36776 vec![cast_ts],
36777 ))))
36778 }
36779 _ => Ok(Expression::Function(Box::new(Function::new(
36780 "UNIX_MICROS".to_string(),
36781 vec![ts],
36782 )))),
36783 }
36784 }
36785
36786 // ARRAY_CONCAT / LIST_CONCAT -> target-specific
36787 "ARRAY_CONCAT" | "LIST_CONCAT" => {
36788 match target {
36789 DialectType::Spark | DialectType::Databricks | DialectType::Hive => {
36790 // CONCAT(arr1, arr2, ...)
36791 Ok(Expression::Function(Box::new(Function::new(
36792 "CONCAT".to_string(),
36793 args,
36794 ))))
36795 }
36796 DialectType::Presto | DialectType::Trino => {
36797 // CONCAT(arr1, arr2, ...)
36798 Ok(Expression::Function(Box::new(Function::new(
36799 "CONCAT".to_string(),
36800 args,
36801 ))))
36802 }
36803 DialectType::Snowflake => {
36804 // ARRAY_CAT(arr1, ARRAY_CAT(arr2, arr3))
36805 if args.len() == 1 {
36806 // ARRAY_CAT requires 2 args, add empty array as []
36807 let empty_arr = Expression::ArrayFunc(Box::new(
36808 crate::expressions::ArrayConstructor {
36809 expressions: vec![],
36810 bracket_notation: true,
36811 use_list_keyword: false,
36812 },
36813 ));
36814 let mut new_args = args;
36815 new_args.push(empty_arr);
36816 Ok(Expression::Function(Box::new(Function::new(
36817 "ARRAY_CAT".to_string(),
36818 new_args,
36819 ))))
36820 } else if args.is_empty() {
36821 Ok(Expression::Function(Box::new(Function::new(
36822 "ARRAY_CAT".to_string(),
36823 args,
36824 ))))
36825 } else {
36826 let mut it = args.into_iter().rev();
36827 let mut result = it.next().unwrap();
36828 for arr in it {
36829 result = Expression::Function(Box::new(Function::new(
36830 "ARRAY_CAT".to_string(),
36831 vec![arr, result],
36832 )));
36833 }
36834 Ok(result)
36835 }
36836 }
36837 DialectType::PostgreSQL => {
36838 // ARRAY_CAT(arr1, ARRAY_CAT(arr2, arr3))
36839 if args.len() <= 1 {
36840 Ok(Expression::Function(Box::new(Function::new(
36841 "ARRAY_CAT".to_string(),
36842 args,
36843 ))))
36844 } else {
36845 let mut it = args.into_iter().rev();
36846 let mut result = it.next().unwrap();
36847 for arr in it {
36848 result = Expression::Function(Box::new(Function::new(
36849 "ARRAY_CAT".to_string(),
36850 vec![arr, result],
36851 )));
36852 }
36853 Ok(result)
36854 }
36855 }
36856 DialectType::Redshift => {
36857 // ARRAY_CONCAT(arr1, ARRAY_CONCAT(arr2, arr3))
36858 if args.len() <= 2 {
36859 Ok(Expression::Function(Box::new(Function::new(
36860 "ARRAY_CONCAT".to_string(),
36861 args,
36862 ))))
36863 } else {
36864 let mut it = args.into_iter().rev();
36865 let mut result = it.next().unwrap();
36866 for arr in it {
36867 result = Expression::Function(Box::new(Function::new(
36868 "ARRAY_CONCAT".to_string(),
36869 vec![arr, result],
36870 )));
36871 }
36872 Ok(result)
36873 }
36874 }
36875 DialectType::DuckDB => {
36876 // LIST_CONCAT supports multiple args natively in DuckDB
36877 Ok(Expression::Function(Box::new(Function::new(
36878 "LIST_CONCAT".to_string(),
36879 args,
36880 ))))
36881 }
36882 _ => Ok(Expression::Function(Box::new(Function::new(
36883 "ARRAY_CONCAT".to_string(),
36884 args,
36885 )))),
36886 }
36887 }
36888
36889 // ARRAY_CONCAT_AGG -> Snowflake: ARRAY_FLATTEN(ARRAY_AGG(x))
36890 "ARRAY_CONCAT_AGG" if args.len() == 1 => {
36891 let arg = args.remove(0);
36892 match target {
36893 DialectType::Snowflake => {
36894 let array_agg =
36895 Expression::ArrayAgg(Box::new(crate::expressions::AggFunc {
36896 this: arg,
36897 distinct: false,
36898 filter: None,
36899 order_by: vec![],
36900 name: None,
36901 ignore_nulls: None,
36902 having_max: None,
36903 limit: None,
36904 inferred_type: None,
36905 }));
36906 Ok(Expression::Function(Box::new(Function::new(
36907 "ARRAY_FLATTEN".to_string(),
36908 vec![array_agg],
36909 ))))
36910 }
36911 _ => Ok(Expression::Function(Box::new(Function::new(
36912 "ARRAY_CONCAT_AGG".to_string(),
36913 vec![arg],
36914 )))),
36915 }
36916 }
36917
36918 // MD5/SHA1/SHA256/SHA512 -> target-specific hash functions
36919 "MD5" if args.len() == 1 => {
36920 let arg = args.remove(0);
36921 match target {
36922 DialectType::Spark | DialectType::Databricks | DialectType::Hive => {
36923 // UNHEX(MD5(x))
36924 let md5 = Expression::Function(Box::new(Function::new(
36925 "MD5".to_string(),
36926 vec![arg],
36927 )));
36928 Ok(Expression::Function(Box::new(Function::new(
36929 "UNHEX".to_string(),
36930 vec![md5],
36931 ))))
36932 }
36933 DialectType::Snowflake => {
36934 // MD5_BINARY(x)
36935 Ok(Expression::Function(Box::new(Function::new(
36936 "MD5_BINARY".to_string(),
36937 vec![arg],
36938 ))))
36939 }
36940 _ => Ok(Expression::Function(Box::new(Function::new(
36941 "MD5".to_string(),
36942 vec![arg],
36943 )))),
36944 }
36945 }
36946
36947 "SHA1" if args.len() == 1 => {
36948 let arg = args.remove(0);
36949 match target {
36950 DialectType::DuckDB => {
36951 // UNHEX(SHA1(x))
36952 let sha1 = Expression::Function(Box::new(Function::new(
36953 "SHA1".to_string(),
36954 vec![arg],
36955 )));
36956 Ok(Expression::Function(Box::new(Function::new(
36957 "UNHEX".to_string(),
36958 vec![sha1],
36959 ))))
36960 }
36961 _ => Ok(Expression::Function(Box::new(Function::new(
36962 "SHA1".to_string(),
36963 vec![arg],
36964 )))),
36965 }
36966 }
36967
36968 "SHA256" if args.len() == 1 => {
36969 let arg = args.remove(0);
36970 match target {
36971 DialectType::DuckDB => {
36972 // UNHEX(SHA256(x))
36973 let sha = Expression::Function(Box::new(Function::new(
36974 "SHA256".to_string(),
36975 vec![arg],
36976 )));
36977 Ok(Expression::Function(Box::new(Function::new(
36978 "UNHEX".to_string(),
36979 vec![sha],
36980 ))))
36981 }
36982 DialectType::Snowflake => {
36983 // SHA2_BINARY(x, 256)
36984 Ok(Expression::Function(Box::new(Function::new(
36985 "SHA2_BINARY".to_string(),
36986 vec![arg, Expression::number(256)],
36987 ))))
36988 }
36989 DialectType::Redshift | DialectType::Spark => {
36990 // SHA2(x, 256)
36991 Ok(Expression::Function(Box::new(Function::new(
36992 "SHA2".to_string(),
36993 vec![arg, Expression::number(256)],
36994 ))))
36995 }
36996 _ => Ok(Expression::Function(Box::new(Function::new(
36997 "SHA256".to_string(),
36998 vec![arg],
36999 )))),
37000 }
37001 }
37002
37003 "SHA512" if args.len() == 1 => {
37004 let arg = args.remove(0);
37005 match target {
37006 DialectType::Snowflake => {
37007 // SHA2_BINARY(x, 512)
37008 Ok(Expression::Function(Box::new(Function::new(
37009 "SHA2_BINARY".to_string(),
37010 vec![arg, Expression::number(512)],
37011 ))))
37012 }
37013 DialectType::Redshift | DialectType::Spark => {
37014 // SHA2(x, 512)
37015 Ok(Expression::Function(Box::new(Function::new(
37016 "SHA2".to_string(),
37017 vec![arg, Expression::number(512)],
37018 ))))
37019 }
37020 _ => Ok(Expression::Function(Box::new(Function::new(
37021 "SHA512".to_string(),
37022 vec![arg],
37023 )))),
37024 }
37025 }
37026
37027 // REGEXP_EXTRACT_ALL(str, pattern) -> add default group arg
37028 "REGEXP_EXTRACT_ALL" if args.len() == 2 => {
37029 let str_expr = args.remove(0);
37030 let pattern = args.remove(0);
37031
37032 // Check if pattern contains capturing groups (parentheses)
37033 let has_groups = match &pattern {
37034 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
37035 let Literal::String(s) = lit.as_ref() else {
37036 unreachable!()
37037 };
37038 s.contains('(') && s.contains(')')
37039 }
37040 _ => false,
37041 };
37042
37043 match target {
37044 DialectType::DuckDB => {
37045 let group = if has_groups {
37046 Expression::number(1)
37047 } else {
37048 Expression::number(0)
37049 };
37050 Ok(Expression::Function(Box::new(Function::new(
37051 "REGEXP_EXTRACT_ALL".to_string(),
37052 vec![str_expr, pattern, group],
37053 ))))
37054 }
37055 DialectType::Spark | DialectType::Databricks => {
37056 // Spark's default group_index is 1 (same as BigQuery), so omit for capturing groups
37057 if has_groups {
37058 Ok(Expression::Function(Box::new(Function::new(
37059 "REGEXP_EXTRACT_ALL".to_string(),
37060 vec![str_expr, pattern],
37061 ))))
37062 } else {
37063 Ok(Expression::Function(Box::new(Function::new(
37064 "REGEXP_EXTRACT_ALL".to_string(),
37065 vec![str_expr, pattern, Expression::number(0)],
37066 ))))
37067 }
37068 }
37069 DialectType::Presto | DialectType::Trino => {
37070 if has_groups {
37071 Ok(Expression::Function(Box::new(Function::new(
37072 "REGEXP_EXTRACT_ALL".to_string(),
37073 vec![str_expr, pattern, Expression::number(1)],
37074 ))))
37075 } else {
37076 Ok(Expression::Function(Box::new(Function::new(
37077 "REGEXP_EXTRACT_ALL".to_string(),
37078 vec![str_expr, pattern],
37079 ))))
37080 }
37081 }
37082 DialectType::Snowflake => {
37083 if has_groups {
37084 // REGEXP_EXTRACT_ALL(str, pattern, 1, 1, 'c', 1)
37085 Ok(Expression::Function(Box::new(Function::new(
37086 "REGEXP_EXTRACT_ALL".to_string(),
37087 vec![
37088 str_expr,
37089 pattern,
37090 Expression::number(1),
37091 Expression::number(1),
37092 Expression::Literal(Box::new(Literal::String("c".to_string()))),
37093 Expression::number(1),
37094 ],
37095 ))))
37096 } else {
37097 Ok(Expression::Function(Box::new(Function::new(
37098 "REGEXP_EXTRACT_ALL".to_string(),
37099 vec![str_expr, pattern],
37100 ))))
37101 }
37102 }
37103 _ => Ok(Expression::Function(Box::new(Function::new(
37104 "REGEXP_EXTRACT_ALL".to_string(),
37105 vec![str_expr, pattern],
37106 )))),
37107 }
37108 }
37109
37110 // MOD(x, y) -> x % y for dialects that prefer or require the infix operator.
37111 "MOD" if args.len() == 2 => {
37112 match target {
37113 DialectType::PostgreSQL
37114 | DialectType::DuckDB
37115 | DialectType::Presto
37116 | DialectType::Trino
37117 | DialectType::Athena
37118 | DialectType::Snowflake
37119 | DialectType::TSQL
37120 | DialectType::Fabric => {
37121 let x = args.remove(0);
37122 let y = args.remove(0);
37123 // Wrap complex expressions in parens to preserve precedence
37124 let needs_paren = |e: &Expression| {
37125 matches!(
37126 e,
37127 Expression::Add(_)
37128 | Expression::Sub(_)
37129 | Expression::Mul(_)
37130 | Expression::Div(_)
37131 | Expression::Mod(_)
37132 | Expression::ModFunc(_)
37133 )
37134 };
37135 let x = if needs_paren(&x) {
37136 Expression::Paren(Box::new(crate::expressions::Paren {
37137 this: x,
37138 trailing_comments: vec![],
37139 }))
37140 } else {
37141 x
37142 };
37143 let y = if needs_paren(&y) {
37144 Expression::Paren(Box::new(crate::expressions::Paren {
37145 this: y,
37146 trailing_comments: vec![],
37147 }))
37148 } else {
37149 y
37150 };
37151 Ok(Expression::Mod(Box::new(
37152 crate::expressions::BinaryOp::new(x, y),
37153 )))
37154 }
37155 DialectType::Hive | DialectType::Spark | DialectType::Databricks => {
37156 // Hive/Spark: a % b
37157 let x = args.remove(0);
37158 let y = args.remove(0);
37159 let needs_paren = |e: &Expression| {
37160 matches!(
37161 e,
37162 Expression::Add(_)
37163 | Expression::Sub(_)
37164 | Expression::Mul(_)
37165 | Expression::Div(_)
37166 | Expression::Mod(_)
37167 | Expression::ModFunc(_)
37168 )
37169 };
37170 let x = if needs_paren(&x) {
37171 Expression::Paren(Box::new(crate::expressions::Paren {
37172 this: x,
37173 trailing_comments: vec![],
37174 }))
37175 } else {
37176 x
37177 };
37178 let y = if needs_paren(&y) {
37179 Expression::Paren(Box::new(crate::expressions::Paren {
37180 this: y,
37181 trailing_comments: vec![],
37182 }))
37183 } else {
37184 y
37185 };
37186 Ok(Expression::Mod(Box::new(
37187 crate::expressions::BinaryOp::new(x, y),
37188 )))
37189 }
37190 _ => Ok(Expression::Function(Box::new(Function::new(
37191 "MOD".to_string(),
37192 args,
37193 )))),
37194 }
37195 }
37196
37197 // ARRAY_FILTER(arr, lambda) -> FILTER for Hive/Spark/Presto, ARRAY_FILTER for StarRocks
37198 "ARRAY_FILTER" if args.len() == 2 => {
37199 let name = match target {
37200 DialectType::DuckDB => "LIST_FILTER",
37201 DialectType::StarRocks => "ARRAY_FILTER",
37202 _ => "FILTER",
37203 };
37204 Ok(Expression::Function(Box::new(Function::new(
37205 name.to_string(),
37206 args,
37207 ))))
37208 }
37209 // FILTER(arr, lambda) -> ARRAY_FILTER for StarRocks, LIST_FILTER for DuckDB
37210 "FILTER" if args.len() == 2 => {
37211 let name = match target {
37212 DialectType::DuckDB => "LIST_FILTER",
37213 DialectType::StarRocks => "ARRAY_FILTER",
37214 _ => "FILTER",
37215 };
37216 Ok(Expression::Function(Box::new(Function::new(
37217 name.to_string(),
37218 args,
37219 ))))
37220 }
37221 // REDUCE(arr, init, lambda1, lambda2) -> AGGREGATE for Spark
37222 "REDUCE" if args.len() >= 3 => {
37223 let name = match target {
37224 DialectType::Spark | DialectType::Databricks => "AGGREGATE",
37225 _ => "REDUCE",
37226 };
37227 Ok(Expression::Function(Box::new(Function::new(
37228 name.to_string(),
37229 args,
37230 ))))
37231 }
37232 // ARRAY_REVERSE(x) -> arrayReverse for ClickHouse (handled by generator)
37233 "ARRAY_REVERSE" if args.len() == 1 => Ok(Expression::Function(Box::new(
37234 Function::new("ARRAY_REVERSE".to_string(), args),
37235 ))),
37236
37237 // CONCAT(a, b, ...) -> a || b || ... for DuckDB with 3+ args
37238 "CONCAT" if args.len() > 2 => match target {
37239 DialectType::DuckDB => {
37240 let mut it = args.into_iter();
37241 let mut result = it.next().unwrap();
37242 for arg in it {
37243 result = Expression::DPipe(Box::new(crate::expressions::DPipe {
37244 this: Box::new(result),
37245 expression: Box::new(arg),
37246 safe: None,
37247 }));
37248 }
37249 Ok(result)
37250 }
37251 _ => Ok(Expression::Function(Box::new(Function::new(
37252 "CONCAT".to_string(),
37253 args,
37254 )))),
37255 },
37256
37257 // GENERATE_DATE_ARRAY(start, end[, step]) -> target-specific
37258 "GENERATE_DATE_ARRAY" => {
37259 if matches!(target, DialectType::BigQuery) {
37260 // BQ->BQ: add default interval if not present
37261 if args.len() == 2 {
37262 let start = args.remove(0);
37263 let end = args.remove(0);
37264 let default_interval =
37265 Expression::Interval(Box::new(crate::expressions::Interval {
37266 this: Some(Expression::Literal(Box::new(Literal::String(
37267 "1".to_string(),
37268 )))),
37269 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
37270 unit: crate::expressions::IntervalUnit::Day,
37271 use_plural: false,
37272 }),
37273 }));
37274 Ok(Expression::Function(Box::new(Function::new(
37275 "GENERATE_DATE_ARRAY".to_string(),
37276 vec![start, end, default_interval],
37277 ))))
37278 } else {
37279 Ok(Expression::Function(Box::new(Function::new(
37280 "GENERATE_DATE_ARRAY".to_string(),
37281 args,
37282 ))))
37283 }
37284 } else if matches!(target, DialectType::DuckDB) {
37285 // DuckDB: CAST(GENERATE_SERIES(CAST(start AS DATE), CAST(end AS DATE), step) AS DATE[])
37286 let start = args.get(0).cloned();
37287 let end = args.get(1).cloned();
37288 let step = args.get(2).cloned().or_else(|| {
37289 Some(Expression::Interval(Box::new(
37290 crate::expressions::Interval {
37291 this: Some(Expression::Literal(Box::new(Literal::String(
37292 "1".to_string(),
37293 )))),
37294 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
37295 unit: crate::expressions::IntervalUnit::Day,
37296 use_plural: false,
37297 }),
37298 },
37299 )))
37300 });
37301
37302 // Wrap start/end in CAST(... AS DATE) only for string literals
37303 let maybe_cast_date = |expr: Expression| -> Expression {
37304 if matches!(&expr, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)))
37305 {
37306 Expression::Cast(Box::new(Cast {
37307 this: expr,
37308 to: DataType::Date,
37309 trailing_comments: vec![],
37310 double_colon_syntax: false,
37311 format: None,
37312 default: None,
37313 inferred_type: None,
37314 }))
37315 } else {
37316 expr
37317 }
37318 };
37319 let cast_start = start.map(maybe_cast_date);
37320 let cast_end = end.map(maybe_cast_date);
37321
37322 let gen_series =
37323 Expression::GenerateSeries(Box::new(crate::expressions::GenerateSeries {
37324 start: cast_start.map(Box::new),
37325 end: cast_end.map(Box::new),
37326 step: step.map(Box::new),
37327 is_end_exclusive: None,
37328 }));
37329
37330 // Wrap in CAST(... AS DATE[])
37331 Ok(Expression::Cast(Box::new(Cast {
37332 this: gen_series,
37333 to: DataType::Array {
37334 element_type: Box::new(DataType::Date),
37335 dimension: None,
37336 },
37337 trailing_comments: vec![],
37338 double_colon_syntax: false,
37339 format: None,
37340 default: None,
37341 inferred_type: None,
37342 })))
37343 } else if matches!(target, DialectType::Snowflake) {
37344 // Snowflake: keep as GENERATE_DATE_ARRAY function for later transform
37345 // (transform_generate_date_array_snowflake will convert to ARRAY_GENERATE_RANGE + DATEADD)
37346 if args.len() == 2 {
37347 let start = args.remove(0);
37348 let end = args.remove(0);
37349 let default_interval =
37350 Expression::Interval(Box::new(crate::expressions::Interval {
37351 this: Some(Expression::Literal(Box::new(Literal::String(
37352 "1".to_string(),
37353 )))),
37354 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
37355 unit: crate::expressions::IntervalUnit::Day,
37356 use_plural: false,
37357 }),
37358 }));
37359 Ok(Expression::Function(Box::new(Function::new(
37360 "GENERATE_DATE_ARRAY".to_string(),
37361 vec![start, end, default_interval],
37362 ))))
37363 } else {
37364 Ok(Expression::Function(Box::new(Function::new(
37365 "GENERATE_DATE_ARRAY".to_string(),
37366 args,
37367 ))))
37368 }
37369 } else {
37370 // Convert to GenerateSeries for other targets
37371 let start = args.get(0).cloned();
37372 let end = args.get(1).cloned();
37373 let step = args.get(2).cloned().or_else(|| {
37374 Some(Expression::Interval(Box::new(
37375 crate::expressions::Interval {
37376 this: Some(Expression::Literal(Box::new(Literal::String(
37377 "1".to_string(),
37378 )))),
37379 unit: Some(crate::expressions::IntervalUnitSpec::Simple {
37380 unit: crate::expressions::IntervalUnit::Day,
37381 use_plural: false,
37382 }),
37383 },
37384 )))
37385 });
37386 Ok(Expression::GenerateSeries(Box::new(
37387 crate::expressions::GenerateSeries {
37388 start: start.map(Box::new),
37389 end: end.map(Box::new),
37390 step: step.map(Box::new),
37391 is_end_exclusive: None,
37392 },
37393 )))
37394 }
37395 }
37396
37397 // PARSE_DATE(format, str) -> target-specific
37398 "PARSE_DATE" if args.len() == 2 => {
37399 let format = args.remove(0);
37400 let str_expr = args.remove(0);
37401 match target {
37402 DialectType::DuckDB => {
37403 // CAST(STRPTIME(str, duck_format) AS DATE)
37404 let duck_format = Self::bq_format_to_duckdb(&format);
37405 let strptime = Expression::Function(Box::new(Function::new(
37406 "STRPTIME".to_string(),
37407 vec![str_expr, duck_format],
37408 )));
37409 Ok(Expression::Cast(Box::new(Cast {
37410 this: strptime,
37411 to: DataType::Date,
37412 trailing_comments: vec![],
37413 double_colon_syntax: false,
37414 format: None,
37415 default: None,
37416 inferred_type: None,
37417 })))
37418 }
37419 DialectType::Snowflake => {
37420 // _POLYGLOT_DATE(str, snowflake_format)
37421 // Use marker so Snowflake target transform keeps it as DATE() instead of TO_DATE()
37422 let sf_format = Self::bq_format_to_snowflake(&format);
37423 Ok(Expression::Function(Box::new(Function::new(
37424 "_POLYGLOT_DATE".to_string(),
37425 vec![str_expr, sf_format],
37426 ))))
37427 }
37428 _ => Ok(Expression::Function(Box::new(Function::new(
37429 "PARSE_DATE".to_string(),
37430 vec![format, str_expr],
37431 )))),
37432 }
37433 }
37434
37435 // PARSE_DATETIME(format, str) -> target-specific
37436 "PARSE_DATETIME" if args.len() == 2 => {
37437 let format = args.remove(0);
37438 let str_expr = args.remove(0);
37439 let c_format = Self::bq_format_to_duckdb(&format);
37440 match target {
37441 DialectType::DuckDB => {
37442 // DuckDB STRPTIME needs a date-bearing format for DATETIME parsing.
37443 let str_with_year = Expression::Concat(Box::new(BinaryOp::new(
37444 Expression::string("1970 "),
37445 str_expr,
37446 )));
37447 let format_with_year = Expression::Concat(Box::new(BinaryOp::new(
37448 Expression::string("%Y "),
37449 c_format,
37450 )));
37451 Ok(Expression::Function(Box::new(Function::new(
37452 "STRPTIME".to_string(),
37453 vec![str_with_year, format_with_year],
37454 ))))
37455 }
37456 DialectType::Snowflake => {
37457 // SQLGlot emits PARSE_DATETIME(value, format) with expanded C-style format tokens.
37458 Ok(Expression::Function(Box::new(Function::new(
37459 "PARSE_DATETIME".to_string(),
37460 vec![str_expr, c_format],
37461 ))))
37462 }
37463 _ => Ok(Expression::Function(Box::new(Function::new(
37464 "PARSE_DATETIME".to_string(),
37465 vec![format, str_expr],
37466 )))),
37467 }
37468 }
37469
37470 // PARSE_TIMESTAMP(format, str) -> target-specific
37471 "PARSE_TIMESTAMP" if args.len() >= 2 => {
37472 let format = args.remove(0);
37473 let str_expr = args.remove(0);
37474 let tz = if !args.is_empty() {
37475 Some(args.remove(0))
37476 } else {
37477 None
37478 };
37479 match target {
37480 DialectType::DuckDB => {
37481 let duck_format = Self::bq_format_to_duckdb(&format);
37482 let strptime = Expression::Function(Box::new(Function::new(
37483 "STRPTIME".to_string(),
37484 vec![str_expr, duck_format],
37485 )));
37486 Ok(strptime)
37487 }
37488 _ => {
37489 let mut result_args = vec![format, str_expr];
37490 if let Some(tz_arg) = tz {
37491 result_args.push(tz_arg);
37492 }
37493 Ok(Expression::Function(Box::new(Function::new(
37494 "PARSE_TIMESTAMP".to_string(),
37495 result_args,
37496 ))))
37497 }
37498 }
37499 }
37500
37501 // FORMAT_DATE(format, date) -> target-specific
37502 "FORMAT_DATE" if args.len() == 2 => {
37503 let format = args.remove(0);
37504 let date_expr = args.remove(0);
37505 match target {
37506 DialectType::DuckDB => {
37507 // STRFTIME(CAST(date AS DATE), format)
37508 let cast_date = Expression::Cast(Box::new(Cast {
37509 this: date_expr,
37510 to: DataType::Date,
37511 trailing_comments: vec![],
37512 double_colon_syntax: false,
37513 format: None,
37514 default: None,
37515 inferred_type: None,
37516 }));
37517 Ok(Expression::Function(Box::new(Function::new(
37518 "STRFTIME".to_string(),
37519 vec![cast_date, format],
37520 ))))
37521 }
37522 _ => Ok(Expression::Function(Box::new(Function::new(
37523 "FORMAT_DATE".to_string(),
37524 vec![format, date_expr],
37525 )))),
37526 }
37527 }
37528
37529 // FORMAT_DATETIME(format, datetime) -> target-specific
37530 "FORMAT_DATETIME" if args.len() == 2 => {
37531 let format = args.remove(0);
37532 let dt_expr = args.remove(0);
37533
37534 if matches!(target, DialectType::BigQuery) {
37535 // BQ->BQ: normalize %H:%M:%S to %T, %x to %D
37536 let norm_format = Self::bq_format_normalize_bq(&format);
37537 // Also strip DATETIME keyword from typed literals
37538 let norm_dt = match dt_expr {
37539 Expression::Literal(lit)
37540 if matches!(lit.as_ref(), Literal::Timestamp(_)) =>
37541 {
37542 let Literal::Timestamp(s) = lit.as_ref() else {
37543 unreachable!()
37544 };
37545 Expression::Cast(Box::new(Cast {
37546 this: Expression::Literal(Box::new(Literal::String(s.clone()))),
37547 to: DataType::Custom {
37548 name: "DATETIME".to_string(),
37549 },
37550 trailing_comments: vec![],
37551 double_colon_syntax: false,
37552 format: None,
37553 default: None,
37554 inferred_type: None,
37555 }))
37556 }
37557 other => other,
37558 };
37559 return Ok(Expression::Function(Box::new(Function::new(
37560 "FORMAT_DATETIME".to_string(),
37561 vec![norm_format, norm_dt],
37562 ))));
37563 }
37564
37565 match target {
37566 DialectType::DuckDB => {
37567 // STRFTIME(CAST(dt AS TIMESTAMP), duckdb_format)
37568 let cast_dt = Self::ensure_cast_timestamp(dt_expr);
37569 let duck_format = Self::bq_format_to_duckdb(&format);
37570 Ok(Expression::Function(Box::new(Function::new(
37571 "STRFTIME".to_string(),
37572 vec![cast_dt, duck_format],
37573 ))))
37574 }
37575 _ => Ok(Expression::Function(Box::new(Function::new(
37576 "FORMAT_DATETIME".to_string(),
37577 vec![format, dt_expr],
37578 )))),
37579 }
37580 }
37581
37582 // FORMAT_TIMESTAMP(format, ts) -> target-specific
37583 "FORMAT_TIMESTAMP" if args.len() == 2 => {
37584 let format = args.remove(0);
37585 let ts_expr = args.remove(0);
37586 match target {
37587 DialectType::DuckDB => {
37588 // STRFTIME(CAST(CAST(ts AS TIMESTAMPTZ) AS TIMESTAMP), format)
37589 let cast_tstz = Self::ensure_cast_timestamptz(ts_expr);
37590 let cast_ts = Expression::Cast(Box::new(Cast {
37591 this: cast_tstz,
37592 to: DataType::Timestamp {
37593 timezone: false,
37594 precision: None,
37595 },
37596 trailing_comments: vec![],
37597 double_colon_syntax: false,
37598 format: None,
37599 default: None,
37600 inferred_type: None,
37601 }));
37602 Ok(Expression::Function(Box::new(Function::new(
37603 "STRFTIME".to_string(),
37604 vec![cast_ts, format],
37605 ))))
37606 }
37607 DialectType::Snowflake => {
37608 // TO_CHAR(CAST(CAST(ts AS TIMESTAMPTZ) AS TIMESTAMP), snowflake_format)
37609 let cast_tstz = Self::ensure_cast_timestamptz(ts_expr);
37610 let cast_ts = Expression::Cast(Box::new(Cast {
37611 this: cast_tstz,
37612 to: DataType::Timestamp {
37613 timezone: false,
37614 precision: None,
37615 },
37616 trailing_comments: vec![],
37617 double_colon_syntax: false,
37618 format: None,
37619 default: None,
37620 inferred_type: None,
37621 }));
37622 let sf_format = Self::bq_format_to_snowflake(&format);
37623 Ok(Expression::Function(Box::new(Function::new(
37624 "TO_CHAR".to_string(),
37625 vec![cast_ts, sf_format],
37626 ))))
37627 }
37628 _ => Ok(Expression::Function(Box::new(Function::new(
37629 "FORMAT_TIMESTAMP".to_string(),
37630 vec![format, ts_expr],
37631 )))),
37632 }
37633 }
37634
37635 // UNIX_DATE(date) -> DATE_DIFF('DAY', '1970-01-01', date) for DuckDB
37636 "UNIX_DATE" if args.len() == 1 => {
37637 let date = args.remove(0);
37638 match target {
37639 DialectType::DuckDB => {
37640 let epoch = Expression::Cast(Box::new(Cast {
37641 this: Expression::Literal(Box::new(Literal::String(
37642 "1970-01-01".to_string(),
37643 ))),
37644 to: DataType::Date,
37645 trailing_comments: vec![],
37646 double_colon_syntax: false,
37647 format: None,
37648 default: None,
37649 inferred_type: None,
37650 }));
37651 // DATE_DIFF('DAY', epoch, date) but date might be DATE '...' literal
37652 // Need to convert DATE literal to CAST
37653 let norm_date = Self::date_literal_to_cast(date);
37654 Ok(Expression::Function(Box::new(Function::new(
37655 "DATE_DIFF".to_string(),
37656 vec![
37657 Expression::Literal(Box::new(Literal::String("DAY".to_string()))),
37658 epoch,
37659 norm_date,
37660 ],
37661 ))))
37662 }
37663 _ => Ok(Expression::Function(Box::new(Function::new(
37664 "UNIX_DATE".to_string(),
37665 vec![date],
37666 )))),
37667 }
37668 }
37669
37670 // UNIX_SECONDS(ts) -> target-specific
37671 "UNIX_SECONDS" if args.len() == 1 => {
37672 let ts = args.remove(0);
37673 match target {
37674 DialectType::DuckDB => {
37675 // CAST(EPOCH(CAST(ts AS TIMESTAMPTZ)) AS BIGINT)
37676 let norm_ts = Self::ts_literal_to_cast_tz(ts);
37677 let epoch = Expression::Function(Box::new(Function::new(
37678 "EPOCH".to_string(),
37679 vec![norm_ts],
37680 )));
37681 Ok(Expression::Cast(Box::new(Cast {
37682 this: epoch,
37683 to: DataType::BigInt { length: None },
37684 trailing_comments: vec![],
37685 double_colon_syntax: false,
37686 format: None,
37687 default: None,
37688 inferred_type: None,
37689 })))
37690 }
37691 DialectType::Snowflake => {
37692 // TIMESTAMPDIFF(SECONDS, CAST('1970-01-01 00:00:00+00' AS TIMESTAMPTZ), ts)
37693 let epoch = Expression::Cast(Box::new(Cast {
37694 this: Expression::Literal(Box::new(Literal::String(
37695 "1970-01-01 00:00:00+00".to_string(),
37696 ))),
37697 to: DataType::Timestamp {
37698 timezone: true,
37699 precision: None,
37700 },
37701 trailing_comments: vec![],
37702 double_colon_syntax: false,
37703 format: None,
37704 default: None,
37705 inferred_type: None,
37706 }));
37707 Ok(Expression::Function(Box::new(Function::new(
37708 "TIMESTAMPDIFF".to_string(),
37709 vec![
37710 Expression::Identifier(Identifier::new("SECONDS".to_string())),
37711 epoch,
37712 ts,
37713 ],
37714 ))))
37715 }
37716 _ => Ok(Expression::Function(Box::new(Function::new(
37717 "UNIX_SECONDS".to_string(),
37718 vec![ts],
37719 )))),
37720 }
37721 }
37722
37723 // UNIX_MILLIS(ts) -> target-specific
37724 "UNIX_MILLIS" if args.len() == 1 => {
37725 let ts = args.remove(0);
37726 match target {
37727 DialectType::DuckDB => {
37728 let norm_ts = Self::ts_literal_to_cast_tz(ts);
37729 Ok(Expression::Function(Box::new(Function::new(
37730 "EPOCH_MS".to_string(),
37731 vec![norm_ts],
37732 ))))
37733 }
37734 _ => Ok(Expression::Function(Box::new(Function::new(
37735 "UNIX_MILLIS".to_string(),
37736 vec![ts],
37737 )))),
37738 }
37739 }
37740
37741 // UNIX_MICROS(ts) -> target-specific
37742 "UNIX_MICROS" if args.len() == 1 => {
37743 let ts = args.remove(0);
37744 match target {
37745 DialectType::DuckDB => {
37746 let norm_ts = Self::ts_literal_to_cast_tz(ts);
37747 Ok(Expression::Function(Box::new(Function::new(
37748 "EPOCH_US".to_string(),
37749 vec![norm_ts],
37750 ))))
37751 }
37752 _ => Ok(Expression::Function(Box::new(Function::new(
37753 "UNIX_MICROS".to_string(),
37754 vec![ts],
37755 )))),
37756 }
37757 }
37758
37759 // INSTR(str, substr) -> target-specific
37760 "INSTR" => {
37761 if matches!(target, DialectType::BigQuery) {
37762 // BQ->BQ: keep as INSTR
37763 Ok(Expression::Function(Box::new(Function::new(
37764 "INSTR".to_string(),
37765 args,
37766 ))))
37767 } else if matches!(target, DialectType::Snowflake) && args.len() == 2 {
37768 // Snowflake: CHARINDEX(substr, str) - swap args
37769 let str_expr = args.remove(0);
37770 let substr = args.remove(0);
37771 Ok(Expression::Function(Box::new(Function::new(
37772 "CHARINDEX".to_string(),
37773 vec![substr, str_expr],
37774 ))))
37775 } else {
37776 // Keep as INSTR for other targets
37777 Ok(Expression::Function(Box::new(Function::new(
37778 "INSTR".to_string(),
37779 args,
37780 ))))
37781 }
37782 }
37783
37784 // CURRENT_TIMESTAMP / CURRENT_DATE handling - parens normalization and timezone
37785 "CURRENT_TIMESTAMP" | "CURRENT_DATE" | "CURRENT_DATETIME" | "CURRENT_TIME" => {
37786 if matches!(target, DialectType::BigQuery) {
37787 // BQ->BQ: always output with parens (function form), keep any timezone arg
37788 Ok(Expression::Function(Box::new(Function::new(name, args))))
37789 } else if name == "CURRENT_DATE" && args.len() == 1 {
37790 // CURRENT_DATE('UTC') - has timezone arg
37791 let tz_arg = args.remove(0);
37792 match target {
37793 DialectType::DuckDB => {
37794 // CAST(CURRENT_TIMESTAMP AT TIME ZONE 'UTC' AS DATE)
37795 let ct = Expression::CurrentTimestamp(
37796 crate::expressions::CurrentTimestamp {
37797 precision: None,
37798 sysdate: false,
37799 },
37800 );
37801 let at_tz =
37802 Expression::AtTimeZone(Box::new(crate::expressions::AtTimeZone {
37803 this: ct,
37804 zone: tz_arg,
37805 }));
37806 Ok(Expression::Cast(Box::new(Cast {
37807 this: at_tz,
37808 to: DataType::Date,
37809 trailing_comments: vec![],
37810 double_colon_syntax: false,
37811 format: None,
37812 default: None,
37813 inferred_type: None,
37814 })))
37815 }
37816 DialectType::Snowflake => {
37817 // CAST(CONVERT_TIMEZONE('UTC', CURRENT_TIMESTAMP()) AS DATE)
37818 let ct = Expression::Function(Box::new(Function::new(
37819 "CURRENT_TIMESTAMP".to_string(),
37820 vec![],
37821 )));
37822 let convert = Expression::Function(Box::new(Function::new(
37823 "CONVERT_TIMEZONE".to_string(),
37824 vec![tz_arg, ct],
37825 )));
37826 Ok(Expression::Cast(Box::new(Cast {
37827 this: convert,
37828 to: DataType::Date,
37829 trailing_comments: vec![],
37830 double_colon_syntax: false,
37831 format: None,
37832 default: None,
37833 inferred_type: None,
37834 })))
37835 }
37836 _ => {
37837 // PostgreSQL, MySQL, etc.: CURRENT_DATE AT TIME ZONE 'UTC'
37838 let cd = Expression::CurrentDate(crate::expressions::CurrentDate);
37839 Ok(Expression::AtTimeZone(Box::new(
37840 crate::expressions::AtTimeZone {
37841 this: cd,
37842 zone: tz_arg,
37843 },
37844 )))
37845 }
37846 }
37847 } else if (name == "CURRENT_TIMESTAMP"
37848 || name == "CURRENT_TIME"
37849 || name == "CURRENT_DATE")
37850 && args.is_empty()
37851 && matches!(
37852 target,
37853 DialectType::PostgreSQL
37854 | DialectType::DuckDB
37855 | DialectType::Presto
37856 | DialectType::Trino
37857 )
37858 {
37859 // These targets want no-parens CURRENT_TIMESTAMP / CURRENT_DATE / CURRENT_TIME
37860 if name == "CURRENT_TIMESTAMP" {
37861 Ok(Expression::CurrentTimestamp(
37862 crate::expressions::CurrentTimestamp {
37863 precision: None,
37864 sysdate: false,
37865 },
37866 ))
37867 } else if name == "CURRENT_DATE" {
37868 Ok(Expression::CurrentDate(crate::expressions::CurrentDate))
37869 } else {
37870 // CURRENT_TIME
37871 Ok(Expression::CurrentTime(crate::expressions::CurrentTime {
37872 precision: None,
37873 }))
37874 }
37875 } else {
37876 // All other targets: keep as function (with parens)
37877 Ok(Expression::Function(Box::new(Function::new(name, args))))
37878 }
37879 }
37880
37881 // JSON_QUERY(json, path) -> target-specific
37882 "JSON_QUERY" if args.len() == 2 => {
37883 match target {
37884 DialectType::DuckDB | DialectType::SQLite => {
37885 // json -> path syntax
37886 let json_expr = args.remove(0);
37887 let path = args.remove(0);
37888 Ok(Expression::JsonExtract(Box::new(
37889 crate::expressions::JsonExtractFunc {
37890 this: json_expr,
37891 path,
37892 returning: None,
37893 arrow_syntax: true,
37894 hash_arrow_syntax: false,
37895 wrapper_option: None,
37896 quotes_option: None,
37897 on_scalar_string: false,
37898 on_error: None,
37899 },
37900 )))
37901 }
37902 DialectType::Spark | DialectType::Databricks | DialectType::Hive => {
37903 Ok(Expression::Function(Box::new(Function::new(
37904 "GET_JSON_OBJECT".to_string(),
37905 args,
37906 ))))
37907 }
37908 DialectType::PostgreSQL | DialectType::Redshift => Ok(Expression::Function(
37909 Box::new(Function::new("JSON_EXTRACT_PATH".to_string(), args)),
37910 )),
37911 _ => Ok(Expression::Function(Box::new(Function::new(
37912 "JSON_QUERY".to_string(),
37913 args,
37914 )))),
37915 }
37916 }
37917
37918 // JSON_VALUE_ARRAY(json, path) -> target-specific
37919 "JSON_VALUE_ARRAY" if args.len() == 2 => {
37920 match target {
37921 DialectType::DuckDB => {
37922 // CAST(json -> path AS TEXT[])
37923 let json_expr = args.remove(0);
37924 let path = args.remove(0);
37925 let arrow = Expression::JsonExtract(Box::new(
37926 crate::expressions::JsonExtractFunc {
37927 this: json_expr,
37928 path,
37929 returning: None,
37930 arrow_syntax: true,
37931 hash_arrow_syntax: false,
37932 wrapper_option: None,
37933 quotes_option: None,
37934 on_scalar_string: false,
37935 on_error: None,
37936 },
37937 ));
37938 Ok(Expression::Cast(Box::new(Cast {
37939 this: arrow,
37940 to: DataType::Array {
37941 element_type: Box::new(DataType::Text),
37942 dimension: None,
37943 },
37944 trailing_comments: vec![],
37945 double_colon_syntax: false,
37946 format: None,
37947 default: None,
37948 inferred_type: None,
37949 })))
37950 }
37951 DialectType::Snowflake => {
37952 let json_expr = args.remove(0);
37953 let path_expr = args.remove(0);
37954 // Convert JSON path from $.path to just path
37955 let sf_path = if let Expression::Literal(ref lit) = path_expr {
37956 if let Literal::String(ref s) = lit.as_ref() {
37957 let trimmed = s.trim_start_matches('$').trim_start_matches('.');
37958 Expression::Literal(Box::new(Literal::String(trimmed.to_string())))
37959 } else {
37960 path_expr.clone()
37961 }
37962 } else {
37963 path_expr
37964 };
37965 let parse_json = Expression::Function(Box::new(Function::new(
37966 "PARSE_JSON".to_string(),
37967 vec![json_expr],
37968 )));
37969 let get_path = Expression::Function(Box::new(Function::new(
37970 "GET_PATH".to_string(),
37971 vec![parse_json, sf_path],
37972 )));
37973 // TRANSFORM(get_path, x -> CAST(x AS VARCHAR))
37974 let cast_expr = Expression::Cast(Box::new(Cast {
37975 this: Expression::Identifier(Identifier::new("x")),
37976 to: DataType::VarChar {
37977 length: None,
37978 parenthesized_length: false,
37979 },
37980 trailing_comments: vec![],
37981 double_colon_syntax: false,
37982 format: None,
37983 default: None,
37984 inferred_type: None,
37985 }));
37986 let lambda = Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
37987 parameters: vec![Identifier::new("x")],
37988 body: cast_expr,
37989 colon: false,
37990 parameter_types: vec![],
37991 }));
37992 Ok(Expression::Function(Box::new(Function::new(
37993 "TRANSFORM".to_string(),
37994 vec![get_path, lambda],
37995 ))))
37996 }
37997 _ => Ok(Expression::Function(Box::new(Function::new(
37998 "JSON_VALUE_ARRAY".to_string(),
37999 args,
38000 )))),
38001 }
38002 }
38003
38004 // BigQuery REGEXP_EXTRACT(val, regex[, position[, occurrence]]) -> target dialects
38005 // BigQuery's 3rd arg is "position" (starting char index), 4th is "occurrence" (which match to return)
38006 // This is different from Hive/Spark where 3rd arg is "group_index"
38007 "REGEXP_EXTRACT" if matches!(source, DialectType::BigQuery) => {
38008 match target {
38009 DialectType::DuckDB
38010 | DialectType::Presto
38011 | DialectType::Trino
38012 | DialectType::Athena => {
38013 if args.len() == 2 {
38014 // REGEXP_EXTRACT(val, regex) -> REGEXP_EXTRACT(val, regex, 1)
38015 args.push(Expression::number(1));
38016 Ok(Expression::Function(Box::new(Function::new(
38017 "REGEXP_EXTRACT".to_string(),
38018 args,
38019 ))))
38020 } else if args.len() == 3 {
38021 let val = args.remove(0);
38022 let regex = args.remove(0);
38023 let position = args.remove(0);
38024 let is_pos_1 = matches!(&position, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(n) if n == "1"));
38025 if is_pos_1 {
38026 Ok(Expression::Function(Box::new(Function::new(
38027 "REGEXP_EXTRACT".to_string(),
38028 vec![val, regex, Expression::number(1)],
38029 ))))
38030 } else {
38031 let substring_expr = Expression::Function(Box::new(Function::new(
38032 "SUBSTRING".to_string(),
38033 vec![val, position],
38034 )));
38035 let nullif_expr = Expression::Function(Box::new(Function::new(
38036 "NULLIF".to_string(),
38037 vec![
38038 substring_expr,
38039 Expression::Literal(Box::new(Literal::String(
38040 String::new(),
38041 ))),
38042 ],
38043 )));
38044 Ok(Expression::Function(Box::new(Function::new(
38045 "REGEXP_EXTRACT".to_string(),
38046 vec![nullif_expr, regex, Expression::number(1)],
38047 ))))
38048 }
38049 } else if args.len() == 4 {
38050 let val = args.remove(0);
38051 let regex = args.remove(0);
38052 let position = args.remove(0);
38053 let occurrence = args.remove(0);
38054 let is_pos_1 = matches!(&position, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(n) if n == "1"));
38055 let is_occ_1 = matches!(&occurrence, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(n) if n == "1"));
38056 if is_pos_1 && is_occ_1 {
38057 Ok(Expression::Function(Box::new(Function::new(
38058 "REGEXP_EXTRACT".to_string(),
38059 vec![val, regex, Expression::number(1)],
38060 ))))
38061 } else {
38062 let subject = if is_pos_1 {
38063 val
38064 } else {
38065 let substring_expr = Expression::Function(Box::new(
38066 Function::new("SUBSTRING".to_string(), vec![val, position]),
38067 ));
38068 Expression::Function(Box::new(Function::new(
38069 "NULLIF".to_string(),
38070 vec![
38071 substring_expr,
38072 Expression::Literal(Box::new(Literal::String(
38073 String::new(),
38074 ))),
38075 ],
38076 )))
38077 };
38078 let extract_all = Expression::Function(Box::new(Function::new(
38079 "REGEXP_EXTRACT_ALL".to_string(),
38080 vec![subject, regex, Expression::number(1)],
38081 )));
38082 Ok(Expression::Function(Box::new(Function::new(
38083 "ARRAY_EXTRACT".to_string(),
38084 vec![extract_all, occurrence],
38085 ))))
38086 }
38087 } else {
38088 Ok(Expression::Function(Box::new(Function {
38089 name: f.name,
38090 args,
38091 distinct: f.distinct,
38092 trailing_comments: f.trailing_comments,
38093 use_bracket_syntax: f.use_bracket_syntax,
38094 no_parens: f.no_parens,
38095 quoted: f.quoted,
38096 span: None,
38097 inferred_type: None,
38098 })))
38099 }
38100 }
38101 DialectType::Snowflake => {
38102 // BigQuery REGEXP_EXTRACT -> Snowflake REGEXP_SUBSTR
38103 Ok(Expression::Function(Box::new(Function::new(
38104 "REGEXP_SUBSTR".to_string(),
38105 args,
38106 ))))
38107 }
38108 _ => {
38109 // For other targets (Hive/Spark/BigQuery): pass through as-is
38110 // BigQuery's default group behavior matches Hive/Spark for 2-arg case
38111 Ok(Expression::Function(Box::new(Function {
38112 name: f.name,
38113 args,
38114 distinct: f.distinct,
38115 trailing_comments: f.trailing_comments,
38116 use_bracket_syntax: f.use_bracket_syntax,
38117 no_parens: f.no_parens,
38118 quoted: f.quoted,
38119 span: None,
38120 inferred_type: None,
38121 })))
38122 }
38123 }
38124 }
38125
38126 // BigQuery STRUCT(args) -> target-specific struct expression
38127 "STRUCT" => {
38128 // Convert Function args to Struct fields
38129 let mut fields: Vec<(Option<String>, Expression)> = Vec::new();
38130 for (i, arg) in args.into_iter().enumerate() {
38131 match arg {
38132 Expression::Alias(a) => {
38133 // Named field: expr AS name
38134 fields.push((Some(a.alias.name.clone()), a.this));
38135 }
38136 other => {
38137 // Unnamed field: for Spark/Hive, keep as None
38138 // For Snowflake, auto-name as _N
38139 // For DuckDB, use column name for column refs, _N for others
38140 if matches!(target, DialectType::Snowflake) {
38141 fields.push((Some(format!("_{}", i)), other));
38142 } else if matches!(target, DialectType::DuckDB) {
38143 let auto_name = match &other {
38144 Expression::Column(col) => col.name.name.clone(),
38145 _ => format!("_{}", i),
38146 };
38147 fields.push((Some(auto_name), other));
38148 } else {
38149 fields.push((None, other));
38150 }
38151 }
38152 }
38153 }
38154
38155 match target {
38156 DialectType::Snowflake => {
38157 // OBJECT_CONSTRUCT('name', value, ...)
38158 let mut oc_args = Vec::new();
38159 for (name, val) in &fields {
38160 if let Some(n) = name {
38161 oc_args.push(Expression::Literal(Box::new(Literal::String(
38162 n.clone(),
38163 ))));
38164 oc_args.push(val.clone());
38165 } else {
38166 oc_args.push(val.clone());
38167 }
38168 }
38169 Ok(Expression::Function(Box::new(Function::new(
38170 "OBJECT_CONSTRUCT".to_string(),
38171 oc_args,
38172 ))))
38173 }
38174 DialectType::DuckDB => {
38175 // {'name': value, ...}
38176 Ok(Expression::Struct(Box::new(crate::expressions::Struct {
38177 fields,
38178 })))
38179 }
38180 DialectType::Hive => {
38181 // STRUCT(val1, val2, ...) - strip aliases
38182 let hive_fields: Vec<(Option<String>, Expression)> =
38183 fields.into_iter().map(|(_, v)| (None, v)).collect();
38184 Ok(Expression::Struct(Box::new(crate::expressions::Struct {
38185 fields: hive_fields,
38186 })))
38187 }
38188 DialectType::Spark | DialectType::Databricks => {
38189 // Use Expression::Struct to bypass Spark target transform auto-naming
38190 Ok(Expression::Struct(Box::new(crate::expressions::Struct {
38191 fields,
38192 })))
38193 }
38194 DialectType::Presto | DialectType::Trino | DialectType::Athena => {
38195 // Check if all fields are named AND all have inferable types - if so, wrap in CAST(ROW(...) AS ROW(name TYPE, ...))
38196 let all_named =
38197 !fields.is_empty() && fields.iter().all(|(name, _)| name.is_some());
38198 let all_types_inferable = all_named
38199 && fields
38200 .iter()
38201 .all(|(_, val)| Self::can_infer_presto_type(val));
38202 let row_args: Vec<Expression> =
38203 fields.iter().map(|(_, v)| v.clone()).collect();
38204 let row_expr = Expression::Function(Box::new(Function::new(
38205 "ROW".to_string(),
38206 row_args,
38207 )));
38208 if all_named && all_types_inferable {
38209 // Build ROW type with inferred types
38210 let mut row_type_fields = Vec::new();
38211 for (name, val) in &fields {
38212 if let Some(n) = name {
38213 let type_str = Self::infer_sql_type_for_presto(val);
38214 row_type_fields.push(crate::expressions::StructField::new(
38215 n.clone(),
38216 crate::expressions::DataType::Custom { name: type_str },
38217 ));
38218 }
38219 }
38220 let row_type = crate::expressions::DataType::Struct {
38221 fields: row_type_fields,
38222 nested: true,
38223 };
38224 Ok(Expression::Cast(Box::new(Cast {
38225 this: row_expr,
38226 to: row_type,
38227 trailing_comments: Vec::new(),
38228 double_colon_syntax: false,
38229 format: None,
38230 default: None,
38231 inferred_type: None,
38232 })))
38233 } else {
38234 Ok(row_expr)
38235 }
38236 }
38237 _ => {
38238 // Default: keep as STRUCT function with original args
38239 let mut new_args = Vec::new();
38240 for (name, val) in fields {
38241 if let Some(n) = name {
38242 new_args.push(Expression::Alias(Box::new(
38243 crate::expressions::Alias::new(val, Identifier::new(n)),
38244 )));
38245 } else {
38246 new_args.push(val);
38247 }
38248 }
38249 Ok(Expression::Function(Box::new(Function::new(
38250 "STRUCT".to_string(),
38251 new_args,
38252 ))))
38253 }
38254 }
38255 }
38256
38257 // ROUND(x, n, 'ROUND_HALF_EVEN') -> ROUND_EVEN(x, n) for DuckDB
38258 "ROUND" if args.len() == 3 => {
38259 let x = args.remove(0);
38260 let n = args.remove(0);
38261 let mode = args.remove(0);
38262 // Check if mode is 'ROUND_HALF_EVEN'
38263 let is_half_even = matches!(&mode, Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(s) if s.eq_ignore_ascii_case("ROUND_HALF_EVEN")));
38264 if is_half_even && matches!(target, DialectType::DuckDB) {
38265 Ok(Expression::Function(Box::new(Function::new(
38266 "ROUND_EVEN".to_string(),
38267 vec![x, n],
38268 ))))
38269 } else {
38270 // Pass through with all args
38271 Ok(Expression::Function(Box::new(Function::new(
38272 "ROUND".to_string(),
38273 vec![x, n, mode],
38274 ))))
38275 }
38276 }
38277
38278 // MAKE_INTERVAL(year, month, named_args...) -> INTERVAL string for Snowflake/DuckDB
38279 "MAKE_INTERVAL" => {
38280 // MAKE_INTERVAL(1, 2, minute => 5, day => 3)
38281 // The positional args are: year, month
38282 // Named args are: day =>, minute =>, etc.
38283 // For Snowflake: INTERVAL '1 year, 2 month, 5 minute, 3 day'
38284 // For DuckDB: INTERVAL '1 year 2 month 5 minute 3 day'
38285 // For BigQuery->BigQuery: reorder named args (day before minute)
38286 if matches!(target, DialectType::Snowflake | DialectType::DuckDB) {
38287 let mut parts: Vec<(String, String)> = Vec::new();
38288 let mut pos_idx = 0;
38289 let pos_units = ["year", "month"];
38290 for arg in &args {
38291 if let Expression::NamedArgument(na) = arg {
38292 // Named arg like minute => 5
38293 let unit = na.name.name.clone();
38294 if let Expression::Literal(lit) = &na.value {
38295 if let Literal::Number(n) = lit.as_ref() {
38296 parts.push((unit, n.clone()));
38297 }
38298 }
38299 } else if pos_idx < pos_units.len() {
38300 if let Expression::Literal(lit) = arg {
38301 if let Literal::Number(n) = lit.as_ref() {
38302 parts.push((pos_units[pos_idx].to_string(), n.clone()));
38303 }
38304 }
38305 pos_idx += 1;
38306 }
38307 }
38308 // Don't sort - preserve original argument order
38309 let separator = if matches!(target, DialectType::Snowflake) {
38310 ", "
38311 } else {
38312 " "
38313 };
38314 let interval_str = parts
38315 .iter()
38316 .map(|(u, v)| format!("{} {}", v, u))
38317 .collect::<Vec<_>>()
38318 .join(separator);
38319 Ok(Expression::Interval(Box::new(
38320 crate::expressions::Interval {
38321 this: Some(Expression::Literal(Box::new(Literal::String(
38322 interval_str,
38323 )))),
38324 unit: None,
38325 },
38326 )))
38327 } else if matches!(target, DialectType::BigQuery) {
38328 // BigQuery->BigQuery: reorder named args (day, minute, etc.)
38329 let mut positional = Vec::new();
38330 let mut named: Vec<(
38331 String,
38332 Expression,
38333 crate::expressions::NamedArgSeparator,
38334 )> = Vec::new();
38335 let _pos_units = ["year", "month"];
38336 let mut _pos_idx = 0;
38337 for arg in args {
38338 if let Expression::NamedArgument(na) = arg {
38339 named.push((na.name.name.clone(), na.value, na.separator));
38340 } else {
38341 positional.push(arg);
38342 _pos_idx += 1;
38343 }
38344 }
38345 // Sort named args by: day, hour, minute, second
38346 let unit_order = |u: &str| -> usize {
38347 match u.to_ascii_lowercase().as_str() {
38348 "day" => 0,
38349 "hour" => 1,
38350 "minute" => 2,
38351 "second" => 3,
38352 _ => 4,
38353 }
38354 };
38355 named.sort_by_key(|(u, _, _)| unit_order(u));
38356 let mut result_args = positional;
38357 for (name, value, sep) in named {
38358 result_args.push(Expression::NamedArgument(Box::new(
38359 crate::expressions::NamedArgument {
38360 name: Identifier::new(&name),
38361 value,
38362 separator: sep,
38363 },
38364 )));
38365 }
38366 Ok(Expression::Function(Box::new(Function::new(
38367 "MAKE_INTERVAL".to_string(),
38368 result_args,
38369 ))))
38370 } else {
38371 Ok(Expression::Function(Box::new(Function::new(
38372 "MAKE_INTERVAL".to_string(),
38373 args,
38374 ))))
38375 }
38376 }
38377
38378 // ARRAY_TO_STRING(array, sep, null_text) -> ARRAY_TO_STRING(LIST_TRANSFORM(array, x -> COALESCE(x, null_text)), sep) for DuckDB
38379 "ARRAY_TO_STRING" if args.len() == 3 => {
38380 let arr = args.remove(0);
38381 let sep = args.remove(0);
38382 let null_text = args.remove(0);
38383 match target {
38384 DialectType::DuckDB => {
38385 // LIST_TRANSFORM(array, x -> COALESCE(x, null_text))
38386 let _lambda_param =
38387 Expression::Identifier(crate::expressions::Identifier::new("x"));
38388 let coalesce =
38389 Expression::Coalesce(Box::new(crate::expressions::VarArgFunc {
38390 original_name: None,
38391 expressions: vec![
38392 Expression::Identifier(crate::expressions::Identifier::new(
38393 "x",
38394 )),
38395 null_text,
38396 ],
38397 inferred_type: None,
38398 }));
38399 let lambda = Expression::Lambda(Box::new(crate::expressions::LambdaExpr {
38400 parameters: vec![crate::expressions::Identifier::new("x")],
38401 body: coalesce,
38402 colon: false,
38403 parameter_types: vec![],
38404 }));
38405 let list_transform = Expression::Function(Box::new(Function::new(
38406 "LIST_TRANSFORM".to_string(),
38407 vec![arr, lambda],
38408 )));
38409 Ok(Expression::Function(Box::new(Function::new(
38410 "ARRAY_TO_STRING".to_string(),
38411 vec![list_transform, sep],
38412 ))))
38413 }
38414 _ => Ok(Expression::Function(Box::new(Function::new(
38415 "ARRAY_TO_STRING".to_string(),
38416 vec![arr, sep, null_text],
38417 )))),
38418 }
38419 }
38420
38421 // LENGTH(x) -> CASE TYPEOF(x) ... for DuckDB
38422 "LENGTH" if args.len() == 1 => {
38423 let arg = args.remove(0);
38424 match target {
38425 DialectType::DuckDB => {
38426 // CASE TYPEOF(foo) WHEN 'BLOB' THEN OCTET_LENGTH(CAST(foo AS BLOB)) ELSE LENGTH(CAST(foo AS TEXT)) END
38427 let typeof_func = Expression::Function(Box::new(Function::new(
38428 "TYPEOF".to_string(),
38429 vec![arg.clone()],
38430 )));
38431 let blob_cast = Expression::Cast(Box::new(Cast {
38432 this: arg.clone(),
38433 to: DataType::VarBinary { length: None },
38434 trailing_comments: vec![],
38435 double_colon_syntax: false,
38436 format: None,
38437 default: None,
38438 inferred_type: None,
38439 }));
38440 let octet_length = Expression::Function(Box::new(Function::new(
38441 "OCTET_LENGTH".to_string(),
38442 vec![blob_cast],
38443 )));
38444 let text_cast = Expression::Cast(Box::new(Cast {
38445 this: arg,
38446 to: DataType::Text,
38447 trailing_comments: vec![],
38448 double_colon_syntax: false,
38449 format: None,
38450 default: None,
38451 inferred_type: None,
38452 }));
38453 let length_text = Expression::Function(Box::new(Function::new(
38454 "LENGTH".to_string(),
38455 vec![text_cast],
38456 )));
38457 Ok(Expression::Case(Box::new(crate::expressions::Case {
38458 operand: Some(typeof_func),
38459 whens: vec![(
38460 Expression::Literal(Box::new(Literal::String("BLOB".to_string()))),
38461 octet_length,
38462 )],
38463 else_: Some(length_text),
38464 comments: Vec::new(),
38465 inferred_type: None,
38466 })))
38467 }
38468 _ => Ok(Expression::Function(Box::new(Function::new(
38469 "LENGTH".to_string(),
38470 vec![arg],
38471 )))),
38472 }
38473 }
38474
38475 // PERCENTILE_CONT(x, fraction RESPECT NULLS) -> QUANTILE_CONT(x, fraction) for DuckDB
38476 "PERCENTILE_CONT" if args.len() >= 2 && matches!(source, DialectType::BigQuery) => {
38477 // BigQuery PERCENTILE_CONT(x, fraction [RESPECT|IGNORE NULLS]) OVER ()
38478 // The args should be [x, fraction] with the null handling stripped
38479 // For DuckDB: QUANTILE_CONT(x, fraction)
38480 // For Spark: PERCENTILE_CONT(x, fraction) RESPECT NULLS (handled at window level)
38481 match target {
38482 DialectType::DuckDB => {
38483 // Strip down to just 2 args, rename to QUANTILE_CONT
38484 let x = args[0].clone();
38485 let frac = args[1].clone();
38486 Ok(Expression::Function(Box::new(Function::new(
38487 "QUANTILE_CONT".to_string(),
38488 vec![x, frac],
38489 ))))
38490 }
38491 _ => Ok(Expression::Function(Box::new(Function::new(
38492 "PERCENTILE_CONT".to_string(),
38493 args,
38494 )))),
38495 }
38496 }
38497
38498 // All others: pass through
38499 _ => Ok(Expression::Function(Box::new(Function {
38500 name: f.name,
38501 args,
38502 distinct: f.distinct,
38503 trailing_comments: f.trailing_comments,
38504 use_bracket_syntax: f.use_bracket_syntax,
38505 no_parens: f.no_parens,
38506 quoted: f.quoted,
38507 span: None,
38508 inferred_type: None,
38509 }))),
38510 }
38511 }
38512
38513 /// Check if we can reliably infer the SQL type for Presto/Trino ROW CAST.
38514 /// Returns false for column references and other non-literal expressions where the type is unknown.
38515 fn can_infer_presto_type(expr: &Expression) -> bool {
38516 match expr {
38517 Expression::Literal(_) => true,
38518 Expression::Boolean(_) => true,
38519 Expression::Array(_) | Expression::ArrayFunc(_) => true,
38520 Expression::Struct(_) | Expression::StructFunc(_) => true,
38521 Expression::Function(f) => {
38522 f.name.eq_ignore_ascii_case("STRUCT")
38523 || f.name.eq_ignore_ascii_case("ROW")
38524 || f.name.eq_ignore_ascii_case("CURRENT_DATE")
38525 || f.name.eq_ignore_ascii_case("CURRENT_TIMESTAMP")
38526 || f.name.eq_ignore_ascii_case("NOW")
38527 }
38528 Expression::Cast(_) => true,
38529 Expression::Neg(inner) => Self::can_infer_presto_type(&inner.this),
38530 _ => false,
38531 }
38532 }
38533
38534 /// Infer SQL type name for a Presto/Trino ROW CAST from a literal expression
38535 fn infer_sql_type_for_presto(expr: &Expression) -> String {
38536 use crate::expressions::Literal;
38537 match expr {
38538 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
38539 "VARCHAR".to_string()
38540 }
38541 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)) => {
38542 let Literal::Number(n) = lit.as_ref() else {
38543 unreachable!()
38544 };
38545 if n.contains('.') {
38546 "DOUBLE".to_string()
38547 } else {
38548 "INTEGER".to_string()
38549 }
38550 }
38551 Expression::Boolean(_) => "BOOLEAN".to_string(),
38552 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Date(_)) => {
38553 "DATE".to_string()
38554 }
38555 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Timestamp(_)) => {
38556 "TIMESTAMP".to_string()
38557 }
38558 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Datetime(_)) => {
38559 "TIMESTAMP".to_string()
38560 }
38561 Expression::Array(_) | Expression::ArrayFunc(_) => "ARRAY(VARCHAR)".to_string(),
38562 Expression::Struct(_) | Expression::StructFunc(_) => "ROW".to_string(),
38563 Expression::Function(f) => {
38564 if f.name.eq_ignore_ascii_case("STRUCT") || f.name.eq_ignore_ascii_case("ROW") {
38565 "ROW".to_string()
38566 } else if f.name.eq_ignore_ascii_case("CURRENT_DATE") {
38567 "DATE".to_string()
38568 } else if f.name.eq_ignore_ascii_case("CURRENT_TIMESTAMP")
38569 || f.name.eq_ignore_ascii_case("NOW")
38570 {
38571 "TIMESTAMP".to_string()
38572 } else {
38573 "VARCHAR".to_string()
38574 }
38575 }
38576 Expression::Cast(c) => {
38577 // If already cast, use the target type
38578 Self::data_type_to_presto_string(&c.to)
38579 }
38580 _ => "VARCHAR".to_string(),
38581 }
38582 }
38583
38584 /// Convert a DataType to its Presto/Trino string representation for ROW type
38585 fn data_type_to_presto_string(dt: &crate::expressions::DataType) -> String {
38586 use crate::expressions::DataType;
38587 match dt {
38588 DataType::VarChar { .. } | DataType::Text | DataType::String { .. } => {
38589 "VARCHAR".to_string()
38590 }
38591 DataType::Int { .. }
38592 | DataType::BigInt { .. }
38593 | DataType::SmallInt { .. }
38594 | DataType::TinyInt { .. } => "INTEGER".to_string(),
38595 DataType::Float { .. } | DataType::Double { .. } => "DOUBLE".to_string(),
38596 DataType::Boolean => "BOOLEAN".to_string(),
38597 DataType::Date => "DATE".to_string(),
38598 DataType::Timestamp { .. } => "TIMESTAMP".to_string(),
38599 DataType::Struct { fields, .. } => {
38600 let field_strs: Vec<String> = fields
38601 .iter()
38602 .map(|f| {
38603 format!(
38604 "{} {}",
38605 f.name,
38606 Self::data_type_to_presto_string(&f.data_type)
38607 )
38608 })
38609 .collect();
38610 format!("ROW({})", field_strs.join(", "))
38611 }
38612 DataType::Array { element_type, .. } => {
38613 format!("ARRAY({})", Self::data_type_to_presto_string(element_type))
38614 }
38615 DataType::Custom { name } => {
38616 // Pass through custom type names (e.g., "INTEGER", "VARCHAR" from earlier inference)
38617 name.clone()
38618 }
38619 _ => "VARCHAR".to_string(),
38620 }
38621 }
38622
38623 /// Convert IntervalUnit to string
38624 fn interval_unit_to_string(unit: &crate::expressions::IntervalUnit) -> &'static str {
38625 match unit {
38626 crate::expressions::IntervalUnit::Year => "YEAR",
38627 crate::expressions::IntervalUnit::Quarter => "QUARTER",
38628 crate::expressions::IntervalUnit::Month => "MONTH",
38629 crate::expressions::IntervalUnit::Week => "WEEK",
38630 crate::expressions::IntervalUnit::Day => "DAY",
38631 crate::expressions::IntervalUnit::Hour => "HOUR",
38632 crate::expressions::IntervalUnit::Minute => "MINUTE",
38633 crate::expressions::IntervalUnit::Second => "SECOND",
38634 crate::expressions::IntervalUnit::Millisecond => "MILLISECOND",
38635 crate::expressions::IntervalUnit::Microsecond => "MICROSECOND",
38636 crate::expressions::IntervalUnit::Nanosecond => "NANOSECOND",
38637 }
38638 }
38639
38640 /// Extract unit string from an expression (uppercased)
38641 fn get_unit_str_static(expr: &Expression) -> String {
38642 use crate::expressions::Literal;
38643 match expr {
38644 Expression::Identifier(id) => id.name.to_ascii_uppercase(),
38645 Expression::Var(v) => v.this.to_ascii_uppercase(),
38646 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
38647 let Literal::String(s) = lit.as_ref() else {
38648 unreachable!()
38649 };
38650 s.to_ascii_uppercase()
38651 }
38652 Expression::Column(col) => col.name.name.to_ascii_uppercase(),
38653 Expression::Function(f) => {
38654 let base = f.name.to_ascii_uppercase();
38655 if !f.args.is_empty() {
38656 let inner = Self::get_unit_str_static(&f.args[0]);
38657 format!("{}({})", base, inner)
38658 } else {
38659 base
38660 }
38661 }
38662 _ => "DAY".to_string(),
38663 }
38664 }
38665
38666 /// Parse unit string to IntervalUnit
38667 fn parse_interval_unit_static(s: &str) -> crate::expressions::IntervalUnit {
38668 match s {
38669 "YEAR" | "YY" | "YYYY" => crate::expressions::IntervalUnit::Year,
38670 "QUARTER" | "QQ" | "Q" => crate::expressions::IntervalUnit::Quarter,
38671 "MONTH" | "MONTHS" | "MON" | "MONS" | "MM" | "M" => {
38672 crate::expressions::IntervalUnit::Month
38673 }
38674 "WEEK" | "WK" | "WW" | "ISOWEEK" => crate::expressions::IntervalUnit::Week,
38675 "DAY" | "DD" | "D" | "DY" => crate::expressions::IntervalUnit::Day,
38676 "HOUR" | "HH" => crate::expressions::IntervalUnit::Hour,
38677 "MINUTE" | "MI" | "N" => crate::expressions::IntervalUnit::Minute,
38678 "SECOND" | "SS" | "S" => crate::expressions::IntervalUnit::Second,
38679 "MILLISECOND" | "MS" => crate::expressions::IntervalUnit::Millisecond,
38680 "MICROSECOND" | "MCS" | "US" => crate::expressions::IntervalUnit::Microsecond,
38681 _ if s.starts_with("WEEK(") => crate::expressions::IntervalUnit::Week,
38682 _ => crate::expressions::IntervalUnit::Day,
38683 }
38684 }
38685
38686 /// Convert expression to simple string for interval building
38687 fn expr_to_string_static(expr: &Expression) -> String {
38688 use crate::expressions::Literal;
38689 match expr {
38690 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)) => {
38691 let Literal::Number(s) = lit.as_ref() else {
38692 unreachable!()
38693 };
38694 s.clone()
38695 }
38696 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
38697 let Literal::String(s) = lit.as_ref() else {
38698 unreachable!()
38699 };
38700 s.clone()
38701 }
38702 Expression::Identifier(id) => id.name.clone(),
38703 Expression::Neg(f) => format!("-{}", Self::expr_to_string_static(&f.this)),
38704 _ => "1".to_string(),
38705 }
38706 }
38707
38708 /// Extract a simple string representation from a literal expression
38709 fn expr_to_string(expr: &Expression) -> String {
38710 use crate::expressions::Literal;
38711 match expr {
38712 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)) => {
38713 let Literal::Number(s) = lit.as_ref() else {
38714 unreachable!()
38715 };
38716 s.clone()
38717 }
38718 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
38719 let Literal::String(s) = lit.as_ref() else {
38720 unreachable!()
38721 };
38722 s.clone()
38723 }
38724 Expression::Neg(f) => format!("-{}", Self::expr_to_string(&f.this)),
38725 Expression::Identifier(id) => id.name.clone(),
38726 _ => "1".to_string(),
38727 }
38728 }
38729
38730 /// Quote an interval value expression as a string literal if it's a number (or negated number)
38731 fn quote_interval_val(expr: &Expression) -> Expression {
38732 use crate::expressions::Literal;
38733 match expr {
38734 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Number(_)) => {
38735 let Literal::Number(n) = lit.as_ref() else {
38736 unreachable!()
38737 };
38738 Expression::Literal(Box::new(Literal::String(n.clone())))
38739 }
38740 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => expr.clone(),
38741 Expression::Neg(inner) => {
38742 if let Expression::Literal(lit) = &inner.this {
38743 if let Literal::Number(n) = lit.as_ref() {
38744 Expression::Literal(Box::new(Literal::String(format!("-{}", n))))
38745 } else {
38746 inner.this.clone()
38747 }
38748 } else {
38749 expr.clone()
38750 }
38751 }
38752 _ => expr.clone(),
38753 }
38754 }
38755
38756 /// Check if a timestamp string contains timezone info (offset like +02:00, or named timezone)
38757 fn timestamp_string_has_timezone(ts: &str) -> bool {
38758 let trimmed = ts.trim();
38759 // Check for numeric timezone offsets: +N, -N, +NN:NN, -NN:NN at end
38760 if let Some(last_space) = trimmed.rfind(' ') {
38761 let suffix = &trimmed[last_space + 1..];
38762 if (suffix.starts_with('+') || suffix.starts_with('-')) && suffix.len() > 1 {
38763 let rest = &suffix[1..];
38764 if rest.chars().all(|c| c.is_ascii_digit() || c == ':') {
38765 return true;
38766 }
38767 }
38768 }
38769 // Check for named timezone abbreviations
38770 let ts_lower = trimmed.to_ascii_lowercase();
38771 let tz_abbrevs = [" utc", " gmt", " cet", " est", " pst", " cst", " mst"];
38772 for abbrev in &tz_abbrevs {
38773 if ts_lower.ends_with(abbrev) {
38774 return true;
38775 }
38776 }
38777 false
38778 }
38779
38780 /// Maybe CAST timestamp literal to TIMESTAMPTZ for Snowflake
38781 fn maybe_cast_ts_to_tz(expr: Expression, func_name: &str) -> Expression {
38782 use crate::expressions::{Cast, DataType, Literal};
38783 match expr {
38784 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Timestamp(_)) => {
38785 let Literal::Timestamp(s) = lit.as_ref() else {
38786 unreachable!()
38787 };
38788 let tz = func_name.starts_with("TIMESTAMP");
38789 Expression::Cast(Box::new(Cast {
38790 this: Expression::Literal(Box::new(Literal::String(s.clone()))),
38791 to: if tz {
38792 DataType::Timestamp {
38793 timezone: true,
38794 precision: None,
38795 }
38796 } else {
38797 DataType::Timestamp {
38798 timezone: false,
38799 precision: None,
38800 }
38801 },
38802 trailing_comments: vec![],
38803 double_colon_syntax: false,
38804 format: None,
38805 default: None,
38806 inferred_type: None,
38807 }))
38808 }
38809 other => other,
38810 }
38811 }
38812
38813 /// Maybe CAST timestamp literal to TIMESTAMP (no tz)
38814 fn maybe_cast_ts(expr: Expression) -> Expression {
38815 use crate::expressions::{Cast, DataType, Literal};
38816 match expr {
38817 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Timestamp(_)) => {
38818 let Literal::Timestamp(s) = lit.as_ref() else {
38819 unreachable!()
38820 };
38821 Expression::Cast(Box::new(Cast {
38822 this: Expression::Literal(Box::new(Literal::String(s.clone()))),
38823 to: DataType::Timestamp {
38824 timezone: false,
38825 precision: None,
38826 },
38827 trailing_comments: vec![],
38828 double_colon_syntax: false,
38829 format: None,
38830 default: None,
38831 inferred_type: None,
38832 }))
38833 }
38834 other => other,
38835 }
38836 }
38837
38838 /// Convert DATE 'x' literal to CAST('x' AS DATE)
38839 fn date_literal_to_cast(expr: Expression) -> Expression {
38840 use crate::expressions::{Cast, DataType, Literal};
38841 match expr {
38842 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Date(_)) => {
38843 let Literal::Date(s) = lit.as_ref() else {
38844 unreachable!()
38845 };
38846 Expression::Cast(Box::new(Cast {
38847 this: Expression::Literal(Box::new(Literal::String(s.clone()))),
38848 to: DataType::Date,
38849 trailing_comments: vec![],
38850 double_colon_syntax: false,
38851 format: None,
38852 default: None,
38853 inferred_type: None,
38854 }))
38855 }
38856 other => other,
38857 }
38858 }
38859
38860 /// Ensure an expression that should be a date is CAST(... AS DATE).
38861 /// Handles both DATE literals and string literals that look like dates.
38862 fn ensure_cast_date(expr: Expression) -> Expression {
38863 use crate::expressions::{Cast, DataType, Literal};
38864 match expr {
38865 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Date(_)) => {
38866 let Literal::Date(s) = lit.as_ref() else {
38867 unreachable!()
38868 };
38869 Expression::Cast(Box::new(Cast {
38870 this: Expression::Literal(Box::new(Literal::String(s.clone()))),
38871 to: DataType::Date,
38872 trailing_comments: vec![],
38873 double_colon_syntax: false,
38874 format: None,
38875 default: None,
38876 inferred_type: None,
38877 }))
38878 }
38879 Expression::Literal(ref lit) if matches!(lit.as_ref(), Literal::String(ref _s)) => {
38880 // String literal that should be a date -> CAST('s' AS DATE)
38881 Expression::Cast(Box::new(Cast {
38882 this: expr,
38883 to: DataType::Date,
38884 trailing_comments: vec![],
38885 double_colon_syntax: false,
38886 format: None,
38887 default: None,
38888 inferred_type: None,
38889 }))
38890 }
38891 // Already a CAST or other expression -> leave as-is
38892 other => other,
38893 }
38894 }
38895
38896 /// Force CAST(expr AS DATE) for any expression (not just literals)
38897 /// Skips if the expression is already a CAST to DATE
38898 fn force_cast_date(expr: Expression) -> Expression {
38899 use crate::expressions::{Cast, DataType};
38900 // If it's already a CAST to DATE, don't double-wrap
38901 if let Expression::Cast(ref c) = expr {
38902 if matches!(c.to, DataType::Date) {
38903 return expr;
38904 }
38905 }
38906 Expression::Cast(Box::new(Cast {
38907 this: expr,
38908 to: DataType::Date,
38909 trailing_comments: vec![],
38910 double_colon_syntax: false,
38911 format: None,
38912 default: None,
38913 inferred_type: None,
38914 }))
38915 }
38916
38917 /// Internal TO_DATE function that won't be converted to CAST by the Snowflake handler.
38918 /// Uses the name `_POLYGLOT_TO_DATE` which is not recognized by the TO_DATE -> CAST logic.
38919 /// The Snowflake DATEDIFF handler converts these back to TO_DATE.
38920 const PRESERVED_TO_DATE: &'static str = "_POLYGLOT_TO_DATE";
38921
38922 fn ensure_to_date_preserved(expr: Expression) -> Expression {
38923 use crate::expressions::{Function, Literal};
38924 if matches!(expr, Expression::Literal(ref lit) if matches!(lit.as_ref(), Literal::String(_)))
38925 {
38926 Expression::Function(Box::new(Function::new(
38927 Self::PRESERVED_TO_DATE.to_string(),
38928 vec![expr],
38929 )))
38930 } else {
38931 expr
38932 }
38933 }
38934
38935 /// TRY_CAST(expr AS DATE) - used for DuckDB when TO_DATE is unwrapped
38936 fn try_cast_date(expr: Expression) -> Expression {
38937 use crate::expressions::{Cast, DataType};
38938 Expression::TryCast(Box::new(Cast {
38939 this: expr,
38940 to: DataType::Date,
38941 trailing_comments: vec![],
38942 double_colon_syntax: false,
38943 format: None,
38944 default: None,
38945 inferred_type: None,
38946 }))
38947 }
38948
38949 /// CAST(CAST(expr AS TIMESTAMP) AS DATE) - used when Hive string dates need to be cast
38950 fn double_cast_timestamp_date(expr: Expression) -> Expression {
38951 use crate::expressions::{Cast, DataType};
38952 let inner = Expression::Cast(Box::new(Cast {
38953 this: expr,
38954 to: DataType::Timestamp {
38955 timezone: false,
38956 precision: None,
38957 },
38958 trailing_comments: vec![],
38959 double_colon_syntax: false,
38960 format: None,
38961 default: None,
38962 inferred_type: None,
38963 }));
38964 Expression::Cast(Box::new(Cast {
38965 this: inner,
38966 to: DataType::Date,
38967 trailing_comments: vec![],
38968 double_colon_syntax: false,
38969 format: None,
38970 default: None,
38971 inferred_type: None,
38972 }))
38973 }
38974
38975 /// CAST(CAST(expr AS DATETIME) AS DATE) - BigQuery variant
38976 fn double_cast_datetime_date(expr: Expression) -> Expression {
38977 use crate::expressions::{Cast, DataType};
38978 let inner = Expression::Cast(Box::new(Cast {
38979 this: expr,
38980 to: DataType::Custom {
38981 name: "DATETIME".to_string(),
38982 },
38983 trailing_comments: vec![],
38984 double_colon_syntax: false,
38985 format: None,
38986 default: None,
38987 inferred_type: None,
38988 }));
38989 Expression::Cast(Box::new(Cast {
38990 this: inner,
38991 to: DataType::Date,
38992 trailing_comments: vec![],
38993 double_colon_syntax: false,
38994 format: None,
38995 default: None,
38996 inferred_type: None,
38997 }))
38998 }
38999
39000 /// CAST(CAST(expr AS DATETIME2) AS DATE) - TSQL variant
39001 fn double_cast_datetime2_date(expr: Expression) -> Expression {
39002 use crate::expressions::{Cast, DataType};
39003 let inner = Expression::Cast(Box::new(Cast {
39004 this: expr,
39005 to: DataType::Custom {
39006 name: "DATETIME2".to_string(),
39007 },
39008 trailing_comments: vec![],
39009 double_colon_syntax: false,
39010 format: None,
39011 default: None,
39012 inferred_type: None,
39013 }));
39014 Expression::Cast(Box::new(Cast {
39015 this: inner,
39016 to: DataType::Date,
39017 trailing_comments: vec![],
39018 double_colon_syntax: false,
39019 format: None,
39020 default: None,
39021 inferred_type: None,
39022 }))
39023 }
39024
39025 /// Convert Hive/Java-style date format strings to C-style (strftime) format
39026 /// e.g., "yyyy-MM-dd'T'HH" -> "%Y-%m-%d'T'%H"
39027 fn hive_format_to_c_format(fmt: &str) -> String {
39028 let mut result = String::new();
39029 let chars: Vec<char> = fmt.chars().collect();
39030 let mut i = 0;
39031 while i < chars.len() {
39032 match chars[i] {
39033 'y' => {
39034 let mut count = 0;
39035 while i < chars.len() && chars[i] == 'y' {
39036 count += 1;
39037 i += 1;
39038 }
39039 if count >= 4 {
39040 result.push_str("%Y");
39041 } else if count == 2 {
39042 result.push_str("%y");
39043 } else {
39044 result.push_str("%Y");
39045 }
39046 }
39047 'M' => {
39048 let mut count = 0;
39049 while i < chars.len() && chars[i] == 'M' {
39050 count += 1;
39051 i += 1;
39052 }
39053 if count >= 3 {
39054 result.push_str("%b");
39055 } else if count == 2 {
39056 result.push_str("%m");
39057 } else {
39058 result.push_str("%m");
39059 }
39060 }
39061 'd' => {
39062 let mut _count = 0;
39063 while i < chars.len() && chars[i] == 'd' {
39064 _count += 1;
39065 i += 1;
39066 }
39067 result.push_str("%d");
39068 }
39069 'H' => {
39070 let mut _count = 0;
39071 while i < chars.len() && chars[i] == 'H' {
39072 _count += 1;
39073 i += 1;
39074 }
39075 result.push_str("%H");
39076 }
39077 'h' => {
39078 let mut _count = 0;
39079 while i < chars.len() && chars[i] == 'h' {
39080 _count += 1;
39081 i += 1;
39082 }
39083 result.push_str("%I");
39084 }
39085 'm' => {
39086 let mut _count = 0;
39087 while i < chars.len() && chars[i] == 'm' {
39088 _count += 1;
39089 i += 1;
39090 }
39091 result.push_str("%M");
39092 }
39093 's' => {
39094 let mut _count = 0;
39095 while i < chars.len() && chars[i] == 's' {
39096 _count += 1;
39097 i += 1;
39098 }
39099 result.push_str("%S");
39100 }
39101 'S' => {
39102 // Fractional seconds - skip
39103 while i < chars.len() && chars[i] == 'S' {
39104 i += 1;
39105 }
39106 result.push_str("%f");
39107 }
39108 'a' => {
39109 // AM/PM
39110 while i < chars.len() && chars[i] == 'a' {
39111 i += 1;
39112 }
39113 result.push_str("%p");
39114 }
39115 'E' => {
39116 let mut count = 0;
39117 while i < chars.len() && chars[i] == 'E' {
39118 count += 1;
39119 i += 1;
39120 }
39121 if count >= 4 {
39122 result.push_str("%A");
39123 } else {
39124 result.push_str("%a");
39125 }
39126 }
39127 '\'' => {
39128 // Quoted literal text - pass through the quotes and content
39129 result.push('\'');
39130 i += 1;
39131 while i < chars.len() && chars[i] != '\'' {
39132 result.push(chars[i]);
39133 i += 1;
39134 }
39135 if i < chars.len() {
39136 result.push('\'');
39137 i += 1;
39138 }
39139 }
39140 c => {
39141 result.push(c);
39142 i += 1;
39143 }
39144 }
39145 }
39146 result
39147 }
39148
39149 /// Convert Hive/Java format to Presto format (uses %T for HH:mm:ss)
39150 fn hive_format_to_presto_format(fmt: &str) -> String {
39151 let c_fmt = Self::hive_format_to_c_format(fmt);
39152 // Presto uses %T for HH:MM:SS
39153 c_fmt.replace("%H:%M:%S", "%T")
39154 }
39155
39156 /// Ensure a timestamp-like expression for DuckDB with CAST(... AS TIMESTAMP)
39157 fn ensure_cast_timestamp(expr: Expression) -> Expression {
39158 use crate::expressions::{Cast, DataType, Literal};
39159 match expr {
39160 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Timestamp(_)) => {
39161 let Literal::Timestamp(s) = lit.as_ref() else {
39162 unreachable!()
39163 };
39164 Expression::Cast(Box::new(Cast {
39165 this: Expression::Literal(Box::new(Literal::String(s.clone()))),
39166 to: DataType::Timestamp {
39167 timezone: false,
39168 precision: None,
39169 },
39170 trailing_comments: vec![],
39171 double_colon_syntax: false,
39172 format: None,
39173 default: None,
39174 inferred_type: None,
39175 }))
39176 }
39177 Expression::Literal(ref lit) if matches!(lit.as_ref(), Literal::String(ref _s)) => {
39178 Expression::Cast(Box::new(Cast {
39179 this: expr,
39180 to: DataType::Timestamp {
39181 timezone: false,
39182 precision: None,
39183 },
39184 trailing_comments: vec![],
39185 double_colon_syntax: false,
39186 format: None,
39187 default: None,
39188 inferred_type: None,
39189 }))
39190 }
39191 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Datetime(_)) => {
39192 let Literal::Datetime(s) = lit.as_ref() else {
39193 unreachable!()
39194 };
39195 Expression::Cast(Box::new(Cast {
39196 this: Expression::Literal(Box::new(Literal::String(s.clone()))),
39197 to: DataType::Timestamp {
39198 timezone: false,
39199 precision: None,
39200 },
39201 trailing_comments: vec![],
39202 double_colon_syntax: false,
39203 format: None,
39204 default: None,
39205 inferred_type: None,
39206 }))
39207 }
39208 other => other,
39209 }
39210 }
39211
39212 /// Force CAST to TIMESTAMP for any expression (not just literals)
39213 /// Used when transpiling from Redshift/TSQL where DATEDIFF/DATEADD args need explicit timestamp cast
39214 fn force_cast_timestamp(expr: Expression) -> Expression {
39215 use crate::expressions::{Cast, DataType};
39216 // Don't double-wrap if already a CAST to TIMESTAMP
39217 if let Expression::Cast(ref c) = expr {
39218 if matches!(c.to, DataType::Timestamp { .. }) {
39219 return expr;
39220 }
39221 }
39222 Expression::Cast(Box::new(Cast {
39223 this: expr,
39224 to: DataType::Timestamp {
39225 timezone: false,
39226 precision: None,
39227 },
39228 trailing_comments: vec![],
39229 double_colon_syntax: false,
39230 format: None,
39231 default: None,
39232 inferred_type: None,
39233 }))
39234 }
39235
39236 /// Ensure a timestamp-like expression for DuckDB with CAST(... AS TIMESTAMPTZ)
39237 fn ensure_cast_timestamptz(expr: Expression) -> Expression {
39238 use crate::expressions::{Cast, DataType, Literal};
39239 match expr {
39240 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Timestamp(_)) => {
39241 let Literal::Timestamp(s) = lit.as_ref() else {
39242 unreachable!()
39243 };
39244 Expression::Cast(Box::new(Cast {
39245 this: Expression::Literal(Box::new(Literal::String(s.clone()))),
39246 to: DataType::Timestamp {
39247 timezone: true,
39248 precision: None,
39249 },
39250 trailing_comments: vec![],
39251 double_colon_syntax: false,
39252 format: None,
39253 default: None,
39254 inferred_type: None,
39255 }))
39256 }
39257 Expression::Literal(ref lit) if matches!(lit.as_ref(), Literal::String(ref _s)) => {
39258 Expression::Cast(Box::new(Cast {
39259 this: expr,
39260 to: DataType::Timestamp {
39261 timezone: true,
39262 precision: None,
39263 },
39264 trailing_comments: vec![],
39265 double_colon_syntax: false,
39266 format: None,
39267 default: None,
39268 inferred_type: None,
39269 }))
39270 }
39271 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Datetime(_)) => {
39272 let Literal::Datetime(s) = lit.as_ref() else {
39273 unreachable!()
39274 };
39275 Expression::Cast(Box::new(Cast {
39276 this: Expression::Literal(Box::new(Literal::String(s.clone()))),
39277 to: DataType::Timestamp {
39278 timezone: true,
39279 precision: None,
39280 },
39281 trailing_comments: vec![],
39282 double_colon_syntax: false,
39283 format: None,
39284 default: None,
39285 inferred_type: None,
39286 }))
39287 }
39288 other => other,
39289 }
39290 }
39291
39292 /// Ensure expression is CAST to DATETIME (for BigQuery)
39293 fn ensure_cast_datetime(expr: Expression) -> Expression {
39294 use crate::expressions::{Cast, DataType, Literal};
39295 match expr {
39296 Expression::Literal(ref lit) if matches!(lit.as_ref(), Literal::String(ref _s)) => {
39297 Expression::Cast(Box::new(Cast {
39298 this: expr,
39299 to: DataType::Custom {
39300 name: "DATETIME".to_string(),
39301 },
39302 trailing_comments: vec![],
39303 double_colon_syntax: false,
39304 format: None,
39305 default: None,
39306 inferred_type: None,
39307 }))
39308 }
39309 other => other,
39310 }
39311 }
39312
39313 /// Force CAST expression to DATETIME (for BigQuery) - always wraps unless already DATETIME
39314 fn force_cast_datetime(expr: Expression) -> Expression {
39315 use crate::expressions::{Cast, DataType};
39316 if let Expression::Cast(ref c) = expr {
39317 if let DataType::Custom { ref name } = c.to {
39318 if name.eq_ignore_ascii_case("DATETIME") {
39319 return expr;
39320 }
39321 }
39322 }
39323 Expression::Cast(Box::new(Cast {
39324 this: expr,
39325 to: DataType::Custom {
39326 name: "DATETIME".to_string(),
39327 },
39328 trailing_comments: vec![],
39329 double_colon_syntax: false,
39330 format: None,
39331 default: None,
39332 inferred_type: None,
39333 }))
39334 }
39335
39336 /// Ensure expression is CAST to DATETIME2 (for TSQL)
39337 fn ensure_cast_datetime2(expr: Expression) -> Expression {
39338 use crate::expressions::{Cast, DataType, Literal};
39339 match expr {
39340 Expression::Literal(ref lit) if matches!(lit.as_ref(), Literal::String(ref _s)) => {
39341 Expression::Cast(Box::new(Cast {
39342 this: expr,
39343 to: DataType::Custom {
39344 name: "DATETIME2".to_string(),
39345 },
39346 trailing_comments: vec![],
39347 double_colon_syntax: false,
39348 format: None,
39349 default: None,
39350 inferred_type: None,
39351 }))
39352 }
39353 other => other,
39354 }
39355 }
39356
39357 /// Convert TIMESTAMP 'x' literal to CAST('x' AS TIMESTAMPTZ) for DuckDB
39358 fn ts_literal_to_cast_tz(expr: Expression) -> Expression {
39359 use crate::expressions::{Cast, DataType, Literal};
39360 match expr {
39361 Expression::Literal(lit) if matches!(lit.as_ref(), Literal::Timestamp(_)) => {
39362 let Literal::Timestamp(s) = lit.as_ref() else {
39363 unreachable!()
39364 };
39365 Expression::Cast(Box::new(Cast {
39366 this: Expression::Literal(Box::new(Literal::String(s.clone()))),
39367 to: DataType::Timestamp {
39368 timezone: true,
39369 precision: None,
39370 },
39371 trailing_comments: vec![],
39372 double_colon_syntax: false,
39373 format: None,
39374 default: None,
39375 inferred_type: None,
39376 }))
39377 }
39378 other => other,
39379 }
39380 }
39381
39382 /// Convert BigQuery format string to Snowflake format string
39383 fn bq_format_to_snowflake(format_expr: &Expression) -> Expression {
39384 use crate::expressions::Literal;
39385 if let Expression::Literal(lit) = format_expr {
39386 if let Literal::String(s) = lit.as_ref() {
39387 let sf = s
39388 .replace("%Y", "yyyy")
39389 .replace("%m", "mm")
39390 .replace("%d", "DD")
39391 .replace("%H", "HH24")
39392 .replace("%M", "MI")
39393 .replace("%S", "SS")
39394 .replace("%b", "mon")
39395 .replace("%B", "Month")
39396 .replace("%e", "FMDD");
39397 Expression::Literal(Box::new(Literal::String(sf)))
39398 } else {
39399 format_expr.clone()
39400 }
39401 } else {
39402 format_expr.clone()
39403 }
39404 }
39405
39406 /// Convert BigQuery format string to DuckDB format string
39407 fn bq_format_to_duckdb(format_expr: &Expression) -> Expression {
39408 use crate::expressions::Literal;
39409 if let Expression::Literal(lit) = format_expr {
39410 if let Literal::String(s) = lit.as_ref() {
39411 let duck = s
39412 .replace("%T", "%H:%M:%S")
39413 .replace("%F", "%Y-%m-%d")
39414 .replace("%D", "%m/%d/%y")
39415 .replace("%x", "%m/%d/%y")
39416 .replace("%c", "%a %b %-d %H:%M:%S %Y")
39417 .replace("%e", "%-d")
39418 .replace("%E6S", "%S.%f");
39419 Expression::Literal(Box::new(Literal::String(duck)))
39420 } else {
39421 format_expr.clone()
39422 }
39423 } else {
39424 format_expr.clone()
39425 }
39426 }
39427
39428 /// Convert BigQuery CAST FORMAT elements (like YYYY, MM, DD) to strftime (like %Y, %m, %d)
39429 fn bq_cast_format_to_strftime(format_expr: &Expression) -> Expression {
39430 use crate::expressions::Literal;
39431 if let Expression::Literal(lit) = format_expr {
39432 if let Literal::String(s) = lit.as_ref() {
39433 // Replace format elements from longest to shortest to avoid partial matches
39434 let result = s
39435 .replace("YYYYMMDD", "%Y%m%d")
39436 .replace("YYYY", "%Y")
39437 .replace("YY", "%y")
39438 .replace("MONTH", "%B")
39439 .replace("MON", "%b")
39440 .replace("MM", "%m")
39441 .replace("DD", "%d")
39442 .replace("HH24", "%H")
39443 .replace("HH12", "%I")
39444 .replace("HH", "%I")
39445 .replace("MI", "%M")
39446 .replace("SSTZH", "%S%z")
39447 .replace("SS", "%S")
39448 .replace("TZH", "%z");
39449 Expression::Literal(Box::new(Literal::String(result)))
39450 } else {
39451 format_expr.clone()
39452 }
39453 } else {
39454 format_expr.clone()
39455 }
39456 }
39457
39458 /// Normalize BigQuery format strings for BQ->BQ output
39459 fn bq_format_normalize_bq(format_expr: &Expression) -> Expression {
39460 use crate::expressions::Literal;
39461 if let Expression::Literal(lit) = format_expr {
39462 if let Literal::String(s) = lit.as_ref() {
39463 let norm = s.replace("%H:%M:%S", "%T").replace("%x", "%D");
39464 Expression::Literal(Box::new(Literal::String(norm)))
39465 } else {
39466 format_expr.clone()
39467 }
39468 } else {
39469 format_expr.clone()
39470 }
39471 }
39472}
39473
39474#[cfg(test)]
39475mod tests {
39476 use super::*;
39477
39478 #[test]
39479 fn test_dialect_type_from_str() {
39480 assert_eq!(
39481 "postgres".parse::<DialectType>().unwrap(),
39482 DialectType::PostgreSQL
39483 );
39484 assert_eq!(
39485 "postgresql".parse::<DialectType>().unwrap(),
39486 DialectType::PostgreSQL
39487 );
39488 assert_eq!("mysql".parse::<DialectType>().unwrap(), DialectType::MySQL);
39489 assert_eq!(
39490 "bigquery".parse::<DialectType>().unwrap(),
39491 DialectType::BigQuery
39492 );
39493 }
39494
39495 #[test]
39496 fn test_basic_transpile() {
39497 let dialect = Dialect::get(DialectType::Generic);
39498 let result = dialect
39499 .transpile("SELECT 1", DialectType::PostgreSQL)
39500 .unwrap();
39501 assert_eq!(result.len(), 1);
39502 assert_eq!(result[0], "SELECT 1");
39503 }
39504
39505 #[test]
39506 fn test_sqlite_double_quoted_column_defaults_to_postgres_strings() {
39507 let sqlite = Dialect::get(DialectType::SQLite);
39508 let result = sqlite
39509 .transpile(
39510 r#"CREATE TABLE "_collections" (
39511 "type" TEXT DEFAULT "base" NOT NULL,
39512 "fields" JSON DEFAULT "[]" NOT NULL,
39513 "options" JSON DEFAULT "{}" NOT NULL
39514 )"#,
39515 DialectType::PostgreSQL,
39516 )
39517 .unwrap();
39518
39519 assert!(result[0].contains(r#""type" TEXT DEFAULT 'base' NOT NULL"#));
39520 assert!(result[0].contains(r#""fields" JSON DEFAULT '[]' NOT NULL"#));
39521 assert!(result[0].contains(r#""options" JSON DEFAULT '{}' NOT NULL"#));
39522 }
39523
39524 #[test]
39525 fn test_sqlite_identity_preserves_double_quoted_column_defaults() {
39526 let sqlite = Dialect::get(DialectType::SQLite);
39527 let result = sqlite
39528 .transpile(
39529 r#"CREATE TABLE "_collections" ("type" TEXT DEFAULT "base" NOT NULL)"#,
39530 DialectType::SQLite,
39531 )
39532 .unwrap();
39533
39534 assert_eq!(
39535 result[0],
39536 r#"CREATE TABLE "_collections" ("type" TEXT DEFAULT "base" NOT NULL)"#
39537 );
39538 }
39539
39540 #[test]
39541 fn test_function_transformation_mysql() {
39542 // NVL should be transformed to IFNULL in MySQL
39543 let dialect = Dialect::get(DialectType::Generic);
39544 let result = dialect
39545 .transpile("SELECT NVL(a, b)", DialectType::MySQL)
39546 .unwrap();
39547 assert_eq!(result[0], "SELECT IFNULL(a, b)");
39548 }
39549
39550 #[test]
39551 fn test_get_path_duckdb() {
39552 // Test: step by step
39553 let snowflake = Dialect::get(DialectType::Snowflake);
39554
39555 // Step 1: Parse and check what Snowflake produces as intermediate
39556 let result_sf_sf = snowflake
39557 .transpile(
39558 "SELECT PARSE_JSON('{\"fruit\":\"banana\"}'):fruit",
39559 DialectType::Snowflake,
39560 )
39561 .unwrap();
39562 eprintln!("Snowflake->Snowflake colon: {}", result_sf_sf[0]);
39563
39564 // Step 2: DuckDB target
39565 let result_sf_dk = snowflake
39566 .transpile(
39567 "SELECT PARSE_JSON('{\"fruit\":\"banana\"}'):fruit",
39568 DialectType::DuckDB,
39569 )
39570 .unwrap();
39571 eprintln!("Snowflake->DuckDB colon: {}", result_sf_dk[0]);
39572
39573 // Step 3: GET_PATH directly
39574 let result_gp = snowflake
39575 .transpile(
39576 "SELECT GET_PATH(PARSE_JSON('{\"fruit\":\"banana\"}'), 'fruit')",
39577 DialectType::DuckDB,
39578 )
39579 .unwrap();
39580 eprintln!("Snowflake->DuckDB explicit GET_PATH: {}", result_gp[0]);
39581 }
39582
39583 #[test]
39584 fn test_function_transformation_postgres() {
39585 // IFNULL should be transformed to COALESCE in PostgreSQL
39586 let dialect = Dialect::get(DialectType::Generic);
39587 let result = dialect
39588 .transpile("SELECT IFNULL(a, b)", DialectType::PostgreSQL)
39589 .unwrap();
39590 assert_eq!(result[0], "SELECT COALESCE(a, b)");
39591
39592 // NVL should also be transformed to COALESCE
39593 let result = dialect
39594 .transpile("SELECT NVL(a, b)", DialectType::PostgreSQL)
39595 .unwrap();
39596 assert_eq!(result[0], "SELECT COALESCE(a, b)");
39597 }
39598
39599 #[test]
39600 fn test_hive_cast_to_trycast() {
39601 // Hive CAST should become TRY_CAST for targets that support it
39602 let hive = Dialect::get(DialectType::Hive);
39603 let result = hive
39604 .transpile("CAST(1 AS INT)", DialectType::DuckDB)
39605 .unwrap();
39606 assert_eq!(result[0], "TRY_CAST(1 AS INT)");
39607
39608 let result = hive
39609 .transpile("CAST(1 AS INT)", DialectType::Presto)
39610 .unwrap();
39611 assert_eq!(result[0], "TRY_CAST(1 AS INTEGER)");
39612 }
39613
39614 #[test]
39615 fn test_hive_array_identity() {
39616 // Hive ARRAY<DATE> should preserve angle bracket syntax
39617 let sql = "CREATE EXTERNAL TABLE `my_table` (`a7` ARRAY<DATE>) ROW FORMAT SERDE 'a' STORED AS INPUTFORMAT 'b' OUTPUTFORMAT 'c' LOCATION 'd' TBLPROPERTIES ('e'='f')";
39618 let hive = Dialect::get(DialectType::Hive);
39619
39620 // Test via transpile (this works)
39621 let result = hive.transpile(sql, DialectType::Hive).unwrap();
39622 eprintln!("Hive ARRAY via transpile: {}", result[0]);
39623 assert!(
39624 result[0].contains("ARRAY<DATE>"),
39625 "transpile: Expected ARRAY<DATE>, got: {}",
39626 result[0]
39627 );
39628
39629 // Test via parse -> transform -> generate (identity test path)
39630 let ast = hive.parse(sql).unwrap();
39631 let transformed = hive.transform(ast[0].clone()).unwrap();
39632 let output = hive.generate(&transformed).unwrap();
39633 eprintln!("Hive ARRAY via identity path: {}", output);
39634 assert!(
39635 output.contains("ARRAY<DATE>"),
39636 "identity path: Expected ARRAY<DATE>, got: {}",
39637 output
39638 );
39639 }
39640
39641 #[test]
39642 fn test_starrocks_delete_between_expansion() {
39643 // StarRocks doesn't support BETWEEN in DELETE statements
39644 let dialect = Dialect::get(DialectType::Generic);
39645
39646 // BETWEEN should be expanded to >= AND <= in DELETE
39647 let result = dialect
39648 .transpile(
39649 "DELETE FROM t WHERE a BETWEEN b AND c",
39650 DialectType::StarRocks,
39651 )
39652 .unwrap();
39653 assert_eq!(result[0], "DELETE FROM t WHERE a >= b AND a <= c");
39654
39655 // NOT BETWEEN should be expanded to < OR > in DELETE
39656 let result = dialect
39657 .transpile(
39658 "DELETE FROM t WHERE a NOT BETWEEN b AND c",
39659 DialectType::StarRocks,
39660 )
39661 .unwrap();
39662 assert_eq!(result[0], "DELETE FROM t WHERE a < b OR a > c");
39663
39664 // BETWEEN in SELECT should NOT be expanded (StarRocks supports it there)
39665 let result = dialect
39666 .transpile(
39667 "SELECT * FROM t WHERE a BETWEEN b AND c",
39668 DialectType::StarRocks,
39669 )
39670 .unwrap();
39671 assert!(
39672 result[0].contains("BETWEEN"),
39673 "BETWEEN should be preserved in SELECT"
39674 );
39675 }
39676
39677 #[test]
39678 fn test_snowflake_ltrim_rtrim_parse() {
39679 let sf = Dialect::get(DialectType::Snowflake);
39680 let sql = "SELECT LTRIM(RTRIM(col)) FROM t1";
39681 let result = sf.transpile(sql, DialectType::DuckDB);
39682 match &result {
39683 Ok(r) => eprintln!("LTRIM/RTRIM result: {}", r[0]),
39684 Err(e) => eprintln!("LTRIM/RTRIM error: {}", e),
39685 }
39686 assert!(
39687 result.is_ok(),
39688 "Expected successful parse of LTRIM(RTRIM(col)), got error: {:?}",
39689 result.err()
39690 );
39691 }
39692
39693 #[test]
39694 fn test_duckdb_count_if_parse() {
39695 let duck = Dialect::get(DialectType::DuckDB);
39696 let sql = "COUNT_IF(x)";
39697 let result = duck.transpile(sql, DialectType::DuckDB);
39698 match &result {
39699 Ok(r) => eprintln!("COUNT_IF result: {}", r[0]),
39700 Err(e) => eprintln!("COUNT_IF error: {}", e),
39701 }
39702 assert!(
39703 result.is_ok(),
39704 "Expected successful parse of COUNT_IF(x), got error: {:?}",
39705 result.err()
39706 );
39707 }
39708
39709 #[test]
39710 fn test_tsql_cast_tinyint_parse() {
39711 let tsql = Dialect::get(DialectType::TSQL);
39712 let sql = "CAST(X AS TINYINT)";
39713 let result = tsql.transpile(sql, DialectType::DuckDB);
39714 match &result {
39715 Ok(r) => eprintln!("TSQL CAST TINYINT result: {}", r[0]),
39716 Err(e) => eprintln!("TSQL CAST TINYINT error: {}", e),
39717 }
39718 assert!(
39719 result.is_ok(),
39720 "Expected successful transpile, got error: {:?}",
39721 result.err()
39722 );
39723 }
39724
39725 #[test]
39726 fn test_pg_hash_bitwise_xor() {
39727 let dialect = Dialect::get(DialectType::PostgreSQL);
39728 let result = dialect.transpile("x # y", DialectType::PostgreSQL).unwrap();
39729 assert_eq!(result[0], "x # y");
39730 }
39731
39732 #[test]
39733 fn test_pg_array_to_duckdb() {
39734 let dialect = Dialect::get(DialectType::PostgreSQL);
39735 let result = dialect
39736 .transpile("SELECT ARRAY[1, 2, 3] @> ARRAY[1, 2]", DialectType::DuckDB)
39737 .unwrap();
39738 assert_eq!(result[0], "SELECT [1, 2, 3] @> [1, 2]");
39739 }
39740
39741 #[test]
39742 fn test_array_remove_bigquery() {
39743 let dialect = Dialect::get(DialectType::Generic);
39744 let result = dialect
39745 .transpile("ARRAY_REMOVE(the_array, target)", DialectType::BigQuery)
39746 .unwrap();
39747 assert_eq!(
39748 result[0],
39749 "ARRAY(SELECT _u FROM UNNEST(the_array) AS _u WHERE _u <> target)"
39750 );
39751 }
39752
39753 #[test]
39754 fn test_map_clickhouse_case() {
39755 let dialect = Dialect::get(DialectType::Generic);
39756 let parsed = dialect
39757 .parse("CAST(MAP('a', '1') AS MAP(TEXT, TEXT))")
39758 .unwrap();
39759 eprintln!("MAP parsed: {:?}", parsed);
39760 let result = dialect
39761 .transpile(
39762 "CAST(MAP('a', '1') AS MAP(TEXT, TEXT))",
39763 DialectType::ClickHouse,
39764 )
39765 .unwrap();
39766 eprintln!("MAP result: {}", result[0]);
39767 }
39768
39769 #[test]
39770 fn test_generate_date_array_presto() {
39771 let dialect = Dialect::get(DialectType::Generic);
39772 let result = dialect.transpile(
39773 "SELECT * FROM UNNEST(GENERATE_DATE_ARRAY(DATE '2020-01-01', DATE '2020-02-01', INTERVAL 1 WEEK))",
39774 DialectType::Presto,
39775 ).unwrap();
39776 eprintln!("GDA -> Presto: {}", result[0]);
39777 assert_eq!(result[0], "SELECT * FROM UNNEST(SEQUENCE(CAST('2020-01-01' AS DATE), CAST('2020-02-01' AS DATE), (1 * INTERVAL '7' DAY)))");
39778 }
39779
39780 #[test]
39781 fn test_generate_date_array_postgres() {
39782 let dialect = Dialect::get(DialectType::Generic);
39783 let result = dialect.transpile(
39784 "SELECT * FROM UNNEST(GENERATE_DATE_ARRAY(DATE '2020-01-01', DATE '2020-02-01', INTERVAL 1 WEEK))",
39785 DialectType::PostgreSQL,
39786 ).unwrap();
39787 eprintln!("GDA -> PostgreSQL: {}", result[0]);
39788 }
39789
39790 #[test]
39791 fn test_generate_date_array_snowflake() {
39792 let dialect = Dialect::get(DialectType::Generic);
39793 let result = dialect
39794 .transpile(
39795 "SELECT * FROM UNNEST(GENERATE_DATE_ARRAY(DATE '2020-01-01', DATE '2020-02-01', INTERVAL 1 WEEK))",
39796 DialectType::Snowflake,
39797 )
39798 .unwrap();
39799 eprintln!("GDA -> Snowflake: {}", result[0]);
39800 }
39801
39802 #[test]
39803 fn test_array_length_generate_date_array_snowflake() {
39804 let dialect = Dialect::get(DialectType::Generic);
39805 let result = dialect.transpile(
39806 "SELECT ARRAY_LENGTH(GENERATE_DATE_ARRAY(DATE '2020-01-01', DATE '2020-02-01', INTERVAL 1 WEEK))",
39807 DialectType::Snowflake,
39808 ).unwrap();
39809 eprintln!("ARRAY_LENGTH(GDA) -> Snowflake: {}", result[0]);
39810 }
39811
39812 #[test]
39813 fn test_generate_date_array_mysql() {
39814 let dialect = Dialect::get(DialectType::Generic);
39815 let result = dialect.transpile(
39816 "SELECT * FROM UNNEST(GENERATE_DATE_ARRAY(DATE '2020-01-01', DATE '2020-02-01', INTERVAL 1 WEEK))",
39817 DialectType::MySQL,
39818 ).unwrap();
39819 eprintln!("GDA -> MySQL: {}", result[0]);
39820 }
39821
39822 #[test]
39823 fn test_generate_date_array_redshift() {
39824 let dialect = Dialect::get(DialectType::Generic);
39825 let result = dialect.transpile(
39826 "SELECT * FROM UNNEST(GENERATE_DATE_ARRAY(DATE '2020-01-01', DATE '2020-02-01', INTERVAL 1 WEEK))",
39827 DialectType::Redshift,
39828 ).unwrap();
39829 eprintln!("GDA -> Redshift: {}", result[0]);
39830 }
39831
39832 #[test]
39833 fn test_generate_date_array_tsql() {
39834 let dialect = Dialect::get(DialectType::Generic);
39835 let result = dialect.transpile(
39836 "SELECT * FROM UNNEST(GENERATE_DATE_ARRAY(DATE '2020-01-01', DATE '2020-02-01', INTERVAL 1 WEEK))",
39837 DialectType::TSQL,
39838 ).unwrap();
39839 eprintln!("GDA -> TSQL: {}", result[0]);
39840 }
39841
39842 #[test]
39843 fn test_struct_colon_syntax() {
39844 let dialect = Dialect::get(DialectType::Generic);
39845 // Test without colon first
39846 let result = dialect.transpile(
39847 "CAST((1, 2, 3, 4) AS STRUCT<a TINYINT, b SMALLINT, c INT, d BIGINT>)",
39848 DialectType::ClickHouse,
39849 );
39850 match result {
39851 Ok(r) => eprintln!("STRUCT no colon -> ClickHouse: {}", r[0]),
39852 Err(e) => eprintln!("STRUCT no colon error: {}", e),
39853 }
39854 // Now test with colon
39855 let result = dialect.transpile(
39856 "CAST((1, 2, 3, 4) AS STRUCT<a: TINYINT, b: SMALLINT, c: INT, d: BIGINT>)",
39857 DialectType::ClickHouse,
39858 );
39859 match result {
39860 Ok(r) => eprintln!("STRUCT colon -> ClickHouse: {}", r[0]),
39861 Err(e) => eprintln!("STRUCT colon error: {}", e),
39862 }
39863 }
39864
39865 #[test]
39866 fn test_generate_date_array_cte_wrapped_mysql() {
39867 let dialect = Dialect::get(DialectType::Generic);
39868 let result = dialect.transpile(
39869 "WITH dates AS (SELECT * FROM UNNEST(GENERATE_DATE_ARRAY(DATE '2020-01-01', DATE '2020-02-01', INTERVAL 1 WEEK))) SELECT * FROM dates",
39870 DialectType::MySQL,
39871 ).unwrap();
39872 eprintln!("GDA CTE -> MySQL: {}", result[0]);
39873 }
39874
39875 #[test]
39876 fn test_generate_date_array_cte_wrapped_tsql() {
39877 let dialect = Dialect::get(DialectType::Generic);
39878 let result = dialect.transpile(
39879 "WITH dates AS (SELECT * FROM UNNEST(GENERATE_DATE_ARRAY(DATE '2020-01-01', DATE '2020-02-01', INTERVAL 1 WEEK))) SELECT * FROM dates",
39880 DialectType::TSQL,
39881 ).unwrap();
39882 eprintln!("GDA CTE -> TSQL: {}", result[0]);
39883 }
39884
39885 #[test]
39886 fn test_decode_literal_no_null_check() {
39887 // Oracle DECODE with all literals should produce simple equality, no IS NULL
39888 let dialect = Dialect::get(DialectType::Oracle);
39889 let result = dialect
39890 .transpile("SELECT decode(1,2,3,4)", DialectType::DuckDB)
39891 .unwrap();
39892 assert_eq!(
39893 result[0], "SELECT CASE WHEN 1 = 2 THEN 3 ELSE 4 END",
39894 "Literal DECODE should not have IS NULL checks"
39895 );
39896 }
39897
39898 #[test]
39899 fn test_decode_column_vs_literal_no_null_check() {
39900 // Oracle DECODE with column vs literal should use simple equality (like sqlglot)
39901 let dialect = Dialect::get(DialectType::Oracle);
39902 let result = dialect
39903 .transpile("SELECT decode(col, 2, 3, 4) FROM t", DialectType::DuckDB)
39904 .unwrap();
39905 assert_eq!(
39906 result[0], "SELECT CASE WHEN col = 2 THEN 3 ELSE 4 END FROM t",
39907 "Column vs literal DECODE should not have IS NULL checks"
39908 );
39909 }
39910
39911 #[test]
39912 fn test_decode_column_vs_column_keeps_null_check() {
39913 // Oracle DECODE with column vs column should keep null-safe comparison
39914 let dialect = Dialect::get(DialectType::Oracle);
39915 let result = dialect
39916 .transpile("SELECT decode(col, col2, 3, 4) FROM t", DialectType::DuckDB)
39917 .unwrap();
39918 assert!(
39919 result[0].contains("IS NULL"),
39920 "Column vs column DECODE should have IS NULL checks, got: {}",
39921 result[0]
39922 );
39923 }
39924
39925 #[test]
39926 fn test_decode_null_search() {
39927 // Oracle DECODE with NULL search should use IS NULL
39928 let dialect = Dialect::get(DialectType::Oracle);
39929 let result = dialect
39930 .transpile("SELECT decode(col, NULL, 3, 4) FROM t", DialectType::DuckDB)
39931 .unwrap();
39932 assert_eq!(
39933 result[0],
39934 "SELECT CASE WHEN col IS NULL THEN 3 ELSE 4 END FROM t",
39935 );
39936 }
39937
39938 // =========================================================================
39939 // REGEXP function transpilation tests
39940 // =========================================================================
39941
39942 #[test]
39943 fn test_regexp_substr_snowflake_to_duckdb_2arg() {
39944 let dialect = Dialect::get(DialectType::Snowflake);
39945 let result = dialect
39946 .transpile("SELECT REGEXP_SUBSTR(s, 'pattern')", DialectType::DuckDB)
39947 .unwrap();
39948 assert_eq!(result[0], "SELECT REGEXP_EXTRACT(s, 'pattern')");
39949 }
39950
39951 #[test]
39952 fn test_regexp_substr_snowflake_to_duckdb_3arg_pos1() {
39953 let dialect = Dialect::get(DialectType::Snowflake);
39954 let result = dialect
39955 .transpile("SELECT REGEXP_SUBSTR(s, 'pattern', 1)", DialectType::DuckDB)
39956 .unwrap();
39957 assert_eq!(result[0], "SELECT REGEXP_EXTRACT(s, 'pattern')");
39958 }
39959
39960 #[test]
39961 fn test_regexp_substr_snowflake_to_duckdb_3arg_pos_gt1() {
39962 let dialect = Dialect::get(DialectType::Snowflake);
39963 let result = dialect
39964 .transpile("SELECT REGEXP_SUBSTR(s, 'pattern', 3)", DialectType::DuckDB)
39965 .unwrap();
39966 assert_eq!(
39967 result[0],
39968 "SELECT REGEXP_EXTRACT(NULLIF(SUBSTRING(s, 3), ''), 'pattern')"
39969 );
39970 }
39971
39972 #[test]
39973 fn test_regexp_substr_snowflake_to_duckdb_4arg_occ_gt1() {
39974 let dialect = Dialect::get(DialectType::Snowflake);
39975 let result = dialect
39976 .transpile(
39977 "SELECT REGEXP_SUBSTR(s, 'pattern', 1, 3)",
39978 DialectType::DuckDB,
39979 )
39980 .unwrap();
39981 assert_eq!(
39982 result[0],
39983 "SELECT ARRAY_EXTRACT(REGEXP_EXTRACT_ALL(s, 'pattern'), 3)"
39984 );
39985 }
39986
39987 #[test]
39988 fn test_regexp_substr_snowflake_to_duckdb_5arg_e_flag() {
39989 let dialect = Dialect::get(DialectType::Snowflake);
39990 let result = dialect
39991 .transpile(
39992 "SELECT REGEXP_SUBSTR(s, 'pattern', 1, 1, 'e')",
39993 DialectType::DuckDB,
39994 )
39995 .unwrap();
39996 assert_eq!(result[0], "SELECT REGEXP_EXTRACT(s, 'pattern')");
39997 }
39998
39999 #[test]
40000 fn test_regexp_substr_snowflake_to_duckdb_6arg_group0() {
40001 let dialect = Dialect::get(DialectType::Snowflake);
40002 let result = dialect
40003 .transpile(
40004 "SELECT REGEXP_SUBSTR(s, 'pattern', 1, 1, 'e', 0)",
40005 DialectType::DuckDB,
40006 )
40007 .unwrap();
40008 assert_eq!(result[0], "SELECT REGEXP_EXTRACT(s, 'pattern')");
40009 }
40010
40011 #[test]
40012 fn test_regexp_substr_snowflake_identity_strip_group0() {
40013 let dialect = Dialect::get(DialectType::Snowflake);
40014 let result = dialect
40015 .transpile(
40016 "SELECT REGEXP_SUBSTR(s, 'pattern', 1, 1, 'e', 0)",
40017 DialectType::Snowflake,
40018 )
40019 .unwrap();
40020 assert_eq!(result[0], "SELECT REGEXP_SUBSTR(s, 'pattern', 1, 1, 'e')");
40021 }
40022
40023 #[test]
40024 fn test_regexp_substr_all_snowflake_to_duckdb_2arg() {
40025 let dialect = Dialect::get(DialectType::Snowflake);
40026 let result = dialect
40027 .transpile(
40028 "SELECT REGEXP_SUBSTR_ALL(s, 'pattern')",
40029 DialectType::DuckDB,
40030 )
40031 .unwrap();
40032 assert_eq!(result[0], "SELECT REGEXP_EXTRACT_ALL(s, 'pattern')");
40033 }
40034
40035 #[test]
40036 fn test_regexp_substr_all_snowflake_to_duckdb_3arg_pos_gt1() {
40037 let dialect = Dialect::get(DialectType::Snowflake);
40038 let result = dialect
40039 .transpile(
40040 "SELECT REGEXP_SUBSTR_ALL(s, 'pattern', 3)",
40041 DialectType::DuckDB,
40042 )
40043 .unwrap();
40044 assert_eq!(
40045 result[0],
40046 "SELECT REGEXP_EXTRACT_ALL(SUBSTRING(s, 3), 'pattern')"
40047 );
40048 }
40049
40050 #[test]
40051 fn test_regexp_substr_all_snowflake_to_duckdb_5arg_e_flag() {
40052 let dialect = Dialect::get(DialectType::Snowflake);
40053 let result = dialect
40054 .transpile(
40055 "SELECT REGEXP_SUBSTR_ALL(s, 'pattern', 1, 1, 'e')",
40056 DialectType::DuckDB,
40057 )
40058 .unwrap();
40059 assert_eq!(result[0], "SELECT REGEXP_EXTRACT_ALL(s, 'pattern')");
40060 }
40061
40062 #[test]
40063 fn test_regexp_substr_all_snowflake_to_duckdb_6arg_group0() {
40064 let dialect = Dialect::get(DialectType::Snowflake);
40065 let result = dialect
40066 .transpile(
40067 "SELECT REGEXP_SUBSTR_ALL(s, 'pattern', 1, 1, 'e', 0)",
40068 DialectType::DuckDB,
40069 )
40070 .unwrap();
40071 assert_eq!(result[0], "SELECT REGEXP_EXTRACT_ALL(s, 'pattern')");
40072 }
40073
40074 #[test]
40075 fn test_regexp_substr_all_snowflake_identity_strip_group0() {
40076 let dialect = Dialect::get(DialectType::Snowflake);
40077 let result = dialect
40078 .transpile(
40079 "SELECT REGEXP_SUBSTR_ALL(s, 'pattern', 1, 1, 'e', 0)",
40080 DialectType::Snowflake,
40081 )
40082 .unwrap();
40083 assert_eq!(
40084 result[0],
40085 "SELECT REGEXP_SUBSTR_ALL(s, 'pattern', 1, 1, 'e')"
40086 );
40087 }
40088
40089 #[test]
40090 fn test_regexp_count_snowflake_to_duckdb_2arg() {
40091 let dialect = Dialect::get(DialectType::Snowflake);
40092 let result = dialect
40093 .transpile("SELECT REGEXP_COUNT(s, 'pattern')", DialectType::DuckDB)
40094 .unwrap();
40095 assert_eq!(
40096 result[0],
40097 "SELECT CASE WHEN 'pattern' = '' THEN 0 ELSE LENGTH(REGEXP_EXTRACT_ALL(s, 'pattern')) END"
40098 );
40099 }
40100
40101 #[test]
40102 fn test_regexp_count_snowflake_to_duckdb_3arg() {
40103 let dialect = Dialect::get(DialectType::Snowflake);
40104 let result = dialect
40105 .transpile("SELECT REGEXP_COUNT(s, 'pattern', 3)", DialectType::DuckDB)
40106 .unwrap();
40107 assert_eq!(
40108 result[0],
40109 "SELECT CASE WHEN 'pattern' = '' THEN 0 ELSE LENGTH(REGEXP_EXTRACT_ALL(SUBSTRING(s, 3), 'pattern')) END"
40110 );
40111 }
40112
40113 #[test]
40114 fn test_regexp_count_snowflake_to_duckdb_4arg_flags() {
40115 let dialect = Dialect::get(DialectType::Snowflake);
40116 let result = dialect
40117 .transpile(
40118 "SELECT REGEXP_COUNT(s, 'pattern', 1, 'i')",
40119 DialectType::DuckDB,
40120 )
40121 .unwrap();
40122 assert_eq!(
40123 result[0],
40124 "SELECT CASE WHEN '(?i)' || 'pattern' = '' THEN 0 ELSE LENGTH(REGEXP_EXTRACT_ALL(SUBSTRING(s, 1), '(?i)' || 'pattern')) END"
40125 );
40126 }
40127
40128 #[test]
40129 fn test_regexp_count_snowflake_to_duckdb_4arg_flags_literal_string() {
40130 let dialect = Dialect::get(DialectType::Snowflake);
40131 let result = dialect
40132 .transpile(
40133 "SELECT REGEXP_COUNT('Hello World', 'L', 1, 'im')",
40134 DialectType::DuckDB,
40135 )
40136 .unwrap();
40137 assert_eq!(
40138 result[0],
40139 "SELECT CASE WHEN '(?im)' || 'L' = '' THEN 0 ELSE LENGTH(REGEXP_EXTRACT_ALL(SUBSTRING('Hello World', 1), '(?im)' || 'L')) END"
40140 );
40141 }
40142
40143 #[test]
40144 fn test_regexp_replace_snowflake_to_duckdb_5arg_pos1_occ1() {
40145 let dialect = Dialect::get(DialectType::Snowflake);
40146 let result = dialect
40147 .transpile(
40148 "SELECT REGEXP_REPLACE(s, 'pattern', 'repl', 1, 1)",
40149 DialectType::DuckDB,
40150 )
40151 .unwrap();
40152 assert_eq!(result[0], "SELECT REGEXP_REPLACE(s, 'pattern', 'repl')");
40153 }
40154
40155 #[test]
40156 fn test_regexp_replace_snowflake_to_duckdb_5arg_pos_gt1_occ0() {
40157 let dialect = Dialect::get(DialectType::Snowflake);
40158 let result = dialect
40159 .transpile(
40160 "SELECT REGEXP_REPLACE(s, 'pattern', 'repl', 3, 0)",
40161 DialectType::DuckDB,
40162 )
40163 .unwrap();
40164 assert_eq!(
40165 result[0],
40166 "SELECT SUBSTRING(s, 1, 2) || REGEXP_REPLACE(SUBSTRING(s, 3), 'pattern', 'repl', 'g')"
40167 );
40168 }
40169
40170 #[test]
40171 fn test_regexp_replace_snowflake_to_duckdb_5arg_pos_gt1_occ1() {
40172 let dialect = Dialect::get(DialectType::Snowflake);
40173 let result = dialect
40174 .transpile(
40175 "SELECT REGEXP_REPLACE(s, 'pattern', 'repl', 3, 1)",
40176 DialectType::DuckDB,
40177 )
40178 .unwrap();
40179 assert_eq!(
40180 result[0],
40181 "SELECT SUBSTRING(s, 1, 2) || REGEXP_REPLACE(SUBSTRING(s, 3), 'pattern', 'repl')"
40182 );
40183 }
40184
40185 #[test]
40186 fn test_rlike_snowflake_to_duckdb_2arg() {
40187 let dialect = Dialect::get(DialectType::Snowflake);
40188 let result = dialect
40189 .transpile("SELECT RLIKE(a, b)", DialectType::DuckDB)
40190 .unwrap();
40191 assert_eq!(result[0], "SELECT REGEXP_FULL_MATCH(a, b)");
40192 }
40193
40194 #[test]
40195 fn test_rlike_snowflake_to_duckdb_3arg_flags() {
40196 let dialect = Dialect::get(DialectType::Snowflake);
40197 let result = dialect
40198 .transpile("SELECT RLIKE(a, b, 'i')", DialectType::DuckDB)
40199 .unwrap();
40200 assert_eq!(result[0], "SELECT REGEXP_FULL_MATCH(a, b, 'i')");
40201 }
40202
40203 #[test]
40204 fn test_regexp_extract_all_bigquery_to_snowflake_no_capture() {
40205 let dialect = Dialect::get(DialectType::BigQuery);
40206 let result = dialect
40207 .transpile(
40208 "SELECT REGEXP_EXTRACT_ALL(s, 'pattern')",
40209 DialectType::Snowflake,
40210 )
40211 .unwrap();
40212 assert_eq!(result[0], "SELECT REGEXP_SUBSTR_ALL(s, 'pattern')");
40213 }
40214
40215 #[test]
40216 fn test_regexp_extract_all_bigquery_to_snowflake_with_capture() {
40217 let dialect = Dialect::get(DialectType::BigQuery);
40218 let result = dialect
40219 .transpile(
40220 "SELECT REGEXP_EXTRACT_ALL(s, '(a)[0-9]')",
40221 DialectType::Snowflake,
40222 )
40223 .unwrap();
40224 assert_eq!(
40225 result[0],
40226 "SELECT REGEXP_SUBSTR_ALL(s, '(a)[0-9]', 1, 1, 'c', 1)"
40227 );
40228 }
40229
40230 #[test]
40231 fn test_regexp_instr_snowflake_to_duckdb_2arg() {
40232 let dialect = Dialect::get(DialectType::Snowflake);
40233 let result = dialect
40234 .transpile("SELECT REGEXP_INSTR(s, 'pattern')", DialectType::DuckDB)
40235 .unwrap();
40236 assert!(
40237 result[0].contains("CASE WHEN"),
40238 "Expected CASE WHEN in result: {}",
40239 result[0]
40240 );
40241 assert!(
40242 result[0].contains("LIST_SUM"),
40243 "Expected LIST_SUM in result: {}",
40244 result[0]
40245 );
40246 }
40247
40248 #[test]
40249 fn test_array_except_generic_to_duckdb() {
40250 let dialect = Dialect::get(DialectType::Generic);
40251 let result = dialect
40252 .transpile(
40253 "SELECT ARRAY_EXCEPT(ARRAY(1, 2, 3), ARRAY(2))",
40254 DialectType::DuckDB,
40255 )
40256 .unwrap();
40257 eprintln!("ARRAY_EXCEPT Generic->DuckDB: {}", result[0]);
40258 assert!(
40259 result[0].contains("CASE WHEN"),
40260 "Expected CASE WHEN: {}",
40261 result[0]
40262 );
40263 assert!(
40264 result[0].contains("LIST_FILTER"),
40265 "Expected LIST_FILTER: {}",
40266 result[0]
40267 );
40268 assert!(
40269 result[0].contains("LIST_DISTINCT"),
40270 "Expected LIST_DISTINCT: {}",
40271 result[0]
40272 );
40273 assert!(
40274 result[0].contains("IS NOT DISTINCT FROM"),
40275 "Expected IS NOT DISTINCT FROM: {}",
40276 result[0]
40277 );
40278 assert!(
40279 result[0].contains("= 0"),
40280 "Expected = 0 filter: {}",
40281 result[0]
40282 );
40283 }
40284
40285 #[test]
40286 fn test_array_except_generic_to_snowflake() {
40287 let dialect = Dialect::get(DialectType::Generic);
40288 let result = dialect
40289 .transpile(
40290 "SELECT ARRAY_EXCEPT(ARRAY(1, 2, 3), ARRAY(2))",
40291 DialectType::Snowflake,
40292 )
40293 .unwrap();
40294 eprintln!("ARRAY_EXCEPT Generic->Snowflake: {}", result[0]);
40295 assert_eq!(result[0], "SELECT ARRAY_EXCEPT([1, 2, 3], [2])");
40296 }
40297
40298 #[test]
40299 fn test_array_except_generic_to_presto() {
40300 let dialect = Dialect::get(DialectType::Generic);
40301 let result = dialect
40302 .transpile(
40303 "SELECT ARRAY_EXCEPT(ARRAY(1, 2, 3), ARRAY(2))",
40304 DialectType::Presto,
40305 )
40306 .unwrap();
40307 eprintln!("ARRAY_EXCEPT Generic->Presto: {}", result[0]);
40308 assert_eq!(result[0], "SELECT ARRAY_EXCEPT(ARRAY[1, 2, 3], ARRAY[2])");
40309 }
40310
40311 #[test]
40312 fn test_array_except_snowflake_to_duckdb() {
40313 let dialect = Dialect::get(DialectType::Snowflake);
40314 let result = dialect
40315 .transpile("SELECT ARRAY_EXCEPT([1, 2, 3], [2])", DialectType::DuckDB)
40316 .unwrap();
40317 eprintln!("ARRAY_EXCEPT Snowflake->DuckDB: {}", result[0]);
40318 assert!(
40319 result[0].contains("CASE WHEN"),
40320 "Expected CASE WHEN: {}",
40321 result[0]
40322 );
40323 assert!(
40324 result[0].contains("LIST_TRANSFORM"),
40325 "Expected LIST_TRANSFORM: {}",
40326 result[0]
40327 );
40328 }
40329
40330 #[test]
40331 fn test_array_contains_snowflake_to_snowflake() {
40332 let dialect = Dialect::get(DialectType::Snowflake);
40333 let result = dialect
40334 .transpile(
40335 "SELECT ARRAY_CONTAINS(x, [1, NULL, 3])",
40336 DialectType::Snowflake,
40337 )
40338 .unwrap();
40339 eprintln!("ARRAY_CONTAINS Snowflake->Snowflake: {}", result[0]);
40340 assert_eq!(result[0], "SELECT ARRAY_CONTAINS(x, [1, NULL, 3])");
40341 }
40342
40343 #[test]
40344 fn test_array_contains_snowflake_to_duckdb() {
40345 let dialect = Dialect::get(DialectType::Snowflake);
40346 let result = dialect
40347 .transpile(
40348 "SELECT ARRAY_CONTAINS(x, [1, NULL, 3])",
40349 DialectType::DuckDB,
40350 )
40351 .unwrap();
40352 eprintln!("ARRAY_CONTAINS Snowflake->DuckDB: {}", result[0]);
40353 assert!(
40354 result[0].contains("CASE WHEN"),
40355 "Expected CASE WHEN: {}",
40356 result[0]
40357 );
40358 assert!(
40359 result[0].contains("NULLIF"),
40360 "Expected NULLIF: {}",
40361 result[0]
40362 );
40363 assert!(
40364 result[0].contains("ARRAY_CONTAINS"),
40365 "Expected ARRAY_CONTAINS: {}",
40366 result[0]
40367 );
40368 }
40369
40370 #[test]
40371 fn test_array_distinct_snowflake_to_duckdb() {
40372 let dialect = Dialect::get(DialectType::Snowflake);
40373 let result = dialect
40374 .transpile(
40375 "SELECT ARRAY_DISTINCT([1, 2, 2, 3, 1])",
40376 DialectType::DuckDB,
40377 )
40378 .unwrap();
40379 eprintln!("ARRAY_DISTINCT Snowflake->DuckDB: {}", result[0]);
40380 assert!(
40381 result[0].contains("CASE WHEN"),
40382 "Expected CASE WHEN: {}",
40383 result[0]
40384 );
40385 assert!(
40386 result[0].contains("LIST_DISTINCT"),
40387 "Expected LIST_DISTINCT: {}",
40388 result[0]
40389 );
40390 assert!(
40391 result[0].contains("LIST_APPEND"),
40392 "Expected LIST_APPEND: {}",
40393 result[0]
40394 );
40395 assert!(
40396 result[0].contains("LIST_FILTER"),
40397 "Expected LIST_FILTER: {}",
40398 result[0]
40399 );
40400 }
40401}