pub(crate) mod csharp_microsoft_sqlite;
pub(crate) mod csharp_mysqlconnector;
pub(crate) mod csharp_npgsql;
pub(crate) mod csharp_oracle;
pub(crate) mod csharp_snowflake;
pub(crate) mod csharp_sqlclient;
pub(crate) mod elixir_common;
pub(crate) mod elixir_ecto;
pub(crate) mod elixir_exqlite;
pub(crate) mod elixir_jamdb;
pub(crate) mod elixir_myxql;
pub(crate) mod elixir_postgrex;
pub(crate) mod elixir_tds;
pub(crate) mod go_common;
pub(crate) mod go_database_sql;
pub(crate) mod go_godror;
pub(crate) mod go_gosnowflake;
pub(crate) mod go_pgx;
pub(crate) mod java_jdbc;
pub(crate) mod java_r2dbc;
pub(crate) mod jvm_common;
pub(crate) mod kotlin_exposed;
pub(crate) mod kotlin_jdbc;
pub(crate) mod kotlin_r2dbc;
pub(crate) mod php_amphp;
pub(crate) mod php_common;
pub(crate) mod php_pdo;
pub(crate) mod python_aiomysql;
pub(crate) mod python_aiosqlite;
pub(crate) mod python_asyncpg;
pub(crate) mod python_common;
pub(crate) mod python_duckdb;
pub(crate) mod python_oracledb;
pub(crate) mod python_psycopg3;
pub(crate) mod python_pyodbc;
pub(crate) mod python_snowflake;
pub(crate) mod ruby_mysql2;
pub(crate) mod ruby_oci8;
pub(crate) mod ruby_pg;
pub(crate) mod ruby_rbs;
pub(crate) mod ruby_sqlite3;
pub(crate) mod ruby_tiny_tds;
pub(crate) mod ruby_trilogy;
pub(crate) mod rust_sibyl;
pub(crate) mod rust_tiberius;
pub(crate) mod sqlx;
pub(crate) mod tokio_postgres;
pub(crate) mod typescript_better_sqlite3;
pub(crate) mod typescript_common;
pub(crate) mod typescript_duckdb;
pub(crate) mod typescript_kysely;
pub(crate) mod typescript_mssql;
pub(crate) mod typescript_mysql2;
pub(crate) mod typescript_node_sqlite;
pub(crate) mod typescript_oracledb;
pub(crate) mod typescript_pg;
pub(crate) mod typescript_postgres;
pub(crate) mod typescript_snowflake;
pub(crate) mod typescript_wasm_sqlite;
use scythe_backend::manifest::BackendManifest;
use scythe_backend::naming::NamingConfig;
use scythe_core::SqlDialect;
use scythe_core::analyzer::AnalyzedParam;
use scythe_core::errors::{ErrorCode, ScytheError};
use crate::backend_trait::{CodegenBackend, ResolvedParam};
pub(crate) fn parse_manifest(manifest_toml: &str) -> Result<BackendManifest, ScytheError> {
toml::from_str(manifest_toml).map_err(|e| ScytheError::new(ErrorCode::InternalError, format!("manifest: {e}")))
}
pub(crate) fn apply_field_case_option(
naming: &mut NamingConfig,
backend_name: &str,
value: &str,
) -> Result<(), ScytheError> {
match value {
"snake_case" | "camelCase" => {
naming.field_case = value.to_string();
Ok(())
}
other => Err(ScytheError::new(
ErrorCode::InvalidConfig,
format!("{backend_name}: invalid field_case '{other}' (expected 'snake_case' or 'camelCase')"),
)),
}
}
pub(crate) fn engine_supports_nested_aggregates(engine: &str) -> bool {
matches!(engine, "postgresql" | "postgres" | "pg")
}
pub(crate) fn csharp_typed_reader_method(lang_type: &str) -> String {
format!("GetFieldValue<{lang_type}>")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SqlSpanKind {
Code,
SingleQuoted,
DoubleQuoted,
DollarQuoted,
Backtick,
Bracketed,
LineComment,
BlockComment,
}
fn tokenize_sql(sql: &str, dialect: SqlDialect) -> Vec<(SqlSpanKind, String)> {
let chars: Vec<char> = sql.chars().collect();
let len = chars.len();
let mut spans: Vec<(SqlSpanKind, String)> = Vec::new();
let mut code_buf = String::new();
let mut i = 0;
while i < len {
let c = chars[i];
if c == '-' && i + 1 < len && chars[i + 1] == '-' {
flush_code_buf(&mut code_buf, &mut spans);
let start = i;
i += 2;
while i < len && chars[i] != '\n' {
i += 1;
}
spans.push((SqlSpanKind::LineComment, chars[start..i].iter().collect()));
continue;
}
if c == '/' && i + 1 < len && chars[i + 1] == '*' {
flush_code_buf(&mut code_buf, &mut spans);
let start = i;
i += 2;
let mut depth = 1u32;
while i < len && depth > 0 {
if chars[i] == '/' && i + 1 < len && chars[i + 1] == '*' {
depth += 1;
i += 2;
} else if chars[i] == '*' && i + 1 < len && chars[i + 1] == '/' {
depth -= 1;
i += 2;
} else {
i += 1;
}
}
spans.push((SqlSpanKind::BlockComment, chars[start..i].iter().collect()));
continue;
}
if c == '\'' {
flush_code_buf(&mut code_buf, &mut spans);
let start = i;
i += 1;
while i < len {
if chars[i] == '\'' {
if i + 1 < len && chars[i + 1] == '\'' {
i += 2;
continue;
}
i += 1;
break;
}
i += 1;
}
spans.push((SqlSpanKind::SingleQuoted, chars[start..i].iter().collect()));
continue;
}
if c == '"' {
flush_code_buf(&mut code_buf, &mut spans);
let start = i;
i += 1;
while i < len {
if chars[i] == '"' {
if i + 1 < len && chars[i + 1] == '"' {
i += 2;
continue;
}
i += 1;
break;
}
i += 1;
}
spans.push((SqlSpanKind::DoubleQuoted, chars[start..i].iter().collect()));
continue;
}
if c == '`' && dialect == SqlDialect::MySQL {
flush_code_buf(&mut code_buf, &mut spans);
let start = i;
i += 1;
while i < len {
if chars[i] == '`' {
if i + 1 < len && chars[i + 1] == '`' {
i += 2;
continue;
}
i += 1;
break;
}
i += 1;
}
spans.push((SqlSpanKind::Backtick, chars[start..i].iter().collect()));
continue;
}
if c == '#' && dialect == SqlDialect::MySQL {
flush_code_buf(&mut code_buf, &mut spans);
let start = i;
while i < len && chars[i] != '\n' {
i += 1;
}
spans.push((SqlSpanKind::LineComment, chars[start..i].iter().collect()));
continue;
}
if c == '[' && dialect == SqlDialect::MsSql {
flush_code_buf(&mut code_buf, &mut spans);
let start = i;
i += 1;
while i < len {
if chars[i] == ']' {
if i + 1 < len && chars[i + 1] == ']' {
i += 2;
continue;
}
i += 1;
break;
}
i += 1;
}
spans.push((SqlSpanKind::Bracketed, chars[start..i].iter().collect()));
continue;
}
if c == '$'
&& let Some(open_end) = match_dollar_quote_open(&chars, i)
{
flush_code_buf(&mut code_buf, &mut spans);
let start = i;
let delim: Vec<char> = chars[i..open_end].to_vec();
let mut j = open_end;
let mut found = false;
while j < len {
if chars[j] == '$' && j + delim.len() <= len && chars[j..j + delim.len()] == delim[..] {
j += delim.len();
found = true;
break;
}
j += 1;
}
if !found {
j = len;
}
spans.push((SqlSpanKind::DollarQuoted, chars[start..j].iter().collect()));
i = j;
continue;
}
code_buf.push(c);
i += 1;
}
flush_code_buf(&mut code_buf, &mut spans);
spans
}
fn flush_code_buf(code_buf: &mut String, spans: &mut Vec<(SqlSpanKind, String)>) {
if !code_buf.is_empty() {
spans.push((SqlSpanKind::Code, std::mem::take(code_buf)));
}
}
fn match_dollar_quote_open(chars: &[char], i: usize) -> Option<usize> {
let mut j = i + 1;
if j < chars.len() && chars[j] == '$' {
return Some(j + 1);
}
let tag_start = j;
while j < chars.len() && (chars[j].is_alphanumeric() || chars[j] == '_') {
j += 1;
}
if j > tag_start && j < chars.len() && chars[j] == '$' && !chars[tag_start].is_ascii_digit() {
return Some(j + 1);
}
None
}
pub(crate) fn clean_sql(sql: &str) -> String {
clean_sql_lines(sql, SqlDialect::PostgreSQL)
.join("\n")
.trim()
.trim_end_matches(';')
.trim()
.to_string()
}
pub(crate) fn clean_sql_oneline(sql: &str) -> String {
clean_sql_lines(sql, SqlDialect::PostgreSQL)
.join(" ")
.trim()
.trim_end_matches(';')
.trim()
.to_string()
}
#[cfg_attr(
not(test),
expect(dead_code, reason = "staged board #148 migration; see doc comment")
)]
pub(crate) fn clean_sql_dialect(sql: &str, dialect: SqlDialect) -> String {
clean_sql_lines(sql, dialect)
.join("\n")
.trim()
.trim_end_matches(';')
.trim()
.to_string()
}
pub(crate) fn clean_sql_oneline_dialect(sql: &str, dialect: SqlDialect) -> String {
clean_sql_lines(sql, dialect)
.join(" ")
.trim()
.trim_end_matches(';')
.trim()
.to_string()
}
fn clean_sql_lines(sql: &str, dialect: SqlDialect) -> Vec<String> {
let normalized = sql.replace("\r\n", "\n");
let spans = tokenize_sql(&normalized, dialect);
let mut lines: Vec<String> = vec![String::new()];
let mut touched_by_comment: Vec<bool> = vec![false];
for (kind, text) in &spans {
let is_comment = matches!(kind, SqlSpanKind::LineComment | SqlSpanKind::BlockComment);
for ch in text.chars() {
if ch == '\n' {
if is_comment {
*touched_by_comment.last_mut().expect("at least one line") = true;
}
lines.push(String::new());
touched_by_comment.push(false);
} else if is_comment {
*touched_by_comment.last_mut().expect("at least one line") = true;
} else {
lines.last_mut().expect("at least one line").push(ch);
}
}
}
lines
.into_iter()
.zip(touched_by_comment)
.filter(|(kept, touched)| !(*touched && kept.trim().is_empty()))
.map(|(kept, _)| kept)
.collect()
}
pub(crate) fn rewrite_optional_params(sql: &str, optional_params: &[String], params: &[AnalyzedParam]) -> String {
if optional_params.is_empty() {
return sql.to_string();
}
let mut result = sql.to_string();
for opt_name in optional_params {
let Some(param) = params.iter().find(|p| p.name == *opt_name) else {
continue;
};
let placeholder = format!("${}", param.position);
for op in &[
">=",
"<=",
"<>",
"!=",
">",
"<",
"=",
"NOT ILIKE",
"not ilike",
"NOT LIKE",
"not like",
"ILIKE",
"ilike",
"LIKE",
"like",
] {
result = rewrite_comparison(&result, &placeholder, op);
}
}
result
}
fn rewrite_comparison(sql: &str, placeholder: &str, op: &str) -> String {
let mut result = String::with_capacity(sql.len() + 32);
let chars: Vec<char> = sql.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
if let Some((_start, col, end)) = try_match_col_op_ph(&chars, i, op, placeholder) {
result.push_str(&format!("({placeholder} IS NULL OR {col} {op} {placeholder})"));
i = end;
continue;
}
if let Some((end, col)) = try_match_ph_op_col(&chars, i, op, placeholder) {
result.push_str(&format!("({placeholder} IS NULL OR {col} {op} {placeholder})"));
i = end;
continue;
}
result.push(chars[i]);
i += 1;
}
result
}
fn is_reserved_non_column_keyword(ident: &str) -> bool {
matches!(
ident.to_ascii_uppercase().as_str(),
"NOT" | "AND" | "OR" | "IS" | "NULL"
)
}
fn match_op_tokens(chars: &[char], mut j: usize, op: &str) -> Option<usize> {
for (idx, word) in op.split_whitespace().enumerate() {
if idx > 0 {
let ws_start = j;
while j < chars.len() && chars[j].is_whitespace() {
j += 1;
}
if j == ws_start {
return None;
}
}
let word_chars: Vec<char> = word.chars().collect();
if j + word_chars.len() > chars.len() {
return None;
}
for (k, wc) in word_chars.iter().enumerate() {
if chars[j + k] != *wc {
return None;
}
}
j += word_chars.len();
}
Some(j)
}
fn try_match_col_op_ph(chars: &[char], i: usize, op: &str, placeholder: &str) -> Option<(usize, String, usize)> {
if !is_ident_char(chars[i]) {
return None;
}
if i > 0 && is_ident_char(chars[i - 1]) {
return None;
}
let ident_start = i;
let mut j = i;
while j < chars.len() && is_ident_char(chars[j]) {
j += 1;
}
let ident: String = chars[ident_start..j].iter().collect();
if is_reserved_non_column_keyword(&ident) {
return None;
}
while j < chars.len() && chars[j].is_whitespace() {
j += 1;
}
j = match_op_tokens(chars, j, op)?;
while j < chars.len() && chars[j].is_whitespace() {
j += 1;
}
let ph_chars: Vec<char> = placeholder.chars().collect();
if j + ph_chars.len() > chars.len() {
return None;
}
for (k, pc) in ph_chars.iter().enumerate() {
if chars[j + k] != *pc {
return None;
}
}
j += ph_chars.len();
if j < chars.len() && chars[j].is_ascii_digit() {
return None;
}
Some((i, ident, j))
}
fn try_match_ph_op_col(chars: &[char], i: usize, op: &str, placeholder: &str) -> Option<(usize, String)> {
let ph_chars: Vec<char> = placeholder.chars().collect();
if i + ph_chars.len() > chars.len() {
return None;
}
if i > 0 && (chars[i - 1] == '$' || chars[i - 1].is_ascii_digit()) {
return None;
}
for (k, pc) in ph_chars.iter().enumerate() {
if chars[i + k] != *pc {
return None;
}
}
let mut j = i + ph_chars.len();
if j < chars.len() && chars[j].is_ascii_digit() {
return None;
}
while j < chars.len() && chars[j].is_whitespace() {
j += 1;
}
j = match_op_tokens(chars, j, op)?;
while j < chars.len() && chars[j].is_whitespace() {
j += 1;
}
if j >= chars.len() || !is_ident_char(chars[j]) {
return None;
}
let ident_start = j;
while j < chars.len() && is_ident_char(chars[j]) {
j += 1;
}
let ident: String = chars[ident_start..j].iter().collect();
if is_reserved_non_column_keyword(&ident) {
return None;
}
Some((j, ident))
}
pub(crate) fn clean_sql_with_optional(sql: &str, optional_params: &[String], params: &[AnalyzedParam]) -> String {
let cleaned = clean_sql(sql);
rewrite_optional_params(&cleaned, optional_params, params)
}
pub(crate) fn clean_sql_oneline_with_optional(
sql: &str,
optional_params: &[String],
params: &[AnalyzedParam],
) -> String {
let cleaned = clean_sql_oneline(sql);
rewrite_optional_params(&cleaned, optional_params, params)
}
pub(crate) fn clean_sql_oneline_with_optional_dialect(
sql: &str,
dialect: SqlDialect,
optional_params: &[String],
params: &[AnalyzedParam],
) -> String {
let cleaned = clean_sql_oneline_dialect(sql, dialect);
rewrite_optional_params(&cleaned, optional_params, params)
}
fn is_ident_char(c: char) -> bool {
c.is_alphanumeric() || c == '_' || c == '.'
}
pub(crate) fn rewrite_pg_placeholders(sql: &str, formatter: impl Fn(u32) -> String) -> String {
let spans = tokenize_sql(sql, SqlDialect::PostgreSQL);
let uses_dollar_placeholders = spans
.iter()
.any(|(kind, text)| *kind == SqlSpanKind::Code && code_span_has_dollar_number(text));
let mut result = String::with_capacity(sql.len());
let mut positional_counter: u32 = 0;
let mut occurrences: Vec<u32> = Vec::new();
for (kind, text) in &spans {
if *kind == SqlSpanKind::Code {
rewrite_code_span_placeholders(
text,
!uses_dollar_placeholders,
&formatter,
&mut positional_counter,
&mut result,
&mut occurrences,
);
} else {
result.push_str(text);
}
}
result
}
fn dialect_allows_bare_question_mark_placeholder(dialect: SqlDialect) -> bool {
!matches!(dialect, SqlDialect::PostgreSQL)
}
pub(crate) fn rewrite_placeholders_indexed(
sql: &str,
dialect: SqlDialect,
formatter: impl Fn(u32) -> String,
) -> (String, Vec<u32>) {
let spans = tokenize_sql(sql, dialect);
let bare_question_is_placeholder = dialect_allows_bare_question_mark_placeholder(dialect);
let mut result = String::with_capacity(sql.len());
let mut positional_counter: u32 = 0;
let mut occurrences: Vec<u32> = Vec::new();
for (kind, text) in &spans {
if *kind == SqlSpanKind::Code {
rewrite_code_span_placeholders(
text,
bare_question_is_placeholder,
&formatter,
&mut positional_counter,
&mut result,
&mut occurrences,
);
} else {
result.push_str(text);
}
}
(result, occurrences)
}
pub(crate) fn resolved_param_for_position<'a>(
analyzed_params: &[AnalyzedParam],
resolved: &'a [ResolvedParam],
position: u32,
) -> &'a ResolvedParam {
let idx = analyzed_params
.iter()
.position(|p| p.position == i64::from(position))
.unwrap_or_else(|| panic!("no analyzed parameter at position {position}; analyzed_params={analyzed_params:?}"));
&resolved[idx]
}
pub(crate) fn rewrite_pg_typed_placeholders(
sql: &str,
analyzed_params: &[AnalyzedParam],
resolved_params: &[ResolvedParam],
cast_enums: bool,
formatter: impl Fn(u32) -> String,
) -> (String, Vec<u32>) {
rewrite_placeholders_indexed(sql, SqlDialect::PostgreSQL, |position| {
let mut placeholder = formatter(position);
let param = resolved_param_for_position(analyzed_params, resolved_params, position);
if cast_enums && let Some(enum_type) = param.neutral_type.strip_prefix("enum::") {
placeholder.push_str("::");
placeholder.push_str(enum_type);
} else if let Some(composite_type) = param.neutral_type.strip_prefix("composite::") {
placeholder.push_str("::text::");
placeholder.push_str(composite_type);
}
placeholder
})
}
fn code_span_has_dollar_number(text: &str) -> bool {
let chars: Vec<char> = text.chars().collect();
(0..chars.len()).any(|i| chars[i] == '$' && chars.get(i + 1).is_some_and(|c| c.is_ascii_digit()))
}
const UNAMBIGUOUS_OPERATORS: [&str; 6] = ["?-|", "?||", "?|", "?&", "?-", "@?"];
fn match_unambiguous_operator(chars: &[char], i: usize) -> Option<usize> {
UNAMBIGUOUS_OPERATORS.iter().find_map(|op| {
let op_chars: Vec<char> = op.chars().collect();
let end = i + op_chars.len();
(end <= chars.len() && chars[i..end] == op_chars[..]).then_some(op_chars.len())
})
}
fn rewrite_code_span_placeholders(
text: &str,
bare_question_is_placeholder: bool,
formatter: &impl Fn(u32) -> String,
counter: &mut u32,
out: &mut String,
occurrences: &mut Vec<u32>,
) {
let chars: Vec<char> = text.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
if let Some(op_len) = match_unambiguous_operator(&chars, i) {
out.extend(&chars[i..i + op_len]);
i += op_len;
continue;
}
let ch = chars[i];
if ch == '$' && chars.get(i + 1).is_some_and(|c| c.is_ascii_digit()) {
let mut j = i + 1;
let mut num_str = String::new();
while j < len && chars[j].is_ascii_digit() {
num_str.push(chars[j]);
j += 1;
}
let num: u32 = num_str.parse().unwrap_or(0);
out.push_str(&formatter(num));
occurrences.push(num);
i = j;
} else if ch == '?' && bare_question_is_placeholder && !chars.get(i + 1).is_some_and(|c| c.is_ascii_digit()) {
*counter += 1;
out.push_str(&formatter(*counter));
occurrences.push(*counter);
i += 1;
} else {
out.push(ch);
i += 1;
}
}
}
pub fn get_backend(name: &str, engine: &str) -> Result<Box<dyn CodegenBackend>, ScytheError> {
let canonical_engine = normalize_engine(engine);
let backend: Box<dyn CodegenBackend> = match name {
"rust-sqlx" | "sqlx" | "rust" => Box::new(sqlx::SqlxBackend::new(canonical_engine)?),
"rust-tokio-postgres" | "tokio-postgres" => {
Box::new(tokio_postgres::TokioPostgresBackend::new(canonical_engine)?)
}
"python-psycopg3" | "python" => Box::new(python_psycopg3::PythonPsycopg3Backend::new(canonical_engine)?),
"python-asyncpg" => Box::new(python_asyncpg::PythonAsyncpgBackend::new(canonical_engine)?),
"python-aiomysql" => Box::new(python_aiomysql::PythonAiomysqlBackend::new(canonical_engine)?),
"python-aiosqlite" => Box::new(python_aiosqlite::PythonAiosqliteBackend::new(canonical_engine)?),
"python-duckdb" => Box::new(python_duckdb::PythonDuckdbBackend::new(canonical_engine)?),
"typescript-postgres" | "ts" | "typescript" => {
Box::new(typescript_postgres::TypescriptPostgresBackend::new(canonical_engine)?)
}
"javascript-postgres" => Box::new(typescript_postgres::TypescriptPostgresBackend::new_js(
canonical_engine,
)?),
"typescript-pg" => Box::new(typescript_pg::TypescriptPgBackend::new(canonical_engine)?),
"javascript-pg" => Box::new(typescript_pg::TypescriptPgBackend::new_js(canonical_engine)?),
"typescript-mysql2" => Box::new(typescript_mysql2::TypescriptMysql2Backend::new(canonical_engine)?),
"javascript-mysql2" => Box::new(typescript_mysql2::TypescriptMysql2Backend::new_js(canonical_engine)?),
"typescript-better-sqlite3" => Box::new(typescript_better_sqlite3::TypescriptBetterSqlite3Backend::new(
canonical_engine,
)?),
"javascript-better-sqlite3" => Box::new(typescript_better_sqlite3::TypescriptBetterSqlite3Backend::new_js(
canonical_engine,
)?),
"typescript-duckdb" => Box::new(typescript_duckdb::TypescriptDuckdbBackend::new(canonical_engine)?),
"javascript-duckdb" => Box::new(typescript_duckdb::TypescriptDuckdbBackend::new_js(canonical_engine)?),
"typescript-node-sqlite" => Box::new(typescript_node_sqlite::TypescriptNodeSqliteBackend::new(
canonical_engine,
)?),
"javascript-node-sqlite" => Box::new(typescript_node_sqlite::TypescriptNodeSqliteBackend::new_js(
canonical_engine,
)?),
"typescript-wasm-sqlite" => Box::new(typescript_wasm_sqlite::TypescriptWasmSqliteBackend::new(
canonical_engine,
)?),
"javascript-wasm-sqlite" => Box::new(typescript_wasm_sqlite::TypescriptWasmSqliteBackend::new_js(
canonical_engine,
)?),
"typescript-kysely" | "kysely" => Box::new(typescript_kysely::TypescriptKyselyBackend::new(canonical_engine)?),
"go-database-sql" => Box::new(go_database_sql::GoDatabaseSqlBackend::new(canonical_engine)?),
"go-pgx" | "go" => Box::new(go_pgx::GoPgxBackend::new(canonical_engine)?),
"java-jdbc" | "java" => Box::new(java_jdbc::JavaJdbcBackend::new(canonical_engine)?),
"java-r2dbc" | "r2dbc-java" => Box::new(java_r2dbc::JavaR2dbcBackend::new(canonical_engine)?),
"kotlin-exposed" | "exposed" => Box::new(kotlin_exposed::KotlinExposedBackend::new(canonical_engine)?),
"kotlin-jdbc" | "kotlin" | "kt" => Box::new(kotlin_jdbc::KotlinJdbcBackend::new(canonical_engine)?),
"kotlin-r2dbc" | "r2dbc-kotlin" => Box::new(kotlin_r2dbc::KotlinR2dbcBackend::new(canonical_engine)?),
"csharp-npgsql" | "csharp" | "c#" | "dotnet" => {
Box::new(csharp_npgsql::CsharpNpgsqlBackend::new(canonical_engine)?)
}
"csharp-mysqlconnector" => Box::new(csharp_mysqlconnector::CsharpMysqlConnectorBackend::new(
canonical_engine,
)?),
"csharp-microsoft-sqlite" => Box::new(csharp_microsoft_sqlite::CsharpMicrosoftSqliteBackend::new(
canonical_engine,
)?),
"elixir-postgrex" | "elixir" | "ex" => Box::new(elixir_postgrex::ElixirPostgrexBackend::new(canonical_engine)?),
"elixir-ecto" | "ecto" => Box::new(elixir_ecto::ElixirEctoBackend::new(canonical_engine)?),
"elixir-myxql" => Box::new(elixir_myxql::ElixirMyxqlBackend::new(canonical_engine)?),
"elixir-exqlite" => Box::new(elixir_exqlite::ElixirExqliteBackend::new(canonical_engine)?),
"ruby-pg" | "ruby" | "rb" => Box::new(ruby_pg::RubyPgBackend::new(canonical_engine)?),
"ruby-mysql2" => Box::new(ruby_mysql2::RubyMysql2Backend::new(canonical_engine)?),
"ruby-sqlite3" => Box::new(ruby_sqlite3::RubySqlite3Backend::new(canonical_engine)?),
"ruby-trilogy" | "trilogy" => Box::new(ruby_trilogy::RubyTrilogyBackend::new(canonical_engine)?),
"php-pdo" | "php" => Box::new(php_pdo::PhpPdoBackend::new(canonical_engine)?),
"php-amphp" | "amphp" => Box::new(php_amphp::PhpAmphpBackend::new(canonical_engine)?),
"rust-tiberius" | "tiberius" => Box::new(rust_tiberius::RustTiberiusBackend::new(canonical_engine)?),
"python-pyodbc" | "pyodbc" => Box::new(python_pyodbc::PythonPyodbcBackend::new(canonical_engine)?),
"typescript-mssql" | "tedious" => Box::new(typescript_mssql::TypescriptMssqlBackend::new(canonical_engine)?),
"javascript-mssql" => Box::new(typescript_mssql::TypescriptMssqlBackend::new_js(canonical_engine)?),
"csharp-sqlclient" => Box::new(csharp_sqlclient::CsharpSqlClientBackend::new(canonical_engine)?),
"ruby-tiny-tds" | "tiny-tds" | "tiny_tds" => {
Box::new(ruby_tiny_tds::RubyTinyTdsBackend::new(canonical_engine)?)
}
"elixir-tds" | "tds" => Box::new(elixir_tds::ElixirTdsBackend::new(canonical_engine)?),
"rust-sibyl" | "sibyl" => Box::new(rust_sibyl::RustSibylBackend::new(canonical_engine)?),
"python-oracledb" | "oracledb" => Box::new(python_oracledb::PythonOracledbBackend::new(canonical_engine)?),
"typescript-oracledb" => Box::new(typescript_oracledb::TypescriptOracledbBackend::new(canonical_engine)?),
"javascript-oracledb" => Box::new(typescript_oracledb::TypescriptOracledbBackend::new_js(
canonical_engine,
)?),
"go-godror" | "godror" => Box::new(go_godror::GoGodrorBackend::new(canonical_engine)?),
"csharp-oracle" => Box::new(csharp_oracle::CsharpOracleBackend::new(canonical_engine)?),
"ruby-oci8" | "oci8" => Box::new(ruby_oci8::RubyOci8Backend::new(canonical_engine)?),
"elixir-jamdb" | "jamdb" => Box::new(elixir_jamdb::ElixirJamdbBackend::new(canonical_engine)?),
"python-snowflake" => Box::new(python_snowflake::PythonSnowflakeBackend::new(canonical_engine)?),
"typescript-snowflake" => Box::new(typescript_snowflake::TypescriptSnowflakeBackend::new(canonical_engine)?),
"javascript-snowflake" => Box::new(typescript_snowflake::TypescriptSnowflakeBackend::new_js(
canonical_engine,
)?),
"go-gosnowflake" | "gosnowflake" => Box::new(go_gosnowflake::GoGosnowflakeBackend::new(canonical_engine)?),
"csharp-snowflake" => Box::new(csharp_snowflake::CsharpSnowflakeBackend::new(canonical_engine)?),
_ => {
return Err(ScytheError::new(
ErrorCode::InvalidConfig,
format!("unknown backend: {}", name),
));
}
};
if !backend
.supported_engines()
.iter()
.any(|e| normalize_engine(e) == canonical_engine)
{
return Err(ScytheError::new(
ErrorCode::InvalidConfig,
format!(
"backend '{}' does not support engine '{}'. Supported: {:?}",
name,
engine,
backend.supported_engines()
),
));
}
Ok(backend)
}
pub fn normalize_engine(engine: &str) -> &str {
match engine {
"postgresql" | "postgres" | "pg" | "cockroachdb" | "crdb" => "postgresql",
"mysql" => "mysql",
"mariadb" => "mariadb",
"sqlite" | "sqlite3" => "sqlite",
"duckdb" => "duckdb",
"mssql" | "sqlserver" | "tsql" => "mssql",
"oracle" => "oracle",
"snowflake" => "snowflake",
"redshift" => "redshift",
other => other,
}
}
#[cfg(test)]
mod sql_text_characterization_tests;
#[cfg(test)]
mod tests {
use super::*;
fn param(name: &str, position: i64) -> AnalyzedParam {
AnalyzedParam {
name: name.to_string(),
neutral_type: "string".to_string(),
nullable: true,
position,
source_relation: None,
}
}
fn resolved_param(name: &str, neutral_type: &str) -> ResolvedParam {
ResolvedParam {
name: name.to_string(),
field_name: name.to_string(),
lang_type: "String".to_string(),
full_type: "String".to_string(),
borrowed_type: "&str".to_string(),
neutral_type: neutral_type.to_string(),
nullable: false,
}
}
#[test]
fn typed_placeholder_casts_only_rewrite_live_tokens_and_keep_number_boundaries() {
let analyzed: Vec<AnalyzedParam> = (1..=10)
.map(|position| param(&format!("p{position}"), position))
.collect();
let resolved: Vec<ResolvedParam> = (1..=10)
.map(|position| {
let neutral_type = match position {
1 => "composite::address",
10 => "enum::status",
_ => "string",
};
resolved_param(&format!("p{position}"), neutral_type)
})
.collect();
let sql = "SELECT $1, $10, '$1', \"$10\", $$ $1 $10 $$ -- $1\n/* $10 */ WHERE x = $2";
let (rewritten, occurrences) =
rewrite_pg_typed_placeholders(sql, &analyzed, &resolved, true, |position| format!("@p{position}"));
assert_eq!(
rewritten,
"SELECT @p1::text::address, @p10::status, '$1', \"$10\", $$ $1 $10 $$ -- $1\n/* $10 */ WHERE x = @p2"
);
assert_eq!(occurrences, vec![1, 10, 2]);
}
#[test]
fn test_no_backends_relative_manifest_path_literals() {
let root = concat!(env!("CARGO_MANIFEST_DIR"), "/src/backends");
let word: String = format!("backend{}", "s/");
let needle: String = format!("{}{}", '"', word);
let mut offenders = Vec::new();
collect_rs_files(std::path::Path::new(root), &mut offenders);
let offenders: Vec<_> = offenders
.into_iter()
.filter(|path| {
let contents = std::fs::read_to_string(path).expect("failed to read source file");
contents.contains(&needle)
})
.collect();
assert!(
offenders.is_empty(),
"found a working-directory-relative manifest path literal in: {offenders:?} (#82 regression \
-- manifest selection must be a pure function of (backend, engine), no filesystem lookup)"
);
}
fn collect_rs_files(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
let entries = std::fs::read_dir(dir).expect("failed to read backends dir");
for entry in entries {
let entry = entry.expect("failed to read dir entry");
let path = entry.path();
if path.is_dir() {
collect_rs_files(&path, out);
} else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
out.push(path);
}
}
}
#[test]
fn test_get_backend_rust_sqlx_mysql_has_no_postgresql_scalars() {
let backend = get_backend("rust-sqlx", "mysql").expect("rust-sqlx should support mysql");
let scalars = &backend.manifest().types.scalars;
assert_ne!(
scalars.get("uuid").map(String::as_str),
Some("uuid::Uuid"),
"rust-sqlx/mysql manifest should not carry the PostgreSQL-specific 'uuid' mapping"
);
assert_ne!(
scalars.get("inet").map(String::as_str),
Some("ipnetwork::IpNetwork"),
"rust-sqlx/mysql manifest should not carry the PostgreSQL-specific 'inet' mapping"
);
}
#[test]
fn test_get_backend_java_jdbc_sqlite_has_no_postgresql_scalars() {
let backend = get_backend("java-jdbc", "sqlite").expect("java-jdbc should support sqlite");
let scalars = &backend.manifest().types.scalars;
assert_ne!(
scalars.get("uuid").map(String::as_str),
Some("java.util.UUID"),
"java-jdbc/sqlite manifest should not carry the PostgreSQL-specific 'uuid' mapping"
);
assert_ne!(
scalars.get("decimal").map(String::as_str),
Some("java.math.BigDecimal"),
"java-jdbc/sqlite manifest should not carry the PostgreSQL-specific 'decimal' mapping"
);
}
#[test]
fn test_normalize_engine_cockroachdb() {
assert_eq!(normalize_engine("cockroachdb"), "postgresql");
assert_eq!(normalize_engine("crdb"), "postgresql");
}
#[test]
fn test_get_backend_cockroachdb_with_pg_backends() {
let pg_backends = [
"rust-sqlx",
"rust-tokio-postgres",
"python-psycopg3",
"python-asyncpg",
"typescript-postgres",
"typescript-pg",
"typescript-kysely",
"go-pgx",
"ruby-pg",
"elixir-postgrex",
"csharp-npgsql",
"php-pdo",
"php-amphp",
];
for backend_name in &pg_backends {
let result = get_backend(backend_name, "cockroachdb");
assert!(
result.is_ok(),
"backend '{}' should accept cockroachdb engine, got: {:?}",
backend_name,
result.err()
);
}
}
#[test]
fn test_get_backend_crdb_alias() {
let result = get_backend("rust-sqlx", "crdb");
assert!(result.is_ok(), "rust-sqlx should accept 'crdb' engine alias");
}
#[test]
fn test_normalize_engine_duckdb() {
assert_eq!(normalize_engine("duckdb"), "duckdb");
}
#[test]
fn test_get_backend_duckdb_with_compatible_backends() {
let duckdb_backends = [
"python-duckdb",
"typescript-duckdb",
"go-database-sql",
"java-jdbc",
"kotlin-jdbc",
];
for backend_name in &duckdb_backends {
let result = get_backend(backend_name, "duckdb");
assert!(
result.is_ok(),
"backend '{}' should accept duckdb engine, got: {:?}",
backend_name,
result.err()
);
}
}
#[test]
fn test_get_backend_duckdb_rejected_by_pg_only() {
let result = get_backend("rust-sqlx", "duckdb");
assert!(result.is_err(), "rust-sqlx should reject duckdb engine");
}
#[test]
fn test_rewrite_simple_equality() {
let sql = "SELECT * FROM users WHERE status = $1";
let params = vec![param("status", 1)];
let result = rewrite_optional_params(sql, &["status".to_string()], ¶ms);
assert_eq!(result, "SELECT * FROM users WHERE ($1 IS NULL OR status = $1)");
}
#[test]
fn test_rewrite_qualified_column() {
let sql = "SELECT * FROM users u WHERE u.status = $1";
let params = vec![param("status", 1)];
let result = rewrite_optional_params(sql, &["status".to_string()], ¶ms);
assert_eq!(result, "SELECT * FROM users u WHERE ($1 IS NULL OR u.status = $1)");
}
#[test]
fn test_rewrite_multiple_optional() {
let sql = "SELECT * FROM users WHERE status = $1 AND name = $2";
let params = vec![param("status", 1), param("name", 2)];
let result = rewrite_optional_params(sql, &["status".to_string(), "name".to_string()], ¶ms);
assert_eq!(
result,
"SELECT * FROM users WHERE ($1 IS NULL OR status = $1) AND ($2 IS NULL OR name = $2)"
);
}
#[test]
fn test_rewrite_mixed_optional_required() {
let sql = "SELECT * FROM users WHERE id = $1 AND status = $2";
let params = vec![param("id", 1), param("status", 2)];
let result = rewrite_optional_params(sql, &["status".to_string()], ¶ms);
assert_eq!(
result,
"SELECT * FROM users WHERE id = $1 AND ($2 IS NULL OR status = $2)"
);
}
#[test]
fn test_rewrite_like_operator() {
let sql = "SELECT * FROM users WHERE name LIKE $1";
let params = vec![param("name", 1)];
let result = rewrite_optional_params(sql, &["name".to_string()], ¶ms);
assert_eq!(result, "SELECT * FROM users WHERE ($1 IS NULL OR name LIKE $1)");
}
#[test]
fn test_rewrite_ilike_operator() {
let sql = "SELECT * FROM users WHERE name ILIKE $1";
let params = vec![param("name", 1)];
let result = rewrite_optional_params(sql, &["name".to_string()], ¶ms);
assert_eq!(result, "SELECT * FROM users WHERE ($1 IS NULL OR name ILIKE $1)");
}
#[test]
fn test_rewrite_comparison_operators() {
let sql = "SELECT * FROM users WHERE age >= $1";
let params = vec![param("age", 1)];
let result = rewrite_optional_params(sql, &["age".to_string()], ¶ms);
assert_eq!(result, "SELECT * FROM users WHERE ($1 IS NULL OR age >= $1)");
}
#[test]
fn test_rewrite_less_than() {
let sql = "SELECT * FROM users WHERE age < $1";
let params = vec![param("age", 1)];
let result = rewrite_optional_params(sql, &["age".to_string()], ¶ms);
assert_eq!(result, "SELECT * FROM users WHERE ($1 IS NULL OR age < $1)");
}
#[test]
fn test_no_rewrite_without_optional() {
let sql = "SELECT * FROM users WHERE status = $1";
let params = vec![param("status", 1)];
let result = rewrite_optional_params(sql, &[], ¶ms);
assert_eq!(result, sql);
}
#[test]
fn test_rewrite_not_equal() {
let sql = "SELECT * FROM users WHERE status <> $1";
let params = vec![param("status", 1)];
let result = rewrite_optional_params(sql, &["status".to_string()], ¶ms);
assert_eq!(result, "SELECT * FROM users WHERE ($1 IS NULL OR status <> $1)");
}
#[test]
fn test_rewrite_does_not_match_similar_placeholder() {
let sql = "SELECT * FROM users WHERE status = $10";
let params = vec![param("status", 1)];
let result = rewrite_optional_params(sql, &["status".to_string()], ¶ms);
assert_eq!(result, sql);
}
#[test]
fn test_normalize_engine_mariadb() {
assert_eq!(normalize_engine("mariadb"), "mariadb");
}
#[test]
fn test_get_backend_mariadb_with_mysql_backends() {
let mariadb_backends = [
"rust-sqlx",
"python-aiomysql",
"typescript-mysql2",
"go-database-sql",
"java-jdbc",
"java-r2dbc",
"kotlin-jdbc",
"kotlin-r2dbc",
"csharp-mysqlconnector",
"elixir-myxql",
"ruby-mysql2",
"ruby-trilogy",
"php-pdo",
"php-amphp",
];
for backend_name in &mariadb_backends {
let result = get_backend(backend_name, "mariadb");
assert!(
result.is_ok(),
"backend '{}' should accept mariadb engine, got: {:?}",
backend_name,
result.err()
);
}
}
#[test]
fn test_clean_sql_oneline_strips_mid_line_comment_but_keeps_rest_of_query() {
let sql = "SELECT id, name\nFROM users\nWHERE id > $1 -- skip low ids\n AND name IS NOT NULL;";
let result = clean_sql_oneline(sql);
assert!(
result.contains("AND name IS NOT NULL"),
"mid-line comment must not swallow the rest of the query, got: {result:?}"
);
assert!(
!result.contains("--"),
"comment marker must be stripped, got: {result:?}"
);
}
#[test]
fn test_clean_sql_strips_mid_line_comment_but_keeps_rest_of_query() {
let sql = "SELECT id\nFROM users\nWHERE id > $1 -- skip low ids\nAND active = true;";
let result = clean_sql(sql);
assert!(result.contains("AND active = true"), "got: {result:?}");
assert!(!result.contains("--"), "got: {result:?}");
}
#[test]
fn test_clean_sql_preserves_double_dash_inside_single_quoted_string() {
let sql = "SELECT * FROM t WHERE label = 'a -- not a comment' AND id = $1;";
let result = clean_sql_oneline(sql);
assert!(result.contains("'a -- not a comment'"), "got: {result:?}");
assert!(result.contains("id = $1"), "got: {result:?}");
}
#[test]
fn test_clean_sql_preserves_double_dash_inside_double_quoted_identifier() {
let sql = "SELECT \"weird--col\" FROM t WHERE id = $1;";
let result = clean_sql_oneline(sql);
assert!(result.contains("\"weird--col\""), "got: {result:?}");
}
#[test]
fn test_clean_sql_preserves_double_dash_inside_dollar_quoted_string() {
let sql = "SELECT $$has -- inside$$ AS x FROM t WHERE id = $1;";
let result = clean_sql_oneline(sql);
assert!(result.contains("$$has -- inside$$"), "got: {result:?}");
}
#[test]
fn test_clean_sql_strips_nested_block_comments() {
let sql = "SELECT id /* outer /* inner */ still outer */ FROM t WHERE id = $1;";
let result = clean_sql_oneline(sql);
assert!(!result.contains("outer"), "got: {result:?}");
assert!(!result.contains("inner"), "got: {result:?}");
assert!(result.contains("SELECT id"), "got: {result:?}");
assert!(result.contains("FROM t WHERE id = $1"), "got: {result:?}");
}
#[test]
fn test_clean_sql_unterminated_block_comment_consumes_to_end_without_panicking() {
let sql = "SELECT id FROM t /* oops forgot to close";
let result = clean_sql_oneline(sql);
assert_eq!(result, "SELECT id FROM t");
}
#[test]
fn test_clean_sql_drops_lines_that_are_only_a_header_comment() {
let sql = "-- @name Foo\n-- @returns :many\nSELECT id FROM t WHERE id = $1;";
let result = clean_sql(sql);
assert_eq!(result, "SELECT id FROM t WHERE id = $1");
}
#[test]
fn test_rewrite_pg_placeholders_preserves_dollar_quoted_body() {
let sql =
"SELECT id FROM t WHERE body = $$literal -- not comment, ' quote, $1 not a placeholder$$ AND id = $2;";
let result = rewrite_pg_placeholders(sql, |n| format!("@p{n}"));
assert!(
result.contains("$$literal -- not comment, ' quote, $1 not a placeholder$$"),
"dollar-quoted body must survive untouched, got: {result:?}"
);
assert!(result.contains("id = @p2"), "got: {result:?}");
}
#[test]
fn test_rewrite_pg_placeholders_handles_escaped_single_quotes() {
let sql = "SELECT id FROM t WHERE label = 'it''s $1 not a placeholder' AND id = $2;";
let result = rewrite_pg_placeholders(sql, |n| format!("@p{n}"));
assert!(result.contains("'it''s $1 not a placeholder'"), "got: {result:?}");
assert!(result.contains("id = @p2"), "got: {result:?}");
}
#[test]
fn test_rewrite_pg_placeholders_preserves_jsonb_operators() {
let sql = "SELECT id FROM docs WHERE data ? 'mykey' AND tags ?| array['a','b'] AND tags ?& array['a','b'] AND id = $1;";
let result = rewrite_pg_placeholders(sql, |n| format!("@p{n}"));
assert!(result.contains("data ? 'mykey'"), "got: {result:?}");
assert!(result.contains("tags ?| array"), "got: {result:?}");
assert!(result.contains("tags ?& array"), "got: {result:?}");
assert!(result.contains("id = @p1"), "got: {result:?}");
}
#[test]
fn test_rewrite_pg_placeholders_preserves_jsonb_multi_char_operators_with_zero_dollar_placeholders() {
let sql = "SELECT * FROM docs WHERE data ? 'active' AND tags ?| ARRAY['a'] AND meta ?& ARRAY['b']";
let result = rewrite_pg_placeholders(sql, |n| format!("${n}"));
assert!(result.contains("tags ?| ARRAY['a']"), "got: {result:?}");
assert!(result.contains("meta ?& ARRAY['b']"), "got: {result:?}");
}
#[test]
fn test_rewrite_pg_placeholders_preserves_geometry_operators() {
let sql = "SELECT id FROM shapes WHERE line1 ?- line2 AND line1 ?-| line2 AND line1 ?|| line2 AND id = $1;";
let result = rewrite_pg_placeholders(sql, |n| format!("@p{n}"));
assert!(result.contains("line1 ?- line2"), "got: {result:?}");
assert!(result.contains("line1 ?-| line2"), "got: {result:?}");
assert!(result.contains("line1 ?|| line2"), "got: {result:?}");
assert!(result.contains("id = @p1"), "got: {result:?}");
}
#[test]
fn test_rewrite_pg_placeholders_preserves_operators_mixed_with_dollar_placeholder() {
let sql = "SELECT id FROM docs WHERE tags ?| ARRAY['a'] AND doc @? '$.a' AND id = $1;";
let result = rewrite_pg_placeholders(sql, |n| format!("@p{n}"));
assert!(result.contains("tags ?| ARRAY['a']"), "got: {result:?}");
assert!(result.contains("doc @? '$.a'"), "got: {result:?}");
assert!(result.contains("id = @p1"), "got: {result:?}");
}
#[test]
fn test_rewrite_pg_placeholders_number_boundary() {
let sql = "SELECT $1, $10, $1x";
let result = rewrite_pg_placeholders(sql, |n| format!("P{n}"));
assert_eq!(result, "SELECT P1, P10, P1x");
}
#[test]
fn test_rewrite_pg_placeholders_ignores_question_mark_inside_string_literal() {
let sql = "SELECT id FROM t WHERE note = 'is this a question?' AND age > ?;";
let result = rewrite_pg_placeholders(sql, |n| format!("${n}"));
assert!(result.contains("'is this a question?'"), "got: {result:?}");
assert!(result.contains("age > $1"), "got: {result:?}");
}
#[test]
fn test_clean_sql_handles_non_ascii_in_comment_and_string() {
let sql = "SELECT name -- 名前を取得 comment\nFROM t WHERE label = 'héllo wörld' AND id = $1;";
let result = clean_sql_oneline(sql);
assert!(!result.contains('名'), "got: {result:?}");
assert!(result.contains("'héllo wörld'"), "got: {result:?}");
assert!(result.contains("id = $1"), "got: {result:?}");
}
#[test]
fn test_clean_sql_handles_crlf_line_endings() {
let sql = "SELECT id\r\nFROM users\r\n-- a comment\r\nWHERE id = $1;";
let result = clean_sql(sql);
assert!(!result.contains('\r'), "got: {result:?}");
assert!(!result.contains("-- a comment"), "got: {result:?}");
assert_eq!(result, "SELECT id\nFROM users\nWHERE id = $1");
}
#[test]
fn test_rewrite_optional_params_handles_not_like() {
let sql = "SELECT id FROM t WHERE name NOT LIKE $1";
let params = vec![param("name", 1)];
let result = rewrite_optional_params(sql, &["name".to_string()], ¶ms);
assert_eq!(result, "SELECT id FROM t WHERE ($1 IS NULL OR name NOT LIKE $1)");
}
#[test]
fn test_bare_question_mark_is_never_a_placeholder_under_postgresql_even_with_zero_dollar_placeholders() {
let sql = "SELECT * FROM docs WHERE data ? 'active'";
let old_result = rewrite_pg_placeholders(sql, |n| format!("${n}"));
assert_eq!(
old_result, "SELECT * FROM docs WHERE data $1 'active'",
"pins the OLD heuristic's corruption of the JSONB operator"
);
let (new_result, occurrences) = rewrite_placeholders_indexed(sql, SqlDialect::PostgreSQL, |n| format!("${n}"));
assert_eq!(
new_result, sql,
"dialect-driven rewrite must leave the JSONB operator untouched"
);
assert!(
occurrences.is_empty(),
"no placeholder was ever present; got: {occurrences:?}"
);
}
#[test]
fn test_bare_question_mark_is_a_placeholder_under_mysql() {
let sql = "SELECT * FROM docs WHERE data ? 'active'";
let (result, occurrences) = rewrite_placeholders_indexed(sql, SqlDialect::MySQL, |n| format!("${n}"));
assert_eq!(result, "SELECT * FROM docs WHERE data $1 'active'");
assert_eq!(occurrences, vec![1]);
}
#[test]
fn test_rewrite_placeholders_indexed_returns_positions_in_textual_order_out_of_order() {
let (result, occurrences) =
rewrite_placeholders_indexed("WHERE b = $2 AND a = $1", SqlDialect::PostgreSQL, |_| "?".to_string());
assert_eq!(result, "WHERE b = ? AND a = ?");
assert_eq!(occurrences, vec![2, 1]);
}
#[test]
fn test_rewrite_placeholders_indexed_repeated_placeholder_reports_one_occurrence_per_use() {
let (result, occurrences) =
rewrite_placeholders_indexed("WHERE a = $1 OR b = $1", SqlDialect::PostgreSQL, |_| "?".to_string());
assert_eq!(result, "WHERE a = ? OR b = ?");
assert_eq!(occurrences, vec![1, 1]);
}
#[test]
fn test_mysql_backtick_identifier_is_not_corrupted_by_bare_question_mark() {
let sql = "SELECT `a?b` FROM t WHERE c = ?";
let (result, occurrences) = rewrite_placeholders_indexed(sql, SqlDialect::MySQL, |n| format!("[P{n}]"));
assert_eq!(result, "SELECT `a?b` FROM t WHERE c = [P1]");
assert_eq!(occurrences, vec![1]);
}
#[test]
fn test_mssql_bracket_identifier_is_not_corrupted_by_bare_question_mark() {
let sql = "SELECT [a?b] FROM t WHERE c = ?";
let (result, occurrences) = rewrite_placeholders_indexed(sql, SqlDialect::MsSql, |n| format!("[P{n}]"));
assert_eq!(result, "SELECT [a?b] FROM t WHERE c = [P1]");
assert_eq!(occurrences, vec![1]);
}
#[test]
fn test_clean_sql_dialect_mysql_backtick_preserves_double_dash_inside() {
let sql = "SELECT `a -- b` FROM t";
assert_eq!(
clean_sql(sql),
"SELECT `a",
"pins the OLD dialect-blind clean_sql's corruption"
);
assert_eq!(clean_sql_dialect(sql, SqlDialect::MySQL), sql);
}
#[test]
fn test_clean_sql_dialect_mssql_bracket_preserves_double_dash_inside() {
let sql = "SELECT [a -- b] FROM t";
assert_eq!(
clean_sql(sql),
"SELECT [a",
"pins the OLD dialect-blind clean_sql's corruption"
);
assert_eq!(clean_sql_dialect(sql, SqlDialect::MsSql), sql);
}
#[test]
fn test_clean_sql_dialect_mysql_hash_comment_is_stripped() {
let sql = "SELECT 1 # note\nFROM t";
assert_eq!(
clean_sql(sql),
"SELECT 1 # note\nFROM t",
"pins the OLD dialect-blind clean_sql leaving the # comment in place"
);
assert_eq!(clean_sql_dialect(sql, SqlDialect::MySQL), "SELECT 1 \nFROM t");
}
#[test]
fn test_clean_sql_oneline_dialect_mysql_hash_comment_is_stripped() {
let sql = "SELECT 1 # note\nFROM t";
assert_eq!(clean_sql_oneline_dialect(sql, SqlDialect::MySQL), "SELECT 1 FROM t");
}
#[test]
fn test_clean_sql_dialect_postgresql_hash_is_not_a_comment() {
let sql = "SELECT data #> '{a,b}' AS x FROM t";
assert_eq!(clean_sql_dialect(sql, SqlDialect::PostgreSQL), sql);
}
}