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 } ;
11- use spacetimedb_schema:: schema:: { column_schemas_from_defs, IndexSchema , Schema , SequenceSchema , TableSchema } ;
12+ use spacetimedb_schema:: auto_migrate:: { AutoMigratePlan , AutoMigrateStep , ManualMigratePlan , MigratePlan } ;
13+ use spacetimedb_schema:: def:: { ModuleDef , ModuleDefLookup , TableDef , ViewDef } ;
14+ use spacetimedb_schema:: schema:: {
15+ column_schemas_from_defs, ConstraintSchema , IndexSchema , Schema , SequenceSchema , TableSchema ,
16+ } ;
1217
1318/// The logger used for by [`update_database`] and friends.
1419pub trait UpdateLogger {
@@ -24,6 +29,91 @@ pub enum UpdateResult {
2429 EvaluateSubscribedViews ,
2530}
2631
32+ /// Build a repair-only migration plan for views whose stored backing table row
33+ /// layout is stale compared to the currently loaded module definition.
34+ pub fn stale_view_backing_table_recreate_plan < ' def > (
35+ stdb : & RelationalDB ,
36+ module_def : & ' def ModuleDef ,
37+ ) -> anyhow:: Result < Option < MigratePlan < ' def > > > {
38+ let steps = stale_view_backing_table_recreate_steps ( stdb, module_def) ?;
39+ if steps. is_empty ( ) {
40+ return Ok ( None ) ;
41+ }
42+
43+ Ok ( Some ( MigratePlan :: Auto ( AutoMigratePlan {
44+ old : module_def,
45+ new : module_def,
46+ prechecks : Vec :: new ( ) ,
47+ steps,
48+ } ) ) )
49+ }
50+
51+ fn stale_view_backing_table_recreate_steps < ' def > (
52+ stdb : & RelationalDB ,
53+ module_def : & ' def ModuleDef ,
54+ ) -> anyhow:: Result < Vec < AutoMigrateStep < ' def > > > {
55+ stdb. with_read_only ( Workload :: Internal , |tx| -> anyhow:: Result < _ > {
56+ let mut steps = Vec :: new ( ) ;
57+
58+ for view in module_def. views ( ) {
59+ if view_backing_table_needs_recreate ( stdb, tx, module_def, view) ? {
60+ steps. extend ( [
61+ AutoMigrateStep :: RemoveView ( view. key ( ) ) ,
62+ AutoMigrateStep :: AddView ( view. key ( ) ) ,
63+ ] ) ;
64+ }
65+ }
66+
67+ if !steps. is_empty ( ) {
68+ ensure_disconnect_all_users ( & mut steps) ;
69+ steps. sort ( ) ;
70+ }
71+
72+ Ok ( steps)
73+ } )
74+ }
75+
76+ fn view_backing_table_needs_recreate (
77+ stdb : & RelationalDB ,
78+ tx : & mut TxId ,
79+ module_def : & ModuleDef ,
80+ view : & ViewDef ,
81+ ) -> anyhow:: Result < bool > {
82+ let Some ( table_id) = view_backing_table_id ( tx, view) ? else {
83+ return Ok ( false ) ;
84+ } ;
85+
86+ let actual = stdb. schema_for_table ( tx, table_id) ?;
87+ let expected = TableSchema :: from_view_def_for_datastore ( module_def, view) ;
88+
89+ Ok ( view_backing_row_layout_changed ( & actual, & expected) )
90+ }
91+
92+ fn view_backing_table_id ( tx : & mut TxId , view : & ViewDef ) -> anyhow:: Result < Option < TableId > > {
93+ let view_name = AlgebraicValue :: from ( <Box < str > >:: from ( & * view. name ) ) ;
94+ let Some ( row) = tx
95+ . iter_by_col_eq ( ST_VIEW_ID , StViewFields :: ViewName , & view_name) ?
96+ . next ( )
97+ else {
98+ return Ok ( None ) ;
99+ } ;
100+
101+ Ok ( StViewRow :: try_from ( row) ?. table_id )
102+ }
103+
104+ fn view_backing_row_layout_changed ( actual : & TableSchema , expected : & TableSchema ) -> bool {
105+ actual. row_type != expected. row_type
106+ }
107+
108+ fn ensure_disconnect_all_users ( steps : & mut Vec < AutoMigrateStep < ' _ > > ) {
109+ if !steps
110+ . iter ( )
111+ . any ( |step| matches ! ( step, AutoMigrateStep :: DisconnectAllUsers ) )
112+ {
113+ steps. push ( AutoMigrateStep :: DisconnectAllUsers ) ;
114+ }
115+ }
116+
27117/// Update the database according to the migration plan.
28118///
29119/// The update is performed within the transactional context `tx`.
@@ -550,6 +640,86 @@ mod test {
550640 . expect ( "should be a valid module definition" )
551641 }
552642
643+ fn view_module ( ) -> ModuleDef {
644+ let mut builder = RawModuleDefV10Builder :: new ( ) ;
645+ let return_type_ref = builder. add_algebraic_type (
646+ [ ] ,
647+ "my_view_return_type" ,
648+ AlgebraicType :: product ( [ ( "a" , AlgebraicType :: U64 ) ] ) ,
649+ true ,
650+ ) ;
651+ builder. add_view (
652+ "my_view" ,
653+ 0 ,
654+ true ,
655+ true ,
656+ ProductType :: unit ( ) ,
657+ AlgebraicType :: array ( AlgebraicType :: Ref ( return_type_ref) ) ,
658+ ) ;
659+ builder. add_view_primary_key ( "my_view" , [ "a" ] ) ;
660+ builder
661+ . finish ( )
662+ . try_into ( )
663+ . expect ( "should be a valid module definition" )
664+ }
665+
666+ fn old_view_backing_schema ( module_def : & ModuleDef ) -> TableSchema {
667+ let view = module_def. view ( "my_view" ) . unwrap ( ) ;
668+ let mut schema = TableSchema :: from_view_def_for_datastore ( module_def, view) ;
669+
670+ schema. columns . remove ( 0 ) ;
671+ for ( pos, col) in schema. columns . iter_mut ( ) . enumerate ( ) {
672+ col. col_pos = pos. into ( ) ;
673+ }
674+ schema. indexes . clear ( ) ;
675+ schema. constraints . clear ( ) ;
676+ schema. reset ( ) ;
677+
678+ schema
679+ }
680+
681+ fn create_backing_table ( stdb : & TestDB , schema : TableSchema ) -> anyhow:: Result < ( ) > {
682+ let mut tx = begin_mut_tx ( stdb) ;
683+ stdb. create_table ( & mut tx, schema) ?;
684+ stdb. commit_tx ( tx) ?;
685+ Ok ( ( ) )
686+ }
687+
688+ #[ test]
689+ fn stale_view_backing_schema_generates_startup_repair_plan ( ) -> anyhow:: Result < ( ) > {
690+ let stdb = TestDB :: durable ( ) ?;
691+ let module_def = view_module ( ) ;
692+ create_backing_table ( & stdb, old_view_backing_schema ( & module_def) ) ?;
693+
694+ let plan = stale_view_backing_table_recreate_plan ( & stdb, & module_def) ?. expect ( "expected repair plan" ) ;
695+
696+ let MigratePlan :: Auto ( plan) = & plan else {
697+ panic ! ( "expected auto migration" ) ;
698+ } ;
699+ let my_view = module_def. view ( "my_view" ) . unwrap ( ) . key ( ) ;
700+ assert ! ( plan. steps. contains( & AutoMigrateStep :: RemoveView ( my_view) ) ) ;
701+ assert ! ( plan. steps. contains( & AutoMigrateStep :: AddView ( my_view) ) ) ;
702+ assert ! ( plan. steps. contains( & AutoMigrateStep :: DisconnectAllUsers ) ) ;
703+ assert ! ( !plan. steps. contains( & AutoMigrateStep :: UpdateView ( my_view) ) ) ;
704+
705+ Ok ( ( ) )
706+ }
707+
708+ #[ test]
709+ fn current_view_backing_schema_skips_startup_repair_plan ( ) -> anyhow:: Result < ( ) > {
710+ let stdb = TestDB :: durable ( ) ?;
711+ let module_def = view_module ( ) ;
712+ let view = module_def. view ( "my_view" ) . unwrap ( ) ;
713+ let backing_schema = TableSchema :: from_view_def_for_datastore ( & module_def, view) ;
714+ create_backing_table ( & stdb, backing_schema) ?;
715+
716+ let plan = stale_view_backing_table_recreate_plan ( & stdb, & module_def) ?;
717+
718+ assert ! ( plan. is_none( ) , "{plan:#?}" ) ;
719+
720+ Ok ( ( ) )
721+ }
722+
553723 enum TakeSnapshot {
554724 None ,
555725 BeforeAutomigration ,
0 commit comments