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()],
);
let link = vec![eq(self.src_col, src.clone()), eq(self.dst_col, dst.clone())];
if !insert_link(self.link_key(), link, &query, pool).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();
let locks = self.source_locks(&src);
if locks.is_empty() {
tracing::warn!(
through = self.through,
src_col = self.src_col,
"M2M set: no registered source model, so concurrent sets are not serialized"
);
}
let changed = replace(
locks,
self.junction(),
vec![eq(self.src_col, src.clone())],
self.dst_col,
&dsts,
vec![self.src_col, self.dst_col],
rows,
pool,
)
.await?;
if !changed {
return Ok(());
}
#[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()?;
if delete(self.junction(), vec![eq(self.src_col, src.clone())], pool).await? == 0 {
return Ok(());
}
#[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 source_locks(&self, src: &SqlValue) -> Vec<SelectQuery> {
inventory::iter::<crate::core::ModelEntry>
.into_iter()
.map(|e| e.schema)
.filter(|s| {
s.m2m
.iter()
.any(|r| r.through == self.through && r.src_col == self.src_col)
})
.filter_map(|s| source_lock(s, src))
.collect()
}
fn junction(&self) -> &'static ModelSchema {
junction(self.through, &[self.src_col, self.dst_col])
}
fn link_key(&self) -> LinkKey {
let auto = inventory::iter::<crate::core::ModelEntry>
.into_iter()
.flat_map(|e| e.schema.m2m.iter())
.any(|r| r.auto_create && r.through == self.through);
let ends = [self.src_col, self.dst_col];
let keyed = |s: &ModelSchema| {
s.fields
.iter()
.any(|f| (f.primary_key || f.unique) && ends.contains(&f.column))
|| s.indexes.iter().any(|i| {
i.unique
&& i.where_clause.is_none()
&& i.columns.iter().all(|c| ends.contains(c))
})
};
let registered = crate::core::ModelEntry::for_table(self.through).map(|e| e.schema);
if auto || registered.is_some_and(keyed) {
LinkKey::Unique
} else {
LinkKey::Unkeyed
}
}
}
#[derive(Clone, Copy)]
enum LinkKey {
Unique,
Unkeyed,
}
pub(crate) fn junction(through: &'static str, cols: &[&'static str]) -> &'static ModelSchema {
let cols: Vec<_> = cols
.iter()
.map(|c| (*c, auto_column_type(through, c).unwrap_or(FieldType::I64)))
.collect();
typed_junction(through, &cols)
}
pub(crate) fn end_type(target: &ModelSchema) -> Option<FieldType> {
target.primary_key().map(|pk| junction_column_type(pk.ty))
}
pub(crate) fn junction_column_type(pk: FieldType) -> FieldType {
match pk {
FieldType::I16 | FieldType::I32 => FieldType::I64,
ty => ty,
}
}
fn auto_column_type(through: &str, column: &str) -> Option<FieldType> {
inventory::iter::<crate::core::ModelEntry>
.into_iter()
.flat_map(|e| e.schema.m2m.iter().map(move |r| (e.schema, r)))
.filter(|(_, r)| r.auto_create && r.through == through)
.find_map(|(src, r)| {
let end = if r.dst_col == column {
r.to
} else if r.src_col == column {
src.table
} else {
return None;
};
let target = crate::core::ModelEntry::for_table(end).map(|e| e.schema);
Some(target.and_then(end_type).unwrap_or(FieldType::I64))
})
}
pub(crate) fn typed_junction(
through: &'static str,
cols: &[(&'static str, FieldType)],
) -> &'static ModelSchema {
type Key = (&'static str, Vec<(&'static str, FieldType)>);
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, ty)| FieldSchema::new(c, c, *ty))
.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]);
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
}
fn source_lock(source: &'static ModelSchema, pk: &SqlValue) -> Option<SelectQuery> {
let col = source.pk_column()?;
let mut q =
SelectQuery::new(source).where_clause(WhereExpr::and_predicates(vec![eq(col, pk.clone())]));
q.projection = Some(vec![col]);
q.lock_mode = Some(crate::core::LockMode {
no_key: true,
silent_on_sqlite: true,
..crate::core::LockMode::default()
});
Some(q)
}
async fn insert_link(
key: LinkKey,
link: Vec<Filter>,
query: &InsertQuery,
pool: &Pool,
) -> Result<bool, ExecError> {
if let LinkKey::Unique = key {
return super::executor::insert_or_ignore(pool, query).await;
}
query.validate()?;
let mut tx = crate::sql::write_transaction(pool).await?;
let present = SelectQuery::new(query.model).where_clause(WhereExpr::and_predicates(link));
if super::executor::count_rows_on(&mut tx, &CountQuery::from_select(present)).await? > 0 {
return Ok(false);
}
let inserted = super::executor::insert_or_ignore_tx(&mut tx, query).await?;
tx.commit().await.map_err(ExecError::Driver)?;
Ok(inserted)
}
#[allow(clippy::too_many_arguments)]
async fn replace(
locks: Vec<SelectQuery>,
junction: &'static ModelSchema,
owner: Vec<Filter>,
dst_col: &'static str,
dsts: &[SqlValue],
columns: Vec<&'static str>,
rows: Vec<Vec<SqlValue>>,
pool: &Pool,
) -> Result<bool, ExecError> {
let ins = BulkInsertQuery::new(junction, columns, rows);
ins.validate()?;
let mut tx = crate::sql::write_transaction(pool).await?;
for lock in locks {
let stmt = pool.dialect().compile_select(&lock)?;
super::executor::raw_execute_tx(&mut tx, &stmt.sql, stmt.params).await?;
}
let mut old = SelectQuery::new(junction).where_clause(WhereExpr::and_predicates(owner.clone()));
old.projection = Some(vec![dst_col]);
let old = match dst_column_type(junction.table, dst_col) {
Some(ty) => keys_on(&mut tx, &old, Some(ty)).await?,
None => {
let ty = dsts.first().and_then(SqlValue::field_type);
match keys_on(&mut tx, &old, ty).await {
Err(ExecError::Driver(sqlx::Error::ColumnDecode { .. })) => None,
r => r?,
}
}
};
let del = DeleteQuery::new(junction, WhereExpr::and_predicates(owner));
let deleted = super::executor::delete_tx(&mut tx, &del).await?;
super::executor::bulk_insert_tx(&mut tx, &ins).await?;
tx.commit().await.map_err(ExecError::Driver)?;
if dsts.is_empty() {
return Ok(deleted > 0);
}
let new: std::collections::HashSet<String> =
dsts.iter().map(SqlValue::to_display_string).collect();
Ok(old.is_none_or(|old| old != new))
}
async fn keys_on(
tx: &mut crate::sql::PoolTx<'_>,
query: &SelectQuery,
ty: Option<FieldType>,
) -> Result<Option<std::collections::HashSet<String>>, ExecError> {
use super::executor::fetch_values_flat_on as fetch;
let keys: Vec<String> = match ty {
Some(FieldType::I16) => strings(fetch::<i16, _>(tx, query).await?),
Some(FieldType::I32) => strings(fetch::<i32, _>(tx, query).await?),
Some(FieldType::I64) => strings(fetch::<i64, _>(tx, query).await?),
Some(FieldType::String) => fetch::<String, _>(tx, query).await?,
Some(FieldType::Uuid) => strings(fetch::<uuid::Uuid, _>(tx, query).await?),
_ => return Ok(None),
};
Ok(Some(keys.into_iter().collect()))
}
fn strings<K: ToString>(keys: Vec<K>) -> Vec<String> {
keys.iter().map(ToString::to_string).collect()
}
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);
}
auto_column_type(through, dst_col)
}
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,
}
pub(crate) fn generic_m2m_links(query: &DeleteQuery) -> Vec<DeleteQuery> {
let owner = query.model;
let mut entries = inventory::iter::<crate::core::GenericM2MEntry>
.into_iter()
.filter(|e| e.owner.table == owner.table)
.peekable();
if entries.peek().is_none() {
return Vec::new();
}
let (Some(pk), Some(model)) = (
owner.primary_key(),
crate::core::ModelEntry::for_schema(owner),
) else {
return Vec::new();
};
let app = model.resolved_app_label().unwrap_or("project");
let name = owner.name.to_ascii_lowercase();
let ct = <crate::contenttypes::ContentType as crate::core::Model>::SCHEMA;
let mut ct_ids = SelectQuery::new(ct).where_clause(WhereExpr::and_predicates(vec![
eq("app_label", SqlValue::String(app.to_owned())),
eq("model_name", SqlValue::String(name)),
]));
ct_ids.projection = Some(vec!["id"]);
let mut owner_pks = SelectQuery::new(owner).where_clause(query.where_clause.clone());
owner_pks.projection = Some(vec![pk.column]);
let every_row = query.where_clause.is_empty();
entries
.map(|e| {
let mut preds = vec![WhereExpr::InSubquery {
column: e.ct_col,
negated: false,
subquery: Box::new(ct_ids.clone()),
}];
if !every_row {
preds.push(WhereExpr::InSubquery {
column: e.pk_col,
negated: false,
subquery: Box::new(owner_pks.clone()),
});
}
DeleteQuery::new(
junction(e.through, &[e.pk_col, e.ct_col]),
WhereExpr::And(preds),
)
})
.collect()
}
pub(crate) fn generic_m2m_truncate_links(table: &str) -> Vec<DeleteQuery> {
inventory::iter::<crate::core::GenericM2MEntry>
.into_iter()
.find(|e| e.owner.table == table)
.map(|e| generic_m2m_links(&DeleteQuery::new(e.owner, WhereExpr::And(Vec::new()))))
.unwrap_or_default()
}
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];
let junction = self.junction();
let locks = source_lock(self.src_schema, &self.src_key()?)
.into_iter()
.collect();
if !replace(
locks,
junction,
owner,
self.dst_col,
&dsts,
columns,
rows,
pool,
)
.await?
{
return Ok(());
}
#[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?;
if delete(self.junction(), owner, pool).await? == 0 {
return Ok(());
}
#[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
}
}