use scythe_codegen::{GeneratedCode, generate_with_backend, get_backend};
use scythe_core::analyzer::{NestedFieldInfo, NestedStructInfo, analyze};
use scythe_core::catalog::Catalog;
use scythe_core::dialect::SqlDialect;
use scythe_core::errors::ScytheError;
use scythe_core::parser::parse_query_with_dialect;
const NESTED_ARRAY_SCHEMA: &str = "\
CREATE TYPE order_status AS ENUM ('pending', 'shipped'); \
CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT NOT NULL); \
CREATE TABLE orders (id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL, status order_status NOT NULL);";
const NESTED_ARRAY_QUERY: &str = "-- @name GetUserOrders\n-- @returns :many\n\
SELECT u.id, json_agg(o.*) AS orders FROM users u JOIN orders o ON o.user_id = u.id GROUP BY u.id;";
fn generate(backend_name: &str) -> Result<GeneratedCode, ScytheError> {
let backend = get_backend(backend_name, "postgresql").expect("backend must support postgresql");
let catalog =
Catalog::from_ddl_with_dialect(&[NESTED_ARRAY_SCHEMA], &SqlDialect::PostgreSQL).expect("schema must parse");
let parsed = parse_query_with_dialect(NESTED_ARRAY_QUERY, &SqlDialect::PostgreSQL).expect("query must parse");
let analyzed = analyze(&catalog, &parsed).expect("query must analyze");
generate_with_backend(&analyzed, &*backend)
}
#[test]
fn unsupported_backend_reports_the_degraded_column_struct_and_backend() {
let code = generate("java-jdbc").expect("degradation is not a hard error by default");
assert_eq!(
code.degraded_nested_structs.len(),
1,
"exactly one column (orders) referenced the unsupported struct: {:?}",
code.degraded_nested_structs
);
let degradation = &code.degraded_nested_structs[0];
assert_eq!(degradation.column, "orders");
assert_eq!(degradation.struct_name, "GetUserOrdersRowOrders");
assert_eq!(degradation.fallback_type, "json");
assert_eq!(degradation.backend, "java-jdbc");
assert!(code.row_struct.is_some(), "codegen must still produce a row struct");
}
#[test]
fn typescript_postgresql_backends_emit_native_nested_structs() {
for backend in ["typescript-pg", "typescript-postgres", "typescript-kysely"] {
let code = generate(backend).expect("TypeScript nested JSON codegen must succeed");
assert!(
code.degraded_nested_structs.is_empty(),
"{backend} must preserve nested JSON structure: {:?}",
code.degraded_nested_structs
);
assert_eq!(
code.nested_struct_defs.len(),
1,
"{backend} must emit one nested interface"
);
assert!(
code.nested_struct_defs[0].code.contains("status: OrderStatus;"),
"{backend} must preserve an enum inside the JSON object"
);
assert!(
code.row_struct
.as_deref()
.is_some_and(|row| row.contains("orders: Array<GetUserOrdersRowOrders> | null;")),
"{backend} must use Array<T> around the nested interface"
);
}
}
#[test]
fn javascript_aliases_keep_the_plain_json_fallback() {
for backend in ["javascript-pg", "javascript-postgres"] {
let code = generate(backend).expect("JavaScript fallback codegen must succeed");
assert_eq!(code.degraded_nested_structs.len(), 1);
assert_eq!(code.degraded_nested_structs[0].fallback_type, "json_array");
assert!(code.nested_struct_defs.is_empty());
}
}
#[test]
fn unsupported_future_json_field_degrades_the_entire_nested_struct() {
let nested = NestedStructInfo {
name: "future_payload".to_string(),
fields: vec![NestedFieldInfo {
name: "location".to_string(),
neutral_type: "future_geography".to_string(),
nullable: false,
}],
};
for backend_name in ["typescript-pg", "typescript-postgres", "typescript-kysely"] {
let backend = get_backend(backend_name, "postgresql").expect("backend must support PostgreSQL");
assert!(
backend
.generate_nested_struct_def(&nested)
.expect("unsupported nested field must not be a hard error")
.is_none(),
"{backend_name} must request deterministic plain-JSON degradation"
);
}
}
#[test]
fn supporting_backend_reports_no_degradation() {
let code = generate("rust-sqlx").expect("codegen must succeed");
assert!(
code.degraded_nested_structs.is_empty(),
"rust-sqlx supports the nested struct and must not report a degradation: {:?}",
code.degraded_nested_structs
);
assert_eq!(
code.nested_struct_defs.len(),
1,
"the struct definition must still be emitted"
);
}