diff --git a/crates/bindings-csharp/Runtime/Exceptions.cs b/crates/bindings-csharp/Runtime/Exceptions.cs index 96b59deca2f..e591e3a0c00 100644 --- a/crates/bindings-csharp/Runtime/Exceptions.cs +++ b/crates/bindings-csharp/Runtime/Exceptions.cs @@ -27,6 +27,16 @@ public class NoSuchIndexException : StdbException public override string Message => "No such index"; } +public class IndexNotUniqueException : StdbException +{ + public override string Message => "The index was not unique"; +} + +public class NoSuchRowException : StdbException +{ + public override string Message => "The row was not found, e.g., in an update call"; +} + public class UniqueConstraintViolationException : StdbException { public override string Message => "Value with given unique identifier already exists"; diff --git a/crates/bindings-csharp/Runtime/Internal/FFI.cs b/crates/bindings-csharp/Runtime/Internal/FFI.cs index 916b36d5721..626883902fa 100644 --- a/crates/bindings-csharp/Runtime/Internal/FFI.cs +++ b/crates/bindings-csharp/Runtime/Internal/FFI.cs @@ -34,6 +34,8 @@ public enum Errno : short BUFFER_TOO_SMALL = 11, UNIQUE_ALREADY_EXISTS = 12, SCHEDULE_AT_DELAY_TOO_LONG = 13, + INDEX_NOT_UNIQUE = 14, + NO_SUCH_ROW = 15, } #pragma warning disable IDE1006 // Naming Styles - Not applicable to FFI stuff. @@ -84,6 +86,8 @@ public static CheckedStatus ConvertToManaged(Errno status) Errno.BUFFER_TOO_SMALL => new BufferTooSmallException(), Errno.UNIQUE_ALREADY_EXISTS => new UniqueConstraintViolationException(), Errno.SCHEDULE_AT_DELAY_TOO_LONG => new ScheduleAtDelayTooLongException(), + Errno.INDEX_NOT_UNIQUE => new IndexNotUniqueException(), + Errno.NO_SUCH_ROW => new NoSuchRowException(), _ => new UnknownException(status), }; } diff --git a/crates/bindings-sys/src/lib.rs b/crates/bindings-sys/src/lib.rs index 2f8e2eb37ec..2b17873e6b5 100644 --- a/crates/bindings-sys/src/lib.rs +++ b/crates/bindings-sys/src/lib.rs @@ -332,11 +332,59 @@ pub mod raw { /// - `NOT_IN_TRANSACTION`, when called outside of a transaction. /// - `NO_SUCH_TABLE`, when `table_id` is not a known ID of a table. /// - `BSATN_DECODE_ERROR`, when `row` cannot be decoded to a `ProductValue`. - /// typed at the `ProductType` the table's schema specifies. + /// typed at the `ProductType` the table's schema specifies. /// - `UNIQUE_ALREADY_EXISTS`, when inserting `row` would violate a unique constraint. /// - `SCHEDULE_AT_DELAY_TOO_LONG`, when the delay specified in the row was too long. pub fn datastore_insert_bsatn(table_id: TableId, row_ptr: *mut u8, row_len_ptr: *mut usize) -> u16; + /// Updates a row in the table identified by `table_id` to `row` + /// where the row is read from the byte string `row = row_ptr[..row_len]` in WASM memory + /// where `row_len = row_len_ptr[..size_of::()]` stores the capacity of `row`. + /// + /// The byte string `row` must be a BSATN-encoded `ProductValue` + /// typed at the table's `ProductType` row-schema. + /// + /// The row to update is found by projecting `row` + /// to the type of the *unique* index identified by `index_id`. + /// If no row is found, the error `NO_SUCH_ROW` is returned. + /// + /// To handle auto-incrementing columns, + /// when the call is successful, + /// the `row` is written back to with the generated sequence values. + /// These values are written as a BSATN-encoded `pv: ProductValue`. + /// Each `v: AlgebraicValue` in `pv` is typed at the sequence's column type. + /// The `v`s in `pv` are ordered by the order of the columns, in the schema of the table. + /// When the table has no sequences, + /// this implies that the `pv`, and thus `row`, will be empty. + /// The `row_len` is set to the length of `bsatn(pv)`. + /// + /// # Traps + /// + /// Traps if: + /// - `row_len_ptr` is NULL or `row_len` is not in bounds of WASM memory. + /// - `row_ptr` is NULL or `row` is not in bounds of WASM memory. + /// + /// # Errors + /// + /// Returns an error: + /// + /// - `NOT_IN_TRANSACTION`, when called outside of a transaction. + /// - `NO_SUCH_TABLE`, when `table_id` is not a known ID of a table. + /// - `NO_SUCH_INDEX`, when `index_id` is not a known ID of an index. + /// - `INDEX_NOT_UNIQUE`, when the index was not unique. + /// - `NO_SUCH_ROW`, when the row was not found in the unique index. + /// - `BSATN_DECODE_ERROR`, when `row` cannot be decoded to a `ProductValue` + /// typed at the `ProductType` the table's schema specifies + /// or when it cannot be projected to the index identified by `index_id`. + /// - `UNIQUE_ALREADY_EXISTS`, when inserting `row` would violate a unique constraint. + /// - `SCHEDULE_AT_DELAY_TOO_LONG`, when the delay specified in the row was too long. + pub fn datastore_update_bsatn( + table_id: TableId, + index_id: IndexId, + row_ptr: *mut u8, + row_len_ptr: *mut usize, + ) -> u16; + /// Schedules a reducer to be called asynchronously, nonatomically, /// and immediately on a best effort basis. /// @@ -752,6 +800,31 @@ pub fn datastore_insert_bsatn(table_id: TableId, row: &mut [u8]) -> Result<&[u8] cvt(unsafe { raw::datastore_insert_bsatn(table_id, row_ptr, row_len) }).map(|()| &row[..*row_len]) } +/// Updates a row into the table identified by `table_id`, +/// where the row is a BSATN-encoded `ProductValue` +/// matching the table's `ProductType` row-schema. +/// +/// The row to update is found by projecting `row` +/// to the type of the *unique* index identified by `index_id`. +/// If no row is found, `row` is inserted. +/// +/// The `row` is `&mut` due to auto-incrementing columns. +/// So `row` is written to with the updated row re-encoded. +/// +/// Returns an error if +/// - a table with the provided `table_id` doesn't exist +/// - an index with the provided `index_id` doesn't exist +/// - there were unique constraint violations +/// - `row` doesn't decode from BSATN to a `ProductValue` +/// according to the `ProductType` that the table's schema specifies +/// or if `row` cannot project to the index's type. +#[inline] +pub fn datastore_update_bsatn(table_id: TableId, index_id: IndexId, row: &mut [u8]) -> Result<&[u8], Errno> { + let row_ptr = row.as_mut_ptr(); + let row_len = &mut row.len(); + cvt(unsafe { raw::datastore_update_bsatn(table_id, index_id, row_ptr, row_len) }).map(|()| &row[..*row_len]) +} + /// Deletes those rows, in the table identified by `table_id`, /// that match any row in the byte string `relation`. /// diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index 6eba969e534..25240de98b7 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -337,6 +337,8 @@ pub enum NodesError { TableNotFound, #[error("index with provided name or id doesn't exist")] IndexNotFound, + #[error("index was not unique")] + IndexNotUnique, #[error("column is out of bounds")] BadColumn, #[error("can't perform operation; not inside transaction")] diff --git a/crates/core/src/host/instance_env.rs b/crates/core/src/host/instance_env.rs index 0220c8e741b..2402d6e4277 100644 --- a/crates/core/src/host/instance_env.rs +++ b/crates/core/src/host/instance_env.rs @@ -177,6 +177,15 @@ impl InstanceEnv { Ok(row_len) } + pub fn update(&self, table_id: TableId, index_id: IndexId, buffer: &mut [u8]) -> Result { + #![allow(unused)] + + let stdb = &*self.replica_ctx.relational_db; + let tx = &mut *self.get_tx()?; + + Ok(todo!()) + } + #[tracing::instrument(skip_all)] pub fn datastore_delete_by_btree_scan_bsatn( &self, diff --git a/crates/core/src/host/mod.rs b/crates/core/src/host/mod.rs index ba015cb37f8..4dc5f0c0da6 100644 --- a/crates/core/src/host/mod.rs +++ b/crates/core/src/host/mod.rs @@ -153,6 +153,7 @@ pub enum AbiCall { RowIterBsatnAdvance, RowIterBsatnClose, DatastoreInsertBsatn, + DatastoreUpdateBsatn, DatastoreDeleteByBtreeScanBsatn, DatastoreDeleteAllByEqBsatn, BytesSourceRead, diff --git a/crates/core/src/host/wasm_common.rs b/crates/core/src/host/wasm_common.rs index 9baacf66563..0250c82f23a 100644 --- a/crates/core/src/host/wasm_common.rs +++ b/crates/core/src/host/wasm_common.rs @@ -350,6 +350,7 @@ pub fn err_to_errno(err: &NodesError) -> Option { NodesError::DecodeRow(_) => Some(errno::BSATN_DECODE_ERROR), NodesError::TableNotFound => Some(errno::NO_SUCH_TABLE), NodesError::IndexNotFound => Some(errno::NO_SUCH_INDEX), + NodesError::IndexNotUnique => Some(errno::INDEX_NOT_UNIQUE), NodesError::ScheduleError(ScheduleError::DelayTooLong(_)) => Some(errno::SCHEDULE_AT_DELAY_TOO_LONG), NodesError::AlreadyExists(_) => Some(errno::UNIQUE_ALREADY_EXISTS), NodesError::Internal(internal) => match **internal { @@ -382,6 +383,7 @@ macro_rules! abi_funcs { "spacetime_10.0"::row_iter_bsatn_advance, "spacetime_10.0"::row_iter_bsatn_close, "spacetime_10.0"::datastore_insert_bsatn, + "spacetime_10.0"::datastore_update_bsatn, "spacetime_10.0"::datastore_delete_all_by_eq_bsatn, "spacetime_10.0"::bytes_source_read, "spacetime_10.0"::bytes_sink_write, diff --git a/crates/core/src/host/wasmtime/wasm_instance_env.rs b/crates/core/src/host/wasmtime/wasm_instance_env.rs index afb6ec82dd8..3f00498f0ee 100644 --- a/crates/core/src/host/wasmtime/wasm_instance_env.rs +++ b/crates/core/src/host/wasmtime/wasm_instance_env.rs @@ -682,6 +682,70 @@ impl WasmInstanceEnv { }) } + /// Updates a row in the table identified by `table_id` to `row` + /// where the row is read from the byte string `row = row_ptr[..row_len]` in WASM memory + /// where `row_len = row_len_ptr[..size_of::()]` stores the capacity of `row`. + /// + /// The byte string `row` must be a BSATN-encoded `ProductValue` + /// typed at the table's `ProductType` row-schema. + /// + /// The row to update is found by projecting `row` + /// to the type of the *unique* index identified by `index_id`. + /// If no row is found, the error `NO_SUCH_ROW` is returned. + /// + /// To handle auto-incrementing columns, + /// when the call is successful, + /// the `row` is written back to with the generated sequence values. + /// These values are written as a BSATN-encoded `pv: ProductValue`. + /// Each `v: AlgebraicValue` in `pv` is typed at the sequence's column type. + /// The `v`s in `pv` are ordered by the order of the columns, in the schema of the table. + /// When the table has no sequences, + /// this implies that the `pv`, and thus `row`, will be empty. + /// The `row_len` is set to the length of `bsatn(pv)`. + /// + /// # Traps + /// + /// Traps if: + /// - `row_len_ptr` is NULL or `row_len` is not in bounds of WASM memory. + /// - `row_ptr` is NULL or `row` is not in bounds of WASM memory. + /// + /// # Errors + /// + /// Returns an error: + /// + /// - `NOT_IN_TRANSACTION`, when called outside of a transaction. + /// - `NO_SUCH_TABLE`, when `table_id` is not a known ID of a table. + /// - `NO_SUCH_INDEX`, when `index_id` is not a known ID of an index. + /// - `INDEX_NOT_UNIQUE`, when the index was not unique. + /// - `BSATN_DECODE_ERROR`, when `row` cannot be decoded to a `ProductValue` + /// typed at the `ProductType` the table's schema specifies + /// or when it cannot be projected to the index identified by `index_id`. + /// - `NO_SUCH_ROW`, when the row was not found in the unique index. + /// - `UNIQUE_ALREADY_EXISTS`, when inserting `row` would violate a unique constraint. + /// - `SCHEDULE_AT_DELAY_TOO_LONG`, when the delay specified in the row was too long. + #[tracing::instrument(skip_all)] + pub fn datastore_update_bsatn( + caller: Caller<'_, Self>, + table_id: u32, + index_id: u32, + row_ptr: WasmPtr, + row_len_ptr: WasmPtr, + ) -> RtResult { + Self::cvt(caller, AbiCall::DatastoreUpdateBsatn, |caller| { + let (mem, env) = Self::mem_env(caller); + + // Read `row-len`, i.e., the capacity of `row` pointed to by `row_ptr`. + let row_len = u32::read_from(mem, row_len_ptr)?; + // Get a mutable view to the `row`. + let row = mem.deref_slice_mut(row_ptr, row_len)?; + + // Update the row in the DB and write back the generated column values. + let row_len = env.instance_env.update(table_id.into(), index_id.into(), row)?; + u32::try_from(row_len).unwrap().write_to(mem, row_len_ptr)?; + Ok(()) + }) + } + /// Deletes all rows found in the index identified by `index_id`, /// according to the: /// - `prefix = prefix_ptr[..prefix_len]`, diff --git a/crates/primitives/src/errno.rs b/crates/primitives/src/errno.rs index f46c74b20c6..b00cea7a579 100644 --- a/crates/primitives/src/errno.rs +++ b/crates/primitives/src/errno.rs @@ -20,6 +20,8 @@ macro_rules! errnos { BUFFER_TOO_SMALL(11, "The provided buffer is not large enough to store the data"), UNIQUE_ALREADY_EXISTS(12, "Value with given unique identifier already exists"), SCHEDULE_AT_DELAY_TOO_LONG(13, "Specified delay in scheduling row was too long"), + INDEX_NOT_UNIQUE(14, "The index was not unique"), + NO_SUCH_ROW(15, "The row was not found, e.g., in an update call"), ); }; }