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
318impl FunctionField {
319    /// The argument list, as PostgreSQL would write it -- `a integer, b text`.
320    ///
321    /// Two overloads differ only here, so this is what distinguishes them in a
322    /// warning and what orders them against each other.
323    pub fn argument_signature(&self) -> String {
324        self.arguments
325            .iter()
326            .map(|(name, sql_type, _)| format!("{name} {sql_type}"))
327            .collect::<Vec<_>>()
328            .join(", ")
329    }
330}
331
332/// What a computed relationship's function takes from the caller.
333///
334/// Everything but the parent row, which is what makes it a relationship, and
335/// the session, which is the server's to supply. Read from the routine rather
336/// than from the relationship because the relationship loader knows only which
337/// argument is the row -- the rest are the same parameters any function has,
338/// and they are already loaded.
339/// One relationship field per foreign key the catalogue carries.
340fn reflected_fields(
341    table: &Table,
342    schema_cache: &SchemaCache,
343    config: &SchemaConfig,
344    base_names: &HashMap<(String, String), String>,
345) -> Vec<RelationshipField> {
346    schema_cache
347        .get_relationships(&table.qualified_identifier(), &table.schema)
348        .map(|relationships| {
349            relationships
350                .iter()
351                .filter_map(|rel| {
352                    // A relationship whose target is not exposed would
353                    // reference a GraphQL type that was never registered.
354                    let foreign = rel.foreign_table();
355                    let target_base =
356                        base_names.get(&(foreign.schema.clone(), foreign.name.clone()))?;
357                    let mut field = RelationshipField::from_relationship_named(
358                        rel,
359                        target_base,
360                        relationship_keys(rel).iter().find_map(|key| {
361                            config.names.relationship(&table.schema, &table.name, key)
362                        }),
363                    );
364                    field.arguments = caller_arguments(rel, schema_cache);
365                    Some(field)
366                })
367                .collect()
368        })
369        .unwrap_or_default()
370}
371
372/// One relationship field per relationship metadata declares, and no others.
373///
374/// A declaration reached by a key names a relationship reflection already
375/// found, so the catalogue's own cardinality and columns are used -- a key
376/// says which side has one row and which has many, and guessing that from a
377/// name would be guessing. One reached by a column mapping is not in the
378/// catalogue at all and is built from the mapping, with the direction taken
379/// from the declaration: Hasura writes object and array relationships
380/// separately, which is where `to_one` comes from.
381///
382/// A declaration this server cannot resolve is left out rather than guessed
383/// at. It is a field that would not work; the ones beside it still do.
384fn declared_fields(
385    declared: &[crate::names::DeclaredRelationship],
386    table: &Table,
387    schema_cache: &SchemaCache,
388    config: &SchemaConfig,
389    base_names: &HashMap<(String, String), String>,
390) -> Vec<RelationshipField> {
391    use postrust_core::schema_cache::Relationship;
392
393    let reflected = schema_cache
394        .get_relationships(&table.qualified_identifier(), &table.schema)
395        .map(|found| found.to_vec())
396        .unwrap_or_default();
397
398    let mut fields = Vec::with_capacity(declared.len());
399    for entry in declared {
400        // Reached by a key reflection already found.
401        if let Some(key) = entry.using.as_deref() {
402            let Some(rel) = reflected
403                .iter()
404                .find(|rel| relationship_keys(rel).iter().any(|found| found == key))
405            else {
406                tracing::warn!(
407                    "GraphQL: {}.{} declares the relationship \"{}\" through \"{}\", \
408                     which no foreign key carries; it is left out",
409                    table.schema,
410                    table.name,
411                    entry.name,
412                    key
413                );
414                continue;
415            };
416            let foreign = rel.foreign_table();
417            let Some(target_base) = base_names.get(&(foreign.schema.clone(), foreign.name.clone()))
418            else {
419                continue;
420            };
421            let mut field =
422                RelationshipField::from_relationship_named(rel, target_base, Some(&entry.name));
423            field.arguments = caller_arguments(rel, schema_cache);
424            fields.push(field);
425            continue;
426        }
427
428        // Reached by a column mapping, which is a join no key describes.
429        let Some((schema, name)) = entry.target(config.default_schema()) else {
430            tracing::warn!(
431                "GraphQL: {}.{} declares the relationship \"{}\" with neither a key \
432                 nor a table to join to; it is left out",
433                table.schema,
434                table.name,
435                entry.name
436            );
437            continue;
438        };
439        let Some(target_base) = base_names.get(&(schema, name)) else {
440            continue;
441        };
442        match mapped_relationship_field(
443            entry,
444            table,
445            schema_cache,
446            config.default_schema(),
447            target_base,
448        ) {
449            Some(field) => fields.push(field),
450            None => tracing::warn!(
451                "GraphQL: {}.{} declares the relationship \"{}\" with no columns to \
452                 join on; it is left out",
453                table.schema,
454                table.name,
455                entry.name
456            ),
457        }
458    }
459
460    // A computed relationship is declared by `add_computed_field`, not by
461    // `create_object_relationship`, so it is not in this list and is not what
462    // the list is exhaustive about. Leaving it to reflection is what keeps
463    // `author.get_articles` on a table whose foreign keys are all declared.
464    let already: std::collections::HashSet<String> =
465        fields.iter().map(|field| field.name.clone()).collect();
466    for rel in reflected
467        .iter()
468        .filter(|rel| matches!(rel, Relationship::Computed { .. }))
469    {
470        let foreign = rel.foreign_table();
471        let Some(target_base) = base_names.get(&(foreign.schema.clone(), foreign.name.clone()))
472        else {
473            continue;
474        };
475        let mut field = RelationshipField::from_relationship_named(
476            rel,
477            target_base,
478            relationship_keys(rel)
479                .iter()
480                .find_map(|key| config.names.relationship(&table.schema, &table.name, key)),
481        );
482        if already.contains(&field.name) {
483            continue;
484        }
485        field.arguments = caller_arguments(rel, schema_cache);
486        fields.push(field);
487    }
488    fields
489}
490
491/// One relationship field for a declaration that maps column to column.
492///
493/// The join is the declaration's, the direction is the declaration's, and the
494/// constraint name is this server's -- nothing reads it back, and a mapped
495/// join has no constraint to borrow. `None` where the declaration names no
496/// columns to join on, which is not a join.
497pub(crate) fn mapped_relationship_field(
498    entry: &crate::names::DeclaredRelationship,
499    table: &Table,
500    schema_cache: &SchemaCache,
501    default_schema: &str,
502    target_base: &str,
503) -> Option<RelationshipField> {
504    use postrust_core::schema_cache::{Cardinality, Relationship};
505
506    let (schema, name) = entry.target(default_schema)?;
507    if entry.columns.is_empty() {
508        return None;
509    }
510    let foreign_table = postrust_core::api_request::QualifiedIdentifier::new(&schema, &name);
511    let here = table.qualified_identifier();
512    let columns: Vec<(String, String)> = entry
513        .columns
514        .iter()
515        .map(|(mine, theirs)| (mine.clone(), theirs.clone()))
516        .collect();
517    let constraint = format!("{}.{}", table.name, entry.name);
518    // Which side holds the key, which is what a nested insert needs and a
519    // column mapping does not say. `M2O` is "this row's column carries their
520    // key", so the related row is written first; `O2O { is_parent: true }` is
521    // the other way round, and is how `after_parent` is spelled here. A
522    // relationship to many is written after its parent either way.
523    let cardinality = match (entry.to_one, entry.after_parent) {
524        (true, false) => Cardinality::M2O {
525            constraint: constraint.clone(),
526            columns,
527        },
528        (true, true) => Cardinality::O2O {
529            constraint: constraint.clone(),
530            columns,
531            is_parent: true,
532        },
533        (false, _) => Cardinality::O2M {
534            constraint: constraint.clone(),
535            columns,
536        },
537    };
538    let rel = Relationship::ForeignKey {
539        is_self: here == foreign_table,
540        table: here,
541        foreign_table_is_view: schema_cache
542            .get_table(&foreign_table)
543            .is_some_and(|t| t.is_view),
544        foreign_table,
545        cardinality,
546        table_is_view: table.is_view,
547        constraint_name: constraint,
548    };
549    Some(RelationshipField::from_relationship_named(
550        &rel,
551        target_base,
552        Some(&entry.name),
553    ))
554}
555
556fn caller_arguments(
557    relationship: &postrust_core::schema_cache::Relationship,
558    schema_cache: &SchemaCache,
559) -> Vec<(String, String, bool)> {
560    use postrust_core::schema_cache::Relationship;
561    let Relationship::Computed {
562        function,
563        row_argument,
564        ..
565    } = relationship
566    else {
567        return Vec::new();
568    };
569    let Some(row_argument) = row_argument else {
570        return Vec::new();
571    };
572    schema_cache
573        .routines
574        .get(function)
575        .into_iter()
576        .flatten()
577        .find(|routine| routine.name == function.name)
578        .map(|routine| {
579            routine
580                .params
581                .iter()
582                .filter(|param| &param.name != row_argument)
583                .filter(|param| !is_session_argument(param))
584                .filter(|param| !param.name.is_empty())
585                .map(|param| (param.name.clone(), param.param_type.clone(), param.required))
586                .collect()
587        })
588        .unwrap_or_default()
589}
590
591/// What a computed *column*'s function takes from the caller.
592///
593/// The same rule as [`caller_arguments`], one field kind along: everything but
594/// the row, which is what makes it a field of this table, and the session,
595/// which is the server's to supply.
596pub(crate) fn computed_caller_arguments(
597    computed: &postrust_core::schema_cache::ComputedColumn,
598    schema_cache: &SchemaCache,
599) -> Vec<(String, String, bool)> {
600    let Some(row_argument) = &computed.row_argument else {
601        return Vec::new();
602    };
603    schema_cache
604        .routines
605        .get(&computed.function)
606        .into_iter()
607        .flatten()
608        .find(|routine| routine.name == computed.function.name)
609        .map(|routine| {
610            routine
611                .params
612                .iter()
613                .filter(|param| &param.name != row_argument)
614                .filter(|param| !is_session_argument(param))
615                .filter(|param| !param.name.is_empty())
616                .map(|param| (param.name.clone(), param.param_type.clone(), param.required))
617                .collect()
618        })
619        .unwrap_or_default()
620}
621
622/// Whether a parameter is the one Hasura fills from the session.
623///
624/// Recognised by name and type, which is the only thing the database records:
625/// Hasura writes `session_argument: hasura_session` into metadata, and every
626/// function that has one calls it that.
627fn is_session_argument(param: &postrust_core::schema_cache::RoutineParam) -> bool {
628    param.name == "hasura_session" && matches!(param.param_type.as_str(), "json" | "jsonb")
629}
630
631impl GeneratedSchema {
632    /// Get an object type by name.
633    pub fn get_object_type(&self, name: &str) -> Option<&TableObjectType> {
634        self.object_types.get(name)
635    }
636
637    /// Get query fields for a table.
638    pub fn get_query_field(&self, table_name: &str) -> Option<&QueryField> {
639        self.query_fields
640            .iter()
641            .find(|f| f.table_name == table_name)
642    }
643
644    /// Get mutation fields for a table.
645    pub fn get_mutation_fields(&self, table_name: &str) -> Vec<&MutationField> {
646        self.mutation_fields
647            .iter()
648            .filter(|f| f.table_name == table_name)
649            .collect()
650    }
651
652    /// Get relationship fields for a type.
653    pub fn get_relationship_fields(&self, type_name: &str) -> Option<&Vec<RelationshipField>> {
654        self.relationship_fields.get(type_name)
655    }
656
657    /// Get all table names.
658    pub fn table_names(&self) -> Vec<&str> {
659        self.object_types
660            .values()
661            .map(|t| t.table.name.as_str())
662            .collect()
663    }
664
665    /// Get all type names.
666    pub fn type_names(&self) -> Vec<&str> {
667        self.object_types.keys().map(|s| s.as_str()).collect()
668    }
669}
670
671/// A query field for a table (e.g., users, userByPk).
672#[derive(Debug, Clone)]
673pub struct QueryField {
674    /// Field name (e.g., "users")
675    pub name: String,
676    /// Table name
677    pub table_name: String,
678    /// Schema the table lives in
679    pub schema_name: String,
680    /// GraphQL object type name (e.g., "Users")
681    pub type_name: String,
682    /// GraphQL return type
683    pub return_type: String,
684    /// Whether this returns a list
685    pub is_list: bool,
686    /// Whether this is a "by PK" query
687    pub is_by_pk: bool,
688    /// Primary key columns, as `(column name, PostgreSQL type)`.
689    ///
690    /// Populated for by-PK queries so the resolver can filter on the table's
691    /// actual key rather than assuming a column called `id`. Empty for list
692    /// queries.
693    pub pk_columns: Vec<(String, String)>,
694    /// Field description
695    pub description: Option<String>,
696    /// The name given to this table's aggregate root, if one was.
697    ///
698    /// The aggregate field is built beside the list field rather than being a
699    /// `QueryField` of its own, so the name it was given rides along with the
700    /// list field that spawns it.
701    pub aggregate_name: Option<String>,
702    /// The description given to that aggregate root, if one was. `Some("")`
703    /// means metadata said it has none.
704    pub aggregate_description: Option<String>,
705    /// Whether the aggregate root is built at all.
706    ///
707    /// The one thing a role's view of the schema cache cannot say. Dropping a
708    /// table or a column is a fact about the database as this role sees it;
709    /// whether that role may *count* the rows it can already read is a fact
710    /// about the permission, and Hasura keeps the two apart because counting
711    /// rows you cannot see is a way of seeing them. True unless a select
712    /// permission says otherwise, which is what leaves an unconfigured server
713    /// exactly as it was.
714    pub aggregates: bool,
715    /// Whether the rows themselves can be asked for.
716    ///
717    /// False for a table a role may count and not read: Hasura's way of
718    /// granting "how many" without granting "which" is a select permission
719    /// naming no columns, and such a table has an aggregate root and no row
720    /// type to hang a list root on. The field is still carried so that the
721    /// aggregate root beside it is built.
722    pub rows: bool,
723}
724
725impl QueryField {
726    /// Create a list query field (e.g., users), named after the table.
727    pub fn list(table: &Table) -> Self {
728        Self::list_named(table, &table.name)
729    }
730
731    /// Create a list query field using an explicit base name.
732    pub fn list_named(table: &Table, base_name: &str) -> Self {
733        let type_name = base_name.to_string();
734        let name = base_name.to_string();
735
736        Self {
737            name,
738            table_name: table.name.clone(),
739            schema_name: table.schema.clone(),
740            type_name: type_name.clone(),
741            return_type: format!("[{}!]!", type_name),
742            is_list: true,
743            is_by_pk: false,
744            pk_columns: Vec::new(),
745            description: Some(format!("fetch data from the table: \"{}\"", table.name)),
746            aggregate_name: None,
747            aggregate_description: None,
748            aggregates: true,
749            rows: true,
750        }
751    }
752
753    /// Create a by-PK query field (e.g., userByPk), named after the table.
754    pub fn by_pk(table: &Table) -> Option<Self> {
755        Self::by_pk_named(table, &table.name)
756    }
757
758    /// Create a by-PK query field using an explicit base name.
759    pub fn by_pk_named(table: &Table, base_name: &str) -> Option<Self> {
760        if !has_whole_key(table) {
761            return None;
762        }
763
764        let type_name = base_name.to_string();
765        let field_name = format!("{}_by_pk", base_name);
766
767        // Carry the key columns and their types so the resolver can filter on
768        // the real primary key.
769        let pk_columns = pk_columns_of(table);
770
771        Some(Self {
772            name: field_name,
773            table_name: table.name.clone(),
774            schema_name: table.schema.clone(),
775            type_name: type_name.clone(),
776            return_type: type_name,
777            is_list: false,
778            is_by_pk: true,
779            pk_columns,
780            description: Some(format!(
781                "fetch data from the table: \"{}\" using primary key columns",
782                table.name
783            )),
784            aggregate_name: None,
785            aggregate_description: None,
786            aggregates: true,
787            rows: true,
788        })
789    }
790}
791
792/// A mutation field for a table.
793#[derive(Debug, Clone)]
794pub struct MutationField {
795    /// Field name (e.g., "insertUsers")
796    pub name: String,
797    /// Table name
798    pub table_name: String,
799    /// Schema the table lives in
800    pub schema_name: String,
801    /// Mutation type
802    pub mutation_type: MutationType,
803    /// Primary key columns, as `(column name, PostgreSQL type)`.
804    ///
805    /// Populated for by-PK mutations so the resolver can target the row by its
806    /// key. Empty for bulk mutations.
807    pub pk_columns: Vec<(String, String)>,
808    /// GraphQL return type
809    pub return_type: String,
810    /// Field description
811    pub description: Option<String>,
812}
813
814/// Types of mutations.
815#[derive(Debug, Clone, Copy, PartialEq, Eq)]
816pub enum MutationType {
817    /// Insert multiple records
818    Insert,
819    /// Insert a single record
820    InsertOne,
821    /// Update records matching a filter
822    Update,
823    /// Update a single record by PK
824    UpdateByPk,
825    /// Several updates, each with its own filter, in one transaction
826    UpdateMany,
827    /// Delete records matching a filter
828    Delete,
829    /// Delete a single record by PK
830    DeleteByPk,
831}
832
833impl MutationField {
834    /// Create insert mutation fields for a table.
835    pub fn insert_fields(table: &Table) -> Vec<Self> {
836        Self::insert_fields_named(table, &table.name)
837    }
838
839    /// As [`Self::insert_fields`], with an explicit base name for the generated
840    /// field and type names.
841    pub fn insert_fields_named(table: &Table, base_name: &str) -> Vec<Self> {
842        if !is_insertable(table) {
843            return vec![];
844        }
845
846        let type_name = base_name.to_string();
847
848        let mut fields = vec![];
849
850        // insert_users (batch insert)
851        let name = format!("insert_{}", base_name);
852        fields.push(Self {
853            name,
854            table_name: table.name.clone(),
855            schema_name: table.schema.clone(),
856            mutation_type: MutationType::Insert,
857            pk_columns: Vec::new(),
858            return_type: format!("{}_mutation_response", type_name),
859            description: Some(format!("insert data into the table: \"{}\"", table.name)),
860        });
861
862        // insert_user_one (single insert)
863        let name = format!("insert_{}_one", base_name);
864        fields.push(Self {
865            name,
866            table_name: table.name.clone(),
867            schema_name: table.schema.clone(),
868            mutation_type: MutationType::InsertOne,
869            pk_columns: Vec::new(),
870            return_type: type_name.clone(),
871            description: Some(format!(
872                "insert a single row into the table: \"{}\"",
873                table.name
874            )),
875        });
876
877        fields
878    }
879
880    /// Create update mutation fields for a table.
881    pub fn update_fields(table: &Table) -> Vec<Self> {
882        Self::update_fields_named(table, &table.name)
883    }
884
885    /// As [`Self::update_fields`], with an explicit base name for the generated
886    /// field and type names.
887    pub fn update_fields_named(table: &Table, base_name: &str) -> Vec<Self> {
888        if !is_updatable(table) {
889            return vec![];
890        }
891
892        let type_name = base_name.to_string();
893
894        let mut fields = vec![];
895
896        // update_users (batch update)
897        let name = format!("update_{}", base_name);
898        fields.push(Self {
899            name,
900            table_name: table.name.clone(),
901            schema_name: table.schema.clone(),
902            mutation_type: MutationType::Update,
903            pk_columns: Vec::new(),
904            return_type: format!("{}_mutation_response", type_name),
905            description: Some(format!("update data of the table: \"{}\"", table.name)),
906        });
907
908        // update_users_many (several filters, each with its own values)
909        let name = format!("update_{}_many", base_name);
910        fields.push(Self {
911            name,
912            table_name: table.name.clone(),
913            schema_name: table.schema.clone(),
914            mutation_type: MutationType::UpdateMany,
915            pk_columns: Vec::new(),
916            return_type: format!("[{}_mutation_response]", type_name),
917            description: Some(format!(
918                "update multiples rows of table: \"{}\"",
919                table.name
920            )),
921        });
922
923        // update_user_by_pk (single update by PK)
924        if has_whole_key(table) {
925            let name = format!("update_{}_by_pk", base_name);
926            fields.push(Self {
927                name,
928                table_name: table.name.clone(),
929                schema_name: table.schema.clone(),
930                mutation_type: MutationType::UpdateByPk,
931                pk_columns: pk_columns_of(table),
932                return_type: type_name,
933                description: Some(format!(
934                    "update single row of the table: \"{}\"",
935                    table.name
936                )),
937            });
938        }
939
940        fields
941    }
942
943    /// Create delete mutation fields for a table.
944    pub fn delete_fields(table: &Table) -> Vec<Self> {
945        Self::delete_fields_named(table, &table.name)
946    }
947
948    /// As [`Self::delete_fields`], with an explicit base name for the generated
949    /// field and type names.
950    pub fn delete_fields_named(table: &Table, base_name: &str) -> Vec<Self> {
951        if !is_deletable(table) {
952            return vec![];
953        }
954
955        let type_name = base_name.to_string();
956
957        let mut fields = vec![];
958
959        // delete_users (batch delete)
960        let name = format!("delete_{}", base_name);
961        fields.push(Self {
962            name,
963            table_name: table.name.clone(),
964            schema_name: table.schema.clone(),
965            mutation_type: MutationType::Delete,
966            pk_columns: Vec::new(),
967            return_type: format!("{}_mutation_response", type_name),
968            description: Some(format!("delete data from the table: \"{}\"", table.name)),
969        });
970
971        // delete_user_by_pk (single delete by PK)
972        if has_whole_key(table) {
973            let name = format!("delete_{}_by_pk", base_name);
974            fields.push(Self {
975                name,
976                table_name: table.name.clone(),
977                schema_name: table.schema.clone(),
978                mutation_type: MutationType::DeleteByPk,
979                pk_columns: pk_columns_of(table),
980                return_type: type_name,
981                description: Some(format!(
982                    "delete single row from the table: \"{}\"",
983                    table.name
984                )),
985            });
986        }
987
988        fields
989    }
990}
991
992/// Build a GraphQL schema from a schema cache.
993pub fn build_schema(schema_cache: &SchemaCache, config: &SchemaConfig) -> GeneratedSchema {
994    let mut object_types = HashMap::new();
995    let mut query_fields = Vec::new();
996    let mut mutation_fields = Vec::new();
997    let mut relationship_fields = HashMap::new();
998
999    // Tables are visited in a stable order: the cache is a hash map, and any
1000    // name disambiguation below must not shift between restarts.
1001    let mut tables: Vec<&Table> = schema_cache
1002        .tables
1003        .values()
1004        .filter(|table| config.is_schema_exposed(&table.schema))
1005        .collect();
1006    tables.sort_by(|a, b| (&a.schema, &a.name).cmp(&(&b.schema, &b.name)));
1007
1008    // Base names already carry the schema for non-default schemas, so a clash
1009    // here needs contrived naming (a `public.api_users` table alongside
1010    // `api.users`). Resolve it with a numeric suffix rather than letting one
1011    // table overwrite the other.
1012    let mut used_base_names: HashMap<String, u32> = HashMap::new();
1013    // (schema, table) -> resolved base name, needed when naming relationship
1014    // targets.
1015    let mut base_names: HashMap<(String, String), String> = HashMap::new();
1016
1017    for table in &tables {
1018        let preferred = base_name_for(table, config);
1019        let base_name = match used_base_names.get_mut(&preferred) {
1020            None => {
1021                used_base_names.insert(preferred.clone(), 1);
1022                preferred
1023            }
1024            Some(count) => {
1025                *count += 1;
1026                let disambiguated = format!("{}_{}", preferred, count);
1027                tracing::warn!(
1028                    "GraphQL name collision: {}.{} would generate the same names as \
1029                     an earlier table; exposing it as \"{}\" instead",
1030                    table.schema,
1031                    table.name,
1032                    disambiguated
1033                );
1034                disambiguated
1035            }
1036        };
1037
1038        base_names.insert(
1039            (table.schema.clone(), table.name.clone()),
1040            base_name.clone(),
1041        );
1042    }
1043
1044    // The tables this role may read, by base name. Needed after the loop, by
1045    // anything that would answer with a row of one.
1046    let mut readable_bases: HashSet<String> = HashSet::new();
1047
1048    for table in &tables {
1049        let base_name = base_names
1050            .get(&(table.schema.clone(), table.name.clone()))
1051            .expect("every visited table has a resolved base name")
1052            .clone();
1053
1054        // Create object type
1055        let mut obj_type = TableObjectType::from_table_named(table, &base_name, &config.names);
1056        // Whether this role can read the table at all. A table it may only
1057        // write has no GraphQL type -- a type with no fields is not a legal
1058        // one -- so nothing that would return that type is generated: no query
1059        // root, no `_by_pk`, no `insert_one`. The bulk write stays, and
1060        // answers with the count alone.
1061        //
1062        // Asked of the permission rather than of the fields, because the
1063        // fields are not split into the readable and the writable half until
1064        // every rewrite below has been applied to both.
1065        let readable = config.role.as_deref().is_none_or(|role| {
1066            config
1067                .names
1068                .permissions(&table.schema, &table.name, role)
1069                .is_none_or(|granted| crate::role::reads_anything(granted, table))
1070        });
1071        if readable {
1072            readable_bases.insert(base_name.clone());
1073        }
1074        // What each computed field takes from the caller. Read from the
1075        // routine, for the reason `caller_arguments` gives.
1076        for field in &mut obj_type.fields {
1077            let Some(function) = config
1078                .names
1079                .computed_source(&table.schema, &table.name, &field.name)
1080                .or(Some(field.name.as_str()))
1081            else {
1082                continue;
1083            };
1084            let Some(computed) = table.get_computed_column(function) else {
1085                continue;
1086            };
1087            field.arguments = computed_caller_arguments(computed, schema_cache);
1088        }
1089        let type_name = obj_type.name.clone();
1090
1091        // Add query fields. Each root may be named separately -- Hasura names
1092        // them one at a time, and a set that does not agree on a base name has
1093        // nowhere else to be written down.
1094        // A root may be given a name, a description, or both. An empty
1095        // description is one that was given and is empty: metadata said the
1096        // field has none.
1097        let rename = |field: &mut QueryField, kind: &str| {
1098            if let Some(given) = config.names.root(&table.schema, &table.name, kind) {
1099                field.name = given.to_string();
1100            }
1101            if let Some(comment) = config.names.root_comment(&table.schema, &table.name, kind) {
1102                field.description = Some(comment.to_string()).filter(|c| !c.is_empty());
1103            }
1104        };
1105        let mut list = QueryField::list_named(table, &base_name);
1106        rename(&mut list, "select");
1107        // The aggregate root is generated from the type name rather than being
1108        // a `QueryField` of its own, so its name travels with the list field
1109        // that spawns it.
1110        list.aggregate_name = config
1111            .names
1112            .root(&table.schema, &table.name, "select_aggregate")
1113            .map(str::to_string);
1114        list.aggregate_description = config
1115            .names
1116            .root_comment(&table.schema, &table.name, "select_aggregate")
1117            .map(str::to_string);
1118        // Whether this role may count what it can read. Only asked where a
1119        // role is building its own schema; the unrestricted one aggregates
1120        // whatever the database will.
1121        if let Some(role) = &config.role {
1122            list.aggregates =
1123                crate::role::allows_aggregations(&config.names, &table.schema, &table.name, role);
1124        }
1125        // A table with no readable field still gets its list entry when the
1126        // role may count it: the aggregate root is built beside that entry
1127        // rather than being one of its own, and `rows` is what says the list
1128        // root itself is not there. By key there is nothing to answer with at
1129        // all.
1130        list.rows = readable;
1131        if readable || list.aggregates {
1132            query_fields.push(list);
1133        }
1134        if readable {
1135            if let Some(mut by_pk) = QueryField::by_pk_named(table, &base_name) {
1136                rename(&mut by_pk, "select_by_pk");
1137                query_fields.push(by_pk);
1138            }
1139        }
1140
1141        // Add mutation fields if enabled
1142        if config.enable_mutations {
1143            let mut fields: Vec<MutationField> = Vec::new();
1144            fields.extend(MutationField::insert_fields_named(table, &base_name));
1145            fields.extend(MutationField::update_fields_named(table, &base_name));
1146            fields.extend(MutationField::delete_fields_named(table, &base_name));
1147            // The three that answer with a row rather than with a count.
1148            if !readable {
1149                fields.retain(|field| {
1150                    !matches!(
1151                        field.mutation_type,
1152                        MutationType::InsertOne
1153                            | MutationType::UpdateByPk
1154                            | MutationType::DeleteByPk
1155                    )
1156                });
1157            }
1158            for field in &mut fields {
1159                let kind = match field.mutation_type {
1160                    MutationType::Insert => "insert",
1161                    MutationType::InsertOne => "insert_one",
1162                    MutationType::Update => "update",
1163                    MutationType::UpdateByPk => "update_by_pk",
1164                    MutationType::UpdateMany => "update_many",
1165                    MutationType::Delete => "delete",
1166                    MutationType::DeleteByPk => "delete_by_pk",
1167                };
1168                if let Some(given) = config.names.root(&table.schema, &table.name, kind) {
1169                    field.name = given.to_string();
1170                }
1171                if let Some(comment) = config.names.root_comment(&table.schema, &table.name, kind) {
1172                    field.description = Some(comment.to_string()).filter(|c| !c.is_empty());
1173                }
1174            }
1175            mutation_fields.extend(fields);
1176        }
1177
1178        // Add relationship fields. Where metadata declares them, they are
1179        // what the table has; otherwise reflection offers one per foreign
1180        // key, which is what a document saying nothing about a table has
1181        // always meant. See `TableNames::declared_relationships`.
1182        let rels = match config
1183            .names
1184            .declared_relationships(&table.schema, &table.name)
1185        {
1186            Some(declared) => declared_fields(declared, table, schema_cache, config, &base_names),
1187            None => reflected_fields(table, schema_cache, config, &base_names),
1188        };
1189
1190        if !rels.is_empty() {
1191            relationship_fields.insert(type_name.clone(), rels);
1192        }
1193
1194        object_types.insert(type_name, obj_type);
1195    }
1196
1197    // A table marked as a set of allowed values becomes a GraphQL enum, and
1198    // every column with a foreign key to it is typed as that enum rather than
1199    // as text. The values are rows, so they were read at startup rather than
1200    // reflected.
1201    let mut enum_types: HashMap<String, Vec<(String, Option<String>)>> = HashMap::new();
1202    let mut enum_type_of: HashMap<(String, String), String> = HashMap::new();
1203
1204    for (schema_name, table_name) in config.names.enum_tables() {
1205        // A role that may not read the table's rows still sees the enum. The
1206        // values are a *type*, and a column typed by it is readable -- Hasura
1207        // publishes `colors_enum` to a role granted nothing on `colors`, and
1208        // the query `where: {favorite_color: {_eq: red}}` is one it answers.
1209        // So the name is derived rather than looked up, since a table the
1210        // role cannot read has no entry to look up.
1211        let base = match base_names.get(&(schema_name.clone(), table_name.clone())) {
1212            Some(found) => found.clone(),
1213            None => config
1214                .names
1215                .base_name(&schema_name, &table_name)
1216                .map(str::to_string)
1217                .unwrap_or_else(|| match schema_name == config.default_schema() {
1218                    true => table_name.clone(),
1219                    false => format!("{}_{}", schema_name, table_name),
1220                }),
1221        };
1222        let key = format!("{}.{}", schema_name, table_name);
1223        let members: Vec<(String, Option<String>)> = config
1224            .enum_values
1225            .get(&key)
1226            .map(|values| {
1227                values
1228                    .iter()
1229                    .filter(|(value, _)| is_graphql_name(value))
1230                    .cloned()
1231                    .collect()
1232            })
1233            .unwrap_or_default();
1234
1235        if members.is_empty() {
1236            // No rows, or none whose value is a legal GraphQL name. An empty
1237            // enum is not a legal type, so the table stays an ordinary one.
1238            tracing::warn!(
1239                "{}.{} is marked as an enumeration but has no values that can name an \
1240                 enum member; exposing it as an ordinary table",
1241                schema_name,
1242                table_name
1243            );
1244            continue;
1245        }
1246
1247        let type_name = format!("{}_enum", base);
1248        enum_type_of.insert((schema_name.clone(), table_name.clone()), type_name.clone());
1249        enum_types.insert(type_name, members);
1250    }
1251
1252    // Retype the columns that point at one.
1253    //
1254    // Read from the catalogue's foreign keys rather than from the
1255    // relationships this schema exposes, which are two different questions: a
1256    // relationship *to* an enum table is never exposed at all -- see just
1257    // below -- and a column's type has nothing to do with whether the row at
1258    // the other end can be traversed to. Keying one on the other meant a table
1259    // whose relationships metadata had declared away lost its enum columns'
1260    // type with them.
1261    if !enum_type_of.is_empty() {
1262        for table in &tables {
1263            let Some(type_name) = base_names.get(&(table.schema.clone(), table.name.clone()))
1264            else {
1265                continue;
1266            };
1267            let found = schema_cache
1268                .get_relationships(&table.qualified_identifier(), &table.schema)
1269                .map(|found| found.to_vec())
1270                .unwrap_or_default();
1271            for relationship in &found {
1272                if !relationship.is_to_one() {
1273                    continue;
1274                }
1275                let foreign = relationship.foreign_table();
1276                let Some(enum_type) =
1277                    enum_type_of.get(&(foreign.schema.clone(), foreign.name.clone()))
1278                else {
1279                    continue;
1280                };
1281                let Some(column) = relationship.single_local_column() else {
1282                    continue;
1283                };
1284                if let Some(object) = object_types.get_mut(type_name) {
1285                    // Under the name the column is exposed as, which is not
1286                    // always its own.
1287                    let exposed = config
1288                        .names
1289                        .column(&object.table.schema, &object.table.name, column)
1290                        .unwrap_or(column)
1291                        .to_string();
1292                    for field in &mut object.fields {
1293                        if field.name == exposed {
1294                            field.graphql_type =
1295                                crate::types::GraphQLType::Custom(enum_type.clone());
1296                        }
1297                    }
1298                }
1299            }
1300        }
1301    }
1302
1303    // A table marked as a set of allowed values is a set of *values*: the
1304    // column typed as the enum is the whole of what a client needs, and there
1305    // is nothing at the other end of a relationship to it worth traversing to.
1306    // Hasura exposes none, and a stray `user { color { ... } }` beside
1307    // `favorite_color` is a second spelling of the same fact.
1308    if !enum_type_of.is_empty() {
1309        for relationships in relationship_fields.values_mut() {
1310            relationships.retain(|relationship| {
1311                let foreign = relationship.relationship.foreign_table();
1312                !enum_type_of.contains_key(&(foreign.schema.clone(), foreign.name.clone()))
1313            });
1314        }
1315    }
1316
1317    // Split each type's fields into the half that may be read and the half
1318    // that may be written. Last, so that everything above -- the computed
1319    // fields' arguments, the retyping of an enum table's columns -- has been
1320    // applied to the whole list before either half is taken out of it.
1321    //
1322    // A role may be granted a column to set that it is not granted to see, so
1323    // the cache carries the union and this is where the two part company. See
1324    // [`crate::role`].
1325    for object in object_types.values_mut() {
1326        // Taken unconditionally, even where no role narrows anything: the
1327        // write inputs are built from this list, and leaving it as it was when
1328        // the type was first made would leave it behind every rewrite since.
1329        object.writable_fields = object.fields.clone();
1330        if let Some(role) = &config.role {
1331            let (schema_name, table_name) =
1332                (object.table.schema.clone(), object.table.name.clone());
1333            let Some(granted) = config.names.permissions(&schema_name, &table_name, role) else {
1334                continue;
1335            };
1336            let table = object.table.clone();
1337            object.fields.retain(|field| {
1338                let column = config
1339                    .names
1340                    .column_source(&schema_name, &table_name, &field.name)
1341                    .unwrap_or(&field.name);
1342                match table.get_column(column) {
1343                    // A computed field rather than a column, and those were
1344                    // reduced to the granted ones with the cache.
1345                    None => true,
1346                    Some(_) => granted
1347                        .select
1348                        .as_ref()
1349                        .is_some_and(|select| select.columns.allows(column)),
1350                }
1351            });
1352        }
1353    }
1354
1355    // Functions that answer with rows of a table it already exposes. One
1356    // returning anything else has no type here to return: a scalar-returning
1357    // function is not a root field, it is an RPC, which the REST surface
1358    // already offers.
1359    let mut function_fields = Vec::new();
1360    for routines in schema_cache.routines.values() {
1361        for routine in routines {
1362            if !config.is_schema_exposed(&routine.schema) || routine.is_procedure {
1363                continue;
1364            }
1365            let postrust_core::schema_cache::RetType::SetOf(returned) = &routine.return_type else {
1366                continue;
1367            };
1368            // A function taking a table's row is that table's computed field,
1369            // not a root field: `fetch_articles(author_row author)` is asked
1370            // of an author, and a row type is not something a client can send
1371            // as an argument. It is already exposed where it belongs.
1372            if routine.params.iter().any(|param| {
1373                let (_, named) = type_name_of(&param.param_type);
1374                base_names.keys().any(|(_, table)| table == &named)
1375            }) {
1376                continue;
1377            }
1378            // `SETOF <table>`, where the table is one this schema exposes. The
1379            // catalogue names the returned type without a schema, so the
1380            // function's own schema is tried first -- a function and the table
1381            // it returns usually live together, and two schemas may both have
1382            // a table of that name.
1383            let (returned_schema, returned_table) = type_name_of(returned);
1384            let found = base_names
1385                .iter()
1386                .find(|((schema, table), _)| {
1387                    table == &returned_table
1388                        && schema == returned_schema.as_ref().unwrap_or(&routine.schema)
1389                })
1390                .or_else(|| {
1391                    base_names
1392                        .iter()
1393                        .find(|((_, table), _)| table == &returned_table)
1394                });
1395            let Some(((target_schema, target_table), base)) = found else {
1396                continue;
1397            };
1398            // A function returning rows of a table this role cannot read has
1399            // nowhere to put them: that table has no GraphQL type, and naming
1400            // one that was never registered is a schema that will not build.
1401            if !readable_bases.contains(base) {
1402                continue;
1403            }
1404            let target = (target_schema.clone(), target_table.clone());
1405
1406            // Placed by what PostgreSQL says it does, unless metadata said
1407            // otherwise: `track_function` with `configuration: {exposed_as:
1408            // query}` puts a VOLATILE function on the query root, which is a
1409            // decision a person made and no catalogue remembers.
1410            let volatile = match config.names.exposed_as(&routine.schema, &routine.name) {
1411                Some("query") => false,
1412                Some("mutation") => true,
1413                _ => matches!(
1414                    routine.volatility,
1415                    postrust_core::schema_cache::FuncVolatility::Volatile
1416                ),
1417            };
1418            // A mutation a role was never granted. Hasura infers a query
1419            // function's permission from the select permission on the table it
1420            // returns -- which is the check just above -- and infers nothing
1421            // for a mutation: reading a table is not permission to change it,
1422            // so the role has to be named. A role granted no mutation at all
1423            // then has no mutation root, and a request for one is answered
1424            // `no mutations exist` rather than by a field that is missing.
1425            if volatile {
1426                if let Some(role) = config.role.as_deref() {
1427                    if !config
1428                        .names
1429                        .function_grants(&routine.schema, &routine.name, role)
1430                    {
1431                        continue;
1432                    }
1433                }
1434            }
1435
1436            function_fields.push(FunctionField {
1437                name: routine.name.clone(),
1438                schema_name: routine.schema.clone(),
1439                function_name: routine.name.clone(),
1440                returns: base.clone(),
1441                returns_table: target,
1442                arguments: routine
1443                    .params
1444                    .iter()
1445                    .filter(|param| !param.name.is_empty())
1446                    .filter(|param| !is_session_argument(param))
1447                    .map(|param| (param.name.clone(), param.param_type.clone(), param.required))
1448                    .collect(),
1449                session_argument: routine
1450                    .params
1451                    .iter()
1452                    .find(|param| is_session_argument(param))
1453                    .map(|param| param.name.clone()),
1454                volatile,
1455                description: routine.description.clone(),
1456            });
1457        }
1458    }
1459    // Sorted by name, and then by signature so that which overload survives
1460    // the deduplication below does not depend on hash order.
1461    function_fields.sort_by(|a, b| {
1462        a.name.cmp(&b.name).then_with(|| {
1463            a.arguments
1464                .len()
1465                .cmp(&b.arguments.len())
1466                .then_with(|| a.argument_signature().cmp(&b.argument_signature()))
1467        })
1468    });
1469
1470    // PostgreSQL overloads functions; GraphQL does not overload fields. Two
1471    // `SETOF` overloads of one name therefore want the same root field, and
1472    // `dynamic::Object::field` *panics* on the second -- so this is not a
1473    // cosmetic choice, it is the difference between starting and not. A schema
1474    // with `f(a int)` and `f(a int, b text)` both returning `SETOF t` crashed
1475    // the server at boot.
1476    //
1477    // The one taking fewest arguments is kept, being the one most callers can
1478    // call, and every other is named in a warning: a function that is not
1479    // exposed should say so rather than be discovered missing.
1480    let mut seen: HashSet<String> = HashSet::new();
1481    function_fields.retain(|function| {
1482        if seen.insert(function.name.clone()) {
1483            return true;
1484        }
1485        tracing::warn!(
1486            "not exposing {}.{}({}) -- another overload of {} is already a root \
1487             field, and GraphQL has no overloading",
1488            function.schema_name,
1489            function.function_name,
1490            function.argument_signature(),
1491            function.name,
1492        );
1493        false
1494    });
1495
1496    GeneratedSchema {
1497        object_types,
1498        query_fields,
1499        mutation_fields,
1500        relationship_fields,
1501        enum_types,
1502        function_fields,
1503    }
1504}
1505
1506/// Whether a value can name a GraphQL enum member.
1507///
1508/// `[_A-Za-z][_0-9A-Za-z]*`, which is what the specification allows. A value
1509/// that cannot is left out rather than mangled into one: a client asking for
1510/// `light blue` should be told there is no such member, not handed
1511/// `light_blue` and left to wonder which row it means.
1512fn is_graphql_name(value: &str) -> bool {
1513    let mut chars = value.chars();
1514    match chars.next() {
1515        Some(first) if first == '_' || first.is_ascii_alphabetic() => {}
1516        _ => return false,
1517    }
1518    chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
1519}
1520
1521#[cfg(test)]
1522mod tests {
1523    use super::*;
1524    use indexmap::IndexMap;
1525    use postrust_core::schema_cache::Column;
1526    use pretty_assertions::assert_eq;
1527
1528    fn create_test_table(name: &str, insertable: bool, updatable: bool, deletable: bool) -> Table {
1529        let mut columns = IndexMap::new();
1530        columns.insert(
1531            "id".into(),
1532            Column {
1533                name: "id".into(),
1534                description: None,
1535                nullable: false,
1536                data_type: "integer".into(),
1537                nominal_type: "int4".into(),
1538                max_len: None,
1539                default: Some("nextval('id_seq')".into()),
1540                enum_values: vec![],
1541                is_pk: true,
1542                position: 1,
1543                always_generated: false,
1544                domain_type: None,
1545            },
1546        );
1547        columns.insert(
1548            "name".into(),
1549            Column {
1550                name: "name".into(),
1551                description: None,
1552                nullable: false,
1553                data_type: "text".into(),
1554                nominal_type: "text".into(),
1555                max_len: None,
1556                default: None,
1557                enum_values: vec![],
1558                is_pk: false,
1559                position: 2,
1560                always_generated: false,
1561                domain_type: None,
1562            },
1563        );
1564
1565        Table {
1566            schema: "public".into(),
1567            name: name.into(),
1568            description: None,
1569            is_view: false,
1570            insertable,
1571            updatable,
1572            deletable,
1573            pk_cols: vec!["id".into()],
1574            unique_constraints: Vec::new(),
1575            columns,
1576            computed_columns: Default::default(),
1577            is_partitioned: false,
1578        }
1579    }
1580
1581    fn create_test_schema_cache() -> SchemaCache {
1582        use std::collections::{HashMap, HashSet};
1583
1584        let mut tables = HashMap::new();
1585
1586        let users = create_test_table("users", true, true, true);
1587        let posts = create_test_table("posts", true, true, true);
1588        let comments = create_test_table("comments", true, false, false);
1589
1590        tables.insert(users.qualified_identifier(), users);
1591        tables.insert(posts.qualified_identifier(), posts);
1592        tables.insert(comments.qualified_identifier(), comments);
1593
1594        SchemaCache {
1595            tables,
1596            relationships: HashMap::new(),
1597            routines: HashMap::new(),
1598            timezones: HashSet::new(),
1599            media_handlers: HashMap::new(),
1600            pg_version: 150000,
1601            representations: Default::default(),
1602        }
1603    }
1604
1605    /// A `SETOF users` function, with the given argument names and types.
1606    fn setof_users_routine(
1607        name: &str,
1608        args: &[(&str, &str)],
1609    ) -> postrust_core::schema_cache::Routine {
1610        use postrust_core::schema_cache::{FuncVolatility, RetType, Routine, RoutineParam};
1611        Routine {
1612            schema: "public".into(),
1613            name: name.into(),
1614            description: None,
1615            params: args
1616                .iter()
1617                .map(|(pname, ptype)| RoutineParam {
1618                    name: (*pname).into(),
1619                    param_type: (*ptype).into(),
1620                    type_max_length: (*ptype).into(),
1621                    required: true,
1622                    variadic: false,
1623                })
1624                .collect(),
1625            return_type: RetType::SetOf("users".into()),
1626            returns_composite: true,
1627            volatility: FuncVolatility::Stable,
1628            has_variadic: false,
1629            isolation_level: None,
1630            settings: Vec::new(),
1631            is_procedure: false,
1632            media_type: None,
1633            media_base_type: None,
1634            output_columns: Vec::new(),
1635        }
1636    }
1637
1638    /// Two `SETOF` overloads of one name want one root field, and
1639    /// `dynamic::Object::field` panics on the second -- so this is the
1640    /// difference between the server starting and not. Reproduced from
1641    /// PostgREST's `overloaded_default` fixture, which crashed it at boot.
1642    #[test]
1643    fn overloads_of_one_function_become_a_single_root_field() {
1644        let mut cache = create_test_schema_cache();
1645        let one = setof_users_routine("overloaded", &[("a", "integer")]);
1646        let two = setof_users_routine("overloaded", &[("a", "integer"), ("b", "text")]);
1647        cache
1648            .routines
1649            .insert(one.qualified_identifier(), vec![two, one]);
1650
1651        let generated = build_schema(&cache, &SchemaConfig::default());
1652        let named: Vec<&str> = generated
1653            .function_fields
1654            .iter()
1655            .filter(|f| f.name == "overloaded")
1656            .map(|f| f.name.as_str())
1657            .collect();
1658        assert_eq!(named.len(), 1, "exactly one root field per function name");
1659    }
1660
1661    /// And which overload survives cannot depend on hash order, or the schema
1662    /// differs between two runs of the same binary against one database.
1663    #[test]
1664    fn the_surviving_overload_is_the_one_taking_fewest_arguments() {
1665        let long = setof_users_routine("overloaded", &[("a", "integer"), ("b", "text")]);
1666        let short = setof_users_routine("overloaded", &[("a", "integer")]);
1667
1668        for order in [vec![long.clone(), short.clone()], vec![short, long]] {
1669            let mut cache = create_test_schema_cache();
1670            cache
1671                .routines
1672                .insert(order[0].qualified_identifier(), order);
1673            let generated = build_schema(&cache, &SchemaConfig::default());
1674            let kept = generated
1675                .function_fields
1676                .iter()
1677                .find(|f| f.name == "overloaded")
1678                .expect("the function is exposed");
1679            assert_eq!(kept.argument_signature(), "a integer");
1680        }
1681    }
1682
1683    // ============================================================================
1684    // SchemaConfig Tests
1685    // ============================================================================
1686
1687    #[test]
1688    fn test_schema_config_default() {
1689        let config = SchemaConfig::default();
1690        assert!(config.is_schema_exposed("public"));
1691        assert!(!config.is_schema_exposed("private"));
1692        assert!(config.enable_mutations);
1693        assert!(!config.enable_subscriptions);
1694    }
1695
1696    #[test]
1697    fn test_schema_config_with_schemas() {
1698        let config =
1699            SchemaConfig::new().with_schemas(vec!["api".to_string(), "public".to_string()]);
1700        assert!(config.is_schema_exposed("api"));
1701        assert!(config.is_schema_exposed("public"));
1702        assert!(!config.is_schema_exposed("private"));
1703    }
1704
1705    #[test]
1706    fn test_schema_config_mutations_disabled() {
1707        let config = SchemaConfig::new().with_mutations(false);
1708        assert!(!config.enable_mutations);
1709    }
1710
1711    // ============================================================================
1712    // QueryField Tests
1713    // ============================================================================
1714
1715    #[test]
1716    fn test_query_field_list() {
1717        let table = create_test_table("users", true, true, true);
1718        let field = QueryField::list(&table);
1719
1720        assert_eq!(field.name, "users");
1721        assert_eq!(field.return_type, "[users!]!");
1722        assert!(field.is_list);
1723        assert!(!field.is_by_pk);
1724    }
1725
1726    #[test]
1727    fn test_query_field_by_pk() {
1728        let table = create_test_table("users", true, true, true);
1729        let field = QueryField::by_pk(&table).unwrap();
1730
1731        assert_eq!(field.name, "users_by_pk");
1732        assert_eq!(field.return_type, "users");
1733        assert!(!field.is_list);
1734        assert!(field.is_by_pk);
1735    }
1736
1737    #[test]
1738    fn test_query_field_by_pk_no_pk() {
1739        let mut table = create_test_table("users", true, true, true);
1740        table.pk_cols = vec![];
1741        let field = QueryField::by_pk(&table);
1742
1743        assert!(field.is_none());
1744    }
1745
1746    // ============================================================================
1747    // MutationField Tests
1748    // ============================================================================
1749
1750    #[test]
1751    fn test_mutation_field_insert() {
1752        let table = create_test_table("users", true, true, true);
1753        let fields = MutationField::insert_fields(&table);
1754
1755        assert_eq!(fields.len(), 2);
1756        assert_eq!(fields[0].name, "insert_users");
1757        assert_eq!(fields[0].mutation_type, MutationType::Insert);
1758        assert_eq!(fields[1].name, "insert_users_one");
1759        assert_eq!(fields[1].mutation_type, MutationType::InsertOne);
1760    }
1761
1762    #[test]
1763    fn test_mutation_field_insert_not_insertable() {
1764        let table = create_test_table("users", false, true, true);
1765        let fields = MutationField::insert_fields(&table);
1766
1767        assert!(fields.is_empty());
1768    }
1769
1770    #[test]
1771    fn test_mutation_field_update() {
1772        let table = create_test_table("users", true, true, true);
1773        let fields = MutationField::update_fields(&table);
1774
1775        assert_eq!(fields.len(), 3);
1776        assert_eq!(fields[0].name, "update_users");
1777        assert_eq!(fields[0].mutation_type, MutationType::Update);
1778        assert_eq!(fields[1].name, "update_users_many");
1779        assert_eq!(fields[1].mutation_type, MutationType::UpdateMany);
1780        assert_eq!(fields[2].name, "update_users_by_pk");
1781        assert_eq!(fields[2].mutation_type, MutationType::UpdateByPk);
1782    }
1783
1784    #[test]
1785    fn test_mutation_field_update_not_updatable() {
1786        let table = create_test_table("users", true, false, true);
1787        let fields = MutationField::update_fields(&table);
1788
1789        assert!(fields.is_empty());
1790    }
1791
1792    #[test]
1793    fn test_mutation_field_delete() {
1794        let table = create_test_table("users", true, true, true);
1795        let fields = MutationField::delete_fields(&table);
1796
1797        assert_eq!(fields.len(), 2);
1798        assert_eq!(fields[0].name, "delete_users");
1799        assert_eq!(fields[0].mutation_type, MutationType::Delete);
1800        assert_eq!(fields[1].name, "delete_users_by_pk");
1801        assert_eq!(fields[1].mutation_type, MutationType::DeleteByPk);
1802    }
1803
1804    #[test]
1805    fn test_mutation_field_delete_not_deletable() {
1806        let table = create_test_table("users", true, true, false);
1807        let fields = MutationField::delete_fields(&table);
1808
1809        assert!(fields.is_empty());
1810    }
1811
1812    // ============================================================================
1813    // Singularize Tests
1814    // ============================================================================
1815
1816    // ============================================================================
1817    // Build Schema Tests
1818    // ============================================================================
1819
1820    #[test]
1821    fn test_build_schema_object_types() {
1822        let cache = create_test_schema_cache();
1823        let config = SchemaConfig::default();
1824        let schema = build_schema(&cache, &config);
1825
1826        assert_eq!(schema.object_types.len(), 3);
1827        assert!(schema.get_object_type("users").is_some());
1828        assert!(schema.get_object_type("posts").is_some());
1829        assert!(schema.get_object_type("comments").is_some());
1830    }
1831
1832    #[test]
1833    fn test_build_schema_query_fields() {
1834        let cache = create_test_schema_cache();
1835        let config = SchemaConfig::default();
1836        let schema = build_schema(&cache, &config);
1837
1838        // 3 tables * 2 (list + byPk) = 6 query fields
1839        assert_eq!(schema.query_fields.len(), 6);
1840
1841        // Check users query fields
1842        let users_field = schema.get_query_field("users").unwrap();
1843        assert_eq!(users_field.name, "users");
1844        assert!(users_field.is_list);
1845    }
1846
1847    #[test]
1848    fn test_build_schema_mutation_fields() {
1849        let cache = create_test_schema_cache();
1850        let config = SchemaConfig::default();
1851        let schema = build_schema(&cache, &config);
1852
1853        // users: 2 insert + 3 update + 2 delete = 7
1854        // posts: 2 insert + 3 update + 2 delete = 7
1855        // comments: 2 insert + 0 update + 0 delete = 2
1856        // Total: 16
1857        assert_eq!(schema.mutation_fields.len(), 16);
1858
1859        let users_mutations = schema.get_mutation_fields("users");
1860        assert_eq!(users_mutations.len(), 7);
1861    }
1862
1863    #[test]
1864    fn test_build_schema_mutations_disabled() {
1865        let cache = create_test_schema_cache();
1866        let config = SchemaConfig::new().with_mutations(false);
1867        let schema = build_schema(&cache, &config);
1868
1869        assert!(schema.mutation_fields.is_empty());
1870    }
1871
1872    #[test]
1873    fn test_build_schema_table_names() {
1874        let cache = create_test_schema_cache();
1875        let config = SchemaConfig::default();
1876        let schema = build_schema(&cache, &config);
1877
1878        let names = schema.table_names();
1879        assert_eq!(names.len(), 3);
1880        assert!(names.contains(&"users"));
1881        assert!(names.contains(&"posts"));
1882        assert!(names.contains(&"comments"));
1883    }
1884
1885    #[test]
1886    fn test_build_schema_type_names() {
1887        let cache = create_test_schema_cache();
1888        let config = SchemaConfig::default();
1889        let schema = build_schema(&cache, &config);
1890
1891        let names = schema.type_names();
1892        assert_eq!(names.len(), 3);
1893        assert!(names.contains(&"users"));
1894        assert!(names.contains(&"posts"));
1895        assert!(names.contains(&"comments"));
1896    }
1897
1898    #[test]
1899    fn test_build_schema_exposed_schemas() {
1900        let mut cache = create_test_schema_cache();
1901
1902        // Add a table in a different schema
1903        let private_table = Table {
1904            schema: "private".into(),
1905            name: "secrets".into(),
1906            description: None,
1907            is_view: false,
1908            insertable: true,
1909            updatable: true,
1910            deletable: true,
1911            pk_cols: vec!["id".into()],
1912            unique_constraints: Vec::new(),
1913            columns: indexmap::IndexMap::new(),
1914            computed_columns: Default::default(),
1915            is_partitioned: false,
1916        };
1917        cache
1918            .tables
1919            .insert(private_table.qualified_identifier(), private_table);
1920
1921        let config = SchemaConfig::default(); // Only exposes "public"
1922        let schema = build_schema(&cache, &config);
1923
1924        // Should only have 3 tables from public schema
1925        assert_eq!(schema.object_types.len(), 3);
1926        assert!(schema.get_object_type("Secrets").is_none());
1927    }
1928
1929    // ============================================================================
1930    // GeneratedSchema Tests
1931    // ============================================================================
1932
1933    #[test]
1934    fn test_generated_schema_get_object_type() {
1935        let cache = create_test_schema_cache();
1936        let config = SchemaConfig::default();
1937        let schema = build_schema(&cache, &config);
1938
1939        let users = schema.get_object_type("users").unwrap();
1940        assert_eq!(users.table.name, "users");
1941    }
1942
1943    #[test]
1944    fn test_generated_schema_get_query_field() {
1945        let cache = create_test_schema_cache();
1946        let config = SchemaConfig::default();
1947        let schema = build_schema(&cache, &config);
1948
1949        let field = schema.get_query_field("posts").unwrap();
1950        assert_eq!(field.table_name, "posts");
1951    }
1952
1953    #[test]
1954    fn test_generated_schema_get_mutation_fields() {
1955        let cache = create_test_schema_cache();
1956        let config = SchemaConfig::default();
1957        let schema = build_schema(&cache, &config);
1958
1959        let fields = schema.get_mutation_fields("comments");
1960        // comments is only insertable
1961        assert_eq!(fields.len(), 2); // insert_comments + insert_comments_one
1962    }
1963}