diff --git a/crates/bindings/README.md b/crates/bindings/README.md index 982886d4b65..68dc56c1e65 100644 --- a/crates/bindings/README.md +++ b/crates/bindings/README.md @@ -378,9 +378,9 @@ ctx.db.person().ssn() - [`UniqueColumn::find`] - [`UniqueColumn::delete`] - [`UniqueColumn::update`] - +- [`UniqueColumn::try_update`] -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. +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. The `#[primary_key]` annotation implies `#[unique]` annotation, but avails additional methods in the [client]-side SDKs. diff --git a/crates/bindings/src/table.rs b/crates/bindings/src/table.rs index 8a093ab0dd2..069283705d0 100644 --- a/crates/bindings/src/table.rs +++ b/crates/bindings/src/table.rs @@ -243,6 +243,48 @@ impl From> for String { } } +/// The error type returned from [`UniqueColumn::try_update()`], signalling a constraint violation. +pub enum TryUpdateError { + /// A [`UniqueConstraintViolation`]. + /// + /// Returned from [`Table::try_update`] if an attempted update + /// has the same value in a unique column as an already-present row + /// (excluding the update key itself). + UniqueConstraintViolation(Tbl::UniqueConstraintViolation), +} + +impl fmt::Debug for TryUpdateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "TryUpdateError::<{}>::", Tbl::TABLE_NAME)?; + match self { + Self::UniqueConstraintViolation(e) => fmt::Debug::fmt(e, f), + } + } +} + +impl fmt::Display for TryUpdateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "update error on table `{}`:", Tbl::TABLE_NAME)?; + match self { + Self::UniqueConstraintViolation(e) => fmt::Display::fmt(e, f), + } + } +} + +impl std::error::Error for TryUpdateError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(match self { + Self::UniqueConstraintViolation(e) => e, + }) + } +} + +impl From> for String { + fn from(err: TryUpdateError) -> Self { + err.to_string() + } +} + #[doc(hidden)] pub trait MaybeError: std::error::Error + Send + Sync + Sized + 'static { fn get() -> Option; @@ -357,13 +399,22 @@ impl> UniqueColumn Tbl::Row { + self.try_update(new_row).unwrap_or_else(|e| panic!("{e}")) + } + + /// Counterpart to [`Self::update`] which allows handling failed updates. + /// + /// This method returns an `Err` when the update fails rather than panicking. + #[track_caller] + pub fn try_update(&self, new_row: Tbl::Row) -> Result> { let buf = IterBuf::take(); update::(Col::index_id(), new_row, buf) } @@ -1090,7 +1141,7 @@ fn insert(mut row: T::Row, mut buf: IterBuf) -> Result(index_id: IndexId, mut row: T::Row, mut buf: IterBuf) -> T::Row { +fn update(index_id: IndexId, mut row: T::Row, mut buf: IterBuf) -> Result> { let table_id = T::table_id(); // Encode the row as bsatn into the buffer `buf`. buf.clear(); @@ -1103,9 +1154,15 @@ fn update(index_id: IndexId, mut row: T::Row, mut buf: IterBuf) -> T:: T::integrate_generated_columns(&mut row, gen_cols); row }); - - // TODO(centril): introduce a `TryUpdateError`. - res.unwrap_or_else(|e| panic!("unexpected update error: {e}")) + res.map_err(|e| { + let err = match e { + sys::Errno::UNIQUE_ALREADY_EXISTS => { + T::UniqueConstraintViolation::get().map(TryUpdateError::UniqueConstraintViolation) + } + _ => None, + }; + err.unwrap_or_else(|| panic!("unexpected update error: {e}")) + }) } /// A table iterator which yields values of the `TableType` corresponding to the table.