Skip to main content

postrust_core/schema_cache/
mod.rs

1//! PostgreSQL schema introspection and caching.
2//!
3//! This module provides functionality to discover and cache database schema
4//! metadata including tables, columns, relationships, and functions.
5
6mod queries;
7mod relationship;
8mod routine;
9mod table;
10
11pub use relationship::{
12    Cardinality, Junction, MediaHandler, MediaHandlerMap, Relationship, RelationshipsMap,
13};
14pub use routine::{FuncVolatility, RetType, Routine, RoutineMap, RoutineParam};
15pub use table::{Column, ColumnMap, ComputedColumn, Table, TablesMap};
16
17use crate::api_request::QualifiedIdentifier;
18use crate::error::{Error, Result};
19use sqlx::PgPool;
20use std::collections::{HashMap, HashSet};
21use std::sync::Arc;
22use tracing::info;
23
24/// Cached PostgreSQL schema metadata.
25#[derive(Clone, Debug)]
26pub struct SchemaCache {
27    /// Tables and views by qualified identifier.
28    pub tables: TablesMap,
29    /// Relationships between tables.
30    pub relationships: RelationshipsMap,
31    /// Stored functions/procedures.
32    pub routines: RoutineMap,
33    /// Valid timezone names.
34    pub timezones: HashSet<String>,
35    /// PostgreSQL version.
36    pub pg_version: i32,
37    /// User-defined renderers for media types, by (schema, media type).
38    pub media_handlers: MediaHandlerMap,
39    /// Casts between a domain and `json`/`text`, by (source, target) type.
40    ///
41    /// A schema uses these to decide how a value of one of its domains is
42    /// written on the wire and how one written by a client is read back.
43    pub representations: std::collections::HashMap<(String, String), String>,
44}
45
46impl SchemaCache {
47    /// Load schema cache from the database.
48    pub async fn load(pool: &PgPool, schemas: &[String]) -> Result<Self> {
49        Self::load_with_search_path(pool, schemas, &[]).await
50    }
51
52    /// Load the cache, resolving functions along `extra_search_path` as well.
53    ///
54    /// A computed column's function is reached the way the database reaches
55    /// it, which is along the search path -- so it may live in a schema the
56    /// API does not expose. The table it reads must still be exposed.
57    pub async fn load_with_search_path(
58        pool: &PgPool,
59        schemas: &[String],
60        extra_search_path: &[String],
61    ) -> Result<Self> {
62        info!("Loading schema cache for schemas: {:?}", schemas);
63
64        let function_schemas: Vec<String> = schemas
65            .iter()
66            .chain(extra_search_path)
67            .cloned()
68            .collect::<std::collections::BTreeSet<_>>()
69            .into_iter()
70            .collect();
71
72        // Get PostgreSQL version
73        let pg_version = queries::get_pg_version(pool).await?;
74        info!("PostgreSQL version: {}", pg_version);
75
76        // Load tables and columns
77        let mut tables = queries::load_tables(pool, schemas).await?;
78        info!("Loaded {} tables/views", tables.len());
79
80        // Which base-table columns each view carries. Loaded before the
81        // relationships, because a view's base table may live in a schema
82        // that is not itself exposed -- `private.articles` behind a public
83        // view -- and its foreign keys are needed all the same.
84        let view_columns = queries::load_view_columns(pool, schemas).await?;
85
86        // Functions that read as columns. Attached to the tables they belong
87        // to, so resolving a field name has one place to look.
88        for computed in queries::load_computed_columns(pool, schemas, &function_schemas).await? {
89            if let Some(table) = tables.get_mut(&computed.table) {
90                table.computed_columns.insert(
91                    computed.name,
92                    ComputedColumn {
93                        function: computed.function,
94                        data_type: computed.return_type,
95                        description: computed.description,
96                        row_argument: computed.row_argument,
97                        session_argument: computed.session_argument,
98                        takes_arguments: computed.takes_arguments,
99                    },
100                );
101            }
102        }
103
104        let mut relationship_schemas: Vec<String> = schemas.to_vec();
105        for column in &view_columns {
106            if !relationship_schemas.contains(&column.base.schema) {
107                relationship_schemas.push(column.base.schema.clone());
108            }
109        }
110
111        // Load relationships
112        let relationships = queries::load_relationships(pool, &relationship_schemas).await?;
113        info!("Loaded {} relationship sets", relationships.len());
114
115        // Load routines
116        let routines = queries::load_routines(pool, schemas).await?;
117        info!("Loaded {} routines", routines.len());
118
119        // Load timezone names
120        let timezones = queries::load_timezones(pool).await?;
121        info!("Loaded {} timezones", timezones.len());
122
123        // Junctions first, then views: a many-to-many derived from a
124        // junction is itself a relationship a view can carry, and the
125        // junction may live in a schema that is not exposed.
126        let primary_keys = queries::load_primary_keys(pool, &relationship_schemas).await?;
127
128        // Which uniqueness an upsert resolves against has to be named, and a
129        // table may have several.
130        let mut tables = tables;
131        for (qi, constraints) in queries::load_unique_constraints(pool, schemas).await? {
132            if let Some(table) = tables.get_mut(&qi) {
133                table.unique_constraints = constraints;
134            }
135        }
136
137        let mut relationships = relationships;
138        add_junction_relationships(&primary_keys, &mut relationships);
139        substitute_hidden_junctions(&tables, &view_columns, &mut relationships);
140        add_view_relationships(&tables, &view_columns, &mut relationships);
141
142        // A view over a junction is a junction. It has no key of its own, so
143        // this runs only once the view's own relationships exist -- which is
144        // why the pass is here and not with the first one.
145        let view_keys = view_primary_keys(&primary_keys, &view_columns);
146        add_junction_relationships(&view_keys, &mut relationships);
147
148        let media_handlers = queries::load_media_handlers(pool, schemas).await?;
149        info!("Loaded {} media type handlers", media_handlers.len());
150
151        let representations = queries::load_representations(pool).await?;
152        info!("Loaded {} data representations", representations.len());
153
154        Ok(Self {
155            tables,
156            relationships,
157            routines,
158            timezones,
159            pg_version,
160            media_handlers,
161            representations,
162        })
163    }
164
165    /// Get a table by qualified identifier.
166    pub fn get_table(&self, qi: &QualifiedIdentifier) -> Option<&Table> {
167        self.tables.get(qi)
168    }
169
170    /// Get a table, returning an error if not found.
171    pub fn require_table(&self, qi: &QualifiedIdentifier) -> Result<&Table> {
172        self.get_table(qi).ok_or_else(|| Error::TableNotFound {
173            name: qi.to_string(),
174            suggestion: self.similar_table(qi),
175        })
176    }
177
178    /// The exposed table whose name is closest to one that does not exist.
179    ///
180    /// PostgREST answers a schema-cache miss with a suggestion where there is
181    /// an obviously intended table -- `projectx` for `projects` -- and with
182    /// nothing where there is not, which is what the similarity floor decides.
183    /// Below it the "suggestion" would be noise, and would also say more about
184    /// the schema than the client asked.
185    fn similar_table(&self, qi: &QualifiedIdentifier) -> Option<String> {
186        const MIN_SIMILARITY: f64 = 0.5;
187
188        // Scored by shared character n-grams rather than by edit distance.
189        // A Levenshtein ratio cannot tell `items` from `items3` as the table
190        // `itemsx` was meant to be -- both are one edit away, so both score
191        // 0.833 and the winner is whichever the map happened to yield last.
192        // Counting 3-grams separates them, because it notices that one
193        // candidate is the query's own length and the other is not: `items`
194        // scores 0.73 against `itemsx` and `items3` scores 0.67.
195        let asked = grams(&qi.name, 3);
196
197        self.tables
198            .keys()
199            .filter(|candidate| candidate.schema == qi.schema)
200            .map(|candidate| (cosine(&asked, &grams(&candidate.name, 3)), candidate))
201            .filter(|(score, _)| *score >= MIN_SIMILARITY)
202            // Ties go to the shorter name, and then to the earlier one, so
203            // that the suggestion does not depend on iteration order.
204            .max_by(|(a, left), (b, right)| {
205                a.total_cmp(b)
206                    .then_with(|| right.name.len().cmp(&left.name.len()))
207                    .then_with(|| right.name.cmp(&left.name))
208            })
209            .map(|(_, candidate)| candidate.to_string())
210    }
211
212    /// Get relationships for a table.
213    pub fn get_relationships(
214        &self,
215        qi: &QualifiedIdentifier,
216        schema: &str,
217    ) -> Option<&Vec<Relationship>> {
218        self.relationships.get(&(qi.clone(), schema.to_string()))
219    }
220
221    /// Get a routine by qualified identifier.
222    pub fn get_routines(&self, qi: &QualifiedIdentifier) -> Option<&Vec<Routine>> {
223        self.routines.get(qi)
224    }
225
226    /// Check if a timezone is valid.
227    pub fn is_valid_timezone(&self, tz: &str) -> bool {
228        self.timezones.contains(tz)
229    }
230
231    /// Get a summary of the cached schema.
232    pub fn summary(&self) -> String {
233        format!(
234            "SchemaCache: {} tables, {} relationship sets, {} routines, PG {}",
235            self.tables.len(),
236            self.relationships.len(),
237            self.routines.len(),
238            self.pg_version
239        )
240    }
241
242    /// The table whose rows a function returns, if it returns rows of one.
243    ///
244    /// This is what makes a function behave like a table for everything after
245    /// the call: its result can be selected from, filtered, ordered, paged and
246    /// embedded on, all of which need a table to resolve columns against.
247    ///
248    /// `format_type` may or may not qualify the name, depending on the search
249    /// path it was rendered against, so both spellings are tried -- and an
250    /// unqualified one is looked for in the function's own schema.
251    pub fn routine_returned_table(&self, qi: &QualifiedIdentifier) -> Option<&Table> {
252        self.returned_table(self.get_routines(qi)?.first()?)
253    }
254
255    /// The same, for a routine already chosen.
256    ///
257    /// A name may carry several signatures returning different things, so
258    /// which overload was selected decides which table the result is shaped
259    /// by -- and only the caller knows that.
260    pub fn returned_table(&self, routine: &Routine) -> Option<&Table> {
261        // Only a row type names a table. Without this, a function returning
262        // `xml` is shaped by whatever relation happens to be called `xml` --
263        // and the fixtures have one, so this is not hypothetical.
264        if !routine.returns_composite {
265            return None;
266        }
267        let type_name = routine.return_type.type_name()?;
268        let candidate = match type_name.split_once('.') {
269            Some((schema, name)) => QualifiedIdentifier::new(schema, name),
270            None => QualifiedIdentifier::new(&routine.schema, type_name),
271        };
272        self.get_table(&candidate)
273    }
274
275    /// The function that renders a value of `pg_type` as `target`, if the
276    /// schema declared one.
277    pub fn representation(&self, pg_type: &str, target: &str) -> Option<&str> {
278        self.representations
279            .get(&(pg_type.to_string(), target.to_string()))
280            .map(String::as_str)
281    }
282
283    /// Find a user-defined renderer for a media type on a given table.
284    ///
285    /// A handler declared for the table itself wins over one taking
286    /// `anyelement`, which is the schema-wide fallback.
287    /// A schema may also declare a handler for `*/*`, which renders whatever
288    /// was asked for. What comes back is the handler and the media type the
289    /// response should be labelled with -- not always the one requested, since
290    /// `*/*` is not a type a response can claim to be. PostgREST resolves it
291    /// to `application/octet-stream`, which is what a body of unnamed bytes
292    /// is, and this follows it.
293    /// A handler declared for one table renders that table as it is, so it
294    /// only applies to a request that asked for the table as it is:
295    /// `?select=id` is a different shape from the one the handler was written
296    /// against, and running it over that shape produces either nonsense or an
297    /// error. PostgREST guards both table-specific lookups on the selection
298    /// being the default one, and `default_select` is that guard.
299    pub fn media_handler<'a>(
300        &'a self,
301        schema: &str,
302        media_type: &'a str,
303        table: &QualifiedIdentifier,
304        default_select: bool,
305    ) -> Option<(&'a MediaHandler, &'a str)> {
306        let named = |media: &str| {
307            let candidates = self
308                .media_handlers
309                .get(&(schema.to_string(), media.to_string()))?;
310            candidates
311                .iter()
312                .find(|h| default_select && h.table.as_ref() == Some(table))
313                .or_else(|| candidates.iter().find(|h| h.table.is_none()))
314        };
315
316        match named(media_type) {
317            Some(handler) => Some((handler, media_type)),
318            // `*/*` is declared on a table, so it is a table lookup too.
319            None if default_select => {
320                named("*/*").map(|handler| (handler, "application/octet-stream"))
321            }
322            None => None,
323        }
324    }
325
326    /// The error to report when no relationship connects two resources.
327    ///
328    /// Built here rather than at the call site because the suggestion needs
329    /// the relationships the origin actually has, which is what the client
330    /// most likely meant to name.
331    pub fn relationship_not_found(
332        &self,
333        from: &QualifiedIdentifier,
334        to_name: &str,
335        hint: Option<&str>,
336        schema: &str,
337    ) -> Error {
338        // Looser than the table suggestion: a relationship is named by the far
339        // table, and a client that got it wrong tends to have got it wrong by
340        // a whole word -- `car_model_sales_202101` for `car_model_sales`.
341        const MIN_SIMILARITY: f64 = 0.4;
342
343        let related: Vec<&str> = self
344            .get_relationships(from, schema)
345            .into_iter()
346            .flatten()
347            .map(|rel| rel.foreign_table().name.as_str())
348            .collect();
349
350        // A relationship of exactly that name exists, so the name is not what
351        // went wrong -- the hint is, and there is nothing to suggest. Dropping
352        // only the exact match and then offering the next-nearest name told a
353        // client that asked for `person!space` that it had perhaps meant
354        // `person_detail`, when what it had meant was `person` all along.
355        let suggestion = match related.contains(&to_name) {
356            true => None,
357            false => related
358                .iter()
359                .map(|name| (similarity(name, to_name), *name))
360                .filter(|(score, _)| *score >= MIN_SIMILARITY)
361                .max_by(|(a, _), (b, _)| a.total_cmp(b))
362                .map(|(_, name)| name.to_string()),
363        };
364
365        Error::RelationshipNotFound {
366            origin: from.name.clone(),
367            target: to_name.to_string(),
368            hint: hint.map(str::to_string),
369            schema: schema.to_string(),
370            suggestion,
371        }
372    }
373
374    /// The exposed function whose name is closest to one that does not exist.
375    ///
376    /// Only when the name itself is wrong: an existing name whose arguments do
377    /// not match is a different mistake, and the overloads answer it better.
378    pub fn similar_routine(&self, qi: &QualifiedIdentifier) -> Option<String> {
379        const MIN_SIMILARITY: f64 = 0.75;
380
381        self.routines
382            .keys()
383            .filter(|candidate| candidate.schema == qi.schema)
384            .map(|candidate| (similarity(&candidate.name, &qi.name), candidate))
385            .filter(|(score, _)| *score >= MIN_SIMILARITY)
386            .max_by(|(a, _), (b, _)| a.total_cmp(b))
387            .map(|(_, candidate)| candidate.to_string())
388    }
389
390    /// Find a relationship between two tables by name.
391    ///
392    /// `hint` is the `!hint` of `person_detail!message_sender_fkey(name)`,
393    /// which names the relationship when the two resources are connected by
394    /// more than one.
395    ///
396    /// Returns `Ok(None)` when nothing matches, and an error when several do
397    /// and no hint told them apart -- picking one arbitrarily would answer a
398    /// question the client did not ask.
399    pub fn find_relationship(
400        &self,
401        from: &QualifiedIdentifier,
402        to_name: &str,
403        hint: Option<&str>,
404        schema: &str,
405    ) -> Result<Option<&Relationship>> {
406        let Some(candidates) = self.get_relationships(from, schema) else {
407            return Ok(None);
408        };
409
410        // An embedding may be named three ways, and they are tried in the
411        // order of how specifically they identify one relationship.
412        //
413        // The target table's name is the usual spelling, but it stops
414        // identifying anything as soon as two foreign keys point at the same
415        // table. The foreign key constraint names exactly one relationship,
416        // and so does the column that joins them -- `/messages?select=*,
417        // sender(*)` embeds through the `sender` column, and
418        // `/projects?select=*,client(*)` through the constraint called
419        // `client`.
420        let by_target: Vec<&Relationship> = candidates
421            .iter()
422            .filter(|r| match r {
423                // A computed relationship is named by its function. Two
424                // functions may return the same table, so the table's name
425                // would not tell them apart.
426                Relationship::Computed { function, .. } => function.name == to_name,
427                // One foreign key pointing at its own table is two
428                // relationships, and the table's name alone cannot tell them
429                // apart. The convention is PostgREST's: the table's name means
430                // the rows that point here -- the children -- and the key's own
431                // column means the row this one points at. A hint names the
432                // column the children point with, and so always means children.
433                r if r.is_self_referential() => match hint {
434                    None => {
435                        (r.foreign_table().name == to_name && r.is_one_to_many())
436                            || (r.single_local_column() == Some(to_name) && !r.is_one_to_many())
437                    }
438                    Some(hint) => {
439                        r.foreign_table().name == to_name
440                            && r.is_one_to_many()
441                            && r.single_foreign_column() == Some(hint)
442                    }
443                },
444                Relationship::ForeignKey { foreign_table, .. } => foreign_table.name == to_name,
445            })
446            .collect();
447
448        // A relationship projected onto a view carries the constraint and
449        // columns of the base one it came from, so naming either of those
450        // would match both. Only the view's own name selects the projection;
451        // the constraint and the column mean the relationship they belong to.
452        // PostgreSQL cannot point a foreign key at a view, so a view on the
453        // far side is exactly what marks a projection.
454        let declared = |r: &&Relationship| !matches!(r, Relationship::ForeignKey { foreign_table_is_view, .. } if *foreign_table_is_view);
455
456        // A computed relationship declared under a name overrides whatever the
457        // catalogue found under it. That is deliberate on the schema author's
458        // part -- naming the function after the table is how a schema replaces
459        // the derived relationship with one of its own -- so the two are not
460        // an ambiguity to report back.
461        let by_target = match by_target
462            .iter()
463            .any(|r| matches!(r, Relationship::Computed { .. }))
464        {
465            true => by_target
466                .into_iter()
467                .filter(|r| matches!(r, Relationship::Computed { .. }))
468                .collect(),
469            false => by_target,
470        };
471
472        let mut matches = match by_target.is_empty() {
473            false => by_target,
474            true => {
475                let by_constraint: Vec<&Relationship> = candidates
476                    .iter()
477                    .filter(declared)
478                    .filter(|r| r.constraint_name() == to_name)
479                    .collect();
480                match by_constraint.is_empty() {
481                    false => by_constraint,
482                    true => candidates
483                        .iter()
484                        .filter(declared)
485                        .filter(|r| r.join_columns().iter().any(|(local, _)| local == to_name))
486                        .collect(),
487                }
488            }
489        };
490
491        // A self-referential match has already accounted for the hint, and the
492        // general rule would reject it: both directions join on the same pair
493        // of columns, so the hint names them both.
494        if let Some(hint) = hint {
495            matches.retain(|r| r.is_self_referential() || r.matches_hint(hint));
496        }
497
498        match matches.len() {
499            0 => Ok(None),
500            1 => Ok(Some(matches[0])),
501            _ => Err(Error::AmbiguousRelationship {
502                origin: from.name.clone(),
503                target: to_name.to_string(),
504                candidates: matches
505                    .iter()
506                    .map(|r| (r.cardinality_name().to_string(), r.describe()))
507                    .collect(),
508                // What the client would have to write instead. The details
509                // say which relationships were found; this says how to ask for
510                // one of them, which is the part it can act on.
511                disambiguated: matches
512                    .iter()
513                    .map(|r| format!("{}!{}", to_name, r.disambiguator()))
514                    .collect(),
515            }),
516        }
517    }
518}
519
520/// How alike two names are, from 0 (nothing in common) to 1 (identical).
521///
522/// Edit distance over the longer of the two, which is enough to tell a typo
523/// from a different word.
524pub(crate) fn similarity(a: &str, b: &str) -> f64 {
525    let longest = a.chars().count().max(b.chars().count());
526    if longest == 0 {
527        return 1.0;
528    }
529    1.0 - edit_distance(a, b) as f64 / longest as f64
530}
531
532/// The closest of `candidates` to `query`, or `None` if none is close enough.
533///
534/// Scored the way PostgREST scores its hints: cosine similarity over the
535/// character n-grams of both strings, three at a time and falling back to two
536/// where three finds nothing, with the ends marked so that a shared prefix
537/// counts for something. A Levenshtein ratio, which is the obvious thing to
538/// reach for, answers differently on the cases that matter -- it calls
539/// `(any_arg)` and `(name)` a third alike, because they are both short, where
540/// sharing no letter sequence at all is what a client needs to be told.
541///
542/// The floor is strict, and applies to the similarity; among what clears it,
543/// the nearest by edit distance wins. A floor of zero therefore means "shares
544/// any sequence of characters at all", which is a real filter -- two strings
545/// with no run of two characters in common score nothing and never reach the
546/// ranking.
547pub(crate) fn closest<'a>(
548    candidates: impl IntoIterator<Item = &'a str> + Clone,
549    query: &str,
550    min_score: f64,
551) -> Option<&'a str> {
552    for size in [3usize, 2] {
553        let asked = grams(query, size);
554        let mut fitting: Vec<&str> = candidates
555            .clone()
556            .into_iter()
557            .filter(|candidate| cosine(&asked, &grams(candidate, size)) > min_score)
558            .collect();
559
560        // Ranked by edit distance only among those the similarity admitted,
561        // which is why a short unrelated name never surfaces: it never got
562        // this far.
563        fitting.sort_by(|a, b| {
564            similarity(&normalize(b), &normalize(query))
565                .total_cmp(&similarity(&normalize(a), &normalize(query)))
566        });
567        if let Some(best) = fitting.first() {
568            return Some(best);
569        }
570    }
571    None
572}
573
574/// A string reduced to what a comparison should see.
575///
576/// Punctuation is dropped, so `(name)` and `name` are the same word; commas
577/// and spaces are kept, because a parameter list is compared as the list it
578/// is written as.
579fn normalize(value: &str) -> String {
580    value
581        .chars()
582        .flat_map(char::to_lowercase)
583        .filter(|c| c.is_alphanumeric() || *c == ',' || *c == ' ')
584        .collect()
585}
586
587/// How many times each run of `size` characters occurs, ends marked.
588fn grams(value: &str, size: usize) -> HashMap<String, usize> {
589    let mut padded: Vec<char> = std::iter::once('-')
590        .chain(normalize(value).chars())
591        .chain(std::iter::once('-'))
592        .collect();
593    while padded.len() < size {
594        padded.push('-');
595    }
596
597    let mut counts: HashMap<String, usize> = HashMap::new();
598    for window in padded.windows(size) {
599        *counts.entry(window.iter().collect()).or_insert(0) += 1;
600    }
601    counts
602}
603
604/// The cosine of the angle between two gram counts.
605fn cosine(a: &HashMap<String, usize>, b: &HashMap<String, usize>) -> f64 {
606    let magnitude = |counts: &HashMap<String, usize>| -> f64 {
607        counts.values().map(|n| (n * n) as f64).sum::<f64>().sqrt()
608    };
609    let (a_size, b_size) = (magnitude(a), magnitude(b));
610    if a_size == 0.0 || b_size == 0.0 {
611        return 0.0;
612    }
613    let shared: f64 = a
614        .iter()
615        .map(|(gram, count)| (count * b.get(gram).copied().unwrap_or(0)) as f64)
616        .sum();
617    shared / (a_size * b_size)
618}
619
620/// Levenshtein distance, computed one row at a time.
621fn edit_distance(a: &str, b: &str) -> usize {
622    let b: Vec<char> = b.chars().collect();
623    let mut previous: Vec<usize> = (0..=b.len()).collect();
624    let mut current = vec![0; b.len() + 1];
625
626    for (i, a_char) in a.chars().enumerate() {
627        current[0] = i + 1;
628        for (j, b_char) in b.iter().enumerate() {
629            let substitution = previous[j] + usize::from(a_char != *b_char);
630            current[j + 1] = substitution.min(previous[j + 1] + 1).min(current[j] + 1);
631        }
632        std::mem::swap(&mut previous, &mut current);
633    }
634
635    previous[b.len()]
636}
637
638/// Route a many-to-many through the exposed view of its junction.
639///
640/// The junction is often the one table of the three that the API does not
641/// expose -- `private.personnages` behind `test.personnages` -- and joining
642/// through the base table is a `permission denied for schema private` at
643/// request time even though the relationship itself is real. Where a view over
644/// it is exposed and carries both keys, that view is joined through instead.
645fn substitute_hidden_junctions(
646    tables: &TablesMap,
647    view_columns: &[queries::ViewColumn],
648    relationships: &mut RelationshipsMap,
649) {
650    use std::collections::HashMap;
651
652    // Which name an exposed view gives each of a base table's columns.
653    type ColumnRenames<'a> = HashMap<&'a str, &'a str>;
654    /// The exposed views over one base table, each with its renames.
655    type ExposedViews<'a> = Vec<(&'a QualifiedIdentifier, ColumnRenames<'a>)>;
656
657    let mut views_over: HashMap<&QualifiedIdentifier, ExposedViews<'_>> = HashMap::new();
658    for column in view_columns {
659        if !tables.contains_key(&column.view) {
660            continue;
661        }
662        let entry = views_over.entry(&column.base).or_default();
663        match entry.iter_mut().find(|(view, _)| *view == &column.view) {
664            Some((_, mapping)) => {
665                mapping.insert(&column.base_column, &column.view_column);
666            }
667            None => {
668                let mut mapping = HashMap::new();
669                mapping.insert(column.base_column.as_str(), column.view_column.as_str());
670                entry.push((&column.view, mapping));
671            }
672        }
673    }
674
675    for rel in relationships.values_mut().flatten() {
676        let Relationship::ForeignKey {
677            cardinality: Cardinality::M2M(junction),
678            ..
679        } = rel
680        else {
681            continue;
682        };
683        if tables.contains_key(&junction.table) {
684            continue;
685        }
686
687        let needed: Vec<&str> = junction
688            .source_columns
689            .iter()
690            .map(|(_, through)| through.as_str())
691            .chain(
692                junction
693                    .target_columns
694                    .iter()
695                    .map(|(through, _)| through.as_str()),
696            )
697            .collect();
698
699        let Some((view, mapping)) = views_over.get(&junction.table).and_then(|views| {
700            views
701                .iter()
702                .find(|(_, mapping)| needed.iter().all(|name| mapping.contains_key(name)))
703        }) else {
704            // Nothing exposes the junction, so nothing can join through it.
705            // Marked rather than removed here, since the relationship is being
706            // iterated; the sweep below drops them.
707            junction.table = QualifiedIdentifier::new("", "");
708            continue;
709        };
710
711        junction.table = (*view).clone();
712        for (_, through) in junction.source_columns.iter_mut() {
713            if let Some(renamed) = mapping.get(through.as_str()) {
714                *through = (*renamed).to_string();
715            }
716        }
717        for (through, _) in junction.target_columns.iter_mut() {
718            if let Some(renamed) = mapping.get(through.as_str()) {
719                *through = (*renamed).to_string();
720            }
721        }
722    }
723
724    // Offering an unreachable relationship would answer `permission denied for
725    // schema private` where PostgREST answers that no relationship was found,
726    // which is both the truer answer and the one that says nothing about a
727    // schema the client was never shown.
728    for rels in relationships.values_mut() {
729        rels.retain(|rel| {
730            !matches!(
731                rel,
732                Relationship::ForeignKey {
733                    cardinality: Cardinality::M2M(junction),
734                    ..
735                } if junction.table.name.is_empty()
736            )
737        });
738    }
739    relationships.retain(|_, rels| !rels.is_empty());
740}
741
742/// Project relationships from base tables onto the views that select them.
743///
744/// A view carrying both ends of a foreign key can stand in for either side of
745/// it, so `/articleStars?select=*,articles(*)` embeds through the key on
746/// `private.article_stars` without the client ever naming the private table.
747///
748/// The join columns are rewritten to the names the view gives them: a view is
749/// free to rename what it selects, and the projected relationship has to join
750/// on the names the client can actually see. Where a view selects one base
751/// column twice under different names, each spelling is a relationship of its
752/// own -- they are genuinely different joins, and `!t1_id1` is how a client
753/// tells them apart.
754fn add_view_relationships(
755    tables: &TablesMap,
756    view_columns: &[queries::ViewColumn],
757    relationships: &mut RelationshipsMap,
758) {
759    use std::collections::HashMap;
760
761    // base table -> view -> base column -> the names that view gives it
762    type ColumnNames = HashMap<String, Vec<String>>;
763    let mut views_over: HashMap<&QualifiedIdentifier, HashMap<&QualifiedIdentifier, ColumnNames>> =
764        HashMap::new();
765    for column in view_columns {
766        views_over
767            .entry(&column.base)
768            .or_default()
769            .entry(&column.view)
770            .or_default()
771            .entry(column.base_column.clone())
772            .or_default()
773            .push(column.view_column.clone());
774    }
775
776    let existing: Vec<((QualifiedIdentifier, String), Relationship)> = relationships
777        .iter()
778        .flat_map(|(key, rels)| rels.iter().map(move |rel| (key.clone(), rel.clone())))
779        .collect();
780
781    let mut derived: Vec<((QualifiedIdentifier, String), Relationship)> = Vec::new();
782
783    for ((source, _), rel) in &existing {
784        let Relationship::ForeignKey {
785            foreign_table,
786            cardinality,
787            table_is_view,
788            foreign_table_is_view,
789            constraint_name,
790            ..
791        } = rel
792        else {
793            continue;
794        };
795
796        // Which columns each end has to carry for the view to stand in for
797        // it. A many-to-many joins through a junction, so the two ends are
798        // described by different halves of it -- the near side by the columns
799        // going into the junction, the far side by the ones coming out.
800        let (near_needs, far_needs): (Vec<String>, Vec<String>) = match cardinality {
801            Cardinality::M2M(junction) => (
802                junction
803                    .source_columns
804                    .iter()
805                    .map(|(near, _)| near.clone())
806                    .collect(),
807                junction
808                    .target_columns
809                    .iter()
810                    .map(|(_, far)| far.clone())
811                    .collect(),
812            ),
813            _ => {
814                let columns = cardinality.columns();
815                (
816                    columns.iter().map(|(near, _)| near.clone()).collect(),
817                    columns.iter().map(|(_, far)| far.clone()).collect(),
818                )
819            }
820        };
821
822        // Every way the view can spell the columns this end joins on. Empty
823        // when it does not carry all of them, which is what disqualifies it
824        // from standing in at all.
825        let spellings = |view_cols: &ColumnNames, needed: &[String]| -> Vec<Vec<String>> {
826            let mut combinations: Vec<Vec<String>> = vec![vec![]];
827            for name in needed {
828                let Some(candidates) = view_cols.get(name) else {
829                    return vec![];
830                };
831                combinations = combinations
832                    .into_iter()
833                    .flat_map(|prefix| {
834                        candidates.iter().map(move |candidate| {
835                            let mut extended = prefix.clone();
836                            extended.push(candidate.clone());
837                            extended
838                        })
839                    })
840                    .collect();
841            }
842            combinations
843        };
844
845        // The view stands in for the near side.
846        if let Some(views) = views_over.get(source) {
847            for (view, view_cols) in views {
848                for near in spellings(view_cols, &near_needs) {
849                    derived.push((
850                        ((*view).clone(), view.schema.clone()),
851                        Relationship::ForeignKey {
852                            table: (*view).clone(),
853                            foreign_table: foreign_table.clone(),
854                            is_self: *view == foreign_table,
855                            cardinality: rename_cardinality(cardinality, Some(&near), None),
856                            table_is_view: true,
857                            foreign_table_is_view: *foreign_table_is_view,
858                            constraint_name: constraint_name.clone(),
859                        },
860                    ));
861                }
862            }
863        }
864
865        // The view stands in for the far side, and likewise.
866        if let Some(views) = views_over.get(foreign_table) {
867            for (view, view_cols) in views {
868                for far in spellings(view_cols, &far_needs) {
869                    derived.push((
870                        (source.clone(), source.schema.clone()),
871                        Relationship::ForeignKey {
872                            table: source.clone(),
873                            foreign_table: (*view).clone(),
874                            is_self: source == *view,
875                            cardinality: rename_cardinality(cardinality, None, Some(&far)),
876                            table_is_view: *table_is_view,
877                            foreign_table_is_view: true,
878                            constraint_name: constraint_name.clone(),
879                        },
880                    ));
881                }
882            }
883        }
884
885        // Both sides are views. Neither of the cases above covers it: each
886        // replaces one end and leaves the other as the base table.
887        if let (Some(near_views), Some(far_views)) =
888            (views_over.get(source), views_over.get(foreign_table))
889        {
890            for (near_view, near_cols) in near_views {
891                for near in spellings(near_cols, &near_needs) {
892                    for (far_view, far_cols) in far_views {
893                        for far in spellings(far_cols, &far_needs) {
894                            derived.push((
895                                ((*near_view).clone(), near_view.schema.clone()),
896                                Relationship::ForeignKey {
897                                    table: (*near_view).clone(),
898                                    foreign_table: (*far_view).clone(),
899                                    is_self: near_view == far_view,
900                                    cardinality: rename_cardinality(
901                                        cardinality,
902                                        Some(&near),
903                                        Some(&far),
904                                    ),
905                                    table_is_view: true,
906                                    foreign_table_is_view: true,
907                                    constraint_name: constraint_name.clone(),
908                                },
909                            ));
910                        }
911                    }
912                }
913            }
914        }
915    }
916
917    // A projection is only useful if the client can name both ends. The base
918    // table behind a view is often in a schema that is not exposed, and
919    // offering it as a target would produce a relationship that resolves to a
920    // table the request has no permission to read.
921    for (key, rel) in derived {
922        if !tables.contains_key(&key.0) || !tables.contains_key(rel.foreign_table()) {
923            continue;
924        }
925        relationships.entry(key).or_default().push(rel);
926    }
927}
928
929/// A cardinality with one or both ends' columns renamed.
930///
931/// `near` and `far` are given in the order the cardinality lists them, so the
932/// nth name replaces the nth column. `None` leaves that end alone.
933fn rename_cardinality(
934    cardinality: &Cardinality,
935    near: Option<&[String]>,
936    far: Option<&[String]>,
937) -> Cardinality {
938    let rename_pairs = |columns: &[(String, String)]| -> Vec<(String, String)> {
939        columns
940            .iter()
941            .enumerate()
942            .map(|(i, (local, foreign))| {
943                (
944                    near.and_then(|names| names.get(i)).unwrap_or(local).clone(),
945                    far.and_then(|names| names.get(i))
946                        .unwrap_or(foreign)
947                        .clone(),
948                )
949            })
950            .collect()
951    };
952
953    match cardinality {
954        Cardinality::O2M {
955            constraint,
956            columns,
957        } => Cardinality::O2M {
958            constraint: constraint.clone(),
959            columns: rename_pairs(columns),
960        },
961        Cardinality::M2O {
962            constraint,
963            columns,
964        } => Cardinality::M2O {
965            constraint: constraint.clone(),
966            columns: rename_pairs(columns),
967        },
968        Cardinality::O2O {
969            constraint,
970            columns,
971            is_parent,
972        } => Cardinality::O2O {
973            constraint: constraint.clone(),
974            columns: rename_pairs(columns),
975            is_parent: *is_parent,
976        },
977        // Only the outer ends move: the junction keeps its own column names,
978        // since the view stands in for a side of the join and not for the
979        // table doing the joining.
980        Cardinality::M2M(junction) => {
981            let mut junction = junction.clone();
982            if let Some(names) = near {
983                for (i, (source, _)) in junction.source_columns.iter_mut().enumerate() {
984                    if let Some(name) = names.get(i) {
985                        *source = name.clone();
986                    }
987                }
988            }
989            if let Some(names) = far {
990                for (i, (_, target)) in junction.target_columns.iter_mut().enumerate() {
991                    if let Some(name) = names.get(i) {
992                        *target = name.clone();
993                    }
994                }
995            }
996            Cardinality::M2M(junction)
997        }
998    }
999}
1000
1001/// The key a view inherits from the table it selects from.
1002///
1003/// A view over a junction is a junction: `main_jobs` selects the columns of
1004/// `jobs` that make it one, and PostgREST embeds `sites` through it and
1005/// through `jobs` alike -- reporting both as candidates when the two cannot be
1006/// told apart. A view has no key of its own, so the base table's is mapped
1007/// through the names the view gives those columns.
1008///
1009/// Only where the view exposes every one of them: a key missing a column is
1010/// not that key, and a junction is exactly the relation whose key *is* the
1011/// pairing.
1012fn view_primary_keys(
1013    primary_keys: &std::collections::HashMap<QualifiedIdentifier, Vec<String>>,
1014    view_columns: &[queries::ViewColumn],
1015) -> std::collections::HashMap<QualifiedIdentifier, Vec<String>> {
1016    use std::collections::HashMap;
1017
1018    // view -> base table -> base column -> the name the view gives it
1019    let mut by_view: HashMap<
1020        &QualifiedIdentifier,
1021        HashMap<&QualifiedIdentifier, HashMap<&str, &str>>,
1022    > = HashMap::new();
1023    for column in view_columns {
1024        by_view
1025            .entry(&column.view)
1026            .or_default()
1027            .entry(&column.base)
1028            .or_default()
1029            .entry(column.base_column.as_str())
1030            .or_insert(column.view_column.as_str());
1031    }
1032
1033    let mut keys = HashMap::new();
1034    for (view, bases) in by_view {
1035        for (base, columns) in bases {
1036            let Some(key) = primary_keys.get(base).filter(|key| !key.is_empty()) else {
1037                continue;
1038            };
1039            let mapped: Option<Vec<String>> = key
1040                .iter()
1041                .map(|column| columns.get(column.as_str()).map(|name| name.to_string()))
1042                .collect();
1043            if let Some(mapped) = mapped {
1044                keys.insert(view.clone(), mapped);
1045            }
1046        }
1047    }
1048
1049    keys
1050}
1051
1052/// Derive many-to-many relationships from junction tables.
1053///
1054/// A junction is a table that exists only to join two others: it has exactly
1055/// two foreign keys, to two different tables, and its primary key is precisely
1056/// the columns of those keys. That last condition is what separates a junction
1057/// from an ordinary table that happens to reference two others, which would
1058/// otherwise sprout a relationship it has no business having.
1059///
1060/// Both sides get the relationship, named after the table across the junction,
1061/// so `/users?select=name,tasks(name)` works from either end.
1062fn add_junction_relationships(
1063    primary_keys: &std::collections::HashMap<QualifiedIdentifier, Vec<String>>,
1064    relationships: &mut RelationshipsMap,
1065) {
1066    let mut derived: Vec<((QualifiedIdentifier, String), Relationship)> = Vec::new();
1067
1068    for (junction_qi, junction_pk) in primary_keys {
1069        let key = (junction_qi.clone(), junction_qi.schema.clone());
1070        let Some(rels) = relationships.get(&key) else {
1071            continue;
1072        };
1073
1074        // The keys out of the junction, however many columns each is written
1075        // over. Requiring one column apiece looked like what separates a
1076        // junction from a table that merely references two others, but it is
1077        // not: `touched_files` joins `files(project_id, filename)` to
1078        // `users_tasks(user_id, task_id)` and is as much a junction as any
1079        // other. What separates them is the primary-key test below, which this
1080        // duplicated and then over-applied -- so a composite junction was
1081        // answered "no relationship" for a relationship that is plainly there.
1082        let outgoing: Vec<(&Relationship, Vec<(String, String)>)> = rels
1083            .iter()
1084            .filter(|r| matches!(r, Relationship::ForeignKey { table, .. } if table == junction_qi))
1085            .filter(|r| r.is_to_one())
1086            .filter_map(|r| {
1087                let mut columns = r.join_columns();
1088                columns.dedup();
1089                match columns.is_empty() {
1090                    true => None,
1091                    false => Some((r, columns)),
1092                }
1093            })
1094            .collect();
1095
1096        let pk: Vec<&str> = junction_pk.iter().map(String::as_str).collect();
1097
1098        // The pair whose columns are part of the primary key is what makes
1099        // this a junction: keying on both of them is what says a row of it is
1100        // one pairing. A junction may carry more in its key than the pair --
1101        // `group_yard` keys on `(id, group_id, yard_id)` -- and requiring the
1102        // key to be exactly the two columns missed those.
1103        for near in 0..outgoing.len() {
1104            for far in 0..outgoing.len() {
1105                if near == far {
1106                    continue;
1107                }
1108                let (near_rel, near_columns) = &outgoing[near];
1109                let (far_rel, far_columns) = &outgoing[far];
1110                if near_rel.foreign_table() == far_rel.foreign_table() {
1111                    continue;
1112                }
1113                // Every column either key is written over has to be part of
1114                // the junction's own key: that is what says a row of it is one
1115                // pairing and nothing more.
1116                if !near_columns
1117                    .iter()
1118                    .chain(far_columns)
1119                    .all(|(local, _)| pk.contains(&local.as_str()))
1120                {
1121                    continue;
1122                }
1123
1124                let source = near_rel.foreign_table().clone();
1125                let target = far_rel.foreign_table().clone();
1126
1127                derived.push((
1128                    (source.clone(), source.schema.clone()),
1129                    Relationship::ForeignKey {
1130                        table: source,
1131                        foreign_table: target,
1132                        is_self: false,
1133                        cardinality: Cardinality::M2M(Junction {
1134                            table: junction_qi.clone(),
1135                            constraint1: near_rel.constraint_name().to_string(),
1136                            constraint2: far_rel.constraint_name().to_string(),
1137                            // Source to junction, then junction to target,
1138                            // each in the order the constraint declares so a
1139                            // composite key joins column for column.
1140                            source_columns: near_columns
1141                                .iter()
1142                                .map(|(local, foreign)| (foreign.clone(), local.clone()))
1143                                .collect(),
1144                            target_columns: far_columns
1145                                .iter()
1146                                .map(|(local, foreign)| (local.clone(), foreign.clone()))
1147                                .collect(),
1148                        }),
1149                        table_is_view: false,
1150                        foreign_table_is_view: false,
1151                        constraint_name: String::new(),
1152                    },
1153                ));
1154            }
1155        }
1156    }
1157
1158    for (key, rel) in derived {
1159        relationships.entry(key).or_default().push(rel);
1160    }
1161}
1162
1163/// Thread-safe schema cache wrapper.
1164#[derive(Clone)]
1165pub struct SchemaCacheRef(Arc<tokio::sync::RwLock<Option<SchemaCache>>>);
1166
1167impl SchemaCacheRef {
1168    /// Create a new empty schema cache reference.
1169    pub fn new() -> Self {
1170        Self(Arc::new(tokio::sync::RwLock::new(None)))
1171    }
1172
1173    /// Create a schema cache reference from a static cache.
1174    pub fn from_static(cache: SchemaCache) -> Self {
1175        Self(Arc::new(tokio::sync::RwLock::new(Some(cache))))
1176    }
1177
1178    /// Load or reload the schema cache.
1179    pub async fn load(&self, pool: &PgPool, schemas: &[String]) -> Result<()> {
1180        let cache = SchemaCache::load(pool, schemas).await?;
1181        let mut guard = self.0.write().await;
1182        *guard = Some(cache);
1183        Ok(())
1184    }
1185
1186    /// Get a read reference to the schema cache.
1187    pub async fn get(&self) -> Result<tokio::sync::RwLockReadGuard<'_, Option<SchemaCache>>> {
1188        let guard = self.0.read().await;
1189        if guard.is_none() {
1190            return Err(Error::SchemaCacheNotLoaded);
1191        }
1192        Ok(guard)
1193    }
1194
1195    /// Check if the cache is loaded.
1196    pub async fn is_loaded(&self) -> bool {
1197        self.0.read().await.is_some()
1198    }
1199}
1200
1201impl Default for SchemaCacheRef {
1202    fn default() -> Self {
1203        Self::new()
1204    }
1205}