1pub mod aggregate;
7pub mod object;
8pub mod relationship;
9
10use crate::input::mutation::{is_deletable, is_insertable, is_updatable};
11use crate::schema::object::TableObjectType;
12use crate::schema::relationship::RelationshipField;
13use postrust_core::schema_cache::{SchemaCache, Table};
14use postrust_core::QualifiedIdentifier;
15use std::collections::{HashMap, HashSet};
16
17#[derive(Debug, Clone)]
19#[non_exhaustive]
20pub struct SchemaConfig {
21 pub exposed_schemas: Vec<String>,
23 pub enable_mutations: bool,
25 pub enable_subscriptions: bool,
27
28 pub subscription_refresh_seconds: u64,
37 pub enum_values: HashMap<String, Vec<(String, Option<String>)>>,
46 pub names: crate::names::NameOverrides,
51 pub max_rows: Option<i64>,
56
57 pub enable_federation: bool,
59 pub type_prefix: Option<String>,
61
62 pub role: Option<String>,
69}
70
71impl Default for SchemaConfig {
72 fn default() -> Self {
73 Self {
74 exposed_schemas: vec!["public".to_string()],
75 enum_values: HashMap::new(),
76 names: crate::names::NameOverrides::default(),
77 enable_mutations: true,
78 enable_subscriptions: false,
79 subscription_refresh_seconds: 30,
80 max_rows: None,
81 enable_federation: false,
82 type_prefix: None,
83 role: None,
84 }
85 }
86}
87
88impl SchemaConfig {
89 pub fn new() -> Self {
91 Self::default()
92 }
93
94 pub fn with_schemas(mut self, schemas: Vec<String>) -> Self {
96 self.exposed_schemas = schemas;
97 self
98 }
99
100 pub fn with_mutations(mut self, enable: bool) -> Self {
102 self.enable_mutations = enable;
103 self
104 }
105
106 pub fn with_subscriptions(mut self, enable: bool) -> Self {
108 self.enable_subscriptions = enable;
109 self
110 }
111
112 pub fn is_shared_entity(&self, table: &QualifiedIdentifier) -> bool {
114 self.enable_federation && self.names.federation_shared(&table.schema, &table.name)
115 }
116
117 pub fn table_type_name(&self, table: &QualifiedIdentifier, base_name: &str) -> String {
119 match self.type_prefix.as_deref() {
120 Some(prefix) if !prefix.is_empty() && !self.is_shared_entity(table) => {
121 format!("{prefix}_{base_name}")
122 }
123 _ => base_name.to_string(),
124 }
125 }
126
127 pub fn root_field_name(&self, base_name: &str) -> String {
129 match self.type_prefix.as_deref() {
130 Some(prefix) if !prefix.is_empty() => format!("{prefix}_{base_name}"),
131 _ => base_name.to_string(),
132 }
133 }
134
135 pub fn subscription_refresh(&self) -> std::time::Duration {
137 match self.subscription_refresh_seconds {
138 0 => std::time::Duration::from_secs(60 * 60 * 24 * 365),
141 seconds => std::time::Duration::from_secs(seconds),
142 }
143 }
144
145 pub fn is_schema_exposed(&self, schema: &str) -> bool {
147 self.exposed_schemas.iter().any(|s| s == schema)
148 }
149
150 pub fn default_schema(&self) -> &str {
155 self.exposed_schemas
156 .first()
157 .map(|s| s.as_str())
158 .unwrap_or("public")
159 }
160}
161
162fn has_whole_key(table: &Table) -> bool {
175 !table.pk_cols.is_empty()
176 && table
177 .pk_cols
178 .iter()
179 .all(|column| table.get_column(column).is_some())
180}
181
182fn pk_columns_of(table: &Table) -> Vec<(String, String)> {
183 table
184 .pk_cols
185 .iter()
186 .map(|col_name| {
187 let pg_type = table
188 .get_column(col_name)
189 .map(|c| c.nominal_type.clone())
190 .unwrap_or_else(|| "text".to_string());
191 (col_name.clone(), pg_type)
192 })
193 .collect()
194}
195
196pub(crate) fn relationship_keys(rel: &postrust_core::schema_cache::Relationship) -> Vec<String> {
208 use postrust_core::schema_cache::{Cardinality, Relationship};
209
210 if let Relationship::Computed { function, .. } = rel {
211 return vec![function.name.clone()];
212 }
213
214 let mut keys = Vec::new();
215 if let Relationship::ForeignKey {
220 cardinality: Cardinality::M2M(junction),
221 ..
222 } = rel
223 {
224 keys.push(junction.table.name.clone());
225 keys.push(junction.constraint1.clone());
226 keys.push(junction.constraint2.clone());
227 }
228 match rel.constraint_name() {
229 "" => {}
230 constraint => keys.push(constraint.to_string()),
231 }
232 if let Some(column) = rel.single_foreign_column() {
238 keys.push(format!("{}.{}", rel.foreign_table().name, column));
239 }
240 if !rel.is_one_to_many() {
241 if let Some(column) = rel.single_local_column() {
242 keys.push(column.to_string());
243 }
244 }
245 let joined = |mut columns: Vec<String>| {
252 columns.sort();
253 columns.join(",")
254 };
255 let columns = rel.join_columns();
256 if columns.len() > 1 {
257 keys.push(format!(
258 "{}.{}",
259 rel.foreign_table().name,
260 joined(columns.iter().map(|(_, foreign)| foreign.clone()).collect())
261 ));
262 if !rel.is_one_to_many() {
263 keys.push(joined(
264 columns.iter().map(|(local, _)| local.clone()).collect(),
265 ));
266 }
267 }
268 keys
269}
270
271fn type_name_of(rendered: &str) -> (Option<String>, String) {
279 let (schema, name) = match rendered.rsplit_once('.') {
280 Some((schema, name)) => (Some(schema.trim_matches('"').to_string()), name),
281 None => (None, rendered),
282 };
283 (schema, name.trim_matches('"').to_string())
284}
285
286fn base_name_for(table: &Table, config: &SchemaConfig) -> String {
294 if let Some(given) = config.names.base_name(&table.schema, &table.name) {
297 return given.to_string();
298 }
299 if table.schema == config.default_schema() {
300 table.name.clone()
301 } else {
302 format!("{}_{}", table.schema, table.name)
303 }
304}
305
306#[derive(Debug, Clone)]
308#[non_exhaustive]
309pub struct GeneratedSchema {
310 pub object_types: HashMap<String, TableObjectType>,
312 pub query_fields: Vec<QueryField>,
314 pub mutation_fields: Vec<MutationField>,
316 pub relationship_fields: HashMap<String, Vec<RelationshipField>>,
318 pub enum_types: HashMap<String, Vec<(String, Option<String>)>>,
321 pub function_fields: Vec<FunctionField>,
323 pub federation_entities: HashMap<String, FederationEntity>,
325 pub enable_federation: bool,
327}
328
329#[derive(Debug, Clone)]
331#[non_exhaustive]
332pub struct FederationEntity {
333 pub type_name: String,
335 pub table: QualifiedIdentifier,
337 pub key_fields: Vec<String>,
339 pub shared: bool,
341}
342
343impl FederationEntity {
344 pub fn key_fields_sdl(&self) -> String {
346 self.key_fields.join(" ")
347 }
348}
349
350#[derive(Debug, Clone)]
362#[non_exhaustive]
363pub struct FunctionField {
364 pub name: String,
366 pub schema_name: String,
368 pub function_name: String,
370 pub returns: String,
372 pub local_returns: String,
374 pub returns_table: (String, String),
376 pub arguments: Vec<(String, String, bool)>,
379 pub session_argument: Option<String>,
386 pub volatile: bool,
388 pub description: Option<String>,
390}
391
392impl FunctionField {
393 pub fn argument_signature(&self) -> String {
398 self.arguments
399 .iter()
400 .map(|(name, sql_type, _)| format!("{name} {sql_type}"))
401 .collect::<Vec<_>>()
402 .join(", ")
403 }
404}
405
406fn reflected_fields(
415 table: &Table,
416 schema_cache: &SchemaCache,
417 config: &SchemaConfig,
418 base_names: &HashMap<(String, String), String>,
419 type_names: &HashMap<(String, String), String>,
420 local_type_names: &HashMap<(String, String), String>,
421) -> Vec<RelationshipField> {
422 schema_cache
423 .get_relationships(&table.qualified_identifier(), &table.schema)
424 .map(|relationships| {
425 relationships
426 .iter()
427 .filter_map(|rel| {
428 let foreign = rel.foreign_table();
431 let target_key = (foreign.schema.clone(), foreign.name.clone());
432 let target_base = base_names.get(&target_key)?;
433 let target_type = type_names.get(&target_key)?;
434 let target_local_type = local_type_names.get(&target_key)?;
435 let mut field = RelationshipField::from_relationship_typed(
436 rel,
437 target_base,
438 target_type,
439 target_local_type,
440 relationship_keys(rel).iter().find_map(|key| {
441 config.names.relationship(&table.schema, &table.name, key)
442 }),
443 );
444 field.arguments = caller_arguments(rel, schema_cache);
445 Some(field)
446 })
447 .collect()
448 })
449 .unwrap_or_default()
450}
451
452fn declared_fields(
465 declared: &[crate::names::DeclaredRelationship],
466 table: &Table,
467 schema_cache: &SchemaCache,
468 config: &SchemaConfig,
469 base_names: &HashMap<(String, String), String>,
470 type_names: &HashMap<(String, String), String>,
471 local_type_names: &HashMap<(String, String), String>,
472) -> Vec<RelationshipField> {
473 use postrust_core::schema_cache::Relationship;
474
475 let reflected = schema_cache
476 .get_relationships(&table.qualified_identifier(), &table.schema)
477 .map(|found| found.to_vec())
478 .unwrap_or_default();
479
480 let mut fields = Vec::with_capacity(declared.len());
481 for entry in declared {
482 if let Some(key) = entry.using.as_deref() {
484 let Some(rel) = reflected
485 .iter()
486 .find(|rel| relationship_keys(rel).iter().any(|found| found == key))
487 else {
488 tracing::warn!(
489 "GraphQL: {}.{} declares the relationship \"{}\" through \"{}\", \
490 which no foreign key carries; it is left out",
491 table.schema,
492 table.name,
493 entry.name,
494 key
495 );
496 continue;
497 };
498 let foreign = rel.foreign_table();
499 let target_key = (foreign.schema.clone(), foreign.name.clone());
500 let (Some(target_base), Some(target_type), Some(target_local_type)) = (
501 base_names.get(&target_key),
502 type_names.get(&target_key),
503 local_type_names.get(&target_key),
504 ) else {
505 continue;
506 };
507 let mut field = RelationshipField::from_relationship_typed(
508 rel,
509 target_base,
510 target_type,
511 target_local_type,
512 Some(&entry.name),
513 );
514 field.arguments = caller_arguments(rel, schema_cache);
515 fields.push(field);
516 continue;
517 }
518
519 let Some((schema, name)) = entry.target(config.default_schema()) else {
521 tracing::warn!(
522 "GraphQL: {}.{} declares the relationship \"{}\" with neither a key \
523 nor a table to join to; it is left out",
524 table.schema,
525 table.name,
526 entry.name
527 );
528 continue;
529 };
530 let target_key = (schema, name);
531 let (Some(target_base), Some(target_type), Some(target_local_type)) = (
532 base_names.get(&target_key),
533 type_names.get(&target_key),
534 local_type_names.get(&target_key),
535 ) else {
536 continue;
537 };
538 match mapped_relationship_field(
539 entry,
540 table,
541 schema_cache,
542 config.default_schema(),
543 target_base,
544 target_type,
545 target_local_type,
546 ) {
547 Some(field) => fields.push(field),
548 None => tracing::warn!(
549 "GraphQL: {}.{} declares the relationship \"{}\" with no columns to \
550 join on; it is left out",
551 table.schema,
552 table.name,
553 entry.name
554 ),
555 }
556 }
557
558 let already: std::collections::HashSet<String> =
563 fields.iter().map(|field| field.name.clone()).collect();
564 for rel in reflected
565 .iter()
566 .filter(|rel| matches!(rel, Relationship::Computed { .. }))
567 {
568 let foreign = rel.foreign_table();
569 let target_key = (foreign.schema.clone(), foreign.name.clone());
570 let (Some(target_base), Some(target_type), Some(target_local_type)) = (
571 base_names.get(&target_key),
572 type_names.get(&target_key),
573 local_type_names.get(&target_key),
574 ) else {
575 continue;
576 };
577 let mut field = RelationshipField::from_relationship_typed(
578 rel,
579 target_base,
580 target_type,
581 target_local_type,
582 relationship_keys(rel)
583 .iter()
584 .find_map(|key| config.names.relationship(&table.schema, &table.name, key)),
585 );
586 if already.contains(&field.name) {
587 continue;
588 }
589 field.arguments = caller_arguments(rel, schema_cache);
590 fields.push(field);
591 }
592 fields
593}
594
595pub(crate) fn mapped_relationship_field(
602 entry: &crate::names::DeclaredRelationship,
603 table: &Table,
604 schema_cache: &SchemaCache,
605 default_schema: &str,
606 target_base: &str,
607 target_type: &str,
608 target_local_type: &str,
609) -> Option<RelationshipField> {
610 use postrust_core::schema_cache::{Cardinality, Relationship};
611
612 let (schema, name) = entry.target(default_schema)?;
613 if entry.columns.is_empty() {
614 return None;
615 }
616 let foreign_table = postrust_core::api_request::QualifiedIdentifier::new(&schema, &name);
617 let here = table.qualified_identifier();
618 let columns: Vec<(String, String)> = entry
619 .columns
620 .iter()
621 .map(|(mine, theirs)| (mine.clone(), theirs.clone()))
622 .collect();
623 let constraint = format!("{}.{}", table.name, entry.name);
624 let cardinality = match (entry.to_one, entry.after_parent) {
630 (true, false) => Cardinality::M2O {
631 constraint: constraint.clone(),
632 columns,
633 },
634 (true, true) => Cardinality::O2O {
635 constraint: constraint.clone(),
636 columns,
637 is_parent: true,
638 },
639 (false, _) => Cardinality::O2M {
640 constraint: constraint.clone(),
641 columns,
642 },
643 };
644 let rel = Relationship::ForeignKey {
645 is_self: here == foreign_table,
646 table: here,
647 foreign_table_is_view: schema_cache
648 .get_table(&foreign_table)
649 .is_some_and(|t| t.is_view),
650 foreign_table,
651 cardinality,
652 table_is_view: table.is_view,
653 constraint_name: constraint,
654 };
655 Some(RelationshipField::from_relationship_typed(
656 &rel,
657 target_base,
658 target_type,
659 target_local_type,
660 Some(&entry.name),
661 ))
662}
663
664fn caller_arguments(
665 relationship: &postrust_core::schema_cache::Relationship,
666 schema_cache: &SchemaCache,
667) -> Vec<(String, String, bool)> {
668 use postrust_core::schema_cache::Relationship;
669 let Relationship::Computed {
670 function,
671 row_argument,
672 ..
673 } = relationship
674 else {
675 return Vec::new();
676 };
677 let Some(row_argument) = row_argument else {
678 return Vec::new();
679 };
680 schema_cache
681 .routines
682 .get(function)
683 .into_iter()
684 .flatten()
685 .find(|routine| routine.name == function.name)
686 .map(|routine| {
687 routine
688 .params
689 .iter()
690 .filter(|param| ¶m.name != row_argument)
691 .filter(|param| !is_session_argument(param))
692 .filter(|param| !param.name.is_empty())
693 .map(|param| (param.name.clone(), param.param_type.clone(), param.required))
694 .collect()
695 })
696 .unwrap_or_default()
697}
698
699pub(crate) fn computed_caller_arguments(
705 computed: &postrust_core::schema_cache::ComputedColumn,
706 schema_cache: &SchemaCache,
707) -> Vec<(String, String, bool)> {
708 let Some(row_argument) = &computed.row_argument else {
709 return Vec::new();
710 };
711 schema_cache
712 .routines
713 .get(&computed.function)
714 .into_iter()
715 .flatten()
716 .find(|routine| routine.name == computed.function.name)
717 .map(|routine| {
718 routine
719 .params
720 .iter()
721 .filter(|param| ¶m.name != row_argument)
722 .filter(|param| !is_session_argument(param))
723 .filter(|param| !param.name.is_empty())
724 .map(|param| (param.name.clone(), param.param_type.clone(), param.required))
725 .collect()
726 })
727 .unwrap_or_default()
728}
729
730fn is_session_argument(param: &postrust_core::schema_cache::RoutineParam) -> bool {
736 param.name == "hasura_session" && matches!(param.param_type.as_str(), "json" | "jsonb")
737}
738
739impl GeneratedSchema {
740 pub fn get_object_type(&self, name: &str) -> Option<&TableObjectType> {
742 self.object_types.get(name)
743 }
744
745 pub fn get_query_field(&self, table_name: &str) -> Option<&QueryField> {
747 self.query_fields
748 .iter()
749 .find(|f| f.table_name == table_name)
750 }
751
752 pub fn get_mutation_fields(&self, table_name: &str) -> Vec<&MutationField> {
754 self.mutation_fields
755 .iter()
756 .filter(|f| f.table_name == table_name)
757 .collect()
758 }
759
760 pub fn get_relationship_fields(&self, type_name: &str) -> Option<&Vec<RelationshipField>> {
762 self.relationship_fields.get(type_name)
763 }
764
765 pub fn get_federation_entity(&self, type_name: &str) -> Option<&FederationEntity> {
767 self.federation_entities.get(type_name)
768 }
769
770 pub fn table_names(&self) -> Vec<&str> {
772 self.object_types
773 .values()
774 .map(|t| t.table.name.as_str())
775 .collect()
776 }
777
778 pub fn type_names(&self) -> Vec<&str> {
780 self.object_types.keys().map(|s| s.as_str()).collect()
781 }
782}
783
784#[derive(Debug, Clone)]
786#[non_exhaustive]
787pub struct QueryField {
788 pub name: String,
790 pub table_name: String,
792 pub schema_name: String,
794 pub type_name: String,
796 pub local_type_name: String,
798 pub return_type: String,
800 pub is_list: bool,
802 pub is_by_pk: bool,
804 pub pk_columns: Vec<(String, String)>,
810 pub description: Option<String>,
812 pub aggregate_name: Option<String>,
818 pub aggregate_description: Option<String>,
821 pub aggregates: bool,
831 pub rows: bool,
839}
840
841impl QueryField {
842 pub fn list(table: &Table) -> Self {
844 Self::list_named(table, &table.name)
845 }
846
847 pub fn list_named(table: &Table, base_name: &str) -> Self {
849 Self::list_typed(table, base_name, base_name, base_name)
850 }
851
852 pub fn list_typed(
854 table: &Table,
855 root_base_name: &str,
856 type_name: &str,
857 local_type_name: &str,
858 ) -> Self {
859 let name = root_base_name.to_string();
860
861 Self {
862 name,
863 table_name: table.name.clone(),
864 schema_name: table.schema.clone(),
865 type_name: type_name.to_string(),
866 local_type_name: local_type_name.to_string(),
867 return_type: format!("[{}!]!", type_name),
868 is_list: true,
869 is_by_pk: false,
870 pk_columns: Vec::new(),
871 description: Some(format!("fetch data from the table: \"{}\"", table.name)),
872 aggregate_name: None,
873 aggregate_description: None,
874 aggregates: true,
875 rows: true,
876 }
877 }
878
879 pub fn by_pk(table: &Table) -> Option<Self> {
881 Self::by_pk_named(table, &table.name)
882 }
883
884 pub fn by_pk_named(table: &Table, base_name: &str) -> Option<Self> {
886 Self::by_pk_typed(table, base_name, base_name, base_name)
887 }
888
889 pub fn by_pk_typed(
891 table: &Table,
892 root_base_name: &str,
893 type_name: &str,
894 local_type_name: &str,
895 ) -> Option<Self> {
896 if !has_whole_key(table) {
897 return None;
898 }
899
900 let field_name = format!("{}_by_pk", root_base_name);
901
902 let pk_columns = pk_columns_of(table);
905
906 Some(Self {
907 name: field_name,
908 table_name: table.name.clone(),
909 schema_name: table.schema.clone(),
910 type_name: type_name.to_string(),
911 local_type_name: local_type_name.to_string(),
912 return_type: type_name.to_string(),
913 is_list: false,
914 is_by_pk: true,
915 pk_columns,
916 description: Some(format!(
917 "fetch data from the table: \"{}\" using primary key columns",
918 table.name
919 )),
920 aggregate_name: None,
921 aggregate_description: None,
922 aggregates: true,
923 rows: true,
924 })
925 }
926}
927
928#[derive(Debug, Clone)]
930#[non_exhaustive]
931pub struct MutationField {
932 pub name: String,
934 pub table_name: String,
936 pub schema_name: String,
938 pub mutation_type: MutationType,
940 pub pk_columns: Vec<(String, String)>,
945 pub return_type: String,
947 pub type_name: String,
949 pub local_type_name: String,
951 pub description: Option<String>,
953}
954
955#[derive(Debug, Clone, Copy, PartialEq, Eq)]
957pub enum MutationType {
958 Insert,
960 InsertOne,
962 Update,
964 UpdateByPk,
966 UpdateMany,
968 Delete,
970 DeleteByPk,
972}
973
974impl MutationField {
975 pub fn insert_fields(table: &Table) -> Vec<Self> {
977 Self::insert_fields_named(table, &table.name)
978 }
979
980 pub fn insert_fields_named(table: &Table, base_name: &str) -> Vec<Self> {
983 Self::insert_fields_typed(table, base_name, base_name)
984 }
985
986 pub fn insert_fields_typed(table: &Table, local_type_name: &str, type_name: &str) -> Vec<Self> {
988 if !is_insertable(table) {
989 return vec![];
990 }
991
992 let mut fields = vec![];
993
994 let name = format!("insert_{}", local_type_name);
996 fields.push(Self {
997 name,
998 table_name: table.name.clone(),
999 schema_name: table.schema.clone(),
1000 mutation_type: MutationType::Insert,
1001 pk_columns: Vec::new(),
1002 return_type: format!("{}_mutation_response", local_type_name),
1003 type_name: type_name.to_string(),
1004 local_type_name: local_type_name.to_string(),
1005 description: Some(format!("insert data into the table: \"{}\"", table.name)),
1006 });
1007
1008 let name = format!("insert_{}_one", local_type_name);
1010 fields.push(Self {
1011 name,
1012 table_name: table.name.clone(),
1013 schema_name: table.schema.clone(),
1014 mutation_type: MutationType::InsertOne,
1015 pk_columns: Vec::new(),
1016 return_type: type_name.to_string(),
1017 type_name: type_name.to_string(),
1018 local_type_name: local_type_name.to_string(),
1019 description: Some(format!(
1020 "insert a single row into the table: \"{}\"",
1021 table.name
1022 )),
1023 });
1024
1025 fields
1026 }
1027
1028 pub fn update_fields(table: &Table) -> Vec<Self> {
1030 Self::update_fields_named(table, &table.name)
1031 }
1032
1033 pub fn update_fields_named(table: &Table, base_name: &str) -> Vec<Self> {
1036 Self::update_fields_typed(table, base_name, base_name)
1037 }
1038
1039 pub fn update_fields_typed(table: &Table, local_type_name: &str, type_name: &str) -> Vec<Self> {
1041 if !is_updatable(table) {
1042 return vec![];
1043 }
1044
1045 let mut fields = vec![];
1046
1047 let name = format!("update_{}", local_type_name);
1049 fields.push(Self {
1050 name,
1051 table_name: table.name.clone(),
1052 schema_name: table.schema.clone(),
1053 mutation_type: MutationType::Update,
1054 pk_columns: Vec::new(),
1055 return_type: format!("{}_mutation_response", local_type_name),
1056 type_name: type_name.to_string(),
1057 local_type_name: local_type_name.to_string(),
1058 description: Some(format!("update data of the table: \"{}\"", table.name)),
1059 });
1060
1061 let name = format!("update_{}_many", local_type_name);
1063 fields.push(Self {
1064 name,
1065 table_name: table.name.clone(),
1066 schema_name: table.schema.clone(),
1067 mutation_type: MutationType::UpdateMany,
1068 pk_columns: Vec::new(),
1069 return_type: format!("[{}_mutation_response]", local_type_name),
1070 type_name: type_name.to_string(),
1071 local_type_name: local_type_name.to_string(),
1072 description: Some(format!(
1073 "update multiples rows of table: \"{}\"",
1074 table.name
1075 )),
1076 });
1077
1078 if has_whole_key(table) {
1080 let name = format!("update_{}_by_pk", local_type_name);
1081 fields.push(Self {
1082 name,
1083 table_name: table.name.clone(),
1084 schema_name: table.schema.clone(),
1085 mutation_type: MutationType::UpdateByPk,
1086 pk_columns: pk_columns_of(table),
1087 return_type: type_name.to_string(),
1088 type_name: type_name.to_string(),
1089 local_type_name: local_type_name.to_string(),
1090 description: Some(format!(
1091 "update single row of the table: \"{}\"",
1092 table.name
1093 )),
1094 });
1095 }
1096
1097 fields
1098 }
1099
1100 pub fn delete_fields(table: &Table) -> Vec<Self> {
1102 Self::delete_fields_named(table, &table.name)
1103 }
1104
1105 pub fn delete_fields_named(table: &Table, base_name: &str) -> Vec<Self> {
1108 Self::delete_fields_typed(table, base_name, base_name)
1109 }
1110
1111 pub fn delete_fields_typed(table: &Table, local_type_name: &str, type_name: &str) -> Vec<Self> {
1113 if !is_deletable(table) {
1114 return vec![];
1115 }
1116
1117 let mut fields = vec![];
1118
1119 let name = format!("delete_{}", local_type_name);
1121 fields.push(Self {
1122 name,
1123 table_name: table.name.clone(),
1124 schema_name: table.schema.clone(),
1125 mutation_type: MutationType::Delete,
1126 pk_columns: Vec::new(),
1127 return_type: format!("{}_mutation_response", local_type_name),
1128 type_name: type_name.to_string(),
1129 local_type_name: local_type_name.to_string(),
1130 description: Some(format!("delete data from the table: \"{}\"", table.name)),
1131 });
1132
1133 if has_whole_key(table) {
1135 let name = format!("delete_{}_by_pk", local_type_name);
1136 fields.push(Self {
1137 name,
1138 table_name: table.name.clone(),
1139 schema_name: table.schema.clone(),
1140 mutation_type: MutationType::DeleteByPk,
1141 pk_columns: pk_columns_of(table),
1142 return_type: type_name.to_string(),
1143 type_name: type_name.to_string(),
1144 local_type_name: local_type_name.to_string(),
1145 description: Some(format!(
1146 "delete single row from the table: \"{}\"",
1147 table.name
1148 )),
1149 });
1150 }
1151
1152 fields
1153 }
1154}
1155
1156pub fn build_schema(schema_cache: &SchemaCache, config: &SchemaConfig) -> GeneratedSchema {
1158 let mut object_types = HashMap::new();
1159 let mut query_fields = Vec::new();
1160 let mut mutation_fields = Vec::new();
1161 let mut relationship_fields = HashMap::new();
1162 let mut federation_entities = HashMap::new();
1163
1164 let mut tables: Vec<&Table> = schema_cache
1167 .tables
1168 .values()
1169 .filter(|table| config.is_schema_exposed(&table.schema))
1170 .collect();
1171 tables.sort_by(|a, b| (&a.schema, &a.name).cmp(&(&b.schema, &b.name)));
1172
1173 let mut used_base_names: HashMap<String, u32> = HashMap::new();
1178 let mut base_names: HashMap<(String, String), String> = HashMap::new();
1181
1182 for table in &tables {
1183 let preferred = base_name_for(table, config);
1184 let base_name = match used_base_names.get_mut(&preferred) {
1185 None => {
1186 used_base_names.insert(preferred.clone(), 1);
1187 preferred
1188 }
1189 Some(count) => {
1190 *count += 1;
1191 let disambiguated = format!("{}_{}", preferred, count);
1192 tracing::warn!(
1193 "GraphQL name collision: {}.{} would generate the same names as \
1194 an earlier table; exposing it as \"{}\" instead",
1195 table.schema,
1196 table.name,
1197 disambiguated
1198 );
1199 disambiguated
1200 }
1201 };
1202
1203 base_names.insert(
1204 (table.schema.clone(), table.name.clone()),
1205 base_name.clone(),
1206 );
1207 }
1208
1209 let mut used_type_names: HashMap<String, u32> = HashMap::new();
1213 let mut type_names: HashMap<(String, String), String> = HashMap::new();
1214 let mut local_type_names: HashMap<(String, String), String> = HashMap::new();
1215 for table in &tables {
1216 let key = (table.schema.clone(), table.name.clone());
1217 let base_name = base_names
1218 .get(&key)
1219 .expect("every visited table has a resolved base name");
1220 let preferred = config.table_type_name(&table.qualified_identifier(), base_name);
1221 let type_name = match used_type_names.get_mut(&preferred) {
1222 None => {
1223 used_type_names.insert(preferred.clone(), 1);
1224 preferred
1225 }
1226 Some(count) => {
1227 *count += 1;
1228 let disambiguated = format!("{}_{}", preferred, count);
1229 tracing::warn!(
1230 "GraphQL type name collision: {}.{} would generate the same type as \
1231 an earlier table; exposing it as \"{}\" instead",
1232 table.schema,
1233 table.name,
1234 disambiguated
1235 );
1236 disambiguated
1237 }
1238 };
1239 let local_type_name = config.root_field_name(base_name);
1240 local_type_names.insert(key.clone(), local_type_name);
1241 type_names.insert(key, type_name);
1242 }
1243
1244 let mut readable_tables: HashSet<QualifiedIdentifier> = HashSet::new();
1247
1248 for table in &tables {
1249 let base_name = base_names
1250 .get(&(table.schema.clone(), table.name.clone()))
1251 .expect("every visited table has a resolved base name")
1252 .clone();
1253 let type_name = type_names
1254 .get(&(table.schema.clone(), table.name.clone()))
1255 .expect("every visited table has a resolved type name")
1256 .clone();
1257 let local_type_name = local_type_names
1258 .get(&(table.schema.clone(), table.name.clone()))
1259 .expect("every visited table has a resolved local type name")
1260 .clone();
1261
1262 let mut obj_type = TableObjectType::from_table_named(table, &type_name, &config.names);
1264 obj_type.local_name = local_type_name.clone();
1265 let readable = config.role.as_deref().is_none_or(|role| {
1275 config
1276 .names
1277 .permissions(&table.schema, &table.name, role)
1278 .is_none_or(|granted| crate::role::reads_anything(granted, table))
1279 });
1280 if readable {
1281 readable_tables.insert(table.qualified_identifier());
1282 }
1283 for field in &mut obj_type.fields {
1286 let Some(function) = config
1287 .names
1288 .computed_source(&table.schema, &table.name, &field.name)
1289 .or(Some(field.name.as_str()))
1290 else {
1291 continue;
1292 };
1293 let Some(computed) = table.get_computed_column(function) else {
1294 continue;
1295 };
1296 field.arguments = computed_caller_arguments(computed, schema_cache);
1297 }
1298 let rename = |field: &mut QueryField, kind: &str| {
1305 if let Some(given) = config.names.root(&table.schema, &table.name, kind) {
1306 field.name = given.to_string();
1307 }
1308 if let Some(comment) = config.names.root_comment(&table.schema, &table.name, kind) {
1309 field.description = Some(comment.to_string()).filter(|c| !c.is_empty());
1310 }
1311 };
1312 let mut list = QueryField::list_typed(table, &base_name, &type_name, &local_type_name);
1313 list.name = base_name.clone();
1314 rename(&mut list, "select");
1315 list.name = config.root_field_name(&list.name);
1316 let aggregate_name = config
1320 .names
1321 .root(&table.schema, &table.name, "select_aggregate")
1322 .map(str::to_string)
1323 .unwrap_or_else(|| format!("{}_aggregate", base_name));
1324 list.aggregate_name = Some(config.root_field_name(&aggregate_name));
1325 list.aggregate_description = config
1326 .names
1327 .root_comment(&table.schema, &table.name, "select_aggregate")
1328 .map(str::to_string);
1329 if let Some(role) = &config.role {
1333 list.aggregates =
1334 crate::role::allows_aggregations(&config.names, &table.schema, &table.name, role);
1335 }
1336 list.rows = readable;
1342 if readable || list.aggregates {
1343 query_fields.push(list);
1344 }
1345 if readable {
1346 if let Some(mut by_pk) =
1347 QueryField::by_pk_typed(table, &base_name, &type_name, &local_type_name)
1348 {
1349 by_pk.name = format!("{}_by_pk", base_name);
1350 rename(&mut by_pk, "select_by_pk");
1351 by_pk.name = config.root_field_name(&by_pk.name);
1352 query_fields.push(by_pk);
1353 }
1354 }
1355
1356 if config.enable_mutations {
1358 let mut fields: Vec<MutationField> = Vec::new();
1359 fields.extend(MutationField::insert_fields_typed(
1360 table,
1361 &local_type_name,
1362 &type_name,
1363 ));
1364 fields.extend(MutationField::update_fields_typed(
1365 table,
1366 &local_type_name,
1367 &type_name,
1368 ));
1369 fields.extend(MutationField::delete_fields_typed(
1370 table,
1371 &local_type_name,
1372 &type_name,
1373 ));
1374 if !readable {
1376 fields.retain(|field| {
1377 !matches!(
1378 field.mutation_type,
1379 MutationType::InsertOne
1380 | MutationType::UpdateByPk
1381 | MutationType::DeleteByPk
1382 )
1383 });
1384 }
1385 for field in &mut fields {
1386 let kind = match field.mutation_type {
1387 MutationType::Insert => "insert",
1388 MutationType::InsertOne => "insert_one",
1389 MutationType::Update => "update",
1390 MutationType::UpdateByPk => "update_by_pk",
1391 MutationType::UpdateMany => "update_many",
1392 MutationType::Delete => "delete",
1393 MutationType::DeleteByPk => "delete_by_pk",
1394 };
1395 field.name = match field.mutation_type {
1396 MutationType::Insert => format!("insert_{base_name}"),
1397 MutationType::InsertOne => format!("insert_{base_name}_one"),
1398 MutationType::Update => format!("update_{base_name}"),
1399 MutationType::UpdateByPk => format!("update_{base_name}_by_pk"),
1400 MutationType::UpdateMany => format!("update_{base_name}_many"),
1401 MutationType::Delete => format!("delete_{base_name}"),
1402 MutationType::DeleteByPk => format!("delete_{base_name}_by_pk"),
1403 };
1404 if let Some(given) = config.names.root(&table.schema, &table.name, kind) {
1405 field.name = given.to_string();
1406 }
1407 field.name = config.root_field_name(&field.name);
1408 if let Some(comment) = config.names.root_comment(&table.schema, &table.name, kind) {
1409 field.description = Some(comment.to_string()).filter(|c| !c.is_empty());
1410 }
1411 }
1412 mutation_fields.extend(fields);
1413 }
1414
1415 let rels = match config
1420 .names
1421 .declared_relationships(&table.schema, &table.name)
1422 {
1423 Some(declared) => declared_fields(
1424 declared,
1425 table,
1426 schema_cache,
1427 config,
1428 &base_names,
1429 &type_names,
1430 &local_type_names,
1431 ),
1432 None => reflected_fields(
1433 table,
1434 schema_cache,
1435 config,
1436 &base_names,
1437 &type_names,
1438 &local_type_names,
1439 ),
1440 };
1441
1442 if !rels.is_empty() {
1443 relationship_fields.insert(type_name.clone(), rels);
1444 }
1445
1446 if config.enable_federation && readable && has_whole_key(table) {
1447 let key_fields: Vec<String> = obj_type
1448 .fields
1449 .iter()
1450 .filter(|field| field.is_pk)
1451 .map(|field| field.name.clone())
1452 .collect();
1453 if key_fields.len() == table.pk_cols.len() {
1454 federation_entities.insert(
1455 type_name.clone(),
1456 FederationEntity {
1457 type_name: type_name.clone(),
1458 table: table.qualified_identifier(),
1459 key_fields,
1460 shared: config.is_shared_entity(&table.qualified_identifier()),
1461 },
1462 );
1463 }
1464 }
1465
1466 object_types.insert(type_name, obj_type);
1467 }
1468
1469 let mut enum_types: HashMap<String, Vec<(String, Option<String>)>> = HashMap::new();
1474 let mut enum_type_of: HashMap<(String, String), String> = HashMap::new();
1475
1476 for (schema_name, table_name) in config.names.enum_tables() {
1477 let table_key = (schema_name.clone(), table_name.clone());
1484 let base = match type_names.get(&table_key) {
1485 Some(found) => found.clone(),
1486 None => {
1487 let unprefixed = config
1488 .names
1489 .base_name(&schema_name, &table_name)
1490 .map(str::to_string)
1491 .unwrap_or_else(|| match schema_name == config.default_schema() {
1492 true => table_name.clone(),
1493 false => format!("{}_{}", schema_name, table_name),
1494 });
1495 config.table_type_name(
1496 &QualifiedIdentifier::new(&schema_name, &table_name),
1497 &unprefixed,
1498 )
1499 }
1500 };
1501 let key = format!("{}.{}", schema_name, table_name);
1502 let members: Vec<(String, Option<String>)> = config
1503 .enum_values
1504 .get(&key)
1505 .map(|values| {
1506 values
1507 .iter()
1508 .filter(|(value, _)| is_graphql_name(value))
1509 .cloned()
1510 .collect()
1511 })
1512 .unwrap_or_default();
1513
1514 if members.is_empty() {
1515 tracing::warn!(
1518 "{}.{} is marked as an enumeration but has no values that can name an \
1519 enum member; exposing it as an ordinary table",
1520 schema_name,
1521 table_name
1522 );
1523 continue;
1524 }
1525
1526 let type_name = format!("{}_enum", base);
1527 enum_type_of.insert((schema_name.clone(), table_name.clone()), type_name.clone());
1528 enum_types.insert(type_name, members);
1529 }
1530
1531 if !enum_type_of.is_empty() {
1541 for table in &tables {
1542 let Some(type_name) = type_names.get(&(table.schema.clone(), table.name.clone()))
1543 else {
1544 continue;
1545 };
1546 let found = schema_cache
1547 .get_relationships(&table.qualified_identifier(), &table.schema)
1548 .map(|found| found.to_vec())
1549 .unwrap_or_default();
1550 for relationship in &found {
1551 if !relationship.is_to_one() {
1552 continue;
1553 }
1554 let foreign = relationship.foreign_table();
1555 let Some(enum_type) =
1556 enum_type_of.get(&(foreign.schema.clone(), foreign.name.clone()))
1557 else {
1558 continue;
1559 };
1560 let Some(column) = relationship.single_local_column() else {
1561 continue;
1562 };
1563 if let Some(object) = object_types.get_mut(type_name) {
1564 let exposed = config
1567 .names
1568 .column(&object.table.schema, &object.table.name, column)
1569 .unwrap_or(column)
1570 .to_string();
1571 for field in &mut object.fields {
1572 if field.name == exposed {
1573 field.graphql_type =
1574 crate::types::GraphQLType::Custom(enum_type.clone());
1575 }
1576 }
1577 }
1578 }
1579 }
1580 }
1581
1582 if !enum_type_of.is_empty() {
1588 for relationships in relationship_fields.values_mut() {
1589 relationships.retain(|relationship| {
1590 let foreign = relationship.relationship.foreign_table();
1591 !enum_type_of.contains_key(&(foreign.schema.clone(), foreign.name.clone()))
1592 });
1593 }
1594 }
1595
1596 for object in object_types.values_mut() {
1605 object.writable_fields = object.fields.clone();
1609 if let Some(role) = &config.role {
1610 let (schema_name, table_name) =
1611 (object.table.schema.clone(), object.table.name.clone());
1612 let Some(granted) = config.names.permissions(&schema_name, &table_name, role) else {
1613 continue;
1614 };
1615 let table = object.table.clone();
1616 object.fields.retain(|field| {
1617 let column = config
1618 .names
1619 .column_source(&schema_name, &table_name, &field.name)
1620 .unwrap_or(&field.name);
1621 match table.get_column(column) {
1622 None => true,
1625 Some(_) => granted
1626 .select
1627 .as_ref()
1628 .is_some_and(|select| select.columns.allows(column)),
1629 }
1630 });
1631 }
1632 }
1633
1634 let mut function_fields = Vec::new();
1639 for routines in schema_cache.routines.values() {
1640 for routine in routines {
1641 if !config.is_schema_exposed(&routine.schema) || routine.is_procedure {
1642 continue;
1643 }
1644 let postrust_core::schema_cache::RetType::SetOf(returned) = &routine.return_type else {
1645 continue;
1646 };
1647 if routine.params.iter().any(|param| {
1652 let (_, named) = type_name_of(¶m.param_type);
1653 base_names.keys().any(|(_, table)| table == &named)
1654 }) {
1655 continue;
1656 }
1657 let (returned_schema, returned_table) = type_name_of(returned);
1663 let found = base_names
1664 .iter()
1665 .find(|((schema, table), _)| {
1666 table == &returned_table
1667 && schema == returned_schema.as_ref().unwrap_or(&routine.schema)
1668 })
1669 .or_else(|| {
1670 base_names
1671 .iter()
1672 .find(|((_, table), _)| table == &returned_table)
1673 });
1674 let Some(((target_schema, target_table), _)) = found else {
1675 continue;
1676 };
1677 let target = QualifiedIdentifier::new(target_schema, target_table);
1678 if !readable_tables.contains(&target) {
1682 continue;
1683 }
1684 let Some(return_type) = type_names.get(&(target_schema.clone(), target_table.clone()))
1685 else {
1686 continue;
1687 };
1688 let Some(local_return_type) =
1689 local_type_names.get(&(target_schema.clone(), target_table.clone()))
1690 else {
1691 continue;
1692 };
1693 let returns_table = (target_schema.clone(), target_table.clone());
1694
1695 let volatile = match config.names.exposed_as(&routine.schema, &routine.name) {
1700 Some("query") => false,
1701 Some("mutation") => true,
1702 _ => matches!(
1703 routine.volatility,
1704 postrust_core::schema_cache::FuncVolatility::Volatile
1705 ),
1706 };
1707 if volatile {
1715 if let Some(role) = config.role.as_deref() {
1716 if !config
1717 .names
1718 .function_grants(&routine.schema, &routine.name, role)
1719 {
1720 continue;
1721 }
1722 }
1723 }
1724
1725 function_fields.push(FunctionField {
1726 name: config.root_field_name(&routine.name),
1727 schema_name: routine.schema.clone(),
1728 function_name: routine.name.clone(),
1729 returns: return_type.clone(),
1730 local_returns: local_return_type.clone(),
1731 returns_table,
1732 arguments: routine
1733 .params
1734 .iter()
1735 .filter(|param| !param.name.is_empty())
1736 .filter(|param| !is_session_argument(param))
1737 .map(|param| (param.name.clone(), param.param_type.clone(), param.required))
1738 .collect(),
1739 session_argument: routine
1740 .params
1741 .iter()
1742 .find(|param| is_session_argument(param))
1743 .map(|param| param.name.clone()),
1744 volatile,
1745 description: routine.description.clone(),
1746 });
1747 }
1748 }
1749 function_fields.sort_by(|a, b| {
1752 a.name.cmp(&b.name).then_with(|| {
1753 a.arguments
1754 .len()
1755 .cmp(&b.arguments.len())
1756 .then_with(|| a.argument_signature().cmp(&b.argument_signature()))
1757 })
1758 });
1759
1760 let mut seen: HashSet<String> = HashSet::new();
1771 function_fields.retain(|function| {
1772 if seen.insert(function.name.clone()) {
1773 return true;
1774 }
1775 tracing::warn!(
1776 "not exposing {}.{}({}) -- another overload of {} is already a root \
1777 field, and GraphQL has no overloading",
1778 function.schema_name,
1779 function.function_name,
1780 function.argument_signature(),
1781 function.name,
1782 );
1783 false
1784 });
1785
1786 GeneratedSchema {
1787 object_types,
1788 query_fields,
1789 mutation_fields,
1790 relationship_fields,
1791 enum_types,
1792 function_fields,
1793 federation_entities,
1794 enable_federation: config.enable_federation,
1795 }
1796}
1797
1798fn is_graphql_name(value: &str) -> bool {
1805 let mut chars = value.chars();
1806 match chars.next() {
1807 Some(first) if first == '_' || first.is_ascii_alphabetic() => {}
1808 _ => return false,
1809 }
1810 chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
1811}
1812
1813#[cfg(test)]
1814mod tests {
1815 use super::*;
1816 use indexmap::IndexMap;
1817 use postrust_core::schema_cache::Column;
1818 use pretty_assertions::assert_eq;
1819
1820 fn create_test_table(name: &str, insertable: bool, updatable: bool, deletable: bool) -> Table {
1821 let mut columns = IndexMap::new();
1822 columns.insert(
1823 "id".into(),
1824 Column {
1825 name: "id".into(),
1826 description: None,
1827 nullable: false,
1828 data_type: "integer".into(),
1829 nominal_type: "int4".into(),
1830 max_len: None,
1831 default: Some("nextval('id_seq')".into()),
1832 enum_values: vec![],
1833 is_pk: true,
1834 position: 1,
1835 always_generated: false,
1836 domain_type: None,
1837 },
1838 );
1839 columns.insert(
1840 "name".into(),
1841 Column {
1842 name: "name".into(),
1843 description: None,
1844 nullable: false,
1845 data_type: "text".into(),
1846 nominal_type: "text".into(),
1847 max_len: None,
1848 default: None,
1849 enum_values: vec![],
1850 is_pk: false,
1851 position: 2,
1852 always_generated: false,
1853 domain_type: None,
1854 },
1855 );
1856
1857 Table {
1858 schema: "public".into(),
1859 name: name.into(),
1860 description: None,
1861 is_view: false,
1862 insertable,
1863 updatable,
1864 deletable,
1865 pk_cols: vec!["id".into()],
1866 unique_constraints: Vec::new(),
1867 columns,
1868 computed_columns: Default::default(),
1869 is_partitioned: false,
1870 }
1871 }
1872
1873 fn create_test_schema_cache() -> SchemaCache {
1874 use std::collections::{HashMap, HashSet};
1875
1876 let mut tables = HashMap::new();
1877
1878 let users = create_test_table("users", true, true, true);
1879 let posts = create_test_table("posts", true, true, true);
1880 let comments = create_test_table("comments", true, false, false);
1881
1882 tables.insert(users.qualified_identifier(), users);
1883 tables.insert(posts.qualified_identifier(), posts);
1884 tables.insert(comments.qualified_identifier(), comments);
1885
1886 SchemaCache {
1887 tables,
1888 relationships: HashMap::new(),
1889 routines: HashMap::new(),
1890 timezones: HashSet::new(),
1891 media_handlers: HashMap::new(),
1892 pg_version: 150000,
1893 representations: Default::default(),
1894 }
1895 }
1896
1897 #[test]
1898 fn a_many_to_many_relationship_can_be_named_by_its_junction() {
1899 use postrust_core::schema_cache::{Cardinality, Junction, Relationship};
1900
1901 let relationship = Relationship::ForeignKey {
1902 table: QualifiedIdentifier::new("public", "products"),
1903 foreign_table: QualifiedIdentifier::new("public", "product_research"),
1904 is_self: false,
1905 cardinality: Cardinality::M2M(Junction {
1906 table: QualifiedIdentifier::new("public", "product_research_links"),
1907 constraint1: "product_research_links_product_id_fkey".into(),
1908 constraint2: "product_research_links_research_id_fkey".into(),
1909 source_columns: vec![("id".into(), "product_id".into())],
1910 target_columns: vec![("research_id".into(), "id".into())],
1911 }),
1912 table_is_view: false,
1913 foreign_table_is_view: false,
1914 constraint_name: String::new(),
1915 };
1916
1917 assert_eq!(
1918 relationship_keys(&relationship),
1919 vec![
1920 "product_research_links",
1921 "product_research_links_product_id_fkey",
1922 "product_research_links_research_id_fkey"
1923 ]
1924 );
1925
1926 let mut cache = create_test_schema_cache();
1927 let products = create_test_table("products", true, true, true);
1928 let product_research = create_test_table("product_research", true, true, true);
1929 let products_key = products.qualified_identifier();
1930 cache.tables.insert(products_key.clone(), products);
1931 cache
1932 .tables
1933 .insert(product_research.qualified_identifier(), product_research);
1934 cache
1935 .relationships
1936 .insert((products_key, "public".into()), vec![relationship]);
1937
1938 let names = crate::names::NameOverrides::parse(
1939 r#"{"tables": {"public.products": {
1940 "relationships": {"product_research_links": "research"}
1941 }}}"#,
1942 )
1943 .unwrap();
1944 let schema = build_schema(
1945 &cache,
1946 &SchemaConfig {
1947 names,
1948 ..SchemaConfig::default()
1949 },
1950 );
1951 let field = schema
1952 .get_relationship_fields("products")
1953 .and_then(|fields| fields.iter().find(|field| field.name == "research"))
1954 .expect("junction metadata renames the inferred relationship");
1955
1956 assert_eq!(field.target_type, "product_research");
1957 assert!(field.is_list);
1958 }
1959
1960 fn setof_users_routine(
1962 name: &str,
1963 args: &[(&str, &str)],
1964 ) -> postrust_core::schema_cache::Routine {
1965 use postrust_core::schema_cache::{FuncVolatility, RetType, Routine, RoutineParam};
1966 Routine {
1967 schema: "public".into(),
1968 name: name.into(),
1969 description: None,
1970 params: args
1971 .iter()
1972 .map(|(pname, ptype)| RoutineParam {
1973 name: (*pname).into(),
1974 param_type: (*ptype).into(),
1975 type_max_length: (*ptype).into(),
1976 required: true,
1977 variadic: false,
1978 })
1979 .collect(),
1980 return_type: RetType::SetOf("users".into()),
1981 returns_composite: true,
1982 volatility: FuncVolatility::Stable,
1983 has_variadic: false,
1984 isolation_level: None,
1985 settings: Vec::new(),
1986 is_procedure: false,
1987 media_type: None,
1988 media_base_type: None,
1989 output_columns: Vec::new(),
1990 }
1991 }
1992
1993 #[test]
1998 fn overloads_of_one_function_become_a_single_root_field() {
1999 let mut cache = create_test_schema_cache();
2000 let one = setof_users_routine("overloaded", &[("a", "integer")]);
2001 let two = setof_users_routine("overloaded", &[("a", "integer"), ("b", "text")]);
2002 cache
2003 .routines
2004 .insert(one.qualified_identifier(), vec![two, one]);
2005
2006 let generated = build_schema(&cache, &SchemaConfig::default());
2007 let named: Vec<&str> = generated
2008 .function_fields
2009 .iter()
2010 .filter(|f| f.name == "overloaded")
2011 .map(|f| f.name.as_str())
2012 .collect();
2013 assert_eq!(named.len(), 1, "exactly one root field per function name");
2014 }
2015
2016 #[test]
2019 fn the_surviving_overload_is_the_one_taking_fewest_arguments() {
2020 let long = setof_users_routine("overloaded", &[("a", "integer"), ("b", "text")]);
2021 let short = setof_users_routine("overloaded", &[("a", "integer")]);
2022
2023 for order in [vec![long.clone(), short.clone()], vec![short, long]] {
2024 let mut cache = create_test_schema_cache();
2025 cache
2026 .routines
2027 .insert(order[0].qualified_identifier(), order);
2028 let generated = build_schema(&cache, &SchemaConfig::default());
2029 let kept = generated
2030 .function_fields
2031 .iter()
2032 .find(|f| f.name == "overloaded")
2033 .expect("the function is exposed");
2034 assert_eq!(kept.argument_signature(), "a integer");
2035 }
2036 }
2037
2038 #[test]
2043 fn test_schema_config_default() {
2044 let config = SchemaConfig::default();
2045 assert!(config.is_schema_exposed("public"));
2046 assert!(!config.is_schema_exposed("private"));
2047 assert!(config.enable_mutations);
2048 assert!(!config.enable_subscriptions);
2049 }
2050
2051 #[test]
2052 fn test_schema_config_with_schemas() {
2053 let config =
2054 SchemaConfig::new().with_schemas(vec!["api".to_string(), "public".to_string()]);
2055 assert!(config.is_schema_exposed("api"));
2056 assert!(config.is_schema_exposed("public"));
2057 assert!(!config.is_schema_exposed("private"));
2058 }
2059
2060 #[test]
2061 fn test_schema_config_mutations_disabled() {
2062 let config = SchemaConfig::new().with_mutations(false);
2063 assert!(!config.enable_mutations);
2064 }
2065
2066 #[test]
2071 fn test_query_field_list() {
2072 let table = create_test_table("users", true, true, true);
2073 let field = QueryField::list(&table);
2074
2075 assert_eq!(field.name, "users");
2076 assert_eq!(field.return_type, "[users!]!");
2077 assert!(field.is_list);
2078 assert!(!field.is_by_pk);
2079 }
2080
2081 #[test]
2082 fn test_query_field_by_pk() {
2083 let table = create_test_table("users", true, true, true);
2084 let field = QueryField::by_pk(&table).unwrap();
2085
2086 assert_eq!(field.name, "users_by_pk");
2087 assert_eq!(field.return_type, "users");
2088 assert!(!field.is_list);
2089 assert!(field.is_by_pk);
2090 }
2091
2092 #[test]
2093 fn test_query_field_by_pk_no_pk() {
2094 let mut table = create_test_table("users", true, true, true);
2095 table.pk_cols = vec![];
2096 let field = QueryField::by_pk(&table);
2097
2098 assert!(field.is_none());
2099 }
2100
2101 #[test]
2106 fn test_mutation_field_insert() {
2107 let table = create_test_table("users", true, true, true);
2108 let fields = MutationField::insert_fields(&table);
2109
2110 assert_eq!(fields.len(), 2);
2111 assert_eq!(fields[0].name, "insert_users");
2112 assert_eq!(fields[0].mutation_type, MutationType::Insert);
2113 assert_eq!(fields[1].name, "insert_users_one");
2114 assert_eq!(fields[1].mutation_type, MutationType::InsertOne);
2115 }
2116
2117 #[test]
2118 fn test_mutation_field_insert_not_insertable() {
2119 let table = create_test_table("users", false, true, true);
2120 let fields = MutationField::insert_fields(&table);
2121
2122 assert!(fields.is_empty());
2123 }
2124
2125 #[test]
2126 fn test_mutation_field_update() {
2127 let table = create_test_table("users", true, true, true);
2128 let fields = MutationField::update_fields(&table);
2129
2130 assert_eq!(fields.len(), 3);
2131 assert_eq!(fields[0].name, "update_users");
2132 assert_eq!(fields[0].mutation_type, MutationType::Update);
2133 assert_eq!(fields[1].name, "update_users_many");
2134 assert_eq!(fields[1].mutation_type, MutationType::UpdateMany);
2135 assert_eq!(fields[2].name, "update_users_by_pk");
2136 assert_eq!(fields[2].mutation_type, MutationType::UpdateByPk);
2137 }
2138
2139 #[test]
2140 fn test_mutation_field_update_not_updatable() {
2141 let table = create_test_table("users", true, false, true);
2142 let fields = MutationField::update_fields(&table);
2143
2144 assert!(fields.is_empty());
2145 }
2146
2147 #[test]
2148 fn test_mutation_field_delete() {
2149 let table = create_test_table("users", true, true, true);
2150 let fields = MutationField::delete_fields(&table);
2151
2152 assert_eq!(fields.len(), 2);
2153 assert_eq!(fields[0].name, "delete_users");
2154 assert_eq!(fields[0].mutation_type, MutationType::Delete);
2155 assert_eq!(fields[1].name, "delete_users_by_pk");
2156 assert_eq!(fields[1].mutation_type, MutationType::DeleteByPk);
2157 }
2158
2159 #[test]
2160 fn test_mutation_field_delete_not_deletable() {
2161 let table = create_test_table("users", true, true, false);
2162 let fields = MutationField::delete_fields(&table);
2163
2164 assert!(fields.is_empty());
2165 }
2166
2167 #[test]
2176 fn test_build_schema_object_types() {
2177 let cache = create_test_schema_cache();
2178 let config = SchemaConfig::default();
2179 let schema = build_schema(&cache, &config);
2180
2181 assert_eq!(schema.object_types.len(), 3);
2182 assert!(schema.get_object_type("users").is_some());
2183 assert!(schema.get_object_type("posts").is_some());
2184 assert!(schema.get_object_type("comments").is_some());
2185 }
2186
2187 #[test]
2188 fn test_build_schema_query_fields() {
2189 let cache = create_test_schema_cache();
2190 let config = SchemaConfig::default();
2191 let schema = build_schema(&cache, &config);
2192
2193 assert_eq!(schema.query_fields.len(), 6);
2195
2196 let users_field = schema.get_query_field("users").unwrap();
2198 assert_eq!(users_field.name, "users");
2199 assert!(users_field.is_list);
2200 }
2201
2202 #[test]
2203 fn test_build_schema_mutation_fields() {
2204 let cache = create_test_schema_cache();
2205 let config = SchemaConfig::default();
2206 let schema = build_schema(&cache, &config);
2207
2208 assert_eq!(schema.mutation_fields.len(), 16);
2213
2214 let users_mutations = schema.get_mutation_fields("users");
2215 assert_eq!(users_mutations.len(), 7);
2216 }
2217
2218 #[test]
2219 fn test_build_schema_mutations_disabled() {
2220 let cache = create_test_schema_cache();
2221 let config = SchemaConfig::new().with_mutations(false);
2222 let schema = build_schema(&cache, &config);
2223
2224 assert!(schema.mutation_fields.is_empty());
2225 }
2226
2227 #[test]
2228 fn test_build_schema_table_names() {
2229 let cache = create_test_schema_cache();
2230 let config = SchemaConfig::default();
2231 let schema = build_schema(&cache, &config);
2232
2233 let names = schema.table_names();
2234 assert_eq!(names.len(), 3);
2235 assert!(names.contains(&"users"));
2236 assert!(names.contains(&"posts"));
2237 assert!(names.contains(&"comments"));
2238 }
2239
2240 #[test]
2241 fn test_build_schema_type_names() {
2242 let cache = create_test_schema_cache();
2243 let config = SchemaConfig::default();
2244 let schema = build_schema(&cache, &config);
2245
2246 let names = schema.type_names();
2247 assert_eq!(names.len(), 3);
2248 assert!(names.contains(&"users"));
2249 assert!(names.contains(&"posts"));
2250 assert!(names.contains(&"comments"));
2251 }
2252
2253 #[test]
2254 fn type_prefix_namespaces_generated_types_and_roots() {
2255 let mut cache = create_test_schema_cache();
2256 let routine = setof_users_routine("search_users", &[("name", "text")]);
2257 cache
2258 .routines
2259 .insert(routine.qualified_identifier(), vec![routine]);
2260 let config = SchemaConfig {
2261 type_prefix: Some("test".into()),
2262 ..SchemaConfig::default()
2263 };
2264
2265 let schema = build_schema(&cache, &config);
2266
2267 assert!(schema.get_object_type("test_users").is_some());
2268 let users = schema
2269 .query_fields
2270 .iter()
2271 .find(|field| field.table_name == "users" && field.is_list)
2272 .expect("users list field");
2273 assert_eq!(users.name, "test_users");
2274 assert_eq!(users.type_name, "test_users");
2275 assert_eq!(
2276 users.aggregate_name.as_deref(),
2277 Some("test_users_aggregate")
2278 );
2279 assert!(schema
2280 .mutation_fields
2281 .iter()
2282 .any(|field| field.name == "test_insert_users"));
2283 let function = schema
2284 .function_fields
2285 .iter()
2286 .find(|field| field.function_name == "search_users")
2287 .expect("set-returning function field");
2288 assert_eq!(function.name, "test_search_users");
2289 assert_eq!(function.returns, "test_users");
2290 }
2291
2292 #[test]
2293 fn shared_entities_keep_their_type_name_but_not_their_root_names() {
2294 use postrust_core::schema_cache::{Cardinality, Relationship};
2295
2296 let mut cache = create_test_schema_cache();
2297 let posts = QualifiedIdentifier::new("public", "posts");
2298 cache.relationships.insert(
2299 (posts.clone(), "public".into()),
2300 vec![Relationship::ForeignKey {
2301 table: posts,
2302 foreign_table: QualifiedIdentifier::new("public", "users"),
2303 is_self: false,
2304 cardinality: Cardinality::M2O {
2305 constraint: "posts_user_id_fkey".into(),
2306 columns: vec![("id".into(), "id".into())],
2307 },
2308 table_is_view: false,
2309 foreign_table_is_view: false,
2310 constraint_name: "posts_user_id_fkey".into(),
2311 }],
2312 );
2313 let config = SchemaConfig {
2314 enable_federation: true,
2315 type_prefix: Some("test".into()),
2316 names: crate::names::NameOverrides::parse(
2317 r#"{"tables": {"public.users": {"federation": {"shared": true}}}}"#,
2318 )
2319 .expect("metadata"),
2320 ..SchemaConfig::default()
2321 };
2322
2323 let schema = build_schema(&cache, &config);
2324
2325 let users_object = schema.get_object_type("users").expect("users object");
2326 assert_eq!(users_object.local_name, "test_users");
2327 let posts_object = schema.get_object_type("test_posts").expect("posts object");
2328 assert_eq!(posts_object.local_name, "test_posts");
2329 let users = schema
2330 .query_fields
2331 .iter()
2332 .find(|field| field.table_name == "users" && field.is_list)
2333 .expect("users list field");
2334 assert_eq!(users.name, "test_users");
2335 assert_eq!(users.type_name, "users");
2336 assert_eq!(users.local_type_name, "test_users");
2337 let posts_relationships = schema
2338 .get_relationship_fields("test_posts")
2339 .expect("posts relationships");
2340 assert_eq!(posts_relationships[0].name, "user");
2341 assert_eq!(posts_relationships[0].target_type, "users");
2342 assert_eq!(posts_relationships[0].target_local_name, "test_users");
2343 }
2344
2345 #[test]
2346 fn federation_entities_are_generated_only_when_enabled() {
2347 let cache = create_test_schema_cache();
2348
2349 let disabled = build_schema(&cache, &SchemaConfig::default());
2350 assert!(!disabled.enable_federation);
2351 assert!(disabled.federation_entities.is_empty());
2352
2353 let enabled = build_schema(
2354 &cache,
2355 &SchemaConfig {
2356 enable_federation: true,
2357 type_prefix: Some("test".into()),
2358 names: crate::names::NameOverrides::parse(
2359 r#"{"tables": {"public.users": {"federation": {"shared": true}}}}"#,
2360 )
2361 .expect("metadata"),
2362 ..SchemaConfig::default()
2363 },
2364 );
2365
2366 assert!(enabled.enable_federation);
2367 let users = enabled
2368 .get_federation_entity("users")
2369 .expect("shared users entity");
2370 assert_eq!(users.type_name, "users");
2371 assert_eq!(users.table, QualifiedIdentifier::new("public", "users"));
2372 assert_eq!(users.key_fields_sdl(), "id");
2373 assert!(users.shared);
2374
2375 let posts = enabled
2376 .get_federation_entity("test_posts")
2377 .expect("prefixed posts entity");
2378 assert_eq!(posts.type_name, "test_posts");
2379 assert_eq!(posts.key_fields_sdl(), "id");
2380 assert!(!posts.shared);
2381 }
2382
2383 #[test]
2384 fn test_build_schema_exposed_schemas() {
2385 let mut cache = create_test_schema_cache();
2386
2387 let private_table = Table {
2389 schema: "private".into(),
2390 name: "secrets".into(),
2391 description: None,
2392 is_view: false,
2393 insertable: true,
2394 updatable: true,
2395 deletable: true,
2396 pk_cols: vec!["id".into()],
2397 unique_constraints: Vec::new(),
2398 columns: indexmap::IndexMap::new(),
2399 computed_columns: Default::default(),
2400 is_partitioned: false,
2401 };
2402 cache
2403 .tables
2404 .insert(private_table.qualified_identifier(), private_table);
2405
2406 let config = SchemaConfig::default(); let schema = build_schema(&cache, &config);
2408
2409 assert_eq!(schema.object_types.len(), 3);
2411 assert!(schema.get_object_type("Secrets").is_none());
2412 }
2413
2414 #[test]
2419 fn test_generated_schema_get_object_type() {
2420 let cache = create_test_schema_cache();
2421 let config = SchemaConfig::default();
2422 let schema = build_schema(&cache, &config);
2423
2424 let users = schema.get_object_type("users").unwrap();
2425 assert_eq!(users.table.name, "users");
2426 }
2427
2428 #[test]
2429 fn test_generated_schema_get_query_field() {
2430 let cache = create_test_schema_cache();
2431 let config = SchemaConfig::default();
2432 let schema = build_schema(&cache, &config);
2433
2434 let field = schema.get_query_field("posts").unwrap();
2435 assert_eq!(field.table_name, "posts");
2436 }
2437
2438 #[test]
2439 fn test_generated_schema_get_mutation_fields() {
2440 let cache = create_test_schema_cache();
2441 let config = SchemaConfig::default();
2442 let schema = build_schema(&cache, &config);
2443
2444 let fields = schema.get_mutation_fields("comments");
2445 assert_eq!(fields.len(), 2); }
2448}