use std::any::{Any, TypeId, type_name};
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use crate::query::SqlValue;
use crate::row::{FromRow, RangoRow, RowError};
pub type RelationCache = HashMap<TypeId, Box<dyn Any + Send + Sync>>;
pub struct WithRelated<M> {
pub inner: M,
cache: RelationCache,
}
impl<M> WithRelated<M> {
pub fn into_inner(self) -> M {
self.inner
}
#[doc(hidden)]
pub fn new(inner: M) -> Self {
Self {
inner,
cache: HashMap::new(),
}
}
pub fn related<R: Any + Send + Sync + 'static>(&self) -> &R {
self.cache
.get(&TypeId::of::<R>())
.and_then(|b| b.downcast_ref::<R>())
.unwrap_or_else(|| {
panic!(
"select_related<{ty}> was not loaded; \
add .select_related(..) for {ty} to the QueryBuilder chain",
ty = type_name::<R>()
)
})
}
pub fn prefetched<R: Any + Send + Sync + 'static>(&self) -> &[R] {
self.cache
.get(&TypeId::of::<Vec<R>>())
.and_then(|b| b.downcast_ref::<Vec<R>>())
.map(Vec::as_slice)
.unwrap_or_else(|| {
panic!(
"prefetch_related<{ty}> was not loaded; \
add .prefetch_related(..) for {ty} to the QueryBuilder chain",
ty = type_name::<R>()
)
})
}
#[doc(hidden)]
pub fn insert_raw(&mut self, type_id: TypeId, boxed: Box<dyn Any + Send + Sync>) {
self.cache.insert(type_id, boxed);
}
}
impl<M> Deref for WithRelated<M> {
type Target = M;
fn deref(&self) -> &M {
&self.inner
}
}
impl<M> DerefMut for WithRelated<M> {
fn deref_mut(&mut self) -> &mut M {
&mut self.inner
}
}
#[doc(hidden)]
pub type FromRowFn = fn(&dyn RangoRow) -> Result<Box<dyn Any + Send + Sync>, RowError>;
#[doc(hidden)]
pub type CollectVecFn = fn(Vec<Box<dyn Any + Send + Sync>>) -> Box<dyn Any + Send + Sync>;
#[doc(hidden)]
pub fn erased_from_row<R>(row: &dyn RangoRow) -> Result<Box<dyn Any + Send + Sync>, RowError>
where
R: FromRow + Send + Sync + 'static,
{
R::from_row(row).map(|v| Box::new(v) as Box<dyn Any + Send + Sync>)
}
#[doc(hidden)]
pub fn collect_vec_erased<R>(items: Vec<Box<dyn Any + Send + Sync>>) -> Box<dyn Any + Send + Sync>
where
R: Any + Send + Sync + 'static,
{
let typed: Vec<R> = items
.into_iter()
.map(|b| {
*b.downcast::<R>()
.expect("prefetch_related type mismatch — this is a bug")
})
.collect();
Box::new(typed)
}
pub fn m2m_pivot(from_table: &str, to_table: &str) -> (String, String, String) {
let (from_col, to_col) = if from_table == to_table {
(format!("from_{from_table}_id"), format!("to_{to_table}_id"))
} else {
(format!("{from_table}_id"), format!("{to_table}_id"))
};
(format!("{from_table}_{to_table}"), from_col, to_col)
}
#[doc(hidden)]
pub fn sql_value_key(v: &SqlValue) -> Option<String> {
match v {
SqlValue::Uuid(u) => Some(u.to_string()),
SqlValue::BigInt(i) => Some(i.to_string()),
SqlValue::Int(i) => Some(i.to_string()),
SqlValue::SmallInt(i) => Some(i.to_string()),
SqlValue::Text(s) => Some(s.clone()),
_ => None,
}
}
#[doc(hidden)]
pub fn row_key(row: &dyn RangoRow, col: &str, like: &SqlValue) -> Option<String> {
match like {
SqlValue::Uuid(_) => row.get_uuid(col).ok().map(|v| v.to_string()),
SqlValue::BigInt(_) => row.get_i64(col).ok().map(|v| v.to_string()),
SqlValue::Int(_) => row.get_i32(col).ok().map(|v| v.to_string()),
SqlValue::SmallInt(_) => row.get_i16(col).ok().map(|v| v.to_string()),
SqlValue::Text(_) => row.get_string(col).ok(),
_ => None,
}
}
#[cfg(test)]
mod pivot_tests {
use super::m2m_pivot;
#[test]
fn pivot_columns_are_distinct_for_self_reference() {
let s = |t: &str| t.to_string();
assert_eq!(m2m_pivot("a", "b"), (s("a_b"), s("a_id"), s("b_id")));
assert_eq!(
m2m_pivot("u", "u"),
(s("u_u"), s("from_u_id"), s("to_u_id"))
);
}
}
#[doc(hidden)]
#[doc(hidden)]
pub struct M2mSql {
dialect: crate::Dialect,
pivot: String,
from_col: String,
to_col: String,
from_table: String,
to_table: String,
}
impl M2mSql {
pub fn new(dialect: crate::Dialect, from_table: &str, to_table: &str) -> Self {
let (pivot, from_col, to_col) = m2m_pivot(from_table, to_table);
Self {
dialect,
pivot,
from_col,
to_col,
from_table: from_table.to_owned(),
to_table: to_table.to_owned(),
}
}
pub fn pivot_table(&self) -> &str {
&self.pivot
}
fn q(&self, ident: &str) -> String {
self.dialect.quote_ident(ident)
}
fn ph(&self, n: usize) -> String {
match self.dialect {
crate::Dialect::Postgres => format!("${n}"),
_ => "?".to_owned(),
}
}
pub fn insert(&self) -> String {
let (pivot, from, to) = (
self.q(&self.pivot),
self.q(&self.from_col),
self.q(&self.to_col),
);
let (p1, p2) = (self.ph(1), self.ph(2));
match self.dialect {
crate::Dialect::Mysql => format!(
"INSERT INTO {pivot} ({from}, {to}) VALUES ({p1}, {p2}) \
ON DUPLICATE KEY UPDATE {from} = {from}"
),
_ => format!(
"INSERT INTO {pivot} ({from}, {to}) VALUES ({p1}, {p2}) ON CONFLICT DO NOTHING"
),
}
}
pub fn clear(&self) -> String {
format!(
"DELETE FROM {} WHERE {} = {}",
self.q(&self.pivot),
self.q(&self.from_col),
self.ph(1)
)
}
pub fn remove(&self) -> String {
format!(
"DELETE FROM {} WHERE {} = {} AND {} = {}",
self.q(&self.pivot),
self.q(&self.from_col),
self.ph(1),
self.q(&self.to_col),
self.ph(2)
)
}
pub fn exists(&self) -> String {
format!(
"SELECT 1 FROM {} WHERE {} = {} AND {} = {} LIMIT 1",
self.q(&self.pivot),
self.q(&self.from_col),
self.ph(1),
self.q(&self.to_col),
self.ph(2)
)
}
pub fn all(&self, to_pk: &str) -> String {
format!(
r#"SELECT t.* FROM {to_table} t
INNER JOIN {pivot} p ON t.{to_pk} = p.{to_col}
WHERE p.{from_col} = {p1}"#,
to_table = self.q(&self.to_table),
pivot = self.q(&self.pivot),
to_pk = self.q(to_pk),
to_col = self.q(&self.to_col),
from_col = self.q(&self.from_col),
p1 = self.ph(1),
)
}
pub fn reverse(&self, from_pk: &str) -> String {
format!(
r#"SELECT f.* FROM {from_table} f
INNER JOIN {pivot} p ON f.{from_pk} = p.{from_col}
WHERE p.{to_col} = {p1}"#,
from_table = self.q(&self.from_table),
pivot = self.q(&self.pivot),
from_pk = self.q(from_pk),
from_col = self.q(&self.from_col),
to_col = self.q(&self.to_col),
p1 = self.ph(1),
)
}
}
#[cfg(test)]
mod m2m_sql_tests {
use super::M2mSql;
use crate::Dialect;
#[test]
fn per_dialect_sql() {
let pg = M2mSql::new(Dialect::Postgres, "post", "tag");
assert_eq!(
pg.insert(),
"INSERT INTO \"post_tag\" (\"post_id\", \"tag_id\") VALUES ($1, $2) ON CONFLICT DO NOTHING"
);
assert_eq!(
pg.remove(),
"DELETE FROM \"post_tag\" WHERE \"post_id\" = $1 AND \"tag_id\" = $2"
);
let my = M2mSql::new(Dialect::Mysql, "post", "tag");
assert_eq!(
my.insert(),
"INSERT INTO `post_tag` (`post_id`, `tag_id`) VALUES (?, ?) \
ON DUPLICATE KEY UPDATE `post_id` = `post_id`"
);
let lite = M2mSql::new(Dialect::Sqlite, "post", "tag");
assert_eq!(
lite.clear(),
"DELETE FROM \"post_tag\" WHERE \"post_id\" = ?"
);
}
}