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