1use std::{collections::HashMap, fmt::Write as _};
17
18use serde::{Deserialize, Serialize};
19
20use super::{directive::DirectiveDefinition, mutation::MutationDefinition, query::QueryDefinition};
21use crate::{
22 compiler::fact_table::FactTableMetadata,
23 schema::{
24 config_types::{
25 DebugConfig, FederationConfig, GrpcConfig, McpConfig, ObserversConfig, RestConfig,
26 SubscriptionsConfig, ValidationConfig,
27 },
28 graphql_type_defs::{
29 EnumDefinition, InputObjectDefinition, InterfaceDefinition, TypeDefinition,
30 UnionDefinition,
31 },
32 observer_types::ObserverDefinition,
33 security_config::{RoleDefinition, SecurityConfig},
34 subscription_types::SubscriptionDefinition,
35 },
36 validation::CustomTypeRegistry,
37};
38
39pub const CURRENT_SCHEMA_FORMAT_VERSION: u32 = 1;
44
45#[derive(Debug, Clone, Default, Serialize, Deserialize)]
66pub struct CompiledSchema {
67 #[serde(default)]
69 pub types: Vec<TypeDefinition>,
70
71 #[serde(default)]
73 pub enums: Vec<EnumDefinition>,
74
75 #[serde(default)]
77 pub input_types: Vec<InputObjectDefinition>,
78
79 #[serde(default)]
81 pub interfaces: Vec<InterfaceDefinition>,
82
83 #[serde(default)]
85 pub unions: Vec<UnionDefinition>,
86
87 #[serde(default)]
89 pub queries: Vec<QueryDefinition>,
90
91 #[serde(default)]
93 pub mutations: Vec<MutationDefinition>,
94
95 #[serde(default)]
97 pub subscriptions: Vec<SubscriptionDefinition>,
98
99 #[serde(default, skip_serializing_if = "Vec::is_empty")]
102 pub directives: Vec<DirectiveDefinition>,
103
104 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
107 pub fact_tables: HashMap<String, FactTableMetadata>,
108
109 #[serde(default, skip_serializing_if = "Vec::is_empty")]
111 pub observers: Vec<ObserverDefinition>,
112
113 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub federation: Option<FederationConfig>,
116
117 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub security: Option<SecurityConfig>,
120
121 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub observers_config: Option<ObserversConfig>,
127
128 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub subscriptions_config: Option<SubscriptionsConfig>,
132
133 #[serde(default, skip_serializing_if = "Option::is_none")]
136 pub validation_config: Option<ValidationConfig>,
137
138 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub debug_config: Option<DebugConfig>,
142
143 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub mcp_config: Option<McpConfig>,
147
148 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub rest_config: Option<RestConfig>,
152
153 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub grpc_config: Option<GrpcConfig>,
157
158 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub schema_format_version: Option<u32>,
164
165 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub schema_sdl: Option<String>,
168
169 #[serde(skip)]
175 pub custom_scalars: CustomTypeRegistry,
176
177 #[serde(skip)]
182 pub query_index: HashMap<String, usize>,
183
184 #[serde(skip)]
189 pub mutation_index: HashMap<String, usize>,
190
191 #[serde(skip)]
196 pub subscription_index: HashMap<String, usize>,
197}
198
199impl PartialEq for CompiledSchema {
200 fn eq(&self, other: &Self) -> bool {
201 self.schema_format_version == other.schema_format_version
203 && self.types == other.types
204 && self.enums == other.enums
205 && self.input_types == other.input_types
206 && self.interfaces == other.interfaces
207 && self.unions == other.unions
208 && self.queries == other.queries
209 && self.mutations == other.mutations
210 && self.subscriptions == other.subscriptions
211 && self.directives == other.directives
212 && self.fact_tables == other.fact_tables
213 && self.observers == other.observers
214 && self.federation == other.federation
215 && self.security == other.security
216 && self.observers_config == other.observers_config
217 && self.subscriptions_config == other.subscriptions_config
218 && self.validation_config == other.validation_config
219 && self.debug_config == other.debug_config
220 && self.mcp_config == other.mcp_config
221 && self.schema_sdl == other.schema_sdl
222 }
223}
224
225impl CompiledSchema {
226 #[must_use]
228 pub fn new() -> Self {
229 Self::default()
230 }
231
232 pub fn validate_format_version(&self) -> Result<(), String> {
242 match self.schema_format_version {
243 None => {
244 Ok(())
246 },
247 Some(v) if v == CURRENT_SCHEMA_FORMAT_VERSION => Ok(()),
248 Some(v) => Err(format!(
249 "Schema format version mismatch: compiled schema has version {v}, \
250 but this runtime expects version {CURRENT_SCHEMA_FORMAT_VERSION}. \
251 Please recompile your schema with the matching fraiseql-cli version."
252 )),
253 }
254 }
255
256 pub fn build_indexes(&mut self) {
261 self.query_index =
262 self.queries.iter().enumerate().map(|(i, q)| (q.name.clone(), i)).collect();
263 self.mutation_index =
264 self.mutations.iter().enumerate().map(|(i, m)| (m.name.clone(), i)).collect();
265 self.subscription_index = self
266 .subscriptions
267 .iter()
268 .enumerate()
269 .map(|(i, s)| (s.name.clone(), i))
270 .collect();
271 }
272
273 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
310 let mut schema: Self = serde_json::from_str(json)?;
311 schema.build_indexes();
312 Ok(schema)
313 }
314
315 pub fn to_json(&self) -> Result<String, serde_json::Error> {
321 serde_json::to_string(self)
322 }
323
324 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
330 serde_json::to_string_pretty(self)
331 }
332
333 #[must_use]
335 pub fn find_type(&self, name: &str) -> Option<&TypeDefinition> {
336 self.types.iter().find(|t| t.name == name)
337 }
338
339 #[must_use]
341 pub fn find_enum(&self, name: &str) -> Option<&EnumDefinition> {
342 self.enums.iter().find(|e| e.name == name)
343 }
344
345 #[must_use]
347 pub fn find_input_type(&self, name: &str) -> Option<&InputObjectDefinition> {
348 self.input_types.iter().find(|i| i.name == name)
349 }
350
351 #[must_use]
353 pub fn find_interface(&self, name: &str) -> Option<&InterfaceDefinition> {
354 self.interfaces.iter().find(|i| i.name == name)
355 }
356
357 #[must_use]
359 pub fn find_implementors(&self, interface_name: &str) -> Vec<&TypeDefinition> {
360 self.types
361 .iter()
362 .filter(|t| t.implements.contains(&interface_name.to_string()))
363 .collect()
364 }
365
366 #[must_use]
368 pub fn find_union(&self, name: &str) -> Option<&UnionDefinition> {
369 self.unions.iter().find(|u| u.name == name)
370 }
371
372 #[must_use]
377 pub fn find_query(&self, name: &str) -> Option<&QueryDefinition> {
378 if self.query_index.is_empty() && !self.queries.is_empty() {
379 self.queries.iter().find(|q| q.name == name)
380 } else {
381 self.query_index.get(name).map(|&i| &self.queries[i])
382 }
383 }
384
385 #[must_use]
390 pub fn find_mutation(&self, name: &str) -> Option<&MutationDefinition> {
391 if self.mutation_index.is_empty() && !self.mutations.is_empty() {
392 self.mutations.iter().find(|m| m.name == name)
393 } else {
394 self.mutation_index.get(name).map(|&i| &self.mutations[i])
395 }
396 }
397
398 #[must_use]
403 pub fn find_subscription(&self, name: &str) -> Option<&SubscriptionDefinition> {
404 if self.subscription_index.is_empty() && !self.subscriptions.is_empty() {
405 self.subscriptions.iter().find(|s| s.name == name)
406 } else {
407 self.subscription_index.get(name).map(|&i| &self.subscriptions[i])
408 }
409 }
410
411 #[must_use]
413 pub fn find_directive(&self, name: &str) -> Option<&DirectiveDefinition> {
414 self.directives.iter().find(|d| d.name == name)
415 }
416
417 #[must_use]
419 pub const fn operation_count(&self) -> usize {
420 self.queries.len() + self.mutations.len() + self.subscriptions.len()
421 }
422
423 pub fn add_fact_table(&mut self, table_name: String, metadata: FactTableMetadata) {
430 self.fact_tables.insert(table_name, metadata);
431 }
432
433 #[must_use]
443 pub fn get_fact_table(&self, name: &str) -> Option<&FactTableMetadata> {
444 self.fact_tables.get(name)
445 }
446
447 #[must_use]
453 pub fn list_fact_tables(&self) -> Vec<&str> {
454 self.fact_tables.keys().map(String::as_str).collect()
455 }
456
457 #[must_use]
459 pub fn has_fact_tables(&self) -> bool {
460 !self.fact_tables.is_empty()
461 }
462
463 #[must_use]
465 pub fn find_observer(&self, name: &str) -> Option<&ObserverDefinition> {
466 self.observers.iter().find(|o| o.name == name)
467 }
468
469 #[must_use]
471 pub fn find_observers_for_entity(&self, entity: &str) -> Vec<&ObserverDefinition> {
472 self.observers.iter().filter(|o| o.entity == entity).collect()
473 }
474
475 #[must_use]
477 pub fn find_observers_for_event(&self, event: &str) -> Vec<&ObserverDefinition> {
478 self.observers.iter().filter(|o| o.event == event).collect()
479 }
480
481 #[must_use]
483 pub const fn has_observers(&self) -> bool {
484 !self.observers.is_empty()
485 }
486
487 #[must_use]
489 pub const fn observer_count(&self) -> usize {
490 self.observers.len()
491 }
492
493 #[cfg(feature = "federation")]
499 #[must_use]
500 pub fn federation_metadata(&self) -> Option<crate::federation::FederationMetadata> {
501 self.federation.as_ref().filter(|fed| fed.enabled).map(|fed| {
502 let types = fed
503 .entities
504 .iter()
505 .map(|e| crate::federation::types::FederatedType {
506 name: e.name.clone(),
507 keys: vec![crate::federation::types::KeyDirective {
508 fields: e.key_fields.clone(),
509 resolvable: true,
510 }],
511 is_extends: false,
512 external_fields: Vec::new(),
513 shareable_fields: Vec::new(),
514 field_directives: std::collections::HashMap::new(),
515 })
516 .collect();
517
518 crate::federation::FederationMetadata {
519 enabled: fed.enabled,
520 version: fed.version.clone().unwrap_or_else(|| "v2".to_string()),
521 types,
522 }
523 })
524 }
525
526 #[cfg(not(feature = "federation"))]
528 #[must_use]
529 pub const fn federation_metadata(&self) -> Option<()> {
530 None
531 }
532
533 #[must_use]
539 pub const fn security_config(&self) -> Option<&SecurityConfig> {
540 self.security.as_ref()
541 }
542
543 #[must_use]
551 pub fn is_multi_tenant(&self) -> bool {
552 self.security.as_ref().is_some_and(|s| s.multi_tenant)
553 }
554
555 #[must_use]
565 pub fn find_role(&self, role_name: &str) -> Option<RoleDefinition> {
566 self.security.as_ref().and_then(|config| config.find_role(role_name).cloned())
567 }
568
569 #[must_use]
579 pub fn get_role_scopes(&self, role_name: &str) -> Vec<String> {
580 self.security
581 .as_ref()
582 .map(|config| config.get_role_scopes(role_name))
583 .unwrap_or_default()
584 }
585
586 #[must_use]
597 pub fn role_has_scope(&self, role_name: &str, scope: &str) -> bool {
598 self.security
599 .as_ref()
600 .is_some_and(|config| config.role_has_scope(role_name, scope))
601 }
602
603 #[must_use]
626 pub fn content_hash(&self) -> String {
627 use sha2::{Digest, Sha256};
628 let json = self.to_json().expect("CompiledSchema always serialises — BUG if this fails");
629 let digest = Sha256::digest(json.as_bytes());
630 hex::encode(&digest[..16]) }
632
633 #[must_use]
648 pub fn has_rls_configured(&self) -> bool {
649 self.security.as_ref().is_some_and(|s| {
650 !s.additional
651 .get("policies")
652 .and_then(|p: &serde_json::Value| p.as_array())
653 .is_none_or(|a| a.is_empty())
654 })
655 }
656
657 #[must_use]
663 pub fn raw_schema(&self) -> String {
664 self.schema_sdl.clone().unwrap_or_else(|| {
665 let mut sdl = String::new();
667
668 for type_def in &self.types {
670 let _ = writeln!(sdl, "type {} {{", type_def.name);
671 for field in &type_def.fields {
672 let _ = writeln!(sdl, " {}: {}", field.name, field.field_type);
673 }
674 sdl.push_str("}\n\n");
675 }
676
677 sdl
678 })
679 }
680
681 pub fn validate(&self) -> Result<(), Vec<String>> {
692 let mut errors = Vec::new();
693
694 let mut type_names: std::collections::HashSet<&str> = std::collections::HashSet::new();
696 for type_def in &self.types {
697 if !type_names.insert(type_def.name.as_str()) {
698 errors.push(format!("Duplicate type name: {}", type_def.name));
699 }
700 }
701
702 let mut query_names: std::collections::HashSet<&str> = std::collections::HashSet::new();
704 for query in &self.queries {
705 if !query_names.insert(&query.name) {
706 errors.push(format!("Duplicate query name: {}", query.name));
707 }
708 }
709
710 let mut mutation_names: std::collections::HashSet<&str> = std::collections::HashSet::new();
712 for mutation in &self.mutations {
713 if !mutation_names.insert(&mutation.name) {
714 errors.push(format!("Duplicate mutation name: {}", mutation.name));
715 }
716 }
717
718 for query in &self.queries {
720 if !type_names.contains(query.return_type.as_str())
721 && !is_builtin_type(&query.return_type)
722 {
723 errors.push(format!(
724 "Query '{}' references undefined type '{}'",
725 query.name, query.return_type
726 ));
727 }
728 }
729
730 for mutation in &self.mutations {
732 if !type_names.contains(mutation.return_type.as_str())
733 && !is_builtin_type(&mutation.return_type)
734 {
735 errors.push(format!(
736 "Mutation '{}' references undefined type '{}'",
737 mutation.name, mutation.return_type
738 ));
739 }
740 }
741
742 if errors.is_empty() {
743 Ok(())
744 } else {
745 Err(errors)
746 }
747 }
748}
749
750fn is_builtin_type(name: &str) -> bool {
752 matches!(
753 name,
754 "String"
755 | "Int"
756 | "Float"
757 | "Boolean"
758 | "ID"
759 | "DateTime"
760 | "Date"
761 | "Time"
762 | "JSON"
763 | "UUID"
764 | "Decimal"
765 )
766}
767
768#[cfg(test)]
769mod tests {
770 #![allow(clippy::unwrap_used)] use super::*;
772 #[cfg(feature = "federation")]
773 use crate::schema::config_types::FederationEntity;
774 use crate::schema::{
775 config_types::FederationConfig,
776 graphql_type_defs::TypeDefinition,
777 observer_types::ObserverDefinition,
778 security_config::{RoleDefinition, SecurityConfig},
779 };
780
781 fn make_type_def(name: &str) -> TypeDefinition {
786 TypeDefinition {
787 name: name.into(),
788 sql_source: format!("v_{}", name.to_lowercase()).as_str().into(),
789 jsonb_column: "data".to_string(),
790 fields: vec![],
791 description: None,
792 sql_projection_hint: None,
793 implements: vec![],
794 requires_role: None,
795 is_error: false,
796 relay: false,
797 relationships: vec![],
798 }
799 }
800
801 fn make_query(name: &str, return_type: &str) -> QueryDefinition {
802 QueryDefinition::new(name, return_type)
803 }
804
805 fn make_mutation(name: &str, return_type: &str) -> MutationDefinition {
806 MutationDefinition::new(name, return_type)
807 }
808
809 #[test]
814 fn new_returns_empty_schema() {
815 let schema = CompiledSchema::new();
816 assert!(schema.types.is_empty());
817 assert!(schema.queries.is_empty());
818 assert!(schema.mutations.is_empty());
819 assert!(schema.subscriptions.is_empty());
820 assert!(schema.enums.is_empty());
821 assert!(schema.interfaces.is_empty());
822 assert!(schema.unions.is_empty());
823 }
824
825 #[test]
826 fn from_json_empty_array_fields() {
827 let json = r#"{"types":[],"queries":[],"mutations":[],"subscriptions":[]}"#;
828 let schema = CompiledSchema::from_json(json).unwrap();
829 assert_eq!(schema.types.len(), 0);
830 assert_eq!(schema.queries.len(), 0);
831 assert_eq!(schema.mutations.len(), 0);
832 assert_eq!(schema.subscriptions.len(), 0);
833 }
834
835 #[test]
836 fn from_json_minimal_empty_object() {
837 let schema = CompiledSchema::from_json("{}").unwrap();
839 assert!(schema.types.is_empty());
840 assert!(schema.queries.is_empty());
841 }
842
843 #[test]
844 fn from_json_invalid_returns_error() {
845 let result = CompiledSchema::from_json("not json at all");
846 assert!(result.is_err());
847 }
848
849 #[test]
850 fn from_json_builds_query_index() {
851 let json = r#"{
852 "types": [{"name":"User","sql_source":"v_user","fields":[]}],
853 "queries": [{"name":"users","return_type":"User"}],
854 "mutations": [],
855 "subscriptions": []
856 }"#;
857 let schema = CompiledSchema::from_json(json).unwrap();
858 assert!(schema.query_index.contains_key("users"));
859 assert_eq!(schema.query_index["users"], 0);
860 }
861
862 #[test]
863 fn from_json_builds_mutation_index() {
864 let json = r#"{
865 "types": [{"name":"User","sql_source":"v_user","fields":[]}],
866 "mutations": [{"name":"createUser","return_type":"User"}],
867 "queries": [],
868 "subscriptions": []
869 }"#;
870 let schema = CompiledSchema::from_json(json).unwrap();
871 assert!(schema.mutation_index.contains_key("createUser"));
872 }
873
874 #[test]
879 fn to_json_and_back_is_identity() {
880 let mut schema = CompiledSchema::new();
881 schema.schema_format_version = Some(1);
882 let json = schema.to_json().unwrap();
883 let schema2 = CompiledSchema::from_json(&json).unwrap();
884 assert_eq!(schema, schema2);
885 }
886
887 #[test]
888 fn to_json_pretty_is_valid_json() {
889 let schema = CompiledSchema::new();
890 let pretty = schema.to_json_pretty().unwrap();
891 let _: serde_json::Value = serde_json::from_str(&pretty).unwrap();
893 }
894
895 #[test]
900 fn validate_format_version_none_is_ok() {
901 let schema = CompiledSchema::new(); assert!(schema.validate_format_version().is_ok());
903 }
904
905 #[test]
906 fn validate_format_version_current_is_ok() {
907 let mut schema = CompiledSchema::new();
908 schema.schema_format_version = Some(CURRENT_SCHEMA_FORMAT_VERSION);
909 assert!(schema.validate_format_version().is_ok());
910 }
911
912 #[test]
913 fn validate_format_version_mismatch_is_err() {
914 let mut schema = CompiledSchema::new();
915 schema.schema_format_version = Some(CURRENT_SCHEMA_FORMAT_VERSION + 1);
916 let result = schema.validate_format_version();
917 assert!(result.is_err());
918 let msg = result.unwrap_err();
919 assert!(msg.contains("mismatch"));
920 }
921
922 #[test]
927 fn build_indexes_populates_all_three_maps() {
928 let mut schema = CompiledSchema::new();
929 schema.queries.push(make_query("getUser", "User"));
930 schema.mutations.push(make_mutation("createUser", "User"));
931 schema.build_indexes();
932 assert!(schema.query_index.contains_key("getUser"));
933 assert!(schema.mutation_index.contains_key("createUser"));
934 }
935
936 #[test]
937 fn build_indexes_multiple_queries() {
938 let mut schema = CompiledSchema::new();
939 schema.queries.push(make_query("alpha", "A"));
940 schema.queries.push(make_query("beta", "B"));
941 schema.queries.push(make_query("gamma", "C"));
942 schema.build_indexes();
943 assert_eq!(schema.query_index["alpha"], 0);
944 assert_eq!(schema.query_index["beta"], 1);
945 assert_eq!(schema.query_index["gamma"], 2);
946 }
947
948 #[test]
953 fn find_type_returns_none_for_missing() {
954 let schema = CompiledSchema::new();
955 assert!(schema.find_type("Ghost").is_none());
956 }
957
958 #[test]
959 fn find_type_returns_existing() {
960 let mut schema = CompiledSchema::new();
961 schema.types.push(make_type_def("User"));
962 assert!(schema.find_type("User").is_some());
963 assert_eq!(schema.find_type("User").unwrap().name, "User");
964 }
965
966 #[test]
967 fn find_query_uses_index_when_populated() {
968 let json = r#"{
969 "types": [{"name":"User","sql_source":"v_user","fields":[]}],
970 "queries": [{"name":"users","return_type":"User"}],
971 "mutations": [],
972 "subscriptions": []
973 }"#;
974 let schema = CompiledSchema::from_json(json).unwrap();
975 let q = schema.find_query("users");
976 assert!(q.is_some());
977 assert_eq!(q.unwrap().name, "users");
978 }
979
980 #[test]
981 fn find_query_falls_back_to_linear_scan_without_index() {
982 let mut schema = CompiledSchema::new();
984 schema.queries.push(make_query("direct", "String"));
985 let q = schema.find_query("direct");
987 assert!(q.is_some());
988 }
989
990 #[test]
991 fn find_query_returns_none_for_missing() {
992 let schema = CompiledSchema::from_json("{}").unwrap();
993 assert!(schema.find_query("nope").is_none());
994 }
995
996 #[test]
997 fn find_mutation_returns_correct_entry() {
998 let json = r#"{
999 "types": [{"name":"User","sql_source":"v_user","fields":[]}],
1000 "mutations": [{"name":"createUser","return_type":"User"}],
1001 "queries": [],
1002 "subscriptions": []
1003 }"#;
1004 let schema = CompiledSchema::from_json(json).unwrap();
1005 assert!(schema.find_mutation("createUser").is_some());
1006 assert!(schema.find_mutation("nope").is_none());
1007 }
1008
1009 #[test]
1010 fn find_interface_returns_none_when_absent() {
1011 let schema = CompiledSchema::new();
1012 assert!(schema.find_interface("Node").is_none());
1013 }
1014
1015 #[test]
1016 fn find_implementors_filters_by_interface() {
1017 let mut schema = CompiledSchema::new();
1018 let mut user = make_type_def("User");
1019 user.implements = vec!["Node".to_string()];
1020 schema.types.push(user);
1021 schema.types.push(make_type_def("Product")); let implementors = schema.find_implementors("Node");
1024 assert_eq!(implementors.len(), 1);
1025 assert_eq!(implementors[0].name, "User");
1026 }
1027
1028 #[test]
1033 fn operation_count_sums_all_three() {
1034 let mut schema = CompiledSchema::new();
1035 schema.queries.push(make_query("q1", "String"));
1036 schema.queries.push(make_query("q2", "String"));
1037 schema.mutations.push(make_mutation("m1", "String"));
1038 assert_eq!(schema.operation_count(), 3);
1039 }
1040
1041 #[test]
1042 fn operation_count_zero_for_empty_schema() {
1043 assert_eq!(CompiledSchema::new().operation_count(), 0);
1044 }
1045
1046 #[test]
1051 fn fact_table_add_and_get() {
1052 use crate::compiler::fact_table::{DimensionColumn, FactTableMetadata};
1053
1054 let mut schema = CompiledSchema::new();
1055 assert!(!schema.has_fact_tables());
1056
1057 let meta = FactTableMetadata {
1058 table_name: "tf_sales".to_string(),
1059 measures: vec![],
1060 dimensions: DimensionColumn {
1061 name: "data".to_string(),
1062 paths: vec![],
1063 },
1064 denormalized_filters: vec![],
1065 calendar_dimensions: vec![],
1066 };
1067 schema.add_fact_table("tf_sales".to_string(), meta);
1068
1069 assert!(schema.has_fact_tables());
1070 assert!(schema.get_fact_table("tf_sales").is_some());
1071 assert!(schema.get_fact_table("tf_missing").is_none());
1072 }
1073
1074 #[test]
1075 fn list_fact_tables_returns_all_names() {
1076 use crate::compiler::fact_table::{DimensionColumn, FactTableMetadata};
1077
1078 let make_meta = |name: &str| FactTableMetadata {
1079 table_name: name.to_string(),
1080 measures: vec![],
1081 dimensions: DimensionColumn {
1082 name: "data".to_string(),
1083 paths: vec![],
1084 },
1085 denormalized_filters: vec![],
1086 calendar_dimensions: vec![],
1087 };
1088
1089 let mut schema = CompiledSchema::new();
1090 schema.add_fact_table("tf_a".to_string(), make_meta("tf_a"));
1091 schema.add_fact_table("tf_b".to_string(), make_meta("tf_b"));
1092
1093 let names = schema.list_fact_tables();
1094 assert_eq!(names.len(), 2);
1095 assert!(names.contains(&"tf_a"));
1096 assert!(names.contains(&"tf_b"));
1097 }
1098
1099 #[test]
1104 fn has_observers_false_for_empty_schema() {
1105 assert!(!CompiledSchema::new().has_observers());
1106 }
1107
1108 #[test]
1109 fn find_observer_returns_by_name() {
1110 let mut schema = CompiledSchema::new();
1111 schema.observers.push(ObserverDefinition::new("onInsert", "Order", "INSERT"));
1112 assert!(schema.find_observer("onInsert").is_some());
1113 assert!(schema.find_observer("missing").is_none());
1114 }
1115
1116 #[test]
1117 fn find_observers_for_entity_filters_correctly() {
1118 let mut schema = CompiledSchema::new();
1119 schema.observers.push(ObserverDefinition::new("obs1", "Order", "INSERT"));
1120 schema.observers.push(ObserverDefinition::new("obs2", "Order", "UPDATE"));
1121 schema.observers.push(ObserverDefinition::new("obs3", "User", "INSERT"));
1122
1123 let order_obs = schema.find_observers_for_entity("Order");
1124 assert_eq!(order_obs.len(), 2);
1125 let user_obs = schema.find_observers_for_entity("User");
1126 assert_eq!(user_obs.len(), 1);
1127 }
1128
1129 #[test]
1130 fn find_observers_for_event_filters_correctly() {
1131 let mut schema = CompiledSchema::new();
1132 schema.observers.push(ObserverDefinition::new("obs1", "Order", "INSERT"));
1133 schema.observers.push(ObserverDefinition::new("obs2", "User", "INSERT"));
1134 schema.observers.push(ObserverDefinition::new("obs3", "Order", "DELETE"));
1135
1136 let inserts = schema.find_observers_for_event("INSERT");
1137 assert_eq!(inserts.len(), 2);
1138 }
1139
1140 #[test]
1141 fn observer_count_matches_vec_length() {
1142 let mut schema = CompiledSchema::new();
1143 assert_eq!(schema.observer_count(), 0);
1144 schema.observers.push(ObserverDefinition::new("o1", "A", "INSERT"));
1145 assert_eq!(schema.observer_count(), 1);
1146 }
1147
1148 #[test]
1153 fn is_multi_tenant_false_by_default() {
1154 assert!(!CompiledSchema::new().is_multi_tenant());
1155 }
1156
1157 #[test]
1158 fn is_multi_tenant_true_when_configured() {
1159 let mut schema = CompiledSchema::new();
1160 let mut sec = SecurityConfig::new();
1161 sec.multi_tenant = true;
1162 schema.security = Some(sec);
1163 assert!(schema.is_multi_tenant());
1164 }
1165
1166 #[test]
1167 fn find_role_returns_none_without_security_config() {
1168 assert!(CompiledSchema::new().find_role("admin").is_none());
1169 }
1170
1171 #[test]
1172 fn find_role_returns_defined_role() {
1173 let mut schema = CompiledSchema::new();
1174 let mut sec = SecurityConfig::new();
1175 sec.add_role(RoleDefinition::new("editor", vec!["read:*".to_string()]));
1176 schema.security = Some(sec);
1177 assert!(schema.find_role("editor").is_some());
1178 }
1179
1180 #[test]
1181 fn role_has_scope_false_without_security() {
1182 assert!(!CompiledSchema::new().role_has_scope("admin", "read:*"));
1183 }
1184
1185 #[test]
1186 fn role_has_scope_true_when_granted() {
1187 let mut schema = CompiledSchema::new();
1188 let mut sec = SecurityConfig::new();
1189 sec.add_role(RoleDefinition::new("admin", vec!["read:*".to_string()]));
1190 schema.security = Some(sec);
1191 assert!(schema.role_has_scope("admin", "read:anything"));
1192 assert!(!schema.role_has_scope("admin", "write:anything"));
1193 }
1194
1195 #[test]
1196 fn get_role_scopes_empty_for_missing_role() {
1197 let schema = CompiledSchema::new();
1198 assert!(schema.get_role_scopes("ghost").is_empty());
1199 }
1200
1201 #[test]
1206 fn federation_metadata_none_when_no_federation() {
1207 assert!(CompiledSchema::new().federation_metadata().is_none());
1208 }
1209
1210 #[test]
1211 fn federation_metadata_none_when_disabled() {
1212 let mut schema = CompiledSchema::new();
1213 schema.federation = Some(FederationConfig {
1214 enabled: false,
1215 ..Default::default()
1216 });
1217 assert!(schema.federation_metadata().is_none());
1218 }
1219
1220 #[test]
1221 #[cfg(feature = "federation")]
1222 fn federation_metadata_some_when_enabled() {
1223 let mut schema = CompiledSchema::new();
1224 schema.federation = Some(FederationConfig {
1225 enabled: true,
1226 version: Some("v2".to_string()),
1227 entities: vec![FederationEntity {
1228 name: "User".to_string(),
1229 key_fields: vec!["id".to_string()],
1230 }],
1231 ..Default::default()
1232 });
1233 let meta = schema.federation_metadata();
1234 assert!(meta.is_some());
1235 let meta = meta.unwrap();
1236 assert!(meta.enabled);
1237 assert_eq!(meta.types.len(), 1);
1238 assert_eq!(meta.types[0].name, "User");
1239 }
1240
1241 #[test]
1246 fn content_hash_is_32_hex_chars() {
1247 let hash = CompiledSchema::new().content_hash();
1248 assert_eq!(hash.len(), 32);
1249 assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
1250 }
1251
1252 #[test]
1253 fn content_hash_is_stable() {
1254 let schema = CompiledSchema::new();
1255 assert_eq!(schema.content_hash(), schema.content_hash());
1256 }
1257
1258 #[test]
1259 fn content_hash_differs_for_different_schemas() {
1260 let s1 = CompiledSchema::new();
1261 let mut s2 = CompiledSchema::new();
1262 s2.schema_format_version = Some(1);
1263 assert_ne!(s1.content_hash(), s2.content_hash());
1264 }
1265
1266 #[test]
1271 fn has_rls_configured_false_without_security() {
1272 assert!(!CompiledSchema::new().has_rls_configured());
1273 }
1274
1275 #[test]
1276 fn has_rls_configured_false_when_policies_empty() {
1277 let mut schema = CompiledSchema::new();
1278 let mut sec = SecurityConfig::new();
1279 sec.additional.insert("policies".to_string(), serde_json::json!([]));
1280 schema.security = Some(sec);
1281 assert!(!schema.has_rls_configured());
1282 }
1283
1284 #[test]
1285 fn has_rls_configured_true_when_policies_present() {
1286 let mut schema = CompiledSchema::new();
1287 let mut sec = SecurityConfig::new();
1288 sec.additional.insert(
1289 "policies".to_string(),
1290 serde_json::json!([{"table": "orders", "using": "tenant_id = current_setting('app.tenant_id')"}]),
1291 );
1292 schema.security = Some(sec);
1293 assert!(schema.has_rls_configured());
1294 }
1295
1296 #[test]
1301 fn validate_empty_schema_is_ok() {
1302 assert!(CompiledSchema::new().validate().is_ok());
1303 }
1304
1305 #[test]
1306 fn validate_detects_duplicate_type_names() {
1307 let mut schema = CompiledSchema::new();
1308 schema.types.push(make_type_def("User"));
1309 schema.types.push(make_type_def("User")); let result = schema.validate();
1311 assert!(result.is_err());
1312 let errors = result.unwrap_err();
1313 assert!(errors.iter().any(|e| e.contains("Duplicate type name")));
1314 }
1315
1316 #[test]
1317 fn validate_detects_duplicate_query_names() {
1318 let mut schema = CompiledSchema::new();
1319 schema.queries.push(make_query("getUser", "String"));
1320 schema.queries.push(make_query("getUser", "String")); let result = schema.validate();
1322 assert!(result.is_err());
1323 let errors = result.unwrap_err();
1324 assert!(errors.iter().any(|e| e.contains("Duplicate query name")));
1325 }
1326
1327 #[test]
1328 fn validate_detects_duplicate_mutation_names() {
1329 let mut schema = CompiledSchema::new();
1330 schema.mutations.push(make_mutation("createUser", "String"));
1331 schema.mutations.push(make_mutation("createUser", "String")); let result = schema.validate();
1333 assert!(result.is_err());
1334 }
1335
1336 #[test]
1337 fn validate_undefined_return_type_in_query_is_error() {
1338 let mut schema = CompiledSchema::new();
1339 schema.queries.push(make_query("getWidget", "Widget"));
1341 let result = schema.validate();
1342 assert!(result.is_err());
1343 let errors = result.unwrap_err();
1344 assert!(errors.iter().any(|e| e.contains("Widget")));
1345 }
1346
1347 #[test]
1348 fn validate_builtin_scalar_return_type_is_ok() {
1349 let mut schema = CompiledSchema::new();
1350 schema.queries.push(make_query("ping", "String"));
1351 schema.queries.push(make_query("count", "Int"));
1352 assert!(schema.validate().is_ok());
1353 }
1354
1355 #[test]
1356 fn validate_defined_type_as_return_type_is_ok() {
1357 let mut schema = CompiledSchema::new();
1358 schema.types.push(make_type_def("User"));
1359 schema.queries.push(make_query("getUser", "User"));
1360 assert!(schema.validate().is_ok());
1361 }
1362
1363 #[test]
1368 fn raw_schema_returns_sdl_when_set() {
1369 let mut schema = CompiledSchema::new();
1370 schema.schema_sdl = Some("type Query { ping: String }".to_string());
1371 assert_eq!(schema.raw_schema(), "type Query { ping: String }");
1372 }
1373
1374 #[test]
1375 fn raw_schema_generates_from_types_when_sdl_absent() {
1376 let mut schema = CompiledSchema::new();
1377 schema.types.push(make_type_def("User"));
1378 let sdl = schema.raw_schema();
1379 assert!(sdl.contains("User"));
1380 }
1381
1382 #[test]
1387 fn builtin_scalar_types_pass_validation() {
1388 let scalars = [
1389 "String", "Int", "Float", "Boolean", "ID", "DateTime", "Date", "Time", "JSON", "UUID",
1390 "Decimal",
1391 ];
1392 for scalar in scalars {
1393 let mut schema = CompiledSchema::new();
1394 schema.queries.push(make_query("q", scalar));
1395 assert!(schema.validate().is_ok(), "{scalar} should be a recognised built-in");
1396 }
1397 }
1398
1399 #[test]
1400 fn unknown_scalar_fails_validation() {
1401 let mut schema = CompiledSchema::new();
1402 schema.queries.push(make_query("q", "Blob"));
1403 assert!(schema.validate().is_err());
1404 }
1405}