Delete rows safely
Soft delete, hard delete, dependency resolution, and the guard that stops you locking yourself out.
Owned by Admin · 9 steps · about 20 minutes
Why this exists
Deleting through the data suite is designed around one belief: most deletes should be reversible, and the irreversible ones should be hard to do by accident.
So soft delete is the default. Any table with an is_deleted column can be soft-deleted, which is a flag flip that keeps the row, its id and every foreign key pointing at it intact. It is the only kind of delete that has a restore button. Hard delete really removes the row and demands a typed confirmation string — not a checkbox, a typed word — because the muscle memory of clicking OK is exactly what the guard exists to defeat.
The second idea is that the suite refuses to leave the database inconsistent. Before a hard delete it computes what references the row. If anything does, it stops with 409 and hands you a report, and you must say what to do with each referencing column: reassign the children to another parent, cascade the delete down to them, or nullify the column if it is nullable. Cascades are recursive, but bounded — depth and row limits, children before parents, cycle-safe — because an unbounded cascade in a generic console is a way to lose a database.
Everything happens in one transaction, all or nothing, and every side effect is audited under a shared batch id, so a cascade that touched nine tables reads back as one operation.
Finally, the self-lockout guard. Deleting your own user row, your own session, or the grant that makes you an admin is refused unless you explicitly confirm you meant it. It has saved more evenings than any other check in this module.
Before you start
- An admin session.
- Sandbox rows to practise on: /debug/adminsuite/sandbox/seed creates parents with children.
- Read the dependency report before every hard delete. Every time.
Practise with
| Persona | Password | Note | |
|---|---|---|---|
| admin | admin@club.test | admin123 | the only persona with delete rights anywhere in the suite |
Steps 1–9 — Admin
their manual →-
1Seed the sandbox fixtures so you can practise destructive operations on rows nobody cares about.Expected result Parent rows with children hanging off them.Watch out for Practise here first. The suite will happily let you cascade real business data, and it will be right to.
-
2Open the row you intend to delete and read the dependencies panel at the bottom.Expected result Every inbound foreign key, how many rows point at this one, whether that column is nullable, and whether the referencing table is even writable.Watch out for A read-only referencing table is the reason a cascade will refuse later. Find that out now, not halfway through.
-
3Pull the same report as JSON when you want to check several rows or keep a record of what you were told.Expected result A list of referencing tables and columns with sample primary keys.Watch out for Sample keys are a sample. The count is the number that matters.
-
4Dry-run the whole thing: the dependency report plus a projected cascade plan with per-table delete counts and depth.Expected result A plan that executes nothing and tells you whether it is within the depth and row limits.Watch out for If within_limits is false, do not go looking for a bigger limit. Delete in smaller pieces or fix the data model.
-
5Do the ordinary thing first: a soft delete. Send the primary keys and soft mode.Expected result is_deleted flips to 1, updated_at is stamped, and one audit row per row is written.Watch out for A table with no is_deleted column answers 422 soft_unavailable and tells you to resubmit as hard. That is your cue to go and read the dependency report, not to just change the word.
-
6Restore what you just soft-deleted, and notice how boring it is.Expected result The row comes back exactly as it was, with a restore audit row.Watch out for 409 restore_conflict means a partial unique index has been filled by something else while the row was away — typically a second row now holds the email or the slug. The restore is refused rather than breaking the index.
-
7Now a hard delete on a parent that has children: send hard mode, the typed confirmation, and a resolution for each referencing column — reassign to another parent, cascade, or nullify.Expected result One transaction, a per-table breakdown of what was deleted, reassigned or nullified, and every effect audited under a shared batch id.Watch out for Without the typed confirmation you get 422 confirmation_required. Without resolutions you get 409 fk_dependencies with the report attached. Neither is the system being awkward — it is refusing to guess.
-
8Deliberately try to delete your own admin grant, so you have seen the guard fire in a safe moment.Expected result 409 self_lockout_guard, telling you to resubmit with an explicit self-confirmation.Watch out for It also covers your own user row and your own session. The confirmation exists for the legitimate case of decommissioning your own account — think carefully about who is left holding admin before you use it.
-
9Filter the audit browser by the batch id from the cascade and read the whole operation as one story.Expected result Children before parents, each with its full old snapshot.Watch out for Those old snapshots are the only copy of a hard-deleted row. If a hard delete turns out to be wrong, the audit log is where the data is — there is no undelete.