cratestack_sqlx/query/write/upsert_outcome.rs
1//! Return type for `.upsert(..).do_nothing()` (cratestack#487).
2//!
3//! A genuine `ON CONFLICT ... DO NOTHING` returns nothing at all for
4//! the conflicting row — Postgres only RETURNs rows a statement
5//! actually touched, and DO NOTHING touches none. Callers therefore
6//! need "inserted" and "already existed" to be distinguishable in the
7//! type, not collapsed into a single `M` the way the DO UPDATE path's
8//! `.upsert(..).run(..)` returns it.
9
10/// Outcome of a `.upsert(..).do_nothing().run(..)` call.
11///
12/// # Race semantics
13///
14/// The runtime always resolves the conflict under a `SELECT ... FOR
15/// UPDATE` row lock held for the lifetime of the surrounding
16/// transaction (see `upsert_do_nothing_exec::run_upsert_do_nothing_in_tx`
17/// for the exact sequencing):
18///
19/// * If the probe finds an existing row, that row is locked before this
20/// call returns — no concurrent transaction can delete or modify it
21/// until the caller commits — so [`Existing`](Self::Existing) is a
22/// guarantee about the row's state *at the moment this call
23/// returns*, not merely "at some point during the call".
24/// * If the probe finds nothing, the actual `INSERT ... ON CONFLICT
25/// DO NOTHING` is still the statement that runs (not a plain
26/// `INSERT`), because the probe's "no row" answer does not itself
27/// lock anything — a concurrent transaction can commit a conflicting
28/// row in the gap between the probe and the INSERT. When that race
29/// is lost, the runtime performs one more locked read to hand back
30/// the row the other transaction actually committed, so callers never
31/// see a phantom "existing" row invented from stale data — see
32/// [`Existing`](Self::Existing) below for what happens if *that* row
33/// is deleted before the fallback read completes.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum UpsertOutcome<M> {
36 /// This call performed the insert. No row previously existed at
37 /// the conflict target.
38 Inserted(M),
39 /// A row already existed at the conflict target and was left
40 /// completely untouched by this call — no columns written, no
41 /// `Updated` event emitted, no audit entry recorded, because
42 /// nothing about the row changed.
43 ///
44 /// If this outcome was reached via the race-fallback path (the
45 /// insert branch lost a concurrent-insert race and had to read the
46 /// winning row back), and *that* row was deleted before the
47 /// fallback read could complete, the call surfaces
48 /// `CoolError::Conflict` instead of ever constructing an
49 /// `Existing` from data that might not be current — see
50 /// `upsert_do_nothing_exec` for that narrower race.
51 Existing(M),
52}
53
54impl<M> UpsertOutcome<M> {
55 /// `true` for [`Self::Inserted`].
56 pub fn was_inserted(&self) -> bool {
57 matches!(self, Self::Inserted(_))
58 }
59
60 /// Discard the inserted-vs-existing distinction and take the row.
61 /// Prefer matching on the enum when the distinction matters (that's
62 /// the entire reason this type exists) — this is for callers that
63 /// only ever need the record, e.g. read-modify-report call sites
64 /// that already branched on [`Self::was_inserted`].
65 pub fn into_record(self) -> M {
66 match self {
67 Self::Inserted(record) | Self::Existing(record) => record,
68 }
69 }
70
71 /// Borrow the record regardless of which variant this is.
72 pub fn record(&self) -> &M {
73 match self {
74 Self::Inserted(record) | Self::Existing(record) => record,
75 }
76 }
77}