Skip to content

Commit 4ae01ae

Browse files
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
1 parent 315afc9 commit 4ae01ae

6 files changed

Lines changed: 92 additions & 13 deletions

File tree

docs/docs/00200-core-concepts/00100-databases/00500-cheat-sheet.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,12 @@ export const top_players = spacetimedb.view({ name: 'top_players', public: true
609609
return ctx.db.player.score.filter(1000);
610610
});
611611
612+
// Procedural view with update callbacks.
613+
// The returned row type has exactly one `.primaryKey()` column.
614+
export const top_players_with_updates = spacetimedb.view({ name: 'top_players_with_updates', public: true }, t.array(player.rowType), ctx => {
615+
return ctx.db.player.score.filter(1000);
616+
});
617+
612618
// Perform a generic filter using the query builder.
613619
// Equivalent to `SELECT * FROM player WHERE score < 1000`.
614620
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)
643649
return ctx.Db.Player.Score.Filter(1000);
644650
}
645651

652+
// Procedural view with update callbacks.
653+
[SpacetimeDB.View(Accessor = "TopPlayersWithUpdates", Public = true, PrimaryKey = "Id")]
654+
public static IEnumerable<Player> TopPlayersWithUpdates(ViewContext ctx)
655+
{
656+
return ctx.Db.Player.Score.Filter(1000);
657+
}
658+
646659
// Perform a generic filter using the query builder.
647660
// Equivalent to `SELECT * FROM player WHERE score < 1000`.
648661
[SpacetimeDB.View(Accessor = "BottomPlayers", Public = true)]
@@ -683,6 +696,12 @@ fn top_players(ctx: &ViewContext) -> Vec<Player> {
683696
ctx.db.player().score().filter(1000).collect()
684697
}
685698

699+
// Procedural view with update callbacks.
700+
#[view(accessor = top_players_with_updates, public, primary_key = id)]
701+
fn top_players_with_updates(ctx: &ViewContext) -> Vec<Player> {
702+
ctx.db.player().score().filter(1000).collect()
703+
}
704+
686705
// Perform a generic filter using the query builder.
687706
// Equivalent to `SELECT * FROM player WHERE score < 1000`.
688707
#[view(accessor = bottom_players, public)]

docs/docs/00200-core-concepts/00200-functions/00500-views.md

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1429,13 +1429,73 @@ SPACETIMEDB_VIEW(Query<PlayerLevel>, levels_for_high_scorers, Public, AnonymousV
14291429
</TabItem>
14301430
</Tabs>
14311431

1432-
### Primary Key Inference for Query Builder Views
1432+
### Primary Keys for Views
14331433

1434-
Query builder views may carry primary-key semantics from their underlying row type.
1435-
When a view's row type maps to a table with a primary key,
1434+
Views can have primary-key semantics when SpacetimeDB knows which column identifies each returned row.
1435+
Generated client bindings treat these views like primary-key tables for client-cache updates.
1436+
In particular, update callbacks (`on_update` / `OnUpdate` / `onUpdate`) are generated for these view handles.
1437+
Views without a known primary key fall back to insert/delete-only behavior.
1438+
1439+
For procedural views, primary keys are declared in the view definition for Rust and C#.
1440+
In TypeScript, you mark one of the columns in the returned row type with `.primaryKey()`.
1441+
View primary keys refer to source/accessor names, not case-converted or canonical database column names.
1442+
The examples below use a `Player` row with a primary-key `id` / `Id` column and an indexed `owner` / `Owner` column.
1443+
1444+
<Tabs groupId="server-language" queryString>
1445+
<TabItem value="typescript" label="TypeScript">
1446+
1447+
```typescript
1448+
import { schema, table, t } from 'spacetimedb/server';
1449+
1450+
const players = table(
1451+
{ name: 'players', public: true },
1452+
{
1453+
id: t.u64().primaryKey().autoInc(),
1454+
owner: t.identity().index('btree'),
1455+
name: t.string(),
1456+
}
1457+
);
1458+
1459+
const spacetimedb = schema({ players });
1460+
export default spacetimedb;
1461+
1462+
export const my_players = spacetimedb.view(
1463+
{ name: 'my_players', public: true },
1464+
t.array(players.rowType),
1465+
(ctx) => Array.from(ctx.db.players.owner.filter(ctx.sender))
1466+
);
1467+
```
1468+
1469+
</TabItem>
1470+
<TabItem value="csharp" label="C#">
1471+
1472+
```csharp
1473+
[SpacetimeDB.View(Accessor = "MyPlayers", Public = true, PrimaryKey = "Id")]
1474+
public static IEnumerable<Player> MyPlayers(ViewContext ctx)
1475+
{
1476+
return ctx.Db.Player.Owner.Filter(ctx.Sender);
1477+
}
1478+
```
1479+
1480+
</TabItem>
1481+
<TabItem value="rust" label="Rust">
1482+
1483+
```rust
1484+
#[view(accessor = my_players, public, primary_key = id)]
1485+
fn my_players(ctx: &ViewContext) -> Vec<Player> {
1486+
ctx.db.player().owner().filter(ctx.sender()).collect()
1487+
}
1488+
```
1489+
1490+
</TabItem>
1491+
</Tabs>
1492+
1493+
Query builder views may also carry primary-key semantics from their underlying row type.
1494+
When a query builder view's row type maps to a table with a primary key,
14361495
generated client bindings can treat the view like a primary-key table for client-cache updates.
1437-
In particular, update callbacks (`on_update` / `OnUpdate` / `onUpdate`) will be generated for these view handles.
1438-
If a primary key cannot be inferred for the view row type, clients fall back to insert/delete-only behavior for that view handle.
1496+
1497+
A view result must not contain two rows with the same primary key.
1498+
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.
14391499

14401500
## Next Steps
14411501

docs/docs/00200-core-concepts/00600-clients/00200-codegen.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ For each [view](../00200-functions/00500-views.md) in your module, codegen gener
346346
- **Type definitions** for the view's return type
347347
- **Subscription interfaces** for subscribing to view results
348348
- **Query methods** for accessing cached view results
349+
- **Update callbacks** when the view has a known primary key
349350
350351
Views provide subscribable, computed queries over your data.
351352

docs/docs/00200-core-concepts/00600-clients/00500-rust-reference.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,7 +1057,8 @@ The `on_delete` callback runs whenever a previously-resident row is deleted from
10571057
spacetimedb_sdk::TableWithPrimaryKey
10581058
```
10591059

1060-
Implemented for handles whose rows have a known primary key, including query builder views with inferred primary keys.
1060+
Implemented for handles whose rows have a known primary key.
1061+
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.
10611062

10621063
| Name | Description |
10631064
| ------------------------------------------- | ------------------------------------------------------------------------------------ |
@@ -1077,7 +1078,7 @@ trait spacetimedb_sdk::TableWithPrimaryKey {
10771078

10781079
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.
10791080

1080-
This also applies to query builder views over tables with primary keys.
1081+
This also applies to views with known primary keys, including query builder views with inferred keys and procedural views that declare a primary key.
10811082

10821083
### Unique constraint index access
10831084

docs/docs/00200-core-concepts/00600-clients/00600-csharp-reference.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,9 +1027,7 @@ class RemoteTableHandle
10271027
}
10281028
```
10291029

1030-
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.
1031-
1032-
This also applies to query builder views over tables with primary keys.
1030+
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.
10331031

10341032
See [the quickstart](../../00100-intro/00200-quickstarts/00600-c-sharp.md) for examples of registering and unregistering row callbacks.
10351033

docs/docs/00200-core-concepts/00600-clients/00700-typescript-reference.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -905,9 +905,9 @@ The `reducers` field of the context provides access to reducers exposed by the r
905905

906906
## Access the client cache
907907

908-
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.
908+
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.
909909

910-
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.
910+
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.
911911

912912
| Name | Description |
913913
| ------------------------------------------------------ | -------------------------------------------------------------------------------- |
@@ -990,7 +990,7 @@ class TableHandle {
990990

991991
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.
992992

993-
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.
993+
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()`.
994994

995995
The `Row` type will be an autogenerated type which matches the row type defined by the module.
996996

0 commit comments

Comments
 (0)