Ergonomic Partial Updates via AsChangeset Macro
RequestedJul 22, 2026I’d like to request a quality-of-life feature for partial row update requests, similar to Diesel’s [#[derive(AsChangeset)]](https://docs.diesel.rs/master/diesel/query_builder/derive.AsChangeset.html) macro.
**To be clear up front**: I am not asking for column-level mutations at the storage layer. I know SpacetimeDB currently requires whole-row operations under the hood. Rather, this is a request for a DX wrapper to hide the boilerplate of requesting an update to specific fields while preserving the rest of the row.
Currently, modifying a single field requires a manual "read-modify-write" pattern in user-land, meaning we have to execute a `find()` query first just to preserve the unmodified data. A macro could generate a changeset struct where non-primary-key fields are wrapped in `Option<T>`. This would allow us to call an `update_changeset` method, and the host (or a generated wrapper) would handle fetching the old row, applying the `Some` values, and writing the full row back.
This eliminates the boilerplate of the initial read query, allows for clean `..Default::default()` syntax when updating large tables, and potentially offers a minor performance perk by removing user-land boundary crossing.
```rust
// CURRENT: Requires a read to avoid overwriting `zone`
let prev = ctx.db.locality_tbl().actor_id().find(actor_id).unwrap();
ctx.db.locality_tbl().actor_id().update(Locality {
actor_id,
chunk_x: new_x,
chunk_z: new_z,
zone: prev.zone,
});
// PROPOSED: Using a generated changeset struct
ctx.db.locality_tbl().actor_id().update_partial(LocalityChangeset {
actor_id,
chunk_x: Some(new_x),
chunk_z: Some(new_z),
..Default::default() // `zone` defaults to None and is untouched
});
```
**Table definition with macro**
```rust
#[table(accessor = locality_tbl)]
#[derive(AsChangeset)]
pub struct Locality {
#[primary_key]
pub actor_id: ActorId,
pub zone: ZoneName,
pub chunk_x: i8,
pub chunk_z: i8,
}
// ...which would automatically yield:
pub struct LocalityChangeset {
pub actor_id: ActorId,
pub zone: Option<ZoneName>,
pub chunk_x: Option<i8>,
pub chunk_z: Option<i8>,
}
```