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