todo-app/lib/data/legacy_sqlite_migration.dart
Krzysztof kuhy Rudnicki f4913daaa8
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
E2E-encrypt todo sync
Todo: fda71ed1

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-12 08:39:05 +01:00

87 lines
3.0 KiB
Dart

import 'dart:io';
import 'package:crdt_sync/crdt_sync.dart';
// Only SqliteCrdt/CrdtTableExecutor are needed to read the legacy DB; hide its
// Hlc so the crdt_sync Hlc (the store's clock type) is unambiguous.
import 'package:sqlite_crdt/sqlite_crdt.dart' hide Hlc;
import 'package:todo/data/note.dart';
import 'package:todo/data/note_repository.dart';
/// Reads a pre-crdt_sync `sqlite_crdt` database into a crdt_sync [Log].
///
/// Lives in its own `dart:io` library so [NoteRepository] can stay pure Dart:
/// the desktop app is a web build, and one `dart:io` import anywhere in the
/// graph fails the whole web compile. Mobile still runs this on first launch
/// after the cutover; web never does, because there is no legacy DB in a
/// browser profile.
class LegacySqliteMigration {
/// Whether a legacy database exists at [path] to migrate from.
static bool exists(String path) => File(path).existsSync();
/// Opens the legacy DB at [path] and returns its notes as a [Log], plus the
/// legacy CRDT node id.
///
/// Reusing the legacy node id keeps a device that already synced on the same
/// `devices/<id>` identity across the cutover, so it does not appear as a new
/// peer and re-upload everything.
static Future<({Log log, String nodeId})> read(
String path,
String preferredNodeId,
) async {
final legacy = await SqliteCrdt.open(
path,
version: _version,
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
final nodeId = preferredNodeId.isEmpty ? legacy.nodeId : preferredNodeId;
final rows = await legacy.query(
'SELECT id, text, priority, status, created_at, updated_at, is_deleted '
'FROM notes',
);
final log = <String, Record>{};
for (final row in rows) {
final note = Note.fromRow(row);
log[note.id] = NoteRepository.recordAtNoteTime(
note,
nodeId,
deleted: (row['is_deleted'] as int? ?? 0) != 0,
);
}
await legacy.close();
return (log: log, nodeId: nodeId);
}
static const int _version = 3;
// coverage:ignore-start
// Unreachable: migration only ever opens a legacy DB that already exists
// (guarded by [exists]), so sqlite never runs onCreate. Kept because
// SqliteCrdt.open requires the callback.
static Future<void> _onCreate(CrdtTableExecutor db, int version) async {
await db.execute('''
CREATE TABLE notes (
id TEXT NOT NULL,
text TEXT NOT NULL DEFAULT '',
priority INTEGER NOT NULL DEFAULT 2,
status INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (id)
)
''');
}
// coverage:ignore-end
static Future<void> _onUpgrade(CrdtTableExecutor db, int from, int to) async {
if (from < 2) {
await db.execute(
'ALTER TABLE notes ADD COLUMN status INTEGER NOT NULL DEFAULT 0',
);
}
if (from < 3) {
await db.execute('UPDATE notes SET priority = 2 WHERE priority = 0');
}
}
}