diff --git a/lib/core/db/db.dart b/lib/core/db/db.dart index 527e2275..46a570ea 100644 --- a/lib/core/db/db.dart +++ b/lib/core/db/db.dart @@ -46,18 +46,44 @@ class Db extends _$Db with InfraLogger { await m.createTable(schema.geoAssetEntries); }, from3To4: (m, schema) async { - await m.addColumn(schema.profileEntries, schema.profileEntries.testUrl); + final testUrlExists = await _columnExists( + schema.profileEntries.actualTableName, + schema.profileEntries.testUrl.name, + ); + if (!testUrlExists) { + await m.addColumn(schema.profileEntries, schema.profileEntries.testUrl); + } }, from4To5: (m, schema) async { await m.deleteTable('geo_asset_entries'); await m.renameColumn(schema.profileEntries, 'test_url', schema.profileEntries.profileOverride); - await m.addColumn(schema.profileEntries, schema.profileEntries.userOverride); - await m.addColumn(schema.profileEntries, schema.profileEntries.populatedHeaders); + + final userOverrideExists = await _columnExists( + schema.profileEntries.actualTableName, + schema.profileEntries.userOverride.name, + ); + if (!userOverrideExists) { + await m.addColumn(schema.profileEntries, schema.profileEntries.userOverride); + } + + final populatedHeadersExists = await _columnExists( + schema.profileEntries.actualTableName, + schema.profileEntries.populatedHeaders.name, + ); + if (!populatedHeadersExists) { + await m.addColumn(schema.profileEntries, schema.profileEntries.populatedHeaders); + } + await m.createTable(schema.appProxyEntries); }, ), ); } + + Future _columnExists(String table, String column) async { + final result = await customSelect('PRAGMA table_info($table);').get(); + return result.any((row) => row.data['name'] == column); + } } @DataClassName('ProfileEntry') diff --git a/test/drift/db/migration_test.dart b/test/drift/db/migration_test.dart index fed3b51c..2b5440ae 100644 --- a/test/drift/db/migration_test.dart +++ b/test/drift/db/migration_test.dart @@ -8,6 +8,8 @@ import 'generated/schema.dart'; import 'generated/schema_v1.dart' as v1; import 'generated/schema_v2.dart' as v2; +import 'generated/schema_v3.dart' as v3; +import 'generated/schema_v4.dart' as v4; void main() { driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; @@ -68,4 +70,62 @@ void main() { }, ); }); + + group('_columnExists-backed migrations', () { + test('migration from v3 to v4 adds test_url when missing', () async { + final schema = await verifier.schemaAt(3); + addTearDown(() => schema.rawDatabase.dispose()); + + final oldDb = v3.DatabaseAtV3(schema.newConnection()); + final oldColumns = await oldDb + .customSelect('PRAGMA table_info(profile_entries);') + .get(); + + expect( + oldColumns.where((row) => row.data['name'] == 'test_url'), + isEmpty, + ); + await oldDb.close(); + + final migratedDb = Db(schema.newConnection()); + await verifier.migrateAndValidate(migratedDb, 4); + await migratedDb.close(); + + final newDb = v4.DatabaseAtV4(schema.newConnection()); + final newColumns = await newDb + .customSelect('PRAGMA table_info(profile_entries);') + .get(); + expect( + newColumns.where((row) => row.data['name'] == 'test_url'), + hasLength(1), + ); + await newDb.close(); + }); + + test( + 'migration from v3 to v4 skips adding test_url when it already exists', + () async { + final schema = await verifier.schemaAt(3); + addTearDown(() => schema.rawDatabase.dispose()); + + schema.rawDatabase.execute( + 'ALTER TABLE profile_entries ADD COLUMN test_url TEXT NULL;', + ); + + final migratedDb = Db(schema.newConnection()); + await verifier.migrateAndValidate(migratedDb, 4); + await migratedDb.close(); + + final newDb = v4.DatabaseAtV4(schema.newConnection()); + final newColumns = await newDb + .customSelect('PRAGMA table_info(profile_entries);') + .get(); + expect( + newColumns.where((row) => row.data['name'] == 'test_url'), + hasLength(1), + ); + await newDb.close(); + }, + ); + }); }