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