use super::error::ExecError;
use super::{FlatScalar, Pool};
use crate::core::{FieldType, QueryError, SqlValue};
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 dialect = pool.dialect();
let src = self.src_key()?;
let sql = format!(
"SELECT {dst} FROM {through} WHERE {src} = {p1}",
through = dialect.quote_ident(self.through),
src = dialect.quote_ident(self.src_col),
dst = dialect.quote_ident(self.dst_col),
p1 = dialect.placeholder(1),
);
let binds = vec![src.clone()];
fetch_col_pool::<K>(pool, &sql, binds).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 dialect = pool.dialect();
let src = self.src_key()?;
let src_q = dialect.quote_ident(self.src_col);
let sql = format!(
"INSERT INTO {through} ({src_q}, {dst}) VALUES ({p1}, {p2}) {skip}",
through = dialect.quote_ident(self.through),
dst = dialect.quote_ident(self.dst_col),
skip = skip_duplicate(dialect, &src_q),
p1 = dialect.placeholder(1),
p2 = dialect.placeholder(2),
);
let binds = vec![src.clone(), dst.clone()];
super::executor::raw_execute_pool(pool, &sql, binds).await?;
#[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 dialect = pool.dialect();
let src = self.src_key()?;
let sql = format!(
"DELETE FROM {through} WHERE {src} = {p1} AND {dst} = {p2}",
through = dialect.quote_ident(self.through),
src = dialect.quote_ident(self.src_col),
dst = dialect.quote_ident(self.dst_col),
p1 = dialect.placeholder(1),
p2 = dialect.placeholder(2),
);
let binds = vec![src.clone(), dst.clone()];
super::executor::raw_execute_pool(pool, &sql, binds).await?;
#[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 dialect = pool.dialect();
let src = self.src_key()?;
let del_sql = format!(
"DELETE FROM {through} WHERE {src} = {p1}",
through = dialect.quote_ident(self.through),
src = dialect.quote_ident(self.src_col),
p1 = dialect.placeholder(1),
);
let dsts = ids
.iter()
.map(|k| dst_key(k.clone(), self.through, self.dst_col))
.collect::<Result<Vec<_>, _>>()?;
let ins_sql_with_binds = if dsts.is_empty() {
None
} else {
let mut sql = format!(
"INSERT INTO {through} ({src}, {dst}) VALUES ",
through = dialect.quote_ident(self.through),
src = dialect.quote_ident(self.src_col),
dst = dialect.quote_ident(self.dst_col),
);
let mut binds = Vec::with_capacity(ids.len() * 2);
for (i, dst) in dsts.iter().enumerate() {
if i > 0 {
sql.push_str(", ");
}
let p_src = dialect.placeholder(i * 2 + 1);
let p_dst = dialect.placeholder(i * 2 + 2);
sql.push_str(&format!("({p_src}, {p_dst})"));
binds.push(src.clone());
binds.push(dst.clone());
}
Some((sql, binds))
};
let mut tx = crate::sql::transaction_pool(pool).await?;
crate::sql::raw_execute_tx(&mut tx, &del_sql, vec![src.clone()]).await?;
if let Some((ins_sql, binds)) = ins_sql_with_binds {
crate::sql::raw_execute_tx(&mut tx, &ins_sql, binds).await?;
}
tx.commit().await.map_err(ExecError::Driver)?;
#[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 dialect = pool.dialect();
let src = self.src_key()?;
let sql = format!(
"DELETE FROM {through} WHERE {src} = {p1}",
through = dialect.quote_ident(self.through),
src = dialect.quote_ident(self.src_col),
p1 = dialect.placeholder(1),
);
let binds = vec![src.clone()];
super::executor::raw_execute_pool(pool, &sql, binds).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 dialect = pool.dialect();
let src = self.src_key()?;
let sql = format!(
"SELECT COUNT(*) FROM {through} WHERE {src} = {p1} AND {dst} = {p2}",
through = dialect.quote_ident(self.through),
src = dialect.quote_ident(self.src_col),
dst = dialect.quote_ident(self.dst_col),
p1 = dialect.placeholder(1),
p2 = dialect.placeholder(2),
);
let binds = vec![src.clone(), dst.clone()];
let rows = fetch_col_pool::<i64>(pool, &sql, binds).await?;
Ok(rows.first().is_some_and(|n| *n > 0))
}
fn src_key(&self) -> Result<SqlValue, ExecError> {
src_key(&self.src_pk, self.through)
}
}
fn skip_duplicate(dialect: &dyn super::Dialect, pivot: &str) -> String {
if dialect.name() == "mysql" {
dialect.insert_on_conflict_skip(&[pivot])
} else {
"ON CONFLICT DO NOTHING".to_owned()
}
}
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_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)
}
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 dialect = pool.dialect();
let src = self.src_key()?;
let ct = self.ct_id(pool).await?;
let sql = format!(
"SELECT {dst} FROM {through} WHERE {pk} = {p1} AND {ctc} = {p2}",
through = dialect.quote_ident(self.through),
pk = dialect.quote_ident(self.pk_col),
ctc = dialect.quote_ident(self.ct_col),
dst = dialect.quote_ident(self.dst_col),
p1 = dialect.placeholder(1),
p2 = dialect.placeholder(2),
);
let binds = vec![src.clone(), SqlValue::I64(ct)];
fetch_col_pool::<K>(pool, &sql, binds).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 dialect = pool.dialect();
let src = self.src_key()?;
let ct = self.ct_id(pool).await?;
let pk = dialect.quote_ident(self.pk_col);
let sql = format!(
"INSERT INTO {through} ({pk}, {ctc}, {dst}) VALUES ({p1}, {p2}, {p3}) {skip}",
through = dialect.quote_ident(self.through),
skip = skip_duplicate(dialect, &pk),
ctc = dialect.quote_ident(self.ct_col),
dst = dialect.quote_ident(self.dst_col),
p1 = dialect.placeholder(1),
p2 = dialect.placeholder(2),
p3 = dialect.placeholder(3),
);
let binds = vec![src.clone(), SqlValue::I64(ct), dst.clone()];
super::executor::raw_execute_pool(pool, &sql, binds).await?;
#[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 dialect = pool.dialect();
let src = self.src_key()?;
let ct = self.ct_id(pool).await?;
let sql = format!(
"DELETE FROM {through} WHERE {pk} = {p1} AND {ctc} = {p2} AND {dst} = {p3}",
through = dialect.quote_ident(self.through),
pk = dialect.quote_ident(self.pk_col),
ctc = dialect.quote_ident(self.ct_col),
dst = dialect.quote_ident(self.dst_col),
p1 = dialect.placeholder(1),
p2 = dialect.placeholder(2),
p3 = dialect.placeholder(3),
);
let binds = vec![src.clone(), SqlValue::I64(ct), dst.clone()];
super::executor::raw_execute_pool(pool, &sql, binds).await?;
#[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 dialect = pool.dialect();
let src = self.src_key()?;
let ct = self.ct_id(pool).await?;
let del_sql = format!(
"DELETE FROM {through} WHERE {pk} = {p1} AND {ctc} = {p2}",
through = dialect.quote_ident(self.through),
pk = dialect.quote_ident(self.pk_col),
ctc = dialect.quote_ident(self.ct_col),
p1 = dialect.placeholder(1),
p2 = dialect.placeholder(2),
);
let dsts = ids
.iter()
.map(|k| dst_key(k.clone(), self.through, self.dst_col))
.collect::<Result<Vec<_>, _>>()?;
let ins = if dsts.is_empty() {
None
} else {
let mut sql = format!(
"INSERT INTO {through} ({pk}, {ctc}, {dst}) VALUES ",
through = dialect.quote_ident(self.through),
pk = dialect.quote_ident(self.pk_col),
ctc = dialect.quote_ident(self.ct_col),
dst = dialect.quote_ident(self.dst_col),
);
let mut binds = Vec::with_capacity(ids.len() * 3);
for (i, dst) in dsts.iter().enumerate() {
if i > 0 {
sql.push_str(", ");
}
let p1 = dialect.placeholder(i * 3 + 1);
let p2 = dialect.placeholder(i * 3 + 2);
let p3 = dialect.placeholder(i * 3 + 3);
sql.push_str(&format!("({p1}, {p2}, {p3})"));
binds.push(src.clone());
binds.push(SqlValue::I64(ct));
binds.push(dst.clone());
}
Some((sql, binds))
};
let mut tx = crate::sql::transaction_pool(pool).await?;
crate::sql::raw_execute_tx(&mut tx, &del_sql, vec![src.clone(), SqlValue::I64(ct)]).await?;
if let Some((ins_sql, binds)) = ins {
crate::sql::raw_execute_tx(&mut tx, &ins_sql, binds).await?;
}
tx.commit().await.map_err(ExecError::Driver)?;
#[cfg(feature = "signals")]
self.signal(crate::signals::m2m::M2mAction::Set, dsts).await;
Ok(())
}
pub async fn clear(&self, pool: &Pool) -> Result<(), ExecError> {
let dialect = pool.dialect();
let src = self.src_key()?;
let ct = self.ct_id(pool).await?;
let sql = format!(
"DELETE FROM {through} WHERE {pk} = {p1} AND {ctc} = {p2}",
through = dialect.quote_ident(self.through),
pk = dialect.quote_ident(self.pk_col),
ctc = dialect.quote_ident(self.ct_col),
p1 = dialect.placeholder(1),
p2 = dialect.placeholder(2),
);
let binds = vec![src.clone(), SqlValue::I64(ct)];
super::executor::raw_execute_pool(pool, &sql, binds).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 dialect = pool.dialect();
let src = self.src_key()?;
let ct = self.ct_id(pool).await?;
let sql = format!(
"SELECT COUNT(*) FROM {through} WHERE {pk} = {p1} AND {ctc} = {p2} AND {dst} = {p3}",
through = dialect.quote_ident(self.through),
pk = dialect.quote_ident(self.pk_col),
ctc = dialect.quote_ident(self.ct_col),
dst = dialect.quote_ident(self.dst_col),
p1 = dialect.placeholder(1),
p2 = dialect.placeholder(2),
p3 = dialect.placeholder(3),
);
let binds = vec![src.clone(), SqlValue::I64(ct), dst.clone()];
let rows = fetch_col_pool::<i64>(pool, &sql, binds).await?;
Ok(rows.first().is_some_and(|n| *n > 0))
}
}
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
}
}
async fn fetch_col_pool<K: FlatScalar>(
pool: &Pool,
sql: &str,
binds: Vec<SqlValue>,
) -> Result<Vec<K>, ExecError> {
crate::test_assertions::query_counter::bump();
super::executor::fetch_flat_raw(pool, sql, binds).await
}