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 postrust_core::QualifiedIdentifier;
15use std::collections::{HashMap, HashSet};
16
17/// Configuration for schema generation.
18#[derive(Debug, Clone)]
19#[non_exhaustive]
20pub struct SchemaConfig {
21    /// Schemas to expose in GraphQL (e.g., ["public"])
22    pub exposed_schemas: Vec<String>,
23    /// Whether to generate mutation types
24    pub enable_mutations: bool,
25    /// Whether to generate subscription types
26    pub enable_subscriptions: bool,
27
28    /// How often a live query re-reads itself when nothing has notified it.
29    ///
30    /// A subscription is woken by the trigger on the table it reads, which
31    /// costs nothing while nothing is written. A trigger cannot see
32    /// everything: a view has none, an embedded row may live in a table that
33    /// carries none, and a predicate written against the clock changes with
34    /// no write at all. This is how often those are noticed anyway. Seconds;
35    /// zero turns the refresh off and leaves only the notifications.
36    pub subscription_refresh_seconds: u64,
37    /// The members of each enum table, keyed by `schema.table`, as
38    /// `(value, comment)`.
39    ///
40    /// These are rows rather than schema, so they cannot come from the schema
41    /// cache: they are read at startup, and again whenever the schema is
42    /// rebuilt, for the tables the configuration marks. A table marked as an enum whose values are absent is exposed as
43    /// an ordinary table -- an empty GraphQL enum is not a legal type, and
44    /// refusing to start over a table with no rows in it would be worse.
45    pub enum_values: HashMap<String, Vec<(String, Option<String>)>>,
46    /// Names given rather than derived.
47    ///
48    /// Empty by default, which is every name derived from the schema exactly
49    /// as before.
50    pub names: crate::names::NameOverrides,
51    /// Ceiling on rows a single query may return (`PGRST_MAX_ROWS`).
52    ///
53    /// Applied when a query supplies no `limit` of its own, and as an upper
54    /// bound when it supplies a larger one. `None` leaves queries unbounded.
55    pub max_rows: Option<i64>,
56
57    /// Whether this schema is emitted as an Apollo Federation subgraph.
58    pub enable_federation: bool,
59    /// Namespace applied to generated table types and root fields.
60    pub type_prefix: Option<String>,
61
62    /// Whose schema this is.
63    ///
64    /// `None` is the unrestricted one: what an administrator sees, and what
65    /// every caller sees on a server with no permission document. `Some(role)`
66    /// is built from a cache already reduced to what that role may see, so the
67    /// only thing left for the builders to ask about is the aggregate root.
68    pub role: Option<String>,
69}
70
71impl Default for SchemaConfig {
72    fn default() -> Self {
73        Self {
74            exposed_schemas: vec!["public".to_string()],
75            enum_values: HashMap::new(),
76            names: crate::names::NameOverrides::default(),
77            enable_mutations: true,
78            enable_subscriptions: false,
79            subscription_refresh_seconds: 30,
80            max_rows: None,
81            enable_federation: false,
82            type_prefix: None,
83            role: None,
84        }
85    }
86}
87
88impl SchemaConfig {
89    /// Create a new schema config.
90    pub fn new() -> Self {
91        Self::default()
92    }
93
94    /// Set the exposed schemas.
95    pub fn with_schemas(mut self, schemas: Vec<String>) -> Self {
96        self.exposed_schemas = schemas;
97        self
98    }
99
100    /// Enable or disable mutations.
101    pub fn with_mutations(mut self, enable: bool) -> Self {
102        self.enable_mutations = enable;
103        self
104    }
105
106    /// Enable or disable subscriptions.
107    pub fn with_subscriptions(mut self, enable: bool) -> Self {
108        self.enable_subscriptions = enable;
109        self
110    }
111
112    /// Whether a table keeps its unprefixed type identity across subgraphs.
113    pub fn is_shared_entity(&self, table: &QualifiedIdentifier) -> bool {
114        self.enable_federation && self.names.federation_shared(&table.schema, &table.name)
115    }
116
117    /// Name a table type, applying the configured namespace unless it is shared.
118    pub fn table_type_name(&self, table: &QualifiedIdentifier, base_name: &str) -> String {
119        match self.type_prefix.as_deref() {
120            Some(prefix) if !prefix.is_empty() && !self.is_shared_entity(table) => {
121                format!("{prefix}_{base_name}")
122            }
123            _ => base_name.to_string(),
124        }
125    }
126
127    /// Namespace a root field so independently generated subgraphs do not collide.
128    pub fn root_field_name(&self, base_name: &str) -> String {
129        match self.type_prefix.as_deref() {
130            Some(prefix) if !prefix.is_empty() => format!("{prefix}_{base_name}"),
131            _ => base_name.to_string(),
132        }
133    }
134
135    /// How often a live query re-reads itself when nothing has notified it.
136    pub fn subscription_refresh(&self) -> std::time::Duration {
137        match self.subscription_refresh_seconds {
138            // A zero interval is not a tick every instant; it is no tick at
139            // all, which leaves the notifications alone to wake it.
140            0 => std::time::Duration::from_secs(60 * 60 * 24 * 365),
141            seconds => std::time::Duration::from_secs(seconds),
142        }
143    }
144
145    /// Check if a schema is exposed.
146    pub fn is_schema_exposed(&self, schema: &str) -> bool {
147        self.exposed_schemas.iter().any(|s| s == schema)
148    }
149
150    /// The schema whose tables get unqualified GraphQL names.
151    ///
152    /// This is the first exposed schema, mirroring how the REST surface treats
153    /// the first entry of `PGRST_DB_SCHEMAS` as the default.
154    pub fn default_schema(&self) -> &str {
155        self.exposed_schemas
156            .first()
157            .map(|s| s.as_str())
158            .unwrap_or("public")
159    }
160}
161
162/// Primary key columns of a table, as `(column name, PostgreSQL type)`.
163///
164/// `nominal_type` (the underlying `udt_name`) is used because it is always a
165/// castable type name.
166/// Whether the whole primary key is present on this table.
167///
168/// A `_by_pk` field addresses a row by its key, so a key the caller cannot see
169/// is a field it cannot use: `books_by_pk(id: 1, book_name: "...")` for a role
170/// granted `book_name` and not `id` asks for a row by half a key. Hasura does
171/// not publish the field at all, and neither does this -- which is also what
172/// stops `pk_columns_of` falling back to `text` for a column it cannot find
173/// and typing the argument `String`.
174fn has_whole_key(table: &Table) -> bool {
175    !table.pk_cols.is_empty()
176        && table
177            .pk_cols
178            .iter()
179            .all(|column| table.get_column(column).is_some())
180}
181
182fn pk_columns_of(table: &Table) -> Vec<(String, String)> {
183    table
184        .pk_cols
185        .iter()
186        .map(|col_name| {
187            let pg_type = table
188                .get_column(col_name)
189                .map(|c| c.nominal_type.clone())
190                .unwrap_or_else(|| "text".to_string());
191            (col_name.clone(), pg_type)
192        })
193        .collect()
194}
195
196/// Every key a relationship may be named under, most specific first.
197///
198/// More than one, because there is more than one way to identify the same
199/// relationship and a document may be written by hand or converted from
200/// Hasura's metadata, which uses a different one. A constraint names exactly
201/// one relationship even where two of them point at the same table; a computed
202/// relationship has no constraint and is named by its function; and Hasura
203/// names both directions by a column -- the foreign key column on this table
204/// for a relationship to one row, and `table.column` on the far side for a
205/// relationship to many. All of them are accepted, so a converted document
206/// needs no database to turn a column into the constraint that carries it.
207pub(crate) fn relationship_keys(rel: &postrust_core::schema_cache::Relationship) -> Vec<String> {
208    use postrust_core::schema_cache::{Cardinality, Relationship};
209
210    if let Relationship::Computed { function, .. } = rel {
211        return vec![function.name.clone()];
212    }
213
214    let mut keys = Vec::new();
215    // A many-to-many has no single constraint of its own: it is inferred from
216    // the two constraints on its junction table. The junction is the stable,
217    // unambiguous name clients already use as a PostgREST relationship hint,
218    // and either constituent constraint is useful to hand-written metadata.
219    if let Relationship::ForeignKey {
220        cardinality: Cardinality::M2M(junction),
221        ..
222    } = rel
223    {
224        keys.push(junction.table.name.clone());
225        keys.push(junction.constraint1.clone());
226        keys.push(junction.constraint2.clone());
227    }
228    match rel.constraint_name() {
229        "" => {}
230        constraint => keys.push(constraint.to_string()),
231    }
232    // The column that carries the key, on whichever side carries it. A
233    // one-to-one may have it on either, and two relationships between the same
234    // pair of tables can share the local column while differing in the far one
235    // -- so the more specific spelling is offered first and the bare column
236    // last.
237    if let Some(column) = rel.single_foreign_column() {
238        keys.push(format!("{}.{}", rel.foreign_table().name, column));
239    }
240    if !rel.is_one_to_many() {
241        if let Some(column) = rel.single_local_column() {
242            keys.push(column.to_string());
243        }
244    }
245    // A key over more than one column, spelled as the columns in order. A
246    // composite foreign key has no single column to be named by, and Hasura
247    // names one by its columns rather than by its constraint -- so a
248    // relationship over `(author_id1, author_id2)` is unreachable under any
249    // of the spellings above. Sorted, because the order two sides list the
250    // same columns in is not something either of them promises.
251    let joined = |mut columns: Vec<String>| {
252        columns.sort();
253        columns.join(",")
254    };
255    let columns = rel.join_columns();
256    if columns.len() > 1 {
257        keys.push(format!(
258            "{}.{}",
259            rel.foreign_table().name,
260            joined(columns.iter().map(|(_, foreign)| foreign.clone()).collect())
261        ));
262        if !rel.is_one_to_many() {
263            keys.push(joined(
264                columns.iter().map(|(local, _)| local.clone()).collect(),
265            ));
266        }
267    }
268    keys
269}
270
271/// A type name as the catalogue writes it, reduced to the table it names.
272///
273/// `pg_catalog.format_type` quotes anything that needs quoting and qualifies
274/// anything outside the search path, so a function returning rows of a table
275/// called `user` -- a reserved word -- reports `"user"`, and one returning rows
276/// of `other.thing` reports `other.thing`. Neither is the table's name, and
277/// comparing them against one finds nothing.
278fn type_name_of(rendered: &str) -> (Option<String>, String) {
279    let (schema, name) = match rendered.rsplit_once('.') {
280        Some((schema, name)) => (Some(schema.trim_matches('"').to_string()), name),
281        None => (None, rendered),
282    };
283    (schema, name.trim_matches('"').to_string())
284}
285
286/// Base name used to derive a table's GraphQL type and field names.
287///
288/// Tables in the default schema keep their bare name, so a single-schema
289/// deployment is unaffected. Tables in any other exposed schema are prefixed
290/// with the schema, because GraphQL has one flat namespace: without this, a
291/// `users` table in both `public` and `api` would generate the same type and
292/// field names and one would silently replace the other.
293fn base_name_for(table: &Table, config: &SchemaConfig) -> String {
294    // A name that was given is the answer; the rest of this is what to call a
295    // table nobody named.
296    if let Some(given) = config.names.base_name(&table.schema, &table.name) {
297        return given.to_string();
298    }
299    if table.schema == config.default_schema() {
300        table.name.clone()
301    } else {
302        format!("{}_{}", table.schema, table.name)
303    }
304}
305
306/// Represents a generated GraphQL schema.
307#[derive(Debug, Clone)]
308#[non_exhaustive]
309pub struct GeneratedSchema {
310    /// Object types for each table
311    pub object_types: HashMap<String, TableObjectType>,
312    /// Query fields
313    pub query_fields: Vec<QueryField>,
314    /// Mutation fields (if enabled)
315    pub mutation_fields: Vec<MutationField>,
316    /// Relationship fields for each type
317    pub relationship_fields: HashMap<String, Vec<RelationshipField>>,
318    /// GraphQL enums generated from enum tables: type name to
319    /// `(member, description)`.
320    pub enum_types: HashMap<String, Vec<(String, Option<String>)>>,
321    /// Database functions exposed as root fields.
322    pub function_fields: Vec<FunctionField>,
323    /// Apollo Federation entities keyed by GraphQL type name.
324    pub federation_entities: HashMap<String, FederationEntity>,
325    /// Whether Apollo Federation system fields should be exposed.
326    pub enable_federation: bool,
327}
328
329/// A table exposed as an Apollo Federation entity.
330#[derive(Debug, Clone)]
331#[non_exhaustive]
332pub struct FederationEntity {
333    /// GraphQL type name of the entity.
334    pub type_name: String,
335    /// Table this entity resolves to.
336    pub table: QualifiedIdentifier,
337    /// Primary key field names as exposed in GraphQL.
338    pub key_fields: Vec<String>,
339    /// Whether the type is configured as shared across subgraphs.
340    pub shared: bool,
341}
342
343impl FederationEntity {
344    /// The `fields` argument used by Apollo Federation's `@key` directive.
345    pub fn key_fields_sdl(&self) -> String {
346        self.key_fields.join(" ")
347    }
348}
349
350/// A database function returning rows of a table, exposed as a root field.
351///
352/// A function that returns `SETOF <table>` answers the same question a table
353/// does, from a query somebody wrote in SQL rather than from the table
354/// directly. It filters, orders and pages like the table it returns, and its
355/// own arguments arrive under `args` so they cannot collide with those.
356///
357/// Where it appears follows from what PostgreSQL says it does: a function
358/// declared VOLATILE may write, so it is a mutation; one declared STABLE or
359/// IMMUTABLE may not, so it is a query. Nothing else is a safe place to draw
360/// that line -- the alternative is trusting a name.
361#[derive(Debug, Clone)]
362#[non_exhaustive]
363pub struct FunctionField {
364    /// The field's name, which is the function's.
365    pub name: String,
366    /// Schema the function lives in.
367    pub schema_name: String,
368    /// The function's own name, which the field is named after.
369    pub function_name: String,
370    /// The GraphQL type of the rows it returns.
371    pub returns: String,
372    /// Base name for helper types owned by the returned rows.
373    pub local_returns: String,
374    /// The table those rows belong to, as `(schema, table)`.
375    pub returns_table: (String, String),
376    /// Its arguments, as `(name, PostgreSQL type, required)`. The session
377    /// argument is not among them: the client does not supply it.
378    pub arguments: Vec<(String, String, bool)>,
379    /// The name of its session argument, if it has one.
380    ///
381    /// Hasura's convention: a `hasura_session json` parameter is filled from
382    /// the caller's session variables rather than taken from the request, so a
383    /// function can read who is asking. Exposing it as an argument would let
384    /// the caller name its own identity, which is the opposite of the point.
385    pub session_argument: Option<String>,
386    /// Whether it may write, and so belongs on the mutation root.
387    pub volatile: bool,
388    /// Description from the function's comment.
389    pub description: Option<String>,
390}
391
392impl FunctionField {
393    /// The argument list, as PostgreSQL would write it -- `a integer, b text`.
394    ///
395    /// Two overloads differ only here, so this is what distinguishes them in a
396    /// warning and what orders them against each other.
397    pub fn argument_signature(&self) -> String {
398        self.arguments
399            .iter()
400            .map(|(name, sql_type, _)| format!("{name} {sql_type}"))
401            .collect::<Vec<_>>()
402            .join(", ")
403    }
404}
405
406/// What a computed relationship's function takes from the caller.
407///
408/// Everything but the parent row, which is what makes it a relationship, and
409/// the session, which is the server's to supply. Read from the routine rather
410/// than from the relationship because the relationship loader knows only which
411/// argument is the row -- the rest are the same parameters any function has,
412/// and they are already loaded.
413/// One relationship field per foreign key the catalogue carries.
414fn reflected_fields(
415    table: &Table,
416    schema_cache: &SchemaCache,
417    config: &SchemaConfig,
418    base_names: &HashMap<(String, String), String>,
419    type_names: &HashMap<(String, String), String>,
420    local_type_names: &HashMap<(String, String), String>,
421) -> Vec<RelationshipField> {
422    schema_cache
423        .get_relationships(&table.qualified_identifier(), &table.schema)
424        .map(|relationships| {
425            relationships
426                .iter()
427                .filter_map(|rel| {
428                    // A relationship whose target is not exposed would
429                    // reference a GraphQL type that was never registered.
430                    let foreign = rel.foreign_table();
431                    let target_key = (foreign.schema.clone(), foreign.name.clone());
432                    let target_base = base_names.get(&target_key)?;
433                    let target_type = type_names.get(&target_key)?;
434                    let target_local_type = local_type_names.get(&target_key)?;
435                    let mut field = RelationshipField::from_relationship_typed(
436                        rel,
437                        target_base,
438                        target_type,
439                        target_local_type,
440                        relationship_keys(rel).iter().find_map(|key| {
441                            config.names.relationship(&table.schema, &table.name, key)
442                        }),
443                    );
444                    field.arguments = caller_arguments(rel, schema_cache);
445                    Some(field)
446                })
447                .collect()
448        })
449        .unwrap_or_default()
450}
451
452/// One relationship field per relationship metadata declares, and no others.
453///
454/// A declaration reached by a key names a relationship reflection already
455/// found, so the catalogue's own cardinality and columns are used -- a key
456/// says which side has one row and which has many, and guessing that from a
457/// name would be guessing. One reached by a column mapping is not in the
458/// catalogue at all and is built from the mapping, with the direction taken
459/// from the declaration: Hasura writes object and array relationships
460/// separately, which is where `to_one` comes from.
461///
462/// A declaration this server cannot resolve is left out rather than guessed
463/// at. It is a field that would not work; the ones beside it still do.
464fn declared_fields(
465    declared: &[crate::names::DeclaredRelationship],
466    table: &Table,
467    schema_cache: &SchemaCache,
468    config: &SchemaConfig,
469    base_names: &HashMap<(String, String), String>,
470    type_names: &HashMap<(String, String), String>,
471    local_type_names: &HashMap<(String, String), String>,
472) -> Vec<RelationshipField> {
473    use postrust_core::schema_cache::Relationship;
474
475    let reflected = schema_cache
476        .get_relationships(&table.qualified_identifier(), &table.schema)
477        .map(|found| found.to_vec())
478        .unwrap_or_default();
479
480    let mut fields = Vec::with_capacity(declared.len());
481    for entry in declared {
482        // Reached by a key reflection already found.
483        if let Some(key) = entry.using.as_deref() {
484            let Some(rel) = reflected
485                .iter()
486                .find(|rel| relationship_keys(rel).iter().any(|found| found == key))
487            else {
488                tracing::warn!(
489                    "GraphQL: {}.{} declares the relationship \"{}\" through \"{}\", \
490                     which no foreign key carries; it is left out",
491                    table.schema,
492                    table.name,
493                    entry.name,
494                    key
495                );
496                continue;
497            };
498            let foreign = rel.foreign_table();
499            let target_key = (foreign.schema.clone(), foreign.name.clone());
500            let (Some(target_base), Some(target_type), Some(target_local_type)) = (
501                base_names.get(&target_key),
502                type_names.get(&target_key),
503                local_type_names.get(&target_key),
504            ) else {
505                continue;
506            };
507            let mut field = RelationshipField::from_relationship_typed(
508                rel,
509                target_base,
510                target_type,
511                target_local_type,
512                Some(&entry.name),
513            );
514            field.arguments = caller_arguments(rel, schema_cache);
515            fields.push(field);
516            continue;
517        }
518
519        // Reached by a column mapping, which is a join no key describes.
520        let Some((schema, name)) = entry.target(config.default_schema()) else {
521            tracing::warn!(
522                "GraphQL: {}.{} declares the relationship \"{}\" with neither a key \
523                 nor a table to join to; it is left out",
524                table.schema,
525                table.name,
526                entry.name
527            );
528            continue;
529        };
530        let target_key = (schema, name);
531        let (Some(target_base), Some(target_type), Some(target_local_type)) = (
532            base_names.get(&target_key),
533            type_names.get(&target_key),
534            local_type_names.get(&target_key),
535        ) else {
536            continue;
537        };
538        match mapped_relationship_field(
539            entry,
540            table,
541            schema_cache,
542            config.default_schema(),
543            target_base,
544            target_type,
545            target_local_type,
546        ) {
547            Some(field) => fields.push(field),
548            None => tracing::warn!(
549                "GraphQL: {}.{} declares the relationship \"{}\" with no columns to \
550                 join on; it is left out",
551                table.schema,
552                table.name,
553                entry.name
554            ),
555        }
556    }
557
558    // A computed relationship is declared by `add_computed_field`, not by
559    // `create_object_relationship`, so it is not in this list and is not what
560    // the list is exhaustive about. Leaving it to reflection is what keeps
561    // `author.get_articles` on a table whose foreign keys are all declared.
562    let already: std::collections::HashSet<String> =
563        fields.iter().map(|field| field.name.clone()).collect();
564    for rel in reflected
565        .iter()
566        .filter(|rel| matches!(rel, Relationship::Computed { .. }))
567    {
568        let foreign = rel.foreign_table();
569        let target_key = (foreign.schema.clone(), foreign.name.clone());
570        let (Some(target_base), Some(target_type), Some(target_local_type)) = (
571            base_names.get(&target_key),
572            type_names.get(&target_key),
573            local_type_names.get(&target_key),
574        ) else {
575            continue;
576        };
577        let mut field = RelationshipField::from_relationship_typed(
578            rel,
579            target_base,
580            target_type,
581            target_local_type,
582            relationship_keys(rel)
583                .iter()
584                .find_map(|key| config.names.relationship(&table.schema, &table.name, key)),
585        );
586        if already.contains(&field.name) {
587            continue;
588        }
589        field.arguments = caller_arguments(rel, schema_cache);
590        fields.push(field);
591    }
592    fields
593}
594
595/// One relationship field for a declaration that maps column to column.
596///
597/// The join is the declaration's, the direction is the declaration's, and the
598/// constraint name is this server's -- nothing reads it back, and a mapped
599/// join has no constraint to borrow. `None` where the declaration names no
600/// columns to join on, which is not a join.
601pub(crate) fn mapped_relationship_field(
602    entry: &crate::names::DeclaredRelationship,
603    table: &Table,
604    schema_cache: &SchemaCache,
605    default_schema: &str,
606    target_base: &str,
607    target_type: &str,
608    target_local_type: &str,
609) -> Option<RelationshipField> {
610    use postrust_core::schema_cache::{Cardinality, Relationship};
611
612    let (schema, name) = entry.target(default_schema)?;
613    if entry.columns.is_empty() {
614        return None;
615    }
616    let foreign_table = postrust_core::api_request::QualifiedIdentifier::new(&schema, &name);
617    let here = table.qualified_identifier();
618    let columns: Vec<(String, String)> = entry
619        .columns
620        .iter()
621        .map(|(mine, theirs)| (mine.clone(), theirs.clone()))
622        .collect();
623    let constraint = format!("{}.{}", table.name, entry.name);
624    // Which side holds the key, which is what a nested insert needs and a
625    // column mapping does not say. `M2O` is "this row's column carries their
626    // key", so the related row is written first; `O2O { is_parent: true }` is
627    // the other way round, and is how `after_parent` is spelled here. A
628    // relationship to many is written after its parent either way.
629    let cardinality = match (entry.to_one, entry.after_parent) {
630        (true, false) => Cardinality::M2O {
631            constraint: constraint.clone(),
632            columns,
633        },
634        (true, true) => Cardinality::O2O {
635            constraint: constraint.clone(),
636            columns,
637            is_parent: true,
638        },
639        (false, _) => Cardinality::O2M {
640            constraint: constraint.clone(),
641            columns,
642        },
643    };
644    let rel = Relationship::ForeignKey {
645        is_self: here == foreign_table,
646        table: here,
647        foreign_table_is_view: schema_cache
648            .get_table(&foreign_table)
649            .is_some_and(|t| t.is_view),
650        foreign_table,
651        cardinality,
652        table_is_view: table.is_view,
653        constraint_name: constraint,
654    };
655    Some(RelationshipField::from_relationship_typed(
656        &rel,
657        target_base,
658        target_type,
659        target_local_type,
660        Some(&entry.name),
661    ))
662}
663
664fn caller_arguments(
665    relationship: &postrust_core::schema_cache::Relationship,
666    schema_cache: &SchemaCache,
667) -> Vec<(String, String, bool)> {
668    use postrust_core::schema_cache::Relationship;
669    let Relationship::Computed {
670        function,
671        row_argument,
672        ..
673    } = relationship
674    else {
675        return Vec::new();
676    };
677    let Some(row_argument) = row_argument else {
678        return Vec::new();
679    };
680    schema_cache
681        .routines
682        .get(function)
683        .into_iter()
684        .flatten()
685        .find(|routine| routine.name == function.name)
686        .map(|routine| {
687            routine
688                .params
689                .iter()
690                .filter(|param| &param.name != row_argument)
691                .filter(|param| !is_session_argument(param))
692                .filter(|param| !param.name.is_empty())
693                .map(|param| (param.name.clone(), param.param_type.clone(), param.required))
694                .collect()
695        })
696        .unwrap_or_default()
697}
698
699/// What a computed *column*'s function takes from the caller.
700///
701/// The same rule as [`caller_arguments`], one field kind along: everything but
702/// the row, which is what makes it a field of this table, and the session,
703/// which is the server's to supply.
704pub(crate) fn computed_caller_arguments(
705    computed: &postrust_core::schema_cache::ComputedColumn,
706    schema_cache: &SchemaCache,
707) -> Vec<(String, String, bool)> {
708    let Some(row_argument) = &computed.row_argument else {
709        return Vec::new();
710    };
711    schema_cache
712        .routines
713        .get(&computed.function)
714        .into_iter()
715        .flatten()
716        .find(|routine| routine.name == computed.function.name)
717        .map(|routine| {
718            routine
719                .params
720                .iter()
721                .filter(|param| &param.name != row_argument)
722                .filter(|param| !is_session_argument(param))
723                .filter(|param| !param.name.is_empty())
724                .map(|param| (param.name.clone(), param.param_type.clone(), param.required))
725                .collect()
726        })
727        .unwrap_or_default()
728}
729
730/// Whether a parameter is the one Hasura fills from the session.
731///
732/// Recognised by name and type, which is the only thing the database records:
733/// Hasura writes `session_argument: hasura_session` into metadata, and every
734/// function that has one calls it that.
735fn is_session_argument(param: &postrust_core::schema_cache::RoutineParam) -> bool {
736    param.name == "hasura_session" && matches!(param.param_type.as_str(), "json" | "jsonb")
737}
738
739impl GeneratedSchema {
740    /// Get an object type by name.
741    pub fn get_object_type(&self, name: &str) -> Option<&TableObjectType> {
742        self.object_types.get(name)
743    }
744
745    /// Get query fields for a table.
746    pub fn get_query_field(&self, table_name: &str) -> Option<&QueryField> {
747        self.query_fields
748            .iter()
749            .find(|f| f.table_name == table_name)
750    }
751
752    /// Get mutation fields for a table.
753    pub fn get_mutation_fields(&self, table_name: &str) -> Vec<&MutationField> {
754        self.mutation_fields
755            .iter()
756            .filter(|f| f.table_name == table_name)
757            .collect()
758    }
759
760    /// Get relationship fields for a type.
761    pub fn get_relationship_fields(&self, type_name: &str) -> Option<&Vec<RelationshipField>> {
762        self.relationship_fields.get(type_name)
763    }
764
765    /// Get federation entity metadata for a type.
766    pub fn get_federation_entity(&self, type_name: &str) -> Option<&FederationEntity> {
767        self.federation_entities.get(type_name)
768    }
769
770    /// Get all table names.
771    pub fn table_names(&self) -> Vec<&str> {
772        self.object_types
773            .values()
774            .map(|t| t.table.name.as_str())
775            .collect()
776    }
777
778    /// Get all type names.
779    pub fn type_names(&self) -> Vec<&str> {
780        self.object_types.keys().map(|s| s.as_str()).collect()
781    }
782}
783
784/// A query field for a table (e.g., users, userByPk).
785#[derive(Debug, Clone)]
786#[non_exhaustive]
787pub struct QueryField {
788    /// Field name (e.g., "users")
789    pub name: String,
790    /// Table name
791    pub table_name: String,
792    /// Schema the table lives in
793    pub schema_name: String,
794    /// GraphQL object type name (e.g., "Users")
795    pub type_name: String,
796    /// Base name for helper types owned by this table.
797    pub local_type_name: String,
798    /// GraphQL return type
799    pub return_type: String,
800    /// Whether this returns a list
801    pub is_list: bool,
802    /// Whether this is a "by PK" query
803    pub is_by_pk: bool,
804    /// Primary key columns, as `(column name, PostgreSQL type)`.
805    ///
806    /// Populated for by-PK queries so the resolver can filter on the table's
807    /// actual key rather than assuming a column called `id`. Empty for list
808    /// queries.
809    pub pk_columns: Vec<(String, String)>,
810    /// Field description
811    pub description: Option<String>,
812    /// The name given to this table's aggregate root, if one was.
813    ///
814    /// The aggregate field is built beside the list field rather than being a
815    /// `QueryField` of its own, so the name it was given rides along with the
816    /// list field that spawns it.
817    pub aggregate_name: Option<String>,
818    /// The description given to that aggregate root, if one was. `Some("")`
819    /// means metadata said it has none.
820    pub aggregate_description: Option<String>,
821    /// Whether the aggregate root is built at all.
822    ///
823    /// The one thing a role's view of the schema cache cannot say. Dropping a
824    /// table or a column is a fact about the database as this role sees it;
825    /// whether that role may *count* the rows it can already read is a fact
826    /// about the permission, and Hasura keeps the two apart because counting
827    /// rows you cannot see is a way of seeing them. True unless a select
828    /// permission says otherwise, which is what leaves an unconfigured server
829    /// exactly as it was.
830    pub aggregates: bool,
831    /// Whether the rows themselves can be asked for.
832    ///
833    /// False for a table a role may count and not read: Hasura's way of
834    /// granting "how many" without granting "which" is a select permission
835    /// naming no columns, and such a table has an aggregate root and no row
836    /// type to hang a list root on. The field is still carried so that the
837    /// aggregate root beside it is built.
838    pub rows: bool,
839}
840
841impl QueryField {
842    /// Create a list query field (e.g., users), named after the table.
843    pub fn list(table: &Table) -> Self {
844        Self::list_named(table, &table.name)
845    }
846
847    /// Create a list query field using an explicit base name.
848    pub fn list_named(table: &Table, base_name: &str) -> Self {
849        Self::list_typed(table, base_name, base_name, base_name)
850    }
851
852    /// Create a list query field with separate root, row type and helper names.
853    pub fn list_typed(
854        table: &Table,
855        root_base_name: &str,
856        type_name: &str,
857        local_type_name: &str,
858    ) -> Self {
859        let name = root_base_name.to_string();
860
861        Self {
862            name,
863            table_name: table.name.clone(),
864            schema_name: table.schema.clone(),
865            type_name: type_name.to_string(),
866            local_type_name: local_type_name.to_string(),
867            return_type: format!("[{}!]!", type_name),
868            is_list: true,
869            is_by_pk: false,
870            pk_columns: Vec::new(),
871            description: Some(format!("fetch data from the table: \"{}\"", table.name)),
872            aggregate_name: None,
873            aggregate_description: None,
874            aggregates: true,
875            rows: true,
876        }
877    }
878
879    /// Create a by-PK query field (e.g., userByPk), named after the table.
880    pub fn by_pk(table: &Table) -> Option<Self> {
881        Self::by_pk_named(table, &table.name)
882    }
883
884    /// Create a by-PK query field using an explicit base name.
885    pub fn by_pk_named(table: &Table, base_name: &str) -> Option<Self> {
886        Self::by_pk_typed(table, base_name, base_name, base_name)
887    }
888
889    /// Create a by-PK query field with separate root, row type and helper names.
890    pub fn by_pk_typed(
891        table: &Table,
892        root_base_name: &str,
893        type_name: &str,
894        local_type_name: &str,
895    ) -> Option<Self> {
896        if !has_whole_key(table) {
897            return None;
898        }
899
900        let field_name = format!("{}_by_pk", root_base_name);
901
902        // Carry the key columns and their types so the resolver can filter on
903        // the real primary key.
904        let pk_columns = pk_columns_of(table);
905
906        Some(Self {
907            name: field_name,
908            table_name: table.name.clone(),
909            schema_name: table.schema.clone(),
910            type_name: type_name.to_string(),
911            local_type_name: local_type_name.to_string(),
912            return_type: type_name.to_string(),
913            is_list: false,
914            is_by_pk: true,
915            pk_columns,
916            description: Some(format!(
917                "fetch data from the table: \"{}\" using primary key columns",
918                table.name
919            )),
920            aggregate_name: None,
921            aggregate_description: None,
922            aggregates: true,
923            rows: true,
924        })
925    }
926}
927
928/// A mutation field for a table.
929#[derive(Debug, Clone)]
930#[non_exhaustive]
931pub struct MutationField {
932    /// Field name (e.g., "insertUsers")
933    pub name: String,
934    /// Table name
935    pub table_name: String,
936    /// Schema the table lives in
937    pub schema_name: String,
938    /// Mutation type
939    pub mutation_type: MutationType,
940    /// Primary key columns, as `(column name, PostgreSQL type)`.
941    ///
942    /// Populated for by-PK mutations so the resolver can target the row by its
943    /// key. Empty for bulk mutations.
944    pub pk_columns: Vec<(String, String)>,
945    /// GraphQL return type
946    pub return_type: String,
947    /// GraphQL row object type name.
948    pub type_name: String,
949    /// Base name for helper types owned by this table.
950    pub local_type_name: String,
951    /// Field description
952    pub description: Option<String>,
953}
954
955/// Types of mutations.
956#[derive(Debug, Clone, Copy, PartialEq, Eq)]
957pub enum MutationType {
958    /// Insert multiple records
959    Insert,
960    /// Insert a single record
961    InsertOne,
962    /// Update records matching a filter
963    Update,
964    /// Update a single record by PK
965    UpdateByPk,
966    /// Several updates, each with its own filter, in one transaction
967    UpdateMany,
968    /// Delete records matching a filter
969    Delete,
970    /// Delete a single record by PK
971    DeleteByPk,
972}
973
974impl MutationField {
975    /// Create insert mutation fields for a table.
976    pub fn insert_fields(table: &Table) -> Vec<Self> {
977        Self::insert_fields_named(table, &table.name)
978    }
979
980    /// As [`Self::insert_fields`], with an explicit base name for the generated
981    /// field and type names.
982    pub fn insert_fields_named(table: &Table, base_name: &str) -> Vec<Self> {
983        Self::insert_fields_typed(table, base_name, base_name)
984    }
985
986    /// Create insert mutation fields using separate helper and row type names.
987    pub fn insert_fields_typed(table: &Table, local_type_name: &str, type_name: &str) -> Vec<Self> {
988        if !is_insertable(table) {
989            return vec![];
990        }
991
992        let mut fields = vec![];
993
994        // insert_users (batch insert)
995        let name = format!("insert_{}", local_type_name);
996        fields.push(Self {
997            name,
998            table_name: table.name.clone(),
999            schema_name: table.schema.clone(),
1000            mutation_type: MutationType::Insert,
1001            pk_columns: Vec::new(),
1002            return_type: format!("{}_mutation_response", local_type_name),
1003            type_name: type_name.to_string(),
1004            local_type_name: local_type_name.to_string(),
1005            description: Some(format!("insert data into the table: \"{}\"", table.name)),
1006        });
1007
1008        // insert_user_one (single insert)
1009        let name = format!("insert_{}_one", local_type_name);
1010        fields.push(Self {
1011            name,
1012            table_name: table.name.clone(),
1013            schema_name: table.schema.clone(),
1014            mutation_type: MutationType::InsertOne,
1015            pk_columns: Vec::new(),
1016            return_type: type_name.to_string(),
1017            type_name: type_name.to_string(),
1018            local_type_name: local_type_name.to_string(),
1019            description: Some(format!(
1020                "insert a single row into the table: \"{}\"",
1021                table.name
1022            )),
1023        });
1024
1025        fields
1026    }
1027
1028    /// Create update mutation fields for a table.
1029    pub fn update_fields(table: &Table) -> Vec<Self> {
1030        Self::update_fields_named(table, &table.name)
1031    }
1032
1033    /// As [`Self::update_fields`], with an explicit base name for the generated
1034    /// field and type names.
1035    pub fn update_fields_named(table: &Table, base_name: &str) -> Vec<Self> {
1036        Self::update_fields_typed(table, base_name, base_name)
1037    }
1038
1039    /// Create update mutation fields using separate helper and row type names.
1040    pub fn update_fields_typed(table: &Table, local_type_name: &str, type_name: &str) -> Vec<Self> {
1041        if !is_updatable(table) {
1042            return vec![];
1043        }
1044
1045        let mut fields = vec![];
1046
1047        // update_users (batch update)
1048        let name = format!("update_{}", local_type_name);
1049        fields.push(Self {
1050            name,
1051            table_name: table.name.clone(),
1052            schema_name: table.schema.clone(),
1053            mutation_type: MutationType::Update,
1054            pk_columns: Vec::new(),
1055            return_type: format!("{}_mutation_response", local_type_name),
1056            type_name: type_name.to_string(),
1057            local_type_name: local_type_name.to_string(),
1058            description: Some(format!("update data of the table: \"{}\"", table.name)),
1059        });
1060
1061        // update_users_many (several filters, each with its own values)
1062        let name = format!("update_{}_many", local_type_name);
1063        fields.push(Self {
1064            name,
1065            table_name: table.name.clone(),
1066            schema_name: table.schema.clone(),
1067            mutation_type: MutationType::UpdateMany,
1068            pk_columns: Vec::new(),
1069            return_type: format!("[{}_mutation_response]", local_type_name),
1070            type_name: type_name.to_string(),
1071            local_type_name: local_type_name.to_string(),
1072            description: Some(format!(
1073                "update multiples rows of table: \"{}\"",
1074                table.name
1075            )),
1076        });
1077
1078        // update_user_by_pk (single update by PK)
1079        if has_whole_key(table) {
1080            let name = format!("update_{}_by_pk", local_type_name);
1081            fields.push(Self {
1082                name,
1083                table_name: table.name.clone(),
1084                schema_name: table.schema.clone(),
1085                mutation_type: MutationType::UpdateByPk,
1086                pk_columns: pk_columns_of(table),
1087                return_type: type_name.to_string(),
1088                type_name: type_name.to_string(),
1089                local_type_name: local_type_name.to_string(),
1090                description: Some(format!(
1091                    "update single row of the table: \"{}\"",
1092                    table.name
1093                )),
1094            });
1095        }
1096
1097        fields
1098    }
1099
1100    /// Create delete mutation fields for a table.
1101    pub fn delete_fields(table: &Table) -> Vec<Self> {
1102        Self::delete_fields_named(table, &table.name)
1103    }
1104
1105    /// As [`Self::delete_fields`], with an explicit base name for the generated
1106    /// field and type names.
1107    pub fn delete_fields_named(table: &Table, base_name: &str) -> Vec<Self> {
1108        Self::delete_fields_typed(table, base_name, base_name)
1109    }
1110
1111    /// Create delete mutation fields using separate helper and row type names.
1112    pub fn delete_fields_typed(table: &Table, local_type_name: &str, type_name: &str) -> Vec<Self> {
1113        if !is_deletable(table) {
1114            return vec![];
1115        }
1116
1117        let mut fields = vec![];
1118
1119        // delete_users (batch delete)
1120        let name = format!("delete_{}", local_type_name);
1121        fields.push(Self {
1122            name,
1123            table_name: table.name.clone(),
1124            schema_name: table.schema.clone(),
1125            mutation_type: MutationType::Delete,
1126            pk_columns: Vec::new(),
1127            return_type: format!("{}_mutation_response", local_type_name),
1128            type_name: type_name.to_string(),
1129            local_type_name: local_type_name.to_string(),
1130            description: Some(format!("delete data from the table: \"{}\"", table.name)),
1131        });
1132
1133        // delete_user_by_pk (single delete by PK)
1134        if has_whole_key(table) {
1135            let name = format!("delete_{}_by_pk", local_type_name);
1136            fields.push(Self {
1137                name,
1138                table_name: table.name.clone(),
1139                schema_name: table.schema.clone(),
1140                mutation_type: MutationType::DeleteByPk,
1141                pk_columns: pk_columns_of(table),
1142                return_type: type_name.to_string(),
1143                type_name: type_name.to_string(),
1144                local_type_name: local_type_name.to_string(),
1145                description: Some(format!(
1146                    "delete single row from the table: \"{}\"",
1147                    table.name
1148                )),
1149            });
1150        }
1151
1152        fields
1153    }
1154}
1155
1156/// Build a GraphQL schema from a schema cache.
1157pub fn build_schema(schema_cache: &SchemaCache, config: &SchemaConfig) -> GeneratedSchema {
1158    let mut object_types = HashMap::new();
1159    let mut query_fields = Vec::new();
1160    let mut mutation_fields = Vec::new();
1161    let mut relationship_fields = HashMap::new();
1162    let mut federation_entities = HashMap::new();
1163
1164    // Tables are visited in a stable order: the cache is a hash map, and any
1165    // name disambiguation below must not shift between restarts.
1166    let mut tables: Vec<&Table> = schema_cache
1167        .tables
1168        .values()
1169        .filter(|table| config.is_schema_exposed(&table.schema))
1170        .collect();
1171    tables.sort_by(|a, b| (&a.schema, &a.name).cmp(&(&b.schema, &b.name)));
1172
1173    // Base names already carry the schema for non-default schemas, so a clash
1174    // here needs contrived naming (a `public.api_users` table alongside
1175    // `api.users`). Resolve it with a numeric suffix rather than letting one
1176    // table overwrite the other.
1177    let mut used_base_names: HashMap<String, u32> = HashMap::new();
1178    // (schema, table) -> resolved base name, needed when naming relationship
1179    // targets.
1180    let mut base_names: HashMap<(String, String), String> = HashMap::new();
1181
1182    for table in &tables {
1183        let preferred = base_name_for(table, config);
1184        let base_name = match used_base_names.get_mut(&preferred) {
1185            None => {
1186                used_base_names.insert(preferred.clone(), 1);
1187                preferred
1188            }
1189            Some(count) => {
1190                *count += 1;
1191                let disambiguated = format!("{}_{}", preferred, count);
1192                tracing::warn!(
1193                    "GraphQL name collision: {}.{} would generate the same names as \
1194                     an earlier table; exposing it as \"{}\" instead",
1195                    table.schema,
1196                    table.name,
1197                    disambiguated
1198                );
1199                disambiguated
1200            }
1201        };
1202
1203        base_names.insert(
1204            (table.schema.clone(), table.name.clone()),
1205            base_name.clone(),
1206        );
1207    }
1208
1209    // Object and derived input types share one GraphQL namespace. Resolve the
1210    // prefixed names independently from field bases because shared entities
1211    // keep their type identity while their root fields remain namespaced.
1212    let mut used_type_names: HashMap<String, u32> = HashMap::new();
1213    let mut type_names: HashMap<(String, String), String> = HashMap::new();
1214    let mut local_type_names: HashMap<(String, String), String> = HashMap::new();
1215    for table in &tables {
1216        let key = (table.schema.clone(), table.name.clone());
1217        let base_name = base_names
1218            .get(&key)
1219            .expect("every visited table has a resolved base name");
1220        let preferred = config.table_type_name(&table.qualified_identifier(), base_name);
1221        let type_name = match used_type_names.get_mut(&preferred) {
1222            None => {
1223                used_type_names.insert(preferred.clone(), 1);
1224                preferred
1225            }
1226            Some(count) => {
1227                *count += 1;
1228                let disambiguated = format!("{}_{}", preferred, count);
1229                tracing::warn!(
1230                    "GraphQL type name collision: {}.{} would generate the same type as \
1231                     an earlier table; exposing it as \"{}\" instead",
1232                    table.schema,
1233                    table.name,
1234                    disambiguated
1235                );
1236                disambiguated
1237            }
1238        };
1239        let local_type_name = config.root_field_name(base_name);
1240        local_type_names.insert(key.clone(), local_type_name);
1241        type_names.insert(key, type_name);
1242    }
1243
1244    // The tables this role may read. Needed after the loop, by
1245    // anything that would answer with a row of one.
1246    let mut readable_tables: HashSet<QualifiedIdentifier> = HashSet::new();
1247
1248    for table in &tables {
1249        let base_name = base_names
1250            .get(&(table.schema.clone(), table.name.clone()))
1251            .expect("every visited table has a resolved base name")
1252            .clone();
1253        let type_name = type_names
1254            .get(&(table.schema.clone(), table.name.clone()))
1255            .expect("every visited table has a resolved type name")
1256            .clone();
1257        let local_type_name = local_type_names
1258            .get(&(table.schema.clone(), table.name.clone()))
1259            .expect("every visited table has a resolved local type name")
1260            .clone();
1261
1262        // Create object type
1263        let mut obj_type = TableObjectType::from_table_named(table, &type_name, &config.names);
1264        obj_type.local_name = local_type_name.clone();
1265        // Whether this role can read the table at all. A table it may only
1266        // write has no GraphQL type -- a type with no fields is not a legal
1267        // one -- so nothing that would return that type is generated: no query
1268        // root, no `_by_pk`, no `insert_one`. The bulk write stays, and
1269        // answers with the count alone.
1270        //
1271        // Asked of the permission rather than of the fields, because the
1272        // fields are not split into the readable and the writable half until
1273        // every rewrite below has been applied to both.
1274        let readable = config.role.as_deref().is_none_or(|role| {
1275            config
1276                .names
1277                .permissions(&table.schema, &table.name, role)
1278                .is_none_or(|granted| crate::role::reads_anything(granted, table))
1279        });
1280        if readable {
1281            readable_tables.insert(table.qualified_identifier());
1282        }
1283        // What each computed field takes from the caller. Read from the
1284        // routine, for the reason `caller_arguments` gives.
1285        for field in &mut obj_type.fields {
1286            let Some(function) = config
1287                .names
1288                .computed_source(&table.schema, &table.name, &field.name)
1289                .or(Some(field.name.as_str()))
1290            else {
1291                continue;
1292            };
1293            let Some(computed) = table.get_computed_column(function) else {
1294                continue;
1295            };
1296            field.arguments = computed_caller_arguments(computed, schema_cache);
1297        }
1298        // Add query fields. Each root may be named separately -- Hasura names
1299        // them one at a time, and a set that does not agree on a base name has
1300        // nowhere else to be written down.
1301        // A root may be given a name, a description, or both. An empty
1302        // description is one that was given and is empty: metadata said the
1303        // field has none.
1304        let rename = |field: &mut QueryField, kind: &str| {
1305            if let Some(given) = config.names.root(&table.schema, &table.name, kind) {
1306                field.name = given.to_string();
1307            }
1308            if let Some(comment) = config.names.root_comment(&table.schema, &table.name, kind) {
1309                field.description = Some(comment.to_string()).filter(|c| !c.is_empty());
1310            }
1311        };
1312        let mut list = QueryField::list_typed(table, &base_name, &type_name, &local_type_name);
1313        list.name = base_name.clone();
1314        rename(&mut list, "select");
1315        list.name = config.root_field_name(&list.name);
1316        // The aggregate root is generated from the type name rather than being
1317        // a `QueryField` of its own, so its name travels with the list field
1318        // that spawns it.
1319        let aggregate_name = config
1320            .names
1321            .root(&table.schema, &table.name, "select_aggregate")
1322            .map(str::to_string)
1323            .unwrap_or_else(|| format!("{}_aggregate", base_name));
1324        list.aggregate_name = Some(config.root_field_name(&aggregate_name));
1325        list.aggregate_description = config
1326            .names
1327            .root_comment(&table.schema, &table.name, "select_aggregate")
1328            .map(str::to_string);
1329        // Whether this role may count what it can read. Only asked where a
1330        // role is building its own schema; the unrestricted one aggregates
1331        // whatever the database will.
1332        if let Some(role) = &config.role {
1333            list.aggregates =
1334                crate::role::allows_aggregations(&config.names, &table.schema, &table.name, role);
1335        }
1336        // A table with no readable field still gets its list entry when the
1337        // role may count it: the aggregate root is built beside that entry
1338        // rather than being one of its own, and `rows` is what says the list
1339        // root itself is not there. By key there is nothing to answer with at
1340        // all.
1341        list.rows = readable;
1342        if readable || list.aggregates {
1343            query_fields.push(list);
1344        }
1345        if readable {
1346            if let Some(mut by_pk) =
1347                QueryField::by_pk_typed(table, &base_name, &type_name, &local_type_name)
1348            {
1349                by_pk.name = format!("{}_by_pk", base_name);
1350                rename(&mut by_pk, "select_by_pk");
1351                by_pk.name = config.root_field_name(&by_pk.name);
1352                query_fields.push(by_pk);
1353            }
1354        }
1355
1356        // Add mutation fields if enabled
1357        if config.enable_mutations {
1358            let mut fields: Vec<MutationField> = Vec::new();
1359            fields.extend(MutationField::insert_fields_typed(
1360                table,
1361                &local_type_name,
1362                &type_name,
1363            ));
1364            fields.extend(MutationField::update_fields_typed(
1365                table,
1366                &local_type_name,
1367                &type_name,
1368            ));
1369            fields.extend(MutationField::delete_fields_typed(
1370                table,
1371                &local_type_name,
1372                &type_name,
1373            ));
1374            // The three that answer with a row rather than with a count.
1375            if !readable {
1376                fields.retain(|field| {
1377                    !matches!(
1378                        field.mutation_type,
1379                        MutationType::InsertOne
1380                            | MutationType::UpdateByPk
1381                            | MutationType::DeleteByPk
1382                    )
1383                });
1384            }
1385            for field in &mut fields {
1386                let kind = match field.mutation_type {
1387                    MutationType::Insert => "insert",
1388                    MutationType::InsertOne => "insert_one",
1389                    MutationType::Update => "update",
1390                    MutationType::UpdateByPk => "update_by_pk",
1391                    MutationType::UpdateMany => "update_many",
1392                    MutationType::Delete => "delete",
1393                    MutationType::DeleteByPk => "delete_by_pk",
1394                };
1395                field.name = match field.mutation_type {
1396                    MutationType::Insert => format!("insert_{base_name}"),
1397                    MutationType::InsertOne => format!("insert_{base_name}_one"),
1398                    MutationType::Update => format!("update_{base_name}"),
1399                    MutationType::UpdateByPk => format!("update_{base_name}_by_pk"),
1400                    MutationType::UpdateMany => format!("update_{base_name}_many"),
1401                    MutationType::Delete => format!("delete_{base_name}"),
1402                    MutationType::DeleteByPk => format!("delete_{base_name}_by_pk"),
1403                };
1404                if let Some(given) = config.names.root(&table.schema, &table.name, kind) {
1405                    field.name = given.to_string();
1406                }
1407                field.name = config.root_field_name(&field.name);
1408                if let Some(comment) = config.names.root_comment(&table.schema, &table.name, kind) {
1409                    field.description = Some(comment.to_string()).filter(|c| !c.is_empty());
1410                }
1411            }
1412            mutation_fields.extend(fields);
1413        }
1414
1415        // Add relationship fields. Where metadata declares them, they are
1416        // what the table has; otherwise reflection offers one per foreign
1417        // key, which is what a document saying nothing about a table has
1418        // always meant. See `TableNames::declared_relationships`.
1419        let rels = match config
1420            .names
1421            .declared_relationships(&table.schema, &table.name)
1422        {
1423            Some(declared) => declared_fields(
1424                declared,
1425                table,
1426                schema_cache,
1427                config,
1428                &base_names,
1429                &type_names,
1430                &local_type_names,
1431            ),
1432            None => reflected_fields(
1433                table,
1434                schema_cache,
1435                config,
1436                &base_names,
1437                &type_names,
1438                &local_type_names,
1439            ),
1440        };
1441
1442        if !rels.is_empty() {
1443            relationship_fields.insert(type_name.clone(), rels);
1444        }
1445
1446        if config.enable_federation && readable && has_whole_key(table) {
1447            let key_fields: Vec<String> = obj_type
1448                .fields
1449                .iter()
1450                .filter(|field| field.is_pk)
1451                .map(|field| field.name.clone())
1452                .collect();
1453            if key_fields.len() == table.pk_cols.len() {
1454                federation_entities.insert(
1455                    type_name.clone(),
1456                    FederationEntity {
1457                        type_name: type_name.clone(),
1458                        table: table.qualified_identifier(),
1459                        key_fields,
1460                        shared: config.is_shared_entity(&table.qualified_identifier()),
1461                    },
1462                );
1463            }
1464        }
1465
1466        object_types.insert(type_name, obj_type);
1467    }
1468
1469    // A table marked as a set of allowed values becomes a GraphQL enum, and
1470    // every column with a foreign key to it is typed as that enum rather than
1471    // as text. The values are rows, so they were read at startup rather than
1472    // reflected.
1473    let mut enum_types: HashMap<String, Vec<(String, Option<String>)>> = HashMap::new();
1474    let mut enum_type_of: HashMap<(String, String), String> = HashMap::new();
1475
1476    for (schema_name, table_name) in config.names.enum_tables() {
1477        // A role that may not read the table's rows still sees the enum. The
1478        // values are a *type*, and a column typed by it is readable -- Hasura
1479        // publishes `colors_enum` to a role granted nothing on `colors`, and
1480        // the query `where: {favorite_color: {_eq: red}}` is one it answers.
1481        // So the name is derived rather than looked up, since a table the
1482        // role cannot read has no entry to look up.
1483        let table_key = (schema_name.clone(), table_name.clone());
1484        let base = match type_names.get(&table_key) {
1485            Some(found) => found.clone(),
1486            None => {
1487                let unprefixed = config
1488                    .names
1489                    .base_name(&schema_name, &table_name)
1490                    .map(str::to_string)
1491                    .unwrap_or_else(|| match schema_name == config.default_schema() {
1492                        true => table_name.clone(),
1493                        false => format!("{}_{}", schema_name, table_name),
1494                    });
1495                config.table_type_name(
1496                    &QualifiedIdentifier::new(&schema_name, &table_name),
1497                    &unprefixed,
1498                )
1499            }
1500        };
1501        let key = format!("{}.{}", schema_name, table_name);
1502        let members: Vec<(String, Option<String>)> = config
1503            .enum_values
1504            .get(&key)
1505            .map(|values| {
1506                values
1507                    .iter()
1508                    .filter(|(value, _)| is_graphql_name(value))
1509                    .cloned()
1510                    .collect()
1511            })
1512            .unwrap_or_default();
1513
1514        if members.is_empty() {
1515            // No rows, or none whose value is a legal GraphQL name. An empty
1516            // enum is not a legal type, so the table stays an ordinary one.
1517            tracing::warn!(
1518                "{}.{} is marked as an enumeration but has no values that can name an \
1519                 enum member; exposing it as an ordinary table",
1520                schema_name,
1521                table_name
1522            );
1523            continue;
1524        }
1525
1526        let type_name = format!("{}_enum", base);
1527        enum_type_of.insert((schema_name.clone(), table_name.clone()), type_name.clone());
1528        enum_types.insert(type_name, members);
1529    }
1530
1531    // Retype the columns that point at one.
1532    //
1533    // Read from the catalogue's foreign keys rather than from the
1534    // relationships this schema exposes, which are two different questions: a
1535    // relationship *to* an enum table is never exposed at all -- see just
1536    // below -- and a column's type has nothing to do with whether the row at
1537    // the other end can be traversed to. Keying one on the other meant a table
1538    // whose relationships metadata had declared away lost its enum columns'
1539    // type with them.
1540    if !enum_type_of.is_empty() {
1541        for table in &tables {
1542            let Some(type_name) = type_names.get(&(table.schema.clone(), table.name.clone()))
1543            else {
1544                continue;
1545            };
1546            let found = schema_cache
1547                .get_relationships(&table.qualified_identifier(), &table.schema)
1548                .map(|found| found.to_vec())
1549                .unwrap_or_default();
1550            for relationship in &found {
1551                if !relationship.is_to_one() {
1552                    continue;
1553                }
1554                let foreign = relationship.foreign_table();
1555                let Some(enum_type) =
1556                    enum_type_of.get(&(foreign.schema.clone(), foreign.name.clone()))
1557                else {
1558                    continue;
1559                };
1560                let Some(column) = relationship.single_local_column() else {
1561                    continue;
1562                };
1563                if let Some(object) = object_types.get_mut(type_name) {
1564                    // Under the name the column is exposed as, which is not
1565                    // always its own.
1566                    let exposed = config
1567                        .names
1568                        .column(&object.table.schema, &object.table.name, column)
1569                        .unwrap_or(column)
1570                        .to_string();
1571                    for field in &mut object.fields {
1572                        if field.name == exposed {
1573                            field.graphql_type =
1574                                crate::types::GraphQLType::Custom(enum_type.clone());
1575                        }
1576                    }
1577                }
1578            }
1579        }
1580    }
1581
1582    // A table marked as a set of allowed values is a set of *values*: the
1583    // column typed as the enum is the whole of what a client needs, and there
1584    // is nothing at the other end of a relationship to it worth traversing to.
1585    // Hasura exposes none, and a stray `user { color { ... } }` beside
1586    // `favorite_color` is a second spelling of the same fact.
1587    if !enum_type_of.is_empty() {
1588        for relationships in relationship_fields.values_mut() {
1589            relationships.retain(|relationship| {
1590                let foreign = relationship.relationship.foreign_table();
1591                !enum_type_of.contains_key(&(foreign.schema.clone(), foreign.name.clone()))
1592            });
1593        }
1594    }
1595
1596    // Split each type's fields into the half that may be read and the half
1597    // that may be written. Last, so that everything above -- the computed
1598    // fields' arguments, the retyping of an enum table's columns -- has been
1599    // applied to the whole list before either half is taken out of it.
1600    //
1601    // A role may be granted a column to set that it is not granted to see, so
1602    // the cache carries the union and this is where the two part company. See
1603    // [`crate::role`].
1604    for object in object_types.values_mut() {
1605        // Taken unconditionally, even where no role narrows anything: the
1606        // write inputs are built from this list, and leaving it as it was when
1607        // the type was first made would leave it behind every rewrite since.
1608        object.writable_fields = object.fields.clone();
1609        if let Some(role) = &config.role {
1610            let (schema_name, table_name) =
1611                (object.table.schema.clone(), object.table.name.clone());
1612            let Some(granted) = config.names.permissions(&schema_name, &table_name, role) else {
1613                continue;
1614            };
1615            let table = object.table.clone();
1616            object.fields.retain(|field| {
1617                let column = config
1618                    .names
1619                    .column_source(&schema_name, &table_name, &field.name)
1620                    .unwrap_or(&field.name);
1621                match table.get_column(column) {
1622                    // A computed field rather than a column, and those were
1623                    // reduced to the granted ones with the cache.
1624                    None => true,
1625                    Some(_) => granted
1626                        .select
1627                        .as_ref()
1628                        .is_some_and(|select| select.columns.allows(column)),
1629                }
1630            });
1631        }
1632    }
1633
1634    // Functions that answer with rows of a table it already exposes. One
1635    // returning anything else has no type here to return: a scalar-returning
1636    // function is not a root field, it is an RPC, which the REST surface
1637    // already offers.
1638    let mut function_fields = Vec::new();
1639    for routines in schema_cache.routines.values() {
1640        for routine in routines {
1641            if !config.is_schema_exposed(&routine.schema) || routine.is_procedure {
1642                continue;
1643            }
1644            let postrust_core::schema_cache::RetType::SetOf(returned) = &routine.return_type else {
1645                continue;
1646            };
1647            // A function taking a table's row is that table's computed field,
1648            // not a root field: `fetch_articles(author_row author)` is asked
1649            // of an author, and a row type is not something a client can send
1650            // as an argument. It is already exposed where it belongs.
1651            if routine.params.iter().any(|param| {
1652                let (_, named) = type_name_of(&param.param_type);
1653                base_names.keys().any(|(_, table)| table == &named)
1654            }) {
1655                continue;
1656            }
1657            // `SETOF <table>`, where the table is one this schema exposes. The
1658            // catalogue names the returned type without a schema, so the
1659            // function's own schema is tried first -- a function and the table
1660            // it returns usually live together, and two schemas may both have
1661            // a table of that name.
1662            let (returned_schema, returned_table) = type_name_of(returned);
1663            let found = base_names
1664                .iter()
1665                .find(|((schema, table), _)| {
1666                    table == &returned_table
1667                        && schema == returned_schema.as_ref().unwrap_or(&routine.schema)
1668                })
1669                .or_else(|| {
1670                    base_names
1671                        .iter()
1672                        .find(|((_, table), _)| table == &returned_table)
1673                });
1674            let Some(((target_schema, target_table), _)) = found else {
1675                continue;
1676            };
1677            let target = QualifiedIdentifier::new(target_schema, target_table);
1678            // A function returning rows of a table this role cannot read has
1679            // nowhere to put them: that table has no GraphQL type, and naming
1680            // one that was never registered is a schema that will not build.
1681            if !readable_tables.contains(&target) {
1682                continue;
1683            }
1684            let Some(return_type) = type_names.get(&(target_schema.clone(), target_table.clone()))
1685            else {
1686                continue;
1687            };
1688            let Some(local_return_type) =
1689                local_type_names.get(&(target_schema.clone(), target_table.clone()))
1690            else {
1691                continue;
1692            };
1693            let returns_table = (target_schema.clone(), target_table.clone());
1694
1695            // Placed by what PostgreSQL says it does, unless metadata said
1696            // otherwise: `track_function` with `configuration: {exposed_as:
1697            // query}` puts a VOLATILE function on the query root, which is a
1698            // decision a person made and no catalogue remembers.
1699            let volatile = match config.names.exposed_as(&routine.schema, &routine.name) {
1700                Some("query") => false,
1701                Some("mutation") => true,
1702                _ => matches!(
1703                    routine.volatility,
1704                    postrust_core::schema_cache::FuncVolatility::Volatile
1705                ),
1706            };
1707            // A mutation a role was never granted. Hasura infers a query
1708            // function's permission from the select permission on the table it
1709            // returns -- which is the check just above -- and infers nothing
1710            // for a mutation: reading a table is not permission to change it,
1711            // so the role has to be named. A role granted no mutation at all
1712            // then has no mutation root, and a request for one is answered
1713            // `no mutations exist` rather than by a field that is missing.
1714            if volatile {
1715                if let Some(role) = config.role.as_deref() {
1716                    if !config
1717                        .names
1718                        .function_grants(&routine.schema, &routine.name, role)
1719                    {
1720                        continue;
1721                    }
1722                }
1723            }
1724
1725            function_fields.push(FunctionField {
1726                name: config.root_field_name(&routine.name),
1727                schema_name: routine.schema.clone(),
1728                function_name: routine.name.clone(),
1729                returns: return_type.clone(),
1730                local_returns: local_return_type.clone(),
1731                returns_table,
1732                arguments: routine
1733                    .params
1734                    .iter()
1735                    .filter(|param| !param.name.is_empty())
1736                    .filter(|param| !is_session_argument(param))
1737                    .map(|param| (param.name.clone(), param.param_type.clone(), param.required))
1738                    .collect(),
1739                session_argument: routine
1740                    .params
1741                    .iter()
1742                    .find(|param| is_session_argument(param))
1743                    .map(|param| param.name.clone()),
1744                volatile,
1745                description: routine.description.clone(),
1746            });
1747        }
1748    }
1749    // Sorted by name, and then by signature so that which overload survives
1750    // the deduplication below does not depend on hash order.
1751    function_fields.sort_by(|a, b| {
1752        a.name.cmp(&b.name).then_with(|| {
1753            a.arguments
1754                .len()
1755                .cmp(&b.arguments.len())
1756                .then_with(|| a.argument_signature().cmp(&b.argument_signature()))
1757        })
1758    });
1759
1760    // PostgreSQL overloads functions; GraphQL does not overload fields. Two
1761    // `SETOF` overloads of one name therefore want the same root field, and
1762    // `dynamic::Object::field` *panics* on the second -- so this is not a
1763    // cosmetic choice, it is the difference between starting and not. A schema
1764    // with `f(a int)` and `f(a int, b text)` both returning `SETOF t` crashed
1765    // the server at boot.
1766    //
1767    // The one taking fewest arguments is kept, being the one most callers can
1768    // call, and every other is named in a warning: a function that is not
1769    // exposed should say so rather than be discovered missing.
1770    let mut seen: HashSet<String> = HashSet::new();
1771    function_fields.retain(|function| {
1772        if seen.insert(function.name.clone()) {
1773            return true;
1774        }
1775        tracing::warn!(
1776            "not exposing {}.{}({}) -- another overload of {} is already a root \
1777             field, and GraphQL has no overloading",
1778            function.schema_name,
1779            function.function_name,
1780            function.argument_signature(),
1781            function.name,
1782        );
1783        false
1784    });
1785
1786    GeneratedSchema {
1787        object_types,
1788        query_fields,
1789        mutation_fields,
1790        relationship_fields,
1791        enum_types,
1792        function_fields,
1793        federation_entities,
1794        enable_federation: config.enable_federation,
1795    }
1796}
1797
1798/// Whether a value can name a GraphQL enum member.
1799///
1800/// `[_A-Za-z][_0-9A-Za-z]*`, which is what the specification allows. A value
1801/// that cannot is left out rather than mangled into one: a client asking for
1802/// `light blue` should be told there is no such member, not handed
1803/// `light_blue` and left to wonder which row it means.
1804fn is_graphql_name(value: &str) -> bool {
1805    let mut chars = value.chars();
1806    match chars.next() {
1807        Some(first) if first == '_' || first.is_ascii_alphabetic() => {}
1808        _ => return false,
1809    }
1810    chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
1811}
1812
1813#[cfg(test)]
1814mod tests {
1815    use super::*;
1816    use indexmap::IndexMap;
1817    use postrust_core::schema_cache::Column;
1818    use pretty_assertions::assert_eq;
1819
1820    fn create_test_table(name: &str, insertable: bool, updatable: bool, deletable: bool) -> Table {
1821        let mut columns = IndexMap::new();
1822        columns.insert(
1823            "id".into(),
1824            Column {
1825                name: "id".into(),
1826                description: None,
1827                nullable: false,
1828                data_type: "integer".into(),
1829                nominal_type: "int4".into(),
1830                max_len: None,
1831                default: Some("nextval('id_seq')".into()),
1832                enum_values: vec![],
1833                is_pk: true,
1834                position: 1,
1835                always_generated: false,
1836                domain_type: None,
1837            },
1838        );
1839        columns.insert(
1840            "name".into(),
1841            Column {
1842                name: "name".into(),
1843                description: None,
1844                nullable: false,
1845                data_type: "text".into(),
1846                nominal_type: "text".into(),
1847                max_len: None,
1848                default: None,
1849                enum_values: vec![],
1850                is_pk: false,
1851                position: 2,
1852                always_generated: false,
1853                domain_type: None,
1854            },
1855        );
1856
1857        Table {
1858            schema: "public".into(),
1859            name: name.into(),
1860            description: None,
1861            is_view: false,
1862            insertable,
1863            updatable,
1864            deletable,
1865            pk_cols: vec!["id".into()],
1866            unique_constraints: Vec::new(),
1867            columns,
1868            computed_columns: Default::default(),
1869            is_partitioned: false,
1870        }
1871    }
1872
1873    fn create_test_schema_cache() -> SchemaCache {
1874        use std::collections::{HashMap, HashSet};
1875
1876        let mut tables = HashMap::new();
1877
1878        let users = create_test_table("users", true, true, true);
1879        let posts = create_test_table("posts", true, true, true);
1880        let comments = create_test_table("comments", true, false, false);
1881
1882        tables.insert(users.qualified_identifier(), users);
1883        tables.insert(posts.qualified_identifier(), posts);
1884        tables.insert(comments.qualified_identifier(), comments);
1885
1886        SchemaCache {
1887            tables,
1888            relationships: HashMap::new(),
1889            routines: HashMap::new(),
1890            timezones: HashSet::new(),
1891            media_handlers: HashMap::new(),
1892            pg_version: 150000,
1893            representations: Default::default(),
1894        }
1895    }
1896
1897    #[test]
1898    fn a_many_to_many_relationship_can_be_named_by_its_junction() {
1899        use postrust_core::schema_cache::{Cardinality, Junction, Relationship};
1900
1901        let relationship = Relationship::ForeignKey {
1902            table: QualifiedIdentifier::new("public", "products"),
1903            foreign_table: QualifiedIdentifier::new("public", "product_research"),
1904            is_self: false,
1905            cardinality: Cardinality::M2M(Junction {
1906                table: QualifiedIdentifier::new("public", "product_research_links"),
1907                constraint1: "product_research_links_product_id_fkey".into(),
1908                constraint2: "product_research_links_research_id_fkey".into(),
1909                source_columns: vec![("id".into(), "product_id".into())],
1910                target_columns: vec![("research_id".into(), "id".into())],
1911            }),
1912            table_is_view: false,
1913            foreign_table_is_view: false,
1914            constraint_name: String::new(),
1915        };
1916
1917        assert_eq!(
1918            relationship_keys(&relationship),
1919            vec![
1920                "product_research_links",
1921                "product_research_links_product_id_fkey",
1922                "product_research_links_research_id_fkey"
1923            ]
1924        );
1925
1926        let mut cache = create_test_schema_cache();
1927        let products = create_test_table("products", true, true, true);
1928        let product_research = create_test_table("product_research", true, true, true);
1929        let products_key = products.qualified_identifier();
1930        cache.tables.insert(products_key.clone(), products);
1931        cache
1932            .tables
1933            .insert(product_research.qualified_identifier(), product_research);
1934        cache
1935            .relationships
1936            .insert((products_key, "public".into()), vec![relationship]);
1937
1938        let names = crate::names::NameOverrides::parse(
1939            r#"{"tables": {"public.products": {
1940                "relationships": {"product_research_links": "research"}
1941            }}}"#,
1942        )
1943        .unwrap();
1944        let schema = build_schema(
1945            &cache,
1946            &SchemaConfig {
1947                names,
1948                ..SchemaConfig::default()
1949            },
1950        );
1951        let field = schema
1952            .get_relationship_fields("products")
1953            .and_then(|fields| fields.iter().find(|field| field.name == "research"))
1954            .expect("junction metadata renames the inferred relationship");
1955
1956        assert_eq!(field.target_type, "product_research");
1957        assert!(field.is_list);
1958    }
1959
1960    /// A `SETOF users` function, with the given argument names and types.
1961    fn setof_users_routine(
1962        name: &str,
1963        args: &[(&str, &str)],
1964    ) -> postrust_core::schema_cache::Routine {
1965        use postrust_core::schema_cache::{FuncVolatility, RetType, Routine, RoutineParam};
1966        Routine {
1967            schema: "public".into(),
1968            name: name.into(),
1969            description: None,
1970            params: args
1971                .iter()
1972                .map(|(pname, ptype)| RoutineParam {
1973                    name: (*pname).into(),
1974                    param_type: (*ptype).into(),
1975                    type_max_length: (*ptype).into(),
1976                    required: true,
1977                    variadic: false,
1978                })
1979                .collect(),
1980            return_type: RetType::SetOf("users".into()),
1981            returns_composite: true,
1982            volatility: FuncVolatility::Stable,
1983            has_variadic: false,
1984            isolation_level: None,
1985            settings: Vec::new(),
1986            is_procedure: false,
1987            media_type: None,
1988            media_base_type: None,
1989            output_columns: Vec::new(),
1990        }
1991    }
1992
1993    /// Two `SETOF` overloads of one name want one root field, and
1994    /// `dynamic::Object::field` panics on the second -- so this is the
1995    /// difference between the server starting and not. Reproduced from
1996    /// PostgREST's `overloaded_default` fixture, which crashed it at boot.
1997    #[test]
1998    fn overloads_of_one_function_become_a_single_root_field() {
1999        let mut cache = create_test_schema_cache();
2000        let one = setof_users_routine("overloaded", &[("a", "integer")]);
2001        let two = setof_users_routine("overloaded", &[("a", "integer"), ("b", "text")]);
2002        cache
2003            .routines
2004            .insert(one.qualified_identifier(), vec![two, one]);
2005
2006        let generated = build_schema(&cache, &SchemaConfig::default());
2007        let named: Vec<&str> = generated
2008            .function_fields
2009            .iter()
2010            .filter(|f| f.name == "overloaded")
2011            .map(|f| f.name.as_str())
2012            .collect();
2013        assert_eq!(named.len(), 1, "exactly one root field per function name");
2014    }
2015
2016    /// And which overload survives cannot depend on hash order, or the schema
2017    /// differs between two runs of the same binary against one database.
2018    #[test]
2019    fn the_surviving_overload_is_the_one_taking_fewest_arguments() {
2020        let long = setof_users_routine("overloaded", &[("a", "integer"), ("b", "text")]);
2021        let short = setof_users_routine("overloaded", &[("a", "integer")]);
2022
2023        for order in [vec![long.clone(), short.clone()], vec![short, long]] {
2024            let mut cache = create_test_schema_cache();
2025            cache
2026                .routines
2027                .insert(order[0].qualified_identifier(), order);
2028            let generated = build_schema(&cache, &SchemaConfig::default());
2029            let kept = generated
2030                .function_fields
2031                .iter()
2032                .find(|f| f.name == "overloaded")
2033                .expect("the function is exposed");
2034            assert_eq!(kept.argument_signature(), "a integer");
2035        }
2036    }
2037
2038    // ============================================================================
2039    // SchemaConfig Tests
2040    // ============================================================================
2041
2042    #[test]
2043    fn test_schema_config_default() {
2044        let config = SchemaConfig::default();
2045        assert!(config.is_schema_exposed("public"));
2046        assert!(!config.is_schema_exposed("private"));
2047        assert!(config.enable_mutations);
2048        assert!(!config.enable_subscriptions);
2049    }
2050
2051    #[test]
2052    fn test_schema_config_with_schemas() {
2053        let config =
2054            SchemaConfig::new().with_schemas(vec!["api".to_string(), "public".to_string()]);
2055        assert!(config.is_schema_exposed("api"));
2056        assert!(config.is_schema_exposed("public"));
2057        assert!(!config.is_schema_exposed("private"));
2058    }
2059
2060    #[test]
2061    fn test_schema_config_mutations_disabled() {
2062        let config = SchemaConfig::new().with_mutations(false);
2063        assert!(!config.enable_mutations);
2064    }
2065
2066    // ============================================================================
2067    // QueryField Tests
2068    // ============================================================================
2069
2070    #[test]
2071    fn test_query_field_list() {
2072        let table = create_test_table("users", true, true, true);
2073        let field = QueryField::list(&table);
2074
2075        assert_eq!(field.name, "users");
2076        assert_eq!(field.return_type, "[users!]!");
2077        assert!(field.is_list);
2078        assert!(!field.is_by_pk);
2079    }
2080
2081    #[test]
2082    fn test_query_field_by_pk() {
2083        let table = create_test_table("users", true, true, true);
2084        let field = QueryField::by_pk(&table).unwrap();
2085
2086        assert_eq!(field.name, "users_by_pk");
2087        assert_eq!(field.return_type, "users");
2088        assert!(!field.is_list);
2089        assert!(field.is_by_pk);
2090    }
2091
2092    #[test]
2093    fn test_query_field_by_pk_no_pk() {
2094        let mut table = create_test_table("users", true, true, true);
2095        table.pk_cols = vec![];
2096        let field = QueryField::by_pk(&table);
2097
2098        assert!(field.is_none());
2099    }
2100
2101    // ============================================================================
2102    // MutationField Tests
2103    // ============================================================================
2104
2105    #[test]
2106    fn test_mutation_field_insert() {
2107        let table = create_test_table("users", true, true, true);
2108        let fields = MutationField::insert_fields(&table);
2109
2110        assert_eq!(fields.len(), 2);
2111        assert_eq!(fields[0].name, "insert_users");
2112        assert_eq!(fields[0].mutation_type, MutationType::Insert);
2113        assert_eq!(fields[1].name, "insert_users_one");
2114        assert_eq!(fields[1].mutation_type, MutationType::InsertOne);
2115    }
2116
2117    #[test]
2118    fn test_mutation_field_insert_not_insertable() {
2119        let table = create_test_table("users", false, true, true);
2120        let fields = MutationField::insert_fields(&table);
2121
2122        assert!(fields.is_empty());
2123    }
2124
2125    #[test]
2126    fn test_mutation_field_update() {
2127        let table = create_test_table("users", true, true, true);
2128        let fields = MutationField::update_fields(&table);
2129
2130        assert_eq!(fields.len(), 3);
2131        assert_eq!(fields[0].name, "update_users");
2132        assert_eq!(fields[0].mutation_type, MutationType::Update);
2133        assert_eq!(fields[1].name, "update_users_many");
2134        assert_eq!(fields[1].mutation_type, MutationType::UpdateMany);
2135        assert_eq!(fields[2].name, "update_users_by_pk");
2136        assert_eq!(fields[2].mutation_type, MutationType::UpdateByPk);
2137    }
2138
2139    #[test]
2140    fn test_mutation_field_update_not_updatable() {
2141        let table = create_test_table("users", true, false, true);
2142        let fields = MutationField::update_fields(&table);
2143
2144        assert!(fields.is_empty());
2145    }
2146
2147    #[test]
2148    fn test_mutation_field_delete() {
2149        let table = create_test_table("users", true, true, true);
2150        let fields = MutationField::delete_fields(&table);
2151
2152        assert_eq!(fields.len(), 2);
2153        assert_eq!(fields[0].name, "delete_users");
2154        assert_eq!(fields[0].mutation_type, MutationType::Delete);
2155        assert_eq!(fields[1].name, "delete_users_by_pk");
2156        assert_eq!(fields[1].mutation_type, MutationType::DeleteByPk);
2157    }
2158
2159    #[test]
2160    fn test_mutation_field_delete_not_deletable() {
2161        let table = create_test_table("users", true, true, false);
2162        let fields = MutationField::delete_fields(&table);
2163
2164        assert!(fields.is_empty());
2165    }
2166
2167    // ============================================================================
2168    // Singularize Tests
2169    // ============================================================================
2170
2171    // ============================================================================
2172    // Build Schema Tests
2173    // ============================================================================
2174
2175    #[test]
2176    fn test_build_schema_object_types() {
2177        let cache = create_test_schema_cache();
2178        let config = SchemaConfig::default();
2179        let schema = build_schema(&cache, &config);
2180
2181        assert_eq!(schema.object_types.len(), 3);
2182        assert!(schema.get_object_type("users").is_some());
2183        assert!(schema.get_object_type("posts").is_some());
2184        assert!(schema.get_object_type("comments").is_some());
2185    }
2186
2187    #[test]
2188    fn test_build_schema_query_fields() {
2189        let cache = create_test_schema_cache();
2190        let config = SchemaConfig::default();
2191        let schema = build_schema(&cache, &config);
2192
2193        // 3 tables * 2 (list + byPk) = 6 query fields
2194        assert_eq!(schema.query_fields.len(), 6);
2195
2196        // Check users query fields
2197        let users_field = schema.get_query_field("users").unwrap();
2198        assert_eq!(users_field.name, "users");
2199        assert!(users_field.is_list);
2200    }
2201
2202    #[test]
2203    fn test_build_schema_mutation_fields() {
2204        let cache = create_test_schema_cache();
2205        let config = SchemaConfig::default();
2206        let schema = build_schema(&cache, &config);
2207
2208        // users: 2 insert + 3 update + 2 delete = 7
2209        // posts: 2 insert + 3 update + 2 delete = 7
2210        // comments: 2 insert + 0 update + 0 delete = 2
2211        // Total: 16
2212        assert_eq!(schema.mutation_fields.len(), 16);
2213
2214        let users_mutations = schema.get_mutation_fields("users");
2215        assert_eq!(users_mutations.len(), 7);
2216    }
2217
2218    #[test]
2219    fn test_build_schema_mutations_disabled() {
2220        let cache = create_test_schema_cache();
2221        let config = SchemaConfig::new().with_mutations(false);
2222        let schema = build_schema(&cache, &config);
2223
2224        assert!(schema.mutation_fields.is_empty());
2225    }
2226
2227    #[test]
2228    fn test_build_schema_table_names() {
2229        let cache = create_test_schema_cache();
2230        let config = SchemaConfig::default();
2231        let schema = build_schema(&cache, &config);
2232
2233        let names = schema.table_names();
2234        assert_eq!(names.len(), 3);
2235        assert!(names.contains(&"users"));
2236        assert!(names.contains(&"posts"));
2237        assert!(names.contains(&"comments"));
2238    }
2239
2240    #[test]
2241    fn test_build_schema_type_names() {
2242        let cache = create_test_schema_cache();
2243        let config = SchemaConfig::default();
2244        let schema = build_schema(&cache, &config);
2245
2246        let names = schema.type_names();
2247        assert_eq!(names.len(), 3);
2248        assert!(names.contains(&"users"));
2249        assert!(names.contains(&"posts"));
2250        assert!(names.contains(&"comments"));
2251    }
2252
2253    #[test]
2254    fn type_prefix_namespaces_generated_types_and_roots() {
2255        let mut cache = create_test_schema_cache();
2256        let routine = setof_users_routine("search_users", &[("name", "text")]);
2257        cache
2258            .routines
2259            .insert(routine.qualified_identifier(), vec![routine]);
2260        let config = SchemaConfig {
2261            type_prefix: Some("test".into()),
2262            ..SchemaConfig::default()
2263        };
2264
2265        let schema = build_schema(&cache, &config);
2266
2267        assert!(schema.get_object_type("test_users").is_some());
2268        let users = schema
2269            .query_fields
2270            .iter()
2271            .find(|field| field.table_name == "users" && field.is_list)
2272            .expect("users list field");
2273        assert_eq!(users.name, "test_users");
2274        assert_eq!(users.type_name, "test_users");
2275        assert_eq!(
2276            users.aggregate_name.as_deref(),
2277            Some("test_users_aggregate")
2278        );
2279        assert!(schema
2280            .mutation_fields
2281            .iter()
2282            .any(|field| field.name == "test_insert_users"));
2283        let function = schema
2284            .function_fields
2285            .iter()
2286            .find(|field| field.function_name == "search_users")
2287            .expect("set-returning function field");
2288        assert_eq!(function.name, "test_search_users");
2289        assert_eq!(function.returns, "test_users");
2290    }
2291
2292    #[test]
2293    fn shared_entities_keep_their_type_name_but_not_their_root_names() {
2294        use postrust_core::schema_cache::{Cardinality, Relationship};
2295
2296        let mut cache = create_test_schema_cache();
2297        let posts = QualifiedIdentifier::new("public", "posts");
2298        cache.relationships.insert(
2299            (posts.clone(), "public".into()),
2300            vec![Relationship::ForeignKey {
2301                table: posts,
2302                foreign_table: QualifiedIdentifier::new("public", "users"),
2303                is_self: false,
2304                cardinality: Cardinality::M2O {
2305                    constraint: "posts_user_id_fkey".into(),
2306                    columns: vec![("id".into(), "id".into())],
2307                },
2308                table_is_view: false,
2309                foreign_table_is_view: false,
2310                constraint_name: "posts_user_id_fkey".into(),
2311            }],
2312        );
2313        let config = SchemaConfig {
2314            enable_federation: true,
2315            type_prefix: Some("test".into()),
2316            names: crate::names::NameOverrides::parse(
2317                r#"{"tables": {"public.users": {"federation": {"shared": true}}}}"#,
2318            )
2319            .expect("metadata"),
2320            ..SchemaConfig::default()
2321        };
2322
2323        let schema = build_schema(&cache, &config);
2324
2325        let users_object = schema.get_object_type("users").expect("users object");
2326        assert_eq!(users_object.local_name, "test_users");
2327        let posts_object = schema.get_object_type("test_posts").expect("posts object");
2328        assert_eq!(posts_object.local_name, "test_posts");
2329        let users = schema
2330            .query_fields
2331            .iter()
2332            .find(|field| field.table_name == "users" && field.is_list)
2333            .expect("users list field");
2334        assert_eq!(users.name, "test_users");
2335        assert_eq!(users.type_name, "users");
2336        assert_eq!(users.local_type_name, "test_users");
2337        let posts_relationships = schema
2338            .get_relationship_fields("test_posts")
2339            .expect("posts relationships");
2340        assert_eq!(posts_relationships[0].name, "user");
2341        assert_eq!(posts_relationships[0].target_type, "users");
2342        assert_eq!(posts_relationships[0].target_local_name, "test_users");
2343    }
2344
2345    #[test]
2346    fn federation_entities_are_generated_only_when_enabled() {
2347        let cache = create_test_schema_cache();
2348
2349        let disabled = build_schema(&cache, &SchemaConfig::default());
2350        assert!(!disabled.enable_federation);
2351        assert!(disabled.federation_entities.is_empty());
2352
2353        let enabled = build_schema(
2354            &cache,
2355            &SchemaConfig {
2356                enable_federation: true,
2357                type_prefix: Some("test".into()),
2358                names: crate::names::NameOverrides::parse(
2359                    r#"{"tables": {"public.users": {"federation": {"shared": true}}}}"#,
2360                )
2361                .expect("metadata"),
2362                ..SchemaConfig::default()
2363            },
2364        );
2365
2366        assert!(enabled.enable_federation);
2367        let users = enabled
2368            .get_federation_entity("users")
2369            .expect("shared users entity");
2370        assert_eq!(users.type_name, "users");
2371        assert_eq!(users.table, QualifiedIdentifier::new("public", "users"));
2372        assert_eq!(users.key_fields_sdl(), "id");
2373        assert!(users.shared);
2374
2375        let posts = enabled
2376            .get_federation_entity("test_posts")
2377            .expect("prefixed posts entity");
2378        assert_eq!(posts.type_name, "test_posts");
2379        assert_eq!(posts.key_fields_sdl(), "id");
2380        assert!(!posts.shared);
2381    }
2382
2383    #[test]
2384    fn test_build_schema_exposed_schemas() {
2385        let mut cache = create_test_schema_cache();
2386
2387        // Add a table in a different schema
2388        let private_table = Table {
2389            schema: "private".into(),
2390            name: "secrets".into(),
2391            description: None,
2392            is_view: false,
2393            insertable: true,
2394            updatable: true,
2395            deletable: true,
2396            pk_cols: vec!["id".into()],
2397            unique_constraints: Vec::new(),
2398            columns: indexmap::IndexMap::new(),
2399            computed_columns: Default::default(),
2400            is_partitioned: false,
2401        };
2402        cache
2403            .tables
2404            .insert(private_table.qualified_identifier(), private_table);
2405
2406        let config = SchemaConfig::default(); // Only exposes "public"
2407        let schema = build_schema(&cache, &config);
2408
2409        // Should only have 3 tables from public schema
2410        assert_eq!(schema.object_types.len(), 3);
2411        assert!(schema.get_object_type("Secrets").is_none());
2412    }
2413
2414    // ============================================================================
2415    // GeneratedSchema Tests
2416    // ============================================================================
2417
2418    #[test]
2419    fn test_generated_schema_get_object_type() {
2420        let cache = create_test_schema_cache();
2421        let config = SchemaConfig::default();
2422        let schema = build_schema(&cache, &config);
2423
2424        let users = schema.get_object_type("users").unwrap();
2425        assert_eq!(users.table.name, "users");
2426    }
2427
2428    #[test]
2429    fn test_generated_schema_get_query_field() {
2430        let cache = create_test_schema_cache();
2431        let config = SchemaConfig::default();
2432        let schema = build_schema(&cache, &config);
2433
2434        let field = schema.get_query_field("posts").unwrap();
2435        assert_eq!(field.table_name, "posts");
2436    }
2437
2438    #[test]
2439    fn test_generated_schema_get_mutation_fields() {
2440        let cache = create_test_schema_cache();
2441        let config = SchemaConfig::default();
2442        let schema = build_schema(&cache, &config);
2443
2444        let fields = schema.get_mutation_fields("comments");
2445        // comments is only insertable
2446        assert_eq!(fields.len(), 2); // insert_comments + insert_comments_one
2447    }
2448}