mod queries;
mod relationship;
mod routine;
mod table;
pub use relationship::{
Cardinality, Junction, MediaHandler, MediaHandlerMap, Relationship, RelationshipsMap,
};
pub use routine::{FuncVolatility, RetType, Routine, RoutineMap, RoutineParam};
#[cfg(feature = "sqlx")]
pub use queries::enum_values;
pub use table::{Column, ColumnMap, ComputedColumn, Table, TablesMap};
use crate::api_request::QualifiedIdentifier;
use crate::db::{Db, Tx};
use crate::error::{Error, Result};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use tracing::info;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SchemaCache {
#[serde(with = "pairs")]
pub tables: TablesMap,
#[serde(with = "pairs")]
pub relationships: RelationshipsMap,
#[serde(with = "pairs")]
pub routines: RoutineMap,
pub timezones: HashSet<String>,
pub pg_version: i32,
#[serde(with = "pairs")]
pub media_handlers: MediaHandlerMap,
#[serde(with = "pairs")]
pub representations: std::collections::HashMap<(String, String), String>,
}
mod pairs {
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::HashMap;
use std::hash::Hash;
pub fn serialize<K, V, S>(map: &HashMap<K, V>, serializer: S) -> Result<S::Ok, S::Error>
where
K: Serialize,
V: Serialize,
S: Serializer,
{
serializer.collect_seq(map.iter())
}
pub fn deserialize<'de, K, V, D>(deserializer: D) -> Result<HashMap<K, V>, D::Error>
where
K: Deserialize<'de> + Eq + Hash,
V: Deserialize<'de>,
D: Deserializer<'de>,
{
Ok(Vec::<(K, V)>::deserialize(deserializer)?
.into_iter()
.collect())
}
}
impl SchemaCache {
#[cfg(feature = "sqlx")]
pub async fn load(pool: &sqlx::PgPool, schemas: &[String]) -> Result<Self> {
Self::load_from(pool, schemas).await
}
#[cfg(feature = "sqlx")]
pub async fn load_with_search_path(
pool: &sqlx::PgPool,
schemas: &[String],
extra_search_path: &[String],
) -> Result<Self> {
Self::load_with_search_path_from(pool, schemas, extra_search_path).await
}
pub async fn load_from<D: Db>(db: &D, schemas: &[String]) -> Result<Self> {
Self::load_with_search_path_from(db, schemas, &[]).await
}
pub async fn load_with_search_path_from<D: Db>(
db: &D,
schemas: &[String],
extra_search_path: &[String],
) -> Result<Self> {
info!("Loading schema cache for schemas: {:?}", schemas);
let function_schemas: Vec<String> = schemas
.iter()
.chain(extra_search_path)
.cloned()
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect();
let failed = |e: crate::db::DbError| Error::SchemaCacheLoadFailed(e.to_string());
let mut tx = db.begin().await.map_err(failed)?;
let pg_version = queries::get_pg_version(&mut tx).await?;
info!("PostgreSQL version: {}", pg_version);
let mut tables = queries::load_tables(&mut tx, schemas).await?;
info!("Loaded {} tables/views", tables.len());
let view_columns = queries::load_view_columns(&mut tx, schemas).await?;
for computed in queries::load_computed_columns(&mut tx, schemas, &function_schemas).await? {
if let Some(table) = tables.get_mut(&computed.table) {
table.computed_columns.insert(
computed.name,
ComputedColumn {
function: computed.function,
data_type: computed.return_type,
description: computed.description,
row_argument: computed.row_argument,
session_argument: computed.session_argument,
takes_arguments: computed.takes_arguments,
},
);
}
}
let mut relationship_schemas: Vec<String> = schemas.to_vec();
for column in &view_columns {
if !relationship_schemas.contains(&column.base.schema) {
relationship_schemas.push(column.base.schema.clone());
}
}
let relationships = queries::load_relationships(&mut tx, &relationship_schemas).await?;
info!("Loaded {} relationship sets", relationships.len());
let routines = queries::load_routines(&mut tx, schemas).await?;
info!("Loaded {} routines", routines.len());
let timezones = queries::load_timezones(&mut tx).await?;
info!("Loaded {} timezones", timezones.len());
let primary_keys = queries::load_primary_keys(&mut tx, &relationship_schemas).await?;
let mut tables = tables;
for (qi, constraints) in queries::load_unique_constraints(&mut tx, schemas).await? {
if let Some(table) = tables.get_mut(&qi) {
table.unique_constraints = constraints;
}
}
let mut relationships = relationships;
add_junction_relationships(&primary_keys, &mut relationships);
substitute_hidden_junctions(&tables, &view_columns, &mut relationships);
add_view_relationships(&tables, &view_columns, &mut relationships);
let view_keys = view_primary_keys(&primary_keys, &view_columns);
add_junction_relationships(&view_keys, &mut relationships);
let media_handlers = queries::load_media_handlers(&mut tx, schemas).await?;
info!("Loaded {} media type handlers", media_handlers.len());
let representations = queries::load_representations(&mut tx).await?;
info!("Loaded {} data representations", representations.len());
tx.commit().await.map_err(failed)?;
Ok(Self {
tables,
relationships,
routines,
timezones,
pg_version,
media_handlers,
representations,
})
}
pub fn get_table(&self, qi: &QualifiedIdentifier) -> Option<&Table> {
self.tables.get(qi)
}
pub fn require_table(&self, qi: &QualifiedIdentifier) -> Result<&Table> {
self.get_table(qi).ok_or_else(|| Error::TableNotFound {
name: qi.to_string(),
suggestion: self.similar_table(qi),
})
}
fn similar_table(&self, qi: &QualifiedIdentifier) -> Option<String> {
const MIN_SIMILARITY: f64 = 0.5;
let asked = grams(&qi.name, 3);
self.tables
.keys()
.filter(|candidate| candidate.schema == qi.schema)
.map(|candidate| (cosine(&asked, &grams(&candidate.name, 3)), candidate))
.filter(|(score, _)| *score >= MIN_SIMILARITY)
.max_by(|(a, left), (b, right)| {
a.total_cmp(b)
.then_with(|| right.name.len().cmp(&left.name.len()))
.then_with(|| right.name.cmp(&left.name))
})
.map(|(_, candidate)| candidate.to_string())
}
pub fn get_relationships(
&self,
qi: &QualifiedIdentifier,
schema: &str,
) -> Option<&Vec<Relationship>> {
self.relationships.get(&(qi.clone(), schema.to_string()))
}
pub fn get_routines(&self, qi: &QualifiedIdentifier) -> Option<&Vec<Routine>> {
self.routines.get(qi)
}
pub fn is_valid_timezone(&self, tz: &str) -> bool {
self.timezones.contains(tz)
}
pub fn summary(&self) -> String {
format!(
"SchemaCache: {} tables, {} relationship sets, {} routines, PG {}",
self.tables.len(),
self.relationships.len(),
self.routines.len(),
self.pg_version
)
}
pub fn routine_returned_table(&self, qi: &QualifiedIdentifier) -> Option<&Table> {
self.returned_table(self.get_routines(qi)?.first()?)
}
pub fn returned_table(&self, routine: &Routine) -> Option<&Table> {
if !routine.returns_composite {
return None;
}
let type_name = routine.return_type.type_name()?;
let candidate = match type_name.split_once('.') {
Some((schema, name)) => QualifiedIdentifier::new(schema, name),
None => QualifiedIdentifier::new(&routine.schema, type_name),
};
self.get_table(&candidate)
}
pub fn representation(&self, pg_type: &str, target: &str) -> Option<&str> {
self.representations
.get(&(pg_type.to_string(), target.to_string()))
.map(String::as_str)
}
pub fn media_handler<'a>(
&'a self,
schema: &str,
media_type: &'a str,
table: &QualifiedIdentifier,
default_select: bool,
) -> Option<(&'a MediaHandler, &'a str)> {
let named = |media: &str| {
let candidates = self
.media_handlers
.get(&(schema.to_string(), media.to_string()))?;
candidates
.iter()
.find(|h| default_select && h.table.as_ref() == Some(table))
.or_else(|| candidates.iter().find(|h| h.table.is_none()))
};
match named(media_type) {
Some(handler) => Some((handler, media_type)),
None if default_select => {
named("*/*").map(|handler| (handler, "application/octet-stream"))
}
None => None,
}
}
pub fn relationship_not_found(
&self,
from: &QualifiedIdentifier,
to_name: &str,
hint: Option<&str>,
schema: &str,
) -> Error {
const MIN_SIMILARITY: f64 = 0.4;
let related: Vec<&str> = self
.get_relationships(from, schema)
.into_iter()
.flatten()
.map(|rel| rel.foreign_table().name.as_str())
.collect();
let suggestion = match related.contains(&to_name) {
true => None,
false => related
.iter()
.map(|name| (similarity(name, to_name), *name))
.filter(|(score, _)| *score >= MIN_SIMILARITY)
.max_by(|(a, _), (b, _)| a.total_cmp(b))
.map(|(_, name)| name.to_string()),
};
Error::RelationshipNotFound {
origin: from.name.clone(),
target: to_name.to_string(),
hint: hint.map(str::to_string),
schema: schema.to_string(),
suggestion,
}
}
pub fn similar_routine(&self, qi: &QualifiedIdentifier) -> Option<String> {
const MIN_SIMILARITY: f64 = 0.75;
self.routines
.keys()
.filter(|candidate| candidate.schema == qi.schema)
.map(|candidate| (similarity(&candidate.name, &qi.name), candidate))
.filter(|(score, _)| *score >= MIN_SIMILARITY)
.max_by(|(a, _), (b, _)| a.total_cmp(b))
.map(|(_, candidate)| candidate.to_string())
}
pub fn find_relationship(
&self,
from: &QualifiedIdentifier,
to_name: &str,
hint: Option<&str>,
schema: &str,
) -> Result<Option<&Relationship>> {
let Some(candidates) = self.get_relationships(from, schema) else {
return Ok(None);
};
let by_target: Vec<&Relationship> = candidates
.iter()
.filter(|r| match r {
Relationship::Computed { function, .. } => function.name == to_name,
r if r.is_self_referential() => match hint {
None => {
(r.foreign_table().name == to_name && r.is_one_to_many())
|| (r.single_local_column() == Some(to_name) && !r.is_one_to_many())
}
Some(hint) => {
r.foreign_table().name == to_name
&& r.is_one_to_many()
&& r.single_foreign_column() == Some(hint)
}
},
Relationship::ForeignKey { foreign_table, .. } => foreign_table.name == to_name,
})
.collect();
let declared = |r: &&Relationship| !matches!(r, Relationship::ForeignKey { foreign_table_is_view, .. } if *foreign_table_is_view);
let by_target = match by_target
.iter()
.any(|r| matches!(r, Relationship::Computed { .. }))
{
true => by_target
.into_iter()
.filter(|r| matches!(r, Relationship::Computed { .. }))
.collect(),
false => by_target,
};
let mut matches = match by_target.is_empty() {
false => by_target,
true => {
let by_constraint: Vec<&Relationship> = candidates
.iter()
.filter(declared)
.filter(|r| r.constraint_name() == to_name)
.collect();
match by_constraint.is_empty() {
false => by_constraint,
true => candidates
.iter()
.filter(declared)
.filter(|r| r.join_columns().iter().any(|(local, _)| local == to_name))
.collect(),
}
}
};
if let Some(hint) = hint {
matches.retain(|r| r.is_self_referential() || r.matches_hint(hint));
}
matches.sort_by(|a, b| {
fn rank(name: &str) -> u8 {
match name {
"one-to-many" => 0,
"one-to-one" => 1,
"many-to-one" => 2,
_ => 3,
}
}
rank(a.cardinality_name())
.cmp(&rank(b.cardinality_name()))
.then_with(|| a.describe().cmp(&b.describe()))
});
match matches.len() {
0 => Ok(None),
1 => Ok(Some(matches[0])),
_ => Err(Error::AmbiguousRelationship {
origin: from.name.clone(),
target: to_name.to_string(),
candidates: matches
.iter()
.map(|r| (r.cardinality_name().to_string(), r.describe()))
.collect(),
disambiguated: matches
.iter()
.map(|r| format!("{}!{}", to_name, r.disambiguator()))
.collect(),
}),
}
}
}
pub(crate) fn similarity(a: &str, b: &str) -> f64 {
let longest = a.chars().count().max(b.chars().count());
if longest == 0 {
return 1.0;
}
1.0 - edit_distance(a, b) as f64 / longest as f64
}
pub(crate) fn closest<'a>(
candidates: impl IntoIterator<Item = &'a str> + Clone,
query: &str,
min_score: f64,
) -> Option<&'a str> {
for size in [3usize, 2] {
let asked = grams(query, size);
let mut fitting: Vec<&str> = candidates
.clone()
.into_iter()
.filter(|candidate| cosine(&asked, &grams(candidate, size)) > min_score)
.collect();
fitting.sort_by(|a, b| {
similarity(&normalize(b), &normalize(query))
.total_cmp(&similarity(&normalize(a), &normalize(query)))
});
if let Some(best) = fitting.first() {
return Some(best);
}
}
None
}
fn normalize(value: &str) -> String {
value
.chars()
.flat_map(char::to_lowercase)
.filter(|c| c.is_alphanumeric() || *c == ',' || *c == ' ')
.collect()
}
fn grams(value: &str, size: usize) -> HashMap<String, usize> {
let mut padded: Vec<char> = std::iter::once('-')
.chain(normalize(value).chars())
.chain(std::iter::once('-'))
.collect();
while padded.len() < size {
padded.push('-');
}
let mut counts: HashMap<String, usize> = HashMap::new();
for window in padded.windows(size) {
*counts.entry(window.iter().collect()).or_insert(0) += 1;
}
counts
}
fn cosine(a: &HashMap<String, usize>, b: &HashMap<String, usize>) -> f64 {
let magnitude = |counts: &HashMap<String, usize>| -> f64 {
counts.values().map(|n| (n * n) as f64).sum::<f64>().sqrt()
};
let (a_size, b_size) = (magnitude(a), magnitude(b));
if a_size == 0.0 || b_size == 0.0 {
return 0.0;
}
let shared: f64 = a
.iter()
.map(|(gram, count)| (count * b.get(gram).copied().unwrap_or(0)) as f64)
.sum();
shared / (a_size * b_size)
}
fn edit_distance(a: &str, b: &str) -> usize {
let b: Vec<char> = b.chars().collect();
let mut previous: Vec<usize> = (0..=b.len()).collect();
let mut current = vec![0; b.len() + 1];
for (i, a_char) in a.chars().enumerate() {
current[0] = i + 1;
for (j, b_char) in b.iter().enumerate() {
let substitution = previous[j] + usize::from(a_char != *b_char);
current[j + 1] = substitution.min(previous[j + 1] + 1).min(current[j] + 1);
}
std::mem::swap(&mut previous, &mut current);
}
previous[b.len()]
}
fn substitute_hidden_junctions(
tables: &TablesMap,
view_columns: &[queries::ViewColumn],
relationships: &mut RelationshipsMap,
) {
use std::collections::HashMap;
type ColumnRenames<'a> = HashMap<&'a str, &'a str>;
type ExposedViews<'a> = Vec<(&'a QualifiedIdentifier, ColumnRenames<'a>)>;
let mut views_over: HashMap<&QualifiedIdentifier, ExposedViews<'_>> = HashMap::new();
for column in view_columns {
if !tables.contains_key(&column.view) {
continue;
}
let entry = views_over.entry(&column.base).or_default();
match entry.iter_mut().find(|(view, _)| *view == &column.view) {
Some((_, mapping)) => {
mapping.insert(&column.base_column, &column.view_column);
}
None => {
let mut mapping = HashMap::new();
mapping.insert(column.base_column.as_str(), column.view_column.as_str());
entry.push((&column.view, mapping));
}
}
}
for rel in relationships.values_mut().flatten() {
let Relationship::ForeignKey {
cardinality: Cardinality::M2M(junction),
..
} = rel
else {
continue;
};
if tables.contains_key(&junction.table) {
continue;
}
let needed: Vec<&str> = junction
.source_columns
.iter()
.map(|(_, through)| through.as_str())
.chain(
junction
.target_columns
.iter()
.map(|(through, _)| through.as_str()),
)
.collect();
let Some((view, mapping)) = views_over.get(&junction.table).and_then(|views| {
views
.iter()
.find(|(_, mapping)| needed.iter().all(|name| mapping.contains_key(name)))
}) else {
junction.table = QualifiedIdentifier::new("", "");
continue;
};
junction.table = (*view).clone();
for (_, through) in junction.source_columns.iter_mut() {
if let Some(renamed) = mapping.get(through.as_str()) {
*through = (*renamed).to_string();
}
}
for (through, _) in junction.target_columns.iter_mut() {
if let Some(renamed) = mapping.get(through.as_str()) {
*through = (*renamed).to_string();
}
}
}
for rels in relationships.values_mut() {
rels.retain(|rel| {
!matches!(
rel,
Relationship::ForeignKey {
cardinality: Cardinality::M2M(junction),
..
} if junction.table.name.is_empty()
)
});
}
relationships.retain(|_, rels| !rels.is_empty());
}
fn add_view_relationships(
tables: &TablesMap,
view_columns: &[queries::ViewColumn],
relationships: &mut RelationshipsMap,
) {
use std::collections::HashMap;
type ColumnNames = HashMap<String, Vec<String>>;
let mut views_over: HashMap<&QualifiedIdentifier, HashMap<&QualifiedIdentifier, ColumnNames>> =
HashMap::new();
for column in view_columns {
views_over
.entry(&column.base)
.or_default()
.entry(&column.view)
.or_default()
.entry(column.base_column.clone())
.or_default()
.push(column.view_column.clone());
}
let existing: Vec<((QualifiedIdentifier, String), Relationship)> = relationships
.iter()
.flat_map(|(key, rels)| rels.iter().map(move |rel| (key.clone(), rel.clone())))
.collect();
let mut derived: Vec<((QualifiedIdentifier, String), Relationship)> = Vec::new();
for ((source, _), rel) in &existing {
let Relationship::ForeignKey {
foreign_table,
cardinality,
table_is_view,
foreign_table_is_view,
constraint_name,
..
} = rel
else {
continue;
};
let (near_needs, far_needs): (Vec<String>, Vec<String>) = match cardinality {
Cardinality::M2M(junction) => (
junction
.source_columns
.iter()
.map(|(near, _)| near.clone())
.collect(),
junction
.target_columns
.iter()
.map(|(_, far)| far.clone())
.collect(),
),
_ => {
let columns = cardinality.columns();
(
columns.iter().map(|(near, _)| near.clone()).collect(),
columns.iter().map(|(_, far)| far.clone()).collect(),
)
}
};
let spellings = |view_cols: &ColumnNames, needed: &[String]| -> Vec<Vec<String>> {
let mut combinations: Vec<Vec<String>> = vec![vec![]];
for name in needed {
let Some(candidates) = view_cols.get(name) else {
return vec![];
};
combinations = combinations
.into_iter()
.flat_map(|prefix| {
candidates.iter().map(move |candidate| {
let mut extended = prefix.clone();
extended.push(candidate.clone());
extended
})
})
.collect();
}
combinations
};
if let Some(views) = views_over.get(source) {
for (view, view_cols) in views {
for near in spellings(view_cols, &near_needs) {
derived.push((
((*view).clone(), view.schema.clone()),
Relationship::ForeignKey {
table: (*view).clone(),
foreign_table: foreign_table.clone(),
is_self: *view == foreign_table,
cardinality: rename_cardinality(cardinality, Some(&near), None),
table_is_view: true,
foreign_table_is_view: *foreign_table_is_view,
constraint_name: constraint_name.clone(),
},
));
}
}
}
if let Some(views) = views_over.get(foreign_table) {
for (view, view_cols) in views {
for far in spellings(view_cols, &far_needs) {
derived.push((
(source.clone(), source.schema.clone()),
Relationship::ForeignKey {
table: source.clone(),
foreign_table: (*view).clone(),
is_self: source == *view,
cardinality: rename_cardinality(cardinality, None, Some(&far)),
table_is_view: *table_is_view,
foreign_table_is_view: true,
constraint_name: constraint_name.clone(),
},
));
}
}
}
if let (Some(near_views), Some(far_views)) =
(views_over.get(source), views_over.get(foreign_table))
{
for (near_view, near_cols) in near_views {
for near in spellings(near_cols, &near_needs) {
for (far_view, far_cols) in far_views {
for far in spellings(far_cols, &far_needs) {
derived.push((
((*near_view).clone(), near_view.schema.clone()),
Relationship::ForeignKey {
table: (*near_view).clone(),
foreign_table: (*far_view).clone(),
is_self: near_view == far_view,
cardinality: rename_cardinality(
cardinality,
Some(&near),
Some(&far),
),
table_is_view: true,
foreign_table_is_view: true,
constraint_name: constraint_name.clone(),
},
));
}
}
}
}
}
}
for (key, rel) in derived {
if !tables.contains_key(&key.0) || !tables.contains_key(rel.foreign_table()) {
continue;
}
relationships.entry(key).or_default().push(rel);
}
}
fn rename_cardinality(
cardinality: &Cardinality,
near: Option<&[String]>,
far: Option<&[String]>,
) -> Cardinality {
let rename_pairs = |columns: &[(String, String)]| -> Vec<(String, String)> {
columns
.iter()
.enumerate()
.map(|(i, (local, foreign))| {
(
near.and_then(|names| names.get(i)).unwrap_or(local).clone(),
far.and_then(|names| names.get(i))
.unwrap_or(foreign)
.clone(),
)
})
.collect()
};
match cardinality {
Cardinality::O2M {
constraint,
columns,
} => Cardinality::O2M {
constraint: constraint.clone(),
columns: rename_pairs(columns),
},
Cardinality::M2O {
constraint,
columns,
} => Cardinality::M2O {
constraint: constraint.clone(),
columns: rename_pairs(columns),
},
Cardinality::O2O {
constraint,
columns,
is_parent,
} => Cardinality::O2O {
constraint: constraint.clone(),
columns: rename_pairs(columns),
is_parent: *is_parent,
},
Cardinality::M2M(junction) => {
let mut junction = junction.clone();
if let Some(names) = near {
for (i, (source, _)) in junction.source_columns.iter_mut().enumerate() {
if let Some(name) = names.get(i) {
*source = name.clone();
}
}
}
if let Some(names) = far {
for (i, (_, target)) in junction.target_columns.iter_mut().enumerate() {
if let Some(name) = names.get(i) {
*target = name.clone();
}
}
}
Cardinality::M2M(junction)
}
}
}
fn view_primary_keys(
primary_keys: &std::collections::HashMap<QualifiedIdentifier, Vec<String>>,
view_columns: &[queries::ViewColumn],
) -> std::collections::HashMap<QualifiedIdentifier, Vec<String>> {
use std::collections::HashMap;
let mut by_view: HashMap<
&QualifiedIdentifier,
HashMap<&QualifiedIdentifier, HashMap<&str, &str>>,
> = HashMap::new();
for column in view_columns {
by_view
.entry(&column.view)
.or_default()
.entry(&column.base)
.or_default()
.entry(column.base_column.as_str())
.or_insert(column.view_column.as_str());
}
let mut keys = HashMap::new();
for (view, bases) in by_view {
for (base, columns) in bases {
let Some(key) = primary_keys.get(base).filter(|key| !key.is_empty()) else {
continue;
};
let mapped: Option<Vec<String>> = key
.iter()
.map(|column| columns.get(column.as_str()).map(|name| name.to_string()))
.collect();
if let Some(mapped) = mapped {
keys.insert(view.clone(), mapped);
}
}
}
keys
}
fn add_junction_relationships(
primary_keys: &std::collections::HashMap<QualifiedIdentifier, Vec<String>>,
relationships: &mut RelationshipsMap,
) {
let mut derived: Vec<((QualifiedIdentifier, String), Relationship)> = Vec::new();
for (junction_qi, junction_pk) in primary_keys {
let key = (junction_qi.clone(), junction_qi.schema.clone());
let Some(rels) = relationships.get(&key) else {
continue;
};
let outgoing: Vec<(&Relationship, Vec<(String, String)>)> = rels
.iter()
.filter(|r| matches!(r, Relationship::ForeignKey { table, .. } if table == junction_qi))
.filter(|r| r.is_to_one())
.filter_map(|r| {
let mut columns = r.join_columns();
columns.dedup();
match columns.is_empty() {
true => None,
false => Some((r, columns)),
}
})
.collect();
let pk: Vec<&str> = junction_pk.iter().map(String::as_str).collect();
for near in 0..outgoing.len() {
for far in 0..outgoing.len() {
if near == far {
continue;
}
let (near_rel, near_columns) = &outgoing[near];
let (far_rel, far_columns) = &outgoing[far];
if near_rel.foreign_table() == far_rel.foreign_table() {
continue;
}
if !near_columns
.iter()
.chain(far_columns)
.all(|(local, _)| pk.contains(&local.as_str()))
{
continue;
}
let source = near_rel.foreign_table().clone();
let target = far_rel.foreign_table().clone();
derived.push((
(source.clone(), source.schema.clone()),
Relationship::ForeignKey {
table: source,
foreign_table: target,
is_self: false,
cardinality: Cardinality::M2M(Junction {
table: junction_qi.clone(),
constraint1: near_rel.constraint_name().to_string(),
constraint2: far_rel.constraint_name().to_string(),
source_columns: near_columns
.iter()
.map(|(local, foreign)| (foreign.clone(), local.clone()))
.collect(),
target_columns: far_columns
.iter()
.map(|(local, foreign)| (local.clone(), foreign.clone()))
.collect(),
}),
table_is_view: false,
foreign_table_is_view: false,
constraint_name: String::new(),
},
));
}
}
}
for (key, rel) in derived {
relationships.entry(key).or_default().push(rel);
}
}
#[derive(Clone)]
pub struct SchemaCacheRef(Arc<tokio::sync::RwLock<Option<SchemaCache>>>);
impl SchemaCacheRef {
pub fn new() -> Self {
Self(Arc::new(tokio::sync::RwLock::new(None)))
}
pub fn from_static(cache: SchemaCache) -> Self {
Self(Arc::new(tokio::sync::RwLock::new(Some(cache))))
}
#[cfg(feature = "sqlx")]
pub async fn load(&self, pool: &sqlx::PgPool, schemas: &[String]) -> Result<()> {
let cache = SchemaCache::load(pool, schemas).await?;
let mut guard = self.0.write().await;
*guard = Some(cache);
Ok(())
}
pub async fn get(&self) -> Result<tokio::sync::RwLockReadGuard<'_, Option<SchemaCache>>> {
let guard = self.0.read().await;
if guard.is_none() {
return Err(Error::SchemaCacheNotLoaded);
}
Ok(guard)
}
pub async fn is_loaded(&self) -> bool {
self.0.read().await.is_some()
}
}
impl Default for SchemaCacheRef {
fn default() -> Self {
Self::new()
}
}