#[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_delete_basic() {
let schema_sql = &["CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL);"];
let query_sql = "-- @name DeleteUser\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, "DeleteUser", "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, "delete_basic", 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,
"delete_basic"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"delete_basic",
errors
);
}
}
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"delete_basic"
);
if *backend_name == "rust-sqlx" {
let actual = generated.query_fn.clone().unwrap_or_default();
assert!(
normalize_whitespace(&actual).contains(&normalize_whitespace("pub async fn delete_user(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, "delete_basic", "pub async fn delete_user(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_delete_returning() {
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 DeleteUserReturning\n-- @returns :one\nDELETE FROM users WHERE id = $1 RETURNING *;";
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, "DeleteUserReturning", "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(), 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, "delete_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,
"delete_returning"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"delete_returning",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"delete_returning"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"delete_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 DeleteUserReturningRow {\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, "delete_returning", "#[derive(Debug, Clone, sqlx::FromRow)]\npub struct DeleteUserReturningRow {\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_insert_exec() {
let schema_sql = &["CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL);"];
let query_sql = "-- @name InsertUser\n-- @returns :exec\nINSERT INTO users (name, email) VALUES ($1, $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, "InsertUser", "query name");
assert_eq!(analyzed.command.to_string(), "exec", "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");
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_exec", 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_exec"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"insert_exec",
errors
);
}
}
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"insert_exec"
);
if *backend_name == "rust-sqlx" {
let actual = generated.query_fn.clone().unwrap_or_default();
assert!(
normalize_whitespace(&actual).contains(&normalize_whitespace("pub async fn insert_user(pool: &sqlx::PgPool, name: &str, email: &str) -> Result<(), sqlx::Error> {\n sqlx::query!(\"INSERT INTO users (name, email) VALUES ($1, $2)\", name, email)\n .execute(pool)\n .await?;\n Ok(())\n}")),
"backend {} query_fn mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "insert_exec", "pub async fn insert_user(pool: &sqlx::PgPool, name: &str, email: &str) -> Result<(), sqlx::Error> {\n sqlx::query!(\"INSERT INTO users (name, email) VALUES ($1, $2)\", name, email)\n .execute(pool)\n .await?;\n Ok(())\n}", actual
);
}
}
}
}
#[test]
fn test_insert_returning() {
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 CreateUserReturning\n-- @returns :one\nINSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, 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, "CreateUserReturning", "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(), 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, "insert_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_returning"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"insert_returning",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"insert_returning"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"insert_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 CreateUserReturningRow {\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, "insert_returning", "#[derive(Debug, Clone, sqlx::FromRow)]\npub struct CreateUserReturningRow {\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 create_user_returning(pool: &sqlx::PgPool, name: &str, email: &str) -> Result<CreateUserReturningRow, sqlx::Error> {\n sqlx::query_as!(CreateUserReturningRow, \"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, created_at\", name, email)\n .fetch_one(pool)\n .await\n}")),
"backend {} query_fn mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "insert_returning", "pub async fn create_user_returning(pool: &sqlx::PgPool, name: &str, email: &str) -> Result<CreateUserReturningRow, sqlx::Error> {\n sqlx::query_as!(CreateUserReturningRow, \"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, created_at\", name, email)\n .fetch_one(pool)\n .await\n}", actual
);
}
}
}
}
#[test]
fn test_insert_returning_star() {
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 CreateUserReturningStar\n-- @returns :one\nINSERT INTO users (name, email) VALUES ($1, $2) RETURNING *;";
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, "CreateUserReturningStar", "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(), 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, "insert_returning_star", 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_returning_star"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"insert_returning_star",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"insert_returning_star"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"insert_returning_star"
);
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 CreateUserReturningStarRow {\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, "insert_returning_star", "#[derive(Debug, Clone, sqlx::FromRow)]\npub struct CreateUserReturningStarRow {\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_upsert() {
let schema_sql = &[
"CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW());",
];
let query_sql = "-- @name UpsertUser\n-- @returns :one\nINSERT INTO users (name, email) VALUES ($1, $2) ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name RETURNING *;";
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, "UpsertUser", "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(), 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, "upsert", 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,
"upsert"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"upsert",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"upsert"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"upsert"
);
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 UpsertUserRow {\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, "upsert", "#[derive(Debug, Clone, sqlx::FromRow)]\npub struct UpsertUserRow {\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 upsert_user(pool: &sqlx::PgPool, name: &str, email: &str) -> Result<UpsertUserRow, sqlx::Error> {\n sqlx::query_as!(UpsertUserRow, \"INSERT INTO users (name, email) VALUES ($1, $2) ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name RETURNING *\", name, email)\n .fetch_one(pool)\n .await\n}")),
"backend {} query_fn mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "upsert", "pub async fn upsert_user(pool: &sqlx::PgPool, name: &str, email: &str) -> Result<UpsertUserRow, sqlx::Error> {\n sqlx::query_as!(UpsertUserRow, \"INSERT INTO users (name, email) VALUES ($1, $2) ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name RETURNING *\", name, email)\n .fetch_one(pool)\n .await\n}", actual
);
}
}
}
}
#[test]
fn test_returning_specific_columns() {
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 CreateUserReturnIdAndDate\n-- @returns :one\nINSERT INTO users (name, email) VALUES ($1, $2) 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, "CreateUserReturnIdAndDate", "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(), 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, "returning_specific_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,
"returning_specific_columns"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"returning_specific_columns",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"returning_specific_columns"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"returning_specific_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 CreateUserReturnIdAndDateRow {\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, "returning_specific_columns", "#[derive(Debug, Clone, sqlx::FromRow)]\npub struct CreateUserReturnIdAndDateRow {\n pub id: i32,\n pub created_at: chrono::DateTime<chrono::Utc>,\n}", actual
);
}
}
}
}
#[test]
fn test_insert_returning_now() {
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 CreateEventWithServerTime\n-- @returns :one\nINSERT INTO events (name) VALUES ($1) RETURNING id, NOW() AS server_time";
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, "CreateEventWithServerTime", "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.params[0].position, 1, "param position for name");
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, "server_time", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "datetime_tz",
"column neutral_type for server_time"
);
assert!(!analyzed.columns[1].nullable, "column nullable for server_time");
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_returning_now", 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_returning_now"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"insert_returning_now",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"insert_returning_now"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"insert_returning_now"
);
}
}
}
#[test]
fn test_update_returning_computed() {
let schema_sql = &["CREATE TABLE orders (id SERIAL PRIMARY KEY, total INTEGER NOT NULL);"];
let query_sql = "-- @name UpdateOrderReturnDoubled\n-- @returns :one\nUPDATE orders SET total = $1 WHERE id = $2 RETURNING id, total * 2 AS doubled";
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, "UpdateOrderReturnDoubled", "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, "total", "param name");
assert_eq!(analyzed.params[0].neutral_type, "int32", "param neutral_type for total");
assert!(!analyzed.params[0].nullable, "param nullable for total");
assert_eq!(analyzed.params[0].position, 1, "param position for total");
assert_eq!(analyzed.params[1].name, "id", "param name");
assert_eq!(analyzed.params[1].neutral_type, "int32", "param neutral_type for id");
assert!(!analyzed.params[1].nullable, "param nullable for id");
assert_eq!(analyzed.params[1].position, 2, "param position 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, "doubled", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "int32",
"column neutral_type for doubled"
);
assert!(!analyzed.columns[1].nullable, "column nullable for doubled");
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, "update_returning_computed", 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,
"update_returning_computed"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"update_returning_computed",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"update_returning_computed"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"update_returning_computed"
);
}
}
}
#[test]
fn test_update_basic() {
let schema_sql = &["CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL);"];
let query_sql = "-- @name UpdateUserName\n-- @returns :exec\nUPDATE users SET name = $1 WHERE id = $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, "UpdateUserName", "query name");
assert_eq!(analyzed.command.to_string(), "exec", "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, "id", "param name");
assert_eq!(analyzed.params[1].neutral_type, "int32", "param neutral_type for id");
assert!(!analyzed.params[1].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, "update_basic", 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,
"update_basic"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"update_basic",
errors
);
}
}
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"update_basic"
);
if *backend_name == "rust-sqlx" {
let actual = generated.query_fn.clone().unwrap_or_default();
assert!(
normalize_whitespace(&actual).contains(&normalize_whitespace("pub async fn update_user_name(pool: &sqlx::PgPool, name: &str, id: i32) -> Result<(), sqlx::Error> {\n sqlx::query!(\"UPDATE users SET name = $1 WHERE id = $2\", name, id)\n .execute(pool)\n .await?;\n Ok(())\n}")),
"backend {} query_fn mismatch for {}\n--- expected (substring) ---\n{}\n--- actual ---\n{}",
backend_name, "update_basic", "pub async fn update_user_name(pool: &sqlx::PgPool, name: &str, id: i32) -> Result<(), sqlx::Error> {\n sqlx::query!(\"UPDATE users SET name = $1 WHERE id = $2\", name, id)\n .execute(pool)\n .await?;\n Ok(())\n}", actual
);
}
}
}
}
#[test]
fn test_update_returning() {
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 UpdateUserReturning\n-- @returns :one\nUPDATE users SET name = $1, email = $2 WHERE id = $3 RETURNING *;";
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, "UpdateUserReturning", "query name");
assert_eq!(analyzed.command.to_string(), "one", "query command");
assert_eq!(analyzed.params.len(), 3, "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.params[2].name, "id", "param name");
assert_eq!(analyzed.params[2].neutral_type, "int32", "param neutral_type for id");
assert!(!analyzed.params[2].nullable, "param nullable for id");
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, "update_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,
"update_returning"
);
} else {
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"update_returning",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"update_returning"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"update_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 UpdateUserReturningRow {\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, "update_returning", "#[derive(Debug, Clone, sqlx::FromRow)]\npub struct UpdateUserReturningRow {\n pub id: i32,\n pub name: String,\n pub email: String,\n pub created_at: chrono::DateTime<chrono::Utc>,\n}", actual
);
}
}
}
}