Skip to content

feat(l1): add automatic DB migration framework#6519

Merged
azteca1998 merged 7 commits intomainfrom
feat/automatic-db-migrations
Apr 29, 2026
Merged

feat(l1): add automatic DB migration framework#6519
azteca1998 merged 7 commits intomainfrom
feat/automatic-db-migrations

Conversation

@azteca1998
Copy link
Copy Markdown
Contributor

Summary

  • Adds a migration framework to ethrex-storage so schema version bumps run upgrade functions automatically instead of forcing a full resync
  • MIGRATIONS array in migrations.rs maps each version step (index 0 = v1→v2, index 1 = v2→v3, etc.) with a compile-time assert to keep it in sync with STORE_SCHEMA_VERSION
  • Store::new detects the on-disk version and runs pending migrations before proceeding, writing metadata with fsync after each step for crash safety
  • has_valid_db now accepts any migratable version (1..=STORE_SCHEMA_VERSION)

Files changed

File Change
crates/storage/migrations.rs NewMigrationFn type, MIGRATIONS array, run_pending_migrations(), unit tests
crates/storage/lib.rs Add pub mod migrations, update docstring
crates/storage/store.rs Replace validate_store_schema_versionread_store_schema_version, restructure Store::new with migration support, update has_valid_db
crates/storage/error.rs Add MigrationFailed variant
cmd/ethrex/initializers.rs Catch MigrationFailed at startup

How to add a future migration

  1. Write fn migrate_1_to_2(backend: &dyn StorageBackend) -> Result<(), StoreError> in migrations.rs
  2. Append to MIGRATIONS array (index 0)
  3. Bump STORE_SCHEMA_VERSION to 2 in lib.rs
  4. Compile-time assert ensures array length matches

Test plan

  • cargo check -p ethrex-storage — clean
  • cargo check -p ethrex — clean
  • cargo test -p ethrex-storage — 3 unit tests pass (array length, no-op migration, metadata correctness)

Instead of forcing a full resync on every schema version bump, the
storage layer now runs migration functions automatically. The
MIGRATIONS array in migrations.rs maps each version upgrade (v1→v2
is index 0, v2→v3 is index 1, etc.), with a compile-time assert
ensuring the array length stays in sync with STORE_SCHEMA_VERSION.

Store::new detects the on-disk version and runs pending migrations
before proceeding, writing metadata with fsync after each step for
crash safety. has_valid_db now accepts any migratable version
(1..=STORE_SCHEMA_VERSION).

Currently empty (version stays at 1) — this is the framework only.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 23, 2026

Lines of code report

Total lines added: 109
Total lines removed: 0
Total lines changed: 109

Detailed view
+-------------------------------------+-------+------+
| File                                | Lines | Diff |
+-------------------------------------+-------+------+
| ethrex/cmd/ethrex/initializers.rs   | 648   | +5   |
+-------------------------------------+-------+------+
| ethrex/crates/storage/error.rs      | 53    | +4   |
+-------------------------------------+-------+------+
| ethrex/crates/storage/lib.rs        | 16    | +1   |
+-------------------------------------+-------+------+
| ethrex/crates/storage/migrations.rs | 78    | +78  |
+-------------------------------------+-------+------+
| ethrex/crates/storage/store.rs      | 2647  | +21  |
+-------------------------------------+-------+------+

@azteca1998 azteca1998 changed the title feat(storage): add automatic DB migration framework feat(l1): add automatic DB migration framework Apr 23, 2026
@github-actions github-actions Bot added the L1 Ethereum client label Apr 23, 2026
@azteca1998 azteca1998 marked this pull request as ready for review April 23, 2026 14:10
@azteca1998 azteca1998 requested a review from a team as a code owner April 23, 2026 14:10
@ethrex-project-sync ethrex-project-sync Bot moved this to In Review in ethrex_l1 Apr 23, 2026
@github-actions
Copy link
Copy Markdown

🤖 Claude Code Review

I have enough from the diff to write a thorough review. Here is the full analysis:


Code Review: PR #6519 — Automatic DB Migration Framework

Overview

This PR introduces a lightweight migration framework for ethrex-storage so that schema version bumps can run upgrade functions automatically instead of requiring a full resync. The overall design is sound and the compile-time invariant check is a nice touch. There are, however, a few correctness and safety issues worth addressing before merging.


Critical Issues

1. Metadata write is NOT crash-safe despite the claim

write_metadata_version in migrations.rs (lines 74–83) uses File::create + write_all + sync_all. File::create truncates the existing file to 0 bytes before any new content is written. If the process or OS crashes between the truncate and the completion of write_all, the metadata file will be empty or partial. On the next startup, serde_json::from_str on empty/corrupt content returns an error that bubbles up through read_store_schema_version, leaving the user with a database that:

  • Has already had some migration writes applied (the migration function ran successfully before the crash), and
  • Has a corrupt or empty metadata file that prevents the store from opening at all.

A truly atomic write requires the write-to-tempfile-then-rename pattern:

let tmp_path = metadata_path.with_extension("tmp");
let mut file = std::fs::File::create(&tmp_path)?;
file.write_all(serialized.as_bytes())?;
file.sync_all()?;
std::fs::rename(&tmp_path, &metadata_path)?;
// Optionally: sync the parent directory fd for durability on Linux

The claim in the PR description ("writing metadata with fsync after each step for crash safety") is currently not accurate.

2. No rollback on partial migration

If MIGRATIONS[idx](backend) succeeds but write_metadata_version fails (e.g., disk full), the migration data has been written but the version hasn't advanced. On the next startup, the same migration will re-run against already-migrated data. For idempotent migrations this is fine, but the framework imposes no idempotency requirement on MigrationFn. This should either be explicitly documented as a requirement, or the framework should detect and handle partial-migration state.


Design Issues

3. Migration silently falls back to IncompatibleDBVersion for non-rocksdb backends

The migration arm is gated on #[cfg(feature = "rocksdb")]. For any other backend (e.g., libmdbx), a database with v < STORE_SCHEMA_VERSION hits the #[cfg(not(feature = "rocksdb"))] arm and returns IncompatibleDBVersion — i.e., the user is forced to resync. This is probably intentional for the initial implementation but the asymmetry is invisible to contributors writing migration functions. A comment in migrations.rs and in the "How to add a future migration" guide in the PR description should call this out explicitly.

4. StoreMetadata visibility leak

StoreMetadata and schema_version are now pub solely to satisfy the unit test in migrations.rs that deserializes the file. The struct's internal layout is an implementation detail of the storage crate. Options:

  • Keep it pub(crate) and move the test that reads it into store.rs, or
  • Re-export it from migrations.rs as pub(crate) only, not from the crate root.

5. pub mod migrations exposes internal migration machinery

run_pending_migrations and write_metadata_version are only ever called from store.rs. Exporting the whole module pub invites external callers to bypass the normal Store::new initialization path. Consider pub(crate) mod migrations unless there is a planned external use case.


Minor Issues

6. Redundant runtime assert

run_pending_migrations (line 47) has a runtime assert! that duplicates the compile-time const _: () assert above it. Since the compile-time check already fires for any build where the array length is wrong, the runtime assert adds no value and will never be reached. Remove it.

7. Test isolation: shared temp directory paths

Both tests in the tests module use fixed subdirectory names under std::env::temp_dir() (e.g., "ethrex_migration_test_noop"). When cargo test runs tests in parallel, two test threads can collide on the same path. Use tempfile::TempDir (already a dev-dependency in many Rust projects) or append a unique suffix (e.g., a random UUID) to the path.

8. run_pending_migrations_noop_when_current is a weak test

The test confirms Ok(()) is returned but doesn't verify that no file I/O occurred (i.e., the metadata file wasn't needlessly rewritten). This is low-severity but worth noting.

9. The Some(v) if v < 1 arm is unreachable in practice

init_metadata_file always writes STORE_SCHEMA_VERSION which is currently 1. Version 0 could only appear via external corruption. The arm is harmless but worth a comment to that effect so future readers don't wonder why it's there.


What Looks Good

  • The compile-time const _: () assert tying MIGRATIONS.len() to STORE_SCHEMA_VERSION - 1 is a clean design that prevents contributors from forgetting to add a migration function.
  • Splitting validate_store_schema_version into read_store_schema_version (returning Option<u64>) cleanly separates the read from the decision logic.
  • The has_valid_db change to >= 1 && <= STORE_SCHEMA_VERSION is correct.
  • Error message in initializers.rs correctly warns about potential inconsistency and suggests removedb.
  • The early-return migration path reuses the same Arc<dyn StorageBackend> for Self::from_backend, avoiding a double-open of the database.

Summary

The most important fix before merging is Item 1 (atomic metadata write) — without it the "crash safety" guarantee doesn't hold and a power failure at the wrong moment can permanently brick the database. Item 2 (no rollback / idempotency contract) should at minimum be documented. The remaining items are lower-severity but Items 4–5 are worth cleaning up to avoid exposing internals that don't need to be public.


Automated review by Claude (Anthropic) · sonnet · custom prompt

@github-actions
Copy link
Copy Markdown

🤖 Kimi Code Review

Review of the database migration framework PR. Overall this is a solid implementation with proper crash safety considerations (fsync after each migration step) and good error handling. Below are specific findings:

crates/storage/migrations.rs

  1. Redundant runtime assertion (Line 54-57): The compile-time assert on Lines 30-33 already guarantees this invariant. The runtime check in run_pending_migrations adds no value since MIGRATIONS and STORE_SCHEMA_VERSION are both constants.

  2. Potential integer underflow (Line 59): If current_version is 0, (version - 1) as usize will underflow. While the caller in store.rs guards against this (Line 1471-1475), this function should document the precondition current_version >= 1 or use checked_sub to be defensive.

  3. Idempotency requirement (Lines 46-52): The comment mentions crash safety via fsync, but doesn't document that individual migrations must be idempotent. If a crash occurs between a migration's backend changes and the metadata write, the migration will rerun on restart. Consider adding a doc comment warning that MigrationFn implementations must handle being called multiple times safely.

crates/storage/store.rs

  1. Inconsistent error context (Lines 1460-1463 vs 1476-1480): The pre-metadata DB error (Lines 1460-1463) and the v < 1 error (Lines 1476-1480) both return IncompatibleDBVersion, but the pre-metadata case returns NotFoundDBVersion. Consider using IncompatibleDBVersion for both with found: 0 for the pre-metadata case, or document why they need different variants.

  2. Early return bypasses engine_type match (Lines 1485-1490): The migration path returns early via return Self::from_backend(...). While functionally correct (and matches the RocksDB arm logic), this creates a maintenance hazard if additional initialization is added to the match engine_type block later. Consider adding a comment explaining that this early return intentionally mirrors the RocksDB arm below


Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 23, 2026

Greptile Summary

This PR introduces an automatic DB migration framework for ethrex-storage: Store::new now reads the on-disk schema version and runs pending migration functions (gated to RocksDB) before proceeding, with a compile-time assert keeping MIGRATIONS.len() in sync with STORE_SCHEMA_VERSION. The design is sound, but there is one reliability issue worth addressing before merge:

  • write_metadata_version (called after each migration step) uses File::create (truncate-in-place) + write_all + sync_all. This is not atomic: a crash after truncation but before all bytes are flushed leaves an empty or partial JSON file, making the database unreadable on next startup even though the RocksDB migration already succeeded. The standard POSIX pattern is write-to-temp → fsync → rename.

Confidence Score: 4/5

Safe to merge after fixing the non-atomic metadata write, which can make a successfully migrated DB unrecoverable on crash.

One P1 finding: write_metadata_version is not crash-atomic, directly undermining the crash-safety guarantee advertised by this PR. The remaining findings are P2 (dead code, test temp-path hygiene). Score is 4 because the P1 is straightforward to fix with a write-tmp+rename pattern.

crates/storage/migrations.rs — write_metadata_version needs atomic write (temp file + rename)

Important Files Changed

Filename Overview
crates/storage/migrations.rs New migration framework — contains a P1 crash-safety bug: write_metadata_version uses File::create (truncate-in-place) rather than the atomic write-tmp+rename pattern, risking a corrupt metadata file on crash mid-write. Also uses fixed temp paths in tests.
crates/storage/store.rs Store::new restructured to detect on-disk version and dispatch to run_pending_migrations; has_valid_db broadened to accept any version in 1..=STORE_SCHEMA_VERSION. The #[cfg(not(feature = "rocksdb"))] migration arm is unreachable dead code.
crates/storage/error.rs Adds MigrationFailed { from, to, reason } variant to StoreError — clean addition.
crates/storage/lib.rs Exposes pub mod migrations and updates the STORE_SCHEMA_VERSION docstring to guide future contributors.
cmd/ethrex/initializers.rs Adds MigrationFailed arm to the Store::new error handler with a user-facing message directing operators to removedb.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Store::new called] --> B{engine_type == InMemory?}
    B -- Yes --> Z[Open InMemoryBackend]
    B -- No --> C[read_store_schema_version]
    C --> D{metadata.json exists?}
    D -- No, dir non-empty --> E[Err: NotFoundDBVersion\npre-metadata DB]
    D -- No, dir empty/missing --> F[init_metadata_file\nwrite v=STORE_SCHEMA_VERSION]
    F --> Z2[Open RocksDBBackend normally]
    D -- Yes --> G{version value?}
    G -- v < 1 --> H[Err: IncompatibleDBVersion]
    G -- v > STORE_SCHEMA_VERSION --> H
    G -- v < STORE_SCHEMA_VERSION & rocksdb on --> I[Open RocksDBBackend\nrun_pending_migrations]
    I --> J{migration loop\nv..STORE_SCHEMA_VERSION}
    J -- per step --> K[MIGRATIONS idx apply fn]
    K --> L[write_metadata_version\nFile::create + write_all + sync_all\n⚠️ NOT atomic]
    L --> J
    J -- done --> M[from_backend return Store]
    G -- v == STORE_SCHEMA_VERSION --> Z2
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: crates/storage/migrations.rs
Line: 74-84

Comment:
**Non-atomic metadata write contradicts crash-safety claim**

`write_metadata_version` is documented as "Atomically writes" but it is not atomic. `File::create` truncates the existing file immediately; if the process crashes after the truncation but before `write_all` completes, the metadata file will be empty or partial. `serde_json::from_str` will then fail on the next startup, making the database inaccessible even though the underlying RocksDB migration completed successfully.

The safe pattern for POSIX filesystems is: write to a sibling temp file → `sync_all()``rename()` (which is atomic).

```rust
fn write_metadata_version(db_path: &Path, version: u64) -> Result<(), StoreError> {
    let metadata_path = db_path.join(STORE_METADATA_FILENAME);
    let tmp_path = db_path.join(format!("{}.tmp", STORE_METADATA_FILENAME));
    let metadata = StoreMetadata::new(version);
    let serialized = serde_json::to_string_pretty(&metadata)?;

    let mut file = std::fs::File::create(&tmp_path)?;
    file.write_all(serialized.as_bytes())?;
    file.sync_all()?;
    std::fs::rename(&tmp_path, &metadata_path)?;

    Ok(())
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: crates/storage/store.rs
Line: 1490-1496

Comment:
**Unreachable dead code in `#[cfg(not(feature = "rocksdb"))]` branch**

`EngineType` only has two variants: `InMemory` and `#[cfg(feature = "rocksdb")] RocksDB`. The outer guard is `if engine_type != EngineType::InMemory`, so when the `rocksdb` feature is disabled, this block is never entered (there is no non-`InMemory` engine type to select). This arm can therefore never be reached and will silently become dead code, which may confuse future contributors writing their first migration.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: crates/storage/migrations.rs
Line: 105-106

Comment:
**Fixed temp paths risk test flakiness under parallel test runners**

Both tests use a fixed path under `std::env::temp_dir()` (e.g. `ethrex_migration_test_noop`). If `cargo test` is run with parallelism across multiple invocations, or if a previous run left the directory behind from a failed cleanup, tests can observe stale state or race with one another. Consider using `tempfile::TempDir` (or `std::env::temp_dir().join(format!(".._{}", std::process::id()))`) to guarantee unique, isolated directories.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "style(l1): fix formatting in store.rs" | Re-trigger Greptile

@github-actions
Copy link
Copy Markdown

🤖 Codex Code Review

  1. High: migration progress is persisted more durably than the migrated data itself. In crates/storage/migrations.rs:59 the migration step runs first, then write_metadata_version() advances metadata.json. But RocksDB commits use plain db.write(batch) with no synchronous write option, while the JSON file is explicitly sync_all()’d. With the current RocksDB settings (rocksdb.rs:54), a crash/power loss can leave metadata.json at vN+1 while the actual DB changes from the migration step were never made durable. On restart, Store::new will skip the migration and open inconsistent state. The schema version needs to be advanced in the same durable store/transaction as the migration, or each migration step needs an explicit durable barrier before updating metadata.

  2. Medium: the “atomic” metadata writer is not atomic and is not actually crash-safe. write_metadata_version() overwrites metadata.json in place via File::create, which truncates first, then writes and fsyncs the file. A crash between truncate and full write can leave partial/invalid JSON. read_store_schema_version() then fails startup on parse. If the intent is crash safety, this needs the usual temp-file -> sync_all -> rename -> fsync parent dir sequence.

Aside from those durability issues, the Store::new control flow and the user-facing handling for MigrationFailed look reasonable.

I couldn’t run the storage tests here because cargo test tried to use a read-only rustup temp directory in this environment.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

Comment thread crates/storage/migrations.rs Outdated
Comment on lines +74 to +84
/// Atomically writes the schema version to metadata.json with fsync for crash safety.
fn write_metadata_version(db_path: &Path, version: u64) -> Result<(), StoreError> {
let metadata_path = db_path.join(STORE_METADATA_FILENAME);
let metadata = StoreMetadata::new(version);
let serialized = serde_json::to_string_pretty(&metadata)?;

let mut file = std::fs::File::create(&metadata_path)?;
file.write_all(serialized.as_bytes())?;
file.sync_all()?;

Ok(())
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Non-atomic metadata write contradicts crash-safety claim

write_metadata_version is documented as "Atomically writes" but it is not atomic. File::create truncates the existing file immediately; if the process crashes after the truncation but before write_all completes, the metadata file will be empty or partial. serde_json::from_str will then fail on the next startup, making the database inaccessible even though the underlying RocksDB migration completed successfully.

The safe pattern for POSIX filesystems is: write to a sibling temp file → sync_all()rename() (which is atomic).

fn write_metadata_version(db_path: &Path, version: u64) -> Result<(), StoreError> {
    let metadata_path = db_path.join(STORE_METADATA_FILENAME);
    let tmp_path = db_path.join(format!("{}.tmp", STORE_METADATA_FILENAME));
    let metadata = StoreMetadata::new(version);
    let serialized = serde_json::to_string_pretty(&metadata)?;

    let mut file = std::fs::File::create(&tmp_path)?;
    file.write_all(serialized.as_bytes())?;
    file.sync_all()?;
    std::fs::rename(&tmp_path, &metadata_path)?;

    Ok(())
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/storage/migrations.rs
Line: 74-84

Comment:
**Non-atomic metadata write contradicts crash-safety claim**

`write_metadata_version` is documented as "Atomically writes" but it is not atomic. `File::create` truncates the existing file immediately; if the process crashes after the truncation but before `write_all` completes, the metadata file will be empty or partial. `serde_json::from_str` will then fail on the next startup, making the database inaccessible even though the underlying RocksDB migration completed successfully.

The safe pattern for POSIX filesystems is: write to a sibling temp file → `sync_all()``rename()` (which is atomic).

```rust
fn write_metadata_version(db_path: &Path, version: u64) -> Result<(), StoreError> {
    let metadata_path = db_path.join(STORE_METADATA_FILENAME);
    let tmp_path = db_path.join(format!("{}.tmp", STORE_METADATA_FILENAME));
    let metadata = StoreMetadata::new(version);
    let serialized = serde_json::to_string_pretty(&metadata)?;

    let mut file = std::fs::File::create(&tmp_path)?;
    file.write_all(serialized.as_bytes())?;
    file.sync_all()?;
    std::fs::rename(&tmp_path, &metadata_path)?;

    Ok(())
}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread crates/storage/store.rs Outdated
Comment on lines +1490 to +1496
#[cfg(not(feature = "rocksdb"))]
Some(v) if v < STORE_SCHEMA_VERSION => {
return Err(StoreError::IncompatibleDBVersion {
found: v,
expected: STORE_SCHEMA_VERSION,
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Unreachable dead code in #[cfg(not(feature = "rocksdb"))] branch

EngineType only has two variants: InMemory and #[cfg(feature = "rocksdb")] RocksDB. The outer guard is if engine_type != EngineType::InMemory, so when the rocksdb feature is disabled, this block is never entered (there is no non-InMemory engine type to select). This arm can therefore never be reached and will silently become dead code, which may confuse future contributors writing their first migration.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/storage/store.rs
Line: 1490-1496

Comment:
**Unreachable dead code in `#[cfg(not(feature = "rocksdb"))]` branch**

`EngineType` only has two variants: `InMemory` and `#[cfg(feature = "rocksdb")] RocksDB`. The outer guard is `if engine_type != EngineType::InMemory`, so when the `rocksdb` feature is disabled, this block is never entered (there is no non-`InMemory` engine type to select). This arm can therefore never be reached and will silently become dead code, which may confuse future contributors writing their first migration.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread crates/storage/migrations.rs Outdated
Comment on lines +105 to +106
let temp_dir = std::env::temp_dir().join("ethrex_migration_test_noop");
std::fs::create_dir_all(&temp_dir).ok();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Fixed temp paths risk test flakiness under parallel test runners

Both tests use a fixed path under std::env::temp_dir() (e.g. ethrex_migration_test_noop). If cargo test is run with parallelism across multiple invocations, or if a previous run left the directory behind from a failed cleanup, tests can observe stale state or race with one another. Consider using tempfile::TempDir (or std::env::temp_dir().join(format!(".._{}", std::process::id()))) to guarantee unique, isolated directories.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/storage/migrations.rs
Line: 105-106

Comment:
**Fixed temp paths risk test flakiness under parallel test runners**

Both tests use a fixed path under `std::env::temp_dir()` (e.g. `ethrex_migration_test_noop`). If `cargo test` is run with parallelism across multiple invocations, or if a previous run left the directory behind from a failed cleanup, tests can observe stale state or race with one another. Consider using `tempfile::TempDir` (or `std::env::temp_dir().join(format!(".._{}", std::process::id()))`) to guarantee unique, isolated directories.

How can I resolve this? If you propose a fix, please make it concise.

- Use write-to-temp-then-rename pattern in write_metadata_version for
  crash-safe metadata updates (atomic on POSIX filesystems)
- Remove unreachable #[cfg(not(feature = "rocksdb"))] match arm in
  Store::new — without rocksdb the only engine is InMemory, so the
  outer guard already excludes this path
- Use tempfile::tempdir() in tests instead of fixed paths to avoid
  flakiness under parallel test runners
Comment thread crates/storage/migrations.rs Outdated
})?;

// Persist the new version to metadata.json after each migration step
write_metadata_version(db_path, target)?;
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.

We should also wrap inside MigrationFailed

If write_metadata_version fails during a migration, the error is now
wrapped in MigrationFailed with the version context, matching the
handling for the migration function itself.
Copy link
Copy Markdown
Contributor

@iovoid iovoid left a comment

Choose a reason for hiding this comment

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

It probably doesn't matter, but update of the db and metadata file aren't atomic.

Copy link
Copy Markdown
Collaborator

@Arkenan Arkenan left a comment

Choose a reason for hiding this comment

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

LGTM with some nit considerations. We can merge as is but if we do so let's start a PR fixing these issues.

Comment thread crates/storage/store.rs Outdated
Comment on lines +1471 to +1473
return Err(StoreError::IncompatibleDBVersion {
found: v,
expected: STORE_SCHEMA_VERSION,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Shouldn't this be a specific migration error? ie, something like

MigrationFailed { from: v, to: STORE_SCHEMA_VERSION, reason: "DB version {from} is previous to the existence of migrations." }

Comment thread crates/storage/store.rs
Comment on lines +1476 to +1481
Some(v) if v > STORE_SCHEMA_VERSION => {
return Err(StoreError::IncompatibleDBVersion {
found: v,
expected: STORE_SCHEMA_VERSION,
});
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Similar, the message could be something like

MigrationFailed { from: v, to: STORE_SCHEMA_VERSION, reason: "DB version is more recent than the one the client expects. Migrating would involve rolling back, which is not supported." }

Comment thread crates/storage/store.rs Outdated
Comment on lines +1462 to +1464
return Err(StoreError::NotFoundDBVersion {
expected: STORE_SCHEMA_VERSION,
});
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is it possible to have this more explicit somehow? It's not that we expect STORE_SCHEMA_VERSION specifically, we can't migrate this at all because there's no version data in the db at all. We should output something like

Cannot migrate the database. It's version is unavailable, which means that it predates versioning and migrations.

we may just remove the "expected" part of the message entirely, as we are not really expecting that specific version now, we're expecting any migratable version.

Comment thread crates/storage/migrations.rs Outdated
Comment on lines +48 to +51
assert!(
MIGRATIONS.len() == (STORE_SCHEMA_VERSION - 1) as usize,
"MIGRATIONS length must equal STORE_SCHEMA_VERSION - 1"
);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Isn't this done before as a compile time check just above? I don't think we need to do it at runtime again.

Comment thread crates/storage/migrations.rs Outdated
);

for version in current_version..STORE_SCHEMA_VERSION {
let idx = (version - 1) as usize; // v1→v2 is index 0
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: let's have a migration_for_version(i) function that returns the correct migration for that version. That way we don't think about the indexing tricks in here..

})?;

// Persist the new version to metadata.json after each migration step
write_metadata_version(db_path, target).map_err(|e| StoreError::MigrationFailed {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Shouldn't this be part of a storage abstraction instead? Having to pass the path_db around is a bit weird.

- Make NotFoundDBVersion fieldless with descriptive error message
- Use MigrationFailed for invalid version (v < 1) and future version
  (v > STORE_SCHEMA_VERSION) instead of IncompatibleDBVersion
- Remove redundant runtime assert (compile-time const assert suffices)
- Extract migration_for_version() helper for clarity
- Add TODO for moving metadata persistence into StorageBackend
@azteca1998 azteca1998 enabled auto-merge April 29, 2026 10:21
@azteca1998 azteca1998 added this pull request to the merge queue Apr 29, 2026
Merged via the queue into main with commit aefc1cb Apr 29, 2026
54 checks passed
@azteca1998 azteca1998 deleted the feat/automatic-db-migrations branch April 29, 2026 11:53
@github-project-automation github-project-automation Bot moved this from In Review to Done in ethrex_l1 Apr 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L1 Ethereum client

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants