use serde::{Deserialize, Serialize};
use crate::ast::*;
pub mod plugin;
pub mod time;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Dialect {
Ansi,
Athena,
BigQuery,
ClickHouse,
Databricks,
DuckDb,
Hive,
Mysql,
Oracle,
Postgres,
Presto,
Redshift,
Snowflake,
Spark,
Sqlite,
StarRocks,
Trino,
Tsql,
Doris,
Dremio,
Drill,
Druid,
Exasol,
Fabric,
Materialize,
Prql,
RisingWave,
SingleStore,
Tableau,
Teradata,
}
impl std::fmt::Display for Dialect {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Dialect::Ansi => write!(f, "ANSI SQL"),
Dialect::Athena => write!(f, "Athena"),
Dialect::BigQuery => write!(f, "BigQuery"),
Dialect::ClickHouse => write!(f, "ClickHouse"),
Dialect::Databricks => write!(f, "Databricks"),
Dialect::DuckDb => write!(f, "DuckDB"),
Dialect::Hive => write!(f, "Hive"),
Dialect::Mysql => write!(f, "MySQL"),
Dialect::Oracle => write!(f, "Oracle"),
Dialect::Postgres => write!(f, "PostgreSQL"),
Dialect::Presto => write!(f, "Presto"),
Dialect::Redshift => write!(f, "Redshift"),
Dialect::Snowflake => write!(f, "Snowflake"),
Dialect::Spark => write!(f, "Spark"),
Dialect::Sqlite => write!(f, "SQLite"),
Dialect::StarRocks => write!(f, "StarRocks"),
Dialect::Trino => write!(f, "Trino"),
Dialect::Tsql => write!(f, "T-SQL"),
Dialect::Doris => write!(f, "Doris"),
Dialect::Dremio => write!(f, "Dremio"),
Dialect::Drill => write!(f, "Drill"),
Dialect::Druid => write!(f, "Druid"),
Dialect::Exasol => write!(f, "Exasol"),
Dialect::Fabric => write!(f, "Fabric"),
Dialect::Materialize => write!(f, "Materialize"),
Dialect::Prql => write!(f, "PRQL"),
Dialect::RisingWave => write!(f, "RisingWave"),
Dialect::SingleStore => write!(f, "SingleStore"),
Dialect::Tableau => write!(f, "Tableau"),
Dialect::Teradata => write!(f, "Teradata"),
}
}
}
impl Dialect {
#[must_use]
pub fn support_level(&self) -> &'static str {
match self {
Dialect::Ansi
| Dialect::Athena
| Dialect::BigQuery
| Dialect::ClickHouse
| Dialect::Databricks
| Dialect::DuckDb
| Dialect::Hive
| Dialect::Mysql
| Dialect::Oracle
| Dialect::Postgres
| Dialect::Presto
| Dialect::Redshift
| Dialect::Snowflake
| Dialect::Spark
| Dialect::Sqlite
| Dialect::StarRocks
| Dialect::Trino
| Dialect::Tsql => "Official",
Dialect::Doris
| Dialect::Dremio
| Dialect::Drill
| Dialect::Druid
| Dialect::Exasol
| Dialect::Fabric
| Dialect::Materialize
| Dialect::Prql
| Dialect::RisingWave
| Dialect::SingleStore
| Dialect::Tableau
| Dialect::Teradata => "Community",
}
}
#[must_use]
pub fn all() -> &'static [Dialect] {
&[
Dialect::Ansi,
Dialect::Athena,
Dialect::BigQuery,
Dialect::ClickHouse,
Dialect::Databricks,
Dialect::Doris,
Dialect::Dremio,
Dialect::Drill,
Dialect::Druid,
Dialect::DuckDb,
Dialect::Exasol,
Dialect::Fabric,
Dialect::Hive,
Dialect::Materialize,
Dialect::Mysql,
Dialect::Oracle,
Dialect::Postgres,
Dialect::Presto,
Dialect::Prql,
Dialect::Redshift,
Dialect::RisingWave,
Dialect::SingleStore,
Dialect::Snowflake,
Dialect::Spark,
Dialect::Sqlite,
Dialect::StarRocks,
Dialect::Tableau,
Dialect::Teradata,
Dialect::Trino,
Dialect::Tsql,
]
}
pub fn from_str(s: &str) -> Option<Dialect> {
match s.to_lowercase().as_str() {
"" | "ansi" => Some(Dialect::Ansi),
"athena" => Some(Dialect::Athena),
"bigquery" => Some(Dialect::BigQuery),
"clickhouse" => Some(Dialect::ClickHouse),
"databricks" => Some(Dialect::Databricks),
"doris" => Some(Dialect::Doris),
"dremio" => Some(Dialect::Dremio),
"drill" => Some(Dialect::Drill),
"druid" => Some(Dialect::Druid),
"duckdb" => Some(Dialect::DuckDb),
"exasol" => Some(Dialect::Exasol),
"fabric" => Some(Dialect::Fabric),
"hive" => Some(Dialect::Hive),
"materialize" => Some(Dialect::Materialize),
"mysql" => Some(Dialect::Mysql),
"oracle" => Some(Dialect::Oracle),
"postgres" | "postgresql" => Some(Dialect::Postgres),
"presto" => Some(Dialect::Presto),
"prql" => Some(Dialect::Prql),
"redshift" => Some(Dialect::Redshift),
"risingwave" => Some(Dialect::RisingWave),
"singlestore" => Some(Dialect::SingleStore),
"snowflake" => Some(Dialect::Snowflake),
"spark" => Some(Dialect::Spark),
"sqlite" => Some(Dialect::Sqlite),
"starrocks" => Some(Dialect::StarRocks),
"tableau" => Some(Dialect::Tableau),
"teradata" => Some(Dialect::Teradata),
"trino" => Some(Dialect::Trino),
"tsql" | "mssql" | "sqlserver" => Some(Dialect::Tsql),
_ => None,
}
}
}
fn is_mysql_family(d: Dialect) -> bool {
matches!(
d,
Dialect::Mysql | Dialect::Doris | Dialect::SingleStore | Dialect::StarRocks
)
}
fn is_postgres_family(d: Dialect) -> bool {
matches!(
d,
Dialect::Postgres | Dialect::Redshift | Dialect::Materialize | Dialect::RisingWave
)
}
fn is_presto_family(d: Dialect) -> bool {
matches!(d, Dialect::Presto | Dialect::Trino | Dialect::Athena)
}
fn is_hive_family(d: Dialect) -> bool {
matches!(d, Dialect::Hive | Dialect::Spark | Dialect::Databricks)
}
pub(crate) fn is_tsql_family(d: Dialect) -> bool {
matches!(d, Dialect::Tsql | Dialect::Fabric)
}
#[must_use]
pub(crate) fn is_tsql_reserved(name: &str) -> bool {
const RESERVED: &[&str] = &[
"ABSOLUTE",
"ACTION",
"ADA",
"ADD",
"ALL",
"ALLOCATE",
"ALTER",
"AND",
"ANY",
"ARE",
"AS",
"ASC",
"ASSERTION",
"AT",
"AUTHORIZATION",
"AVG",
"BACKUP",
"BEGIN",
"BETWEEN",
"BIT",
"BIT_LENGTH",
"BOTH",
"BREAK",
"BROWSE",
"BULK",
"BY",
"CASCADE",
"CASCADED",
"CASE",
"CAST",
"CATALOG",
"CHAR",
"CHARACTER",
"CHARACTER_LENGTH",
"CHAR_LENGTH",
"CHECK",
"CHECKPOINT",
"CLOSE",
"CLUSTERED",
"COALESCE",
"COLLATE",
"COLLATION",
"COLUMN",
"COMMIT",
"COMPUTE",
"CONNECT",
"CONNECTION",
"CONSTRAINT",
"CONSTRAINTS",
"CONTAINS",
"CONTAINSTABLE",
"CONTINUE",
"CONVERT",
"CORRESPONDING",
"COUNT",
"CREATE",
"CROSS",
"CURRENT",
"CURRENT_DATE",
"CURRENT_TIME",
"CURRENT_TIMESTAMP",
"CURRENT_USER",
"CURSOR",
"DATABASE",
"DATE",
"DBCC",
"DEALLOCATE",
"DEC",
"DECIMAL",
"DECLARE",
"DEFAULT",
"DEFERRABLE",
"DEFERRED",
"DELETE",
"DENY",
"DESC",
"DESCRIBE",
"DESCRIPTOR",
"DIAGNOSTICS",
"DISCONNECT",
"DISK",
"DISTINCT",
"DISTRIBUTED",
"DOMAIN",
"DOUBLE",
"DROP",
"DUMP",
"ELSE",
"END",
"ERRLVL",
"ESCAPE",
"EXCEPT",
"EXCEPTION",
"EXEC",
"EXECUTE",
"EXISTS",
"EXIT",
"EXTERNAL",
"EXTRACT",
"FETCH",
"FILE",
"FILLFACTOR",
"FLOAT",
"FOR",
"FOREIGN",
"FORTRAN",
"FOUND",
"FREETEXT",
"FREETEXTTABLE",
"FROM",
"FULL",
"FUNCTION",
"GET",
"GLOBAL",
"GO",
"GOTO",
"GRANT",
"GROUP",
"HAVING",
"HOLDLOCK",
"HOUR",
"IDENTITY",
"IDENTITYCOL",
"IDENTITY_INSERT",
"IF",
"IMMEDIATE",
"IN",
"INCLUDE",
"INDEX",
"INDICATOR",
"INITIALLY",
"INNER",
"INPUT",
"INSENSITIVE",
"INSERT",
"INT",
"INTEGER",
"INTERSECT",
"INTERVAL",
"INTO",
"IS",
"ISOLATION",
"JOIN",
"KEY",
"KILL",
"LANGUAGE",
"LAST",
"LEADING",
"LEFT",
"LEVEL",
"LIKE",
"LINENO",
"LOAD",
"LOCAL",
"LOWER",
"MATCH",
"MAX",
"MERGE",
"MIN",
"MINUTE",
"MODULE",
"MONTH",
"NAMES",
"NATIONAL",
"NATURAL",
"NCHAR",
"NEXT",
"NO",
"NOCHECK",
"NONCLUSTERED",
"NONE",
"NOT",
"NULL",
"NULLIF",
"NUMERIC",
"OCTET_LENGTH",
"OF",
"OFF",
"OFFSETS",
"ON",
"ONLY",
"OPEN",
"OPENDATASOURCE",
"OPENQUERY",
"OPENROWSET",
"OPENXML",
"OPTION",
"OR",
"ORDER",
"OUTER",
"OUTPUT",
"OVER",
"OVERLAPS",
"PAD",
"PARTIAL",
"PASCAL",
"PERCENT",
"PIVOT",
"PLAN",
"POSITION",
"PRECISION",
"PREPARE",
"PRESERVE",
"PRIMARY",
"PRINT",
"PRIOR",
"PRIVILEGES",
"PROC",
"PROCEDURE",
"PUBLIC",
"RAISERROR",
"READ",
"READTEXT",
"REAL",
"RECONFIGURE",
"REFERENCES",
"RELATIVE",
"REPLICATION",
"RESTORE",
"RESTRICT",
"RETURN",
"REVERT",
"REVOKE",
"RIGHT",
"ROLLBACK",
"ROWCOUNT",
"ROWGUIDCOL",
"ROWS",
"RULE",
"SAVE",
"SCHEMA",
"SCROLL",
"SECOND",
"SECTION",
"SECURITYAUDIT",
"SELECT",
"SEMANTICKEYPHRASETABLE",
"SEMANTICSIMILARITYDETAILSTABLE",
"SEMANTICSIMILARITYTABLE",
"SESSION",
"SESSION_USER",
"SET",
"SETUSER",
"SHUTDOWN",
"SIZE",
"SMALLINT",
"SOME",
"SPACE",
"SQL",
"SQLCA",
"SQLCODE",
"SQLERROR",
"SQLSTATE",
"SQLWARNING",
"STATISTICS",
"SUBSTRING",
"SUM",
"SYSTEM_USER",
"TABLE",
"TABLESAMPLE",
"TEMPORARY",
"TEXTSIZE",
"THEN",
"TIME",
"TIMESTAMP",
"TIMEZONE_HOUR",
"TIMEZONE_MINUTE",
"TO",
"TOP",
"TRAILING",
"TRAN",
"TRANSACTION",
"TRANSLATE",
"TRANSLATION",
"TRIGGER",
"TRIM",
"TRUE",
"TRUNCATE",
"TRY_CONVERT",
"TSEQUAL",
"UNION",
"UNIQUE",
"UNKNOWN",
"UNPIVOT",
"UPDATE",
"UPDATETEXT",
"UPPER",
"USAGE",
"USE",
"USER",
"USING",
"VALUE",
"VALUES",
"VARCHAR",
"VARYING",
"VIEW",
"WAITFOR",
"WHEN",
"WHENEVER",
"WHERE",
"WHILE",
"WITH",
"WITHIN GROUP",
"WORK",
"WRITE",
"WRITETEXT",
"YEAR",
"ZONE",
];
if name.is_empty() || name.len() > 32 {
return false;
}
let mut buf = [0u8; 32];
for (i, b) in name.as_bytes().iter().enumerate() {
buf[i] = b.to_ascii_uppercase();
}
let upper = match std::str::from_utf8(&buf[..name.len()]) {
Ok(s) => s,
Err(_) => return false,
};
RESERVED.binary_search(&upper).is_ok()
}
pub(crate) fn supports_ilike_builtin(d: Dialect) -> bool {
matches!(
d,
Dialect::Postgres
| Dialect::Redshift
| Dialect::Materialize
| Dialect::RisingWave
| Dialect::DuckDb
| Dialect::Snowflake
| Dialect::ClickHouse
| Dialect::Trino
| Dialect::Presto
| Dialect::Athena
| Dialect::Databricks
| Dialect::Spark
| Dialect::Hive
| Dialect::StarRocks
| Dialect::Exasol
| Dialect::Druid
| Dialect::Dremio
)
}
#[must_use]
pub fn transform(statement: &Statement, from: Dialect, to: Dialect) -> Statement {
if from == to {
return statement.clone();
}
let mut stmt = statement.clone();
transform_statement(&mut stmt, to);
stmt
}
fn transform_statement(statement: &mut Statement, target: Dialect) {
match statement {
Statement::Select(sel) => {
transform_limit(sel, target);
transform_quotes_in_select(sel, target);
for item in &mut sel.columns {
if let SelectItem::Expr { expr, .. } = item {
*expr = transform_expr(expr.clone(), target);
}
}
if let Some(wh) = &mut sel.where_clause {
*wh = transform_expr(wh.clone(), target);
}
for gb in &mut sel.group_by {
*gb = transform_expr(gb.clone(), target);
}
if let Some(having) = &mut sel.having {
*having = transform_expr(having.clone(), target);
}
}
Statement::Insert(ins) => {
if let InsertSource::Values(rows) = &mut ins.source {
for row in rows {
for val in row {
*val = transform_expr(val.clone(), target);
}
}
}
for item in &mut ins.returning {
if let SelectItem::Expr { expr, .. } = item {
*expr = transform_expr(expr.clone(), target);
}
}
}
Statement::Update(upd) => {
for (_, val) in &mut upd.assignments {
*val = transform_expr(val.clone(), target);
}
if let Some(wh) = &mut upd.where_clause {
*wh = transform_expr(wh.clone(), target);
}
for item in &mut upd.returning {
if let SelectItem::Expr { expr, .. } = item {
*expr = transform_expr(expr.clone(), target);
}
}
}
Statement::Delete(del) => {
if let Some(wh) = &mut del.where_clause {
*wh = transform_expr(wh.clone(), target);
}
for item in &mut del.returning {
if let SelectItem::Expr { expr, .. } = item {
*expr = transform_expr(expr.clone(), target);
}
}
}
Statement::CreateTable(ct) => {
for col in &mut ct.columns {
col.data_type = map_data_type(col.data_type.clone(), target);
if let Some(default) = &mut col.default {
*default = transform_expr(default.clone(), target);
}
}
for constraint in &mut ct.constraints {
if let TableConstraint::Check { expr, .. } = constraint {
*expr = transform_expr(expr.clone(), target);
}
}
if let Some(as_select) = &mut ct.as_select {
transform_statement(as_select, target);
}
}
Statement::AlterTable(alt) => {
for action in &mut alt.actions {
match action {
AlterTableAction::AddColumn(col) => {
col.data_type = map_data_type(col.data_type.clone(), target);
if let Some(default) = &mut col.default {
*default = transform_expr(default.clone(), target);
}
}
AlterTableAction::AlterColumnType { data_type, .. } => {
*data_type = map_data_type(data_type.clone(), target);
}
_ => {}
}
}
}
_ => {}
}
}
fn transform_expr(expr: Expr, target: Dialect) -> Expr {
match expr {
Expr::Function {
name,
args,
distinct,
filter,
over,
} => {
let new_name = map_function_name(&name, target);
let new_args: Vec<Expr> = args
.into_iter()
.map(|a| transform_expr(a, target))
.collect();
Expr::Function {
name: new_name,
args: new_args,
distinct,
filter: filter.map(|f| Box::new(transform_expr(*f, target))),
over,
}
}
Expr::TypedFunction { func, filter, over } => {
let transformed_func = transform_typed_function(func, target);
Expr::TypedFunction {
func: transformed_func,
filter: filter.map(|f| Box::new(transform_expr(*f, target))),
over,
}
}
Expr::ILike {
expr,
pattern,
negated,
escape,
} if !supports_ilike_builtin(target) => Expr::Like {
expr: Box::new(Expr::TypedFunction {
func: TypedFunction::Lower {
expr: Box::new(transform_expr(*expr, target)),
},
filter: None,
over: None,
}),
pattern: Box::new(Expr::TypedFunction {
func: TypedFunction::Lower {
expr: Box::new(transform_expr(*pattern, target)),
},
filter: None,
over: None,
}),
negated,
escape,
},
Expr::SimilarTo {
expr,
pattern,
negated,
escape,
} if is_tsql_family(target) => {
let transformed_pattern = transform_expr(*pattern, target);
let simplified = simplify_similar_to_pattern(&transformed_pattern);
Expr::Like {
expr: Box::new(transform_expr(*expr, target)),
pattern: Box::new(simplified),
negated,
escape,
}
}
Expr::Cast { expr, data_type } => Expr::Cast {
expr: Box::new(transform_expr(*expr, target)),
data_type: map_data_type(data_type, target),
},
Expr::BinaryOp { left, op, right } => {
if op == BinaryOperator::Concat && is_tsql_family(target) {
let mut args = Vec::new();
collect_concat_args(
&Expr::BinaryOp {
left,
op: BinaryOperator::Concat,
right,
},
&mut args,
);
let args = args
.into_iter()
.map(|a| transform_expr(a, target))
.collect();
return Expr::Function {
name: "CONCAT".to_string(),
args,
distinct: false,
filter: None,
over: None,
};
}
let left_transformed = transform_expr(*left, target);
let right_transformed = transform_expr(*right, target);
if is_tsql_family(target) && matches!(op, BinaryOperator::Plus | BinaryOperator::Minus)
{
if let Some(dateadd) =
try_transform_interval_arithmetic(&left_transformed, &op, &right_transformed)
{
return dateadd;
}
}
Expr::BinaryOp {
left: Box::new(left_transformed),
op,
right: Box::new(right_transformed),
}
}
Expr::UnaryOp { op, expr } => Expr::UnaryOp {
op,
expr: Box::new(transform_expr(*expr, target)),
},
Expr::Nested(inner) => Expr::Nested(Box::new(transform_expr(*inner, target))),
Expr::Column {
table,
name,
quote_style,
table_quote_style,
} => {
let new_qs = if quote_style.is_quoted() {
QuoteStyle::for_dialect(target)
} else {
QuoteStyle::None
};
let new_tqs = if table_quote_style.is_quoted() {
QuoteStyle::for_dialect(target)
} else {
QuoteStyle::None
};
Expr::Column {
table,
name,
quote_style: new_qs,
table_quote_style: new_tqs,
}
}
other => other,
}
}
fn transform_typed_function(func: TypedFunction, target: Dialect) -> TypedFunction {
match func {
TypedFunction::TimeToStr { expr, format } => {
let transformed_expr = Box::new(transform_expr(*expr, target));
let transformed_format = transform_format_expr(*format, target);
TypedFunction::TimeToStr {
expr: transformed_expr,
format: Box::new(transformed_format),
}
}
TypedFunction::StrToTime { expr, format } => {
let transformed_expr = Box::new(transform_expr(*expr, target));
let transformed_format = transform_format_expr(*format, target);
TypedFunction::StrToTime {
expr: transformed_expr,
format: Box::new(transformed_format),
}
}
other => other.transform_children(&|e| transform_expr(e, target)),
}
}
fn transform_format_expr(expr: Expr, target: Dialect) -> Expr {
match &expr {
Expr::StringLiteral(s) | Expr::NationalStringLiteral(s) => {
let detected_source = detect_format_style(s);
let target_style = time::TimeFormatStyle::for_dialect(target);
if detected_source != target_style {
let converted = time::format_time(s, detected_source, target_style);
match expr {
Expr::NationalStringLiteral(_) => Expr::NationalStringLiteral(converted),
_ => Expr::StringLiteral(converted),
}
} else {
expr
}
}
_ => transform_expr(expr, target),
}
}
fn detect_format_style(format_str: &str) -> time::TimeFormatStyle {
if format_str.contains('%') {
if format_str.contains("%i") {
time::TimeFormatStyle::Mysql
} else {
time::TimeFormatStyle::Strftime
}
} else if format_str.contains("YYYY") || format_str.contains("yyyy") {
if format_str.contains("HH24") || format_str.contains("MI") || format_str.contains("SS") {
time::TimeFormatStyle::Postgres
} else if format_str.contains("mm") && format_str.contains("ss") {
time::TimeFormatStyle::Java
} else if format_str.contains("FF") {
time::TimeFormatStyle::Snowflake
} else if format_str.contains("MM") && format_str.contains("DD") {
time::TimeFormatStyle::Postgres
} else {
time::TimeFormatStyle::Java
}
} else {
time::TimeFormatStyle::Strftime
}
}
pub(crate) fn map_function_name(name: &str, target: Dialect) -> String {
let upper = name.to_uppercase();
match upper.as_str() {
"NOW" => {
if is_tsql_family(target) {
"GETDATE".to_string()
} else if matches!(
target,
Dialect::Ansi
| Dialect::BigQuery
| Dialect::Snowflake
| Dialect::Oracle
| Dialect::ClickHouse
| Dialect::Exasol
| Dialect::Teradata
| Dialect::Druid
| Dialect::Dremio
| Dialect::Tableau
) || is_presto_family(target)
|| is_hive_family(target)
{
"CURRENT_TIMESTAMP".to_string()
} else {
name.to_string()
}
}
"GETDATE" => {
if is_tsql_family(target) {
name.to_string()
} else if is_postgres_family(target)
|| matches!(target, Dialect::Mysql | Dialect::DuckDb | Dialect::Sqlite)
{
"NOW".to_string()
} else {
"CURRENT_TIMESTAMP".to_string()
}
}
"LEN" => {
if is_tsql_family(target) || matches!(target, Dialect::BigQuery | Dialect::Snowflake) {
name.to_string()
} else {
"LENGTH".to_string()
}
}
"LENGTH" if is_tsql_family(target) => "LEN".to_string(),
"SUBSTR" => {
if is_mysql_family(target)
|| matches!(target, Dialect::Sqlite | Dialect::Oracle)
|| is_hive_family(target)
{
"SUBSTR".to_string()
} else {
"SUBSTRING".to_string()
}
}
"SUBSTRING" => {
if is_mysql_family(target)
|| matches!(target, Dialect::Sqlite | Dialect::Oracle)
|| is_hive_family(target)
{
"SUBSTR".to_string()
} else {
name.to_string()
}
}
"IFNULL" => {
if is_tsql_family(target) {
"ISNULL".to_string()
} else if is_mysql_family(target) || matches!(target, Dialect::Sqlite) {
name.to_string()
} else {
"COALESCE".to_string()
}
}
"ISNULL" => {
if is_tsql_family(target) {
name.to_string()
} else if is_mysql_family(target) || matches!(target, Dialect::Sqlite) {
"IFNULL".to_string()
} else {
"COALESCE".to_string()
}
}
"NVL" => {
if matches!(target, Dialect::Oracle | Dialect::Snowflake) {
name.to_string()
} else if is_mysql_family(target) || matches!(target, Dialect::Sqlite) {
"IFNULL".to_string()
} else if is_tsql_family(target) {
"ISNULL".to_string()
} else {
"COALESCE".to_string()
}
}
"RANDOM" => {
if matches!(
target,
Dialect::Postgres | Dialect::Sqlite | Dialect::DuckDb
) {
name.to_string()
} else {
"RAND".to_string()
}
}
"RAND" => {
if matches!(
target,
Dialect::Postgres | Dialect::Sqlite | Dialect::DuckDb
) {
"RANDOM".to_string()
} else {
name.to_string()
}
}
"POSITION" if is_tsql_family(target) => "CHARINDEX".to_string(),
"CHARINDEX" if is_postgres_family(target) => "POSITION".to_string(),
_ => name.to_string(),
}
}
pub(crate) fn map_data_type(dt: DataType, target: Dialect) -> DataType {
match (dt, target) {
(DataType::Text, t) if is_tsql_family(t) => {
DataType::Varchar(None) }
(DataType::Boolean, t) if is_tsql_family(t) => DataType::Bit(None),
(DataType::Bytea, t) if is_tsql_family(t) => DataType::Varbinary(None),
(DataType::Json, t) if is_tsql_family(t) => DataType::Varchar(None),
(DataType::Jsonb, t) if is_tsql_family(t) => DataType::Varchar(None),
(DataType::Uuid, t) if is_tsql_family(t) => {
DataType::Unknown("UNIQUEIDENTIFIER".to_string())
}
(DataType::Serial, t) if is_tsql_family(t) => DataType::Int,
(DataType::BigSerial, t) if is_tsql_family(t) => DataType::BigInt,
(DataType::SmallSerial, t) if is_tsql_family(t) => DataType::SmallInt,
(DataType::Timestamp { .. }, t) if is_tsql_family(t) => {
DataType::Unknown("DATETIME2".to_string())
}
(DataType::Real, t) if is_tsql_family(t) => DataType::Real,
(DataType::Text, t) if matches!(t, Dialect::BigQuery) || is_hive_family(t) => {
DataType::String
}
(DataType::String, t)
if is_postgres_family(t) || is_mysql_family(t) || matches!(t, Dialect::Sqlite) =>
{
DataType::Text
}
(DataType::Int, Dialect::BigQuery) => DataType::BigInt,
(DataType::Float, Dialect::BigQuery) => DataType::Double,
(DataType::Bytea, t)
if is_mysql_family(t)
|| matches!(t, Dialect::Sqlite | Dialect::Oracle)
|| is_hive_family(t) =>
{
DataType::Blob
}
(DataType::Blob, t) if is_postgres_family(t) => DataType::Bytea,
(DataType::Varbinary(_), t) if is_postgres_family(t) => DataType::Bytea,
(DataType::Boolean, Dialect::Mysql) => DataType::Boolean,
(dt, _) => dt,
}
}
fn transform_limit(sel: &mut SelectStatement, target: Dialect) {
if is_tsql_family(target) {
if let Some(limit) = sel.limit.take() {
if sel.offset.is_none() {
sel.top = Some(Box::new(limit));
} else {
sel.fetch_first = Some(limit);
if sel.order_by.is_empty() {
sel.order_by = vec![OrderByItem {
expr: Expr::Subquery(Box::new(Statement::Select(SelectStatement {
comments: Vec::new(),
ctes: Vec::new(),
distinct: false,
top: None,
columns: vec![SelectItem::Expr {
expr: Expr::Null,
alias: None,
alias_quote_style: QuoteStyle::None,
}],
from: None,
joins: Vec::new(),
where_clause: None,
group_by: Vec::new(),
having: None,
order_by: Vec::new(),
limit: None,
offset: None,
fetch_first: None,
qualify: None,
window_definitions: Vec::new(),
}))),
ascending: true,
nulls_first: None,
}];
}
}
}
if sel.offset.is_none() {
if let Some(fetch) = sel.fetch_first.take() {
sel.top = Some(Box::new(fetch));
}
}
} else if matches!(target, Dialect::Oracle) {
if let Some(limit) = sel.limit.take() {
sel.fetch_first = Some(limit);
}
if let Some(top) = sel.top.take() {
sel.fetch_first = Some(*top);
}
} else {
if let Some(top) = sel.top.take() {
if sel.limit.is_none() {
sel.limit = Some(*top);
}
}
if let Some(fetch) = sel.fetch_first.take() {
if sel.limit.is_none() {
sel.limit = Some(fetch);
}
}
}
}
fn transform_quotes(expr: Expr, target: Dialect) -> Expr {
match expr {
Expr::Column {
table,
name,
quote_style,
table_quote_style,
} => {
let new_qs = if quote_style.is_quoted() {
QuoteStyle::for_dialect(target)
} else {
QuoteStyle::None
};
let new_tqs = if table_quote_style.is_quoted() {
QuoteStyle::for_dialect(target)
} else {
QuoteStyle::None
};
Expr::Column {
table,
name,
quote_style: new_qs,
table_quote_style: new_tqs,
}
}
Expr::BinaryOp { left, op, right } => Expr::BinaryOp {
left: Box::new(transform_quotes(*left, target)),
op,
right: Box::new(transform_quotes(*right, target)),
},
Expr::UnaryOp { op, expr } => Expr::UnaryOp {
op,
expr: Box::new(transform_quotes(*expr, target)),
},
Expr::Function {
name,
args,
distinct,
filter,
over,
} => Expr::Function {
name,
args: args
.into_iter()
.map(|a| transform_quotes(a, target))
.collect(),
distinct,
filter: filter.map(|f| Box::new(transform_quotes(*f, target))),
over,
},
Expr::TypedFunction { func, filter, over } => Expr::TypedFunction {
func: func.transform_children(&|e| transform_quotes(e, target)),
filter: filter.map(|f| Box::new(transform_quotes(*f, target))),
over,
},
Expr::Nested(inner) => Expr::Nested(Box::new(transform_quotes(*inner, target))),
Expr::Alias { expr, name } => Expr::Alias {
expr: Box::new(transform_quotes(*expr, target)),
name,
},
other => other,
}
}
fn transform_quotes_in_select(sel: &mut SelectStatement, target: Dialect) {
for item in &mut sel.columns {
if let SelectItem::Expr { expr, .. } = item {
*expr = transform_quotes(expr.clone(), target);
}
}
if let Some(wh) = &mut sel.where_clause {
*wh = transform_quotes(wh.clone(), target);
}
for gb in &mut sel.group_by {
*gb = transform_quotes(gb.clone(), target);
}
if let Some(having) = &mut sel.having {
*having = transform_quotes(having.clone(), target);
}
for ob in &mut sel.order_by {
ob.expr = transform_quotes(ob.expr.clone(), target);
}
if let Some(from) = &mut sel.from {
transform_quotes_in_table_source(&mut from.source, target);
}
for join in &mut sel.joins {
transform_quotes_in_table_source(&mut join.table, target);
if let Some(on) = &mut join.on {
*on = transform_quotes(on.clone(), target);
}
}
}
fn transform_quotes_in_table_source(source: &mut TableSource, target: Dialect) {
match source {
TableSource::Table(tref) => {
if tref.name_quote_style.is_quoted() {
tref.name_quote_style = QuoteStyle::for_dialect(target);
}
}
TableSource::Subquery { .. } => {}
TableSource::TableFunction { .. } => {}
TableSource::Lateral { source } => transform_quotes_in_table_source(source, target),
TableSource::Pivot { source, .. } | TableSource::Unpivot { source, .. } => {
transform_quotes_in_table_source(source, target);
}
TableSource::Unnest { .. } => {}
}
}
fn collect_concat_args(expr: &Expr, args: &mut Vec<Expr>) {
match expr {
Expr::BinaryOp {
left,
op: BinaryOperator::Concat,
right,
} => {
collect_concat_args(left, args);
collect_concat_args(right, args);
}
other => args.push(other.clone()),
}
}
fn try_transform_interval_arithmetic(
left: &Expr,
op: &BinaryOperator,
right: &Expr,
) -> Option<Expr> {
if let Expr::Interval { value, unit } = right {
if let Some((count, unit_name)) = parse_interval_value(value, unit) {
let final_count = if matches!(op, BinaryOperator::Minus) {
-count
} else {
count
};
return Some(Expr::Function {
name: "DATEADD".to_string(),
args: vec![
Expr::Column {
table: None,
name: unit_name,
quote_style: QuoteStyle::None,
table_quote_style: QuoteStyle::None,
},
Expr::Number(final_count.to_string()),
left.clone(),
],
distinct: false,
filter: None,
over: None,
});
}
}
if let Expr::Interval { value, unit } = left {
if matches!(op, BinaryOperator::Plus) {
if let Some((count, unit_name)) = parse_interval_value(value, unit) {
return Some(Expr::Function {
name: "DATEADD".to_string(),
args: vec![
Expr::Column {
table: None,
name: unit_name,
quote_style: QuoteStyle::None,
table_quote_style: QuoteStyle::None,
},
Expr::Number(count.to_string()),
right.clone(),
],
distinct: false,
filter: None,
over: None,
});
}
}
}
None
}
fn parse_interval_value(value: &Expr, unit: &Option<DateTimeField>) -> Option<(i64, String)> {
if let Expr::StringLiteral(s) = value {
let parts: Vec<&str> = s.trim().split_whitespace().collect();
if parts.len() == 2 {
let count: i64 = parts[0].parse().ok()?;
let unit_name = normalize_interval_unit(parts[1])?;
return Some((count, unit_name));
}
if parts.len() == 1 {
let count: i64 = parts[0].parse().ok()?;
if let Some(u) = unit {
let unit_name = datetime_field_to_tsql(u)?;
return Some((count, unit_name));
}
}
}
if let Expr::Number(n) = value {
let count: i64 = n.parse().ok()?;
if let Some(u) = unit {
let unit_name = datetime_field_to_tsql(u)?;
return Some((count, unit_name));
}
}
None
}
fn normalize_interval_unit(unit: &str) -> Option<String> {
let lower = unit.to_lowercase();
let normalized = lower.trim_end_matches('s');
match normalized {
"year" => Some("YEAR".to_string()),
"month" => Some("MONTH".to_string()),
"week" => Some("WEEK".to_string()),
"day" => Some("DAY".to_string()),
"hour" => Some("HOUR".to_string()),
"minute" => Some("MINUTE".to_string()),
"second" => Some("SECOND".to_string()),
"millisecond" => Some("MILLISECOND".to_string()),
"microsecond" => Some("MICROSECOND".to_string()),
_ => None,
}
}
fn datetime_field_to_tsql(field: &DateTimeField) -> Option<String> {
match field {
DateTimeField::Year => Some("YEAR".to_string()),
DateTimeField::Quarter => Some("QUARTER".to_string()),
DateTimeField::Month => Some("MONTH".to_string()),
DateTimeField::Week => Some("WEEK".to_string()),
DateTimeField::Day => Some("DAY".to_string()),
DateTimeField::Hour => Some("HOUR".to_string()),
DateTimeField::Minute => Some("MINUTE".to_string()),
DateTimeField::Second => Some("SECOND".to_string()),
DateTimeField::Millisecond => Some("MILLISECOND".to_string()),
DateTimeField::Microsecond => Some("MICROSECOND".to_string()),
_ => None,
}
}
fn simplify_similar_to_pattern(pattern: &Expr) -> Expr {
if let Expr::StringLiteral(s) = pattern {
let simplified = s.replace('|', "%").replace('(', "").replace(')', "");
Expr::StringLiteral(simplified)
} else {
pattern.clone()
}
}