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