#[allow(unused_imports)]
use scythe_codegen as _codegen;
#[allow(unused_imports)]
use syn as _syn;
#[allow(dead_code)]
fn normalize_whitespace(s: &str) -> String {
s.split_whitespace().collect::<Vec<_>>().join(" ")
}
#[test]
fn test_between_dates() {
let schema_sql = &[
"CREATE TABLE events (id SERIAL PRIMARY KEY, name TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW());",
];
let query_sql = "-- @name GetEventsBetween\n-- @returns :many\nSELECT id, name, created_at FROM events WHERE created_at BETWEEN $1 AND $2;";
let catalog = scythe_core::catalog::Catalog::from_ddl(schema_sql).unwrap();
let query = scythe_core::parser::parse_query(query_sql).unwrap();
let analyzed = scythe_core::analyzer::analyze(&catalog, &query).unwrap();
assert_eq!(analyzed.name, "GetEventsBetween", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
assert_eq!(analyzed.params.len(), 2, "param count");
assert_eq!(analyzed.params[0].name, "start", "param name");
assert_eq!(
analyzed.params[0].neutral_type, "datetime_tz",
"param neutral_type for start"
);
assert!(!analyzed.params[0].nullable, "param nullable for start");
assert_eq!(analyzed.params[1].name, "end", "param name");
assert_eq!(
analyzed.params[1].neutral_type, "datetime_tz",
"param neutral_type for end"
);
assert!(!analyzed.params[1].nullable, "param nullable for end");
assert_eq!(analyzed.columns.len(), 3, "column count");
assert_eq!(analyzed.columns[0].name, "id", "column name");
assert_eq!(analyzed.columns[0].neutral_type, "int32", "column neutral_type for id");
assert!(!analyzed.columns[0].nullable, "column nullable for id");
assert_eq!(analyzed.columns[1].name, "name", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "string",
"column neutral_type for name"
);
assert!(!analyzed.columns[1].nullable, "column nullable for name");
assert_eq!(analyzed.columns[2].name, "created_at", "column name");
assert_eq!(
analyzed.columns[2].neutral_type, "datetime_tz",
"column neutral_type for created_at"
);
assert!(!analyzed.columns[2].nullable, "column nullable for created_at");
let engine = "postgresql";
let all_backends = [
"rust-sqlx",
"rust-tokio-postgres",
"python-psycopg3",
"python-asyncpg",
"typescript-postgres",
"typescript-pg",
"typescript-kysely",
"go-pgx",
"java-jdbc",
"java-r2dbc",
"kotlin-exposed",
"kotlin-jdbc",
"kotlin-r2dbc",
"csharp-npgsql",
"elixir-postgrex",
"elixir-ecto",
"ruby-pg",
"php-pdo",
"php-amphp",
];
for backend_name in &all_backends {
let backend = match scythe_codegen::get_backend(backend_name, engine) {
Ok(b) => b,
Err(e) => panic!(
"backend {} failed to construct for engine {} in fixture {}: {}",
backend_name, engine, "between_dates", e
),
};
if let Ok(generated) = scythe_codegen::generate_with_backend(&analyzed, &*backend) {
let preamble = backend.file_preamble();
let header = backend.file_header();
let mut body = String::new();
if header.is_empty() {
body.push_str("#![allow(dead_code, unused_imports)]\n");
} else {
body.push_str(&header);
body.push('\n');
}
if let Some(ref s) = generated.enum_def {
body.push_str(s);
body.push('\n');
}
for def in &generated.nested_struct_defs {
body.push_str(&def.code);
body.push('\n');
}
if let Some(ref s) = generated.model_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.row_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.query_fn {
body.push_str(s);
body.push('\n');
}
let code = scythe_codegen::provenance::assemble_file(
&preamble,
&scythe_codegen::provenance::header_line(
&*backend,
env!("CARGO_PKG_VERSION"),
engine,
"sch1:0123456789abcdef",
"q1:fedcba9876543210",
),
&body,
);
if body.lines().count() > 1 {
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"between_dates"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"between_dates",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"between_dates"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"between_dates"
);
if *backend_name == "rust-sqlx" {
let actual = generated.row_struct.clone().unwrap_or_default();
assert!(
normalize_whitespace(&actual).contains(&normalize_whitespace("#[derive(Debug, Clone, sqlx::FromRow)]\npub struct GetEventsBetweenRow {\n pub id: i32,\n pub name: String,\n pub created_at: chrono::DateTime<chrono::Utc>,\n}")),
"backend {} row_struct mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "between_dates", "#[derive(Debug, Clone, sqlx::FromRow)]\npub struct GetEventsBetweenRow {\n pub id: i32,\n pub name: String,\n pub created_at: chrono::DateTime<chrono::Utc>,\n}", actual
);
let actual = generated.query_fn.clone().unwrap_or_default();
assert!(
normalize_whitespace(&actual).contains(&normalize_whitespace("pub async fn get_events_between(pool: &sqlx::PgPool, start: &chrono::DateTime<chrono::Utc>, end: &chrono::DateTime<chrono::Utc>) -> Result<Vec<GetEventsBetweenRow>, sqlx::Error> {\n sqlx::query_as!(GetEventsBetweenRow, \"SELECT id, name, created_at FROM events WHERE created_at BETWEEN $1 AND $2\", start, end)\n .fetch_all(pool)\n .await\n}")),
"backend {} query_fn mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "between_dates", "pub async fn get_events_between(pool: &sqlx::PgPool, start: &chrono::DateTime<chrono::Utc>, end: &chrono::DateTime<chrono::Utc>) -> Result<Vec<GetEventsBetweenRow>, sqlx::Error> {\n sqlx::query_as!(GetEventsBetweenRow, \"SELECT id, name, created_at FROM events WHERE created_at BETWEEN $1 AND $2\", start, end)\n .fetch_all(pool)\n .await\n}", actual
);
}
}
}
}
#[test]
fn test_coalesce_param_type() {
let schema_sql = &["CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL);"];
let query_sql = "-- @name GetUserByCoalesceName\n-- @returns :many\nSELECT id, name, email FROM users WHERE name = COALESCE($1, name);";
let catalog = scythe_core::catalog::Catalog::from_ddl(schema_sql).unwrap();
let query = scythe_core::parser::parse_query(query_sql).unwrap();
let analyzed = scythe_core::analyzer::analyze(&catalog, &query).unwrap();
assert_eq!(analyzed.name, "GetUserByCoalesceName", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
assert_eq!(analyzed.params.len(), 1, "param count");
assert_eq!(analyzed.params[0].name, "name", "param name");
assert_eq!(analyzed.params[0].neutral_type, "string", "param neutral_type for name");
assert!(!analyzed.params[0].nullable, "param nullable for name");
assert_eq!(analyzed.columns.len(), 3, "column count");
assert_eq!(analyzed.columns[0].name, "id", "column name");
assert_eq!(analyzed.columns[0].neutral_type, "int32", "column neutral_type for id");
assert!(!analyzed.columns[0].nullable, "column nullable for id");
assert_eq!(analyzed.columns[1].name, "name", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "string",
"column neutral_type for name"
);
assert!(!analyzed.columns[1].nullable, "column nullable for name");
assert_eq!(analyzed.columns[2].name, "email", "column name");
assert_eq!(
analyzed.columns[2].neutral_type, "string",
"column neutral_type for email"
);
assert!(!analyzed.columns[2].nullable, "column nullable for email");
let engine = "postgresql";
let all_backends = [
"rust-sqlx",
"rust-tokio-postgres",
"python-psycopg3",
"python-asyncpg",
"typescript-postgres",
"typescript-pg",
"typescript-kysely",
"go-pgx",
"java-jdbc",
"java-r2dbc",
"kotlin-exposed",
"kotlin-jdbc",
"kotlin-r2dbc",
"csharp-npgsql",
"elixir-postgrex",
"elixir-ecto",
"ruby-pg",
"php-pdo",
"php-amphp",
];
for backend_name in &all_backends {
let backend = match scythe_codegen::get_backend(backend_name, engine) {
Ok(b) => b,
Err(e) => panic!(
"backend {} failed to construct for engine {} in fixture {}: {}",
backend_name, engine, "coalesce_param_type", e
),
};
if let Ok(generated) = scythe_codegen::generate_with_backend(&analyzed, &*backend) {
let preamble = backend.file_preamble();
let header = backend.file_header();
let mut body = String::new();
if header.is_empty() {
body.push_str("#![allow(dead_code, unused_imports)]\n");
} else {
body.push_str(&header);
body.push('\n');
}
if let Some(ref s) = generated.enum_def {
body.push_str(s);
body.push('\n');
}
for def in &generated.nested_struct_defs {
body.push_str(&def.code);
body.push('\n');
}
if let Some(ref s) = generated.model_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.row_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.query_fn {
body.push_str(s);
body.push('\n');
}
let code = scythe_codegen::provenance::assemble_file(
&preamble,
&scythe_codegen::provenance::header_line(
&*backend,
env!("CARGO_PKG_VERSION"),
engine,
"sch1:0123456789abcdef",
"q1:fedcba9876543210",
),
&body,
);
if body.lines().count() > 1 {
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"coalesce_param_type"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"coalesce_param_type",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"coalesce_param_type"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"coalesce_param_type"
);
if *backend_name == "rust-sqlx" {
let actual = generated.row_struct.clone().unwrap_or_default();
assert!(
normalize_whitespace(&actual).contains(&normalize_whitespace("#[derive(Debug, Clone, sqlx::FromRow)]\npub struct GetUserByCoalesceNameRow {\n pub id: i32,\n pub name: String,\n pub email: String,\n}")),
"backend {} row_struct mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "coalesce_param_type", "#[derive(Debug, Clone, sqlx::FromRow)]\npub struct GetUserByCoalesceNameRow {\n pub id: i32,\n pub name: String,\n pub email: String,\n}", actual
);
}
}
}
}
#[test]
fn test_param_as_limit_offset() {
let schema_sql = &["CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL);"];
let query_sql =
"-- @name ListUsersPaginated\n-- @returns :many\nSELECT id, name FROM users ORDER BY id LIMIT $1 OFFSET $2";
let catalog = scythe_core::catalog::Catalog::from_ddl(schema_sql).unwrap();
let query = scythe_core::parser::parse_query(query_sql).unwrap();
let analyzed = scythe_core::analyzer::analyze(&catalog, &query).unwrap();
assert_eq!(analyzed.name, "ListUsersPaginated", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
assert_eq!(analyzed.params.len(), 2, "param count");
assert_eq!(analyzed.params[0].name, "limit_val", "param name");
assert_eq!(
analyzed.params[0].neutral_type, "int64",
"param neutral_type for limit_val"
);
assert!(!analyzed.params[0].nullable, "param nullable for limit_val");
assert_eq!(analyzed.params[0].position, 1, "param position for limit_val");
assert_eq!(analyzed.params[1].name, "offset_val", "param name");
assert_eq!(
analyzed.params[1].neutral_type, "int64",
"param neutral_type for offset_val"
);
assert!(!analyzed.params[1].nullable, "param nullable for offset_val");
assert_eq!(analyzed.params[1].position, 2, "param position for offset_val");
assert_eq!(analyzed.columns.len(), 2, "column count");
assert_eq!(analyzed.columns[0].name, "id", "column name");
assert_eq!(analyzed.columns[0].neutral_type, "int32", "column neutral_type for id");
assert!(!analyzed.columns[0].nullable, "column nullable for id");
assert_eq!(analyzed.columns[1].name, "name", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "string",
"column neutral_type for name"
);
assert!(!analyzed.columns[1].nullable, "column nullable for name");
let engine = "postgresql";
let all_backends = [
"rust-sqlx",
"rust-tokio-postgres",
"python-psycopg3",
"python-asyncpg",
"typescript-postgres",
"typescript-pg",
"typescript-kysely",
"go-pgx",
"java-jdbc",
"java-r2dbc",
"kotlin-exposed",
"kotlin-jdbc",
"kotlin-r2dbc",
"csharp-npgsql",
"elixir-postgrex",
"elixir-ecto",
"ruby-pg",
"php-pdo",
"php-amphp",
];
for backend_name in &all_backends {
let backend = match scythe_codegen::get_backend(backend_name, engine) {
Ok(b) => b,
Err(e) => panic!(
"backend {} failed to construct for engine {} in fixture {}: {}",
backend_name, engine, "param_as_limit_offset", e
),
};
if let Ok(generated) = scythe_codegen::generate_with_backend(&analyzed, &*backend) {
let preamble = backend.file_preamble();
let header = backend.file_header();
let mut body = String::new();
if header.is_empty() {
body.push_str("#![allow(dead_code, unused_imports)]\n");
} else {
body.push_str(&header);
body.push('\n');
}
if let Some(ref s) = generated.enum_def {
body.push_str(s);
body.push('\n');
}
for def in &generated.nested_struct_defs {
body.push_str(&def.code);
body.push('\n');
}
if let Some(ref s) = generated.model_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.row_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.query_fn {
body.push_str(s);
body.push('\n');
}
let code = scythe_codegen::provenance::assemble_file(
&preamble,
&scythe_codegen::provenance::header_line(
&*backend,
env!("CARGO_PKG_VERSION"),
engine,
"sch1:0123456789abcdef",
"q1:fedcba9876543210",
),
&body,
);
if body.lines().count() > 1 {
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"param_as_limit_offset"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"param_as_limit_offset",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"param_as_limit_offset"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"param_as_limit_offset"
);
}
}
}
#[test]
fn test_param_in_array_literal() {
let schema_sql = &["CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL);"];
let query_sql =
"-- @name GetUsersByIds\n-- @returns :many\nSELECT id, name FROM users WHERE id = ANY(ARRAY[$1, $2, $3])";
let catalog = scythe_core::catalog::Catalog::from_ddl(schema_sql).unwrap();
let query = scythe_core::parser::parse_query(query_sql).unwrap();
let analyzed = scythe_core::analyzer::analyze(&catalog, &query).unwrap();
assert_eq!(analyzed.name, "GetUsersByIds", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
assert_eq!(analyzed.params.len(), 3, "param count");
assert_eq!(analyzed.params[0].name, "id1", "param name");
assert_eq!(analyzed.params[0].neutral_type, "int32", "param neutral_type for id1");
assert!(!analyzed.params[0].nullable, "param nullable for id1");
assert_eq!(analyzed.params[0].position, 1, "param position for id1");
assert_eq!(analyzed.params[1].name, "id2", "param name");
assert_eq!(analyzed.params[1].neutral_type, "int32", "param neutral_type for id2");
assert!(!analyzed.params[1].nullable, "param nullable for id2");
assert_eq!(analyzed.params[1].position, 2, "param position for id2");
assert_eq!(analyzed.params[2].name, "id3", "param name");
assert_eq!(analyzed.params[2].neutral_type, "int32", "param neutral_type for id3");
assert!(!analyzed.params[2].nullable, "param nullable for id3");
assert_eq!(analyzed.params[2].position, 3, "param position for id3");
assert_eq!(analyzed.columns.len(), 2, "column count");
assert_eq!(analyzed.columns[0].name, "id", "column name");
assert_eq!(analyzed.columns[0].neutral_type, "int32", "column neutral_type for id");
assert!(!analyzed.columns[0].nullable, "column nullable for id");
assert_eq!(analyzed.columns[1].name, "name", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "string",
"column neutral_type for name"
);
assert!(!analyzed.columns[1].nullable, "column nullable for name");
let engine = "postgresql";
let all_backends = [
"rust-sqlx",
"rust-tokio-postgres",
"python-psycopg3",
"python-asyncpg",
"typescript-postgres",
"typescript-pg",
"typescript-kysely",
"go-pgx",
"java-jdbc",
"java-r2dbc",
"kotlin-exposed",
"kotlin-jdbc",
"kotlin-r2dbc",
"csharp-npgsql",
"elixir-postgrex",
"elixir-ecto",
"ruby-pg",
"php-pdo",
"php-amphp",
];
for backend_name in &all_backends {
let backend = match scythe_codegen::get_backend(backend_name, engine) {
Ok(b) => b,
Err(e) => panic!(
"backend {} failed to construct for engine {} in fixture {}: {}",
backend_name, engine, "param_in_array_literal", e
),
};
if let Ok(generated) = scythe_codegen::generate_with_backend(&analyzed, &*backend) {
let preamble = backend.file_preamble();
let header = backend.file_header();
let mut body = String::new();
if header.is_empty() {
body.push_str("#![allow(dead_code, unused_imports)]\n");
} else {
body.push_str(&header);
body.push('\n');
}
if let Some(ref s) = generated.enum_def {
body.push_str(s);
body.push('\n');
}
for def in &generated.nested_struct_defs {
body.push_str(&def.code);
body.push('\n');
}
if let Some(ref s) = generated.model_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.row_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.query_fn {
body.push_str(s);
body.push('\n');
}
let code = scythe_codegen::provenance::assemble_file(
&preamble,
&scythe_codegen::provenance::header_line(
&*backend,
env!("CARGO_PKG_VERSION"),
engine,
"sch1:0123456789abcdef",
"q1:fedcba9876543210",
),
&body,
);
if body.lines().count() > 1 {
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"param_in_array_literal"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"param_in_array_literal",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"param_in_array_literal"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"param_in_array_literal"
);
}
}
}
#[test]
fn test_param_in_case_when() {
let schema_sql = &["CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL);"];
let query_sql = "-- @name GetUserDisplay\n-- @returns :many\nSELECT id, CASE WHEN $1 THEN name ELSE email END AS display FROM users";
let catalog = scythe_core::catalog::Catalog::from_ddl(schema_sql).unwrap();
let query = scythe_core::parser::parse_query(query_sql).unwrap();
let analyzed = scythe_core::analyzer::analyze(&catalog, &query).unwrap();
assert_eq!(analyzed.name, "GetUserDisplay", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
assert_eq!(analyzed.params.len(), 1, "param count");
assert_eq!(analyzed.params[0].name, "flag", "param name");
assert_eq!(analyzed.params[0].neutral_type, "bool", "param neutral_type for flag");
assert!(!analyzed.params[0].nullable, "param nullable for flag");
assert_eq!(analyzed.params[0].position, 1, "param position for flag");
assert_eq!(analyzed.columns.len(), 2, "column count");
assert_eq!(analyzed.columns[0].name, "id", "column name");
assert_eq!(analyzed.columns[0].neutral_type, "int32", "column neutral_type for id");
assert!(!analyzed.columns[0].nullable, "column nullable for id");
assert_eq!(analyzed.columns[1].name, "display", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "string",
"column neutral_type for display"
);
assert!(!analyzed.columns[1].nullable, "column nullable for display");
let engine = "postgresql";
let all_backends = [
"rust-sqlx",
"rust-tokio-postgres",
"python-psycopg3",
"python-asyncpg",
"typescript-postgres",
"typescript-pg",
"typescript-kysely",
"go-pgx",
"java-jdbc",
"java-r2dbc",
"kotlin-exposed",
"kotlin-jdbc",
"kotlin-r2dbc",
"csharp-npgsql",
"elixir-postgrex",
"elixir-ecto",
"ruby-pg",
"php-pdo",
"php-amphp",
];
for backend_name in &all_backends {
let backend = match scythe_codegen::get_backend(backend_name, engine) {
Ok(b) => b,
Err(e) => panic!(
"backend {} failed to construct for engine {} in fixture {}: {}",
backend_name, engine, "param_in_case_when", e
),
};
if let Ok(generated) = scythe_codegen::generate_with_backend(&analyzed, &*backend) {
let preamble = backend.file_preamble();
let header = backend.file_header();
let mut body = String::new();
if header.is_empty() {
body.push_str("#![allow(dead_code, unused_imports)]\n");
} else {
body.push_str(&header);
body.push('\n');
}
if let Some(ref s) = generated.enum_def {
body.push_str(s);
body.push('\n');
}
for def in &generated.nested_struct_defs {
body.push_str(&def.code);
body.push('\n');
}
if let Some(ref s) = generated.model_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.row_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.query_fn {
body.push_str(s);
body.push('\n');
}
let code = scythe_codegen::provenance::assemble_file(
&preamble,
&scythe_codegen::provenance::header_line(
&*backend,
env!("CARGO_PKG_VERSION"),
engine,
"sch1:0123456789abcdef",
"q1:fedcba9876543210",
),
&body,
);
if body.lines().count() > 1 {
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"param_in_case_when"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"param_in_case_when",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"param_in_case_when"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"param_in_case_when"
);
}
}
}
#[test]
fn test_param_in_interval_arithmetic() {
let schema_sql =
&["CREATE TABLE events (id SERIAL PRIMARY KEY, name TEXT NOT NULL, occurred_at TIMESTAMPTZ NOT NULL);"];
let query_sql = "-- @name GetRecentEvents\n-- @returns :many\nSELECT id, name FROM events WHERE occurred_at > NOW() - $1::interval";
let catalog = scythe_core::catalog::Catalog::from_ddl(schema_sql).unwrap();
let query = scythe_core::parser::parse_query(query_sql).unwrap();
let analyzed = scythe_core::analyzer::analyze(&catalog, &query).unwrap();
assert_eq!(analyzed.name, "GetRecentEvents", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
assert_eq!(analyzed.params.len(), 1, "param count");
assert_eq!(analyzed.params[0].name, "duration", "param name");
assert_eq!(
analyzed.params[0].neutral_type, "interval",
"param neutral_type for duration"
);
assert!(!analyzed.params[0].nullable, "param nullable for duration");
assert_eq!(analyzed.params[0].position, 1, "param position for duration");
assert_eq!(analyzed.columns.len(), 2, "column count");
assert_eq!(analyzed.columns[0].name, "id", "column name");
assert_eq!(analyzed.columns[0].neutral_type, "int32", "column neutral_type for id");
assert!(!analyzed.columns[0].nullable, "column nullable for id");
assert_eq!(analyzed.columns[1].name, "name", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "string",
"column neutral_type for name"
);
assert!(!analyzed.columns[1].nullable, "column nullable for name");
let engine = "postgresql";
let all_backends = [
"rust-sqlx",
"rust-tokio-postgres",
"python-psycopg3",
"python-asyncpg",
"typescript-postgres",
"typescript-pg",
"typescript-kysely",
"go-pgx",
"java-jdbc",
"java-r2dbc",
"kotlin-exposed",
"kotlin-jdbc",
"kotlin-r2dbc",
"csharp-npgsql",
"elixir-postgrex",
"elixir-ecto",
"ruby-pg",
"php-pdo",
"php-amphp",
];
for backend_name in &all_backends {
let backend = match scythe_codegen::get_backend(backend_name, engine) {
Ok(b) => b,
Err(e) => panic!(
"backend {} failed to construct for engine {} in fixture {}: {}",
backend_name, engine, "param_in_interval_arithmetic", e
),
};
if let Ok(generated) = scythe_codegen::generate_with_backend(&analyzed, &*backend) {
let preamble = backend.file_preamble();
let header = backend.file_header();
let mut body = String::new();
if header.is_empty() {
body.push_str("#![allow(dead_code, unused_imports)]\n");
} else {
body.push_str(&header);
body.push('\n');
}
if let Some(ref s) = generated.enum_def {
body.push_str(s);
body.push('\n');
}
for def in &generated.nested_struct_defs {
body.push_str(&def.code);
body.push('\n');
}
if let Some(ref s) = generated.model_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.row_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.query_fn {
body.push_str(s);
body.push('\n');
}
let code = scythe_codegen::provenance::assemble_file(
&preamble,
&scythe_codegen::provenance::header_line(
&*backend,
env!("CARGO_PKG_VERSION"),
engine,
"sch1:0123456789abcdef",
"q1:fedcba9876543210",
),
&body,
);
if body.lines().count() > 1 {
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"param_in_interval_arithmetic"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"param_in_interval_arithmetic",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"param_in_interval_arithmetic"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"param_in_interval_arithmetic"
);
}
}
}
#[test]
fn test_param_in_simple_case() {
let schema_sql = &["CREATE TABLE users (id SERIAL PRIMARY KEY, status TEXT NOT NULL)"];
let query_sql = "-- @name GetUserStatusLabel\n-- @returns :many\nSELECT CASE status WHEN $1 THEN 'match' ELSE 'other' END AS m FROM users";
let catalog = scythe_core::catalog::Catalog::from_ddl(schema_sql).unwrap();
let query = scythe_core::parser::parse_query(query_sql).unwrap();
let analyzed = scythe_core::analyzer::analyze(&catalog, &query).unwrap();
assert_eq!(analyzed.name, "GetUserStatusLabel", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
assert_eq!(analyzed.params.len(), 1, "param count");
assert_eq!(analyzed.params[0].name, "status", "param name");
assert_eq!(
analyzed.params[0].neutral_type, "string",
"param neutral_type for status"
);
assert!(!analyzed.params[0].nullable, "param nullable for status");
assert_eq!(analyzed.params[0].position, 1, "param position for status");
assert_eq!(analyzed.columns.len(), 1, "column count");
assert_eq!(analyzed.columns[0].name, "m", "column name");
assert_eq!(analyzed.columns[0].neutral_type, "string", "column neutral_type for m");
assert!(!analyzed.columns[0].nullable, "column nullable for m");
let engine = "postgresql";
let all_backends = [
"rust-sqlx",
"rust-tokio-postgres",
"python-psycopg3",
"python-asyncpg",
"typescript-postgres",
"typescript-pg",
"typescript-kysely",
"go-pgx",
"java-jdbc",
"java-r2dbc",
"kotlin-exposed",
"kotlin-jdbc",
"kotlin-r2dbc",
"csharp-npgsql",
"elixir-postgrex",
"elixir-ecto",
"ruby-pg",
"php-pdo",
"php-amphp",
];
for backend_name in &all_backends {
let backend = match scythe_codegen::get_backend(backend_name, engine) {
Ok(b) => b,
Err(e) => panic!(
"backend {} failed to construct for engine {} in fixture {}: {}",
backend_name, engine, "param_in_simple_case", e
),
};
if let Ok(generated) = scythe_codegen::generate_with_backend(&analyzed, &*backend) {
let preamble = backend.file_preamble();
let header = backend.file_header();
let mut body = String::new();
if header.is_empty() {
body.push_str("#![allow(dead_code, unused_imports)]\n");
} else {
body.push_str(&header);
body.push('\n');
}
if let Some(ref s) = generated.enum_def {
body.push_str(s);
body.push('\n');
}
for def in &generated.nested_struct_defs {
body.push_str(&def.code);
body.push('\n');
}
if let Some(ref s) = generated.model_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.row_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.query_fn {
body.push_str(s);
body.push('\n');
}
let code = scythe_codegen::provenance::assemble_file(
&preamble,
&scythe_codegen::provenance::header_line(
&*backend,
env!("CARGO_PKG_VERSION"),
engine,
"sch1:0123456789abcdef",
"q1:fedcba9876543210",
),
&body,
);
if body.lines().count() > 1 {
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"param_in_simple_case"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"param_in_simple_case",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"param_in_simple_case"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"param_in_simple_case"
);
}
}
}
#[test]
fn test_cast_to_int() {
let schema_sql = &["CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL);"];
let query_sql = "-- @name GetUserByCastInt\n-- @returns :one\nSELECT id, name FROM users WHERE id = $1::integer;";
let catalog = scythe_core::catalog::Catalog::from_ddl(schema_sql).unwrap();
let query = scythe_core::parser::parse_query(query_sql).unwrap();
let analyzed = scythe_core::analyzer::analyze(&catalog, &query).unwrap();
assert_eq!(analyzed.name, "GetUserByCastInt", "query name");
assert_eq!(analyzed.command.to_string(), "one", "query command");
assert_eq!(analyzed.params.len(), 1, "param count");
assert_eq!(analyzed.params[0].name, "id", "param name");
assert_eq!(analyzed.params[0].neutral_type, "int32", "param neutral_type for id");
assert!(!analyzed.params[0].nullable, "param nullable for id");
assert_eq!(analyzed.columns.len(), 2, "column count");
assert_eq!(analyzed.columns[0].name, "id", "column name");
assert_eq!(analyzed.columns[0].neutral_type, "int32", "column neutral_type for id");
assert!(!analyzed.columns[0].nullable, "column nullable for id");
assert_eq!(analyzed.columns[1].name, "name", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "string",
"column neutral_type for name"
);
assert!(!analyzed.columns[1].nullable, "column nullable for name");
let engine = "postgresql";
let all_backends = [
"rust-sqlx",
"rust-tokio-postgres",
"python-psycopg3",
"python-asyncpg",
"typescript-postgres",
"typescript-pg",
"typescript-kysely",
"go-pgx",
"java-jdbc",
"java-r2dbc",
"kotlin-exposed",
"kotlin-jdbc",
"kotlin-r2dbc",
"csharp-npgsql",
"elixir-postgrex",
"elixir-ecto",
"ruby-pg",
"php-pdo",
"php-amphp",
];
for backend_name in &all_backends {
let backend = match scythe_codegen::get_backend(backend_name, engine) {
Ok(b) => b,
Err(e) => panic!(
"backend {} failed to construct for engine {} in fixture {}: {}",
backend_name, engine, "cast_to_int", e
),
};
if let Ok(generated) = scythe_codegen::generate_with_backend(&analyzed, &*backend) {
let preamble = backend.file_preamble();
let header = backend.file_header();
let mut body = String::new();
if header.is_empty() {
body.push_str("#![allow(dead_code, unused_imports)]\n");
} else {
body.push_str(&header);
body.push('\n');
}
if let Some(ref s) = generated.enum_def {
body.push_str(s);
body.push('\n');
}
for def in &generated.nested_struct_defs {
body.push_str(&def.code);
body.push('\n');
}
if let Some(ref s) = generated.model_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.row_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.query_fn {
body.push_str(s);
body.push('\n');
}
let code = scythe_codegen::provenance::assemble_file(
&preamble,
&scythe_codegen::provenance::header_line(
&*backend,
env!("CARGO_PKG_VERSION"),
engine,
"sch1:0123456789abcdef",
"q1:fedcba9876543210",
),
&body,
);
if body.lines().count() > 1 {
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"cast_to_int"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"cast_to_int",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"cast_to_int"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"cast_to_int"
);
if *backend_name == "rust-sqlx" {
let actual = generated.row_struct.clone().unwrap_or_default();
assert!(
normalize_whitespace(&actual).contains(&normalize_whitespace("#[derive(Debug, Clone, sqlx::FromRow)]\npub struct GetUserByCastIntRow {\n pub id: i32,\n pub name: String,\n}")),
"backend {} row_struct mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "cast_to_int", "#[derive(Debug, Clone, sqlx::FromRow)]\npub struct GetUserByCastIntRow {\n pub id: i32,\n pub name: String,\n}", actual
);
}
}
}
}
#[test]
fn test_cast_to_uuid() {
let schema_sql = &["CREATE TABLE users (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL);"];
let query_sql = "-- @name GetUserByCastUuid\n-- @returns :one\nSELECT id, name FROM users WHERE id = $1::uuid;";
let catalog = scythe_core::catalog::Catalog::from_ddl(schema_sql).unwrap();
let query = scythe_core::parser::parse_query(query_sql).unwrap();
let analyzed = scythe_core::analyzer::analyze(&catalog, &query).unwrap();
assert_eq!(analyzed.name, "GetUserByCastUuid", "query name");
assert_eq!(analyzed.command.to_string(), "one", "query command");
assert_eq!(analyzed.params.len(), 1, "param count");
assert_eq!(analyzed.params[0].name, "id", "param name");
assert_eq!(analyzed.params[0].neutral_type, "uuid", "param neutral_type for id");
assert!(!analyzed.params[0].nullable, "param nullable for id");
assert_eq!(analyzed.columns.len(), 2, "column count");
assert_eq!(analyzed.columns[0].name, "id", "column name");
assert_eq!(analyzed.columns[0].neutral_type, "uuid", "column neutral_type for id");
assert!(!analyzed.columns[0].nullable, "column nullable for id");
assert_eq!(analyzed.columns[1].name, "name", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "string",
"column neutral_type for name"
);
assert!(!analyzed.columns[1].nullable, "column nullable for name");
let engine = "postgresql";
let all_backends = [
"rust-sqlx",
"rust-tokio-postgres",
"python-psycopg3",
"python-asyncpg",
"typescript-postgres",
"typescript-pg",
"typescript-kysely",
"go-pgx",
"java-jdbc",
"java-r2dbc",
"kotlin-exposed",
"kotlin-jdbc",
"kotlin-r2dbc",
"csharp-npgsql",
"elixir-postgrex",
"elixir-ecto",
"ruby-pg",
"php-pdo",
"php-amphp",
];
for backend_name in &all_backends {
let backend = match scythe_codegen::get_backend(backend_name, engine) {
Ok(b) => b,
Err(e) => panic!(
"backend {} failed to construct for engine {} in fixture {}: {}",
backend_name, engine, "cast_to_uuid", e
),
};
if let Ok(generated) = scythe_codegen::generate_with_backend(&analyzed, &*backend) {
let preamble = backend.file_preamble();
let header = backend.file_header();
let mut body = String::new();
if header.is_empty() {
body.push_str("#![allow(dead_code, unused_imports)]\n");
} else {
body.push_str(&header);
body.push('\n');
}
if let Some(ref s) = generated.enum_def {
body.push_str(s);
body.push('\n');
}
for def in &generated.nested_struct_defs {
body.push_str(&def.code);
body.push('\n');
}
if let Some(ref s) = generated.model_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.row_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.query_fn {
body.push_str(s);
body.push('\n');
}
let code = scythe_codegen::provenance::assemble_file(
&preamble,
&scythe_codegen::provenance::header_line(
&*backend,
env!("CARGO_PKG_VERSION"),
engine,
"sch1:0123456789abcdef",
"q1:fedcba9876543210",
),
&body,
);
if body.lines().count() > 1 {
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"cast_to_uuid"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"cast_to_uuid",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"cast_to_uuid"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"cast_to_uuid"
);
if *backend_name == "rust-sqlx" {
let actual = generated.row_struct.clone().unwrap_or_default();
assert!(
normalize_whitespace(&actual).contains(&normalize_whitespace("#[derive(Debug, Clone, sqlx::FromRow)]\npub struct GetUserByCastUuidRow {\n pub id: uuid::Uuid,\n pub name: String,\n}")),
"backend {} row_struct mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "cast_to_uuid", "#[derive(Debug, Clone, sqlx::FromRow)]\npub struct GetUserByCastUuidRow {\n pub id: uuid::Uuid,\n pub name: String,\n}", actual
);
}
}
}
}
#[test]
fn test_any_array_param() {
let schema_sql = &["CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL);"];
let query_sql = "-- @name GetUsersByIds\n-- @returns :many\nSELECT id, name, email FROM users WHERE id = ANY($1);";
let catalog = scythe_core::catalog::Catalog::from_ddl(schema_sql).unwrap();
let query = scythe_core::parser::parse_query(query_sql).unwrap();
let analyzed = scythe_core::analyzer::analyze(&catalog, &query).unwrap();
assert_eq!(analyzed.name, "GetUsersByIds", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
assert_eq!(analyzed.params.len(), 1, "param count");
assert_eq!(analyzed.params[0].name, "ids", "param name");
assert_eq!(
analyzed.params[0].neutral_type, "array<int32>",
"param neutral_type for ids"
);
assert!(!analyzed.params[0].nullable, "param nullable for ids");
assert_eq!(analyzed.columns.len(), 3, "column count");
assert_eq!(analyzed.columns[0].name, "id", "column name");
assert_eq!(analyzed.columns[0].neutral_type, "int32", "column neutral_type for id");
assert!(!analyzed.columns[0].nullable, "column nullable for id");
assert_eq!(analyzed.columns[1].name, "name", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "string",
"column neutral_type for name"
);
assert!(!analyzed.columns[1].nullable, "column nullable for name");
assert_eq!(analyzed.columns[2].name, "email", "column name");
assert_eq!(
analyzed.columns[2].neutral_type, "string",
"column neutral_type for email"
);
assert!(!analyzed.columns[2].nullable, "column nullable for email");
let engine = "postgresql";
let all_backends = [
"rust-sqlx",
"rust-tokio-postgres",
"python-psycopg3",
"python-asyncpg",
"typescript-postgres",
"typescript-pg",
"typescript-kysely",
"go-pgx",
"java-jdbc",
"java-r2dbc",
"kotlin-exposed",
"kotlin-jdbc",
"kotlin-r2dbc",
"csharp-npgsql",
"elixir-postgrex",
"elixir-ecto",
"ruby-pg",
"php-pdo",
"php-amphp",
];
for backend_name in &all_backends {
let backend = match scythe_codegen::get_backend(backend_name, engine) {
Ok(b) => b,
Err(e) => panic!(
"backend {} failed to construct for engine {} in fixture {}: {}",
backend_name, engine, "any_array_param", e
),
};
if let Ok(generated) = scythe_codegen::generate_with_backend(&analyzed, &*backend) {
let preamble = backend.file_preamble();
let header = backend.file_header();
let mut body = String::new();
if header.is_empty() {
body.push_str("#![allow(dead_code, unused_imports)]\n");
} else {
body.push_str(&header);
body.push('\n');
}
if let Some(ref s) = generated.enum_def {
body.push_str(s);
body.push('\n');
}
for def in &generated.nested_struct_defs {
body.push_str(&def.code);
body.push('\n');
}
if let Some(ref s) = generated.model_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.row_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.query_fn {
body.push_str(s);
body.push('\n');
}
let code = scythe_codegen::provenance::assemble_file(
&preamble,
&scythe_codegen::provenance::header_line(
&*backend,
env!("CARGO_PKG_VERSION"),
engine,
"sch1:0123456789abcdef",
"q1:fedcba9876543210",
),
&body,
);
if body.lines().count() > 1 {
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"any_array_param"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"any_array_param",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"any_array_param"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"any_array_param"
);
if *backend_name == "rust-sqlx" {
let actual = generated.row_struct.clone().unwrap_or_default();
assert!(
normalize_whitespace(&actual).contains(&normalize_whitespace("#[derive(Debug, Clone, sqlx::FromRow)]\npub struct GetUsersByIdsRow {\n pub id: i32,\n pub name: String,\n pub email: String,\n}")),
"backend {} row_struct mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "any_array_param", "#[derive(Debug, Clone, sqlx::FromRow)]\npub struct GetUsersByIdsRow {\n pub id: i32,\n pub name: String,\n pub email: String,\n}", actual
);
let actual = generated.query_fn.clone().unwrap_or_default();
assert!(
normalize_whitespace(&actual).contains(&normalize_whitespace("pub async fn get_users_by_ids(pool: &sqlx::PgPool, ids: &[i32]) -> Result<Vec<GetUsersByIdsRow>, sqlx::Error> {\n sqlx::query_as!(GetUsersByIdsRow, \"SELECT id, name, email FROM users WHERE id = ANY($1)\", ids)\n .fetch_all(pool)\n .await\n}")),
"backend {} query_fn mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "any_array_param", "pub async fn get_users_by_ids(pool: &sqlx::PgPool, ids: &[i32]) -> Result<Vec<GetUsersByIdsRow>, sqlx::Error> {\n sqlx::query_as!(GetUsersByIdsRow, \"SELECT id, name, email FROM users WHERE id = ANY($1)\", ids)\n .fetch_all(pool)\n .await\n}", actual
);
}
}
}
}
#[test]
fn test_insert_all_columns() {
let schema_sql = &[
"CREATE TABLE products (id SERIAL PRIMARY KEY, name TEXT NOT NULL, price NUMERIC(10,2) NOT NULL, quantity INTEGER NOT NULL, active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW());",
];
let query_sql = "-- @name CreateProduct\n-- @returns :one\nINSERT INTO products (name, price, quantity, active) VALUES ($1, $2, $3, $4) RETURNING id, created_at;";
let catalog = scythe_core::catalog::Catalog::from_ddl(schema_sql).unwrap();
let query = scythe_core::parser::parse_query(query_sql).unwrap();
let analyzed = scythe_core::analyzer::analyze(&catalog, &query).unwrap();
assert_eq!(analyzed.name, "CreateProduct", "query name");
assert_eq!(analyzed.command.to_string(), "one", "query command");
assert_eq!(analyzed.params.len(), 4, "param count");
assert_eq!(analyzed.params[0].name, "name", "param name");
assert_eq!(analyzed.params[0].neutral_type, "string", "param neutral_type for name");
assert!(!analyzed.params[0].nullable, "param nullable for name");
assert_eq!(analyzed.params[1].name, "price", "param name");
assert_eq!(
analyzed.params[1].neutral_type, "decimal",
"param neutral_type for price"
);
assert!(!analyzed.params[1].nullable, "param nullable for price");
assert_eq!(analyzed.params[2].name, "quantity", "param name");
assert_eq!(
analyzed.params[2].neutral_type, "int32",
"param neutral_type for quantity"
);
assert!(!analyzed.params[2].nullable, "param nullable for quantity");
assert_eq!(analyzed.params[3].name, "active", "param name");
assert_eq!(analyzed.params[3].neutral_type, "bool", "param neutral_type for active");
assert!(!analyzed.params[3].nullable, "param nullable for active");
assert_eq!(analyzed.columns.len(), 2, "column count");
assert_eq!(analyzed.columns[0].name, "id", "column name");
assert_eq!(analyzed.columns[0].neutral_type, "int32", "column neutral_type for id");
assert!(!analyzed.columns[0].nullable, "column nullable for id");
assert_eq!(analyzed.columns[1].name, "created_at", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "datetime_tz",
"column neutral_type for created_at"
);
assert!(!analyzed.columns[1].nullable, "column nullable for created_at");
let engine = "postgresql";
let all_backends = [
"rust-sqlx",
"rust-tokio-postgres",
"python-psycopg3",
"python-asyncpg",
"typescript-postgres",
"typescript-pg",
"typescript-kysely",
"go-pgx",
"java-jdbc",
"java-r2dbc",
"kotlin-exposed",
"kotlin-jdbc",
"kotlin-r2dbc",
"csharp-npgsql",
"elixir-postgrex",
"elixir-ecto",
"ruby-pg",
"php-pdo",
"php-amphp",
];
for backend_name in &all_backends {
let backend = match scythe_codegen::get_backend(backend_name, engine) {
Ok(b) => b,
Err(e) => panic!(
"backend {} failed to construct for engine {} in fixture {}: {}",
backend_name, engine, "insert_all_columns", e
),
};
if let Ok(generated) = scythe_codegen::generate_with_backend(&analyzed, &*backend) {
let preamble = backend.file_preamble();
let header = backend.file_header();
let mut body = String::new();
if header.is_empty() {
body.push_str("#![allow(dead_code, unused_imports)]\n");
} else {
body.push_str(&header);
body.push('\n');
}
if let Some(ref s) = generated.enum_def {
body.push_str(s);
body.push('\n');
}
for def in &generated.nested_struct_defs {
body.push_str(&def.code);
body.push('\n');
}
if let Some(ref s) = generated.model_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.row_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.query_fn {
body.push_str(s);
body.push('\n');
}
let code = scythe_codegen::provenance::assemble_file(
&preamble,
&scythe_codegen::provenance::header_line(
&*backend,
env!("CARGO_PKG_VERSION"),
engine,
"sch1:0123456789abcdef",
"q1:fedcba9876543210",
),
&body,
);
if body.lines().count() > 1 {
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"insert_all_columns"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"insert_all_columns",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"insert_all_columns"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"insert_all_columns"
);
if *backend_name == "rust-sqlx" {
let actual = generated.row_struct.clone().unwrap_or_default();
assert!(
normalize_whitespace(&actual).contains(&normalize_whitespace("#[derive(Debug, Clone, sqlx::FromRow)]\npub struct CreateProductRow {\n pub id: i32,\n pub created_at: chrono::DateTime<chrono::Utc>,\n}")),
"backend {} row_struct mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "insert_all_columns", "#[derive(Debug, Clone, sqlx::FromRow)]\npub struct CreateProductRow {\n pub id: i32,\n pub created_at: chrono::DateTime<chrono::Utc>,\n}", actual
);
}
}
}
}
#[test]
fn test_insert_basic_returning() {
let schema_sql = &["CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL);"];
let query_sql =
"-- @name CreateUser\n-- @returns :one\nINSERT INTO users (name, email) VALUES ($1, $2) RETURNING id;";
let catalog = scythe_core::catalog::Catalog::from_ddl(schema_sql).unwrap();
let query = scythe_core::parser::parse_query(query_sql).unwrap();
let analyzed = scythe_core::analyzer::analyze(&catalog, &query).unwrap();
assert_eq!(analyzed.name, "CreateUser", "query name");
assert_eq!(analyzed.command.to_string(), "one", "query command");
assert_eq!(analyzed.params.len(), 2, "param count");
assert_eq!(analyzed.params[0].name, "name", "param name");
assert_eq!(analyzed.params[0].neutral_type, "string", "param neutral_type for name");
assert!(!analyzed.params[0].nullable, "param nullable for name");
assert_eq!(analyzed.params[1].name, "email", "param name");
assert_eq!(
analyzed.params[1].neutral_type, "string",
"param neutral_type for email"
);
assert!(!analyzed.params[1].nullable, "param nullable for email");
assert_eq!(analyzed.columns.len(), 1, "column count");
assert_eq!(analyzed.columns[0].name, "id", "column name");
assert_eq!(analyzed.columns[0].neutral_type, "int32", "column neutral_type for id");
assert!(!analyzed.columns[0].nullable, "column nullable for id");
let engine = "postgresql";
let all_backends = [
"rust-sqlx",
"rust-tokio-postgres",
"python-psycopg3",
"python-asyncpg",
"typescript-postgres",
"typescript-pg",
"typescript-kysely",
"go-pgx",
"java-jdbc",
"java-r2dbc",
"kotlin-exposed",
"kotlin-jdbc",
"kotlin-r2dbc",
"csharp-npgsql",
"elixir-postgrex",
"elixir-ecto",
"ruby-pg",
"php-pdo",
"php-amphp",
];
for backend_name in &all_backends {
let backend = match scythe_codegen::get_backend(backend_name, engine) {
Ok(b) => b,
Err(e) => panic!(
"backend {} failed to construct for engine {} in fixture {}: {}",
backend_name, engine, "insert_basic_returning", e
),
};
if let Ok(generated) = scythe_codegen::generate_with_backend(&analyzed, &*backend) {
let preamble = backend.file_preamble();
let header = backend.file_header();
let mut body = String::new();
if header.is_empty() {
body.push_str("#![allow(dead_code, unused_imports)]\n");
} else {
body.push_str(&header);
body.push('\n');
}
if let Some(ref s) = generated.enum_def {
body.push_str(s);
body.push('\n');
}
for def in &generated.nested_struct_defs {
body.push_str(&def.code);
body.push('\n');
}
if let Some(ref s) = generated.model_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.row_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.query_fn {
body.push_str(s);
body.push('\n');
}
let code = scythe_codegen::provenance::assemble_file(
&preamble,
&scythe_codegen::provenance::header_line(
&*backend,
env!("CARGO_PKG_VERSION"),
engine,
"sch1:0123456789abcdef",
"q1:fedcba9876543210",
),
&body,
);
if body.lines().count() > 1 {
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"insert_basic_returning"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"insert_basic_returning",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"insert_basic_returning"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"insert_basic_returning"
);
if *backend_name == "rust-sqlx" {
let actual = generated.row_struct.clone().unwrap_or_default();
assert!(
normalize_whitespace(&actual).contains(&normalize_whitespace(
"#[derive(Debug, Clone, sqlx::FromRow)]\npub struct CreateUserRow {\n pub id: i32,\n}"
)),
"backend {} row_struct mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name,
"insert_basic_returning",
"#[derive(Debug, Clone, sqlx::FromRow)]\npub struct CreateUserRow {\n pub id: i32,\n}",
actual
);
let actual = generated.query_fn.clone().unwrap_or_default();
assert!(
normalize_whitespace(&actual).contains(&normalize_whitespace("pub async fn create_user(pool: &sqlx::PgPool, name: &str, email: &str) -> Result<CreateUserRow, sqlx::Error> {\n sqlx::query_as!(CreateUserRow, \"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id\", name, email)\n .fetch_one(pool)\n .await\n}")),
"backend {} query_fn mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "insert_basic_returning", "pub async fn create_user(pool: &sqlx::PgPool, name: &str, email: &str) -> Result<CreateUserRow, sqlx::Error> {\n sqlx::query_as!(CreateUserRow, \"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id\", name, email)\n .fetch_one(pool)\n .await\n}", actual
);
}
}
}
}
#[test]
fn test_insert_coalesce_param_collected() {
let schema_sql = &["CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, bio TEXT);"];
let query_sql = "-- @name InsertBio\n-- @returns :exec\nINSERT INTO users (bio) VALUES (COALESCE($1, 'unknown'));";
let catalog = scythe_core::catalog::Catalog::from_ddl(schema_sql).unwrap();
let query = scythe_core::parser::parse_query(query_sql).unwrap();
let analyzed = scythe_core::analyzer::analyze(&catalog, &query).unwrap();
assert_eq!(analyzed.name, "InsertBio", "query name");
assert_eq!(analyzed.command.to_string(), "exec", "query command");
assert_eq!(analyzed.params.len(), 1, "param count");
assert_eq!(analyzed.params[0].name, "bio", "param name");
assert_eq!(analyzed.params[0].neutral_type, "string", "param neutral_type for bio");
assert!(analyzed.params[0].nullable, "param nullable for bio");
assert_eq!(analyzed.params[0].position, 1, "param position for bio");
let engine = "postgresql";
let all_backends = [
"rust-sqlx",
"rust-tokio-postgres",
"python-psycopg3",
"python-asyncpg",
"typescript-postgres",
"typescript-pg",
"typescript-kysely",
"go-pgx",
"java-jdbc",
"java-r2dbc",
"kotlin-exposed",
"kotlin-jdbc",
"kotlin-r2dbc",
"csharp-npgsql",
"elixir-postgrex",
"elixir-ecto",
"ruby-pg",
"php-pdo",
"php-amphp",
];
for backend_name in &all_backends {
let backend = match scythe_codegen::get_backend(backend_name, engine) {
Ok(b) => b,
Err(e) => panic!(
"backend {} failed to construct for engine {} in fixture {}: {}",
backend_name, engine, "insert_coalesce_param_collected", e
),
};
if let Ok(generated) = scythe_codegen::generate_with_backend(&analyzed, &*backend) {
let preamble = backend.file_preamble();
let header = backend.file_header();
let mut body = String::new();
if header.is_empty() {
body.push_str("#![allow(dead_code, unused_imports)]\n");
} else {
body.push_str(&header);
body.push('\n');
}
if let Some(ref s) = generated.enum_def {
body.push_str(s);
body.push('\n');
}
for def in &generated.nested_struct_defs {
body.push_str(&def.code);
body.push('\n');
}
if let Some(ref s) = generated.model_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.row_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.query_fn {
body.push_str(s);
body.push('\n');
}
let code = scythe_codegen::provenance::assemble_file(
&preamble,
&scythe_codegen::provenance::header_line(
&*backend,
env!("CARGO_PKG_VERSION"),
engine,
"sch1:0123456789abcdef",
"q1:fedcba9876543210",
),
&body,
);
if body.lines().count() > 1 {
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"insert_coalesce_param_collected"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"insert_coalesce_param_collected",
errors
);
}
}
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"insert_coalesce_param_collected"
);
}
}
}
#[test]
fn test_insert_no_column_list_positional_binding() {
let schema_sql =
&["CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL, age INTEGER);"];
let query_sql = "-- @name CreateUserNoColumns\n-- @returns :exec\nINSERT INTO users VALUES ($1, $2, $3, $4);";
let catalog = scythe_core::catalog::Catalog::from_ddl(schema_sql).unwrap();
let query = scythe_core::parser::parse_query(query_sql).unwrap();
let analyzed = scythe_core::analyzer::analyze(&catalog, &query).unwrap();
assert_eq!(analyzed.name, "CreateUserNoColumns", "query name");
assert_eq!(analyzed.command.to_string(), "exec", "query command");
assert_eq!(analyzed.params.len(), 4, "param count");
assert_eq!(analyzed.params[0].name, "id", "param name");
assert_eq!(analyzed.params[0].neutral_type, "int32", "param neutral_type for id");
assert!(!analyzed.params[0].nullable, "param nullable for id");
assert_eq!(analyzed.params[0].position, 1, "param position for id");
assert_eq!(analyzed.params[1].name, "name", "param name");
assert_eq!(analyzed.params[1].neutral_type, "string", "param neutral_type for name");
assert!(!analyzed.params[1].nullable, "param nullable for name");
assert_eq!(analyzed.params[1].position, 2, "param position for name");
assert_eq!(analyzed.params[2].name, "email", "param name");
assert_eq!(
analyzed.params[2].neutral_type, "string",
"param neutral_type for email"
);
assert!(!analyzed.params[2].nullable, "param nullable for email");
assert_eq!(analyzed.params[2].position, 3, "param position for email");
assert_eq!(analyzed.params[3].name, "age", "param name");
assert_eq!(analyzed.params[3].neutral_type, "int32", "param neutral_type for age");
assert!(analyzed.params[3].nullable, "param nullable for age");
assert_eq!(analyzed.params[3].position, 4, "param position for age");
let engine = "postgresql";
let all_backends = [
"rust-sqlx",
"rust-tokio-postgres",
"python-psycopg3",
"python-asyncpg",
"typescript-postgres",
"typescript-pg",
"typescript-kysely",
"go-pgx",
"java-jdbc",
"java-r2dbc",
"kotlin-exposed",
"kotlin-jdbc",
"kotlin-r2dbc",
"csharp-npgsql",
"elixir-postgrex",
"elixir-ecto",
"ruby-pg",
"php-pdo",
"php-amphp",
];
for backend_name in &all_backends {
let backend = match scythe_codegen::get_backend(backend_name, engine) {
Ok(b) => b,
Err(e) => panic!(
"backend {} failed to construct for engine {} in fixture {}: {}",
backend_name, engine, "insert_no_column_list_positional_binding", e
),
};
if let Ok(generated) = scythe_codegen::generate_with_backend(&analyzed, &*backend) {
let preamble = backend.file_preamble();
let header = backend.file_header();
let mut body = String::new();
if header.is_empty() {
body.push_str("#![allow(dead_code, unused_imports)]\n");
} else {
body.push_str(&header);
body.push('\n');
}
if let Some(ref s) = generated.enum_def {
body.push_str(s);
body.push('\n');
}
for def in &generated.nested_struct_defs {
body.push_str(&def.code);
body.push('\n');
}
if let Some(ref s) = generated.model_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.row_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.query_fn {
body.push_str(s);
body.push('\n');
}
let code = scythe_codegen::provenance::assemble_file(
&preamble,
&scythe_codegen::provenance::header_line(
&*backend,
env!("CARGO_PKG_VERSION"),
engine,
"sch1:0123456789abcdef",
"q1:fedcba9876543210",
),
&body,
);
if body.lines().count() > 1 {
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"insert_no_column_list_positional_binding"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"insert_no_column_list_positional_binding",
errors
);
}
}
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"insert_no_column_list_positional_binding"
);
}
}
}
#[test]
fn test_boolean_param() {
let schema_sql =
&["CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, active BOOLEAN NOT NULL DEFAULT true);"];
let query_sql = "-- @name GetUsersByActive\n-- @returns :many\nSELECT id, name FROM users WHERE active = $1;";
let catalog = scythe_core::catalog::Catalog::from_ddl(schema_sql).unwrap();
let query = scythe_core::parser::parse_query(query_sql).unwrap();
let analyzed = scythe_core::analyzer::analyze(&catalog, &query).unwrap();
assert_eq!(analyzed.name, "GetUsersByActive", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
assert_eq!(analyzed.params.len(), 1, "param count");
assert_eq!(analyzed.params[0].name, "active", "param name");
assert_eq!(analyzed.params[0].neutral_type, "bool", "param neutral_type for active");
assert!(!analyzed.params[0].nullable, "param nullable for active");
assert_eq!(analyzed.columns.len(), 2, "column count");
assert_eq!(analyzed.columns[0].name, "id", "column name");
assert_eq!(analyzed.columns[0].neutral_type, "int32", "column neutral_type for id");
assert!(!analyzed.columns[0].nullable, "column nullable for id");
assert_eq!(analyzed.columns[1].name, "name", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "string",
"column neutral_type for name"
);
assert!(!analyzed.columns[1].nullable, "column nullable for name");
let engine = "postgresql";
let all_backends = [
"rust-sqlx",
"rust-tokio-postgres",
"python-psycopg3",
"python-asyncpg",
"typescript-postgres",
"typescript-pg",
"typescript-kysely",
"go-pgx",
"java-jdbc",
"java-r2dbc",
"kotlin-exposed",
"kotlin-jdbc",
"kotlin-r2dbc",
"csharp-npgsql",
"elixir-postgrex",
"elixir-ecto",
"ruby-pg",
"php-pdo",
"php-amphp",
];
for backend_name in &all_backends {
let backend = match scythe_codegen::get_backend(backend_name, engine) {
Ok(b) => b,
Err(e) => panic!(
"backend {} failed to construct for engine {} in fixture {}: {}",
backend_name, engine, "boolean_param", e
),
};
if let Ok(generated) = scythe_codegen::generate_with_backend(&analyzed, &*backend) {
let preamble = backend.file_preamble();
let header = backend.file_header();
let mut body = String::new();
if header.is_empty() {
body.push_str("#![allow(dead_code, unused_imports)]\n");
} else {
body.push_str(&header);
body.push('\n');
}
if let Some(ref s) = generated.enum_def {
body.push_str(s);
body.push('\n');
}
for def in &generated.nested_struct_defs {
body.push_str(&def.code);
body.push('\n');
}
if let Some(ref s) = generated.model_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.row_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.query_fn {
body.push_str(s);
body.push('\n');
}
let code = scythe_codegen::provenance::assemble_file(
&preamble,
&scythe_codegen::provenance::header_line(
&*backend,
env!("CARGO_PKG_VERSION"),
engine,
"sch1:0123456789abcdef",
"q1:fedcba9876543210",
),
&body,
);
if body.lines().count() > 1 {
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"boolean_param"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"boolean_param",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"boolean_param"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"boolean_param"
);
if *backend_name == "rust-sqlx" {
let actual = generated.row_struct.clone().unwrap_or_default();
assert!(
normalize_whitespace(&actual).contains(&normalize_whitespace("#[derive(Debug, Clone, sqlx::FromRow)]\npub struct GetUsersByActiveRow {\n pub id: i32,\n pub name: String,\n}")),
"backend {} row_struct mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "boolean_param", "#[derive(Debug, Clone, sqlx::FromRow)]\npub struct GetUsersByActiveRow {\n pub id: i32,\n pub name: String,\n}", actual
);
let actual = generated.query_fn.clone().unwrap_or_default();
assert!(
normalize_whitespace(&actual).contains(&normalize_whitespace("pub async fn get_users_by_active(pool: &sqlx::PgPool, active: bool) -> Result<Vec<GetUsersByActiveRow>, sqlx::Error> {\n sqlx::query_as!(GetUsersByActiveRow, \"SELECT id, name FROM users WHERE active = $1\", active)\n .fetch_all(pool)\n .await\n}")),
"backend {} query_fn mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "boolean_param", "pub async fn get_users_by_active(pool: &sqlx::PgPool, active: bool) -> Result<Vec<GetUsersByActiveRow>, sqlx::Error> {\n sqlx::query_as!(GetUsersByActiveRow, \"SELECT id, name FROM users WHERE active = $1\", active)\n .fetch_all(pool)\n .await\n}", actual
);
}
}
}
}
#[test]
fn test_integer_param() {
let schema_sql = &["CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL);"];
let query_sql = "-- @name GetUserById\n-- @returns :one\nSELECT id, name, email FROM users WHERE id = $1;";
let catalog = scythe_core::catalog::Catalog::from_ddl(schema_sql).unwrap();
let query = scythe_core::parser::parse_query(query_sql).unwrap();
let analyzed = scythe_core::analyzer::analyze(&catalog, &query).unwrap();
assert_eq!(analyzed.name, "GetUserById", "query name");
assert_eq!(analyzed.command.to_string(), "one", "query command");
assert_eq!(analyzed.params.len(), 1, "param count");
assert_eq!(analyzed.params[0].name, "id", "param name");
assert_eq!(analyzed.params[0].neutral_type, "int32", "param neutral_type for id");
assert!(!analyzed.params[0].nullable, "param nullable for id");
assert_eq!(analyzed.columns.len(), 3, "column count");
assert_eq!(analyzed.columns[0].name, "id", "column name");
assert_eq!(analyzed.columns[0].neutral_type, "int32", "column neutral_type for id");
assert!(!analyzed.columns[0].nullable, "column nullable for id");
assert_eq!(analyzed.columns[1].name, "name", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "string",
"column neutral_type for name"
);
assert!(!analyzed.columns[1].nullable, "column nullable for name");
assert_eq!(analyzed.columns[2].name, "email", "column name");
assert_eq!(
analyzed.columns[2].neutral_type, "string",
"column neutral_type for email"
);
assert!(!analyzed.columns[2].nullable, "column nullable for email");
let engine = "postgresql";
let all_backends = [
"rust-sqlx",
"rust-tokio-postgres",
"python-psycopg3",
"python-asyncpg",
"typescript-postgres",
"typescript-pg",
"typescript-kysely",
"go-pgx",
"java-jdbc",
"java-r2dbc",
"kotlin-exposed",
"kotlin-jdbc",
"kotlin-r2dbc",
"csharp-npgsql",
"elixir-postgrex",
"elixir-ecto",
"ruby-pg",
"php-pdo",
"php-amphp",
];
for backend_name in &all_backends {
let backend = match scythe_codegen::get_backend(backend_name, engine) {
Ok(b) => b,
Err(e) => panic!(
"backend {} failed to construct for engine {} in fixture {}: {}",
backend_name, engine, "integer_param", e
),
};
if let Ok(generated) = scythe_codegen::generate_with_backend(&analyzed, &*backend) {
let preamble = backend.file_preamble();
let header = backend.file_header();
let mut body = String::new();
if header.is_empty() {
body.push_str("#![allow(dead_code, unused_imports)]\n");
} else {
body.push_str(&header);
body.push('\n');
}
if let Some(ref s) = generated.enum_def {
body.push_str(s);
body.push('\n');
}
for def in &generated.nested_struct_defs {
body.push_str(&def.code);
body.push('\n');
}
if let Some(ref s) = generated.model_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.row_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.query_fn {
body.push_str(s);
body.push('\n');
}
let code = scythe_codegen::provenance::assemble_file(
&preamble,
&scythe_codegen::provenance::header_line(
&*backend,
env!("CARGO_PKG_VERSION"),
engine,
"sch1:0123456789abcdef",
"q1:fedcba9876543210",
),
&body,
);
if body.lines().count() > 1 {
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"integer_param"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"integer_param",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"integer_param"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"integer_param"
);
if *backend_name == "rust-sqlx" {
let actual = generated.row_struct.clone().unwrap_or_default();
assert!(
normalize_whitespace(&actual).contains(&normalize_whitespace("#[derive(Debug, Clone, sqlx::FromRow)]\npub struct GetUserByIdRow {\n pub id: i32,\n pub name: String,\n pub email: String,\n}")),
"backend {} row_struct mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "integer_param", "#[derive(Debug, Clone, sqlx::FromRow)]\npub struct GetUserByIdRow {\n pub id: i32,\n pub name: String,\n pub email: String,\n}", actual
);
let actual = generated.query_fn.clone().unwrap_or_default();
assert!(
normalize_whitespace(&actual).contains(&normalize_whitespace("pub async fn get_user_by_id(pool: &sqlx::PgPool, id: i32) -> Result<GetUserByIdRow, sqlx::Error> {\n sqlx::query_as!(GetUserByIdRow, \"SELECT id, name, email FROM users WHERE id = $1\", id)\n .fetch_one(pool)\n .await\n}")),
"backend {} query_fn mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "integer_param", "pub async fn get_user_by_id(pool: &sqlx::PgPool, id: i32) -> Result<GetUserByIdRow, sqlx::Error> {\n sqlx::query_as!(GetUserByIdRow, \"SELECT id, name, email FROM users WHERE id = $1\", id)\n .fetch_one(pool)\n .await\n}", actual
);
}
}
}
}
#[test]
fn test_string_param() {
let schema_sql = &["CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL);"];
let query_sql = "-- @name GetUserByName\n-- @returns :one\nSELECT id, name, email FROM users WHERE name = $1;";
let catalog = scythe_core::catalog::Catalog::from_ddl(schema_sql).unwrap();
let query = scythe_core::parser::parse_query(query_sql).unwrap();
let analyzed = scythe_core::analyzer::analyze(&catalog, &query).unwrap();
assert_eq!(analyzed.name, "GetUserByName", "query name");
assert_eq!(analyzed.command.to_string(), "one", "query command");
assert_eq!(analyzed.params.len(), 1, "param count");
assert_eq!(analyzed.params[0].name, "name", "param name");
assert_eq!(analyzed.params[0].neutral_type, "string", "param neutral_type for name");
assert!(!analyzed.params[0].nullable, "param nullable for name");
assert_eq!(analyzed.columns.len(), 3, "column count");
assert_eq!(analyzed.columns[0].name, "id", "column name");
assert_eq!(analyzed.columns[0].neutral_type, "int32", "column neutral_type for id");
assert!(!analyzed.columns[0].nullable, "column nullable for id");
assert_eq!(analyzed.columns[1].name, "name", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "string",
"column neutral_type for name"
);
assert!(!analyzed.columns[1].nullable, "column nullable for name");
assert_eq!(analyzed.columns[2].name, "email", "column name");
assert_eq!(
analyzed.columns[2].neutral_type, "string",
"column neutral_type for email"
);
assert!(!analyzed.columns[2].nullable, "column nullable for email");
let engine = "postgresql";
let all_backends = [
"rust-sqlx",
"rust-tokio-postgres",
"python-psycopg3",
"python-asyncpg",
"typescript-postgres",
"typescript-pg",
"typescript-kysely",
"go-pgx",
"java-jdbc",
"java-r2dbc",
"kotlin-exposed",
"kotlin-jdbc",
"kotlin-r2dbc",
"csharp-npgsql",
"elixir-postgrex",
"elixir-ecto",
"ruby-pg",
"php-pdo",
"php-amphp",
];
for backend_name in &all_backends {
let backend = match scythe_codegen::get_backend(backend_name, engine) {
Ok(b) => b,
Err(e) => panic!(
"backend {} failed to construct for engine {} in fixture {}: {}",
backend_name, engine, "string_param", e
),
};
if let Ok(generated) = scythe_codegen::generate_with_backend(&analyzed, &*backend) {
let preamble = backend.file_preamble();
let header = backend.file_header();
let mut body = String::new();
if header.is_empty() {
body.push_str("#![allow(dead_code, unused_imports)]\n");
} else {
body.push_str(&header);
body.push('\n');
}
if let Some(ref s) = generated.enum_def {
body.push_str(s);
body.push('\n');
}
for def in &generated.nested_struct_defs {
body.push_str(&def.code);
body.push('\n');
}
if let Some(ref s) = generated.model_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.row_struct {
body.push_str(s);
body.push('\n');
}
if let Some(ref s) = generated.query_fn {
body.push_str(s);
body.push('\n');
}
let code = scythe_codegen::provenance::assemble_file(
&preamble,
&scythe_codegen::provenance::header_line(
&*backend,
env!("CARGO_PKG_VERSION"),
engine,
"sch1:0123456789abcdef",
"q1:fedcba9876543210",
),
&body,
);
if body.lines().count() > 1 {
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"string_param"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"string_param",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"string_param"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"string_param"
);
if *backend_name == "rust-sqlx" {
let actual = generated.row_struct.clone().unwrap_or_default();
assert!(
normalize_whitespace(&actual).contains(&normalize_whitespace("#[derive(Debug, Clone, sqlx::FromRow)]\npub struct GetUserByNameRow {\n pub id: i32,\n pub name: String,\n pub email: String,\n}")),
"backend {} row_struct mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "string_param", "#[derive(Debug, Clone, sqlx::FromRow)]\npub struct GetUserByNameRow {\n pub id: i32,\n pub name: String,\n pub email: String,\n}", actual
);
let actual = generated.query_fn.clone().unwrap_or_default();
assert!(
normalize_whitespace(&actual).contains(&normalize_whitespace("pub async fn get_user_by_name(pool: &sqlx::PgPool, name: &str) -> Result<GetUserByNameRow, sqlx::Error> {\n sqlx::query_as!(GetUserByNameRow, \"SELECT id, name, email FROM users WHERE name = $1\", name)\n .fetch_one(pool)\n .await\n}")),
"backend {} query_fn mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "string_param", "pub async fn get_user_by_name(pool: &sqlx::PgPool, name: &str) -> Result<GetUserByNameRow, sqlx::Error> {\n sqlx::query_as!(GetUserByNameRow, \"SELECT id, name, email FROM users WHERE name = $1\", name)\n .fetch_one(pool)\n .await\n}", actual
);
}
}
}
}