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"))
}
const EXPECTED_RAISE: &str = "raise ScytheNoRowsError(\"GetItem: no rows returned\")";
struct PythonBackend {
name: &'static str,
engine: &'static str,
execrows_return_line: &'static str,
}
const PYTHON_BACKENDS: &[PythonBackend] = &[
PythonBackend {
name: "python-psycopg3",
engine: "postgresql",
execrows_return_line: " return cur.rowcount",
},
PythonBackend {
name: "python-asyncpg",
engine: "postgresql",
execrows_return_line: " return int(result.split()[-1])",
},
PythonBackend {
name: "python-aiomysql",
engine: "mysql",
execrows_return_line: " return cur.rowcount",
},
PythonBackend {
name: "python-aiosqlite",
engine: "sqlite",
execrows_return_line: " return cursor.rowcount",
},
PythonBackend {
name: "python-duckdb",
engine: "duckdb",
execrows_return_line: " return row[0] if row else 0",
},
PythonBackend {
name: "python-pyodbc",
engine: "mssql",
execrows_return_line: " return cursor.rowcount",
},
PythonBackend {
name: "python-oracledb",
engine: "oracle",
execrows_return_line: " return cur.rowcount",
},
PythonBackend {
name: "python-snowflake",
engine: "snowflake",
execrows_return_line: " return cur.rowcount or 0",
},
];
#[test]
fn python_one_declares_non_optional_return_and_raises_on_missing_row() {
let mut failures = Vec::new();
for backend in PYTHON_BACKENDS {
let one_fn = query_fn_for(backend.name, backend.engine, QueryCommand::One);
if one_fn.contains("| None") {
failures.push(format!(
"{}: :one return annotation still contains `| None`; got:\n{one_fn}",
backend.name
));
}
if !one_fn.contains(EXPECTED_RAISE) {
failures.push(format!(
"{}: :one does not raise `{EXPECTED_RAISE}` on a missing row; got:\n{one_fn}",
backend.name
));
}
}
assert!(
failures.is_empty(),
"{} python backend(s) failed the :one contract:\n{}",
failures.len(),
failures.join("\n---\n")
);
}
#[test]
fn python_opt_keeps_optional_return_and_never_raises() {
let mut failures = Vec::new();
for backend in PYTHON_BACKENDS {
let opt_fn = query_fn_for(backend.name, backend.engine, QueryCommand::Opt);
if !opt_fn.contains("| None:") {
failures.push(format!(
"{}: :opt return annotation lost its `| None:` optional shape; got:\n{opt_fn}",
backend.name
));
}
if !opt_fn.contains("return None") {
failures.push(format!(
"{}: :opt no longer returns None on a missing row; got:\n{opt_fn}",
backend.name
));
}
if opt_fn.contains("ScytheNoRowsError") {
failures.push(format!(
"{}: :opt must never raise ScytheNoRowsError -- that is :one's contract; got:\n{opt_fn}",
backend.name
));
}
}
assert!(
failures.is_empty(),
"{} python backend(s) failed the :opt contract:\n{}",
failures.len(),
failures.join("\n---\n")
);
}
#[test]
fn python_one_and_opt_render_different_code() {
let mut failures = Vec::new();
for backend in PYTHON_BACKENDS {
let one_fn = query_fn_for(backend.name, backend.engine, QueryCommand::One);
let opt_fn = query_fn_for(backend.name, backend.engine, QueryCommand::Opt);
if one_fn == opt_fn {
failures.push(backend.name);
}
}
assert!(
failures.is_empty(),
"these python backends still render identical code for :one and :opt: {failures:?}"
);
}
#[test]
fn python_execrows_returns_the_expected_line() {
let mut failures = Vec::new();
for backend in PYTHON_BACKENDS {
let exec_fn = query_fn_for(backend.name, backend.engine, QueryCommand::ExecRows);
if !exec_fn.contains(backend.execrows_return_line) {
failures.push(format!(
"{}: expected :execrows to contain `{}`; got:\n{exec_fn}",
backend.name, backend.execrows_return_line
));
}
}
assert!(
failures.is_empty(),
"{} python backend(s) failed the :execrows return-line check:\n{}",
failures.len(),
failures.join("\n---\n")
);
}