Skip to content

Commit 82eb6ff

Browse files
committed
Log SQLite recovery approval codes
Print the final incident token and document the quarantine-only recovery path.
1 parent cbfdf9e commit 82eb6ff

3 files changed

Lines changed: 34 additions & 10 deletions

File tree

README.md

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -619,17 +619,14 @@ this prevents Docker restart policies from repeating the same failure.
619619

620620
The report identifies the incident with a fingerprint and issues a separate
621621
one-time **incident token**. The startup log prints the exact value to set, and
622-
the report repeats it under `actions`. Copy that token; the fingerprint is an
623-
identifier, not an approval. There are three choices:
622+
the report repeats it under `actions`. Copy the approval code from the startup
623+
log, or use the matching `incident_token`/`actions` entry in the protected
624+
report; the fingerprint is an identifier, not an approval. There are two
625+
choices:
624626

625627
1. **Restore or repair:** stop Floppy, back up the database with its `-wal` and
626628
`-shm` files, then restore a known-good copy or repair the named rows.
627-
2. **Accept:** set `FLOPPY_SQLITE_CONFLICT_ACTION=accept:<incident-token>` and
628-
recreate the container. Floppy starts without changing the conflicting rows.
629-
Accept gets you back online on your current schema. It is not an upgrade
630-
path: Django re-checks every foreign key while it applies a migration, so a
631-
pending migration keeps failing until you repair or quarantine the rows.
632-
3. **Quarantine:** set
629+
2. **Quarantine:** set
633630
`FLOPPY_SQLITE_CONFLICT_ACTION=quarantine:<incident-token>` and recreate the
634631
container. Floppy first writes and verifies a full backup under
635632
`sqlite-recovery/`, then removes the orphaned child rows and verifies all

src/config/sqlite_recovery_policy.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,18 @@ def _annotate_blocked_report(
538538
return plan
539539

540540

541+
def _log_blocked_recovery_options(report: dict, plan: dict) -> None:
542+
"""Print the current repair approval code for a final blocked incident."""
543+
token = report.get("incident_token")
544+
if not plan.get("can_repair") or not isinstance(token, str) or not token:
545+
return
546+
_log(f"[entrypoint] SQLite recovery approval code: {token}")
547+
_log(
548+
"[entrypoint] To approve relationship repair, set "
549+
f"{_ACTION_ENV}=quarantine:{token} and restart Floppy.",
550+
)
551+
552+
541553
def check_database_for_startup(db_path: str) -> None:
542554
"""Repair safe relationship damage or block before migrations."""
543555
emit = _status_emitter(db_path)
@@ -568,6 +580,7 @@ def check_database_for_startup(db_path: str) -> None:
568580
and plan.get("can_repair")
569581
and int(plan.get("safe_relationships", 0)) > 0
570582
):
583+
_log_blocked_recovery_options(report, plan)
571584
raise SystemExit(1)
572585

573586
emit("running", "repair")
@@ -615,9 +628,10 @@ def check_database_for_startup(db_path: str) -> None:
615628
refreshed = _scan_and_publish_block(db_path, emit)
616629
if refreshed is None:
617630
return
618-
_annotate_blocked_report(
631+
refreshed_plan = _annotate_blocked_report(
619632
db_path,
620633
refreshed,
621634
prior_safe_repair=safe_summary,
622635
)
636+
_log_blocked_recovery_options(refreshed, refreshed_plan)
623637
raise SystemExit(1)

src/config/tests/test_sqlite_data_preserving_recovery.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import io
12
import json
23
import os
34
import sqlite3
@@ -95,7 +96,7 @@ def test_mixed_music_damage_preserves_tracking_until_explicit_repair(self):
9596
},
9697
),
9798
self.assertRaises(SystemExit) as blocked,
98-
mock.patch("sys.stderr"),
99+
mock.patch("sys.stderr", new_callable=io.StringIO) as stderr,
99100
):
100101
check_database_for_startup(db_path)
101102

@@ -123,6 +124,18 @@ def test_mixed_music_damage_preserves_tracking_until_explicit_repair(self):
123124
conn.close()
124125

125126
report = self._read_report(db_path)
127+
token = report["incident_token"]
128+
log_output = stderr.getvalue()
129+
self.assertIn(f"SQLite recovery approval code: {token}", log_output)
130+
self.assertIn(
131+
f"FLOPPY_SQLITE_CONFLICT_ACTION=quarantine:{token}",
132+
log_output,
133+
)
134+
self.assertEqual(
135+
log_output.count("FLOPPY_SQLITE_CONFLICT_ACTION=quarantine:"),
136+
1,
137+
)
138+
self.assertNotIn("FLOPPY_SQLITE_CONFLICT_ACTION=accept:", log_output)
126139
self.assertEqual(report["status"], "blocked")
127140
self.assertNotIn("accept", report["actions"])
128141
self.assertIn("quarantine", report["actions"])

0 commit comments

Comments
 (0)