cratestack_sqlx/query/write/
upsert.rs1use cratestack_core::{CratestackContext, CratestackError};
16
17use crate::audit::{RunInTxOutcome, dispatch_audit_sink};
18use crate::{
19 ConflictTarget, ModelDescriptor, SqlxRuntime, UpsertModelInput, cratestack_error_from_sqlx,
20 sqlx,
21};
22
23use super::upsert_do_nothing::UpsertRecordDoNothing;
24use super::upsert_exec::run_upsert_in_tx;
25
26#[derive(Debug, Clone)]
27pub struct UpsertRecord<'a, M: 'static, PK: 'static, I> {
28 pub(crate) runtime: &'a SqlxRuntime,
29 pub(crate) descriptor: &'static ModelDescriptor<M, PK>,
30 pub(crate) input: I,
31 pub(crate) conflict_target: ConflictTarget,
32}
33
34impl<'a, M: 'static, PK: 'static, I> UpsertRecord<'a, M, PK, I>
35where
36 I: UpsertModelInput<M>,
37{
38 pub fn on_conflict(mut self, target: ConflictTarget) -> Self {
43 self.conflict_target = target;
44 self
45 }
46
47 pub fn do_nothing(self) -> UpsertRecordDoNothing<'a, M, PK, I> {
66 UpsertRecordDoNothing {
67 runtime: self.runtime,
68 descriptor: self.descriptor,
69 input: self.input,
70 conflict_target: self.conflict_target,
71 }
72 }
73
74 pub fn preview_sql(&self) -> String {
78 let values = self.input.sql_values();
79 let placeholders = (1..=values.len())
80 .map(|index| format!("${index}"))
81 .collect::<Vec<_>>()
82 .join(", ");
83 let columns = values
84 .iter()
85 .map(|value| value.column)
86 .collect::<Vec<_>>()
87 .join(", ");
88 let update_assignments = self
89 .descriptor
90 .upsert_update_columns
91 .iter()
92 .map(|column| format!("{column} = EXCLUDED.{column}"))
93 .collect::<Vec<_>>()
94 .join(", ");
95 let version_bump = match self.descriptor.version_column {
96 Some(col) => format!(
97 ", {col} = {table}.{col} + 1",
98 table = self.descriptor.table_name,
99 col = col
100 ),
101 None => String::new(),
102 };
103 let conflict_tuple = match self.conflict_target {
104 ConflictTarget::PrimaryKey => self.descriptor.primary_key.to_owned(),
105 ConflictTarget::Columns(cols) => cols.join(", "),
106 };
107
108 format!(
109 "INSERT INTO {table} ({columns}) VALUES ({placeholders}) \
110 ON CONFLICT ({conflict_tuple}) DO UPDATE SET {update_assignments}{version_bump} \
111 RETURNING {projection}",
112 table = self.descriptor.table_name,
113 projection = self.descriptor.select_projection(),
114 )
115 }
116
117 pub async fn run(self, ctx: &CratestackContext) -> Result<M, CratestackError>
118 where
119 for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow> + serde::Serialize,
120 PK: Send + sqlx::Type<sqlx::Postgres> + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
121 {
122 let runtime = self.runtime;
123 let mut tx = runtime
124 .pool()
125 .begin()
126 .await
127 .map_err(cratestack_error_from_sqlx)?;
128 let (record, emits_event, audit_event) = run_upsert_in_tx(
129 &mut tx,
130 runtime,
131 self.descriptor,
132 self.input,
133 self.conflict_target,
134 ctx,
135 )
136 .await?;
137 tx.commit().await.map_err(cratestack_error_from_sqlx)?;
138 if emits_event {
139 let _ = runtime.drain_event_outbox().await;
140 }
141 if let Some(event) = &audit_event {
142 dispatch_audit_sink(runtime, std::slice::from_ref(event)).await;
143 }
144 Ok(record)
145 }
146
147 pub async fn run_in_tx<'tx>(
154 self,
155 tx: &mut sqlx::Transaction<'tx, sqlx::Postgres>,
156 ctx: &CratestackContext,
157 ) -> Result<RunInTxOutcome<M>, CratestackError>
158 where
159 for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow> + serde::Serialize,
160 PK: Send + sqlx::Type<sqlx::Postgres> + for<'q> sqlx::Encode<'q, sqlx::Postgres>,
161 {
162 let (record, _emits_event, audit_event) = run_upsert_in_tx(
163 tx,
164 self.runtime,
165 self.descriptor,
166 self.input,
167 self.conflict_target,
168 ctx,
169 )
170 .await?;
171 Ok(RunInTxOutcome::new(
172 record,
173 audit_event.into_iter().collect(),
174 ))
175 }
176}