pub mod duck_to_pg;
pub mod duck_to_tsql;
pub mod engine;
pub mod pg_to_duck;
pub mod semantics;
pub mod tokens;
pub mod tsql_to_duck;
pub mod types;
use std::collections::HashMap;
use tokens::{tokenize, Tok};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Dialect {
Tsql,
DuckDb,
Postgres,
}
impl Dialect {
pub fn parse(s: &str) -> Option<Dialect> {
match s.to_lowercase().as_str() {
"tsql" | "mssql" | "sqlserver" | "sql-server" => Some(Dialect::Tsql),
"duckdb" | "duck" => Some(Dialect::DuckDb),
"postgres" | "pg" | "postgresql" => Some(Dialect::Postgres),
_ => None,
}
}
pub fn bracket_idents(self) -> bool {
self == Dialect::Tsql
}
}
pub mod codes {
pub const DIV_DECIMAL: &str = "W-DIV-DECIMAL";
pub const ROUND_TRUNC: &str = "W-ROUND-TRUNC";
pub const FORMAT_NUM: &str = "W-FORMAT-NUM";
pub const TRYCAST_EMPTY: &str = "W-TRYCAST-EMPTY";
pub const CI_COMPARE: &str = "W-CI-COMPARE";
pub const TRAILING_SPACE: &str = "W-TRAILING-SPACE";
pub const PROCEDURAL: &str = "W-PROCEDURAL";
pub const POWER_DOUBLE: &str = "W-POWER-DOUBLE";
pub const GENERIC: &str = "W-GENERIC";
pub const ALL: &[&str] = &[DIV_DECIMAL, ROUND_TRUNC, FORMAT_NUM, TRYCAST_EMPTY, CI_COMPARE, TRAILING_SPACE, PROCEDURAL, POWER_DOUBLE, GENERIC];
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Warning {
pub code: &'static str,
pub line: usize,
pub col: usize,
pub message: String,
}
impl std::fmt::Display for Warning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} (line {}:{}): {}", self.code, self.line, self.col, self.message)
}
}
#[derive(Debug, Clone, Default)]
pub struct Translated {
pub sql: String,
pub warnings: Vec<String>,
pub diagnostics: Vec<Warning>,
}
impl Translated {
fn with_sql(mut self, sql: &str) -> Self { self.sql = sql.to_string(); self }
fn new(sql: String, diagnostics: Vec<Warning>) -> Self {
Translated { sql, warnings: diagnostics.iter().map(|w| w.to_string()).collect(), diagnostics }
}
}
fn render_list(ws: &[Warning]) -> String {
ws.iter().map(|w| w.to_string()).collect::<Vec<_>>().join("\n")
}
#[derive(Debug, thiserror::Error)]
pub enum TranslateError {
#[error("unsupported translation {0:?} -> {1:?}")]
Unsupported(Dialect, Dialect),
#[error("strict mode: {} translation warning(s):\n{}", .0.len(), render_list(.0))]
Strict(Vec<Warning>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NocaseMode {
Global,
Rewrite,
Off,
}
impl NocaseMode {
pub fn parse(s: &str) -> Option<NocaseMode> {
match s.to_ascii_lowercase().as_str() {
"global" => Some(NocaseMode::Global),
"rewrite" => Some(NocaseMode::Rewrite),
"off" | "none" | "false" => Some(NocaseMode::Off),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct Options {
pub target_version: u16,
pub prelude: bool,
pub nocase: bool,
pub nocase_mode: Option<NocaseMode>,
pub float_division: bool,
pub strict: bool,
pub mssql_math: bool,
pub types: HashMap<String, String>,
}
impl Default for Options {
fn default() -> Self {
Options { target_version: 2019, prelude: true, nocase: true, nocase_mode: None, float_division: true, strict: false, mssql_math: false, types: HashMap::new() }
}
}
impl Options {
pub fn nocase_mode(&self) -> NocaseMode {
self.nocase_mode.unwrap_or(if self.nocase { NocaseMode::Rewrite } else { NocaseMode::Off })
}
}
pub fn parse_types(spec: &str) -> HashMap<String, String> {
let mut out = HashMap::new();
let mut depth = 0i32;
let mut cur = String::new();
let mut items = vec![];
for ch in spec.chars() {
match ch {
'(' => { depth += 1; cur.push(ch); }
')' => { depth -= 1; cur.push(ch); }
',' | ';' | '\n' if depth == 0 => items.push(std::mem::take(&mut cur)),
_ => cur.push(ch),
}
}
items.push(cur);
for it in items {
if let Some((k, v)) = it.split_once('=').or_else(|| it.split_once(':')) {
let (k, v) = (k.trim(), v.trim());
if !k.is_empty() && !v.is_empty() { out.insert(k.to_lowercase(), v.to_lowercase()); }
}
}
out
}
pub fn duckdb_prelude(nocase: bool) -> Vec<&'static str> {
duckdb_prelude_for(if nocase { NocaseMode::Global } else { NocaseMode::Off })
}
pub fn duckdb_prelude_for(mode: NocaseMode) -> Vec<&'static str> {
let mut v = vec![
"SET integer_division = true", "SET default_null_order = 'NULLS_FIRST_ON_ASC_LAST_ON_DESC'", "SET TimeZone = 'UTC'",
];
if mode == NocaseMode::Global { v.push("SET default_collation = 'nocase'"); } v
}
pub fn translate(sql: &str, src: Dialect, dst: Dialect, schema_map: &HashMap<String, String>) -> Result<Translated, TranslateError> {
translate_with(sql, src, dst, schema_map, &Options { prelude: false, ..Options::default() })
}
pub fn translate_with(sql: &str, src: Dialect, dst: Dialect, schema_map: &HashMap<String, String>, opts: &Options) -> Result<Translated, TranslateError> {
if src == dst {
return Ok(Translated::default().with_sql(sql));
}
let toks = tokenize(sql, src.bracket_idents());
let mode = opts.nocase_mode();
let mut ctx = engine::Ctx::new(schema_map.clone());
ctx.target_version = opts.target_version;
ctx.nocase_mode = mode;
ctx.nocase = mode != NocaseMode::Off;
ctx.float_division = opts.float_division;
ctx.mssql_math = opts.mssql_math && src == Dialect::Tsql;
ctx.types = opts.types.iter().map(|(k, v)| (k.to_lowercase(), v.to_lowercase())).collect();
let mut out = String::new();
if opts.prelude && src == Dialect::Tsql && dst == Dialect::DuckDb {
out.push_str("-- duckbridge prelude: make DuckDB behave like SQL Server for this script\n");
for p in duckdb_prelude_for(mode) { out.push_str(p); out.push_str(";\n"); }
if opts.mssql_math {
for m in crate::mathcompat::macros() { out.push_str(&m); out.push_str(";\n"); }
}
}
let stmts = tokens::statements(&toks);
if src == Dialect::Tsql {
for st in &stmts {
for (k, v) in semantics::declared_types(st) { ctx.types.entry(k).or_insert(v); }
}
}
let n = stmts.len();
for (i, st) in stmts.iter().enumerate() {
for tk in st.iter().take_while(|t| t.is_trivia()) {
if tk.kind == tokens::Kind::Comment { out.push_str(&tk.text); out.push('\n'); }
}
let st = tokens::trim(st).to_vec();
if st.is_empty() {
continue;
}
ctx.begin_statement(st.first());
let pieces = translate_stmt(&st, src, dst, &mut ctx)?;
for p in pieces {
let s = tokens::join(&p).trim().to_string();
if s.is_empty() {
continue;
}
out.push_str(&s);
if !s.ends_with(';') && !s.starts_with("--") && !s.starts_with("/*") {
out.push(';');
}
out.push('\n');
}
if i + 1 < n && !out.ends_with("\n\n") {
}
}
let diags = ctx.take_warnings();
if opts.strict && !diags.is_empty() {
return Err(TranslateError::Strict(diags));
}
Ok(Translated::new(out.trim_end().to_string(), diags))
}
pub fn translate_expr(expr: &str, src: Dialect, dst: Dialect) -> Result<Translated, TranslateError> {
if src == dst {
return Ok(Translated::default().with_sql(expr));
}
let toks = tokenize(expr, src.bracket_idents());
let mut ctx = engine::Ctx::new(HashMap::new());
let out = rewrite_expr(&toks, src, dst, &mut ctx)?;
Ok(Translated::new(tokens::join(&out).trim().to_string(), ctx.take_warnings()))
}
fn translate_stmt(st: &[Tok], src: Dialect, dst: Dialect, ctx: &mut engine::Ctx) -> Result<Vec<Vec<Tok>>, TranslateError> {
use Dialect::*;
Ok(match (src, dst) {
(Tsql, DuckDb) => tsql_to_duck::statement(st, ctx),
(DuckDb, Tsql) => duck_to_tsql::statement(st, ctx),
(DuckDb, Postgres) => duck_to_pg::statement(st, ctx),
(Postgres, DuckDb) => pg_to_duck::statement(st, ctx),
(Tsql, Postgres) => {
let mut out = vec![];
for s in tsql_to_duck::statement(st, ctx) {
out.extend(duck_to_pg::statement(&s, ctx));
}
out
}
(Postgres, Tsql) => {
let mut out = vec![];
for s in pg_to_duck::statement(st, ctx) {
out.extend(duck_to_tsql::statement(&s, ctx));
}
out
}
(a, b) => return Err(TranslateError::Unsupported(a, b)),
})
}
fn rewrite_expr(toks: &[Tok], src: Dialect, dst: Dialect, ctx: &mut engine::Ctx) -> Result<Vec<Tok>, TranslateError> {
use Dialect::*;
Ok(match (src, dst) {
(Tsql, DuckDb) => tsql_to_duck::expression(toks, ctx),
(DuckDb, Tsql) => duck_to_tsql::expression(toks, ctx),
(DuckDb, Postgres) => duck_to_pg::expression(toks, ctx),
(Postgres, DuckDb) => pg_to_duck::expression(toks, ctx),
(Tsql, Postgres) => {
let mid = tsql_to_duck::expression(toks, ctx);
duck_to_pg::expression(&mid, ctx)
}
(Postgres, Tsql) => {
let mid = pg_to_duck::expression(toks, ctx);
duck_to_tsql::expression(&mid, ctx)
}
(a, b) => return Err(TranslateError::Unsupported(a, b)),
})
}
pub fn view_body(sql: &str, bracket_idents: bool) -> String {
let toks = tokenize(sql, bracket_idents);
let sig: Vec<usize> = toks.iter().enumerate().filter(|(_, t)| !t.is_trivia()).map(|(i, _)| i).collect();
if sig.is_empty() || !toks[sig[0]].is_kw("CREATE") { return sql.trim().trim_end_matches(';').to_string(); }
let mut depth = 0i32;
for (i, t) in toks.iter().enumerate() {
if t.is_punct("(") { depth += 1; } else if t.is_punct(")") { depth -= 1; }
if depth == 0 && t.is_kw("AS") {
return tokens::join(&toks[i + 1..]).trim().trim_end_matches(';').to_string();
}
}
sql.trim().trim_end_matches(';').to_string()
}