Skip to content

chore: #1780 PROMOTE FORK: fork to new primary via the restore/PITR path#1920

Merged
filipeforattini merged 20 commits into
mainfrom
afk/wSZK6/1780-promote-fork-fork-to-new-primary-via-the
Jul 8, 2026
Merged

chore: #1780 PROMOTE FORK: fork to new primary via the restore/PITR path#1920
filipeforattini merged 20 commits into
mainfrom
afk/wSZK6/1780-promote-fork-fork-to-new-primary-via-the

Conversation

@filipeforattini

@filipeforattini filipeforattini commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Automated AFK landing for #1780. Per-attempt history lives in the issue Envelopes, the JSONL logs, and the afk-attempts/* snapshot branches.

Closes #1780


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

Summary by CodeRabbit

  • New Features

    • Added support for PROMOTE FORK <name> to promote a fork into the primary store.
    • Promotion now preserves the previous primary as an archived parent and returns the promoted fork’s details.
    • The command is recognized in SQL parsing, planning, and execution paths.
  • Bug Fixes

    • Fixed handling so promoted forks clear their fork origin and no longer appear as live child forks after promotion.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@filipeforattini, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 15 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 880a5c44-1727-4aae-8dac-e17813a1ded2

📥 Commits

Reviewing files that changed from the base of the PR and between 8dd5901 and 0918d00.

📒 Files selected for processing (2)
  • crates/reddb-file/src/operational_manifest/fork.rs
  • crates/reddb-file/src/operational_manifest/mod.rs
📝 Walkthrough

Walkthrough

Adds a PROMOTE FORK <name> command that promotes a store fork to become the primary store. Implemented via a new OperationalManifest::promote_fork method performing directory renames and archival, plumbed through RQL's SQL parser, AST, query planner/optimizer passthrough logic, and server runtime DDL execution, with an integration test.

Changes

PROMOTE FORK command

Layer / File(s) Summary
Manifest promotion logic
crates/reddb-file/src/operational_manifest.rs
Adds PromoteForkOutcome, promote_fork (validates fork origin/identity, stages and renames directories, archives old primary, clears fork origin), deterministic path helpers, and a unit test.
QueryExpr/SqlCommand contract
crates/reddb-rql/src/core.rs, crates/reddb-rql/src/sql.rs
Adds PromoteForkQuery struct, PromoteFork variants on QueryExpr, SqlCommand, and SqlAdminCommand, parser function parse_promote_fork_command, dispatch routing, and conversion mappings between statement/command/query-expr forms; includes a parser unit test.
RQL planner passthrough
crates/reddb-rql/src/builders.rs, crates/reddb-rql/src/modes/detect.rs, crates/reddb-rql/src/planner/optimizer.rs, crates/reddb-rql/src/planner/rewriter.rs
Treats PromoteFork as a command-like passthrough variant in join aliasing, mode detection, size estimation, and filter normalization/simplification rules.
Server runtime dispatch and DDL executor
crates/reddb-server/src/runtime/impl_core.rs, crates/reddb-server/src/runtime/impl_ddl.rs, crates/reddb-server/src/runtime/statement_frame.rs, crates/reddb-server/src/runtime.rs, crates/reddb-server/src/storage/query/executors/vector.rs, crates/reddb-server/src/storage/query/planner/cost.rs, crates/reddb-server/src/storage/query/planner/logical.rs, crates/reddb-server/src/storage/query/planner/logical_helpers.rs, crates/reddb-server/src/storage/query/unified/executor.rs
Adds execute_promote_fork DDL method, routes QueryExpr::PromoteFork dispatch to it, classifies PROMOTE as a DDL statement, and updates cost/logical planner and unified executor variant handling for PromoteFork.
Integration test
crates/reddb-server/tests/store_fork_profile.rs
Adds a test forking via SQL, writing to the fork, promoting it via SQL, and asserting the promoted primary reflects fork data with the parent archived and no remaining forks.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant SQLParser
  participant Runtime
  participant OperationalManifest
  participant FileSystem

  Client->>SQLParser: PROMOTE FORK name
  SQLParser->>SQLParser: parse_promote_fork_command
  SQLParser-->>Runtime: QueryExpr::PromoteFork
  Runtime->>Runtime: execute_promote_fork (check DDL write access, flush state)
  Runtime->>OperationalManifest: promote_fork(name)
  OperationalManifest->>FileSystem: rename fork to staging
  OperationalManifest->>FileSystem: rename primary to archived_parent
  OperationalManifest->>FileSystem: rename staging to primary
  OperationalManifest-->>Runtime: PromoteForkOutcome
  Runtime-->>Client: success message with fork LSN and archived parent
Loading

Possibly related PRs

  • reddb-io/reddb#1142: Both PRs modify the same QueryExpr variant-dispatch logic in JoinQueryBuilder::right_alias and the QueryExpr enum.
  • reddb-io/reddb#1581: Both PRs extend the same RQL command dispatcher plumbing with new command variants in shared planner/builder passthrough paths.
  • reddb-io/reddb#1882: Both PRs modify fork lifecycle code in operational_manifest.rs, hydrating shared collections and updating/clearing fork_origin.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is specific and accurately summarizes the PROMOTE FORK-to-primary change.
Linked Issues check ✅ Passed [#1780] The PR implements PROMOTE FORK, makes the fork primary, archives the old parent, and preserves fork-side writes.
Out of Scope Changes check ✅ Passed The only incidental change is an import rewrap; the rest is directly tied to PROMOTE FORK support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch afk/wSZK6/1780-promote-fork-fork-to-new-primary-via-the

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@blacksmith-sh

This comment has been minimized.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
crates/reddb-file/src/operational_manifest.rs (1)

700-763: 🧹 Nitpick | 🔵 Trivial

Archived parent stores are never garbage collected.

Each successful promotion leaves a .retired-by-promote-{name} sibling directory holding a full copy of the superseded primary's data, with no visible cleanup path. Over repeated promotions this accumulates unbounded disk usage. Consider documenting a retention/GC policy (or exposing an explicit DROP ARCHIVED <name>-style command) for these retired roots.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/reddb-file/src/operational_manifest.rs` around lines 700 - 763, The
promote_fork flow creates a .retired-by-promote-{name} archived parent via
archived_parent_handle and never provides any cleanup path, so repeated
promotions will accumulate disk usage. Update promote_fork/PromoteForkOutcome
handling to define and expose a retention or garbage-collection policy for these
retired roots, and add an explicit removal path (for example through a dedicated
drop/cleanup operation) so callers can delete the archived parent when it is no
longer needed.
crates/reddb-server/tests/store_fork_profile.rs (1)

91-130: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Solid integration test for the promote path.

Approving. As an optional enhancement, consider also verifying the promoted data is readable via a real SQL SELECT against users (post-promotion), not just via raw byte comparison — this would more directly validate the "all fork-side writes durable" acceptance criterion from the linked issue, beyond the directory-rename mechanics.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/reddb-server/tests/store_fork_profile.rs` around lines 91 - 130, The
promote-fork integration test currently validates durability only by comparing
raw collection bytes and checking fork cleanup; add a real post-promotion SQL
readback to cover the acceptance criterion more directly. In
promote_fork_sql_installs_fork_as_primary_and_archives_parent, after PROMOTE
FORK experiment, execute a SELECT against users via RedDBRuntime and assert the
fork-side write is returned through normal SQL access, using the existing
runtime and manifest setup to confirm the promoted primary is readable
end-to-end.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/reddb-file/src/operational_manifest.rs`:
- Around line 700-763: promote_fork currently leaves the store non-restartable
if a crash happens after renaming self.root to the retired parent but before
moving staging back to self.root. Update promote_fork and the open/recovery path
to detect and resume an in-flight promotion by checking the
promoting_fork_handle and archived_parent_handle siblings, mirroring the resume
behavior used by detach_fork, or introduce a promotion marker/sweep that
restores the staged manifest instead of treating the missing live path as a
fresh store.

In `@crates/reddb-server/src/runtime/impl_ddl.rs`:
- Around line 1439-1466: The PROMOTE FORK flow in execute_promote_fork only
updates the on-disk manifest and leaves the current RedDB instance using stale
open handles and caches. Add a post-promotion reopen/reload step in
execute_promote_fork (or document/enforce a restart-only contract) so the
runtime switches to the promoted store immediately; use the existing flush(),
OperationalManifest::promote_fork, and RuntimeQueryResult::ok_message path to
locate the update point.

---

Nitpick comments:
In `@crates/reddb-file/src/operational_manifest.rs`:
- Around line 700-763: The promote_fork flow creates a
.retired-by-promote-{name} archived parent via archived_parent_handle and never
provides any cleanup path, so repeated promotions will accumulate disk usage.
Update promote_fork/PromoteForkOutcome handling to define and expose a retention
or garbage-collection policy for these retired roots, and add an explicit
removal path (for example through a dedicated drop/cleanup operation) so callers
can delete the archived parent when it is no longer needed.

In `@crates/reddb-server/tests/store_fork_profile.rs`:
- Around line 91-130: The promote-fork integration test currently validates
durability only by comparing raw collection bytes and checking fork cleanup; add
a real post-promotion SQL readback to cover the acceptance criterion more
directly. In promote_fork_sql_installs_fork_as_primary_and_archives_parent,
after PROMOTE FORK experiment, execute a SELECT against users via RedDBRuntime
and assert the fork-side write is returned through normal SQL access, using the
existing runtime and manifest setup to confirm the promoted primary is readable
end-to-end.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 413e4563-8702-401b-a202-8878f28bc179

📥 Commits

Reviewing files that changed from the base of the PR and between 294bf1a and 8dd5901.

📒 Files selected for processing (17)
  • crates/reddb-file/src/operational_manifest.rs
  • crates/reddb-rql/src/builders.rs
  • crates/reddb-rql/src/core.rs
  • crates/reddb-rql/src/modes/detect.rs
  • crates/reddb-rql/src/planner/optimizer.rs
  • crates/reddb-rql/src/planner/rewriter.rs
  • crates/reddb-rql/src/sql.rs
  • crates/reddb-server/src/runtime.rs
  • crates/reddb-server/src/runtime/impl_core.rs
  • crates/reddb-server/src/runtime/impl_ddl.rs
  • crates/reddb-server/src/runtime/statement_frame.rs
  • crates/reddb-server/src/storage/query/executors/vector.rs
  • crates/reddb-server/src/storage/query/planner/cost.rs
  • crates/reddb-server/src/storage/query/planner/logical.rs
  • crates/reddb-server/src/storage/query/planner/logical_helpers.rs
  • crates/reddb-server/src/storage/query/unified/executor.rs
  • crates/reddb-server/tests/store_fork_profile.rs

Comment on lines +700 to +763
/// Promote a store fork to the primary operational root.
///
/// The promoted fork is first hydrated through the same materialization path
/// used by restore/fork detach. The superseded primary is then moved to a
/// deterministic retired root, so its disposition is explicit and cannot be
/// mistaken for the active store.
pub fn promote_fork(&self, name: &str) -> io::Result<Option<PromoteForkOutcome>> {
let fork = self.fork_handle(name);
if !fork.root.exists() {
return Ok(None);
}
let origin = fork
.fork_origin()?
.ok_or_else(|| invalid_data(format!("store fork is missing origin: {name}")))?;
if origin.parent_store != self.store_identity() {
return Err(invalid_data(format!(
"store fork {name} belongs to {}, not {}",
origin.parent_store,
self.store_identity()
)));
}

let staging = self.promoting_fork_handle(name);
if staging.root.exists() {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("store fork promotion staging path already exists: {name}"),
));
}
let archived_parent = self.archived_parent_handle(name);
if archived_parent.root.exists() {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
format!("retired parent store already exists for promoted fork: {name}"),
));
}

fork.hydrate_shared_collections()?;
if let Some(parent) = staging.root.parent() {
fs::create_dir_all(parent)?;
}
fs::rename(&fork.root, &staging.root)?;
sync_dir(&self.forks_dir())?;
if let Some(parent) = staging.root.parent() {
sync_dir(parent)?;
}

fs::rename(&self.root, &archived_parent.root)?;
if let Some(parent) = self.root.parent() {
sync_dir(parent)?;
}
fs::rename(&staging.root, &self.root)?;
if let Some(parent) = self.root.parent() {
sync_dir(parent)?;
}
self.clear_fork_origin()?;

Ok(Some(PromoteForkOutcome {
name: origin.name,
fork_lsn: origin.fork_lsn,
archived_parent,
}))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

Rust std::fs::rename atomicity crash safety multiple renames durability

💡 Result:

In Rust, std::fs::rename provides an atomic operation to rename or replace a file [1][2]. However, "atomicity" and "durability" are distinct concepts in crash-consistent systems [3]. While std::fs::rename is generally atomic at the filesystem level, ensuring your data survives a system crash requires additional steps [3][4]. Atomicity On Unix-like platforms, rename(2) is defined by POSIX to be atomic [5][6]. If the destination path exists, it is replaced atomically, ensuring that other processes will never see the path in an incomplete state [5]. On Windows, Rust's implementation uses modern APIs (FileRenameInfoEx with POSIX semantics where available) to mimic this behavior [7]. Recent improvements in Rust have worked to resolve historical inconsistencies on Windows, such as access-denied errors during concurrent operations [8][7][9]. Durability and Crash Safety Atomicity does not guarantee that your data is written to stable storage; it only ensures the name switch is atomic [3][4]. To achieve a crash-consistent "atomic save" (where the file is replaced by new, fully-persisted content), you must follow this pattern [3][4]: 1. Write your data to a temporary file located on the same filesystem as the destination [3][4]. 2. Call sync_all (which invokes fsync) on the temporary file to ensure the data contents are persisted [3][4]. 3. Call std::fs::rename to move the temporary file to the final destination [3][4]. 4. Open the parent directory and call sync_all on it [3]. This is the critical, often-missed step that ensures the directory entry update (the rename itself) is persisted to stable storage [10][3][6]. Failure to sync the parent directory may result in the rename being reverted or lost after a power loss or kernel crash, even if the file content itself was successfully synced [10][3][6]. Note that "durability" is ultimately limited by the filesystem and hardware; on some systems, even these steps may not prevent data loss under extreme conditions, though this pattern is the standard for POSIX-compliant crash consistency [3][6].

Citations:


Make promote_fork restartable across the rename sequence
A crash between self.root -> *.retired-by-promote-* and *.promoting-* -> self.root leaves no manifest at the live path, and the normal open path can treat that as a fresh store instead of resuming the in-flight promotion. Mirror detach_fork’s resume logic here or add a promotion marker/sweep path so staged and archived siblings are recovered instead of orphaned.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/reddb-file/src/operational_manifest.rs` around lines 700 - 763,
promote_fork currently leaves the store non-restartable if a crash happens after
renaming self.root to the retired parent but before moving staging back to
self.root. Update promote_fork and the open/recovery path to detect and resume
an in-flight promotion by checking the promoting_fork_handle and
archived_parent_handle siblings, mirroring the resume behavior used by
detach_fork, or introduce a promotion marker/sweep that restores the staged
manifest instead of treating the missing live path as a fresh store.

Comment on lines +1439 to +1466
pub fn execute_promote_fork(
&self,
raw_query: &str,
query: &PromoteForkQuery,
) -> RedDBResult<RuntimeQueryResult> {
self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
let path = self.inner.db.path().ok_or_else(|| {
RedDBError::Query("PROMOTE FORK requires a persistent store".to_string())
})?;
self.flush()?;
let manifest = reddb_file::OperationalManifest::for_db_path(path);
let outcome = manifest
.promote_fork(&query.name)
.map_err(|err| RedDBError::Query(format!("failed to promote store fork: {err}")))?;
let outcome =
outcome.ok_or_else(|| RedDBError::NotFound(format!("store fork '{}'", query.name)))?;
Ok(RuntimeQueryResult::ok_message(
raw_query.to_string(),
&format!(
"store fork '{}' promoted at LSN {}; retired parent archived at {}",
outcome.name,
outcome.fork_lsn,
outcome.archived_parent.store_identity()
),
"promote_fork",
))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for any reopen/reload of the store tied to fork/restore/PITR paths,
# and confirm whether restart is documented as required.
rg -n -i --type=rust -C3 '\breopen\b|\breload\b|restart' crates/reddb-server/src crates/reddb-file/src | rg -i 'fork|restore|pitr|promote'
rg -n --type=rust -C5 'fn flush\(' crates/reddb-server/src
rg -n --type=rust -B2 -A15 'fn recover_or_bootstrap' crates/reddb-file/src/operational_manifest.rs

Repository: reddb-io/reddb

Length of output: 12165


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant files first.
ast-grep outline crates/reddb-server/src/runtime/impl_ddl.rs --view expanded
ast-grep outline crates/reddb-file/src/operational_manifest.rs --view expanded

# Read the specific sections around promote_fork and nearby mutating DDL paths.
sed -n '1400,1495p' crates/reddb-server/src/runtime/impl_ddl.rs
printf '\n---\n'
sed -n '620,760p' crates/reddb-file/src/operational_manifest.rs
printf '\n---\n'
sed -n '1,260p' crates/reddb-server/src/runtime/impl_lifecycle.rs

# Find any reopen/reload/invalidate behavior tied to DDL/catalog mutation.
rg -n -C3 'invalidate_result_cache|invalidate_plan_cache|reopen|reload|open\(|flush\(\)\?|promote_fork|restore|restart' crates/reddb-server/src/runtime crates/reddb-file/src

Repository: reddb-io/reddb

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Focus on the promotion implementation, its tests, and any explicit docs about restart/reopen.
sed -n '706,850p' crates/reddb-file/src/operational_manifest.rs
printf '\n---\n'
sed -n '1980,2035p' crates/reddb-file/src/operational_manifest.rs
printf '\n---\n'
sed -n '1439,1472p' crates/reddb-server/src/runtime/impl_ddl.rs
printf '\n---\n'
rg -n -C2 'PROMOTE FORK|promote_fork|restart|reopen|reload|invalidate_result_cache|invalidate_plan_cache' crates/reddb-file/src crates/reddb-server/src | head -n 200
printf '\n---\n'
rg -n -C2 'store_identity|db\.path\(|open_with_options|open\(' crates/reddb-server/src crates/reddb-file/src | head -n 200

Repository: reddb-io/reddb

Length of output: 39478


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the store's open/path semantics and how the runtime wraps it.
rg -n -C3 'pub fn path\(|struct RedDB|impl RedDB|open_with_options|open\(' crates/reddb-server/src/storage crates/reddb-server/src | head -n 200
printf '\n---\n'
ast-grep outline crates/reddb-server/src/storage/unified/store.rs --view expanded
printf '\n---\n'
sed -n '1,260p' crates/reddb-server/src/storage/unified/store.rs
printf '\n---\n'
sed -n '260,520p' crates/reddb-server/src/storage/unified/store.rs

Repository: reddb-io/reddb

Length of output: 34563


PROMOTE FORK needs a reopen step or an explicit restart-only contract. This path only swaps directories on disk; the current RedDB instance keeps its open handles and caches, so it can keep serving the pre-promotion store until the process is recreated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/reddb-server/src/runtime/impl_ddl.rs` around lines 1439 - 1466, The
PROMOTE FORK flow in execute_promote_fork only updates the on-disk manifest and
leaves the current RedDB instance using stale open handles and caches. Add a
post-promotion reopen/reload step in execute_promote_fork (or document/enforce a
restart-only contract) so the runtime switches to the promoted store
immediately; use the existing flush(), OperationalManifest::promote_fork, and
RuntimeQueryResult::ok_message path to locate the update point.

@filipeforattini filipeforattini merged commit aeb9c2d into main Jul 8, 2026
26 checks passed
@filipeforattini filipeforattini deleted the afk/wSZK6/1780-promote-fork-fork-to-new-primary-via-the branch July 8, 2026 01:31
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.

PROMOTE FORK: fork to new primary via the restore/PITR path

1 participant