1pub(crate) mod csharp_microsoft_sqlite;
2pub(crate) mod csharp_mysqlconnector;
3pub(crate) mod csharp_npgsql;
4pub(crate) mod csharp_oracle;
5pub(crate) mod csharp_snowflake;
6pub(crate) mod csharp_sqlclient;
7pub(crate) mod elixir_ecto;
8pub(crate) mod elixir_exqlite;
9pub(crate) mod elixir_jamdb;
10pub(crate) mod elixir_myxql;
11pub(crate) mod elixir_postgrex;
12pub(crate) mod elixir_tds;
13pub(crate) mod go_common;
14pub(crate) mod go_database_sql;
15pub(crate) mod go_godror;
16pub(crate) mod go_gosnowflake;
17pub(crate) mod go_pgx;
18pub(crate) mod java_jdbc;
19pub(crate) mod java_r2dbc;
20pub(crate) mod kotlin_exposed;
21pub(crate) mod kotlin_jdbc;
22pub(crate) mod kotlin_r2dbc;
23pub(crate) mod php_amphp;
24pub(crate) mod php_pdo;
25pub(crate) mod python_aiomysql;
26pub(crate) mod python_aiosqlite;
27pub(crate) mod python_asyncpg;
28pub(crate) mod python_common;
29pub(crate) mod python_duckdb;
30pub(crate) mod python_oracledb;
31pub(crate) mod python_psycopg3;
32pub(crate) mod python_pyodbc;
33pub(crate) mod python_snowflake;
34pub(crate) mod ruby_mysql2;
35pub(crate) mod ruby_oci8;
36pub(crate) mod ruby_pg;
37pub(crate) mod ruby_rbs;
38pub(crate) mod ruby_sqlite3;
39pub(crate) mod ruby_tiny_tds;
40pub(crate) mod ruby_trilogy;
41pub(crate) mod rust_sibyl;
42pub(crate) mod rust_tiberius;
43pub(crate) mod sqlx;
44pub(crate) mod tokio_postgres;
45pub(crate) mod typescript_better_sqlite3;
46pub(crate) mod typescript_common;
47pub(crate) mod typescript_duckdb;
48pub(crate) mod typescript_kysely;
49pub(crate) mod typescript_mssql;
50pub(crate) mod typescript_mysql2;
51pub(crate) mod typescript_node_sqlite;
52pub(crate) mod typescript_oracledb;
53pub(crate) mod typescript_pg;
54pub(crate) mod typescript_postgres;
55pub(crate) mod typescript_snowflake;
56pub(crate) mod typescript_wasm_sqlite;
57
58use scythe_backend::manifest::BackendManifest;
59use scythe_backend::naming::NamingConfig;
60use scythe_core::analyzer::AnalyzedParam;
61use scythe_core::errors::{ErrorCode, ScytheError};
62
63use crate::backend_trait::CodegenBackend;
64
65pub(crate) fn parse_manifest(manifest_toml: &str) -> Result<BackendManifest, ScytheError> {
71 toml::from_str(manifest_toml).map_err(|e| ScytheError::new(ErrorCode::InternalError, format!("manifest: {e}")))
72}
73
74pub(crate) fn apply_field_case_option(
91 naming: &mut NamingConfig,
92 backend_name: &str,
93 value: &str,
94) -> Result<(), ScytheError> {
95 match value {
96 "snake_case" | "camelCase" => {
97 naming.field_case = value.to_string();
98 Ok(())
99 }
100 other => Err(ScytheError::new(
101 ErrorCode::InvalidConfig,
102 format!("{backend_name}: invalid field_case '{other}' (expected 'snake_case' or 'camelCase')"),
103 )),
104 }
105}
106
107pub(crate) fn engine_supports_nested_aggregates(engine: &str) -> bool {
122 matches!(engine, "postgresql" | "postgres" | "pg")
123}
124
125pub(crate) fn clean_sql(sql: &str) -> String {
128 sql.lines()
129 .filter(|line| !line.trim_start().starts_with("--"))
130 .collect::<Vec<_>>()
131 .join("\n")
132 .trim()
133 .trim_end_matches(';')
134 .trim()
135 .to_string()
136}
137
138pub(crate) fn clean_sql_oneline(sql: &str) -> String {
140 sql.lines()
141 .filter(|line| !line.trim_start().starts_with("--"))
142 .collect::<Vec<_>>()
143 .join(" ")
144 .trim()
145 .trim_end_matches(';')
146 .trim()
147 .to_string()
148}
149
150pub(crate) fn rewrite_optional_params(sql: &str, optional_params: &[String], params: &[AnalyzedParam]) -> String {
158 if optional_params.is_empty() {
159 return sql.to_string();
160 }
161
162 let mut result = sql.to_string();
163
164 for opt_name in optional_params {
165 let Some(param) = params.iter().find(|p| p.name == *opt_name) else {
166 continue;
167 };
168 let placeholder = format!("${}", param.position);
169
170 for op in &[">=", "<=", "<>", "!=", ">", "<", "=", "ILIKE", "ilike", "LIKE", "like"] {
171 result = rewrite_comparison(&result, &placeholder, op);
172 }
173 }
174
175 result
176}
177
178fn rewrite_comparison(sql: &str, placeholder: &str, op: &str) -> String {
181 let mut result = String::with_capacity(sql.len() + 32);
182 let chars: Vec<char> = sql.chars().collect();
183 let len = chars.len();
184 let mut i = 0;
185
186 while i < len {
187 if let Some((_start, col, end)) = try_match_col_op_ph(&chars, i, op, placeholder) {
188 result.push_str(&format!("({placeholder} IS NULL OR {col} {op} {placeholder})"));
189 i = end;
190 continue;
191 }
192
193 if let Some((end, col)) = try_match_ph_op_col(&chars, i, op, placeholder) {
194 result.push_str(&format!("({placeholder} IS NULL OR {col} {op} {placeholder})"));
195 i = end;
196 continue;
197 }
198
199 result.push(chars[i]);
200 i += 1;
201 }
202
203 result
204}
205
206fn try_match_col_op_ph(chars: &[char], i: usize, op: &str, placeholder: &str) -> Option<(usize, String, usize)> {
209 if !is_ident_char(chars[i]) {
210 return None;
211 }
212 if i > 0 && is_ident_char(chars[i - 1]) {
213 return None;
214 }
215
216 let ident_start = i;
217 let mut j = i;
218 while j < chars.len() && is_ident_char(chars[j]) {
219 j += 1;
220 }
221 let ident: String = chars[ident_start..j].iter().collect();
222
223 while j < chars.len() && chars[j].is_whitespace() {
224 j += 1;
225 }
226
227 let op_chars: Vec<char> = op.chars().collect();
228 if j + op_chars.len() > chars.len() {
229 return None;
230 }
231 for (k, oc) in op_chars.iter().enumerate() {
232 if chars[j + k] != *oc {
233 return None;
234 }
235 }
236 j += op_chars.len();
237
238 while j < chars.len() && chars[j].is_whitespace() {
239 j += 1;
240 }
241
242 let ph_chars: Vec<char> = placeholder.chars().collect();
243 if j + ph_chars.len() > chars.len() {
244 return None;
245 }
246 for (k, pc) in ph_chars.iter().enumerate() {
247 if chars[j + k] != *pc {
248 return None;
249 }
250 }
251 j += ph_chars.len();
252
253 if j < chars.len() && chars[j].is_ascii_digit() {
254 return None;
255 }
256
257 Some((i, ident, j))
258}
259
260fn try_match_ph_op_col(chars: &[char], i: usize, op: &str, placeholder: &str) -> Option<(usize, String)> {
263 let ph_chars: Vec<char> = placeholder.chars().collect();
264 if i + ph_chars.len() > chars.len() {
265 return None;
266 }
267
268 if i > 0 && (chars[i - 1] == '$' || chars[i - 1].is_ascii_digit()) {
269 return None;
270 }
271
272 for (k, pc) in ph_chars.iter().enumerate() {
273 if chars[i + k] != *pc {
274 return None;
275 }
276 }
277 let mut j = i + ph_chars.len();
278
279 if j < chars.len() && chars[j].is_ascii_digit() {
280 return None;
281 }
282
283 while j < chars.len() && chars[j].is_whitespace() {
284 j += 1;
285 }
286
287 let op_chars: Vec<char> = op.chars().collect();
288 if j + op_chars.len() > chars.len() {
289 return None;
290 }
291 for (k, oc) in op_chars.iter().enumerate() {
292 if chars[j + k] != *oc {
293 return None;
294 }
295 }
296 j += op_chars.len();
297
298 while j < chars.len() && chars[j].is_whitespace() {
299 j += 1;
300 }
301
302 if j >= chars.len() || !is_ident_char(chars[j]) {
303 return None;
304 }
305 let ident_start = j;
306 while j < chars.len() && is_ident_char(chars[j]) {
307 j += 1;
308 }
309 let ident: String = chars[ident_start..j].iter().collect();
310
311 if ident == "NULL" {
312 return None;
313 }
314
315 Some((j, ident))
316}
317
318pub(crate) fn clean_sql_with_optional(sql: &str, optional_params: &[String], params: &[AnalyzedParam]) -> String {
320 let cleaned = clean_sql(sql);
321 rewrite_optional_params(&cleaned, optional_params, params)
322}
323
324pub(crate) fn clean_sql_oneline_with_optional(
326 sql: &str,
327 optional_params: &[String],
328 params: &[AnalyzedParam],
329) -> String {
330 let cleaned = clean_sql_oneline(sql);
331 rewrite_optional_params(&cleaned, optional_params, params)
332}
333
334fn is_ident_char(c: char) -> bool {
335 c.is_alphanumeric() || c == '_' || c == '.'
336}
337
338pub(crate) fn rewrite_pg_placeholders(sql: &str, formatter: impl Fn(u32) -> String) -> String {
344 let mut result = String::with_capacity(sql.len());
345 let mut chars = sql.chars().peekable();
346 let mut positional_counter: u32 = 0;
347 while let Some(ch) = chars.next() {
348 if ch == '\'' {
349 result.push(ch);
350 while let Some(inner) = chars.next() {
351 result.push(inner);
352 if inner == '\'' {
353 if chars.peek() == Some(&'\'') {
354 result.push(chars.next().unwrap());
355 } else {
356 break;
357 }
358 }
359 }
360 } else if ch == '$' {
361 if chars.peek().is_some_and(|c| c.is_ascii_digit()) {
362 let mut num_str = String::new();
363 while chars.peek().is_some_and(|c| c.is_ascii_digit()) {
364 num_str.push(chars.next().unwrap());
365 }
366 let num: u32 = num_str.parse().unwrap_or(0);
367 result.push_str(&formatter(num));
368 } else {
369 result.push(ch);
370 }
371 } else if ch == '?' && !chars.peek().is_some_and(|c| c.is_ascii_digit()) {
372 positional_counter += 1;
373 result.push_str(&formatter(positional_counter));
374 } else {
375 result.push(ch);
376 }
377 }
378 result
379}
380
381pub fn get_backend(name: &str, engine: &str) -> Result<Box<dyn CodegenBackend>, ScytheError> {
386 let canonical_engine = normalize_engine(engine);
387 let backend: Box<dyn CodegenBackend> = match name {
388 "rust-sqlx" | "sqlx" | "rust" => Box::new(sqlx::SqlxBackend::new(canonical_engine)?),
389 "rust-tokio-postgres" | "tokio-postgres" => {
390 Box::new(tokio_postgres::TokioPostgresBackend::new(canonical_engine)?)
391 }
392 "python-psycopg3" | "python" => Box::new(python_psycopg3::PythonPsycopg3Backend::new(canonical_engine)?),
393 "python-asyncpg" => Box::new(python_asyncpg::PythonAsyncpgBackend::new(canonical_engine)?),
394 "python-aiomysql" => Box::new(python_aiomysql::PythonAiomysqlBackend::new(canonical_engine)?),
395 "python-aiosqlite" => Box::new(python_aiosqlite::PythonAiosqliteBackend::new(canonical_engine)?),
396 "python-duckdb" => Box::new(python_duckdb::PythonDuckdbBackend::new(canonical_engine)?),
397 "typescript-postgres" | "ts" | "typescript" => {
398 Box::new(typescript_postgres::TypescriptPostgresBackend::new(canonical_engine)?)
399 }
400 "javascript-postgres" => Box::new(typescript_postgres::TypescriptPostgresBackend::new_js(
401 canonical_engine,
402 )?),
403 "typescript-pg" => Box::new(typescript_pg::TypescriptPgBackend::new(canonical_engine)?),
404 "javascript-pg" => Box::new(typescript_pg::TypescriptPgBackend::new_js(canonical_engine)?),
405 "typescript-mysql2" => Box::new(typescript_mysql2::TypescriptMysql2Backend::new(canonical_engine)?),
406 "javascript-mysql2" => Box::new(typescript_mysql2::TypescriptMysql2Backend::new_js(canonical_engine)?),
407 "typescript-better-sqlite3" => Box::new(typescript_better_sqlite3::TypescriptBetterSqlite3Backend::new(
408 canonical_engine,
409 )?),
410 "javascript-better-sqlite3" => Box::new(typescript_better_sqlite3::TypescriptBetterSqlite3Backend::new_js(
411 canonical_engine,
412 )?),
413 "typescript-duckdb" => Box::new(typescript_duckdb::TypescriptDuckdbBackend::new(canonical_engine)?),
414 "typescript-node-sqlite" => Box::new(typescript_node_sqlite::TypescriptNodeSqliteBackend::new(
415 canonical_engine,
416 )?),
417 "typescript-wasm-sqlite" => Box::new(typescript_wasm_sqlite::TypescriptWasmSqliteBackend::new(
418 canonical_engine,
419 )?),
420 "typescript-kysely" | "kysely" => Box::new(typescript_kysely::TypescriptKyselyBackend::new(canonical_engine)?),
421 "go-database-sql" => Box::new(go_database_sql::GoDatabaseSqlBackend::new(canonical_engine)?),
422 "go-pgx" | "go" => Box::new(go_pgx::GoPgxBackend::new(canonical_engine)?),
423 "java-jdbc" | "java" => Box::new(java_jdbc::JavaJdbcBackend::new(canonical_engine)?),
424 "java-r2dbc" | "r2dbc-java" => Box::new(java_r2dbc::JavaR2dbcBackend::new(canonical_engine)?),
425 "kotlin-exposed" | "exposed" => Box::new(kotlin_exposed::KotlinExposedBackend::new(canonical_engine)?),
426 "kotlin-jdbc" | "kotlin" | "kt" => Box::new(kotlin_jdbc::KotlinJdbcBackend::new(canonical_engine)?),
427 "kotlin-r2dbc" | "r2dbc-kotlin" => Box::new(kotlin_r2dbc::KotlinR2dbcBackend::new(canonical_engine)?),
428 "csharp-npgsql" | "csharp" | "c#" | "dotnet" => {
429 Box::new(csharp_npgsql::CsharpNpgsqlBackend::new(canonical_engine)?)
430 }
431 "csharp-mysqlconnector" => Box::new(csharp_mysqlconnector::CsharpMysqlConnectorBackend::new(
432 canonical_engine,
433 )?),
434 "csharp-microsoft-sqlite" => Box::new(csharp_microsoft_sqlite::CsharpMicrosoftSqliteBackend::new(
435 canonical_engine,
436 )?),
437 "elixir-postgrex" | "elixir" | "ex" => Box::new(elixir_postgrex::ElixirPostgrexBackend::new(canonical_engine)?),
438 "elixir-ecto" | "ecto" => Box::new(elixir_ecto::ElixirEctoBackend::new(canonical_engine)?),
439 "elixir-myxql" => Box::new(elixir_myxql::ElixirMyxqlBackend::new(canonical_engine)?),
440 "elixir-exqlite" => Box::new(elixir_exqlite::ElixirExqliteBackend::new(canonical_engine)?),
441 "ruby-pg" | "ruby" | "rb" => Box::new(ruby_pg::RubyPgBackend::new(canonical_engine)?),
442 "ruby-mysql2" => Box::new(ruby_mysql2::RubyMysql2Backend::new(canonical_engine)?),
443 "ruby-sqlite3" => Box::new(ruby_sqlite3::RubySqlite3Backend::new(canonical_engine)?),
444 "ruby-trilogy" | "trilogy" => Box::new(ruby_trilogy::RubyTrilogyBackend::new(canonical_engine)?),
445 "php-pdo" | "php" => Box::new(php_pdo::PhpPdoBackend::new(canonical_engine)?),
446 "php-amphp" | "amphp" => Box::new(php_amphp::PhpAmphpBackend::new(canonical_engine)?),
447 "rust-tiberius" | "tiberius" => Box::new(rust_tiberius::RustTiberiusBackend::new(canonical_engine)?),
448 "python-pyodbc" | "pyodbc" => Box::new(python_pyodbc::PythonPyodbcBackend::new(canonical_engine)?),
449 "typescript-mssql" | "tedious" => Box::new(typescript_mssql::TypescriptMssqlBackend::new(canonical_engine)?),
450 "csharp-sqlclient" => Box::new(csharp_sqlclient::CsharpSqlClientBackend::new(canonical_engine)?),
451 "ruby-tiny-tds" | "tiny-tds" | "tiny_tds" => {
452 Box::new(ruby_tiny_tds::RubyTinyTdsBackend::new(canonical_engine)?)
453 }
454 "elixir-tds" | "tds" => Box::new(elixir_tds::ElixirTdsBackend::new(canonical_engine)?),
455 "rust-sibyl" | "sibyl" => Box::new(rust_sibyl::RustSibylBackend::new(canonical_engine)?),
456 "python-oracledb" | "oracledb" => Box::new(python_oracledb::PythonOracledbBackend::new(canonical_engine)?),
457 "typescript-oracledb" => Box::new(typescript_oracledb::TypescriptOracledbBackend::new(canonical_engine)?),
458 "go-godror" | "godror" => Box::new(go_godror::GoGodrorBackend::new(canonical_engine)?),
459 "csharp-oracle" => Box::new(csharp_oracle::CsharpOracleBackend::new(canonical_engine)?),
460 "ruby-oci8" | "oci8" => Box::new(ruby_oci8::RubyOci8Backend::new(canonical_engine)?),
461 "elixir-jamdb" | "jamdb" => Box::new(elixir_jamdb::ElixirJamdbBackend::new(canonical_engine)?),
462 "python-snowflake" => Box::new(python_snowflake::PythonSnowflakeBackend::new(canonical_engine)?),
463 "typescript-snowflake" => Box::new(typescript_snowflake::TypescriptSnowflakeBackend::new(canonical_engine)?),
464 "go-gosnowflake" | "gosnowflake" => Box::new(go_gosnowflake::GoGosnowflakeBackend::new(canonical_engine)?),
465 "csharp-snowflake" => Box::new(csharp_snowflake::CsharpSnowflakeBackend::new(canonical_engine)?),
466 _ => {
467 return Err(ScytheError::new(
468 ErrorCode::InvalidConfig,
469 format!("unknown backend: {}", name),
470 ));
471 }
472 };
473
474 if !backend
475 .supported_engines()
476 .iter()
477 .any(|e| normalize_engine(e) == canonical_engine)
478 {
479 return Err(ScytheError::new(
480 ErrorCode::InvalidConfig,
481 format!(
482 "backend '{}' does not support engine '{}'. Supported: {:?}",
483 name,
484 engine,
485 backend.supported_engines()
486 ),
487 ));
488 }
489
490 Ok(backend)
491}
492
493pub fn normalize_engine(engine: &str) -> &str {
500 match engine {
501 "postgresql" | "postgres" | "pg" | "cockroachdb" | "crdb" => "postgresql",
502 "mysql" => "mysql",
503 "mariadb" => "mariadb",
504 "sqlite" | "sqlite3" => "sqlite",
505 "duckdb" => "duckdb",
506 "mssql" | "sqlserver" | "tsql" => "mssql",
507 "oracle" => "oracle",
508 "snowflake" => "snowflake",
509 "redshift" => "redshift",
510 other => other,
511 }
512}
513
514#[cfg(test)]
515mod tests {
516 use super::*;
517
518 fn param(name: &str, position: i64) -> AnalyzedParam {
519 AnalyzedParam {
520 name: name.to_string(),
521 neutral_type: "string".to_string(),
522 nullable: true,
523 position,
524 }
525 }
526
527 #[test]
539 fn test_no_backends_relative_manifest_path_literals() {
540 let root = concat!(env!("CARGO_MANIFEST_DIR"), "/src/backends");
541 let word: String = format!("backend{}", "s/");
542 let needle: String = format!("{}{}", '"', word);
543
544 let mut offenders = Vec::new();
545 collect_rs_files(std::path::Path::new(root), &mut offenders);
546
547 let offenders: Vec<_> = offenders
548 .into_iter()
549 .filter(|path| {
550 let contents = std::fs::read_to_string(path).expect("failed to read source file");
551 contents.contains(&needle)
552 })
553 .collect();
554
555 assert!(
556 offenders.is_empty(),
557 "found a working-directory-relative manifest path literal in: {offenders:?} (#82 regression \
558 -- manifest selection must be a pure function of (backend, engine), no filesystem lookup)"
559 );
560 }
561
562 fn collect_rs_files(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
564 let entries = std::fs::read_dir(dir).expect("failed to read backends dir");
565 for entry in entries {
566 let entry = entry.expect("failed to read dir entry");
567 let path = entry.path();
568 if path.is_dir() {
569 collect_rs_files(&path, out);
570 } else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
571 out.push(path);
572 }
573 }
574 }
575
576 #[test]
582 fn test_get_backend_rust_sqlx_mysql_has_no_postgresql_scalars() {
583 let backend = get_backend("rust-sqlx", "mysql").expect("rust-sqlx should support mysql");
584 let scalars = &backend.manifest().types.scalars;
585 assert_ne!(
586 scalars.get("uuid").map(String::as_str),
587 Some("uuid::Uuid"),
588 "rust-sqlx/mysql manifest should not carry the PostgreSQL-specific 'uuid' mapping"
589 );
590 assert_ne!(
591 scalars.get("inet").map(String::as_str),
592 Some("ipnetwork::IpNetwork"),
593 "rust-sqlx/mysql manifest should not carry the PostgreSQL-specific 'inet' mapping"
594 );
595 }
596
597 #[test]
598 fn test_get_backend_java_jdbc_sqlite_has_no_postgresql_scalars() {
599 let backend = get_backend("java-jdbc", "sqlite").expect("java-jdbc should support sqlite");
600 let scalars = &backend.manifest().types.scalars;
601 assert_ne!(
602 scalars.get("uuid").map(String::as_str),
603 Some("java.util.UUID"),
604 "java-jdbc/sqlite manifest should not carry the PostgreSQL-specific 'uuid' mapping"
605 );
606 assert_ne!(
607 scalars.get("decimal").map(String::as_str),
608 Some("java.math.BigDecimal"),
609 "java-jdbc/sqlite manifest should not carry the PostgreSQL-specific 'decimal' mapping"
610 );
611 }
612
613 #[test]
614 fn test_normalize_engine_cockroachdb() {
615 assert_eq!(normalize_engine("cockroachdb"), "postgresql");
616 assert_eq!(normalize_engine("crdb"), "postgresql");
617 }
618
619 #[test]
620 fn test_get_backend_cockroachdb_with_pg_backends() {
621 let pg_backends = [
622 "rust-sqlx",
623 "rust-tokio-postgres",
624 "python-psycopg3",
625 "python-asyncpg",
626 "typescript-postgres",
627 "typescript-pg",
628 "typescript-kysely",
629 "go-pgx",
630 "ruby-pg",
631 "elixir-postgrex",
632 "csharp-npgsql",
633 "php-pdo",
634 "php-amphp",
635 ];
636 for backend_name in &pg_backends {
637 let result = get_backend(backend_name, "cockroachdb");
638 assert!(
639 result.is_ok(),
640 "backend '{}' should accept cockroachdb engine, got: {:?}",
641 backend_name,
642 result.err()
643 );
644 }
645 }
646
647 #[test]
648 fn test_get_backend_crdb_alias() {
649 let result = get_backend("rust-sqlx", "crdb");
650 assert!(result.is_ok(), "rust-sqlx should accept 'crdb' engine alias");
651 }
652
653 #[test]
654 fn test_normalize_engine_duckdb() {
655 assert_eq!(normalize_engine("duckdb"), "duckdb");
656 }
657
658 #[test]
659 fn test_get_backend_duckdb_with_compatible_backends() {
660 let duckdb_backends = [
661 "python-duckdb",
662 "typescript-duckdb",
663 "go-database-sql",
664 "java-jdbc",
665 "kotlin-jdbc",
666 ];
667 for backend_name in &duckdb_backends {
668 let result = get_backend(backend_name, "duckdb");
669 assert!(
670 result.is_ok(),
671 "backend '{}' should accept duckdb engine, got: {:?}",
672 backend_name,
673 result.err()
674 );
675 }
676 }
677
678 #[test]
679 fn test_get_backend_duckdb_rejected_by_pg_only() {
680 let result = get_backend("rust-sqlx", "duckdb");
681 assert!(result.is_err(), "rust-sqlx should reject duckdb engine");
682 }
683
684 #[test]
685 fn test_rewrite_simple_equality() {
686 let sql = "SELECT * FROM users WHERE status = $1";
687 let params = vec![param("status", 1)];
688 let result = rewrite_optional_params(sql, &["status".to_string()], ¶ms);
689 assert_eq!(result, "SELECT * FROM users WHERE ($1 IS NULL OR status = $1)");
690 }
691
692 #[test]
693 fn test_rewrite_qualified_column() {
694 let sql = "SELECT * FROM users u WHERE u.status = $1";
695 let params = vec![param("status", 1)];
696 let result = rewrite_optional_params(sql, &["status".to_string()], ¶ms);
697 assert_eq!(result, "SELECT * FROM users u WHERE ($1 IS NULL OR u.status = $1)");
698 }
699
700 #[test]
701 fn test_rewrite_multiple_optional() {
702 let sql = "SELECT * FROM users WHERE status = $1 AND name = $2";
703 let params = vec![param("status", 1), param("name", 2)];
704 let result = rewrite_optional_params(sql, &["status".to_string(), "name".to_string()], ¶ms);
705 assert_eq!(
706 result,
707 "SELECT * FROM users WHERE ($1 IS NULL OR status = $1) AND ($2 IS NULL OR name = $2)"
708 );
709 }
710
711 #[test]
712 fn test_rewrite_mixed_optional_required() {
713 let sql = "SELECT * FROM users WHERE id = $1 AND status = $2";
714 let params = vec![param("id", 1), param("status", 2)];
715 let result = rewrite_optional_params(sql, &["status".to_string()], ¶ms);
716 assert_eq!(
717 result,
718 "SELECT * FROM users WHERE id = $1 AND ($2 IS NULL OR status = $2)"
719 );
720 }
721
722 #[test]
723 fn test_rewrite_like_operator() {
724 let sql = "SELECT * FROM users WHERE name LIKE $1";
725 let params = vec![param("name", 1)];
726 let result = rewrite_optional_params(sql, &["name".to_string()], ¶ms);
727 assert_eq!(result, "SELECT * FROM users WHERE ($1 IS NULL OR name LIKE $1)");
728 }
729
730 #[test]
731 fn test_rewrite_ilike_operator() {
732 let sql = "SELECT * FROM users WHERE name ILIKE $1";
733 let params = vec![param("name", 1)];
734 let result = rewrite_optional_params(sql, &["name".to_string()], ¶ms);
735 assert_eq!(result, "SELECT * FROM users WHERE ($1 IS NULL OR name ILIKE $1)");
736 }
737
738 #[test]
739 fn test_rewrite_comparison_operators() {
740 let sql = "SELECT * FROM users WHERE age >= $1";
741 let params = vec![param("age", 1)];
742 let result = rewrite_optional_params(sql, &["age".to_string()], ¶ms);
743 assert_eq!(result, "SELECT * FROM users WHERE ($1 IS NULL OR age >= $1)");
744 }
745
746 #[test]
747 fn test_rewrite_less_than() {
748 let sql = "SELECT * FROM users WHERE age < $1";
749 let params = vec![param("age", 1)];
750 let result = rewrite_optional_params(sql, &["age".to_string()], ¶ms);
751 assert_eq!(result, "SELECT * FROM users WHERE ($1 IS NULL OR age < $1)");
752 }
753
754 #[test]
755 fn test_no_rewrite_without_optional() {
756 let sql = "SELECT * FROM users WHERE status = $1";
757 let params = vec![param("status", 1)];
758 let result = rewrite_optional_params(sql, &[], ¶ms);
759 assert_eq!(result, sql);
760 }
761
762 #[test]
763 fn test_rewrite_not_equal() {
764 let sql = "SELECT * FROM users WHERE status <> $1";
765 let params = vec![param("status", 1)];
766 let result = rewrite_optional_params(sql, &["status".to_string()], ¶ms);
767 assert_eq!(result, "SELECT * FROM users WHERE ($1 IS NULL OR status <> $1)");
768 }
769
770 #[test]
771 fn test_rewrite_does_not_match_similar_placeholder() {
772 let sql = "SELECT * FROM users WHERE status = $10";
773 let params = vec![param("status", 1)];
774 let result = rewrite_optional_params(sql, &["status".to_string()], ¶ms);
775 assert_eq!(result, sql);
776 }
777
778 #[test]
779 fn test_normalize_engine_mariadb() {
780 assert_eq!(normalize_engine("mariadb"), "mariadb");
781 }
782
783 #[test]
784 fn test_get_backend_mariadb_with_mysql_backends() {
785 let mariadb_backends = [
786 "rust-sqlx",
787 "python-aiomysql",
788 "typescript-mysql2",
789 "go-database-sql",
790 "java-jdbc",
791 "java-r2dbc",
792 "kotlin-jdbc",
793 "kotlin-r2dbc",
794 "csharp-mysqlconnector",
795 "elixir-myxql",
796 "ruby-mysql2",
797 "ruby-trilogy",
798 "php-pdo",
799 "php-amphp",
800 ];
801 for backend_name in &mariadb_backends {
802 let result = get_backend(backend_name, "mariadb");
803 assert!(
804 result.is_ok(),
805 "backend '{}' should accept mariadb engine, got: {:?}",
806 backend_name,
807 result.err()
808 );
809 }
810 }
811}