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