Skip to main content

cratestack_sqlx/query/write/
upsert_do_nothing.rs

1//! `UpsertRecordDoNothing` — the `.upsert(..).do_nothing()` builder
2//! (cratestack#487). Produced by [`super::upsert::UpsertRecord::do_nothing`];
3//! see that method's doc comment for why this is a distinct type
4//! rather than a flag on `UpsertRecord` itself.
5
6use cratestack_core::{CratestackContext, CratestackError};
7
8use crate::audit::{RunInTxOutcome, dispatch_audit_sink};
9use crate::{
10    ConflictTarget, ModelDescriptor, SqlxRuntime, UpsertModelInput, cratestack_error_from_sqlx,
11    sqlx,
12};
13
14use super::upsert_do_nothing_exec::run_upsert_do_nothing_in_tx;
15use super::upsert_outcome::UpsertOutcome;
16
17#[derive(Debug, Clone)]
18pub struct UpsertRecordDoNothing<'a, M: 'static, PK: 'static, I> {
19    pub(crate) runtime: &'a SqlxRuntime,
20    pub(crate) descriptor: &'static ModelDescriptor<M, PK>,
21    pub(crate) input: I,
22    pub(crate) conflict_target: ConflictTarget,
23}
24
25impl<'a, M: 'static, PK: 'static, I> UpsertRecordDoNothing<'a, M, PK, I>
26where
27    I: UpsertModelInput<M>,
28{
29    /// Choose the conflict target. See [`super::upsert::UpsertRecord::on_conflict`];
30    /// works identically here, and can be called either before or
31    /// after `.do_nothing()`.
32    pub fn on_conflict(mut self, target: ConflictTarget) -> Self {
33        self.conflict_target = target;
34        self
35    }
36
37    /// Render an approximate SQL preview of the insert-branch
38    /// statement. The actual call wraps a `SELECT ... FOR UPDATE`
39    /// probe around it and may perform a fallback `SELECT` on a lost
40    /// race — see [`UpsertOutcome`] for the full sequencing.
41    pub fn preview_sql(&self) -> String {
42        let values = self.input.sql_values();
43        let placeholders = (1..=values.len())
44            .map(|index| format!("${index}"))
45            .collect::<Vec<_>>()
46            .join(", ");
47        let columns = values
48            .iter()
49            .map(|value| value.column)
50            .collect::<Vec<_>>()
51            .join(", ");
52        let conflict_tuple = match self.conflict_target {
53            ConflictTarget::PrimaryKey => self.descriptor.primary_key.to_owned(),
54            ConflictTarget::Columns(cols) => cols.join(", "),
55        };
56
57        format!(
58            "INSERT INTO {table} ({columns}) VALUES ({placeholders}) \
59             ON CONFLICT ({conflict_tuple}) DO NOTHING \
60             RETURNING {projection}",
61            table = self.descriptor.table_name,
62            projection = self.descriptor.select_projection(),
63        )
64    }
65
66    pub async fn run(self, ctx: &CratestackContext) -> Result<UpsertOutcome<M>, CratestackError>
67    where
68        for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow> + serde::Serialize,
69        PK: Send + sqlx::Type<sqlx::Postgres> + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
70    {
71        let runtime = self.runtime;
72        let mut tx = runtime
73            .pool()
74            .begin()
75            .await
76            .map_err(cratestack_error_from_sqlx)?;
77        let (outcome, emits_event, audit_event) = run_upsert_do_nothing_in_tx(
78            &mut tx,
79            runtime,
80            self.descriptor,
81            self.input,
82            self.conflict_target,
83            ctx,
84        )
85        .await?;
86        tx.commit().await.map_err(cratestack_error_from_sqlx)?;
87        if emits_event {
88            let _ = runtime.drain_event_outbox().await;
89        }
90        if let Some(event) = &audit_event {
91            dispatch_audit_sink(runtime, std::slice::from_ref(event)).await;
92        }
93        Ok(outcome)
94    }
95
96    /// Like [`Self::run`] but participates in a caller-supplied
97    /// transaction. The conflict probe (and, on the insert branch, the
98    /// `ON CONFLICT DO NOTHING`) run against `tx`, so any row lock is
99    /// held until the caller commits. Neither the event outbox drain
100    /// nor the `AuditSink` fan-out happens here — see `create.rs`'s
101    /// `run_in_tx` doc comment for the full contract and how a caller
102    /// opts into both after their own commit.
103    pub async fn run_in_tx<'tx>(
104        self,
105        tx: &mut sqlx::Transaction<'tx, sqlx::Postgres>,
106        ctx: &CratestackContext,
107    ) -> Result<RunInTxOutcome<UpsertOutcome<M>>, CratestackError>
108    where
109        for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow> + serde::Serialize,
110        PK: Send + sqlx::Type<sqlx::Postgres> + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
111    {
112        let (outcome, _emits_event, audit_event) = run_upsert_do_nothing_in_tx(
113            tx,
114            self.runtime,
115            self.descriptor,
116            self.input,
117            self.conflict_target,
118            ctx,
119        )
120        .await?;
121        Ok(RunInTxOutcome::new(
122            outcome,
123            audit_event.into_iter().collect(),
124        ))
125    }
126}