feat(#335): add drift and prove it opens on the targets buildable here
Step 1 of #335, deliberately before any data migration: prove the store works before trusting it with message history. Adds drift + drift_flutter, a key/value StoredBlobs table mapping 1:1 onto the existing SharedPreferences keys, and verifyReadWrite() as a runtime probe. Key/value rather than relational tables on purpose: it maps directly onto the current store interfaces, so callers do not change and the migration can be verified row-for-row against the old keys. Relational schemas come later, once this is proven and querying is actually wanted. Verified here: - 3 tests pass, including a 6 MiB round-trip - larger than the 5 MiB localStorage cap that makes the web build impossible today - flutter analyze clean - Windows release build succeeds - Web release build compiles Two findings from the gate that a plan-on-paper would have missed: 1. sqlite3_flutter_libs 0.6.0+eol is a DEPRECATED no-op. From sqlite3 v3.x it is unnecessary and drift_flutter already covers it, so it is not a direct dependency here. 2. The web build COMPILES BUT WOULD FAIL AT RUNTIME: drift needs sqlite3.wasm and drift_worker.dart.js shipped in web/, and neither is present. Per drift docs they are downloaded from GitHub releases, version-matched to pubspec.lock (drift 2.34.2, sqlite3 3.5.0), and the server must serve .wasm as Content-Type: application/wasm. Not done here - downloading files needs explicit human approval. Also noted: the drift_dev CLI does not compile at these versions (allSchemaEntities missing from the drift3_preview GeneratedDatabase), so its asset tooling is unavailable. build_runner code generation is unaffected. No user data is touched. No store is switched over. The migration is the next step and remains gated on the web assets question.integration/290-306-335
parent
e17f539d7e
commit
19858698ee
@ -0,0 +1,76 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_flutter/drift_flutter.dart';
|
||||
|
||||
part 'offband_database.g.dart';
|
||||
|
||||
/// Bulk data store (#335). Replaces SharedPreferences for message history,
|
||||
/// contacts and discovered contacts.
|
||||
///
|
||||
/// SharedPreferences is a settings store, and using it for bulk data is what
|
||||
/// causes #306: on Windows every mutation re-encodes and rewrites the entire
|
||||
/// file synchronously, and on web it is backed by `localStorage`, which is
|
||||
/// capped at 5 MiB per origin and is also synchronous. This install already
|
||||
/// holds ~7 MB, so the web build cannot function at all today.
|
||||
///
|
||||
/// drift is used because it is the only option verified to cover all six
|
||||
/// targets: native SQLite on Android, iOS, macOS, Windows and Linux, and
|
||||
/// SQLite compiled to WebAssembly on web, where it stores through OPFS or
|
||||
/// IndexedDB and runs in a worker rather than on the UI thread.
|
||||
///
|
||||
/// Settings stay in SharedPreferences. Only bulk data moves here.
|
||||
@DriftDatabase(tables: [StoredBlobs])
|
||||
class OffbandDatabase extends _$OffbandDatabase {
|
||||
OffbandDatabase([QueryExecutor? executor])
|
||||
: super(executor ?? _defaultExecutor());
|
||||
|
||||
/// `drift_flutter` selects the platform backend: native SQLite on desktop
|
||||
/// and mobile, WASM on web. `web:` names the assets that must be shipped for
|
||||
/// the web build (`sqlite3.wasm`, `drift_worker.dart.js`).
|
||||
static QueryExecutor _defaultExecutor() {
|
||||
return driftDatabase(
|
||||
name: 'offband_store',
|
||||
web: DriftWebOptions(
|
||||
sqlite3Wasm: Uri.parse('sqlite3.wasm'),
|
||||
driftWorker: Uri.parse('drift_worker.dart.js'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
/// Step-1 gate for #335: proves the database opens, writes and reads back on
|
||||
/// the current platform. Deliberately trivial - no user data is involved, so
|
||||
/// this can be run on every target before any migration is written.
|
||||
Future<bool> verifyReadWrite() async {
|
||||
const probeKey = '__offband_probe__';
|
||||
final probeValue = 'ok-${DateTime.now().microsecondsSinceEpoch}';
|
||||
|
||||
await into(storedBlobs).insertOnConflictUpdate(
|
||||
StoredBlobsCompanion.insert(key: probeKey, value: probeValue),
|
||||
);
|
||||
|
||||
final row = await (select(
|
||||
storedBlobs,
|
||||
)..where((t) => t.key.equals(probeKey))).getSingleOrNull();
|
||||
|
||||
await (delete(storedBlobs)..where((t) => t.key.equals(probeKey))).go();
|
||||
|
||||
return row?.value == probeValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// One row per stored blob, replacing one SharedPreferences key each.
|
||||
///
|
||||
/// Deliberately key/value for the first migration: it maps 1:1 onto the
|
||||
/// existing store interfaces, so callers do not change and the migration is
|
||||
/// verifiable row-for-row against the old keys. Relational tables for messages
|
||||
/// and contacts are a later step, once this is proven and querying is wanted.
|
||||
class StoredBlobs extends Table {
|
||||
TextColumn get key => text()();
|
||||
TextColumn get value => text()();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {key};
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:meshcore_open/storage/drift/offband_database.dart';
|
||||
|
||||
/// Step-1 gate for #335: the database must open, write and read back before
|
||||
/// any user data is migrated into it.
|
||||
///
|
||||
/// This runs against an in-memory SQLite instance so it exercises the drift
|
||||
/// layer and generated code on any host. It does NOT prove the per-platform
|
||||
/// backends (native libs on desktop/mobile, WASM on web) — those are proven by
|
||||
/// building each target and by `verifyReadWrite()` at runtime.
|
||||
void main() {
|
||||
late OffbandDatabase db;
|
||||
|
||||
setUp(() => db = OffbandDatabase(NativeDatabase.memory()));
|
||||
tearDown(() => db.close());
|
||||
|
||||
test('opens, writes and reads back (the #335 gate)', () async {
|
||||
expect(await db.verifyReadWrite(), isTrue);
|
||||
});
|
||||
|
||||
test('round-trips a payload larger than the 5 MiB localStorage cap', () async {
|
||||
// The web build currently stores through localStorage, capped at 5 MiB per
|
||||
// origin, while this install already holds ~7 MB. Proving a >5 MiB value
|
||||
// survives is the whole point of the migration.
|
||||
final big = 'x' * (6 * 1024 * 1024);
|
||||
await db
|
||||
.into(db.storedBlobs)
|
||||
.insertOnConflictUpdate(
|
||||
StoredBlobsCompanion.insert(key: 'big', value: big),
|
||||
);
|
||||
|
||||
final row = await (db.select(
|
||||
db.storedBlobs,
|
||||
)..where((t) => t.key.equals('big'))).getSingle();
|
||||
|
||||
expect(row.value.length, big.length);
|
||||
});
|
||||
|
||||
test('upsert replaces rather than duplicating a key', () async {
|
||||
for (final v in ['first', 'second']) {
|
||||
await db
|
||||
.into(db.storedBlobs)
|
||||
.insertOnConflictUpdate(
|
||||
StoredBlobsCompanion.insert(key: 'k', value: v),
|
||||
);
|
||||
}
|
||||
|
||||
final rows = await (db.select(
|
||||
db.storedBlobs,
|
||||
)..where((t) => t.key.equals('k'))).get();
|
||||
|
||||
expect(rows, hasLength(1));
|
||||
expect(rows.single.value, 'second');
|
||||
});
|
||||
}
|
||||
Loading…
Reference in new issue