From da5c2fedbee22369224d5acdd8307db4d6bd80eb Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:14:55 +0530 Subject: [PATCH] fix: save user_version in migration transaction (#31039) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/lib/data/db/main/database.dart | 21 ++++++-- .../drift/main/concurrent_migration_test.dart | 49 +++++++++++++++++++ 2 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 mobile/test/drift/main/concurrent_migration_test.dart diff --git a/mobile/lib/data/db/main/database.dart b/mobile/lib/data/db/main/database.dart index 6152bbd6a..48667c56e 100644 --- a/mobile/lib/data/db/main/database.dart +++ b/mobile/lib/data/db/main/database.dart @@ -168,9 +168,15 @@ class Drift extends $Drift { await customStatement('PRAGMA foreign_keys = OFF'); try { - await transaction( - () => m.runMigrationSteps( - from: from, + await transaction(() async { + // Re-read the current version inside the transaction to avoid race between different connections. + final current = (await customSelect('PRAGMA user_version').getSingle()).read('user_version'); + if (current >= to) { + return; + } + + await m.runMigrationSteps( + from: current, to: to, steps: migrationSteps( from1To2: (m, v2) async { @@ -368,8 +374,13 @@ class Drift extends $Drift { await _healV33DateTimes(this); }, ), - ), - ); + ); + + // This prevents a drift between the current connection and other connections in the pool, + // which is waiting for the transaction to finish. Drift updates this outside of the transaction, + // so doing it inside the transaction ensures that all connections are in sync. + await customStatement('PRAGMA user_version = $to'); + }); if (kDebugMode) { // Fail if the migration broke foreign keys diff --git a/mobile/test/drift/main/concurrent_migration_test.dart b/mobile/test/drift/main/concurrent_migration_test.dart new file mode 100644 index 000000000..357a62d18 --- /dev/null +++ b/mobile/test/drift/main/concurrent_migration_test.dart @@ -0,0 +1,49 @@ +import 'dart:io'; + +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/data/db/main/database.dart'; + +import 'generated/schema_v22.dart' as v22; + +void main() { + driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; + + late Directory dir; + late File file; + + setUp(() async { + dir = await Directory.systemTemp.createTemp('immich_migration_test'); + file = File('${dir.path}/db.sqlite'); + + final old = v22.DatabaseAtV22(NativeDatabase(file)); + await old.customStatement('PRAGMA journal_mode = WAL'); + await old.localAssetEntity.insertOne( + v22.LocalAssetEntityCompanion.insert(id: 'asset', name: 'asset.mp4', type: 0, durationInSeconds: const .new(5)), + ); + await old.close(); + }); + + tearDown(() async => await dir.delete(recursive: true)); + + Drift open() => + Drift(NativeDatabase.createInBackground(file, setup: (db) => db.execute('PRAGMA busy_timeout = 30000;'))); + + test('two isolates migrating the same database concurrently both succeed', () async { + final first = open(); + final second = open(); + addTearDown(() async { + await first.close(); + await second.close(); + }); + + // Migrations are executed lazily; The below forces both instances to run the migration concurrently + await Future.wait([first.customSelect('SELECT 1').get(), second.customSelect('SELECT 1').get()]); + + for (final db in [first, second]) { + final version = await db.customSelect('PRAGMA user_version').getSingle(); + expect(version.read('user_version'), db.schemaVersion); + } + }); +}