Skip to content

Recover Old Dittos: always-visible menu + WAL sidecar fallback#8

Merged
kern merged 4 commits into
masterfrom
claude/recover-dittos-menu-missing-GjLze
May 22, 2026
Merged

Recover Old Dittos: always-visible menu + WAL sidecar fallback#8
kern merged 4 commits into
masterfrom
claude/recover-dittos-menu-missing-GjLze

Conversation

@kern
Copy link
Copy Markdown
Owner

@kern kern commented May 22, 2026

Summary

Two changes to the "Recover Old Dittos" flow, motivated by a TestFlight report from a user on 3.0.2 (1) who saw no dittos return and no menu entry to retry.

1. Always show the menu

The entry point was gated on LegacyDataMigrator.hasRecoverableLegacyData, i.e. a legacy Ditto.sqlite being present on disk. Users whose 2.x data was destroyed by 3.0.0's cleanup therefore saw no menu at all — exactly the cohort that most needs a clear explanation.

runLegacyRecovery() already surfaces a structured result for every terminal state (nothing on disk, found-but-unreadable, empty store, inserted N), so unconditionally showing the button gives those users a real explanation instead of a silently-missing affordance. hasRecoverableLegacyData and its test are deleted (no remaining callers).

2. Recover from orphaned WAL sidecars

When 3.0.0 deleted the main Ditto.sqlite but left Ditto.sqlite-wal behind, the WAL still holds pages that would have been checkpointed to the main DB. We can't reopen them through SQLite (the WAL salt references a DB header we no longer have), so a new WALSidecarRecovery type parses raw WAL frames and walks SQLite B-tree leaf pages directly, extracting TEXT-typed record values.

LegacyDataMigrator.recoverNow now falls through to this path when the main .sqlite is missing but a -wal exists. Recovered phrases land in a single "Recovered" category (relational structure is unrecoverable from the WAL alone). previewRecoverableData and the confirmation alert surface a distinct WAL-mode message so users know what to expect.

Caveats:

  • -shm-only orphans aren't recoverable (no page data).
  • Cells whose payload spills onto SQLite overflow pages are skipped — fine for ditto-length text.
  • WAL checksums aren't verified; the < 2 / > 5000 char filter, UTF-8 validity check, and Core Data internal-prefix filter (Z_PRIMARYKEY, Z_METADATA, etc.) catch the common garbage cases.

Test plan

  • WALSidecarRecovery.parse against a synthetic WAL built byte-by-byte (TEXT records extracted, dedup, internal-prefix filter, too-short filter, non-WAL files rejected)
  • Existing LegacyDataMigrator tests still pass (Core Data path unchanged)
  • TestFlight: install over 3.0.2 (1), tap ⋯ → menu should now show "Recover Old Dittos" even when no legacy file is found
  • TestFlight: for a user with an orphan -wal, confirm the alert shows "Found N recoverable phrases in a backup file…" and importing creates the "Recovered" category

https://claude.ai/code/session_019wBT9jw5uskSPiuFSyy7gQ

claude added 2 commits May 22, 2026 19:40
The entry point was gated on a legacy SQLite file being present on disk,
so users whose 2.x data was destroyed by 3.0.0's cleanup saw no menu at
all — exactly the cohort that most needs a clear explanation.

runLegacyRecovery() already surfaces a structured result for every case
(nothing on disk, found-but-unreadable, empty store, inserted N), so
unconditionally showing the button gives those users an explanation
instead of a missing UI affordance.

https://claude.ai/code/session_019wBT9jw5uskSPiuFSyy7gQ
When 3.0.0's cleanup deleted the main Ditto.sqlite but left -wal behind,
the file holds pages that would have been checkpointed to the main DB on
next open. We can't reopen them through SQLite (the WAL salt references a
header we no longer have), so this parses raw WAL frames and walks SQLite
B-tree leaf pages to extract TEXT record values directly.

WALSidecarRecovery is a standalone parser exercised by LegacyDataMigrator
when recoverNow finds no main .sqlite but does find an orphan -wal. Phrases
land in a single "Recovered" category since category structure is
unrecoverable from the WAL alone. The confirmation alert surfaces a
distinct message for that mode so users know what to expect.

Best-effort by construction: WAL checksums aren't verified, overflow
chains aren't followed (we don't have the main DB's free-page list), and
filtering drops obvious Core Data internals (Z_PRIMARYKEY, etc.) plus
too-short/too-long strings. Synthetic-WAL tests exercise the parser
against the SQLite file format directly.

https://claude.ai/code/session_019wBT9jw5uskSPiuFSyy7gQ
@kern kern changed the title Always show "Recover Old Dittos" in the menu Recover Old Dittos: always-visible menu + WAL sidecar fallback May 22, 2026
claude added 2 commits May 22, 2026 20:28
- file_length/type_body_length: LegacyDataMigrator legitimately
  concentrates discovery/read/write/telemetry plus the orphan-WAL
  fallback in one place because they share App-Group state and the
  outcome telemetry pipeline. Disable both rules at the file level
  with a comment explaining why.
- consecutiveSpaces: drop the vertical alignment on inline comments
  in the synthetic-WAL test helper; SwiftFormat collapses those.

https://claude.ai/code/session_019wBT9jw5uskSPiuFSyy7gQ
- blanket_disable_command on LegacyDataMigrator.swift: add a matching
  swiftlint:enable for the file_length / type_body_length pair so the
  disable isn't blanket.
- cyclomatic_complexity on parseTableLeafCell: split out parseRecordHeader,
  extractTexts, and decodeKeepableText. Behavior is identical; complexity
  per function drops from 12 to ≤6.

https://claude.ai/code/session_019wBT9jw5uskSPiuFSyy7gQ
@kern kern merged commit cedaf98 into master May 22, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants