#![allow(clippy::unwrap_used, clippy::panic)] use field_type::{DistanceMetric, VectorConfig, VectorIndexType};
use super::*;
use crate::validation::CustomTypeRegistry;
#[test]
fn test_empty_schema_creation() {
let schema = CompiledSchema::new();
assert!(schema.types.is_empty());
assert!(schema.queries.is_empty());
assert!(schema.mutations.is_empty());
assert!(schema.subscriptions.is_empty());
assert_eq!(schema.operation_count(), 0);
}
#[test]
fn test_schema_from_json_empty() {
let json = r#"{"types": [], "queries": [], "mutations": [], "subscriptions": []}"#;
let schema = CompiledSchema::from_json(json, false).unwrap();
assert!(schema.types.is_empty());
}
#[test]
fn test_schema_from_json_with_defaults() {
let json = r"{}";
let schema = CompiledSchema::from_json(json, false).unwrap();
assert!(schema.types.is_empty());
assert!(schema.queries.is_empty());
}
#[test]
#[allow(clippy::cognitive_complexity)] fn test_schema_from_json_full() {
let json = r#"{
"types": [{
"name": "User",
"sql_source": "v_user",
"jsonb_column": "data",
"fields": [
{"name": "id", "field_type": "ID", "nullable": false},
{"name": "email", "field_type": "String", "nullable": false},
{"name": "name", "field_type": "String", "nullable": true}
],
"description": "A user in the system"
}],
"queries": [{
"name": "users",
"return_type": "User",
"returns_list": true,
"nullable": false,
"sql_source": "v_user",
"auto_params": {
"has_where": true,
"has_order_by": true,
"has_limit": true,
"has_offset": true
}
}, {
"name": "user",
"return_type": "User",
"returns_list": false,
"nullable": true,
"arguments": [
{"name": "id", "arg_type": "ID", "nullable": false}
]
}],
"mutations": [{
"name": "createUser",
"return_type": "User",
"sql_source": "fn_create_user",
"arguments": [
{"name": "email", "arg_type": "String", "nullable": false}
],
"operation": {"Insert": {"table": "users"}}
}],
"subscriptions": [{
"name": "userCreated",
"return_type": "User",
"topic": "user_created"
}]
}"#;
let schema = CompiledSchema::from_json(json, false).unwrap();
assert_eq!(schema.types.len(), 1);
let user_type = &schema.types[0];
assert_eq!(user_type.name, "User");
assert_eq!(user_type.sql_source, "v_user");
assert_eq!(user_type.jsonb_column, "data");
assert_eq!(user_type.fields.len(), 3);
assert_eq!(user_type.description, Some("A user in the system".to_string()));
assert!(!user_type.is_error);
assert!(!user_type.relay);
assert!(user_type.requires_role.is_none());
assert!(user_type.implements.is_empty());
assert_eq!(schema.queries.len(), 2);
let users_query = schema.find_query("users").unwrap();
assert!(users_query.returns_list);
assert_eq!(users_query.sql_source.as_deref(), Some("v_user"));
assert!(users_query.auto_params.has_where);
assert!(users_query.auto_params.has_order_by);
assert!(users_query.auto_params.has_limit);
assert!(users_query.auto_params.has_offset);
assert!(!users_query.relay);
assert!(users_query.relay_cursor_column.is_none());
assert_eq!(users_query.relay_cursor_type, CursorType::Int64);
assert!(users_query.inject_params.is_empty());
assert!(users_query.cache_ttl_seconds.is_none());
assert!(users_query.additional_views.is_empty());
assert!(users_query.requires_role.is_none());
assert!(users_query.deprecation.is_none());
let user_query = schema.find_query("user").unwrap();
assert!(!user_query.returns_list);
assert!(user_query.nullable);
assert_eq!(user_query.arguments.len(), 1);
assert_eq!(schema.mutations.len(), 1);
let create_user = schema.find_mutation("createUser").unwrap();
assert_eq!(create_user.sql_source.as_deref(), Some("fn_create_user"));
assert_eq!(create_user.arguments.len(), 1);
assert!(matches!(
&create_user.operation,
MutationOperation::Insert { table } if table == "users"
));
assert!(create_user.inject_params.is_empty());
assert!(create_user.invalidates_fact_tables.is_empty());
assert!(create_user.invalidates_views.is_empty());
assert!(create_user.deprecation.is_none());
assert_eq!(schema.subscriptions.len(), 1);
let sub = schema.find_subscription("userCreated").unwrap();
assert_eq!(sub.topic, Some("user_created".to_string()));
}
#[test]
fn test_query_full_fields() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("tests/fixtures/golden/05-security-inject-cache.json");
let json =
std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("Cannot read fixture 05: {e}"));
let schema = CompiledSchema::from_json(&json, false).unwrap();
let q = schema.find_query("orders").unwrap();
assert_eq!(q.inject_params.len(), 1);
let src = q.inject_params.get("tenant_id").unwrap();
assert_eq!(*src, InjectedParamSource::Jwt("tenant_id".to_string()));
assert_eq!(q.cache_ttl_seconds, Some(300));
assert_eq!(q.additional_views, vec!["v_order_summary", "v_order_items"]);
assert_eq!(q.requires_role.as_deref(), Some("admin"));
}
#[test]
fn test_mutation_full_fields() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("tests/fixtures/golden/05-security-inject-cache.json");
let json =
std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("Cannot read fixture 05: {e}"));
let schema = CompiledSchema::from_json(&json, false).unwrap();
let m = schema.find_mutation("createOrder").unwrap();
assert_eq!(m.sql_source.as_deref(), Some("fn_create_order"));
assert_eq!(m.inject_params.len(), 2);
assert!(m.inject_params.contains_key("user_id"));
assert!(m.inject_params.contains_key("tenant_id"));
assert_eq!(m.invalidates_fact_tables, vec!["tf_sales", "tf_order_count"]);
assert_eq!(m.invalidates_views, vec!["v_order_summary", "v_order_items"]);
}
#[test]
fn test_schema_to_json_roundtrip() {
let schema = CompiledSchema {
types: vec![
TypeDefinition::new("User", "v_user")
.with_field(FieldDefinition::new("id", FieldType::Id))
.with_field(FieldDefinition::new("email", FieldType::String)),
],
enums: vec![],
input_types: vec![],
interfaces: vec![],
unions: vec![],
queries: vec![QueryDefinition::new("users", "User").returning_list()],
mutations: vec![],
subscriptions: vec![],
directives: vec![],
fact_tables: std::collections::HashMap::new(),
observers: Vec::new(),
federation: None,
security: None,
observers_config: None,
subscriptions_config: None,
validation_config: None,
debug_config: None,
mcp_config: None,
schema_format_version: None,
schema_sdl: None,
custom_scalars: CustomTypeRegistry::default(),
..CompiledSchema::default()
};
let json = schema.to_json().unwrap();
let parsed = CompiledSchema::from_json(&json, false).unwrap();
assert_eq!(schema, parsed);
}
#[test]
fn test_schema_validation_duplicate_types() {
let schema = CompiledSchema {
types: vec![
TypeDefinition::new("User", "v_user"),
TypeDefinition::new("User", "v_user2"), ],
..Default::default()
};
let result = schema.validate();
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.contains("Duplicate type name: User")));
}
#[test]
fn test_schema_validation_undefined_type_reference() {
let schema = CompiledSchema {
types: vec![TypeDefinition::new("User", "v_user")],
queries: vec![QueryDefinition::new("posts", "Post")], ..Default::default()
};
let result = schema.validate();
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.contains("undefined type 'Post'")));
}
#[test]
fn test_schema_validation_success() {
let schema = CompiledSchema {
types: vec![TypeDefinition::new("User", "v_user")],
queries: vec![QueryDefinition::new("users", "User")],
..Default::default()
};
schema.validate().unwrap_or_else(|e| panic!("expected valid schema: {e:?}"));
}
#[test]
fn test_schema_validation_builtin_types_ok() {
let schema = CompiledSchema {
types: vec![],
queries: vec![
QueryDefinition::new("version", "String"),
QueryDefinition::new("count", "Int"),
QueryDefinition::new("active", "Boolean"),
],
..Default::default()
};
schema
.validate()
.unwrap_or_else(|e| panic!("expected built-in types to pass validation: {e:?}"));
}
#[test]
fn test_field_type_serialization() {
let field = FieldDefinition::new("id", FieldType::Id);
let json = serde_json::to_string(&field).unwrap();
assert!(json.contains(r#""field_type":"ID""#));
let list_field = FieldDefinition::new("tags", FieldType::List(Box::new(FieldType::String)));
let list_json = serde_json::to_string(&list_field).unwrap();
assert!(list_json.contains(r#""field_type":{"List":"String"}"#));
let obj_field = FieldDefinition::new("author", FieldType::Object("User".to_string()));
let obj_json = serde_json::to_string(&obj_field).unwrap();
assert!(obj_json.contains(r#""field_type":{"Object":"User"}"#));
}
#[test]
fn test_field_type_is_scalar() {
assert!(FieldType::String.is_scalar());
assert!(FieldType::Int.is_scalar());
assert!(FieldType::Boolean.is_scalar());
assert!(FieldType::Id.is_scalar());
assert!(FieldType::DateTime.is_scalar());
assert!(FieldType::Json.is_scalar());
assert!(!FieldType::List(Box::new(FieldType::String)).is_scalar());
assert!(!FieldType::Object("User".to_string()).is_scalar());
}
#[test]
fn test_field_type_to_graphql_string() {
assert_eq!(FieldType::String.to_graphql_string(), "String");
assert_eq!(FieldType::Int.to_graphql_string(), "Int");
assert_eq!(FieldType::Id.to_graphql_string(), "ID");
assert_eq!(FieldType::List(Box::new(FieldType::String)).to_graphql_string(), "[String]");
assert_eq!(
FieldType::List(Box::new(FieldType::List(Box::new(FieldType::Int)))).to_graphql_string(),
"[[Int]]"
);
assert_eq!(FieldType::Object("User".to_string()).to_graphql_string(), "User");
}
#[test]
fn test_type_definition_builder() {
let user = TypeDefinition::new("User", "v_user")
.with_jsonb_column("payload")
.with_description("A user entity")
.with_field(FieldDefinition::new("id", FieldType::Id))
.with_field(
FieldDefinition::nullable("email", FieldType::String).with_description("Email"),
);
assert_eq!(user.name, "User");
assert_eq!(user.sql_source, "v_user");
assert_eq!(user.jsonb_column, "payload");
assert_eq!(user.description, Some("A user entity".to_string()));
assert_eq!(user.fields.len(), 2);
let email_field = user.find_field("email").unwrap();
assert!(email_field.nullable);
assert_eq!(email_field.description, Some("Email".to_string()));
}
#[test]
fn test_mutation_operation_serialization() {
let insert = MutationOperation::Insert {
table: "users".to_string(),
};
let json = serde_json::to_string(&insert).unwrap();
assert_eq!(json, r#"{"Insert":{"table":"users"}}"#);
let custom = MutationOperation::Custom;
let json = serde_json::to_string(&custom).unwrap();
assert_eq!(json, r#""Custom""#);
}
#[test]
fn test_auto_params_presets() {
let all = AutoParams::all();
assert!(all.has_where);
assert!(all.has_order_by);
assert!(all.has_limit);
assert!(all.has_offset);
let none = AutoParams::none();
assert!(!none.has_where);
assert!(!none.has_order_by);
assert!(!none.has_limit);
assert!(!none.has_offset);
}
#[test]
fn test_argument_definition() {
let required = ArgumentDefinition::new("id", FieldType::Id);
assert!(!required.nullable);
let optional = ArgumentDefinition::optional("limit", FieldType::Int);
assert!(optional.nullable);
}
#[test]
fn test_operation_count() {
let schema = CompiledSchema {
types: vec![TypeDefinition::new("User", "v_user")],
enums: vec![],
input_types: vec![],
interfaces: vec![],
unions: vec![],
queries: vec![
QueryDefinition::new("users", "User"),
QueryDefinition::new("user", "User"),
],
mutations: vec![MutationDefinition::new("createUser", "User")],
subscriptions: vec![SubscriptionDefinition::new("userCreated", "User")],
directives: vec![],
fact_tables: std::collections::HashMap::new(),
observers: Vec::new(),
federation: None,
security: None,
observers_config: None,
subscriptions_config: None,
validation_config: None,
debug_config: None,
mcp_config: None,
schema_format_version: None,
schema_sdl: None,
custom_scalars: CustomTypeRegistry::default(),
..CompiledSchema::default()
};
assert_eq!(schema.operation_count(), 4); }
#[test]
fn test_python_generated_json_compat() {
let python_json = r#"{
"types": [
{
"name": "User",
"sql_source": "v_user",
"jsonb_column": "data",
"fields": [
{
"name": "id",
"field_type": "ID",
"nullable": false
},
{
"name": "name",
"field_type": "String",
"nullable": false
},
{
"name": "email",
"field_type": "String",
"nullable": true
}
],
"description": "A user in the system."
}
],
"queries": [
{
"name": "get_users",
"return_type": "User",
"returns_list": true,
"nullable": false,
"description": "Get all users."
},
{
"name": "get_user",
"return_type": "User",
"returns_list": false,
"nullable": true,
"arguments": [
{
"name": "id",
"arg_type": "ID",
"nullable": false
}
],
"description": "Get a single user by ID."
}
],
"mutations": [
{
"name": "create_user",
"return_type": "User",
"arguments": [
{
"name": "name",
"arg_type": "String",
"nullable": false
},
{
"name": "email",
"arg_type": "String",
"nullable": false
}
],
"description": "Create a new user.",
"operation": "Custom"
}
],
"subscriptions": []
}"#;
let schema = CompiledSchema::from_json(python_json, false)
.expect("Python-generated JSON should parse successfully");
assert_eq!(schema.types.len(), 1);
let user_type = &schema.types[0];
assert_eq!(user_type.name, "User");
assert_eq!(user_type.sql_source, "v_user");
assert_eq!(user_type.jsonb_column, "data");
assert_eq!(user_type.fields.len(), 3);
assert_eq!(user_type.description, Some("A user in the system.".to_string()));
let id_field = user_type.find_field("id").expect("id field");
assert_eq!(id_field.field_type, super::field_type::FieldType::Id);
assert!(!id_field.nullable);
let email_field = user_type.find_field("email").expect("email field");
assert!(email_field.nullable);
assert_eq!(schema.queries.len(), 2);
let get_users = schema.find_query("get_users").expect("get_users query");
assert_eq!(get_users.return_type, "User");
assert!(get_users.returns_list);
assert!(!get_users.nullable);
let get_user = schema.find_query("get_user").expect("get_user query");
assert!(!get_user.returns_list);
assert!(get_user.nullable);
assert_eq!(get_user.arguments.len(), 1);
assert_eq!(get_user.arguments[0].name, "id");
assert_eq!(schema.mutations.len(), 1);
let create_user = schema.find_mutation("create_user").expect("create_user mutation");
assert_eq!(create_user.return_type, "User");
assert_eq!(create_user.arguments.len(), 2);
assert!(matches!(create_user.operation, MutationOperation::Custom));
assert!(schema.subscriptions.is_empty());
schema
.validate()
.unwrap_or_else(|e| panic!("expected Python-generated schema to pass validation: {e:?}"));
}
#[test]
fn test_vector_config_creation() {
let config = VectorConfig::new(1536);
assert_eq!(config.dimensions, 1536);
assert_eq!(config.index_type, VectorIndexType::Hnsw);
assert_eq!(config.distance_metric, DistanceMetric::Cosine);
}
#[test]
fn test_vector_config_openai() {
let config = VectorConfig::openai();
assert_eq!(config.dimensions, 1536);
assert_eq!(config.index_type, VectorIndexType::Hnsw);
assert_eq!(config.distance_metric, DistanceMetric::Cosine);
}
#[test]
fn test_vector_config_openai_small() {
let config = VectorConfig::openai_small();
assert_eq!(config.dimensions, 512);
assert_eq!(config.index_type, VectorIndexType::Hnsw);
assert_eq!(config.distance_metric, DistanceMetric::Cosine);
}
#[test]
fn test_vector_config_builder() {
let config = VectorConfig::new(768)
.with_index(VectorIndexType::IvfFlat)
.with_distance(DistanceMetric::L2);
assert_eq!(config.dimensions, 768);
assert_eq!(config.index_type, VectorIndexType::IvfFlat);
assert_eq!(config.distance_metric, DistanceMetric::L2);
}
#[test]
fn test_distance_metric_operators() {
assert_eq!(DistanceMetric::Cosine.operator(), "<=>");
assert_eq!(DistanceMetric::L2.operator(), "<->");
assert_eq!(DistanceMetric::InnerProduct.operator(), "<#>");
}
#[test]
fn test_distance_metric_ops_classes() {
assert_eq!(DistanceMetric::Cosine.hnsw_ops_class(), "vector_cosine_ops");
assert_eq!(DistanceMetric::L2.hnsw_ops_class(), "vector_l2_ops");
assert_eq!(DistanceMetric::InnerProduct.hnsw_ops_class(), "vector_ip_ops");
}
#[test]
fn test_vector_index_sql() {
let hnsw_sql =
VectorIndexType::Hnsw.index_sql("documents", "embedding", DistanceMetric::Cosine);
assert_eq!(
hnsw_sql,
Some("CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops)".to_string())
);
let ivf_sql = VectorIndexType::IvfFlat.index_sql("docs", "vec", DistanceMetric::L2);
assert_eq!(
ivf_sql,
Some("CREATE INDEX ON docs USING ivfflat (vec vector_l2_ops)".to_string())
);
let none_sql = VectorIndexType::None.index_sql("t", "c", DistanceMetric::Cosine);
assert_eq!(none_sql, None);
}
#[test]
fn test_field_definition_vector() {
let embedding = FieldDefinition::vector("embedding", VectorConfig::openai());
assert_eq!(embedding.name, "embedding");
assert!(embedding.is_vector());
assert!(matches!(embedding.field_type, FieldType::Vector));
assert!(!embedding.nullable);
let config = embedding.vector_config.expect("should have vector config");
assert_eq!(config.dimensions, 1536);
}
#[test]
fn test_field_type_vector_is_scalar() {
assert!(FieldType::Vector.is_scalar());
assert!(FieldType::Vector.is_vector());
assert!(!FieldType::String.is_vector());
}
#[test]
fn test_field_type_vector_graphql_string() {
assert_eq!(FieldType::Vector.to_graphql_string(), "[Float!]!");
}
#[test]
fn test_field_type_vector_sql_type() {
let config = VectorConfig::new(1536);
assert_eq!(FieldType::Vector.to_sql_type(Some(&config)), "vector(1536)");
assert_eq!(FieldType::Vector.to_sql_type(None), "vector");
}
#[test]
fn test_vector_config_serialization() {
let config = VectorConfig {
dimensions: 1536,
index_type: VectorIndexType::Hnsw,
distance_metric: DistanceMetric::Cosine,
};
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains(r#""dimensions":1536"#));
assert!(json.contains(r#""index_type":"hnsw""#));
assert!(json.contains(r#""distance_metric":"cosine""#));
let parsed: VectorConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, config);
}
#[test]
fn test_vector_config_all_distance_metrics() {
let metrics = [
(DistanceMetric::Cosine, "cosine"),
(DistanceMetric::L2, "l2"),
(DistanceMetric::InnerProduct, "inner_product"),
];
for (metric, expected_name) in metrics {
let json = serde_json::to_string(&metric).unwrap();
assert_eq!(json, format!(r#""{}""#, expected_name));
let parsed: DistanceMetric = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, metric);
}
}
#[test]
fn test_vector_config_all_index_types() {
let types = [
(VectorIndexType::Hnsw, "hnsw"),
(VectorIndexType::IvfFlat, "ivf_flat"),
(VectorIndexType::None, "none"),
];
for (index_type, expected_name) in types {
let json = serde_json::to_string(&index_type).unwrap();
assert_eq!(json, format!(r#""{}""#, expected_name));
let parsed: VectorIndexType = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, index_type);
}
}
#[test]
fn test_field_definition_with_vector_config() {
let field = FieldDefinition::new("data", FieldType::Vector)
.with_vector_config(VectorConfig::openai())
.with_description("Document embedding");
assert_eq!(field.name, "data");
assert!(field.is_vector());
assert_eq!(field.description, Some("Document embedding".to_string()));
let config = field.vector_config.expect("should have vector config");
assert_eq!(config.dimensions, 1536);
}
#[test]
fn test_schema_with_vector_field_json() {
let json = r#"{
"types": [{
"name": "Document",
"sql_source": "documents",
"fields": [
{"name": "id", "field_type": "ID", "nullable": false},
{"name": "content", "field_type": "String", "nullable": false},
{
"name": "embedding",
"field_type": "Vector",
"nullable": false,
"vector_config": {
"dimensions": 1536,
"index_type": "hnsw",
"distance_metric": "cosine"
}
}
]
}],
"queries": [],
"mutations": [],
"subscriptions": []
}"#;
let schema = CompiledSchema::from_json(json, false).unwrap();
assert_eq!(schema.types.len(), 1);
let doc_type = &schema.types[0];
assert_eq!(doc_type.name, "Document");
assert_eq!(doc_type.fields.len(), 3);
let embedding_field = doc_type.find_field("embedding").expect("embedding field");
assert!(embedding_field.is_vector());
assert!(matches!(embedding_field.field_type, FieldType::Vector));
let config = embedding_field.vector_config.as_ref().expect("should have vector config");
assert_eq!(config.dimensions, 1536);
assert_eq!(config.index_type, VectorIndexType::Hnsw);
assert_eq!(config.distance_metric, DistanceMetric::Cosine);
}
#[test]
fn test_vector_field_roundtrip() {
let schema = CompiledSchema {
types: vec![
TypeDefinition::new("Document", "documents")
.with_field(FieldDefinition::new("id", FieldType::Id))
.with_field(
FieldDefinition::vector("embedding", VectorConfig::openai())
.with_description("OpenAI embedding"),
),
],
enums: vec![],
input_types: vec![],
interfaces: vec![],
unions: vec![],
queries: vec![],
mutations: vec![],
subscriptions: vec![],
directives: vec![],
fact_tables: std::collections::HashMap::new(),
observers: Vec::new(),
federation: None,
security: None,
observers_config: None,
subscriptions_config: None,
validation_config: None,
debug_config: None,
mcp_config: None,
schema_format_version: None,
schema_sdl: None,
custom_scalars: CustomTypeRegistry::default(),
..CompiledSchema::default()
};
let json = schema.to_json().unwrap();
let parsed = CompiledSchema::from_json(&json, false).unwrap();
assert_eq!(schema, parsed);
let doc_type = &parsed.types[0];
let embedding = doc_type.find_field("embedding").unwrap();
assert!(embedding.is_vector());
assert_eq!(embedding.vector_config.as_ref().unwrap().dimensions, 1536);
}
#[test]
fn test_python_generated_vector_schema_compat() {
let python_json = r#"{
"types": [{
"name": "Document",
"sql_source": "documents",
"jsonb_column": "data",
"fields": [
{"name": "id", "field_type": "String", "nullable": false},
{"name": "content", "field_type": "String", "nullable": false},
{
"name": "embedding",
"field_type": "Vector",
"nullable": false,
"vector_config": {
"dimensions": 1536,
"index_type": "hnsw",
"distance_metric": "cosine"
},
"description": "OpenAI embedding"
}
],
"description": "A document with vector embedding."
}],
"queries": [],
"mutations": [],
"subscriptions": []
}"#;
let schema = CompiledSchema::from_json(python_json, false)
.expect("Python-generated vector schema should parse");
assert_eq!(schema.types.len(), 1);
let doc_type = &schema.types[0];
assert_eq!(doc_type.name, "Document");
assert_eq!(doc_type.sql_source, "documents");
let embedding = doc_type.find_field("embedding").expect("embedding field");
assert!(embedding.is_vector());
assert!(matches!(embedding.field_type, FieldType::Vector));
assert!(!embedding.nullable);
assert_eq!(embedding.description, Some("OpenAI embedding".to_string()));
let config = embedding.vector_config.as_ref().expect("vector_config");
assert_eq!(config.dimensions, 1536);
assert_eq!(config.index_type, VectorIndexType::Hnsw);
assert_eq!(config.distance_metric, DistanceMetric::Cosine);
schema.validate().unwrap_or_else(|e| {
panic!("expected Python-generated vector schema to pass validation: {e:?}")
});
}
#[test]
fn test_compiled_schema_has_version_after_stamp() {
let schema = CompiledSchema {
schema_format_version: Some(CURRENT_SCHEMA_FORMAT_VERSION),
..Default::default()
};
let json = serde_json::to_string(&schema).unwrap();
let reloaded: CompiledSchema = serde_json::from_str(&json).unwrap();
assert_eq!(reloaded.schema_format_version, Some(CURRENT_SCHEMA_FORMAT_VERSION));
reloaded
.validate_format_version()
.unwrap_or_else(|e| panic!("expected current version to pass format validation: {e:?}"));
}
#[test]
fn test_future_schema_version_is_rejected() {
let schema = CompiledSchema {
schema_format_version: Some(999),
..Default::default()
};
let result = schema.validate_format_version();
assert!(result.is_err(), "expected future version 999 to be rejected, got: {result:?}");
}
#[test]
fn test_legacy_schema_without_version_warns_but_loads() {
let schema = CompiledSchema::default();
schema
.validate_format_version()
.unwrap_or_else(|e| panic!("expected legacy schema (no version) to be accepted: {e:?}"));
}
#[test]
fn roundtrip_int() {
let v = GraphQLValue::Int(42);
assert_eq!(GraphQLValue::from_json(&v.to_json()).expect("roundtrip"), v);
}
#[test]
fn roundtrip_float() {
let v = GraphQLValue::Float(1.5);
let rt = GraphQLValue::from_json(&v.to_json()).expect("roundtrip");
assert!(matches!(rt, GraphQLValue::Float(_)));
}
#[test]
fn roundtrip_string() {
let v = GraphQLValue::String("hello".to_string());
assert_eq!(GraphQLValue::from_json(&v.to_json()).expect("roundtrip"), v);
}
#[test]
fn roundtrip_list() {
let v = GraphQLValue::List(vec![GraphQLValue::Int(1), GraphQLValue::Null]);
assert_eq!(GraphQLValue::from_json(&v.to_json()).expect("roundtrip"), v);
}
#[test]
fn roundtrip_null() {
let v = GraphQLValue::Null;
assert_eq!(GraphQLValue::from_json(&v.to_json()).expect("roundtrip"), v);
}
#[test]
fn roundtrip_boolean() {
let v = GraphQLValue::Boolean(true);
assert_eq!(GraphQLValue::from_json(&v.to_json()).expect("roundtrip"), v);
}
#[test]
fn json_null_parses_as_null() {
assert_eq!(
GraphQLValue::from_json(&serde_json::Value::Null).expect("parse"),
GraphQLValue::Null
);
}
#[test]
fn serde_roundtrip_via_json_string() {
let v = GraphQLValue::List(vec![GraphQLValue::Int(1), GraphQLValue::Null]);
let json_str = serde_json::to_string(&v).expect("serialize");
let back: GraphQLValue = serde_json::from_str(&json_str).expect("deserialize");
assert_eq!(back, v);
}
#[test]
fn test_builtin_scalars_recognized() {
for &scalar in BUILTIN_SCALARS {
assert!(is_known_scalar(scalar), "Builtin scalar '{}' should be recognized", scalar);
}
}
#[test]
fn test_rich_scalars_recognized() {
for &scalar in RICH_SCALARS {
assert!(is_known_scalar(scalar), "Rich scalar '{}' should be recognized", scalar);
}
}
#[test]
fn test_unknown_types_not_recognized() {
assert!(!is_known_scalar("User"));
assert!(!is_known_scalar("Post"));
assert!(!is_known_scalar("CustomType"));
assert!(!is_known_scalar(""));
}
#[test]
fn test_builtin_scalar_count() {
assert_eq!(BUILTIN_SCALARS.len(), 14);
}
#[test]
fn test_rich_scalar_count() {
assert_eq!(RICH_SCALARS.len(), 51);
}
#[test]
fn test_no_duplicate_scalars() {
for &builtin in BUILTIN_SCALARS {
assert!(
!RICH_SCALARS.contains(&builtin),
"Scalar '{}' appears in both BUILTIN and RICH lists",
builtin
);
}
}
#[test]
fn test_specific_builtin_scalars() {
assert!(is_known_scalar("ID"));
assert!(is_known_scalar("String"));
assert!(is_known_scalar("Int"));
assert!(is_known_scalar("Float"));
assert!(is_known_scalar("Boolean"));
assert!(is_known_scalar("DateTime"));
}
#[test]
fn test_specific_rich_scalars() {
assert!(is_known_scalar("Email"));
assert!(is_known_scalar("UUID"));
assert!(is_known_scalar("URL"));
assert!(is_known_scalar("IBAN"));
assert!(is_known_scalar("IPAddress"));
}
#[test]
fn test_case_sensitive_matching() {
assert!(is_known_scalar("String"));
assert!(!is_known_scalar("string"));
assert!(is_known_scalar("Email"));
assert!(!is_known_scalar("email"));
}
#[test]
fn tenancy_mode_default_is_none() {
assert_eq!(TenancyMode::default(), TenancyMode::None);
}
#[test]
fn tenancy_mode_serde_round_trip() {
for (mode, expected_str) in [
(TenancyMode::None, "\"none\""),
(TenancyMode::Row, "\"row\""),
(TenancyMode::Schema, "\"schema\""),
] {
let json = serde_json::to_string(&mode).unwrap();
assert_eq!(json, expected_str, "serialization of {mode}");
let back: TenancyMode = serde_json::from_str(&json).unwrap();
assert_eq!(back, mode, "deserialization of {expected_str}");
}
}
#[test]
fn tenancy_mode_invalid_string_rejected() {
let result: Result<TenancyMode, _> = serde_json::from_str("\"invalid\"");
assert!(result.is_err(), "unknown variant must fail");
}
#[test]
fn tenancy_mode_display() {
assert_eq!(TenancyMode::None.to_string(), "none");
assert_eq!(TenancyMode::Row.to_string(), "row");
assert_eq!(TenancyMode::Schema.to_string(), "schema");
}
#[test]
fn tenancy_config_default_values() {
let config = TenancyConfig::default();
assert_eq!(config.mode, TenancyMode::None);
assert_eq!(config.tenant_claim, "tenant_id");
}
#[test]
fn tenancy_config_serde_round_trip() {
let config = TenancyConfig {
mode: TenancyMode::Row,
tenant_claim: "org_id".to_string(),
};
let json = serde_json::to_string(&config).unwrap();
let back: TenancyConfig = serde_json::from_str(&json).unwrap();
assert_eq!(back, config);
}
#[test]
fn tenancy_config_deserialize_from_compiled_json() {
let json = r#"{"mode": "schema", "tenant_claim": "tenant_id"}"#;
let config: TenancyConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.mode, TenancyMode::Schema);
assert_eq!(config.tenant_claim, "tenant_id");
}
#[test]
fn tenancy_config_defaults_when_empty() {
let config: TenancyConfig = serde_json::from_str("{}").unwrap();
assert_eq!(config.mode, TenancyMode::None);
assert_eq!(config.tenant_claim, "tenant_id");
}
#[test]
fn security_config_tenancy_defaults_to_none() {
let config = SecurityConfig::default();
assert_eq!(config.tenancy.mode, TenancyMode::None);
}
#[test]
fn security_config_tenancy_skipped_when_default() {
let config = SecurityConfig::default();
let json = serde_json::to_string(&config).unwrap();
assert!(!json.contains("tenancy"), "default tenancy should be skipped in serialization");
}
#[test]
fn security_config_tenancy_present_when_non_default() {
let config = SecurityConfig {
tenancy: TenancyConfig {
mode: TenancyMode::Row,
tenant_claim: "tenant_id".to_string(),
},
..SecurityConfig::default()
};
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains("tenancy"), "non-default tenancy should be serialized");
assert!(json.contains("\"row\""), "mode=row should appear in JSON");
}
#[test]
fn security_config_with_tenancy_round_trip() {
let config = SecurityConfig {
tenancy: TenancyConfig {
mode: TenancyMode::Schema,
tenant_claim: "org_id".to_string(),
},
..SecurityConfig::default()
};
let json = serde_json::to_string(&config).unwrap();
let back: SecurityConfig = serde_json::from_str(&json).unwrap();
assert_eq!(back.tenancy.mode, TenancyMode::Schema);
assert_eq!(back.tenancy.tenant_claim, "org_id");
}
#[test]
fn test_parse_builtin_scalars() {
assert_eq!(FieldType::parse("String"), FieldType::String);
assert_eq!(FieldType::parse("Int"), FieldType::Int);
assert_eq!(FieldType::parse("Float"), FieldType::Float);
assert_eq!(FieldType::parse("Boolean"), FieldType::Boolean);
assert_eq!(FieldType::parse("ID"), FieldType::Id);
assert_eq!(FieldType::parse("DateTime"), FieldType::DateTime);
assert_eq!(FieldType::parse("Date"), FieldType::Date);
assert_eq!(FieldType::parse("Time"), FieldType::Time);
assert_eq!(FieldType::parse("JSON"), FieldType::Json);
assert_eq!(FieldType::parse("UUID"), FieldType::Uuid);
}
#[test]
fn test_parse_rich_scalars_exact_case() {
let result = FieldType::parse("Email");
assert_eq!(result, FieldType::Scalar("Email".to_string()));
let result = FieldType::parse("IBAN");
assert_eq!(result, FieldType::Scalar("IBAN".to_string()));
let result = FieldType::parse("URL");
assert_eq!(result, FieldType::Scalar("URL".to_string()));
}
#[test]
fn test_parse_rich_scalars_case_insensitive() {
let result = FieldType::parse("email");
assert_eq!(result, FieldType::Scalar("Email".to_string()));
let result = FieldType::parse("EMAIL");
assert_eq!(result, FieldType::Scalar("Email".to_string()));
let result = FieldType::parse("iban");
assert_eq!(result, FieldType::Scalar("IBAN".to_string()));
let result = FieldType::parse("phonenumber");
assert_eq!(result, FieldType::Scalar("PhoneNumber".to_string()));
}
#[test]
fn test_parse_all_rich_scalars() {
let rich_scalars = vec![
"Email",
"PhoneNumber",
"URL",
"DomainName",
"Hostname",
"PostalCode",
"Latitude",
"Longitude",
"Coordinates",
"Timezone",
"IBAN",
"CUSIP",
"CurrencyCode",
"Money",
"StockSymbol",
"Slug",
"SemanticVersion",
"APIKey",
"VIN",
"IPAddress",
"IPv4",
"IPv6",
"MACAddress",
"CIDR",
"AirportCode",
"FlightNumber",
"Markdown",
"HTML",
"MimeType",
"Color",
"LTree",
"DateRange",
"Duration",
"Percentage",
];
for scalar_name in rich_scalars {
let result = FieldType::parse(scalar_name);
assert_eq!(
result,
FieldType::Scalar(scalar_name.to_string()),
"Failed to parse rich scalar: {}",
scalar_name
);
}
}
#[test]
fn test_parse_unknown_type_as_object() {
let result = FieldType::parse("CustomType");
assert_eq!(result, FieldType::Object("CustomType".to_string()));
let result = FieldType::parse("User");
assert_eq!(result, FieldType::Object("User".to_string()));
}
#[test]
fn test_parse_with_list_syntax() {
let result = FieldType::parse("[String]");
assert_eq!(result, FieldType::List(Box::new(FieldType::String)));
let result = FieldType::parse("[Email]");
assert_eq!(result, FieldType::List(Box::new(FieldType::Scalar("Email".to_string()))));
let result = FieldType::parse("[User]");
assert_eq!(result, FieldType::List(Box::new(FieldType::Object("User".to_string()))));
}
#[test]
fn test_parse_with_non_null_marker() {
let result = FieldType::parse("String!");
assert_eq!(result, FieldType::String);
let result = FieldType::parse("Email!");
assert_eq!(result, FieldType::Scalar("Email".to_string()));
let result = FieldType::parse("[String!]!");
assert_eq!(result, FieldType::List(Box::new(FieldType::String)));
}
#[test]
fn test_parse_nested_lists() {
let result = FieldType::parse("[[String]]");
assert_eq!(result, FieldType::List(Box::new(FieldType::List(Box::new(FieldType::String)))));
let result = FieldType::parse("[[Email]]");
assert_eq!(
result,
FieldType::List(Box::new(FieldType::List(Box::new(FieldType::Scalar(
"Email".to_string()
)))))
);
}
#[test]
fn test_parse_as_scalar_if_unknown_converts_objects() {
let mut known_types = std::collections::HashSet::new();
let result = FieldType::parse("CustomType");
assert_eq!(result, FieldType::Object("CustomType".to_string()));
let result = FieldType::parse_as_scalar_if_unknown("CustomType", &known_types);
assert_eq!(result, FieldType::Scalar("CustomType".to_string()));
known_types.insert("CustomType".to_string());
let result = FieldType::parse_as_scalar_if_unknown("CustomType", &known_types);
assert_eq!(result, FieldType::Object("CustomType".to_string()));
}
#[test]
fn test_parse_case_variations() {
assert_eq!(FieldType::parse("string"), FieldType::String);
assert_eq!(FieldType::parse("STRING"), FieldType::String);
assert_eq!(FieldType::parse("String"), FieldType::String);
assert_eq!(FieldType::parse("int"), FieldType::Int);
assert_eq!(FieldType::parse("INT"), FieldType::Int);
assert_eq!(FieldType::parse("integer"), FieldType::Int);
assert_eq!(FieldType::parse("INTEGER"), FieldType::Int);
}
#[test]
fn test_field_encryption_config_deserialization() {
let json = r#"{
"name": "email",
"field_type": "String",
"encryption": {
"key_reference": "keys/email",
"algorithm": "AES-256-GCM"
}
}"#;
let field: FieldDefinition = serde_json::from_str(json).unwrap();
assert!(field.encryption.is_some());
let enc = field.encryption.unwrap();
assert_eq!(enc.key_reference, "keys/email");
assert_eq!(enc.algorithm, "AES-256-GCM");
}
#[test]
fn test_field_without_encryption() {
let json = r#"{"name": "id", "field_type": "Int"}"#;
let field: FieldDefinition = serde_json::from_str(json).unwrap();
assert!(field.encryption.is_none());
assert!(!field.is_encrypted());
}
#[test]
fn test_field_encryption_default_algorithm() {
let json = r#"{"key_reference": "keys/ssn"}"#;
let config: FieldEncryptionConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.algorithm, "AES-256-GCM");
}
#[test]
fn test_field_with_encryption_builder() {
let field =
FieldDefinition::new("email", FieldType::String).with_encryption(FieldEncryptionConfig {
key_reference: "keys/email".to_string(),
algorithm: "AES-256-GCM".to_string(),
});
assert!(field.is_encrypted());
assert_eq!(field.encryption.unwrap().key_reference, "keys/email");
}
#[test]
fn test_field_encryption_roundtrip_serialization() {
let field =
FieldDefinition::new("email", FieldType::String).with_encryption(FieldEncryptionConfig {
key_reference: "keys/email".to_string(),
algorithm: "AES-256-GCM".to_string(),
});
let json = serde_json::to_string(&field).unwrap();
let deserialized: FieldDefinition = serde_json::from_str(&json).unwrap();
assert_eq!(field, deserialized);
}
#[test]
fn test_federation_config_default() {
let config = FederationConfig::default();
assert!(!config.enabled);
assert_eq!(config.version, None);
assert!(config.entities.is_empty());
assert!(config.circuit_breaker.is_none());
}
#[test]
fn test_circuit_breaker_config_default() {
let config = CircuitBreakerConfig::default();
assert!(!config.enabled);
assert_eq!(config.failure_threshold, 5);
assert_eq!(config.recovery_timeout_secs, 30);
assert_eq!(config.success_threshold, 2);
assert!(config.per_entity.is_empty());
}
#[test]
fn test_security_config_default() {
let config = CompiledSecurityConfig::default();
assert!(config.default_policy.is_none());
assert!(config.rules.is_empty());
assert!(config.policies.is_empty());
assert!(config.field_auth.is_empty());
assert!(config.enterprise.rate_limiting_enabled);
}
#[test]
fn test_observers_config_default() {
let config = ObserversConfig::default();
assert!(!config.enabled);
assert_eq!(config.backend, "redis");
assert!(config.handlers.is_empty());
}
#[test]
fn test_federation_config_serde() {
let json = r#"{
"enabled": true,
"version": "v2",
"entities": [{"name": "User", "key_fields": ["id"]}],
"circuit_breaker": {
"enabled": true,
"failure_threshold": 3,
"recovery_timeout_secs": 15,
"success_threshold": 1
}
}"#;
let config: FederationConfig = serde_json::from_str(json).unwrap();
assert!(config.enabled);
assert_eq!(config.version, Some("v2".to_string()));
assert_eq!(config.entities.len(), 1);
assert_eq!(config.entities[0].name, "User");
let cb = config.circuit_breaker.unwrap();
assert!(cb.enabled);
assert_eq!(cb.failure_threshold, 3);
}
#[test]
fn test_entity_override() {
let config = CircuitBreakerConfig {
per_entity: vec![EntityCircuitBreakerOverride {
entity: "Product".to_string(),
failure_threshold: Some(2),
recovery_timeout: None,
success_threshold: None,
}],
..Default::default()
};
assert_eq!(config.per_entity[0].entity, "Product");
assert_eq!(config.per_entity[0].failure_threshold, Some(2));
}
#[test]
fn test_roundtrip_serialization() {
let config = FederationConfig {
enabled: true,
version: Some("v2".to_string()),
service_name: Some("my-service".to_string()),
schema_url: None,
shareable_types: Vec::new(),
entities: vec![FederationEntity {
name: "User".to_string(),
key_fields: vec!["id".to_string()],
..Default::default()
}],
circuit_breaker: Some(CircuitBreakerConfig::default()),
};
let json = serde_json::to_string(&config).unwrap();
let restored: FederationConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config, restored);
}
#[test]
fn crud_trinity_resolves_create() {
let cfg = CrudNamingConfig {
function_naming: Some(CrudNamingPreset::Trinity),
..Default::default()
};
assert_eq!(cfg.resolve("CREATE", "user"), Some("create_user".to_string()));
}
#[test]
fn crud_trinity_resolves_update() {
let cfg = CrudNamingConfig {
function_naming: Some(CrudNamingPreset::Trinity),
..Default::default()
};
assert_eq!(cfg.resolve("UPDATE", "user"), Some("update_user".to_string()));
}
#[test]
fn crud_trinity_resolves_delete() {
let cfg = CrudNamingConfig {
function_naming: Some(CrudNamingPreset::Trinity),
..Default::default()
};
assert_eq!(cfg.resolve("DELETE", "user"), Some("delete_user".to_string()));
}
#[test]
fn crud_function_schema_prefix_applied() {
let cfg = CrudNamingConfig {
function_schema: Some("app".to_string()),
function_naming: Some(CrudNamingPreset::Trinity),
..Default::default()
};
assert_eq!(cfg.resolve("CREATE", "user"), Some("app.create_user".to_string()));
}
#[test]
fn crud_function_schema_prefix_applied_to_custom_template() {
let cfg = CrudNamingConfig {
function_schema: Some("app".to_string()),
create_template: Some("insert_{entity}".to_string()),
..Default::default()
};
assert_eq!(cfg.resolve("CREATE", "order"), Some("app.insert_order".to_string()));
}
#[test]
fn crud_custom_template_overrides_preset() {
let cfg = CrudNamingConfig {
function_naming: Some(CrudNamingPreset::Trinity),
create_template: Some("insert_{entity}".to_string()),
..Default::default()
};
assert_eq!(cfg.resolve("CREATE", "user"), Some("insert_user".to_string()));
assert_eq!(cfg.resolve("UPDATE", "user"), Some("update_user".to_string()));
}
#[test]
fn crud_no_config_returns_none() {
let cfg = CrudNamingConfig::default();
assert_eq!(cfg.resolve("CREATE", "user"), None);
assert_eq!(cfg.resolve("UPDATE", "user"), None);
assert_eq!(cfg.resolve("DELETE", "user"), None);
}
#[test]
fn crud_unknown_operation_returns_none() {
let cfg = CrudNamingConfig {
function_naming: Some(CrudNamingPreset::Trinity),
..Default::default()
};
assert_eq!(cfg.resolve("UPSERT", "user"), None);
}
#[test]
fn crud_operation_case_insensitive() {
let cfg = CrudNamingConfig {
function_naming: Some(CrudNamingPreset::Trinity),
..Default::default()
};
assert_eq!(cfg.resolve("create", "user"), Some("create_user".to_string()));
assert_eq!(cfg.resolve("Create", "user"), Some("create_user".to_string()));
}
#[test]
fn crud_entity_with_underscores() {
let cfg = CrudNamingConfig {
function_naming: Some(CrudNamingPreset::Trinity),
..Default::default()
};
assert_eq!(cfg.resolve("CREATE", "user_profile"), Some("create_user_profile".to_string()));
}
#[test]
fn crud_serde_roundtrip_trinity() {
let cfg = CrudNamingConfig {
function_schema: Some("app".to_string()),
function_naming: Some(CrudNamingPreset::Trinity),
..Default::default()
};
let json = serde_json::to_string(&cfg).unwrap();
let restored: CrudNamingConfig = serde_json::from_str(&json).unwrap();
assert_eq!(cfg, restored);
}
#[test]
fn crud_serde_roundtrip_custom_templates() {
let cfg = CrudNamingConfig {
function_schema: Some("app".to_string()),
create_template: Some("insert_{entity}".to_string()),
update_template: Some("upsert_{entity}".to_string()),
delete_template: Some("remove_{entity}".to_string()),
..Default::default()
};
let json = serde_json::to_string(&cfg).unwrap();
let restored: CrudNamingConfig = serde_json::from_str(&json).unwrap();
assert_eq!(cfg, restored);
}
#[test]
fn test_naming_convention_default_is_preserve() {
assert_eq!(NamingConvention::default(), NamingConvention::Preserve);
}
#[test]
fn test_naming_convention_serde_roundtrip() {
let camel = NamingConvention::CamelCase;
let json = serde_json::to_string(&camel).unwrap();
assert_eq!(json, r#""camelCase""#);
let restored: NamingConvention = serde_json::from_str(&json).unwrap();
assert_eq!(restored, NamingConvention::CamelCase);
let preserve = NamingConvention::Preserve;
let json = serde_json::to_string(&preserve).unwrap();
assert_eq!(json, r#""preserve""#);
let restored: NamingConvention = serde_json::from_str(&json).unwrap();
assert_eq!(restored, NamingConvention::Preserve);
}
#[test]
fn test_field_definition_hierarchy_serde_roundtrip() {
let mut field = FieldDefinition::new("category_path", FieldType::String);
field.hierarchy = Some("category".to_string());
let json = serde_json::to_string(&field).unwrap();
assert!(json.contains(r#""hierarchy":"category""#));
let restored: FieldDefinition = serde_json::from_str(&json).unwrap();
assert_eq!(restored.hierarchy, Some("category".to_string()));
}
#[test]
fn test_field_definition_hierarchy_absent_defaults_to_none() {
let json = r#"{"name":"id","field_type":"ID","nullable":false}"#;
let field: FieldDefinition = serde_json::from_str(json).unwrap();
assert!(field.hierarchy.is_none());
}
#[test]
fn test_field_definition_hierarchy_skipped_when_none() {
let field = FieldDefinition::new("id", FieldType::Id);
let json = serde_json::to_string(&field).unwrap();
assert!(!json.contains("hierarchy"));
}