use scythe_codegen::{generate_with_backend, get_backend};
use scythe_core::analyzer::{AnalyzedColumn, AnalyzedQuery};
use scythe_core::parser::QueryCommand;
fn one_column_query(command: QueryCommand) -> AnalyzedQuery {
AnalyzedQuery::build(|query| {
query.name = "GetItem".to_string();
query.command = command;
query.sql = "SELECT value FROM t WHERE id = 1".to_string();
query.columns = vec![AnalyzedColumn {
name: "value".to_string(),
neutral_type: "string".to_string(),
nullable: false,
sql_type: "string".to_string(),
..Default::default()
}];
})
}
fn query_fn_for(backend_name: &str, engine: &str, command: QueryCommand) -> String {
let backend = get_backend(backend_name, engine)
.unwrap_or_else(|error| panic!("{backend_name}/{engine}: backend must support engine: {error}"));
let query = one_column_query(command.clone());
let generated = generate_with_backend(&query, &*backend)
.unwrap_or_else(|error| panic!("{backend_name}/{engine}: codegen failed for {command:?}: {error}"));
generated
.query_fn
.unwrap_or_else(|| panic!("{backend_name}/{engine}: {command:?} produced no query fn"))
}
#[test]
fn rust_tiberius_opt_returns_option_and_does_not_panic_on_missing_row() {
let opt_fn = query_fn_for("rust-tiberius", "mssql", QueryCommand::Opt);
assert!(
opt_fn.contains("Result<Option<GetItemRow>, tiberius::error::Error>"),
"rust-tiberius :opt must return an Option-wrapped row type; got:\n{opt_fn}"
);
assert!(
!opt_fn.contains(".expect(\"expected one row\")"),
"rust-tiberius :opt must not use .expect() on a possibly-absent row -- that panics \
at runtime for the exact case :opt exists to handle (issue #197):\n{opt_fn}"
);
assert!(
opt_fn.contains(".transpose()"),
"rust-tiberius :opt is expected to map the optional row through `from_row` and fold it \
with `.transpose()` into Result<Option<Row>, Error>, so an absent row becomes `Ok(None)` \
rather than a panic; got:\n{opt_fn}"
);
}
#[test]
fn rust_tiberius_one_is_unchanged_by_the_opt_fix() {
let one_fn = query_fn_for("rust-tiberius", "mssql", QueryCommand::One);
assert!(
one_fn.contains("-> Result<GetItemRow, tiberius::error::Error>"),
"rust-tiberius :one must keep returning the bare row type, not Option-wrapped; got:\n{one_fn}"
);
assert!(
one_fn.contains(".expect(\"expected one row\")"),
"rust-tiberius :one's exactly-one-row behaviour must be unchanged by the :opt fix; \
got:\n{one_fn}"
);
}
#[test]
fn rust_tiberius_opt_and_one_generate_different_code() {
let one_fn = query_fn_for("rust-tiberius", "mssql", QueryCommand::One);
let opt_fn = query_fn_for("rust-tiberius", "mssql", QueryCommand::Opt);
assert_ne!(
one_fn, opt_fn,
"rust-tiberius must generate different code for :one and :opt -- identical code is the \
exact fold GH #197 named (\"opt silently treated as one\")"
);
}
const ALL_BACKEND_NAMES: &[&str] = &[
"rust-sqlx",
"rust-tokio-postgres",
"rust-tiberius",
"rust-sibyl",
"python-psycopg3",
"python-asyncpg",
"python-aiomysql",
"python-aiosqlite",
"python-duckdb",
"python-pyodbc",
"python-oracledb",
"python-snowflake",
"typescript-postgres",
"javascript-postgres",
"typescript-pg",
"javascript-pg",
"typescript-mysql2",
"javascript-mysql2",
"typescript-better-sqlite3",
"javascript-better-sqlite3",
"typescript-duckdb",
"javascript-duckdb",
"typescript-node-sqlite",
"javascript-node-sqlite",
"typescript-wasm-sqlite",
"javascript-wasm-sqlite",
"typescript-kysely",
"typescript-mssql",
"javascript-mssql",
"typescript-oracledb",
"javascript-oracledb",
"typescript-snowflake",
"javascript-snowflake",
"go-database-sql",
"go-pgx",
"go-godror",
"go-gosnowflake",
"java-jdbc",
"java-r2dbc",
"kotlin-exposed",
"kotlin-jdbc",
"kotlin-r2dbc",
"csharp-npgsql",
"csharp-mysqlconnector",
"csharp-microsoft-sqlite",
"csharp-sqlclient",
"csharp-oracle",
"csharp-snowflake",
"elixir-postgrex",
"elixir-ecto",
"elixir-myxql",
"elixir-exqlite",
"elixir-tds",
"elixir-jamdb",
"ruby-pg",
"ruby-mysql2",
"ruby-sqlite3",
"ruby-trilogy",
"ruby-tiny-tds",
"ruby-oci8",
"php-pdo",
"php-amphp",
];
const ENGINES: &[&str] = &[
"postgresql",
"mysql",
"sqlite",
"mssql",
"mariadb",
"duckdb",
"oracle",
"snowflake",
"redshift",
];
fn first_supported_engine(name: &str) -> &'static str {
for engine in ENGINES {
if get_backend(name, engine).is_ok() {
return engine;
}
}
panic!("no known engine works for backend '{name}' -- ALL_BACKEND_NAMES or ENGINES has gone stale");
}
struct BackendNote {
backend: &'static str,
reason: &'static str,
}
const KNOWN_UNDIFFERENTIATED_BACKENDS: &[BackendNote] = &[];
#[test]
fn one_and_opt_render_different_code_except_on_the_known_allowlist() {
let mut regressions = Vec::new();
let mut stale_entries = Vec::new();
for &name in ALL_BACKEND_NAMES {
let engine = first_supported_engine(name);
let one_fn = query_fn_for(name, engine, QueryCommand::One);
let opt_fn = query_fn_for(name, engine, QueryCommand::Opt);
let differs = one_fn != opt_fn;
let listed = KNOWN_UNDIFFERENTIATED_BACKENDS.iter().find(|n| n.backend == name);
match (differs, listed) {
(false, None) => regressions.push(name),
(true, Some(_)) => stale_entries.push(name),
(false, Some(note)) => eprintln!("expected fold, {name}: {}", note.reason),
(true, None) => {}
}
}
let mut failures = Vec::new();
for name in ®ressions {
failures.push(format!(
"REGRESSION: {name}: :one and :opt render identical code and {name} is not in \
KNOWN_UNDIFFERENTIATED_BACKENDS -- investigate before adding a line"
));
}
for name in &stale_entries {
failures.push(format!(
"STALE ALLOWLIST: {name}: listed in KNOWN_UNDIFFERENTIATED_BACKENDS but :one and \
:opt now render different code -- delete its entry"
));
}
assert!(
failures.is_empty(),
"{} of {} backends need attention:\n{}",
failures.len(),
ALL_BACKEND_NAMES.len(),
failures.join("\n")
);
}