Skip to main content

postrust_graphql/schema/
mod.rs

1//! GraphQL schema generation from PostgreSQL schema cache.
2//!
3//! Builds a dynamic GraphQL schema from the database schema cache,
4//! creating query and mutation types for each table.
5
6pub mod object;
7pub mod relationship;
8
9use crate::input::mutation::{is_deletable, is_insertable, is_updatable};
10use crate::schema::object::{to_camel_case, to_pascal_case, TableObjectType};
11use crate::schema::relationship::RelationshipField;
12use postrust_core::schema_cache::{SchemaCache, Table};
13use std::collections::HashMap;
14
15/// Configuration for schema generation.
16#[derive(Debug, Clone)]
17pub struct SchemaConfig {
18    /// Schemas to expose in GraphQL (e.g., ["public"])
19    pub exposed_schemas: Vec<String>,
20    /// Whether to generate mutation types
21    pub enable_mutations: bool,
22    /// Whether to generate subscription types
23    pub enable_subscriptions: bool,
24    /// Prefix for query fields (e.g., "all" -> "allUsers")
25    pub query_prefix: Option<String>,
26    /// Suffix for query fields (e.g., "Collection" -> "usersCollection")
27    pub query_suffix: Option<String>,
28    /// Whether to use camelCase for field names
29    pub use_camel_case: bool,
30}
31
32impl Default for SchemaConfig {
33    fn default() -> Self {
34        Self {
35            exposed_schemas: vec!["public".to_string()],
36            enable_mutations: true,
37            enable_subscriptions: false,
38            query_prefix: None,
39            query_suffix: None,
40            use_camel_case: true,
41        }
42    }
43}
44
45impl SchemaConfig {
46    /// Create a new schema config.
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Set the exposed schemas.
52    pub fn with_schemas(mut self, schemas: Vec<String>) -> Self {
53        self.exposed_schemas = schemas;
54        self
55    }
56
57    /// Enable or disable mutations.
58    pub fn with_mutations(mut self, enable: bool) -> Self {
59        self.enable_mutations = enable;
60        self
61    }
62
63    /// Enable or disable subscriptions.
64    pub fn with_subscriptions(mut self, enable: bool) -> Self {
65        self.enable_subscriptions = enable;
66        self
67    }
68
69    /// Check if a schema is exposed.
70    pub fn is_schema_exposed(&self, schema: &str) -> bool {
71        self.exposed_schemas.iter().any(|s| s == schema)
72    }
73}
74
75/// Represents a generated GraphQL schema.
76#[derive(Debug, Clone)]
77pub struct GeneratedSchema {
78    /// Object types for each table
79    pub object_types: HashMap<String, TableObjectType>,
80    /// Query fields
81    pub query_fields: Vec<QueryField>,
82    /// Mutation fields (if enabled)
83    pub mutation_fields: Vec<MutationField>,
84    /// Relationship fields for each type
85    pub relationship_fields: HashMap<String, Vec<RelationshipField>>,
86}
87
88impl GeneratedSchema {
89    /// Get an object type by name.
90    pub fn get_object_type(&self, name: &str) -> Option<&TableObjectType> {
91        self.object_types.get(name)
92    }
93
94    /// Get query fields for a table.
95    pub fn get_query_field(&self, table_name: &str) -> Option<&QueryField> {
96        self.query_fields
97            .iter()
98            .find(|f| f.table_name == table_name)
99    }
100
101    /// Get mutation fields for a table.
102    pub fn get_mutation_fields(&self, table_name: &str) -> Vec<&MutationField> {
103        self.mutation_fields
104            .iter()
105            .filter(|f| f.table_name == table_name)
106            .collect()
107    }
108
109    /// Get relationship fields for a type.
110    pub fn get_relationship_fields(&self, type_name: &str) -> Option<&Vec<RelationshipField>> {
111        self.relationship_fields.get(type_name)
112    }
113
114    /// Get all table names.
115    pub fn table_names(&self) -> Vec<&str> {
116        self.object_types
117            .values()
118            .map(|t| t.table.name.as_str())
119            .collect()
120    }
121
122    /// Get all type names.
123    pub fn type_names(&self) -> Vec<&str> {
124        self.object_types.keys().map(|s| s.as_str()).collect()
125    }
126}
127
128/// A query field for a table (e.g., users, userByPk).
129#[derive(Debug, Clone)]
130pub struct QueryField {
131    /// Field name (e.g., "users")
132    pub name: String,
133    /// Table name
134    pub table_name: String,
135    /// GraphQL object type name (e.g., "Users")
136    pub type_name: String,
137    /// GraphQL return type
138    pub return_type: String,
139    /// Whether this returns a list
140    pub is_list: bool,
141    /// Whether this is a "by PK" query
142    pub is_by_pk: bool,
143    /// Field description
144    pub description: Option<String>,
145}
146
147impl QueryField {
148    /// Create a list query field (e.g., users).
149    pub fn list(table: &Table, config: &SchemaConfig) -> Self {
150        let type_name = to_pascal_case(&table.name);
151        let field_name = if config.use_camel_case {
152            to_camel_case(&table.name)
153        } else {
154            table.name.clone()
155        };
156
157        let name = match (&config.query_prefix, &config.query_suffix) {
158            (Some(prefix), None) => format!("{}{}", prefix, to_pascal_case(&field_name)),
159            (None, Some(suffix)) => format!("{}{}", field_name, suffix),
160            (Some(prefix), Some(suffix)) => {
161                format!("{}{}{}", prefix, to_pascal_case(&field_name), suffix)
162            }
163            (None, None) => field_name,
164        };
165
166        Self {
167            name,
168            table_name: table.name.clone(),
169            type_name: type_name.clone(),
170            return_type: format!("[{}!]!", type_name),
171            is_list: true,
172            is_by_pk: false,
173            description: Some(format!("Query {} records", table.name)),
174        }
175    }
176
177    /// Create a by-PK query field (e.g., userByPk).
178    pub fn by_pk(table: &Table, config: &SchemaConfig) -> Option<Self> {
179        if table.pk_cols.is_empty() {
180            return None;
181        }
182
183        let type_name = to_pascal_case(&table.name);
184        let singular = singularize(&table.name);
185        let field_name = if config.use_camel_case {
186            format!("{}ByPk", to_camel_case(&singular))
187        } else {
188            format!("{}_by_pk", singular)
189        };
190
191        Some(Self {
192            name: field_name,
193            table_name: table.name.clone(),
194            type_name: type_name.clone(),
195            return_type: type_name,
196            is_list: false,
197            is_by_pk: true,
198            description: Some(format!("Get a single {} by primary key", singular)),
199        })
200    }
201}
202
203/// A mutation field for a table.
204#[derive(Debug, Clone)]
205pub struct MutationField {
206    /// Field name (e.g., "insertUsers")
207    pub name: String,
208    /// Table name
209    pub table_name: String,
210    /// Mutation type
211    pub mutation_type: MutationType,
212    /// GraphQL return type
213    pub return_type: String,
214    /// Field description
215    pub description: Option<String>,
216}
217
218/// Types of mutations.
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub enum MutationType {
221    /// Insert multiple records
222    Insert,
223    /// Insert a single record
224    InsertOne,
225    /// Update records matching a filter
226    Update,
227    /// Update a single record by PK
228    UpdateByPk,
229    /// Delete records matching a filter
230    Delete,
231    /// Delete a single record by PK
232    DeleteByPk,
233}
234
235impl MutationField {
236    /// Create insert mutation fields for a table.
237    pub fn insert_fields(table: &Table, config: &SchemaConfig) -> Vec<Self> {
238        if !is_insertable(table) {
239            return vec![];
240        }
241
242        let type_name = to_pascal_case(&table.name);
243        let singular = singularize(&table.name);
244
245        let mut fields = vec![];
246
247        // insert_users (batch insert)
248        let name = if config.use_camel_case {
249            format!("insert{}", to_pascal_case(&table.name))
250        } else {
251            format!("insert_{}", table.name)
252        };
253        fields.push(Self {
254            name,
255            table_name: table.name.clone(),
256            mutation_type: MutationType::Insert,
257            return_type: format!("[{}!]!", type_name),
258            description: Some(format!("Insert multiple {} records", table.name)),
259        });
260
261        // insert_user_one (single insert)
262        let name = if config.use_camel_case {
263            format!("insert{}One", to_pascal_case(&singular))
264        } else {
265            format!("insert_{}_one", singular)
266        };
267        fields.push(Self {
268            name,
269            table_name: table.name.clone(),
270            mutation_type: MutationType::InsertOne,
271            return_type: type_name.clone(),
272            description: Some(format!("Insert a single {} record", singular)),
273        });
274
275        fields
276    }
277
278    /// Create update mutation fields for a table.
279    pub fn update_fields(table: &Table, config: &SchemaConfig) -> Vec<Self> {
280        if !is_updatable(table) {
281            return vec![];
282        }
283
284        let type_name = to_pascal_case(&table.name);
285        let singular = singularize(&table.name);
286
287        let mut fields = vec![];
288
289        // update_users (batch update)
290        let name = if config.use_camel_case {
291            format!("update{}", to_pascal_case(&table.name))
292        } else {
293            format!("update_{}", table.name)
294        };
295        fields.push(Self {
296            name,
297            table_name: table.name.clone(),
298            mutation_type: MutationType::Update,
299            return_type: format!("[{}!]!", type_name),
300            description: Some(format!("Update {} records", table.name)),
301        });
302
303        // update_user_by_pk (single update by PK)
304        if !table.pk_cols.is_empty() {
305            let name = if config.use_camel_case {
306                format!("update{}ByPk", to_pascal_case(&singular))
307            } else {
308                format!("update_{}_by_pk", singular)
309            };
310            fields.push(Self {
311                name,
312                table_name: table.name.clone(),
313                mutation_type: MutationType::UpdateByPk,
314                return_type: type_name,
315                description: Some(format!("Update a single {} by primary key", singular)),
316            });
317        }
318
319        fields
320    }
321
322    /// Create delete mutation fields for a table.
323    pub fn delete_fields(table: &Table, config: &SchemaConfig) -> Vec<Self> {
324        if !is_deletable(table) {
325            return vec![];
326        }
327
328        let type_name = to_pascal_case(&table.name);
329        let singular = singularize(&table.name);
330
331        let mut fields = vec![];
332
333        // delete_users (batch delete)
334        let name = if config.use_camel_case {
335            format!("delete{}", to_pascal_case(&table.name))
336        } else {
337            format!("delete_{}", table.name)
338        };
339        fields.push(Self {
340            name,
341            table_name: table.name.clone(),
342            mutation_type: MutationType::Delete,
343            return_type: format!("[{}!]!", type_name),
344            description: Some(format!("Delete {} records", table.name)),
345        });
346
347        // delete_user_by_pk (single delete by PK)
348        if !table.pk_cols.is_empty() {
349            let name = if config.use_camel_case {
350                format!("delete{}ByPk", to_pascal_case(&singular))
351            } else {
352                format!("delete_{}_by_pk", singular)
353            };
354            fields.push(Self {
355                name,
356                table_name: table.name.clone(),
357                mutation_type: MutationType::DeleteByPk,
358                return_type: type_name,
359                description: Some(format!("Delete a single {} by primary key", singular)),
360            });
361        }
362
363        fields
364    }
365}
366
367/// Build a GraphQL schema from a schema cache.
368pub fn build_schema(schema_cache: &SchemaCache, config: &SchemaConfig) -> GeneratedSchema {
369    let mut object_types = HashMap::new();
370    let mut query_fields = Vec::new();
371    let mut mutation_fields = Vec::new();
372    let mut relationship_fields = HashMap::new();
373
374    // Process each table in the schema cache
375    for table in schema_cache.tables.values() {
376        // Skip tables not in exposed schemas
377        if !config.is_schema_exposed(&table.schema) {
378            continue;
379        }
380
381        // Create object type
382        let obj_type = TableObjectType::from_table(table);
383        let type_name = obj_type.name.clone();
384
385        // Add query fields
386        query_fields.push(QueryField::list(table, config));
387        if let Some(by_pk) = QueryField::by_pk(table, config) {
388            query_fields.push(by_pk);
389        }
390
391        // Add mutation fields if enabled
392        if config.enable_mutations {
393            mutation_fields.extend(MutationField::insert_fields(table, config));
394            mutation_fields.extend(MutationField::update_fields(table, config));
395            mutation_fields.extend(MutationField::delete_fields(table, config));
396        }
397
398        // Add relationship fields
399        let rels: Vec<RelationshipField> = schema_cache
400            .get_relationships(&table.qualified_identifier(), &table.schema)
401            .map(|relationships| {
402                relationships
403                    .iter()
404                    .map(RelationshipField::from_relationship)
405                    .collect()
406            })
407            .unwrap_or_default();
408
409        if !rels.is_empty() {
410            relationship_fields.insert(type_name.clone(), rels);
411        }
412
413        object_types.insert(type_name, obj_type);
414    }
415
416    GeneratedSchema {
417        object_types,
418        query_fields,
419        mutation_fields,
420        relationship_fields,
421    }
422}
423
424/// Simple singularization for field names.
425fn singularize(s: &str) -> String {
426    if let Some(stem) = s.strip_suffix("ies") {
427        format!("{}y", stem)
428    } else if s.ends_with("ses") || s.ends_with("xes") || s.ends_with("ches") || s.ends_with("shes")
429    {
430        s.strip_suffix("es").unwrap_or(s).to_string()
431    } else if s.ends_with('s') && !s.ends_with("ss") {
432        s.strip_suffix('s').unwrap_or(s).to_string()
433    } else {
434        s.to_string()
435    }
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441    use indexmap::IndexMap;
442    use postrust_core::schema_cache::Column;
443    use pretty_assertions::assert_eq;
444
445    fn create_test_table(name: &str, insertable: bool, updatable: bool, deletable: bool) -> Table {
446        let mut columns = IndexMap::new();
447        columns.insert(
448            "id".into(),
449            Column {
450                name: "id".into(),
451                description: None,
452                nullable: false,
453                data_type: "integer".into(),
454                nominal_type: "int4".into(),
455                max_len: None,
456                default: Some("nextval('id_seq')".into()),
457                enum_values: vec![],
458                is_pk: true,
459                position: 1,
460            },
461        );
462        columns.insert(
463            "name".into(),
464            Column {
465                name: "name".into(),
466                description: None,
467                nullable: false,
468                data_type: "text".into(),
469                nominal_type: "text".into(),
470                max_len: None,
471                default: None,
472                enum_values: vec![],
473                is_pk: false,
474                position: 2,
475            },
476        );
477
478        Table {
479            schema: "public".into(),
480            name: name.into(),
481            description: None,
482            is_view: false,
483            insertable,
484            updatable,
485            deletable,
486            pk_cols: vec!["id".into()],
487            columns,
488        }
489    }
490
491    fn create_test_schema_cache() -> SchemaCache {
492        use std::collections::{HashMap, HashSet};
493
494        let mut tables = HashMap::new();
495
496        let users = create_test_table("users", true, true, true);
497        let posts = create_test_table("posts", true, true, true);
498        let comments = create_test_table("comments", true, false, false);
499
500        tables.insert(users.qualified_identifier(), users);
501        tables.insert(posts.qualified_identifier(), posts);
502        tables.insert(comments.qualified_identifier(), comments);
503
504        SchemaCache {
505            tables,
506            relationships: HashMap::new(),
507            routines: HashMap::new(),
508            timezones: HashSet::new(),
509            pg_version: 150000,
510        }
511    }
512
513    // ============================================================================
514    // SchemaConfig Tests
515    // ============================================================================
516
517    #[test]
518    fn test_schema_config_default() {
519        let config = SchemaConfig::default();
520        assert!(config.is_schema_exposed("public"));
521        assert!(!config.is_schema_exposed("private"));
522        assert!(config.enable_mutations);
523        assert!(!config.enable_subscriptions);
524    }
525
526    #[test]
527    fn test_schema_config_with_schemas() {
528        let config =
529            SchemaConfig::new().with_schemas(vec!["api".to_string(), "public".to_string()]);
530        assert!(config.is_schema_exposed("api"));
531        assert!(config.is_schema_exposed("public"));
532        assert!(!config.is_schema_exposed("private"));
533    }
534
535    #[test]
536    fn test_schema_config_mutations_disabled() {
537        let config = SchemaConfig::new().with_mutations(false);
538        assert!(!config.enable_mutations);
539    }
540
541    // ============================================================================
542    // QueryField Tests
543    // ============================================================================
544
545    #[test]
546    fn test_query_field_list() {
547        let table = create_test_table("users", true, true, true);
548        let config = SchemaConfig::default();
549        let field = QueryField::list(&table, &config);
550
551        assert_eq!(field.name, "users");
552        assert_eq!(field.return_type, "[Users!]!");
553        assert!(field.is_list);
554        assert!(!field.is_by_pk);
555    }
556
557    #[test]
558    fn test_query_field_list_with_prefix() {
559        let table = create_test_table("users", true, true, true);
560        let config = SchemaConfig {
561            query_prefix: Some("all".to_string()),
562            ..Default::default()
563        };
564        let field = QueryField::list(&table, &config);
565
566        assert_eq!(field.name, "allUsers");
567    }
568
569    #[test]
570    fn test_query_field_list_with_suffix() {
571        let table = create_test_table("users", true, true, true);
572        let config = SchemaConfig {
573            query_suffix: Some("Collection".to_string()),
574            ..Default::default()
575        };
576        let field = QueryField::list(&table, &config);
577
578        assert_eq!(field.name, "usersCollection");
579    }
580
581    #[test]
582    fn test_query_field_by_pk() {
583        let table = create_test_table("users", true, true, true);
584        let config = SchemaConfig::default();
585        let field = QueryField::by_pk(&table, &config).unwrap();
586
587        assert_eq!(field.name, "userByPk");
588        assert_eq!(field.return_type, "Users");
589        assert!(!field.is_list);
590        assert!(field.is_by_pk);
591    }
592
593    #[test]
594    fn test_query_field_by_pk_no_pk() {
595        let mut table = create_test_table("users", true, true, true);
596        table.pk_cols = vec![];
597        let config = SchemaConfig::default();
598        let field = QueryField::by_pk(&table, &config);
599
600        assert!(field.is_none());
601    }
602
603    // ============================================================================
604    // MutationField Tests
605    // ============================================================================
606
607    #[test]
608    fn test_mutation_field_insert() {
609        let table = create_test_table("users", true, true, true);
610        let config = SchemaConfig::default();
611        let fields = MutationField::insert_fields(&table, &config);
612
613        assert_eq!(fields.len(), 2);
614        assert_eq!(fields[0].name, "insertUsers");
615        assert_eq!(fields[0].mutation_type, MutationType::Insert);
616        assert_eq!(fields[1].name, "insertUserOne");
617        assert_eq!(fields[1].mutation_type, MutationType::InsertOne);
618    }
619
620    #[test]
621    fn test_mutation_field_insert_not_insertable() {
622        let table = create_test_table("users", false, true, true);
623        let config = SchemaConfig::default();
624        let fields = MutationField::insert_fields(&table, &config);
625
626        assert!(fields.is_empty());
627    }
628
629    #[test]
630    fn test_mutation_field_update() {
631        let table = create_test_table("users", true, true, true);
632        let config = SchemaConfig::default();
633        let fields = MutationField::update_fields(&table, &config);
634
635        assert_eq!(fields.len(), 2);
636        assert_eq!(fields[0].name, "updateUsers");
637        assert_eq!(fields[0].mutation_type, MutationType::Update);
638        assert_eq!(fields[1].name, "updateUserByPk");
639        assert_eq!(fields[1].mutation_type, MutationType::UpdateByPk);
640    }
641
642    #[test]
643    fn test_mutation_field_update_not_updatable() {
644        let table = create_test_table("users", true, false, true);
645        let config = SchemaConfig::default();
646        let fields = MutationField::update_fields(&table, &config);
647
648        assert!(fields.is_empty());
649    }
650
651    #[test]
652    fn test_mutation_field_delete() {
653        let table = create_test_table("users", true, true, true);
654        let config = SchemaConfig::default();
655        let fields = MutationField::delete_fields(&table, &config);
656
657        assert_eq!(fields.len(), 2);
658        assert_eq!(fields[0].name, "deleteUsers");
659        assert_eq!(fields[0].mutation_type, MutationType::Delete);
660        assert_eq!(fields[1].name, "deleteUserByPk");
661        assert_eq!(fields[1].mutation_type, MutationType::DeleteByPk);
662    }
663
664    #[test]
665    fn test_mutation_field_delete_not_deletable() {
666        let table = create_test_table("users", true, true, false);
667        let config = SchemaConfig::default();
668        let fields = MutationField::delete_fields(&table, &config);
669
670        assert!(fields.is_empty());
671    }
672
673    // ============================================================================
674    // Singularize Tests
675    // ============================================================================
676
677    #[test]
678    fn test_singularize() {
679        assert_eq!(singularize("users"), "user");
680        assert_eq!(singularize("posts"), "post");
681        assert_eq!(singularize("categories"), "category");
682        assert_eq!(singularize("boxes"), "box");
683        assert_eq!(singularize("matches"), "match");
684        assert_eq!(singularize("class"), "class");
685    }
686
687    // ============================================================================
688    // Build Schema Tests
689    // ============================================================================
690
691    #[test]
692    fn test_build_schema_object_types() {
693        let cache = create_test_schema_cache();
694        let config = SchemaConfig::default();
695        let schema = build_schema(&cache, &config);
696
697        assert_eq!(schema.object_types.len(), 3);
698        assert!(schema.get_object_type("Users").is_some());
699        assert!(schema.get_object_type("Posts").is_some());
700        assert!(schema.get_object_type("Comments").is_some());
701    }
702
703    #[test]
704    fn test_build_schema_query_fields() {
705        let cache = create_test_schema_cache();
706        let config = SchemaConfig::default();
707        let schema = build_schema(&cache, &config);
708
709        // 3 tables * 2 (list + byPk) = 6 query fields
710        assert_eq!(schema.query_fields.len(), 6);
711
712        // Check users query fields
713        let users_field = schema.get_query_field("users").unwrap();
714        assert_eq!(users_field.name, "users");
715        assert!(users_field.is_list);
716    }
717
718    #[test]
719    fn test_build_schema_mutation_fields() {
720        let cache = create_test_schema_cache();
721        let config = SchemaConfig::default();
722        let schema = build_schema(&cache, &config);
723
724        // users: 2 insert + 2 update + 2 delete = 6
725        // posts: 2 insert + 2 update + 2 delete = 6
726        // comments: 2 insert + 0 update + 0 delete = 2
727        // Total: 14
728        assert_eq!(schema.mutation_fields.len(), 14);
729
730        let users_mutations = schema.get_mutation_fields("users");
731        assert_eq!(users_mutations.len(), 6);
732    }
733
734    #[test]
735    fn test_build_schema_mutations_disabled() {
736        let cache = create_test_schema_cache();
737        let config = SchemaConfig::new().with_mutations(false);
738        let schema = build_schema(&cache, &config);
739
740        assert!(schema.mutation_fields.is_empty());
741    }
742
743    #[test]
744    fn test_build_schema_table_names() {
745        let cache = create_test_schema_cache();
746        let config = SchemaConfig::default();
747        let schema = build_schema(&cache, &config);
748
749        let names = schema.table_names();
750        assert_eq!(names.len(), 3);
751        assert!(names.contains(&"users"));
752        assert!(names.contains(&"posts"));
753        assert!(names.contains(&"comments"));
754    }
755
756    #[test]
757    fn test_build_schema_type_names() {
758        let cache = create_test_schema_cache();
759        let config = SchemaConfig::default();
760        let schema = build_schema(&cache, &config);
761
762        let names = schema.type_names();
763        assert_eq!(names.len(), 3);
764        assert!(names.contains(&"Users"));
765        assert!(names.contains(&"Posts"));
766        assert!(names.contains(&"Comments"));
767    }
768
769    #[test]
770    fn test_build_schema_exposed_schemas() {
771        let mut cache = create_test_schema_cache();
772
773        // Add a table in a different schema
774        let private_table = Table {
775            schema: "private".into(),
776            name: "secrets".into(),
777            description: None,
778            is_view: false,
779            insertable: true,
780            updatable: true,
781            deletable: true,
782            pk_cols: vec!["id".into()],
783            columns: indexmap::IndexMap::new(),
784        };
785        cache
786            .tables
787            .insert(private_table.qualified_identifier(), private_table);
788
789        let config = SchemaConfig::default(); // Only exposes "public"
790        let schema = build_schema(&cache, &config);
791
792        // Should only have 3 tables from public schema
793        assert_eq!(schema.object_types.len(), 3);
794        assert!(schema.get_object_type("Secrets").is_none());
795    }
796
797    // ============================================================================
798    // GeneratedSchema Tests
799    // ============================================================================
800
801    #[test]
802    fn test_generated_schema_get_object_type() {
803        let cache = create_test_schema_cache();
804        let config = SchemaConfig::default();
805        let schema = build_schema(&cache, &config);
806
807        let users = schema.get_object_type("Users").unwrap();
808        assert_eq!(users.table.name, "users");
809    }
810
811    #[test]
812    fn test_generated_schema_get_query_field() {
813        let cache = create_test_schema_cache();
814        let config = SchemaConfig::default();
815        let schema = build_schema(&cache, &config);
816
817        let field = schema.get_query_field("posts").unwrap();
818        assert_eq!(field.table_name, "posts");
819    }
820
821    #[test]
822    fn test_generated_schema_get_mutation_fields() {
823        let cache = create_test_schema_cache();
824        let config = SchemaConfig::default();
825        let schema = build_schema(&cache, &config);
826
827        let fields = schema.get_mutation_fields("comments");
828        // comments is only insertable
829        assert_eq!(fields.len(), 2); // insertComments + insertCommentOne
830    }
831}