1use std::collections::HashMap;
6
7use super::{GraphQLScalar, RelationType, to_pascal_case, to_camel_case};
8
9#[derive(Debug, Clone)]
11pub struct GraphQLSchema {
12 pub types: Vec<GraphQLType>,
14 pub queries: Vec<QueryDefinition>,
16 pub mutations: Vec<MutationDefinition>,
18 pub relationships: Vec<Relationship>,
20 pub input_types: Vec<GraphQLInputType>,
22 pub enum_types: Vec<GraphQLEnumType>,
24}
25
26impl GraphQLSchema {
27 pub fn new() -> Self {
29 Self {
30 types: Vec::new(),
31 queries: Vec::new(),
32 mutations: Vec::new(),
33 relationships: Vec::new(),
34 input_types: Vec::new(),
35 enum_types: Vec::new(),
36 }
37 }
38
39 pub fn add_type(&mut self, type_def: GraphQLType) {
41 self.types.push(type_def);
42 }
43
44 pub fn add_query(&mut self, query: QueryDefinition) {
46 self.queries.push(query);
47 }
48
49 pub fn add_mutation(&mut self, mutation: MutationDefinition) {
51 self.mutations.push(mutation);
52 }
53
54 pub fn add_relationship(&mut self, relationship: Relationship) {
56 self.relationships.push(relationship);
57 }
58
59 pub fn get_type(&self, name: &str) -> Option<&GraphQLType> {
61 self.types.iter().find(|t| t.name == name)
62 }
63
64 pub fn get_relationships_for(&self, type_name: &str) -> Vec<&Relationship> {
66 self.relationships.iter()
67 .filter(|r| r.from_type == type_name)
68 .collect()
69 }
70
71 pub fn to_sdl(&self) -> String {
73 let mut sdl = String::new();
74
75 sdl.push_str("# Custom Scalars\n");
77 sdl.push_str("scalar DateTime\n");
78 sdl.push_str("scalar Date\n");
79 sdl.push_str("scalar Time\n");
80 sdl.push_str("scalar JSON\n");
81 sdl.push_str("scalar Decimal\n");
82 sdl.push_str("scalar BigInt\n");
83 sdl.push_str("\n");
84
85 for enum_type in &self.enum_types {
87 sdl.push_str(&format!("enum {} {{\n", enum_type.name));
88 for value in &enum_type.values {
89 sdl.push_str(&format!(" {}\n", value));
90 }
91 sdl.push_str("}\n\n");
92 }
93
94 for type_def in &self.types {
96 if let Some(ref desc) = type_def.description {
97 sdl.push_str(&format!("\"\"\"{}\"\"\"\n", desc));
98 }
99 sdl.push_str(&format!("type {} {{\n", type_def.name));
100
101 for field in &type_def.fields {
102 if let Some(ref desc) = field.description {
103 sdl.push_str(&format!(" \"\"\"{}\"\"\"\n", desc));
104 }
105
106 let type_str = if field.nullable {
107 field.graphql_type.to_string()
108 } else {
109 format!("{}!", field.graphql_type)
110 };
111
112 sdl.push_str(&format!(" {}: {}\n", field.name, type_str));
113 }
114
115 for rel in self.get_relationships_for(&type_def.name) {
117 let type_str = if rel.relation_type.is_list() {
118 format!("[{}!]!", rel.to_type)
119 } else {
120 format!("{}!", rel.to_type)
121 };
122 sdl.push_str(&format!(" {}: {}\n", rel.field_name, type_str));
123 }
124
125 sdl.push_str("}\n\n");
126 }
127
128 for input_type in &self.input_types {
130 sdl.push_str(&format!("input {} {{\n", input_type.name));
131 for field in &input_type.fields {
132 let type_str = if field.nullable {
133 field.graphql_type.to_string()
134 } else {
135 format!("{}!", field.graphql_type)
136 };
137 sdl.push_str(&format!(" {}: {}\n", field.name, type_str));
138 }
139 sdl.push_str("}\n\n");
140 }
141
142 sdl.push_str("type Query {\n");
144 for query in &self.queries {
145 let args: Vec<String> = query.arguments.iter()
146 .map(|a| {
147 let type_str = if a.nullable {
148 a.graphql_type.to_string()
149 } else {
150 format!("{}!", a.graphql_type)
151 };
152 format!("{}: {}", a.name, type_str)
153 })
154 .collect();
155
156 let args_str = if args.is_empty() {
157 String::new()
158 } else {
159 format!("({})", args.join(", "))
160 };
161
162 let return_type = if query.returns_list {
163 format!("[{}!]!", query.return_type)
164 } else {
165 query.return_type.clone()
166 };
167
168 sdl.push_str(&format!(" {}{}: {}\n", query.name, args_str, return_type));
169 }
170 sdl.push_str("}\n\n");
171
172 if !self.mutations.is_empty() {
174 sdl.push_str("type Mutation {\n");
175 for mutation in &self.mutations {
176 let args: Vec<String> = mutation.arguments.iter()
177 .map(|a| {
178 let type_str = if a.nullable {
179 a.graphql_type.to_string()
180 } else {
181 format!("{}!", a.graphql_type)
182 };
183 format!("{}: {}", a.name, type_str)
184 })
185 .collect();
186
187 let args_str = if args.is_empty() {
188 String::new()
189 } else {
190 format!("({})", args.join(", "))
191 };
192
193 sdl.push_str(&format!(" {}{}: {}\n", mutation.name, args_str, mutation.return_type));
194 }
195 sdl.push_str("}\n");
196 }
197
198 sdl
199 }
200}
201
202impl Default for GraphQLSchema {
203 fn default() -> Self {
204 Self::new()
205 }
206}
207
208#[derive(Debug, Clone)]
210pub struct GraphQLType {
211 pub name: String,
213 pub fields: Vec<GraphQLField>,
215 pub description: Option<String>,
217 pub table_name: Option<String>,
219}
220
221impl GraphQLType {
222 pub fn new(name: impl Into<String>) -> Self {
224 Self {
225 name: name.into(),
226 fields: Vec::new(),
227 description: None,
228 table_name: None,
229 }
230 }
231
232 pub fn add_field(&mut self, field: GraphQLField) {
234 self.fields.push(field);
235 }
236
237 pub fn with_description(mut self, description: impl Into<String>) -> Self {
239 self.description = Some(description.into());
240 self
241 }
242
243 pub fn from_table(mut self, table_name: impl Into<String>) -> Self {
245 self.table_name = Some(table_name.into());
246 self
247 }
248
249 pub fn get_field(&self, name: &str) -> Option<&GraphQLField> {
251 self.fields.iter().find(|f| f.name == name)
252 }
253}
254
255#[derive(Debug, Clone)]
257pub struct GraphQLField {
258 pub name: String,
260 pub graphql_type: FieldType,
262 pub nullable: bool,
264 pub description: Option<String>,
266 pub column_name: Option<String>,
268 pub deprecated: bool,
270 pub deprecation_reason: Option<String>,
272}
273
274impl GraphQLField {
275 pub fn new(name: impl Into<String>, graphql_type: FieldType) -> Self {
277 Self {
278 name: name.into(),
279 graphql_type,
280 nullable: true,
281 description: None,
282 column_name: None,
283 deprecated: false,
284 deprecation_reason: None,
285 }
286 }
287
288 pub fn nullable(mut self, nullable: bool) -> Self {
290 self.nullable = nullable;
291 self
292 }
293
294 pub fn with_description(mut self, description: impl Into<String>) -> Self {
296 self.description = Some(description.into());
297 self
298 }
299
300 pub fn from_column(mut self, column_name: impl Into<String>) -> Self {
302 self.column_name = Some(column_name.into());
303 self
304 }
305
306 pub fn deprecated(mut self, reason: impl Into<String>) -> Self {
308 self.deprecated = true;
309 self.deprecation_reason = Some(reason.into());
310 self
311 }
312}
313
314#[derive(Debug, Clone, PartialEq, Eq)]
316pub enum FieldType {
317 Scalar(GraphQLScalar),
319 Object(String),
321 List(Box<FieldType>),
323 NonNull(Box<FieldType>),
325}
326
327impl FieldType {
328 pub fn scalar(scalar: GraphQLScalar) -> Self {
330 FieldType::Scalar(scalar)
331 }
332
333 pub fn object(name: impl Into<String>) -> Self {
335 FieldType::Object(name.into())
336 }
337
338 pub fn list(inner: FieldType) -> Self {
340 FieldType::List(Box::new(inner))
341 }
342
343 pub fn non_null(inner: FieldType) -> Self {
345 FieldType::NonNull(Box::new(inner))
346 }
347}
348
349impl std::fmt::Display for FieldType {
350 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
351 match self {
352 FieldType::Scalar(s) => write!(f, "{}", s.to_sdl()),
353 FieldType::Object(name) => write!(f, "{}", name),
354 FieldType::List(inner) => write!(f, "[{}]", inner),
355 FieldType::NonNull(inner) => write!(f, "{}!", inner),
356 }
357 }
358}
359
360#[derive(Debug, Clone)]
362pub struct GraphQLInputType {
363 pub name: String,
365 pub fields: Vec<GraphQLField>,
367}
368
369#[derive(Debug, Clone)]
371pub struct GraphQLEnumType {
372 pub name: String,
374 pub values: Vec<String>,
376}
377
378#[derive(Debug, Clone)]
380pub struct QueryDefinition {
381 pub name: String,
383 pub arguments: Vec<ArgumentDefinition>,
385 pub return_type: String,
387 pub returns_list: bool,
389 pub table_name: Option<String>,
391}
392
393impl QueryDefinition {
394 pub fn new(name: impl Into<String>, return_type: impl Into<String>) -> Self {
396 Self {
397 name: name.into(),
398 arguments: Vec::new(),
399 return_type: return_type.into(),
400 returns_list: false,
401 table_name: None,
402 }
403 }
404
405 pub fn arg(mut self, arg: ArgumentDefinition) -> Self {
407 self.arguments.push(arg);
408 self
409 }
410
411 pub fn returns_list(mut self, list: bool) -> Self {
413 self.returns_list = list;
414 self
415 }
416
417 pub fn from_table(mut self, table: impl Into<String>) -> Self {
419 self.table_name = Some(table.into());
420 self
421 }
422}
423
424#[derive(Debug, Clone)]
426pub struct MutationDefinition {
427 pub name: String,
429 pub arguments: Vec<ArgumentDefinition>,
431 pub return_type: String,
433 pub table_name: Option<String>,
435 pub kind: MutationKind,
437}
438
439impl MutationDefinition {
440 pub fn new(name: impl Into<String>, return_type: impl Into<String>, kind: MutationKind) -> Self {
442 Self {
443 name: name.into(),
444 arguments: Vec::new(),
445 return_type: return_type.into(),
446 table_name: None,
447 kind,
448 }
449 }
450
451 pub fn arg(mut self, arg: ArgumentDefinition) -> Self {
453 self.arguments.push(arg);
454 self
455 }
456
457 pub fn from_table(mut self, table: impl Into<String>) -> Self {
459 self.table_name = Some(table.into());
460 self
461 }
462}
463
464#[derive(Debug, Clone, Copy, PartialEq, Eq)]
466pub enum MutationKind {
467 Create,
469 Update,
471 Delete,
473}
474
475#[derive(Debug, Clone)]
477pub struct ArgumentDefinition {
478 pub name: String,
480 pub graphql_type: FieldType,
482 pub nullable: bool,
484 pub default_value: Option<serde_json::Value>,
486}
487
488impl ArgumentDefinition {
489 pub fn new(name: impl Into<String>, graphql_type: FieldType) -> Self {
491 Self {
492 name: name.into(),
493 graphql_type,
494 nullable: true,
495 default_value: None,
496 }
497 }
498
499 pub fn required(mut self) -> Self {
501 self.nullable = false;
502 self
503 }
504
505 pub fn default(mut self, value: serde_json::Value) -> Self {
507 self.default_value = Some(value);
508 self
509 }
510}
511
512#[derive(Debug, Clone)]
514pub struct Relationship {
515 pub name: String,
517 pub from_type: String,
519 pub to_type: String,
521 pub from_column: String,
523 pub to_column: String,
525 pub relation_type: RelationType,
527 pub field_name: String,
529}
530
531impl Relationship {
532 pub fn new(
534 name: impl Into<String>,
535 from_type: impl Into<String>,
536 to_type: impl Into<String>,
537 relation_type: RelationType,
538 ) -> Self {
539 let name = name.into();
540 let field_name = to_camel_case(&name);
541
542 Self {
543 name: name.clone(),
544 from_type: from_type.into(),
545 to_type: to_type.into(),
546 from_column: "id".to_string(),
547 to_column: "id".to_string(),
548 relation_type,
549 field_name,
550 }
551 }
552
553 pub fn columns(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
555 self.from_column = from.into();
556 self.to_column = to.into();
557 self
558 }
559
560 pub fn field(mut self, name: impl Into<String>) -> Self {
562 self.field_name = name.into();
563 self
564 }
565}
566
567#[derive(Debug)]
569pub struct SchemaIntrospector {
570 excluded_tables: Vec<String>,
572 excluded_columns: HashMap<String, Vec<String>>,
574 type_names: HashMap<String, String>,
576}
577
578impl SchemaIntrospector {
579 pub fn new() -> Self {
581 Self {
582 excluded_tables: vec![
583 "pg_catalog".to_string(),
584 "information_schema".to_string(),
585 ],
586 excluded_columns: HashMap::new(),
587 type_names: HashMap::new(),
588 }
589 }
590
591 pub fn exclude_table(&mut self, table: impl Into<String>) {
593 self.excluded_tables.push(table.into());
594 }
595
596 pub fn exclude_column(&mut self, table: impl Into<String>, column: impl Into<String>) {
598 self.excluded_columns
599 .entry(table.into())
600 .or_default()
601 .push(column.into());
602 }
603
604 pub fn set_type_name(&mut self, table: impl Into<String>, type_name: impl Into<String>) {
606 self.type_names.insert(table.into(), type_name.into());
607 }
608
609 pub fn build_schema(&self, tables: &[TableDefinition]) -> GraphQLSchema {
611 let mut schema = GraphQLSchema::new();
612
613 for table in tables {
614 if self.excluded_tables.contains(&table.name) {
615 continue;
616 }
617
618 let type_def = self.generate_type(table);
620 let type_name = type_def.name.clone();
621 schema.add_type(type_def);
622
623 schema.add_query(
625 QueryDefinition::new(to_camel_case(&table.name), &type_name)
626 .arg(ArgumentDefinition::new("id", FieldType::scalar(GraphQLScalar::ID)).required())
627 .from_table(&table.name)
628 );
629
630 schema.add_query(
631 QueryDefinition::new(format!("{}s", to_camel_case(&table.name)), &type_name)
632 .arg(ArgumentDefinition::new("limit", FieldType::scalar(GraphQLScalar::Int)))
633 .arg(ArgumentDefinition::new("offset", FieldType::scalar(GraphQLScalar::Int)))
634 .arg(ArgumentDefinition::new("where", FieldType::object(format!("{}Filter", type_name))))
635 .returns_list(true)
636 .from_table(&table.name)
637 );
638
639 schema.add_mutation(
641 MutationDefinition::new(format!("create{}", type_name), &type_name, MutationKind::Create)
642 .arg(ArgumentDefinition::new("input", FieldType::object(format!("Create{}Input", type_name))).required())
643 .from_table(&table.name)
644 );
645
646 schema.add_mutation(
647 MutationDefinition::new(format!("update{}", type_name), &type_name, MutationKind::Update)
648 .arg(ArgumentDefinition::new("id", FieldType::scalar(GraphQLScalar::ID)).required())
649 .arg(ArgumentDefinition::new("input", FieldType::object(format!("Update{}Input", type_name))).required())
650 .from_table(&table.name)
651 );
652
653 schema.add_mutation(
654 MutationDefinition::new(format!("delete{}", type_name), "Boolean".to_string(), MutationKind::Delete)
655 .arg(ArgumentDefinition::new("id", FieldType::scalar(GraphQLScalar::ID)).required())
656 .from_table(&table.name)
657 );
658
659 let filter_type = self.generate_filter_type(table);
661 schema.input_types.push(filter_type);
662
663 let create_input = self.generate_create_input(table, &type_name);
665 schema.input_types.push(create_input);
666
667 let update_input = self.generate_update_input(table, &type_name);
669 schema.input_types.push(update_input);
670 }
671
672 for table in tables {
674 for fk in &table.foreign_keys {
675 let from_type = self.get_type_name(&table.name);
676 let to_type = self.get_type_name(&fk.referenced_table);
677
678 schema.add_relationship(
680 Relationship::new(&fk.name, &from_type, &to_type, RelationType::ManyToOne)
681 .columns(&fk.column, &fk.referenced_column)
682 .field(to_camel_case(&fk.name))
683 );
684
685 let reverse_name = format!("{}s", to_camel_case(&table.name));
687 schema.add_relationship(
688 Relationship::new(&reverse_name, &to_type, &from_type, RelationType::OneToMany)
689 .columns(&fk.referenced_column, &fk.column)
690 .field(&reverse_name)
691 );
692 }
693 }
694
695 schema
696 }
697
698 fn generate_type(&self, table: &TableDefinition) -> GraphQLType {
700 let type_name = self.get_type_name(&table.name);
701 let mut type_def = GraphQLType::new(&type_name)
702 .from_table(&table.name);
703
704 let excluded = self.excluded_columns.get(&table.name);
705
706 for column in &table.columns {
707 if let Some(excluded) = excluded {
708 if excluded.contains(&column.name) {
709 continue;
710 }
711 }
712
713 let scalar = GraphQLScalar::from_sql_type(&column.data_type);
714 let field = GraphQLField::new(
715 to_camel_case(&column.name),
716 FieldType::scalar(scalar),
717 )
718 .nullable(column.nullable)
719 .from_column(&column.name);
720
721 type_def.add_field(field);
722 }
723
724 type_def
725 }
726
727 fn generate_filter_type(&self, table: &TableDefinition) -> GraphQLInputType {
729 let type_name = self.get_type_name(&table.name);
730 let mut input = GraphQLInputType {
731 name: format!("{}Filter", type_name),
732 fields: Vec::new(),
733 };
734
735 for column in &table.columns {
736 let scalar = GraphQLScalar::from_sql_type(&column.data_type);
737 let filter_type_name = format!("{}Filter", scalar.to_sdl());
738
739 input.fields.push(GraphQLField::new(
740 to_camel_case(&column.name),
741 FieldType::object(filter_type_name),
742 ));
743 }
744
745 input.fields.push(GraphQLField::new(
747 "AND",
748 FieldType::list(FieldType::object(format!("{}Filter", type_name))),
749 ));
750 input.fields.push(GraphQLField::new(
751 "OR",
752 FieldType::list(FieldType::object(format!("{}Filter", type_name))),
753 ));
754
755 input
756 }
757
758 fn generate_create_input(&self, table: &TableDefinition, type_name: &str) -> GraphQLInputType {
760 let mut input = GraphQLInputType {
761 name: format!("Create{}Input", type_name),
762 fields: Vec::new(),
763 };
764
765 for column in &table.columns {
766 if column.is_primary_key && column.data_type.to_lowercase().contains("serial") {
768 continue;
769 }
770
771 let scalar = GraphQLScalar::from_sql_type(&column.data_type);
772 input.fields.push(GraphQLField::new(
773 to_camel_case(&column.name),
774 FieldType::scalar(scalar),
775 ).nullable(column.nullable || column.has_default));
776 }
777
778 input
779 }
780
781 fn generate_update_input(&self, table: &TableDefinition, type_name: &str) -> GraphQLInputType {
783 let mut input = GraphQLInputType {
784 name: format!("Update{}Input", type_name),
785 fields: Vec::new(),
786 };
787
788 for column in &table.columns {
789 if column.is_primary_key {
791 continue;
792 }
793
794 let scalar = GraphQLScalar::from_sql_type(&column.data_type);
795 input.fields.push(GraphQLField::new(
796 to_camel_case(&column.name),
797 FieldType::scalar(scalar),
798 ));
799 }
800
801 input
802 }
803
804 fn get_type_name(&self, table_name: &str) -> String {
806 self.type_names
807 .get(table_name)
808 .cloned()
809 .unwrap_or_else(|| to_pascal_case(table_name))
810 }
811}
812
813impl Default for SchemaIntrospector {
814 fn default() -> Self {
815 Self::new()
816 }
817}
818
819#[derive(Debug, Clone)]
821pub struct TableDefinition {
822 pub name: String,
824 pub schema: String,
826 pub columns: Vec<ColumnDefinition>,
828 pub foreign_keys: Vec<ForeignKeyDefinition>,
830}
831
832impl TableDefinition {
833 pub fn new(name: impl Into<String>) -> Self {
835 Self {
836 name: name.into(),
837 schema: "public".to_string(),
838 columns: Vec::new(),
839 foreign_keys: Vec::new(),
840 }
841 }
842
843 pub fn column(mut self, column: ColumnDefinition) -> Self {
845 self.columns.push(column);
846 self
847 }
848
849 pub fn foreign_key(mut self, fk: ForeignKeyDefinition) -> Self {
851 self.foreign_keys.push(fk);
852 self
853 }
854}
855
856#[derive(Debug, Clone)]
858pub struct ColumnDefinition {
859 pub name: String,
861 pub data_type: String,
863 pub nullable: bool,
865 pub is_primary_key: bool,
867 pub has_default: bool,
869}
870
871impl ColumnDefinition {
872 pub fn new(name: impl Into<String>, data_type: impl Into<String>) -> Self {
874 Self {
875 name: name.into(),
876 data_type: data_type.into(),
877 nullable: true,
878 is_primary_key: false,
879 has_default: false,
880 }
881 }
882
883 pub fn nullable(mut self, nullable: bool) -> Self {
885 self.nullable = nullable;
886 self
887 }
888
889 pub fn primary_key(mut self) -> Self {
891 self.is_primary_key = true;
892 self.nullable = false;
893 self
894 }
895
896 pub fn with_default(mut self) -> Self {
898 self.has_default = true;
899 self
900 }
901}
902
903#[derive(Debug, Clone)]
905pub struct ForeignKeyDefinition {
906 pub name: String,
908 pub column: String,
910 pub referenced_table: String,
912 pub referenced_column: String,
914}
915
916impl ForeignKeyDefinition {
917 pub fn new(
919 name: impl Into<String>,
920 column: impl Into<String>,
921 referenced_table: impl Into<String>,
922 referenced_column: impl Into<String>,
923 ) -> Self {
924 Self {
925 name: name.into(),
926 column: column.into(),
927 referenced_table: referenced_table.into(),
928 referenced_column: referenced_column.into(),
929 }
930 }
931}
932
933#[cfg(test)]
934mod tests {
935 use super::*;
936
937 fn create_test_tables() -> Vec<TableDefinition> {
938 vec![
939 TableDefinition::new("users")
940 .column(ColumnDefinition::new("id", "serial").primary_key())
941 .column(ColumnDefinition::new("name", "varchar(255)").nullable(false))
942 .column(ColumnDefinition::new("email", "varchar(255)").nullable(false))
943 .column(ColumnDefinition::new("created_at", "timestamp").with_default()),
944 TableDefinition::new("posts")
945 .column(ColumnDefinition::new("id", "serial").primary_key())
946 .column(ColumnDefinition::new("title", "varchar(255)").nullable(false))
947 .column(ColumnDefinition::new("content", "text"))
948 .column(ColumnDefinition::new("user_id", "integer").nullable(false))
949 .foreign_key(ForeignKeyDefinition::new("author", "user_id", "users", "id")),
950 ]
951 }
952
953 #[test]
954 fn test_introspector_build_schema() {
955 let introspector = SchemaIntrospector::new();
956 let tables = create_test_tables();
957 let schema = introspector.build_schema(&tables);
958
959 assert_eq!(schema.types.len(), 2);
960 assert!(schema.get_type("Users").is_some());
961 assert!(schema.get_type("Posts").is_some());
962 }
963
964 #[test]
965 fn test_schema_to_sdl() {
966 let introspector = SchemaIntrospector::new();
967 let tables = create_test_tables();
968 let schema = introspector.build_schema(&tables);
969
970 let sdl = schema.to_sdl();
971
972 assert!(sdl.contains("type Users"));
973 assert!(sdl.contains("type Posts"));
974 assert!(sdl.contains("type Query"));
975 assert!(sdl.contains("type Mutation"));
976 }
977
978 #[test]
979 fn test_type_generation() {
980 let introspector = SchemaIntrospector::new();
981 let table = TableDefinition::new("users")
982 .column(ColumnDefinition::new("id", "serial").primary_key())
983 .column(ColumnDefinition::new("name", "varchar").nullable(false));
984
985 let type_def = introspector.generate_type(&table);
986
987 assert_eq!(type_def.name, "Users");
988 assert_eq!(type_def.fields.len(), 2);
989 assert_eq!(type_def.fields[0].name, "id");
990 assert_eq!(type_def.fields[1].name, "name");
991 }
992
993 #[test]
994 fn test_relationship_generation() {
995 let introspector = SchemaIntrospector::new();
996 let tables = create_test_tables();
997 let schema = introspector.build_schema(&tables);
998
999 let post_relationships = schema.get_relationships_for("Posts");
1000 assert_eq!(post_relationships.len(), 1);
1001 assert_eq!(post_relationships[0].to_type, "Users");
1002 assert_eq!(post_relationships[0].relation_type, RelationType::ManyToOne);
1003
1004 let user_relationships = schema.get_relationships_for("Users");
1005 assert_eq!(user_relationships.len(), 1);
1006 assert_eq!(user_relationships[0].to_type, "Posts");
1007 assert_eq!(user_relationships[0].relation_type, RelationType::OneToMany);
1008 }
1009
1010 #[test]
1011 fn test_excluded_columns() {
1012 let mut introspector = SchemaIntrospector::new();
1013 introspector.exclude_column("users", "password_hash");
1014
1015 let table = TableDefinition::new("users")
1016 .column(ColumnDefinition::new("id", "serial").primary_key())
1017 .column(ColumnDefinition::new("password_hash", "varchar"));
1018
1019 let type_def = introspector.generate_type(&table);
1020
1021 assert_eq!(type_def.fields.len(), 1);
1022 assert!(type_def.get_field("passwordHash").is_none());
1023 }
1024
1025 #[test]
1026 fn test_type_name_override() {
1027 let mut introspector = SchemaIntrospector::new();
1028 introspector.set_type_name("users", "User");
1029
1030 let table = TableDefinition::new("users")
1031 .column(ColumnDefinition::new("id", "serial").primary_key());
1032
1033 let type_def = introspector.generate_type(&table);
1034
1035 assert_eq!(type_def.name, "User");
1036 }
1037
1038 #[test]
1039 fn test_field_type_display() {
1040 assert_eq!(FieldType::scalar(GraphQLScalar::String).to_string(), "String");
1041 assert_eq!(FieldType::object("User").to_string(), "User");
1042 assert_eq!(FieldType::list(FieldType::object("User")).to_string(), "[User]");
1043 assert_eq!(
1044 FieldType::non_null(FieldType::list(FieldType::object("User"))).to_string(),
1045 "[User]!"
1046 );
1047 }
1048
1049 #[test]
1050 fn test_graphql_schema_default() {
1051 let schema = GraphQLSchema::default();
1052 assert!(schema.types.is_empty());
1053 assert!(schema.queries.is_empty());
1054 assert!(schema.mutations.is_empty());
1055 }
1056}