Skip to content

Stores written by 0.94 to 0.98 fail to open under 0.99+ with TableTypeMismatch #104

@cbenhagen

Description

@cbenhagen

Summary

Any redb file created or opened by iroh-docs 0.94 through 0.98 fails to open under 0.99 with TableError::TableTypeMismatch on the records-1 table (and two others). The db.upgrade() shim added in #69 runs as intended, but it does not actually cover what #100 needs it to cover. #100 then bumps redb straight from 2.6 to 4.1, skipping the only release line where the documented migration path can run.

Result: the on-disk store is now unreachable for anyone who upgrades to 0.99 without first running a redb 3.x release that wraps these three tables in Legacy<...>. There is no such iroh-docs release.

Repro

  1. Create a doc store with iroh-docs 0.98 (redb 2.6.3). The db.upgrade() call added in feat: upgrade redb to v3 compatible format #69 runs on first open and stamps the file format header as v3.
  2. Upgrade to iroh-docs 0.99 (redb 4.1.0).
  3. Open the same store. Tables::new(&write_tx) returns:
records-1 is of type Table<([u8;32],[u8;32],&[u8]), (u64,[u8;64],[u8;64],u64,[u8;32])>

The redb error formatter only prints the on-disk type, which makes this look tautological at first glance. It is in fact a mismatch on the type classification byte stored in the table metadata. See below.

Root cause

redb 3.0 changed how variable-width tuple types are tagged in table metadata. The type-name string is unchanged, but the leading classification byte goes from Internal = 1 to Internal2 = 3. From cberner/redb at src/tuple_types.rs in 4.1.0:

if Self::fixed_width().is_some() {
    TypeName::internal(&result)
} else {
    TypeName::internal2(&result)
}

Three tables in src/store/fs/tables.rs use a tuple containing &[u8], which makes the tuple variable-width:

  • RECORDS_TABLE (records-1), key (&[u8;32], &[u8;32], &[u8])
  • RECORDS_BY_KEY_TABLE (records-by-key-1), key (&[u8;32], &[u8], &[u8;32])
  • LATEST_PER_AUTHOR_TABLE (latest-by-author-1), value (u64, &[u8])

Stores written by redb 2.6 have these stamped with classification Internal = 1. redb 4.1 expects Internal2 = 3 for the same definitions. The type check fails on open.

The remaining tables (authors-1, namespaces-1, namespaces-2, sync-peers-1, download-policy-1) are fixed-width or non-tuple, so they keep the Internal = 1 classification under both redb versions and migrate cleanly.

What #69 attempted

#69 ("feat: upgrade redb to v3 compatible format") kept redb pinned at 2.6 and called db.upgrade() after Database::create():

let mut db = Database::create(&path)?;
match db.upgrade() {
    Ok(true) => info!("Database was upgraded to redb v3 compatible format"),
    Ok(false) => {}
    Err(err) => warn!(...),
}

The intent reads correctly: do the 2 to 3 migration that redb's CHANGELOG describes before crossing into a redb 3.x release. That part does run.

Why that wasn't enough

Database::upgrade() in redb 2.6.3 only updates the file format header. It does not walk tables and does not rewrite per-table type metadata. From redb-2.6.3/src/tree_store/page_store/page_manager.rs:

pub(crate) fn upgrade_to_v3(&mut self) -> Result {
    let data_root = self.get_data_root();
    let system_root = self.get_system_root();
    let transaction_id = self.get_last_committed_transaction_id()?;
    assert!(self.get_freed_root().is_none());

    let tracker_page = self.tracker_page();
    self.file_format = FILE_FORMAT_VERSION3;
    for _ in 0..2 {
        match self.commit(data_root, system_root, None, transaction_id, false, true) {
            Ok(()) => {}
            Err(err) => {
                self.storage.set_irrecoverable_io_error();
                return Err(err);
            }
        }
    }
    self.free(tracker_page, &mut PageTrackerPolicy::Ignore);
    Ok(())
}

That is the whole thing. The classification byte stamped into each table's type metadata at table creation time stays as Internal = 1. There is no mechanism in redb 2.6 to re-stamp it as Internal2 = 3, because redb 2.6 doesn't know Internal2 exists.

So #69 successfully bumps the file format header. It cannot, even in principle, fix the per-table classification metadata.

What the redb CHANGELOG actually says

From cberner/redb CHANGELOG.md:

redb 3.0.0:

Optimize storage of tuple types. (...) This encoding is not compatible with the previous format. To load tuple data created prior to version 3.0, wrap them in the Legacy type.

redb 4.0.0:

Remove Legacy type. To migrate off the Legacy type, use the Legacy type in the 3.x release and copy the data to a table with plain tuples, before upgrading to the 4.x release.

So the documented path is three steps, not two:

  1. redb 2.6: call db.upgrade() to migrate the file format header.
  2. redb 3.x: open the same store with RECORDS_TABLE, RECORDS_BY_KEY_TABLE, and LATEST_PER_AUTHOR_TABLE redefined to use Legacy<...> wrappers, read each row, write it back into the same table without the wrapper. That commit re-stamps the metadata as Internal2.
  3. redb 4.x: drop the wrappers entirely. Open normally.

#100 goes from step 1 to step 3 with no intermediate Legacy pass. After redb 4.0 dropped the Legacy type, there is no longer a way to do step 2 from inside an iroh-docs build that depends on redb 4.x. The migration window is closed for any store still on the old classification.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status
    ✅ Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions