Skip to main content

fraiseql_db/traits/
mutations.rs

1//! The write capability: [`Writer`], one method, [`Writer::execute_write`] (ruling AA 6).
2
3use async_trait::async_trait;
4use fraiseql_error::Result;
5
6use super::{ChangeLogWrite, DatabaseAdapter, MutationRowGate};
7
8/// Whether a write commits.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum WriteMode {
12    /// Commit once the gate admits the rows the function returned.
13    Commit,
14    /// Everything [`Commit`](Self::Commit) does — session variables, the change-log outbox
15    /// row, the timing stamp, the gate — then roll back: a dry run exercises exactly the
16    /// write it stands in for, and nothing persists.
17    DryRun,
18}
19
20/// One call of a write function.
21///
22/// `#[non_exhaustive]`: build it with [`WriteRequest::new`] and the `with_*` builders, so a
23/// field added later breaks no adapter and no caller.
24#[derive(Debug, Clone, Copy)]
25#[non_exhaustive]
26pub struct WriteRequest<'a> {
27    /// The function to call.
28    pub function:     &'a str,
29    /// Its arguments, positionally.
30    pub args:         &'a [serde_json::Value],
31    /// Transaction-local settings applied before the call (`set_config(.., true)`).
32    pub session_vars: &'a [(&'a str, &'a str)],
33    /// The change-log outbox row to write in the same transaction, if any.
34    pub changelog:    Option<&'a ChangeLogWrite<'a>>,
35    /// Whether the write commits.
36    pub mode:         WriteMode,
37}
38
39impl<'a> WriteRequest<'a> {
40    /// A committing call of `function` with `args`, no session variables and no outbox row.
41    #[must_use]
42    pub const fn new(function: &'a str, args: &'a [serde_json::Value]) -> Self {
43        Self {
44            function,
45            args,
46            session_vars: &[],
47            changelog: None,
48            mode: WriteMode::Commit,
49        }
50    }
51
52    /// With these transaction-local session variables.
53    #[must_use]
54    pub const fn with_session_vars(mut self, session_vars: &'a [(&'a str, &'a str)]) -> Self {
55        self.session_vars = session_vars;
56        self
57    }
58
59    /// With this change-log outbox row, written in the write's transaction.
60    #[must_use]
61    pub const fn with_changelog(mut self, changelog: Option<&'a ChangeLogWrite<'a>>) -> Self {
62        self.changelog = changelog;
63        self
64    }
65
66    /// In `mode`.
67    #[must_use]
68    pub const fn with_mode(mut self, mode: WriteMode) -> Self {
69        self.mode = mode;
70        self
71    }
72}
73
74/// A database adapter that writes.
75///
76/// Implementing [`execute_write`](Self::execute_write) **is** the capability: there is no
77/// flag to set beside it and no default to override, so an adapter cannot be write-capable
78/// to the type system and read-only at run time, or the other way round. A write-capable
79/// `Executor` is constructed only from a `Writer` (`Executor::new`, `Executor::with_config`);
80/// `Executor::read_only` takes any adapter.
81///
82/// # Which adapters implement this?
83///
84/// | Adapter | Implements |
85/// |---------|-----------|
86/// | `PostgresAdapter` | ✅ Yes |
87/// | `FraiseWireAdapter` | ❌ No — read-only wire protocol |
88/// | `CachedDatabaseAdapter<A>` | ✅ When `A: Writer` |
89///
90/// # Contract
91///
92/// In ONE transaction: apply `session_vars` transaction-locally, run the function (and write
93/// the change-log outbox row when `changelog` is `Some`), call `gate` on the rows it returned
94/// **before** the transaction ends, then roll back and return the gate's error verbatim if it
95/// errs; otherwise commit in [`WriteMode::Commit`] and roll back in [`WriteMode::DryRun`], and
96/// return the rows. An adapter that cannot hold a transaction open for the gate must not
97/// implement this trait: it stays read-only.
98// async_trait: dyn-dispatch required; remove when RTN + Send is stable (RFC 3425)
99#[async_trait]
100pub trait Writer: DatabaseAdapter {
101    /// Run `request` under `gate` (see the trait's contract).
102    ///
103    /// # Errors
104    ///
105    /// The gate's error, verbatim, after the rollback; a database error from any step.
106    async fn execute_write(
107        &self,
108        request: &WriteRequest<'_>,
109        gate: MutationRowGate<'_>,
110    ) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>>;
111
112    /// Bump the version counters of `tables` after a committed write that changed them, so
113    /// cached aggregates over them are invalidated by version key.
114    ///
115    /// Provided: nothing to bump for an adapter with no aggregate cache. The cache wrapper
116    /// overrides it, bumping through [`execute_write`](Self::execute_write).
117    ///
118    /// # Errors
119    ///
120    /// A database error from the bump.
121    async fn bump_fact_table_versions(&self, _tables: &[String]) -> Result<()> {
122        Ok(())
123    }
124}