1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//! `db.transaction(...)` combinator (cratestack#513): compose several
//! write-builder calls in one Postgres transaction using only CrateStack's
//! own API — no `sqlx` dependency in the caller's `Cargo.toml`, and no
//! `sqlx::Transaction` named in the caller's own source.
//!
//! **Design (see the PR body for the sub-questions this answers in full):**
//!
//! - [`Tx`] is an opaque, crate-owned newtype around
//! `sqlx::Transaction<'static, sqlx::Postgres>`. It implements
//! [`Deref`]/[`DerefMut`] to that type, which is what lets every existing
//! `run_in_tx(&mut sqlx::Transaction<'tx, Postgres>, ctx)` on the write
//! builders (`create.rs`, `update.rs`, ...) keep their exact current
//! signature: passing `&mut Tx` at a `&mut sqlx::Transaction<'_, _>` call
//! site coerces automatically via `DerefMut`, so nothing downstream of
//! `run_in_tx` had to change. The caller's closure never has to name
//! `sqlx::Transaction` (or even `Tx` — the type is inferred), it's not a
//! breaking change to `run_in_tx`, and the transaction still round-trips
//! through the real `sqlx` machinery underneath.
//! - The closure is bound by `AsyncFnOnce(&mut Tx) -> Result<T, CoolError>`
//! (the native async-closure traits stabilized in Rust 1.85; this
//! workspace pins 1.95) rather than the `FnMut(...) -> Fut` shape
//! `run_in_isolated_tx` uses. That older shape requires the body to hand
//! the transaction *back* out of the future on every call (see
//! `isolation.rs`) because a plain closure returning `async move { .. }`
//! can't express "the returned future borrows the argument for its own
//! lifetime" — the classic Rust lending-closure problem. `AsyncFnOnce`
//! solves exactly that: callers can write
//! `db.transaction(async |tx| { ...; Ok(value) }).await` and reuse `tx`
//! across as many sequential `.await`s as they like without threading it
//! back through the return type. Verified against a standalone
//! reproduction before adopting it here — see the PR body.
//! - No retry loop: unlike [`crate::run_in_isolated_tx`], `transaction`
//! doesn't re-run `body` on a serialization failure, since `body` isn't
//! guaranteed idempotent (it's arbitrary caller code, not caller code
//! already scoped to "safe to retry" the way `@isolation` procedures
//! are). Retrying is exactly what `run_in_isolated_tx` is for; the two
//! are orthogonal and composable (see the PR body's isolation
//! discussion), not alternatives to pick between.
//!
//! ## Composing through here does not close the `AuditSink`/outbox gap (cratestack#534)
//!
//! It is tempting to assume that because this is the *sanctioned* way to
//! compose several write-builder calls, it also gets you the fan-out that
//! `run()` gives you automatically — an installed [`cratestack_core::AuditSink`]
//! observing every `@@audit` write, and `@@emit` events reaching their
//! subscribers. **It does not.** `body` still calls each write builder's
//! `run_in_tx`, which still only writes the in-database `cratestack_audit`
//! row / outbox row and hands back a `RunInTxOutcome` — it never dispatches
//! anything itself, for exactly the same reason it doesn't when called
//! against a transaction obtained directly from `db.pool().begin()`: there
//! is still no reliable "after commit" point *inside this crate*, because
//! `transaction` only knows `body` returned `Ok::<T, _>` for an arbitrary,
//! caller-chosen `T` — it has no way to discover which `RunInTxOutcome`s
//! (if any) `body` produced along the way unless `body` hands them back as
//! part of its own return value.
//!
//! This was investigated as a candidate host for cratestack#534's option
//! (b) ("the runtime takes ownership of dispatch") and found not cleanly
//! achievable here: even setting aside the arbitrary-`T` problem above,
//! [`SqlxRuntime::pool`] stays public, so a caller can always open a
//! transaction with `db.pool().begin()` directly and pass it straight to
//! `run_in_tx`, bypassing this combinator entirely — the same call
//! `run_in_tx` accepts from here, because [`Tx`] derefs to a plain
//! `sqlx::Transaction` before `run_in_tx` ever sees it (see above), so
//! `run_in_tx` cannot even tell which door the transaction came through.
//! Any auto-dispatch hook attached only to `transaction()` would therefore
//! be incomplete by construction, reproducing the exact invisible gap
//! cratestack#534 exists to close, just for a subset of callers instead of
//! all of them. **The contract is caller-driven, unconditionally**: after
//! `transaction()` returns `Ok`, dispatch the audit events yourself via
//! the generated `Cratestack::dispatch_audit_sink` and drain the outbox
//! yourself via `Cratestack::events().drain()` — see
//! [`crate::dispatch_audit_sink`]'s doc comment for the full reasoning,
//! which applies here unchanged.
use ;
use CoolError;
use crateSqlxRuntime;
use cratecool_error_from_sqlx;
use cratesqlx;
/// Opaque handle onto a live Postgres transaction. Obtained only via
/// [`SqlxRuntime::transaction`]; never constructed directly by consumers.
///
/// Derefs to `sqlx::Transaction<'static, sqlx::Postgres>` purely so the
/// existing write-builder `run_in_tx` methods keep working unchanged (see
/// the module doc comment) — this is an implementation detail, not an
/// invitation to import `sqlx` yourself. Nothing about the public
/// `db.transaction(...)` call site requires it.
;