Skip to content

Commit 4f2c064

Browse files
Shubham8287Centril
andauthored
fix: update seq table when migrating table. (#4902)
# Description of Changes `MutTxId::add_columns_to_table` creates new table but only copies sequences to in-memory state, which causes `autoinc` columns to reset on module restart. The existing implementation relies on `create_table_and_update_seq` helper, which only updates the sequence state in memory. This change ensures that the `allocation` is also persisted to the system table, keeping it consistent across restarts. # API and ABI breaking changes NA # Expected complexity level and risk 2 # Testing Added a test, which migrate table and checks for `autoinc` column value without and with restart. --------- Signed-off-by: Shubham Mishra <shivam828787@gmail.com> Co-authored-by: Mazdak Farrokhzad <twingoow@gmail.com>
1 parent 458eac8 commit 4f2c064

3 files changed

Lines changed: 187 additions & 23 deletions

File tree

crates/core/src/db/update.rs

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ mod test {
340340
host::module_host::create_table_from_def,
341341
};
342342
use spacetimedb_datastore::locking_tx_datastore::PendingSchemaChange;
343-
use spacetimedb_lib::db::raw_def::v9::{btree, RawModuleDefV9Builder, TableAccess};
343+
use spacetimedb_lib::db::raw_def::v9::{btree, RawIndexAlgorithm, RawModuleDefV9Builder, TableAccess};
344344
use spacetimedb_sats::{product, AlgebraicType, AlgebraicType::U64};
345345
use spacetimedb_schema::{auto_migrate::ponder_migrate, def::ModuleDef};
346346

@@ -432,7 +432,7 @@ mod test {
432432
let stdb = TestDB::durable()?;
433433

434434
// Step 1: Table with a primary key (requires unique constraint + index).
435-
let module_v1 = {
435+
let module_v1: ModuleDef = {
436436
let mut builder = RawModuleDefV9Builder::new();
437437
builder
438438
.build_table_with_new_type("person", [("name", AlgebraicType::String)], true)
@@ -446,7 +446,7 @@ mod test {
446446
};
447447

448448
// Step 2: Same table, but primary key removed.
449-
let module_v2 = {
449+
let module_v2: ModuleDef = {
450450
let mut builder = RawModuleDefV9Builder::new();
451451
builder
452452
.build_table_with_new_type("person", [("name", AlgebraicType::String)], true)
@@ -580,4 +580,118 @@ mod test {
580580
);
581581
Ok(())
582582
}
583+
584+
/// Verifies that `autoinc` sequence survives a schema migration that adds a column,
585+
/// and is also correctly persisted across database replay.
586+
///
587+
/// Flow:
588+
/// - Create v1 schema and consume a few sequence values.
589+
/// - Migrate to v2 (adds a column with a default).
590+
/// - Ensure next insert continues the sequence (no reset).
591+
/// - Reopen DB and verify allocation cursor is still preserved.
592+
#[test]
593+
fn auto_inc_sequence_survives_add_column_migration() -> anyhow::Result<()> {
594+
let auth_ctx = AuthCtx::for_testing();
595+
let stdb = TestDB::durable()?;
596+
597+
// Define the old module that was before.
598+
let module_v1: ModuleDef = {
599+
let mut b = RawModuleDefV9Builder::new();
600+
b.build_table_with_new_type("seq_t", [("id", AlgebraicType::I64)], true)
601+
.with_auto_inc_primary_key(0)
602+
.with_index_no_accessor_name(RawIndexAlgorithm::BTree { columns: 0.into() })
603+
.with_access(TableAccess::Public)
604+
.finish();
605+
b.finish().try_into().expect("valid module v1")
606+
};
607+
608+
// Define the module that we're migrating to.
609+
let module_v2: ModuleDef = {
610+
let mut b = RawModuleDefV9Builder::new();
611+
b.build_table_with_new_type(
612+
"seq_t",
613+
[("id", AlgebraicType::I64), ("payload", AlgebraicType::U64)],
614+
true,
615+
)
616+
.with_auto_inc_primary_key(0)
617+
.with_index_no_accessor_name(btree(0))
618+
.with_access(TableAccess::Public)
619+
.with_default_column_value(1, product![0u64].into())
620+
.finish();
621+
b.finish().try_into().expect("valid module v2")
622+
};
623+
624+
// helper to insert + collect sorted ids
625+
let insert_and_collect_ids = |stdb: &TestDB, payload: AlgebraicValue| -> anyhow::Result<Vec<i64>> {
626+
let mut tx = begin_mut_tx(stdb);
627+
let table_id = stdb.table_id_from_name_mut(&tx, "seq_t")?.expect("seq_t should exist");
628+
629+
insert(stdb, &mut tx, table_id, &payload)?;
630+
631+
let mut ids = stdb
632+
.iter_mut(&tx, table_id)?
633+
.map(|r| r.read_col::<i64>(0))
634+
.collect::<Result<Vec<_>, _>>()?;
635+
636+
ids.sort();
637+
stdb.commit_tx(tx)?;
638+
Ok(ids)
639+
};
640+
641+
// Create the old tables and insert two rows
642+
// that use the auto-inc sequence.
643+
{
644+
let mut tx = begin_mut_tx(&stdb);
645+
646+
for def in module_v1.tables() {
647+
create_table_from_def(&stdb, &mut tx, &module_v1, def)?;
648+
}
649+
650+
let table_id = stdb.table_id_from_name_mut(&tx, "seq_t")?.expect("seq_t should exist");
651+
652+
insert(&stdb, &mut tx, table_id, &product![0i64])?;
653+
insert(&stdb, &mut tx, table_id, &product![0i64])?;
654+
655+
stdb.commit_tx(tx)?;
656+
}
657+
658+
// Successfully update the database to the new module.
659+
{
660+
let mut tx = begin_mut_tx(&stdb);
661+
662+
let plan = ponder_migrate(&module_v1, &module_v2)?;
663+
let res = update_database(&stdb, &mut tx, auth_ctx, plan, &TestLogger)?;
664+
665+
assert!(matches!(
666+
res,
667+
UpdateResult::Success | UpdateResult::RequiresClientDisconnect
668+
));
669+
670+
stdb.commit_tx(tx)?;
671+
}
672+
673+
// Check that the new table has reused the sequence
674+
// from the old table such that the last row has the value 3.
675+
{
676+
let ids = insert_and_collect_ids(&stdb, product![0i64, 99u64].into())?;
677+
assert!(
678+
ids.iter().last().unwrap() == &3,
679+
"expected id 3 after migration, got {ids:?}"
680+
);
681+
}
682+
683+
// Check that we can replay.
684+
let stdb = stdb.reopen()?;
685+
686+
// After replay, the allocation cursor should be preserved.
687+
{
688+
let ids = insert_and_collect_ids(&stdb, product![0i64, 99u64].into())?;
689+
assert!(
690+
ids.iter().last().unwrap() == &4097,
691+
"expected id 4097 after reopen, got {ids:?}"
692+
);
693+
}
694+
695+
Ok(())
696+
}
583697
}

crates/datastore/src/locking_tx_datastore/mut_tx.rs

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,6 +1015,27 @@ impl MutTxId {
10151015
Ok(ret)
10161016
}
10171017

1018+
fn update_st_sequence_row<R>(
1019+
&mut self,
1020+
sequence_id: SequenceId,
1021+
updater: impl FnOnce(&mut StSequenceRow) -> R,
1022+
) -> Result<R> {
1023+
// Fetch the row.
1024+
let st_sequence_ref = self
1025+
.iter_by_col_eq(ST_SEQUENCE_ID, StSequenceFields::SequenceId, &sequence_id.into())?
1026+
.last()
1027+
.ok_or(SequenceError::NotFound(sequence_id))?;
1028+
let ptr = st_sequence_ref.pointer();
1029+
let mut row = StSequenceRow::try_from(st_sequence_ref)?;
1030+
1031+
// Delete the row, run updates, and insert again.
1032+
self.delete(ST_SEQUENCE_ID, ptr)?;
1033+
let ret = updater(&mut row);
1034+
self.insert_via_serialize_bsatn(ST_SEQUENCE_ID, &row)?;
1035+
1036+
Ok(ret)
1037+
}
1038+
10181039
pub fn view_id_from_name(&self, view_name: &str) -> Result<Option<ViewId>> {
10191040
let view_name = &view_name.into();
10201041
let row = self
@@ -1207,19 +1228,20 @@ impl MutTxId {
12071228
// Store sequence values to restore them later with new table.
12081229
// Using a map from name to value as the new sequence ids will be different.
12091230
// and I am not sure if we should rely on the order of sequences in the table schema.
1210-
let seq_values: HashMap<_, i128> = original_table_schema
1211-
.sequences
1212-
.iter()
1213-
.map(|s| {
1214-
(
1215-
s.sequence_name.clone(),
1216-
self.sequence_state_lock
1217-
.get_sequence_mut(s.sequence_id)
1218-
.expect("sequence exists in original schema and should in sequence state.")
1219-
.get_value(),
1220-
)
1221-
})
1222-
.collect();
1231+
let mut seq_values: HashMap<_, (i128, i128)> = HashMap::default();
1232+
for seq in &original_table_schema.sequences {
1233+
let value = self
1234+
.sequence_state_lock
1235+
.get_sequence_mut(seq.sequence_id)
1236+
.expect("sequence exists in original schema and should in sequence state.")
1237+
.get_value();
1238+
let allocated = self
1239+
.iter_by_col_eq(ST_SEQUENCE_ID, StSequenceFields::SequenceId, &seq.sequence_id.into())?
1240+
.last()
1241+
.ok_or(SequenceError::NotFound(seq.sequence_id))?
1242+
.read_col(StSequenceFields::Allocated)?;
1243+
seq_values.insert(seq.sequence_name.clone(), (value, allocated));
1244+
}
12231245

12241246
// Drop existing table first due to unique constraints on table name in `st_table`
12251247
self.drop_table(table_id)?;
@@ -1249,23 +1271,40 @@ impl MutTxId {
12491271
Ok(new_table_id)
12501272
}
12511273

1274+
/// Recreate a table and restore sequence runtime state after a destructive
1275+
/// schema change (for example `add_columns_to_table`).
1276+
///
1277+
/// `create_table(...)` generates fresh table/sequence IDs and inserts fresh
1278+
/// rows into `st_sequence`. We then restore preserved `(value, allocated)`
1279+
/// by sequence name:
1280+
/// - update in-memory sequence state (`SequencesState`) so this process keeps
1281+
/// allocating from the same point;
1282+
/// - patch the newly created `st_sequence` row so reopen/replay restores the
1283+
/// same allocation cursor instead of sequence start.
12521284
fn create_table_and_update_seq(
12531285
&mut self,
12541286
table_schema: TableSchema,
1255-
seq_values: HashMap<RawIdentifier, i128>,
1287+
seq_values: HashMap<RawIdentifier, (i128, i128)>,
12561288
) -> Result<TableId> {
12571289
let table_id = self.create_table(table_schema)?;
12581290
let table_schema = self.schema_for_table(table_id)?;
12591291

12601292
for seq in table_schema.sequences.iter() {
1261-
let new_seq = self
1262-
.sequence_state_lock
1263-
.get_sequence_mut(seq.sequence_id)
1264-
.expect("sequence just created");
1265-
let value = *seq_values
1293+
let (value, allocated) = *seq_values
12661294
.get(&seq.sequence_name)
12671295
.ok_or_else(|| SequenceError::NotFound(seq.sequence_id))?;
1268-
new_seq.update_value(value);
1296+
{
1297+
let new_seq = self
1298+
.sequence_state_lock
1299+
.get_sequence_mut(seq.sequence_id)
1300+
.expect("sequence just created");
1301+
new_seq.update_value(value);
1302+
new_seq.update_allocation(allocated);
1303+
}
1304+
1305+
// This updates the new `st_sequence` row created by `create_table(...)`
1306+
// above (old table rows are already dropped).
1307+
self.update_st_sequence_row(seq.sequence_id, |st| st.allocated = allocated)?;
12691308
}
12701309

12711310
Ok(table_id)

crates/datastore/src/locking_tx_datastore/sequence.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,17 @@ impl Sequence {
8282
self.value = new_value;
8383
}
8484

85+
/// Update the persisted allocation cursor for the sequence.
86+
pub(super) fn update_allocation(&mut self, new_allocated: i128) {
87+
if !(self.schema.min_value..=self.schema.max_value).contains(&new_allocated) {
88+
panic!(
89+
"Invalid sequence allocation update: new allocated {} is out of bounds for sequence with min_value {} and max_value {}",
90+
new_allocated, self.schema.min_value, self.schema.max_value
91+
);
92+
}
93+
self.allocated = new_allocated;
94+
}
95+
8596
pub(super) fn get_value(&self) -> i128 {
8697
self.value
8798
}

0 commit comments

Comments
 (0)