11use super :: relational_db:: RelationalDB ;
22use crate :: sql:: rls:: RowLevelExpr ;
33use 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 } ;
58use spacetimedb_lib:: db:: auth:: StTableType ;
69use spacetimedb_lib:: identity:: AuthCtx ;
710use spacetimedb_lib:: AlgebraicValue ;
811use 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 } ;
1114use 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 ,
0 commit comments