use scythe_codegen::validation::{strict_mode_enabled, validate_with_tools};
use scythe_codegen::{CodegenBackend, 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;
const SCHEMA: &str = "CREATE TABLE notes (\
id SERIAL PRIMARY KEY, \
owner TEXT NOT NULL, \
body TEXT NOT NULL, \
tag TEXT NOT NULL\
);";
const BATCH_QUERY: &str = "-- @name AddNote\n-- @returns :batch\n\
INSERT INTO notes (owner, body, tag) VALUES ($1, $2, 'ticket $2 for $1');";
const SINGLE_PARAM_BATCH_QUERY: &str = "-- @name TagNotes\n-- @returns :batch\n\
UPDATE notes SET tag = $1 WHERE body = 'literal $1 here';";
const SINGLE_PARAM_BATCH_QUERY_WITH_FIELD_NAME_IN_LITERAL: &str = "-- @name TagNotes\n-- @returns :batch\n\
UPDATE notes SET tag = $1 WHERE body = 'literal ${tag} here';";
fn query_fn(backend_name: &str, sql: &str) -> String {
let backend: Box<dyn CodegenBackend> =
get_backend(backend_name, "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(sql, &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 batch_placeholders_inside_a_sql_string_literal_stay_literal() {
for backend_name in ["typescript-postgres", "javascript-postgres"] {
let generated = query_fn(backend_name, BATCH_QUERY);
assert!(
generated.contains("'ticket $2 for $1'"),
"{backend_name}: the SQL string literal must survive untouched (#219); got:\n{generated}"
);
assert!(
!generated.contains("'ticket ${item"),
"{backend_name}: a `$N` inside a literal became a live binding (#219); got:\n{generated}"
);
assert!(
generated.contains("VALUES (${item.owner}, ${item.body},"),
"{backend_name}: the actual placeholders must still bind; got:\n{generated}"
);
}
}
#[test]
fn a_single_parameter_batch_also_leaves_the_literal_alone() {
for backend_name in ["typescript-postgres", "javascript-postgres"] {
let generated = query_fn(backend_name, SINGLE_PARAM_BATCH_QUERY);
assert!(
generated.contains("'literal $1 here'"),
"{backend_name}: got:\n{generated}"
);
assert!(
generated.contains("SET tag = ${item}"),
"{backend_name}: got:\n{generated}"
);
}
}
#[test]
fn a_single_parameter_batch_leaves_a_literal_containing_the_field_name_placeholder_alone() {
for backend_name in ["typescript-postgres", "javascript-postgres"] {
let generated = query_fn(backend_name, SINGLE_PARAM_BATCH_QUERY_WITH_FIELD_NAME_IN_LITERAL);
assert!(
generated.contains(r"'literal \${tag} here'"),
"{backend_name}: the SQL literal's own text must survive untouched (#219 residual); got:\n{generated}"
);
assert!(
!generated.contains(r"'literal \${item} here'"),
"{backend_name}: the literal's escaped `${{tag}}` must not become a live `${{item}}` binding; \
got:\n{generated}"
);
assert!(
generated.contains("SET tag = ${item}"),
"{backend_name}: the actual placeholder must still bind to the batch item; got:\n{generated}"
);
}
}
#[test]
fn the_non_batch_paths_remain_literal_aware() {
let generated = query_fn(
"typescript-postgres",
"-- @name FindNote\n-- @returns :many\n\
SELECT id FROM notes WHERE tag = 'literal $1 here' AND owner = $1;",
);
assert!(generated.contains("'literal $1 here'"), "got:\n{generated}");
assert!(generated.contains("owner = ${owner}"), "got:\n{generated}");
}
#[test]
fn the_generated_batch_file_passes_tool_validation() {
for backend_name in ["typescript-postgres", "javascript-postgres"] {
let backend = get_backend(backend_name, "postgresql").unwrap();
let catalog = Catalog::from_ddl_with_dialect(&[SCHEMA], &SqlDialect::PostgreSQL).unwrap();
let parsed = parse_query_with_dialect(BATCH_QUERY, &SqlDialect::PostgreSQL).unwrap();
let analyzed = analyze(&catalog, &parsed).unwrap();
let code = generate_with_backend(&analyzed, &*backend).unwrap();
let mut file = backend.file_header_for_results(std::slice::from_ref(&code));
file.push('\n');
for text in [&code.row_struct, &code.query_fn].into_iter().flatten() {
file.push_str(text);
file.push_str("\n\n");
}
let validation = validate_with_tools(&file, backend_name);
assert!(
validation.errors().is_empty(),
"{backend_name}: {:#?}\n\nfile:\n{file}",
validation.errors()
);
if strict_mode_enabled() {
assert!(
validation.fully_checked(),
"{backend_name}: strict mode requires every checker to have run, got {:?} run / {:?} missing",
validation.tools_run(),
validation.missing_tools()
);
}
}
}