Skip to content

Commit 601936d

Browse files
PIG208gnprice
authored andcommitted
db: Drop all tables on downgrade
We previously missed tables that are not known to the schema. This becomes an issue if a new table is added at a newer schema level. When we go back to an earlier schema, that new table remains in the database, so subsequent attempts to upgrade to the later schema level that adds the table will fail, because it already exists. The test for this relies on some undocumented drift internals to modify the schema version it sees when running the migration. References: https://github.com/simolus3/drift/blob/18cede15/drift/lib/src/runtime/executor/helpers/engines.dart#L459-L495 https://github.com/simolus3/drift/blob/18cede15/drift/lib/src/sqlite3/database.dart#L198-L211 https://github.com/simolus3/sqlite3.dart/blob/4de46afd/sqlite3/lib/src/implementation/database.dart#L69-L85 Fixes: zulip#1172 Signed-off-by: Zixuan James Li <[email protected]>
1 parent 5fb8158 commit 601936d

File tree

2 files changed

+67
-7
lines changed

2 files changed

+67
-7
lines changed

lib/model/database.dart

+44-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import 'package:drift/drift.dart';
2+
import 'package:drift/internal/versioned_schema.dart';
23
import 'package:drift/remote.dart';
34
import 'package:sqlite3/common.dart';
45

6+
import '../log.dart';
57
import 'schema_versions.g.dart';
68

79
part 'database.g.dart';
@@ -49,6 +51,19 @@ class UriConverter extends TypeConverter<Uri, String> {
4951
@override Uri fromSql(String fromDb) => Uri.parse(fromDb);
5052
}
5153

54+
// TODO(drift): generate this
55+
VersionedSchema _getSchema({
56+
required DatabaseConnectionUser database,
57+
required int schemaVersion,
58+
}) {
59+
switch (schemaVersion) {
60+
case 2:
61+
return Schema2(database: database);
62+
default:
63+
throw Exception('unknown schema version: $schemaVersion');
64+
}
65+
}
66+
5267
@DriftDatabase(tables: [Accounts])
5368
class AppDatabase extends _$AppDatabase {
5469
AppDatabase(super.e);
@@ -60,11 +75,37 @@ class AppDatabase extends _$AppDatabase {
6075
// and generate database code with build_runner.
6176
// See ../../README.md#generated-files for more
6277
// information on using the build_runner.
78+
// * Update [_getSchema] to handle the new schemaVersion.
6379
// * Write a migration in `onUpgrade` below.
6480
// * Write tests.
6581
@override
6682
int get schemaVersion => 2; // See note.
6783

84+
Future<void> _dropAndCreateAll(Migrator m, {
85+
required int schemaVersion,
86+
}) async {
87+
await m.database.transaction(() async {
88+
final query = m.database.customSelect(
89+
"SELECT name FROM sqlite_master WHERE type='table'");
90+
for (final row in await query.get()) {
91+
final data = row.data;
92+
final tableName = data['name'] as String;
93+
// Skip sqlite-internal tables. See for comparison:
94+
// https://www.sqlite.org/fileformat2.html#intschema
95+
// https://github.com/simolus3/drift/blob/0901c984a/drift_dev/lib/src/services/schema/verifier_common.dart#L9-L22
96+
if (tableName.startsWith('sqlite_')) continue;
97+
// No need to worry about SQL injection; this table name
98+
// was already a table name in the database, not something
99+
// that should be affected by user data.
100+
await m.database.customStatement('DROP TABLE $tableName');
101+
}
102+
final schema = _getSchema(database: m.database, schemaVersion: schemaVersion);
103+
for (final entity in schema.entities) {
104+
await m.create(entity);
105+
}
106+
});
107+
}
108+
68109
@override
69110
MigrationStrategy get migration {
70111
return MigrationStrategy(
@@ -73,15 +114,11 @@ class AppDatabase extends _$AppDatabase {
73114
},
74115
onUpgrade: (Migrator m, int from, int to) async {
75116
if (from > to) {
76-
// TODO(log): log schema downgrade as an error
77117
// This should only ever happen in dev. As a dev convenience,
78118
// drop everything from the database and start over.
79-
for (final entity in allSchemaEntities) {
80-
// This will miss any entire tables (or indexes, etc.) that
81-
// don't exist at this version. For a dev-only feature, that's OK.
82-
await m.drop(entity);
83-
}
84-
await m.createAll();
119+
// TODO(log): log schema downgrade as an error
120+
assert(debugLog('Downgrading schema from v$from to v$to.'));
121+
await _dropAndCreateAll(m, schemaVersion: to);
85122
return;
86123
}
87124
assert(1 <= from && from <= to && to <= schemaVersion);

test/model/database_test.dart

+23
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,29 @@ void main() {
9898
verifier = SchemaVerifier(GeneratedHelper());
9999
});
100100

101+
test('downgrading', () async {
102+
final schema = await verifier.schemaAt(2);
103+
104+
// This simulates the scenario during development when running the app
105+
// with a future schema version that has additional tables and columns.
106+
final before = AppDatabase(schema.newConnection());
107+
await before.customStatement('CREATE TABLE test_extra (num int)');
108+
await before.customStatement('ALTER TABLE accounts ADD extra_column int');
109+
await check(verifier.migrateAndValidate(
110+
before, 2, validateDropped: true)).throws<SchemaMismatch>();
111+
// Override the schema version by modifying the underlying value
112+
// drift internally keeps track of in the database.
113+
// TODO(drift): Expose a better interface for testing this.
114+
await before.customStatement('PRAGMA user_version = 999;');
115+
await before.close();
116+
117+
// Simulate starting up the app, with an older schema version that
118+
// does not have the extra tables and columns.
119+
final after = AppDatabase(schema.newConnection());
120+
await verifier.migrateAndValidate(after, 2, validateDropped: true);
121+
await after.close();
122+
});
123+
101124
group('migrate without data', () {
102125
const versions = GeneratedHelper.versions;
103126
final latestVersion = versions.last;

0 commit comments

Comments
 (0)