use toasty::{
schema::app::{self, FieldId, FieldTy, ModelId},
stmt::{IntoExpr, Path, Value},
};
use toasty_core::stmt as core;
use super::{
model::AppSchema,
pk::{self, parse_key},
};
use crate::declaration::ManyToManyFault;
#[derive(Debug, Clone)]
pub(crate) struct JoinTable {
model: ModelId,
blank: Vec<core::Expr>,
owner: FieldId,
target: FieldId,
key: core::Type,
target_model: ModelId,
}
impl JoinTable {
pub(crate) fn of<M: toasty::schema::Model>(
schema: &AppSchema,
name: &str,
) -> Result<Self, ManyToManyFault> {
let schema = schema.inner();
let owner = schema
.app
.get_model(M::id())
.and_then(app::Model::as_root)
.ok_or(ManyToManyFault::NotJoinModel)?;
let field = owner
.fields
.iter()
.find(|field| field.name.app.as_deref() == Some(name))
.ok_or(ManyToManyFault::NotJoinModel)?;
let FieldTy::Via(via) = &field.ty else {
return Err(ManyToManyFault::NotJoinModel);
};
let [has, belongs] = via.path.projection.as_slice() else {
return Err(ManyToManyFault::NotJoinModel);
};
if !via.is_many() || via.is_scalar() {
return Err(ManyToManyFault::NotJoinModel);
}
let has = owner
.fields
.get(*has)
.and_then(|field| field.ty.as_has_many())
.ok_or(ManyToManyFault::NotJoinModel)?;
let join = schema
.app
.get_model(has.target)
.and_then(app::Model::as_root)
.ok_or(ManyToManyFault::NotJoinModel)?;
let to_owner = join
.fields
.get(has.pair_id.index)
.and_then(|field| field.ty.as_belongs_to())
.ok_or(ManyToManyFault::NotJoinModel)?;
let to_target = join
.fields
.get(*belongs)
.and_then(|field| field.ty.as_belongs_to())
.ok_or(ManyToManyFault::NotJoinModel)?;
let target = schema
.app
.get_model(to_target.target)
.and_then(app::Model::as_root)
.ok_or(ManyToManyFault::NotJoinModel)?;
let owner_key = key_column(to_owner, owner)?;
let target_key = key_column(to_target, target)?;
let key = match target
.fields
.get(target_key_of(to_target)?.index)
.map(|f| &f.ty)
{
Some(FieldTy::Primitive(primitive)) => primitive.ty.clone(),
_ => return Err(ManyToManyFault::CompositeKey),
};
let mut blank = Vec::with_capacity(join.fields.len());
for field in &join.fields {
let id = field.id;
blank.push(if field.auto().is_some() {
core::Expr::Default
} else {
if !(field.is_relation() || field.nullable() || id == owner_key || id == target_key)
{
return Err(ManyToManyFault::UnwritableColumn {
model: join.name.upper_camel_case(),
column: field.name.app_unwrap().to_string(),
});
}
core::Expr::Value(core::Value::Null)
});
}
Ok(Self {
model: join.id,
blank,
owner: owner_key,
target: target_key,
key,
target_model: target.id,
})
}
pub(crate) fn target_model(&self) -> ModelId {
self.target_model
}
pub(crate) async fn link<P>(
&self,
owner: &P,
keys: &[String],
ex: &mut dyn toasty::Executor,
) -> toasty::Result<()>
where
P: toasty::schema::Model + IntoExpr<P>,
{
let owner = owner_key(owner)?;
let mut targets: Vec<Value> = Vec::with_capacity(keys.len());
for key in keys {
let target = self.parse(key)?;
if !targets.contains(&target) {
targets.push(target);
}
}
for target in targets {
let mut row = self.blank.clone();
row[self.owner.index] = core::Expr::Value(owner.clone());
row[self.target.index] = core::Expr::Value(target);
let insert = core::Insert {
target: core::InsertTarget::Model(self.model),
source: core::Query::new_single(vec![core::ExprRecord::from_vec(row).into()]),
upsert: None,
returning: None,
};
toasty::stmt::Statement::<()>::from_untyped_stmt(insert.into())
.exec(&mut *ex)
.await?;
}
Ok(())
}
pub(crate) async fn unlink<P>(
&self,
owner: &P,
keys: &[String],
ex: &mut dyn toasty::Executor,
) -> toasty::Result<()>
where
P: toasty::schema::Model + IntoExpr<P>,
{
if keys.is_empty() {
return Ok(());
}
let owner = owner_key(owner)?;
let targets = keys
.iter()
.map(|key| self.parse(key).map(core::Expr::Value))
.collect::<toasty::Result<Vec<_>>>()?;
let filter = core::Expr::and_from_vec(vec![
core::Expr::eq(core::Expr::ref_self_field(self.owner), owner),
core::Expr::in_list(
core::Expr::ref_self_field(self.target),
core::Expr::list(targets),
),
]);
let delete = core::Delete {
from: core::Source::from(self.model),
filter: core::Filter::new(filter),
returning: None,
condition: core::Condition::default(),
};
toasty::stmt::Statement::<()>::from_untyped_stmt(delete.into())
.exec(ex)
.await
}
fn parse(&self, text: &str) -> toasty::Result<Value> {
parse_key(&self.key, text.trim()).ok_or_else(|| {
toasty::Error::from_args(format_args!("`{text}` is not a key of the linked model"))
})
}
}
fn key_column(from: &app::BelongsTo, to: &app::ModelRoot) -> Result<FieldId, ManyToManyFault> {
match (
from.foreign_key.fields.as_slice(),
to.primary_key.fields.as_slice(),
) {
([column], [key]) if column.target == *key => Ok(column.source),
_ => Err(ManyToManyFault::CompositeKey),
}
}
fn target_key_of(from: &app::BelongsTo) -> Result<FieldId, ManyToManyFault> {
match from.foreign_key.fields.as_slice() {
[column] => Ok(column.target),
_ => Err(ManyToManyFault::CompositeKey),
}
}
fn owner_key<P>(owner: &P) -> toasty::Result<Value>
where
P: toasty::schema::Model + IntoExpr<P>,
{
match pk::pk_values(owner).as_slice() {
[value] => Ok(value.clone()),
_ => Err(toasty::Error::from_args(format_args!(
"`{}` has no single-column primary key",
std::any::type_name::<P>()
))),
}
}
pub(crate) fn linked_to<P, C>(
owner: &P,
via: Path<P, toasty::stmt::List<C>>,
) -> toasty::stmt::Expr<bool>
where
P: toasty::schema::Model + IntoExpr<P>,
C: toasty::schema::Model,
{
let source = toasty::stmt::Query::<toasty::stmt::List<P>>::all().filter(pk::pk_filter(owner));
let association = toasty::stmt::Association::many(source, via);
let mut linked = toasty::stmt::IntoStatement::into_statement(association)
.into_untyped()
.into_query_unwrap();
let Some(&[key]) = C::schema()
.as_root()
.map(|root| root.primary_key.fields.as_slice())
else {
return toasty::stmt::Expr::from_untyped(core::Expr::from(false));
};
if let core::ExprSet::Select(select) = &mut linked.body {
select.returning = core::Returning::Project(core::Expr::ref_self_field(key));
}
toasty::stmt::Expr::from_untyped(core::Expr::in_subquery(
core::Expr::ref_self_field(key),
linked,
))
}
#[cfg(test)]
mod tests;