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