feat: drop_ledger drops the whole ledger; root-branch check is struct…#1244
Conversation
…ural drop_ledger now removes every branch under a ledger name (including retracted-but-not-purged branches) plus the cross-branch @shared/dicts/ namespace. Branches are dropped leaf-first via source_branch pointers so a partial failure leaves orphan parents rather than dangling children. Hard mode uses the parent-aware AdminPublisher::drop_branch path so surviving parents keep accurate child counts even on abort. Inputs are validated: the bare ledger name "mydb" is canonical; the branch-qualified form "mydb:main" is accepted with a warning; non-default branch suffixes like "mydb:dev" are rejected with a 400 that points the caller at drop_branch. A real per-branch nameservice failure aborts with ApiError::Drop before touching parents or shared cleanup; NotFound races are treated as success. DropReport gains branch_reports for per-branch detail; the HTTP DropResponse exposes the same as branches_dropped. The "cannot drop main" guards in drop_branch and rebase_branch are replaced with structural source_branch.is_none() root checks. "main" carries no special meaning anymore — a ledger whose root is named "trunk" refuses operations on "trunk", and a non-root branch named "main" can be dropped or rebased like any other branch. Docs updated end-to-end: api/endpoints.md, cli/drop.md, cli/server-integration.md, concepts/ledgers-and-nameservice.md, getting-started/rust-api.md, operations/admin-and-health.md. CLI output now reports per-branch artifact counts on multi-branch drops. Tests cover whole-ledger leaf-first hard drop with retracted-branch cleanup and @shared/dicts/ wipe, bare-name canonical form, the main-suffix backwards-compat warning, the non-default-suffix rejection, structural root refusal in both drop_branch and rebase_branch, and the allowance for a non-root branch named "main".
zonotope
left a comment
There was a problem hiding this comment.
The only thing I'd flag here is the missing event for purging a ledger. Lgtm otherwise.
| let source_name = branch_record.source_branch.as_ref().ok_or_else(|| { | ||
| ApiError::InvalidBranch(format!("Branch {branch_id} has no source branch")) | ||
| ApiError::InvalidBranch(format!( | ||
| "Cannot rebase the root branch '{branch}' of ledger '{ledger_name}' \ |
There was a problem hiding this comment.
This is hairsplitting, I know, so take or leave it as you wish. It's totally subjective, but perhaps we could just say "root" here. main (or whatever) is the root of the ledger, and everything else is a branch. It seems weird to me to have a notion of "root branch". I think "root" fits better or, to continue the tree metaphor, we could say "trunk" or "stem". But, like I said, very subjective, so take it or leave it.
| .map_err(|e| bad_input(format!("Invalid ledger id '{input}': {e}")))?; | ||
|
|
||
| if branch == DEFAULT_BRANCH { | ||
| let warning = format!( |
There was a problem hiding this comment.
I feel like this should also be an error to eliminate any possibility of surprise, especially since we're no longer treating the main name specially.
| // stale one. Soft mode just retracts. | ||
| let ns_result = if matches!(mode, DropMode::Hard) { | ||
| publisher | ||
| .drop_branch(&branch.ledger_id) |
There was a problem hiding this comment.
This bypasses the publisher.purge call that we used to make in favor of publisher.drop_branch for every branch. This matters because .purge used to emit the LedgerRectracted event to the subscribers. Now the subscribers won't know to clear their local caches.
| // to `NotFound` would let the HTTP route fall through to the | ||
| // drop_graph_source path, potentially deleting an unrelated graph | ||
| // source with the same name. | ||
| let all = self.nameservice().all_records().await?; |
There was a problem hiding this comment.
This will do a full nameservice scan then a filter to get the branches. I think it would be better to do the initial scan scoped to the ledger name prefix for the nameservice back ends that support it, but I think that would require a larger refactor of the nameservice traits, so it probably isn't for this pr.
There was a problem hiding this comment.
Agreed — not for this PR. Tracking in #1248.
… branch' Once 'main' carries no special meaning, calling the no-source-branch record the 'root branch' is doubly-loaded — every record is a branch, and one of them is the root. Lean on the tree metaphor instead: the record whose source_branch is None is the root, and the user's parameter is just the branch they named. Reword drop_branch and rebase_branch error messages, and update the test assertions that check those strings. Behavior unchanged.
drop_ledger previously accepted 'mydb:main' with a warning while rejecting any other suffix. With 'main' no longer carrying special meaning, that asymmetry invites the very surprise the PR is trying to remove — a caller passing 'mydb:main' may believe they're scoped to the main branch and instead wipe the whole ledger. Reject every branch-qualified id with the same 400 / ApiError::Http shape that non-default suffixes already returned, and route the caller to drop_branch with a concrete suggestion. parse_whole_ledger_input loses its Option<String> warning slot since no path produces one. Tests + docs (cli/drop.md, operations/admin-and-health.md, getting-started/rust-api.md, api/endpoints.md) updated to reflect the single accepted input form.
The whole-ledger drop path now invokes AdminPublisher::drop_branch for each branch (parent-aware row sweep) instead of Publisher::purge. The notifying wrapper had no event hook on that admin method, so subscribers — ledger cache, query peers — no longer received the LedgerRetracted signal they used to drive cache invalidation. Add the same event_bus.notify(LedgerRetracted) emission that retract/purge already perform. One event per branch drop, matching the per-branch loop above the call site.
Missed in the prior commit: the S3 testcontainers hard-drop test was still calling drop_ledger with the ':main'-qualified id, which the tightened parser now rejects with a 400. Switch to the bare name.
Sits on top of #1243
drop_ledgerwas misnamed: it accepted a ledger ID like"mydb:main", normalized it to a branch, and dropped only that one branch. That made dropping a multi-branch ledger a per-call iteration on the caller, and made dropping a ledger via name ("mydb") silently equivalent to dropping justmydb:mainwhile leaving siblings + the cross-branch@shared/dicts/namespace orphaned.This PR makes
drop_ledgerdo what it says: remove the entire ledger — every branch, every retracted-but-not-purged record, plus the shared dict namespace. It also replaces the "main is special" name-based guards indrop_branchandrebase_branchwith a structural check onsource_branch.is_none().Changes
drop_ledgeris now whole-ledgerall_records()(notlist_branches, which excludes retracted) so hard-drop cleans up retracted-but-not-purged branches too. Enumeration failures propagate asErr— silently coercing toNotFoundwould let the HTTP route fall through todrop_graph_sourceand potentially delete an unrelated graph source with the same name.source_branchpointers (cycle-safe). Partial failure leaves orphan parents, never dangling children.AdminPublisher::drop_branch(hard). The hard-mode path is parent-aware, so surviving parents keep accuratebranchescounts even if the operation aborts. ConcurrentNotFoundis race-as-success; any other nameservice failure bails withApiError::Dropbefore touching parents or@shared/. Retry is safe — every step is idempotent under partial prior progress.{ledger_name}/@shared/dicts/. No more orphan shared blobs on full-ledger drop.Input parsing is explicit
"mydb""mydb:main""mydb:dev"(or any non-default branch suffix)400/ApiError::Http. Caller almost certainly meantdrop_branch("mydb", "dev"); the error message says so.This is deliberately the conservative migration:
"mydb"cannot accidentally drop onlymydb:main, and"mydb:dev"cannot accidentally drop the wholemydbledger.Root-branch refusal is structural
Both
drop_branchandrebase_branchpreviously refused the literal string"main". The new check is record-based:record.source_branch.is_none(). That means:mydb:trunk) correctly refuses operations on its root."main"(e.g. created as a child oftrunk) is droppable / rebaseable like any other."main"is now purely a default-name convention —DEFAULT_BRANCHinfluree-db-core/src/ledger_id.rs. No code path treats the string"main"as semantically special.DropReportand HTTP wire formatAdditive shape change:
The HTTP
DropResponseexposes the per-branch detail asbranches_dropped: Vec<String>(omitted viaskip_serializing_ifwhen empty).{ "ledger_id": "mydb", "status": "dropped", "files_deleted": 73, "branches_dropped": ["mydb:feature-x", "mydb:dev", "mydb:main"] }The CLI's
fluree dropoutput gains a branch count on multi-branch drops:Docs
End-to-end refresh:
docs/api/endpoints.md—POST /dropreflects whole-ledger semantics, thebranches_droppedfield, leaf-first ordering, the input-form rules, and the400on non-default suffixes.POST /drop-branchlink fixed (was/branch/drop/#post-branchdrop).docs/getting-started/rust-api.md—drop_ledgersection reframed around whole-ledger semantics;drop_branchcallout for single-branch drops; updated idempotency to reflect newNotFoundbehavior.docs/cli/drop.md— bare-name input, leaf-first semantics, branch-suffix rejection, updated example output.docs/cli/server-integration.md— branch-drop and rebase contracts reframed as root-branch refusals (no more "Cannot be 'main'").docs/concepts/ledgers-and-nameservice.md— "main" is the default, root refusal is structural.docs/operations/admin-and-health.md—POST /dropsummary updated to match.Stale doc comment fixed
The Rust doc-comment above
drop_ledgerpreviously described the old branch-level flow ("Normalizes the ledger ID (ensures:mainsuffix)" → "Retracts from nameservice") and claimed hard mode still attempts cleanup for not-found ledgers. Rewritten end-to-end:NotFoundreturns without touching storage — orphaned storage with no NsRecord pointer is not swept bydrop_ledger, that's a separate admin concern.ApiError::Dropbehavior on real nameservice failure and that retry is safe.New / updated tests:
drop_ledger_accepts_bare_name_and_default_suffix— bare name is canonical; report uses the bare name; no warning.drop_ledger_main_suffix_warns_but_works—:mainform is accepted but attaches the migration warning.drop_ledger_rejects_non_default_branch_suffix—mydb:devis rejected and the error namesdrop_branch.drop_ledger_hard_clears_every_branch_and_shared— multi-branch ledger with one branch retracted-as-deferred (because it has live children); harddrop_ledgerwalks leaf-first, wipes@shared/dicts/, and re-creating the alias succeeds.drop_main_refused— refuses the default root with a "root branch" error.drop_branch_refuses_non_main_root— refuses a non-main-named root structurally.drop_branch_allows_non_root_named_main— allows a non-root branch literally named"main".rebase_main_refused/rebase_refuses_non_main_root— same structural refusal for rebase.LocalStack integration test from the prior PR (
s3_testcontainers_hard_drop_clears_ledger) still passes — exercises the same code paths against real S3 + DynamoDB.Migration notes
POST /drop: callers passing"mydb"now drop the whole ledger; previously they dropped onlymydb:main. Callers passing"mydb:main"see the same outcome but a warning is attached."mydb:dev"(any non-default branch suffix) will now get a 400. They should switch toPOST /drop-branch.drop_branch(the right tool for branch-scoped drops) is unchanged in shape.DropReport.ledger_idis now the bare ledger name. Anything asserting against the oldmydb:mainform needs updating.