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
This commit is contained in:
joshua-spacetime
2026-06-15 16:44:46 -07:00
committed by GitHub
parent 315afc938f
commit 4ae01aefbd
6 changed files with 92 additions and 13 deletions
@@ -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<Player> 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<Player> 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<Player> {
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<Player> {
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)]
@@ -1429,13 +1429,73 @@ SPACETIMEDB_VIEW(Query<PlayerLevel>, levels_for_high_scorers, Public, AnonymousV
</TabItem>
</Tabs>
### 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.
<Tabs groupId="server-language" queryString>
<TabItem value="typescript" label="TypeScript">
```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))
);
```
</TabItem>
<TabItem value="csharp" label="C#">
```csharp
[SpacetimeDB.View(Accessor = "MyPlayers", Public = true, PrimaryKey = "Id")]
public static IEnumerable<Player> MyPlayers(ViewContext ctx)
{
return ctx.Db.Player.Owner.Filter(ctx.Sender);
}
```
</TabItem>
<TabItem value="rust" label="Rust">
```rust
#[view(accessor = my_players, public, primary_key = id)]
fn my_players(ctx: &ViewContext) -> Vec<Player> {
ctx.db.player().owner().filter(ctx.sender()).collect()
}
```
</TabItem>
</Tabs>
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
@@ -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.
@@ -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
@@ -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.
@@ -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.