Skip to content

Conversation

@cloutiertyler
Copy link
Contributor

@cloutiertyler cloutiertyler commented Feb 6, 2026

Description of Changes

Adds event tables support to SpacetimeDB (Rust server bindings only, V10 ABI).

Event tables are write-only tables whose rows persist to the commitlog but are NOT merged into committed state. Their rows are ephemeral in memory — only visible to V2 subscribers in the transaction that inserted them.

Schema & ABI:

  • Add is_event: bool to RawTableDefV10, TableDef, and TableSchema
  • New st_event_table system table (TableId 17) tracks which tables are event tables, avoiding migration of StTableRow
  • is_event is reconstructed from st_event_table in schema_for_table_raw()

Rust bindings macro:

  • Add event keyword: #[spacetimedb::table(name = my_events, public, event)]
  • Emits const IS_EVENT: bool = true; on TableInternal
  • Wired through register_table() to V10 builder's .with_event()

Core behavior:

  • merge_apply_inserts(): event table rows are recorded in TxData (for commitlog + subscriptions) but skip merging into committed state
  • replay_insert(): event table rows return early — no committed state rebuilt on restart

Compile-time semi-join validation:

  • CanBeLookupTable marker trait in query-builder — event tables don't implement it
  • Semi-join methods require R: CanBeLookupTable, giving a compile error if an event table is used as the lookup side

Cherry-picks joshua/shub/raw-module-def-v10 (#4153) as a prerequisite. V9 is completely untouched.

Deferred: V2 subscription evaluation, runtime SQL semi-join validation, view validation.

API and ABI breaking changes

New field is_event: bool added to RawTableDefV10 (appended at end for ABI compat). Defaults to false, so existing modules are unaffected.

CanBeLookupTable trait bound replaces HasIxCols on semi-join methods in the query builder. All non-event tables implement CanBeLookupTable, so existing code compiles unchanged.

Expected complexity level and risk

3 — The changes touch the schema pipeline end-to-end (raw def → validated def → runtime schema → system table → committed state merge/replay), but each individual change is straightforward. The main risk area is the merge skip logic in committed_state.rs, which is on the hot path and must correctly distinguish event tables from persistent ones. The system table approach avoids any migration risk for existing databases.

Testing

  • cargo check passes (verified locally)
  • Verify event table inserts are recorded in TxData for commitlog persistence
  • Verify committed state remains empty for event tables after merge
  • Verify replay skips event table rows (state stays empty after restart)
  • Verify compile error when using event table as right side of query builder semi-join
  • Verify non-event tables are completely unaffected

@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 from 2ddac79 to 614401f Compare February 10, 2026 05:26
Each validated schema type (TableDef, IndexDef, ReducerDef, etc.) now
has its own From impl for the corresponding V10 raw type, mirroring
the existing V9 conversions. The main From<ModuleDef> for RawModuleDefV10
is simplified to use .into() calls.
- Split Table trait: independent EventTable (insert-only) and Table
  (insert + delete) traits in the Rust client SDK
- Codegen generates EventTable impl for event tables, Table impl for
  persistent tables; skips TableWithPrimaryKey, unique accessors, and
  cache diff for event tables
- Event table inserts fire on_insert callbacks without persisting to
  the client cache via into_event_diff()
- Filter event tables from V1 SELECT * FROM * subscriptions
- Reject event tables as lookup table in subscription semi-joins
- Disallow event tables in view definitions
- Switch extract_schema to V10 output format
- Regenerate test client bindings with V10 codegen
…t tables

- Add sdk-test-event-table module and event-table-client test harness
- Add three integration tests: basic event table, multiple events in one
  reducer, and events-don't-persist-across-transactions
- Add compile-time tests for CanBeLookupTable enforcement on event tables
- Add returns_event_table() to SubscriptionPlan and Plan
- Reject v1 WebSocket subscriptions to event tables with a clear error
  message directing developers to upgrade to v2
- Add exec_v1_rejects_event_table test verifying v1 clients get a clear
  error when subscribing to event tables
- Mark the 3 event table integration tests as #[ignore] since they
  require v2 WebSocket support in the Rust SDK (not yet implemented)
- Add docs/event-tables-status.md tracking what's implemented vs
  remaining from the event tables proposal
The client can synthesize on_delete from the insert since every event
table row is a noop. Implementers may defer on_delete generation.
- Replace on_<reducer>() callbacks with _then() invocation pattern
- Replace Status::Failed/OutOfEnergy with Status::Err/Panic
- Remove CallReducerFlags, set_reducer_flags, Event::UnknownTransaction
- Remove ReducerEvent.caller_identity/caller_connection_id field access
- Use noop_then() instead of on_noop() in event-table-client
- Pass the generated request_id into CallReducer message instead of
  hardcoded 0, matching the procedure code pattern
- Remove #[ignore] from event table tests now that SDK uses v2
- Remove v1_rejects_event_table test (SDK no longer uses v1)
Move completed items (v2 subscription path, Rust SDK v2, reducer
callback deprecation, active integration tests) from "Not Yet
Implemented" to "Implemented". Add "Deferred" section for well-defined
but postponed work (on_delete codegen, joins, views). Add "Known
Issues" section documenting v2 test stability and the fixed request_id
bug. Fix proposal link path.
@cloutiertyler cloutiertyler force-pushed the tyler/impl-event-tables branch from 614401f to ea81292 Compare February 10, 2026 21:59
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.

3 participants