Skip to content

Commit ceca2d8

Browse files
authored
feat(sqlite): run down migrations #594 (#605)
1 parent d55a38f commit ceca2d8

File tree

5 files changed

+134
-15
lines changed

5 files changed

+134
-15
lines changed

packages/brick_sqlite/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 4.1.0
2+
3+
- Add ability to run migrations `down` by calling `#migrate(down: true)` #594
4+
15
## 4.0.2
26

37
- Fix query statements with mixed non-association and association fields to permit any order (#573)

packages/brick_sqlite/lib/src/sqlite_provider.dart

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,9 @@ class SqliteProvider<TProviderModel extends SqliteModel> implements Provider<TPr
171171

172172
/// Update database structure with latest migrations. Note that this will run
173173
/// the [migrations] in the order provided.
174-
Future<void> migrate(List<Migration> migrations) async {
174+
///
175+
/// If [down] is `true`, the migrations will be run in reverse order.
176+
Future<void> migrate(List<Migration> migrations, {bool down = false}) async {
175177
final db = await getDb();
176178

177179
// Ensure foreign keys are enabled
@@ -181,13 +183,15 @@ class SqliteProvider<TProviderModel extends SqliteModel> implements Provider<TPr
181183
final latestSqliteMigrationVersion = await lastMigrationVersion();
182184

183185
// Guard if migration has already been committed.
184-
if (latestSqliteMigrationVersion == latestMigrationVersion) {
186+
if (latestSqliteMigrationVersion == latestMigrationVersion && !down) {
185187
_logger.info('Already at latest migration version ($latestMigrationVersion)');
186188
return;
187189
}
188190

189-
for (final migration in migrations) {
190-
for (final command in migration.up) {
191+
final migrationsToRun = down ? migrations.reversed.toList() : migrations;
192+
for (final migration in migrationsToRun) {
193+
final commands = down ? migration.down : migration.up;
194+
for (final command in commands) {
191195
_logger.finer(
192196
'Running migration (${migration.version}): ${command.statement ?? command.forGenerator}',
193197
);
@@ -202,10 +206,17 @@ class SqliteProvider<TProviderModel extends SqliteModel> implements Provider<TPr
202206
});
203207
}
204208

205-
await db.rawInsert(
206-
'INSERT INTO $_migrationVersionsTableName(version) VALUES(?)',
207-
[migration.version],
208-
);
209+
if (down) {
210+
await db.rawDelete(
211+
'DELETE FROM $_migrationVersionsTableName WHERE version = ?',
212+
[migration.version],
213+
);
214+
} else {
215+
await db.rawInsert(
216+
'INSERT INTO $_migrationVersionsTableName(version) VALUES(?)',
217+
[migration.version],
218+
);
219+
}
209220
}
210221
}
211222

packages/brick_sqlite/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ homepage: https://github.com/GetDutchie/brick/tree/main/packages/brick_sqlite
44
issue_tracker: https://github.com/GetDutchie/brick/issues
55
repository: https://github.com/GetDutchie/brick
66

7-
version: 4.0.2
7+
version: 4.1.0
88

99
environment:
1010
sdk: ">=3.4.0 <4.0.0"

packages/brick_sqlite/test/__mocks__.dart

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import 'package:brick_sqlite/src/db/column.dart';
22
import 'package:brick_sqlite/src/db/migration.dart';
33
import 'package:brick_sqlite/src/db/migration_commands/create_index.dart';
4+
import 'package:brick_sqlite/src/db/migration_commands/drop_table.dart';
45
import 'package:brick_sqlite/src/db/migration_commands/insert_column.dart';
56
import 'package:brick_sqlite/src/db/migration_commands/insert_foreign_key.dart';
67
import 'package:brick_sqlite/src/db/migration_commands/insert_table.dart';
8+
import 'package:brick_sqlite/src/db/migration_commands/migration_command.dart';
79
import 'package:brick_sqlite/src/models/sqlite_model.dart';
810
import 'package:brick_sqlite/src/sqlite_adapter.dart';
911
import 'package:brick_sqlite/src/sqlite_model_dictionary.dart';
@@ -48,12 +50,21 @@ const _demoModelMigrationCommands = [
4850
),
4951
];
5052

53+
const _demoModelDownMigrationCommands = [
54+
DropTable('DemoModelAssoc'),
55+
DropTable('_brick_DemoModel_many_assoc'),
56+
DropTable('DemoModel'),
57+
];
58+
5159
class DemoModelMigration extends Migration {
52-
const DemoModelMigration()
53-
: super(
54-
version: 2,
55-
up: _demoModelMigrationCommands,
56-
down: _demoModelMigrationCommands,
60+
const DemoModelMigration([
61+
int version = 1,
62+
List<MigrationCommand> up = _demoModelMigrationCommands,
63+
List<MigrationCommand> down = _demoModelDownMigrationCommands,
64+
]) : super(
65+
version: version,
66+
up: up,
67+
down: down,
5768
);
5869
}
5970

packages/brick_sqlite/test/sqlite_provider_test.dart

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,100 @@ void main() {
8989
expect(existsAfterDelete, isFalse);
9090
});
9191

92-
test('#migrate', () {}, skip: 'Write test');
92+
group('#migrate', () {
93+
late SqliteProvider cleanProvider;
94+
95+
setUp(() {
96+
cleanProvider = SqliteProvider(
97+
inMemoryDatabasePath,
98+
databaseFactory: databaseFactoryFfi,
99+
modelDictionary: dictionary,
100+
);
101+
});
102+
103+
tearDown(() async {
104+
await cleanProvider.resetDb();
105+
});
106+
107+
test('runs migrations for the first time', () async {
108+
await cleanProvider.migrate([const DemoModelMigration()]);
109+
110+
final version = await cleanProvider.lastMigrationVersion();
111+
expect(version, 1);
112+
113+
// Verify tables were created
114+
final tables = await cleanProvider.rawQuery(
115+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
116+
);
117+
final tableNames = tables.map((t) => t['name']).toList();
118+
expect(tableNames, contains('DemoModelAssoc'));
119+
expect(tableNames, contains('DemoModel'));
120+
});
121+
122+
test('skips migrations when already at latest version', () async {
123+
// Run migration first time
124+
await cleanProvider.migrate([const DemoModelMigration()]);
125+
expect(await cleanProvider.lastMigrationVersion(), 1);
126+
127+
// Run again - should skip
128+
await cleanProvider.migrate([const DemoModelMigration()]);
129+
expect(await cleanProvider.lastMigrationVersion(), 1);
130+
});
131+
132+
test('enables foreign keys pragma', () async {
133+
const migration = DemoModelMigration();
134+
135+
await cleanProvider.migrate([migration]);
136+
137+
final result = await cleanProvider.rawQuery('PRAGMA foreign_keys');
138+
expect(result.first['foreign_keys'], 1);
139+
});
140+
141+
test('tracks migration versions correctly', () async {
142+
const migration1 = DemoModelMigration(2, [], []);
143+
const migration2 = DemoModelMigration(3, [], []);
144+
145+
// After first migration
146+
await cleanProvider.migrate([migration1]);
147+
expect(await cleanProvider.lastMigrationVersion(), 2);
148+
149+
// After second migration
150+
await cleanProvider.migrate([migration2]);
151+
expect(await cleanProvider.lastMigrationVersion(), 3);
152+
153+
// Verify version records exist
154+
// ignore: invalid_use_of_protected_member
155+
final db = await cleanProvider.getDb();
156+
final versions = await db.query('MigrationVersions', orderBy: 'version');
157+
expect(versions, hasLength(3));
158+
expect(versions[0]['version'], 1);
159+
expect(versions[1]['version'], 2);
160+
expect(versions[2]['version'], 3);
161+
});
162+
163+
test('runs down migrations correctly', () async {
164+
const migration1 = DemoModelMigration(2, [], []);
165+
const migration2 = DemoModelMigration(3, [], []);
166+
167+
// After first migration
168+
await cleanProvider.migrate([migration1]);
169+
expect(await cleanProvider.lastMigrationVersion(), 2);
170+
171+
// After second migration
172+
await cleanProvider.migrate([migration2]);
173+
expect(await cleanProvider.lastMigrationVersion(), 3);
174+
175+
await cleanProvider.migrate([migration1, migration2], down: true);
176+
expect(await cleanProvider.lastMigrationVersion(), 1);
177+
178+
// Verify version records exist
179+
// ignore: invalid_use_of_protected_member
180+
final db = await cleanProvider.getDb();
181+
final versions = await db.query('MigrationVersions', orderBy: 'version');
182+
expect(versions, hasLength(1));
183+
expect(versions[0]['version'], 1);
184+
});
185+
});
93186

94187
group('#exists', () {
95188
test('specific', () async {

0 commit comments

Comments
 (0)