-
Notifications
You must be signed in to change notification settings - Fork 1k
feat: Allow toggling event flag during auto-migration for empty tables #4875
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -282,6 +282,27 @@ fn auto_migrate_database( | |
| let table_def = plan.new.stored_in_table_def(&table_name.clone().into()).unwrap(); | ||
| stdb.alter_table_access(tx, table_name, table_def.table_access.into())?; | ||
| } | ||
| spacetimedb_schema::auto_migrate::AutoMigrateStep::ChangeEventFlag(table_name) => { | ||
| let table_def: &TableDef = plan.new.expect_lookup(table_name); | ||
| let table_id = stdb | ||
| .table_id_from_name_mut(tx, table_name)? | ||
| .expect("ChangeEventFlag references a table that should exist"); | ||
|
|
||
| // Pre-validate: flipping is only safe when the table has no committed rows. | ||
| if stdb.table_row_count_mut(tx, table_id).unwrap_or(0) > 0 { | ||
| anyhow::bail!( | ||
| "Cannot change `event` flag on table `{table_name}`: table contains data. \ | ||
| Clear the table's rows (e.g. via a reducer) before toggling the `event` annotation." | ||
| ); | ||
| } | ||
|
|
||
| log!( | ||
| logger, | ||
| "Changing `event` flag on table `{table_name}` to `{}`", | ||
| table_def.is_event | ||
| ); | ||
| stdb.alter_table_event_flag(tx, table_name, table_def.is_event)?; | ||
| } | ||
| spacetimedb_schema::auto_migrate::AutoMigrateStep::ChangePrimaryKey(table_name) => { | ||
| let table_def = plan.new.stored_in_table_def(&table_name.clone().into()).unwrap(); | ||
| log!(logger, "Changing primary key for table `{table_name}`"); | ||
|
|
@@ -339,6 +360,8 @@ mod test { | |
| db::relational_db::tests_utils::{begin_mut_tx, insert, TestDB}, | ||
| host::module_host::create_table_from_def, | ||
| }; | ||
| use pretty_assertions::assert_matches; | ||
| use spacetimedb_datastore::locking_tx_datastore::test_helpers::assert_is_event_state; | ||
| use spacetimedb_datastore::locking_tx_datastore::PendingSchemaChange; | ||
| use spacetimedb_lib::db::raw_def::v9::{btree, RawModuleDefV9Builder, TableAccess}; | ||
| use spacetimedb_sats::{product, AlgebraicType, AlgebraicType::U64}; | ||
|
|
@@ -580,4 +603,89 @@ mod test { | |
| ); | ||
| Ok(()) | ||
| } | ||
|
|
||
| /// Build a minimal v10 module with a single user table `events` whose | ||
| /// `is_event` flag matches `is_event`. | ||
| fn single_event_table_module_v10(is_event: bool) -> ModuleDef { | ||
| use spacetimedb_lib::db::raw_def::v10::RawModuleDefV10Builder; | ||
|
|
||
| let mut builder = RawModuleDefV10Builder::new(); | ||
| builder | ||
| .build_table_with_new_type("events", [("id", U64)], true) | ||
| .with_event(is_event) | ||
| .finish(); | ||
| builder | ||
| .finish() | ||
| .try_into() | ||
| .expect("should be a valid v10 module definition") | ||
| } | ||
|
|
||
| /// Create a non-event `events` table from the schema of `single_event_table_module_v10(false)` | ||
| /// in a fresh tx, commit it, and return the `TableId`. Leaves the table empty. | ||
| fn setup_events_table(stdb: &TestDB, module: &ModuleDef) -> anyhow::Result<TableId> { | ||
| let mut tx = begin_mut_tx(stdb); | ||
| for def in module.tables() { | ||
| create_table_from_def(stdb, &mut tx, module, def)?; | ||
| } | ||
| let table_id = stdb | ||
| .table_id_from_name_mut(&tx, "events")? | ||
| .expect("table should exist"); | ||
| stdb.commit_tx(tx)?; | ||
| Ok(table_id) | ||
| } | ||
|
|
||
| #[test] | ||
| fn change_event_flag_empty_table_succeeds() -> anyhow::Result<()> { | ||
| let auth_ctx = AuthCtx::for_testing(); | ||
| let stdb = TestDB::durable()?; | ||
|
|
||
| let old = single_event_table_module_v10(false); | ||
| let new = single_event_table_module_v10(true); | ||
| let table_id = setup_events_table(&stdb, &old)?; | ||
|
|
||
| let mut tx = begin_mut_tx(&stdb); | ||
| assert_is_event_state(&tx, table_id, false); | ||
|
|
||
| let plan = ponder_migrate(&old, &new)?; | ||
| let res = update_database(&stdb, &mut tx, auth_ctx, plan, &TestLogger)?; | ||
|
|
||
| assert!( | ||
| matches!(res, UpdateResult::RequiresClientDisconnect), | ||
| "flipping the `event` flag should disconnect clients" | ||
| ); | ||
| assert_is_event_state(&tx, table_id, true); | ||
| assert_matches!( | ||
| tx.pending_schema_changes(), | ||
| [PendingSchemaChange::TableAlterEventFlag(t, false), ..] if *t == table_id | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can use |
||
| ); | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[test] | ||
| fn change_event_flag_nonempty_table_fails() -> anyhow::Result<()> { | ||
| let auth_ctx = AuthCtx::for_testing(); | ||
| let stdb = TestDB::durable()?; | ||
|
|
||
| let old = single_event_table_module_v10(false); | ||
| let new = single_event_table_module_v10(true); | ||
| let table_id = setup_events_table(&stdb, &old)?; | ||
|
|
||
| // Insert a row in a separate tx so the pre-flip table state is committed. | ||
| let mut tx = begin_mut_tx(&stdb); | ||
| insert(&stdb, &mut tx, table_id, &product![42u64])?; | ||
| stdb.commit_tx(tx)?; | ||
|
|
||
| let mut tx = begin_mut_tx(&stdb); | ||
| let plan = ponder_migrate(&old, &new)?; | ||
| let err = update_database(&stdb, &mut tx, auth_ctx, plan, &TestLogger) | ||
| .err() | ||
| .expect("flipping `is_event` on a non-empty table should fail"); | ||
| assert!( | ||
| err.to_string().contains("contains data"), | ||
| "error should mention that the table contains data, got: {err}" | ||
| ); | ||
| assert_is_event_state(&tx, table_id, false); | ||
| assert_eq!(tx.pending_schema_changes(), []); | ||
| Ok(()) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -280,6 +280,14 @@ impl Locking { | |
| tx.alter_table_access(table_id, access) | ||
| } | ||
|
|
||
| pub fn alter_table_event_flag_mut_tx(&self, tx: &mut MutTxId, name: &str, is_event: bool) -> Result<()> { | ||
| let table_id = self | ||
| .table_id_from_name_mut_tx(tx, name)? | ||
| .ok_or_else(|| TableError::NotFound(name.into()))?; | ||
|
|
||
| tx.alter_table_event_flag(table_id, is_event) | ||
| } | ||
|
|
||
| pub fn alter_table_primary_key_mut_tx( | ||
| &self, | ||
| tx: &mut MutTxId, | ||
|
|
@@ -3141,6 +3149,105 @@ pub(crate) mod tests { | |
| Ok(()) | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_alter_table_event_flag_non_event_to_event() -> ResultTest<()> { | ||
|
Centril marked this conversation as resolved.
|
||
| use crate::locking_tx_datastore::test_helpers::check_table_event_flag_altered; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could be imported once for the test module instead of in each function. |
||
|
|
||
| let (datastore, tx, table_id) = setup_table()?; | ||
| commit(&datastore, tx)?; | ||
|
|
||
| let mut tx = begin_mut_tx(&datastore); | ||
| check_table_event_flag_altered(&datastore, &tx, table_id, false); | ||
|
|
||
| tx.alter_table_event_flag(table_id, true)?; | ||
| assert_matches!( | ||
| tx.pending_schema_changes(), | ||
| &[PendingSchemaChange::TableAlterEventFlag(t, false)] if t == table_id | ||
| ); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Repeated 3x; Let's make a function out of this:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. By this, I meant a function that checks |
||
| check_table_event_flag_altered(&datastore, &tx, table_id, true); | ||
|
|
||
| let tx_data = commit(&datastore, tx)?; | ||
| // Flipping to event inserts one row into `st_event_table` | ||
| // and does not touch the user table's row data. | ||
| assert_eq!( | ||
| tx_data.inserts_for_table(ST_EVENT_TABLE_ID).map(<[_]>::len), | ||
| Some(1), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please assert the contents of the row too. |
||
| ); | ||
| assert_eq!(tx_data.inserts_for_table(table_id), None); | ||
| assert_eq!(tx_data.deletes_for_table(table_id), None); | ||
|
|
||
| let tx = begin_mut_tx(&datastore); | ||
| check_table_event_flag_altered(&datastore, &tx, table_id, true); | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_alter_table_event_flag_event_to_non_event() -> ResultTest<()> { | ||
| use crate::locking_tx_datastore::test_helpers::check_table_event_flag_altered; | ||
|
|
||
| let (datastore, tx, table_id) = setup_event_table()?; | ||
| commit(&datastore, tx)?; | ||
|
|
||
| let mut tx = begin_mut_tx(&datastore); | ||
| check_table_event_flag_altered(&datastore, &tx, table_id, true); | ||
|
|
||
| tx.alter_table_event_flag(table_id, false)?; | ||
| assert_matches!( | ||
| tx.pending_schema_changes(), | ||
| &[PendingSchemaChange::TableAlterEventFlag(t, true)] if t == table_id | ||
| ); | ||
| check_table_event_flag_altered(&datastore, &tx, table_id, false); | ||
|
|
||
| let tx_data = commit(&datastore, tx)?; | ||
| // Flipping away from event deletes one row from `st_event_table` | ||
| // and does not touch the user table's row data. | ||
| assert_eq!( | ||
| tx_data.deletes_for_table(ST_EVENT_TABLE_ID).map(<[_]>::len), | ||
| Some(1), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please assert the row contents here too. |
||
| ); | ||
| assert_eq!(tx_data.inserts_for_table(table_id), None); | ||
| assert_eq!(tx_data.deletes_for_table(table_id), None); | ||
|
|
||
| let tx = begin_mut_tx(&datastore); | ||
| check_table_event_flag_altered(&datastore, &tx, table_id, false); | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_alter_table_event_flag_rollback_reverts_live_state_and_st_event_table() -> ResultTest<()> { | ||
| use crate::locking_tx_datastore::test_helpers::check_table_event_flag_altered; | ||
|
|
||
| let (datastore, tx, table_id) = setup_table()?; | ||
| commit(&datastore, tx)?; | ||
|
|
||
| let mut tx = begin_mut_tx(&datastore); | ||
| check_table_event_flag_altered(&datastore, &tx, table_id, false); | ||
|
|
||
| tx.alter_table_event_flag(table_id, true)?; | ||
| assert_matches!( | ||
| tx.pending_schema_changes(), | ||
| &[PendingSchemaChange::TableAlterEventFlag(t, false)] if t == table_id | ||
| ); | ||
| check_table_event_flag_altered(&datastore, &tx, table_id, true); | ||
| let _ = datastore.rollback_mut_tx(tx); | ||
|
|
||
| let tx = begin_mut_tx(&datastore); | ||
| assert_eq!(tx.pending_schema_changes(), []); | ||
| check_table_event_flag_altered(&datastore, &tx, table_id, false); | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_alter_table_event_flag_idempotent_no_pending_change() -> ResultTest<()> { | ||
| let (datastore, tx, table_id) = setup_table()?; | ||
| commit(&datastore, tx)?; | ||
|
|
||
| let mut tx = begin_mut_tx(&datastore); | ||
| tx.alter_table_event_flag(table_id, false)?; | ||
| assert_eq!(tx.pending_schema_changes(), []); | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_alter_table_row_type_rejects_some_bad_changes() -> ResultTest<()> { | ||
| let datastore = get_datastore()?; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.