Skip to content

Implement event tables (server, Rust/TS/C# codegen + client SDKs)#4217

Open
cloutiertyler wants to merge 22 commits intomasterfrom
tyler/impl-event-tables
Open

Implement event tables (server, Rust/TS/C# codegen + client SDKs)#4217
cloutiertyler wants to merge 22 commits intomasterfrom
tyler/impl-event-tables

Conversation

@cloutiertyler
Copy link
Contributor

@cloutiertyler cloutiertyler commented Feb 6, 2026

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

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

@cloutiertyler cloutiertyler force-pushed the tyler/impl-event-tables branch from c96550a to 57cc9a5 Compare February 6, 2026 01:21
@cloutiertyler cloutiertyler force-pushed the tyler/impl-event-tables branch 2 times, most recently from c13a4b1 to 07d57a5 Compare February 6, 2026 04:18
@Centril
Copy link
Contributor

Centril commented Feb 9, 2026

A few things:

  • It would be good to review and land the datastore changes first.
  • When doing so, let's add unit tests (in the datastore crate) asserting that:
    a) an insert/delete pair does nothing to committed state (check this by querying txn +1) and neither to TxData.
    b) an update does nothing to committed state and neither to TxData.
    c) an insertion to an event table results in no changes to committed state and 1 change to TxData.
    d) replaying should also be tested as a unit test by saving the commitlog and then replaying and checking committed state after.
  • I didn't see any code handling migrations from/to event tables. Ostensibly, such transitions should not be allowed. If this is already the case, a test confirming so would be good.

@cloutiertyler cloutiertyler changed the base branch from jsdt/ws-v2 to phoebe/rust-sdk-ws-v2 February 10, 2026 04:03
@cloutiertyler cloutiertyler force-pushed the tyler/impl-event-tables branch 2 times, most recently from 614401f to ea81292 Compare February 10, 2026 21:59
@cloutiertyler cloutiertyler force-pushed the tyler/impl-event-tables branch 2 times, most recently from 4f698fc to 1ec6de5 Compare February 13, 2026 16:08
@cloutiertyler cloutiertyler changed the base branch from phoebe/rust-sdk-ws-v2 to master February 13, 2026 16:08
@cloutiertyler cloutiertyler force-pushed the tyler/impl-event-tables branch from 1ec6de5 to 1d669cb Compare February 13, 2026 17:34
@cloutiertyler cloutiertyler changed the base branch from master to jlarabie/csharp-sdk-ws-v2-rebase2 February 13, 2026 17:36
…ings-macro, and codegen

- Add `event` attribute to #[table] macro for declaring event tables
- Add CanBeLookupTable trait (non-event tables implement it)
- Generate CanBeLookupTable impls in Rust codegen
- Update physical plan and query-builder for event table support
- Update module-test with example event table
- Update codegen snapshots
- Add EventTable trait for transient event table rows (no client cache persistence)
- Add from_event_inserts() and into_event_diff() for event table update handling
- Parse EventTable rows from v2 TableUpdateRows
- Exclude event tables from automatic subscription plans (get_all)
- Reject v1 client subscriptions to event tables with upgrade message
- Prevent event tables from being used as join lookup tables
- Export EventTable in SDK public API
- Add sdk-test-event-table module with event table and reducers
- Add event-table-client test binary with generated bindings
- Register event-table-client and sdk-test-event-table in workspace
- Update test configuration
- Add event-tables-status.md tracking implementation progress
- Add 1.0 to 2.0 migration guide
The MyEvent table and emit_event reducer belong in sdk-test-event-table,
not in module-test which must stay in sync with C# and TypeScript modules.
- Rust codegen: branch on is_event to emit EventTable trait, skip delete/update callbacks
- TS module bindings: add event option to table(), pass isEvent through schema
- TS codegen: emit event: true for event tables
- TS client SDK: event tables fire on_insert but skip cache storage
- C# SDK: IsEventTable flag, parse EventTableRows, skip cache/indices, fire OnInsert only
- C# codegen: pass isEventTable: true in constructor for event tables
- Datastore: 5 new tests for constraints/indexes/auto_inc on event tables
- Docs: update event-tables-status.md with completed items
Event tables only expose onInsert/removeOnInsert at the type level,
matching the Rust SDK's EventTable trait. Split ClientTableMethods into
ClientTableInsertMethods and ClientTableDeleteMethods, and use an
IsEventTable conditional type in ClientTableCore to exclude delete and
update methods for event tables.
…s in C# SDK

Event tables only expose OnInsert at the type level, matching Rust's
EventTable trait and the TypeScript conditional types. RemoteEventTableHandle
inherits from RemoteTableHandle and uses private new to shadow the
OnDelete, OnBeforeDelete, and OnUpdate events. Codegen generates event
table classes inheriting from RemoteEventTableHandle instead.
@cloutiertyler cloutiertyler changed the base branch from jlarabie/csharp-sdk-ws-v2-rebase2 to master February 13, 2026 18:36
@cloutiertyler cloutiertyler force-pushed the tyler/impl-event-tables branch from fdf598e to f0f22d1 Compare February 13, 2026 18:36
cloutiertyler and others added 2 commits February 13, 2026 13:44
At some point, a rebase on this branch appears to have accidentally overwritten
the Rust codegen changes in #4257 .
This commit fixes that, to integrate the event tables codegen changes
with the prior WS V2 codegen changes.

I've also taken the minor liberty of re-ordering the emitted top-level forms
in a table definition file, in order to allow merging the definitions of
`register_table` and `parse_table_update`,
which this branch previously had duplicated into the `if` and `else` cases
of the conditional on whether the table was an event table or not.
Plus I added a few comments.
@cloutiertyler cloutiertyler changed the title Implement event tables Implement event tables (server, Rust/TS/C# codegen + client SDKs) Feb 13, 2026
JasonAtClockwork and others added 5 commits February 13, 2026 15:28
…eforeDelete/OnUpdate from RemoteEventTableHandle
The server was always sending event table rows as PersistentTable inserts,
causing clients to store them in the cache. Fix the server to send the
EventTable variant, fix Rust codegen to use into_event_diff() (bypassing
the cache), revert C# Table.cs tolerance checks that are no longer needed,
and fix event table test race conditions where tests passed without actually
running assertions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants