Skip to content

Commit bfc2003

Browse files
Automigrate views using new schema for backing table
1 parent 796f3da commit bfc2003

4 files changed

Lines changed: 231 additions & 24 deletions

File tree

crates/core/src/host/host_controller.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,13 +1276,18 @@ impl Host {
12761276
);
12771277

12781278
let res = match ponder_migrate(&old_module.module_def, &module_def) {
1279-
Ok(plan) => MigratePlanResult::Success {
1280-
old_module_hash: old_module.module_hash,
1281-
new_module_hash: program.hash,
1282-
breaks_client: plan.breaks_client(),
1283-
plan: plan.pretty_print(style)?.into(),
1284-
major_version_upgrade,
1285-
},
1279+
Ok(mut plan) => {
1280+
let stdb = self.replica_ctx.relational_db();
1281+
db::update::add_view_backing_table_recreate_steps(stdb, &mut plan)?;
1282+
1283+
MigratePlanResult::Success {
1284+
old_module_hash: old_module.module_hash,
1285+
new_module_hash: program.hash,
1286+
breaks_client: plan.breaks_client(),
1287+
plan: plan.pretty_print(style)?.into(),
1288+
major_version_upgrade,
1289+
}
1290+
}
12861291
Err(e) => MigratePlanResult::AutoMigrationError {
12871292
error: e,
12881293
major_version_upgrade,

crates/core/src/host/wasm_common/module_host_actor.rs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ use spacetimedb_lib::{bsatn, http as st_http, ConnectionId, Hash, ProductType, R
4949
use spacetimedb_primitives::{HttpHandlerId, ProcedureId, TableId, ViewFnPtr, ViewId};
5050
use spacetimedb_sats::algebraic_type::fmt::fmt_algebraic_type;
5151
use spacetimedb_sats::{AlgebraicType, AlgebraicTypeRef, Deserialize, ProductValue, Typespace, WithTypespace};
52-
use spacetimedb_schema::auto_migrate::{MigratePlan, MigrationPolicy, MigrationPolicyError};
52+
use spacetimedb_schema::auto_migrate::{ponder_migrate, MigratePlan, MigrationPolicy, MigrationPolicyError};
5353
use spacetimedb_schema::def::deserialize::FunctionDef;
5454
use spacetimedb_schema::def::{ModuleDef, ViewDef};
5555
use spacetimedb_schema::identifier::Identifier;
@@ -641,21 +641,26 @@ impl InstanceCommon {
641641
let system_logger = replica_ctx.logger.system_logger();
642642
let stdb = &replica_ctx.relational_db();
643643

644-
let plan: MigratePlan = match policy.try_migrate(
644+
let mut plan: MigratePlan = match ponder_migrate(&old_module_info.module_def, &self.info.module_def) {
645+
Ok(plan) => plan,
646+
Err(e) => return Ok(UpdateDatabaseResult::AutoMigrateError(e.into())),
647+
};
648+
649+
if let Err(e) = crate::db::update::add_view_backing_table_recreate_steps(stdb, &mut plan) {
650+
return Ok(UpdateDatabaseResult::ErrorExecutingMigration(e));
651+
}
652+
653+
if let Err(e) = policy.permits_migrate_plan(
645654
self.info.database_identity,
646655
old_module_info.module_hash,
647-
&old_module_info.module_def,
648656
self.info.module_hash,
649-
&self.info.module_def,
657+
&plan,
650658
) {
651-
Ok(plan) => plan,
652-
Err(e) => {
653-
return match e {
654-
MigrationPolicyError::AutoMigrateFailure(e) => Ok(UpdateDatabaseResult::AutoMigrateError(e.into())),
655-
_ => Ok(UpdateDatabaseResult::ErrorExecutingMigration(e.into())),
656-
}
657-
}
658-
};
659+
return match e {
660+
MigrationPolicyError::AutoMigrateFailure(e) => Ok(UpdateDatabaseResult::AutoMigrateError(e.into())),
661+
_ => Ok(UpdateDatabaseResult::ErrorExecutingMigration(e.into())),
662+
};
663+
}
659664

660665
let program_hash = program.hash;
661666
let host_type = HostType::from(program.kind);

crates/engine/src/update.rs

Lines changed: 189 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
use super::relational_db::RelationalDB;
22
use crate::sql::rls::RowLevelExpr;
33
use anyhow::Context;
4-
use spacetimedb_datastore::locking_tx_datastore::MutTxId;
4+
use spacetimedb_datastore::execution_context::Workload;
5+
use spacetimedb_datastore::locking_tx_datastore::state_view::StateView;
6+
use spacetimedb_datastore::locking_tx_datastore::{MutTxId, TxId};
7+
use spacetimedb_datastore::system_tables::{StViewFields, StViewRow, ST_VIEW_ID};
58
use spacetimedb_lib::db::auth::StTableType;
69
use spacetimedb_lib::identity::AuthCtx;
710
use spacetimedb_lib::AlgebraicValue;
811
use spacetimedb_primitives::{ColSet, TableId};
9-
use spacetimedb_schema::auto_migrate::{AutoMigratePlan, ManualMigratePlan, MigratePlan};
10-
use spacetimedb_schema::def::{ModuleDef, TableDef, ViewDef};
12+
use spacetimedb_schema::auto_migrate::{AutoMigratePlan, AutoMigrateStep, ManualMigratePlan, MigratePlan};
13+
use spacetimedb_schema::def::{ModuleDef, ModuleDefLookup, TableDef, ViewDef};
1114
use spacetimedb_schema::schema::{
1215
column_schemas_from_defs, ConstraintSchema, IndexSchema, Schema, SequenceSchema, TableSchema,
1316
};
@@ -26,6 +29,100 @@ pub enum UpdateResult {
2629
EvaluateSubscribedViews,
2730
}
2831

32+
type ViewKey<'a> = <ViewDef as ModuleDefLookup>::Key<'a>;
33+
34+
/// Force view recreation when the stored backing table's row layout no longer
35+
/// matches the layout expected by the new module definition.
36+
pub fn add_view_backing_table_recreate_steps<'def>(
37+
stdb: &RelationalDB,
38+
plan: &mut MigratePlan<'def>,
39+
) -> anyhow::Result<()> {
40+
let MigratePlan::Auto(plan) = plan else {
41+
return Ok(());
42+
};
43+
44+
stdb.with_read_only(Workload::Internal, |tx| -> anyhow::Result<()> {
45+
let mut changed = false;
46+
47+
for view in plan.new.views() {
48+
let key = view.key();
49+
50+
// A normal view schema change is already handled by the public migration plan.
51+
// This pass only upgrades stale backing tables.
52+
if view_recreate_is_planned(&plan.steps, key) {
53+
continue;
54+
}
55+
56+
let Some(table_id) = view_backing_table_id(tx, view)? else {
57+
continue;
58+
};
59+
60+
let actual = stdb.schema_for_table(tx, table_id)?;
61+
let expected = TableSchema::from_view_def_for_datastore(plan.new, view);
62+
63+
if !view_backing_row_layout_changed(&actual, &expected) {
64+
continue;
65+
}
66+
67+
// UpdateView would recompute rows into the stale physical layout.
68+
// Dropping and recreating the view rebuilds the backing table.
69+
replace_view_update_with_recreate_steps(&mut plan.steps, key);
70+
changed = true;
71+
}
72+
73+
if changed {
74+
plan.steps.sort();
75+
}
76+
77+
Ok(())
78+
})
79+
}
80+
81+
fn view_recreate_is_planned<'def>(steps: &[AutoMigrateStep<'def>], key: ViewKey<'def>) -> bool {
82+
steps.iter().any(|step| {
83+
matches!(
84+
step,
85+
AutoMigrateStep::AddView(existing) | AutoMigrateStep::RemoveView(existing) if *existing == key
86+
)
87+
})
88+
}
89+
90+
fn view_backing_table_id(tx: &mut TxId, view: &ViewDef) -> anyhow::Result<Option<TableId>> {
91+
let view_name = AlgebraicValue::from(<Box<str>>::from(&*view.name));
92+
let Some(row) = tx
93+
.iter_by_col_eq(ST_VIEW_ID, StViewFields::ViewName, &view_name)?
94+
.next()
95+
else {
96+
return Ok(None);
97+
};
98+
99+
Ok(StViewRow::try_from(row)?.table_id)
100+
}
101+
102+
fn view_backing_row_layout_changed(actual: &TableSchema, expected: &TableSchema) -> bool {
103+
actual.row_type != expected.row_type
104+
}
105+
106+
fn replace_view_update_with_recreate_steps<'def>(steps: &mut Vec<AutoMigrateStep<'def>>, key: ViewKey<'def>) {
107+
steps.retain(|step| {
108+
!matches!(
109+
step,
110+
AutoMigrateStep::UpdateView(existing) | AutoMigrateStep::ChangeAccess(existing) if *existing == key
111+
)
112+
});
113+
steps.extend([AutoMigrateStep::RemoveView(key), AutoMigrateStep::AddView(key)]);
114+
ensure_disconnect_all_users(steps);
115+
}
116+
117+
fn ensure_disconnect_all_users(steps: &mut Vec<AutoMigrateStep<'_>>) {
118+
if !steps
119+
.iter()
120+
.any(|step| matches!(step, AutoMigrateStep::DisconnectAllUsers))
121+
{
122+
steps.push(AutoMigrateStep::DisconnectAllUsers);
123+
}
124+
}
125+
29126
/// Update the database according to the migration plan.
30127
///
31128
/// The update is performed within the transactional context `tx`.
@@ -575,6 +672,95 @@ mod test {
575672
.expect("should be a valid module definition")
576673
}
577674

675+
fn view_module() -> ModuleDef {
676+
let mut builder = RawModuleDefV10Builder::new();
677+
let return_type_ref = builder.add_algebraic_type(
678+
[],
679+
"my_view_return_type",
680+
AlgebraicType::product([("a", AlgebraicType::U64)]),
681+
true,
682+
);
683+
builder.add_view(
684+
"my_view",
685+
0,
686+
true,
687+
true,
688+
ProductType::unit(),
689+
AlgebraicType::array(AlgebraicType::Ref(return_type_ref)),
690+
);
691+
builder.add_view_primary_key("my_view", ["a"]);
692+
builder
693+
.finish()
694+
.try_into()
695+
.expect("should be a valid module definition")
696+
}
697+
698+
fn old_view_backing_schema(module_def: &ModuleDef) -> TableSchema {
699+
let view = module_def.view("my_view").unwrap();
700+
let mut schema = TableSchema::from_view_def_for_datastore(module_def, view);
701+
702+
schema.columns.remove(0);
703+
for (pos, col) in schema.columns.iter_mut().enumerate() {
704+
col.col_pos = pos.into();
705+
}
706+
schema.indexes.clear();
707+
schema.constraints.clear();
708+
schema.reset();
709+
710+
schema
711+
}
712+
713+
fn create_backing_table(stdb: &TestDB, schema: TableSchema) -> anyhow::Result<()> {
714+
let mut tx = begin_mut_tx(stdb);
715+
stdb.create_table(&mut tx, schema)?;
716+
stdb.commit_tx(tx)?;
717+
Ok(())
718+
}
719+
720+
#[test]
721+
fn stale_view_backing_schema_forces_remove_add_view() -> anyhow::Result<()> {
722+
let stdb = TestDB::durable()?;
723+
let module_def = view_module();
724+
create_backing_table(&stdb, old_view_backing_schema(&module_def))?;
725+
726+
let mut plan = ponder_migrate(&module_def, &module_def)?;
727+
add_view_backing_table_recreate_steps(&stdb, &mut plan)?;
728+
729+
let MigratePlan::Auto(plan) = &plan else {
730+
panic!("expected auto migration");
731+
};
732+
let my_view = module_def.view("my_view").unwrap().key();
733+
assert!(plan.steps.contains(&AutoMigrateStep::RemoveView(my_view)));
734+
assert!(plan.steps.contains(&AutoMigrateStep::AddView(my_view)));
735+
assert!(plan.steps.contains(&AutoMigrateStep::DisconnectAllUsers));
736+
assert!(!plan.steps.contains(&AutoMigrateStep::UpdateView(my_view)));
737+
738+
Ok(())
739+
}
740+
741+
#[test]
742+
fn current_view_backing_schema_does_not_force_remove_add_view() -> anyhow::Result<()> {
743+
let stdb = TestDB::durable()?;
744+
let module_def = view_module();
745+
let view = module_def.view("my_view").unwrap();
746+
let backing_schema = TableSchema::from_view_def_for_datastore(&module_def, view);
747+
create_backing_table(&stdb, backing_schema)?;
748+
749+
let mut plan = ponder_migrate(&module_def, &module_def)?;
750+
add_view_backing_table_recreate_steps(&stdb, &mut plan)?;
751+
752+
let MigratePlan::Auto(plan) = &plan else {
753+
panic!("expected auto migration");
754+
};
755+
let my_view = view.key();
756+
assert!(plan.steps.contains(&AutoMigrateStep::UpdateView(my_view)));
757+
assert!(!plan.steps.contains(&AutoMigrateStep::RemoveView(my_view)));
758+
assert!(!plan.steps.contains(&AutoMigrateStep::AddView(my_view)));
759+
assert!(!plan.steps.contains(&AutoMigrateStep::DisconnectAllUsers));
760+
761+
Ok(())
762+
}
763+
578764
enum TakeSnapshot {
579765
None,
580766
BeforeAutomigration,

crates/schema/src/auto_migrate.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,14 +131,25 @@ impl MigrationPolicy {
131131
new_module_def: &'def ModuleDef,
132132
) -> anyhow::Result<MigratePlan<'def>, MigrationPolicyError> {
133133
let plan = ponder_migrate(old_module_def, new_module_def).map_err(MigrationPolicyError::AutoMigrateFailure)?;
134+
self.permits_migrate_plan(database_identity, old_module_hash, new_module_hash, &plan)?;
135+
Ok(plan)
136+
}
134137

138+
/// Validate an already-generated migration plan under this policy.
139+
pub fn permits_migrate_plan(
140+
&self,
141+
database_identity: Identity,
142+
old_module_hash: spacetimedb_lib::Hash,
143+
new_module_hash: spacetimedb_lib::Hash,
144+
plan: &MigratePlan<'_>,
145+
) -> anyhow::Result<(), MigrationPolicyError> {
135146
let token = MigrationToken {
136147
database_identity,
137148
old_module_hash,
138149
new_module_hash,
139150
};
140-
self.permits_plan(&plan, &token)?;
141-
Ok(plan)
151+
self.permits_plan(plan, &token)?;
152+
Ok(())
142153
}
143154
}
144155

0 commit comments

Comments
 (0)