feat(storage): one-time SQLite to sidecar migration
Some checks failed
CI / Windows build (push) Has been cancelled
Some checks failed
CI / Windows build (push) Has been cancelled
Phase 5. On first launch with a valid vault, migrate legacy SQLite data into vault sidecars so nothing is lost on upgrade. - SqliteToSidecarMigrator: documents (+ per-page ink, scratch-links + scratchpads, bookmarks) -> notebook folder + <file>.badnote.json; notes (+ strokes) -> notebook.badnote.json. Reuses existing JSON. - Idempotent (skips already-migrated targets); missing source files still get their annotations migrated. - DB relocates to <vault>/.badnote/index.sqlite; the legacy DB is renamed to .premigration ONLY after a successful pass, so a failed migration leaves data intact and the run-once flag unset. - main.dart runs it once, gated on vaultMigrationDone. Golden migrator tests (seeded legacy DB -> sidecars, idempotent re-run, legacy preserved). analyze clean, tests green.
This commit is contained in:
@@ -221,10 +221,13 @@ class BadnoteSidecar {
|
||||
Map<int, List<SidecarHighlight>>? highlights,
|
||||
List<Bookmark>? bookmarks,
|
||||
List<SidecarScratchLink>? scratchLinks,
|
||||
Map<int, String>? legacyAnnotations,
|
||||
this.legacyId,
|
||||
}) : strokes = strokes ?? <int, List<EditorStroke>>{},
|
||||
highlights = highlights ?? <int, List<SidecarHighlight>>{},
|
||||
bookmarks = bookmarks ?? <Bookmark>[],
|
||||
scratchLinks = scratchLinks ?? <SidecarScratchLink>[];
|
||||
scratchLinks = scratchLinks ?? <SidecarScratchLink>[],
|
||||
legacyAnnotations = legacyAnnotations ?? <int, String>{};
|
||||
|
||||
/// Schema version (`badnoteSidecarVersion`).
|
||||
final int version;
|
||||
@@ -253,6 +256,19 @@ class BadnoteSidecar {
|
||||
final List<Bookmark> bookmarks;
|
||||
final List<SidecarScratchLink> scratchLinks;
|
||||
|
||||
/// Raw legacy per-page `annotation_json` blobs preserved verbatim from the
|
||||
/// DEAD pre-editor `annotations` SQLite table (keyed by page number). Populated
|
||||
/// only by the one-time SQLite→sidecar migration so no legacy data is silently
|
||||
/// dropped; the live editor ignores it. Empty for all freshly authored
|
||||
/// sidecars.
|
||||
final Map<int, String> legacyAnnotations;
|
||||
|
||||
/// The legacy SQLite row id this sidecar was migrated from (a `documents.id`
|
||||
/// or `notes.id`). Set ONLY by the one-time migration; it makes the migration
|
||||
/// idempotent (a re-run recognizes an already-migrated item by this id even if
|
||||
/// its folder name collided). Null for all freshly authored sidecars.
|
||||
final String? legacyId;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'badnoteSidecarVersion': version,
|
||||
if (sourceFile != null) 'sourceFile': sourceFile,
|
||||
@@ -274,6 +290,12 @@ class BadnoteSidecar {
|
||||
},
|
||||
'bookmarks': bookmarks.map((b) => b.toJson()).toList(),
|
||||
'scratchLinks': scratchLinks.map((s) => s.toJson()).toList(),
|
||||
if (legacyAnnotations.isNotEmpty)
|
||||
'legacyAnnotations': {
|
||||
for (final entry in legacyAnnotations.entries)
|
||||
entry.key.toString(): entry.value,
|
||||
},
|
||||
if (legacyId != null) 'legacyId': legacyId,
|
||||
};
|
||||
|
||||
factory BadnoteSidecar.fromJson(Map<String, dynamic> json) {
|
||||
@@ -316,6 +338,18 @@ class BadnoteSidecar {
|
||||
scratchLinks: ((json['scratchLinks'] as List<dynamic>?) ?? const [])
|
||||
.map((e) => SidecarScratchLink.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
legacyAnnotations: () {
|
||||
final raw = json['legacyAnnotations'];
|
||||
final out = <int, String>{};
|
||||
if (raw is Map) {
|
||||
raw.forEach((key, value) {
|
||||
final page = int.tryParse(key.toString());
|
||||
if (page != null && value is String) out[page] = value;
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}(),
|
||||
legacyId: json['legacyId'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user