Skip to content

Commit 33af57f

Browse files
committed
Suggest testing data for columns without default
Closes #3490
1 parent e09cfe7 commit 33af57f

File tree

3 files changed

+115
-10
lines changed

3 files changed

+115
-10
lines changed

drift_dev/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
- Fix warning on Dart-defined views referencing the same table multiple times.
44
- Don't generate unnecessary verification code.
5+
- Suggest a test with data integrity when adding a new column without a default
6+
value in `make-migrations`.
57

68
## 2.25.2
79

drift_dev/lib/src/cli/commands/make_migrations.dart

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ targets:
145145
// Write the generated test
146146
await writer.writeTest();
147147
await writer.flush();
148+
writer.suggestDataMigrationTest();
148149
}
149150
}
150151
}
@@ -200,11 +201,10 @@ class _MigrationTestEmitter {
200201
}
201202

202203
/// All the schema files for this database
203-
Map<int, ExportedSchema> schemas;
204+
Map<int, ExportedSchema> schemas = const {};
204205

205206
/// Migration writer for each migration
206-
List<_MigrationTestWriter> get migrations =>
207-
_MigrationTestWriter.fromSchema(schemas);
207+
List<_MigrationTestWriter> migrations = const [];
208208

209209
_MigrationTestEmitter(
210210
{required this.cli,
@@ -219,7 +219,6 @@ class _MigrationTestEmitter {
219219
required this.dbClassName,
220220
required this.db,
221221
required this.driftElements,
222-
required this.schemas,
223222
required this.dumpGeneratedSchemaCode,
224223
required this.needsToUseFlutterTest});
225224

@@ -260,7 +259,6 @@ class _MigrationTestEmitter {
260259
if (db == null) {
261260
cli.exit('Could not read database class from the "$dbName" database.');
262261
}
263-
final schemas = await parseSchema(schemaDir);
264262

265263
final config = await cli.project.packageConfig;
266264
final hasTest = config?.packages.any((e) => e.name == 'test') == true;
@@ -271,7 +269,7 @@ class _MigrationTestEmitter {
271269
'dependency on flutter_test or test.');
272270
}
273271

274-
return _MigrationTestEmitter(
272+
final emitter = _MigrationTestEmitter(
275273
cli: cli,
276274
rootSchemaDir: rootSchemaDir,
277275
rootTestDir: rootTestDir,
@@ -280,14 +278,21 @@ class _MigrationTestEmitter {
280278
schemaDir: schemaDir,
281279
testDir: testDir,
282280
db: db,
283-
schemas: schemas,
284281
driftElements: elements,
285282
dbClassName: db.definingDartClass.toString(),
286283
testDatabasesDir: testDatabasesDir,
287284
schemaVersion: schemaVersion,
288285
dumpGeneratedSchemaCode: dumpGeneratedSchemaCode,
289286
needsToUseFlutterTest: !hasTest,
290287
);
288+
await emitter._readSchemas();
289+
return emitter;
290+
}
291+
292+
Future<void> _readSchemas() async {
293+
schemas = await parseSchema(schemaDir);
294+
migrations = _MigrationTestWriter.fromSchema(schemas)
295+
.sorted((a, b) => a.from.compareTo(b.from));
291296
}
292297

293298
/// Create a .json dump of the current schema
@@ -310,7 +315,7 @@ class _MigrationTestEmitter {
310315
.info('$dbName: Creating schema file for version $schemaVersion');
311316
schemaFile.writeAsStringSync(content);
312317
// Re-parse the schema to include the newly created schema file
313-
schemas = await parseSchema(schemaDir);
318+
await _readSchemas();
314319
} else if (schemaFile.readAsStringSync() != content) {
315320
cli.exit(
316321
"A schema for version $schemaVersion of the $dbName database already exists and differs from the current schema."
@@ -372,8 +377,7 @@ ${blue.wrap("class")} ${green.wrap(dbClassName)} ${blue.wrap("extends")} ${green
372377
return;
373378
}
374379

375-
final firstMigration =
376-
migrations.sorted((a, b) => a.from.compareTo(b.from)).first;
380+
final firstMigration = migrations.first;
377381

378382
final packageName = cli.project.buildConfig.packageName;
379383
final relativeDbPath = p.posix.relative(dbClassFile.path,
@@ -464,6 +468,52 @@ void main() {
464468
return File(dbClassFile.absolute.path
465469
.replaceFirst(RegExp(r'\.dart$'), '.steps.dart'));
466470
}
471+
472+
void suggestDataMigrationTest() {
473+
final lastMigration = migrations.last;
474+
final schemaBefore = schemas[lastMigration.from]!;
475+
final schemaAfter = schemas[lastMigration.to]!;
476+
477+
// Find changes that suggest a test with data (instead of the default tests
478+
// only using empty tables) might be useful.
479+
final reasons = <String>[];
480+
481+
for (final elementAfter in schemaAfter.schema) {
482+
final elementBefore = schemaBefore.schema
483+
.firstWhereOrNull((e) => e.id.name == elementAfter.id.name);
484+
485+
if (elementBefore == null) {
486+
continue;
487+
}
488+
489+
if (elementAfter is DriftTable && elementBefore is DriftTable) {
490+
for (final columnAfter in elementAfter.columns) {
491+
final columnBefore =
492+
elementBefore.columnBySqlName[columnAfter.nameInSql];
493+
494+
if (columnBefore == null &&
495+
elementAfter.isColumnRequiredForInsert(columnAfter)) {
496+
reasons.add(
497+
'Added column "${columnAfter.nameInSql}" in "${elementAfter.schemaName}" without a default.');
498+
}
499+
}
500+
}
501+
}
502+
503+
if (reasons.isNotEmpty) {
504+
cli.logger.info(
505+
'${cyan.wrap('Hint')}: Your latest migration might benefit from a test '
506+
'with data, as it might need special setup to transform existing data. '
507+
'Drit has detected the following reasons for that: ',
508+
);
509+
for (final reason in reasons) {
510+
cli.logger.info(' - $reason');
511+
}
512+
513+
cli.logger.info(
514+
'For more information on writing these tests, see ${yellow.wrap('https://drift.simonbinder.eu/migrations/tests/#verifying-data-integrity')}');
515+
}
516+
}
467517
}
468518

469519
/// A writer that generates example code for a migration test with data

drift_dev/test/cli/make_migrations_test.dart

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,59 @@ targets:
156156
]))
157157
.validate();
158158
});
159+
160+
group('suggests data migration test', () {
161+
test('when adding new column without default', () async {
162+
project = await TestDriftProject.create([
163+
d.dir('lib', [
164+
d.file('db.dart', '''
165+
import 'package:drift/drift.dart';
166+
167+
class Examples extends Table {
168+
IntColumn get id => integer().autoIncrement()();
169+
}
170+
171+
@DriftDatabase(tables: [Examples])
172+
class MyDatabase {
173+
@override
174+
int get schemaVersion => 1;
175+
}
176+
''')
177+
]),
178+
d.file('build.yaml', """
179+
targets:
180+
\$default:
181+
builders:
182+
drift_dev:
183+
options:
184+
databases:
185+
my_database: lib/db.dart""")
186+
]);
187+
await project.runDriftCli(['make-migrations']);
188+
await File(p.join(project.root.path, 'lib/db.dart')).writeAsString('''
189+
import 'package:drift/drift.dart';
190+
191+
class Examples extends Table {
192+
IntColumn get id => integer().autoIncrement()();
193+
TextColumn get content => text()();
194+
}
195+
196+
@DriftDatabase(tables: [Examples])
197+
class MyDatabase {
198+
@override
199+
int get schemaVersion => 2;
200+
}
201+
''');
202+
203+
expect(
204+
() => project.runDriftCli(['make-migrations'], dropPrints: false),
205+
prints(allOf(
206+
contains('Your latest migration might benefit from a test with data'),
207+
contains('Added column "content" in "examples" without a default'),
208+
)),
209+
);
210+
});
211+
});
159212
}
160213

161214
const _dbContent = '''

0 commit comments

Comments
 (0)