Skip to content

Commit 434894f

Browse files
authored
Fix adding enum variants (#3200)
# Description of Changes First commit reverts disabling adding enum variants (#3178). Second commit makes replay recognize inserts / deletes to `st_column` and triggers a refresh of the in-memory table that was referenced in the `st_column` change. # API and ABI breaking changes None # Expected complexity level and risk 3, small code bug "deep". # Testing I've confirmed manually that a module that couldn't be restarted before this PR can now be restarted. We should probably follow up this PR with a smoketest.
1 parent b9d3099 commit 434894f

File tree

5 files changed

+129
-34
lines changed

5 files changed

+129
-34
lines changed

crates/core/src/db/relational_db.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1043,7 +1043,6 @@ impl RelationalDB {
10431043
Ok(self.inner.alter_table_access_mut_tx(tx, name, access)?)
10441044
}
10451045

1046-
#[allow(unused)]
10471046
pub(crate) fn alter_table_row_type(
10481047
&self,
10491048
tx: &mut MutTx,

crates/core/src/db/update.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use spacetimedb_lib::AlgebraicValue;
99
use spacetimedb_primitives::{ColSet, TableId};
1010
use spacetimedb_schema::auto_migrate::{AutoMigratePlan, ManualMigratePlan, MigratePlan};
1111
use spacetimedb_schema::def::TableDef;
12-
use spacetimedb_schema::schema::{IndexSchema, Schema, SequenceSchema, TableSchema};
12+
use spacetimedb_schema::schema::{column_schemas_from_defs, IndexSchema, Schema, SequenceSchema, TableSchema};
1313
use std::sync::Arc;
1414

1515
/// The logger used for by [`update_database`] and friends.
@@ -222,8 +222,14 @@ fn auto_migrate_database(
222222
);
223223
stdb.drop_sequence(tx, sequence_schema.sequence_id)?;
224224
}
225-
spacetimedb_schema::auto_migrate::AutoMigrateStep::ChangeColumns(_table_name) => {
226-
anyhow::bail!("Unsupported: Changing column types");
225+
spacetimedb_schema::auto_migrate::AutoMigrateStep::ChangeColumns(table_name) => {
226+
let table_def = plan.new.stored_in_table_def(table_name).unwrap();
227+
let table_id = stdb.table_id_from_name_mut(tx, table_name).unwrap().unwrap();
228+
let column_schemas = column_schemas_from_defs(plan.new, &table_def.columns, table_id);
229+
230+
log!(logger, "Changing columns of table `{}`", table_name);
231+
232+
stdb.alter_table_row_type(tx, table_id, column_schemas)?;
227233
}
228234
spacetimedb_schema::auto_migrate::AutoMigrateStep::ChangeAccess(table_name) => {
229235
let table_def = plan.new.stored_in_table_def(table_name).unwrap();

crates/datastore/src/locking_tx_datastore/committed_state.rs

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ use super::{
88
};
99
use crate::{
1010
db_metrics::DB_METRICS,
11-
error::{IndexError, TableError},
11+
error::{DatastoreError, IndexError, TableError},
1212
execution_context::ExecutionContext,
13+
locking_tx_datastore::state_view::iter_st_column_for_table,
1314
system_tables::{
1415
system_tables, StColumnRow, StConstraintData, StConstraintRow, StIndexRow, StSequenceRow, StTableFields,
1516
StTableRow, SystemTable, ST_CLIENT_ID, ST_CLIENT_IDX, ST_COLUMN_ID, ST_COLUMN_IDX, ST_COLUMN_NAME,
@@ -28,8 +29,8 @@ use spacetimedb_lib::{
2829
db::auth::{StAccess, StTableType},
2930
Identity,
3031
};
31-
use spacetimedb_primitives::{ColList, ColSet, IndexId, TableId};
32-
use spacetimedb_sats::memory_usage::MemoryUsage;
32+
use spacetimedb_primitives::{ColId, ColList, ColSet, IndexId, TableId};
33+
use spacetimedb_sats::{algebraic_value::de::ValueDeserializer, memory_usage::MemoryUsage, Deserialize};
3334
use spacetimedb_sats::{AlgebraicValue, ProductValue};
3435
use spacetimedb_schema::{def::IndexAlgorithm, schema::TableSchema};
3536
use spacetimedb_table::{
@@ -339,6 +340,7 @@ impl CommittedState {
339340
.delete_equal_row(&self.page_pool, blob_store, rel)
340341
.map_err(TableError::Bflatn)?
341342
.ok_or_else(|| anyhow!("Delete for non-existent row when replaying transaction"))?;
343+
342344
Ok(())
343345
}
344346

@@ -349,11 +351,55 @@ impl CommittedState {
349351
row: &ProductValue,
350352
) -> Result<()> {
351353
let (table, blob_store, pool) = self.get_table_and_blob_store_or_create(table_id, schema);
352-
table.insert(pool, blob_store, row).map(drop).map_err(|e| match e {
353-
InsertError::Bflatn(e) => TableError::Bflatn(e).into(),
354-
InsertError::Duplicate(e) => TableError::Duplicate(e).into(),
355-
InsertError::IndexError(e) => IndexError::UniqueConstraintViolation(e).into(),
356-
})
354+
let (_, row_ref) = table.insert(pool, blob_store, row).map_err(|e| -> DatastoreError {
355+
match e {
356+
InsertError::Bflatn(e) => TableError::Bflatn(e).into(),
357+
InsertError::Duplicate(e) => TableError::Duplicate(e).into(),
358+
InsertError::IndexError(e) => IndexError::UniqueConstraintViolation(e).into(),
359+
}
360+
})?;
361+
362+
if table_id == ST_COLUMN_ID {
363+
// We've made a modification to `st_column`.
364+
// The type of a table has changed, so figure out which.
365+
// The first column in `StColumnRow` is `table_id`.
366+
let row_ptr = row_ref.pointer();
367+
self.st_column_changed(row, row_ptr)?;
368+
}
369+
370+
Ok(())
371+
}
372+
373+
/// Refreshes the columns and layout of a table
374+
/// when a `row` has been inserted from `st_column`.
375+
///
376+
/// The `row_ptr` is a pointer to `row`.
377+
fn st_column_changed(&mut self, row: &ProductValue, row_ptr: RowPointer) -> Result<()> {
378+
let target_table_id = TableId::deserialize(ValueDeserializer::from_ref(&row.elements[0]))
379+
.expect("first field in `st_column` should decode to a `TableId`");
380+
let target_col_id = ColId::deserialize(ValueDeserializer::from_ref(&row.elements[1]))
381+
.expect("second field in `st_column` should decode to a `ColId`");
382+
383+
// We're replaying and we don't have unique constraints yet.
384+
// Due to replay handling all inserts first and deletes after,
385+
// when processing `st_column` insert/deletes,
386+
// we may end up with two definitions for the same `col_pos`.
387+
// Of those two, we're interested in the one we just inserted
388+
// and not the other one, as it is being replaced.
389+
let columns = iter_st_column_for_table(self, &target_table_id.into())?
390+
.filter_map(|row_ref| {
391+
StColumnRow::try_from(row_ref)
392+
.map(|c| (c.col_pos != target_col_id || row_ref.pointer() == row_ptr).then(|| c.into()))
393+
.transpose()
394+
})
395+
.collect::<Result<Vec<_>>>()?;
396+
397+
// Update the columns and layout of the the in-memory table.
398+
if let Some(table) = self.tables.get_mut(&target_table_id) {
399+
table.change_columns_to(columns).map_err(TableError::from)?;
400+
}
401+
402+
Ok(())
357403
}
358404

359405
pub(super) fn build_sequence_state(&mut self, sequence_state: &mut SequencesState) -> Result<()> {

crates/datastore/src/locking_tx_datastore/state_view.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,8 @@ pub trait StateView {
7979
let table_primary_key = row.table_primary_key.as_ref().and_then(ColList::as_singleton);
8080

8181
// Look up the columns for the table in question.
82-
let mut columns: Vec<ColumnSchema> = self
83-
.iter_by_col_eq(ST_COLUMN_ID, StColumnFields::TableId, value_eq)?
84-
.map(|row| {
85-
let row = StColumnRow::try_from(row)?;
86-
Ok(row.into())
87-
})
82+
let mut columns: Vec<ColumnSchema> = iter_st_column_for_table(self, &table_id.into())?
83+
.map(|row| Ok(StColumnRow::try_from(row)?.into()))
8884
.collect::<Result<Vec<_>>>()?;
8985
columns.sort_by_key(|col| col.col_pos);
9086

@@ -149,6 +145,14 @@ pub trait StateView {
149145
}
150146
}
151147

148+
/// Returns an iterator over all `st_column` rows for `table_id`.
149+
pub(crate) fn iter_st_column_for_table<'a>(
150+
this: &'a (impl StateView + ?Sized),
151+
table_id: &'a AlgebraicValue,
152+
) -> Result<impl 'a + Iterator<Item = RowRef<'a>>> {
153+
this.iter_by_col_eq(ST_COLUMN_ID, StColumnFields::TableId, table_id)
154+
}
155+
152156
pub struct IterMutTx<'a> {
153157
tx_state_ins: Option<(&'a Table, &'a HashMapBlobStore)>,
154158
stage: ScanStage<'a>,

smoketests/tests/auto_migration.py

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,27 @@
44

55

66
class AddTableAutoMigration(Smoketest):
7-
MODULE_CODE = """
7+
MODULE_CODE_INIT = """
88
use spacetimedb::{log, ReducerContext, Table, SpacetimeType};
9+
use PersonKind::*;
910
1011
#[spacetimedb::table(name = person, public)]
1112
pub struct Person {
1213
name: String,
14+
kind: PersonKind,
1315
}
1416
1517
#[spacetimedb::reducer]
16-
pub fn add_person(ctx: &ReducerContext, name: String) {
17-
ctx.db.person().insert(Person { name });
18+
pub fn add_person(ctx: &ReducerContext, name: String, kind: String) {
19+
let kind = kind_from_string(kind);
20+
ctx.db.person().insert(Person { name, kind });
1821
}
1922
2023
#[spacetimedb::reducer]
2124
pub fn print_persons(ctx: &ReducerContext, prefix: String) {
2225
for person in ctx.db.person().iter() {
23-
log::info!("{}: {}", prefix, person.name);
26+
let kind = kind_to_string(person.kind);
27+
log::info!("{prefix}: {} - {kind}", person.name);
2428
}
2529
}
2630
@@ -39,11 +43,47 @@ class AddTableAutoMigration(Smoketest):
3943
4044
#[spacetimedb::client_visibility_filter]
4145
const PERSON_VISIBLE: spacetimedb::Filter = spacetimedb::Filter::Sql("SELECT * FROM person");
46+
"""
47+
48+
MODULE_CODE = MODULE_CODE_INIT + """
49+
#[derive(SpacetimeType, Clone, Copy, PartialEq, Eq)]
50+
pub enum PersonKind {
51+
Student,
52+
}
53+
54+
fn kind_from_string(_: String) -> PersonKind {
55+
Student
56+
}
57+
58+
fn kind_to_string(Student: PersonKind) -> &'static str {
59+
"Student"
60+
}
4261
"""
4362

4463
MODULE_CODE_UPDATED = (
45-
MODULE_CODE
64+
MODULE_CODE_INIT
4665
+ """
66+
#[derive(SpacetimeType, Clone, Copy, PartialEq, Eq)]
67+
pub enum PersonKind {
68+
Student,
69+
Professor,
70+
}
71+
72+
fn kind_from_string(kind: String) -> PersonKind {
73+
match &*kind {
74+
"Student" => Student,
75+
"Professor" => Professor,
76+
_ => panic!(),
77+
}
78+
}
79+
80+
fn kind_to_string(kind: PersonKind) -> &'static str {
81+
match kind {
82+
Student => "Student",
83+
Professor => "Professor",
84+
}
85+
}
86+
4787
#[spacetimedb::table(name = book, public)]
4888
pub struct Book {
4989
isbn: String,
@@ -89,14 +129,14 @@ def test_add_table_auto_migration(self):
89129
logging.info("Initial publish complete")
90130
# initial module code is already published by test framework
91131

92-
self.call("add_person", "Robert")
93-
self.call("add_person", "Julie")
94-
self.call("add_person", "Samantha")
132+
self.call("add_person", "Robert", "Student")
133+
self.call("add_person", "Julie", "Student")
134+
self.call("add_person", "Samantha", "Student")
95135
self.call("print_persons", "BEFORE")
96136
logs = self.logs(100)
97-
self.assertIn("BEFORE: Samantha", logs)
98-
self.assertIn("BEFORE: Julie", logs)
99-
self.assertIn("BEFORE: Robert", logs)
137+
self.assertIn("BEFORE: Samantha - Student", logs)
138+
self.assertIn("BEFORE: Julie - Student", logs)
139+
self.assertIn("BEFORE: Robert - Student", logs)
100140

101141
logging.info(
102142
"Initial operations complete, updating module without clear",
@@ -120,16 +160,16 @@ def test_add_table_auto_migration(self):
120160

121161
self.logs(100)
122162

123-
self.call("add_person", "Husserl")
163+
self.call("add_person", "Husserl", "Professor")
124164
self.call("add_book", "1234567890")
125165
self.call("print_persons", "AFTER_PERSON")
126166
self.call("print_books", "AFTER_BOOK")
127167

128168
logs = self.logs(100)
129-
self.assertIn("AFTER_PERSON: Samantha", logs)
130-
self.assertIn("AFTER_PERSON: Julie", logs)
131-
self.assertIn("AFTER_PERSON: Robert", logs)
132-
self.assertIn("AFTER_PERSON: Husserl", logs)
169+
self.assertIn("AFTER_PERSON: Samantha - Student", logs)
170+
self.assertIn("AFTER_PERSON: Julie - Student", logs)
171+
self.assertIn("AFTER_PERSON: Robert - Student", logs)
172+
self.assertIn("AFTER_PERSON: Husserl - Professor", logs)
133173
self.assertIn("AFTER_BOOK: 1234567890", logs)
134174

135175

0 commit comments

Comments
 (0)