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::{GraphQLScalar, RelationType, to_pascal_case, to_camel_case};
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.iter()
67            .filter(|r| r.from_type == type_name)
68            .collect()
69    }
70
71    /// Convert to SDL (Schema Definition Language)
72    pub fn to_sdl(&self) -> String {
73        let mut sdl = String::new();
74
75        // Custom scalars
76        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        // Enum types
86        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        // Object types
95        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            // Add relationship fields
116            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        // Input types
129        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        // Query type
143        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        // Mutation type
173        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/// GraphQL object type
209#[derive(Debug, Clone)]
210pub struct GraphQLType {
211    /// Type name (PascalCase)
212    pub name: String,
213    /// Fields
214    pub fields: Vec<GraphQLField>,
215    /// Description
216    pub description: Option<String>,
217    /// Source table name
218    pub table_name: Option<String>,
219}
220
221impl GraphQLType {
222    /// Create a new type
223    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    /// Add a field
233    pub fn add_field(&mut self, field: GraphQLField) {
234        self.fields.push(field);
235    }
236
237    /// Set description
238    pub fn with_description(mut self, description: impl Into<String>) -> Self {
239        self.description = Some(description.into());
240        self
241    }
242
243    /// Set source table name
244    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    /// Get a field by name
250    pub fn get_field(&self, name: &str) -> Option<&GraphQLField> {
251        self.fields.iter().find(|f| f.name == name)
252    }
253}
254
255/// GraphQL field
256#[derive(Debug, Clone)]
257pub struct GraphQLField {
258    /// Field name (camelCase)
259    pub name: String,
260    /// Field type
261    pub graphql_type: FieldType,
262    /// Is nullable
263    pub nullable: bool,
264    /// Description
265    pub description: Option<String>,
266    /// Source column name
267    pub column_name: Option<String>,
268    /// Is deprecated
269    pub deprecated: bool,
270    /// Deprecation reason
271    pub deprecation_reason: Option<String>,
272}
273
274impl GraphQLField {
275    /// Create a new field
276    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    /// Set nullable
289    pub fn nullable(mut self, nullable: bool) -> Self {
290        self.nullable = nullable;
291        self
292    }
293
294    /// Set description
295    pub fn with_description(mut self, description: impl Into<String>) -> Self {
296        self.description = Some(description.into());
297        self
298    }
299
300    /// Set source column name
301    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    /// Mark as deprecated
307    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/// Field type representation
315#[derive(Debug, Clone, PartialEq, Eq)]
316pub enum FieldType {
317    /// Scalar type
318    Scalar(GraphQLScalar),
319    /// Object type reference
320    Object(String),
321    /// List of type
322    List(Box<FieldType>),
323    /// Non-null wrapper
324    NonNull(Box<FieldType>),
325}
326
327impl FieldType {
328    /// Create a scalar field type
329    pub fn scalar(scalar: GraphQLScalar) -> Self {
330        FieldType::Scalar(scalar)
331    }
332
333    /// Create an object reference field type
334    pub fn object(name: impl Into<String>) -> Self {
335        FieldType::Object(name.into())
336    }
337
338    /// Create a list field type
339    pub fn list(inner: FieldType) -> Self {
340        FieldType::List(Box::new(inner))
341    }
342
343    /// Create a non-null field type
344    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/// GraphQL input type
361#[derive(Debug, Clone)]
362pub struct GraphQLInputType {
363    /// Type name
364    pub name: String,
365    /// Fields
366    pub fields: Vec<GraphQLField>,
367}
368
369/// GraphQL enum type
370#[derive(Debug, Clone)]
371pub struct GraphQLEnumType {
372    /// Enum name
373    pub name: String,
374    /// Values
375    pub values: Vec<String>,
376}
377
378/// Query definition
379#[derive(Debug, Clone)]
380pub struct QueryDefinition {
381    /// Query name
382    pub name: String,
383    /// Arguments
384    pub arguments: Vec<ArgumentDefinition>,
385    /// Return type
386    pub return_type: String,
387    /// Returns list
388    pub returns_list: bool,
389    /// Source table
390    pub table_name: Option<String>,
391}
392
393impl QueryDefinition {
394    /// Create a new query definition
395    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    /// Add an argument
406    pub fn arg(mut self, arg: ArgumentDefinition) -> Self {
407        self.arguments.push(arg);
408        self
409    }
410
411    /// Set returns list
412    pub fn returns_list(mut self, list: bool) -> Self {
413        self.returns_list = list;
414        self
415    }
416
417    /// Set source table
418    pub fn from_table(mut self, table: impl Into<String>) -> Self {
419        self.table_name = Some(table.into());
420        self
421    }
422}
423
424/// Mutation definition
425#[derive(Debug, Clone)]
426pub struct MutationDefinition {
427    /// Mutation name
428    pub name: String,
429    /// Arguments
430    pub arguments: Vec<ArgumentDefinition>,
431    /// Return type
432    pub return_type: String,
433    /// Source table
434    pub table_name: Option<String>,
435    /// Mutation kind
436    pub kind: MutationKind,
437}
438
439impl MutationDefinition {
440    /// Create a new mutation definition
441    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    /// Add an argument
452    pub fn arg(mut self, arg: ArgumentDefinition) -> Self {
453        self.arguments.push(arg);
454        self
455    }
456
457    /// Set source table
458    pub fn from_table(mut self, table: impl Into<String>) -> Self {
459        self.table_name = Some(table.into());
460        self
461    }
462}
463
464/// Mutation kind
465#[derive(Debug, Clone, Copy, PartialEq, Eq)]
466pub enum MutationKind {
467    /// Create operation
468    Create,
469    /// Update operation
470    Update,
471    /// Delete operation
472    Delete,
473}
474
475/// Argument definition
476#[derive(Debug, Clone)]
477pub struct ArgumentDefinition {
478    /// Argument name
479    pub name: String,
480    /// Argument type
481    pub graphql_type: FieldType,
482    /// Is nullable
483    pub nullable: bool,
484    /// Default value
485    pub default_value: Option<serde_json::Value>,
486}
487
488impl ArgumentDefinition {
489    /// Create a new argument
490    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    /// Set nullable
500    pub fn required(mut self) -> Self {
501        self.nullable = false;
502        self
503    }
504
505    /// Set default value
506    pub fn default(mut self, value: serde_json::Value) -> Self {
507        self.default_value = Some(value);
508        self
509    }
510}
511
512/// Relationship between types
513#[derive(Debug, Clone)]
514pub struct Relationship {
515    /// Relationship name
516    pub name: String,
517    /// Source type
518    pub from_type: String,
519    /// Target type
520    pub to_type: String,
521    /// Source column
522    pub from_column: String,
523    /// Target column
524    pub to_column: String,
525    /// Relationship type
526    pub relation_type: RelationType,
527    /// Field name in GraphQL
528    pub field_name: String,
529}
530
531impl Relationship {
532    /// Create a new relationship
533    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    /// Set columns
554    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    /// Set field name
561    pub fn field(mut self, name: impl Into<String>) -> Self {
562        self.field_name = name.into();
563        self
564    }
565}
566
567/// Schema introspector - generates GraphQL schema from database
568#[derive(Debug)]
569pub struct SchemaIntrospector {
570    /// Excluded tables
571    excluded_tables: Vec<String>,
572    /// Excluded columns by table
573    excluded_columns: HashMap<String, Vec<String>>,
574    /// Type name overrides
575    type_names: HashMap<String, String>,
576}
577
578impl SchemaIntrospector {
579    /// Create a new introspector
580    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    /// Exclude a table
592    pub fn exclude_table(&mut self, table: impl Into<String>) {
593        self.excluded_tables.push(table.into());
594    }
595
596    /// Exclude a column
597    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    /// Override type name
605    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    /// Build schema from table definitions
610    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            // Generate type
619            let type_def = self.generate_type(table);
620            let type_name = type_def.name.clone();
621            schema.add_type(type_def);
622
623            // Generate queries
624            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            // Generate mutations
640            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            // Generate filter input type
660            let filter_type = self.generate_filter_type(table);
661            schema.input_types.push(filter_type);
662
663            // Generate create input type
664            let create_input = self.generate_create_input(table, &type_name);
665            schema.input_types.push(create_input);
666
667            // Generate update input type
668            let update_input = self.generate_update_input(table, &type_name);
669            schema.input_types.push(update_input);
670        }
671
672        // Generate relationships from foreign keys
673        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                // Many-to-one relationship
679                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                // Reverse one-to-many relationship
686                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    /// Generate GraphQL type from table
699    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    /// Generate filter input type
728    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        // Add AND/OR
746        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    /// Generate create input type
759    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            // Skip auto-generated columns
767            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    /// Generate update input type
782    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            // Skip primary key
790            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    /// Get type name for a table
805    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/// Table definition from database
820#[derive(Debug, Clone)]
821pub struct TableDefinition {
822    /// Table name
823    pub name: String,
824    /// Schema name
825    pub schema: String,
826    /// Columns
827    pub columns: Vec<ColumnDefinition>,
828    /// Foreign keys
829    pub foreign_keys: Vec<ForeignKeyDefinition>,
830}
831
832impl TableDefinition {
833    /// Create a new table definition
834    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    /// Add a column
844    pub fn column(mut self, column: ColumnDefinition) -> Self {
845        self.columns.push(column);
846        self
847    }
848
849    /// Add a foreign key
850    pub fn foreign_key(mut self, fk: ForeignKeyDefinition) -> Self {
851        self.foreign_keys.push(fk);
852        self
853    }
854}
855
856/// Column definition
857#[derive(Debug, Clone)]
858pub struct ColumnDefinition {
859    /// Column name
860    pub name: String,
861    /// Data type
862    pub data_type: String,
863    /// Is nullable
864    pub nullable: bool,
865    /// Is primary key
866    pub is_primary_key: bool,
867    /// Has default value
868    pub has_default: bool,
869}
870
871impl ColumnDefinition {
872    /// Create a new column definition
873    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    /// Set nullable
884    pub fn nullable(mut self, nullable: bool) -> Self {
885        self.nullable = nullable;
886        self
887    }
888
889    /// Mark as primary key
890    pub fn primary_key(mut self) -> Self {
891        self.is_primary_key = true;
892        self.nullable = false;
893        self
894    }
895
896    /// Mark as having default
897    pub fn with_default(mut self) -> Self {
898        self.has_default = true;
899        self
900    }
901}
902
903/// Foreign key definition
904#[derive(Debug, Clone)]
905pub struct ForeignKeyDefinition {
906    /// Constraint name
907    pub name: String,
908    /// Source column
909    pub column: String,
910    /// Referenced table
911    pub referenced_table: String,
912    /// Referenced column
913    pub referenced_column: String,
914}
915
916impl ForeignKeyDefinition {
917    /// Create a new foreign key
918    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}