Skip to main content

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}