Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
532 changes: 532 additions & 0 deletions crates/reddb-file/src/operational_manifest/fork.rs

Large diffs are not rendered by default.

Large diffs are not rendered by default.

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
1 change: 1 addition & 0 deletions crates/reddb-server/src/storage/query/executors/vector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,7 @@ fn query_expr_name(expr: &QueryExpr) -> &'static str {
QueryExpr::CreateVcsRef(_) => "create_vcs_ref",
QueryExpr::DropVcsRef(_) => "drop_vcs_ref",
QueryExpr::ForkStore(_) => "fork_store",
QueryExpr::PromoteFork(_) => "promote_fork",
QueryExpr::DropFork(_) => "drop_fork",
QueryExpr::VcsCommand(_) => "vcs_command",
QueryExpr::GraphCommand(_) => "graph_command",
Expand Down
2 changes: 2 additions & 0 deletions crates/reddb-server/src/storage/query/planner/cost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ impl CostEstimator {
| QueryExpr::CreateVcsRef(_)
| QueryExpr::DropVcsRef(_)
| QueryExpr::ForkStore(_)
| QueryExpr::PromoteFork(_)
| QueryExpr::DropFork(_)
| QueryExpr::VcsCommand(_)
| QueryExpr::GraphCommand(_)
Expand Down Expand Up @@ -408,6 +409,7 @@ impl CostEstimator {
| QueryExpr::CreateVcsRef(_)
| QueryExpr::DropVcsRef(_)
| QueryExpr::ForkStore(_)
| QueryExpr::PromoteFork(_)
| QueryExpr::DropFork(_)
| QueryExpr::VcsCommand(_)
| QueryExpr::GraphCommand(_)
Expand Down
1 change: 1 addition & 0 deletions crates/reddb-server/src/storage/query/planner/logical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,7 @@ pub(super) fn logical_plan_node_with_catalog(db: &RedDB, expr: &QueryExpr) -> Ca
| QueryExpr::CreateVcsRef(_)
| QueryExpr::DropVcsRef(_)
| QueryExpr::ForkStore(_)
| QueryExpr::PromoteFork(_)
| QueryExpr::DropFork(_)
| QueryExpr::VcsCommand(_)
| QueryExpr::GraphCommand(_)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ pub(crate) fn join_expr_exposes_field_table(expr: &QueryExpr, table: &str) -> bo
| QueryExpr::CreateVcsRef(_)
| QueryExpr::DropVcsRef(_)
| QueryExpr::ForkStore(_)
| QueryExpr::PromoteFork(_)
| QueryExpr::DropFork(_)
| QueryExpr::VcsCommand(_)
| QueryExpr::GraphCommand(_)
Expand Down Expand Up @@ -829,6 +830,7 @@ pub(crate) fn query_expr_kind(expr: &QueryExpr) -> &'static str {
QueryExpr::CreateVcsRef(_) => "create_vcs_ref",
QueryExpr::DropVcsRef(_) => "drop_vcs_ref",
QueryExpr::ForkStore(_) => "fork_store",
QueryExpr::PromoteFork(_) => "promote_fork",
QueryExpr::DropFork(_) => "drop_fork",
QueryExpr::VcsCommand(_) => "vcs_command",
QueryExpr::GraphCommand(_) => "graph_command",
Expand Down
2 changes: 2 additions & 0 deletions crates/reddb-server/src/storage/query/unified/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ impl UnifiedExecutor {
| QueryExpr::CreateVcsRef(_)
| QueryExpr::DropVcsRef(_)
| QueryExpr::ForkStore(_)
| QueryExpr::PromoteFork(_)
| QueryExpr::DropFork(_)
| QueryExpr::VcsCommand(_)
| QueryExpr::GraphCommand(_)
Expand Down Expand Up @@ -441,6 +442,7 @@ impl UnifiedExecutor {
| QueryExpr::CreateVcsRef(_)
| QueryExpr::DropVcsRef(_)
| QueryExpr::ForkStore(_)
| QueryExpr::PromoteFork(_)
| QueryExpr::DropFork(_)
| QueryExpr::VcsCommand(_)
| QueryExpr::GraphCommand(_)
Expand Down
42 changes: 42 additions & 0 deletions crates/reddb-server/tests/store_fork_profile.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use reddb_file::OperationalManifest;
use reddb_server::{RedDBError, RedDBOptions, RedDBRuntime, StorageDeployPreset};
use reddb_types::Value;

fn temp_data_path(name: &str) -> (tempfile::TempDir, std::path::PathBuf) {
let dir = tempfile::Builder::new()
Expand Down Expand Up @@ -86,3 +88,43 @@ fn operational_directory_fork_uses_exported_layout() {
.detach_fork_store("experiment")
.expect("detach missing fork is idempotent"));
}

#[test]
fn promote_fork_sql_installs_fork_as_primary_and_archives_parent() {
let (_dir, path) = temp_data_path("promote-fork-sql");
let runtime = RedDBRuntime::with_options(RedDBOptions::persistent(&path)).expect("runtime");
runtime
.execute_query("CREATE TABLE users (id INT)")
.expect("create table");
runtime
.execute_query("FORK STORE AS experiment")
.expect("fork store");

let manifest = OperationalManifest::for_db_path(&path);
let fork = manifest.fork_handle("experiment");
fork.hydrate_collection("users").expect("hydrate fork");
std::fs::write(fork.collection_path_for_test("users"), b"fork-side-write")
.expect("write fork collection");

let promoted = runtime
.execute_query("PROMOTE FORK experiment")
.expect("promote fork");
let message = match promoted.result.records[0].get("message") {
Some(Value::Text(text)) => text.as_ref(),
other => panic!("unexpected promotion message: {other:?}"),
};

assert!(
message.contains("retired parent archived at"),
"promotion must report explicit retired-parent disposition: {:?}",
message
);
assert_eq!(
std::fs::read(manifest.collection_path_for_test("users")).expect("read primary"),
b"fork-side-write"
);
assert!(
manifest.list_forks().expect("list forks").is_empty(),
"promoted fork must no longer remain a live child fork"
);
}
Loading