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