diff --git a/crates/core/src/db/relational_db.rs b/crates/core/src/db/relational_db.rs index c145a9b79bd..f239645b047 100644 --- a/crates/core/src/db/relational_db.rs +++ b/crates/core/src/db/relational_db.rs @@ -1014,6 +1014,15 @@ 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_row_type( &self, tx: &mut MutTx, diff --git a/crates/core/src/db/update.rs b/crates/core/src/db/update.rs index f9ca4c110d9..d53a396859b 100644 --- a/crates/core/src/db/update.rs +++ b/crates/core/src/db/update.rs @@ -209,11 +209,34 @@ 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::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(); @@ -340,9 +363,13 @@ mod test { host::module_host::create_table_from_def, }; use spacetimedb_datastore::locking_tx_datastore::PendingSchemaChange; - use spacetimedb_lib::db::raw_def::v9::{btree, RawIndexAlgorithm, RawModuleDefV9Builder, TableAccess}; - use spacetimedb_sats::{product, AlgebraicType, AlgebraicType::U64}; - use spacetimedb_schema::{auto_migrate::ponder_migrate, def::ModuleDef}; + use spacetimedb_lib::db::raw_def::{ + v10::{ExplicitNames, RawModuleDefV10Builder}, + v9::{btree, RawIndexAlgorithm, RawModuleDefV9Builder, TableAccess}, + }; + use spacetimedb_sats::{product, raw_identifier::RawIdentifier, AlgebraicType, AlgebraicType::U64, ProductType}; + use spacetimedb_schema::auto_migrate::{ponder_migrate, AutoMigrateStep, MigratePlan}; + use spacetimedb_schema::def::ModuleDef; struct TestLogger; impl UpdateLogger for TestLogger { @@ -424,6 +451,109 @@ 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/datastore/src/locking_tx_datastore/committed_state.rs b/crates/datastore/src/locking_tx_datastore/committed_state.rs index c479be085d9..4086bc93ec2 100644 --- a/crates/datastore/src/locking_tx_datastore/committed_state.rs +++ b/crates/datastore/src/locking_tx_datastore/committed_state.rs @@ -707,6 +707,18 @@ 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 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 e9d67103b16..667990b3e75 100644 --- a/crates/datastore/src/locking_tx_datastore/datastore.rs +++ b/crates/datastore/src/locking_tx_datastore/datastore.rs @@ -294,6 +294,15 @@ 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_row_type_mut_tx( &self, tx: &mut MutTxId, @@ -527,6 +536,15 @@ 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 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 3a36331dc4d..90e00e2f0c4 100644 --- a/crates/datastore/src/locking_tx_datastore/mut_tx.rs +++ b/crates/datastore/src/locking_tx_datastore/mut_tx.rs @@ -1163,6 +1163,46 @@ 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 row type of the table identified by `table_id`. /// /// In practice, this should not error, diff --git a/crates/datastore/src/locking_tx_datastore/state_view.rs b/crates/datastore/src/locking_tx_datastore/state_view.rs index 8c4c978aaa0..6595a0b72c9 100644 --- a/crates/datastore/src/locking_tx_datastore/state_view.rs +++ b/crates/datastore/src/locking_tx_datastore/state_view.rs @@ -16,7 +16,7 @@ use core::ops::RangeBounds; use spacetimedb_lib::ConnectionId; use spacetimedb_primitives::{ColList, TableId}; use spacetimedb_sats::AlgebraicValue; -use spacetimedb_schema::schema::{ColumnSchema, TableSchema, ViewDefInfo}; +use spacetimedb_schema::schema::{ColumnSchema, IndexSchema, TableSchema, ViewDefInfo}; use spacetimedb_table::table::IndexScanPointIter; use spacetimedb_table::{ blob_store::HashMapBlobStore, @@ -120,6 +120,22 @@ pub trait StateView { .transpose() } + /// Look up an `st_index_accessor` row by its canonical index name. + fn find_st_index_accessor_row_by_index_name(&self, index_name: &str) -> Result> { + match self.iter_by_col_eq( + ST_INDEX_ACCESSOR_ID, + StIndexAccessorFields::IndexName, + &index_name.into(), + ) { + Ok(mut iter) => iter.next().map(StIndexAccessorRow::try_from).transpose(), + // `schema_for_table_raw` is called while restoring snapshots, + // before `migrate_system_tables` creates newer system tables. + // Treat a missing `st_index_accessor` as "no aliases yet" here. + Err(DatastoreError::Table(TableError::IdNotFound(..))) => Ok(None), + Err(e) => Err(e), + } + } + /// Look up an `st_column_accessor` row by its canonical table and column names fn find_st_column_accessor_row(&self, table_name: &str, col_name: &str) -> Result> { match self.iter_by_col_eq( @@ -187,7 +203,14 @@ pub trait StateView { // Look up the indexes for the table in question. let indexes = self .iter_by_col_eq(ST_INDEX_ID, StIndexFields::TableId, value_eq)? - .map(|row| StIndexRow::try_from(row).map(Into::into)) + .map(|row| { + let row = StIndexRow::try_from(row)?; + let mut index_schema = IndexSchema::from(row); + index_schema.alias = self + .find_st_index_accessor_row_by_index_name(index_schema.index_name.as_ref())? + .map(|row| row.accessor_name); + Ok(index_schema) + }) .collect::>>()?; let schedule = self diff --git a/crates/datastore/src/locking_tx_datastore/tx_state.rs b/crates/datastore/src/locking_tx_datastore/tx_state.rs index 4e712bcf684..4b56aa1dfca 100644 --- a/crates/datastore/src/locking_tx_datastore/tx_state.rs +++ b/crates/datastore/src/locking_tx_datastore/tx_state.rs @@ -111,6 +111,13 @@ 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 [`Table`] with [`TableId`] was removed. TableRemoved(TableId, Table), /// The table with [`TableId`] was added. @@ -145,6 +152,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(), diff --git a/crates/datastore/src/traits.rs b/crates/datastore/src/traits.rs index e1b99825ae2..07a1d633124 100644 --- a/crates/datastore/src/traits.rs +++ b/crates/datastore/src/traits.rs @@ -627,6 +627,12 @@ 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 index_id_from_name_mut_tx(&self, tx: &Self::MutTx, index_name: &str) -> super::Result>; // TODO: Index data diff --git a/crates/schema/src/auto_migrate.rs b/crates/schema/src/auto_migrate.rs index f2fba813fa8..bb69c4bea55 100644 --- a/crates/schema/src/auto_migrate.rs +++ b/crates/schema/src/auto_migrate.rs @@ -284,6 +284,9 @@ 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>), + /// 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>), @@ -981,6 +984,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(()) } @@ -2039,6 +2044,42 @@ 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_view_disconnect_clients() { struct TestCase { diff --git a/crates/schema/src/auto_migrate/formatter.rs b/crates/schema/src/auto_migrate/formatter.rs index 1f7377209bf..7296c89df44 100644 --- a/crates/schema/src/auto_migrate/formatter.rs +++ b/crates/schema/src/auto_migrate/formatter.rs @@ -59,6 +59,10 @@ 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::RemoveConstraint(constraint) => { let constraint_info = extract_constraint_info(*constraint, plan.old)?; f.format_constraint(&constraint_info, Action::Removed) diff --git a/crates/smoketests/tests/smoketests/mod.rs b/crates/smoketests/tests/smoketests/mod.rs index 959d43eab8f..741512e0714 100644 --- a/crates/smoketests/tests/smoketests/mod.rs +++ b/crates/smoketests/tests/smoketests/mod.rs @@ -39,4 +39,5 @@ mod sql; mod sql_connect_hook; mod templates; mod timestamp_route; +mod typescript_index_source_name; mod views; diff --git a/crates/smoketests/tests/smoketests/typescript_index_source_name.rs b/crates/smoketests/tests/smoketests/typescript_index_source_name.rs new file mode 100644 index 00000000000..04de365bb59 --- /dev/null +++ b/crates/smoketests/tests/smoketests/typescript_index_source_name.rs @@ -0,0 +1,162 @@ +use spacetimedb_smoketests::{random_string, require_local_server, require_pnpm, Smoketest}; + +const TYPESCRIPT_MODULE_V1: &str = r#"import { schema, table, t } from "spacetimedb/server"; + +const appUsers = table( + { name: "users", public: false }, + { + id: t.u64().primaryKey().autoInc(), + name: t.string(), + emailAddress: t.string().index("btree"), + }, +); + +const spacetimedb = schema({ + appUsers, +}); +export default spacetimedb; + +export const insert_user = spacetimedb.reducer( + { + name: t.string(), + emailAddress: t.string(), + }, + (ctx, { name, emailAddress }) => { + ctx.db.appUsers.insert({ + id: 0n, + name, + emailAddress, + }); + }, +); +"#; + +const TYPESCRIPT_MODULE_WITH_NEW_COLUMNS: &str = r#"import { schema, table, t } from "spacetimedb/server"; + +const appUsers = table( + { name: "users", public: false }, + { + id: t.u64().primaryKey().autoInc(), + name: t.string(), + emailAddress: t.string().index("btree"), + age: t.number().optional().default(undefined), + isActive: t.bool().default(false).index(), + }, +); + +const spacetimedb = schema({ + appUsers, +}); +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.appUsers.emailAddress.filter(emailAddress)) { + count += 1; + } + console.info(`matched ${count}`); + }, +); + +export const find_users_by_active_status = spacetimedb.reducer( + { isActive: t.bool() }, + (ctx, { isActive }) => { + let count = 0; + for (const _row of ctx.db.appUsers.isActive.filter(isActive)) { + count += 1; + } + console.info(`matched active users ${count}`); + }, +); +"#; + +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!(); + require_local_server!(); + + let mut test = Smoketest::builder().autopublish(false).build(); + let module_name = format!("typescript-add-optional-columns-{}", random_string()); + + let database_identity = test + .publish_typescript_module_source( + "typescript-add-optional-columns-v1", + &module_name, + TYPESCRIPT_MODULE_V1, + ) + .unwrap(); + + test.call("insert_user", &["Alice", "alice@example.com"]).unwrap(); + + test.restart_server(); + + test.publish_typescript_module_source_clear( + "typescript-add-optional-columns-v2", + &database_identity, + TYPESCRIPT_MODULE_WITH_NEW_COLUMNS, + false, + ) + .unwrap(); + + 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_typescript_module_source( + "typescript-change-source-name-v1", + &module_name, + TYPESCRIPT_MODULE_V1, + ) + .unwrap(); + + test.call("insert_user", &["Alice", "alice@example.com"]).unwrap(); + + test.publish_typescript_module_source_clear( + "typescript-change-source-name-v2", + &database_identity, + TYPESCRIPT_MODULE_V2_RENAMED_ACCESSOR, + false, + ) + .unwrap(); + + test.call("find_user_by_email", &["alice@example.com"]).unwrap(); +}