Skip to content

Rollback safe system table additions - #5679

Open
jsdt wants to merge 5 commits into
masterfrom
jsdt/safe-system-table-additions
Open

Rollback safe system table additions#5679
jsdt wants to merge 5 commits into
masterfrom
jsdt/safe-system-table-additions

Conversation

@jsdt

@jsdt jsdt commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Description of Changes

TLDR: Write system table schema info to the commit log immediately for new databases.

Currently if we add new system tables, there is a specific case where trying to open a database with a previous version of spacetimedb can fail: if we create a new database with the added system table (and write something to that system table), we won't be able to replay the history for that database without a snapshot. In practice, this is an unlikely error case, since we always try to write a snapshot for a new database. Databases with a snapshot don't have a problem (because the new system table schema info is in the snapshot), and existing databases don't have a problem (because we create the new system tables in a transaction).

With this change, when we open a new database, we write all of the system table schema information to the commit log as the first transaction, so older versions will still be able to parse rows for those tables.

Expected complexity level and risk

  1. Anything with system tables carries higher risk, but this has pretty good test coverage.

Testing

This updates some existing tests to make sure that the schema information is written to the commit log, and it adds a few additional tests in relational_db. The most important one is probably replay_from_commitlog_preserves_unknown_future_system_table, which simulates a replay with an unknown system table.

@jsdt jsdt changed the title Jsdt/safe system table additions Rollback safe system table additions Aug 6, 2026
@jsdt
jsdt requested a review from gefjon August 6, 2026 15:24

@gefjon gefjon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this change the creation of a new on-disk database so that the initial snapshot is now at tx_offset 1, rather than zero?

Comment on lines +236 to +298
/// Return the rows which describe the built-in system table schemas.
///
/// These rows are inserted directly into committed state by
/// `CommittedState::bootstrap_system_tables`. Durable databases also write
/// them into the commit log before the first ordinary transaction so replay
/// from offset 0 can learn about system tables which are newer than the replay
/// binary's built-in catalog.
pub fn system_table_schema_rows() -> Vec<(TableId, ProductValue)> {
let schemas = system_tables();
let mut rows = Vec::new();

for schema in &schemas {
rows.push((
ST_TABLE_ID,
ProductValue::from(StTableRow {
table_id: schema.table_id,
table_name: schema.table_name.clone(),
table_type: StTableType::System,
table_access: schema.table_access,
table_primary_key: schema.primary_key.map(Into::into),
}),
));
}

for col in schemas.iter().flat_map(|schema| schema.columns()).cloned() {
rows.push((ST_COLUMN_ID, ProductValue::from(StColumnRow::from(col))));
}

for constraint in schemas.iter().flat_map(|schema| &schema.constraints) {
rows.push((
ST_CONSTRAINT_ID,
ProductValue::from(StConstraintRow {
constraint_id: constraint.constraint_id,
constraint_name: constraint.constraint_name.clone(),
table_id: constraint.table_id,
constraint_data: constraint.data.clone().into(),
}),
));
}

for index in schemas.iter().flat_map(|schema| &schema.indexes).cloned() {
rows.push((ST_INDEX_ID, ProductValue::from(StIndexRow::from(index))));
}

for seq in schemas.iter().flat_map(|schema| &schema.sequences) {
rows.push((
ST_SEQUENCE_ID,
ProductValue::from(StSequenceRow {
sequence_id: seq.sequence_id,
sequence_name: seq.sequence_name.clone(),
table_id: seq.table_id,
col_pos: seq.col_pos,
increment: seq.increment,
min_value: seq.min_value,
max_value: seq.max_value,
start: seq.start,
allocated: seq.start - 1,
}),
));
}

rows
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to combine this definition with that of CommittedState::bootstrap_system_tables? I feel at least a little uncomfortable having effectively two definitions of this same function that must be kept in sync.

Comment on lines +444 to +447
let mut rows_by_table = std::collections::BTreeMap::<_, Vec<_>>::new();
for (table_id, row) in system_table_schema_rows() {
rows_by_table.entry(table_id).or_default().push(row);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a possible implementation of my request in previous comment, could we read the rows out of the in-memory CommittedState rather than re-computing them by calling system_table_schema_rows?

@gefjon gefjon assigned jsdt and unassigned gefjon Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants