use std::collections::BTreeSet;
use scythe_codegen::{generate_with_backend, get_backend};
use scythe_core::analyzer::analyze;
use scythe_core::catalog::Catalog;
use scythe_core::dialect::SqlDialect;
use scythe_core::parser::parse_query_with_dialect;
fn placeholder_names(source: &str) -> BTreeSet<String> {
let mut names = BTreeSet::new();
let mut rest = source;
while let Some(start) = rest.find("%(") {
rest = &rest[start + 2..];
let Some(end) = rest.find(")s") else { break };
names.insert(rest[..end].to_string());
rest = &rest[end + 2..];
}
names
}
fn dict_keys(source: &str) -> BTreeSet<String> {
let mut keys = BTreeSet::new();
let Some(open) = source.find(" {\"") else {
return keys;
};
let dict = &source[open..];
let Some(close) = dict.find("},") else {
return keys;
};
let mut rest = &dict[..close];
while let Some(start) = rest.find('"') {
rest = &rest[start + 1..];
let Some(end) = rest.find('"') else { break };
let key = &rest[..end];
rest = &rest[end + 1..];
if rest.starts_with(':') {
keys.insert(key.to_string());
}
}
keys
}
fn generate(query: &str) -> String {
const SCHEMA: &str = "CREATE TABLE items (id SERIAL PRIMARY KEY, class TEXT NOT NULL, label TEXT NOT NULL);";
let backend = get_backend("python-psycopg3", "postgresql").expect("backend must support postgresql");
let catalog = Catalog::from_ddl_with_dialect(&[SCHEMA], &SqlDialect::PostgreSQL).expect("schema must parse");
let parsed = parse_query_with_dialect(query, &SqlDialect::PostgreSQL).expect("query must parse");
let analyzed = analyze(&catalog, &parsed).expect("query must analyze");
let code = generate_with_backend(&analyzed, &*backend).expect("codegen must succeed");
code.query_fn.expect("expected a query fn")
}
#[test]
fn every_named_placeholder_is_a_key_of_the_params_dict() {
const QUERY: &str = "-- @name FindItem\n-- @returns :one\n\
SELECT id, class, label FROM items WHERE class = $1 AND label = $2;";
let query_fn = generate(QUERY);
let placeholders = placeholder_names(&query_fn);
let keys = dict_keys(&query_fn);
assert!(
!placeholders.is_empty(),
"no %(...)s placeholders were emitted:\n{query_fn}"
);
assert_eq!(
placeholders, keys,
"psycopg binds by name: a placeholder with no matching dict key raises \
\"query parameter missing\" at execute time:\n{query_fn}"
);
}
#[test]
fn a_reserved_word_param_keeps_the_placeholder_and_the_key_in_step() {
const QUERY: &str = "-- @name FindByClass\n-- @returns :one\n\
SELECT id, class, label FROM items WHERE class = $1;";
let query_fn = generate(QUERY);
assert!(
query_fn.contains("%(class_)s"),
"expected the placeholder to follow the mangled param name:\n{query_fn}"
);
assert!(
query_fn.contains("{\"class_\": class_}"),
"expected the dict to be keyed by the mangled param name:\n{query_fn}"
);
assert_eq!(placeholder_names(&query_fn), dict_keys(&query_fn), "got:\n{query_fn}");
}