// Auto-generated by test-generator. Do not edit.
// Category: nullability_live
#[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_live_avg_over_no_matching_rows_is_null() {
// From: testing_data/nullability_live/aggregate_nullability/live_avg_over_no_matching_rows_is_null.json
// "AVG over an empty set returns NULL rather than zero, verified against a live engine"
let schema_sql = &[
"CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now())",
"CREATE TABLE orders (id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id), total NUMERIC(10, 2) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now())",
];
let query_sql =
"-- @name GetAvgOrderTotal\n-- @returns :many\nSELECT AVG(total) AS avg_total FROM orders ORDER BY avg_total";
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, "GetAvgOrderTotal", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
assert_eq!(analyzed.columns.len(), 1, "column count");
assert_eq!(analyzed.columns[0].name, "avg_total", "column name");
assert_eq!(
analyzed.columns[0].neutral_type, "decimal",
"column neutral_type for avg_total"
);
assert!(analyzed.columns[0].nullable, "column nullable for avg_total");
// Codegen verification: every backend that supports this fixture's engine
// should produce valid output. The list below is derived at generation
// time from the fixture's engine via the real `get_backend` registry, so
// every entry here is expected to construct -- a construction failure
// below is a real regression, not an expected engine mismatch. See #156.
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, "live_avg_over_no_matching_rows_is_null", 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 {
// Only validate Rust syntax with syn for Rust backends
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"live_avg_over_no_matching_rows_is_null"
);
} else {
// Structural validation for non-Rust backends
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"live_avg_over_no_matching_rows_is_null",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"live_avg_over_no_matching_rows_is_null"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"live_avg_over_no_matching_rows_is_null"
);
}
}
}
#[test]
fn test_live_max_over_no_matching_rows_is_null() {
// From: testing_data/nullability_live/aggregate_nullability/live_max_over_no_matching_rows_is_null.json
// "MAX over an empty set returns NULL even though the aggregated column is NOT NULL; a live engine demonstrates the NULL rather than the fixture asserting it in the abstract"
let schema_sql = &[
"CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now())",
"CREATE TABLE orders (id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id), total NUMERIC(10, 2) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now())",
];
let query_sql =
"-- @name GetMaxOrderTotal\n-- @returns :many\nSELECT MAX(total) AS max_total FROM orders ORDER BY max_total";
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, "GetMaxOrderTotal", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
assert_eq!(analyzed.columns.len(), 1, "column count");
assert_eq!(analyzed.columns[0].name, "max_total", "column name");
assert_eq!(
analyzed.columns[0].neutral_type, "decimal",
"column neutral_type for max_total"
);
assert!(analyzed.columns[0].nullable, "column nullable for max_total");
// Codegen verification: every backend that supports this fixture's engine
// should produce valid output. The list below is derived at generation
// time from the fixture's engine via the real `get_backend` registry, so
// every entry here is expected to construct -- a construction failure
// below is a real regression, not an expected engine mismatch. See #156.
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, "live_max_over_no_matching_rows_is_null", 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 {
// Only validate Rust syntax with syn for Rust backends
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"live_max_over_no_matching_rows_is_null"
);
} else {
// Structural validation for non-Rust backends
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"live_max_over_no_matching_rows_is_null",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"live_max_over_no_matching_rows_is_null"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"live_max_over_no_matching_rows_is_null"
);
}
}
}
#[test]
fn test_live_sum_over_no_matching_rows_is_null() {
// From: testing_data/nullability_live/aggregate_nullability/live_sum_over_no_matching_rows_is_null.json
// "SUM over an empty set returns NULL rather than zero; the analyzer marks it nullable and this fixture makes a live engine demonstrate the NULL instead of assuming it"
let schema_sql = &[
"CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now())",
"CREATE TABLE orders (id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id), total NUMERIC(10, 2) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now())",
];
let query_sql =
"-- @name GetOrderTotalSum\n-- @returns :many\nSELECT SUM(total) AS total_sum FROM orders ORDER BY total_sum";
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, "GetOrderTotalSum", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
assert_eq!(analyzed.columns.len(), 1, "column count");
assert_eq!(analyzed.columns[0].name, "total_sum", "column name");
assert_eq!(
analyzed.columns[0].neutral_type, "decimal",
"column neutral_type for total_sum"
);
assert!(analyzed.columns[0].nullable, "column nullable for total_sum");
// Codegen verification: every backend that supports this fixture's engine
// should produce valid output. The list below is derived at generation
// time from the fixture's engine via the real `get_backend` registry, so
// every entry here is expected to construct -- a construction failure
// below is a real regression, not an expected engine mismatch. See #156.
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, "live_sum_over_no_matching_rows_is_null", 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 {
// Only validate Rust syntax with syn for Rust backends
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"live_sum_over_no_matching_rows_is_null"
);
} else {
// Structural validation for non-Rust backends
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"live_sum_over_no_matching_rows_is_null",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"live_sum_over_no_matching_rows_is_null"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"live_sum_over_no_matching_rows_is_null"
);
}
}
}
#[test]
fn test_live_case_without_else_is_null() {
// From: testing_data/nullability_live/case_nullable/live_case_without_else_is_null.json
// "A CASE expression with no ELSE branch falls through to NULL when no WHEN matches, so its result is nullable even when every branch returns a NOT NULL column"
let schema_sql = &[
"CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now())",
];
let query_sql = "-- @name GetNamesOfUsersWithEmail\n-- @returns :many\nSELECT u.id, CASE WHEN u.email IS NOT NULL THEN u.name END AS conditional_name FROM users u ORDER BY u.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, "GetNamesOfUsersWithEmail", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
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, "conditional_name", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "string",
"column neutral_type for conditional_name"
);
assert!(analyzed.columns[1].nullable, "column nullable for conditional_name");
// Codegen verification: every backend that supports this fixture's engine
// should produce valid output. The list below is derived at generation
// time from the fixture's engine via the real `get_backend` registry, so
// every entry here is expected to construct -- a construction failure
// below is a real regression, not an expected engine mismatch. See #156.
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, "live_case_without_else_is_null", 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 {
// Only validate Rust syntax with syn for Rust backends
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"live_case_without_else_is_null"
);
} else {
// Structural validation for non-Rust backends
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"live_case_without_else_is_null",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"live_case_without_else_is_null"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"live_case_without_else_is_null"
);
}
}
}
#[test]
fn test_live_coalesce_with_empty_string_default_is_null_on_oracle() {
// From: testing_data/nullability_live/coalesce_non_null/live_coalesce_with_empty_string_default_is_null_on_oracle.json
// "COALESCE over a nullable column with an EMPTY string default is non-nullable on every engine except Oracle, which stores '' as NULL; the analyzer must be dialect-aware here or it renders a non-optional column that Oracle then returns NULL for"
let schema_sql = &[
"CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now())",
];
let query_sql = "-- @name ListUserEmailsWithEmptyDefault\n-- @returns :many\nSELECT id, COALESCE(email, '') AS email_or_empty FROM users ORDER BY 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, "ListUserEmailsWithEmptyDefault", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
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, "email_or_empty", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "string",
"column neutral_type for email_or_empty"
);
assert!(!analyzed.columns[1].nullable, "column nullable for email_or_empty");
// Codegen verification: every backend that supports this fixture's engine
// should produce valid output. The list below is derived at generation
// time from the fixture's engine via the real `get_backend` registry, so
// every entry here is expected to construct -- a construction failure
// below is a real regression, not an expected engine mismatch. See #156.
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, "live_coalesce_with_empty_string_default_is_null_on_oracle", 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 {
// Only validate Rust syntax with syn for Rust backends
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"live_coalesce_with_empty_string_default_is_null_on_oracle"
);
} else {
// Structural validation for non-Rust backends
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"live_coalesce_with_empty_string_default_is_null_on_oracle",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"live_coalesce_with_empty_string_default_is_null_on_oracle"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"live_coalesce_with_empty_string_default_is_null_on_oracle"
);
}
}
}
#[test]
fn test_live_coalesce_with_literal_default_is_never_null() {
// From: testing_data/nullability_live/coalesce_non_null/live_coalesce_with_literal_default_is_never_null.json
// "COALESCE over a nullable column with a literal default is inferred non-nullable; the live engine must never return NULL for it, including for the row whose underlying column is NULL"
let schema_sql = &[
"CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now())",
];
let query_sql = "-- @name ListUserEmailsWithDefault\n-- @returns :many\nSELECT id, COALESCE(email, 'none@example.com') AS email_or_default FROM users ORDER BY 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, "ListUserEmailsWithDefault", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
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, "email_or_default", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "string",
"column neutral_type for email_or_default"
);
assert!(!analyzed.columns[1].nullable, "column nullable for email_or_default");
// Codegen verification: every backend that supports this fixture's engine
// should produce valid output. The list below is derived at generation
// time from the fixture's engine via the real `get_backend` registry, so
// every entry here is expected to construct -- a construction failure
// below is a real regression, not an expected engine mismatch. See #156.
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, "live_coalesce_with_literal_default_is_never_null", 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 {
// Only validate Rust syntax with syn for Rust backends
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"live_coalesce_with_literal_default_is_never_null"
);
} else {
// Structural validation for non-Rust backends
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"live_coalesce_with_literal_default_is_never_null",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"live_coalesce_with_literal_default_is_never_null"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"live_coalesce_with_literal_default_is_never_null"
);
}
}
}
#[test]
fn test_live_age_on_nullable_operand_is_null() {
// From: testing_data/nullability_live/date_functions/live_age_on_nullable_operand_is_null.json
// "AGE over a nullable operand is observed NULL against a live engine, not merely asserted from the model -- the #163 gap: no live fixture exercised AGE at all, so a fix that only changed the analyzer's model of AGE(NOW(), NULL) could regress silently. AGE is PostgreSQL-only (no MySQL/MariaDB/SQLite/MSSQL/Oracle equivalent that this suite's single shared query_sql could target), so this fixture runs on postgresql alone."
let schema_sql = &["CREATE TABLE accounts (id SERIAL PRIMARY KEY, deleted_at TIMESTAMPTZ);"];
let query_sql =
"-- @name AgeLive\n-- @returns :many\nSELECT id, AGE(NOW(), deleted_at) AS elapsed FROM accounts ORDER BY 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, "AgeLive", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
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, "elapsed", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "interval",
"column neutral_type for elapsed"
);
assert!(analyzed.columns[1].nullable, "column nullable for elapsed");
// Codegen verification: every backend that supports this fixture's engine
// should produce valid output. The list below is derived at generation
// time from the fixture's engine via the real `get_backend` registry, so
// every entry here is expected to construct -- a construction failure
// below is a real regression, not an expected engine mismatch. See #156.
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, "live_age_on_nullable_operand_is_null", 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 {
// Only validate Rust syntax with syn for Rust backends
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"live_age_on_nullable_operand_is_null"
);
} else {
// Structural validation for non-Rust backends
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"live_age_on_nullable_operand_is_null",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"live_age_on_nullable_operand_is_null"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"live_age_on_nullable_operand_is_null"
);
}
}
}
#[test]
fn test_live_full_join_makes_both_sides_nullable() {
// From: testing_data/nullability_live/full_join_nullable/live_full_join_makes_both_sides_nullable.json
// "FULL JOIN makes NOT NULL columns on BOTH sides nullable, since a row can be unmatched in either direction. Joins users to tags rather than users to orders: orders.user_id carries a foreign key to users, so no order can ever be unmatched and the join would degenerate to a LEFT JOIN, proving only half the rule."
let schema_sql = &[
"CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now())",
"CREATE TABLE tags (id SERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE)",
];
let query_sql = "-- @name GetUsersFullJoinedToTags\n-- @returns :many\nSELECT u.name AS user_name, t.name AS tag_name FROM users u FULL JOIN tags t ON u.id = t.id ORDER BY COALESCE(u.id, t.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, "GetUsersFullJoinedToTags", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
assert_eq!(analyzed.columns.len(), 2, "column count");
assert_eq!(analyzed.columns[0].name, "user_name", "column name");
assert_eq!(
analyzed.columns[0].neutral_type, "string",
"column neutral_type for user_name"
);
assert!(analyzed.columns[0].nullable, "column nullable for user_name");
assert_eq!(analyzed.columns[1].name, "tag_name", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "string",
"column neutral_type for tag_name"
);
assert!(analyzed.columns[1].nullable, "column nullable for tag_name");
// Codegen verification: every backend that supports this fixture's engine
// should produce valid output. The list below is derived at generation
// time from the fixture's engine via the real `get_backend` registry, so
// every entry here is expected to construct -- a construction failure
// below is a real regression, not an expected engine mismatch. See #156.
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, "live_full_join_makes_both_sides_nullable", 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 {
// Only validate Rust syntax with syn for Rust backends
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"live_full_join_makes_both_sides_nullable"
);
} else {
// Structural validation for non-Rust backends
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"live_full_join_makes_both_sides_nullable",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"live_full_join_makes_both_sides_nullable"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"live_full_join_makes_both_sides_nullable"
);
}
}
}
#[test]
fn test_live_right_side_not_null_becomes_nullable() {
// From: testing_data/nullability_live/left_join_nullable/live_right_side_not_null_becomes_nullable.json
// "LEFT JOIN makes right-side NOT NULL columns nullable, verified against a live engine rather than just the analyzer"
let schema_sql = &[
"CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now())",
"CREATE TABLE orders (id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id), total NUMERIC(10, 2) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now())",
];
let query_sql = "-- @name GetUsersWithOrderTotals\n-- @returns :many\nSELECT u.id, o.total, o.created_at AS order_created_at FROM users u LEFT JOIN orders o ON u.id = o.user_id ORDER BY u.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, "GetUsersWithOrderTotals", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
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, "total", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "decimal",
"column neutral_type for total"
);
assert!(analyzed.columns[1].nullable, "column nullable for total");
assert_eq!(analyzed.columns[2].name, "order_created_at", "column name");
assert_eq!(
analyzed.columns[2].neutral_type, "datetime_tz",
"column neutral_type for order_created_at"
);
assert!(analyzed.columns[2].nullable, "column nullable for order_created_at");
// Codegen verification: every backend that supports this fixture's engine
// should produce valid output. The list below is derived at generation
// time from the fixture's engine via the real `get_backend` registry, so
// every entry here is expected to construct -- a construction failure
// below is a real regression, not an expected engine mismatch. See #156.
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, "live_right_side_not_null_becomes_nullable", 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 {
// Only validate Rust syntax with syn for Rust backends
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"live_right_side_not_null_becomes_nullable"
);
} else {
// Structural validation for non-Rust backends
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"live_right_side_not_null_becomes_nullable",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"live_right_side_not_null_becomes_nullable"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"live_right_side_not_null_becomes_nullable"
);
}
}
}
#[test]
fn test_live_power_sqrt_on_nullable_operand_is_null() {
// From: testing_data/nullability_live/math_functions/live_power_sqrt_on_nullable_operand_is_null.json
// "POWER and SQRT over a nullable operand are observed NULL against live engines, not merely asserted from the model -- the #163 gap: no live fixture exercised either function, so a fix that only changed the analyzer's model could regress silently. SQLite is excluded: this suite's SQLite leg links rusqlite's bundled amalgamation, and libsqlite3-sys's build.rs does not pass -DSQLITE_ENABLE_MATH_FUNCTIONS, so SQRT/POWER are compiled out there too, not merely absent from the system sqlite3 binary -- see the sqlite_leg_lacks_math_functions regression test in crates/scythe-conformance/tests/live.rs, which fails the build if that ever stops being true."
let schema_sql = &["CREATE TABLE measurements (id SERIAL PRIMARY KEY, value DOUBLE PRECISION);"];
let query_sql = "-- @name PowerSqrtLive\n-- @returns :many\nSELECT id, POWER(value, 2) AS squared, SQRT(value) AS root FROM measurements ORDER BY 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, "PowerSqrtLive", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
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, "squared", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "float64",
"column neutral_type for squared"
);
assert!(analyzed.columns[1].nullable, "column nullable for squared");
assert_eq!(analyzed.columns[2].name, "root", "column name");
assert_eq!(
analyzed.columns[2].neutral_type, "float64",
"column neutral_type for root"
);
assert!(analyzed.columns[2].nullable, "column nullable for root");
// Codegen verification: every backend that supports this fixture's engine
// should produce valid output. The list below is derived at generation
// time from the fixture's engine via the real `get_backend` registry, so
// every entry here is expected to construct -- a construction failure
// below is a real regression, not an expected engine mismatch. See #156.
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, "live_power_sqrt_on_nullable_operand_is_null", 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 {
// Only validate Rust syntax with syn for Rust backends
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"live_power_sqrt_on_nullable_operand_is_null"
);
} else {
// Structural validation for non-Rust backends
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"live_power_sqrt_on_nullable_operand_is_null",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"live_power_sqrt_on_nullable_operand_is_null"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"live_power_sqrt_on_nullable_operand_is_null"
);
}
}
}
#[test]
fn test_live_nullable_column_observes_null_and_non_null() {
// From: testing_data/nullability_live/nullable_column/live_nullable_column_observes_null_and_non_null.json
// "A column nullable in the schema is observed both NULL and non-NULL against a live engine, so the analyzer's nullable claim is demonstrated rather than merely asserted"
let schema_sql = &[
"CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now())",
];
let query_sql = "-- @name ListUserEmails\n-- @returns :many\nSELECT id, email FROM users ORDER BY 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, "ListUserEmails", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
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, "email", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "string",
"column neutral_type for email"
);
assert!(analyzed.columns[1].nullable, "column nullable for email");
// Codegen verification: every backend that supports this fixture's engine
// should produce valid output. The list below is derived at generation
// time from the fixture's engine via the real `get_backend` registry, so
// every entry here is expected to construct -- a construction failure
// below is a real regression, not an expected engine mismatch. See #156.
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, "live_nullable_column_observes_null_and_non_null", 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 {
// Only validate Rust syntax with syn for Rust backends
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"live_nullable_column_observes_null_and_non_null"
);
} else {
// Structural validation for non-Rust backends
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"live_nullable_column_observes_null_and_non_null",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"live_nullable_column_observes_null_and_non_null"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"live_nullable_column_observes_null_and_non_null"
);
}
}
}
#[test]
fn test_live_nullif_with_equal_operands_is_null() {
// From: testing_data/nullability_live/nullif_nullable/live_nullif_with_equal_operands_is_null.json
// "NULLIF returns NULL when its two operands are equal, so its result is nullable even when the input column is NOT NULL -- proven against a live engine rather than asserted"
let schema_sql = &[
"CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now())",
];
let query_sql = "-- @name GetUserNamesExceptAda\n-- @returns :many\nSELECT u.id, NULLIF(u.name, 'Ada Lovelace') AS maybe_name FROM users u ORDER BY u.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, "GetUserNamesExceptAda", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
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, "maybe_name", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "string",
"column neutral_type for maybe_name"
);
assert!(analyzed.columns[1].nullable, "column nullable for maybe_name");
// Codegen verification: every backend that supports this fixture's engine
// should produce valid output. The list below is derived at generation
// time from the fixture's engine via the real `get_backend` registry, so
// every entry here is expected to construct -- a construction failure
// below is a real regression, not an expected engine mismatch. See #156.
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, "live_nullif_with_equal_operands_is_null", 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 {
// Only validate Rust syntax with syn for Rust backends
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"live_nullif_with_equal_operands_is_null"
);
} else {
// Structural validation for non-Rust backends
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"live_nullif_with_equal_operands_is_null",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"live_nullif_with_equal_operands_is_null"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"live_nullif_with_equal_operands_is_null"
);
}
}
}
#[test]
fn test_live_like_on_nullable_operand_is_null() {
// From: testing_data/nullability_live/pattern_matching/live_like_on_nullable_operand_is_null.json
// "LIKE over a nullable column is observed NULL against a live engine, not merely asserted from the model -- the #163 gap: no live fixture exercised LIKE at all, so a fix that only changed the analyzer's model of NULL LIKE 'x' could regress silently. Restricted to the engines where a LIKE predicate is a legal value expression in a select list: T-SQL and Oracle have no boolean type there, so MSSQL fails with 'Incorrect syntax near the keyword LIKE' and Oracle with ORA-00923 before any nullability is observed. Covering them needs a CASE wrapper, and the harness runs one literal query_sql per engine with no per-engine rewrite."
let schema_sql = &[
"CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now())",
];
let query_sql = "-- @name LikeOnNullableEmail\n-- @returns :many\nSELECT id, email LIKE 'a%' AS starts_with_a FROM users ORDER BY 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, "LikeOnNullableEmail", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
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, "starts_with_a", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "bool",
"column neutral_type for starts_with_a"
);
assert!(analyzed.columns[1].nullable, "column nullable for starts_with_a");
// Codegen verification: every backend that supports this fixture's engine
// should produce valid output. The list below is derived at generation
// time from the fixture's engine via the real `get_backend` registry, so
// every entry here is expected to construct -- a construction failure
// below is a real regression, not an expected engine mismatch. See #156.
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, "live_like_on_nullable_operand_is_null", 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 {
// Only validate Rust syntax with syn for Rust backends
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"live_like_on_nullable_operand_is_null"
);
} else {
// Structural validation for non-Rust backends
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"live_like_on_nullable_operand_is_null",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"live_like_on_nullable_operand_is_null"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"live_like_on_nullable_operand_is_null"
);
}
}
}
#[test]
fn test_live_right_join_left_side_not_null_becomes_nullable() {
// From: testing_data/nullability_live/right_join_nullable/live_left_side_not_null_becomes_nullable.json
// "RIGHT JOIN makes left-side NOT NULL columns nullable -- the mirror of the LEFT JOIN rule, and a separate code path in the analyzer"
let schema_sql = &[
"CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now())",
"CREATE TABLE orders (id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id), total NUMERIC(10, 2) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now())",
];
let query_sql = "-- @name GetUsersWithOrdersRightJoined\n-- @returns :many\nSELECT u.id, o.id AS order_id, o.total FROM orders o RIGHT JOIN users u ON o.user_id = u.id ORDER BY u.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, "GetUsersWithOrdersRightJoined", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
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, "order_id", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "int32",
"column neutral_type for order_id"
);
assert!(analyzed.columns[1].nullable, "column nullable for order_id");
assert_eq!(analyzed.columns[2].name, "total", "column name");
assert_eq!(
analyzed.columns[2].neutral_type, "decimal",
"column neutral_type for total"
);
assert!(analyzed.columns[2].nullable, "column nullable for total");
// Codegen verification: every backend that supports this fixture's engine
// should produce valid output. The list below is derived at generation
// time from the fixture's engine via the real `get_backend` registry, so
// every entry here is expected to construct -- a construction failure
// below is a real regression, not an expected engine mismatch. See #156.
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, "live_right_join_left_side_not_null_becomes_nullable", 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 {
// Only validate Rust syntax with syn for Rust backends
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"live_right_join_left_side_not_null_becomes_nullable"
);
} else {
// Structural validation for non-Rust backends
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"live_right_join_left_side_not_null_becomes_nullable",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"live_right_join_left_side_not_null_becomes_nullable"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"live_right_join_left_side_not_null_becomes_nullable"
);
}
}
}
#[test]
fn test_live_scalar_subquery_with_no_match_is_null() {
// From: testing_data/nullability_live/scalar_subquery_nullable/live_scalar_subquery_with_no_match_is_null.json
// "A scalar subquery that matches no row evaluates to NULL, so its column is nullable even though the selected column is NOT NULL in the schema"
let schema_sql = &[
"CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now())",
"CREATE TABLE orders (id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id), total NUMERIC(10, 2) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now())",
];
let query_sql = "-- @name GetUsersWithTheirOnlyOrderTotal\n-- @returns :many\nSELECT u.id, (SELECT o.total FROM orders o WHERE o.user_id = u.id) AS only_order_total FROM users u ORDER BY u.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, "GetUsersWithTheirOnlyOrderTotal", "query name");
assert_eq!(analyzed.command.to_string(), "many", "query command");
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, "only_order_total", "column name");
assert_eq!(
analyzed.columns[1].neutral_type, "decimal",
"column neutral_type for only_order_total"
);
assert!(analyzed.columns[1].nullable, "column nullable for only_order_total");
// Codegen verification: every backend that supports this fixture's engine
// should produce valid output. The list below is derived at generation
// time from the fixture's engine via the real `get_backend` registry, so
// every entry here is expected to construct -- a construction failure
// below is a real regression, not an expected engine mismatch. See #156.
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, "live_scalar_subquery_with_no_match_is_null", 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 {
// Only validate Rust syntax with syn for Rust backends
if *backend_name == "rust-sqlx" || *backend_name == "rust-tokio-postgres" {
assert!(
syn::parse_file(&code).is_ok(),
"backend {} generated invalid Rust for {}",
backend_name,
"live_scalar_subquery_with_no_match_is_null"
);
} else {
// Structural validation for non-Rust backends
let errors = scythe_codegen::validation::validate_structural(&code, backend_name);
assert!(
errors.is_empty(),
"backend {} structural validation failed for {}: {:?}",
backend_name,
"live_scalar_subquery_with_no_match_is_null",
errors
);
}
}
assert!(
generated.row_struct.is_some() || generated.model_struct.is_some(),
"backend {} should produce a struct for {}",
backend_name,
"live_scalar_subquery_with_no_match_is_null"
);
assert!(
generated.query_fn.is_some(),
"backend {} should produce query_fn for {}",
backend_name,
"live_scalar_subquery_with_no_match_is_null"
);
}
}
}