Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/docs/00200-core-concepts/00100-databases/00500-cheat-sheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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)]
Expand Down
70 changes: 65 additions & 5 deletions docs/docs/00200-core-concepts/00200-functions/00500-views.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| ------------------------------------------- | ------------------------------------------------------------------------------------ |
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| ------------------------------------------------------ | -------------------------------------------------------------------------------- |
Expand Down Expand Up @@ -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.

Expand Down
Loading