mirror of
https://github.com/clockworklabs/SpacetimeDB.git
synced 2026-05-13 03:08:40 -04:00
73881e38f7
# Description of Changes Major documentation overhaul focusing on tables, column types, and indexes. **Quickstart Guides:** - Updated React, TypeScript, Rust, and C# quickstarts with table/reducer examples - Fixed CLI syntax (positional `--database` argument) - Improved template consistency across languages **Tables Documentation:** - Added "Why Tables" section explaining table-oriented design philosophy (tables as fundamental unit, system tables, data-oriented design principles) - Added "Physical and Logical Independence" section explaining how subscription queries use the relational model independently of physical storage - Added brief sections linking to related pages (Visibility, Constraints, Schedule Tables) - Renamed "Scheduled Tables" to "Schedule Tables" throughout (tables store schedules; reducers are scheduled) **Column Types:** - Split into dedicated page with unified type reference table - Added "Representing Collections" section (Vec/Array vs table tradeoffs) - Added "Binary Data and Files" section for Vec<u8> storage patterns - Added "Type Performance" section (smaller types, fixed-size types, column ordering for alignment) - Added complete example struct demonstrating all type categories - Renamed "Structured" category to "Composite" **Indexes:** - Complete rewrite with textbook-style documentation - Added "When to Use Indexes" guidance - Documented single-column and multi-column index syntax (field-level and table-level) - Comprehensive range query examples with correct TypeScript `Range` class syntax - Explained multi-column index prefix matching semantics - Added index-accelerated deletion examples - Included index design guidelines **Styling:** - Added CSS for table border radius and row separators - Created Check component for green checkmarks in tables # API and ABI breaking changes None. Documentation only. # Expected complexity level and risk 1 - Documentation changes only, no code changes. # Testing - [ ] Verify docs build without errors - [ ] Review rendered pages for formatting issues - [ ] Confirm code examples are syntactically correct --------- Signed-off-by: Tyler Cloutier <cloutiertyler@users.noreply.github.com> Signed-off-by: John Detter <4099508+jdetter@users.noreply.github.com> Co-authored-by: clockwork-labs-bot <clockwork-labs-bot@users.noreply.github.com> Co-authored-by: John Detter <4099508+jdetter@users.noreply.github.com>
531 lines
13 KiB
Plaintext
531 lines
13 KiB
Plaintext
---
|
|
description: "MANDATORY: Read this ENTIRE file before writing ANY SpacetimeDB Rust code. Contains critical SDK patterns and HALLUCINATED APIs to avoid."
|
|
globs: **/*.rs
|
|
alwaysApply: true
|
|
---
|
|
|
|
# SpacetimeDB Rust SDK
|
|
|
|
> **Tested with:** SpacetimeDB runtime 1.11.x, `spacetimedb` crate 1.1.x
|
|
> **Last updated:** 2026-01-14
|
|
|
|
---
|
|
|
|
## HALLUCINATED APIs — DO NOT USE
|
|
|
|
**These APIs DO NOT EXIST. LLMs frequently hallucinate them.**
|
|
|
|
```rust
|
|
// WRONG — these macros/attributes don't exist
|
|
#[spacetimedb::table] // Use #[table] after importing
|
|
#[spacetimedb::reducer] // Use #[reducer] after importing
|
|
#[derive(Table)] // Tables use #[table] attribute, not derive
|
|
#[derive(Reducer)] // Reducers use #[reducer] attribute
|
|
|
|
// WRONG — SpacetimeType on tables
|
|
#[derive(SpacetimeType)] // DO NOT use on #[table] structs!
|
|
#[table(name = my_table)]
|
|
pub struct MyTable { ... }
|
|
|
|
// WRONG — mutable context
|
|
pub fn my_reducer(ctx: &mut ReducerContext, ...) { } // Should be &ReducerContext
|
|
|
|
// WRONG — table access without parentheses
|
|
ctx.db.player // Should be ctx.db.player()
|
|
ctx.db.player.find(id) // Should be ctx.db.player().id().find(&id)
|
|
```
|
|
|
|
### CORRECT PATTERNS:
|
|
|
|
```rust
|
|
// CORRECT IMPORTS
|
|
use spacetimedb::{table, reducer, Table, ReducerContext, Identity, Timestamp};
|
|
use spacetimedb::SpacetimeType; // Only for custom types, NOT tables
|
|
|
|
// CORRECT TABLE — no SpacetimeType derive!
|
|
#[table(name = player, public)]
|
|
pub struct Player {
|
|
#[primary_key]
|
|
pub id: u64,
|
|
pub name: String,
|
|
}
|
|
|
|
// CORRECT REDUCER — immutable context reference
|
|
#[reducer]
|
|
pub fn create_player(ctx: &ReducerContext, name: String) {
|
|
ctx.db.player().insert(Player { id: 0, name });
|
|
}
|
|
|
|
// CORRECT TABLE ACCESS — methods with parentheses
|
|
let player = ctx.db.player().id().find(&player_id);
|
|
```
|
|
|
|
### DO NOT:
|
|
- **Derive `SpacetimeType` on `#[table]` structs** — the macro handles this
|
|
- **Use mutable context** — `&ReducerContext`, not `&mut ReducerContext`
|
|
- **Forget `Table` trait import** — required for table operations
|
|
- **Use field access for tables** — `ctx.db.player()` not `ctx.db.player`
|
|
|
|
---
|
|
|
|
## 1) Common Mistakes Table
|
|
|
|
### Server-side errors
|
|
|
|
| Wrong | Right | Error |
|
|
|-------|-------|-------|
|
|
| `#[derive(SpacetimeType)]` on `#[table]` | Remove it — macro handles this | Conflicting derive macros |
|
|
| `ctx.db.player` (field access) | `ctx.db.player()` (method) | "no field `player` on type" |
|
|
| `ctx.db.player().find(id)` | `ctx.db.player().id().find(&id)` | Must access via index |
|
|
| `&mut ReducerContext` | `&ReducerContext` | Wrong context type |
|
|
| Missing `use spacetimedb::Table;` | Add import | "no method named `insert`" |
|
|
| `#[table(name = "my_table")]` | `#[table(name = my_table)]` | String literals not allowed |
|
|
| Missing `public` on table | Add `public` flag | Clients can't subscribe |
|
|
| `#[spacetimedb::reducer]` | `#[reducer]` after import | Wrong attribute path |
|
|
| Network/filesystem in reducer | Use procedures instead | Sandbox violation |
|
|
| Panic for expected errors | Return `Result<(), String>` | WASM instance destroyed |
|
|
|
|
### Client-side errors
|
|
|
|
| Wrong | Right | Error |
|
|
|-------|-------|-------|
|
|
| Wrong crate name | `spacetimedb-sdk` | Dependency not found |
|
|
| Manual event loop | Use `tokio` runtime | Async issues |
|
|
|
|
---
|
|
|
|
## 2) Table Definition (CRITICAL)
|
|
|
|
**Tables use the `#[table]` attribute macro, NOT `#[derive(SpacetimeType)]`**
|
|
|
|
```rust
|
|
use spacetimedb::{table, Table, Identity, Timestamp};
|
|
|
|
// WRONG — DO NOT derive SpacetimeType on tables!
|
|
#[derive(SpacetimeType)] // REMOVE THIS!
|
|
#[table(name = task)]
|
|
pub struct Task { ... }
|
|
|
|
// RIGHT — just the #[table] attribute
|
|
#[table(name = task, public)]
|
|
pub struct Task {
|
|
#[primary_key]
|
|
#[auto_inc]
|
|
pub id: u64,
|
|
|
|
pub owner_id: Identity,
|
|
pub title: String,
|
|
pub created_at: Timestamp,
|
|
}
|
|
|
|
// With indexes
|
|
#[table(name = task, public, index(name = by_owner, btree(columns = [owner_id])))]
|
|
pub struct Task {
|
|
#[primary_key]
|
|
#[auto_inc]
|
|
pub id: u64,
|
|
|
|
pub owner_id: Identity,
|
|
pub title: String,
|
|
}
|
|
```
|
|
|
|
### Field attributes
|
|
|
|
```rust
|
|
#[primary_key] // Exactly one per table (required)
|
|
#[auto_inc] // Auto-increment (integer primary keys only)
|
|
#[unique] // Unique constraint (can have multiple)
|
|
#[index(btree)] // Single-column BTree index
|
|
```
|
|
|
|
### Column types
|
|
|
|
```rust
|
|
u8, u16, u32, u64, u128 // Unsigned integers
|
|
i8, i16, i32, i64, i128 // Signed integers
|
|
f32, f64 // Floats
|
|
bool // Boolean
|
|
String // Text
|
|
Identity // User identity
|
|
Timestamp // Timestamp
|
|
ScheduleAt // For scheduled tables
|
|
Option<T> // Nullable
|
|
Vec<T> // Arrays
|
|
```
|
|
|
|
### Insert returns the row
|
|
|
|
```rust
|
|
// Insert and get the auto-generated ID
|
|
let row = ctx.db.task().insert(Task {
|
|
id: 0, // Placeholder for auto_inc
|
|
owner_id: ctx.sender,
|
|
title: "New task".to_string(),
|
|
created_at: ctx.timestamp,
|
|
});
|
|
let new_id = row.id; // Get the actual ID
|
|
```
|
|
|
|
---
|
|
|
|
## 3) Index Access
|
|
|
|
### Naming convention
|
|
- **Tables**: snake_case methods on `ctx.db`
|
|
- `#[table(name = my_table)]` → `ctx.db.my_table()`
|
|
- **Indexes**: exact declared name
|
|
- `index(name = by_owner, ...)` → `ctx.db.my_table().by_owner()`
|
|
|
|
### Primary key operations
|
|
|
|
```rust
|
|
// Find by primary key — returns Option<Row>
|
|
if let Some(task) = ctx.db.task().id().find(&task_id) {
|
|
// Use task
|
|
}
|
|
|
|
// Update by primary key
|
|
ctx.db.task().id().update(Task { id: task_id, ...updated_fields });
|
|
|
|
// Delete by primary key
|
|
ctx.db.task().id().delete(&task_id);
|
|
```
|
|
|
|
### Index filter
|
|
|
|
```rust
|
|
// Filter by indexed column — returns iterator
|
|
for task in ctx.db.task().by_owner().filter(&owner_id) {
|
|
// Process each task
|
|
}
|
|
```
|
|
|
|
### Unique column lookup
|
|
|
|
```rust
|
|
// Find by unique column — returns Option<Row>
|
|
if let Some(player) = ctx.db.player().username().find(&"alice".to_string()) {
|
|
// Found player
|
|
}
|
|
```
|
|
|
|
### Iterate all rows
|
|
|
|
```rust
|
|
// Full table scan
|
|
for task in ctx.db.task().iter() {
|
|
// Process each task
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4) Reducers
|
|
|
|
### Definition syntax
|
|
|
|
```rust
|
|
use spacetimedb::{reducer, ReducerContext, Table};
|
|
|
|
#[reducer]
|
|
pub fn create_task(ctx: &ReducerContext, title: String) {
|
|
// Validate
|
|
if title.is_empty() {
|
|
panic!("Title cannot be empty"); // Rolls back transaction
|
|
}
|
|
|
|
// Insert
|
|
ctx.db.task().insert(Task {
|
|
id: 0,
|
|
owner_id: ctx.sender,
|
|
title,
|
|
created_at: ctx.timestamp,
|
|
});
|
|
}
|
|
|
|
// With Result return type (preferred for recoverable errors)
|
|
#[reducer]
|
|
pub fn update_task(ctx: &ReducerContext, task_id: u64, title: String) -> Result<(), String> {
|
|
let task = ctx.db.task().id().find(&task_id)
|
|
.ok_or("Task not found")?;
|
|
|
|
if task.owner_id != ctx.sender {
|
|
return Err("Not authorized".to_string());
|
|
}
|
|
|
|
ctx.db.task().id().update(Task { title, ..task });
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Lifecycle reducers
|
|
|
|
```rust
|
|
#[reducer(init)]
|
|
pub fn init(ctx: &ReducerContext) {
|
|
// Called when module is first published
|
|
}
|
|
|
|
#[reducer(client_connected)]
|
|
pub fn on_connect(ctx: &ReducerContext) {
|
|
// ctx.sender is the connecting client
|
|
log::info!("Client connected: {:?}", ctx.sender);
|
|
}
|
|
|
|
#[reducer(client_disconnected)]
|
|
pub fn on_disconnect(ctx: &ReducerContext) {
|
|
// Clean up client state
|
|
}
|
|
```
|
|
|
|
### ReducerContext fields
|
|
|
|
```rust
|
|
ctx.sender // Identity of the caller
|
|
ctx.timestamp // Current timestamp
|
|
ctx.db // Database access
|
|
ctx.rng // Deterministic RNG (use instead of rand)
|
|
```
|
|
|
|
### Error handling
|
|
|
|
```rust
|
|
// Option 1: Panic (simple, destroys WASM instance)
|
|
if condition_failed {
|
|
panic!("Error message");
|
|
}
|
|
|
|
// Option 2: Result (preferred, graceful error handling)
|
|
#[reducer]
|
|
pub fn my_reducer(ctx: &ReducerContext) -> Result<(), String> {
|
|
do_something().map_err(|e| e.to_string())?;
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5) Custom Types
|
|
|
|
**Use `#[derive(SpacetimeType)]` ONLY for custom structs/enums used as fields or parameters.**
|
|
|
|
```rust
|
|
use spacetimedb::SpacetimeType;
|
|
|
|
// Custom struct for table fields
|
|
#[derive(SpacetimeType, Clone, Debug, PartialEq)]
|
|
pub struct Position {
|
|
pub x: i32,
|
|
pub y: i32,
|
|
}
|
|
|
|
// Custom enum
|
|
#[derive(SpacetimeType, Clone, Debug, PartialEq)]
|
|
pub enum PlayerStatus {
|
|
Idle,
|
|
Walking(Position),
|
|
Fighting(Identity),
|
|
}
|
|
|
|
// Use in table
|
|
#[table(name = player, public)]
|
|
pub struct Player {
|
|
#[primary_key]
|
|
pub id: Identity,
|
|
pub position: Position,
|
|
pub status: PlayerStatus,
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 6) Scheduled Tables
|
|
|
|
```rust
|
|
use spacetimedb::{table, reducer, ReducerContext, Table, ScheduleAt};
|
|
|
|
#[table(name = reminder, scheduled(send_reminder))]
|
|
pub struct Reminder {
|
|
#[primary_key]
|
|
#[auto_inc]
|
|
pub id: u64,
|
|
pub message: String,
|
|
pub scheduled_at: ScheduleAt,
|
|
}
|
|
|
|
// Scheduled reducer receives the full row
|
|
#[reducer]
|
|
fn send_reminder(ctx: &ReducerContext, reminder: Reminder) {
|
|
log::info!("Reminder: {}", reminder.message);
|
|
// Row is automatically deleted after reducer completes
|
|
}
|
|
|
|
// Schedule a reminder
|
|
#[reducer]
|
|
pub fn create_reminder(ctx: &ReducerContext, message: String, delay_secs: u64) {
|
|
let future_time = ctx.timestamp + std::time::Duration::from_secs(delay_secs);
|
|
ctx.db.reminder().insert(Reminder {
|
|
id: 0,
|
|
message,
|
|
scheduled_at: ScheduleAt::Time(future_time),
|
|
});
|
|
}
|
|
|
|
// Cancel by deleting the row
|
|
#[reducer]
|
|
pub fn cancel_reminder(ctx: &ReducerContext, reminder_id: u64) {
|
|
ctx.db.reminder().id().delete(&reminder_id);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 7) Timestamps
|
|
|
|
```rust
|
|
use spacetimedb::Timestamp;
|
|
|
|
// Current time from context
|
|
let now = ctx.timestamp;
|
|
|
|
// Create future timestamp
|
|
let future = ctx.timestamp + std::time::Duration::from_secs(60);
|
|
|
|
// Compare timestamps
|
|
if row.created_at < ctx.timestamp {
|
|
// Row was created before now
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 8) Data Visibility
|
|
|
|
**`public` flag exposes ALL rows to ALL clients.**
|
|
|
|
| Scenario | Pattern |
|
|
|----------|---------|
|
|
| Everyone sees all rows | `#[table(name = x, public)]` |
|
|
| Users see only their data | Private table + row-level security |
|
|
|
|
### Private table (default)
|
|
|
|
```rust
|
|
// No public flag — only server can read
|
|
#[table(name = secret_data)]
|
|
pub struct SecretData { ... }
|
|
```
|
|
|
|
### Row-level security
|
|
|
|
```rust
|
|
// Use row-level security for per-user visibility
|
|
#[table(name = player_data, public)]
|
|
#[rls(filter = |ctx, row| row.owner_id == ctx.sender)]
|
|
pub struct PlayerData {
|
|
#[primary_key]
|
|
pub id: u64,
|
|
pub owner_id: Identity,
|
|
pub data: String,
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 9) Procedures (Beta)
|
|
|
|
**Procedures are for side effects (HTTP, filesystem) that reducers can't do.**
|
|
|
|
Procedures are currently unstable. Enable with:
|
|
|
|
```toml
|
|
# Cargo.toml
|
|
[dependencies]
|
|
spacetimedb = { version = "1.*", features = ["unstable"] }
|
|
```
|
|
|
|
```rust
|
|
use spacetimedb::{procedure, ProcedureContext};
|
|
|
|
// Simple procedure
|
|
#[procedure]
|
|
fn add_numbers(_ctx: &mut ProcedureContext, a: u32, b: u32) -> u64 {
|
|
a as u64 + b as u64
|
|
}
|
|
|
|
// Procedure with database access
|
|
#[procedure]
|
|
fn save_external_data(ctx: &mut ProcedureContext, url: String) -> Result<(), String> {
|
|
// HTTP request (allowed in procedures, not reducers)
|
|
let data = fetch_from_url(&url)?;
|
|
|
|
// Database access requires explicit transaction
|
|
ctx.try_with_tx(|tx| {
|
|
tx.db.external_data().insert(ExternalData {
|
|
id: 0,
|
|
content: data,
|
|
});
|
|
Ok(())
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### Key differences from reducers
|
|
|
|
| Reducers | Procedures |
|
|
|----------|------------|
|
|
| `&ReducerContext` (immutable) | `&mut ProcedureContext` (mutable) |
|
|
| Direct `ctx.db` access | Must use `ctx.with_tx()` |
|
|
| No HTTP/network | HTTP allowed |
|
|
| No return values | Can return data |
|
|
|
|
---
|
|
|
|
## 10) Logging
|
|
|
|
```rust
|
|
use spacetimedb::log;
|
|
|
|
log::trace!("Detailed trace");
|
|
log::debug!("Debug info");
|
|
log::info!("Information");
|
|
log::warn!("Warning");
|
|
log::error!("Error occurred");
|
|
```
|
|
|
|
---
|
|
|
|
## 11) Commands
|
|
|
|
```bash
|
|
# Start local server
|
|
spacetime start
|
|
|
|
# Publish module
|
|
spacetime publish <module-name> --project-path <backend-dir>
|
|
|
|
# Clear database and republish
|
|
spacetime publish <module-name> --clear-database -y --project-path <backend-dir>
|
|
|
|
# Generate bindings
|
|
spacetime generate --lang rust --out-dir <client>/src/module_bindings --project-path <backend-dir>
|
|
|
|
# View logs
|
|
spacetime logs <module-name>
|
|
```
|
|
|
|
---
|
|
|
|
## 12) Hard Requirements
|
|
|
|
1. **DO NOT derive `SpacetimeType` on `#[table]` structs** — the macro handles this
|
|
2. **Import `Table` trait** — required for all table operations
|
|
3. **Use `&ReducerContext`** — not `&mut ReducerContext`
|
|
4. **Tables are methods** — `ctx.db.table()` not `ctx.db.table`
|
|
5. **Reducers must be deterministic** — no filesystem, network, timers, or external RNG
|
|
6. **Use `ctx.rng`** — not `rand` crate for random numbers
|
|
7. **Add `public` flag** — if clients need to subscribe to a table
|