Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bb54f81
feat(fork): promote operational fork roots
filipeforattini Jul 7, 2026
552ab34
feat(rql): add promote fork query
filipeforattini Jul 7, 2026
e09073b
feat(rql): parse promote fork
filipeforattini Jul 7, 2026
9122a85
chore(rql): carry promote fork through builders
filipeforattini Jul 7, 2026
7138732
chore(rql): cost promote fork commands
filipeforattini Jul 7, 2026
0abb978
chore(rql): pass through promote fork
filipeforattini Jul 7, 2026
9b42339
feat(rql): detect promote fork as sql
filipeforattini Jul 7, 2026
a0d10d6
feat(rql): route promote fork frontend
filipeforattini Jul 7, 2026
ccb2102
feat(runtime): import promote fork query
filipeforattini Jul 7, 2026
a7bb790
feat(runtime): dispatch promote fork
filipeforattini Jul 7, 2026
82feaeb
feat(runtime): execute promote fork
filipeforattini Jul 7, 2026
97e2220
feat(runtime): classify promote as ddl
filipeforattini Jul 7, 2026
55785eb
chore(query): name promote fork command
filipeforattini Jul 7, 2026
548a92b
chore(query): cost promote fork command
filipeforattini Jul 7, 2026
3cbf038
chore(query): plan promote fork command
filipeforattini Jul 7, 2026
fcff34e
chore(query): classify promote fork helpers
filipeforattini Jul 7, 2026
d45674f
chore(query): pass promote fork through unified executor
filipeforattini Jul 7, 2026
8dd5901
test(runtime): cover promote fork sql
filipeforattini Jul 7, 2026
af3e3e6
merge: main into 1780 promote-fork (finisher)
filipeforattini Jul 8, 2026
0918d00
refactor(#1780): extract fork lifecycle into operational_manifest/for…
filipeforattini Jul 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions crates/reddb-file/src/operational_manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ pub struct ForkInfo {
pub hydrated: u64,
}

#[derive(Debug, Clone)]
pub struct PromoteForkOutcome {
pub name: String,
pub fork_lsn: u64,
pub archived_parent: OperationalManifest,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ForkHydrationState {
SharedByReference,
Expand Down Expand Up @@ -690,6 +697,70 @@ impl OperationalManifest {
Ok(Some(detached))
}

/// 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.

/// Read this manifest's fork origin, if it is a fork.
pub fn fork_origin(&self) -> io::Result<Option<ForkOrigin>> {
Ok(self
Expand Down Expand Up @@ -730,6 +801,34 @@ impl OperationalManifest {
}
}

fn archived_parent_handle(&self, name: &str) -> Self {
let root_name = self
.root
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| "store.ops".to_string());
Self {
root: self.root.with_file_name(format!(
"{root_name}.retired-by-promote-{}",
sanitize_component(name)
)),
}
}

fn promoting_fork_handle(&self, name: &str) -> Self {
let root_name = self
.root
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| "store.ops".to_string());
Self {
root: self.root.with_file_name(format!(
"{root_name}.promoting-{}",
sanitize_component(name)
)),
}
}

fn hydrate_shared_collections(&self) -> io::Result<()> {
let mut manifest = match self.load_current()? {
Some(manifest) => manifest,
Expand Down Expand Up @@ -1891,6 +1990,30 @@ mod tests {
);
}

#[test]
fn promote_fork_replaces_primary_and_archives_parent() {
let path = temp_db_path("promote_fork");
let parent = OperationalManifest::for_db_path(&path);
parent.recover_or_bootstrap(&["users".to_string()]).unwrap();
write_collection(&parent, "users", b"old-primary");
parent.create_fork("exp", 21).unwrap();
let fork = parent.fork_handle("exp");
fork.hydrate_collection("users").unwrap();
write_collection(&fork, "users", b"new-primary");

let outcome = parent.promote_fork("exp").unwrap().unwrap();

assert_eq!(outcome.name, "exp");
assert_eq!(outcome.fork_lsn, 21);
assert_eq!(read_collection(&parent, "users"), b"new-primary");
assert!(parent.fork_origin().unwrap().is_none());
assert!(parent.list_forks().unwrap().is_empty());
assert_eq!(
read_collection(&outcome.archived_parent, "users"),
b"old-primary"
);
}

#[test]
fn detach_fork_resumes_after_move_before_origin_clear() {
let path = temp_db_path("detach_fork_resume");
Expand Down
1 change: 1 addition & 0 deletions crates/reddb-rql/src/builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ impl JoinQueryBuilder {
| QueryExpr::CreateVcsRef(_)
| QueryExpr::DropVcsRef(_)
| QueryExpr::ForkStore(_)
| QueryExpr::PromoteFork(_)
| QueryExpr::DropFork(_)
| QueryExpr::GraphCommand(_)
| QueryExpr::SearchCommand(_)
Expand Down
8 changes: 8 additions & 0 deletions crates/reddb-rql/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ pub enum QueryExpr {
DropVcsRef(DropVcsRefQuery),
/// FORK STORE AS name [AT LSN n]
ForkStore(ForkStoreQuery),
/// PROMOTE FORK name
PromoteFork(PromoteForkQuery),
/// DROP FORK name
DropFork(DropForkQuery),
/// GRAPH subcommand (NEIGHBORHOOD, SHORTEST_PATH, etc.)
Expand Down Expand Up @@ -2287,6 +2289,12 @@ pub struct ForkStoreQuery {
pub at_lsn: Option<u64>,
}

/// PROMOTE FORK name
#[derive(Debug, Clone)]
pub struct PromoteForkQuery {
pub name: String,
}

/// DROP FORK name
#[derive(Debug, Clone)]
pub struct DropForkQuery {
Expand Down
1 change: 1 addition & 0 deletions crates/reddb-rql/src/modes/detect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ pub fn detect_mode(input: &str) -> QueryMode {
| "revert"
| "resolve"
| "fork"
| "promote"
| "copy"
| "refresh"
| "explain"
Expand Down
1 change: 1 addition & 0 deletions crates/reddb-rql/src/planner/optimizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ impl JoinReorderingPass {
| QueryExpr::CreateVcsRef(_)
| QueryExpr::DropVcsRef(_)
| QueryExpr::ForkStore(_)
| QueryExpr::PromoteFork(_)
| QueryExpr::DropFork(_)
| QueryExpr::GraphCommand(_)
| QueryExpr::SearchCommand(_)
Expand Down
3 changes: 3 additions & 0 deletions crates/reddb-rql/src/planner/rewriter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ impl RewriteRule for NormalizeRule {
| QueryExpr::CreateVcsRef(_)
| QueryExpr::DropVcsRef(_)
| QueryExpr::ForkStore(_)
| QueryExpr::PromoteFork(_)
| QueryExpr::DropFork(_)
| QueryExpr::GraphCommand(_)
| QueryExpr::SearchCommand(_)
Expand Down Expand Up @@ -348,6 +349,7 @@ impl RewriteRule for SimplifyFiltersRule {
| QueryExpr::CreateVcsRef(_)
| QueryExpr::DropVcsRef(_)
| QueryExpr::ForkStore(_)
| QueryExpr::PromoteFork(_)
| QueryExpr::DropFork(_)
| QueryExpr::GraphCommand(_)
| QueryExpr::SearchCommand(_)
Expand Down Expand Up @@ -449,6 +451,7 @@ impl RewriteRule for SimplifyFiltersRule {
| QueryExpr::CreateVcsRef(_)
| QueryExpr::DropVcsRef(_)
| QueryExpr::ForkStore(_)
| QueryExpr::PromoteFork(_)
| QueryExpr::DropFork(_)
| QueryExpr::GraphCommand(_)
| QueryExpr::SearchCommand(_)
Expand Down
43 changes: 37 additions & 6 deletions crates/reddb-rql/src/sql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ use crate::ast::{
DropVectorQuery, DropViewQuery, EventsBackfillQuery, ExplainAlterQuery, ExplainMigrationQuery,
ExplainQuery, Expr, FieldRef, Filter, ForeignColumnDef, ForkStoreQuery, GrantStmt,
GraphCommand, GraphQuery, HybridQuery, InsertQuery, IsolationLevel, JoinQuery, KvCommand,
MaintenanceCommand, PathQuery, PolicyAction, ProbabilisticCommand, QueryExpr, QueueCommand,
QueueSelectQuery, RankOfQuery, RankRangeQuery, RefreshMaterializedViewQuery, RevokeStmt,
RollbackMigrationQuery, SearchCommand, Span, TableQuery, TreeCommand, TruncateQuery,
TxnControl, UpdateQuery, VcsCommand, VcsConflictResolution, VcsRefKind, VcsResetMode,
VectorQuery,
MaintenanceCommand, PathQuery, PolicyAction, ProbabilisticCommand, PromoteForkQuery, QueryExpr,
QueueCommand, QueueSelectQuery, RankOfQuery, RankRangeQuery, RefreshMaterializedViewQuery,
RevokeStmt, RollbackMigrationQuery, SearchCommand, Span, TableQuery, TreeCommand,
TruncateQuery, TxnControl, UpdateQuery, VcsCommand, VcsConflictResolution, VcsRefKind,
VcsResetMode, VectorQuery,
};
use crate::lexer::Token;
use crate::parser::{ParseError, Parser, SafeTokenDisplay};
Expand Down Expand Up @@ -123,6 +123,7 @@ pub enum SqlCommand {
Maintenance(MaintenanceCommand),
Vcs(VcsCommand),
ForkStore(ForkStoreQuery),
PromoteFork(PromoteForkQuery),
DropFork(DropForkQuery),
CreateSchema(CreateSchemaQuery),
DropSchema(DropSchemaQuery),
Expand Down Expand Up @@ -1301,6 +1302,14 @@ mod tests {
));
}

#[test]
fn parse_sql_command_covers_store_fork_promotion() {
let SqlCommand::PromoteFork(query) = sql_command("PROMOTE FORK exp") else {
panic!("PROMOTE FORK should parse as a store fork promotion");
};
assert_eq!(query.name, "exp");
}

#[test]
fn parse_sql_command_covers_iam_and_hypertable_dispatch_edges() {
assert!(matches!(
Expand Down Expand Up @@ -1477,6 +1486,7 @@ pub enum SqlAdminCommand {
CreateUser(CreateUserStmt),
IamPolicy(QueryExpr),
ForkStore(ForkStoreQuery),
PromoteFork(PromoteForkQuery),
DropFork(DropForkQuery),
}

Expand Down Expand Up @@ -1590,6 +1600,7 @@ impl SqlStatement {
SqlStatement::Admin(SqlAdminCommand::Maintenance(cmd)) => SqlCommand::Maintenance(cmd),
SqlStatement::Admin(SqlAdminCommand::Vcs(cmd)) => SqlCommand::Vcs(cmd),
SqlStatement::Admin(SqlAdminCommand::ForkStore(q)) => SqlCommand::ForkStore(q),
SqlStatement::Admin(SqlAdminCommand::PromoteFork(q)) => SqlCommand::PromoteFork(q),
SqlStatement::Admin(SqlAdminCommand::DropFork(q)) => SqlCommand::DropFork(q),
SqlStatement::Schema(SqlSchemaCommand::CreateSchema(q)) => SqlCommand::CreateSchema(q),
SqlStatement::Schema(SqlSchemaCommand::DropSchema(q)) => SqlCommand::DropSchema(q),
Expand Down Expand Up @@ -1742,6 +1753,7 @@ impl SqlCommand {
SqlCommand::Maintenance(cmd) => QueryExpr::MaintenanceCommand(cmd),
SqlCommand::Vcs(cmd) => QueryExpr::VcsCommand(cmd),
SqlCommand::ForkStore(q) => QueryExpr::ForkStore(q),
SqlCommand::PromoteFork(q) => QueryExpr::PromoteFork(q),
SqlCommand::DropFork(q) => QueryExpr::DropFork(q),
SqlCommand::CreateSchema(q) => QueryExpr::CreateSchema(q),
SqlCommand::DropSchema(q) => QueryExpr::DropSchema(q),
Expand Down Expand Up @@ -1878,6 +1890,7 @@ impl SqlCommand {
SqlCommand::Maintenance(cmd) => SqlStatement::Admin(SqlAdminCommand::Maintenance(cmd)),
SqlCommand::Vcs(cmd) => SqlStatement::Admin(SqlAdminCommand::Vcs(cmd)),
SqlCommand::ForkStore(q) => SqlStatement::Admin(SqlAdminCommand::ForkStore(q)),
SqlCommand::PromoteFork(q) => SqlStatement::Admin(SqlAdminCommand::PromoteFork(q)),
SqlCommand::DropFork(q) => SqlStatement::Admin(SqlAdminCommand::DropFork(q)),
SqlCommand::CreateSchema(q) => SqlStatement::Schema(SqlSchemaCommand::CreateSchema(q)),
SqlCommand::DropSchema(q) => SqlStatement::Schema(SqlSchemaCommand::DropSchema(q)),
Expand Down Expand Up @@ -2114,7 +2127,9 @@ impl<'a> Parser<'a> {
Token::Ident(name) if name.eq_ignore_ascii_case("RESET") => {
self.parse_sql_statement().map(FrontendStatement::Sql)
}
Token::Ident(name) if name.eq_ignore_ascii_case("FORK") => {
Token::Ident(name)
if name.eq_ignore_ascii_case("FORK") || name.eq_ignore_ascii_case("PROMOTE") =>
{
self.parse_sql_statement().map(FrontendStatement::Sql)
}
Token::Ident(name)
Expand Down Expand Up @@ -2848,6 +2863,19 @@ impl<'a> Parser<'a> {
Ok(SqlCommand::ForkStore(ForkStoreQuery { name, at_lsn }))
}

fn parse_promote_fork_command(&mut self) -> Result<SqlCommand, ParseError> {
self.advance()?; // PROMOTE
if !self.consume_ident_ci("FORK")? {
return Err(ParseError::expected(
vec!["FORK"],
self.peek(),
self.position(),
));
}
let name = self.expect_ident()?;
Ok(SqlCommand::PromoteFork(PromoteForkQuery { name }))
}

fn parse_dotted_admin_path(&mut self, lowercase: bool) -> Result<String, ParseError> {
let mut path = self.expect_ident()?;
while self.consume(&Token::Dot)? {
Expand Down Expand Up @@ -3452,6 +3480,9 @@ impl<'a> Parser<'a> {
Token::Ident(name) if name.eq_ignore_ascii_case("FORK") => {
self.parse_fork_store_command()
}
Token::Ident(name) if name.eq_ignore_ascii_case("PROMOTE") => {
self.parse_promote_fork_command()
}
Token::Select => match self.parse_select_query()? {
QueryExpr::Table(query) => Ok(SqlCommand::Select(query)),
QueryExpr::Join(query) => Ok(SqlCommand::Join(query)),
Expand Down
5 changes: 3 additions & 2 deletions crates/reddb-server/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ use crate::storage::query::ast::{
DropTimeSeriesQuery, DropTreeQuery, DropVectorQuery, EventsBackfillQuery, ExplainAlterQuery,
ExplainFormat, FieldRef, Filter, ForkStoreQuery, FusionStrategy, GraphCommand, HybridQuery,
IndexMethod, InsertEntityType, InsertQuery, JoinQuery, JoinType, OrderByClause,
ProbabilisticCommand, Projection, QueryExpr, QueueCommand, QueueSelectQuery, QueueSide,
SearchCommand, TableQuery, TreeCommand, TruncateQuery, UpdateQuery, VectorQuery, VectorSource,
ProbabilisticCommand, Projection, PromoteForkQuery, QueryExpr, QueueCommand, QueueSelectQuery,
QueueSide, SearchCommand, TableQuery, TreeCommand, TruncateQuery, UpdateQuery, VectorQuery,
VectorSource,
};
use crate::storage::query::is_universal_entity_source as is_universal_query_source;
use crate::storage::query::modes::{detect_mode, parse_multi, QueryMode};
Expand Down
3 changes: 3 additions & 0 deletions crates/reddb-server/src/runtime/impl_core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,9 @@ impl RedDBRuntime {
QueryExpr::CreateVcsRef(ref create) => self.execute_create_vcs_ref(query, create),
QueryExpr::DropVcsRef(ref drop_ref) => self.execute_drop_vcs_ref(query, drop_ref),
QueryExpr::ForkStore(ref fork) => self.execute_fork_store(query, fork),
QueryExpr::PromoteFork(ref promote_fork) => {
self.execute_promote_fork(query, promote_fork)
}
QueryExpr::DropFork(ref drop_fork) => self.execute_drop_fork(query, drop_fork),
QueryExpr::ExplainAlter(ref explain) => self.execute_explain_alter(query, explain),
// Graph analytics commands
Expand Down
28 changes: 28 additions & 0 deletions crates/reddb-server/src/runtime/impl_ddl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1436,6 +1436,34 @@ impl RedDBRuntime {
))
}

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",
))
}

Comment on lines +1439 to +1466

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.

/// Execute EXPLAIN ALTER FOR CREATE TABLE
///
/// Pure read: computes the schema diff between the target table's
Expand Down
2 changes: 1 addition & 1 deletion crates/reddb-server/src/runtime/statement_frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ fn statement_kind(query: &str) -> &'static str {
| b"APPROX" | b"APPROXIMATE" | b"ZRANK" | b"ZRANGE" | b"LIST" | b"WATCH" | b"GET"
| b"HISTORY" => "read",
b"INSERT" | b"UPDATE" | b"DELETE" | b"UPSERT" | b"MERGE" | b"COPY" | b"TRUNCATE" => "write",
b"CREATE" | b"ALTER" | b"DROP" | b"REINDEX" | b"VACUUM" | b"ANALYZE" => "ddl",
b"CREATE" | b"ALTER" | b"DROP" | b"PROMOTE" | b"REINDEX" | b"VACUUM" | b"ANALYZE" => "ddl",
b"GRANT" | b"REVOKE" => "admin",
b"BEGIN" | b"START" | b"COMMIT" | b"ROLLBACK" | b"SAVEPOINT" | b"RELEASE" | b"END"
| b"SET" | b"RESET" | b"PREPARE" | b"EXECUTE" | b"DEALLOCATE" | b"USE" => "control",
Expand Down
Loading
Loading