#[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_select_all_reuses_model() {
let schema_sql = &[
"CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW());",
];
let query_sql = "-- @name GetAllUsers\n-- @returns :many\nSELECT * 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, "GetAllUsers", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
assert_eq!(analyzed.columns.len(), 4, "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");
assert_eq!(analyzed.columns[3].name, "created_at", "column name");
assert_eq!(
analyzed.columns[3].neutral_type, "datetime_tz",
"column neutral_type for created_at"
);
assert!(!analyzed.columns[3].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, "select_all_reuses_model", 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,
"select_all_reuses_model"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"select_all_reuses_model",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"select_all_reuses_model"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"select_all_reuses_model"
);
if *backend_name == "rust-sqlx" {
let actual = generated.query_fn.clone().unwrap_or_default();
assert!(
normalize_whitespace(&actual).contains(&normalize_whitespace("pub async fn get_all_users(pool: &sqlx::PgPool) -> Result<Vec<User>, sqlx::Error> {\n sqlx::query_as!(User, \"SELECT * FROM users\")\n .fetch_all(pool)\n .await\n}")),
"backend {} query_fn mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "select_all_reuses_model", "pub async fn get_all_users(pool: &sqlx::PgPool) -> Result<Vec<User>, sqlx::Error> {\n sqlx::query_as!(User, \"SELECT * FROM users\")\n .fetch_all(pool)\n .await\n}", actual
);
let actual = generated.model_struct.clone().unwrap_or_default();
assert!(
normalize_whitespace(&actual).contains(&normalize_whitespace("#[derive(Debug, Clone, sqlx::FromRow)]\npub struct User {\n pub id: i32,\n pub name: String,\n pub email: String,\n pub created_at: chrono::DateTime<chrono::Utc>,\n}")),
"backend {} model_struct mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "select_all_reuses_model", "#[derive(Debug, Clone, sqlx::FromRow)]\npub struct User {\n pub id: i32,\n pub name: String,\n pub email: String,\n pub created_at: chrono::DateTime<chrono::Utc>,\n}", actual
);
}
}
}
}
#[test]
fn test_pascal_case_structs() {
let schema_sql = &["CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL);"];
let query_sql = "-- @name get_user\n-- @returns :one\nSELECT id, name 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, "get_user", "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, "pascal_case_structs", 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,
"pascal_case_structs"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"pascal_case_structs",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"pascal_case_structs"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"pascal_case_structs"
);
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 GetUserRow {\n pub id: i32,\n pub name: String,\n}")),
"backend {} row_struct mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "pascal_case_structs", "#[derive(Debug, Clone, sqlx::FromRow)]\npub struct GetUserRow {\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_user(pool: &sqlx::PgPool, id: i32) -> Result<GetUserRow, sqlx::Error> {\n sqlx::query_as!(GetUserRow, \"SELECT id, name 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, "pascal_case_structs", "pub async fn get_user(pool: &sqlx::PgPool, id: i32) -> Result<GetUserRow, sqlx::Error> {\n sqlx::query_as!(GetUserRow, \"SELECT id, name FROM users WHERE id = $1\", id)\n .fetch_one(pool)\n .await\n}", actual
);
}
}
}
}
#[test]
fn test_snake_case_fields() {
let schema_sql = &[
"CREATE TABLE users (id SERIAL PRIMARY KEY, \"userId\" INTEGER NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), \"firstName\" TEXT NOT NULL);",
];
let query_sql =
"-- @name GetUserFields\n-- @returns :many\nSELECT id, \"userId\", created_at, \"firstName\" 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, "GetUserFields", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
assert_eq!(analyzed.columns.len(), 4, "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, "userId", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "int32",
"column neutral_type for userId"
);
assert!(!analyzed.columns[1].nullable, "column nullable for userId");
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");
assert_eq!(analyzed.columns[3].name, "firstName", "column name");
assert_eq!(
analyzed.columns[3].neutral_type, "string",
"column neutral_type for firstName"
);
assert!(!analyzed.columns[3].nullable, "column nullable for firstName");
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, "snake_case_fields", 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,
"snake_case_fields"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"snake_case_fields",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"snake_case_fields"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"snake_case_fields"
);
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 GetUserFieldsRow {\n pub id: i32,\n #[sqlx(rename = \"userId\")]\n pub user_id: i32,\n pub created_at: chrono::DateTime<chrono::Utc>,\n #[sqlx(rename = \"firstName\")]\n pub first_name: String,\n}")),
"backend {} row_struct mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "snake_case_fields", "#[derive(Debug, Clone, sqlx::FromRow)]\npub struct GetUserFieldsRow {\n pub id: i32,\n #[sqlx(rename = \"userId\")]\n pub user_id: i32,\n pub created_at: chrono::DateTime<chrono::Utc>,\n #[sqlx(rename = \"firstName\")]\n pub first_name: String,\n}", actual
);
}
}
}
}
#[test]
fn test_exec_query_fn() {
let schema_sql = &["CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL);"];
let query_sql = "-- @name PurgeUsers\n-- @returns :exec\nDELETE 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, "PurgeUsers", "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, "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");
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, "exec_query_fn", 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,
"exec_query_fn"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"exec_query_fn",
errors
);
}
}
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"exec_query_fn"
);
if *backend_name == "rust-sqlx" {
let actual = generated.query_fn.clone().unwrap_or_default();
assert!(
normalize_whitespace(&actual).contains(&normalize_whitespace("pub async fn purge_users(pool: &sqlx::PgPool, id: i32) -> Result<(), sqlx::Error> {\n sqlx::query!(\"DELETE FROM users WHERE id < $1\", id)\n .execute(pool)\n .await?;\n Ok(())\n}")),
"backend {} query_fn mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "exec_query_fn", "pub async fn purge_users(pool: &sqlx::PgPool, id: i32) -> Result<(), sqlx::Error> {\n sqlx::query!(\"DELETE FROM users WHERE id < $1\", id)\n .execute(pool)\n .await?;\n Ok(())\n}", actual
);
}
}
}
}
#[test]
fn test_one_query_fn() {
let schema_sql = &["CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL);"];
let query_sql = "-- @name FindUser\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, "FindUser", "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, "one_query_fn", 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,
"one_query_fn"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"one_query_fn",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"one_query_fn"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"one_query_fn"
);
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 FindUserRow {\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, "one_query_fn", "#[derive(Debug, Clone, sqlx::FromRow)]\npub struct FindUserRow {\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 find_user(pool: &sqlx::PgPool, id: i32) -> Result<FindUserRow, sqlx::Error> {\n sqlx::query_as!(FindUserRow, \"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, "one_query_fn", "pub async fn find_user(pool: &sqlx::PgPool, id: i32) -> Result<FindUserRow, sqlx::Error> {\n sqlx::query_as!(FindUserRow, \"SELECT id, name, email FROM users WHERE id = $1\", id)\n .fetch_one(pool)\n .await\n}", actual
);
}
}
}
}
#[test]
fn test_simple_query_fn() {
let schema_sql = &[
"CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW());",
];
let query_sql = "-- @name ListUsers\n-- @returns :many\nSELECT id, name, email, created_at FROM users ORDER BY created_at DESC;";
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, "ListUsers", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
assert_eq!(analyzed.columns.len(), 4, "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");
assert_eq!(analyzed.columns[3].name, "created_at", "column name");
assert_eq!(
analyzed.columns[3].neutral_type, "datetime_tz",
"column neutral_type for created_at"
);
assert!(!analyzed.columns[3].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, "simple_query_fn", 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,
"simple_query_fn"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"simple_query_fn",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"simple_query_fn"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"simple_query_fn"
);
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 ListUsersRow {\n pub id: i32,\n pub name: String,\n pub email: String,\n pub created_at: chrono::DateTime<chrono::Utc>,\n}")),
"backend {} row_struct mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "simple_query_fn", "#[derive(Debug, Clone, sqlx::FromRow)]\npub struct ListUsersRow {\n pub id: i32,\n pub name: String,\n pub email: 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 list_users(pool: &sqlx::PgPool) -> Result<Vec<ListUsersRow>, sqlx::Error> {\n sqlx::query_as!(ListUsersRow, \"SELECT id, name, email, created_at FROM users ORDER BY created_at DESC\")\n .fetch_all(pool)\n .await\n}")),
"backend {} query_fn mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "simple_query_fn", "pub async fn list_users(pool: &sqlx::PgPool) -> Result<Vec<ListUsersRow>, sqlx::Error> {\n sqlx::query_as!(ListUsersRow, \"SELECT id, name, email, created_at FROM users ORDER BY created_at DESC\")\n .fetch_all(pool)\n .await\n}", actual
);
}
}
}
}