diff --git a/crates/datastore/src/locking_tx_datastore/committed_state.rs b/crates/datastore/src/locking_tx_datastore/committed_state.rs index 46e2e60131f..0ce629524c4 100644 --- a/crates/datastore/src/locking_tx_datastore/committed_state.rs +++ b/crates/datastore/src/locking_tx_datastore/committed_state.rs @@ -800,6 +800,32 @@ impl CommittedState { table.with_mut_schema(|s| s.remove_index(index_id)); self.index_id_map.remove(&index_id); } + // An index alias/source-name changed. Change it back. + IndexAlterSourceName(table_id, index_id, old_alias) => { + let table = self.tables.get_mut(&table_id)?; + let mut index_schema = table + .get_schema() + .indexes + .iter() + .find(|x| x.index_id == index_id)? + .clone(); + index_schema.alias = old_alias; + table.with_mut_schema(|s| s.update_index(index_schema)); + } + // A table accessor name alias changed. Change it back. + TableAlterAccessorName(table_id, old_alias) => { + let table = self.tables.get_mut(&table_id)?; + table.with_mut_schema(|s| s.alias = old_alias); + } + // A column accessor name alias changed. Change it back. + ColumnAlterAccessorName(table_id, col_id, old_alias) => { + let table = self.tables.get_mut(&table_id)?; + table.with_mut_schema(|s| { + if let Some(col) = s.columns.iter_mut().find(|x| x.col_pos == col_id) { + col.alias = old_alias; + } + }); + } // A table was removed. Add it back. TableRemoved(table_id, table) => { let is_view_table = table.schema.is_view(); diff --git a/crates/datastore/src/locking_tx_datastore/datastore.rs b/crates/datastore/src/locking_tx_datastore/datastore.rs index ffc662f3ce5..1284740c062 100644 --- a/crates/datastore/src/locking_tx_datastore/datastore.rs +++ b/crates/datastore/src/locking_tx_datastore/datastore.rs @@ -309,6 +309,34 @@ impl Locking { tx.alter_table_primary_key(table_id, primary_key) } + pub fn alter_index_source_name_mut_tx( + &self, + tx: &mut MutTxId, + index_id: IndexId, + source_name: spacetimedb_sats::raw_identifier::RawIdentifier, + ) -> Result<()> { + tx.alter_index_source_name(index_id, source_name) + } + + pub fn alter_table_accessor_name_mut_tx( + &self, + tx: &mut MutTxId, + table_id: TableId, + new_alias: spacetimedb_schema::identifier::Identifier, + ) -> Result<()> { + tx.alter_table_accessor_name(table_id, new_alias) + } + + pub fn alter_column_accessor_name_mut_tx( + &self, + tx: &mut MutTxId, + table_id: TableId, + col_id: ColId, + new_alias: spacetimedb_schema::identifier::Identifier, + ) -> Result<()> { + tx.alter_column_accessor_name(table_id, col_id, new_alias) + } + pub fn alter_table_row_type_mut_tx( &self, tx: &mut MutTxId, @@ -551,6 +579,34 @@ impl MutTxDatastore for Locking { tx.drop_index(index_id) } + fn alter_index_source_name_mut_tx( + &self, + tx: &mut Self::MutTx, + index_id: IndexId, + source_name: spacetimedb_sats::raw_identifier::RawIdentifier, + ) -> Result<()> { + tx.alter_index_source_name(index_id, source_name) + } + + fn alter_table_accessor_name_mut_tx( + &self, + tx: &mut Self::MutTx, + table_id: TableId, + new_alias: spacetimedb_schema::identifier::Identifier, + ) -> Result<()> { + tx.alter_table_accessor_name(table_id, new_alias) + } + + fn alter_column_accessor_name_mut_tx( + &self, + tx: &mut Self::MutTx, + table_id: TableId, + col_id: ColId, + new_alias: spacetimedb_schema::identifier::Identifier, + ) -> Result<()> { + tx.alter_column_accessor_name(table_id, col_id, new_alias) + } + fn index_id_from_name_mut_tx(&self, tx: &Self::MutTx, index_name: &str) -> Result> { tx.index_id_from_name_or_alias(index_name) } diff --git a/crates/datastore/src/locking_tx_datastore/mut_tx.rs b/crates/datastore/src/locking_tx_datastore/mut_tx.rs index d230c40c7fe..10912768147 100644 --- a/crates/datastore/src/locking_tx_datastore/mut_tx.rs +++ b/crates/datastore/src/locking_tx_datastore/mut_tx.rs @@ -1349,6 +1349,126 @@ impl MutTxId { Ok(()) } + /// Change the runtime source-name alias of the index identified by `index_id`. + pub(crate) fn alter_index_source_name( + &mut self, + index_id: IndexId, + source_name: spacetimedb_sats::raw_identifier::RawIdentifier, + ) -> Result<()> { + let st_index_ref = self + .iter_by_col_eq(ST_INDEX_ID, StIndexFields::IndexId, &index_id.into())? + .next() + .ok_or_else(|| TableError::IdNotFound(SystemTable::st_index, index_id.into()))?; + let st_index_row = StIndexRow::try_from(st_index_ref)?; + let table_id = st_index_row.table_id; + + let old_alias = self + .find_st_index_accessor_row_by_index_name(st_index_row.index_name.as_ref())? + .map(|row| row.accessor_name); + + if old_alias.as_ref() == Some(&source_name) { + return Ok(()); + } + + let ((tx_table, ..), (commit_table, ..)) = self.get_or_create_insert_table_mut(table_id)?; + let mut index_schema = tx_table + .get_schema() + .indexes + .iter() + .find(|index| index.index_id == index_id) + .cloned() + .ok_or_else(|| TableError::IdNotFound(SystemTable::st_index, index_id.into()))?; + index_schema.alias = Some(source_name.clone()); + tx_table.with_mut_schema_and_clone(commit_table, |s| s.update_index(index_schema)); + + self.drop_st_index_accessor(&st_index_row.index_name)?; + self.insert_st_index_accessor(&st_index_row.index_name, Some(&source_name))?; + + self.push_schema_change(PendingSchemaChange::IndexAlterSourceName(table_id, index_id, old_alias)); + + Ok(()) + } + + /// Change the runtime accessor name alias of the table identified by `table_id`. + pub(crate) fn alter_table_accessor_name( + &mut self, + table_id: TableId, + new_alias: Identifier, + ) -> Result<()> { + let table_name = self.find_st_table_row(table_id)?.table_name; + + let old_alias = self + .iter_by_col_eq(ST_TABLE_ACCESSOR_ID, StTableAccessorFields::TableName, &table_name.as_ref().into())? + .next() + .map(|row| StTableAccessorRow::try_from(row).ok()) + .flatten() + .map(|row| row.accessor_name); + + if old_alias.as_ref() == Some(&new_alias) { + return Ok(()); + } + + let ((tx_table, ..), (commit_table, ..)) = self.get_or_create_insert_table_mut(table_id)?; + tx_table.with_mut_schema_and_clone(commit_table, |s| s.alias = Some(new_alias.clone())); + + self.drop_st_table_accessor(&table_name)?; + self.insert_st_table_accessor(&table_name, Some(&new_alias))?; + + self.push_schema_change(PendingSchemaChange::TableAlterAccessorName(table_id, old_alias)); + + Ok(()) + } + + /// Change the runtime accessor name alias of the column identified by `table_id` and `col_id`. + pub(crate) fn alter_column_accessor_name( + &mut self, + table_id: TableId, + col_id: ColId, + new_alias: Identifier, + ) -> Result<()> { + let table_name = self.find_st_table_row(table_id)?.table_name; + + // Read old column info from the current schema + let col_info: Vec<(Identifier, Option, ColId)> = self + .schema_for_table(table_id)? + .columns + .iter() + .map(|c| (c.col_name.clone(), c.alias.clone(), c.col_pos)) + .collect(); + + let old_alias = col_info + .iter() + .find(|(_, _, pos)| *pos == col_id) + .and_then(|(_, alias, _)| alias.clone()); + + if old_alias.as_ref() == Some(&new_alias) { + return Ok(()); + } + + // Update system tables: drop and re-insert all column accessor rows for this table + self.drop_st_column_accessor(&table_name)?; + for (col_name, alias, pos) in &col_info { + let name = if *pos == col_id { + &new_alias + } else { + alias.as_ref().unwrap_or(col_name) + }; + self.insert_st_column_accessor(&table_name, col_name, Some(name))?; + } + + // Update in-memory schema + let ((tx_table, ..), (commit_table, ..)) = self.get_or_create_insert_table_mut(table_id)?; + tx_table.with_mut_schema_and_clone(commit_table, |s| { + if let Some(col) = s.columns.iter_mut().find(|c| c.col_pos == col_id) { + col.alias = Some(new_alias); + } + }); + + self.push_schema_change(PendingSchemaChange::ColumnAlterAccessorName(table_id, col_id, old_alias)); + + Ok(()) + } + /// Change the row type of the table identified by `table_id`. /// /// In practice, this should not error, diff --git a/crates/datastore/src/locking_tx_datastore/tx_state.rs b/crates/datastore/src/locking_tx_datastore/tx_state.rs index 6b47948d8a3..58037f30275 100644 --- a/crates/datastore/src/locking_tx_datastore/tx_state.rs +++ b/crates/datastore/src/locking_tx_datastore/tx_state.rs @@ -1,8 +1,9 @@ use super::{delete_table::DeleteTable, sequence::Sequence}; use spacetimedb_data_structures::map::IntMap; use spacetimedb_lib::db::auth::StAccess; -use spacetimedb_primitives::{ColList, ConstraintId, IndexId, SequenceId, TableId}; +use spacetimedb_primitives::{ColId, ColList, ConstraintId, IndexId, SequenceId, TableId}; use spacetimedb_sats::memory_usage::MemoryUsage; +use spacetimedb_schema::identifier::Identifier; use spacetimedb_schema::schema::{ColumnSchema, ConstraintSchema, IndexSchema, SequenceSchema}; use spacetimedb_table::{ blob_store::{BlobStore, HashMapBlobStore}, @@ -111,6 +112,19 @@ pub enum PendingSchemaChange { /// If adding this index caused the pointer map to be removed, /// it will be present here. IndexAdded(TableId, IndexId, Option), + /// The source-name alias of the index with [`IndexId`] changed. + /// The old alias is stored for rollback. + IndexAlterSourceName( + TableId, + IndexId, + Option, + ), + /// The accessor name alias of the table with [`TableId`] changed. + /// The old alias is stored for rollback. + TableAlterAccessorName(TableId, Option), + /// The accessor name alias of the column with [`ColId`] in the table with [`TableId`] changed. + /// The old alias is stored for rollback. + ColumnAlterAccessorName(TableId, ColId, Option), /// The [`Table`] with [`TableId`] was removed. TableRemoved(TableId, Table), /// The table with [`TableId`] was added. @@ -153,6 +167,9 @@ impl MemoryUsage for PendingSchemaChange { Self::IndexAdded(table_id, index_id, pointer_map) => { table_id.heap_usage() + index_id.heap_usage() + pointer_map.heap_usage() } + Self::IndexAlterSourceName(table_id, index_id, alias) => { + table_id.heap_usage() + index_id.heap_usage() + alias.heap_usage() + } Self::TableRemoved(table_id, table) => table_id.heap_usage() + table.heap_usage(), Self::TableAdded(table_id) => table_id.heap_usage(), Self::TableAlterAccess(table_id, st_access) => table_id.heap_usage() + st_access.heap_usage(), @@ -168,6 +185,14 @@ impl MemoryUsage for PendingSchemaChange { table_id.heap_usage() + sequence.heap_usage() + sequence_schema.heap_usage() } Self::SequenceAdded(table_id, sequence_id) => table_id.heap_usage() + sequence_id.heap_usage(), + Self::TableAlterAccessorName(table_id, alias) => { + table_id.heap_usage() + alias.as_ref().map(|a| a.as_raw().heap_usage()).unwrap_or(0) + } + Self::ColumnAlterAccessorName(table_id, col_id, alias) => { + table_id.heap_usage() + + col_id.heap_usage() + + alias.as_ref().map(|a| a.as_raw().heap_usage()).unwrap_or(0) + } Self::ReschemaEventTable(table_id, column_schemas) => table_id.heap_usage() + column_schemas.heap_usage(), } } diff --git a/crates/datastore/src/traits.rs b/crates/datastore/src/traits.rs index 22e12b98271..1c9c25ede7b 100644 --- a/crates/datastore/src/traits.rs +++ b/crates/datastore/src/traits.rs @@ -627,6 +627,25 @@ pub trait MutTxDatastore: TxDatastore + MutTx { fn create_index_mut_tx(&self, tx: &mut Self::MutTx, index_schema: IndexSchema, is_unique: bool) -> Result; fn drop_index_mut_tx(&self, tx: &mut Self::MutTx, index_id: IndexId) -> Result<()>; + fn alter_index_source_name_mut_tx( + &self, + tx: &mut Self::MutTx, + index_id: IndexId, + source_name: spacetimedb_sats::raw_identifier::RawIdentifier, + ) -> Result<()>; + fn alter_table_accessor_name_mut_tx( + &self, + tx: &mut Self::MutTx, + table_id: TableId, + new_alias: spacetimedb_schema::identifier::Identifier, + ) -> Result<()>; + fn alter_column_accessor_name_mut_tx( + &self, + tx: &mut Self::MutTx, + table_id: TableId, + col_id: ColId, + new_alias: spacetimedb_schema::identifier::Identifier, + ) -> Result<()>; fn index_id_from_name_mut_tx(&self, tx: &Self::MutTx, index_name: &str) -> super::Result>; // TODO: Index data diff --git a/crates/engine/src/relational_db.rs b/crates/engine/src/relational_db.rs index 7edb9fe20fc..3f0700818c1 100644 --- a/crates/engine/src/relational_db.rs +++ b/crates/engine/src/relational_db.rs @@ -1058,6 +1058,34 @@ impl RelationalDB { Ok(self.inner.alter_table_primary_key_mut_tx(tx, name, primary_key)?) } + pub(crate) fn alter_index_source_name( + &self, + tx: &mut MutTx, + index_id: IndexId, + source_name: spacetimedb_sats::raw_identifier::RawIdentifier, + ) -> Result<(), DBError> { + Ok(self.inner.alter_index_source_name_mut_tx(tx, index_id, source_name)?) + } + + pub(crate) fn alter_table_accessor_name( + &self, + tx: &mut MutTx, + table_id: TableId, + new_alias: spacetimedb_schema::identifier::Identifier, + ) -> Result<(), DBError> { + Ok(self.inner.alter_table_accessor_name_mut_tx(tx, table_id, new_alias)?) + } + + pub(crate) fn alter_column_accessor_name( + &self, + tx: &mut MutTx, + table_id: TableId, + col_id: ColId, + new_alias: spacetimedb_schema::identifier::Identifier, + ) -> Result<(), DBError> { + Ok(self.inner.alter_column_accessor_name_mut_tx(tx, table_id, col_id, new_alias)?) + } + pub(crate) fn alter_table_row_type( &self, tx: &mut MutTx, diff --git a/crates/engine/src/update.rs b/crates/engine/src/update.rs index 98094a69947..4f6d899f470 100644 --- a/crates/engine/src/update.rs +++ b/crates/engine/src/update.rs @@ -293,11 +293,74 @@ fn auto_migrate_database( .indexes .iter() .find(|index| index.index_name[..] == index_name[..]) - .unwrap(); + .ok_or_else(|| anyhow::anyhow!("Index `{index_name}` not found in table `{}`", table_def.name))?; log!(logger, "Dropping index `{}` on table `{}`", index_name, table_def.name); stdb.drop_index(tx, index_schema.index_id)?; } + spacetimedb_schema::auto_migrate::AutoMigrateStep::ChangeTableAccessorName(table_name) => { + let new_table_def: &spacetimedb_schema::def::TableDef = plan.new.expect_lookup(table_name); + + let table_id = stdb.table_id_from_name_mut(tx, table_name)?.unwrap(); + + log!( + logger, + "Changing table accessor name for `{}` to `{}`", + table_name, + new_table_def.accessor_name, + ); + stdb.alter_table_accessor_name(tx, table_id, new_table_def.accessor_name.clone())?; + } + spacetimedb_schema::auto_migrate::AutoMigrateStep::ChangeColumnAccessorName(table_name, col_name) => { + let new_table_def: &spacetimedb_schema::def::TableDef = plan.new.expect_lookup(table_name); + let new_col_def = new_table_def + .columns + .iter() + .find(|col| &col.name == col_name) + .ok_or_else(|| anyhow::anyhow!("Column `{col_name}` not found in table `{table_name}`"))?; + + let table_id = stdb.table_id_from_name_mut(tx, table_name)?.unwrap(); + let table_schema = stdb.schema_for_table_mut(tx, table_id)?; + let col_schema = table_schema + .columns + .iter() + .find(|col| &col.col_name == col_name) + .ok_or_else(|| anyhow::anyhow!("Column `{col_name}` not found in table `{table_name}`"))?; + + log!( + logger, + "Changing column accessor name for `{}`.`{}` to `{}`", + table_name, + col_name, + new_col_def.accessor_name, + ); + stdb.alter_column_accessor_name(tx, table_id, col_schema.col_pos, new_col_def.accessor_name.clone())?; + } + spacetimedb_schema::auto_migrate::AutoMigrateStep::ChangeIndexSourceName(index_name) => { + let old_table_def = plan.old.stored_in_table_def(index_name).unwrap(); + let new_table_def = plan.new.stored_in_table_def(index_name).unwrap(); + let new_index_def = new_table_def.indexes.get(index_name).unwrap(); + + let table_id = stdb.table_id_from_name_mut(tx, &old_table_def.name)?.unwrap(); + let table_schema = stdb.schema_for_table_mut(tx, table_id)?; + let index_schema = table_schema + .indexes + .iter() + .find(|index| index.index_name[..] == index_name[..]) + .ok_or_else(|| { + anyhow::anyhow!("Index `{index_name}` not found in table `{}`", old_table_def.name) + })?; + + log!( + logger, + "Changing index source name for `{}` on table `{}` from `{}` to `{}`", + index_name, + old_table_def.name, + index_schema.alias.as_deref().unwrap_or(""), + new_index_def.source_name, + ); + stdb.alter_index_source_name(tx, index_schema.index_id, new_index_def.source_name.clone())?; + } spacetimedb_schema::auto_migrate::AutoMigrateStep::RemoveConstraint(constraint_name) => { let table_def = plan.old.stored_in_table_def(constraint_name).unwrap(); @@ -472,13 +535,13 @@ mod test { use spacetimedb_datastore::system_tables::ST_EVENT_TABLE_ID; use spacetimedb_lib::{ db::raw_def::{ - v10::RawModuleDefV10Builder, + v10::{ExplicitNames, RawModuleDefV10Builder}, v9::{btree, RawIndexAlgorithm, RawModuleDefV9Builder, TableAccess}, }, Identity, }; - use spacetimedb_sats::{product, AlgebraicType, AlgebraicType::U64, ProductType}; - use spacetimedb_schema::{auto_migrate::ponder_migrate, def::ModuleDef}; + use spacetimedb_sats::{product, raw_identifier::RawIdentifier, AlgebraicType, AlgebraicType::U64, ProductType}; + use spacetimedb_schema::auto_migrate::ponder_migrate; struct TestLogger; impl UpdateLogger for TestLogger { @@ -560,6 +623,110 @@ mod test { Ok(()) } + #[test] + fn update_db_change_index_source_name_updates_lookup_and_persists() -> anyhow::Result<()> { + let auth_ctx = AuthCtx::for_testing(); + let stdb = TestDB::durable()?; + + fn module_def(table_source_name: &str, index_source_name: &str) -> ModuleDef { + let mut builder = RawModuleDefV10Builder::new(); + builder + .build_table_with_new_type( + table_source_name.to_owned(), + ProductType::from([("id", U64), ("emailAddress", AlgebraicType::String)]), + true, + ) + .with_access(TableAccess::Public) + .with_index(btree(1), index_source_name.to_owned(), "emailAddress") + .finish(); + + if table_source_name != "users" { + let mut explicit_names = ExplicitNames::default(); + explicit_names.insert_table(table_source_name.to_owned(), "users"); + builder.add_explicit_names(explicit_names); + } + + builder + .finish() + .try_into() + .expect("builder should create a valid database definition") + } + + let old_source_name = "users_emailAddress_idx_btree"; + let new_source_name = "appUsers_emailAddress_idx_btree"; + let old = module_def("users", old_source_name); + let new = module_def("appUsers", new_source_name); + + let mut tx = begin_mut_tx(&stdb); + for def in old.tables() { + create_table_from_def(&stdb, &mut tx, &old, def)?; + } + stdb.commit_tx(tx)?; + + let tx = begin_mut_tx(&stdb); + let table_id = stdb + .table_id_from_name_mut(&tx, "users")? + .expect("there should be a table named users"); + let table_schema = stdb.schema_for_table_mut(&tx, table_id)?; + let index_schema = table_schema + .indexes + .first() + .expect("there should be a single index") + .clone(); + let canonical_index_name = index_schema.index_name.to_string(); + let index_id = index_schema.index_id; + assert_eq!(stdb.index_id_from_name_mut(&tx, old_source_name)?, Some(index_id)); + assert_eq!(stdb.index_id_from_name_mut(&tx, new_source_name)?, None); + assert_eq!(stdb.index_id_from_name_mut(&tx, &canonical_index_name)?, Some(index_id)); + drop(tx); + + let MigratePlan::Auto(plan) = ponder_migrate(&old, &new)? else { + panic!("expected automatic migration"); + }; + let index_name = RawIdentifier::new(canonical_index_name.as_str()); + assert!( + plan.steps + .contains(&AutoMigrateStep::ChangeIndexSourceName(&index_name)), + "plan steps: {:?}", + plan.steps + ); + assert!( + !plan.steps.contains(&AutoMigrateStep::RemoveIndex(&index_name)), + "plan steps: {:?}", + plan.steps + ); + assert!( + !plan.steps.contains(&AutoMigrateStep::AddIndex(&index_name)), + "plan steps: {:?}", + plan.steps + ); + let mut tx = begin_mut_tx(&stdb); + let res = update_database(&stdb, &mut tx, auth_ctx, MigratePlan::Auto(plan), &TestLogger)?; + assert!(matches!(res, UpdateResult::Success)); + + assert_eq!(stdb.index_id_from_name_mut(&tx, old_source_name)?, None); + assert_eq!(stdb.index_id_from_name_mut(&tx, new_source_name)?, Some(index_id)); + assert_eq!(stdb.index_id_from_name_mut(&tx, &canonical_index_name)?, Some(index_id)); + assert!( + tx.pending_schema_changes().iter().any(|change| matches!( + change, + PendingSchemaChange::IndexAlterSourceName(tid, iid, Some(old_alias)) + if *tid == table_id && *iid == index_id && old_alias.as_ref() == old_source_name + )), + "pending schema changes: {:?}", + tx.pending_schema_changes() + ); + stdb.commit_tx(tx)?; + + let stdb = stdb.reopen()?; + let tx = begin_mut_tx(&stdb); + assert_eq!(stdb.index_id_from_name_mut(&tx, old_source_name)?, None); + assert_eq!(stdb.index_id_from_name_mut(&tx, new_source_name)?, Some(index_id)); + assert_eq!(stdb.index_id_from_name_mut(&tx, &canonical_index_name)?, Some(index_id)); + + Ok(()) + } + /// Regression test for #3934: removing a primary key annotation and then /// re-publishing causes "Primary key mismatch" on the NEXT publish. #[test] diff --git a/crates/schema/src/auto_migrate.rs b/crates/schema/src/auto_migrate.rs index 63239de7ecc..1e5e38635c6 100644 --- a/crates/schema/src/auto_migrate.rs +++ b/crates/schema/src/auto_migrate.rs @@ -298,6 +298,17 @@ pub enum AutoMigrateStep<'def> { /// no `ChangeColumns` steps will be, for the same table. AddColumns(::Key<'def>), + /// Change the runtime source-name alias of an existing index. + ChangeIndexSourceName(::Key<'def>), + + /// Change the runtime accessor name alias of an existing table. + ChangeTableAccessorName(::Key<'def>), + /// Change the runtime accessor name alias of an existing column. + ChangeColumnAccessorName( + ::Key<'def>, + &'def Identifier, + ), + /// Add a table, including all indexes, constraints, and sequences. /// There will NOT be separate steps in the plan for adding indexes, constraints, and sequences. AddTable(::Key<'def>), @@ -712,6 +723,9 @@ fn auto_migrate_table<'def>(plan: &mut AutoMigratePlan<'def>, old: &'def TableDe if old.primary_key != new.primary_key { plan.steps.push(AutoMigrateStep::ChangePrimaryKey(key)); } + if old.accessor_name != new.accessor_name { + plan.steps.push(AutoMigrateStep::ChangeTableAccessorName(key)); + } if old.schedule != new.schedule { // Note: this handles the case where there's an altered ScheduleDef for some reason. if let Some(old_schedule) = old.schedule.as_ref() { @@ -804,6 +818,11 @@ fn auto_migrate_table<'def>(plan: &mut AutoMigratePlan<'def>, old: &'def TableDe .into()) }; + if old.accessor_name != new.accessor_name { + plan.steps + .push(AutoMigrateStep::ChangeColumnAccessorName(key, &old.name)); + } + (types_ok, positions_ok) .combine_errors() // `row_type_changed`, `column_added`, `event_schema_changed` @@ -1061,6 +1080,8 @@ fn auto_migrate_indexes( if old.algorithm != new.algorithm { plan.steps.push(AutoMigrateStep::RemoveIndex(old.key())); plan.steps.push(AutoMigrateStep::AddIndex(old.key())); + } else if old.source_name != new.source_name { + plan.steps.push(AutoMigrateStep::ChangeIndexSourceName(old.key())); } Ok(()) } @@ -2113,6 +2134,118 @@ mod tests { ); } + #[test] + fn migrate_index_with_changed_source_name() { + fn module_def(source_name: &str) -> ModuleDef { + create_module_def_v10(|builder| { + builder + .build_table_with_new_type( + "FruitBasket", + ProductType::from([("basket_id", AlgebraicType::U64), ("fruit_name", AlgebraicType::String)]), + true, + ) + .with_index(btree([0, 1]), source_name.to_owned(), "fruitNameIndex") + .finish(); + }) + } + + let old_def = module_def("OldBasketLookup"); + let new_def = module_def("NewBasketLookup"); + let index_name = RawIdentifier::new("fruit_basket_basket_id_fruit_name_idx_btree"); + + let plan = ponder_auto_migrate(&old_def, &new_def).expect("auto migration should succeed"); + let steps = &plan.steps[..]; + + assert!( + steps.contains(&AutoMigrateStep::ChangeIndexSourceName(&index_name)), + "steps: {steps:?}" + ); + assert!( + !steps.contains(&AutoMigrateStep::RemoveIndex(&index_name)), + "steps: {steps:?}" + ); + assert!( + !steps.contains(&AutoMigrateStep::AddIndex(&index_name)), + "steps: {steps:?}" + ); + } + + #[test] + fn migrate_table_with_changed_accessor_name() { + let old_def = create_module_def_v10(|builder| { + builder + .build_table_with_new_type( + "my_table", + ProductType::from([("id", AlgebraicType::U64)]), + true, + ) + .finish(); + }); + + let new_def = create_module_def_v10(|builder| { + builder + .build_table_with_new_type( + "renamed_table", + ProductType::from([("id", AlgebraicType::U64)]), + true, + ) + .finish(); + let mut explicit = ExplicitNames::default(); + explicit.insert_table("renamed_table", "my_table"); + builder.add_explicit_names(explicit); + }); + + let plan = ponder_auto_migrate(&old_def, &new_def).expect("auto migration should succeed"); + let steps = &plan.steps[..]; + let table_name = expect_identifier("my_table"); + + assert!( + steps.contains(&AutoMigrateStep::ChangeTableAccessorName(&table_name)), + "steps: {steps:?}" + ); + assert!( + !steps.contains(&AutoMigrateStep::RemoveTable(&table_name)), + "steps: {steps:?}" + ); + assert!( + !steps.contains(&AutoMigrateStep::AddTable(&table_name)), + "steps: {steps:?}" + ); + } + + #[test] + fn migrate_column_with_changed_accessor_name() { + let old_def = create_module_def_v10(|builder| { + builder + .build_table_with_new_type( + "my_table", + ProductType::from([("my_field", AlgebraicType::U64)]), + true, + ) + .finish(); + }); + + let new_def = create_module_def_v10(|builder| { + builder + .build_table_with_new_type( + "my_table", + ProductType::from([("myField", AlgebraicType::U64)]), + true, + ) + .finish(); + }); + + let plan = ponder_auto_migrate(&old_def, &new_def).expect("auto migration should succeed"); + let steps = &plan.steps[..]; + let table_name = expect_identifier("my_table"); + let col_name = expect_identifier("my_field"); + + assert!( + steps.contains(&AutoMigrateStep::ChangeColumnAccessorName(&table_name, &col_name)), + "steps: {steps:?}" + ); + } + #[test] fn migrate_view_disconnect_clients() { struct TestCase { diff --git a/crates/schema/src/auto_migrate/formatter.rs b/crates/schema/src/auto_migrate/formatter.rs index cd6c1042077..793a98a52ea 100644 --- a/crates/schema/src/auto_migrate/formatter.rs +++ b/crates/schema/src/auto_migrate/formatter.rs @@ -59,6 +59,17 @@ fn format_step( let index_info = extract_index_info(*index, plan.old)?; f.format_index(&index_info, Action::Removed) } + AutoMigrateStep::ChangeIndexSourceName(index) => { + let index_info = extract_index_info(*index, plan.new)?; + f.format_index(&index_info, Action::Changed) + } + AutoMigrateStep::ChangeTableAccessorName(table) => { + let table_info = extract_table_info(*table, plan)?; + f.format_change_table_accessor_name(table_info.name.as_ref()) + } + AutoMigrateStep::ChangeColumnAccessorName(table, col) => { + f.format_change_column_accessor_name(table, col) + } AutoMigrateStep::RemoveConstraint(constraint) => { let constraint_info = extract_constraint_info(*constraint, plan.old)?; f.format_constraint(&constraint_info, Action::Removed) @@ -177,6 +188,8 @@ pub trait MigrationFormatter { fn format_rls(&mut self, rls_info: &RlsInfo, action: Action) -> io::Result<()>; fn format_change_columns(&mut self, column_changes: &ColumnChanges) -> io::Result<()>; fn format_add_columns(&mut self, new_columns: &NewColumns) -> io::Result<()>; + fn format_change_table_accessor_name(&mut self, table_name: &str) -> io::Result<()>; + fn format_change_column_accessor_name(&mut self, table_name: &str, col_name: &str) -> io::Result<()>; fn format_disconnect_warning(&mut self) -> io::Result<()>; // TODO(format-event-table-reschema): I (pgoldman 2026-06-10) didn't have time to meaningfully format event table reschemas, // so for now we're just printing the table name. diff --git a/crates/schema/src/auto_migrate/termcolor_formatter.rs b/crates/schema/src/auto_migrate/termcolor_formatter.rs index 3f9764cfabf..2871f9a6265 100644 --- a/crates/schema/src/auto_migrate/termcolor_formatter.rs +++ b/crates/schema/src/auto_migrate/termcolor_formatter.rs @@ -417,6 +417,22 @@ impl MigrationFormatter for TermColorFormatter { Ok(()) } + fn format_change_table_accessor_name(&mut self, table_name: &str) -> io::Result<()> { + self.write_action_prefix(&Action::Changed)?; + self.buffer.write_all(b" table accessor name for ")?; + self.write_colored(table_name, Some(self.colors.table_name), true)?; + self.buffer.write_all(b"\n") + } + + fn format_change_column_accessor_name(&mut self, table_name: &str, col_name: &str) -> io::Result<()> { + self.write_action_prefix(&Action::Changed)?; + self.buffer.write_all(b" column accessor name for ")?; + self.write_colored(table_name, Some(self.colors.table_name), true)?; + self.buffer.write_all(b".")?; + self.write_colored(col_name, Some(self.colors.column_type), true)?; + self.buffer.write_all(b"\n") + } + fn format_disconnect_warning(&mut self) -> io::Result<()> { self.write_indent()?; self.write_with_background( diff --git a/crates/schema/tests/ensure_same_schema.rs b/crates/schema/tests/ensure_same_schema.rs index 7e5cb28f626..306fe2340b2 100644 --- a/crates/schema/tests/ensure_same_schema.rs +++ b/crates/schema/tests/ensure_same_schema.rs @@ -36,6 +36,9 @@ fn assert_identical_modules(module_name_prefix: &str, lang_name: &str, suffix: & | AutoMigrateStep::AddView(_) | AutoMigrateStep::RemoveView(_) | AutoMigrateStep::UpdateView(_) + | AutoMigrateStep::ChangeIndexSourceName(_) + | AutoMigrateStep::ChangeTableAccessorName(_) + | AutoMigrateStep::ChangeColumnAccessorName(_, _) ) }); diff --git a/crates/smoketests/tests/smoketests/typescript_index_source_name.rs b/crates/smoketests/tests/smoketests/typescript_index_source_name.rs index 416ef704faa..f3608ccd339 100644 --- a/crates/smoketests/tests/smoketests/typescript_index_source_name.rs +++ b/crates/smoketests/tests/smoketests/typescript_index_source_name.rs @@ -1,6 +1,6 @@ use spacetimedb_smoketests::{random_string, require_local_server, require_pnpm, ModuleLanguage, Smoketest}; -const TYPESCRIPT_MODULE_WITHOUT_NEW_COLUMNS: &str = r#"import { schema, table, t } from "spacetimedb/server"; +const TYPESCRIPT_MODULE_V1: &str = r#"import { schema, table, t } from "spacetimedb/server"; const AppUsers = table( { name: "users", public: false }, @@ -72,6 +72,34 @@ export const find_users_by_active_status = spacetimedb.reducer( ); "#; +const TYPESCRIPT_MODULE_V2_RENAMED_ACCESSOR: &str = r#"import { schema, table, t } from "spacetimedb/server"; + +const renamedUsers = table( + { name: "users", public: false }, + { + id: t.u64().primaryKey().autoInc(), + name: t.string(), + emailAddress: t.string().index("btree"), + }, +); + +const spacetimedb = schema({ + renamedUsers, +}); +export default spacetimedb; + +export const find_user_by_email = spacetimedb.reducer( + { emailAddress: t.string() }, + (ctx, { emailAddress }) => { + let count = 0; + for (const _row of ctx.db.renamedUsers.emailAddress.filter(emailAddress)) { + count += 1; + } + console.info(`matched ${count}`); + }, +); +"#; + #[test] fn test_typescript_add_optional_columns() { require_pnpm!(); @@ -86,7 +114,7 @@ fn test_typescript_add_optional_columns() { .source( ModuleLanguage::TypeScript, "typescript-add-optional-columns-v1", - TYPESCRIPT_MODULE_WITHOUT_NEW_COLUMNS, + TYPESCRIPT_MODULE_V1, ) .run() .unwrap(); @@ -108,3 +136,37 @@ fn test_typescript_add_optional_columns() { test.call("find_user_by_email", &["alice@example.com"]).unwrap(); test.call("find_users_by_active_status", &["false"]).unwrap(); } + +#[test] +fn test_typescript_change_index_source_name() { + require_pnpm!(); + require_local_server!(); + + let mut test = Smoketest::builder().autopublish(false).build(); + let module_name = format!("typescript-change-source-name-{}", random_string()); + + let database_identity = test + .publish() + .name(&module_name) + .source( + ModuleLanguage::TypeScript, + "typescript-change-source-name-v1", + TYPESCRIPT_MODULE_V1, + ) + .run() + .unwrap(); + + test.call("insert_user", &["Alice", "alice@example.com"]).unwrap(); + + test.publish() + .name(&database_identity) + .source( + ModuleLanguage::TypeScript, + "typescript-change-source-name-v2", + TYPESCRIPT_MODULE_V2_RENAMED_ACCESSOR, + ) + .run() + .unwrap(); + + test.call("find_user_by_email", &["alice@example.com"]).unwrap(); +}