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 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 std::collections::{HashMap, HashSet};
15
16/// Configuration for schema generation.
17#[derive(Debug, Clone)]
18pub struct SchemaConfig {
19    /// Schemas to expose in GraphQL (e.g., ["public"])
20    pub exposed_schemas: Vec<String>,
21    /// Whether to generate mutation types
22    pub enable_mutations: bool,
23    /// Whether to generate subscription types
24    pub enable_subscriptions: bool,
25
26    /// How often a live query re-reads itself when nothing has notified it.
27    ///
28    /// A subscription is woken by the trigger on the table it reads, which
29    /// costs nothing while nothing is written. A trigger cannot see
30    /// everything: a view has none, an embedded row may live in a table that
31    /// carries none, and a predicate written against the clock changes with
32    /// no write at all. This is how often those are noticed anyway. Seconds;
33    /// zero turns the refresh off and leaves only the notifications.
34    pub subscription_refresh_seconds: u64,
35    /// The members of each enum table, keyed by `schema.table`, as
36    /// `(value, comment)`.
37    ///
38    /// These are rows rather than schema, so they cannot come from the schema
39    /// cache: they are read once at startup, for the tables the configuration
40    /// marks. A table marked as an enum whose values are absent is exposed as
41    /// an ordinary table -- an empty GraphQL enum is not a legal type, and
42    /// refusing to start over a table with no rows in it would be worse.
43    pub enum_values: HashMap<String, Vec<(String, Option<String>)>>,
44    /// Names given rather than derived.
45    ///
46    /// Empty by default, which is every name derived from the schema exactly
47    /// as before.
48    pub names: crate::names::NameOverrides,
49    /// Ceiling on rows a single query may return (`PGRST_MAX_ROWS`).
50    ///
51    /// Applied when a query supplies no `limit` of its own, and as an upper
52    /// bound when it supplies a larger one. `None` leaves queries unbounded.
53    pub max_rows: Option<i64>,
54
55    /// Whose schema this is.
56    ///
57    /// `None` is the unrestricted one: what an administrator sees, and what
58    /// every caller sees on a server with no permission document. `Some(role)`
59    /// is built from a cache already reduced to what that role may see, so the
60    /// only thing left for the builders to ask about is the aggregate root.
61    pub role: Option<String>,
62}
63
64impl Default for SchemaConfig {
65    fn default() -> Self {
66        Self {
67            exposed_schemas: vec!["public".to_string()],
68            enum_values: HashMap::new(),
69            names: crate::names::NameOverrides::default(),
70            enable_mutations: true,
71            enable_subscriptions: false,
72            subscription_refresh_seconds: 30,
73            max_rows: None,
74            role: None,
75        }
76    }
77}
78
79impl SchemaConfig {
80    /// Create a new schema config.
81    pub fn new() -> Self {
82        Self::default()
83    }
84
85    /// Set the exposed schemas.
86    pub fn with_schemas(mut self, schemas: Vec<String>) -> Self {
87        self.exposed_schemas = schemas;
88        self
89    }
90
91    /// Enable or disable mutations.
92    pub fn with_mutations(mut self, enable: bool) -> Self {
93        self.enable_mutations = enable;
94        self
95    }
96
97    /// Enable or disable subscriptions.
98    pub fn with_subscriptions(mut self, enable: bool) -> Self {
99        self.enable_subscriptions = enable;
100        self
101    }
102
103    /// How often a live query re-reads itself when nothing has notified it.
104    pub fn subscription_refresh(&self) -> std::time::Duration {
105        match self.subscription_refresh_seconds {
106            // A zero interval is not a tick every instant; it is no tick at
107            // all, which leaves the notifications alone to wake it.
108            0 => std::time::Duration::from_secs(60 * 60 * 24 * 365),
109            seconds => std::time::Duration::from_secs(seconds),
110        }
111    }
112
113    /// Check if a schema is exposed.
114    pub fn is_schema_exposed(&self, schema: &str) -> bool {
115        self.exposed_schemas.iter().any(|s| s == schema)
116    }
117
118    /// The schema whose tables get unqualified GraphQL names.
119    ///
120    /// This is the first exposed schema, mirroring how the REST surface treats
121    /// the first entry of `PGRST_DB_SCHEMAS` as the default.
122    pub fn default_schema(&self) -> &str {
123        self.exposed_schemas
124            .first()
125            .map(|s| s.as_str())
126            .unwrap_or("public")
127    }
128}
129
130/// Primary key columns of a table, as `(column name, PostgreSQL type)`.
131///
132/// `nominal_type` (the underlying `udt_name`) is used because it is always a
133/// castable type name.
134/// Whether the whole primary key is present on this table.
135///
136/// A `_by_pk` field addresses a row by its key, so a key the caller cannot see
137/// is a field it cannot use: `books_by_pk(id: 1, book_name: "...")` for a role
138/// granted `book_name` and not `id` asks for a row by half a key. Hasura does
139/// not publish the field at all, and neither does this -- which is also what
140/// stops `pk_columns_of` falling back to `text` for a column it cannot find
141/// and typing the argument `String`.
142fn has_whole_key(table: &Table) -> bool {
143    !table.pk_cols.is_empty()
144        && table
145            .pk_cols
146            .iter()
147            .all(|column| table.get_column(column).is_some())
148}
149
150fn pk_columns_of(table: &Table) -> Vec<(String, String)> {
151    table
152        .pk_cols
153        .iter()
154        .map(|col_name| {
155            let pg_type = table
156                .get_column(col_name)
157                .map(|c| c.nominal_type.clone())
158                .unwrap_or_else(|| "text".to_string());
159            (col_name.clone(), pg_type)
160        })
161        .collect()
162}
163
164/// Every key a relationship may be named under, most specific first.
165///
166/// More than one, because there is more than one way to identify the same
167/// relationship and a document may be written by hand or converted from
168/// Hasura's metadata, which uses a different one. A constraint names exactly
169/// one relationship even where two of them point at the same table; a computed
170/// relationship has no constraint and is named by its function; and Hasura
171/// names both directions by a column -- the foreign key column on this table
172/// for a relationship to one row, and `table.column` on the far side for a
173/// relationship to many. All of them are accepted, so a converted document
174/// needs no database to turn a column into the constraint that carries it.
175pub(crate) fn relationship_keys(rel: &postrust_core::schema_cache::Relationship) -> Vec<String> {
176    use postrust_core::schema_cache::Relationship;
177
178    if let Relationship::Computed { function, .. } = rel {
179        return vec![function.name.clone()];
180    }
181
182    let mut keys = Vec::new();
183    match rel.constraint_name() {
184        "" => {}
185        constraint => keys.push(constraint.to_string()),
186    }
187    // The column that carries the key, on whichever side carries it. A
188    // one-to-one may have it on either, and two relationships between the same
189    // pair of tables can share the local column while differing in the far one
190    // -- so the more specific spelling is offered first and the bare column
191    // last.
192    if let Some(column) = rel.single_foreign_column() {
193        keys.push(format!("{}.{}", rel.foreign_table().name, column));
194    }
195    if !rel.is_one_to_many() {
196        if let Some(column) = rel.single_local_column() {
197            keys.push(column.to_string());
198        }
199    }
200    // A key over more than one column, spelled as the columns in order. A
201    // composite foreign key has no single column to be named by, and Hasura
202    // names one by its columns rather than by its constraint -- so a
203    // relationship over `(author_id1, author_id2)` is unreachable under any
204    // of the spellings above. Sorted, because the order two sides list the
205    // same columns in is not something either of them promises.
206    let joined = |mut columns: Vec<String>| {
207        columns.sort();
208        columns.join(",")
209    };
210    let columns = rel.join_columns();
211    if columns.len() > 1 {
212        keys.push(format!(
213            "{}.{}",
214            rel.foreign_table().name,
215            joined(columns.iter().map(|(_, foreign)| foreign.clone()).collect())
216        ));
217        if !rel.is_one_to_many() {
218            keys.push(joined(
219                columns.iter().map(|(local, _)| local.clone()).collect(),
220            ));
221        }
222    }
223    keys
224}
225
226/// A type name as the catalogue writes it, reduced to the table it names.
227///
228/// `pg_catalog.format_type` quotes anything that needs quoting and qualifies
229/// anything outside the search path, so a function returning rows of a table
230/// called `user` -- a reserved word -- reports `"user"`, and one returning rows
231/// of `other.thing` reports `other.thing`. Neither is the table's name, and
232/// comparing them against one finds nothing.
233fn type_name_of(rendered: &str) -> (Option<String>, String) {
234    let (schema, name) = match rendered.rsplit_once('.') {
235        Some((schema, name)) => (Some(schema.trim_matches('"').to_string()), name),
236        None => (None, rendered),
237    };
238    (schema, name.trim_matches('"').to_string())
239}
240
241/// Base name used to derive a table's GraphQL type and field names.
242///
243/// Tables in the default schema keep their bare name, so a single-schema
244/// deployment is unaffected. Tables in any other exposed schema are prefixed
245/// with the schema, because GraphQL has one flat namespace: without this, a
246/// `users` table in both `public` and `api` would generate the same type and
247/// field names and one would silently replace the other.
248fn base_name_for(table: &Table, config: &SchemaConfig) -> String {
249    // A name that was given is the answer; the rest of this is what to call a
250    // table nobody named.
251    if let Some(given) = config.names.base_name(&table.schema, &table.name) {
252        return given.to_string();
253    }
254    if table.schema == config.default_schema() {
255        table.name.clone()
256    } else {
257        format!("{}_{}", table.schema, table.name)
258    }
259}
260
261/// Represents a generated GraphQL schema.
262#[derive(Debug, Clone)]
263pub struct GeneratedSchema {
264    /// Object types for each table
265    pub object_types: HashMap<String, TableObjectType>,
266    /// Query fields
267    pub query_fields: Vec<QueryField>,
268    /// Mutation fields (if enabled)
269    pub mutation_fields: Vec<MutationField>,
270    /// Relationship fields for each type
271    pub relationship_fields: HashMap<String, Vec<RelationshipField>>,
272    /// GraphQL enums generated from enum tables: type name to
273    /// `(member, description)`.
274    pub enum_types: HashMap<String, Vec<(String, Option<String>)>>,
275    /// Database functions exposed as root fields.
276    pub function_fields: Vec<FunctionField>,
277}
278
279/// A database function returning rows of a table, exposed as a root field.
280///
281/// A function that returns `SETOF <table>` answers the same question a table
282/// does, from a query somebody wrote in SQL rather than from the table
283/// directly. It filters, orders and pages like the table it returns, and its
284/// own arguments arrive under `args` so they cannot collide with those.
285///
286/// Where it appears follows from what PostgreSQL says it does: a function
287/// declared VOLATILE may write, so it is a mutation; one declared STABLE or
288/// IMMUTABLE may not, so it is a query. Nothing else is a safe place to draw
289/// that line -- the alternative is trusting a name.
290#[derive(Debug, Clone)]
291pub struct FunctionField {
292    /// The field's name, which is the function's.
293    pub name: String,
294    /// Schema the function lives in.
295    pub schema_name: String,
296    /// The function's own name, which the field is named after.
297    pub function_name: String,
298    /// The GraphQL type of the rows it returns.
299    pub returns: String,
300    /// The table those rows belong to, as `(schema, table)`.
301    pub returns_table: (String, String),
302    /// Its arguments, as `(name, PostgreSQL type, required)`. The session
303    /// argument is not among them: the client does not supply it.
304    pub arguments: Vec<(String, String, bool)>,
305    /// The name of its session argument, if it has one.
306    ///
307    /// Hasura's convention: a `hasura_session json` parameter is filled from
308    /// the caller's session variables rather than taken from the request, so a
309    /// function can read who is asking. Exposing it as an argument would let
310    /// the caller name its own identity, which is the opposite of the point.
311    pub session_argument: Option<String>,
312    /// Whether it may write, and so belongs on the mutation root.
313    pub volatile: bool,
314    /// Description from the function's comment.
315    pub description: Option<String>,
316}
317
318/// What a computed relationship's function takes from the caller.
319///
320/// Everything but the parent row, which is what makes it a relationship, and
321/// the session, which is the server's to supply. Read from the routine rather
322/// than from the relationship because the relationship loader knows only which
323/// argument is the row -- the rest are the same parameters any function has,
324/// and they are already loaded.
325/// One relationship field per foreign key the catalogue carries.
326fn reflected_fields(
327    table: &Table,
328    schema_cache: &SchemaCache,
329    config: &SchemaConfig,
330    base_names: &HashMap<(String, String), String>,
331) -> Vec<RelationshipField> {
332    schema_cache
333        .get_relationships(&table.qualified_identifier(), &table.schema)
334        .map(|relationships| {
335            relationships
336                .iter()
337                .filter_map(|rel| {
338                    // A relationship whose target is not exposed would
339                    // reference a GraphQL type that was never registered.
340                    let foreign = rel.foreign_table();
341                    let target_base =
342                        base_names.get(&(foreign.schema.clone(), foreign.name.clone()))?;
343                    let mut field = RelationshipField::from_relationship_named(
344                        rel,
345                        target_base,
346                        relationship_keys(rel).iter().find_map(|key| {
347                            config.names.relationship(&table.schema, &table.name, key)
348                        }),
349                    );
350                    field.arguments = caller_arguments(rel, schema_cache);
351                    Some(field)
352                })
353                .collect()
354        })
355        .unwrap_or_default()
356}
357
358/// One relationship field per relationship metadata declares, and no others.
359///
360/// A declaration reached by a key names a relationship reflection already
361/// found, so the catalogue's own cardinality and columns are used -- a key
362/// says which side has one row and which has many, and guessing that from a
363/// name would be guessing. One reached by a column mapping is not in the
364/// catalogue at all and is built from the mapping, with the direction taken
365/// from the declaration: Hasura writes object and array relationships
366/// separately, which is where `to_one` comes from.
367///
368/// A declaration this server cannot resolve is left out rather than guessed
369/// at. It is a field that would not work; the ones beside it still do.
370fn declared_fields(
371    declared: &[crate::names::DeclaredRelationship],
372    table: &Table,
373    schema_cache: &SchemaCache,
374    config: &SchemaConfig,
375    base_names: &HashMap<(String, String), String>,
376) -> Vec<RelationshipField> {
377    use postrust_core::schema_cache::Relationship;
378
379    let reflected = schema_cache
380        .get_relationships(&table.qualified_identifier(), &table.schema)
381        .map(|found| found.to_vec())
382        .unwrap_or_default();
383
384    let mut fields = Vec::with_capacity(declared.len());
385    for entry in declared {
386        // Reached by a key reflection already found.
387        if let Some(key) = entry.using.as_deref() {
388            let Some(rel) = reflected
389                .iter()
390                .find(|rel| relationship_keys(rel).iter().any(|found| found == key))
391            else {
392                tracing::warn!(
393                    "GraphQL: {}.{} declares the relationship \"{}\" through \"{}\", \
394                     which no foreign key carries; it is left out",
395                    table.schema,
396                    table.name,
397                    entry.name,
398                    key
399                );
400                continue;
401            };
402            let foreign = rel.foreign_table();
403            let Some(target_base) = base_names.get(&(foreign.schema.clone(), foreign.name.clone()))
404            else {
405                continue;
406            };
407            let mut field =
408                RelationshipField::from_relationship_named(rel, target_base, Some(&entry.name));
409            field.arguments = caller_arguments(rel, schema_cache);
410            fields.push(field);
411            continue;
412        }
413
414        // Reached by a column mapping, which is a join no key describes.
415        let Some((schema, name)) = entry.target(config.default_schema()) else {
416            tracing::warn!(
417                "GraphQL: {}.{} declares the relationship \"{}\" with neither a key \
418                 nor a table to join to; it is left out",
419                table.schema,
420                table.name,
421                entry.name
422            );
423            continue;
424        };
425        let Some(target_base) = base_names.get(&(schema, name)) else {
426            continue;
427        };
428        match mapped_relationship_field(
429            entry,
430            table,
431            schema_cache,
432            config.default_schema(),
433            target_base,
434        ) {
435            Some(field) => fields.push(field),
436            None => tracing::warn!(
437                "GraphQL: {}.{} declares the relationship \"{}\" with no columns to \
438                 join on; it is left out",
439                table.schema,
440                table.name,
441                entry.name
442            ),
443        }
444    }
445
446    // A computed relationship is declared by `add_computed_field`, not by
447    // `create_object_relationship`, so it is not in this list and is not what
448    // the list is exhaustive about. Leaving it to reflection is what keeps
449    // `author.get_articles` on a table whose foreign keys are all declared.
450    let already: std::collections::HashSet<String> =
451        fields.iter().map(|field| field.name.clone()).collect();
452    for rel in reflected
453        .iter()
454        .filter(|rel| matches!(rel, Relationship::Computed { .. }))
455    {
456        let foreign = rel.foreign_table();
457        let Some(target_base) = base_names.get(&(foreign.schema.clone(), foreign.name.clone()))
458        else {
459            continue;
460        };
461        let mut field = RelationshipField::from_relationship_named(
462            rel,
463            target_base,
464            relationship_keys(rel)
465                .iter()
466                .find_map(|key| config.names.relationship(&table.schema, &table.name, key)),
467        );
468        if already.contains(&field.name) {
469            continue;
470        }
471        field.arguments = caller_arguments(rel, schema_cache);
472        fields.push(field);
473    }
474    fields
475}
476
477/// One relationship field for a declaration that maps column to column.
478///
479/// The join is the declaration's, the direction is the declaration's, and the
480/// constraint name is this server's -- nothing reads it back, and a mapped
481/// join has no constraint to borrow. `None` where the declaration names no
482/// columns to join on, which is not a join.
483pub(crate) fn mapped_relationship_field(
484    entry: &crate::names::DeclaredRelationship,
485    table: &Table,
486    schema_cache: &SchemaCache,
487    default_schema: &str,
488    target_base: &str,
489) -> Option<RelationshipField> {
490    use postrust_core::schema_cache::{Cardinality, Relationship};
491
492    let (schema, name) = entry.target(default_schema)?;
493    if entry.columns.is_empty() {
494        return None;
495    }
496    let foreign_table = postrust_core::api_request::QualifiedIdentifier::new(&schema, &name);
497    let here = table.qualified_identifier();
498    let columns: Vec<(String, String)> = entry
499        .columns
500        .iter()
501        .map(|(mine, theirs)| (mine.clone(), theirs.clone()))
502        .collect();
503    let constraint = format!("{}.{}", table.name, entry.name);
504    // Which side holds the key, which is what a nested insert needs and a
505    // column mapping does not say. `M2O` is "this row's column carries their
506    // key", so the related row is written first; `O2O { is_parent: true }` is
507    // the other way round, and is how `after_parent` is spelled here. A
508    // relationship to many is written after its parent either way.
509    let cardinality = match (entry.to_one, entry.after_parent) {
510        (true, false) => Cardinality::M2O {
511            constraint: constraint.clone(),
512            columns,
513        },
514        (true, true) => Cardinality::O2O {
515            constraint: constraint.clone(),
516            columns,
517            is_parent: true,
518        },
519        (false, _) => Cardinality::O2M {
520            constraint: constraint.clone(),
521            columns,
522        },
523    };
524    let rel = Relationship::ForeignKey {
525        is_self: here == foreign_table,
526        table: here,
527        foreign_table_is_view: schema_cache
528            .get_table(&foreign_table)
529            .is_some_and(|t| t.is_view),
530        foreign_table,
531        cardinality,
532        table_is_view: table.is_view,
533        constraint_name: constraint,
534    };
535    Some(RelationshipField::from_relationship_named(
536        &rel,
537        target_base,
538        Some(&entry.name),
539    ))
540}
541
542fn caller_arguments(
543    relationship: &postrust_core::schema_cache::Relationship,
544    schema_cache: &SchemaCache,
545) -> Vec<(String, String, bool)> {
546    use postrust_core::schema_cache::Relationship;
547    let Relationship::Computed {
548        function,
549        row_argument,
550        ..
551    } = relationship
552    else {
553        return Vec::new();
554    };
555    let Some(row_argument) = row_argument else {
556        return Vec::new();
557    };
558    schema_cache
559        .routines
560        .get(function)
561        .into_iter()
562        .flatten()
563        .find(|routine| routine.name == function.name)
564        .map(|routine| {
565            routine
566                .params
567                .iter()
568                .filter(|param| &param.name != row_argument)
569                .filter(|param| !is_session_argument(param))
570                .filter(|param| !param.name.is_empty())
571                .map(|param| (param.name.clone(), param.param_type.clone(), param.required))
572                .collect()
573        })
574        .unwrap_or_default()
575}
576
577/// What a computed *column*'s function takes from the caller.
578///
579/// The same rule as [`caller_arguments`], one field kind along: everything but
580/// the row, which is what makes it a field of this table, and the session,
581/// which is the server's to supply.
582pub(crate) fn computed_caller_arguments(
583    computed: &postrust_core::schema_cache::ComputedColumn,
584    schema_cache: &SchemaCache,
585) -> Vec<(String, String, bool)> {
586    let Some(row_argument) = &computed.row_argument else {
587        return Vec::new();
588    };
589    schema_cache
590        .routines
591        .get(&computed.function)
592        .into_iter()
593        .flatten()
594        .find(|routine| routine.name == computed.function.name)
595        .map(|routine| {
596            routine
597                .params
598                .iter()
599                .filter(|param| &param.name != row_argument)
600                .filter(|param| !is_session_argument(param))
601                .filter(|param| !param.name.is_empty())
602                .map(|param| (param.name.clone(), param.param_type.clone(), param.required))
603                .collect()
604        })
605        .unwrap_or_default()
606}
607
608/// Whether a parameter is the one Hasura fills from the session.
609///
610/// Recognised by name and type, which is the only thing the database records:
611/// Hasura writes `session_argument: hasura_session` into metadata, and every
612/// function that has one calls it that.
613fn is_session_argument(param: &postrust_core::schema_cache::RoutineParam) -> bool {
614    param.name == "hasura_session" && matches!(param.param_type.as_str(), "json" | "jsonb")
615}
616
617impl GeneratedSchema {
618    /// Get an object type by name.
619    pub fn get_object_type(&self, name: &str) -> Option<&TableObjectType> {
620        self.object_types.get(name)
621    }
622
623    /// Get query fields for a table.
624    pub fn get_query_field(&self, table_name: &str) -> Option<&QueryField> {
625        self.query_fields
626            .iter()
627            .find(|f| f.table_name == table_name)
628    }
629
630    /// Get mutation fields for a table.
631    pub fn get_mutation_fields(&self, table_name: &str) -> Vec<&MutationField> {
632        self.mutation_fields
633            .iter()
634            .filter(|f| f.table_name == table_name)
635            .collect()
636    }
637
638    /// Get relationship fields for a type.
639    pub fn get_relationship_fields(&self, type_name: &str) -> Option<&Vec<RelationshipField>> {
640        self.relationship_fields.get(type_name)
641    }
642
643    /// Get all table names.
644    pub fn table_names(&self) -> Vec<&str> {
645        self.object_types
646            .values()
647            .map(|t| t.table.name.as_str())
648            .collect()
649    }
650
651    /// Get all type names.
652    pub fn type_names(&self) -> Vec<&str> {
653        self.object_types.keys().map(|s| s.as_str()).collect()
654    }
655}
656
657/// A query field for a table (e.g., users, userByPk).
658#[derive(Debug, Clone)]
659pub struct QueryField {
660    /// Field name (e.g., "users")
661    pub name: String,
662    /// Table name
663    pub table_name: String,
664    /// Schema the table lives in
665    pub schema_name: String,
666    /// GraphQL object type name (e.g., "Users")
667    pub type_name: String,
668    /// GraphQL return type
669    pub return_type: String,
670    /// Whether this returns a list
671    pub is_list: bool,
672    /// Whether this is a "by PK" query
673    pub is_by_pk: bool,
674    /// Primary key columns, as `(column name, PostgreSQL type)`.
675    ///
676    /// Populated for by-PK queries so the resolver can filter on the table's
677    /// actual key rather than assuming a column called `id`. Empty for list
678    /// queries.
679    pub pk_columns: Vec<(String, String)>,
680    /// Field description
681    pub description: Option<String>,
682    /// The name given to this table's aggregate root, if one was.
683    ///
684    /// The aggregate field is built beside the list field rather than being a
685    /// `QueryField` of its own, so the name it was given rides along with the
686    /// list field that spawns it.
687    pub aggregate_name: Option<String>,
688    /// The description given to that aggregate root, if one was. `Some("")`
689    /// means metadata said it has none.
690    pub aggregate_description: Option<String>,
691    /// Whether the aggregate root is built at all.
692    ///
693    /// The one thing a role's view of the schema cache cannot say. Dropping a
694    /// table or a column is a fact about the database as this role sees it;
695    /// whether that role may *count* the rows it can already read is a fact
696    /// about the permission, and Hasura keeps the two apart because counting
697    /// rows you cannot see is a way of seeing them. True unless a select
698    /// permission says otherwise, which is what leaves an unconfigured server
699    /// exactly as it was.
700    pub aggregates: bool,
701    /// Whether the rows themselves can be asked for.
702    ///
703    /// False for a table a role may count and not read: Hasura's way of
704    /// granting "how many" without granting "which" is a select permission
705    /// naming no columns, and such a table has an aggregate root and no row
706    /// type to hang a list root on. The field is still carried so that the
707    /// aggregate root beside it is built.
708    pub rows: bool,
709}
710
711impl QueryField {
712    /// Create a list query field (e.g., users), named after the table.
713    pub fn list(table: &Table) -> Self {
714        Self::list_named(table, &table.name)
715    }
716
717    /// Create a list query field using an explicit base name.
718    pub fn list_named(table: &Table, base_name: &str) -> Self {
719        let type_name = base_name.to_string();
720        let name = base_name.to_string();
721
722        Self {
723            name,
724            table_name: table.name.clone(),
725            schema_name: table.schema.clone(),
726            type_name: type_name.clone(),
727            return_type: format!("[{}!]!", type_name),
728            is_list: true,
729            is_by_pk: false,
730            pk_columns: Vec::new(),
731            description: Some(format!("fetch data from the table: \"{}\"", table.name)),
732            aggregate_name: None,
733            aggregate_description: None,
734            aggregates: true,
735            rows: true,
736        }
737    }
738
739    /// Create a by-PK query field (e.g., userByPk), named after the table.
740    pub fn by_pk(table: &Table) -> Option<Self> {
741        Self::by_pk_named(table, &table.name)
742    }
743
744    /// Create a by-PK query field using an explicit base name.
745    pub fn by_pk_named(table: &Table, base_name: &str) -> Option<Self> {
746        if !has_whole_key(table) {
747            return None;
748        }
749
750        let type_name = base_name.to_string();
751        let field_name = format!("{}_by_pk", base_name);
752
753        // Carry the key columns and their types so the resolver can filter on
754        // the real primary key.
755        let pk_columns = pk_columns_of(table);
756
757        Some(Self {
758            name: field_name,
759            table_name: table.name.clone(),
760            schema_name: table.schema.clone(),
761            type_name: type_name.clone(),
762            return_type: type_name,
763            is_list: false,
764            is_by_pk: true,
765            pk_columns,
766            description: Some(format!(
767                "fetch data from the table: \"{}\" using primary key columns",
768                table.name
769            )),
770            aggregate_name: None,
771            aggregate_description: None,
772            aggregates: true,
773            rows: true,
774        })
775    }
776}
777
778/// A mutation field for a table.
779#[derive(Debug, Clone)]
780pub struct MutationField {
781    /// Field name (e.g., "insertUsers")
782    pub name: String,
783    /// Table name
784    pub table_name: String,
785    /// Schema the table lives in
786    pub schema_name: String,
787    /// Mutation type
788    pub mutation_type: MutationType,
789    /// Primary key columns, as `(column name, PostgreSQL type)`.
790    ///
791    /// Populated for by-PK mutations so the resolver can target the row by its
792    /// key. Empty for bulk mutations.
793    pub pk_columns: Vec<(String, String)>,
794    /// GraphQL return type
795    pub return_type: String,
796    /// Field description
797    pub description: Option<String>,
798}
799
800/// Types of mutations.
801#[derive(Debug, Clone, Copy, PartialEq, Eq)]
802pub enum MutationType {
803    /// Insert multiple records
804    Insert,
805    /// Insert a single record
806    InsertOne,
807    /// Update records matching a filter
808    Update,
809    /// Update a single record by PK
810    UpdateByPk,
811    /// Several updates, each with its own filter, in one transaction
812    UpdateMany,
813    /// Delete records matching a filter
814    Delete,
815    /// Delete a single record by PK
816    DeleteByPk,
817}
818
819impl MutationField {
820    /// Create insert mutation fields for a table.
821    pub fn insert_fields(table: &Table) -> Vec<Self> {
822        Self::insert_fields_named(table, &table.name)
823    }
824
825    /// As [`Self::insert_fields`], with an explicit base name for the generated
826    /// field and type names.
827    pub fn insert_fields_named(table: &Table, base_name: &str) -> Vec<Self> {
828        if !is_insertable(table) {
829            return vec![];
830        }
831
832        let type_name = base_name.to_string();
833
834        let mut fields = vec![];
835
836        // insert_users (batch insert)
837        let name = format!("insert_{}", base_name);
838        fields.push(Self {
839            name,
840            table_name: table.name.clone(),
841            schema_name: table.schema.clone(),
842            mutation_type: MutationType::Insert,
843            pk_columns: Vec::new(),
844            return_type: format!("{}_mutation_response", type_name),
845            description: Some(format!("insert data into the table: \"{}\"", table.name)),
846        });
847
848        // insert_user_one (single insert)
849        let name = format!("insert_{}_one", base_name);
850        fields.push(Self {
851            name,
852            table_name: table.name.clone(),
853            schema_name: table.schema.clone(),
854            mutation_type: MutationType::InsertOne,
855            pk_columns: Vec::new(),
856            return_type: type_name.clone(),
857            description: Some(format!(
858                "insert a single row into the table: \"{}\"",
859                table.name
860            )),
861        });
862
863        fields
864    }
865
866    /// Create update mutation fields for a table.
867    pub fn update_fields(table: &Table) -> Vec<Self> {
868        Self::update_fields_named(table, &table.name)
869    }
870
871    /// As [`Self::update_fields`], with an explicit base name for the generated
872    /// field and type names.
873    pub fn update_fields_named(table: &Table, base_name: &str) -> Vec<Self> {
874        if !is_updatable(table) {
875            return vec![];
876        }
877
878        let type_name = base_name.to_string();
879
880        let mut fields = vec![];
881
882        // update_users (batch update)
883        let name = format!("update_{}", base_name);
884        fields.push(Self {
885            name,
886            table_name: table.name.clone(),
887            schema_name: table.schema.clone(),
888            mutation_type: MutationType::Update,
889            pk_columns: Vec::new(),
890            return_type: format!("{}_mutation_response", type_name),
891            description: Some(format!("update data of the table: \"{}\"", table.name)),
892        });
893
894        // update_users_many (several filters, each with its own values)
895        let name = format!("update_{}_many", base_name);
896        fields.push(Self {
897            name,
898            table_name: table.name.clone(),
899            schema_name: table.schema.clone(),
900            mutation_type: MutationType::UpdateMany,
901            pk_columns: Vec::new(),
902            return_type: format!("[{}_mutation_response]", type_name),
903            description: Some(format!(
904                "update multiples rows of table: \"{}\"",
905                table.name
906            )),
907        });
908
909        // update_user_by_pk (single update by PK)
910        if has_whole_key(table) {
911            let name = format!("update_{}_by_pk", base_name);
912            fields.push(Self {
913                name,
914                table_name: table.name.clone(),
915                schema_name: table.schema.clone(),
916                mutation_type: MutationType::UpdateByPk,
917                pk_columns: pk_columns_of(table),
918                return_type: type_name,
919                description: Some(format!(
920                    "update single row of the table: \"{}\"",
921                    table.name
922                )),
923            });
924        }
925
926        fields
927    }
928
929    /// Create delete mutation fields for a table.
930    pub fn delete_fields(table: &Table) -> Vec<Self> {
931        Self::delete_fields_named(table, &table.name)
932    }
933
934    /// As [`Self::delete_fields`], with an explicit base name for the generated
935    /// field and type names.
936    pub fn delete_fields_named(table: &Table, base_name: &str) -> Vec<Self> {
937        if !is_deletable(table) {
938            return vec![];
939        }
940
941        let type_name = base_name.to_string();
942
943        let mut fields = vec![];
944
945        // delete_users (batch delete)
946        let name = format!("delete_{}", base_name);
947        fields.push(Self {
948            name,
949            table_name: table.name.clone(),
950            schema_name: table.schema.clone(),
951            mutation_type: MutationType::Delete,
952            pk_columns: Vec::new(),
953            return_type: format!("{}_mutation_response", type_name),
954            description: Some(format!("delete data from the table: \"{}\"", table.name)),
955        });
956
957        // delete_user_by_pk (single delete by PK)
958        if has_whole_key(table) {
959            let name = format!("delete_{}_by_pk", base_name);
960            fields.push(Self {
961                name,
962                table_name: table.name.clone(),
963                schema_name: table.schema.clone(),
964                mutation_type: MutationType::DeleteByPk,
965                pk_columns: pk_columns_of(table),
966                return_type: type_name,
967                description: Some(format!(
968                    "delete single row from the table: \"{}\"",
969                    table.name
970                )),
971            });
972        }
973
974        fields
975    }
976}
977
978/// Build a GraphQL schema from a schema cache.
979pub fn build_schema(schema_cache: &SchemaCache, config: &SchemaConfig) -> GeneratedSchema {
980    let mut object_types = HashMap::new();
981    let mut query_fields = Vec::new();
982    let mut mutation_fields = Vec::new();
983    let mut relationship_fields = HashMap::new();
984
985    // Tables are visited in a stable order: the cache is a hash map, and any
986    // name disambiguation below must not shift between restarts.
987    let mut tables: Vec<&Table> = schema_cache
988        .tables
989        .values()
990        .filter(|table| config.is_schema_exposed(&table.schema))
991        .collect();
992    tables.sort_by(|a, b| (&a.schema, &a.name).cmp(&(&b.schema, &b.name)));
993
994    // Base names already carry the schema for non-default schemas, so a clash
995    // here needs contrived naming (a `public.api_users` table alongside
996    // `api.users`). Resolve it with a numeric suffix rather than letting one
997    // table overwrite the other.
998    let mut used_base_names: HashMap<String, u32> = HashMap::new();
999    // (schema, table) -> resolved base name, needed when naming relationship
1000    // targets.
1001    let mut base_names: HashMap<(String, String), String> = HashMap::new();
1002
1003    for table in &tables {
1004        let preferred = base_name_for(table, config);
1005        let base_name = match used_base_names.get_mut(&preferred) {
1006            None => {
1007                used_base_names.insert(preferred.clone(), 1);
1008                preferred
1009            }
1010            Some(count) => {
1011                *count += 1;
1012                let disambiguated = format!("{}_{}", preferred, count);
1013                tracing::warn!(
1014                    "GraphQL name collision: {}.{} would generate the same names as \
1015                     an earlier table; exposing it as \"{}\" instead",
1016                    table.schema,
1017                    table.name,
1018                    disambiguated
1019                );
1020                disambiguated
1021            }
1022        };
1023
1024        base_names.insert(
1025            (table.schema.clone(), table.name.clone()),
1026            base_name.clone(),
1027        );
1028    }
1029
1030    // The tables this role may read, by base name. Needed after the loop, by
1031    // anything that would answer with a row of one.
1032    let mut readable_bases: HashSet<String> = HashSet::new();
1033
1034    for table in &tables {
1035        let base_name = base_names
1036            .get(&(table.schema.clone(), table.name.clone()))
1037            .expect("every visited table has a resolved base name")
1038            .clone();
1039
1040        // Create object type
1041        let mut obj_type = TableObjectType::from_table_named(table, &base_name, &config.names);
1042        // Whether this role can read the table at all. A table it may only
1043        // write has no GraphQL type -- a type with no fields is not a legal
1044        // one -- so nothing that would return that type is generated: no query
1045        // root, no `_by_pk`, no `insert_one`. The bulk write stays, and
1046        // answers with the count alone.
1047        //
1048        // Asked of the permission rather than of the fields, because the
1049        // fields are not split into the readable and the writable half until
1050        // every rewrite below has been applied to both.
1051        let readable = config.role.as_deref().is_none_or(|role| {
1052            config
1053                .names
1054                .permissions(&table.schema, &table.name, role)
1055                .is_none_or(|granted| crate::role::reads_anything(granted, table))
1056        });
1057        if readable {
1058            readable_bases.insert(base_name.clone());
1059        }
1060        // What each computed field takes from the caller. Read from the
1061        // routine, for the reason `caller_arguments` gives.
1062        for field in &mut obj_type.fields {
1063            let Some(function) = config
1064                .names
1065                .computed_source(&table.schema, &table.name, &field.name)
1066                .or(Some(field.name.as_str()))
1067            else {
1068                continue;
1069            };
1070            let Some(computed) = table.get_computed_column(function) else {
1071                continue;
1072            };
1073            field.arguments = computed_caller_arguments(computed, schema_cache);
1074        }
1075        let type_name = obj_type.name.clone();
1076
1077        // Add query fields. Each root may be named separately -- Hasura names
1078        // them one at a time, and a set that does not agree on a base name has
1079        // nowhere else to be written down.
1080        // A root may be given a name, a description, or both. An empty
1081        // description is one that was given and is empty: metadata said the
1082        // field has none.
1083        let rename = |field: &mut QueryField, kind: &str| {
1084            if let Some(given) = config.names.root(&table.schema, &table.name, kind) {
1085                field.name = given.to_string();
1086            }
1087            if let Some(comment) = config.names.root_comment(&table.schema, &table.name, kind) {
1088                field.description = Some(comment.to_string()).filter(|c| !c.is_empty());
1089            }
1090        };
1091        let mut list = QueryField::list_named(table, &base_name);
1092        rename(&mut list, "select");
1093        // The aggregate root is generated from the type name rather than being
1094        // a `QueryField` of its own, so its name travels with the list field
1095        // that spawns it.
1096        list.aggregate_name = config
1097            .names
1098            .root(&table.schema, &table.name, "select_aggregate")
1099            .map(str::to_string);
1100        list.aggregate_description = config
1101            .names
1102            .root_comment(&table.schema, &table.name, "select_aggregate")
1103            .map(str::to_string);
1104        // Whether this role may count what it can read. Only asked where a
1105        // role is building its own schema; the unrestricted one aggregates
1106        // whatever the database will.
1107        if let Some(role) = &config.role {
1108            list.aggregates =
1109                crate::role::allows_aggregations(&config.names, &table.schema, &table.name, role);
1110        }
1111        // A table with no readable field still gets its list entry when the
1112        // role may count it: the aggregate root is built beside that entry
1113        // rather than being one of its own, and `rows` is what says the list
1114        // root itself is not there. By key there is nothing to answer with at
1115        // all.
1116        list.rows = readable;
1117        if readable || list.aggregates {
1118            query_fields.push(list);
1119        }
1120        if readable {
1121            if let Some(mut by_pk) = QueryField::by_pk_named(table, &base_name) {
1122                rename(&mut by_pk, "select_by_pk");
1123                query_fields.push(by_pk);
1124            }
1125        }
1126
1127        // Add mutation fields if enabled
1128        if config.enable_mutations {
1129            let mut fields: Vec<MutationField> = Vec::new();
1130            fields.extend(MutationField::insert_fields_named(table, &base_name));
1131            fields.extend(MutationField::update_fields_named(table, &base_name));
1132            fields.extend(MutationField::delete_fields_named(table, &base_name));
1133            // The three that answer with a row rather than with a count.
1134            if !readable {
1135                fields.retain(|field| {
1136                    !matches!(
1137                        field.mutation_type,
1138                        MutationType::InsertOne
1139                            | MutationType::UpdateByPk
1140                            | MutationType::DeleteByPk
1141                    )
1142                });
1143            }
1144            for field in &mut fields {
1145                let kind = match field.mutation_type {
1146                    MutationType::Insert => "insert",
1147                    MutationType::InsertOne => "insert_one",
1148                    MutationType::Update => "update",
1149                    MutationType::UpdateByPk => "update_by_pk",
1150                    MutationType::UpdateMany => "update_many",
1151                    MutationType::Delete => "delete",
1152                    MutationType::DeleteByPk => "delete_by_pk",
1153                };
1154                if let Some(given) = config.names.root(&table.schema, &table.name, kind) {
1155                    field.name = given.to_string();
1156                }
1157                if let Some(comment) = config.names.root_comment(&table.schema, &table.name, kind) {
1158                    field.description = Some(comment.to_string()).filter(|c| !c.is_empty());
1159                }
1160            }
1161            mutation_fields.extend(fields);
1162        }
1163
1164        // Add relationship fields. Where metadata declares them, they are
1165        // what the table has; otherwise reflection offers one per foreign
1166        // key, which is what a document saying nothing about a table has
1167        // always meant. See `TableNames::declared_relationships`.
1168        let rels = match config
1169            .names
1170            .declared_relationships(&table.schema, &table.name)
1171        {
1172            Some(declared) => declared_fields(declared, table, schema_cache, config, &base_names),
1173            None => reflected_fields(table, schema_cache, config, &base_names),
1174        };
1175
1176        if !rels.is_empty() {
1177            relationship_fields.insert(type_name.clone(), rels);
1178        }
1179
1180        object_types.insert(type_name, obj_type);
1181    }
1182
1183    // A table marked as a set of allowed values becomes a GraphQL enum, and
1184    // every column with a foreign key to it is typed as that enum rather than
1185    // as text. The values are rows, so they were read at startup rather than
1186    // reflected.
1187    let mut enum_types: HashMap<String, Vec<(String, Option<String>)>> = HashMap::new();
1188    let mut enum_type_of: HashMap<(String, String), String> = HashMap::new();
1189
1190    for (schema_name, table_name) in config.names.enum_tables() {
1191        // A role that may not read the table's rows still sees the enum. The
1192        // values are a *type*, and a column typed by it is readable -- Hasura
1193        // publishes `colors_enum` to a role granted nothing on `colors`, and
1194        // the query `where: {favorite_color: {_eq: red}}` is one it answers.
1195        // So the name is derived rather than looked up, since a table the
1196        // role cannot read has no entry to look up.
1197        let base = match base_names.get(&(schema_name.clone(), table_name.clone())) {
1198            Some(found) => found.clone(),
1199            None => config
1200                .names
1201                .base_name(&schema_name, &table_name)
1202                .map(str::to_string)
1203                .unwrap_or_else(|| match schema_name == config.default_schema() {
1204                    true => table_name.clone(),
1205                    false => format!("{}_{}", schema_name, table_name),
1206                }),
1207        };
1208        let key = format!("{}.{}", schema_name, table_name);
1209        let members: Vec<(String, Option<String>)> = config
1210            .enum_values
1211            .get(&key)
1212            .map(|values| {
1213                values
1214                    .iter()
1215                    .filter(|(value, _)| is_graphql_name(value))
1216                    .cloned()
1217                    .collect()
1218            })
1219            .unwrap_or_default();
1220
1221        if members.is_empty() {
1222            // No rows, or none whose value is a legal GraphQL name. An empty
1223            // enum is not a legal type, so the table stays an ordinary one.
1224            tracing::warn!(
1225                "{}.{} is marked as an enumeration but has no values that can name an \
1226                 enum member; exposing it as an ordinary table",
1227                schema_name,
1228                table_name
1229            );
1230            continue;
1231        }
1232
1233        let type_name = format!("{}_enum", base);
1234        enum_type_of.insert((schema_name.clone(), table_name.clone()), type_name.clone());
1235        enum_types.insert(type_name, members);
1236    }
1237
1238    // Retype the columns that point at one.
1239    //
1240    // Read from the catalogue's foreign keys rather than from the
1241    // relationships this schema exposes, which are two different questions: a
1242    // relationship *to* an enum table is never exposed at all -- see just
1243    // below -- and a column's type has nothing to do with whether the row at
1244    // the other end can be traversed to. Keying one on the other meant a table
1245    // whose relationships metadata had declared away lost its enum columns'
1246    // type with them.
1247    if !enum_type_of.is_empty() {
1248        for table in &tables {
1249            let Some(type_name) = base_names.get(&(table.schema.clone(), table.name.clone()))
1250            else {
1251                continue;
1252            };
1253            let found = schema_cache
1254                .get_relationships(&table.qualified_identifier(), &table.schema)
1255                .map(|found| found.to_vec())
1256                .unwrap_or_default();
1257            for relationship in &found {
1258                if !relationship.is_to_one() {
1259                    continue;
1260                }
1261                let foreign = relationship.foreign_table();
1262                let Some(enum_type) =
1263                    enum_type_of.get(&(foreign.schema.clone(), foreign.name.clone()))
1264                else {
1265                    continue;
1266                };
1267                let Some(column) = relationship.single_local_column() else {
1268                    continue;
1269                };
1270                if let Some(object) = object_types.get_mut(type_name) {
1271                    // Under the name the column is exposed as, which is not
1272                    // always its own.
1273                    let exposed = config
1274                        .names
1275                        .column(&object.table.schema, &object.table.name, column)
1276                        .unwrap_or(column)
1277                        .to_string();
1278                    for field in &mut object.fields {
1279                        if field.name == exposed {
1280                            field.graphql_type =
1281                                crate::types::GraphQLType::Custom(enum_type.clone());
1282                        }
1283                    }
1284                }
1285            }
1286        }
1287    }
1288
1289    // A table marked as a set of allowed values is a set of *values*: the
1290    // column typed as the enum is the whole of what a client needs, and there
1291    // is nothing at the other end of a relationship to it worth traversing to.
1292    // Hasura exposes none, and a stray `user { color { ... } }` beside
1293    // `favorite_color` is a second spelling of the same fact.
1294    if !enum_type_of.is_empty() {
1295        for relationships in relationship_fields.values_mut() {
1296            relationships.retain(|relationship| {
1297                let foreign = relationship.relationship.foreign_table();
1298                !enum_type_of.contains_key(&(foreign.schema.clone(), foreign.name.clone()))
1299            });
1300        }
1301    }
1302
1303    // Split each type's fields into the half that may be read and the half
1304    // that may be written. Last, so that everything above -- the computed
1305    // fields' arguments, the retyping of an enum table's columns -- has been
1306    // applied to the whole list before either half is taken out of it.
1307    //
1308    // A role may be granted a column to set that it is not granted to see, so
1309    // the cache carries the union and this is where the two part company. See
1310    // [`crate::role`].
1311    for object in object_types.values_mut() {
1312        // Taken unconditionally, even where no role narrows anything: the
1313        // write inputs are built from this list, and leaving it as it was when
1314        // the type was first made would leave it behind every rewrite since.
1315        object.writable_fields = object.fields.clone();
1316        if let Some(role) = &config.role {
1317            let (schema_name, table_name) =
1318                (object.table.schema.clone(), object.table.name.clone());
1319            let Some(granted) = config.names.permissions(&schema_name, &table_name, role) else {
1320                continue;
1321            };
1322            let table = object.table.clone();
1323            object.fields.retain(|field| {
1324                let column = config
1325                    .names
1326                    .column_source(&schema_name, &table_name, &field.name)
1327                    .unwrap_or(&field.name);
1328                match table.get_column(column) {
1329                    // A computed field rather than a column, and those were
1330                    // reduced to the granted ones with the cache.
1331                    None => true,
1332                    Some(_) => granted
1333                        .select
1334                        .as_ref()
1335                        .is_some_and(|select| select.columns.allows(column)),
1336                }
1337            });
1338        }
1339    }
1340
1341    // Functions that answer with rows of a table it already exposes. One
1342    // returning anything else has no type here to return: a scalar-returning
1343    // function is not a root field, it is an RPC, which the REST surface
1344    // already offers.
1345    let mut function_fields = Vec::new();
1346    for routines in schema_cache.routines.values() {
1347        for routine in routines {
1348            if !config.is_schema_exposed(&routine.schema) || routine.is_procedure {
1349                continue;
1350            }
1351            let postrust_core::schema_cache::RetType::SetOf(returned) = &routine.return_type else {
1352                continue;
1353            };
1354            // A function taking a table's row is that table's computed field,
1355            // not a root field: `fetch_articles(author_row author)` is asked
1356            // of an author, and a row type is not something a client can send
1357            // as an argument. It is already exposed where it belongs.
1358            if routine.params.iter().any(|param| {
1359                let (_, named) = type_name_of(&param.param_type);
1360                base_names.keys().any(|(_, table)| table == &named)
1361            }) {
1362                continue;
1363            }
1364            // `SETOF <table>`, where the table is one this schema exposes. The
1365            // catalogue names the returned type without a schema, so the
1366            // function's own schema is tried first -- a function and the table
1367            // it returns usually live together, and two schemas may both have
1368            // a table of that name.
1369            let (returned_schema, returned_table) = type_name_of(returned);
1370            let found = base_names
1371                .iter()
1372                .find(|((schema, table), _)| {
1373                    table == &returned_table
1374                        && schema == returned_schema.as_ref().unwrap_or(&routine.schema)
1375                })
1376                .or_else(|| {
1377                    base_names
1378                        .iter()
1379                        .find(|((_, table), _)| table == &returned_table)
1380                });
1381            let Some(((target_schema, target_table), base)) = found else {
1382                continue;
1383            };
1384            // A function returning rows of a table this role cannot read has
1385            // nowhere to put them: that table has no GraphQL type, and naming
1386            // one that was never registered is a schema that will not build.
1387            if !readable_bases.contains(base) {
1388                continue;
1389            }
1390            let target = (target_schema.clone(), target_table.clone());
1391
1392            // Placed by what PostgreSQL says it does, unless metadata said
1393            // otherwise: `track_function` with `configuration: {exposed_as:
1394            // query}` puts a VOLATILE function on the query root, which is a
1395            // decision a person made and no catalogue remembers.
1396            let volatile = match config.names.exposed_as(&routine.schema, &routine.name) {
1397                Some("query") => false,
1398                Some("mutation") => true,
1399                _ => matches!(
1400                    routine.volatility,
1401                    postrust_core::schema_cache::FuncVolatility::Volatile
1402                ),
1403            };
1404            // A mutation a role was never granted. Hasura infers a query
1405            // function's permission from the select permission on the table it
1406            // returns -- which is the check just above -- and infers nothing
1407            // for a mutation: reading a table is not permission to change it,
1408            // so the role has to be named. A role granted no mutation at all
1409            // then has no mutation root, and a request for one is answered
1410            // `no mutations exist` rather than by a field that is missing.
1411            if volatile {
1412                if let Some(role) = config.role.as_deref() {
1413                    if !config
1414                        .names
1415                        .function_grants(&routine.schema, &routine.name, role)
1416                    {
1417                        continue;
1418                    }
1419                }
1420            }
1421
1422            function_fields.push(FunctionField {
1423                name: routine.name.clone(),
1424                schema_name: routine.schema.clone(),
1425                function_name: routine.name.clone(),
1426                returns: base.clone(),
1427                returns_table: target,
1428                arguments: routine
1429                    .params
1430                    .iter()
1431                    .filter(|param| !param.name.is_empty())
1432                    .filter(|param| !is_session_argument(param))
1433                    .map(|param| (param.name.clone(), param.param_type.clone(), param.required))
1434                    .collect(),
1435                session_argument: routine
1436                    .params
1437                    .iter()
1438                    .find(|param| is_session_argument(param))
1439                    .map(|param| param.name.clone()),
1440                volatile,
1441                description: routine.description.clone(),
1442            });
1443        }
1444    }
1445    function_fields.sort_by(|a, b| a.name.cmp(&b.name));
1446
1447    GeneratedSchema {
1448        object_types,
1449        query_fields,
1450        mutation_fields,
1451        relationship_fields,
1452        enum_types,
1453        function_fields,
1454    }
1455}
1456
1457/// Whether a value can name a GraphQL enum member.
1458///
1459/// `[_A-Za-z][_0-9A-Za-z]*`, which is what the specification allows. A value
1460/// that cannot is left out rather than mangled into one: a client asking for
1461/// `light blue` should be told there is no such member, not handed
1462/// `light_blue` and left to wonder which row it means.
1463fn is_graphql_name(value: &str) -> bool {
1464    let mut chars = value.chars();
1465    match chars.next() {
1466        Some(first) if first == '_' || first.is_ascii_alphabetic() => {}
1467        _ => return false,
1468    }
1469    chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
1470}
1471
1472#[cfg(test)]
1473mod tests {
1474    use super::*;
1475    use indexmap::IndexMap;
1476    use postrust_core::schema_cache::Column;
1477    use pretty_assertions::assert_eq;
1478
1479    fn create_test_table(name: &str, insertable: bool, updatable: bool, deletable: bool) -> Table {
1480        let mut columns = IndexMap::new();
1481        columns.insert(
1482            "id".into(),
1483            Column {
1484                name: "id".into(),
1485                description: None,
1486                nullable: false,
1487                data_type: "integer".into(),
1488                nominal_type: "int4".into(),
1489                max_len: None,
1490                default: Some("nextval('id_seq')".into()),
1491                enum_values: vec![],
1492                is_pk: true,
1493                position: 1,
1494                always_generated: false,
1495                domain_type: None,
1496            },
1497        );
1498        columns.insert(
1499            "name".into(),
1500            Column {
1501                name: "name".into(),
1502                description: None,
1503                nullable: false,
1504                data_type: "text".into(),
1505                nominal_type: "text".into(),
1506                max_len: None,
1507                default: None,
1508                enum_values: vec![],
1509                is_pk: false,
1510                position: 2,
1511                always_generated: false,
1512                domain_type: None,
1513            },
1514        );
1515
1516        Table {
1517            schema: "public".into(),
1518            name: name.into(),
1519            description: None,
1520            is_view: false,
1521            insertable,
1522            updatable,
1523            deletable,
1524            pk_cols: vec!["id".into()],
1525            unique_constraints: Vec::new(),
1526            columns,
1527            computed_columns: Default::default(),
1528            is_partitioned: false,
1529        }
1530    }
1531
1532    fn create_test_schema_cache() -> SchemaCache {
1533        use std::collections::{HashMap, HashSet};
1534
1535        let mut tables = HashMap::new();
1536
1537        let users = create_test_table("users", true, true, true);
1538        let posts = create_test_table("posts", true, true, true);
1539        let comments = create_test_table("comments", true, false, false);
1540
1541        tables.insert(users.qualified_identifier(), users);
1542        tables.insert(posts.qualified_identifier(), posts);
1543        tables.insert(comments.qualified_identifier(), comments);
1544
1545        SchemaCache {
1546            tables,
1547            relationships: HashMap::new(),
1548            routines: HashMap::new(),
1549            timezones: HashSet::new(),
1550            media_handlers: HashMap::new(),
1551            pg_version: 150000,
1552            representations: Default::default(),
1553        }
1554    }
1555
1556    // ============================================================================
1557    // SchemaConfig Tests
1558    // ============================================================================
1559
1560    #[test]
1561    fn test_schema_config_default() {
1562        let config = SchemaConfig::default();
1563        assert!(config.is_schema_exposed("public"));
1564        assert!(!config.is_schema_exposed("private"));
1565        assert!(config.enable_mutations);
1566        assert!(!config.enable_subscriptions);
1567    }
1568
1569    #[test]
1570    fn test_schema_config_with_schemas() {
1571        let config =
1572            SchemaConfig::new().with_schemas(vec!["api".to_string(), "public".to_string()]);
1573        assert!(config.is_schema_exposed("api"));
1574        assert!(config.is_schema_exposed("public"));
1575        assert!(!config.is_schema_exposed("private"));
1576    }
1577
1578    #[test]
1579    fn test_schema_config_mutations_disabled() {
1580        let config = SchemaConfig::new().with_mutations(false);
1581        assert!(!config.enable_mutations);
1582    }
1583
1584    // ============================================================================
1585    // QueryField Tests
1586    // ============================================================================
1587
1588    #[test]
1589    fn test_query_field_list() {
1590        let table = create_test_table("users", true, true, true);
1591        let field = QueryField::list(&table);
1592
1593        assert_eq!(field.name, "users");
1594        assert_eq!(field.return_type, "[users!]!");
1595        assert!(field.is_list);
1596        assert!(!field.is_by_pk);
1597    }
1598
1599    #[test]
1600    fn test_query_field_by_pk() {
1601        let table = create_test_table("users", true, true, true);
1602        let field = QueryField::by_pk(&table).unwrap();
1603
1604        assert_eq!(field.name, "users_by_pk");
1605        assert_eq!(field.return_type, "users");
1606        assert!(!field.is_list);
1607        assert!(field.is_by_pk);
1608    }
1609
1610    #[test]
1611    fn test_query_field_by_pk_no_pk() {
1612        let mut table = create_test_table("users", true, true, true);
1613        table.pk_cols = vec![];
1614        let field = QueryField::by_pk(&table);
1615
1616        assert!(field.is_none());
1617    }
1618
1619    // ============================================================================
1620    // MutationField Tests
1621    // ============================================================================
1622
1623    #[test]
1624    fn test_mutation_field_insert() {
1625        let table = create_test_table("users", true, true, true);
1626        let fields = MutationField::insert_fields(&table);
1627
1628        assert_eq!(fields.len(), 2);
1629        assert_eq!(fields[0].name, "insert_users");
1630        assert_eq!(fields[0].mutation_type, MutationType::Insert);
1631        assert_eq!(fields[1].name, "insert_users_one");
1632        assert_eq!(fields[1].mutation_type, MutationType::InsertOne);
1633    }
1634
1635    #[test]
1636    fn test_mutation_field_insert_not_insertable() {
1637        let table = create_test_table("users", false, true, true);
1638        let fields = MutationField::insert_fields(&table);
1639
1640        assert!(fields.is_empty());
1641    }
1642
1643    #[test]
1644    fn test_mutation_field_update() {
1645        let table = create_test_table("users", true, true, true);
1646        let fields = MutationField::update_fields(&table);
1647
1648        assert_eq!(fields.len(), 3);
1649        assert_eq!(fields[0].name, "update_users");
1650        assert_eq!(fields[0].mutation_type, MutationType::Update);
1651        assert_eq!(fields[1].name, "update_users_many");
1652        assert_eq!(fields[1].mutation_type, MutationType::UpdateMany);
1653        assert_eq!(fields[2].name, "update_users_by_pk");
1654        assert_eq!(fields[2].mutation_type, MutationType::UpdateByPk);
1655    }
1656
1657    #[test]
1658    fn test_mutation_field_update_not_updatable() {
1659        let table = create_test_table("users", true, false, true);
1660        let fields = MutationField::update_fields(&table);
1661
1662        assert!(fields.is_empty());
1663    }
1664
1665    #[test]
1666    fn test_mutation_field_delete() {
1667        let table = create_test_table("users", true, true, true);
1668        let fields = MutationField::delete_fields(&table);
1669
1670        assert_eq!(fields.len(), 2);
1671        assert_eq!(fields[0].name, "delete_users");
1672        assert_eq!(fields[0].mutation_type, MutationType::Delete);
1673        assert_eq!(fields[1].name, "delete_users_by_pk");
1674        assert_eq!(fields[1].mutation_type, MutationType::DeleteByPk);
1675    }
1676
1677    #[test]
1678    fn test_mutation_field_delete_not_deletable() {
1679        let table = create_test_table("users", true, true, false);
1680        let fields = MutationField::delete_fields(&table);
1681
1682        assert!(fields.is_empty());
1683    }
1684
1685    // ============================================================================
1686    // Singularize Tests
1687    // ============================================================================
1688
1689    // ============================================================================
1690    // Build Schema Tests
1691    // ============================================================================
1692
1693    #[test]
1694    fn test_build_schema_object_types() {
1695        let cache = create_test_schema_cache();
1696        let config = SchemaConfig::default();
1697        let schema = build_schema(&cache, &config);
1698
1699        assert_eq!(schema.object_types.len(), 3);
1700        assert!(schema.get_object_type("users").is_some());
1701        assert!(schema.get_object_type("posts").is_some());
1702        assert!(schema.get_object_type("comments").is_some());
1703    }
1704
1705    #[test]
1706    fn test_build_schema_query_fields() {
1707        let cache = create_test_schema_cache();
1708        let config = SchemaConfig::default();
1709        let schema = build_schema(&cache, &config);
1710
1711        // 3 tables * 2 (list + byPk) = 6 query fields
1712        assert_eq!(schema.query_fields.len(), 6);
1713
1714        // Check users query fields
1715        let users_field = schema.get_query_field("users").unwrap();
1716        assert_eq!(users_field.name, "users");
1717        assert!(users_field.is_list);
1718    }
1719
1720    #[test]
1721    fn test_build_schema_mutation_fields() {
1722        let cache = create_test_schema_cache();
1723        let config = SchemaConfig::default();
1724        let schema = build_schema(&cache, &config);
1725
1726        // users: 2 insert + 3 update + 2 delete = 7
1727        // posts: 2 insert + 3 update + 2 delete = 7
1728        // comments: 2 insert + 0 update + 0 delete = 2
1729        // Total: 16
1730        assert_eq!(schema.mutation_fields.len(), 16);
1731
1732        let users_mutations = schema.get_mutation_fields("users");
1733        assert_eq!(users_mutations.len(), 7);
1734    }
1735
1736    #[test]
1737    fn test_build_schema_mutations_disabled() {
1738        let cache = create_test_schema_cache();
1739        let config = SchemaConfig::new().with_mutations(false);
1740        let schema = build_schema(&cache, &config);
1741
1742        assert!(schema.mutation_fields.is_empty());
1743    }
1744
1745    #[test]
1746    fn test_build_schema_table_names() {
1747        let cache = create_test_schema_cache();
1748        let config = SchemaConfig::default();
1749        let schema = build_schema(&cache, &config);
1750
1751        let names = schema.table_names();
1752        assert_eq!(names.len(), 3);
1753        assert!(names.contains(&"users"));
1754        assert!(names.contains(&"posts"));
1755        assert!(names.contains(&"comments"));
1756    }
1757
1758    #[test]
1759    fn test_build_schema_type_names() {
1760        let cache = create_test_schema_cache();
1761        let config = SchemaConfig::default();
1762        let schema = build_schema(&cache, &config);
1763
1764        let names = schema.type_names();
1765        assert_eq!(names.len(), 3);
1766        assert!(names.contains(&"users"));
1767        assert!(names.contains(&"posts"));
1768        assert!(names.contains(&"comments"));
1769    }
1770
1771    #[test]
1772    fn test_build_schema_exposed_schemas() {
1773        let mut cache = create_test_schema_cache();
1774
1775        // Add a table in a different schema
1776        let private_table = Table {
1777            schema: "private".into(),
1778            name: "secrets".into(),
1779            description: None,
1780            is_view: false,
1781            insertable: true,
1782            updatable: true,
1783            deletable: true,
1784            pk_cols: vec!["id".into()],
1785            unique_constraints: Vec::new(),
1786            columns: indexmap::IndexMap::new(),
1787            computed_columns: Default::default(),
1788            is_partitioned: false,
1789        };
1790        cache
1791            .tables
1792            .insert(private_table.qualified_identifier(), private_table);
1793
1794        let config = SchemaConfig::default(); // Only exposes "public"
1795        let schema = build_schema(&cache, &config);
1796
1797        // Should only have 3 tables from public schema
1798        assert_eq!(schema.object_types.len(), 3);
1799        assert!(schema.get_object_type("Secrets").is_none());
1800    }
1801
1802    // ============================================================================
1803    // GeneratedSchema Tests
1804    // ============================================================================
1805
1806    #[test]
1807    fn test_generated_schema_get_object_type() {
1808        let cache = create_test_schema_cache();
1809        let config = SchemaConfig::default();
1810        let schema = build_schema(&cache, &config);
1811
1812        let users = schema.get_object_type("users").unwrap();
1813        assert_eq!(users.table.name, "users");
1814    }
1815
1816    #[test]
1817    fn test_generated_schema_get_query_field() {
1818        let cache = create_test_schema_cache();
1819        let config = SchemaConfig::default();
1820        let schema = build_schema(&cache, &config);
1821
1822        let field = schema.get_query_field("posts").unwrap();
1823        assert_eq!(field.table_name, "posts");
1824    }
1825
1826    #[test]
1827    fn test_generated_schema_get_mutation_fields() {
1828        let cache = create_test_schema_cache();
1829        let config = SchemaConfig::default();
1830        let schema = build_schema(&cache, &config);
1831
1832        let fields = schema.get_mutation_fields("comments");
1833        // comments is only insertable
1834        assert_eq!(fields.len(), 2); // insert_comments + insert_comments_one
1835    }
1836}