Skip to main content

heliosdb_proxy/graphql/
introspector.rs

1//! Schema Introspector
2//!
3//! Introspects database schema and generates GraphQL schema.
4
5use std::collections::HashMap;
6
7use super::{to_camel_case, to_pascal_case, GraphQLScalar, RelationType};
8
9/// GraphQL schema generated from database introspection
10#[derive(Debug, Clone)]
11pub struct GraphQLSchema {
12    /// Object types
13    pub types: Vec<GraphQLType>,
14    /// Query type fields
15    pub queries: Vec<QueryDefinition>,
16    /// Mutation type fields
17    pub mutations: Vec<MutationDefinition>,
18    /// Relationships between types
19    pub relationships: Vec<Relationship>,
20    /// Input types
21    pub input_types: Vec<GraphQLInputType>,
22    /// Enum types
23    pub enum_types: Vec<GraphQLEnumType>,
24}
25
26impl GraphQLSchema {
27    /// Create an empty schema
28    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    /// Add a type to the schema
40    pub fn add_type(&mut self, type_def: GraphQLType) {
41        self.types.push(type_def);
42    }
43
44    /// Add a query to the schema
45    pub fn add_query(&mut self, query: QueryDefinition) {
46        self.queries.push(query);
47    }
48
49    /// Add a mutation to the schema
50    pub fn add_mutation(&mut self, mutation: MutationDefinition) {
51        self.mutations.push(mutation);
52    }
53
54    /// Add a relationship
55    pub fn add_relationship(&mut self, relationship: Relationship) {
56        self.relationships.push(relationship);
57    }
58
59    /// Get a type by name
60    pub fn get_type(&self, name: &str) -> Option<&GraphQLType> {
61        self.types.iter().find(|t| t.name == name)
62    }
63
64    /// Get relationships for a type
65    pub fn get_relationships_for(&self, type_name: &str) -> Vec<&Relationship> {
66        self.relationships
67            .iter()
68            .filter(|r| r.from_type == type_name)
69            .collect()
70    }
71
72    /// Convert to SDL (Schema Definition Language)
73    pub fn to_sdl(&self) -> String {
74        let mut sdl = String::new();
75
76        // Custom scalars
77        sdl.push_str("# Custom Scalars\n");
78        sdl.push_str("scalar DateTime\n");
79        sdl.push_str("scalar Date\n");
80        sdl.push_str("scalar Time\n");
81        sdl.push_str("scalar JSON\n");
82        sdl.push_str("scalar Decimal\n");
83        sdl.push_str("scalar BigInt\n");
84        sdl.push('\n');
85
86        // Enum types
87        for enum_type in &self.enum_types {
88            sdl.push_str(&format!("enum {} {{\n", enum_type.name));
89            for value in &enum_type.values {
90                sdl.push_str(&format!("  {}\n", value));
91            }
92            sdl.push_str("}\n\n");
93        }
94
95        // Object types
96        for type_def in &self.types {
97            if let Some(ref desc) = type_def.description {
98                sdl.push_str(&format!("\"\"\"{}\"\"\"\n", desc));
99            }
100            sdl.push_str(&format!("type {} {{\n", type_def.name));
101
102            for field in &type_def.fields {
103                if let Some(ref desc) = field.description {
104                    sdl.push_str(&format!("  \"\"\"{}\"\"\"\n", desc));
105                }
106
107                let type_str = if field.nullable {
108                    field.graphql_type.to_string()
109                } else {
110                    format!("{}!", field.graphql_type)
111                };
112
113                sdl.push_str(&format!("  {}: {}\n", field.name, type_str));
114            }
115
116            // Add relationship fields
117            for rel in self.get_relationships_for(&type_def.name) {
118                let type_str = if rel.relation_type.is_list() {
119                    format!("[{}!]!", rel.to_type)
120                } else {
121                    format!("{}!", rel.to_type)
122                };
123                sdl.push_str(&format!("  {}: {}\n", rel.field_name, type_str));
124            }
125
126            sdl.push_str("}\n\n");
127        }
128
129        // Input types
130        for input_type in &self.input_types {
131            sdl.push_str(&format!("input {} {{\n", input_type.name));
132            for field in &input_type.fields {
133                let type_str = if field.nullable {
134                    field.graphql_type.to_string()
135                } else {
136                    format!("{}!", field.graphql_type)
137                };
138                sdl.push_str(&format!("  {}: {}\n", field.name, type_str));
139            }
140            sdl.push_str("}\n\n");
141        }
142
143        // Query type
144        sdl.push_str("type Query {\n");
145        for query in &self.queries {
146            let args: Vec<String> = query
147                .arguments
148                .iter()
149                .map(|a| {
150                    let type_str = if a.nullable {
151                        a.graphql_type.to_string()
152                    } else {
153                        format!("{}!", a.graphql_type)
154                    };
155                    format!("{}: {}", a.name, type_str)
156                })
157                .collect();
158
159            let args_str = if args.is_empty() {
160                String::new()
161            } else {
162                format!("({})", args.join(", "))
163            };
164
165            let return_type = if query.returns_list {
166                format!("[{}!]!", query.return_type)
167            } else {
168                query.return_type.clone()
169            };
170
171            sdl.push_str(&format!("  {}{}: {}\n", query.name, args_str, return_type));
172        }
173        sdl.push_str("}\n\n");
174
175        // Mutation type
176        if !self.mutations.is_empty() {
177            sdl.push_str("type Mutation {\n");
178            for mutation in &self.mutations {
179                let args: Vec<String> = mutation
180                    .arguments
181                    .iter()
182                    .map(|a| {
183                        let type_str = if a.nullable {
184                            a.graphql_type.to_string()
185                        } else {
186                            format!("{}!", a.graphql_type)
187                        };
188                        format!("{}: {}", a.name, type_str)
189                    })
190                    .collect();
191
192                let args_str = if args.is_empty() {
193                    String::new()
194                } else {
195                    format!("({})", args.join(", "))
196                };
197
198                sdl.push_str(&format!(
199                    "  {}{}: {}\n",
200                    mutation.name, args_str, mutation.return_type
201                ));
202            }
203            sdl.push_str("}\n");
204        }
205
206        sdl
207    }
208}
209
210impl Default for GraphQLSchema {
211    fn default() -> Self {
212        Self::new()
213    }
214}
215
216/// GraphQL object type
217#[derive(Debug, Clone)]
218pub struct GraphQLType {
219    /// Type name (PascalCase)
220    pub name: String,
221    /// Fields
222    pub fields: Vec<GraphQLField>,
223    /// Description
224    pub description: Option<String>,
225    /// Source table name
226    pub table_name: Option<String>,
227}
228
229impl GraphQLType {
230    /// Create a new type
231    pub fn new(name: impl Into<String>) -> Self {
232        Self {
233            name: name.into(),
234            fields: Vec::new(),
235            description: None,
236            table_name: None,
237        }
238    }
239
240    /// Add a field
241    pub fn add_field(&mut self, field: GraphQLField) {
242        self.fields.push(field);
243    }
244
245    /// Set description
246    pub fn with_description(mut self, description: impl Into<String>) -> Self {
247        self.description = Some(description.into());
248        self
249    }
250
251    /// Set source table name
252    pub fn from_table(mut self, table_name: impl Into<String>) -> Self {
253        self.table_name = Some(table_name.into());
254        self
255    }
256
257    /// Get a field by name
258    pub fn get_field(&self, name: &str) -> Option<&GraphQLField> {
259        self.fields.iter().find(|f| f.name == name)
260    }
261}
262
263/// GraphQL field
264#[derive(Debug, Clone)]
265pub struct GraphQLField {
266    /// Field name (camelCase)
267    pub name: String,
268    /// Field type
269    pub graphql_type: FieldType,
270    /// Is nullable
271    pub nullable: bool,
272    /// Description
273    pub description: Option<String>,
274    /// Source column name
275    pub column_name: Option<String>,
276    /// Is deprecated
277    pub deprecated: bool,
278    /// Deprecation reason
279    pub deprecation_reason: Option<String>,
280}
281
282impl GraphQLField {
283    /// Create a new field
284    pub fn new(name: impl Into<String>, graphql_type: FieldType) -> Self {
285        Self {
286            name: name.into(),
287            graphql_type,
288            nullable: true,
289            description: None,
290            column_name: None,
291            deprecated: false,
292            deprecation_reason: None,
293        }
294    }
295
296    /// Set nullable
297    pub fn nullable(mut self, nullable: bool) -> Self {
298        self.nullable = nullable;
299        self
300    }
301
302    /// Set description
303    pub fn with_description(mut self, description: impl Into<String>) -> Self {
304        self.description = Some(description.into());
305        self
306    }
307
308    /// Set source column name
309    pub fn from_column(mut self, column_name: impl Into<String>) -> Self {
310        self.column_name = Some(column_name.into());
311        self
312    }
313
314    /// Mark as deprecated
315    pub fn deprecated(mut self, reason: impl Into<String>) -> Self {
316        self.deprecated = true;
317        self.deprecation_reason = Some(reason.into());
318        self
319    }
320}
321
322/// Field type representation
323#[derive(Debug, Clone, PartialEq, Eq)]
324pub enum FieldType {
325    /// Scalar type
326    Scalar(GraphQLScalar),
327    /// Object type reference
328    Object(String),
329    /// List of type
330    List(Box<FieldType>),
331    /// Non-null wrapper
332    NonNull(Box<FieldType>),
333}
334
335impl FieldType {
336    /// Create a scalar field type
337    pub fn scalar(scalar: GraphQLScalar) -> Self {
338        FieldType::Scalar(scalar)
339    }
340
341    /// Create an object reference field type
342    pub fn object(name: impl Into<String>) -> Self {
343        FieldType::Object(name.into())
344    }
345
346    /// Create a list field type
347    pub fn list(inner: FieldType) -> Self {
348        FieldType::List(Box::new(inner))
349    }
350
351    /// Create a non-null field type
352    pub fn non_null(inner: FieldType) -> Self {
353        FieldType::NonNull(Box::new(inner))
354    }
355}
356
357impl std::fmt::Display for FieldType {
358    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359        match self {
360            FieldType::Scalar(s) => write!(f, "{}", s.to_sdl()),
361            FieldType::Object(name) => write!(f, "{}", name),
362            FieldType::List(inner) => write!(f, "[{}]", inner),
363            FieldType::NonNull(inner) => write!(f, "{}!", inner),
364        }
365    }
366}
367
368/// GraphQL input type
369#[derive(Debug, Clone)]
370pub struct GraphQLInputType {
371    /// Type name
372    pub name: String,
373    /// Fields
374    pub fields: Vec<GraphQLField>,
375}
376
377/// GraphQL enum type
378#[derive(Debug, Clone)]
379pub struct GraphQLEnumType {
380    /// Enum name
381    pub name: String,
382    /// Values
383    pub values: Vec<String>,
384}
385
386/// Query definition
387#[derive(Debug, Clone)]
388pub struct QueryDefinition {
389    /// Query name
390    pub name: String,
391    /// Arguments
392    pub arguments: Vec<ArgumentDefinition>,
393    /// Return type
394    pub return_type: String,
395    /// Returns list
396    pub returns_list: bool,
397    /// Source table
398    pub table_name: Option<String>,
399}
400
401impl QueryDefinition {
402    /// Create a new query definition
403    pub fn new(name: impl Into<String>, return_type: impl Into<String>) -> Self {
404        Self {
405            name: name.into(),
406            arguments: Vec::new(),
407            return_type: return_type.into(),
408            returns_list: false,
409            table_name: None,
410        }
411    }
412
413    /// Add an argument
414    pub fn arg(mut self, arg: ArgumentDefinition) -> Self {
415        self.arguments.push(arg);
416        self
417    }
418
419    /// Set returns list
420    pub fn returns_list(mut self, list: bool) -> Self {
421        self.returns_list = list;
422        self
423    }
424
425    /// Set source table
426    pub fn from_table(mut self, table: impl Into<String>) -> Self {
427        self.table_name = Some(table.into());
428        self
429    }
430}
431
432/// Mutation definition
433#[derive(Debug, Clone)]
434pub struct MutationDefinition {
435    /// Mutation name
436    pub name: String,
437    /// Arguments
438    pub arguments: Vec<ArgumentDefinition>,
439    /// Return type
440    pub return_type: String,
441    /// Source table
442    pub table_name: Option<String>,
443    /// Mutation kind
444    pub kind: MutationKind,
445}
446
447impl MutationDefinition {
448    /// Create a new mutation definition
449    pub fn new(
450        name: impl Into<String>,
451        return_type: impl Into<String>,
452        kind: MutationKind,
453    ) -> Self {
454        Self {
455            name: name.into(),
456            arguments: Vec::new(),
457            return_type: return_type.into(),
458            table_name: None,
459            kind,
460        }
461    }
462
463    /// Add an argument
464    pub fn arg(mut self, arg: ArgumentDefinition) -> Self {
465        self.arguments.push(arg);
466        self
467    }
468
469    /// Set source table
470    pub fn from_table(mut self, table: impl Into<String>) -> Self {
471        self.table_name = Some(table.into());
472        self
473    }
474}
475
476/// Mutation kind
477#[derive(Debug, Clone, Copy, PartialEq, Eq)]
478pub enum MutationKind {
479    /// Create operation
480    Create,
481    /// Update operation
482    Update,
483    /// Delete operation
484    Delete,
485}
486
487/// Argument definition
488#[derive(Debug, Clone)]
489pub struct ArgumentDefinition {
490    /// Argument name
491    pub name: String,
492    /// Argument type
493    pub graphql_type: FieldType,
494    /// Is nullable
495    pub nullable: bool,
496    /// Default value
497    pub default_value: Option<serde_json::Value>,
498}
499
500impl ArgumentDefinition {
501    /// Create a new argument
502    pub fn new(name: impl Into<String>, graphql_type: FieldType) -> Self {
503        Self {
504            name: name.into(),
505            graphql_type,
506            nullable: true,
507            default_value: None,
508        }
509    }
510
511    /// Set nullable
512    pub fn required(mut self) -> Self {
513        self.nullable = false;
514        self
515    }
516
517    /// Set default value
518    pub fn default(mut self, value: serde_json::Value) -> Self {
519        self.default_value = Some(value);
520        self
521    }
522}
523
524/// Relationship between types
525#[derive(Debug, Clone)]
526pub struct Relationship {
527    /// Relationship name
528    pub name: String,
529    /// Source type
530    pub from_type: String,
531    /// Target type
532    pub to_type: String,
533    /// Source column
534    pub from_column: String,
535    /// Target column
536    pub to_column: String,
537    /// Relationship type
538    pub relation_type: RelationType,
539    /// Field name in GraphQL
540    pub field_name: String,
541}
542
543impl Relationship {
544    /// Create a new relationship
545    pub fn new(
546        name: impl Into<String>,
547        from_type: impl Into<String>,
548        to_type: impl Into<String>,
549        relation_type: RelationType,
550    ) -> Self {
551        let name = name.into();
552        let field_name = to_camel_case(&name);
553
554        Self {
555            name: name.clone(),
556            from_type: from_type.into(),
557            to_type: to_type.into(),
558            from_column: "id".to_string(),
559            to_column: "id".to_string(),
560            relation_type,
561            field_name,
562        }
563    }
564
565    /// Set columns
566    pub fn columns(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
567        self.from_column = from.into();
568        self.to_column = to.into();
569        self
570    }
571
572    /// Set field name
573    pub fn field(mut self, name: impl Into<String>) -> Self {
574        self.field_name = name.into();
575        self
576    }
577}
578
579/// Schema introspector - generates GraphQL schema from database
580#[derive(Debug)]
581pub struct SchemaIntrospector {
582    /// Excluded tables
583    excluded_tables: Vec<String>,
584    /// Excluded columns by table
585    excluded_columns: HashMap<String, Vec<String>>,
586    /// Type name overrides
587    type_names: HashMap<String, String>,
588}
589
590impl SchemaIntrospector {
591    /// Create a new introspector
592    pub fn new() -> Self {
593        Self {
594            excluded_tables: vec!["pg_catalog".to_string(), "information_schema".to_string()],
595            excluded_columns: HashMap::new(),
596            type_names: HashMap::new(),
597        }
598    }
599
600    /// Exclude a table
601    pub fn exclude_table(&mut self, table: impl Into<String>) {
602        self.excluded_tables.push(table.into());
603    }
604
605    /// Exclude a column
606    pub fn exclude_column(&mut self, table: impl Into<String>, column: impl Into<String>) {
607        self.excluded_columns
608            .entry(table.into())
609            .or_default()
610            .push(column.into());
611    }
612
613    /// Override type name
614    pub fn set_type_name(&mut self, table: impl Into<String>, type_name: impl Into<String>) {
615        self.type_names.insert(table.into(), type_name.into());
616    }
617
618    /// Build schema from table definitions
619    pub fn build_schema(&self, tables: &[TableDefinition]) -> GraphQLSchema {
620        let mut schema = GraphQLSchema::new();
621
622        for table in tables {
623            if self.excluded_tables.contains(&table.name) {
624                continue;
625            }
626
627            // Generate type
628            let type_def = self.generate_type(table);
629            let type_name = type_def.name.clone();
630            schema.add_type(type_def);
631
632            // Generate queries
633            schema.add_query(
634                QueryDefinition::new(to_camel_case(&table.name), &type_name)
635                    .arg(
636                        ArgumentDefinition::new("id", FieldType::scalar(GraphQLScalar::ID))
637                            .required(),
638                    )
639                    .from_table(&table.name),
640            );
641
642            schema.add_query(
643                QueryDefinition::new(format!("{}s", to_camel_case(&table.name)), &type_name)
644                    .arg(ArgumentDefinition::new(
645                        "limit",
646                        FieldType::scalar(GraphQLScalar::Int),
647                    ))
648                    .arg(ArgumentDefinition::new(
649                        "offset",
650                        FieldType::scalar(GraphQLScalar::Int),
651                    ))
652                    .arg(ArgumentDefinition::new(
653                        "where",
654                        FieldType::object(format!("{}Filter", type_name)),
655                    ))
656                    .returns_list(true)
657                    .from_table(&table.name),
658            );
659
660            // Generate mutations
661            schema.add_mutation(
662                MutationDefinition::new(
663                    format!("create{}", type_name),
664                    &type_name,
665                    MutationKind::Create,
666                )
667                .arg(
668                    ArgumentDefinition::new(
669                        "input",
670                        FieldType::object(format!("Create{}Input", type_name)),
671                    )
672                    .required(),
673                )
674                .from_table(&table.name),
675            );
676
677            schema.add_mutation(
678                MutationDefinition::new(
679                    format!("update{}", type_name),
680                    &type_name,
681                    MutationKind::Update,
682                )
683                .arg(ArgumentDefinition::new("id", FieldType::scalar(GraphQLScalar::ID)).required())
684                .arg(
685                    ArgumentDefinition::new(
686                        "input",
687                        FieldType::object(format!("Update{}Input", type_name)),
688                    )
689                    .required(),
690                )
691                .from_table(&table.name),
692            );
693
694            schema.add_mutation(
695                MutationDefinition::new(
696                    format!("delete{}", type_name),
697                    "Boolean".to_string(),
698                    MutationKind::Delete,
699                )
700                .arg(ArgumentDefinition::new("id", FieldType::scalar(GraphQLScalar::ID)).required())
701                .from_table(&table.name),
702            );
703
704            // Generate filter input type
705            let filter_type = self.generate_filter_type(table);
706            schema.input_types.push(filter_type);
707
708            // Generate create input type
709            let create_input = self.generate_create_input(table, &type_name);
710            schema.input_types.push(create_input);
711
712            // Generate update input type
713            let update_input = self.generate_update_input(table, &type_name);
714            schema.input_types.push(update_input);
715        }
716
717        // Generate relationships from foreign keys
718        for table in tables {
719            for fk in &table.foreign_keys {
720                let from_type = self.get_type_name(&table.name);
721                let to_type = self.get_type_name(&fk.referenced_table);
722
723                // Many-to-one relationship
724                schema.add_relationship(
725                    Relationship::new(&fk.name, &from_type, &to_type, RelationType::ManyToOne)
726                        .columns(&fk.column, &fk.referenced_column)
727                        .field(to_camel_case(&fk.name)),
728                );
729
730                // Reverse one-to-many relationship
731                let reverse_name = format!("{}s", to_camel_case(&table.name));
732                schema.add_relationship(
733                    Relationship::new(&reverse_name, &to_type, &from_type, RelationType::OneToMany)
734                        .columns(&fk.referenced_column, &fk.column)
735                        .field(&reverse_name),
736                );
737            }
738        }
739
740        schema
741    }
742
743    /// Generate GraphQL type from table
744    fn generate_type(&self, table: &TableDefinition) -> GraphQLType {
745        let type_name = self.get_type_name(&table.name);
746        let mut type_def = GraphQLType::new(&type_name).from_table(&table.name);
747
748        let excluded = self.excluded_columns.get(&table.name);
749
750        for column in &table.columns {
751            if let Some(excluded) = excluded {
752                if excluded.contains(&column.name) {
753                    continue;
754                }
755            }
756
757            let scalar = GraphQLScalar::from_sql_type(&column.data_type);
758            let field = GraphQLField::new(to_camel_case(&column.name), FieldType::scalar(scalar))
759                .nullable(column.nullable)
760                .from_column(&column.name);
761
762            type_def.add_field(field);
763        }
764
765        type_def
766    }
767
768    /// Generate filter input type
769    fn generate_filter_type(&self, table: &TableDefinition) -> GraphQLInputType {
770        let type_name = self.get_type_name(&table.name);
771        let mut input = GraphQLInputType {
772            name: format!("{}Filter", type_name),
773            fields: Vec::new(),
774        };
775
776        for column in &table.columns {
777            let scalar = GraphQLScalar::from_sql_type(&column.data_type);
778            let filter_type_name = format!("{}Filter", scalar.to_sdl());
779
780            input.fields.push(GraphQLField::new(
781                to_camel_case(&column.name),
782                FieldType::object(filter_type_name),
783            ));
784        }
785
786        // Add AND/OR
787        input.fields.push(GraphQLField::new(
788            "AND",
789            FieldType::list(FieldType::object(format!("{}Filter", type_name))),
790        ));
791        input.fields.push(GraphQLField::new(
792            "OR",
793            FieldType::list(FieldType::object(format!("{}Filter", type_name))),
794        ));
795
796        input
797    }
798
799    /// Generate create input type
800    fn generate_create_input(&self, table: &TableDefinition, type_name: &str) -> GraphQLInputType {
801        let mut input = GraphQLInputType {
802            name: format!("Create{}Input", type_name),
803            fields: Vec::new(),
804        };
805
806        for column in &table.columns {
807            // Skip auto-generated columns
808            if column.is_primary_key && column.data_type.to_lowercase().contains("serial") {
809                continue;
810            }
811
812            let scalar = GraphQLScalar::from_sql_type(&column.data_type);
813            input.fields.push(
814                GraphQLField::new(to_camel_case(&column.name), FieldType::scalar(scalar))
815                    .nullable(column.nullable || column.has_default),
816            );
817        }
818
819        input
820    }
821
822    /// Generate update input type
823    fn generate_update_input(&self, table: &TableDefinition, type_name: &str) -> GraphQLInputType {
824        let mut input = GraphQLInputType {
825            name: format!("Update{}Input", type_name),
826            fields: Vec::new(),
827        };
828
829        for column in &table.columns {
830            // Skip primary key
831            if column.is_primary_key {
832                continue;
833            }
834
835            let scalar = GraphQLScalar::from_sql_type(&column.data_type);
836            input.fields.push(GraphQLField::new(
837                to_camel_case(&column.name),
838                FieldType::scalar(scalar),
839            ));
840        }
841
842        input
843    }
844
845    /// Get type name for a table
846    fn get_type_name(&self, table_name: &str) -> String {
847        self.type_names
848            .get(table_name)
849            .cloned()
850            .unwrap_or_else(|| to_pascal_case(table_name))
851    }
852}
853
854impl Default for SchemaIntrospector {
855    fn default() -> Self {
856        Self::new()
857    }
858}
859
860/// Table definition from database
861#[derive(Debug, Clone)]
862pub struct TableDefinition {
863    /// Table name
864    pub name: String,
865    /// Schema name
866    pub schema: String,
867    /// Columns
868    pub columns: Vec<ColumnDefinition>,
869    /// Foreign keys
870    pub foreign_keys: Vec<ForeignKeyDefinition>,
871}
872
873impl TableDefinition {
874    /// Create a new table definition
875    pub fn new(name: impl Into<String>) -> Self {
876        Self {
877            name: name.into(),
878            schema: "public".to_string(),
879            columns: Vec::new(),
880            foreign_keys: Vec::new(),
881        }
882    }
883
884    /// Add a column
885    pub fn column(mut self, column: ColumnDefinition) -> Self {
886        self.columns.push(column);
887        self
888    }
889
890    /// Add a foreign key
891    pub fn foreign_key(mut self, fk: ForeignKeyDefinition) -> Self {
892        self.foreign_keys.push(fk);
893        self
894    }
895}
896
897/// Column definition
898#[derive(Debug, Clone)]
899pub struct ColumnDefinition {
900    /// Column name
901    pub name: String,
902    /// Data type
903    pub data_type: String,
904    /// Is nullable
905    pub nullable: bool,
906    /// Is primary key
907    pub is_primary_key: bool,
908    /// Has default value
909    pub has_default: bool,
910}
911
912impl ColumnDefinition {
913    /// Create a new column definition
914    pub fn new(name: impl Into<String>, data_type: impl Into<String>) -> Self {
915        Self {
916            name: name.into(),
917            data_type: data_type.into(),
918            nullable: true,
919            is_primary_key: false,
920            has_default: false,
921        }
922    }
923
924    /// Set nullable
925    pub fn nullable(mut self, nullable: bool) -> Self {
926        self.nullable = nullable;
927        self
928    }
929
930    /// Mark as primary key
931    pub fn primary_key(mut self) -> Self {
932        self.is_primary_key = true;
933        self.nullable = false;
934        self
935    }
936
937    /// Mark as having default
938    pub fn with_default(mut self) -> Self {
939        self.has_default = true;
940        self
941    }
942}
943
944/// Foreign key definition
945#[derive(Debug, Clone)]
946pub struct ForeignKeyDefinition {
947    /// Constraint name
948    pub name: String,
949    /// Source column
950    pub column: String,
951    /// Referenced table
952    pub referenced_table: String,
953    /// Referenced column
954    pub referenced_column: String,
955}
956
957impl ForeignKeyDefinition {
958    /// Create a new foreign key
959    pub fn new(
960        name: impl Into<String>,
961        column: impl Into<String>,
962        referenced_table: impl Into<String>,
963        referenced_column: impl Into<String>,
964    ) -> Self {
965        Self {
966            name: name.into(),
967            column: column.into(),
968            referenced_table: referenced_table.into(),
969            referenced_column: referenced_column.into(),
970        }
971    }
972}
973
974#[cfg(test)]
975mod tests {
976    use super::*;
977
978    fn create_test_tables() -> Vec<TableDefinition> {
979        vec![
980            TableDefinition::new("users")
981                .column(ColumnDefinition::new("id", "serial").primary_key())
982                .column(ColumnDefinition::new("name", "varchar(255)").nullable(false))
983                .column(ColumnDefinition::new("email", "varchar(255)").nullable(false))
984                .column(ColumnDefinition::new("created_at", "timestamp").with_default()),
985            TableDefinition::new("posts")
986                .column(ColumnDefinition::new("id", "serial").primary_key())
987                .column(ColumnDefinition::new("title", "varchar(255)").nullable(false))
988                .column(ColumnDefinition::new("content", "text"))
989                .column(ColumnDefinition::new("user_id", "integer").nullable(false))
990                .foreign_key(ForeignKeyDefinition::new(
991                    "author", "user_id", "users", "id",
992                )),
993        ]
994    }
995
996    #[test]
997    fn test_introspector_build_schema() {
998        let introspector = SchemaIntrospector::new();
999        let tables = create_test_tables();
1000        let schema = introspector.build_schema(&tables);
1001
1002        assert_eq!(schema.types.len(), 2);
1003        assert!(schema.get_type("Users").is_some());
1004        assert!(schema.get_type("Posts").is_some());
1005    }
1006
1007    #[test]
1008    fn test_schema_to_sdl() {
1009        let introspector = SchemaIntrospector::new();
1010        let tables = create_test_tables();
1011        let schema = introspector.build_schema(&tables);
1012
1013        let sdl = schema.to_sdl();
1014
1015        assert!(sdl.contains("type Users"));
1016        assert!(sdl.contains("type Posts"));
1017        assert!(sdl.contains("type Query"));
1018        assert!(sdl.contains("type Mutation"));
1019    }
1020
1021    #[test]
1022    fn test_type_generation() {
1023        let introspector = SchemaIntrospector::new();
1024        let table = TableDefinition::new("users")
1025            .column(ColumnDefinition::new("id", "serial").primary_key())
1026            .column(ColumnDefinition::new("name", "varchar").nullable(false));
1027
1028        let type_def = introspector.generate_type(&table);
1029
1030        assert_eq!(type_def.name, "Users");
1031        assert_eq!(type_def.fields.len(), 2);
1032        assert_eq!(type_def.fields[0].name, "id");
1033        assert_eq!(type_def.fields[1].name, "name");
1034    }
1035
1036    #[test]
1037    fn test_relationship_generation() {
1038        let introspector = SchemaIntrospector::new();
1039        let tables = create_test_tables();
1040        let schema = introspector.build_schema(&tables);
1041
1042        let post_relationships = schema.get_relationships_for("Posts");
1043        assert_eq!(post_relationships.len(), 1);
1044        assert_eq!(post_relationships[0].to_type, "Users");
1045        assert_eq!(post_relationships[0].relation_type, RelationType::ManyToOne);
1046
1047        let user_relationships = schema.get_relationships_for("Users");
1048        assert_eq!(user_relationships.len(), 1);
1049        assert_eq!(user_relationships[0].to_type, "Posts");
1050        assert_eq!(user_relationships[0].relation_type, RelationType::OneToMany);
1051    }
1052
1053    #[test]
1054    fn test_excluded_columns() {
1055        let mut introspector = SchemaIntrospector::new();
1056        introspector.exclude_column("users", "password_hash");
1057
1058        let table = TableDefinition::new("users")
1059            .column(ColumnDefinition::new("id", "serial").primary_key())
1060            .column(ColumnDefinition::new("password_hash", "varchar"));
1061
1062        let type_def = introspector.generate_type(&table);
1063
1064        assert_eq!(type_def.fields.len(), 1);
1065        assert!(type_def.get_field("passwordHash").is_none());
1066    }
1067
1068    #[test]
1069    fn test_type_name_override() {
1070        let mut introspector = SchemaIntrospector::new();
1071        introspector.set_type_name("users", "User");
1072
1073        let table = TableDefinition::new("users")
1074            .column(ColumnDefinition::new("id", "serial").primary_key());
1075
1076        let type_def = introspector.generate_type(&table);
1077
1078        assert_eq!(type_def.name, "User");
1079    }
1080
1081    #[test]
1082    fn test_field_type_display() {
1083        assert_eq!(
1084            FieldType::scalar(GraphQLScalar::String).to_string(),
1085            "String"
1086        );
1087        assert_eq!(FieldType::object("User").to_string(), "User");
1088        assert_eq!(
1089            FieldType::list(FieldType::object("User")).to_string(),
1090            "[User]"
1091        );
1092        assert_eq!(
1093            FieldType::non_null(FieldType::list(FieldType::object("User"))).to_string(),
1094            "[User]!"
1095        );
1096    }
1097
1098    #[test]
1099    fn test_graphql_schema_default() {
1100        let schema = GraphQLSchema::default();
1101        assert!(schema.types.is_empty());
1102        assert!(schema.queries.is_empty());
1103        assert!(schema.mutations.is_empty());
1104    }
1105}