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