From 4ae01aefbd5de07aa5b7c1d3ecff3b33dd8a709d Mon Sep 17 00:00:00 2001 From: joshua-spacetime Date: Mon, 15 Jun 2026 16:44:46 -0700 Subject: [PATCH] Update docs for view primary keys (#5327) # Description of Changes All of rust, C#, and typescript are included. # API and ABI breaking changes N/A # Expected complexity level and risk 1 # Testing N/A - docs only change --- .../00100-databases/00500-cheat-sheet.md | 19 +++++ .../00200-functions/00500-views.md | 70 +++++++++++++++++-- .../00600-clients/00200-codegen.md | 1 + .../00600-clients/00500-rust-reference.md | 5 +- .../00600-clients/00600-csharp-reference.md | 4 +- .../00700-typescript-reference.md | 6 +- 6 files changed, 92 insertions(+), 13 deletions(-) diff --git a/docs/docs/00200-core-concepts/00100-databases/00500-cheat-sheet.md b/docs/docs/00200-core-concepts/00100-databases/00500-cheat-sheet.md index 4a52f82ac..c76b024f1 100644 --- a/docs/docs/00200-core-concepts/00100-databases/00500-cheat-sheet.md +++ b/docs/docs/00200-core-concepts/00100-databases/00500-cheat-sheet.md @@ -609,6 +609,12 @@ export const top_players = spacetimedb.view({ name: 'top_players', public: true return ctx.db.player.score.filter(1000); }); +// Procedural view with update callbacks. +// The returned row type has exactly one `.primaryKey()` column. +export const top_players_with_updates = spacetimedb.view({ name: 'top_players_with_updates', public: true }, t.array(player.rowType), ctx => { + return ctx.db.player.score.filter(1000); +}); + // Perform a generic filter using the query builder. // Equivalent to `SELECT * FROM player WHERE score < 1000`. export const bottom_players = spacetimedb.view({ name: 'bottom_players', public: true }, t.array(player.rowType), ctx => { @@ -643,6 +649,13 @@ public static IEnumerable TopPlayers(ViewContext ctx) return ctx.Db.Player.Score.Filter(1000); } +// Procedural view with update callbacks. +[SpacetimeDB.View(Accessor = "TopPlayersWithUpdates", Public = true, PrimaryKey = "Id")] +public static IEnumerable TopPlayersWithUpdates(ViewContext ctx) +{ + return ctx.Db.Player.Score.Filter(1000); +} + // Perform a generic filter using the query builder. // Equivalent to `SELECT * FROM player WHERE score < 1000`. [SpacetimeDB.View(Accessor = "BottomPlayers", Public = true)] @@ -683,6 +696,12 @@ fn top_players(ctx: &ViewContext) -> Vec { ctx.db.player().score().filter(1000).collect() } +// Procedural view with update callbacks. +#[view(accessor = top_players_with_updates, public, primary_key = id)] +fn top_players_with_updates(ctx: &ViewContext) -> Vec { + ctx.db.player().score().filter(1000).collect() +} + // Perform a generic filter using the query builder. // Equivalent to `SELECT * FROM player WHERE score < 1000`. #[view(accessor = bottom_players, public)] diff --git a/docs/docs/00200-core-concepts/00200-functions/00500-views.md b/docs/docs/00200-core-concepts/00200-functions/00500-views.md index 1bd194d45..ed81a938c 100644 --- a/docs/docs/00200-core-concepts/00200-functions/00500-views.md +++ b/docs/docs/00200-core-concepts/00200-functions/00500-views.md @@ -1429,13 +1429,73 @@ SPACETIMEDB_VIEW(Query, levels_for_high_scorers, Public, AnonymousV -### Primary Key Inference for Query Builder Views +### Primary Keys for Views -Query builder views may carry primary-key semantics from their underlying row type. -When a view's row type maps to a table with a primary key, +Views can have primary-key semantics when SpacetimeDB knows which column identifies each returned row. +Generated client bindings treat these views like primary-key tables for client-cache updates. +In particular, update callbacks (`on_update` / `OnUpdate` / `onUpdate`) are generated for these view handles. +Views without a known primary key fall back to insert/delete-only behavior. + +For procedural views, primary keys are declared in the view definition for Rust and C#. +In TypeScript, you mark one of the columns in the returned row type with `.primaryKey()`. +View primary keys refer to source/accessor names, not case-converted or canonical database column names. +The examples below use a `Player` row with a primary-key `id` / `Id` column and an indexed `owner` / `Owner` column. + + + + +```typescript +import { schema, table, t } from 'spacetimedb/server'; + +const players = table( + { name: 'players', public: true }, + { + id: t.u64().primaryKey().autoInc(), + owner: t.identity().index('btree'), + name: t.string(), + } +); + +const spacetimedb = schema({ players }); +export default spacetimedb; + +export const my_players = spacetimedb.view( + { name: 'my_players', public: true }, + t.array(players.rowType), + (ctx) => Array.from(ctx.db.players.owner.filter(ctx.sender)) +); +``` + + + + +```csharp +[SpacetimeDB.View(Accessor = "MyPlayers", Public = true, PrimaryKey = "Id")] +public static IEnumerable MyPlayers(ViewContext ctx) +{ + return ctx.Db.Player.Owner.Filter(ctx.Sender); +} +``` + + + + +```rust +#[view(accessor = my_players, public, primary_key = id)] +fn my_players(ctx: &ViewContext) -> Vec { + ctx.db.player().owner().filter(ctx.sender()).collect() +} +``` + + + + +Query builder views may also carry primary-key semantics from their underlying row type. +When a query builder view's row type maps to a table with a primary key, generated client bindings can treat the view like a primary-key table for client-cache updates. -In particular, update callbacks (`on_update` / `OnUpdate` / `onUpdate`) will be generated for these view handles. -If a primary key cannot be inferred for the view row type, clients fall back to insert/delete-only behavior for that view handle. + +A view result must not contain two rows with the same primary key. +If a view returns duplicate primary key values during a view refresh, SpacetimeDB rejects that view result and fails the transaction that triggered the refresh. ## Next Steps diff --git a/docs/docs/00200-core-concepts/00600-clients/00200-codegen.md b/docs/docs/00200-core-concepts/00600-clients/00200-codegen.md index 3c918fb02..f648e17a5 100644 --- a/docs/docs/00200-core-concepts/00600-clients/00200-codegen.md +++ b/docs/docs/00200-core-concepts/00600-clients/00200-codegen.md @@ -346,6 +346,7 @@ For each [view](../00200-functions/00500-views.md) in your module, codegen gener - **Type definitions** for the view's return type - **Subscription interfaces** for subscribing to view results - **Query methods** for accessing cached view results +- **Update callbacks** when the view has a known primary key Views provide subscribable, computed queries over your data. diff --git a/docs/docs/00200-core-concepts/00600-clients/00500-rust-reference.md b/docs/docs/00200-core-concepts/00600-clients/00500-rust-reference.md index ecd0a02c8..7bc50e794 100644 --- a/docs/docs/00200-core-concepts/00600-clients/00500-rust-reference.md +++ b/docs/docs/00200-core-concepts/00600-clients/00500-rust-reference.md @@ -1057,7 +1057,8 @@ The `on_delete` callback runs whenever a previously-resident row is deleted from spacetimedb_sdk::TableWithPrimaryKey ``` -Implemented for handles whose rows have a known primary key, including query builder views with inferred primary keys. +Implemented for handles whose rows have a known primary key. +This includes table handles for tables with primary keys, query builder view handles with inferred primary keys, and procedural view handles with declared primary keys. | Name | Description | | ------------------------------------------- | ------------------------------------------------------------------------------------ | @@ -1077,7 +1078,7 @@ trait spacetimedb_sdk::TableWithPrimaryKey { The `on_update` callback runs whenever an already-resident row in the client cache is updated, i.e. replaced with a new row that has the same primary key. Registering an `on_update` callback returns a callback id, which can later be passed to `remove_on_update` to cancel the callback. Newly registered or canceled callbacks do not take effect until the following event. -This also applies to query builder views over tables with primary keys. +This also applies to views with known primary keys, including query builder views with inferred keys and procedural views that declare a primary key. ### Unique constraint index access diff --git a/docs/docs/00200-core-concepts/00600-clients/00600-csharp-reference.md b/docs/docs/00200-core-concepts/00600-clients/00600-csharp-reference.md index bbde96063..0db7e730c 100644 --- a/docs/docs/00200-core-concepts/00600-clients/00600-csharp-reference.md +++ b/docs/docs/00200-core-concepts/00600-clients/00600-csharp-reference.md @@ -1027,9 +1027,7 @@ class RemoteTableHandle } ``` -The `OnUpdate` callback runs whenever an already-resident row in the client cache is updated, i.e. replaced with a new row that has the same primary key. The table must have a primary key for callbacks to be triggered. Newly registered or canceled callbacks do not take effect until the following event. - -This also applies to query builder views over tables with primary keys. +The `OnUpdate` callback runs whenever an already-resident row in the client cache is updated, i.e. replaced with a new row that has the same primary key. The handle must have a known primary key for callbacks to be triggered. This includes tables with primary keys, query builder views with inferred primary keys, and procedural views declared with `PrimaryKey`. Newly registered or canceled callbacks do not take effect until the following event. See [the quickstart](../../00100-intro/00200-quickstarts/00600-c-sharp.md) for examples of registering and unregistering row callbacks. diff --git a/docs/docs/00200-core-concepts/00600-clients/00700-typescript-reference.md b/docs/docs/00200-core-concepts/00600-clients/00700-typescript-reference.md index fcbd24bae..759a0f85f 100644 --- a/docs/docs/00200-core-concepts/00600-clients/00700-typescript-reference.md +++ b/docs/docs/00200-core-concepts/00600-clients/00700-typescript-reference.md @@ -905,9 +905,9 @@ The `reducers` field of the context provides access to reducers exposed by the r ## Access the client cache -All [`DbContext`](#interface-dbcontext) implementors, including [`DbConnection`](#type-dbconnection) and [`EventContext`](#type-eventcontext), have fields `.db`, which in turn has methods for accessing tables in the client cache. +All [`DbContext`](#interface-dbcontext) implementors, including [`DbConnection`](#type-dbconnection) and [`EventContext`](#type-eventcontext), have fields `.db`, which in turn has methods for accessing tables and views in the client cache. -Each table defined by a module has an accessor method, whose name is the table name converted to `camelCase`, on this `.db` field. The table accessor methods return table handles. Table handles have methods for [accessing rows](#accessing-rows) and [registering `onInsert`](#callback-oninsert) and [`onDelete` callbacks](#callback-ondelete). Handles for tables which have a declared primary key field also expose [`onUpdate` callbacks](#callback-onupdate). Table handles also offer the ability to find subscribed rows by unique index. +Each table or view defined by a module has an accessor method, whose name is the table or view name converted to `camelCase`, on this `.db` field. The accessor methods return table handles. Table handles have methods for [accessing rows](#accessing-rows) and [registering `onInsert`](#callback-oninsert) and [`onDelete` callbacks](#callback-ondelete). Handles with a known primary key also expose [`onUpdate` callbacks](#callback-onupdate). Table handles also offer the ability to find subscribed rows by unique index. | Name | Description | | ------------------------------------------------------ | -------------------------------------------------------------------------------- | @@ -990,7 +990,7 @@ class TableHandle { The `onUpdate` callback runs whenever an already-resident row in the client cache is updated, i.e. replaced with a new row that has the same primary key. -Only handles with a declared or inferred primary key expose `onUpdate` callbacks. Handles for tables or views without a known primary key will not have `onUpdate` or `removeOnUpdate` methods. Only views over tables with primary keys will expose `onUpdate` callbacks. +Only handles with a declared primary key expose `onUpdate` callbacks. Handles for tables or views without a primary key will not have `onUpdate` or `removeOnUpdate` methods. TypeScript view handles expose `onUpdate` callbacks when the returned row type has exactly one column marked with `.primaryKey()`. The `Row` type will be an autogenerated type which matches the row type defined by the module.