Skip to content

Commit d74578d

Browse files
committed
Implemented UniqueColumn::try_update(). Resolves #2128
1 parent 6be3037 commit d74578d

2 files changed

Lines changed: 64 additions & 7 deletions

File tree

crates/bindings/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,9 +378,9 @@ ctx.db.person().ssn()
378378
- [`UniqueColumn::find`]
379379
- [`UniqueColumn::delete`]
380380
- [`UniqueColumn::update`]
381-
<!-- TODO: "current limitations" try_update -->
381+
- [`UniqueColumn::try_update`]
382382

383-
Notice that updating a row is only possible if a row has a unique column -- there is no `update` method in the base [`Table`] trait. SpacetimeDB has no notion of rows having an "identity" aside from their unique / primary keys.
383+
Notice that updating a row is only possible if a row has a unique column -- there is no `update` or `try_update` method in the base [`Table`] trait. SpacetimeDB has no notion of rows having an "identity" aside from their unique / primary keys.
384384

385385
The `#[primary_key]` annotation implies `#[unique]` annotation, but avails additional methods in the [client]-side SDKs.
386386

crates/bindings/src/table.rs

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,48 @@ impl<Tbl: Table> From<TryInsertError<Tbl>> for String {
243243
}
244244
}
245245

246+
/// The error type returned from [`UniqueColumn::try_update()`], signalling a constraint violation.
247+
pub enum TryUpdateError<Tbl: Table> {
248+
/// A [`UniqueConstraintViolation`].
249+
///
250+
/// Returned from [`Table::try_update`] if an attempted update
251+
/// has the same value in a unique column as an already-present row
252+
/// (excluding the update key itself).
253+
UniqueConstraintViolation(Tbl::UniqueConstraintViolation),
254+
}
255+
256+
impl<Tbl: Table> fmt::Debug for TryUpdateError<Tbl> {
257+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258+
write!(f, "TryUpdateError::<{}>::", Tbl::TABLE_NAME)?;
259+
match self {
260+
Self::UniqueConstraintViolation(e) => fmt::Debug::fmt(e, f),
261+
}
262+
}
263+
}
264+
265+
impl<Tbl: Table> fmt::Display for TryUpdateError<Tbl> {
266+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267+
write!(f, "update error on table `{}`:", Tbl::TABLE_NAME)?;
268+
match self {
269+
Self::UniqueConstraintViolation(e) => fmt::Display::fmt(e, f),
270+
}
271+
}
272+
}
273+
274+
impl<Tbl: Table> std::error::Error for TryUpdateError<Tbl> {
275+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
276+
Some(match self {
277+
Self::UniqueConstraintViolation(e) => e,
278+
})
279+
}
280+
}
281+
282+
impl<Tbl: Table> From<TryUpdateError<Tbl>> for String {
283+
fn from(err: TryUpdateError<Tbl>) -> Self {
284+
err.to_string()
285+
}
286+
}
287+
246288
#[doc(hidden)]
247289
pub trait MaybeError<E = Self>: std::error::Error + Send + Sync + Sized + 'static {
248290
fn get() -> Option<Self>;
@@ -357,13 +399,22 @@ impl<Tbl: Table, Col: Index + Column<Table = Tbl>> UniqueColumn<Tbl, Col::ColTyp
357399
/// Deletes the row where the value in the unique column matches that in the corresponding field of `new_row`, and
358400
/// then inserts the `new_row`.
359401
///
360-
/// Returns the new row as actually inserted, with computed values substituted for any auto-inc placeholders.
402+
/// Returns the new row as actually inserted, with computed values substituted for any auto-inc placeholders.
361403
///
362404
/// # Panics
363405
/// Panics if no row was previously present with the matching value in the unique column,
364406
/// or if either the delete or the insertion would violate a constraint.
407+
/// Callers which intend to handle constraint violation errors should instead use [`Self::try_update`].
365408
#[track_caller]
366409
pub fn update(&self, new_row: Tbl::Row) -> Tbl::Row {
410+
self.try_update(new_row).unwrap_or_else(|e| panic!("{e}"))
411+
}
412+
413+
/// Counterpart to [`Self::update`] which allows handling failed updates.
414+
///
415+
/// This method returns an `Err` when the update fails rather than panicking.
416+
#[track_caller]
417+
pub fn try_update(&self, new_row: Tbl::Row) -> Result<Tbl::Row, TryUpdateError<Tbl>> {
367418
let buf = IterBuf::take();
368419
update::<Tbl>(Col::index_id(), new_row, buf)
369420
}
@@ -1090,7 +1141,7 @@ fn insert<T: Table>(mut row: T::Row, mut buf: IterBuf) -> Result<T::Row, TryInse
10901141

10911142
/// Update a row of type `T` to `row` using the index identified by `index_id`.
10921143
#[track_caller]
1093-
fn update<T: Table>(index_id: IndexId, mut row: T::Row, mut buf: IterBuf) -> T::Row {
1144+
fn update<T: Table>(index_id: IndexId, mut row: T::Row, mut buf: IterBuf) -> Result<T::Row, TryUpdateError<T>> {
10941145
let table_id = T::table_id();
10951146
// Encode the row as bsatn into the buffer `buf`.
10961147
buf.clear();
@@ -1103,9 +1154,15 @@ fn update<T: Table>(index_id: IndexId, mut row: T::Row, mut buf: IterBuf) -> T::
11031154
T::integrate_generated_columns(&mut row, gen_cols);
11041155
row
11051156
});
1106-
1107-
// TODO(centril): introduce a `TryUpdateError`.
1108-
res.unwrap_or_else(|e| panic!("unexpected update error: {e}"))
1157+
res.map_err(|e| {
1158+
let err = match e {
1159+
sys::Errno::UNIQUE_ALREADY_EXISTS => {
1160+
T::UniqueConstraintViolation::get().map(TryUpdateError::UniqueConstraintViolation)
1161+
}
1162+
_ => None,
1163+
};
1164+
err.unwrap_or_else(|| panic!("unexpected update error: {e}"))
1165+
})
11091166
}
11101167

11111168
/// A table iterator which yields values of the `TableType` corresponding to the table.

0 commit comments

Comments
 (0)