Skip to content

Commit a2c3f23

Browse files
committed
Add fixup to remove duplicate st_sequence rows for improperly migrated system sequences
1 parent 982f0c6 commit a2c3f23

3 files changed

Lines changed: 116 additions & 9 deletions

File tree

crates/core/src/db/relational_db.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,12 @@ impl RelationalDB {
353353
self.database_identity,
354354
schema.table_name
355355
);
356+
// FIXME: `create_table` of tables with sequences
357+
// gives a different initial allocation than is given by `CommittedState::bootstrap_system_tables`.
358+
// See comment in that method.
359+
// This results in requiring `CommittedState::fixup_delete_duplicate_system_sequence_rows`.
360+
// Fix `migrate_system_tables` to create new system sequences
361+
// with the same initial allocation as `bootstrap_system_tables`.
356362
let _ = self.create_table(&mut tx, schema.clone())?;
357363
}
358364
}

crates/datastore/src/locking_tx_datastore/committed_state.rs

Lines changed: 98 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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
};
3536
use anyhow::anyhow;
3637
use core::{convert::Infallible, ops::RangeBounds};
37-
use spacetimedb_data_structures::map::{HashSet, IntMap, IntSet};
38+
use spacetimedb_data_structures::map::{HashMap, HashSet, IntMap, IntSet};
3839
use spacetimedb_durability::TxOffset;
3940
use 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};
4142
use spacetimedb_sats::{algebraic_value::de::ValueDeserializer, memory_usage::MemoryUsage, Deserialize};
4243
use spacetimedb_sats::{AlgebraicValue, ProductValue};
4344
use 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

crates/datastore/src/locking_tx_datastore/datastore.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,18 @@ impl Locking {
141141
/// There may eventually be better way to do this, but this will have to do for now.
142142
pub fn rebuild_state_after_replay(&self) -> Result<()> {
143143
let mut committed_state = self.committed_state.write_arc();
144+
145+
// Prior versions of `RelationalDb::migrate_system_tables` (defined in the `core` crate)
146+
// initialized newly-created system sequences to `allocation: 4097`,
147+
// while `committed_state::bootstrap_system_tables` sets `allocation: 4096`.
148+
// This affected the system table migration which added
149+
// `st_view_view_id_seq` and `st_view_arg_id_seq`.
150+
// As a result, when replaying these databases' commitlogs without a snapshot,
151+
// we will end up with two rows in `st_sequence` for each of these sequences,
152+
// resulting in a unique constraint violation in `CommittedState::build_indexes`.
153+
// We fix this by, for each system sequence, deleting all but the row with the highest allocation.
154+
committed_state.fixup_delete_duplicate_system_sequence_rows();
155+
144156
// `build_missing_tables` must be called before indexes.
145157
// Honestly this should maybe just be one big procedure.
146158
// See John Carmack's philosophy on this.

0 commit comments

Comments
 (0)