Recover Old Dittos: always-visible menu + WAL sidecar fallback#8
Merged
Conversation
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
- 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 legacyDitto.sqlitebeing 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.hasRecoverableLegacyDataand its test are deleted (no remaining callers).2. Recover from orphaned WAL sidecars
When 3.0.0 deleted the main
Ditto.sqlitebut leftDitto.sqlite-walbehind, 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 newWALSidecarRecoverytype parses raw WAL frames and walks SQLite B-tree leaf pages directly, extracting TEXT-typed record values.LegacyDataMigrator.recoverNownow falls through to this path when the main.sqliteis missing but a-walexists. Recovered phrases land in a single "Recovered" category (relational structure is unrecoverable from the WAL alone).previewRecoverableDataand 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).< 2/> 5000char filter, UTF-8 validity check, and Core Data internal-prefix filter (Z_PRIMARYKEY,Z_METADATA, etc.) catch the common garbage cases.Test plan
WALSidecarRecovery.parseagainst a synthetic WAL built byte-by-byte (TEXT records extracted, dedup, internal-prefix filter, too-short filter, non-WAL files rejected)LegacyDataMigratortests still pass (Core Data path unchanged)-wal, confirm the alert shows "Found N recoverable phrases in a backup file…" and importing creates the "Recovered" categoryhttps://claude.ai/code/session_019wBT9jw5uskSPiuFSyy7gQ