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