@@ -16,12 +16,13 @@ use crate::{
1616 IterByColRangeTx ,
1717 } ,
1818 system_tables:: {
19- is_built_in_meta_row, system_tables, StColumnRow , StConstraintData , StConstraintRow , StIndexRow , StSequenceRow ,
20- StTableFields , StTableRow , StViewRow , SystemTable , ST_CLIENT_ID , ST_CLIENT_IDX , ST_COLUMN_ID , ST_COLUMN_IDX ,
21- ST_COLUMN_NAME , ST_CONSTRAINT_ID , ST_CONSTRAINT_IDX , ST_CONSTRAINT_NAME , ST_INDEX_ID , ST_INDEX_IDX ,
22- ST_INDEX_NAME , ST_MODULE_ID , ST_MODULE_IDX , ST_ROW_LEVEL_SECURITY_ID , ST_ROW_LEVEL_SECURITY_IDX ,
23- ST_SCHEDULED_ID , ST_SCHEDULED_IDX , ST_SEQUENCE_ID , ST_SEQUENCE_IDX , ST_SEQUENCE_NAME , ST_TABLE_ID ,
24- ST_TABLE_IDX , ST_VAR_ID , ST_VAR_IDX , ST_VIEW_ARG_ID , ST_VIEW_ARG_IDX ,
19+ is_built_in_meta_row, system_tables, table_id_is_reserved, StColumnRow , StConstraintData , StConstraintRow ,
20+ StIndexRow , StSequenceFields , StSequenceRow , StTableFields , StTableRow , StViewRow , SystemTable , ST_CLIENT_ID ,
21+ ST_CLIENT_IDX , ST_COLUMN_ID , ST_COLUMN_IDX , ST_COLUMN_NAME , ST_CONSTRAINT_ID , ST_CONSTRAINT_IDX ,
22+ ST_CONSTRAINT_NAME , ST_INDEX_ID , ST_INDEX_IDX , ST_INDEX_NAME , ST_MODULE_ID , ST_MODULE_IDX ,
23+ ST_ROW_LEVEL_SECURITY_ID , ST_ROW_LEVEL_SECURITY_IDX , ST_SCHEDULED_ID , ST_SCHEDULED_IDX , ST_SEQUENCE_ID ,
24+ ST_SEQUENCE_IDX , ST_SEQUENCE_NAME , ST_TABLE_ID , ST_TABLE_IDX , ST_VAR_ID , ST_VAR_IDX , ST_VIEW_ARG_ID ,
25+ ST_VIEW_ARG_IDX ,
2526 } ,
2627 traits:: { EphemeralTables , TxData } ,
2728} ;
@@ -34,10 +35,10 @@ use crate::{
3435} ;
3536use anyhow:: anyhow;
3637use core:: { convert:: Infallible , ops:: RangeBounds } ;
37- use spacetimedb_data_structures:: map:: { HashSet , IntMap , IntSet } ;
38+ use spacetimedb_data_structures:: map:: { HashMap , HashSet , IntMap , IntSet } ;
3839use spacetimedb_durability:: TxOffset ;
3940use spacetimedb_lib:: { db:: auth:: StTableType , Identity } ;
40- use spacetimedb_primitives:: { ColId , ColList , ColSet , IndexId , TableId , ViewId } ;
41+ use spacetimedb_primitives:: { ColId , ColList , ColSet , IndexId , SequenceId , TableId , ViewId } ;
4142use spacetimedb_sats:: { algebraic_value:: de:: ValueDeserializer , memory_usage:: MemoryUsage , Deserialize } ;
4243use spacetimedb_sats:: { AlgebraicValue , ProductValue } ;
4344use spacetimedb_schema:: {
@@ -201,6 +202,93 @@ impl CommittedState {
201202 }
202203 }
203204
205+ /// Delete all but the highest-allocation `st_sequence` row for each system sequence.
206+ ///
207+ /// Prior versions of `RelationalDb::migrate_system_tables` (defined in the `core` crate)
208+ /// initialized newly-created system sequences to `allocation: 4097`,
209+ /// while `committed_state::bootstrap_system_tables` sets `allocation: 4096`.
210+ /// This affected the system table migration which added
211+ /// `st_view_view_id_seq` and `st_view_arg_id_seq`.
212+ /// As a result, when replaying these databases' commitlogs without a snapshot,
213+ /// we will end up with two rows in `st_sequence` for each of these sequences,
214+ /// resulting in a unique constraint violation in `CommittedState::build_indexes`.
215+ /// We call this method in [`super::datastore::Locking::rebuild_state_after_replay`]
216+ /// to avoid that unique constraint violation.
217+ pub ( super ) fn fixup_delete_duplicate_system_sequence_rows ( & mut self ) {
218+ struct StSequenceRowInfo {
219+ sequence_id : SequenceId ,
220+ allocated : i128 ,
221+ row_pointer : RowPointer ,
222+ }
223+
224+ // Get all the `st_sequence` rows which refer to sequences on system tables,
225+ // including any duplicates caused by the bug described above.
226+ let sequence_rows = self
227+ . table_scan ( ST_SEQUENCE_ID )
228+ . expect ( "`st_sequence` should exist" )
229+ . filter_map ( |row_ref| {
230+ // Read the table ID to which the sequence refers,
231+ // in order to determine if this is a system sequence or not.
232+ let table_id = row_ref
233+ . read_col :: < TableId > ( StSequenceFields :: TableId )
234+ . expect ( "`st_sequence` row should conform to `st_sequence` schema" ) ;
235+
236+ // If this sequence refers to a system table, it may need a fixup.
237+ // User tables' sequences will never need fixups.
238+ table_id_is_reserved ( table_id) . then ( || {
239+ let allocated = row_ref
240+ . read_col :: < i128 > ( StSequenceFields :: Allocated )
241+ . expect ( "`st_sequence` row should conform to `st_sequence` schema" ) ;
242+ let sequence_id = row_ref
243+ . read_col :: < SequenceId > ( StSequenceFields :: SequenceId )
244+ . expect ( "`st_sequence` row should conform to `st_sequence` schema" ) ;
245+ StSequenceRowInfo {
246+ allocated,
247+ sequence_id,
248+ row_pointer : row_ref. pointer ( ) ,
249+ }
250+ } )
251+ } )
252+ . collect :: < Vec < _ > > ( ) ;
253+
254+ let ( st_sequence, blob_store, ..) = self
255+ . get_table_and_blob_store_mut ( ST_SEQUENCE_ID )
256+ . expect ( "`st_sequence` should exist" ) ;
257+
258+ // Track the row with the highest allocation for each sequence.
259+ let mut highest_allocations: HashMap < SequenceId , ( i128 , RowPointer ) > = HashMap :: default ( ) ;
260+
261+ for StSequenceRowInfo {
262+ sequence_id,
263+ allocated,
264+ row_pointer,
265+ } in sequence_rows
266+ {
267+ // For each `st_sequence` row which refers to a system table,
268+ // if we've already seen a row for the same sequence,
269+ // keep only the row with the higher allocation.
270+ if let Some ( ( prev_allocated, prev_row_pointer) ) =
271+ highest_allocations. insert ( sequence_id, ( allocated, row_pointer) )
272+ {
273+ // We have a duplicate row. We want to keep whichever has the higher `allocated`,
274+ // and delete the other.
275+ let row_pointer_to_delete = if prev_allocated > allocated {
276+ // The previous row has a higher allocation than the new row,
277+ // so delete the new row and restore `previous` to `highest_allocations`.
278+ highest_allocations. insert ( sequence_id, ( prev_allocated, prev_row_pointer) ) ;
279+ row_pointer
280+ } else {
281+ // The previous row does not have a higher allocation than the new,
282+ // so delete the previous row and keep the new one.
283+ prev_row_pointer
284+ } ;
285+
286+ st_sequence. delete ( blob_store, row_pointer_to_delete, |_| ( ) )
287+ . expect ( "Duplicated `st_sequence` row at `row_pointer_to_delete` should be present in `st_sequence` during fixup" ) ;
288+ }
289+ }
290+ }
291+
204292 /// Extremely delicate function to bootstrap the system tables.
205293 /// Don't update this unless you know what you're doing.
206294 pub ( super ) fn bootstrap_system_tables ( & mut self , database_identity : Identity ) -> Result < ( ) > {
@@ -442,11 +530,12 @@ impl CommittedState {
442530 Err ( InsertError :: IndexError ( e) ) => return Err ( IndexError :: UniqueConstraintViolation ( e) . into ( ) ) ,
443531 } ;
444532
533+ let row_ptr = row_ref. pointer ( ) ;
534+
445535 if table_id == ST_COLUMN_ID {
446536 // We've made a modification to `st_column`.
447537 // The type of a table has changed, so figure out which.
448538 // The first column in `StColumnRow` is `table_id`.
449- let row_ptr = row_ref. pointer ( ) ;
450539 self . st_column_changed ( row, row_ptr) ?;
451540 }
452541
0 commit comments