Skip to content

Commit 3f58b59

Browse files
cloutiertylergefjonJasonAtClockworkjoshua-spacetime
authored
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>
1 parent 124808c commit 3f58b59

268 files changed

Lines changed: 3845 additions & 2783 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,13 @@ members = [
4949
"modules/sdk-test-connect-disconnect",
5050
"modules/sdk-test-procedure",
5151
"modules/sdk-test-view",
52+
"modules/sdk-test-event-table",
5253
"sdks/rust/tests/test-client",
5354
"sdks/rust/tests/test-counter",
5455
"sdks/rust/tests/connect_disconnect_client",
5556
"sdks/rust/tests/procedure-client",
5657
"sdks/rust/tests/view-client",
58+
"sdks/rust/tests/event-table-client",
5759
"tools/ci",
5860
"tools/upgrade-version",
5961
"tools/license-check",

crates/bindings-cpp/include/spacetimedb/internal/autogen/RawMiscModuleExportV9.g.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
#include <memory>
1313
#include "../autogen_base.h"
1414
#include "spacetimedb/bsatn/bsatn.h"
15+
#include "RawViewDefV9.g.h"
1516
#include "RawColumnDefaultValueV9.g.h"
1617
#include "RawProcedureDefV9.g.h"
17-
#include "RawViewDefV9.g.h"
1818

1919
namespace SpacetimeDB::Internal {
2020

crates/bindings-cpp/include/spacetimedb/internal/autogen/RawModuleDefV9.g.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
#include "../autogen_base.h"
1414
#include "spacetimedb/bsatn/bsatn.h"
1515
#include "RawMiscModuleExportV9.g.h"
16-
#include "RawReducerDefV9.g.h"
16+
#include "RawTypeDefV9.g.h"
1717
#include "RawTableDefV9.g.h"
18-
#include "Typespace.g.h"
1918
#include "RawRowLevelSecurityDefV9.g.h"
20-
#include "RawTypeDefV9.g.h"
19+
#include "RawReducerDefV9.g.h"
20+
#include "Typespace.g.h"
2121

2222
namespace SpacetimeDB::Internal {
2323

crates/bindings-cpp/include/spacetimedb/internal/autogen/RawProcedureDefV9.g.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
#include <memory>
1313
#include "../autogen_base.h"
1414
#include "spacetimedb/bsatn/bsatn.h"
15-
#include "ProductType.g.h"
1615
#include "AlgebraicType.g.h"
16+
#include "ProductType.g.h"
1717

1818
namespace SpacetimeDB::Internal {
1919

crates/bindings-cpp/include/spacetimedb/internal/autogen/RawTableDefV9.g.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212
#include <memory>
1313
#include "../autogen_base.h"
1414
#include "spacetimedb/bsatn/bsatn.h"
15-
#include "RawScheduleDefV9.g.h"
1615
#include "TableAccess.g.h"
1716
#include "RawConstraintDefV9.g.h"
17+
#include "RawIndexDefV9.g.h"
1818
#include "TableType.g.h"
1919
#include "RawSequenceDefV9.g.h"
20-
#include "RawIndexDefV9.g.h"
20+
#include "RawScheduleDefV9.g.h"
2121

2222
namespace SpacetimeDB::Internal {
2323

crates/bindings-macro/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ mod sym {
6161
symbol!(unique);
6262
symbol!(update);
6363
symbol!(default);
64+
symbol!(event);
6465

6566
symbol!(u8);
6667
symbol!(i8);

crates/bindings-macro/src/table.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub(crate) struct TableArgs {
1919
scheduled: Option<ScheduledArg>,
2020
name: Ident,
2121
indices: Vec<IndexArg>,
22+
event: Option<Span>,
2223
}
2324

2425
enum TableAccess {
@@ -71,6 +72,7 @@ impl TableArgs {
7172
let mut scheduled = None;
7273
let mut name = None;
7374
let mut indices = Vec::new();
75+
let mut event = None;
7476
syn::meta::parser(|meta| {
7577
match_meta!(match meta {
7678
sym::public => {
@@ -91,6 +93,10 @@ impl TableArgs {
9193
check_duplicate(&scheduled, &meta)?;
9294
scheduled = Some(ScheduledArg::parse_meta(meta)?);
9395
}
96+
sym::event => {
97+
check_duplicate(&event, &meta)?;
98+
event = Some(meta.path.span());
99+
}
94100
});
95101
Ok(())
96102
})
@@ -107,6 +113,7 @@ impl TableArgs {
107113
scheduled,
108114
name,
109115
indices,
116+
event,
110117
})
111118
}
112119
}
@@ -852,6 +859,18 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R
852859
);
853860

854861
let table_access = args.access.iter().map(|acc| acc.to_value());
862+
let is_event = args.event.iter().map(|_| {
863+
quote!(
864+
const IS_EVENT: bool = true;
865+
)
866+
});
867+
let can_be_lookup_impl = if args.event.is_none() {
868+
quote! {
869+
impl spacetimedb::query_builder::CanBeLookupTable for #original_struct_ident {}
870+
}
871+
} else {
872+
quote! {}
873+
};
855874
let unique_col_ids = unique_columns.iter().map(|col| col.index);
856875
let primary_col_id = primary_key_column.clone().into_iter().map(|col| col.index);
857876
let sequence_col_ids = sequenced_columns.iter().map(|col| col.index);
@@ -977,6 +996,7 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R
977996
const TABLE_NAME: &'static str = #table_name;
978997
// the default value if not specified is Private
979998
#(const TABLE_ACCESS: spacetimedb::table::TableAccess = #table_access;)*
999+
#(#is_event)*
9801000
const UNIQUE_COLUMNS: &'static [u16] = &[#(#unique_col_ids),*];
9811001
const INDEXES: &'static [spacetimedb::table::IndexDesc<'static>] = &[#(#index_descs),*];
9821002
#(const PRIMARY_KEY: Option<u16> = Some(#primary_col_id);)*
@@ -1088,6 +1108,8 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R
10881108
}
10891109
}
10901110

1111+
#can_be_lookup_impl
1112+
10911113
};
10921114

10931115
let table_query_handle_def = quote! {

crates/bindings-typescript/src/lib/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export function tableToSchema<
117117
};
118118
}) as T['idxs'],
119119
tableDef,
120+
...(tableDef.isEvent ? { isEvent: true } : {}),
120121
};
121122
}
122123

crates/bindings-typescript/src/lib/table.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export type UntypedTableDef = {
117117
indexes: readonly IndexOpts<any>[];
118118
constraints: readonly ConstraintOpts<any>[];
119119
tableDef: Infer<typeof RawTableDefV10>;
120+
isEvent?: boolean;
120121
};
121122

122123
/**
@@ -179,6 +180,7 @@ export type TableOpts<Row extends RowObj> = {
179180
{ [k: string]: RowBuilder<RowObj> },
180181
ReturnType<typeof t.unit>
181182
>;
183+
event?: boolean;
182184
};
183185

184186
/**
@@ -301,6 +303,7 @@ export function table<Row extends RowObj, const Opts extends TableOpts<Row>>(
301303
public: isPublic = false,
302304
indexes: userIndexes = [],
303305
scheduled,
306+
event: isEvent = false,
304307
} = opts;
305308

306309
// 1. column catalogue + helpers
@@ -476,7 +479,7 @@ export function table<Row extends RowObj, const Opts extends TableOpts<Row>>(
476479
tableType: { tag: 'User' },
477480
tableAccess: { tag: isPublic ? 'Public' : 'Private' },
478481
defaultValues,
479-
isEvent: false,
482+
isEvent,
480483
};
481484
},
482485
idxs: {} as OptsIndices<Opts>,

0 commit comments

Comments
 (0)