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
Strycher 1 day ago
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};
}

@ -62,7 +62,7 @@ dependencies:
url_launcher: ^6.3.0 # Launch URLs in system browser
flutter_linkify: ^6.0.0 # Auto-detect and linkify URLs in text
gpx: ^2.3.0
path_provider: ^2.1.5
path_provider: ^2.1.6
share_plus: ^12.0.1
build_pipe: ^0.3.1
material_symbols_icons: ^4.2906.0
@ -73,6 +73,8 @@ dependencies:
ml_dataframe: ^1.0.0
llamadart: '>=0.6.8 <0.7.0'
flutter_langdetect: ^0.0.1
drift: ^2.34.2
drift_flutter: ^0.3.1
hooks:
user_defines:
@ -95,6 +97,8 @@ dev_dependencies:
# rules and activating additional ones.
flutter_lints: ^6.0.0
flutter_launcher_icons: ^0.14.4
drift_dev: ^2.34.0
build_runner: ^2.15.1
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

@ -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…
Cancel
Save

Powered by TurnKey Linux.