fix: Fix the demo app when migrations mismatch (#93)

This commit is contained in:
Marcelo Mendonça Soares 2026-07-03 17:08:54 -03:00 • committed by GitHub
commit a9fa6b6b91
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 50 additions and 5 deletions

View file

@ -1011,9 +1011,10 @@ class DemoController extends ChangeNotifier {
if (!dbDir.existsSync()) { if (!dbDir.existsSync()) {
dbDir.createSync(recursive: true); dbDir.createSync(recursive: true);
} }
final replica = await OfflineReplica.open( final databasePath = p.join(dbDir.path, '${user.username}-${slot.name}.db');
final replica = await OfflineReplica.openOrReset(
client: client, client: client,
databasePath: p.join(dbDir.path, '${user.username}-${slot.name}.db'), databasePath: databasePath,
persistentUserId: user.authUserId, persistentUserId: user.authUserId,
); );
_sessions[key] = replica; _sessions[key] = replica;

View file

@ -1,4 +1,7 @@
import 'package:flutter/foundation.dart' show kDebugMode; import 'dart:io';
import 'package:flutter/foundation.dart' show debugPrint, kDebugMode;
import 'package:path/path.dart' as p;
import 'package:serverpod_database/serverpod_database.dart' import 'package:serverpod_database/serverpod_database.dart'
show ClientDatabaseSession; show ClientDatabaseSession;
import 'package:serverpod_offline_sync_client/serverpod_offline_sync_client.dart'; import 'package:serverpod_offline_sync_client/serverpod_offline_sync_client.dart';
@ -13,8 +16,10 @@ import 'table_ops.dart' show demoSyncTables;
/// understand the package. The four steps are: /// understand the package. The four steps are:
/// ///
/// 1. [open] opens a local SQLite database ([Client.createSession]) /// 1. [open] opens a local SQLite database ([Client.createSession])
/// and wraps it in a [CrdtDatabaseSession]. The rest of the app /// and wraps it in a [CrdtDatabaseSession]. Use
/// reads and writes generated models through [session]. /// [openOrReset] when the file may predate the shipped
/// schema. The rest of the app reads and writes generated models through
/// [session].
/// 2. CRUD happens against [session] with the generated model APIs (see /// 2. CRUD happens against [session] with the generated model APIs (see
/// the `seed*` methods in `DemoController` for the plain, recommended form, /// the `seed*` methods in `DemoController` for the plain, recommended form,
/// e.g. `Person.db.insertRow(crdtSession, person)`). /// e.g. `Person.db.insertRow(crdtSession, person)`).
@ -63,6 +68,41 @@ class OfflineReplica {
return OfflineReplica._(rawSession, crdtSession, persistentUserId); return OfflineReplica._(rawSession, crdtSession, persistentUserId);
} }
/// Like [open], but deletes [databasePath] and retries once when opening
/// fails — for example when migrations were regenerated during development
/// and a leftover file records a version this build no longer ships.
///
/// A replica is a disposable local cache; syncing re-pulls server state.
static Future<OfflineReplica> openOrReset({
required Client client,
required String databasePath,
required UuidValue persistentUserId,
}) async {
var retry = true;
while (true) {
try {
return await open(
client: client,
databasePath: databasePath,
persistentUserId: persistentUserId,
);
} catch (_) {
if (!retry) rethrow;
retry = false;
for (final suffix in const ['', '-wal', '-shm']) {
final file = File('$databasePath$suffix');
if (file.existsSync()) file.deleteSync();
}
if (kDebugMode) {
debugPrint(
'offline_sync_demo: discarded stale replica '
'${p.basename(databasePath)}.',
);
}
}
}
}
/// Pushes local pending changes through [client] and merges remote ones once. /// Pushes local pending changes through [client] and merges remote ones once.
/// ///
/// Connectivity is expressed entirely by [client]: pass an online client to /// Connectivity is expressed entirely by [client]: pass an online client to

View file

@ -178,6 +178,10 @@ final Map<String, TableOps> demoTableOps = {
create: (id, label, foreignKeys) => create: (id, label, foreignKeys) =>
UniqueComposite(id: id, scope: label, value: label), UniqueComposite(id: id, scope: label, value: label),
), ),
_ops<UniqueDiscriminator>(
create: (id, label, foreignKeys) =>
UniqueDiscriminator(id: id, categoryId: 0, name: label),
),
_ops<UniqueSetNullChild>( _ops<UniqueSetNullChild>(
create: (id, label, foreignKeys) => UniqueSetNullChild( create: (id, label, foreignKeys) => UniqueSetNullChild(
id: id, id: id,