#![allow(clippy::unwrap_used, clippy::print_stdout, clippy::print_stderr)]
use super::*;
#[test]
fn test_postgres_projection_single_field() {
let generator = PostgresProjectionGenerator::new();
let fields = vec!["id".to_string()];
let sql = generator.generate_projection_sql(&fields).unwrap();
assert_eq!(sql, "jsonb_build_object('id', \"data\"->>'id' )");
}
#[test]
fn test_postgres_projection_multiple_fields() {
let generator = PostgresProjectionGenerator::new();
let fields = vec!["id".to_string(), "name".to_string(), "email".to_string()];
let sql = generator.generate_projection_sql(&fields).unwrap();
assert!(sql.contains("jsonb_build_object("));
assert!(sql.contains("'id', \"data\"->>'id'"));
assert!(sql.contains("'name', \"data\"->>'name'"));
assert!(sql.contains("'email', \"data\"->>'email'"));
}
#[test]
fn test_postgres_projection_digit_field_maps_to_snake_key() {
let generator = PostgresProjectionGenerator::new();
let fields = vec!["phone1".to_string(), "userId".to_string()];
let sql = generator.generate_projection_sql(&fields).unwrap();
assert!(sql.contains("'phone1', \"data\"->>'phone_1'"), "got: {sql}");
assert!(sql.contains("'userId', \"data\"->>'user_id'"), "got: {sql}");
}
#[test]
fn test_postgres_projection_empty_fields() {
let generator = PostgresProjectionGenerator::new();
let fields: Vec<String> = vec![];
let sql = generator.generate_projection_sql(&fields).unwrap();
assert_eq!(sql, "\"data\"");
}
#[test]
fn test_postgres_projection_custom_column() {
let generator = PostgresProjectionGenerator::with_column("metadata");
let fields = vec!["id".to_string()];
let sql = generator.generate_projection_sql(&fields).unwrap();
assert_eq!(sql, "jsonb_build_object('id', \"metadata\"->>'id' )");
}
#[test]
fn test_postgres_select_clause() {
let generator = PostgresProjectionGenerator::new();
let fields = vec!["id".to_string(), "name".to_string()];
let sql = generator.generate_select_clause("users", &fields).unwrap();
assert!(sql.starts_with("SELECT jsonb_build_object("));
assert!(sql.contains("as \"data\""));
assert!(sql.contains("FROM \"users\""));
}
#[test]
fn test_to_snake_case_conversion() {
assert_eq!(super::to_snake_case("id"), "id");
assert_eq!(super::to_snake_case("firstName"), "first_name");
assert_eq!(super::to_snake_case("createdAt"), "created_at");
assert_eq!(super::to_snake_case("userId"), "user_id");
assert_eq!(super::to_snake_case("updatedAtTimestamp"), "updated_at_timestamp");
}
#[test]
fn test_postgres_projection_with_field_mapping_snake_case() {
let generator = PostgresProjectionGenerator::new();
let graphql_fields = vec![
"id".to_string(),
"firstName".to_string(),
"createdAt".to_string(),
];
let sql = generator.generate_projection_sql(&graphql_fields).unwrap();
eprintln!("Generated SQL: {}", sql);
assert!(
!sql.contains("->>'firstName'") && !sql.contains("->>'createdAt'"),
"Regression: SQL is using camelCase keys for JSONB access. \
JSONB has snake_case keys ('first_name', 'created_at'). SQL: {}",
sql
);
}
#[test]
fn test_postgres_projection_sql_injection_in_field_name() {
let generator = PostgresProjectionGenerator::new();
let fields = vec!["user'name".to_string()];
let result = generator.generate_projection_sql(&fields);
assert!(result.is_err(), "Field name with single quote must be rejected");
}
#[test]
fn test_postgres_projection_rejects_field_with_semicolon() {
let generator = PostgresProjectionGenerator::new();
let fields = vec!["id; DROP TABLE users--".to_string()];
let result = generator.generate_projection_sql(&fields);
assert!(result.is_err(), "Field name with SQL injection characters must be rejected");
}
#[test]
fn test_validate_field_name_accepts_valid_names() {
assert!(super::validate_field_name("id").is_ok());
assert!(super::validate_field_name("user_id").is_ok());
assert!(super::validate_field_name("firstName").is_ok());
assert!(super::validate_field_name("createdAt").is_ok());
assert!(super::validate_field_name("field123").is_ok());
assert!(super::validate_field_name("_private").is_ok());
}
#[test]
fn test_validate_field_name_rejects_unsafe_chars() {
assert!(super::validate_field_name("user'name").is_err());
assert!(super::validate_field_name("field-name").is_err());
assert!(super::validate_field_name("field.name").is_err());
assert!(super::validate_field_name("field;inject").is_err());
assert!(super::validate_field_name("field\"inject").is_err());
assert!(super::validate_field_name("field`hack").is_err());
}
#[test]
fn test_postgres_projection_camel_to_snake_in_jsonb_key() {
let generator = PostgresProjectionGenerator::new();
let fields = vec!["updatedAt".to_string()];
let sql = generator.generate_projection_sql(&fields).unwrap();
assert!(
sql.contains("'updated_at'"),
"updatedAt must be mapped to updated_at for JSONB key"
);
assert!(sql.contains("'updatedAt'"), "Response key must remain camelCase");
}
#[test]
fn test_postgres_select_clause_contains_from() {
let generator = PostgresProjectionGenerator::new();
let fields = vec!["id".to_string()];
let sql = generator.generate_select_clause("orders", &fields).unwrap();
assert!(
sql.contains("FROM \"orders\""),
"SELECT clause must include FROM clause with table name"
);
assert!(sql.contains("SELECT"), "SELECT clause must start with SELECT");
}
#[test]
fn test_typed_projection_empty_fields_projects_nothing() {
let generator = PostgresProjectionGenerator::new();
let result = generator.generate_typed_projection_sql(&[]).unwrap();
assert_eq!(result, "jsonb_build_object()");
}
#[test]
fn test_typed_projection_text_field_uses_text_extraction() {
let generator = PostgresProjectionGenerator::new();
let fields = vec![ProjectionField::scalar("name")];
let sql = generator.generate_typed_projection_sql(&fields).unwrap();
assert!(sql.contains("->>'name'"), "text field must use ->> operator, got: {sql}");
assert!(!sql.contains("->'name'"), "text field must NOT use -> operator, got: {sql}");
}
#[test]
fn test_typed_projection_composite_field_uses_jsonb_extraction() {
let generator = PostgresProjectionGenerator::new();
let fields = vec![ProjectionField::composite("address")];
let sql = generator.generate_typed_projection_sql(&fields).unwrap();
assert!(sql.contains("->'address'"), "composite field must use -> operator, got: {sql}");
}
#[test]
fn test_typed_projection_mixed_text_native_and_composite() {
let generator = PostgresProjectionGenerator::new();
let fields = vec![
ProjectionField::scalar("id"),
ProjectionField::native("age"),
ProjectionField::composite("address"),
ProjectionField::composite("tags"),
ProjectionField::scalar("email"),
];
let sql = generator.generate_typed_projection_sql(&fields).unwrap();
assert!(sql.contains("->>'id'"), "id (text) must use ->>, got: {sql}");
assert!(sql.contains("->>'email'"), "email (text) must use ->>, got: {sql}");
assert!(sql.contains("->'age'"), "age (native) must use ->, got: {sql}");
assert!(sql.contains("->'address'"), "address (composite) must use ->, got: {sql}");
assert!(sql.contains("->'tags'"), "tags (composite) must use ->, got: {sql}");
assert!(
sql.starts_with("jsonb_build_object("),
"must wrap in jsonb_build_object, got: {sql}"
);
}
#[test]
fn test_typed_projection_camel_case_maps_to_snake_case_jsonb_key() {
let generator = PostgresProjectionGenerator::new();
let fields = vec![ProjectionField::scalar("firstName")];
let sql = generator.generate_typed_projection_sql(&fields).unwrap();
assert!(
sql.contains("'firstName'"),
"response key must be camelCase 'firstName', got: {sql}"
);
assert!(
sql.contains("->>'first_name'"),
"JSONB key must be snake_case 'first_name', got: {sql}"
);
}
#[test]
fn test_typed_projection_single_quote_in_field_name_escaped() {
let generator = PostgresProjectionGenerator::new();
let fields = vec![ProjectionField::scalar("it's")];
let sql = generator.generate_typed_projection_sql(&fields).unwrap();
assert!(
sql.contains("'it''s'"),
"single quote in field name must be escaped, got: {sql}"
);
}
#[test]
fn test_native_field_uses_jsonb_extraction() {
let generator = PostgresProjectionGenerator::new();
let fields = vec![
ProjectionField::native("isActive"),
ProjectionField::scalar("name"),
];
let sql = generator.generate_typed_projection_sql(&fields).unwrap();
assert!(sql.contains("->'is_active'"), "native field must use -> operator, got: {sql}");
assert!(sql.contains("->>'name'"), "text scalar field must use ->> operator, got: {sql}");
}
#[test]
fn test_native_field_mixed_with_composite() {
let generator = PostgresProjectionGenerator::new();
let fields = vec![
ProjectionField::native("isActive"),
ProjectionField::composite("address"),
ProjectionField::scalar("email"),
];
let sql = generator.generate_typed_projection_sql(&fields).unwrap();
assert!(sql.contains("->'is_active'"), "native uses ->, got: {sql}");
assert!(sql.contains("->'address'"), "composite uses ->, got: {sql}");
assert!(sql.contains("->>'email'"), "text scalar uses ->>, got: {sql}");
}
#[test]
fn test_native_int_and_float_use_jsonb_extraction() {
let generator = PostgresProjectionGenerator::new();
let fields = vec![
ProjectionField::native("age"),
ProjectionField::native("price"),
ProjectionField::scalar("name"),
ProjectionField::scalar("id"),
];
let sql = generator.generate_typed_projection_sql(&fields).unwrap();
assert!(sql.contains("->'age'"), "int (native) must use ->, got: {sql}");
assert!(sql.contains("->'price'"), "float (native) must use ->, got: {sql}");
assert!(sql.contains("->>'name'"), "string (text) must use ->>, got: {sql}");
assert!(sql.contains("->>'id'"), "id (text) must use ->>, got: {sql}");
}
#[test]
fn test_typed_projection_nested_sub_fields_generate_nested_jsonb_build_object() {
let generator = PostgresProjectionGenerator::new();
let fields = vec![
ProjectionField::scalar("id"),
ProjectionField::scalar("content"),
ProjectionField::composite_with_sub_fields(
"author",
vec![
ProjectionField::scalar("id"),
ProjectionField::scalar("username"),
ProjectionField::scalar("fullName"),
],
),
];
let sql = generator.generate_typed_projection_sql(&fields).unwrap();
assert!(
sql.contains("'author', CASE WHEN jsonb_typeof("),
"author must produce nested jsonb_build_object, got: {sql}"
);
assert!(
sql.contains("'author'->>'id'"),
"nested 'id' must use path \"data\"->'author'->>'id', got: {sql}"
);
assert!(
sql.contains("'author'->>'username'"),
"nested 'username' must use correct path, got: {sql}"
);
assert!(
sql.contains("->>'full_name'"),
"fullName sub-field must map to snake_case 'full_name', got: {sql}"
);
assert!(
sql.contains("\"data\"->>'id'"),
"root id must use top-level data path, got: {sql}"
);
}
#[test]
fn test_typed_projection_composite_without_sub_fields_returns_full_blob() {
let generator = PostgresProjectionGenerator::new();
let fields = vec![ProjectionField::composite("author")];
let sql = generator.generate_typed_projection_sql(&fields).unwrap();
assert!(
sql.contains("\"data\"->'author'"),
"composite without sub_fields must return full blob, got: {sql}"
);
assert!(
!sql.contains("'author', CASE WHEN jsonb_typeof("),
"must NOT produce nested jsonb_build_object for author when sub_fields is None, got: {sql}"
);
}
#[test]
fn test_typed_projection_depth_2_recursion() {
let generator = PostgresProjectionGenerator::new();
let fields = vec![
ProjectionField::scalar("id"),
ProjectionField::composite_with_sub_fields(
"author",
vec![
ProjectionField::scalar("id"),
ProjectionField::composite_with_sub_fields(
"profile",
vec![ProjectionField::scalar("bio")],
),
],
),
];
let sql = generator.generate_typed_projection_sql(&fields).unwrap();
assert!(
sql.contains("'author', CASE WHEN jsonb_typeof("),
"author must be nested, got: {sql}"
);
assert!(
sql.contains("'profile', CASE WHEN jsonb_typeof("),
"profile must be nested inside author, got: {sql}"
);
assert!(sql.contains("'profile'->>'bio'"), "bio must use depth-2 path, got: {sql}");
}
#[test]
fn test_typed_projection_recurses_at_every_depth() {
for depth in [10, 50] {
let mut field = ProjectionField::composite_with_sub_fields(
"parent",
vec![ProjectionField::scalar("id")],
);
for _ in 1..depth {
field = ProjectionField::composite_with_sub_fields("parent", vec![field]);
}
let sql = PostgresProjectionGenerator::new()
.generate_typed_projection_sql(&[field])
.unwrap();
assert_eq!(sql.matches("jsonb_build_object(").count(), depth + 1, "{sql}");
let leaf = format!("\"data\"{}->>'id'", "->'parent'".repeat(depth));
assert!(sql.contains(&leaf), "{sql}");
}
}
#[test]
fn test_typed_projection_of_an_object_with_no_sub_fields_is_empty() {
let field = ProjectionField::composite_with_sub_fields("author", vec![]);
let sql = PostgresProjectionGenerator::new()
.generate_typed_projection_sql(&[field])
.unwrap();
assert_eq!(
sql,
"jsonb_build_object('author', CASE WHEN jsonb_typeof(\"data\"->'author') = 'object' \
THEN jsonb_build_object() END)"
);
}