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