feat: Allow toggling event flag during auto-migration for empty tables#4875
feat: Allow toggling event flag during auto-migration for empty tables#4875Ludv1gL wants to merge 3 commits intoclockworklabs:masterfrom
Conversation
| assert_matches!( | ||
| tx.pending_schema_changes(), | ||
| &[PendingSchemaChange::TableAlterEventFlag(t, false)] if t == table_id | ||
| ); |
There was a problem hiding this comment.
Repeated 3x; Let's make a function out of this: check_table_event_flag_altered(&tx, table_id, state: bool)
There was a problem hiding this comment.
Done in ead511c3f4. The new check_table_event_flag_altered(datastore, tx, table_id, expected_is_event) helper in test_helpers.rs bundles the schema is_event check with the st_event_table row-presence check. The 4 test_alter_table_event_flag_* tests each call it in lieu of the inline pattern.
There was a problem hiding this comment.
By this, I meant a function that checks pending_schema_changes. This function should be named check_table_event_flag_altered. The one you made in test_helpers doesn't do what its name suggests.
Currently, toggling `#[spacetimedb::table(event)]` on an existing table fails with `AutoMigrateError::ChangeTableEventFlag` and requires a manual migration. This PR allows the flip in either direction as a live auto-migration step when the table has zero committed rows. Non-empty tables fail with an actionable error guiding the user to clear the table first. Clients are disconnected on the flip because the change is observable to subscribers (event tables have no committed state). - **`tx_state.rs`**: New `PendingSchemaChange::TableAlterEventFlag` variant storing the old `is_event` value for rollback. - **`committed_state.rs`**: Rollback branch restores the old value on the live schema. - **`mut_tx.rs`**: New `alter_table_event_flag` — dual-write to the tx + commit table schemas and to `st_event_table`. Idempotent no-op early returns before pushing a pending change. New `delete_st_event_table_row` helper using the existing `delete_col_eq` utility (hits the unique btree index on col 0). - **`replay.rs`**: New hook on `ST_EVENT_TABLE_ID` insert/delete mirrors `reschema_table_for_st_table_update` — flips `is_event` on the referenced user-table's cached schema during replay. This is load-bearing for cold replay across the flip point. - **`relational_db.rs`**: Thin `alter_table_event_flag` wrapper. - **`auto_migrate.rs`**: `event_ok` error branch replaced with `AutoMigrateStep::ChangeEventFlag` + `ensure_disconnect_all_users`. Removed dead `AutoMigrateError::ChangeTableEventFlag` variant. - **`formatter.rs` / `termcolor_formatter.rs`**: New `format_change_event_flag` mirroring `format_change_access`. - **`update.rs`**: New `ChangeEventFlag` handler with an O(1) row-count precheck before any mutation. - **Transaction safety**: Precheck (row count) and all three writes (st_event_table, tx schema, commit schema) run in the same `MutTx`. - **Rollback**: `TableAlterEventFlag` stores the old flag value so failed txs revert `is_event` on the live schema. Idempotent flips do not push a pending change. - **Replay correctness**: Without the replay hook, cold replay from a pre-migration snapshot would miss the schema flip and post-migration inserts would silently land in committed state. The hook mirrors the existing `st_table`/`st_column` pattern. - **Client contract**: Flipping `event` changes observability — v1 subscribers stop seeing updates; v2 subscribers see a different message variant. `ensure_disconnect_all_users` forces reconnection. ``` Cannot change `event` flag on table `my_table`: table contains data. Clear the table's rows (e.g. via a reducer) before toggling the `event` annotation. ``` - [x] `cargo test -p spacetimedb-datastore --features test` — 87 pass (including 4 new `alter_table_event_flag` tests) - [x] `cargo test -p spacetimedb-schema` — 103 pass (including 3 new `change_event_flag` plan tests) - [x] `cargo test -p spacetimedb-core` — 192 pass (including 2 new empty/non-empty integration tests) - [x] `cargo clippy -p spacetimedb-datastore -p spacetimedb-schema -p spacetimedb-core --tests` clean - [x] Pre-existing event-table tests still pass (10 tests)
Address all 16 actionable items from the review — deferred item D1
(moving the row-count precheck into `AutoMigratePrecheck::CheckTableEmpty`)
is not addressed here and will follow in a separate PR as agreed.
- `schema/auto_migrate.rs`: merge `test_change_event_flag_produces_step_{non_to_event,event_to_non}`
into a single test with a closure called twice; make the sub-object-steps
test use the same closure pattern instead of duplicating the old/new
builder with an implicit `false` vs explicit `true`.
- `datastore/replay.rs`: assert `num_rows()==0` inside the
`reschema_table_for_st_event_table_update` hook. This enforces the
feature's core invariant during cold replay.
- `datastore/committed_state.rs`: same assertion on the `TableAlterEventFlag`
rollback arm.
- `datastore/mut_tx.rs`: extract `insert_st_event_table_row(table_id)`
shared by `create_table` and `alter_table_event_flag`.
- `datastore/locking_tx_datastore/test_helpers.rs` (new): host the
cross-crate-reachable `assert_is_event_state`, `st_event_table_has_row`,
and `check_table_event_flag_altered` helpers. Gated by the existing
`test` feature. Re-exported from `locking_tx_datastore::mod`.
- `datastore/datastore.rs`: replace the 4 `test_alter_table_event_flag_*`
tests' inline is-event + st_event_table-row-presence duplication with
`check_table_event_flag_altered` (was repeated 3x by the reviewer's
count; actual count after deduplication drops to 5 call sites via the
helper). Add `TxData` assertions on the successful commits proving the
schema change materializes as an `st_event_table` insert/delete and
does not touch the user-table row data. Swap `is_empty()` /
`assert_matches!(&[])` for `assert_eq!(tx.pending_schema_changes(), [])`.
- `core/update.rs`: cleaner v10 builder via `.with_event(is_event)` chain;
extract `setup_events_table` returning `TableId`; move the row insert
into a separate tx in the non-empty-fails test; replace
`.any(|c| matches!(...))` with `assert_matches!([pat, ..])`; replace
`.is_empty()` with `assert_eq!([])`; adopt the shared
`assert_is_event_state` helper from `spacetimedb-datastore`.
40af039 to
ead511c
Compare
|
Thanks for the review! Addressed all 16 actionable items — the deferred Rebased onto current
Summary of changes in
|
Centril
left a comment
There was a problem hiding this comment.
Getting there but still some things I'd like to change.
| assert_matches!( | ||
| tx.pending_schema_changes(), | ||
| [PendingSchemaChange::TableAlterEventFlag(t, false), ..] if *t == table_id |
There was a problem hiding this comment.
We can use assert_eq! here instead, it's simpler.
| .iter_by_col_eq_mut_tx( | ||
| tx, | ||
| ST_EVENT_TABLE_ID, | ||
| ColList::from(StEventTableFields::TableId.col_id()), |
There was a problem hiding this comment.
This doesn't seem like it was actually done.
|
|
||
| #[test] | ||
| fn test_alter_table_event_flag_non_event_to_event() -> ResultTest<()> { | ||
| // Create a non-event table. |
There was a problem hiding this comment.
Comment was useful, please bring it back.
| let (datastore, tx, table_id) = setup_table()?; | ||
| commit(&datastore, tx)?; | ||
|
|
||
| // Flip `is_event` from `false` to `true`. |
There was a problem hiding this comment.
Comment was useful, please bring it back.
| assert_matches!( | ||
| tx.pending_schema_changes(), | ||
| &[PendingSchemaChange::TableAlterEventFlag(t, false)] if t == table_id | ||
| ); |
There was a problem hiding this comment.
By this, I meant a function that checks pending_schema_changes. This function should be named check_table_event_flag_altered. The one you made in test_helpers doesn't do what its name suggests.
| tx_data.inserts_for_table(ST_EVENT_TABLE_ID).map(<[_]>::len), | ||
| Some(1), |
There was a problem hiding this comment.
Please assert the contents of the row too.
| ); | ||
| check_table_event_flag_altered(&datastore, &tx, table_id, true); | ||
|
|
||
| // Flip `is_event` from `true` to `false`. |
| tx_data.deletes_for_table(ST_EVENT_TABLE_ID).map(<[_]>::len), | ||
| Some(1), |
There was a problem hiding this comment.
Please assert the row contents here too.
| #[test] | ||
| fn test_alter_table_event_flag_non_event_to_event() -> ResultTest<()> { | ||
| // Create a non-event table. | ||
| use crate::locking_tx_datastore::test_helpers::check_table_event_flag_altered; |
There was a problem hiding this comment.
Could be imported once for the test module instead of in each function.
Third round of review fixes. All 9 actionable comments addressed, no
incidental cleanup.
- `test_helpers.rs`:
- `st_event_table_has_row`: drop the `ColList::from(...).col_id()`
wrapping and pass `StEventTableFields::TableId` directly (the
`iter_by_col_eq_mut_tx` `cols: impl Into<ColList>` + blanket
`impl<C: Into<ColId>> From<C> for ColList` handle the conversion).
Matches the idiom in `system_tables.rs` (`with_primary_key`,
`with_unique_constraint`, `btree(...)`).
- Rename: the previous bundled `check_table_event_flag_altered`
(schema + `st_event_table` row-presence check) is replaced by a
version that asserts `tx.pending_schema_changes()` contains exactly
one `TableAlterEventFlag(table_id, !state)` entry — matching the
repeated assertion pattern that Centril asked to factor out in
round 2.
- `datastore.rs` tests:
- Hoist the `test_helpers::{assert_is_event_state,
check_table_event_flag_altered, st_event_table_has_row}` import to
the module level (was duplicated inside each of the 4 tests).
- Restore the per-step `// Create a non-event table.` / `// Flip
is_event from false to true.` / `// After rollback...` narrative
comments and assertion messages that were lost in the previous
refactor.
- Replace the `inserts_for_table(ST_EVENT_TABLE_ID).map(<[_]>::len)
== Some(1)` length-only check (and its `deletes_for_table`
counterpart) with an exact row-content assertion against
`ProductValue::from(StEventTableRow { table_id })`.
- `core/db/update.rs`:
- Swap the inline `assert_matches!(tx.pending_schema_changes(),
[PendingSchemaChange::TableAlterEventFlag(t, false), ..] if *t ==
table_id)` for a call to the shared
`check_table_event_flag_altered` helper (addresses both the
"assert_eq is simpler" suggestion and the round-2 request that the
helper live in `crates/datastore` and be used from `update.rs`).
Validation:
- `cargo test -p spacetimedb-datastore --features test --lib
test_alter_table_event_flag` — 4/4 pass
- `cargo test -p spacetimedb-core --lib change_event_flag` — 2/2 pass
- `cargo clippy -p spacetimedb-datastore --features test --tests` — clean
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
New commit
Validated: datastore event-flag tests 4/4 pass, core |
Summary
Currently, toggling
#[spacetimedb::table(event)]on an existing tablefails auto-migration with
AutoMigrateError::ChangeTableEventFlagandforces a manual migration. This PR allows the flip in either direction
as a live auto-migration step when the table has zero committed
rows. Non-empty tables fail with an actionable error telling the user
to clear the table first. Clients are disconnected on the flip because
the change is observable to subscribers (event tables have no committed
state; v2 subscribers see a different message variant; v1 subscribers
stop seeing updates).
Changes
crates/datastore/src/locking_tx_datastore/tx_state.rs: newPendingSchemaChange::TableAlterEventFlag(TableId, bool)variantstoring the old
is_eventvalue for rollback.crates/datastore/src/locking_tx_datastore/committed_state.rs:rollback branch restoring the old value on the live schema.
crates/datastore/src/locking_tx_datastore/mut_tx.rs: newalter_table_event_flag(table_id, is_event)mirroringalter_table_access— dual-writes to the tx + commit table schemasvia
with_mut_schema_and_clone, and tost_event_tableviainsert_via_serialize_bsatn/delete_col_eq. Idempotent no-opearly-returns before pushing a pending change. New
delete_st_event_table_rowhelper.crates/datastore/src/locking_tx_datastore/replay.rs: new hooks onST_EVENT_TABLE_IDinsert/delete that flipis_eventon thereferenced user-table's cached schema, mirroring
reschema_table_for_st_table_update. This is load-bearing for coldreplay across the flip — without it, a snapshot predating the flip
plus commitlog replay would leave the cached schema stale, and
post-flip inserts would incorrectly land in committed state.
crates/core/src/db/relational_db.rs: thinalter_table_event_flagwrapper analogous toalter_table_access.crates/schema/src/auto_migrate.rs:event_okerror branchreplaced with
AutoMigrateStep::ChangeEventFlag+plan.ensure_disconnect_all_users(). Removed the now-unusedAutoMigrateError::ChangeTableEventFlagvariant. Replacedtest_change_event_flag_rejectedwith three new tests asserting theplan shape in both directions and confirming no orphan sub-object
steps.
crates/schema/src/auto_migrate/formatter.rs/termcolor_formatter.rs: newformat_change_event_flag+EventFlagChangeInfomirroringformat_change_access.crates/core/src/db/update.rs: newChangeEventFlaghandler withan O(1) row-count precheck via
table_row_count_mutbefore anymutation. Fails with a clear message if the table has data.
Safety
(
st_event_tablerow, tx table schema, commit table schema) run inthe same
MutTx. No window for concurrent inserts between check andflip.
TableAlterEventFlagstores the pre-flip flag value sofailed transactions revert
is_eventon the live schema via theexisting
rollback_pending_schema_changepath. Idempotent flips donot push a pending change and thus require no rollback work.
st_event_tablereschema hook is thepiece with no prior analog.
st_event_tablewas already in thecommitlog, but the existing reschema path only covered
st_tableandst_column, so flippingis_eventmid-life would have been invisibleto cold replay. Added hooks in both
replay_insertandreplay_delete_by_relcall the same helper, reusing the existingSelf::read_table_id(row)pattern (no new unsafe code).eventchanges what subscribers see —v1 subscribers stop receiving updates (event tables aren't in default
subscriptions); v2 subscribers receive a different message variant
(
TableUpdateRows::EventTable).ensure_disconnect_all_usersforcesreconnection so clients observe a consistent state.
Example error output
Test plan
cargo test -p spacetimedb-datastore --features test— 87 pass,including four new
alter_table_event_flag_*tests covering bothdirections, rollback, and the idempotent no-op case
cargo test -p spacetimedb-schema— 103 pass, including three newplan-shape tests (both directions + "no orphan sub-object steps")
cargo test -p spacetimedb-core— 192 pass, including two newend-to-end update-execution tests (empty succeeds, non-empty fails)
cargo clippy -p spacetimedb-datastore -p spacetimedb-schema -p spacetimedb-core --testscleaneventflag on an empty user table viaspacetime publish; clientsdisconnected as expected, subsequent inserts correctly routed to the
commitlog only.