use std::collections::HashMap;
use std::fmt::Write as _;
use diesel::sql_types::{BigInt, Nullable};
use diesel_async::RunQueryDsl as _;
use scoped_futures::ScopedFutureExt as _;
use crate::db::{RuntimeConnection, scoped_immediate_transaction};
use crate::{AutumnError, AutumnResult};
#[cfg(not(feature = "sqlite"))]
const PH1: &str = "$1";
#[cfg(not(feature = "sqlite"))]
const PH2: &str = "$2";
#[cfg(feature = "sqlite")]
const PH1: &str = "?";
#[cfg(feature = "sqlite")]
const PH2: &str = "?";
#[cfg(not(feature = "sqlite"))]
const FOR_UPDATE: &str = " FOR UPDATE";
#[cfg(feature = "sqlite")]
const FOR_UPDATE: &str = "";
#[cfg(not(feature = "sqlite"))]
const IS_DISTINCT_FROM: &str = "IS DISTINCT FROM";
#[cfg(feature = "sqlite")]
const IS_DISTINCT_FROM: &str = "IS NOT";
#[cfg(not(feature = "sqlite"))]
const IS_NOT_DISTINCT_FROM: &str = "IS NOT DISTINCT FROM";
#[cfg(feature = "sqlite")]
const IS_NOT_DISTINCT_FROM: &str = "IS";
const CHILD_ALIAS: &str = "__autumn_cc_child";
const DELETED_AT: &str = "deleted_at";
#[derive(Debug)]
pub struct CounterCacheSpec<M: 'static> {
pub child_table: &'static str,
pub child_pk: &'static str,
pub child_soft_delete: bool,
pub fk_column: &'static str,
pub parent_table: &'static str,
pub parent_pk: &'static str,
pub counter_column: &'static str,
pub fk_of: fn(&M) -> Option<i64>,
pub pk_of: fn(&M) -> i64,
pub live_of: fn(&M) -> bool,
pub tenant_column: Option<&'static str>,
}
impl<M: 'static> Clone for CounterCacheSpec<M> {
fn clone(&self) -> Self {
*self
}
}
impl<M: 'static> Copy for CounterCacheSpec<M> {}
pub trait AutumnCounterCaches: Sized + 'static {
const HAS_COUNTER_CACHES: bool = false;
#[must_use]
fn counter_caches() -> &'static [CounterCacheSpec<Self>] {
&[]
}
}
impl<T: Sized + 'static> AutumnCounterCaches for T {}
#[must_use]
pub fn is_plain_identifier(s: &str) -> bool {
!s.is_empty()
&& !s.starts_with(|c: char| c.is_ascii_digit())
&& s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn debug_assert_spec_idents<M: 'static>(spec: &CounterCacheSpec<M>) {
debug_assert!(
is_plain_identifier(spec.child_table)
&& is_plain_identifier(spec.child_pk)
&& is_plain_identifier(spec.fk_column)
&& is_plain_identifier(spec.parent_table)
&& is_plain_identifier(spec.parent_pk)
&& is_plain_identifier(spec.counter_column),
"counter-cache spec carries a non-identifier name; it would be spliced \
verbatim into SQL"
);
}
pub fn quote_ident(ident: &str) -> String {
format!("\"{ident}\"")
}
struct Quoted {
child_table: String,
child_pk: String,
fk_column: String,
parent_table: String,
parent_pk: String,
counter_column: String,
}
fn quoted<M: 'static>(spec: &CounterCacheSpec<M>) -> Quoted {
Quoted {
child_table: quote_ident(spec.child_table),
child_pk: quote_ident(spec.child_pk),
fk_column: quote_ident(spec.fk_column),
parent_table: quote_ident(spec.parent_table),
parent_pk: quote_ident(spec.parent_pk),
counter_column: quote_ident(spec.counter_column),
}
}
fn tenant_predicate_joined<M: 'static>(spec: &CounterCacheSpec<M>) -> String {
let Some(tenant_column) = spec.tenant_column else {
return String::new();
};
let tenant_column = quote_ident(tenant_column);
let parent_table = quote_ident(spec.parent_table);
format!(
" AND {parent_table}.{tenant_column} {IS_NOT_DISTINCT_FROM} \
{CHILD_ALIAS}.{tenant_column}"
)
}
fn tenant_predicate<M: 'static>(spec: &CounterCacheSpec<M>, child_id: i64) -> String {
let Some(tenant_column) = spec.tenant_column else {
return String::new();
};
let tenant_column = quote_ident(tenant_column);
let Quoted {
child_table,
child_pk,
parent_table,
..
} = quoted(spec);
format!(
" AND EXISTS \
(SELECT 1 FROM {child_table} AS {CHILD_ALIAS}_t \
WHERE {CHILD_ALIAS}_t.{child_pk} = {child_id} \
AND {parent_table}.{tenant_column} {IS_NOT_DISTINCT_FROM} \
{CHILD_ALIAS}_t.{tenant_column})"
)
}
fn live_predicate<M: 'static>(spec: &CounterCacheSpec<M>, want_live: bool) -> String {
if !spec.child_soft_delete {
return String::new();
}
let op = if want_live { "IS NULL" } else { "IS NOT NULL" };
let deleted_at = quote_ident(DELETED_AT);
format!(" AND {CHILD_ALIAS}.{deleted_at} {op}")
}
pub async fn counter_cache_apply_delta<M: 'static>(
conn: &mut RuntimeConnection,
spec: &CounterCacheSpec<M>,
parent_id: i64,
delta: i64,
scope: TenantScope,
) -> AutumnResult<()> {
debug_assert_spec_idents(spec);
let Quoted {
parent_table,
parent_pk,
counter_column,
..
} = quoted(spec);
let tenant = match scope {
TenantScope::SameTenantAsChild(child_id) => tenant_predicate(spec, child_id),
TenantScope::Unscoped => String::new(),
};
let sql = format!(
"UPDATE {parent_table} SET {counter_column} = {counter_column} + {PH1} \
WHERE {parent_table}.{parent_pk} = {PH2}{tenant}"
);
diesel::sql_query(sql)
.bind::<BigInt, _>(delta)
.bind::<BigInt, _>(parent_id)
.execute(conn)
.await
.map_err(AutumnError::from)?;
Ok(())
}
#[doc(hidden)]
pub async fn counter_cache_apply_delta_by_child_id<M: 'static>(
conn: &mut RuntimeConnection,
spec: &CounterCacheSpec<M>,
child_id: i64,
delta: i64,
child_state: ChildState,
) -> AutumnResult<()> {
debug_assert_spec_idents(spec);
let Quoted {
child_table,
child_pk,
fk_column,
parent_table,
parent_pk,
counter_column,
..
} = quoted(spec);
let state_predicate = match child_state {
ChildState::Any => String::new(),
ChildState::Live => live_predicate(spec, true),
ChildState::SoftDeleted => live_predicate(spec, false),
};
let tenant = tenant_predicate(spec, child_id);
let sql = format!(
"UPDATE {parent_table} SET {counter_column} = {counter_column} + {PH1} \
WHERE {parent_table}.{parent_pk} IN \
(SELECT {CHILD_ALIAS}.{fk_column} FROM {child_table} AS {CHILD_ALIAS} \
WHERE {CHILD_ALIAS}.{child_pk} = {PH2} \
AND {CHILD_ALIAS}.{fk_column} IS NOT NULL{state_predicate}{FOR_UPDATE}){tenant}"
);
diesel::sql_query(sql)
.bind::<BigInt, _>(delta)
.bind::<BigInt, _>(child_id)
.execute(conn)
.await
.map_err(AutumnError::from)?;
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TenantScope {
SameTenantAsChild(i64),
Unscoped,
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChildState {
Any,
Live,
SoftDeleted,
}
#[doc(hidden)]
pub async fn counter_cache_after_insert<M: Send + Sync + 'static>(
conn: &mut RuntimeConnection,
specs: &[CounterCacheSpec<M>],
record: &M,
) -> AutumnResult<()> {
let mut contributions: Vec<Contribution> = Vec::with_capacity(specs.len());
for (index, spec) in specs.iter().enumerate() {
if !(spec.live_of)(record) {
continue;
}
if let Some(parent_id) = (spec.fk_of)(record) {
contributions.push((index, parent_id, 1, (spec.pk_of)(record)));
}
}
apply_ordered(conn, specs, contributions).await
}
#[doc(hidden)]
pub async fn counter_cache_after_insert_many<M: Send + Sync + 'static>(
conn: &mut RuntimeConnection,
specs: &[CounterCacheSpec<M>],
records: &[M],
) -> AutumnResult<()> {
let mut contributions: Vec<Contribution> = Vec::new();
for (index, spec) in specs.iter().enumerate() {
for record in records {
if !(spec.live_of)(record) {
continue;
}
if let Some(parent_id) = (spec.fk_of)(record) {
contributions.push((index, parent_id, 1, (spec.pk_of)(record)));
}
}
}
apply_ordered(conn, specs, contributions).await
}
type Contribution = (usize, i64, i64, i64);
async fn apply_ordered<M: 'static>(
conn: &mut RuntimeConnection,
specs: &[CounterCacheSpec<M>],
contributions: Vec<Contribution>,
) -> AutumnResult<()> {
if contributions.is_empty() {
return Ok(());
}
for (spec_index, parent_id, delta, witness) in fold_and_order(specs, contributions) {
counter_cache_apply_delta(
conn,
&specs[spec_index],
parent_id,
delta,
TenantScope::SameTenantAsChild(witness),
)
.await?;
}
Ok(())
}
fn fold_and_order<M: 'static>(
specs: &[CounterCacheSpec<M>],
contributions: Vec<Contribution>,
) -> Vec<Contribution> {
let mut folded: Vec<Contribution> = Vec::with_capacity(contributions.len());
let mut seen: HashMap<(usize, i64), usize> = HashMap::new();
for (spec_index, parent_id, delta, witness) in contributions {
if specs[spec_index].tenant_column.is_some() {
folded.push((spec_index, parent_id, delta, witness));
continue;
}
if let Some(&at) = seen.get(&(spec_index, parent_id)) {
folded[at].2 += delta;
} else {
seen.insert((spec_index, parent_id), folded.len());
folded.push((spec_index, parent_id, delta, witness));
}
}
folded.retain(|&(_, _, delta, _)| delta != 0);
folded
.sort_by_key(|&(spec_index, parent_id, _, _)| (specs[spec_index].parent_table, parent_id));
folded
}
fn specs_in_lock_order<M: 'static>(specs: &[CounterCacheSpec<M>]) -> Vec<usize> {
let mut order: Vec<usize> = (0..specs.len()).collect();
order.sort_by_key(|&i| (specs[i].parent_table, specs[i].counter_column));
order
}
pub async fn counter_cache_after_insert_by_id<M: 'static>(
conn: &mut RuntimeConnection,
specs: &[CounterCacheSpec<M>],
child_id: i64,
) -> AutumnResult<()> {
for index in specs_in_lock_order(specs) {
let spec = &specs[index];
let state = if spec.child_soft_delete {
ChildState::Live
} else {
ChildState::Any
};
counter_cache_apply_delta_by_child_id(conn, spec, child_id, 1, state).await?;
}
Ok(())
}
pub async fn counter_cache_before_delete_by_id<M: 'static>(
conn: &mut RuntimeConnection,
specs: &[CounterCacheSpec<M>],
child_id: i64,
) -> AutumnResult<()> {
for index in specs_in_lock_order(specs) {
let spec = &specs[index];
let state = if spec.child_soft_delete {
ChildState::Live
} else {
ChildState::Any
};
counter_cache_apply_delta_by_child_id(conn, spec, child_id, -1, state).await?;
}
Ok(())
}
fn child_lock_sql<M: 'static>(spec: &CounterCacheSpec<M>, id_list: &str) -> String {
let Quoted {
child_table,
child_pk,
..
} = quoted(spec);
format!(
"SELECT {child_pk} AS id FROM {child_table} \
WHERE {child_pk} IN ({id_list}) ORDER BY {child_pk}{FOR_UPDATE}"
)
}
#[doc(hidden)]
pub async fn counter_cache_before_delete_many<M: 'static>(
conn: &mut RuntimeConnection,
specs: &[CounterCacheSpec<M>],
child_ids: &[i64],
) -> AutumnResult<()> {
if specs.is_empty() || child_ids.is_empty() {
return Ok(());
}
let id_list = id_list(child_ids);
debug_assert_spec_idents(&specs[0]);
diesel::sql_query(child_lock_sql(&specs[0], &id_list))
.load::<IdRow>(conn)
.await
.map_err(AutumnError::from)?;
for index in specs_in_lock_order(specs) {
let spec = &specs[index];
debug_assert_spec_idents(spec);
let Quoted {
child_table,
child_pk,
fk_column,
parent_table,
parent_pk,
counter_column,
..
} = quoted(spec);
let live = if spec.child_soft_delete {
live_predicate(spec, true)
} else {
String::new()
};
let tenant = tenant_predicate_joined(spec);
let sql = format!(
"UPDATE {parent_table} SET {counter_column} = {counter_column} - \
(SELECT COUNT(*) FROM {child_table} AS {CHILD_ALIAS} \
WHERE {CHILD_ALIAS}.{fk_column} = {parent_table}.{parent_pk} \
AND {CHILD_ALIAS}.{child_pk} IN ({id_list}){live}{tenant}) \
WHERE {parent_table}.{parent_pk} IN \
(SELECT {CHILD_ALIAS}.{fk_column} FROM {child_table} AS {CHILD_ALIAS} \
WHERE {CHILD_ALIAS}.{child_pk} IN ({id_list}) \
AND {CHILD_ALIAS}.{fk_column} IS NOT NULL{live} \
AND {CHILD_ALIAS}.{fk_column} = {parent_table}.{parent_pk}{tenant})"
);
diesel::sql_query(sql)
.execute(conn)
.await
.map_err(AutumnError::from)?;
}
Ok(())
}
#[doc(hidden)]
pub async fn counter_cache_before_detach_many<M: 'static>(
conn: &mut RuntimeConnection,
specs: &[CounterCacheSpec<M>],
fk_column: &str,
child_ids: &[i64],
) -> AutumnResult<()> {
let detached: Vec<CounterCacheSpec<M>> = specs
.iter()
.filter(|spec| spec.fk_column == fk_column)
.copied()
.collect();
counter_cache_before_delete_many(conn, &detached, child_ids).await
}
fn id_list(ids: &[i64]) -> String {
let mut out = String::new();
for (i, id) in ids.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&id.to_string());
}
out
}
#[doc(hidden)]
pub async fn counter_cache_before_restore_by_id<M: 'static>(
conn: &mut RuntimeConnection,
specs: &[CounterCacheSpec<M>],
child_id: i64,
) -> AutumnResult<()> {
for index in specs_in_lock_order(specs) {
let spec = &specs[index];
if spec.child_soft_delete {
counter_cache_apply_delta_by_child_id(conn, spec, child_id, 1, ChildState::SoftDeleted)
.await?;
}
}
Ok(())
}
#[doc(hidden)]
pub async fn counter_cache_capture_fks<M: 'static>(
conn: &mut RuntimeConnection,
specs: &[CounterCacheSpec<M>],
child_id: i64,
) -> AutumnResult<Vec<Option<i64>>> {
if specs.is_empty() {
return Ok(Vec::new());
}
let mut out = Vec::with_capacity(specs.len());
for spec in specs {
debug_assert_spec_idents(spec);
let Quoted {
child_table,
child_pk,
fk_column,
..
} = quoted(spec);
let live = live_predicate(spec, true);
let sql = format!(
"SELECT {CHILD_ALIAS}.{fk_column} AS fk_value \
FROM {child_table} AS {CHILD_ALIAS} \
WHERE {CHILD_ALIAS}.{child_pk} = {PH1}{live}{FOR_UPDATE}"
);
let row: Option<FkRow> = diesel::sql_query(sql)
.bind::<BigInt, _>(child_id)
.get_result::<FkRow>(conn)
.await
.optional_row()?;
out.push(row.and_then(|r| r.fk_value));
}
Ok(out)
}
#[doc(hidden)]
pub async fn counter_cache_after_update<M: Send + Sync + 'static>(
conn: &mut RuntimeConnection,
specs: &[CounterCacheSpec<M>],
before: &[Option<i64>],
record: &M,
) -> AutumnResult<()> {
let mut moves: Vec<Contribution> = Vec::with_capacity(specs.len() * 2);
for (index, spec) in specs.iter().enumerate() {
let old = before.get(index).copied().flatten();
let new = if (spec.live_of)(record) {
(spec.fk_of)(record)
} else {
None
};
if old == new {
continue;
}
let witness = (spec.pk_of)(record);
if let Some(old_id) = old {
moves.push((index, old_id, -1, witness));
}
if let Some(new_id) = new {
moves.push((index, new_id, 1, witness));
}
}
apply_ordered(conn, specs, moves).await
}
#[doc(hidden)]
pub async fn counter_cache_capture_fks_many<M: 'static>(
conn: &mut RuntimeConnection,
specs: &[CounterCacheSpec<M>],
child_ids: &[i64],
) -> AutumnResult<Vec<(i64, Vec<Option<i64>>)>> {
if specs.is_empty() || child_ids.is_empty() {
return Ok(Vec::new());
}
let id_list = id_list(child_ids);
let mut by_child: HashMap<i64, Vec<Option<i64>>> = HashMap::new();
for (index, spec) in specs.iter().enumerate() {
debug_assert_spec_idents(spec);
let Quoted {
child_table,
child_pk,
fk_column,
..
} = quoted(spec);
let live = live_predicate(spec, true);
let sql = format!(
"SELECT {CHILD_ALIAS}.{child_pk} AS child_id, \
{CHILD_ALIAS}.{fk_column} AS fk_value \
FROM {child_table} AS {CHILD_ALIAS} \
WHERE {CHILD_ALIAS}.{child_pk} IN ({id_list}){live} \
ORDER BY {CHILD_ALIAS}.{child_pk}{FOR_UPDATE}"
);
let rows: Vec<ChildFkRow> = diesel::sql_query(sql)
.load::<ChildFkRow>(conn)
.await
.map_err(AutumnError::from)?;
for row in rows {
let entry = by_child
.entry(row.child_id)
.or_insert_with(|| vec![None; specs.len()]);
entry[index] = row.fk_value;
}
}
let mut out: Vec<(i64, Vec<Option<i64>>)> = by_child.into_iter().collect();
out.sort_unstable_by_key(|(id, _)| *id);
Ok(out)
}
#[doc(hidden)]
pub async fn counter_cache_after_update_many<M: Send + Sync + 'static>(
conn: &mut RuntimeConnection,
specs: &[CounterCacheSpec<M>],
before: &[(i64, Vec<Option<i64>>)],
records: &[M],
) -> AutumnResult<()> {
if specs.is_empty() {
return Ok(());
}
let pk_of = specs[0].pk_of;
let mut contributions: Vec<Contribution> = Vec::new();
for (index, spec) in specs.iter().enumerate() {
for record in records {
let child_id = pk_of(record);
let Ok(found) = before.binary_search_by_key(&child_id, |(id, _)| *id) else {
continue;
};
let old = before[found].1.get(index).copied().flatten();
let new = if (spec.live_of)(record) {
(spec.fk_of)(record)
} else {
None
};
if old == new {
continue;
}
if let Some(old_id) = old {
contributions.push((index, old_id, -1, child_id));
}
if let Some(new_id) = new {
contributions.push((index, new_id, 1, child_id));
}
}
}
apply_ordered(conn, specs, contributions).await
}
#[doc(hidden)]
pub async fn counter_cache_after_upsert_many<M: Send + Sync + 'static>(
conn: &mut RuntimeConnection,
specs: &[CounterCacheSpec<M>],
existing: &[M],
upserted: &[M],
) -> AutumnResult<()> {
if specs.is_empty() {
return Ok(());
}
let pk_of = specs[0].pk_of;
let before: HashMap<i64, Vec<Option<i64>>> = existing
.iter()
.map(|row| {
let live = specs.first().is_none_or(|spec| (spec.live_of)(row));
(
pk_of(row),
specs
.iter()
.map(|spec| if live { (spec.fk_of)(row) } else { None })
.collect(),
)
})
.collect();
let mut contributions: Vec<Contribution> = Vec::new();
for (index, spec) in specs.iter().enumerate() {
for record in upserted {
let child_id = pk_of(record);
let new = if (spec.live_of)(record) {
(spec.fk_of)(record)
} else {
None
};
match before.get(&child_id) {
None => {
if let Some(parent_id) = new {
contributions.push((index, parent_id, 1, child_id));
}
}
Some(old_fks) => {
let old = old_fks.get(index).copied().flatten();
if old == new {
continue;
}
if let Some(old_id) = old {
contributions.push((index, old_id, -1, child_id));
}
if let Some(new_id) = new {
contributions.push((index, new_id, 1, child_id));
}
}
}
}
}
apply_ordered(conn, specs, contributions).await
}
const RECOMPUTE_BATCH: i64 = 1_000;
fn recompute_update_sql<M: 'static>(spec: &CounterCacheSpec<M>, ids: &str) -> String {
let Quoted {
child_table,
fk_column,
parent_table,
parent_pk,
counter_column,
..
} = quoted(spec);
let live = live_predicate(spec, true);
let tenant = tenant_predicate_joined(spec);
let ground_truth = format!(
"(SELECT COUNT(*) FROM {child_table} AS {CHILD_ALIAS} \
WHERE {CHILD_ALIAS}.{fk_column} = {parent_table}.{parent_pk}{live}{tenant})"
);
format!(
"UPDATE {parent_table} SET {counter_column} = {ground_truth} \
WHERE {parent_table}.{parent_pk} IN ({ids}) \
AND {parent_table}.{counter_column} {IS_DISTINCT_FROM} {ground_truth}"
)
}
async fn recompute_batch<M: 'static>(
conn: &mut RuntimeConnection,
spec: &CounterCacheSpec<M>,
ids: &[i64],
) -> AutumnResult<usize> {
if ids.is_empty() {
return Ok(0);
}
let id_list = id_list(ids);
let parent_table = quote_ident(spec.parent_table);
let parent_pk = quote_ident(spec.parent_pk);
let lock_sql = format!(
"SELECT {parent_pk} AS id FROM {parent_table} \
WHERE {parent_pk} IN ({id_list}) ORDER BY {parent_pk}{FOR_UPDATE}"
);
let update_sql = recompute_update_sql(spec, &id_list);
scoped_immediate_transaction::<usize, AutumnError, _>(conn, move |conn| {
async move {
diesel::sql_query(lock_sql)
.load::<IdRow>(&mut *conn)
.await
.map_err(AutumnError::from)?;
diesel::sql_query(update_sql)
.execute(&mut *conn)
.await
.map_err(AutumnError::from)
}
.scope_boxed()
})
.await
}
#[doc(hidden)]
pub async fn counter_cache_recompute<M: 'static>(
conn: &mut RuntimeConnection,
specs: &[CounterCacheSpec<M>],
parent_id: Option<i64>,
) -> AutumnResult<usize> {
let mut touched = 0usize;
for index in specs_in_lock_order(specs) {
let spec = &specs[index];
debug_assert_spec_idents(spec);
if let Some(id) = parent_id {
touched += recompute_batch(conn, spec, &[id]).await?;
continue;
}
let parent_table = quote_ident(spec.parent_table);
let parent_pk = quote_ident(spec.parent_pk);
let mut cursor: Option<i64> = None;
loop {
let mut page_sql = format!("SELECT {parent_pk} AS id FROM {parent_table}");
if cursor.is_some() {
let _ = write!(page_sql, " WHERE {parent_pk} > {PH1}");
}
let _ = write!(page_sql, " ORDER BY {parent_pk} LIMIT {RECOMPUTE_BATCH}");
let query = diesel::sql_query(page_sql);
let page = if let Some(after) = cursor {
query.bind::<BigInt, _>(after).load::<IdRow>(conn).await
} else {
query.load::<IdRow>(conn).await
}
.map_err(AutumnError::from)?;
let Some(last) = page.last() else { break };
cursor = Some(last.id);
let ids: Vec<i64> = page.iter().map(|row| row.id).collect();
touched += recompute_batch(conn, spec, &ids).await?;
}
}
Ok(touched)
}
#[derive(diesel::QueryableByName)]
struct ChildFkRow {
#[diesel(sql_type = BigInt)]
child_id: i64,
#[diesel(sql_type = Nullable<BigInt>)]
fk_value: Option<i64>,
}
#[derive(diesel::QueryableByName)]
struct IdRow {
#[diesel(sql_type = BigInt)]
id: i64,
}
#[derive(diesel::QueryableByName)]
struct FkRow {
#[diesel(sql_type = Nullable<BigInt>)]
fk_value: Option<i64>,
}
trait OptionalRow<T> {
fn optional_row(self) -> AutumnResult<Option<T>>;
}
impl<T> OptionalRow<T> for Result<T, diesel::result::Error> {
fn optional_row(self) -> AutumnResult<Option<T>> {
match self {
Ok(value) => Ok(Some(value)),
Err(diesel::result::Error::NotFound) => Ok(None),
Err(e) => Err(AutumnError::from(e)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Dummy;
fn spec(soft: bool) -> CounterCacheSpec<Dummy> {
CounterCacheSpec {
child_table: "comments",
child_pk: "id",
child_soft_delete: soft,
fk_column: "post_id",
parent_table: "posts",
parent_pk: "id",
counter_column: "comment_count",
fk_of: |_| Some(1),
pk_of: |_| 1,
live_of: |_| true,
tenant_column: None,
}
}
#[test]
fn plain_identifiers_are_accepted_and_sql_fragments_are_not() {
assert!(is_plain_identifier("comment_count"));
assert!(is_plain_identifier("_x9"));
assert!(!is_plain_identifier(""));
assert!(!is_plain_identifier("9lives"));
assert!(!is_plain_identifier("comment_count; DROP TABLE posts"));
assert!(!is_plain_identifier("comment count"));
assert!(!is_plain_identifier("\"comment_count\""));
}
#[test]
fn the_live_predicate_is_emitted_only_for_a_soft_deleting_child() {
assert_eq!(live_predicate(&spec(false), true), "");
assert_eq!(
live_predicate(&spec(true), true),
format!(" AND {CHILD_ALIAS}.\"deleted_at\" IS NULL")
);
assert_eq!(
live_predicate(&spec(true), false),
format!(" AND {CHILD_ALIAS}.\"deleted_at\" IS NOT NULL")
);
}
#[test]
fn a_bulk_decrement_locks_its_children_in_ascending_id_order() {
let sql = child_lock_sql(&spec(false), "3,1,2");
assert!(
sql.starts_with("SELECT \"id\" AS id FROM \"comments\""),
"{sql}"
);
assert!(sql.contains("WHERE \"id\" IN (3,1,2)"), "{sql}");
assert!(
sql.contains(&format!("ORDER BY \"id\"{FOR_UPDATE}")),
"the lock must be taken in a deterministic order: {sql}"
);
}
#[test]
fn a_recompute_batch_is_scoped_to_the_ids_it_locked() {
let sql = recompute_update_sql(&spec(false), "1,2,3");
assert!(sql.contains("\"posts\".\"id\" IN (1,2,3)"), "{sql}");
assert!(
sql.contains(&format!("\"posts\".\"comment_count\" {IS_DISTINCT_FROM}")),
"a healthy parent must still be left unwritten: {sql}"
);
}
fn two_legs() -> Vec<CounterCacheSpec<Dummy>> {
let mut users = spec(false);
users.parent_table = "users";
users.counter_column = "sent_count";
let mut posts = spec(false);
posts.parent_table = "posts";
posts.counter_column = "comment_count";
vec![users, posts]
}
#[test]
fn deltas_go_out_in_a_globally_stable_lock_order() {
let specs = two_legs();
let ordered = fold_and_order(&specs, vec![(0, 5, 1, 100), (1, 9, 1, 100), (1, 2, 1, 100)]);
let keys: Vec<(&str, i64)> = ordered
.iter()
.map(|&(i, parent_id, _, _)| (specs[i].parent_table, parent_id))
.collect();
assert_eq!(keys, vec![("posts", 2), ("posts", 9), ("users", 5)]);
assert_eq!(specs_in_lock_order(&specs), vec![1, 0]);
}
#[test]
fn contributions_to_one_parent_fold_into_a_single_statement() {
let specs = two_legs();
let ordered = fold_and_order(
&specs,
vec![
(1, 2, -1, 10),
(1, 2, 1, 11),
(1, 7, 1, 12),
(1, 7, 1, 13),
(0, 4, -1, 14),
],
);
assert_eq!(ordered, vec![(1, 7, 2, 12), (0, 4, -1, 14)]);
}
#[test]
fn a_tenant_scoped_leg_keeps_one_statement_per_child() {
let mut specs = two_legs();
specs[1].tenant_column = Some("tenant_id");
let ordered = fold_and_order(&specs, vec![(1, 3, 1, 20), (1, 3, 1, 21)]);
assert_eq!(ordered, vec![(1, 3, 1, 20), (1, 3, 1, 21)]);
}
#[test]
fn a_tenant_discriminator_is_matched_null_safely() {
let mut tenanted = spec(false);
tenanted.tenant_column = Some("tenant_id");
let joined = tenant_predicate_joined(&tenanted);
assert!(joined.contains(IS_NOT_DISTINCT_FROM), "{joined}");
assert!(
!joined.contains("\"tenant_id\" = "),
"plain equality is not NULL-safe: {joined}"
);
let keyed = tenant_predicate(&tenanted, 7);
assert!(keyed.contains("EXISTS"), "{keyed}");
assert!(keyed.contains(IS_NOT_DISTINCT_FROM), "{keyed}");
assert!(keyed.contains("\"id\" = 7"), "{keyed}");
assert_eq!(tenant_predicate_joined(&spec(false)), "");
assert_eq!(tenant_predicate(&spec(false), 7), "");
}
#[test]
fn a_counter_column_named_after_a_sql_keyword_still_produces_valid_sql() {
let mut keyword = spec(false);
keyword.counter_column = "order";
keyword.parent_table = "group";
let sql = recompute_update_sql(&keyword, "1");
assert!(
sql.starts_with("UPDATE \"group\" SET \"order\" = "),
"{sql}"
);
assert!(sql.contains("\"group\".\"order\" "), "{sql}");
assert!(
!sql.contains(" order "),
"no bare keyword may survive: {sql}"
);
}
#[test]
fn a_model_without_an_inherent_shadow_resolves_to_the_empty_blanket() {
const { assert!(!Dummy::HAS_COUNTER_CACHES) };
assert!(Dummy::counter_caches().is_empty());
}
}