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_mssql;
48pub(crate) mod typescript_mysql2;
49pub(crate) mod typescript_oracledb;
50pub(crate) mod typescript_pg;
51pub(crate) mod typescript_postgres;
52pub(crate) mod typescript_snowflake;
53
54use scythe_backend::manifest::BackendManifest;
55use scythe_core::analyzer::AnalyzedParam;
56use scythe_core::errors::{ErrorCode, ScytheError};
57
58use crate::backend_trait::CodegenBackend;
59
60pub(crate) fn load_or_default_manifest(
63 override_path: &str,
64 default_toml: &str,
65) -> Result<BackendManifest, ScytheError> {
66 let path = std::path::Path::new(override_path);
67 if path.exists() {
68 scythe_backend::manifest::load_manifest(path)
69 .map_err(|e| ScytheError::new(ErrorCode::InternalError, format!("manifest: {e}")))
70 } else {
71 toml::from_str(default_toml).map_err(|e| ScytheError::new(ErrorCode::InternalError, format!("manifest: {e}")))
72 }
73}
74
75pub(crate) fn clean_sql(sql: &str) -> String {
78 sql.lines()
79 .filter(|line| !line.trim_start().starts_with("--"))
80 .collect::<Vec<_>>()
81 .join("\n")
82 .trim()
83 .trim_end_matches(';')
84 .trim()
85 .to_string()
86}
87
88pub(crate) fn clean_sql_oneline(sql: &str) -> String {
90 sql.lines()
91 .filter(|line| !line.trim_start().starts_with("--"))
92 .collect::<Vec<_>>()
93 .join(" ")
94 .trim()
95 .trim_end_matches(';')
96 .trim()
97 .to_string()
98}
99
100pub(crate) fn rewrite_optional_params(sql: &str, optional_params: &[String], params: &[AnalyzedParam]) -> String {
108 if optional_params.is_empty() {
109 return sql.to_string();
110 }
111
112 let mut result = sql.to_string();
113
114 for opt_name in optional_params {
115 let Some(param) = params.iter().find(|p| p.name == *opt_name) else {
116 continue;
117 };
118 let placeholder = format!("${}", param.position);
119
120 for op in &[">=", "<=", "<>", "!=", ">", "<", "=", "ILIKE", "ilike", "LIKE", "like"] {
122 result = rewrite_comparison(&result, &placeholder, op);
123 }
124 }
125
126 result
127}
128
129fn rewrite_comparison(sql: &str, placeholder: &str, op: &str) -> String {
132 let mut result = String::with_capacity(sql.len() + 32);
133 let chars: Vec<char> = sql.chars().collect();
134 let len = chars.len();
135 let mut i = 0;
136
137 while i < len {
138 if let Some((_start, col, end)) = try_match_col_op_ph(&chars, i, op, placeholder) {
140 result.push_str(&format!("({placeholder} IS NULL OR {col} {op} {placeholder})"));
141 i = end;
142 continue;
143 }
144
145 if let Some((end, col)) = try_match_ph_op_col(&chars, i, op, placeholder) {
147 result.push_str(&format!("({placeholder} IS NULL OR {col} {op} {placeholder})"));
148 i = end;
149 continue;
150 }
151
152 result.push(chars[i]);
153 i += 1;
154 }
155
156 result
157}
158
159fn try_match_col_op_ph(chars: &[char], i: usize, op: &str, placeholder: &str) -> Option<(usize, String, usize)> {
162 if !is_ident_char(chars[i]) {
164 return None;
165 }
166 if i > 0 && is_ident_char(chars[i - 1]) {
168 return None;
169 }
170
171 let ident_start = i;
173 let mut j = i;
174 while j < chars.len() && is_ident_char(chars[j]) {
175 j += 1;
176 }
177 let ident: String = chars[ident_start..j].iter().collect();
178
179 while j < chars.len() && chars[j].is_whitespace() {
181 j += 1;
182 }
183
184 let op_chars: Vec<char> = op.chars().collect();
186 if j + op_chars.len() > chars.len() {
187 return None;
188 }
189 for (k, oc) in op_chars.iter().enumerate() {
190 if chars[j + k] != *oc {
191 return None;
192 }
193 }
194 j += op_chars.len();
195
196 while j < chars.len() && chars[j].is_whitespace() {
198 j += 1;
199 }
200
201 let ph_chars: Vec<char> = placeholder.chars().collect();
203 if j + ph_chars.len() > chars.len() {
204 return None;
205 }
206 for (k, pc) in ph_chars.iter().enumerate() {
207 if chars[j + k] != *pc {
208 return None;
209 }
210 }
211 j += ph_chars.len();
212
213 if j < chars.len() && chars[j].is_ascii_digit() {
215 return None;
216 }
217
218 Some((i, ident, j))
219}
220
221fn try_match_ph_op_col(chars: &[char], i: usize, op: &str, placeholder: &str) -> Option<(usize, String)> {
224 let ph_chars: Vec<char> = placeholder.chars().collect();
225 if i + ph_chars.len() > chars.len() {
226 return None;
227 }
228
229 if i > 0 && (chars[i - 1] == '$' || chars[i - 1].is_ascii_digit()) {
231 return None;
232 }
233
234 for (k, pc) in ph_chars.iter().enumerate() {
236 if chars[i + k] != *pc {
237 return None;
238 }
239 }
240 let mut j = i + ph_chars.len();
241
242 if j < chars.len() && chars[j].is_ascii_digit() {
244 return None;
245 }
246
247 while j < chars.len() && chars[j].is_whitespace() {
249 j += 1;
250 }
251
252 let op_chars: Vec<char> = op.chars().collect();
254 if j + op_chars.len() > chars.len() {
255 return None;
256 }
257 for (k, oc) in op_chars.iter().enumerate() {
258 if chars[j + k] != *oc {
259 return None;
260 }
261 }
262 j += op_chars.len();
263
264 while j < chars.len() && chars[j].is_whitespace() {
266 j += 1;
267 }
268
269 if j >= chars.len() || !is_ident_char(chars[j]) {
271 return None;
272 }
273 let ident_start = j;
274 while j < chars.len() && is_ident_char(chars[j]) {
275 j += 1;
276 }
277 let ident: String = chars[ident_start..j].iter().collect();
278
279 if ident == "NULL" {
281 return None;
282 }
283
284 Some((j, ident))
285}
286
287pub(crate) fn clean_sql_with_optional(sql: &str, optional_params: &[String], params: &[AnalyzedParam]) -> String {
289 let cleaned = clean_sql(sql);
290 rewrite_optional_params(&cleaned, optional_params, params)
291}
292
293pub(crate) fn clean_sql_oneline_with_optional(
295 sql: &str,
296 optional_params: &[String],
297 params: &[AnalyzedParam],
298) -> String {
299 let cleaned = clean_sql_oneline(sql);
300 rewrite_optional_params(&cleaned, optional_params, params)
301}
302
303fn is_ident_char(c: char) -> bool {
304 c.is_alphanumeric() || c == '_' || c == '.'
305}
306
307pub(crate) fn rewrite_pg_placeholders(sql: &str, formatter: impl Fn(u32) -> String) -> String {
313 let mut result = String::with_capacity(sql.len());
314 let mut chars = sql.chars().peekable();
315 let mut positional_counter: u32 = 0;
316 while let Some(ch) = chars.next() {
317 if ch == '\'' {
318 result.push(ch);
319 while let Some(inner) = chars.next() {
320 result.push(inner);
321 if inner == '\'' {
322 if chars.peek() == Some(&'\'') {
323 result.push(chars.next().unwrap());
324 } else {
325 break;
326 }
327 }
328 }
329 } else if ch == '$' {
330 if chars.peek().is_some_and(|c| c.is_ascii_digit()) {
331 let mut num_str = String::new();
332 while chars.peek().is_some_and(|c| c.is_ascii_digit()) {
333 num_str.push(chars.next().unwrap());
334 }
335 let num: u32 = num_str.parse().unwrap_or(0);
336 result.push_str(&formatter(num));
337 } else {
338 result.push(ch);
339 }
340 } else if ch == '?' && !chars.peek().is_some_and(|c| c.is_ascii_digit()) {
341 positional_counter += 1;
342 result.push_str(&formatter(positional_counter));
343 } else {
344 result.push(ch);
345 }
346 }
347 result
348}
349
350pub fn get_backend(name: &str, engine: &str) -> Result<Box<dyn CodegenBackend>, ScytheError> {
355 let canonical_engine = normalize_engine(engine);
359 let backend: Box<dyn CodegenBackend> = match name {
360 "rust-sqlx" | "sqlx" | "rust" => Box::new(sqlx::SqlxBackend::new(canonical_engine)?),
361 "rust-tokio-postgres" | "tokio-postgres" => {
362 Box::new(tokio_postgres::TokioPostgresBackend::new(canonical_engine)?)
363 }
364 "python-psycopg3" | "python" => Box::new(python_psycopg3::PythonPsycopg3Backend::new(canonical_engine)?),
365 "python-asyncpg" => Box::new(python_asyncpg::PythonAsyncpgBackend::new(canonical_engine)?),
366 "python-aiomysql" => Box::new(python_aiomysql::PythonAiomysqlBackend::new(canonical_engine)?),
367 "python-aiosqlite" => Box::new(python_aiosqlite::PythonAiosqliteBackend::new(canonical_engine)?),
368 "python-duckdb" => Box::new(python_duckdb::PythonDuckdbBackend::new(canonical_engine)?),
369 "typescript-postgres" | "ts" | "typescript" => {
370 Box::new(typescript_postgres::TypescriptPostgresBackend::new(canonical_engine)?)
371 }
372 "typescript-pg" => Box::new(typescript_pg::TypescriptPgBackend::new(canonical_engine)?),
373 "typescript-mysql2" => Box::new(typescript_mysql2::TypescriptMysql2Backend::new(canonical_engine)?),
374 "typescript-better-sqlite3" => Box::new(typescript_better_sqlite3::TypescriptBetterSqlite3Backend::new(
375 canonical_engine,
376 )?),
377 "typescript-duckdb" => Box::new(typescript_duckdb::TypescriptDuckdbBackend::new(canonical_engine)?),
378 "go-database-sql" => Box::new(go_database_sql::GoDatabaseSqlBackend::new(canonical_engine)?),
379 "go-pgx" | "go" => Box::new(go_pgx::GoPgxBackend::new(canonical_engine)?),
380 "java-jdbc" | "java" => Box::new(java_jdbc::JavaJdbcBackend::new(canonical_engine)?),
381 "java-r2dbc" | "r2dbc-java" => Box::new(java_r2dbc::JavaR2dbcBackend::new(canonical_engine)?),
382 "kotlin-exposed" | "exposed" => Box::new(kotlin_exposed::KotlinExposedBackend::new(canonical_engine)?),
383 "kotlin-jdbc" | "kotlin" | "kt" => Box::new(kotlin_jdbc::KotlinJdbcBackend::new(canonical_engine)?),
384 "kotlin-r2dbc" | "r2dbc-kotlin" => Box::new(kotlin_r2dbc::KotlinR2dbcBackend::new(canonical_engine)?),
385 "csharp-npgsql" | "csharp" | "c#" | "dotnet" => {
386 Box::new(csharp_npgsql::CsharpNpgsqlBackend::new(canonical_engine)?)
387 }
388 "csharp-mysqlconnector" => Box::new(csharp_mysqlconnector::CsharpMysqlConnectorBackend::new(
389 canonical_engine,
390 )?),
391 "csharp-microsoft-sqlite" => Box::new(csharp_microsoft_sqlite::CsharpMicrosoftSqliteBackend::new(
392 canonical_engine,
393 )?),
394 "elixir-postgrex" | "elixir" | "ex" => Box::new(elixir_postgrex::ElixirPostgrexBackend::new(canonical_engine)?),
395 "elixir-ecto" | "ecto" => Box::new(elixir_ecto::ElixirEctoBackend::new(canonical_engine)?),
396 "elixir-myxql" => Box::new(elixir_myxql::ElixirMyxqlBackend::new(canonical_engine)?),
397 "elixir-exqlite" => Box::new(elixir_exqlite::ElixirExqliteBackend::new(canonical_engine)?),
398 "ruby-pg" | "ruby" | "rb" => Box::new(ruby_pg::RubyPgBackend::new(canonical_engine)?),
399 "ruby-mysql2" => Box::new(ruby_mysql2::RubyMysql2Backend::new(canonical_engine)?),
400 "ruby-sqlite3" => Box::new(ruby_sqlite3::RubySqlite3Backend::new(canonical_engine)?),
401 "ruby-trilogy" | "trilogy" => Box::new(ruby_trilogy::RubyTrilogyBackend::new(canonical_engine)?),
402 "php-pdo" | "php" => Box::new(php_pdo::PhpPdoBackend::new(canonical_engine)?),
403 "php-amphp" | "amphp" => Box::new(php_amphp::PhpAmphpBackend::new(canonical_engine)?),
404 "rust-tiberius" | "tiberius" => Box::new(rust_tiberius::RustTiberiusBackend::new(canonical_engine)?),
406 "python-pyodbc" | "pyodbc" => Box::new(python_pyodbc::PythonPyodbcBackend::new(canonical_engine)?),
407 "typescript-mssql" | "tedious" => Box::new(typescript_mssql::TypescriptMssqlBackend::new(canonical_engine)?),
408 "csharp-sqlclient" => Box::new(csharp_sqlclient::CsharpSqlClientBackend::new(canonical_engine)?),
409 "ruby-tiny-tds" | "tiny-tds" | "tiny_tds" => {
410 Box::new(ruby_tiny_tds::RubyTinyTdsBackend::new(canonical_engine)?)
411 }
412 "elixir-tds" | "tds" => Box::new(elixir_tds::ElixirTdsBackend::new(canonical_engine)?),
413 "rust-sibyl" | "sibyl" => Box::new(rust_sibyl::RustSibylBackend::new(canonical_engine)?),
415 "python-oracledb" | "oracledb" => Box::new(python_oracledb::PythonOracledbBackend::new(canonical_engine)?),
416 "typescript-oracledb" => Box::new(typescript_oracledb::TypescriptOracledbBackend::new(canonical_engine)?),
417 "go-godror" | "godror" => Box::new(go_godror::GoGodrorBackend::new(canonical_engine)?),
418 "csharp-oracle" => Box::new(csharp_oracle::CsharpOracleBackend::new(canonical_engine)?),
419 "ruby-oci8" | "oci8" => Box::new(ruby_oci8::RubyOci8Backend::new(canonical_engine)?),
420 "elixir-jamdb" | "jamdb" => Box::new(elixir_jamdb::ElixirJamdbBackend::new(canonical_engine)?),
421 "python-snowflake" => Box::new(python_snowflake::PythonSnowflakeBackend::new(canonical_engine)?),
423 "typescript-snowflake" => Box::new(typescript_snowflake::TypescriptSnowflakeBackend::new(canonical_engine)?),
424 "go-gosnowflake" | "gosnowflake" => Box::new(go_gosnowflake::GoGosnowflakeBackend::new(canonical_engine)?),
425 "csharp-snowflake" => Box::new(csharp_snowflake::CsharpSnowflakeBackend::new(canonical_engine)?),
426 _ => {
427 return Err(ScytheError::new(
428 ErrorCode::InternalError,
429 format!("unknown backend: {}", name),
430 ));
431 }
432 };
433
434 if !backend
436 .supported_engines()
437 .iter()
438 .any(|e| normalize_engine(e) == canonical_engine)
439 {
440 return Err(ScytheError::new(
441 ErrorCode::InternalError,
442 format!(
443 "backend '{}' does not support engine '{}'. Supported: {:?}",
444 name,
445 engine,
446 backend.supported_engines()
447 ),
448 ));
449 }
450
451 Ok(backend)
452}
453
454fn normalize_engine(engine: &str) -> &str {
456 match engine {
457 "postgresql" | "postgres" | "pg" | "cockroachdb" | "crdb" => "postgresql",
458 "mysql" => "mysql",
459 "mariadb" => "mariadb",
460 "sqlite" | "sqlite3" => "sqlite",
461 "duckdb" => "duckdb",
462 "mssql" | "sqlserver" | "tsql" => "mssql",
463 "oracle" => "oracle",
464 "snowflake" => "snowflake",
465 "redshift" => "redshift",
466 other => other,
467 }
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473
474 fn param(name: &str, position: i64) -> AnalyzedParam {
475 AnalyzedParam {
476 name: name.to_string(),
477 neutral_type: "string".to_string(),
478 nullable: true,
479 position,
480 }
481 }
482
483 #[test]
484 fn test_normalize_engine_cockroachdb() {
485 assert_eq!(normalize_engine("cockroachdb"), "postgresql");
486 assert_eq!(normalize_engine("crdb"), "postgresql");
487 }
488
489 #[test]
490 fn test_get_backend_cockroachdb_with_pg_backends() {
491 let pg_backends = [
493 "rust-sqlx",
494 "rust-tokio-postgres",
495 "python-psycopg3",
496 "python-asyncpg",
497 "typescript-postgres",
498 "typescript-pg",
499 "go-pgx",
500 "ruby-pg",
501 "elixir-postgrex",
502 "csharp-npgsql",
503 "php-pdo",
504 "php-amphp",
505 ];
506 for backend_name in &pg_backends {
507 let result = get_backend(backend_name, "cockroachdb");
508 assert!(
509 result.is_ok(),
510 "backend '{}' should accept cockroachdb engine, got: {:?}",
511 backend_name,
512 result.err()
513 );
514 }
515 }
516
517 #[test]
518 fn test_get_backend_crdb_alias() {
519 let result = get_backend("rust-sqlx", "crdb");
520 assert!(result.is_ok(), "rust-sqlx should accept 'crdb' engine alias");
521 }
522
523 #[test]
524 fn test_normalize_engine_duckdb() {
525 assert_eq!(normalize_engine("duckdb"), "duckdb");
526 }
527
528 #[test]
529 fn test_get_backend_duckdb_with_compatible_backends() {
530 let duckdb_backends = [
531 "python-duckdb",
532 "typescript-duckdb",
533 "go-database-sql",
534 "java-jdbc",
535 "kotlin-jdbc",
536 ];
537 for backend_name in &duckdb_backends {
538 let result = get_backend(backend_name, "duckdb");
539 assert!(
540 result.is_ok(),
541 "backend '{}' should accept duckdb engine, got: {:?}",
542 backend_name,
543 result.err()
544 );
545 }
546 }
547
548 #[test]
549 fn test_get_backend_duckdb_rejected_by_pg_only() {
550 let result = get_backend("rust-sqlx", "duckdb");
551 assert!(result.is_err(), "rust-sqlx should reject duckdb engine");
552 }
553
554 #[test]
555 fn test_rewrite_simple_equality() {
556 let sql = "SELECT * FROM users WHERE status = $1";
557 let params = vec![param("status", 1)];
558 let result = rewrite_optional_params(sql, &["status".to_string()], ¶ms);
559 assert_eq!(result, "SELECT * FROM users WHERE ($1 IS NULL OR status = $1)");
560 }
561
562 #[test]
563 fn test_rewrite_qualified_column() {
564 let sql = "SELECT * FROM users u WHERE u.status = $1";
565 let params = vec![param("status", 1)];
566 let result = rewrite_optional_params(sql, &["status".to_string()], ¶ms);
567 assert_eq!(result, "SELECT * FROM users u WHERE ($1 IS NULL OR u.status = $1)");
568 }
569
570 #[test]
571 fn test_rewrite_multiple_optional() {
572 let sql = "SELECT * FROM users WHERE status = $1 AND name = $2";
573 let params = vec![param("status", 1), param("name", 2)];
574 let result = rewrite_optional_params(sql, &["status".to_string(), "name".to_string()], ¶ms);
575 assert_eq!(
576 result,
577 "SELECT * FROM users WHERE ($1 IS NULL OR status = $1) AND ($2 IS NULL OR name = $2)"
578 );
579 }
580
581 #[test]
582 fn test_rewrite_mixed_optional_required() {
583 let sql = "SELECT * FROM users WHERE id = $1 AND status = $2";
584 let params = vec![param("id", 1), param("status", 2)];
585 let result = rewrite_optional_params(sql, &["status".to_string()], ¶ms);
586 assert_eq!(
587 result,
588 "SELECT * FROM users WHERE id = $1 AND ($2 IS NULL OR status = $2)"
589 );
590 }
591
592 #[test]
593 fn test_rewrite_like_operator() {
594 let sql = "SELECT * FROM users WHERE name LIKE $1";
595 let params = vec![param("name", 1)];
596 let result = rewrite_optional_params(sql, &["name".to_string()], ¶ms);
597 assert_eq!(result, "SELECT * FROM users WHERE ($1 IS NULL OR name LIKE $1)");
598 }
599
600 #[test]
601 fn test_rewrite_ilike_operator() {
602 let sql = "SELECT * FROM users WHERE name ILIKE $1";
603 let params = vec![param("name", 1)];
604 let result = rewrite_optional_params(sql, &["name".to_string()], ¶ms);
605 assert_eq!(result, "SELECT * FROM users WHERE ($1 IS NULL OR name ILIKE $1)");
606 }
607
608 #[test]
609 fn test_rewrite_comparison_operators() {
610 let sql = "SELECT * FROM users WHERE age >= $1";
611 let params = vec![param("age", 1)];
612 let result = rewrite_optional_params(sql, &["age".to_string()], ¶ms);
613 assert_eq!(result, "SELECT * FROM users WHERE ($1 IS NULL OR age >= $1)");
614 }
615
616 #[test]
617 fn test_rewrite_less_than() {
618 let sql = "SELECT * FROM users WHERE age < $1";
619 let params = vec![param("age", 1)];
620 let result = rewrite_optional_params(sql, &["age".to_string()], ¶ms);
621 assert_eq!(result, "SELECT * FROM users WHERE ($1 IS NULL OR age < $1)");
622 }
623
624 #[test]
625 fn test_no_rewrite_without_optional() {
626 let sql = "SELECT * FROM users WHERE status = $1";
627 let params = vec![param("status", 1)];
628 let result = rewrite_optional_params(sql, &[], ¶ms);
629 assert_eq!(result, sql);
630 }
631
632 #[test]
633 fn test_rewrite_not_equal() {
634 let sql = "SELECT * FROM users WHERE status <> $1";
635 let params = vec![param("status", 1)];
636 let result = rewrite_optional_params(sql, &["status".to_string()], ¶ms);
637 assert_eq!(result, "SELECT * FROM users WHERE ($1 IS NULL OR status <> $1)");
638 }
639
640 #[test]
641 fn test_rewrite_does_not_match_similar_placeholder() {
642 let sql = "SELECT * FROM users WHERE status = $10";
644 let params = vec![param("status", 1)];
645 let result = rewrite_optional_params(sql, &["status".to_string()], ¶ms);
646 assert_eq!(result, sql);
648 }
649
650 #[test]
651 fn test_normalize_engine_mariadb() {
652 assert_eq!(normalize_engine("mariadb"), "mariadb");
653 }
654
655 #[test]
656 fn test_get_backend_mariadb_with_mysql_backends() {
657 let mariadb_backends = [
658 "rust-sqlx",
659 "python-aiomysql",
660 "typescript-mysql2",
661 "go-database-sql",
662 "java-jdbc",
663 "java-r2dbc",
664 "kotlin-jdbc",
665 "kotlin-r2dbc",
666 "csharp-mysqlconnector",
667 "elixir-myxql",
668 "ruby-mysql2",
669 "ruby-trilogy",
670 "php-pdo",
671 "php-amphp",
672 ];
673 for backend_name in &mariadb_backends {
674 let result = get_backend(backend_name, "mariadb");
675 assert!(
676 result.is_ok(),
677 "backend '{}' should accept mariadb engine, got: {:?}",
678 backend_name,
679 result.err()
680 );
681 }
682 }
683}