Skip to main content

vantage_table/table/sets/
hooks.rs

1//! Fire a table's lifecycle hooks around a write.
2//!
3//! Before-write hooks run on the record about to be written, in phase order,
4//! just ahead of set-invariant enforcement; an error cancels the operation.
5//! `before_delete` hooks may instead return [`HookReturn::Handled`] to take over
6//! (soft-delete). After-commit hooks run for their side-effects: at once, or,
7//! when the table's datasource is in a transaction, after it commits. Hooks
8//! receive the entity-erased table for relation/datasource access.
9
10use std::future::Future;
11
12use vantage_core::Result;
13use vantage_types::{EmptyEntity, Entity, InvariantValue, Record};
14use vantage_vista::Transaction;
15
16use super::invariants::enforce_invariants;
17use crate::table::{
18    AfterCx, AfterFn, BeforeCx, BeforeDeleteFn, BeforeFn, DeleteAllCx, DeleteAllFn, DeleteCx,
19    HookReturn, Phase, Table,
20};
21use crate::traits::table_source::TableSource;
22use crate::write_context::WriteContext;
23
24/// The two halves of a single-record insert that surround the driver's write.
25/// A driver's bulk path calls them around its batch so that it stays
26/// equivalent to inserting the records one by one.
27impl<T: TableSource, E: Entity<T::Value>> Table<T, E>
28where
29    T::Value: InvariantValue,
30{
31    /// Everything a single insert does to `record` before the driver write:
32    /// the confined-writes check, before-insert hooks, dropping read-only
33    /// imported columns, and conforming to the set's invariants. Returns the
34    /// record to store.
35    pub async fn prepare_insert(&self, record: &Record<T::Value>) -> Result<Record<T::Value>> {
36        self.require_confined_writes("insert")?;
37        let mut record = record.clone();
38        run_before(
39            self.before_insert_hooks(),
40            &mut record,
41            None,
42            self.as_entity_erased(),
43        )
44        .await?;
45        self.strip_imported_columns(&mut record);
46        enforce_invariants(&mut record, self.invariants())?;
47        Ok(record)
48    }
49
50    /// Run the after-insert hooks for a record stored under `id`, as a single
51    /// insert does once the driver write has succeeded. Inside a transaction
52    /// they are held until it commits.
53    pub async fn finish_insert(&self, id: &T::Id, record: &Record<T::Value>) -> Result<()> {
54        run_after(
55            self.after_insert_hooks(),
56            id,
57            record,
58            None,
59            self.as_entity_erased(),
60        )
61        .await
62    }
63}
64
65/// The row an update writes, the row as the set holds it, and whether the
66/// update is a patch (its record holds only the patched fields).
67pub(crate) struct UpdateOf<'a, T: TableSource> {
68    pub id: &'a T::Id,
69    pub stored: Option<&'a Record<T::Value>>,
70    pub patch: bool,
71}
72
73/// `update` is `None` on insert.
74pub(crate) async fn run_before<T: TableSource>(
75    hooks: &[(Phase, BeforeFn<T>)],
76    record: &mut Record<T::Value>,
77    update: Option<UpdateOf<'_, T>>,
78    table: &Table<T, EmptyEntity>,
79) -> Result<()> {
80    if hooks.is_empty() {
81        return Ok(());
82    }
83    let context = WriteContext::current();
84    for (_phase, hook) in hooks {
85        hook(BeforeCx {
86            record: &mut *record,
87            id: update.as_ref().map(|u| u.id),
88            stored: update.as_ref().and_then(|u| u.stored),
89            patch: update.as_ref().is_some_and(|u| u.patch),
90            context: context.as_ref(),
91            table,
92        })
93        .await?;
94    }
95    Ok(())
96}
97
98/// Inside a transaction the hooks are held until it commits.
99pub(crate) async fn run_after<T: TableSource>(
100    hooks: &[AfterFn<T>],
101    id: &T::Id,
102    record: &Record<T::Value>,
103    before: Option<&Record<T::Value>>,
104    table: &Table<T, EmptyEntity>,
105) -> Result<()> {
106    if hooks.is_empty() {
107        return Ok(());
108    }
109    let Some(tx) = table.data_source().transaction() else {
110        return run_after_now(hooks, id, record, before, table).await;
111    };
112    let hooks = hooks.to_vec();
113    let (id, record, before) = (id.clone(), record.clone(), before.cloned());
114    after_commit(&tx, table, move |table| async move {
115        run_after_now(&hooks, &id, &record, before.as_ref(), &table).await
116    });
117    Ok(())
118}
119
120/// Inside a transaction the hooks are held until it commits.
121pub(crate) async fn run_after_delete_all<T: TableSource>(
122    hooks: &[DeleteAllFn<T>],
123    table: &Table<T, EmptyEntity>,
124) -> Result<()> {
125    if hooks.is_empty() {
126        return Ok(());
127    }
128    let Some(tx) = table.data_source().transaction() else {
129        return run_delete_all(hooks, table).await;
130    };
131    let hooks = hooks.to_vec();
132    after_commit(&tx, table, move |table| async move {
133        run_delete_all(&hooks, &table).await
134    });
135    Ok(())
136}
137
138/// Hold `work` until `tx` commits; a rollback drops it. It runs on a copy of
139/// `table` back on its pool, the transaction being gone by then, under the
140/// writer's [`WriteContext`].
141fn after_commit<T, F, Fut>(tx: &Transaction, table: &Table<T, EmptyEntity>, work: F)
142where
143    T: TableSource,
144    F: FnOnce(Table<T, EmptyEntity>) -> Fut + Send + 'static,
145    Fut: Future<Output = Result<()>> + Send + 'static,
146{
147    let mut outside = table.with_data_source(table.data_source().without_transaction());
148    // Out of the transaction a locked read would wait on other writers' row locks.
149    outside.lock = false;
150    let context = WriteContext::current();
151    tx.defer(Box::new(move || {
152        Box::pin(async move {
153            match context {
154                Some(context) => context.scope(work(outside)).await,
155                None => work(outside).await,
156            }
157        })
158    }));
159}
160
161async fn run_after_now<T: TableSource>(
162    hooks: &[AfterFn<T>],
163    id: &T::Id,
164    record: &Record<T::Value>,
165    before: Option<&Record<T::Value>>,
166    table: &Table<T, EmptyEntity>,
167) -> Result<()> {
168    let context = WriteContext::current();
169    for hook in hooks {
170        hook(AfterCx {
171            id,
172            record,
173            before,
174            context: context.as_ref(),
175            table,
176        })
177        .await?;
178    }
179    Ok(())
180}
181
182pub(crate) async fn run_delete_all<T: TableSource>(
183    hooks: &[DeleteAllFn<T>],
184    table: &Table<T, EmptyEntity>,
185) -> Result<()> {
186    if hooks.is_empty() {
187        return Ok(());
188    }
189    let context = WriteContext::current();
190    for hook in hooks {
191        hook(DeleteAllCx {
192            context: context.as_ref(),
193            table,
194        })
195        .await?;
196    }
197    Ok(())
198}
199
200/// Run before-delete hooks; the first `Handled` short-circuits and signals the
201/// caller to skip the underlying delete.
202pub(crate) async fn run_before_delete<T: TableSource>(
203    hooks: &[BeforeDeleteFn<T>],
204    id: &T::Id,
205    record: &Record<T::Value>,
206    table: &Table<T, EmptyEntity>,
207) -> Result<HookReturn> {
208    let context = WriteContext::current();
209    for hook in hooks {
210        let cx = DeleteCx {
211            id,
212            record,
213            context: context.as_ref(),
214            table,
215        };
216        if let HookReturn::Handled = hook(cx).await? {
217            return Ok(HookReturn::Handled);
218        }
219    }
220    Ok(HookReturn::Proceed)
221}