Files
Tyler Cloutier 3f58b5951b Implement event tables (server, Rust/TS/C# codegen + client SDKs) (#4217)
## Summary

Implements event tables end-to-end: server datastore, module bindings
(Rust/TypeScript/C#), client codegen (Rust/TypeScript/C#), client SDKs
(Rust/TypeScript/C#), and integration tests.

Event tables are tables whose rows are ephemeral — they persist to the
commitlog and are delivered to V2 subscribers, but are NOT merged into
committed state. Rows are only visible within the transaction that
inserted them. This is the mechanism that replaces reducer event
callbacks in 2.0.

## What's included

### Server
- `is_event` flag on `RawTableDefV10`, `TableDef`, `TableSchema`
- Event table rows recorded in TxData but skipped during committed state
merge
- Commitlog replay treats event table inserts as no-ops
- Migration validation rejects changing `is_event` between module
versions
- `SELECT * FROM *` excludes event tables
- V1 WebSocket subscriptions to event tables rejected with upgrade
message
- V2 subscription path delivers event table rows correctly
- `CanBeLookupTable` trait — event tables cannot be lookup tables in
semijoins
- Runtime view validation rejects event tables

### Module bindings
- **Rust**: `#[spacetimedb::table(name = my_events, public, event)]`
- **TypeScript**: `table({ event: true }, ...)`
- **C#**: `[Table(Event = true)]`

### Client codegen (`crates/codegen/`)
- **Rust**: Generates `EventTable` impl (insert-only) for event tables,
`Table` impl for normal tables. `CanBeLookupTable` emitted for non-event
tables.
- **TypeScript**: Emits `event: true` in generated table schemas.
`ClientTableCore` type excludes `onDelete`/`onUpdate` for event tables
via conditional types.
- **C#**: Generates classes inheriting from `RemoteEventTableHandle`
(which hides `OnDelete`/`OnBeforeDelete`/`OnUpdate`) for event tables.

### Client SDKs
- **Rust**: `EventTable` trait with insert-only callbacks, client cache
bypass, `count()` returns 0, `iter()` returns empty
- **TypeScript**: Event table cache bypass in `table_cache.ts` — fires
`onInsert` callbacks but doesn't store rows. Type-level narrowing
excludes delete/update methods.
- **C#**: `RemoteEventTableHandle` base class hides delete/update
events. Parse/Apply/PostApply handle `EventTableRows` wire format, skip
cache storage, fire only `OnInsert`.

### Tests
- 9 datastore unit tests (insert/delete/update semantics, replay,
constraints, indexes, auto-inc, cross-tx reset)
- 3 Rust SDK integration tests (basic events, multiple events per
reducer, no persistence across transactions)
- Codegen snapshot tests (Rust, TypeScript, C#)
- Trybuild compile tests (event tables rejected as semijoin lookup
tables)

## Deferred
- `on_delete` codegen for event tables (server only sends inserts;
client synthesis deferred)
- Event tables in subscription joins / views (well-defined but
restricted for now)
- C++ SDK support
- RLS integration test

## API and ABI breaking changes

- `is_event: bool` added to `RawTableDefV10` (appended, defaults to
`false` — existing modules unaffected)
- `CanBeLookupTable` trait bound on semijoin methods in query builder
(all non-event tables implement it, so existing code compiles unchanged)
- `RemoteEventTableHandle` added to C# SDK (new base class for generated
event table handles)

## Expected complexity level and risk

3 — Changes touch the schema pipeline end-to-end and all three client
SDKs, but each individual change is straightforward. The core risk area
is the committed state merge skip in `committed_state.rs`. Client SDK
changes are additive (new code paths for event tables, existing paths
unchanged).

## Testing

- [x] `cargo clippy --workspace --tests --benches` passes
- [x] `cargo test -p spacetimedb-codegen` (snapshot tests)
- [x] `cargo test -p spacetimedb-datastore --features
spacetimedb-schema/test -- event_table` (9 unit tests)
- [x] `pnpm format` passes
- [x] Rust SDK integration tests pass (`event_table_tests` module)

---------

Signed-off-by: Tyler Cloutier <cloutiertyler@users.noreply.github.com>
Co-authored-by: Phoebe Goldman <phoebe@goldman-tribe.org>
Co-authored-by: Jason Larabie <jason@clockworklabs.io>
Co-authored-by: joshua-spacetime <josh@clockworklabs.io>
2026-02-15 22:56:20 +00:00
..
2026-02-07 04:26:45 +00:00
2026-02-07 04:26:45 +00:00
2026-02-07 04:26:45 +00:00
2026-02-07 04:26:45 +00:00
2026-02-07 04:26:45 +00:00
2026-02-07 04:26:45 +00:00
2026-02-07 04:26:45 +00:00
2026-02-07 04:26:45 +00:00
2026-02-07 04:26:45 +00:00

SpacetimeDB C++ Module Library Internal API

This directory contains the internal implementation of the SpacetimeDB C++ Module Library, matching the design of the C# bindings.

Overview

The Internal API provides:

  • Module Definition Management: Automatic generation of module definitions using autogenerated types
  • Type Registration: Type-safe registration system for tables and reducers
  • FFI Wrappers: Safe wrappers around the SpacetimeDB ABI
  • Table Operations: Type-safe CRUD operations on tables
  • Reducer System: Framework for implementing reducers with proper serialization

Directory Structure

  • autogen/ - Autogenerated types from Rust definitions (RawModuleDefV9, RawTableDefV9, etc.)

  • Module.h/cpp - Main module management and registration

  • FFI.h/cpp - Foreign Function Interface definitions

Key Components

1. Module Registration

The Module class is a singleton that manages:

  • Table registration
  • Reducer registration
  • Type registration
  • Module description generation
  • Reducer invocation

2. Autogenerated Types

All module definition types are generated from the Rust codebase:

  • RawModuleDefV9 - Main module definition
  • RawTableDefV9 - Table definitions
  • RawReducerDefV9 - Reducer definitions
  • TableAccess, TableType, etc. - Enums and supporting types

3. Table Implementation

Tables are implemented using the SPACETIMEDB_TABLE macro system:

  1. Define row types with field registration using SPACETIMEDB_MODULE_STRUCT (modules) or SPACETIMEDB_STRUCT (clients)
  2. Register tables with SPACETIMEDB_TABLE(Type, "name", Access, ...constraints)
  3. Use ctx.db.table<T>("name") for table operations (insert, delete)
  4. Tables are automatically registered during module initialization via __preinit__ functions

4. Reducer Implementation

Reducers are implemented using unified macro system:

  1. SPACETIMEDB_REDUCER(name, ReducerContext ctx, ...params) - User-defined reducers
  2. SPACETIMEDB_INIT(name, ReducerContext ctx) - Module initialization
  3. SPACETIMEDB_CLIENT_CONNECTED(name, ReducerContext ctx) - Client connection handler
  4. SPACETIMEDB_CLIENT_DISCONNECTED(name, ReducerContext ctx) - Client disconnection handler
  5. The macros handle serialization, registration, and ABI integration automatically

Usage Example

// Define row type with field registration
struct Person {
    uint32_t id;
    std::string name;
    uint8_t age;
};
SPACETIMEDB_MODULE_STRUCT(Person, id, name, age);

// Define table with constraints
SPACETIMEDB_TABLE(Person, "people", Public,
    PrimaryKeyAutoInc(id),
    Index(name)
);

// Define reducer with ReducerContext
SPACETIMEDB_REDUCER(add_person, ReducerContext ctx, std::string name, uint8_t age) {
    // Access table and insert record
    Person person{0, name, age}; // id will be auto-generated
    ctx.db.table<Person>("people").insert(person);
    LOG_INFO("Added person: " + name);
}

// Lifecycle reducers
SPACETIMEDB_INIT(init, ReducerContext ctx) {
    LOG_INFO("Module initialized!");
    return Ok();
}

SPACETIMEDB_CLIENT_CONNECTED(on_connect, ReducerContext ctx) {
    LOG_INFO("Client connected!");
    return Ok();
}

Migration from Old API

See MIGRATION_GUIDE.md for detailed steps on migrating from the manual module definition approach to this new Internal API.

Future Improvements

  • Automatic BSATN serialization generation
  • Macro-based table/reducer definitions
  • Query builder API
  • Index management