use std::collections::HashMap;
use std::sync::{Mutex, OnceLock, PoisonError};
use super::error::ExecError;
use super::{FlatScalar, Pool};
use crate::core::{
BulkInsertQuery, CountQuery, DeleteQuery, FieldSchema, FieldType, Filter, InsertQuery,
ModelSchema, Op, QueryError, SelectQuery, SqlValue, WhereExpr,
};
pub struct M2MManager {
pub src_pk: SqlValue,
pub through: &'static str,
pub src_col: &'static str,
pub dst_col: &'static str,
}
impl M2MManager {
pub async fn all(&self, pool: &Pool) -> Result<Vec<i64>, ExecError> {
self.all_as::<i64>(pool).await
}
pub async fn all_as<K: FlatScalar>(&self, pool: &Pool) -> Result<Vec<K>, ExecError> {
let src = self.src_key()?;
fetch_dst(
self.junction(),
self.dst_col,
vec![eq(self.src_col, src)],
pool,
)
.await
}
pub async fn add(&self, dst_id: impl Into<SqlValue>, pool: &Pool) -> Result<(), ExecError> {
let dst = dst_key(dst_id, self.through, self.dst_col)?;
let src = self.src_key()?;
let query = InsertQuery::new(
self.junction(),
vec![self.src_col, self.dst_col],
vec![src.clone(), dst.clone()],
);
if !super::executor::insert_or_ignore(pool, &query).await? {
return Ok(());
}
#[cfg(feature = "signals")]
crate::signals::m2m::send_m2m_changed(crate::signals::m2m::M2mChangedContext {
action: crate::signals::m2m::M2mAction::Add,
through: self.through,
src_col: self.src_col,
dst_col: self.dst_col,
src_pk: src,
dst_pks: vec![dst],
})
.await;
Ok(())
}
pub async fn remove(&self, dst_id: impl Into<SqlValue>, pool: &Pool) -> Result<(), ExecError> {
let dst = dst_key(dst_id, self.through, self.dst_col)?;
let src = self.src_key()?;
let filters = vec![eq(self.src_col, src.clone()), eq(self.dst_col, dst.clone())];
if delete(self.junction(), filters, pool).await? == 0 {
return Ok(());
}
#[cfg(feature = "signals")]
crate::signals::m2m::send_m2m_changed(crate::signals::m2m::M2mChangedContext {
action: crate::signals::m2m::M2mAction::Remove,
through: self.through,
src_col: self.src_col,
dst_col: self.dst_col,
src_pk: src,
dst_pks: vec![dst],
})
.await;
Ok(())
}
pub async fn set<K: Clone + Into<SqlValue>>(
&self,
ids: &[K],
pool: &Pool,
) -> Result<(), ExecError> {
let src = self.src_key()?;
let dsts = dst_keys(ids, self.through, self.dst_col)?;
let rows = dsts.iter().map(|d| vec![src.clone(), d.clone()]).collect();
replace(
self.junction(),
vec![eq(self.src_col, src.clone())],
vec![self.src_col, self.dst_col],
rows,
pool,
)
.await?;
#[cfg(feature = "signals")]
crate::signals::m2m::send_m2m_changed(crate::signals::m2m::M2mChangedContext {
action: crate::signals::m2m::M2mAction::Set,
through: self.through,
src_col: self.src_col,
dst_col: self.dst_col,
src_pk: src,
dst_pks: dsts,
})
.await;
Ok(())
}
pub async fn clear(&self, pool: &Pool) -> Result<(), ExecError> {
let src = self.src_key()?;
delete(self.junction(), vec![eq(self.src_col, src.clone())], pool).await?;
#[cfg(feature = "signals")]
crate::signals::m2m::send_m2m_changed(crate::signals::m2m::M2mChangedContext {
action: crate::signals::m2m::M2mAction::Clear,
through: self.through,
src_col: self.src_col,
dst_col: self.dst_col,
src_pk: src,
dst_pks: Vec::new(),
})
.await;
Ok(())
}
pub async fn contains(
&self,
dst_id: impl Into<SqlValue>,
pool: &Pool,
) -> Result<bool, ExecError> {
let dst = dst_key(dst_id, self.through, self.dst_col)?;
let src = self.src_key()?;
let filters = vec![eq(self.src_col, src), eq(self.dst_col, dst)];
exists(self.junction(), filters, pool).await
}
fn src_key(&self) -> Result<SqlValue, ExecError> {
src_key(&self.src_pk, self.through)
}
fn junction(&self) -> &'static ModelSchema {
junction(self.through, &[self.src_col, self.dst_col])
}
}
pub(crate) fn junction(through: &'static str, cols: &[&'static str]) -> &'static ModelSchema {
type Key = (&'static str, Vec<&'static str>);
static CACHE: OnceLock<Mutex<HashMap<Key, &'static ModelSchema>>> = OnceLock::new();
let mut cache = CACHE
.get_or_init(Mutex::default)
.lock()
.unwrap_or_else(PoisonError::into_inner);
cache.entry((through, cols.to_vec())).or_insert_with(|| {
if let Some(entry) = crate::core::ModelEntry::for_table(through) {
return entry.schema;
}
let fields: Vec<FieldSchema> = cols
.iter()
.map(|c| FieldSchema::new(c, c, FieldType::I64))
.collect();
let mut schema = ModelSchema::new(through, through);
schema.fields = Box::leak(fields.into_boxed_slice());
Box::leak(Box::new(schema))
})
}
fn eq(column: &'static str, value: SqlValue) -> Filter {
Filter {
column,
op: Op::Eq,
value,
}
}
async fn fetch_dst<K: FlatScalar>(
junction: &'static ModelSchema,
dst_col: &'static str,
filters: Vec<Filter>,
pool: &Pool,
) -> Result<Vec<K>, ExecError> {
let mut query = SelectQuery::new(junction).where_clause(WhereExpr::and_predicates(filters));
query.projection = Some(vec![dst_col]);
crate::test_assertions::query_counter::bump();
super::executor::fetch_values_flat(pool, &query).await
}
async fn exists(
junction: &'static ModelSchema,
filters: Vec<Filter>,
pool: &Pool,
) -> Result<bool, ExecError> {
let select = SelectQuery::new(junction).where_clause(WhereExpr::and_predicates(filters));
let n = super::executor::count_rows_pool(pool, &CountQuery::from_select(select)).await?;
Ok(n > 0)
}
async fn delete(
junction: &'static ModelSchema,
filters: Vec<Filter>,
pool: &Pool,
) -> Result<u64, ExecError> {
let query = DeleteQuery::new(junction, WhereExpr::and_predicates(filters));
super::executor::delete_pool(pool, &query).await
}
async fn replace(
junction: &'static ModelSchema,
owner: Vec<Filter>,
columns: Vec<&'static str>,
rows: Vec<Vec<SqlValue>>,
pool: &Pool,
) -> Result<(), ExecError> {
let ins = BulkInsertQuery::new(junction, columns, rows);
ins.validate()?;
let mut tx = crate::sql::transaction_pool(pool).await?;
let del = DeleteQuery::new(junction, WhereExpr::and_predicates(owner));
super::executor::delete_tx(&mut tx, &del).await?;
super::executor::bulk_insert_tx(&mut tx, &ins).await?;
tx.commit().await.map_err(ExecError::Driver)?;
Ok(())
}
fn src_key(pk: &SqlValue, through: &'static str) -> Result<SqlValue, ExecError> {
match pk {
SqlValue::Null => Err(ExecError::M2mUnsavedSource { through }),
v => Ok(v.clone()),
}
}
fn dst_keys<K: Clone + Into<SqlValue>>(
ids: &[K],
through: &'static str,
dst_col: &'static str,
) -> Result<Vec<SqlValue>, ExecError> {
let mut seen = std::collections::HashSet::new();
let mut out = Vec::with_capacity(ids.len());
for k in ids {
let d = dst_key(k.clone(), through, dst_col)?;
if seen.insert(d.to_display_string()) {
out.push(d);
}
}
Ok(out)
}
fn dst_key(
v: impl Into<SqlValue>,
through: &'static str,
dst_col: &'static str,
) -> Result<SqlValue, ExecError> {
let v = match v.into() {
SqlValue::I16(n) => SqlValue::I64(n.into()),
SqlValue::I32(n) => SqlValue::I64(n.into()),
v => v,
};
let Some(want) = dst_column_type(through, dst_col) else {
return Ok(v);
};
let Some(got) = v.field_type() else {
return Ok(v);
};
let is_int = |t: FieldType| matches!(t, FieldType::I16 | FieldType::I32 | FieldType::I64);
if got == want || (is_int(got) && is_int(want)) {
return Ok(v);
}
let converted = match &v {
SqlValue::String(s) if is_int(want) => s.parse::<i64>().ok().map(SqlValue::I64),
SqlValue::String(s) if want == FieldType::Uuid => {
uuid::Uuid::parse_str(s).ok().map(SqlValue::Uuid)
}
_ => None,
};
converted.ok_or_else(|| {
ExecError::Query(QueryError::TypeMismatch {
model: through,
field: dst_col.to_owned(),
expected: want,
actual: got,
})
})
}
fn dst_column_type(through: &str, dst_col: &str) -> Option<FieldType> {
if let Some(entry) = crate::core::ModelEntry::for_table(through) {
return entry.schema.field_by_column(dst_col).map(|f| f.ty);
}
inventory::iter::<crate::core::ModelEntry>
.into_iter()
.flat_map(|e| e.schema.m2m)
.any(|r| r.auto_create && r.through == through && r.dst_col == dst_col)
.then_some(FieldType::I64)
}
pub struct GenericM2MManager {
pub src_pk: SqlValue,
pub src_schema: &'static crate::core::ModelSchema,
pub through: &'static str,
pub pk_col: &'static str,
pub ct_col: &'static str,
pub dst_col: &'static str,
}
impl GenericM2MManager {
fn src_key(&self) -> Result<SqlValue, ExecError> {
src_key(&self.src_pk, self.through)
}
fn junction(&self) -> &'static ModelSchema {
junction(self.through, &[self.pk_col, self.ct_col, self.dst_col])
}
async fn owner(&self, pool: &Pool) -> Result<Vec<Filter>, ExecError> {
let src = self.src_key()?;
let ct = self.ct_id(pool).await?;
Ok(vec![
eq(self.pk_col, src),
eq(self.ct_col, SqlValue::I64(ct)),
])
}
async fn ct_id(&self, pool: &Pool) -> Result<i64, ExecError> {
crate::contenttypes::ContentType::get_for_schema(pool, self.src_schema)
.await?
.and_then(|ct| ct.id.get().copied())
.ok_or(ExecError::ContentTypeNotRegistered {
table: self.src_schema.table,
})
}
#[cfg(feature = "signals")]
async fn signal(&self, action: crate::signals::m2m::M2mAction, dst_pks: Vec<SqlValue>) {
crate::signals::m2m::send_m2m_changed(crate::signals::m2m::M2mChangedContext {
action,
through: self.through,
src_col: self.pk_col,
dst_col: self.dst_col,
src_pk: self.src_pk.clone(),
dst_pks,
})
.await;
}
pub async fn all(&self, pool: &Pool) -> Result<Vec<i64>, ExecError> {
self.all_as::<i64>(pool).await
}
pub async fn all_as<K: FlatScalar>(&self, pool: &Pool) -> Result<Vec<K>, ExecError> {
let owner = self.owner(pool).await?;
fetch_dst(self.junction(), self.dst_col, owner, pool).await
}
pub async fn add(&self, dst_id: impl Into<SqlValue>, pool: &Pool) -> Result<(), ExecError> {
let dst = dst_key(dst_id, self.through, self.dst_col)?;
let mut values: Vec<SqlValue> = self
.owner(pool)
.await?
.into_iter()
.map(|f| f.value)
.collect();
values.push(dst.clone());
let query = InsertQuery::new(
self.junction(),
vec![self.pk_col, self.ct_col, self.dst_col],
values,
);
if !super::executor::insert_or_ignore(pool, &query).await? {
return Ok(());
}
#[cfg(feature = "signals")]
self.signal(crate::signals::m2m::M2mAction::Add, vec![dst])
.await;
Ok(())
}
pub async fn remove(&self, dst_id: impl Into<SqlValue>, pool: &Pool) -> Result<(), ExecError> {
let dst = dst_key(dst_id, self.through, self.dst_col)?;
let mut filters = self.owner(pool).await?;
filters.push(eq(self.dst_col, dst.clone()));
if delete(self.junction(), filters, pool).await? == 0 {
return Ok(());
}
#[cfg(feature = "signals")]
self.signal(crate::signals::m2m::M2mAction::Remove, vec![dst])
.await;
Ok(())
}
pub async fn set<K: Clone + Into<SqlValue>>(
&self,
ids: &[K],
pool: &Pool,
) -> Result<(), ExecError> {
let dsts = dst_keys(ids, self.through, self.dst_col)?;
let owner = self.owner(pool).await?;
let rows = dsts
.iter()
.map(|d| {
owner
.iter()
.map(|f| f.value.clone())
.chain([d.clone()])
.collect()
})
.collect();
let columns = vec![self.pk_col, self.ct_col, self.dst_col];
replace(self.junction(), owner, columns, rows, pool).await?;
#[cfg(feature = "signals")]
self.signal(crate::signals::m2m::M2mAction::Set, dsts).await;
Ok(())
}
pub async fn clear(&self, pool: &Pool) -> Result<(), ExecError> {
let owner = self.owner(pool).await?;
delete(self.junction(), owner, pool).await?;
#[cfg(feature = "signals")]
self.signal(crate::signals::m2m::M2mAction::Clear, Vec::new())
.await;
Ok(())
}
pub async fn contains(
&self,
dst_id: impl Into<SqlValue>,
pool: &Pool,
) -> Result<bool, ExecError> {
let dst = dst_key(dst_id, self.through, self.dst_col)?;
let mut filters = self.owner(pool).await?;
filters.push(eq(self.dst_col, dst));
exists(self.junction(), filters, pool).await
}
}
impl M2MManager {
#[deprecated(note = "renamed to `all` — drop the `_pool` suffix")]
pub async fn all_pool(&self, pool: &Pool) -> Result<Vec<i64>, ExecError> {
self.all(pool).await
}
#[deprecated(note = "renamed to `add` — drop the `_pool` suffix")]
pub async fn add_pool(&self, dst_id: i64, pool: &Pool) -> Result<(), ExecError> {
self.add(dst_id, pool).await
}
#[deprecated(note = "renamed to `remove` — drop the `_pool` suffix")]
pub async fn remove_pool(&self, dst_id: i64, pool: &Pool) -> Result<(), ExecError> {
self.remove(dst_id, pool).await
}
#[deprecated(note = "renamed to `set` — drop the `_pool` suffix")]
pub async fn set_pool(&self, ids: &[i64], pool: &Pool) -> Result<(), ExecError> {
self.set(ids, pool).await
}
#[deprecated(note = "renamed to `clear` — drop the `_pool` suffix")]
pub async fn clear_pool(&self, pool: &Pool) -> Result<(), ExecError> {
self.clear(pool).await
}
#[deprecated(note = "renamed to `contains` — drop the `_pool` suffix")]
pub async fn contains_pool(&self, dst_id: i64, pool: &Pool) -> Result<bool, ExecError> {
self.contains(dst_id, pool).await
}
}