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}