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:
@@ -13,6 +13,7 @@ import 'screens/home_screen.dart';
|
||||
import 'screens/vault_setup_screen.dart';
|
||||
import 'services/database_service.dart';
|
||||
import 'services/vault_service.dart';
|
||||
import 'storage/sqlite_to_sidecar_migrator.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
// Kind-aware binding (extends WidgetsFlutterBinding) must be the active
|
||||
@@ -98,6 +99,7 @@ class _VaultGateState extends State<VaultGate> {
|
||||
bool _valid = false;
|
||||
bool _hadStoredPath = false;
|
||||
bool _loading = true;
|
||||
bool _migrating = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -116,6 +118,27 @@ class _VaultGateState extends State<VaultGate> {
|
||||
_hadStoredPath = (vault.vaultRoot?.isNotEmpty ?? false);
|
||||
_loading = false;
|
||||
});
|
||||
if (valid) await _maybeMigrate(vault);
|
||||
}
|
||||
|
||||
/// Run the one-time SQLite→sidecar migration ONCE per vault (Phase 5, §B).
|
||||
/// Gated on [VaultService.vaultMigrationDone]; a fresh install (no legacy DB)
|
||||
/// is a fast no-op. The live DB is first reopened at the vault cache location
|
||||
/// so post-migration reads hit the new index, never the renamed legacy file.
|
||||
Future<void> _maybeMigrate(VaultService vault) async {
|
||||
// Move the live cache DB to the vault location now that the root is valid.
|
||||
await DatabaseService.reopen();
|
||||
if (vault.vaultMigrationDone) return;
|
||||
if (mounted) setState(() => _migrating = true);
|
||||
try {
|
||||
await SqliteToSidecarMigrator(vault).run();
|
||||
await vault.setVaultMigrationDone();
|
||||
} catch (_) {
|
||||
// A failed migration leaves the legacy DB intact (it is only renamed to
|
||||
// `.premigration` after a successful pass) and the flag unset, so the
|
||||
// next launch retries. Never block the user from reaching the app.
|
||||
}
|
||||
if (mounted) setState(() => _migrating = false);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -125,11 +148,28 @@ class _VaultGateState extends State<VaultGate> {
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
if (_migrating) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Migrating your notebooks…'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_valid) return const HomeScreen();
|
||||
return VaultSetupScreen(
|
||||
vaultService: _vault!,
|
||||
missing: _hadStoredPath,
|
||||
onVaultReady: () => setState(() => _valid = true),
|
||||
onVaultReady: () async {
|
||||
if (_vault != null) await _maybeMigrate(_vault!);
|
||||
if (mounted) setState(() => _valid = true);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user