Recovery
Generate SQL to reverse accidental database changes
dbtrail can generate SQL to reverse accidental database changes — undoing DELETEs, reverting UPDATEs, or removing unintended INSERTs.
Always dry-run
Recovery always operates in dry-run mode. dbtrail generates the SQL but never executes it. A DBA must review and run the recovery SQL manually.
How recovery works
dbtrail indexes the full before/after state of every row change. These before/after row images are captured by the open-source bintrail CLI as it parses MySQL binary log events. When you request recovery, dbtrail uses this data to generate the inverse SQL:
| Original event | Recovery action |
|---|---|
| DELETE | INSERT INTO ... using the row_before values |
| UPDATE | UPDATE ... SET reverting to row_before values |
| INSERT | DELETE FROM ... matching the row_after values |
The generated SQL uses primary-key-only WHERE clauses when a schema snapshot is available, falling back to all-columns WHERE otherwise.
Foreign key awareness
Recovery is FK-aware by default. When you recover a deleted row, dbtrail automatically checks whether any parent rows referenced by foreign keys were also deleted — and includes them in the recovery SQL in the correct order.
Why this matters
A naive single-table recovery can fail silently. If you deleted a customer and their orders in the same transaction, re-inserting the orders first would violate the foreign key constraint on orders.customer_id. dbtrail prevents this by:
- Resolving parent rows — walks up the FK chain to find deleted parents that must be restored first (enabled by default via
resolve_fk) - Optionally including dependents — walks down the FK chain to include deleted child rows (opt-in via
include_dependents) - Topologically sorting — uses Kahn's algorithm to order INSERT statements so parents are always restored before children
- Handling circular and self-referencing FKs — when a cycle is detected (e.g.
employees.manager_id → employees.id), the SQL is wrapped withSET FOREIGN_KEY_CHECKS=0/SET FOREIGN_KEY_CHECKS=1
Resolution is recursive up to 5 levels deep. If your schema has deeper FK chains, the remaining rows are skipped with a warning.
Example
Given this schema:
customers (id, name)
orders (id, customer_id → customers.id, total)
order_items (id, order_id → orders.id, product, qty)Recovering a deleted customer with include_dependents: true generates:
-- FK-aware recovery generated by bintrail SaaS
-- Recovery steps: 4
BEGIN;
-- Step 1: customers (pk={'id': 42}) — requested recovery target
INSERT INTO `mydb`.`customers` (`id`, `name`) VALUES (42, 'Acme Corp');
-- Step 2: orders (pk={'id': 100}) — dependent of mydb.customers via customer_id
INSERT INTO `mydb`.`orders` (`id`, `customer_id`, `total`) VALUES (100, 42, 250.00);
-- Step 3: orders (pk={'id': 101}) — dependent of mydb.customers via customer_id
INSERT INTO `mydb`.`orders` (`id`, `customer_id`, `total`) VALUES (101, 42, 75.00);
-- Step 4: order_items (pk={'id': 500}) — dependent of mydb.orders via order_id
INSERT INTO `mydb`.`order_items` (`id`, `order_id`, `product`, `qty`) VALUES (500, 100, 'Widget', 3);
COMMIT;Notice that customers is inserted before orders, and orders before order_items — matching the FK dependency order.
Set resolve_fk: false to get single-table recovery without any FK resolution.
MySQL 8.x CASCADE limitation
ON DELETE CASCADE is invisible to the binlog on MySQL 8.x
When a parent row is deleted and the foreign key uses ON DELETE CASCADE, InnoDB handles the child deletions entirely inside the storage engine — they never appear in the binary log. This is a MySQL/InnoDB limitation, not a bintrail or dbtrail limitation. Since bintrail reads the binlog, cascaded child deletions are invisible to both query and recover.
This means:
include_dependents: truewill not find child rows that were removed by CASCADE (there are no binlog events to discover)- Querying the child table for recent DELETEs will not show CASCADE deletions
- Only the parent DELETE is visible to dbtrail
Workaround: Replace ON DELETE CASCADE with ON DELETE RESTRICT and handle child deletions explicitly in application code or a BEFORE DELETE trigger on the parent table. Explicit DELETEs are logged in the binlog and fully visible to dbtrail.
MySQL 9.6+: This limitation is resolved — cascade enforcement was moved to the SQL layer, making cascaded operations visible to the binlog.
Using recovery
Via the API
The recover endpoint returns the generated SQL, an affected row count, and a list of operations. See the Recover API reference for full request/response details.
Via Claude
Ask Claude:
"Generate recovery SQL for the deleted order 12345 in the orders table"
Claude will call the recover tool and present the generated SQL for your review.
Recovery parameters
| Parameter | Required | Description |
|---|---|---|
server_id | yes | UUID of the registered server |
schema | yes | Database schema name |
table | yes | Table name |
primary_keys | no | Array of primary key values to recover |
target_time | no | ISO 8601 point-in-time to recover to |
resolve_fk | no | Auto-resolve missing FK parent rows (default: true) |
include_dependents | no | Also recover deleted child rows (default: false) |
Best practices
- Always review the SQL before executing — recovery SQL can have unintended side effects if the data has changed since the original event
- Use specific filters — narrow down by table, primary key, and time range to avoid generating more statements than needed
- Test in a staging environment first when recovering large batches
- Run inside a transaction — the generated SQL is already wrapped in
BEGIN/COMMIT, so you canROLLBACKif something looks wrong
Permissions
Recovery requires the recover:execute permission, available to owners, admins, and operators.