From 77720a12151394b11ec75c47048f21b73d858d37 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Sat, 5 Oct 2024 20:36:56 -0400 Subject: [PATCH 01/25] feat: all-in-one-migrator --- docs/build.yaml | 2 + docs/docs/Tools/index.md | 4 + drift_dev/lib/api/migrations.dart | 30 ++ drift_dev/lib/src/analysis/options.dart | 16 + drift_dev/lib/src/cli/cli.dart | 4 +- .../lib/src/cli/commands/make_migrations.dart | 423 ++++++++++++++++++ drift_dev/lib/src/cli/commands/schema.dart | 8 +- .../lib/src/cli/commands/schema/dump.dart | 2 +- .../lib/src/cli/commands/schema/export.dart | 2 +- .../cli/commands/schema/generate_utils.dart | 47 +- .../lib/src/cli/commands/schema/steps.dart | 14 +- 11 files changed, 522 insertions(+), 30 deletions(-) create mode 100644 drift_dev/lib/src/cli/commands/make_migrations.dart diff --git a/docs/build.yaml b/docs/build.yaml index 57d1631e6..d2ab01320 100644 --- a/docs/build.yaml +++ b/docs/build.yaml @@ -41,6 +41,8 @@ targets: dialect: sqlite options: version: "3.39" + databases: + main_db: "lib/snippets/dart_api/manager.dart" generate_for: include: &modular - "lib/snippets/_shared/**" diff --git a/docs/docs/Tools/index.md b/docs/docs/Tools/index.md index 6694c28df..f01125023 100644 --- a/docs/docs/Tools/index.md +++ b/docs/docs/Tools/index.md @@ -48,6 +48,10 @@ INFO: lib/src/data/database.dart has drift databases or daos: AppDatabase INFO: test/fake_db.dart has drift databases or daos: TodoDb, SomeDao ``` +## Make Migration + +TODO + ## Schema tools ### Dump for version control diff --git a/drift_dev/lib/api/migrations.dart b/drift_dev/lib/api/migrations.dart index 73047c8b6..09cc5dfdd 100644 --- a/drift_dev/lib/api/migrations.dart +++ b/drift_dev/lib/api/migrations.dart @@ -214,3 +214,33 @@ class InitializedSchema { /// ``` DatabaseConnection newConnection() => _createConnection(); } + +/// Utility function used by generated tests to: +/// 1. Create a database at a specific version +/// 2. Insert data into the database +/// 3. Migrate the database to a target version +/// 4. Validate that the data is valid after the migration +Future testStepByStepigrations( + {required SchemaVerifier verifier, + required OldDatabase Function(QueryExecutor) oldDbCallback, + required NewDatabase Function(QueryExecutor) newDbCallback, + required GeneratedDatabase Function(QueryExecutor) currentDbCallback, + required void Function(Batch, OldDatabase) createItems, + required Future Function(NewDatabase) validateItems, + required int from, + required int to}) async { + final schema = await verifier.schemaAt(from); + + final oldDb = oldDbCallback(schema.newConnection()); + await oldDb.batch((batch) => createItems(batch, oldDb)); + await oldDb.close(); + + final db = currentDbCallback(schema.newConnection()); + await verifier.migrateAndValidate(db, to); + await db.close(); + + final newDb = newDbCallback(schema.newConnection()); + await validateItems(newDb); + await newDb.close(); +} diff --git a/drift_dev/lib/src/analysis/options.dart b/drift_dev/lib/src/analysis/options.dart index 9c537aee9..b088941aa 100644 --- a/drift_dev/lib/src/analysis/options.dart +++ b/drift_dev/lib/src/analysis/options.dart @@ -6,6 +6,7 @@ import 'package:recase/recase.dart'; import 'package:sqlparser/sqlparser.dart' show BasicType, ResolvedType, SchemaFromCreateTable, SqliteVersion; import 'package:string_scanner/string_scanner.dart'; +import 'package:yaml/yaml.dart'; part '../generated/analysis/options.g.dart'; @@ -123,6 +124,15 @@ class DriftOptions { @JsonKey(name: 'fatal_warnings', defaultValue: false) final bool fatalWarnings; + @JsonKey(name: 'schema_dir', defaultValue: "drift_schemas") + final String schemaDir; + + @JsonKey(name: 'test_dir', defaultValue: "test/drift") + final String testDir; + + @JsonKey(name: 'databases', defaultValue: {}) + final Map databases; + @internal const DriftOptions.defaults({ this.generateFromJsonStringConstructor = false, @@ -151,6 +161,9 @@ class DriftOptions { this.fatalWarnings = false, this.hasDriftAnalyzer = false, this.assumeCorrectReference = false, + this.schemaDir = "drift_schemas", + this.testDir = "test/drift", + this.databases = const {}, }); DriftOptions({ @@ -180,6 +193,9 @@ class DriftOptions { required this.hasDriftAnalyzer, required this.assumeCorrectReference, this.dialect, + required this.schemaDir, + required this.testDir, + required this.databases, }) { // ignore: deprecated_member_use_from_same_package if (sqliteAnalysisOptions != null && modules.isNotEmpty) { diff --git a/drift_dev/lib/src/cli/cli.dart b/drift_dev/lib/src/cli/cli.dart index 5a3b2b4c6..44cf93b65 100644 --- a/drift_dev/lib/src/cli/cli.dart +++ b/drift_dev/lib/src/cli/cli.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:args/command_runner.dart'; +import 'package:drift_dev/src/cli/commands/make_migration.dart'; import 'package:drift_dev/src/cli/project.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; @@ -38,7 +39,8 @@ class DriftDevCli { ..addCommand(AnalyzeCommand(this)) ..addCommand(IdentifyDatabases(this)) ..addCommand(SchemaCommand(this)) - ..addCommand(MigrateCommand(this)); + ..addCommand(MigrateCommand(this)) + ..addCommand(MakeMigrationCommand(this)); _runner.argParser .addFlag('verbose', abbr: 'v', defaultsTo: false, negatable: false); diff --git a/drift_dev/lib/src/cli/commands/make_migrations.dart b/drift_dev/lib/src/cli/commands/make_migrations.dart new file mode 100644 index 000000000..0f808a131 --- /dev/null +++ b/drift_dev/lib/src/cli/commands/make_migrations.dart @@ -0,0 +1,423 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:drift_dev/src/analysis/results/results.dart'; +import 'package:drift_dev/src/cli/cli.dart'; +import 'package:drift_dev/src/cli/commands/schema.dart'; +import 'package:drift_dev/src/cli/commands/schema/generate_utils.dart'; +import 'package:drift_dev/src/cli/commands/schema/steps.dart'; + +import 'package:drift_dev/src/services/schema/schema_files.dart'; +import 'package:io/ansi.dart'; +import 'package:path/path.dart' as p; + +class MakeMigrationCommand extends DriftCommand { + MakeMigrationCommand(super.cli); + + @override + String get description => + "Generates the scaffold for your migrations, an autogenerated test and a data integrity test."; + + @override + String get name => "make-migrations"; + + @override + Future run() async { + if (p.isAbsolute(cli.project.options.schemaDir)) { + cli.exit( + '`schema_dir` must be a relative path. Remove the leading slash'); + } + if (p.isAbsolute(cli.project.options.testDir)) { + cli.exit('`test_dir` must be a relative path. Remove the leading slash'); + } + + /// The root directory where test files for all databases are stored + /// e.g /test/drift/ + final rootSchemaDir = Directory( + p.join(cli.project.directory.path, cli.project.options.schemaDir)) + ..createSync(recursive: true); + + /// The root directory where schema files for all databases are stored + /// e.g /drift_schema/ + final rootTestDir = Directory( + p.join(cli.project.directory.path, cli.project.options.testDir)) + ..createSync(recursive: true); + + if (cli.project.options.databases.isEmpty) { + cli.logger.info( + 'No databases found in the build.yaml file. Check here to see how to add a database TODO: ADD LINK'); + exit(0); + } + + final databaseMigrationsWriters = + await Future.wait(cli.project.options.databases.entries.map( + (entry) async { + final writer = await _DatabaseMigrationWriter.create( + cli: cli, + rootSchemaDir: rootSchemaDir, + rootTestDir: rootTestDir, + dbName: entry.key, + relativeDbClassPath: entry.value); + return writer; + }, + )); + + for (var writer in databaseMigrationsWriters) { + cli.logger + .info('Generating migration scaffold files for ${writer.dbName}'); + // Dump the schema files for all databases + await writer.writeSchemaFile(); + writer.flush(); + // Write the step by step migration files for all databases + // This is done after all the schema files have been written to the disk + // to ensure that the schema files are up to date + await writer.writeStepsFile(); + // Write the generated test databases + await writer.writeTestDatabases(); + // Write the generated test + await writer.writeTests(); + writer.flush(); + } + } +} + +/// Temporary class to store content to be written to the disk +class _WriteFileTask { + final File file; + final String content; + final bool overwrite; + + _WriteFileTask( + {required this.file, required this.content, this.overwrite = true}); + + void write() => file.writeAsStringSync(content); +} + +class _DatabaseMigrationWriter { + final DriftDevCli cli; + final Directory rootSchemaDir; + final Directory rootTestDir; + final String dbName; + late final File dbClassFile; + + /// The directory where the schema files for this database are stored + /// e.g /drift_schema/my_database/ + final Directory schemaDir; + + /// The directory where the tests for this database are stored + /// e.g /test/drift/my_database/ + final Directory testDir; + + /// The directory where the generated test utils are stored + /// e.g /test/drift/my_database/generated/ + final Directory testDatabasesDir; + + /// The directory where the generated test utils are stored + /// e.g /test/drift/my_database/validation/ + final Directory validationModelsDir; + + /// Current schema version of the database + final int schemaVersion; + + /// The name of the database class + final String dbClassName; + + /// The parsed database class + final DriftDatabase db; + + /// The parsed drift elements + final List driftElements; + + /// Stores the tempoarary files to be written to the disk + /// Only is written to the disk once the entire generation process completes without errors + final writeTasks = <_WriteFileTask>[]; + + /// Write all the files to the disk + void flush() { + for (final task in writeTasks) { + task.write(); + } + writeTasks.clear(); + } + + /// All the schema files for this database + Map schemas; + + /// Migration writer for each migration + List<_MigrationWriter> get migrations => _MigrationWriter.fromSchema(schemas); + + _DatabaseMigrationWriter({ + required this.cli, + required this.rootSchemaDir, + required this.rootTestDir, + required this.dbName, + required this.dbClassFile, + required this.schemaDir, + required this.testDir, + required this.testDatabasesDir, + required this.validationModelsDir, + required this.schemaVersion, + required this.dbClassName, + required this.db, + required this.driftElements, + required this.schemas, + }); + + static Future<_DatabaseMigrationWriter> create( + {required DriftDevCli cli, + required Directory rootSchemaDir, + required Directory rootTestDir, + required String dbName, + required String relativeDbClassPath}) async { + if (p.isAbsolute(relativeDbClassPath)) { + cli.exit( + 'The path for the "$dbName" database must be a relative path. Remove the leading slash'); + } + final dbClassFile = + File(p.join(cli.project.directory.path, relativeDbClassPath)); + final schemaDir = Directory(p.join(rootSchemaDir.path, dbName)) + ..createSync(recursive: true); + final testDir = Directory(p.join(rootTestDir.path, dbName)) + ..createSync(recursive: true); + final testDatabasesDir = Directory(p.join(testDir.path, 'schemas')) + ..createSync(recursive: true); + final validationModelsDir = Directory(p.join(testDir.path, 'validation')) + ..createSync(recursive: true); + final (:db, :elements, :schemaVersion) = + await cli.readElementsFromSource(dbClassFile.absolute); + if (schemaVersion == null) { + cli.exit('Could not read schema version from the "$dbName" database.'); + } + if (db == null) { + cli.exit('Could not read database class from the "$dbName" database.'); + } + final schemas = await parseSchema(schemaDir); + return _DatabaseMigrationWriter( + cli: cli, + rootSchemaDir: rootSchemaDir, + rootTestDir: rootTestDir, + dbName: dbName, + dbClassFile: dbClassFile, + schemaDir: schemaDir, + testDir: testDir, + db: db, + schemas: schemas, + driftElements: elements, + dbClassName: db.definingDartClass.toString(), + testDatabasesDir: testDatabasesDir, + validationModelsDir: validationModelsDir, + schemaVersion: schemaVersion); + } + + /// Create a .json dump of the current schema + Future writeSchemaFile() async { + // If the latest schema file version is larger than the current schema version + // then something is wrong + if (schemas.keys.any((v) => v > schemaVersion)) { + cli.exit( + 'The version of your $dbName database ($schemaVersion) is lower than the latest schema version. ' + 'The schema version in the database should never be decreased. '); + } + + final writer = SchemaWriter(driftElements, options: cli.project.options); + final schemaFile = driftSchemaFile(schemaVersion); + final content = json.encode(writer.createSchemaJson()); + if (!schemaFile.existsSync()) { + cli.logger + .info('$dbName: Creating schema file for version $schemaVersion'); + writeTasks.add(_WriteFileTask(file: schemaFile, content: content)); + // Re-parse the schema to include the newly created schema file + schemas = await parseSchema(schemaDir); + } else if (schemaFile.readAsStringSync() != content) { + cli.exit( + "A schema for version $schemaVersion of the $dbName database already exists and differs from the current schema." + " Either delete the existing schema file or update the schema version in the database file."); + } + } + + /// Create a step by step migration file + Future writeStepsFile() async { + cli.logger.info( + '$dbName: Generating step by step migration in ${blue.wrap(p.relative(stepsFile.path))}'); + writeTasks.add(_WriteFileTask( + file: stepsFile, + content: StepsGenerationUtil.generateStepByStepMigration(schemas))); + } + + /// Generate a built database for each schema version + /// This will be used to test the migrations + Future writeTestDatabases() async { + for (final versionAndEntities in schemas.entries) { + final version = versionAndEntities.key; + final entities = versionAndEntities.value; + writeTasks.add(_WriteFileTask( + file: testUtilityFile(version), + content: GenerateUtils.generateSchemaCode( + cli, version, entities, true, true))); + } + writeTasks.add(_WriteFileTask( + file: File(p.join(testDatabasesDir.path, 'schema.dart')), + content: GenerateUtils.generateLibraryCode(schemas.keys))); + } + + Future writeTests() async { + final packageName = cli.project.buildConfig.packageName; + final relativeDbPath = p.relative(dbClassFile.path, + from: p.join(cli.project.directory.path, 'lib')); + + for (final migration in migrations) { + // Generate the validation models + final validationFile = File(p.join(validationModelsDir.path, + 'v${migration.from}_to_v${migration.to}.dart')); + if (!validationFile.existsSync()) { + cli.logger.info( + '$dbName: Generating validation models in ${blue.wrap(p.relative(validationFile.path))}.' + ' Fill this file with before and after data to test the data integrity of the migration'); + writeTasks.add(_WriteFileTask( + file: validationFile, content: migration.validationModelsCode)); + } + } + + final stepByStepTests = migrations + .map((e) => e.testStepByStepMigrationCode(dbName, dbClassName)); + + final code = """ +// ignore_for_file: unused_local_variable, unused_import +// GENERATED CODE, DO NOT EDIT BY HAND. +import 'package:drift/drift.dart'; +import 'package:drift_dev/api/migrations.dart'; +import 'package:$packageName/$relativeDbPath'; +import 'package:test/test.dart'; +import 'schemas/schema.dart'; + +${stepByStepTests.map((e) => e.imports).expand((imports) => imports).toSet().join('\n')} + +void main() { + driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; + late SchemaVerifier verifier; + + setUpAll(() { + verifier = SchemaVerifier(GeneratedHelper()); + }); + + ${stepByStepTests.map((e) => e.test).join('\n')} + +} +"""; + final testFile = File(p.join(testDir.path, 'migration_test.dart')); + cli.logger.info( + '$dbName: Generating test in ${blue.wrap(p.relative(testFile.path))}.' + ' Run this test to validate that the migrations are correct'); + writeTasks.add(_WriteFileTask(file: testFile, content: code)); + } + + /// The json file where the schema for the current version of the database is stored + File driftSchemaFile(int version) { + return File(p.join(schemaDir.path, 'drift_schema_v$version.json')); + } + + File testUtilityFile(int version) { + return File(p.join(testDatabasesDir.path, 'schema_v$version.dart')); + } + + /// Generated file where the step by step migration code is stored + File get stepsFile { + return File(dbClassFile.absolute.path + .replaceFirst(RegExp(r'\.dart$'), '.steps.dart')); + } +} + +/// A writer that generates the code for a migration from one schema version to another +class _MigrationWriter { + final List tables; + final int from; + final int to; + + _MigrationWriter(this.tables, {required this.from, required this.to}); + + /// Create list of migration writers from a map of schema versions + /// A migration writer is created for each pair of schema versions + /// e.g (1,2), (2,3), (3,4) etc + static List<_MigrationWriter> fromSchema(Map schemas) { + final result = <_MigrationWriter>[]; + if (schemas.length < 2) { + return result; + } + final versions = schemas.keys.toList()..sort(); + for (var i = 0; i < versions.length - 1; i++) { + final (from, fromSchema) = (versions[i], schemas[versions[i]]!); + final (to, toSchema) = (versions[i + 1], schemas[versions[i + 1]]!); + final fromTables = fromSchema.schema.whereType(); + final toTables = toSchema.schema.whereType(); + final commonTables = fromTables.where( + (table) => toTables.any((t) => t.schemaName == table.schemaName)); + result.add(_MigrationWriter(commonTables.toList(), from: from, to: to)); + } + return result; + } + + /// Generate a step by step migration test + /// This test will test the migration from version [from] to version [to] + /// It will also import the validation models to test data integrity + ({Set imports, String test}) testStepByStepMigrationCode( + String dbName, String dbClassName) { + final imports = { + "import 'schemas/schema_v$from.dart' as v$from;", + "import 'schemas/schema_v$to.dart' as v$to;", + "import 'validation/v${from}_to_v$to.dart' as v${from}_to_v$to;" + }; + + final test = """ +test( + "$dbName - migrate from v$from to v$to", + () => testStepByStepigrations( + from: $from, to: $to, verifier: verifier, oldDbCallback: (e) => v$from.DatabaseAtV$from(e), + newDbCallback: (e) => v$to.DatabaseAtV$to(e), currentDbCallback: (e) => $dbClassName(e), + createItems: (b, oldDb) { + ${tables.map( + (table) { + return "b.insertAll(oldDb.${table.dbGetterName}, v${from}_to_v$to.${table.dbGetterName}V$from);"; + }, + ).join('\n')} + }, + validateItems: (newDb) async { + ${tables.map( + (table) { + return "expect(v${from}_to_v$to.${table.dbGetterName}V$from, await newDb.select(newDb.${table.dbGetterName}).get());"; + }, + ).join('\n')} + }, + ) +); +"""; + return (imports: imports, test: test); + } + + /// Generate the code for a file which users + /// will fill in with before and after data + /// to validate that the migration was successful and data was not lost + String get validationModelsCode => """ +import 'package:drift/drift.dart'; +import '../schemas/schema_v$from.dart' as v$from; +import '../schemas/schema_v$to.dart' as v$to; + +/// Fill these lists with data that should be present in the database before and after the migration +/// These lists will be used to validate that the migration was successful and that no data was lost +/// e.g. Validate what happens when a column is removed: +/// +/// final usersV1 = [ +/// User(id: Value(1), name: Value('Simon'), isAdmin: Value(false)), +/// User(id: Value(2), name: Value('John'), isAdmin: Value(false)), +/// ]; +/// final usersV2 = [ +/// User(id: Value(1), name: Value('Simon')), +/// User(id: Value(2), name: Value('John')), +/// ]; +${tables.map((table) => """ + +final ${table.dbGetterName}V$from = >[]; +final ${table.dbGetterName}V$to = >[]; +""").join('\n')} + +"""; +} diff --git a/drift_dev/lib/src/cli/commands/schema.dart b/drift_dev/lib/src/cli/commands/schema.dart index a7b5fe63b..0df341049 100644 --- a/drift_dev/lib/src/cli/commands/schema.dart +++ b/drift_dev/lib/src/cli/commands/schema.dart @@ -28,7 +28,11 @@ class SchemaCommand extends Command { } } -typedef AnalyzedDatabase = ({List elements, int? schemaVersion}); +typedef AnalyzedDatabase = ({ + List elements, + int? schemaVersion, + DriftDatabase? db +}); extension ExportSchema on DriftDevCli { /// Extracts available drift elements from a [dart] source file defining a @@ -52,10 +56,10 @@ extension ExportSchema on DriftDevCli { final result = input.fileAnalysis!; final databaseElement = databases.single; final db = result.resolvedDatabases[databaseElement.id]!; - return ( elements: db.availableElements, schemaVersion: databaseElement.schemaVersion, + db: databaseElement, ); } } diff --git a/drift_dev/lib/src/cli/commands/schema/dump.dart b/drift_dev/lib/src/cli/commands/schema/dump.dart index 9aae84f53..5d6d35066 100644 --- a/drift_dev/lib/src/cli/commands/schema/dump.dart +++ b/drift_dev/lib/src/cli/commands/schema/dump.dart @@ -88,7 +88,7 @@ class DumpSchemaCommand extends Command { final userVersion = opened.select('pragma user_version').single.columnAt(0) as int; - return (elements: elements, schemaVersion: userVersion); + return (elements: elements, schemaVersion: userVersion, db: null); } finally { opened.dispose(); } diff --git a/drift_dev/lib/src/cli/commands/schema/export.dart b/drift_dev/lib/src/cli/commands/schema/export.dart index 46ff2900d..94005dcc8 100644 --- a/drift_dev/lib/src/cli/commands/schema/export.dart +++ b/drift_dev/lib/src/cli/commands/schema/export.dart @@ -51,7 +51,7 @@ class ExportSchemaCommand extends Command { final dialect = SqlDialect.values.byName(argResults!.option('dialect') ?? 'sqlite'); - var (:elements, schemaVersion: _) = + var (:elements, schemaVersion: _, db: __) = await cli.readElementsFromSource(File(rest.single).absolute); // The roundtrip through the schema writer ensures that irrelevant things diff --git a/drift_dev/lib/src/cli/commands/schema/generate_utils.dart b/drift_dev/lib/src/cli/commands/schema/generate_utils.dart index 06ecf82d1..b7a66146d 100644 --- a/drift_dev/lib/src/cli/commands/schema/generate_utils.dart +++ b/drift_dev/lib/src/cli/commands/schema/generate_utils.dart @@ -66,23 +66,34 @@ class GenerateUtilsCommand extends Command { final version = versionAndEntities.key; final entities = versionAndEntities.value; - await _writeSchemaFile( - outputDir, - version, - entities, - argResults?['data-classes'] as bool, - argResults?['companions'] as bool, - ); + final file = File( + p.join(outputDir.path, GenerateUtils._filenameForVersion(version))); + await file.writeAsString(GenerateUtils.generateSchemaCode( + cli, + version, + entities, + argResults?['data-classes'] as bool, + argResults?['companions'] as bool)); } final versions = schema.keys.toList()..sort(); - await _writeLibraryFile(outputDir, versions); + final libraryFile = File(p.join(outputDir.path, 'schema.dart')); + await libraryFile + .writeAsString(GenerateUtils.generateLibraryCode(versions)); print( 'Wrote ${schema.length + 1} files into ${p.relative(outputDir.path)}'); } +} + +class GenerateUtils { + static String _filenameForVersion(int version) => 'schema_v$version.dart'; + static const _prefix = '// GENERATED CODE, DO NOT EDIT BY HAND.\n' + '// ignore_for_file: type=lint'; + static final _dartfmt = DartFormatter(); - Future _writeSchemaFile( - Directory output, + /// Generates Dart code for a specific schema version. + static String generateSchemaCode( + DriftDevCli cli, int version, ExportedSchema schema, bool dataClasses, @@ -105,7 +116,6 @@ class GenerateUtilsCommand extends Command { imports: NullImportManager(), ), ); - final file = File(p.join(output.path, _filenameForVersion(version))); writer.leaf() ..writeln(_prefix) @@ -126,10 +136,12 @@ class GenerateUtilsCommand extends Command { DatabaseWriter(input, writer.child()).write(); - return file.writeAsString(_dartfmt.format(writer.writeGenerated())); + return _dartfmt.format(writer.writeGenerated()); } - Future _writeLibraryFile(Directory output, Iterable versions) { + /// Generates the Dart code for a library file that instantiates the schema + /// for each version. + static String generateLibraryCode(Iterable versions) { final buffer = StringBuffer() ..writeln(_prefix) ..writeln('//@dart=2.12') @@ -159,13 +171,6 @@ class GenerateUtilsCommand extends Command { ..writeln('throw MissingSchemaException(version, const $missingAsSet);') ..writeln('}}}'); - final file = File(p.join(output.path, 'schema.dart')); - return file.writeAsString(_dartfmt.format(buffer.toString())); + return _dartfmt.format(buffer.toString()); } - - String _filenameForVersion(int version) => 'schema_v$version.dart'; - - static final _dartfmt = DartFormatter(); - static const _prefix = '// GENERATED CODE, DO NOT EDIT BY HAND.\n' - '// ignore_for_file: type=lint'; } diff --git a/drift_dev/lib/src/cli/commands/schema/steps.dart b/drift_dev/lib/src/cli/commands/schema/steps.dart index 5bfec0baa..827888dfd 100644 --- a/drift_dev/lib/src/cli/commands/schema/steps.dart +++ b/drift_dev/lib/src/cli/commands/schema/steps.dart @@ -47,7 +47,15 @@ class WriteVersions extends Command { if (!await outputDirectory.exists()) { await outputDirectory.create(); } + final schemas = await parseSchema(inputDirectory); + await outputFile.writeAsString( + StepsGenerationUtil.generateStepByStepMigration(schemas)); + } +} +class StepsGenerationUtil { + /// Generate dart code for incremental migrations between schema versions. + static String generateStepByStepMigration(Map schemas) { final imports = LibraryImportManager(); final writer = Writer( const DriftOptions.defaults(), @@ -55,9 +63,8 @@ class WriteVersions extends Command { ); imports.linkToWriter(writer); - final schema = await parseSchema(inputDirectory); final byVersion = [ - for (final MapEntry(key: version, value: schema) in schema.entries) + for (final MapEntry(key: version, value: schema) in schemas.entries) SchemaVersion( version, schema.schema.whereType().toList(), @@ -75,7 +82,6 @@ class WriteVersions extends Command { } on FormatterException { // Ignore. Probably a bug in drift_dev, the user will notice. } - - await outputFile.writeAsString(code); + return code; } } From 1aad30580e9c9720451ee6b5c327de79c532a543 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Sat, 5 Oct 2024 20:38:43 -0400 Subject: [PATCH 02/25] fix: rebuild --- drift_dev/lib/src/cli/cli.dart | 2 +- .../lib/src/generated/analysis/options.g.dart | 23 +++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/drift_dev/lib/src/cli/cli.dart b/drift_dev/lib/src/cli/cli.dart index 44cf93b65..4e9a19eaa 100644 --- a/drift_dev/lib/src/cli/cli.dart +++ b/drift_dev/lib/src/cli/cli.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:args/command_runner.dart'; -import 'package:drift_dev/src/cli/commands/make_migration.dart'; +import 'package:drift_dev/src/cli/commands/make_migrations.dart'; import 'package:drift_dev/src/cli/project.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; diff --git a/drift_dev/lib/src/generated/analysis/options.g.dart b/drift_dev/lib/src/generated/analysis/options.g.dart index 25ee3f547..688bd280a 100644 --- a/drift_dev/lib/src/generated/analysis/options.g.dart +++ b/drift_dev/lib/src/generated/analysis/options.g.dart @@ -38,7 +38,10 @@ DriftOptions _$DriftOptionsFromJson(Map json) => $checkedCreate( 'assume_correct_reference', 'has_separate_analyzer', 'preamble', - 'fatal_warnings' + 'fatal_warnings', + 'schema_dir', + 'test_dir', + 'databases' ], ); final val = DriftOptions( @@ -105,6 +108,17 @@ DriftOptions _$DriftOptionsFromJson(Map json) => $checkedCreate( 'assume_correct_reference', (v) => v as bool? ?? false), dialect: $checkedConvert('sql', (v) => v == null ? null : DialectOptions.fromJson(v as Map)), + schemaDir: $checkedConvert( + 'schema_dir', (v) => v as String? ?? 'drift_schemas'), + testDir: + $checkedConvert('test_dir', (v) => v as String? ?? 'test/drift'), + databases: $checkedConvert( + 'databases', + (v) => + (v as Map?)?.map( + (k, e) => MapEntry(k as String, e as String), + ) ?? + {}), ); return val; }, @@ -136,7 +150,9 @@ DriftOptions _$DriftOptionsFromJson(Map json) => $checkedCreate( 'fatalWarnings': 'fatal_warnings', 'hasDriftAnalyzer': 'has_separate_analyzer', 'assumeCorrectReference': 'assume_correct_reference', - 'dialect': 'sql' + 'dialect': 'sql', + 'schemaDir': 'schema_dir', + 'testDir': 'test_dir' }, ); @@ -175,6 +191,9 @@ Map _$DriftOptionsToJson(DriftOptions instance) => 'has_separate_analyzer': instance.hasDriftAnalyzer, 'preamble': instance.preamble, 'fatal_warnings': instance.fatalWarnings, + 'schema_dir': instance.schemaDir, + 'test_dir': instance.testDir, + 'databases': instance.databases, }; const _$SqlModuleEnumMap = { From 295853d2e7fd364d527191ff2df3f1e9014f1f1a Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Sat, 5 Oct 2024 22:02:54 -0400 Subject: [PATCH 03/25] feat better messages --- drift_dev/lib/src/analysis/options.dart | 1 - .../lib/src/cli/commands/make_migrations.dart | 175 ++++++++++++------ 2 files changed, 122 insertions(+), 54 deletions(-) diff --git a/drift_dev/lib/src/analysis/options.dart b/drift_dev/lib/src/analysis/options.dart index b088941aa..7aa79b53a 100644 --- a/drift_dev/lib/src/analysis/options.dart +++ b/drift_dev/lib/src/analysis/options.dart @@ -6,7 +6,6 @@ import 'package:recase/recase.dart'; import 'package:sqlparser/sqlparser.dart' show BasicType, ResolvedType, SchemaFromCreateTable, SqliteVersion; import 'package:string_scanner/string_scanner.dart'; -import 'package:yaml/yaml.dart'; part '../generated/analysis/options.g.dart'; diff --git a/drift_dev/lib/src/cli/commands/make_migrations.dart b/drift_dev/lib/src/cli/commands/make_migrations.dart index 0f808a131..6351d55ee 100644 --- a/drift_dev/lib/src/cli/commands/make_migrations.dart +++ b/drift_dev/lib/src/cli/commands/make_migrations.dart @@ -15,8 +15,74 @@ class MakeMigrationCommand extends DriftCommand { MakeMigrationCommand(super.cli); @override - String get description => - "Generates the scaffold for your migrations, an autogenerated test and a data integrity test."; + String get description => """ +Generates migrations utilities for drift databases + +### Usage +After defining your database for the first time, run this command to save the schema. +When you are ready to make changes to the database, alter the schema in the database file, bump the schema version and run this command again. +This will generate the following: + +1. A steps file which contains a helper function to write a migration from one version to another. + + Example: + ${blue.wrap("class")} ${green.wrap("Database")} ${blue.wrap("extends")} ${green.wrap("_\$Database")} ${yellow.wrap("{")} + + ... + + ${lightCyan.wrap("@override")} + ${green.wrap("MigrationStrategy")} ${blue.wrap("get")} ${lightCyan.wrap("migration")} ${magenta.wrap("{")} + ${magenta.wrap("return")} ${green.wrap("MigrationStrategy")}${blue.wrap("(")} + ${lightCyan.wrap("onUpgrade")}: ${yellow.wrap("stepByStep(")} + ${lightCyan.wrap("from1To2")}: ${magenta.wrap("(")}${lightCyan.wrap("m")}, ${lightCyan.wrap("schema")}${magenta.wrap(")")} ${magenta.wrap("async {")} + ${magenta.wrap("await")} ${lightCyan.wrap("m")}.${yellow.wrap("stepByStep")}${blue.wrap("(")}${lightCyan.wrap("schema.todoEntries")}, ${lightCyan.wrap("schema.todoEntries.dueDate")}${blue.wrap(")")}; + ${magenta.wrap("}")}${yellow.wrap(")")}, + ${blue.wrap(")")}; + ${yellow.wrap("}")} + +2. A test file which contains tests to validate that the migrations are correct. This will allow you to try out your migrations easily. + +3. (Optional) The generated test can also be used to validate the data integrity of the migrations. + Fill the generated validation models with data that should be present in the database before and after the migration. + These lists will be imported in the test file to validate the data integrity of the migrations + + Example: + // Validate that the data in the database is still correct after removing a the isAdmin column + ${blue.wrap("final")} ${lightCyan.wrap("usersV1")} = ${yellow.wrap("[")} + v1.${green.wrap("User")}${magenta.wrap("(")}${lightCyan.wrap("id")}: ${green.wrap("Value")}${blue.wrap("(")}1${blue.wrap(")")}, ${lightCyan.wrap("name")}: ${green.wrap("Value")}${blue.wrap("(")}${lightRed.wrap("'Simon'")}${blue.wrap(")")}, ${lightCyan.wrap("isAdmin")}: ${green.wrap("Value")}${blue.wrap("(")}${blue.wrap("true")}${blue.wrap(")")}${magenta.wrap(")")}, + v1.${green.wrap("User")}${magenta.wrap("(")}${lightCyan.wrap("id")}: ${green.wrap("Value")}${blue.wrap("(")}2${blue.wrap(")")}, ${lightCyan.wrap("name")}: ${green.wrap("Value")}${blue.wrap("(")}${lightRed.wrap("'John'")}${blue.wrap(")")}, ${lightCyan.wrap("isAdmin")}: ${green.wrap("Value")}${blue.wrap("(")}${blue.wrap("false")}${blue.wrap(")")}${magenta.wrap(")")}, + ${yellow.wrap("]")}; + + ${blue.wrap("final")} ${lightCyan.wrap("usersV2")} = ${yellow.wrap("[")} + v2.${green.wrap("User")}${magenta.wrap("(")}${lightCyan.wrap("id")}: ${green.wrap("Value")}${blue.wrap("(")}1${blue.wrap(")")}, ${lightCyan.wrap("name")}: ${green.wrap("Value")}${blue.wrap("(")}${lightRed.wrap("'Simon'")}${blue.wrap(")")}${magenta.wrap(")")}, + v2.${green.wrap("User")}${magenta.wrap("(")}${lightCyan.wrap("id")}: ${green.wrap("Value")}${blue.wrap("(")}2${blue.wrap(")")}, ${lightCyan.wrap("name")}: ${green.wrap("Value")}${blue.wrap("(")}${lightRed.wrap("'John'")}${blue.wrap(")")}${magenta.wrap(")")}, + ${yellow.wrap("]")}; + +### Configuration + +This tool requires the following configuration in your build.yaml file: + +${blue.wrap(""" +targets: + \$default: + builders: + drift_dev: + options:""")} + ${green.wrap("# Required: The name of the database and the path to the database file")} + ${blue.wrap("databases")}: + ${blue.wrap("my_database")}: ${lightRed.wrap("lib/database.dart")} + + ${green.wrap("# Optional: Add more databases")} + ${blue.wrap("another_db")}: ${lightRed.wrap("lib/database2.dart")} + + + ${green.wrap("# Optional: The directory where the test files are stored")}: + ${blue.wrap("test_dir")}: ${lightRed.wrap("test/drift/")} ${green.wrap("# (default)")} + + ${green.wrap("# Optional: The directory where the schema files are stored")}: + ${blue.wrap("schema_dir")}: ${lightRed.wrap("drift_schema/")} ${green.wrap("# (default)")} + +"""; @override String get name => "make-migrations"; @@ -63,8 +129,6 @@ class MakeMigrationCommand extends DriftCommand { )); for (var writer in databaseMigrationsWriters) { - cli.logger - .info('Generating migration scaffold files for ${writer.dbName}'); // Dump the schema files for all databases await writer.writeSchemaFile(); writer.flush(); @@ -81,18 +145,6 @@ class MakeMigrationCommand extends DriftCommand { } } -/// Temporary class to store content to be written to the disk -class _WriteFileTask { - final File file; - final String content; - final bool overwrite; - - _WriteFileTask( - {required this.file, required this.content, this.overwrite = true}); - - void write() => file.writeAsStringSync(content); -} - class _DatabaseMigrationWriter { final DriftDevCli cli; final Directory rootSchemaDir; @@ -130,12 +182,12 @@ class _DatabaseMigrationWriter { /// Stores the tempoarary files to be written to the disk /// Only is written to the disk once the entire generation process completes without errors - final writeTasks = <_WriteFileTask>[]; + final writeTasks = {}; /// Write all the files to the disk void flush() { - for (final task in writeTasks) { - task.write(); + for (final MapEntry(key: file, value: content) in writeTasks.entries) { + file.writeAsStringSync(content); } writeTasks.clear(); } @@ -225,7 +277,7 @@ class _DatabaseMigrationWriter { if (!schemaFile.existsSync()) { cli.logger .info('$dbName: Creating schema file for version $schemaVersion'); - writeTasks.add(_WriteFileTask(file: schemaFile, content: content)); + writeTasks[schemaFile] = content; // Re-parse the schema to include the newly created schema file schemas = await parseSchema(schemaDir); } else if (schemaFile.readAsStringSync() != content) { @@ -237,11 +289,32 @@ class _DatabaseMigrationWriter { /// Create a step by step migration file Future writeStepsFile() async { - cli.logger.info( - '$dbName: Generating step by step migration in ${blue.wrap(p.relative(stepsFile.path))}'); - writeTasks.add(_WriteFileTask( - file: stepsFile, - content: StepsGenerationUtil.generateStepByStepMigration(schemas))); + if (!stepsFile.existsSync()) { + cli.logger.info( + """$dbName: Generated step by step migration helper in ${blue.wrap(p.relative(stepsFile.path))} +Use this generated `${yellow.wrap("stepByStep")}` to write your migrations. +Example: + +${blue.wrap("class")} ${green.wrap(dbClassName)} ${blue.wrap("extends")} ${green.wrap("_\$$dbClassName")} ${yellow.wrap("{")} + + ... + + ${lightCyan.wrap("@override")} + ${green.wrap("MigrationStrategy")} ${blue.wrap("get")} ${lightCyan.wrap("migration")} ${magenta.wrap("{")} + ${magenta.wrap("return")} ${green.wrap("MigrationStrategy")}${blue.wrap("(")} + ${lightCyan.wrap("onUpgrade")}: ${yellow.wrap("stepByStep(")} + ${lightCyan.wrap("from1To2")}: ${magenta.wrap("(")}${lightCyan.wrap("m")}, ${lightCyan.wrap("schema")}${magenta.wrap(")")} ${magenta.wrap("async {")} + ${backgroundGreen.wrap("// Write your migrations here")} + ${magenta.wrap("}")}${yellow.wrap(")")}, + ${blue.wrap(")")}; + ${yellow.wrap("}")} +"""); + } else { + cli.logger.fine( + "$dbName: Updating step by step migration helper in ${blue.wrap(p.relative(stepsFile.path))}"); + } + writeTasks[stepsFile] = + StepsGenerationUtil.generateStepByStepMigration(schemas); } /// Generate a built database for each schema version @@ -250,14 +323,11 @@ class _DatabaseMigrationWriter { for (final versionAndEntities in schemas.entries) { final version = versionAndEntities.key; final entities = versionAndEntities.value; - writeTasks.add(_WriteFileTask( - file: testUtilityFile(version), - content: GenerateUtils.generateSchemaCode( - cli, version, entities, true, true))); + writeTasks[testUtilityFile(version)] = + GenerateUtils.generateSchemaCode(cli, version, entities, true, true); } - writeTasks.add(_WriteFileTask( - file: File(p.join(testDatabasesDir.path, 'schema.dart')), - content: GenerateUtils.generateLibraryCode(schemas.keys))); + writeTasks[File(p.join(testDatabasesDir.path, 'schema.dart'))] = + GenerateUtils.generateLibraryCode(schemas.keys); } Future writeTests() async { @@ -265,18 +335,22 @@ class _DatabaseMigrationWriter { final relativeDbPath = p.relative(dbClassFile.path, from: p.join(cli.project.directory.path, 'lib')); + final files = []; for (final migration in migrations) { // Generate the validation models final validationFile = File(p.join(validationModelsDir.path, 'v${migration.from}_to_v${migration.to}.dart')); if (!validationFile.existsSync()) { - cli.logger.info( - '$dbName: Generating validation models in ${blue.wrap(p.relative(validationFile.path))}.' - ' Fill this file with before and after data to test the data integrity of the migration'); - writeTasks.add(_WriteFileTask( - file: validationFile, content: migration.validationModelsCode)); + files.add(validationFile); + writeTasks[validationFile] = migration.validationModelsCode; } } + if (files.isNotEmpty) { + cli.logger.info( + '$dbName: Generated validation models in ${blue.wrap(files.map((e) => p.relative(e.path)).join(', '))}\n' + 'Fill these lists with data that should be present in the database before and after the migration.\n' + 'These lists will be used to validate that the migration was successful and that no data was lost'); + } final stepByStepTests = migrations .map((e) => e.testStepByStepMigrationCode(dbName, dbClassName)); @@ -305,10 +379,16 @@ void main() { } """; final testFile = File(p.join(testDir.path, 'migration_test.dart')); - cli.logger.info( - '$dbName: Generating test in ${blue.wrap(p.relative(testFile.path))}.' - ' Run this test to validate that the migrations are correct'); - writeTasks.add(_WriteFileTask(file: testFile, content: code)); + if (testFile.existsSync()) { + cli.logger.fine( + '$dbName: Updated test in ${blue.wrap(p.relative(testFile.path))}'); + } else { + cli.logger.info( + '$dbName: Generated test in ${blue.wrap(p.relative(testFile.path))}.\n' + 'Run this test to validate that your migrations are written correctly. ${yellow.wrap("dart test ${blue.wrap(p.relative(testFile.path))}")}'); + } + + writeTasks[testFile] = code; } /// The json file where the schema for the current version of the database is stored @@ -401,18 +481,7 @@ import 'package:drift/drift.dart'; import '../schemas/schema_v$from.dart' as v$from; import '../schemas/schema_v$to.dart' as v$to; -/// Fill these lists with data that should be present in the database before and after the migration -/// These lists will be used to validate that the migration was successful and that no data was lost -/// e.g. Validate what happens when a column is removed: -/// -/// final usersV1 = [ -/// User(id: Value(1), name: Value('Simon'), isAdmin: Value(false)), -/// User(id: Value(2), name: Value('John'), isAdmin: Value(false)), -/// ]; -/// final usersV2 = [ -/// User(id: Value(1), name: Value('Simon')), -/// User(id: Value(2), name: Value('John')), -/// ]; +/// See `dart run drift_dev make-migrations --help` for more information ${tables.map((table) => """ final ${table.dbGetterName}V$from = >[]; From 6c8463c5a067c485bb21af2f8b6aec2c4961b5ee Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Sat, 5 Oct 2024 22:15:32 -0400 Subject: [PATCH 04/25] fix: typo and fix reparse schemas --- drift_dev/lib/src/cli/commands/make_migrations.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drift_dev/lib/src/cli/commands/make_migrations.dart b/drift_dev/lib/src/cli/commands/make_migrations.dart index 6351d55ee..5ae3f8199 100644 --- a/drift_dev/lib/src/cli/commands/make_migrations.dart +++ b/drift_dev/lib/src/cli/commands/make_migrations.dart @@ -131,7 +131,6 @@ targets: for (var writer in databaseMigrationsWriters) { // Dump the schema files for all databases await writer.writeSchemaFile(); - writer.flush(); // Write the step by step migration files for all databases // This is done after all the schema files have been written to the disk // to ensure that the schema files are up to date @@ -262,6 +261,7 @@ class _DatabaseMigrationWriter { } /// Create a .json dump of the current schema + /// This file is written instantly to the disk Future writeSchemaFile() async { // If the latest schema file version is larger than the current schema version // then something is wrong @@ -277,7 +277,7 @@ class _DatabaseMigrationWriter { if (!schemaFile.existsSync()) { cli.logger .info('$dbName: Creating schema file for version $schemaVersion'); - writeTasks[schemaFile] = content; + schemaFile.writeAsStringSync(content); // Re-parse the schema to include the newly created schema file schemas = await parseSchema(schemaDir); } else if (schemaFile.readAsStringSync() != content) { @@ -481,7 +481,7 @@ import 'package:drift/drift.dart'; import '../schemas/schema_v$from.dart' as v$from; import '../schemas/schema_v$to.dart' as v$to; -/// See `dart run drift_dev make-migrations --help` for more information +/// Run `dart run drift_dev make-migrations --help` for more information ${tables.map((table) => """ final ${table.dbGetterName}V$from = >[]; From ff9332f281758d79adef851eba2d6c6337381f0c Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Sat, 5 Oct 2024 22:22:56 -0400 Subject: [PATCH 05/25] docs: all-in-one docs --- docs/docs/Migrations/index.md | 8 ++++---- docs/docs/Tools/index.md | 4 ---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/docs/docs/Migrations/index.md b/docs/docs/Migrations/index.md index 9e71ca231..495459a1a 100644 --- a/docs/docs/Migrations/index.md +++ b/docs/docs/Migrations/index.md @@ -25,11 +25,11 @@ schema. This is helpful to: 2. Write migrations between older versions your app more easily, as the current schema generated by drift would also include subsequent changes from newer versions. -For this reason, we recommend using drift's tools based on [exporting schemas](exports.md) -for writing migrations. -These also enable [unit tests](tests.md), giving you confidence that your schema migration -is working correctly. +## Make Migration +Drift offers an all-in-one command for writing and testing migrations. +The `make-migration` command will save the current state of your database definition, create a migration helper function and create a test to verify the migration does what it should. +Run `dart run drift_dev make-migrations --help` for a detailed explanation of the command. ## Manual setup diff --git a/docs/docs/Tools/index.md b/docs/docs/Tools/index.md index f01125023..6694c28df 100644 --- a/docs/docs/Tools/index.md +++ b/docs/docs/Tools/index.md @@ -48,10 +48,6 @@ INFO: lib/src/data/database.dart has drift databases or daos: AppDatabase INFO: test/fake_db.dart has drift databases or daos: TodoDb, SomeDao ``` -## Make Migration - -TODO - ## Schema tools ### Dump for version control From 49b0658c4cf03cdff2d17093789679bb1df3fd10 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Sat, 5 Oct 2024 22:36:29 -0400 Subject: [PATCH 06/25] fix lint --- drift_dev/lib/src/cli/commands/make_migrations.dart | 3 ++- drift_dev/lib/src/cli/commands/schema/export.dart | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/drift_dev/lib/src/cli/commands/make_migrations.dart b/drift_dev/lib/src/cli/commands/make_migrations.dart index 5ae3f8199..19ce19a8c 100644 --- a/drift_dev/lib/src/cli/commands/make_migrations.dart +++ b/drift_dev/lib/src/cli/commands/make_migrations.dart @@ -60,7 +60,8 @@ This will generate the following: ### Configuration -This tool requires the following configuration in your build.yaml file: +This tool requires the database be defined in the build.yaml file. +Example: ${blue.wrap(""" targets: diff --git a/drift_dev/lib/src/cli/commands/schema/export.dart b/drift_dev/lib/src/cli/commands/schema/export.dart index 94005dcc8..e714f25f5 100644 --- a/drift_dev/lib/src/cli/commands/schema/export.dart +++ b/drift_dev/lib/src/cli/commands/schema/export.dart @@ -51,7 +51,7 @@ class ExportSchemaCommand extends Command { final dialect = SqlDialect.values.byName(argResults!.option('dialect') ?? 'sqlite'); - var (:elements, schemaVersion: _, db: __) = + var (:elements, schemaVersion: _, db: _) = await cli.readElementsFromSource(File(rest.single).absolute); // The roundtrip through the schema writer ensures that irrelevant things From f99a928d733e9214eafb1122198cf07a4489fa85 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Sun, 6 Oct 2024 00:50:03 -0400 Subject: [PATCH 07/25] test: make migration tests --- .../lib/src/cli/commands/make_migrations.dart | 18 +- drift_dev/test/cli/make_migrations_test.dart | 193 ++++++++++++++++++ 2 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 drift_dev/test/cli/make_migrations_test.dart diff --git a/drift_dev/lib/src/cli/commands/make_migrations.dart b/drift_dev/lib/src/cli/commands/make_migrations.dart index 19ce19a8c..463236916 100644 --- a/drift_dev/lib/src/cli/commands/make_migrations.dart +++ b/drift_dev/lib/src/cli/commands/make_migrations.dart @@ -81,7 +81,7 @@ targets: ${blue.wrap("test_dir")}: ${lightRed.wrap("test/drift/")} ${green.wrap("# (default)")} ${green.wrap("# Optional: The directory where the schema files are stored")}: - ${blue.wrap("schema_dir")}: ${lightRed.wrap("drift_schema/")} ${green.wrap("# (default)")} + ${blue.wrap("schema_dir")}: ${lightRed.wrap("drift_schemas/")} ${green.wrap("# (default)")} """; @@ -105,7 +105,7 @@ targets: ..createSync(recursive: true); /// The root directory where schema files for all databases are stored - /// e.g /drift_schema/ + /// e.g /drift_schemas/ final rootTestDir = Directory( p.join(cli.project.directory.path, cli.project.options.testDir)) ..createSync(recursive: true); @@ -132,6 +132,9 @@ targets: for (var writer in databaseMigrationsWriters) { // Dump the schema files for all databases await writer.writeSchemaFile(); + if (writer.schemaVersion == 1) { + continue; + } // Write the step by step migration files for all databases // This is done after all the schema files have been written to the disk // to ensure that the schema files are up to date @@ -153,7 +156,7 @@ class _DatabaseMigrationWriter { late final File dbClassFile; /// The directory where the schema files for this database are stored - /// e.g /drift_schema/my_database/ + /// e.g /drift_schemas/my_database/ final Directory schemaDir; /// The directory where the tests for this database are stored @@ -225,6 +228,15 @@ class _DatabaseMigrationWriter { cli.exit( 'The path for the "$dbName" database must be a relative path. Remove the leading slash'); } + if (!relativeDbClassPath.endsWith(".dart")) { + if (dbName == "schema_dir" || dbName == "test_dir") { + cli.exit( + "The path for the $dbName must be a dart file. It seems you have $dbName under the `options` section in the build.yaml file instead of under the `databases` section"); + } else { + cli.exit('The path for the "$dbName" database must be a dart file'); + } + } + final dbClassFile = File(p.join(cli.project.directory.path, relativeDbClassPath)); final schemaDir = Directory(p.join(rootSchemaDir.path, dbName)) diff --git a/drift_dev/test/cli/make_migrations_test.dart b/drift_dev/test/cli/make_migrations_test.dart new file mode 100644 index 000000000..fcf93abdc --- /dev/null +++ b/drift_dev/test/cli/make_migrations_test.dart @@ -0,0 +1,193 @@ +import 'dart:io'; + +import 'package:drift_dev/src/cli/cli.dart'; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; +import 'package:path/path.dart' as p; + +import '../utils.dart'; +import 'utils.dart'; + +void main() { + late TestDriftProject project; + + group( + 'make-migrations', + () { + tearDown(() async { + try { + await project.root.delete(recursive: true); + } catch (_) {} + }); + test('default', () async { + project = await TestDriftProject.create([ + d.dir('lib', [d.file('db.dart', _dbContent)]), + d.file('build.yaml', """ +targets: + \$default: + builders: + drift_dev: + options: + databases: + my_database: lib/db.dart""") + ]); + await project.runDriftCli(['make-migrations']); + expect( + d + .file('app/drift_schemas/my_database/drift_schema_v1.json') + .io + .existsSync(), + true); + // No other files should be created for 1st version + expect(d.file('app/test').io.existsSync(), false); + expect(d.file('app/lib/db.steps.dart').io.existsSync(), false); + + // Change the db schema without bumping the version + File(p.join(project.root.path, 'lib/db.dart')) + .writeAsStringSync(_dbWithNewColumnWithoutVersionBump); + // Should throw an error + try { + await project.runDriftCli(['make-migrations']); + fail('Expected an error'); + } catch (e) { + expect(e, isA()); + } + + // Change the db schema and bump the version + File(p.join(project.root.path, 'lib/db.dart')) + .writeAsStringSync(_dbWithNewColumnBump); + await project.runDriftCli(['make-migrations']); + expect( + d + .file('app/drift_schemas/my_database/drift_schema_v2.json') + .io + .existsSync(), + true); + // Test files should be created + await d + .file('app/test/drift/my_database/migration_test.dart', + IsValidDartFile(anything)) + .validate(); + await d + .file('app/test/drift/my_database/schemas/schema.dart', + IsValidDartFile(anything)) + .validate(); + + await d + .file('app/test/drift/my_database/schemas/schema_v1.dart', + IsValidDartFile(anything)) + .validate(); + await d + .file('app/test/drift/my_database/schemas/schema_v2.dart', + IsValidDartFile(anything)) + .validate(); + await d + .file('app/test/drift/my_database/validation/v1_to_v2.dart', + IsValidDartFile(anything)) + .validate(); + + // Steps file should be created + await d + .file('app/lib/db.steps.dart', IsValidDartFile(anything)) + .validate(); + }); + test('schema_dir is respected', () async { + project = await TestDriftProject.create([ + d.dir('lib', [d.file('db.dart', _dbContent)]), + d.file('build.yaml', """ +targets: + \$default: + builders: + drift_dev: + options: + schema_dir : schemas/drift + databases: + my_database: lib/db.dart""") + ]); + await project.runDriftCli(['make-migrations']); + expect( + d + .file('app/schemas/drift/my_database/drift_schema_v1.json') + .io + .existsSync(), + true); + }); + + test('test_dir is respected', () async { + project = await TestDriftProject.create([ + d.dir('lib', [d.file('db.dart', _dbContent)]), + d.file('build.yaml', """ +targets: + \$default: + builders: + drift_dev: + options: + test_dir : custom_test + databases: + my_database: lib/db.dart""") + ]); + await project.runDriftCli(['make-migrations']); + File(p.join(project.root.path, 'lib/db.dart')) + .writeAsStringSync(_dbWithNewColumnBump); + await project.runDriftCli(['make-migrations']); + await d + .file('app/custom_test/my_database/migration_test.dart', + IsValidDartFile(anything)) + .validate(); + }); + }, + ); +} + +const _dbContent = ''' +import 'package:drift/drift.dart'; + +class Examples extends Table { + BoolColumn get isDraft => boolean().withDefault(const Constant(true))(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); +} + +@DriftDatabase(tables: [Examples]) +class MyDatabase { + + @override + int get schemaVersion => 1; +} +'''; + +const _dbWithNewColumnWithoutVersionBump = ''' +import 'package:drift/drift.dart'; + +class Examples extends Table { + BoolColumn get isDraft => boolean().withDefault(const Constant(true))(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + IntColumn get newColumn => integer().nullable()(); +} + +@DriftDatabase(tables: [Examples]) +class MyDatabase { + + @override + int get schemaVersion => 1; +} +'''; + +const _dbWithNewColumnBump = ''' +import 'package:drift/drift.dart'; + +class Examples extends Table { + BoolColumn get isDraft => boolean().withDefault(const Constant(true))(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + IntColumn get newColumn => integer().nullable()(); +} + +@DriftDatabase(tables: [Examples]) +class MyDatabase { + + @override + int get schemaVersion => 2; +} +'''; From 8aeeb41434fa46edc80352e861816688f870ac79 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Sun, 6 Oct 2024 08:24:39 -0400 Subject: [PATCH 08/25] docs & example --- docs/docs/Migrations/api.md | 15 ++ docs/docs/Migrations/exports.md | 34 ++- docs/docs/Migrations/index.md | 163 ++++++++------ docs/docs/Migrations/step_by_step.md | 62 +++--- docs/docs/Migrations/tests.md | 25 ++- .../lib/src/cli/commands/make_migrations.dart | 13 +- examples/migrations_example/README.md | 16 +- examples/migrations_example/build.yaml | 5 + .../default}/drift_schema_v1.json | 0 .../default}/drift_schema_v10.json | 0 .../default}/drift_schema_v11.json | 0 .../default}/drift_schema_v2.json | 0 .../default}/drift_schema_v3.json | 0 .../default}/drift_schema_v4.json | 0 .../default}/drift_schema_v5.json | 0 .../default}/drift_schema_v6.json | 0 .../default}/drift_schema_v7.json | 0 .../default}/drift_schema_v8.json | 0 .../default}/drift_schema_v9.json | 0 examples/migrations_example/lib/database.dart | 10 +- .../versions.dart => database.steps.dart} | 0 .../test/drift/default/migration_test.dart | 204 ++++++++++++++++++ .../default/schemas}/schema.dart | 44 ++-- .../default/schemas}/schema_v1.dart | 0 .../default/schemas}/schema_v10.dart | 0 .../default/schemas}/schema_v11.dart | 0 .../default/schemas}/schema_v2.dart | 0 .../default/schemas}/schema_v3.dart | 0 .../default/schemas}/schema_v4.dart | 0 .../default/schemas}/schema_v5.dart | 0 .../default/schemas}/schema_v6.dart | 0 .../default/schemas}/schema_v7.dart | 0 .../default/schemas}/schema_v8.dart | 0 .../default/schemas}/schema_v9.dart | 0 .../drift/default/validation/v10_to_v11.dart | 17 ++ .../drift/default/validation/v1_to_v2.dart | 11 + .../drift/default/validation/v2_to_v3.dart | 9 + .../drift/default/validation/v3_to_v4.dart | 13 ++ .../drift/default/validation/v4_to_v5.dart | 13 ++ .../drift/default/validation/v5_to_v6.dart | 13 ++ .../drift/default/validation/v6_to_v7.dart | 13 ++ .../drift/default/validation/v7_to_v8.dart | 17 ++ .../drift/default/validation/v8_to_v9.dart | 17 ++ .../drift/default/validation/v9_to_v10.dart | 17 ++ .../test/migration_test.dart | 122 ----------- 45 files changed, 565 insertions(+), 288 deletions(-) rename examples/migrations_example/{drift_migrations => drift_schemas/default}/drift_schema_v1.json (100%) rename examples/migrations_example/{drift_migrations => drift_schemas/default}/drift_schema_v10.json (100%) rename examples/migrations_example/{drift_migrations => drift_schemas/default}/drift_schema_v11.json (100%) rename examples/migrations_example/{drift_migrations => drift_schemas/default}/drift_schema_v2.json (100%) rename examples/migrations_example/{drift_migrations => drift_schemas/default}/drift_schema_v3.json (100%) rename examples/migrations_example/{drift_migrations => drift_schemas/default}/drift_schema_v4.json (100%) rename examples/migrations_example/{drift_migrations => drift_schemas/default}/drift_schema_v5.json (100%) rename examples/migrations_example/{drift_migrations => drift_schemas/default}/drift_schema_v6.json (100%) rename examples/migrations_example/{drift_migrations => drift_schemas/default}/drift_schema_v7.json (100%) rename examples/migrations_example/{drift_migrations => drift_schemas/default}/drift_schema_v8.json (100%) rename examples/migrations_example/{drift_migrations => drift_schemas/default}/drift_schema_v9.json (100%) rename examples/migrations_example/lib/{src/versions.dart => database.steps.dart} (100%) create mode 100644 examples/migrations_example/test/drift/default/migration_test.dart rename examples/migrations_example/test/{generated => drift/default/schemas}/schema.dart (95%) rename examples/migrations_example/test/{generated => drift/default/schemas}/schema_v1.dart (100%) rename examples/migrations_example/test/{generated => drift/default/schemas}/schema_v10.dart (100%) rename examples/migrations_example/test/{generated => drift/default/schemas}/schema_v11.dart (100%) rename examples/migrations_example/test/{generated => drift/default/schemas}/schema_v2.dart (100%) rename examples/migrations_example/test/{generated => drift/default/schemas}/schema_v3.dart (100%) rename examples/migrations_example/test/{generated => drift/default/schemas}/schema_v4.dart (100%) rename examples/migrations_example/test/{generated => drift/default/schemas}/schema_v5.dart (100%) rename examples/migrations_example/test/{generated => drift/default/schemas}/schema_v6.dart (100%) rename examples/migrations_example/test/{generated => drift/default/schemas}/schema_v7.dart (100%) rename examples/migrations_example/test/{generated => drift/default/schemas}/schema_v8.dart (100%) rename examples/migrations_example/test/{generated => drift/default/schemas}/schema_v9.dart (100%) create mode 100644 examples/migrations_example/test/drift/default/validation/v10_to_v11.dart create mode 100644 examples/migrations_example/test/drift/default/validation/v1_to_v2.dart create mode 100644 examples/migrations_example/test/drift/default/validation/v2_to_v3.dart create mode 100644 examples/migrations_example/test/drift/default/validation/v3_to_v4.dart create mode 100644 examples/migrations_example/test/drift/default/validation/v4_to_v5.dart create mode 100644 examples/migrations_example/test/drift/default/validation/v5_to_v6.dart create mode 100644 examples/migrations_example/test/drift/default/validation/v6_to_v7.dart create mode 100644 examples/migrations_example/test/drift/default/validation/v7_to_v8.dart create mode 100644 examples/migrations_example/test/drift/default/validation/v8_to_v9.dart create mode 100644 examples/migrations_example/test/drift/default/validation/v9_to_v10.dart delete mode 100644 examples/migrations_example/test/migration_test.dart diff --git a/docs/docs/Migrations/api.md b/docs/docs/Migrations/api.md index 1b785b31f..323975e26 100644 --- a/docs/docs/Migrations/api.md +++ b/docs/docs/Migrations/api.md @@ -12,6 +12,21 @@ callback. However, the callbacks also give you an instance of `Migrator` as a parameter. This class knows about the target schema of the database and can be used to create, drop and alter most elements in your schema. +## General tips + +To ensure your schema stays consistent during a migration, you can wrap it in a `transaction` block. +However, be aware that some pragmas (including `foreign_keys`) can't be changed inside transactions. +Still, it can be useful to: + +- always re-enable foreign keys before using the database, by enabling them in [`beforeOpen`](#post-migration-callbacks). +- disable foreign-keys before migrations +- run migrations inside a transaction +- make sure your migrations didn't introduce any inconsistencies with `PRAGMA foreign_key_check`. + +With all of this combined, a migration callback can look like this: + +{{ load_snippet('structured','lib/snippets/migrations/migrations.dart.excerpt.json') }} + ## Migrating views, triggers and indices When changing the definition of a view, a trigger or an index, the easiest way diff --git a/docs/docs/Migrations/exports.md b/docs/docs/Migrations/exports.md index 6652b4abb..687d31064 100644 --- a/docs/docs/Migrations/exports.md +++ b/docs/docs/Migrations/exports.md @@ -5,6 +5,12 @@ description: Store all schema versions of your app for validation. --- + +!!! warning "Important Note" + + This command is specifically for exporting schemas. + If you are using the `make-migrations` command, this is already done for you. + By design, drift's code generator can only see the current state of your database schema. When you change it, it can be helpful to store a snapshot of the older schema in a file. @@ -38,27 +44,16 @@ my_app Of course, you can also use another folder or a subfolder somewhere if that suits your workflow better. -!!! note "Examples available" - - - Exporting schemas and generating code for them can't be done with `build_runner` alone, which is - why this setup described here is necessary. - - We hope it's worth it though! Verifying migrations can give you confidence that you won't run - into issues after changing your database. - If you get stuck along the way, don't hesitate to [open a discussion about it](https://github.com/simolus3/drift/discussions). - - Also there are two examples in the drift repository which may be useful as a reference: - - - A [Flutter app](https://github.com/simolus3/drift/tree/latest-release/examples/app) - - An [example specific to migrations](https://github.com/simolus3/drift/tree/latest-release/examples/migrations_example). - - - +Exporting schemas and generating code for them can't be done with `build_runner` alone, which is +why this setup described here is necessary. +We hope it's worth it though! Verifying migrations can give you confidence that you won't run +into issues after changing your database. +If you get stuck along the way, don't hesitate to [open a discussion about it](https://github.com/simolus3/drift/discussions). + ## Exporting the schema -To begin, lets create the first schema representation: +To begin, let's create the first schema representation: ``` $ mkdir drift_schemas @@ -93,9 +88,6 @@ $ dart run drift_dev schema dump lib/database/database.dart drift_schemas/drift_ database file, you can do that as well! `drift_dev schema dump` recognizes a sqlite3 database file as its first argument and can extract the relevant schema from there. - - - ## What now? Having exported your schema versions into files like this, drift tools are able diff --git a/docs/docs/Migrations/index.md b/docs/docs/Migrations/index.md index 495459a1a..895e94e80 100644 --- a/docs/docs/Migrations/index.md +++ b/docs/docs/Migrations/index.md @@ -5,33 +5,104 @@ description: Tooling and APIs to safely change the schema of your database. --- -The strict schema of tables and columns is what enables type-safe queries to -the database. -But since the schema is stored in the database too, changing it needs to happen -through migrations developed as part of your app. Drift provides APIs to make most -migrations easy to write, as well as command-line and testing tools to ensure -the migrations are correct. +Drift ensures type-safe queries through a strict schema. To change this schema, you must write migrations. +Drift provides a range of APIs, command-line tools, and testing utilities to make writing and verifying database migrations easier and more reliable. +## Guided Migrations -## Drift tools +Drift offers an all-in-one command for writing and testing migrations. +This tool helps you write your schema changes incrementally and generates tests to verify that your migrations are correct. -Writing correct migrations is crucial to ensure that your users won't end up with a broken database -in an inconsistent state. -For this reason, drift offers tools that make writing and testing migrations safe and easy. By exporting -each version of your schema to a json file, drift can reconstruct older versions of your database -schema. This is helpful to: +### Configuration -1. Test migrations between any two versions of your app. -2. Write migrations between older versions your app more easily, as the current schema generated by drift - would also include subsequent changes from newer versions. +To use the `make-migrations` command, you must add the location of your database(s) to your `build.yaml` file. -## Make Migration +```yaml title="build.yaml" +targets: + $default: + builders: + drift_dev: + options: + databases: + # Required: A name for the database and it's path + my_database: lib/database.dart -Drift offers an all-in-one command for writing and testing migrations. -The `make-migration` command will save the current state of your database definition, create a migration helper function and create a test to verify the migration does what it should. -Run `dart run drift_dev make-migrations --help` for a detailed explanation of the command. + # Optional: Add more databases + another_db: lib/database2.dart +``` + +You can also optionally specify the directory where the test files and schema (1) files are stored. +{ .annotate } + +1. Drift will generate multiple schema files, one for each version of your database schema. These files are used to compare the current schema with the previous schema and generate the migration code. + +```yaml title="build.yaml" +targets: + $default: + builders: + drift_dev: + options: + # The directory where the test files are stored: + test_dir: test/drift/ # (default) + + # The directory where the schema files are stored: + schema_dir: drift_schemas/ # (default) +``` + +### Usage + +Before you start making changes to your initial database schema, run this command to generate the initial schema file. + +```bash +dart run drift_dev make-migrations +``` +Once this initial schema file is saved, you can start making changes to your database schema. + + +Once you're happy with the changes, bump the `schemaVersion` in your database class and run the command again. + +```bash +dart run drift_dev make-migrations +``` + +This command will generate the following files: + +- A step-by-step migration file will be generated next to your database class. Use this function to write your migrations incrementally. See the [step-by-step migration guide](step_by_step.md) for more information. + + +- Drift will also generate a test file for your migrations. After you've written your migration, run the tests to verify that your migrations are written correctly. + +- Drift will also generate a file which can be used to make the tests validate the data integrity of your migrations. These files should be filled in with before and after data for each migration. + +If you get stuck along the way, don't hesitate to [open a discussion about it](https://github.com/simolus3/drift/discussions). + + +### Example + +See the [example](https://github.com/simolus3/drift/tree/develop/examples/migrations_example) in the drift repository for a complete example of how to use the `make-migrations` command. + +### Switching to `make-migrations` + +If you've already been using the `schema` tools to write migrations, you can switch to `make-migrations` by following these steps: + +1. Run the `make-migrations` command to generate the initial schema file. +2. Move all of your existing `schema` files into the schema directory for your database. +3. Run the `make-migrations` command again to generate the step-by-step migration file and test files. + +## During development + +During development, you might be changing your schema very often and don't want to write migrations for that +yet. You can just delete your apps' data and reinstall the app - the database will be deleted and all tables +will be created again. Please note that uninstalling is not enough sometimes - Android might have backed up +the database file and will re-create it when installing the app again. + +You can also delete and re-create all tables every time your app is opened, see [this comment](https://github.com/simolus3/drift/issues/188#issuecomment-542682912) +on how that can be achieved. -## Manual setup +## Manual Migrations + +!!! warning "Manual migrations are error-prone" + Writing migrations manually is error-prone and can lead to data loss. We recommend using the `make-migrations` command to generate migrations and tests. Drift provides a migration API that can be used to gradually apply schema changes after bumping the `schemaVersion` getter inside the `Database` class. To use it, override the `migration` @@ -54,28 +125,7 @@ However, be aware that drift expects the latest schema when creating SQL stateme For instance, when adding a new column to your database, you shouldn't run a `select` on that table before you've actually added the column. In general, try to avoid running queries in migration callbacks if possible. -Writing migrations without any tooling support isn't easy. Since correct migrations are -essential for app updates to work smoothly, we strongly recommend using the tools and testing -framework provided by drift to ensure your migrations are correct. -To do that, [export old versions](exports.md) to then use easy -[step-by-step migrations](step_by_step.md) or [tests](tests.md). - -## General tips - -To ensure your schema stays consistent during a migration, you can wrap it in a `transaction` block. -However, be aware that some pragmas (including `foreign_keys`) can't be changed inside transactions. -Still, it can be useful to: - -- always re-enable foreign keys before using the database, by enabling them in [`beforeOpen`](#post-migration-callbacks). -- disable foreign-keys before migrations -- run migrations inside a transaction -- make sure your migrations didn't introduce any inconsistencies with `PRAGMA foreign_key_check`. - -With all of this combined, a migration callback can look like this: - -{{ load_snippet('structured','lib/snippets/migrations/migrations.dart.excerpt.json') }} - -## Post-migration callbacks +## Post-Migration callbacks The `beforeOpen` parameter in `MigrationStrategy` can be used to populate data after the database has been created. It runs after migrations, but before any other query. Note that it will be called whenever the database is opened, @@ -114,32 +164,3 @@ beforeOpen: (details) async { } ``` -## During development - -During development, you might be changing your schema very often and don't want to write migrations for that -yet. You can just delete your apps' data and reinstall the app - the database will be deleted and all tables -will be created again. Please note that uninstalling is not enough sometimes - Android might have backed up -the database file and will re-create it when installing the app again. - -You can also delete and re-create all tables every time your app is opened, see [this comment](https://github.com/simolus3/drift/issues/188#issuecomment-542682912) -on how that can be achieved. - -## Verifying a database schema at runtime - -Instead (or in addition to) [writing tests](#verifying-a-database-schema-at-runtime) to ensure your migrations work as they should, -you can use a new API from `drift_dev` 1.5.0 to verify the current schema without any additional setup. - - - -{{ load_snippet('(full)','lib/snippets/migrations/runtime_verification.dart.excerpt.json') }} - -When you use `validateDatabaseSchema`, drift will transparently: - -- collect information about your database by reading from `sqlite3_schema`. -- create a fresh in-memory instance of your database and create a reference schema with `Migrator.createAll()`. -- compare the two. Ideally, your actual schema at runtime should be identical to the fresh one even though it - grew through different versions of your app. - -When a mismatch is found, an exception with a message explaining exactly where another value was expected will -be thrown. -This allows you to find issues with your schema migrations quickly. diff --git a/docs/docs/Migrations/step_by_step.md b/docs/docs/Migrations/step_by_step.md index 80a215d01..387805901 100644 --- a/docs/docs/Migrations/step_by_step.md +++ b/docs/docs/Migrations/step_by_step.md @@ -5,8 +5,6 @@ description: Use generated code reflecting over all schema versions to write mig --- - - Database migrations are typically written incrementally, with one piece of code transforming the database schema to the next version. By chaining these migrations, you can write schema migrations even for very old app versions. @@ -27,31 +25,6 @@ Sure, we could remember that the migration from 1 to 2 is now pointless and just upgrades from 1 to 3 directly, but this adds a lot of complexity. For more complex migration scripts spanning many versions, this can quickly lead to code that's hard to understand and maintain. -## Generating step-by-step code - -Drift provides tools to [export old schema versions](exports.md). After exporting all -your schema versions, you can use the following command to generate code aiding with the implementation -of step-by-step migrations: - -``` -$ dart run drift_dev schema steps drift_schemas/ lib/database/schema_versions.dart -``` - -The first argument (`drift_schemas/`) is the folder storing exported schemas, the second argument is -the path of the file to generate. Typically, you'd generate a file next to your database class. - -The generated file contains a `stepByStep` method which can be used to write migrations easily: - -{{ load_snippet('stepbystep','lib/snippets/migrations/step_by_step.dart.excerpt.json') }} - -`stepByStep` expects a callback for each schema upgrade responsible for running the partial migration. -That callback receives two parameters: A migrator `m` (similar to the regular migrator you'd get for -`onUpgrade` callbacks) and a `schema` parameter that gives you access to the schema at the version you're -migrating to. -For instance, in the `from1To2` function, `schema` provides getters for the database schema at version 2. -The migrator passed to the function is also set up to consider that specific version by default. -A call to `m.recreateAllViews()` would re-create views at the expected state of schema version 2, for instance. - ## Customizing step-by-step migrations The `stepByStep` function generated by the `drift_dev schema steps` command gives you an @@ -64,8 +37,8 @@ shows: {{ load_snippet('stepbystep2','lib/snippets/migrations/step_by_step.dart.excerpt.json') }} -Here, foreign keys are disabled before runnign the migration and re-enabled afterwards. -A check ensuring no inconsistencies occurred helps catching issues with the migration +Here, foreign keys are disabled before running the migration and re-enabled afterwards. +A check ensuring no inconsistencies occurred helps to catch issues with the migration in debug modes. ## Moving to step-by-step migrations @@ -84,3 +57,34 @@ this point. From now on, you can generate step-by-step migrations for each schem If you did not do this, a user migrating from schema 1 directly to schema 3 would not properly walk migrations and apply all migration changes required. + +## Manual Generation + +!!! warning "Important Note" + + This command is specifically for generating the step by step migration helper. + If you are using the `make-migrations` command, this is already done for you. + + +Drift provides tools to [export old schema versions](exports.md). After exporting all +your schema versions, you can use the following command to generate code aiding with the implementation +of step-by-step migrations: + +``` +$ dart run drift_dev schema steps drift_schemas/ lib/database/schema_versions.dart +``` + +The first argument (`drift_schemas/`) is the folder storing exported schemas, the second argument is +the path of the file to generate. Typically, you'd generate a file next to your database class. + +The generated file contains a `stepByStep` method which can be used to write migrations easily: + +{{ load_snippet('stepbystep','lib/snippets/migrations/step_by_step.dart.excerpt.json') }} + +`stepByStep` expects a callback for each schema upgrade responsible for running the partial migration. +That callback receives two parameters: A migrator `m` (similar to the regular migrator you'd get for +`onUpgrade` callbacks) and a `schema` parameter that gives you access to the schema at the version you're +migrating to. +For instance, in the `from1To2` function, `schema` provides getters for the database schema at version 2. +The migrator passed to the function is also set up to consider that specific version by default. +A call to `m.recreateAllViews()` would re-create views at the expected state of schema version 2, for instance. \ No newline at end of file diff --git a/docs/docs/Migrations/tests.md b/docs/docs/Migrations/tests.md index 3b8342709..44cf8b043 100644 --- a/docs/docs/Migrations/tests.md +++ b/docs/docs/Migrations/tests.md @@ -5,6 +5,9 @@ description: Generate test code to write unit tests for your migrations. --- +!!! warning "Important Note" + + If you are using the `make-migrations` command, tests are already generated for you. @@ -31,7 +34,7 @@ based on those schema files. For verifications, drift will generate a much smaller database implementation that can only be used to test migrations. -You can put this test code whereever you want, but it makes sense to put it in a subfolder of `test/`. +You can put this test code wherever you want, but it makes sense to put it in a subfolder of `test/`. If we wanted to write them to `test/generated_migrations/`, we could use ``` @@ -66,8 +69,6 @@ If it sees anything unexpected, it will throw a `SchemaMismatch` exception to fa Or, use [step-by-step migrations](step_by_step.md) which do this automatically. - - ## Verifying data integrity In addition to the changes made in your table structure, its useful to ensure that data that was present before a migration @@ -91,3 +92,21 @@ Then, you can import the generated classes with an alias: This can then be used to manually create and verify data at a specific version: {{ load_snippet('main','lib/snippets/migrations/tests/verify_data_integrity_test.dart.excerpt.json') }} + +## Verifying a database schema at runtime + +Instead (or in addition to) [writing tests](#verifying-a-database-schema-at-runtime) to ensure your migrations work as they should, +you can use a new API from `drift_dev` 1.5.0 to verify the current schema without any additional setup. + +{{ load_snippet('(full)','lib/snippets/migrations/runtime_verification.dart.excerpt.json') }} + +When you use `validateDatabaseSchema`, drift will transparently: + +- collect information about your database by reading from `sqlite3_schema`. +- create a fresh in-memory instance of your database and create a reference schema with `Migrator.createAll()`. +- compare the two. Ideally, your actual schema at runtime should be identical to the fresh one even though it + grew through different versions of your app. + +When a mismatch is found, an exception with a message explaining exactly where another value was expected will +be thrown. +This allows you to find issues with your schema migrations quickly. diff --git a/drift_dev/lib/src/cli/commands/make_migrations.dart b/drift_dev/lib/src/cli/commands/make_migrations.dart index 463236916..f77225ce6 100644 --- a/drift_dev/lib/src/cli/commands/make_migrations.dart +++ b/drift_dev/lib/src/cli/commands/make_migrations.dart @@ -476,7 +476,7 @@ test( validateItems: (newDb) async { ${tables.map( (table) { - return "expect(v${from}_to_v$to.${table.dbGetterName}V$from, await newDb.select(newDb.${table.dbGetterName}).get());"; + return "expect(v${from}_to_v$to.${table.dbGetterName}V$to, await newDb.select(newDb.${table.dbGetterName}).get());"; }, ).join('\n')} }, @@ -490,16 +490,17 @@ test( /// will fill in with before and after data /// to validate that the migration was successful and data was not lost String get validationModelsCode => """ -import 'package:drift/drift.dart'; import '../schemas/schema_v$from.dart' as v$from; import '../schemas/schema_v$to.dart' as v$to; /// Run `dart run drift_dev make-migrations --help` for more information -${tables.map((table) => """ +${tables.map((table) { + return """ -final ${table.dbGetterName}V$from = >[]; -final ${table.dbGetterName}V$to = >[]; -""").join('\n')} +final ${table.dbGetterName}V$from = []; +final ${table.dbGetterName}V$to = []; +"""; + }).join('\n')} """; } diff --git a/examples/migrations_example/README.md b/examples/migrations_example/README.md index 5adb1e9da..682ab7284 100644 --- a/examples/migrations_example/README.md +++ b/examples/migrations_example/README.md @@ -9,20 +9,20 @@ See `test/migration_test.dart` on how to use the generated verification code. After adapting a schema and incrementing the `schemaVersion` in the database, run ``` -dart run drift_dev schema dump lib/database.dart drift_migrations/ +dart run drift_dev make-migrations ``` -### Generating test code +### Testing -Run +Write the migration using the step-by-step migration helper. +To verify the migration, run the tests. ``` -dart run drift_dev schema generate drift_migrations/ test/generated/ --data-classes --companions +dart test ``` -Since we're using the step-by-step generator to make writing migrations easier, this command -is used to generate a helper file in `lib/`: +To test a specific migration, use the `-N` flag with the migration name. ``` -dart run drift_dev schema steps drift_migrations/ lib/src/versions.dart -``` +dart test -N "v1 to v2" +``` \ No newline at end of file diff --git a/examples/migrations_example/build.yaml b/examples/migrations_example/build.yaml index dd14a30a3..2e7f972bc 100644 --- a/examples/migrations_example/build.yaml +++ b/examples/migrations_example/build.yaml @@ -3,6 +3,11 @@ targets: builders: drift_dev: options: + # Relative path to the database file + databases: + default: lib/database.dart + + # Other Drift options store_date_time_values_as_text: true sql: dialect: sqlite diff --git a/examples/migrations_example/drift_migrations/drift_schema_v1.json b/examples/migrations_example/drift_schemas/default/drift_schema_v1.json similarity index 100% rename from examples/migrations_example/drift_migrations/drift_schema_v1.json rename to examples/migrations_example/drift_schemas/default/drift_schema_v1.json diff --git a/examples/migrations_example/drift_migrations/drift_schema_v10.json b/examples/migrations_example/drift_schemas/default/drift_schema_v10.json similarity index 100% rename from examples/migrations_example/drift_migrations/drift_schema_v10.json rename to examples/migrations_example/drift_schemas/default/drift_schema_v10.json diff --git a/examples/migrations_example/drift_migrations/drift_schema_v11.json b/examples/migrations_example/drift_schemas/default/drift_schema_v11.json similarity index 100% rename from examples/migrations_example/drift_migrations/drift_schema_v11.json rename to examples/migrations_example/drift_schemas/default/drift_schema_v11.json diff --git a/examples/migrations_example/drift_migrations/drift_schema_v2.json b/examples/migrations_example/drift_schemas/default/drift_schema_v2.json similarity index 100% rename from examples/migrations_example/drift_migrations/drift_schema_v2.json rename to examples/migrations_example/drift_schemas/default/drift_schema_v2.json diff --git a/examples/migrations_example/drift_migrations/drift_schema_v3.json b/examples/migrations_example/drift_schemas/default/drift_schema_v3.json similarity index 100% rename from examples/migrations_example/drift_migrations/drift_schema_v3.json rename to examples/migrations_example/drift_schemas/default/drift_schema_v3.json diff --git a/examples/migrations_example/drift_migrations/drift_schema_v4.json b/examples/migrations_example/drift_schemas/default/drift_schema_v4.json similarity index 100% rename from examples/migrations_example/drift_migrations/drift_schema_v4.json rename to examples/migrations_example/drift_schemas/default/drift_schema_v4.json diff --git a/examples/migrations_example/drift_migrations/drift_schema_v5.json b/examples/migrations_example/drift_schemas/default/drift_schema_v5.json similarity index 100% rename from examples/migrations_example/drift_migrations/drift_schema_v5.json rename to examples/migrations_example/drift_schemas/default/drift_schema_v5.json diff --git a/examples/migrations_example/drift_migrations/drift_schema_v6.json b/examples/migrations_example/drift_schemas/default/drift_schema_v6.json similarity index 100% rename from examples/migrations_example/drift_migrations/drift_schema_v6.json rename to examples/migrations_example/drift_schemas/default/drift_schema_v6.json diff --git a/examples/migrations_example/drift_migrations/drift_schema_v7.json b/examples/migrations_example/drift_schemas/default/drift_schema_v7.json similarity index 100% rename from examples/migrations_example/drift_migrations/drift_schema_v7.json rename to examples/migrations_example/drift_schemas/default/drift_schema_v7.json diff --git a/examples/migrations_example/drift_migrations/drift_schema_v8.json b/examples/migrations_example/drift_schemas/default/drift_schema_v8.json similarity index 100% rename from examples/migrations_example/drift_migrations/drift_schema_v8.json rename to examples/migrations_example/drift_schemas/default/drift_schema_v8.json diff --git a/examples/migrations_example/drift_migrations/drift_schema_v9.json b/examples/migrations_example/drift_schemas/default/drift_schema_v9.json similarity index 100% rename from examples/migrations_example/drift_migrations/drift_schema_v9.json rename to examples/migrations_example/drift_schemas/default/drift_schema_v9.json diff --git a/examples/migrations_example/lib/database.dart b/examples/migrations_example/lib/database.dart index fb491a331..fa16fd368 100644 --- a/examples/migrations_example/lib/database.dart +++ b/examples/migrations_example/lib/database.dart @@ -1,9 +1,9 @@ import 'package:drift/drift.dart'; import 'package:drift/internal/versioned_schema.dart'; import 'package:drift_dev/api/migrations.dart'; +import 'package:migrations_example/database.steps.dart'; import 'tables.dart'; -import 'src/versions.dart'; part 'database.g.dart'; @@ -16,10 +16,8 @@ const kDebugMode = true; include: {'tables.drift'}, ) class Database extends _$Database { - static const latestSchemaVersion = 11; - @override - int get schemaVersion => latestSchemaVersion; + int get schemaVersion => 11; Database(super.connection); @@ -27,7 +25,7 @@ class Database extends _$Database { MigrationStrategy get migration { return MigrationStrategy( onUpgrade: (m, from, to) async { - // Following the advice from https://drift.simonbinder.eu/docs/advanced-features/migrations/#tips + // Following the advice from https://drift.simonbinder.eu/Migrations/api/#general-tips await customStatement('PRAGMA foreign_keys = OFF'); await transaction( @@ -50,7 +48,7 @@ class Database extends _$Database { }, beforeOpen: (details) async { // For Flutter apps, this should be wrapped in an if (kDebugMode) as - // suggested here: https://drift.simonbinder.eu/docs/advanced-features/migrations/#verifying-a-database-schema-at-runtime + // suggested here: https://drift.simonbinder.eu/Migrations/tests/#verifying-a-database-schema-at-runtime await validateDatabaseSchema(); }, ); diff --git a/examples/migrations_example/lib/src/versions.dart b/examples/migrations_example/lib/database.steps.dart similarity index 100% rename from examples/migrations_example/lib/src/versions.dart rename to examples/migrations_example/lib/database.steps.dart diff --git a/examples/migrations_example/test/drift/default/migration_test.dart b/examples/migrations_example/test/drift/default/migration_test.dart new file mode 100644 index 000000000..4842004d8 --- /dev/null +++ b/examples/migrations_example/test/drift/default/migration_test.dart @@ -0,0 +1,204 @@ +// ignore_for_file: unused_local_variable, unused_import +// GENERATED CODE, DO NOT EDIT BY HAND. +import 'package:drift/drift.dart'; +import 'package:drift_dev/api/migrations.dart'; +import 'package:migrations_example/database.dart'; +import 'package:test/test.dart'; +import 'schemas/schema.dart'; + +import 'schemas/schema_v1.dart' as v1; +import 'schemas/schema_v2.dart' as v2; +import 'validation/v1_to_v2.dart' as v1_to_v2; +import 'schemas/schema_v3.dart' as v3; +import 'validation/v2_to_v3.dart' as v2_to_v3; +import 'schemas/schema_v4.dart' as v4; +import 'validation/v3_to_v4.dart' as v3_to_v4; +import 'schemas/schema_v5.dart' as v5; +import 'validation/v4_to_v5.dart' as v4_to_v5; +import 'schemas/schema_v6.dart' as v6; +import 'validation/v5_to_v6.dart' as v5_to_v6; +import 'schemas/schema_v7.dart' as v7; +import 'validation/v6_to_v7.dart' as v6_to_v7; +import 'schemas/schema_v8.dart' as v8; +import 'validation/v7_to_v8.dart' as v7_to_v8; +import 'schemas/schema_v9.dart' as v9; +import 'validation/v8_to_v9.dart' as v8_to_v9; +import 'schemas/schema_v10.dart' as v10; +import 'validation/v9_to_v10.dart' as v9_to_v10; +import 'schemas/schema_v11.dart' as v11; +import 'validation/v10_to_v11.dart' as v10_to_v11; + +void main() { + driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; + late SchemaVerifier verifier; + + setUpAll(() { + verifier = SchemaVerifier(GeneratedHelper()); + }); + + test( + "default - migrate from v1 to v2", + () => testStepByStepigrations( + from: 1, to: 2, verifier: verifier, oldDbCallback: (e) => v1.DatabaseAtV1(e), + newDbCallback: (e) => v2.DatabaseAtV2(e), currentDbCallback: (e) => Database(e), + createItems: (b, oldDb) { + b.insertAll(oldDb.users, v1_to_v2.usersV1); + }, + validateItems: (newDb) async { + expect(v1_to_v2.usersV2, await newDb.select(newDb.users).get()); + }, + ) +); + +test( + "default - migrate from v2 to v3", + () => testStepByStepigrations( + from: 2, to: 3, verifier: verifier, oldDbCallback: (e) => v2.DatabaseAtV2(e), + newDbCallback: (e) => v3.DatabaseAtV3(e), currentDbCallback: (e) => Database(e), + createItems: (b, oldDb) { + b.insertAll(oldDb.users, v2_to_v3.usersV2); + }, + validateItems: (newDb) async { + expect(v2_to_v3.usersV3, await newDb.select(newDb.users).get()); + }, + ) +); + +test( + "default - migrate from v3 to v4", + () => testStepByStepigrations( + from: 3, to: 4, verifier: verifier, oldDbCallback: (e) => v3.DatabaseAtV3(e), + newDbCallback: (e) => v4.DatabaseAtV4(e), currentDbCallback: (e) => Database(e), + createItems: (b, oldDb) { + b.insertAll(oldDb.users, v3_to_v4.usersV3); +b.insertAll(oldDb.groups, v3_to_v4.groupsV3); + }, + validateItems: (newDb) async { + expect(v3_to_v4.usersV4, await newDb.select(newDb.users).get()); +expect(v3_to_v4.groupsV4, await newDb.select(newDb.groups).get()); + }, + ) +); + +test( + "default - migrate from v4 to v5", + () => testStepByStepigrations( + from: 4, to: 5, verifier: verifier, oldDbCallback: (e) => v4.DatabaseAtV4(e), + newDbCallback: (e) => v5.DatabaseAtV5(e), currentDbCallback: (e) => Database(e), + createItems: (b, oldDb) { + b.insertAll(oldDb.users, v4_to_v5.usersV4); +b.insertAll(oldDb.groups, v4_to_v5.groupsV4); + }, + validateItems: (newDb) async { + expect(v4_to_v5.usersV5, await newDb.select(newDb.users).get()); +expect(v4_to_v5.groupsV5, await newDb.select(newDb.groups).get()); + }, + ) +); + +test( + "default - migrate from v5 to v6", + () => testStepByStepigrations( + from: 5, to: 6, verifier: verifier, oldDbCallback: (e) => v5.DatabaseAtV5(e), + newDbCallback: (e) => v6.DatabaseAtV6(e), currentDbCallback: (e) => Database(e), + createItems: (b, oldDb) { + b.insertAll(oldDb.users, v5_to_v6.usersV5); +b.insertAll(oldDb.groups, v5_to_v6.groupsV5); + }, + validateItems: (newDb) async { + expect(v5_to_v6.usersV6, await newDb.select(newDb.users).get()); +expect(v5_to_v6.groupsV6, await newDb.select(newDb.groups).get()); + }, + ) +); + +test( + "default - migrate from v6 to v7", + () => testStepByStepigrations( + from: 6, to: 7, verifier: verifier, oldDbCallback: (e) => v6.DatabaseAtV6(e), + newDbCallback: (e) => v7.DatabaseAtV7(e), currentDbCallback: (e) => Database(e), + createItems: (b, oldDb) { + b.insertAll(oldDb.users, v6_to_v7.usersV6); +b.insertAll(oldDb.groups, v6_to_v7.groupsV6); + }, + validateItems: (newDb) async { + expect(v6_to_v7.usersV7, await newDb.select(newDb.users).get()); +expect(v6_to_v7.groupsV7, await newDb.select(newDb.groups).get()); + }, + ) +); + +test( + "default - migrate from v7 to v8", + () => testStepByStepigrations( + from: 7, to: 8, verifier: verifier, oldDbCallback: (e) => v7.DatabaseAtV7(e), + newDbCallback: (e) => v8.DatabaseAtV8(e), currentDbCallback: (e) => Database(e), + createItems: (b, oldDb) { + b.insertAll(oldDb.users, v7_to_v8.usersV7); +b.insertAll(oldDb.groups, v7_to_v8.groupsV7); +b.insertAll(oldDb.notes, v7_to_v8.notesV7); + }, + validateItems: (newDb) async { + expect(v7_to_v8.usersV8, await newDb.select(newDb.users).get()); +expect(v7_to_v8.groupsV8, await newDb.select(newDb.groups).get()); +expect(v7_to_v8.notesV8, await newDb.select(newDb.notes).get()); + }, + ) +); + +test( + "default - migrate from v8 to v9", + () => testStepByStepigrations( + from: 8, to: 9, verifier: verifier, oldDbCallback: (e) => v8.DatabaseAtV8(e), + newDbCallback: (e) => v9.DatabaseAtV9(e), currentDbCallback: (e) => Database(e), + createItems: (b, oldDb) { + b.insertAll(oldDb.users, v8_to_v9.usersV8); +b.insertAll(oldDb.groups, v8_to_v9.groupsV8); +b.insertAll(oldDb.notes, v8_to_v9.notesV8); + }, + validateItems: (newDb) async { + expect(v8_to_v9.usersV9, await newDb.select(newDb.users).get()); +expect(v8_to_v9.groupsV9, await newDb.select(newDb.groups).get()); +expect(v8_to_v9.notesV9, await newDb.select(newDb.notes).get()); + }, + ) +); + +test( + "default - migrate from v9 to v10", + () => testStepByStepigrations( + from: 9, to: 10, verifier: verifier, oldDbCallback: (e) => v9.DatabaseAtV9(e), + newDbCallback: (e) => v10.DatabaseAtV10(e), currentDbCallback: (e) => Database(e), + createItems: (b, oldDb) { + b.insertAll(oldDb.users, v9_to_v10.usersV9); +b.insertAll(oldDb.groups, v9_to_v10.groupsV9); +b.insertAll(oldDb.notes, v9_to_v10.notesV9); + }, + validateItems: (newDb) async { + expect(v9_to_v10.usersV10, await newDb.select(newDb.users).get()); +expect(v9_to_v10.groupsV10, await newDb.select(newDb.groups).get()); +expect(v9_to_v10.notesV10, await newDb.select(newDb.notes).get()); + }, + ) +); + +test( + "default - migrate from v10 to v11", + () => testStepByStepigrations( + from: 10, to: 11, verifier: verifier, oldDbCallback: (e) => v10.DatabaseAtV10(e), + newDbCallback: (e) => v11.DatabaseAtV11(e), currentDbCallback: (e) => Database(e), + createItems: (b, oldDb) { + b.insertAll(oldDb.users, v10_to_v11.usersV10); +b.insertAll(oldDb.groups, v10_to_v11.groupsV10); +b.insertAll(oldDb.notes, v10_to_v11.notesV10); + }, + validateItems: (newDb) async { + expect(v10_to_v11.usersV11, await newDb.select(newDb.users).get()); +expect(v10_to_v11.groupsV11, await newDb.select(newDb.groups).get()); +expect(v10_to_v11.notesV11, await newDb.select(newDb.notes).get()); + }, + ) +); + + +} diff --git a/examples/migrations_example/test/generated/schema.dart b/examples/migrations_example/test/drift/default/schemas/schema.dart similarity index 95% rename from examples/migrations_example/test/generated/schema.dart rename to examples/migrations_example/test/drift/default/schemas/schema.dart index 24b3c788a..af423e42e 100644 --- a/examples/migrations_example/test/generated/schema.dart +++ b/examples/migrations_example/test/drift/default/schemas/schema.dart @@ -3,47 +3,47 @@ //@dart=2.12 import 'package:drift/drift.dart'; import 'package:drift/internal/migrations.dart'; -import 'schema_v1.dart' as v1; -import 'schema_v2.dart' as v2; -import 'schema_v3.dart' as v3; -import 'schema_v4.dart' as v4; -import 'schema_v5.dart' as v5; -import 'schema_v6.dart' as v6; import 'schema_v7.dart' as v7; +import 'schema_v5.dart' as v5; import 'schema_v8.dart' as v8; +import 'schema_v3.dart' as v3; +import 'schema_v6.dart' as v6; +import 'schema_v2.dart' as v2; +import 'schema_v1.dart' as v1; +import 'schema_v11.dart' as v11; import 'schema_v9.dart' as v9; import 'schema_v10.dart' as v10; -import 'schema_v11.dart' as v11; +import 'schema_v4.dart' as v4; class GeneratedHelper implements SchemaInstantiationHelper { @override GeneratedDatabase databaseForVersion(QueryExecutor db, int version) { switch (version) { - case 1: - return v1.DatabaseAtV1(db); - case 2: - return v2.DatabaseAtV2(db); - case 3: - return v3.DatabaseAtV3(db); - case 4: - return v4.DatabaseAtV4(db); - case 5: - return v5.DatabaseAtV5(db); - case 6: - return v6.DatabaseAtV6(db); case 7: return v7.DatabaseAtV7(db); + case 5: + return v5.DatabaseAtV5(db); case 8: return v8.DatabaseAtV8(db); + case 3: + return v3.DatabaseAtV3(db); + case 6: + return v6.DatabaseAtV6(db); + case 2: + return v2.DatabaseAtV2(db); + case 1: + return v1.DatabaseAtV1(db); + case 11: + return v11.DatabaseAtV11(db); case 9: return v9.DatabaseAtV9(db); case 10: return v10.DatabaseAtV10(db); - case 11: - return v11.DatabaseAtV11(db); + case 4: + return v4.DatabaseAtV4(db); default: throw MissingSchemaException( - version, const {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}); + version, const {7, 5, 8, 3, 6, 2, 1, 11, 9, 10, 4}); } } } diff --git a/examples/migrations_example/test/generated/schema_v1.dart b/examples/migrations_example/test/drift/default/schemas/schema_v1.dart similarity index 100% rename from examples/migrations_example/test/generated/schema_v1.dart rename to examples/migrations_example/test/drift/default/schemas/schema_v1.dart diff --git a/examples/migrations_example/test/generated/schema_v10.dart b/examples/migrations_example/test/drift/default/schemas/schema_v10.dart similarity index 100% rename from examples/migrations_example/test/generated/schema_v10.dart rename to examples/migrations_example/test/drift/default/schemas/schema_v10.dart diff --git a/examples/migrations_example/test/generated/schema_v11.dart b/examples/migrations_example/test/drift/default/schemas/schema_v11.dart similarity index 100% rename from examples/migrations_example/test/generated/schema_v11.dart rename to examples/migrations_example/test/drift/default/schemas/schema_v11.dart diff --git a/examples/migrations_example/test/generated/schema_v2.dart b/examples/migrations_example/test/drift/default/schemas/schema_v2.dart similarity index 100% rename from examples/migrations_example/test/generated/schema_v2.dart rename to examples/migrations_example/test/drift/default/schemas/schema_v2.dart diff --git a/examples/migrations_example/test/generated/schema_v3.dart b/examples/migrations_example/test/drift/default/schemas/schema_v3.dart similarity index 100% rename from examples/migrations_example/test/generated/schema_v3.dart rename to examples/migrations_example/test/drift/default/schemas/schema_v3.dart diff --git a/examples/migrations_example/test/generated/schema_v4.dart b/examples/migrations_example/test/drift/default/schemas/schema_v4.dart similarity index 100% rename from examples/migrations_example/test/generated/schema_v4.dart rename to examples/migrations_example/test/drift/default/schemas/schema_v4.dart diff --git a/examples/migrations_example/test/generated/schema_v5.dart b/examples/migrations_example/test/drift/default/schemas/schema_v5.dart similarity index 100% rename from examples/migrations_example/test/generated/schema_v5.dart rename to examples/migrations_example/test/drift/default/schemas/schema_v5.dart diff --git a/examples/migrations_example/test/generated/schema_v6.dart b/examples/migrations_example/test/drift/default/schemas/schema_v6.dart similarity index 100% rename from examples/migrations_example/test/generated/schema_v6.dart rename to examples/migrations_example/test/drift/default/schemas/schema_v6.dart diff --git a/examples/migrations_example/test/generated/schema_v7.dart b/examples/migrations_example/test/drift/default/schemas/schema_v7.dart similarity index 100% rename from examples/migrations_example/test/generated/schema_v7.dart rename to examples/migrations_example/test/drift/default/schemas/schema_v7.dart diff --git a/examples/migrations_example/test/generated/schema_v8.dart b/examples/migrations_example/test/drift/default/schemas/schema_v8.dart similarity index 100% rename from examples/migrations_example/test/generated/schema_v8.dart rename to examples/migrations_example/test/drift/default/schemas/schema_v8.dart diff --git a/examples/migrations_example/test/generated/schema_v9.dart b/examples/migrations_example/test/drift/default/schemas/schema_v9.dart similarity index 100% rename from examples/migrations_example/test/generated/schema_v9.dart rename to examples/migrations_example/test/drift/default/schemas/schema_v9.dart diff --git a/examples/migrations_example/test/drift/default/validation/v10_to_v11.dart b/examples/migrations_example/test/drift/default/validation/v10_to_v11.dart new file mode 100644 index 000000000..9ad66c0a2 --- /dev/null +++ b/examples/migrations_example/test/drift/default/validation/v10_to_v11.dart @@ -0,0 +1,17 @@ +import '../schemas/schema_v10.dart' as v10; +import '../schemas/schema_v11.dart' as v11; + +/// Run `dart run drift_dev make-migrations --help` for more information + +final usersV10 = []; +final usersV11 = []; + + +final groupsV10 = []; +final groupsV11 = []; + + +final notesV10 = []; +final notesV11 = []; + + diff --git a/examples/migrations_example/test/drift/default/validation/v1_to_v2.dart b/examples/migrations_example/test/drift/default/validation/v1_to_v2.dart new file mode 100644 index 000000000..17ad3b411 --- /dev/null +++ b/examples/migrations_example/test/drift/default/validation/v1_to_v2.dart @@ -0,0 +1,11 @@ +import '../schemas/schema_v1.dart' as v1; +import '../schemas/schema_v2.dart' as v2; + +/// Run `dart run drift_dev make-migrations --help` for more information + +final usersV1 = [ + v1.UsersData(id: 0), +]; +final usersV2 = [ + v2.UsersData(id: 0, name: 'no name'), +]; diff --git a/examples/migrations_example/test/drift/default/validation/v2_to_v3.dart b/examples/migrations_example/test/drift/default/validation/v2_to_v3.dart new file mode 100644 index 000000000..86d6f2cd7 --- /dev/null +++ b/examples/migrations_example/test/drift/default/validation/v2_to_v3.dart @@ -0,0 +1,9 @@ +import '../schemas/schema_v2.dart' as v2; +import '../schemas/schema_v3.dart' as v3; + +/// Run `dart run drift_dev make-migrations --help` for more information + +final usersV2 = []; +final usersV3 = []; + + diff --git a/examples/migrations_example/test/drift/default/validation/v3_to_v4.dart b/examples/migrations_example/test/drift/default/validation/v3_to_v4.dart new file mode 100644 index 000000000..5bff92d52 --- /dev/null +++ b/examples/migrations_example/test/drift/default/validation/v3_to_v4.dart @@ -0,0 +1,13 @@ +import '../schemas/schema_v3.dart' as v3; +import '../schemas/schema_v4.dart' as v4; + +/// Run `dart run drift_dev make-migrations --help` for more information + +final usersV3 = []; +final usersV4 = []; + + +final groupsV3 = []; +final groupsV4 = []; + + diff --git a/examples/migrations_example/test/drift/default/validation/v4_to_v5.dart b/examples/migrations_example/test/drift/default/validation/v4_to_v5.dart new file mode 100644 index 000000000..53a37a9f5 --- /dev/null +++ b/examples/migrations_example/test/drift/default/validation/v4_to_v5.dart @@ -0,0 +1,13 @@ +import '../schemas/schema_v4.dart' as v4; +import '../schemas/schema_v5.dart' as v5; + +/// Run `dart run drift_dev make-migrations --help` for more information + +final usersV4 = []; +final usersV5 = []; + + +final groupsV4 = []; +final groupsV5 = []; + + diff --git a/examples/migrations_example/test/drift/default/validation/v5_to_v6.dart b/examples/migrations_example/test/drift/default/validation/v5_to_v6.dart new file mode 100644 index 000000000..7fec73616 --- /dev/null +++ b/examples/migrations_example/test/drift/default/validation/v5_to_v6.dart @@ -0,0 +1,13 @@ +import '../schemas/schema_v5.dart' as v5; +import '../schemas/schema_v6.dart' as v6; + +/// Run `dart run drift_dev make-migrations --help` for more information + +final usersV5 = []; +final usersV6 = []; + + +final groupsV5 = []; +final groupsV6 = []; + + diff --git a/examples/migrations_example/test/drift/default/validation/v6_to_v7.dart b/examples/migrations_example/test/drift/default/validation/v6_to_v7.dart new file mode 100644 index 000000000..d17d3b17f --- /dev/null +++ b/examples/migrations_example/test/drift/default/validation/v6_to_v7.dart @@ -0,0 +1,13 @@ +import '../schemas/schema_v6.dart' as v6; +import '../schemas/schema_v7.dart' as v7; + +/// Run `dart run drift_dev make-migrations --help` for more information + +final usersV6 = []; +final usersV7 = []; + + +final groupsV6 = []; +final groupsV7 = []; + + diff --git a/examples/migrations_example/test/drift/default/validation/v7_to_v8.dart b/examples/migrations_example/test/drift/default/validation/v7_to_v8.dart new file mode 100644 index 000000000..77aca91ee --- /dev/null +++ b/examples/migrations_example/test/drift/default/validation/v7_to_v8.dart @@ -0,0 +1,17 @@ +import '../schemas/schema_v7.dart' as v7; +import '../schemas/schema_v8.dart' as v8; + +/// Run `dart run drift_dev make-migrations --help` for more information + +final usersV7 = []; +final usersV8 = []; + + +final groupsV7 = []; +final groupsV8 = []; + + +final notesV7 = []; +final notesV8 = []; + + diff --git a/examples/migrations_example/test/drift/default/validation/v8_to_v9.dart b/examples/migrations_example/test/drift/default/validation/v8_to_v9.dart new file mode 100644 index 000000000..5a07b0baf --- /dev/null +++ b/examples/migrations_example/test/drift/default/validation/v8_to_v9.dart @@ -0,0 +1,17 @@ +import '../schemas/schema_v8.dart' as v8; +import '../schemas/schema_v9.dart' as v9; + +/// Run `dart run drift_dev make-migrations --help` for more information + +final usersV8 = []; +final usersV9 = []; + + +final groupsV8 = []; +final groupsV9 = []; + + +final notesV8 = []; +final notesV9 = []; + + diff --git a/examples/migrations_example/test/drift/default/validation/v9_to_v10.dart b/examples/migrations_example/test/drift/default/validation/v9_to_v10.dart new file mode 100644 index 000000000..73dfcd0f5 --- /dev/null +++ b/examples/migrations_example/test/drift/default/validation/v9_to_v10.dart @@ -0,0 +1,17 @@ +import '../schemas/schema_v9.dart' as v9; +import '../schemas/schema_v10.dart' as v10; + +/// Run `dart run drift_dev make-migrations --help` for more information + +final usersV9 = []; +final usersV10 = []; + + +final groupsV9 = []; +final groupsV10 = []; + + +final notesV9 = []; +final notesV10 = []; + + diff --git a/examples/migrations_example/test/migration_test.dart b/examples/migrations_example/test/migration_test.dart deleted file mode 100644 index b44b4d35f..000000000 --- a/examples/migrations_example/test/migration_test.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'package:drift/native.dart'; -import 'package:migrations_example/database.dart'; -import 'package:drift/drift.dart'; -import 'package:test/test.dart'; -import 'package:drift_dev/api/migrations.dart'; - -// Import the generated schema helper to instantiate databases at old versions. -import 'generated/schema.dart'; - -import 'generated/schema_v1.dart' as v1; -import 'generated/schema_v2.dart' as v2; -import 'generated/schema_v4.dart' as v4; -import 'generated/schema_v5.dart' as v5; - -void main() { - driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; - late SchemaVerifier verifier; - - setUpAll(() { - verifier = SchemaVerifier(GeneratedHelper()); - }); - - // Test all possible schema migrations with a simple test that just ensures - // the schema is correct after the migration. - // More complex tests ensuring data integrity are written below. - group('general migration', () { - const currentSchema = Database.latestSchemaVersion; - - for (var oldVersion = 1; oldVersion < currentSchema; oldVersion++) { - group('from v$oldVersion', () { - for (var targetVersion = oldVersion + 1; - targetVersion <= currentSchema; - targetVersion++) { - test('to v$targetVersion', () async { - final connection = await verifier.startAt(oldVersion); - final db = Database(connection); - addTearDown(db.close); - - await verifier.migrateAndValidate(db, targetVersion); - }); - } - }); - } - }); - - test('preserves existing data in migration from v1 to v2', () async { - final schema = await verifier.schemaAt(1); - - // Add some data to the users table, which only has an id column at v1 - final oldDb = v1.DatabaseAtV1(schema.newConnection()); - await oldDb.into(oldDb.users).insert(const v1.UsersCompanion(id: Value(1))); - await oldDb.close(); - - // Run the migration and verify that it adds the name column. - final db = Database(schema.newConnection()); - await verifier.migrateAndValidate(db, 2); - await db.close(); - - // Make sure the user is still here - final migratedDb = v2.DatabaseAtV2(schema.newConnection()); - final user = await migratedDb.select(migratedDb.users).getSingle(); - expect(user.id, 1); - expect(user.name, 'no name'); // default from the migration - await migratedDb.close(); - }); - - test('foreign key constraints work after upgrade from v4 to v5', () async { - final schema = await verifier.schemaAt(4); - final db = Database(schema.newConnection()); - await verifier.migrateAndValidate(db, 5); - await db.close(); - - // Test that the foreign key reference introduced in v5 works as expected. - final migratedDb = v5.DatabaseAtV5(schema.newConnection()); - // The `foreign_keys` pragma is a per-connection option and the generated - // versioned classes don't enable it by default. So, enable it manually. - await migratedDb.customStatement('pragma foreign_keys = on;'); - await migratedDb.into(migratedDb.users).insert(v5.UsersCompanion.insert()); - await migratedDb - .into(migratedDb.users) - .insert(v5.UsersCompanion.insert(nextUser: Value(1))); - - // Deleting the first user should now fail due to the constraint - await expectLater(migratedDb.users.deleteWhere((tbl) => tbl.id.equals(1)), - throwsA(isA())); - }); - - test('view works after upgrade from v4 to v5', () async { - final schema = await verifier.schemaAt(4); - - final oldDb = v4.DatabaseAtV4(schema.newConnection()); - await oldDb.batch((batch) { - batch - ..insert(oldDb.users, v4.UsersCompanion.insert(id: Value(1))) - ..insert(oldDb.users, v4.UsersCompanion.insert(id: Value(2))) - ..insert( - oldDb.groups, v4.GroupsCompanion.insert(title: 'Test', owner: 1)); - }); - await oldDb.close(); - - // Run the migration and verify that it adds the view. - final db = Database(schema.newConnection()); - await verifier.migrateAndValidate(db, 5); - await db.close(); - - // Make sure the view works! - final migratedDb = v5.DatabaseAtV5(schema.newConnection()); - final viewCount = await migratedDb.select(migratedDb.groupCount).get(); - - expect( - viewCount, - contains(isA() - .having((e) => e.id, 'id', 1) - .having((e) => e.groupCount, 'groupCount', 1))); - expect( - viewCount, - contains(isA() - .having((e) => e.id, 'id', 2) - .having((e) => e.groupCount, 'groupCount', 0))); - await migratedDb.close(); - }); -} From d85fe11f9e885388140b30b8ab0ffff1e9a8d283 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Sun, 6 Oct 2024 08:40:21 -0400 Subject: [PATCH 09/25] lints --- drift_dev/lib/api/migrations.dart | 12 +++--- .../lib/src/cli/commands/make_migrations.dart | 20 +++++----- drift_dev/test/cli/make_migrations_test.dart | 29 ++++---------- .../test/drift/default/migration_test.dart | 40 +++++++++---------- 4 files changed, 43 insertions(+), 58 deletions(-) diff --git a/drift_dev/lib/api/migrations.dart b/drift_dev/lib/api/migrations.dart index 09cc5dfdd..7fc74bd56 100644 --- a/drift_dev/lib/api/migrations.dart +++ b/drift_dev/lib/api/migrations.dart @@ -223,24 +223,24 @@ class InitializedSchema { Future testStepByStepigrations( {required SchemaVerifier verifier, - required OldDatabase Function(QueryExecutor) oldDbCallback, - required NewDatabase Function(QueryExecutor) newDbCallback, - required GeneratedDatabase Function(QueryExecutor) currentDbCallback, + required OldDatabase Function(QueryExecutor) createOld, + required NewDatabase Function(QueryExecutor) createNew, + required GeneratedDatabase Function(QueryExecutor) openTestedDatabase, required void Function(Batch, OldDatabase) createItems, required Future Function(NewDatabase) validateItems, required int from, required int to}) async { final schema = await verifier.schemaAt(from); - final oldDb = oldDbCallback(schema.newConnection()); + final oldDb = createOld(schema.newConnection()); await oldDb.batch((batch) => createItems(batch, oldDb)); await oldDb.close(); - final db = currentDbCallback(schema.newConnection()); + final db = openTestedDatabase(schema.newConnection()); await verifier.migrateAndValidate(db, to); await db.close(); - final newDb = newDbCallback(schema.newConnection()); + final newDb = createNew(schema.newConnection()); await validateItems(newDb); await newDb.close(); } diff --git a/drift_dev/lib/src/cli/commands/make_migrations.dart b/drift_dev/lib/src/cli/commands/make_migrations.dart index f77225ce6..93e522157 100644 --- a/drift_dev/lib/src/cli/commands/make_migrations.dart +++ b/drift_dev/lib/src/cli/commands/make_migrations.dart @@ -18,14 +18,15 @@ class MakeMigrationCommand extends DriftCommand { String get description => """ Generates migrations utilities for drift databases -### Usage +${styleBold.wrap("Usage")}: + After defining your database for the first time, run this command to save the schema. When you are ready to make changes to the database, alter the schema in the database file, bump the schema version and run this command again. This will generate the following: 1. A steps file which contains a helper function to write a migration from one version to another. - Example: + Example: ${blue.wrap("class")} ${green.wrap("Database")} ${blue.wrap("extends")} ${green.wrap("_\$Database")} ${yellow.wrap("{")} ... @@ -46,7 +47,7 @@ This will generate the following: Fill the generated validation models with data that should be present in the database before and after the migration. These lists will be imported in the test file to validate the data integrity of the migrations - Example: + Example: // Validate that the data in the database is still correct after removing a the isAdmin column ${blue.wrap("final")} ${lightCyan.wrap("usersV1")} = ${yellow.wrap("[")} v1.${green.wrap("User")}${magenta.wrap("(")}${lightCyan.wrap("id")}: ${green.wrap("Value")}${blue.wrap("(")}1${blue.wrap(")")}, ${lightCyan.wrap("name")}: ${green.wrap("Value")}${blue.wrap("(")}${lightRed.wrap("'Simon'")}${blue.wrap(")")}, ${lightCyan.wrap("isAdmin")}: ${green.wrap("Value")}${blue.wrap("(")}${blue.wrap("true")}${blue.wrap(")")}${magenta.wrap(")")}, @@ -58,7 +59,7 @@ This will generate the following: v2.${green.wrap("User")}${magenta.wrap("(")}${lightCyan.wrap("id")}: ${green.wrap("Value")}${blue.wrap("(")}2${blue.wrap(")")}, ${lightCyan.wrap("name")}: ${green.wrap("Value")}${blue.wrap("(")}${lightRed.wrap("'John'")}${blue.wrap(")")}${magenta.wrap(")")}, ${yellow.wrap("]")}; -### Configuration +${styleBold.wrap("Configuration")}: This tool requires the database be defined in the build.yaml file. Example: @@ -111,9 +112,8 @@ targets: ..createSync(recursive: true); if (cli.project.options.databases.isEmpty) { - cli.logger.info( - 'No databases found in the build.yaml file. Check here to see how to add a database TODO: ADD LINK'); - exit(0); + cli.exit( + 'No databases found in the build.yaml file. Run `drift_dev make-migrations --help` or check the documentation for more information: https://drift.simonbinder.eu/Migrations/'); } final databaseMigrationsWriters = @@ -132,7 +132,7 @@ targets: for (var writer in databaseMigrationsWriters) { // Dump the schema files for all databases await writer.writeSchemaFile(); - if (writer.schemaVersion == 1) { + if (writer.schemas.length == 1) { continue; } // Write the step by step migration files for all databases @@ -464,8 +464,8 @@ class _MigrationWriter { test( "$dbName - migrate from v$from to v$to", () => testStepByStepigrations( - from: $from, to: $to, verifier: verifier, oldDbCallback: (e) => v$from.DatabaseAtV$from(e), - newDbCallback: (e) => v$to.DatabaseAtV$to(e), currentDbCallback: (e) => $dbClassName(e), + from: $from, to: $to, verifier: verifier, createOld: (e) => v$from.DatabaseAtV$from(e), + createNew: (e) => v$to.DatabaseAtV$to(e), openTestedDatabase: (e) => $dbClassName(e), createItems: (b, oldDb) { ${tables.map( (table) { diff --git a/drift_dev/test/cli/make_migrations_test.dart b/drift_dev/test/cli/make_migrations_test.dart index fcf93abdc..7faf8464b 100644 --- a/drift_dev/test/cli/make_migrations_test.dart +++ b/drift_dev/test/cli/make_migrations_test.dart @@ -64,28 +64,13 @@ targets: .existsSync(), true); // Test files should be created - await d - .file('app/test/drift/my_database/migration_test.dart', - IsValidDartFile(anything)) - .validate(); - await d - .file('app/test/drift/my_database/schemas/schema.dart', - IsValidDartFile(anything)) - .validate(); - - await d - .file('app/test/drift/my_database/schemas/schema_v1.dart', - IsValidDartFile(anything)) - .validate(); - await d - .file('app/test/drift/my_database/schemas/schema_v2.dart', - IsValidDartFile(anything)) - .validate(); - await d - .file('app/test/drift/my_database/validation/v1_to_v2.dart', - IsValidDartFile(anything)) - .validate(); - + await d.dir('app/test/drift/my_database', [ + d.file('migration_test.dart', IsValidDartFile(anything)), + d.file('schemas/schema.dart', IsValidDartFile(anything)), + d.file('schemas/schema_v1.dart', IsValidDartFile(anything)), + d.file('schemas/schema_v2.dart', IsValidDartFile(anything)), + d.file('validation/v1_to_v2.dart', IsValidDartFile(anything)), + ]).validate(); // Steps file should be created await d .file('app/lib/db.steps.dart', IsValidDartFile(anything)) diff --git a/examples/migrations_example/test/drift/default/migration_test.dart b/examples/migrations_example/test/drift/default/migration_test.dart index 4842004d8..6d4328069 100644 --- a/examples/migrations_example/test/drift/default/migration_test.dart +++ b/examples/migrations_example/test/drift/default/migration_test.dart @@ -39,8 +39,8 @@ void main() { test( "default - migrate from v1 to v2", () => testStepByStepigrations( - from: 1, to: 2, verifier: verifier, oldDbCallback: (e) => v1.DatabaseAtV1(e), - newDbCallback: (e) => v2.DatabaseAtV2(e), currentDbCallback: (e) => Database(e), + from: 1, to: 2, verifier: verifier, createOld: (e) => v1.DatabaseAtV1(e), + createNew: (e) => v2.DatabaseAtV2(e), openTestedDatabase: (e) => Database(e), createItems: (b, oldDb) { b.insertAll(oldDb.users, v1_to_v2.usersV1); }, @@ -53,8 +53,8 @@ void main() { test( "default - migrate from v2 to v3", () => testStepByStepigrations( - from: 2, to: 3, verifier: verifier, oldDbCallback: (e) => v2.DatabaseAtV2(e), - newDbCallback: (e) => v3.DatabaseAtV3(e), currentDbCallback: (e) => Database(e), + from: 2, to: 3, verifier: verifier, createOld: (e) => v2.DatabaseAtV2(e), + createNew: (e) => v3.DatabaseAtV3(e), openTestedDatabase: (e) => Database(e), createItems: (b, oldDb) { b.insertAll(oldDb.users, v2_to_v3.usersV2); }, @@ -67,8 +67,8 @@ test( test( "default - migrate from v3 to v4", () => testStepByStepigrations( - from: 3, to: 4, verifier: verifier, oldDbCallback: (e) => v3.DatabaseAtV3(e), - newDbCallback: (e) => v4.DatabaseAtV4(e), currentDbCallback: (e) => Database(e), + from: 3, to: 4, verifier: verifier, createOld: (e) => v3.DatabaseAtV3(e), + createNew: (e) => v4.DatabaseAtV4(e), openTestedDatabase: (e) => Database(e), createItems: (b, oldDb) { b.insertAll(oldDb.users, v3_to_v4.usersV3); b.insertAll(oldDb.groups, v3_to_v4.groupsV3); @@ -83,8 +83,8 @@ expect(v3_to_v4.groupsV4, await newDb.select(newDb.groups).get()); test( "default - migrate from v4 to v5", () => testStepByStepigrations( - from: 4, to: 5, verifier: verifier, oldDbCallback: (e) => v4.DatabaseAtV4(e), - newDbCallback: (e) => v5.DatabaseAtV5(e), currentDbCallback: (e) => Database(e), + from: 4, to: 5, verifier: verifier, createOld: (e) => v4.DatabaseAtV4(e), + createNew: (e) => v5.DatabaseAtV5(e), openTestedDatabase: (e) => Database(e), createItems: (b, oldDb) { b.insertAll(oldDb.users, v4_to_v5.usersV4); b.insertAll(oldDb.groups, v4_to_v5.groupsV4); @@ -99,8 +99,8 @@ expect(v4_to_v5.groupsV5, await newDb.select(newDb.groups).get()); test( "default - migrate from v5 to v6", () => testStepByStepigrations( - from: 5, to: 6, verifier: verifier, oldDbCallback: (e) => v5.DatabaseAtV5(e), - newDbCallback: (e) => v6.DatabaseAtV6(e), currentDbCallback: (e) => Database(e), + from: 5, to: 6, verifier: verifier, createOld: (e) => v5.DatabaseAtV5(e), + createNew: (e) => v6.DatabaseAtV6(e), openTestedDatabase: (e) => Database(e), createItems: (b, oldDb) { b.insertAll(oldDb.users, v5_to_v6.usersV5); b.insertAll(oldDb.groups, v5_to_v6.groupsV5); @@ -115,8 +115,8 @@ expect(v5_to_v6.groupsV6, await newDb.select(newDb.groups).get()); test( "default - migrate from v6 to v7", () => testStepByStepigrations( - from: 6, to: 7, verifier: verifier, oldDbCallback: (e) => v6.DatabaseAtV6(e), - newDbCallback: (e) => v7.DatabaseAtV7(e), currentDbCallback: (e) => Database(e), + from: 6, to: 7, verifier: verifier, createOld: (e) => v6.DatabaseAtV6(e), + createNew: (e) => v7.DatabaseAtV7(e), openTestedDatabase: (e) => Database(e), createItems: (b, oldDb) { b.insertAll(oldDb.users, v6_to_v7.usersV6); b.insertAll(oldDb.groups, v6_to_v7.groupsV6); @@ -131,8 +131,8 @@ expect(v6_to_v7.groupsV7, await newDb.select(newDb.groups).get()); test( "default - migrate from v7 to v8", () => testStepByStepigrations( - from: 7, to: 8, verifier: verifier, oldDbCallback: (e) => v7.DatabaseAtV7(e), - newDbCallback: (e) => v8.DatabaseAtV8(e), currentDbCallback: (e) => Database(e), + from: 7, to: 8, verifier: verifier, createOld: (e) => v7.DatabaseAtV7(e), + createNew: (e) => v8.DatabaseAtV8(e), openTestedDatabase: (e) => Database(e), createItems: (b, oldDb) { b.insertAll(oldDb.users, v7_to_v8.usersV7); b.insertAll(oldDb.groups, v7_to_v8.groupsV7); @@ -149,8 +149,8 @@ expect(v7_to_v8.notesV8, await newDb.select(newDb.notes).get()); test( "default - migrate from v8 to v9", () => testStepByStepigrations( - from: 8, to: 9, verifier: verifier, oldDbCallback: (e) => v8.DatabaseAtV8(e), - newDbCallback: (e) => v9.DatabaseAtV9(e), currentDbCallback: (e) => Database(e), + from: 8, to: 9, verifier: verifier, createOld: (e) => v8.DatabaseAtV8(e), + createNew: (e) => v9.DatabaseAtV9(e), openTestedDatabase: (e) => Database(e), createItems: (b, oldDb) { b.insertAll(oldDb.users, v8_to_v9.usersV8); b.insertAll(oldDb.groups, v8_to_v9.groupsV8); @@ -167,8 +167,8 @@ expect(v8_to_v9.notesV9, await newDb.select(newDb.notes).get()); test( "default - migrate from v9 to v10", () => testStepByStepigrations( - from: 9, to: 10, verifier: verifier, oldDbCallback: (e) => v9.DatabaseAtV9(e), - newDbCallback: (e) => v10.DatabaseAtV10(e), currentDbCallback: (e) => Database(e), + from: 9, to: 10, verifier: verifier, createOld: (e) => v9.DatabaseAtV9(e), + createNew: (e) => v10.DatabaseAtV10(e), openTestedDatabase: (e) => Database(e), createItems: (b, oldDb) { b.insertAll(oldDb.users, v9_to_v10.usersV9); b.insertAll(oldDb.groups, v9_to_v10.groupsV9); @@ -185,8 +185,8 @@ expect(v9_to_v10.notesV10, await newDb.select(newDb.notes).get()); test( "default - migrate from v10 to v11", () => testStepByStepigrations( - from: 10, to: 11, verifier: verifier, oldDbCallback: (e) => v10.DatabaseAtV10(e), - newDbCallback: (e) => v11.DatabaseAtV11(e), currentDbCallback: (e) => Database(e), + from: 10, to: 11, verifier: verifier, createOld: (e) => v10.DatabaseAtV10(e), + createNew: (e) => v11.DatabaseAtV11(e), openTestedDatabase: (e) => Database(e), createItems: (b, oldDb) { b.insertAll(oldDb.users, v10_to_v11.usersV10); b.insertAll(oldDb.groups, v10_to_v11.groupsV10); From 0ed4603f4fd4f17c0709534fc212215cece56b49 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Sun, 6 Oct 2024 08:43:14 -0400 Subject: [PATCH 10/25] add redirects --- docs/docs/_redirects | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/docs/_redirects b/docs/docs/_redirects index 7a3efa696..3e159efc8 100644 --- a/docs/docs/_redirects +++ b/docs/docs/_redirects @@ -1,3 +1,5 @@ +/docs/migrations/#tips /Migrations/api/#general-tips +/docs/migrations/#verifying-a-database-schema-at-runtime /Migrations/tests/#verifying-a-database-schema-at-runtime /docs/other-engines/vm/ /Platforms/vm/ /docs/ / /docs/examples/tracing/ /Examples/tracing/ @@ -51,3 +53,4 @@ /docs/platforms/postgres/ /Platforms/postgres/ /docs/advanced-features/daos/ /dart_api/daos/ /api/* https://pub.dev/documentation/drift/latest/index.html + From 02990dd411cb115747539b93d5dd7700c06a5649 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Sun, 6 Oct 2024 09:17:09 -0400 Subject: [PATCH 11/25] fix github ci --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3b9c476ab..396d27bb2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -327,8 +327,8 @@ jobs: working-directory: examples/migrations_example run: | dart run build_runner build --delete-conflicting-outputs - dart run drift_dev schema generate drift_migrations/ test/generated/ --data-classes --companions - dart run drift_dev schema generate drift_migrations/ lib/src/generated + dart run drift_dev schema generate drift_schemas/default test/generated/ --data-classes --companions + dart run drift_dev schema generate drift_schemas/ lib - name: Test working-directory: examples/migrations_example run: dart test From b4a0285c9586ca8db6572f12035f71fbcee65311 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Tue, 8 Oct 2024 19:49:33 -0400 Subject: [PATCH 12/25] add formatter --- drift_dev/lib/src/cli/commands/make_migrations.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/drift_dev/lib/src/cli/commands/make_migrations.dart b/drift_dev/lib/src/cli/commands/make_migrations.dart index 93e522157..653f74c14 100644 --- a/drift_dev/lib/src/cli/commands/make_migrations.dart +++ b/drift_dev/lib/src/cli/commands/make_migrations.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:dart_style/dart_style.dart'; import 'package:drift_dev/src/analysis/results/results.dart'; import 'package:drift_dev/src/cli/cli.dart'; import 'package:drift_dev/src/cli/commands/schema.dart'; @@ -190,7 +191,7 @@ class _DatabaseMigrationWriter { /// Write all the files to the disk void flush() { for (final MapEntry(key: file, value: content) in writeTasks.entries) { - file.writeAsStringSync(content); + file.writeAsStringSync(DartFormatter().format(content)); } writeTasks.clear(); } @@ -290,7 +291,7 @@ class _DatabaseMigrationWriter { if (!schemaFile.existsSync()) { cli.logger .info('$dbName: Creating schema file for version $schemaVersion'); - schemaFile.writeAsStringSync(content); + schemaFile.writeAsStringSync(DartFormatter().format(content)); // Re-parse the schema to include the newly created schema file schemas = await parseSchema(schemaDir); } else if (schemaFile.readAsStringSync() != content) { From 1c25b507e653a2b338ebd982d1edd4bd0d84d5b2 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Tue, 8 Oct 2024 19:52:02 -0400 Subject: [PATCH 13/25] fix typo --- drift_dev/lib/api/migrations.dart | 2 +- .../lib/src/cli/commands/make_migrations.dart | 2 +- .../test/drift/default/migration_test.dart | 342 ++++++++++-------- 3 files changed, 189 insertions(+), 157 deletions(-) diff --git a/drift_dev/lib/api/migrations.dart b/drift_dev/lib/api/migrations.dart index 7fc74bd56..96daa3502 100644 --- a/drift_dev/lib/api/migrations.dart +++ b/drift_dev/lib/api/migrations.dart @@ -220,7 +220,7 @@ class InitializedSchema { /// 2. Insert data into the database /// 3. Migrate the database to a target version /// 4. Validate that the data is valid after the migration -Future testStepByStepigrations testStepByStepMigrations( {required SchemaVerifier verifier, required OldDatabase Function(QueryExecutor) createOld, diff --git a/drift_dev/lib/src/cli/commands/make_migrations.dart b/drift_dev/lib/src/cli/commands/make_migrations.dart index 653f74c14..cc4e988cd 100644 --- a/drift_dev/lib/src/cli/commands/make_migrations.dart +++ b/drift_dev/lib/src/cli/commands/make_migrations.dart @@ -464,7 +464,7 @@ class _MigrationWriter { final test = """ test( "$dbName - migrate from v$from to v$to", - () => testStepByStepigrations( + () => testStepByStepMigrations( from: $from, to: $to, verifier: verifier, createOld: (e) => v$from.DatabaseAtV$from(e), createNew: (e) => v$to.DatabaseAtV$to(e), openTestedDatabase: (e) => $dbClassName(e), createItems: (b, oldDb) { diff --git a/examples/migrations_example/test/drift/default/migration_test.dart b/examples/migrations_example/test/drift/default/migration_test.dart index 6d4328069..c499112a9 100644 --- a/examples/migrations_example/test/drift/default/migration_test.dart +++ b/examples/migrations_example/test/drift/default/migration_test.dart @@ -37,168 +37,200 @@ void main() { }); test( - "default - migrate from v1 to v2", - () => testStepByStepigrations( - from: 1, to: 2, verifier: verifier, createOld: (e) => v1.DatabaseAtV1(e), - createNew: (e) => v2.DatabaseAtV2(e), openTestedDatabase: (e) => Database(e), - createItems: (b, oldDb) { - b.insertAll(oldDb.users, v1_to_v2.usersV1); - }, - validateItems: (newDb) async { - expect(v1_to_v2.usersV2, await newDb.select(newDb.users).get()); - }, - ) -); + "default - migrate from v1 to v2", + () => testStepByStepMigrations( + from: 1, + to: 2, + verifier: verifier, + createOld: (e) => v1.DatabaseAtV1(e), + createNew: (e) => v2.DatabaseAtV2(e), + openTestedDatabase: (e) => Database(e), + createItems: (b, oldDb) { + b.insertAll(oldDb.users, v1_to_v2.usersV1); + }, + validateItems: (newDb) async { + expect(v1_to_v2.usersV2, await newDb.select(newDb.users).get()); + }, + )); -test( - "default - migrate from v2 to v3", - () => testStepByStepigrations( - from: 2, to: 3, verifier: verifier, createOld: (e) => v2.DatabaseAtV2(e), - createNew: (e) => v3.DatabaseAtV3(e), openTestedDatabase: (e) => Database(e), - createItems: (b, oldDb) { - b.insertAll(oldDb.users, v2_to_v3.usersV2); - }, - validateItems: (newDb) async { - expect(v2_to_v3.usersV3, await newDb.select(newDb.users).get()); - }, - ) -); - -test( - "default - migrate from v3 to v4", - () => testStepByStepigrations( - from: 3, to: 4, verifier: verifier, createOld: (e) => v3.DatabaseAtV3(e), - createNew: (e) => v4.DatabaseAtV4(e), openTestedDatabase: (e) => Database(e), - createItems: (b, oldDb) { - b.insertAll(oldDb.users, v3_to_v4.usersV3); -b.insertAll(oldDb.groups, v3_to_v4.groupsV3); - }, - validateItems: (newDb) async { - expect(v3_to_v4.usersV4, await newDb.select(newDb.users).get()); -expect(v3_to_v4.groupsV4, await newDb.select(newDb.groups).get()); - }, - ) -); - -test( - "default - migrate from v4 to v5", - () => testStepByStepigrations( - from: 4, to: 5, verifier: verifier, createOld: (e) => v4.DatabaseAtV4(e), - createNew: (e) => v5.DatabaseAtV5(e), openTestedDatabase: (e) => Database(e), - createItems: (b, oldDb) { - b.insertAll(oldDb.users, v4_to_v5.usersV4); -b.insertAll(oldDb.groups, v4_to_v5.groupsV4); - }, - validateItems: (newDb) async { - expect(v4_to_v5.usersV5, await newDb.select(newDb.users).get()); -expect(v4_to_v5.groupsV5, await newDb.select(newDb.groups).get()); - }, - ) -); + test( + "default - migrate from v2 to v3", + () => testStepByStepMigrations( + from: 2, + to: 3, + verifier: verifier, + createOld: (e) => v2.DatabaseAtV2(e), + createNew: (e) => v3.DatabaseAtV3(e), + openTestedDatabase: (e) => Database(e), + createItems: (b, oldDb) { + b.insertAll(oldDb.users, v2_to_v3.usersV2); + }, + validateItems: (newDb) async { + expect(v2_to_v3.usersV3, await newDb.select(newDb.users).get()); + }, + )); -test( - "default - migrate from v5 to v6", - () => testStepByStepigrations( - from: 5, to: 6, verifier: verifier, createOld: (e) => v5.DatabaseAtV5(e), - createNew: (e) => v6.DatabaseAtV6(e), openTestedDatabase: (e) => Database(e), - createItems: (b, oldDb) { - b.insertAll(oldDb.users, v5_to_v6.usersV5); -b.insertAll(oldDb.groups, v5_to_v6.groupsV5); - }, - validateItems: (newDb) async { - expect(v5_to_v6.usersV6, await newDb.select(newDb.users).get()); -expect(v5_to_v6.groupsV6, await newDb.select(newDb.groups).get()); - }, - ) -); + test( + "default - migrate from v3 to v4", + () => testStepByStepMigrations( + from: 3, + to: 4, + verifier: verifier, + createOld: (e) => v3.DatabaseAtV3(e), + createNew: (e) => v4.DatabaseAtV4(e), + openTestedDatabase: (e) => Database(e), + createItems: (b, oldDb) { + b.insertAll(oldDb.users, v3_to_v4.usersV3); + b.insertAll(oldDb.groups, v3_to_v4.groupsV3); + }, + validateItems: (newDb) async { + expect(v3_to_v4.usersV4, await newDb.select(newDb.users).get()); + expect(v3_to_v4.groupsV4, await newDb.select(newDb.groups).get()); + }, + )); -test( - "default - migrate from v6 to v7", - () => testStepByStepigrations( - from: 6, to: 7, verifier: verifier, createOld: (e) => v6.DatabaseAtV6(e), - createNew: (e) => v7.DatabaseAtV7(e), openTestedDatabase: (e) => Database(e), - createItems: (b, oldDb) { - b.insertAll(oldDb.users, v6_to_v7.usersV6); -b.insertAll(oldDb.groups, v6_to_v7.groupsV6); - }, - validateItems: (newDb) async { - expect(v6_to_v7.usersV7, await newDb.select(newDb.users).get()); -expect(v6_to_v7.groupsV7, await newDb.select(newDb.groups).get()); - }, - ) -); + test( + "default - migrate from v4 to v5", + () => testStepByStepMigrations( + from: 4, + to: 5, + verifier: verifier, + createOld: (e) => v4.DatabaseAtV4(e), + createNew: (e) => v5.DatabaseAtV5(e), + openTestedDatabase: (e) => Database(e), + createItems: (b, oldDb) { + b.insertAll(oldDb.users, v4_to_v5.usersV4); + b.insertAll(oldDb.groups, v4_to_v5.groupsV4); + }, + validateItems: (newDb) async { + expect(v4_to_v5.usersV5, await newDb.select(newDb.users).get()); + expect(v4_to_v5.groupsV5, await newDb.select(newDb.groups).get()); + }, + )); -test( - "default - migrate from v7 to v8", - () => testStepByStepigrations( - from: 7, to: 8, verifier: verifier, createOld: (e) => v7.DatabaseAtV7(e), - createNew: (e) => v8.DatabaseAtV8(e), openTestedDatabase: (e) => Database(e), - createItems: (b, oldDb) { - b.insertAll(oldDb.users, v7_to_v8.usersV7); -b.insertAll(oldDb.groups, v7_to_v8.groupsV7); -b.insertAll(oldDb.notes, v7_to_v8.notesV7); - }, - validateItems: (newDb) async { - expect(v7_to_v8.usersV8, await newDb.select(newDb.users).get()); -expect(v7_to_v8.groupsV8, await newDb.select(newDb.groups).get()); -expect(v7_to_v8.notesV8, await newDb.select(newDb.notes).get()); - }, - ) -); + test( + "default - migrate from v5 to v6", + () => testStepByStepMigrations( + from: 5, + to: 6, + verifier: verifier, + createOld: (e) => v5.DatabaseAtV5(e), + createNew: (e) => v6.DatabaseAtV6(e), + openTestedDatabase: (e) => Database(e), + createItems: (b, oldDb) { + b.insertAll(oldDb.users, v5_to_v6.usersV5); + b.insertAll(oldDb.groups, v5_to_v6.groupsV5); + }, + validateItems: (newDb) async { + expect(v5_to_v6.usersV6, await newDb.select(newDb.users).get()); + expect(v5_to_v6.groupsV6, await newDb.select(newDb.groups).get()); + }, + )); -test( - "default - migrate from v8 to v9", - () => testStepByStepigrations( - from: 8, to: 9, verifier: verifier, createOld: (e) => v8.DatabaseAtV8(e), - createNew: (e) => v9.DatabaseAtV9(e), openTestedDatabase: (e) => Database(e), - createItems: (b, oldDb) { - b.insertAll(oldDb.users, v8_to_v9.usersV8); -b.insertAll(oldDb.groups, v8_to_v9.groupsV8); -b.insertAll(oldDb.notes, v8_to_v9.notesV8); - }, - validateItems: (newDb) async { - expect(v8_to_v9.usersV9, await newDb.select(newDb.users).get()); -expect(v8_to_v9.groupsV9, await newDb.select(newDb.groups).get()); -expect(v8_to_v9.notesV9, await newDb.select(newDb.notes).get()); - }, - ) -); + test( + "default - migrate from v6 to v7", + () => testStepByStepMigrations( + from: 6, + to: 7, + verifier: verifier, + createOld: (e) => v6.DatabaseAtV6(e), + createNew: (e) => v7.DatabaseAtV7(e), + openTestedDatabase: (e) => Database(e), + createItems: (b, oldDb) { + b.insertAll(oldDb.users, v6_to_v7.usersV6); + b.insertAll(oldDb.groups, v6_to_v7.groupsV6); + }, + validateItems: (newDb) async { + expect(v6_to_v7.usersV7, await newDb.select(newDb.users).get()); + expect(v6_to_v7.groupsV7, await newDb.select(newDb.groups).get()); + }, + )); -test( - "default - migrate from v9 to v10", - () => testStepByStepigrations( - from: 9, to: 10, verifier: verifier, createOld: (e) => v9.DatabaseAtV9(e), - createNew: (e) => v10.DatabaseAtV10(e), openTestedDatabase: (e) => Database(e), - createItems: (b, oldDb) { - b.insertAll(oldDb.users, v9_to_v10.usersV9); -b.insertAll(oldDb.groups, v9_to_v10.groupsV9); -b.insertAll(oldDb.notes, v9_to_v10.notesV9); - }, - validateItems: (newDb) async { - expect(v9_to_v10.usersV10, await newDb.select(newDb.users).get()); -expect(v9_to_v10.groupsV10, await newDb.select(newDb.groups).get()); -expect(v9_to_v10.notesV10, await newDb.select(newDb.notes).get()); - }, - ) -); + test( + "default - migrate from v7 to v8", + () => testStepByStepMigrations( + from: 7, + to: 8, + verifier: verifier, + createOld: (e) => v7.DatabaseAtV7(e), + createNew: (e) => v8.DatabaseAtV8(e), + openTestedDatabase: (e) => Database(e), + createItems: (b, oldDb) { + b.insertAll(oldDb.users, v7_to_v8.usersV7); + b.insertAll(oldDb.groups, v7_to_v8.groupsV7); + b.insertAll(oldDb.notes, v7_to_v8.notesV7); + }, + validateItems: (newDb) async { + expect(v7_to_v8.usersV8, await newDb.select(newDb.users).get()); + expect(v7_to_v8.groupsV8, await newDb.select(newDb.groups).get()); + expect(v7_to_v8.notesV8, await newDb.select(newDb.notes).get()); + }, + )); -test( - "default - migrate from v10 to v11", - () => testStepByStepigrations( - from: 10, to: 11, verifier: verifier, createOld: (e) => v10.DatabaseAtV10(e), - createNew: (e) => v11.DatabaseAtV11(e), openTestedDatabase: (e) => Database(e), - createItems: (b, oldDb) { - b.insertAll(oldDb.users, v10_to_v11.usersV10); -b.insertAll(oldDb.groups, v10_to_v11.groupsV10); -b.insertAll(oldDb.notes, v10_to_v11.notesV10); - }, - validateItems: (newDb) async { - expect(v10_to_v11.usersV11, await newDb.select(newDb.users).get()); -expect(v10_to_v11.groupsV11, await newDb.select(newDb.groups).get()); -expect(v10_to_v11.notesV11, await newDb.select(newDb.notes).get()); - }, - ) -); + test( + "default - migrate from v8 to v9", + () => testStepByStepMigrations( + from: 8, + to: 9, + verifier: verifier, + createOld: (e) => v8.DatabaseAtV8(e), + createNew: (e) => v9.DatabaseAtV9(e), + openTestedDatabase: (e) => Database(e), + createItems: (b, oldDb) { + b.insertAll(oldDb.users, v8_to_v9.usersV8); + b.insertAll(oldDb.groups, v8_to_v9.groupsV8); + b.insertAll(oldDb.notes, v8_to_v9.notesV8); + }, + validateItems: (newDb) async { + expect(v8_to_v9.usersV9, await newDb.select(newDb.users).get()); + expect(v8_to_v9.groupsV9, await newDb.select(newDb.groups).get()); + expect(v8_to_v9.notesV9, await newDb.select(newDb.notes).get()); + }, + )); + test( + "default - migrate from v9 to v10", + () => testStepByStepMigrations( + from: 9, + to: 10, + verifier: verifier, + createOld: (e) => v9.DatabaseAtV9(e), + createNew: (e) => v10.DatabaseAtV10(e), + openTestedDatabase: (e) => Database(e), + createItems: (b, oldDb) { + b.insertAll(oldDb.users, v9_to_v10.usersV9); + b.insertAll(oldDb.groups, v9_to_v10.groupsV9); + b.insertAll(oldDb.notes, v9_to_v10.notesV9); + }, + validateItems: (newDb) async { + expect(v9_to_v10.usersV10, await newDb.select(newDb.users).get()); + expect( + v9_to_v10.groupsV10, await newDb.select(newDb.groups).get()); + expect(v9_to_v10.notesV10, await newDb.select(newDb.notes).get()); + }, + )); + test( + "default - migrate from v10 to v11", + () => testStepByStepMigrations( + from: 10, + to: 11, + verifier: verifier, + createOld: (e) => v10.DatabaseAtV10(e), + createNew: (e) => v11.DatabaseAtV11(e), + openTestedDatabase: (e) => Database(e), + createItems: (b, oldDb) { + b.insertAll(oldDb.users, v10_to_v11.usersV10); + b.insertAll(oldDb.groups, v10_to_v11.groupsV10); + b.insertAll(oldDb.notes, v10_to_v11.notesV10); + }, + validateItems: (newDb) async { + expect( + v10_to_v11.usersV11, await newDb.select(newDb.users).get()); + expect( + v10_to_v11.groupsV11, await newDb.select(newDb.groups).get()); + expect( + v10_to_v11.notesV11, await newDb.select(newDb.notes).get()); + }, + )); } From 83b3feeb0f4fe08e11d5bd66b697850372f13a0c Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Tue, 8 Oct 2024 19:53:26 -0400 Subject: [PATCH 14/25] rename to _MigrationTestEmitter --- drift_dev/lib/src/cli/commands/make_migrations.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/drift_dev/lib/src/cli/commands/make_migrations.dart b/drift_dev/lib/src/cli/commands/make_migrations.dart index cc4e988cd..e9cea3991 100644 --- a/drift_dev/lib/src/cli/commands/make_migrations.dart +++ b/drift_dev/lib/src/cli/commands/make_migrations.dart @@ -120,7 +120,7 @@ targets: final databaseMigrationsWriters = await Future.wait(cli.project.options.databases.entries.map( (entry) async { - final writer = await _DatabaseMigrationWriter.create( + final writer = await _MigrationTestEmitter.create( cli: cli, rootSchemaDir: rootSchemaDir, rootTestDir: rootTestDir, @@ -149,7 +149,7 @@ targets: } } -class _DatabaseMigrationWriter { +class _MigrationTestEmitter { final DriftDevCli cli; final Directory rootSchemaDir; final Directory rootTestDir; @@ -202,7 +202,7 @@ class _DatabaseMigrationWriter { /// Migration writer for each migration List<_MigrationWriter> get migrations => _MigrationWriter.fromSchema(schemas); - _DatabaseMigrationWriter({ + _MigrationTestEmitter({ required this.cli, required this.rootSchemaDir, required this.rootTestDir, @@ -219,7 +219,7 @@ class _DatabaseMigrationWriter { required this.schemas, }); - static Future<_DatabaseMigrationWriter> create( + static Future<_MigrationTestEmitter> create( {required DriftDevCli cli, required Directory rootSchemaDir, required Directory rootTestDir, @@ -257,7 +257,7 @@ class _DatabaseMigrationWriter { cli.exit('Could not read database class from the "$dbName" database.'); } final schemas = await parseSchema(schemaDir); - return _DatabaseMigrationWriter( + return _MigrationTestEmitter( cli: cli, rootSchemaDir: rootSchemaDir, rootTestDir: rootTestDir, From d6398cfa93622de3fe05767303ae8bdaec4e448a Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Tue, 8 Oct 2024 23:31:23 -0400 Subject: [PATCH 15/25] only format dart files (duh) --- drift_dev/lib/src/cli/commands/make_migrations.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/drift_dev/lib/src/cli/commands/make_migrations.dart b/drift_dev/lib/src/cli/commands/make_migrations.dart index e9cea3991..cf71fbd00 100644 --- a/drift_dev/lib/src/cli/commands/make_migrations.dart +++ b/drift_dev/lib/src/cli/commands/make_migrations.dart @@ -190,8 +190,11 @@ class _MigrationTestEmitter { /// Write all the files to the disk void flush() { - for (final MapEntry(key: file, value: content) in writeTasks.entries) { - file.writeAsStringSync(DartFormatter().format(content)); + for (var MapEntry(key: file, value: content) in writeTasks.entries) { + if (file.path.endsWith('.dart')) { + content = DartFormatter().format(content); + } + file.writeAsStringSync(content); } writeTasks.clear(); } @@ -291,7 +294,7 @@ class _MigrationTestEmitter { if (!schemaFile.existsSync()) { cli.logger .info('$dbName: Creating schema file for version $schemaVersion'); - schemaFile.writeAsStringSync(DartFormatter().format(content)); + schemaFile.writeAsStringSync(content); // Re-parse the schema to include the newly created schema file schemas = await parseSchema(schemaDir); } else if (schemaFile.readAsStringSync() != content) { From 0f33ffd607244d9fa24f784bbf80ec1191c97dca Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Wed, 9 Oct 2024 19:25:45 -0400 Subject: [PATCH 16/25] rename `testWithDataIntegrity` and disable foreign key constraints --- drift_dev/lib/api/migrations.dart | 60 +++++++++---------- .../lib/src/cli/commands/make_migrations.dart | 2 +- .../test/drift/default/migration_test.dart | 20 +++---- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/drift_dev/lib/api/migrations.dart b/drift_dev/lib/api/migrations.dart index 96daa3502..e6baf6b51 100644 --- a/drift_dev/lib/api/migrations.dart +++ b/drift_dev/lib/api/migrations.dart @@ -73,6 +73,36 @@ abstract class SchemaVerifier { /// expected exist. Future migrateAndValidate(GeneratedDatabase db, int expectedVersion, {bool validateDropped = false}); + + /// Utility function used by generated tests to verify that migrations + /// modify the database schema as expected. + /// + /// Foreign key constraints are disabled for this operation. + Future testWithDataIntegrity( + {required SchemaVerifier verifier, + required OldDatabase Function(QueryExecutor) createOld, + required NewDatabase Function(QueryExecutor) createNew, + required GeneratedDatabase Function(QueryExecutor) openTestedDatabase, + required void Function(Batch, OldDatabase) createItems, + required Future Function(NewDatabase) validateItems, + required int oldVersion, + required int newVersion}) async { + final schema = await verifier.schemaAt(oldVersion); + + final oldDb = createOld(schema.newConnection()); + await oldDb.customStatement('PRAGMA foreign_keys = OFF'); + await oldDb.batch((batch) => createItems(batch, oldDb)); + await oldDb.close(); + + final db = openTestedDatabase(schema.newConnection()); + await verifier.migrateAndValidate(db, newVersion); + await db.close(); + + final newDb = createNew(schema.newConnection()); + await validateItems(newDb); + await newDb.close(); + } } /// Utilities verifying that the current schema of the database matches what @@ -214,33 +244,3 @@ class InitializedSchema { /// ``` DatabaseConnection newConnection() => _createConnection(); } - -/// Utility function used by generated tests to: -/// 1. Create a database at a specific version -/// 2. Insert data into the database -/// 3. Migrate the database to a target version -/// 4. Validate that the data is valid after the migration -Future testStepByStepMigrations( - {required SchemaVerifier verifier, - required OldDatabase Function(QueryExecutor) createOld, - required NewDatabase Function(QueryExecutor) createNew, - required GeneratedDatabase Function(QueryExecutor) openTestedDatabase, - required void Function(Batch, OldDatabase) createItems, - required Future Function(NewDatabase) validateItems, - required int from, - required int to}) async { - final schema = await verifier.schemaAt(from); - - final oldDb = createOld(schema.newConnection()); - await oldDb.batch((batch) => createItems(batch, oldDb)); - await oldDb.close(); - - final db = openTestedDatabase(schema.newConnection()); - await verifier.migrateAndValidate(db, to); - await db.close(); - - final newDb = createNew(schema.newConnection()); - await validateItems(newDb); - await newDb.close(); -} diff --git a/drift_dev/lib/src/cli/commands/make_migrations.dart b/drift_dev/lib/src/cli/commands/make_migrations.dart index cf71fbd00..68dc8ce92 100644 --- a/drift_dev/lib/src/cli/commands/make_migrations.dart +++ b/drift_dev/lib/src/cli/commands/make_migrations.dart @@ -467,7 +467,7 @@ class _MigrationWriter { final test = """ test( "$dbName - migrate from v$from to v$to", - () => testStepByStepMigrations( + () => testWithDataIntegrity( from: $from, to: $to, verifier: verifier, createOld: (e) => v$from.DatabaseAtV$from(e), createNew: (e) => v$to.DatabaseAtV$to(e), openTestedDatabase: (e) => $dbClassName(e), createItems: (b, oldDb) { diff --git a/examples/migrations_example/test/drift/default/migration_test.dart b/examples/migrations_example/test/drift/default/migration_test.dart index c499112a9..900a59528 100644 --- a/examples/migrations_example/test/drift/default/migration_test.dart +++ b/examples/migrations_example/test/drift/default/migration_test.dart @@ -38,7 +38,7 @@ void main() { test( "default - migrate from v1 to v2", - () => testStepByStepMigrations( + () => testWithDataIntegrity( from: 1, to: 2, verifier: verifier, @@ -55,7 +55,7 @@ void main() { test( "default - migrate from v2 to v3", - () => testStepByStepMigrations( + () => testWithDataIntegrity( from: 2, to: 3, verifier: verifier, @@ -72,7 +72,7 @@ void main() { test( "default - migrate from v3 to v4", - () => testStepByStepMigrations( + () => testWithDataIntegrity( from: 3, to: 4, verifier: verifier, @@ -91,7 +91,7 @@ void main() { test( "default - migrate from v4 to v5", - () => testStepByStepMigrations( + () => testWithDataIntegrity( from: 4, to: 5, verifier: verifier, @@ -110,7 +110,7 @@ void main() { test( "default - migrate from v5 to v6", - () => testStepByStepMigrations( + () => testWithDataIntegrity( from: 5, to: 6, verifier: verifier, @@ -129,7 +129,7 @@ void main() { test( "default - migrate from v6 to v7", - () => testStepByStepMigrations( + () => testWithDataIntegrity( from: 6, to: 7, verifier: verifier, @@ -148,7 +148,7 @@ void main() { test( "default - migrate from v7 to v8", - () => testStepByStepMigrations( + () => testWithDataIntegrity( from: 7, to: 8, verifier: verifier, @@ -169,7 +169,7 @@ void main() { test( "default - migrate from v8 to v9", - () => testStepByStepMigrations( + () => testWithDataIntegrity( from: 8, to: 9, verifier: verifier, @@ -190,7 +190,7 @@ void main() { test( "default - migrate from v9 to v10", - () => testStepByStepMigrations( + () => testWithDataIntegrity( from: 9, to: 10, verifier: verifier, @@ -212,7 +212,7 @@ void main() { test( "default - migrate from v10 to v11", - () => testStepByStepMigrations( + () => testWithDataIntegrity( from: 10, to: 11, verifier: verifier, From e99a3e11b2195143a2145d2fe2fda781f53a8477 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Wed, 9 Oct 2024 20:39:12 -0400 Subject: [PATCH 17/25] single test --- drift_dev/lib/api/migrations.dart | 17 +- .../lib/src/cli/commands/make_migrations.dart | 174 +++++------ .../cli/commands/schema/generate_utils.dart | 12 +- .../src/services/schema/verifier_impl.dart | 27 ++ drift_dev/test/cli/make_migrations_test.dart | 7 +- .../{schemas => generated}/schema.dart | 5 +- .../{schemas => generated}/schema_v1.dart | 0 .../{schemas => generated}/schema_v10.dart | 0 .../{schemas => generated}/schema_v11.dart | 0 .../{schemas => generated}/schema_v2.dart | 0 .../{schemas => generated}/schema_v3.dart | 0 .../{schemas => generated}/schema_v4.dart | 0 .../{schemas => generated}/schema_v5.dart | 0 .../{schemas => generated}/schema_v6.dart | 0 .../{schemas => generated}/schema_v7.dart | 0 .../{schemas => generated}/schema_v8.dart | 0 .../{schemas => generated}/schema_v9.dart | 0 .../test/drift/default/migration_test.dart | 270 ++++-------------- .../drift/default/validation/v10_to_v11.dart | 17 -- .../drift/default/validation/v1_to_v2.dart | 11 - .../drift/default/validation/v2_to_v3.dart | 9 - .../drift/default/validation/v3_to_v4.dart | 13 - .../drift/default/validation/v4_to_v5.dart | 13 - .../drift/default/validation/v5_to_v6.dart | 13 - .../drift/default/validation/v6_to_v7.dart | 13 - .../drift/default/validation/v7_to_v8.dart | 17 -- .../drift/default/validation/v8_to_v9.dart | 17 -- .../drift/default/validation/v9_to_v10.dart | 17 -- 28 files changed, 182 insertions(+), 470 deletions(-) rename examples/migrations_example/test/drift/default/{schemas => generated}/schema.dart (90%) rename examples/migrations_example/test/drift/default/{schemas => generated}/schema_v1.dart (100%) rename examples/migrations_example/test/drift/default/{schemas => generated}/schema_v10.dart (100%) rename examples/migrations_example/test/drift/default/{schemas => generated}/schema_v11.dart (100%) rename examples/migrations_example/test/drift/default/{schemas => generated}/schema_v2.dart (100%) rename examples/migrations_example/test/drift/default/{schemas => generated}/schema_v3.dart (100%) rename examples/migrations_example/test/drift/default/{schemas => generated}/schema_v4.dart (100%) rename examples/migrations_example/test/drift/default/{schemas => generated}/schema_v5.dart (100%) rename examples/migrations_example/test/drift/default/{schemas => generated}/schema_v6.dart (100%) rename examples/migrations_example/test/drift/default/{schemas => generated}/schema_v7.dart (100%) rename examples/migrations_example/test/drift/default/{schemas => generated}/schema_v8.dart (100%) rename examples/migrations_example/test/drift/default/{schemas => generated}/schema_v9.dart (100%) delete mode 100644 examples/migrations_example/test/drift/default/validation/v10_to_v11.dart delete mode 100644 examples/migrations_example/test/drift/default/validation/v1_to_v2.dart delete mode 100644 examples/migrations_example/test/drift/default/validation/v2_to_v3.dart delete mode 100644 examples/migrations_example/test/drift/default/validation/v3_to_v4.dart delete mode 100644 examples/migrations_example/test/drift/default/validation/v4_to_v5.dart delete mode 100644 examples/migrations_example/test/drift/default/validation/v5_to_v6.dart delete mode 100644 examples/migrations_example/test/drift/default/validation/v6_to_v7.dart delete mode 100644 examples/migrations_example/test/drift/default/validation/v7_to_v8.dart delete mode 100644 examples/migrations_example/test/drift/default/validation/v8_to_v9.dart delete mode 100644 examples/migrations_example/test/drift/default/validation/v9_to_v10.dart diff --git a/drift_dev/lib/api/migrations.dart b/drift_dev/lib/api/migrations.dart index e6baf6b51..ec23a622e 100644 --- a/drift_dev/lib/api/migrations.dart +++ b/drift_dev/lib/api/migrations.dart @@ -87,22 +87,7 @@ abstract class SchemaVerifier { required void Function(Batch, OldDatabase) createItems, required Future Function(NewDatabase) validateItems, required int oldVersion, - required int newVersion}) async { - final schema = await verifier.schemaAt(oldVersion); - - final oldDb = createOld(schema.newConnection()); - await oldDb.customStatement('PRAGMA foreign_keys = OFF'); - await oldDb.batch((batch) => createItems(batch, oldDb)); - await oldDb.close(); - - final db = openTestedDatabase(schema.newConnection()); - await verifier.migrateAndValidate(db, newVersion); - await db.close(); - - final newDb = createNew(schema.newConnection()); - await validateItems(newDb); - await newDb.close(); - } + required int newVersion}); } /// Utilities verifying that the current schema of the database matches what diff --git a/drift_dev/lib/src/cli/commands/make_migrations.dart b/drift_dev/lib/src/cli/commands/make_migrations.dart index 68dc8ce92..09332b4e6 100644 --- a/drift_dev/lib/src/cli/commands/make_migrations.dart +++ b/drift_dev/lib/src/cli/commands/make_migrations.dart @@ -7,10 +7,12 @@ import 'package:drift_dev/src/cli/cli.dart'; import 'package:drift_dev/src/cli/commands/schema.dart'; import 'package:drift_dev/src/cli/commands/schema/generate_utils.dart'; import 'package:drift_dev/src/cli/commands/schema/steps.dart'; +import 'package:collection/collection.dart'; import 'package:drift_dev/src/services/schema/schema_files.dart'; import 'package:io/ansi.dart'; import 'package:path/path.dart' as p; +import 'package:recase/recase.dart'; class MakeMigrationCommand extends DriftCommand { MakeMigrationCommand(super.cli); @@ -143,7 +145,7 @@ targets: // Write the generated test databases await writer.writeTestDatabases(); // Write the generated test - await writer.writeTests(); + await writer.writeTest(); writer.flush(); } } @@ -168,10 +170,6 @@ class _MigrationTestEmitter { /// e.g /test/drift/my_database/generated/ final Directory testDatabasesDir; - /// The directory where the generated test utils are stored - /// e.g /test/drift/my_database/validation/ - final Directory validationModelsDir; - /// Current schema version of the database final int schemaVersion; @@ -214,7 +212,6 @@ class _MigrationTestEmitter { required this.schemaDir, required this.testDir, required this.testDatabasesDir, - required this.validationModelsDir, required this.schemaVersion, required this.dbClassName, required this.db, @@ -247,9 +244,7 @@ class _MigrationTestEmitter { ..createSync(recursive: true); final testDir = Directory(p.join(rootTestDir.path, dbName)) ..createSync(recursive: true); - final testDatabasesDir = Directory(p.join(testDir.path, 'schemas')) - ..createSync(recursive: true); - final validationModelsDir = Directory(p.join(testDir.path, 'validation')) + final testDatabasesDir = Directory(p.join(testDir.path, 'generated')) ..createSync(recursive: true); final (:db, :elements, :schemaVersion) = await cli.readElementsFromSource(dbClassFile.absolute); @@ -273,7 +268,6 @@ class _MigrationTestEmitter { driftElements: elements, dbClassName: db.definingDartClass.toString(), testDatabasesDir: testDatabasesDir, - validationModelsDir: validationModelsDir, schemaVersion: schemaVersion); } @@ -347,41 +341,31 @@ ${blue.wrap("class")} ${green.wrap(dbClassName)} ${blue.wrap("extends")} ${green GenerateUtils.generateLibraryCode(schemas.keys); } - Future writeTests() async { - final packageName = cli.project.buildConfig.packageName; - final relativeDbPath = p.relative(dbClassFile.path, - from: p.join(cli.project.directory.path, 'lib')); - - final files = []; - for (final migration in migrations) { - // Generate the validation models - final validationFile = File(p.join(validationModelsDir.path, - 'v${migration.from}_to_v${migration.to}.dart')); - if (!validationFile.existsSync()) { - files.add(validationFile); - writeTasks[validationFile] = migration.validationModelsCode; - } + Future writeTest() async { + final testFile = File(p.join(testDir.path, 'migration_test.dart')); + if (testFile.existsSync()) { + return; } - if (files.isNotEmpty) { - cli.logger.info( - '$dbName: Generated validation models in ${blue.wrap(files.map((e) => p.relative(e.path)).join(', '))}\n' - 'Fill these lists with data that should be present in the database before and after the migration.\n' - 'These lists will be used to validate that the migration was successful and that no data was lost'); + if (migrations.isEmpty) { + return; } - final stepByStepTests = migrations - .map((e) => e.testStepByStepMigrationCode(dbName, dbClassName)); + final firstMigration = + migrations.sorted((a, b) => a.from.compareTo(b.from)).first; + + final packageName = cli.project.buildConfig.packageName; + final relativeDbPath = p.relative(dbClassFile.path, + from: p.join(cli.project.directory.path, 'lib')); final code = """ // ignore_for_file: unused_local_variable, unused_import -// GENERATED CODE, DO NOT EDIT BY HAND. import 'package:drift/drift.dart'; import 'package:drift_dev/api/migrations.dart'; import 'package:$packageName/$relativeDbPath'; import 'package:test/test.dart'; -import 'schemas/schema.dart'; +import 'generated/schema.dart'; -${stepByStepTests.map((e) => e.imports).expand((imports) => imports).toSet().join('\n')} +${firstMigration.schemaImports().join('\n')} void main() { driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; @@ -391,20 +375,35 @@ void main() { verifier = SchemaVerifier(GeneratedHelper()); }); - ${stepByStepTests.map((e) => e.test).join('\n')} + group('$dbName database', () { + ////////////////////////////////////////////////////////////////////////////// + ////////////////////// GENERATED TESTS - DO NOT MODIFY /////////////////////// + ////////////////////////////////////////////////////////////////////////////// + if (GeneratedHelper.versions.length < 2) return; + for (var i + in List.generate(GeneratedHelper.versions.length - 1, (i) => i)) { + final oldVersion = GeneratedHelper.versions.elementAt(i); + final newVersion = GeneratedHelper.versions.elementAt(i + 1); + test("migrate from v\$oldVersion to v\$newVersion", () async { + final schema = await verifier.schemaAt(oldVersion); + final db = $dbClassName(schema.newConnection()); + await verifier.migrateAndValidate(db, newVersion); + await db.close(); + }); + } + ////////////////////////////////////////////////////////////////////////////// + /////////////////////// END OF GENERATED TESTS /////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + ${firstMigration.testStepByStepMigrationCode(dbName, dbClassName)} + }); + } """; - final testFile = File(p.join(testDir.path, 'migration_test.dart')); - if (testFile.existsSync()) { - cli.logger.fine( - '$dbName: Updated test in ${blue.wrap(p.relative(testFile.path))}'); - } else { - cli.logger.info( - '$dbName: Generated test in ${blue.wrap(p.relative(testFile.path))}.\n' - 'Run this test to validate that your migrations are written correctly. ${yellow.wrap("dart test ${blue.wrap(p.relative(testFile.path))}")}'); - } + cli.logger.info( + '$dbName: Generated test in ${blue.wrap(p.relative(testFile.path))}.\n' + 'Run this test to validate that your migrations are written correctly. ${yellow.wrap("dart test ${blue.wrap(p.relative(testFile.path))}")}'); writeTasks[testFile] = code; } @@ -453,58 +452,59 @@ class _MigrationWriter { return result; } + List schemaImports() { + return [ + "import 'generated/schema_v$from.dart' as v$from;", + "import 'generated/schema_v$to.dart' as v$to;" + ]; + } + /// Generate a step by step migration test /// This test will test the migration from version [from] to version [to] /// It will also import the validation models to test data integrity - ({Set imports, String test}) testStepByStepMigrationCode( - String dbName, String dbClassName) { - final imports = { - "import 'schemas/schema_v$from.dart' as v$from;", - "import 'schemas/schema_v$to.dart' as v$to;", - "import 'validation/v${from}_to_v$to.dart' as v${from}_to_v$to;" - }; - - final test = """ -test( - "$dbName - migrate from v$from to v$to", - () => testWithDataIntegrity( - from: $from, to: $to, verifier: verifier, createOld: (e) => v$from.DatabaseAtV$from(e), - createNew: (e) => v$to.DatabaseAtV$to(e), openTestedDatabase: (e) => $dbClassName(e), - createItems: (b, oldDb) { - ${tables.map( + String testStepByStepMigrationCode(String dbName, String dbClassName) { + return """ +////////////////////////////////////////////////////////////////////////////// + ///////////////////// CUSTOM TESTS - MODIFY AS NEEDED //////////////////////// + ////////////////////////////////////////////////////////////////////////////// +test("migration from v$from to v$to does not corrupt data", + () async { + // TODO: Consider writing these kinds of tests when altering tables in a way that might affect existing rows. + // The automatically generated migration tests run with an empty schema, so it's a recommended practice to also test with + // data for relevant migrations. + ${tables.map((table) { + return """ +final old${table.dbGetterName.pascalCase}Data = []; // TODO: Add expected data at version $from using v$from.${table.nameOfRowClass} +final expectedNew${table.dbGetterName.pascalCase}Data = []; // TODO: Add expected data at version $to using v$to.${table.nameOfRowClass} +"""; + }).join('\n')} + + await verifier.testWithDataIntegrity( + oldVersion: $from, + newVersion: $to, + verifier: verifier, + createOld: (e) => v1.DatabaseAtV$from(e), + createNew: (e) => v2.DatabaseAtV$to(e), + openTestedDatabase: (e) => $dbClassName(e), + createItems: (batch, oldDb) { + ${tables.map( (table) { - return "b.insertAll(oldDb.${table.dbGetterName}, v${from}_to_v$to.${table.dbGetterName}V$from);"; + return "batch.insertAll(oldDb.${table.dbGetterName}, old${table.dbGetterName.pascalCase}Data);"; }, ).join('\n')} - }, - validateItems: (newDb) async { - ${tables.map( + }, + validateItems: (newDb) async { + ${tables.map( (table) { - return "expect(v${from}_to_v$to.${table.dbGetterName}V$to, await newDb.select(newDb.${table.dbGetterName}).get());"; + return "expect(expectedNew${table.dbGetterName.pascalCase}Data, await newDb.select(newDb.${table.dbGetterName}).get());"; }, ).join('\n')} - }, - ) -); + }, + ); + }); + /////////////////////////////////////////////////////////////////////////////// + /////////////////////// END OF CUSTOM TESTS /////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////// """; - return (imports: imports, test: test); } - - /// Generate the code for a file which users - /// will fill in with before and after data - /// to validate that the migration was successful and data was not lost - String get validationModelsCode => """ -import '../schemas/schema_v$from.dart' as v$from; -import '../schemas/schema_v$to.dart' as v$to; - -/// Run `dart run drift_dev make-migrations --help` for more information -${tables.map((table) { - return """ - -final ${table.dbGetterName}V$from = []; -final ${table.dbGetterName}V$to = []; -"""; - }).join('\n')} - -"""; } diff --git a/drift_dev/lib/src/cli/commands/schema/generate_utils.dart b/drift_dev/lib/src/cli/commands/schema/generate_utils.dart index b7a66146d..4754b5a0f 100644 --- a/drift_dev/lib/src/cli/commands/schema/generate_utils.dart +++ b/drift_dev/lib/src/cli/commands/schema/generate_utils.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:args/command_runner.dart'; import 'package:dart_style/dart_style.dart'; import 'package:path/path.dart' as p; +import 'package:collection/collection.dart'; import '../../../analysis/results/file_results.dart'; import '../../../analysis/results/results.dart'; @@ -165,11 +166,16 @@ class GenerateUtils { ..writeln('return v$version.DatabaseAtV$version(db);'); } - final missingAsSet = '{${versions.join(', ')}}'; + final versionsSet = + '{${versions.sorted((a, b) => a.compareTo(b)).join(', ')}}'; buffer ..writeln('default:') - ..writeln('throw MissingSchemaException(version, const $missingAsSet);') - ..writeln('}}}'); + ..writeln('throw MissingSchemaException(version, versions);') + ..writeln('}}'); + + buffer + ..writeln('static const versions = const $versionsSet;') + ..writeln('}'); return _dartfmt.format(buffer.toString()); } diff --git a/drift_dev/lib/src/services/schema/verifier_impl.dart b/drift_dev/lib/src/services/schema/verifier_impl.dart index fe85f842f..89c9eaaf3 100644 --- a/drift_dev/lib/src/services/schema/verifier_impl.dart +++ b/drift_dev/lib/src/services/schema/verifier_impl.dart @@ -100,6 +100,33 @@ class VerifierImplementation implements SchemaVerifier { Future startAt(int version) { return schemaAt(version).then((schema) => schema.newConnection()); } + + @override + Future testWithDataIntegrity( + {required SchemaVerifier verifier, + required OldDatabase Function(QueryExecutor p1) createOld, + required NewDatabase Function(QueryExecutor p1) createNew, + required GeneratedDatabase Function(QueryExecutor p1) openTestedDatabase, + required void Function(Batch p1, OldDatabase p2) createItems, + required Future Function(NewDatabase p1) validateItems, + required int oldVersion, + required int newVersion}) async { + final schema = await verifier.schemaAt(oldVersion); + + final oldDb = createOld(schema.newConnection()); + await oldDb.customStatement('PRAGMA foreign_keys = OFF'); + await oldDb.batch((batch) => createItems(batch, oldDb)); + await oldDb.close(); + + final db = openTestedDatabase(schema.newConnection()); + await verifier.migrateAndValidate(db, newVersion); + await db.close(); + + final newDb = createNew(schema.newConnection()); + await validateItems(newDb); + await newDb.close(); + } } Input? _parseInputFromSchemaRow( diff --git a/drift_dev/test/cli/make_migrations_test.dart b/drift_dev/test/cli/make_migrations_test.dart index 7faf8464b..596777499 100644 --- a/drift_dev/test/cli/make_migrations_test.dart +++ b/drift_dev/test/cli/make_migrations_test.dart @@ -66,10 +66,9 @@ targets: // Test files should be created await d.dir('app/test/drift/my_database', [ d.file('migration_test.dart', IsValidDartFile(anything)), - d.file('schemas/schema.dart', IsValidDartFile(anything)), - d.file('schemas/schema_v1.dart', IsValidDartFile(anything)), - d.file('schemas/schema_v2.dart', IsValidDartFile(anything)), - d.file('validation/v1_to_v2.dart', IsValidDartFile(anything)), + d.file('generated/schema.dart', IsValidDartFile(anything)), + d.file('generated/schema_v1.dart', IsValidDartFile(anything)), + d.file('generated/schema_v2.dart', IsValidDartFile(anything)), ]).validate(); // Steps file should be created await d diff --git a/examples/migrations_example/test/drift/default/schemas/schema.dart b/examples/migrations_example/test/drift/default/generated/schema.dart similarity index 90% rename from examples/migrations_example/test/drift/default/schemas/schema.dart rename to examples/migrations_example/test/drift/default/generated/schema.dart index af423e42e..2a9b28ed5 100644 --- a/examples/migrations_example/test/drift/default/schemas/schema.dart +++ b/examples/migrations_example/test/drift/default/generated/schema.dart @@ -42,8 +42,9 @@ class GeneratedHelper implements SchemaInstantiationHelper { case 4: return v4.DatabaseAtV4(db); default: - throw MissingSchemaException( - version, const {7, 5, 8, 3, 6, 2, 1, 11, 9, 10, 4}); + throw MissingSchemaException(version, versions); } } + + static const versions = const {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; } diff --git a/examples/migrations_example/test/drift/default/schemas/schema_v1.dart b/examples/migrations_example/test/drift/default/generated/schema_v1.dart similarity index 100% rename from examples/migrations_example/test/drift/default/schemas/schema_v1.dart rename to examples/migrations_example/test/drift/default/generated/schema_v1.dart diff --git a/examples/migrations_example/test/drift/default/schemas/schema_v10.dart b/examples/migrations_example/test/drift/default/generated/schema_v10.dart similarity index 100% rename from examples/migrations_example/test/drift/default/schemas/schema_v10.dart rename to examples/migrations_example/test/drift/default/generated/schema_v10.dart diff --git a/examples/migrations_example/test/drift/default/schemas/schema_v11.dart b/examples/migrations_example/test/drift/default/generated/schema_v11.dart similarity index 100% rename from examples/migrations_example/test/drift/default/schemas/schema_v11.dart rename to examples/migrations_example/test/drift/default/generated/schema_v11.dart diff --git a/examples/migrations_example/test/drift/default/schemas/schema_v2.dart b/examples/migrations_example/test/drift/default/generated/schema_v2.dart similarity index 100% rename from examples/migrations_example/test/drift/default/schemas/schema_v2.dart rename to examples/migrations_example/test/drift/default/generated/schema_v2.dart diff --git a/examples/migrations_example/test/drift/default/schemas/schema_v3.dart b/examples/migrations_example/test/drift/default/generated/schema_v3.dart similarity index 100% rename from examples/migrations_example/test/drift/default/schemas/schema_v3.dart rename to examples/migrations_example/test/drift/default/generated/schema_v3.dart diff --git a/examples/migrations_example/test/drift/default/schemas/schema_v4.dart b/examples/migrations_example/test/drift/default/generated/schema_v4.dart similarity index 100% rename from examples/migrations_example/test/drift/default/schemas/schema_v4.dart rename to examples/migrations_example/test/drift/default/generated/schema_v4.dart diff --git a/examples/migrations_example/test/drift/default/schemas/schema_v5.dart b/examples/migrations_example/test/drift/default/generated/schema_v5.dart similarity index 100% rename from examples/migrations_example/test/drift/default/schemas/schema_v5.dart rename to examples/migrations_example/test/drift/default/generated/schema_v5.dart diff --git a/examples/migrations_example/test/drift/default/schemas/schema_v6.dart b/examples/migrations_example/test/drift/default/generated/schema_v6.dart similarity index 100% rename from examples/migrations_example/test/drift/default/schemas/schema_v6.dart rename to examples/migrations_example/test/drift/default/generated/schema_v6.dart diff --git a/examples/migrations_example/test/drift/default/schemas/schema_v7.dart b/examples/migrations_example/test/drift/default/generated/schema_v7.dart similarity index 100% rename from examples/migrations_example/test/drift/default/schemas/schema_v7.dart rename to examples/migrations_example/test/drift/default/generated/schema_v7.dart diff --git a/examples/migrations_example/test/drift/default/schemas/schema_v8.dart b/examples/migrations_example/test/drift/default/generated/schema_v8.dart similarity index 100% rename from examples/migrations_example/test/drift/default/schemas/schema_v8.dart rename to examples/migrations_example/test/drift/default/generated/schema_v8.dart diff --git a/examples/migrations_example/test/drift/default/schemas/schema_v9.dart b/examples/migrations_example/test/drift/default/generated/schema_v9.dart similarity index 100% rename from examples/migrations_example/test/drift/default/schemas/schema_v9.dart rename to examples/migrations_example/test/drift/default/generated/schema_v9.dart diff --git a/examples/migrations_example/test/drift/default/migration_test.dart b/examples/migrations_example/test/drift/default/migration_test.dart index 900a59528..ce5c66f34 100644 --- a/examples/migrations_example/test/drift/default/migration_test.dart +++ b/examples/migrations_example/test/drift/default/migration_test.dart @@ -1,32 +1,12 @@ // ignore_for_file: unused_local_variable, unused_import -// GENERATED CODE, DO NOT EDIT BY HAND. import 'package:drift/drift.dart'; import 'package:drift_dev/api/migrations.dart'; import 'package:migrations_example/database.dart'; import 'package:test/test.dart'; -import 'schemas/schema.dart'; +import 'generated/schema.dart'; -import 'schemas/schema_v1.dart' as v1; -import 'schemas/schema_v2.dart' as v2; -import 'validation/v1_to_v2.dart' as v1_to_v2; -import 'schemas/schema_v3.dart' as v3; -import 'validation/v2_to_v3.dart' as v2_to_v3; -import 'schemas/schema_v4.dart' as v4; -import 'validation/v3_to_v4.dart' as v3_to_v4; -import 'schemas/schema_v5.dart' as v5; -import 'validation/v4_to_v5.dart' as v4_to_v5; -import 'schemas/schema_v6.dart' as v6; -import 'validation/v5_to_v6.dart' as v5_to_v6; -import 'schemas/schema_v7.dart' as v7; -import 'validation/v6_to_v7.dart' as v6_to_v7; -import 'schemas/schema_v8.dart' as v8; -import 'validation/v7_to_v8.dart' as v7_to_v8; -import 'schemas/schema_v9.dart' as v9; -import 'validation/v8_to_v9.dart' as v8_to_v9; -import 'schemas/schema_v10.dart' as v10; -import 'validation/v9_to_v10.dart' as v9_to_v10; -import 'schemas/schema_v11.dart' as v11; -import 'validation/v10_to_v11.dart' as v10_to_v11; +import 'generated/schema_v1.dart' as v1; +import 'generated/schema_v2.dart' as v2; void main() { driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; @@ -36,201 +16,55 @@ void main() { verifier = SchemaVerifier(GeneratedHelper()); }); - test( - "default - migrate from v1 to v2", - () => testWithDataIntegrity( - from: 1, - to: 2, - verifier: verifier, - createOld: (e) => v1.DatabaseAtV1(e), - createNew: (e) => v2.DatabaseAtV2(e), - openTestedDatabase: (e) => Database(e), - createItems: (b, oldDb) { - b.insertAll(oldDb.users, v1_to_v2.usersV1); - }, - validateItems: (newDb) async { - expect(v1_to_v2.usersV2, await newDb.select(newDb.users).get()); - }, - )); + group('default database', () { + ////////////////////////////////////////////////////////////////////////////// + ////////////////////// GENERATED TESTS - DO NOT MODIFY /////////////////////// + ////////////////////////////////////////////////////////////////////////////// + if (GeneratedHelper.versions.length < 2) return; + for (var i + in List.generate(GeneratedHelper.versions.length - 1, (i) => i)) { + final oldVersion = GeneratedHelper.versions.elementAt(i); + final newVersion = GeneratedHelper.versions.elementAt(i + 1); + test("migrate from v$oldVersion to v$newVersion", () async { + final schema = await verifier.schemaAt(oldVersion); + final db = Database(schema.newConnection()); + await verifier.migrateAndValidate(db, newVersion); + await db.close(); + }); + } + ////////////////////////////////////////////////////////////////////////////// + /////////////////////// END OF GENERATED TESTS /////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// - test( - "default - migrate from v2 to v3", - () => testWithDataIntegrity( - from: 2, - to: 3, - verifier: verifier, - createOld: (e) => v2.DatabaseAtV2(e), - createNew: (e) => v3.DatabaseAtV3(e), - openTestedDatabase: (e) => Database(e), - createItems: (b, oldDb) { - b.insertAll(oldDb.users, v2_to_v3.usersV2); - }, - validateItems: (newDb) async { - expect(v2_to_v3.usersV3, await newDb.select(newDb.users).get()); - }, - )); + ////////////////////////////////////////////////////////////////////////////// + ///////////////////// CUSTOM TESTS - MODIFY AS NEEDED //////////////////////// + ////////////////////////////////////////////////////////////////////////////// + test("migration from v1 to v2 does not corrupt data", () async { + // TODO: Consider writing these kinds of tests when altering tables in a way that might affect existing rows. + // The automatically generated migration tests run with an empty schema, so it's a recommended practice to also test with + // data for relevant migrations. + final oldUsersData = []; // TODO: Add expected data at version 1 using v1.UsersData + final expectedNewUsersData = []; // TODO: Add expected data at version 2 using v2.UsersData - test( - "default - migrate from v3 to v4", - () => testWithDataIntegrity( - from: 3, - to: 4, - verifier: verifier, - createOld: (e) => v3.DatabaseAtV3(e), - createNew: (e) => v4.DatabaseAtV4(e), - openTestedDatabase: (e) => Database(e), - createItems: (b, oldDb) { - b.insertAll(oldDb.users, v3_to_v4.usersV3); - b.insertAll(oldDb.groups, v3_to_v4.groupsV3); - }, - validateItems: (newDb) async { - expect(v3_to_v4.usersV4, await newDb.select(newDb.users).get()); - expect(v3_to_v4.groupsV4, await newDb.select(newDb.groups).get()); - }, - )); - - test( - "default - migrate from v4 to v5", - () => testWithDataIntegrity( - from: 4, - to: 5, - verifier: verifier, - createOld: (e) => v4.DatabaseAtV4(e), - createNew: (e) => v5.DatabaseAtV5(e), - openTestedDatabase: (e) => Database(e), - createItems: (b, oldDb) { - b.insertAll(oldDb.users, v4_to_v5.usersV4); - b.insertAll(oldDb.groups, v4_to_v5.groupsV4); - }, - validateItems: (newDb) async { - expect(v4_to_v5.usersV5, await newDb.select(newDb.users).get()); - expect(v4_to_v5.groupsV5, await newDb.select(newDb.groups).get()); - }, - )); - - test( - "default - migrate from v5 to v6", - () => testWithDataIntegrity( - from: 5, - to: 6, - verifier: verifier, - createOld: (e) => v5.DatabaseAtV5(e), - createNew: (e) => v6.DatabaseAtV6(e), - openTestedDatabase: (e) => Database(e), - createItems: (b, oldDb) { - b.insertAll(oldDb.users, v5_to_v6.usersV5); - b.insertAll(oldDb.groups, v5_to_v6.groupsV5); - }, - validateItems: (newDb) async { - expect(v5_to_v6.usersV6, await newDb.select(newDb.users).get()); - expect(v5_to_v6.groupsV6, await newDb.select(newDb.groups).get()); - }, - )); - - test( - "default - migrate from v6 to v7", - () => testWithDataIntegrity( - from: 6, - to: 7, - verifier: verifier, - createOld: (e) => v6.DatabaseAtV6(e), - createNew: (e) => v7.DatabaseAtV7(e), - openTestedDatabase: (e) => Database(e), - createItems: (b, oldDb) { - b.insertAll(oldDb.users, v6_to_v7.usersV6); - b.insertAll(oldDb.groups, v6_to_v7.groupsV6); - }, - validateItems: (newDb) async { - expect(v6_to_v7.usersV7, await newDb.select(newDb.users).get()); - expect(v6_to_v7.groupsV7, await newDb.select(newDb.groups).get()); - }, - )); - - test( - "default - migrate from v7 to v8", - () => testWithDataIntegrity( - from: 7, - to: 8, - verifier: verifier, - createOld: (e) => v7.DatabaseAtV7(e), - createNew: (e) => v8.DatabaseAtV8(e), - openTestedDatabase: (e) => Database(e), - createItems: (b, oldDb) { - b.insertAll(oldDb.users, v7_to_v8.usersV7); - b.insertAll(oldDb.groups, v7_to_v8.groupsV7); - b.insertAll(oldDb.notes, v7_to_v8.notesV7); - }, - validateItems: (newDb) async { - expect(v7_to_v8.usersV8, await newDb.select(newDb.users).get()); - expect(v7_to_v8.groupsV8, await newDb.select(newDb.groups).get()); - expect(v7_to_v8.notesV8, await newDb.select(newDb.notes).get()); - }, - )); - - test( - "default - migrate from v8 to v9", - () => testWithDataIntegrity( - from: 8, - to: 9, - verifier: verifier, - createOld: (e) => v8.DatabaseAtV8(e), - createNew: (e) => v9.DatabaseAtV9(e), - openTestedDatabase: (e) => Database(e), - createItems: (b, oldDb) { - b.insertAll(oldDb.users, v8_to_v9.usersV8); - b.insertAll(oldDb.groups, v8_to_v9.groupsV8); - b.insertAll(oldDb.notes, v8_to_v9.notesV8); - }, - validateItems: (newDb) async { - expect(v8_to_v9.usersV9, await newDb.select(newDb.users).get()); - expect(v8_to_v9.groupsV9, await newDb.select(newDb.groups).get()); - expect(v8_to_v9.notesV9, await newDb.select(newDb.notes).get()); - }, - )); - - test( - "default - migrate from v9 to v10", - () => testWithDataIntegrity( - from: 9, - to: 10, - verifier: verifier, - createOld: (e) => v9.DatabaseAtV9(e), - createNew: (e) => v10.DatabaseAtV10(e), - openTestedDatabase: (e) => Database(e), - createItems: (b, oldDb) { - b.insertAll(oldDb.users, v9_to_v10.usersV9); - b.insertAll(oldDb.groups, v9_to_v10.groupsV9); - b.insertAll(oldDb.notes, v9_to_v10.notesV9); - }, - validateItems: (newDb) async { - expect(v9_to_v10.usersV10, await newDb.select(newDb.users).get()); - expect( - v9_to_v10.groupsV10, await newDb.select(newDb.groups).get()); - expect(v9_to_v10.notesV10, await newDb.select(newDb.notes).get()); - }, - )); - - test( - "default - migrate from v10 to v11", - () => testWithDataIntegrity( - from: 10, - to: 11, - verifier: verifier, - createOld: (e) => v10.DatabaseAtV10(e), - createNew: (e) => v11.DatabaseAtV11(e), - openTestedDatabase: (e) => Database(e), - createItems: (b, oldDb) { - b.insertAll(oldDb.users, v10_to_v11.usersV10); - b.insertAll(oldDb.groups, v10_to_v11.groupsV10); - b.insertAll(oldDb.notes, v10_to_v11.notesV10); - }, - validateItems: (newDb) async { - expect( - v10_to_v11.usersV11, await newDb.select(newDb.users).get()); - expect( - v10_to_v11.groupsV11, await newDb.select(newDb.groups).get()); - expect( - v10_to_v11.notesV11, await newDb.select(newDb.notes).get()); - }, - )); + await verifier.testWithDataIntegrity( + oldVersion: 1, + newVersion: 2, + verifier: verifier, + createOld: (e) => v1.DatabaseAtV1(e), + createNew: (e) => v2.DatabaseAtV2(e), + openTestedDatabase: (e) => Database(e), + createItems: (batch, oldDb) { + batch.insertAll(oldDb.users, oldUsersData); + }, + validateItems: (newDb) async { + expect(expectedNewUsersData, await newDb.select(newDb.users).get()); + }, + ); + }); + /////////////////////////////////////////////////////////////////////////////// + /////////////////////// END OF CUSTOM TESTS /////////////////////////////////// + /////////////////////////////////////////////////////////////////////////////// + }); } diff --git a/examples/migrations_example/test/drift/default/validation/v10_to_v11.dart b/examples/migrations_example/test/drift/default/validation/v10_to_v11.dart deleted file mode 100644 index 9ad66c0a2..000000000 --- a/examples/migrations_example/test/drift/default/validation/v10_to_v11.dart +++ /dev/null @@ -1,17 +0,0 @@ -import '../schemas/schema_v10.dart' as v10; -import '../schemas/schema_v11.dart' as v11; - -/// Run `dart run drift_dev make-migrations --help` for more information - -final usersV10 = []; -final usersV11 = []; - - -final groupsV10 = []; -final groupsV11 = []; - - -final notesV10 = []; -final notesV11 = []; - - diff --git a/examples/migrations_example/test/drift/default/validation/v1_to_v2.dart b/examples/migrations_example/test/drift/default/validation/v1_to_v2.dart deleted file mode 100644 index 17ad3b411..000000000 --- a/examples/migrations_example/test/drift/default/validation/v1_to_v2.dart +++ /dev/null @@ -1,11 +0,0 @@ -import '../schemas/schema_v1.dart' as v1; -import '../schemas/schema_v2.dart' as v2; - -/// Run `dart run drift_dev make-migrations --help` for more information - -final usersV1 = [ - v1.UsersData(id: 0), -]; -final usersV2 = [ - v2.UsersData(id: 0, name: 'no name'), -]; diff --git a/examples/migrations_example/test/drift/default/validation/v2_to_v3.dart b/examples/migrations_example/test/drift/default/validation/v2_to_v3.dart deleted file mode 100644 index 86d6f2cd7..000000000 --- a/examples/migrations_example/test/drift/default/validation/v2_to_v3.dart +++ /dev/null @@ -1,9 +0,0 @@ -import '../schemas/schema_v2.dart' as v2; -import '../schemas/schema_v3.dart' as v3; - -/// Run `dart run drift_dev make-migrations --help` for more information - -final usersV2 = []; -final usersV3 = []; - - diff --git a/examples/migrations_example/test/drift/default/validation/v3_to_v4.dart b/examples/migrations_example/test/drift/default/validation/v3_to_v4.dart deleted file mode 100644 index 5bff92d52..000000000 --- a/examples/migrations_example/test/drift/default/validation/v3_to_v4.dart +++ /dev/null @@ -1,13 +0,0 @@ -import '../schemas/schema_v3.dart' as v3; -import '../schemas/schema_v4.dart' as v4; - -/// Run `dart run drift_dev make-migrations --help` for more information - -final usersV3 = []; -final usersV4 = []; - - -final groupsV3 = []; -final groupsV4 = []; - - diff --git a/examples/migrations_example/test/drift/default/validation/v4_to_v5.dart b/examples/migrations_example/test/drift/default/validation/v4_to_v5.dart deleted file mode 100644 index 53a37a9f5..000000000 --- a/examples/migrations_example/test/drift/default/validation/v4_to_v5.dart +++ /dev/null @@ -1,13 +0,0 @@ -import '../schemas/schema_v4.dart' as v4; -import '../schemas/schema_v5.dart' as v5; - -/// Run `dart run drift_dev make-migrations --help` for more information - -final usersV4 = []; -final usersV5 = []; - - -final groupsV4 = []; -final groupsV5 = []; - - diff --git a/examples/migrations_example/test/drift/default/validation/v5_to_v6.dart b/examples/migrations_example/test/drift/default/validation/v5_to_v6.dart deleted file mode 100644 index 7fec73616..000000000 --- a/examples/migrations_example/test/drift/default/validation/v5_to_v6.dart +++ /dev/null @@ -1,13 +0,0 @@ -import '../schemas/schema_v5.dart' as v5; -import '../schemas/schema_v6.dart' as v6; - -/// Run `dart run drift_dev make-migrations --help` for more information - -final usersV5 = []; -final usersV6 = []; - - -final groupsV5 = []; -final groupsV6 = []; - - diff --git a/examples/migrations_example/test/drift/default/validation/v6_to_v7.dart b/examples/migrations_example/test/drift/default/validation/v6_to_v7.dart deleted file mode 100644 index d17d3b17f..000000000 --- a/examples/migrations_example/test/drift/default/validation/v6_to_v7.dart +++ /dev/null @@ -1,13 +0,0 @@ -import '../schemas/schema_v6.dart' as v6; -import '../schemas/schema_v7.dart' as v7; - -/// Run `dart run drift_dev make-migrations --help` for more information - -final usersV6 = []; -final usersV7 = []; - - -final groupsV6 = []; -final groupsV7 = []; - - diff --git a/examples/migrations_example/test/drift/default/validation/v7_to_v8.dart b/examples/migrations_example/test/drift/default/validation/v7_to_v8.dart deleted file mode 100644 index 77aca91ee..000000000 --- a/examples/migrations_example/test/drift/default/validation/v7_to_v8.dart +++ /dev/null @@ -1,17 +0,0 @@ -import '../schemas/schema_v7.dart' as v7; -import '../schemas/schema_v8.dart' as v8; - -/// Run `dart run drift_dev make-migrations --help` for more information - -final usersV7 = []; -final usersV8 = []; - - -final groupsV7 = []; -final groupsV8 = []; - - -final notesV7 = []; -final notesV8 = []; - - diff --git a/examples/migrations_example/test/drift/default/validation/v8_to_v9.dart b/examples/migrations_example/test/drift/default/validation/v8_to_v9.dart deleted file mode 100644 index 5a07b0baf..000000000 --- a/examples/migrations_example/test/drift/default/validation/v8_to_v9.dart +++ /dev/null @@ -1,17 +0,0 @@ -import '../schemas/schema_v8.dart' as v8; -import '../schemas/schema_v9.dart' as v9; - -/// Run `dart run drift_dev make-migrations --help` for more information - -final usersV8 = []; -final usersV9 = []; - - -final groupsV8 = []; -final groupsV9 = []; - - -final notesV8 = []; -final notesV9 = []; - - diff --git a/examples/migrations_example/test/drift/default/validation/v9_to_v10.dart b/examples/migrations_example/test/drift/default/validation/v9_to_v10.dart deleted file mode 100644 index 73dfcd0f5..000000000 --- a/examples/migrations_example/test/drift/default/validation/v9_to_v10.dart +++ /dev/null @@ -1,17 +0,0 @@ -import '../schemas/schema_v9.dart' as v9; -import '../schemas/schema_v10.dart' as v10; - -/// Run `dart run drift_dev make-migrations --help` for more information - -final usersV9 = []; -final usersV10 = []; - - -final groupsV9 = []; -final groupsV10 = []; - - -final notesV9 = []; -final notesV10 = []; - - From e4c196a6172493e4c6725b3a09abfb0fdf32e050 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Wed, 9 Oct 2024 22:42:54 -0400 Subject: [PATCH 18/25] update test --- .../lib/src/cli/commands/make_migrations.dart | 19 +++++++++---------- .../test/drift/default/migration_test.dart | 16 +++++++--------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/drift_dev/lib/src/cli/commands/make_migrations.dart b/drift_dev/lib/src/cli/commands/make_migrations.dart index 09332b4e6..025b0e36c 100644 --- a/drift_dev/lib/src/cli/commands/make_migrations.dart +++ b/drift_dev/lib/src/cli/commands/make_migrations.dart @@ -403,7 +403,7 @@ void main() { cli.logger.info( '$dbName: Generated test in ${blue.wrap(p.relative(testFile.path))}.\n' - 'Run this test to validate that your migrations are written correctly. ${yellow.wrap("dart test ${blue.wrap(p.relative(testFile.path))}")}'); + 'Run this test to validate that your migrations are written correctly. ${yellow.wrap("dart test ${p.relative(testFile.path)}")}'); writeTasks[testFile] = code; } @@ -464,14 +464,13 @@ class _MigrationWriter { /// It will also import the validation models to test data integrity String testStepByStepMigrationCode(String dbName, String dbClassName) { return """ -////////////////////////////////////////////////////////////////////////////// - ///////////////////// CUSTOM TESTS - MODIFY AS NEEDED //////////////////////// - ////////////////////////////////////////////////////////////////////////////// +/// Write data integrity tests for migrations that modify existing tables. +/// These tests are important because the auto-generated tests only check empty schemas. + /// Testing with actual data helps ensure migrations don't corrupt existing information. + /// + /// The following is an example of how to write such a test: test("migration from v$from to v$to does not corrupt data", () async { - // TODO: Consider writing these kinds of tests when altering tables in a way that might affect existing rows. - // The automatically generated migration tests run with an empty schema, so it's a recommended practice to also test with - // data for relevant migrations. ${tables.map((table) { return """ final old${table.dbGetterName.pascalCase}Data = []; // TODO: Add expected data at version $from using v$from.${table.nameOfRowClass} @@ -502,9 +501,9 @@ final expectedNew${table.dbGetterName.pascalCase}Data = []; // TODO: Add expected data at version 1 using v1.UsersData final expectedNewUsersData = Date: Wed, 9 Oct 2024 22:56:53 -0400 Subject: [PATCH 19/25] modify descripton --- .../lib/src/cli/commands/make_migrations.dart | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/drift_dev/lib/src/cli/commands/make_migrations.dart b/drift_dev/lib/src/cli/commands/make_migrations.dart index 025b0e36c..d01b21f35 100644 --- a/drift_dev/lib/src/cli/commands/make_migrations.dart +++ b/drift_dev/lib/src/cli/commands/make_migrations.dart @@ -23,8 +23,11 @@ Generates migrations utilities for drift databases ${styleBold.wrap("Usage")}: -After defining your database for the first time, run this command to save the schema. -When you are ready to make changes to the database, alter the schema in the database file, bump the schema version and run this command again. +Run this command to manage database migrations: + +1. After initially defining your database to save the schema. +2. After modifying the database schema and incrementing the version. + This will generate the following: 1. A steps file which contains a helper function to write a migration from one version to another. @@ -44,27 +47,16 @@ This will generate the following: ${blue.wrap(")")}; ${yellow.wrap("}")} -2. A test file which contains tests to validate that the migrations are correct. This will allow you to try out your migrations easily. - -3. (Optional) The generated test can also be used to validate the data integrity of the migrations. - Fill the generated validation models with data that should be present in the database before and after the migration. - These lists will be imported in the test file to validate the data integrity of the migrations +2. A test file containing: + a) Automated tests to validate the correctness of migrations. - Example: - // Validate that the data in the database is still correct after removing a the isAdmin column - ${blue.wrap("final")} ${lightCyan.wrap("usersV1")} = ${yellow.wrap("[")} - v1.${green.wrap("User")}${magenta.wrap("(")}${lightCyan.wrap("id")}: ${green.wrap("Value")}${blue.wrap("(")}1${blue.wrap(")")}, ${lightCyan.wrap("name")}: ${green.wrap("Value")}${blue.wrap("(")}${lightRed.wrap("'Simon'")}${blue.wrap(")")}, ${lightCyan.wrap("isAdmin")}: ${green.wrap("Value")}${blue.wrap("(")}${blue.wrap("true")}${blue.wrap(")")}${magenta.wrap(")")}, - v1.${green.wrap("User")}${magenta.wrap("(")}${lightCyan.wrap("id")}: ${green.wrap("Value")}${blue.wrap("(")}2${blue.wrap(")")}, ${lightCyan.wrap("name")}: ${green.wrap("Value")}${blue.wrap("(")}${lightRed.wrap("'John'")}${blue.wrap(")")}, ${lightCyan.wrap("isAdmin")}: ${green.wrap("Value")}${blue.wrap("(")}${blue.wrap("false")}${blue.wrap(")")}${magenta.wrap(")")}, - ${yellow.wrap("]")}; - - ${blue.wrap("final")} ${lightCyan.wrap("usersV2")} = ${yellow.wrap("[")} - v2.${green.wrap("User")}${magenta.wrap("(")}${lightCyan.wrap("id")}: ${green.wrap("Value")}${blue.wrap("(")}1${blue.wrap(")")}, ${lightCyan.wrap("name")}: ${green.wrap("Value")}${blue.wrap("(")}${lightRed.wrap("'Simon'")}${blue.wrap(")")}${magenta.wrap(")")}, - v2.${green.wrap("User")}${magenta.wrap("(")}${lightCyan.wrap("id")}: ${green.wrap("Value")}${blue.wrap("(")}2${blue.wrap(")")}, ${lightCyan.wrap("name")}: ${green.wrap("Value")}${blue.wrap("(")}${lightRed.wrap("'John'")}${blue.wrap(")")}${magenta.wrap(")")}, - ${yellow.wrap("]")}; + b) A sample data integrity test for the first migration. This test ensures that the initial schema is created correctly and that basic data operations work as expected. + This sample test should be adapted for subsequent migrations, especially those involving complex modifications to existing tables. ${styleBold.wrap("Configuration")}: This tool requires the database be defined in the build.yaml file. + Example: ${blue.wrap(""" From df48d89fb5b2bc35dcdf502893b551d16184b9a8 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Wed, 9 Oct 2024 23:04:19 -0400 Subject: [PATCH 20/25] update docs --- docs/docs/Migrations/index.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/docs/Migrations/index.md b/docs/docs/Migrations/index.md index 895e94e80..03925446a 100644 --- a/docs/docs/Migrations/index.md +++ b/docs/docs/Migrations/index.md @@ -70,9 +70,8 @@ This command will generate the following files: - A step-by-step migration file will be generated next to your database class. Use this function to write your migrations incrementally. See the [step-by-step migration guide](step_by_step.md) for more information. -- Drift will also generate a test file for your migrations. After you've written your migration, run the tests to verify that your migrations are written correctly. +- Drift will also generate a test file for your migrations. After you've written your migration, run the tests to verify that your migrations are written correctly. This files will also contain a sample data integrity test for the first migration. -- Drift will also generate a file which can be used to make the tests validate the data integrity of your migrations. These files should be filled in with before and after data for each migration. If you get stuck along the way, don't hesitate to [open a discussion about it](https://github.com/simolus3/drift/discussions). From 017a9efc66df8f8401c784d5f0e338375754fa35 Mon Sep 17 00:00:00 2001 From: Moshe Dicker Date: Thu, 10 Oct 2024 15:45:14 -0400 Subject: [PATCH 21/25] remove disable foreign key in migrations tests --- drift_dev/lib/src/services/schema/verifier_impl.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/drift_dev/lib/src/services/schema/verifier_impl.dart b/drift_dev/lib/src/services/schema/verifier_impl.dart index 89c9eaaf3..b8b7433d9 100644 --- a/drift_dev/lib/src/services/schema/verifier_impl.dart +++ b/drift_dev/lib/src/services/schema/verifier_impl.dart @@ -115,7 +115,6 @@ class VerifierImplementation implements SchemaVerifier { final schema = await verifier.schemaAt(oldVersion); final oldDb = createOld(schema.newConnection()); - await oldDb.customStatement('PRAGMA foreign_keys = OFF'); await oldDb.batch((batch) => createItems(batch, oldDb)); await oldDb.close(); From 1f40b14f24ae612f13ad40790b7b3b113dc15691 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 10 Oct 2024 22:05:42 +0200 Subject: [PATCH 22/25] Update migration tests template --- drift_dev/lib/api/migrations.dart | 18 +-- .../lib/src/cli/commands/make_migrations.dart | 67 ++++----- .../cli/commands/schema/generate_utils.dart | 2 +- .../src/services/schema/verifier_impl.dart | 7 +- .../test/drift/default/generated/schema.dart | 44 +++--- .../test/drift/default/migration_test.dart | 138 ++++++++++++------ 6 files changed, 163 insertions(+), 113 deletions(-) diff --git a/drift_dev/lib/api/migrations.dart b/drift_dev/lib/api/migrations.dart index ec23a622e..469f5a868 100644 --- a/drift_dev/lib/api/migrations.dart +++ b/drift_dev/lib/api/migrations.dart @@ -79,15 +79,15 @@ abstract class SchemaVerifier { /// /// Foreign key constraints are disabled for this operation. Future testWithDataIntegrity( - {required SchemaVerifier verifier, - required OldDatabase Function(QueryExecutor) createOld, - required NewDatabase Function(QueryExecutor) createNew, - required GeneratedDatabase Function(QueryExecutor) openTestedDatabase, - required void Function(Batch, OldDatabase) createItems, - required Future Function(NewDatabase) validateItems, - required int oldVersion, - required int newVersion}); + NewDatabase extends GeneratedDatabase>({ + required OldDatabase Function(QueryExecutor) createOld, + required NewDatabase Function(QueryExecutor) createNew, + required GeneratedDatabase Function(QueryExecutor) openTestedDatabase, + required void Function(Batch, OldDatabase) createItems, + required Future Function(NewDatabase) validateItems, + required int oldVersion, + required int newVersion, + }); } /// Utilities verifying that the current schema of the database matches what diff --git a/drift_dev/lib/src/cli/commands/make_migrations.dart b/drift_dev/lib/src/cli/commands/make_migrations.dart index d01b21f35..1e8ee813d 100644 --- a/drift_dev/lib/src/cli/commands/make_migrations.dart +++ b/drift_dev/lib/src/cli/commands/make_migrations.dart @@ -48,7 +48,7 @@ This will generate the following: ${yellow.wrap("}")} 2. A test file containing: - a) Automated tests to validate the correctness of migrations. + a) Automated tests to validate the correctness of migrations. b) A sample data integrity test for the first migration. This test ensures that the initial schema is created correctly and that basic data operations work as expected. This sample test should be adapted for subsequent migrations, especially those involving complex modifications to existing tables. @@ -367,29 +367,33 @@ void main() { verifier = SchemaVerifier(GeneratedHelper()); }); - group('$dbName database', () { - ////////////////////////////////////////////////////////////////////////////// - ////////////////////// GENERATED TESTS - DO NOT MODIFY /////////////////////// - ////////////////////////////////////////////////////////////////////////////// - if (GeneratedHelper.versions.length < 2) return; - for (var i - in List.generate(GeneratedHelper.versions.length - 1, (i) => i)) { - final oldVersion = GeneratedHelper.versions.elementAt(i); - final newVersion = GeneratedHelper.versions.elementAt(i + 1); - test("migrate from v\$oldVersion to v\$newVersion", () async { - final schema = await verifier.schemaAt(oldVersion); - final db = $dbClassName(schema.newConnection()); - await verifier.migrateAndValidate(db, newVersion); - await db.close(); + group('simple database migrations', () { + // These simple tests verify all possible schema updates with a simple (no + // data) migration. This is a quick way to ensure that written database + // migrations properly alter the schema. + final versions = GeneratedHelper.versions; + for (final (i, fromVersion) in versions.indexed) { + group('from \$fromVersion', () { + for (final toVersion in versions.skip(i + 1)) { + test('to \$toVersion', () async { + final schema = await verifier.schemaAt(fromVersion); + final db = Database(schema.newConnection()); + await verifier.migrateAndValidate(db, toVersion); + await db.close(); + }); + } }); - } - ////////////////////////////////////////////////////////////////////////////// - /////////////////////// END OF GENERATED TESTS /////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// + } + }); + // Simple tests ensure the schema is transformed correctly, but some + // migrations benefit from a test verifying that data is transformed correctly + // too. This is particularly true for migrations that change existing columns + // (e.g. altering their type or constraints). Migrations that only add tables + // or columns typically don't need these advanced tests. + // TODO: Check whether you have migrations that could benefit from these tests + // and adapt this example to your database if necessary: ${firstMigration.testStepByStepMigrationCode(dbName, dbClassName)} - }); - } """; @@ -456,27 +460,23 @@ class _MigrationWriter { /// It will also import the validation models to test data integrity String testStepByStepMigrationCode(String dbName, String dbClassName) { return """ -/// Write data integrity tests for migrations that modify existing tables. -/// These tests are important because the auto-generated tests only check empty schemas. - /// Testing with actual data helps ensure migrations don't corrupt existing information. - /// - /// The following is an example of how to write such a test: test("migration from v$from to v$to does not corrupt data", () async { + // Add data to insert into the old database, and the expected rows after the + // migration. ${tables.map((table) { return """ -final old${table.dbGetterName.pascalCase}Data = []; // TODO: Add expected data at version $from using v$from.${table.nameOfRowClass} -final expectedNew${table.dbGetterName.pascalCase}Data = []; // TODO: Add expected data at version $to using v$to.${table.nameOfRowClass} +final old${table.dbGetterName.pascalCase}Data = []; +final expectedNew${table.dbGetterName.pascalCase}Data = []; """; }).join('\n')} await verifier.testWithDataIntegrity( oldVersion: $from, newVersion: $to, - verifier: verifier, - createOld: (e) => v1.DatabaseAtV$from(e), - createNew: (e) => v2.DatabaseAtV$to(e), - openTestedDatabase: (e) => $dbClassName(e), + createOld: v1.DatabaseAtV$from.new, + createNew: v2.DatabaseAtV$to.new, + openTestedDatabase: $dbClassName.new, createItems: (batch, oldDb) { ${tables.map( (table) { @@ -493,9 +493,6 @@ final expectedNew${table.dbGetterName.pascalCase}Data = a.compareTo(b)).join(', ')}}'; + '[${versions.sorted((a, b) => a.compareTo(b)).join(', ')}]'; buffer ..writeln('default:') ..writeln('throw MissingSchemaException(version, versions);') diff --git a/drift_dev/lib/src/services/schema/verifier_impl.dart b/drift_dev/lib/src/services/schema/verifier_impl.dart index b8b7433d9..5d8b2d6d5 100644 --- a/drift_dev/lib/src/services/schema/verifier_impl.dart +++ b/drift_dev/lib/src/services/schema/verifier_impl.dart @@ -104,22 +104,21 @@ class VerifierImplementation implements SchemaVerifier { @override Future testWithDataIntegrity( - {required SchemaVerifier verifier, - required OldDatabase Function(QueryExecutor p1) createOld, + {required OldDatabase Function(QueryExecutor p1) createOld, required NewDatabase Function(QueryExecutor p1) createNew, required GeneratedDatabase Function(QueryExecutor p1) openTestedDatabase, required void Function(Batch p1, OldDatabase p2) createItems, required Future Function(NewDatabase p1) validateItems, required int oldVersion, required int newVersion}) async { - final schema = await verifier.schemaAt(oldVersion); + final schema = await schemaAt(oldVersion); final oldDb = createOld(schema.newConnection()); await oldDb.batch((batch) => createItems(batch, oldDb)); await oldDb.close(); final db = openTestedDatabase(schema.newConnection()); - await verifier.migrateAndValidate(db, newVersion); + await migrateAndValidate(db, newVersion); await db.close(); final newDb = createNew(schema.newConnection()); diff --git a/examples/migrations_example/test/drift/default/generated/schema.dart b/examples/migrations_example/test/drift/default/generated/schema.dart index 2a9b28ed5..787aebd11 100644 --- a/examples/migrations_example/test/drift/default/generated/schema.dart +++ b/examples/migrations_example/test/drift/default/generated/schema.dart @@ -3,48 +3,48 @@ //@dart=2.12 import 'package:drift/drift.dart'; import 'package:drift/internal/migrations.dart'; -import 'schema_v7.dart' as v7; -import 'schema_v5.dart' as v5; +import 'schema_v9.dart' as v9; import 'schema_v8.dart' as v8; -import 'schema_v3.dart' as v3; -import 'schema_v6.dart' as v6; -import 'schema_v2.dart' as v2; import 'schema_v1.dart' as v1; +import 'schema_v2.dart' as v2; +import 'schema_v6.dart' as v6; +import 'schema_v7.dart' as v7; import 'schema_v11.dart' as v11; -import 'schema_v9.dart' as v9; -import 'schema_v10.dart' as v10; import 'schema_v4.dart' as v4; +import 'schema_v5.dart' as v5; +import 'schema_v3.dart' as v3; +import 'schema_v10.dart' as v10; class GeneratedHelper implements SchemaInstantiationHelper { @override GeneratedDatabase databaseForVersion(QueryExecutor db, int version) { switch (version) { - case 7: - return v7.DatabaseAtV7(db); - case 5: - return v5.DatabaseAtV5(db); + case 9: + return v9.DatabaseAtV9(db); case 8: return v8.DatabaseAtV8(db); - case 3: - return v3.DatabaseAtV3(db); - case 6: - return v6.DatabaseAtV6(db); - case 2: - return v2.DatabaseAtV2(db); case 1: return v1.DatabaseAtV1(db); + case 2: + return v2.DatabaseAtV2(db); + case 6: + return v6.DatabaseAtV6(db); + case 7: + return v7.DatabaseAtV7(db); case 11: return v11.DatabaseAtV11(db); - case 9: - return v9.DatabaseAtV9(db); - case 10: - return v10.DatabaseAtV10(db); case 4: return v4.DatabaseAtV4(db); + case 5: + return v5.DatabaseAtV5(db); + case 3: + return v3.DatabaseAtV3(db); + case 10: + return v10.DatabaseAtV10(db); default: throw MissingSchemaException(version, versions); } } - static const versions = const {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; + static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; } diff --git a/examples/migrations_example/test/drift/default/migration_test.dart b/examples/migrations_example/test/drift/default/migration_test.dart index 7b27cc4f7..d2bcb0b17 100644 --- a/examples/migrations_example/test/drift/default/migration_test.dart +++ b/examples/migrations_example/test/drift/default/migration_test.dart @@ -1,5 +1,6 @@ // ignore_for_file: unused_local_variable, unused_import import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; import 'package:drift_dev/api/migrations.dart'; import 'package:migrations_example/database.dart'; import 'package:test/test.dart'; @@ -7,6 +8,8 @@ import 'generated/schema.dart'; import 'generated/schema_v1.dart' as v1; import 'generated/schema_v2.dart' as v2; +import 'generated/schema_v4.dart' as v4; +import 'generated/schema_v5.dart' as v5; void main() { driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; @@ -16,53 +19,104 @@ void main() { verifier = SchemaVerifier(GeneratedHelper()); }); - group('default database', () { - ////////////////////////////////////////////////////////////////////////////// - ////////////////////// GENERATED TESTS - DO NOT MODIFY /////////////////////// - ////////////////////////////////////////////////////////////////////////////// - if (GeneratedHelper.versions.length < 2) return; - for (var i - in List.generate(GeneratedHelper.versions.length - 1, (i) => i)) { - final oldVersion = GeneratedHelper.versions.elementAt(i); - final newVersion = GeneratedHelper.versions.elementAt(i + 1); - test("migrate from v$oldVersion to v$newVersion", () async { - final schema = await verifier.schemaAt(oldVersion); - final db = Database(schema.newConnection()); - await verifier.migrateAndValidate(db, newVersion); - await db.close(); + group('simple database migrations', () { + // These simple tests verify all possible schema updates with a simple (no + // data) migration. This is a quick way to ensure that written database + // migrations properly alter the schema. + final versions = GeneratedHelper.versions; + for (final (i, fromVersion) in versions.indexed) { + group('from $fromVersion', () { + for (final toVersion in versions.skip(i + 1)) { + test('to $toVersion', () async { + final schema = await verifier.schemaAt(fromVersion); + final db = Database(schema.newConnection()); + await verifier.migrateAndValidate(db, toVersion); + await db.close(); + }); + } }); } - ////////////////////////////////////////////////////////////////////////////// - /////////////////////// END OF GENERATED TESTS /////////////////////////////// - ////////////////////////////////////////////////////////////////////////////// + }); + + // Simple tests ensure the schema is transformed correctly, but some + // migrations benefit from a test verifying that data is transformed correctly + // too. This is particularly true for migrations that change existing columns + // (e.g. altering their type or constraints). Migrations that only add tables + // or columns typically don't need these advanced tests. + test("migration from v1 to v2 does not corrupt data", () async { + final oldUsersData = [v1.UsersData(id: 1)]; + final expectedNewUsersData = [ + v2.UsersData(id: 1, name: 'no name') + ]; + + await verifier.testWithDataIntegrity( + oldVersion: 1, + newVersion: 2, + createOld: v1.DatabaseAtV1.new, + createNew: v2.DatabaseAtV2.new, + openTestedDatabase: Database.new, + createItems: (batch, oldDb) { + batch.insertAll(oldDb.users, oldUsersData); + }, + validateItems: (newDb) async { + expect(expectedNewUsersData, await newDb.select(newDb.users).get()); + }, + ); + }); + + test('foreign key constraints work after upgrade from v4 to v5', () async { + final schema = await verifier.schemaAt(4); + final db = Database(schema.newConnection()); + await verifier.migrateAndValidate(db, 5); + await db.close(); - /// Write data integrity tests for migrations that modify existing tables. - /// These tests are important because the auto-generated tests only check empty schemas. - /// Testing with actual data helps ensure migrations don't corrupt existing information. - /// - /// The following is an example of how to write such a test: - test("migration from v1 to v2 does not corrupt data", () async { - final oldUsersData = []; // TODO: Add expected data at version 1 using v1.UsersData - final expectedNewUsersData = []; // TODO: Add expected data at version 2 using v2.UsersData + // Test that the foreign key reference introduced in v5 works as expected. + final migratedDb = v5.DatabaseAtV5(schema.newConnection()); + // The `foreign_keys` pragma is a per-connection option and the generated + // versioned classes don't enable it by default. So, enable it manually. + await migratedDb.customStatement('pragma foreign_keys = on;'); + await migratedDb.into(migratedDb.users).insert(v5.UsersCompanion.insert()); + await migratedDb + .into(migratedDb.users) + .insert(v5.UsersCompanion.insert(nextUser: Value(1))); - await verifier.testWithDataIntegrity( - oldVersion: 1, - newVersion: 2, - verifier: verifier, - createOld: (e) => v1.DatabaseAtV1(e), - createNew: (e) => v2.DatabaseAtV2(e), - openTestedDatabase: (e) => Database(e), - createItems: (batch, oldDb) { - batch.insertAll(oldDb.users, oldUsersData); - }, - validateItems: (newDb) async { - expect(expectedNewUsersData, await newDb.select(newDb.users).get()); - }, - ); + // Deleting the first user should now fail due to the constraint + await expectLater(migratedDb.users.deleteWhere((tbl) => tbl.id.equals(1)), + throwsA(isA())); + }); + + test('view works after upgrade from v4 to v5', () async { + final schema = await verifier.schemaAt(4); + + final oldDb = v4.DatabaseAtV4(schema.newConnection()); + await oldDb.batch((batch) { + batch + ..insert(oldDb.users, v4.UsersCompanion.insert(id: Value(1))) + ..insert(oldDb.users, v4.UsersCompanion.insert(id: Value(2))) + ..insert( + oldDb.groups, v4.GroupsCompanion.insert(title: 'Test', owner: 1)); }); + await oldDb.close(); + + // Run the migration and verify that it adds the view. + final db = Database(schema.newConnection()); + await verifier.migrateAndValidate(db, 5); + await db.close(); + + // Make sure the view works! + final migratedDb = v5.DatabaseAtV5(schema.newConnection()); + final viewCount = await migratedDb.select(migratedDb.groupCount).get(); - /// Add additional data integrity tests here + expect( + viewCount, + contains(isA() + .having((e) => e.id, 'id', 1) + .having((e) => e.groupCount, 'groupCount', 1))); + expect( + viewCount, + contains(isA() + .having((e) => e.id, 'id', 2) + .having((e) => e.groupCount, 'groupCount', 0))); + await migratedDb.close(); }); } From 1d742e042d6e6feffa349b15c5a182369958a6b7 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 10 Oct 2024 22:08:36 +0200 Subject: [PATCH 23/25] Add changelog entry --- drift_dev/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drift_dev/CHANGELOG.md b/drift_dev/CHANGELOG.md index 0cb5dbd1b..0a79a853d 100644 --- a/drift_dev/CHANGELOG.md +++ b/drift_dev/CHANGELOG.md @@ -14,6 +14,8 @@ ``` - Make `build.yaml` definitions pass `build_runner doctor`. - Fix `generate_manager` option not consistently being applied to modular builds. +- Add the `make-migrations` command which combines the existing schema commands + into a single tool. ## 2.20.3 From 04a046d19593a95b8996a015cdce11e1ae729ca0 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 10 Oct 2024 22:20:50 +0200 Subject: [PATCH 24/25] Fix migration tooling test --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 396d27bb2..0323019da 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -335,7 +335,7 @@ jobs: - name: Test with older sqlite3 working-directory: examples/migrations_example run: | - LD_LIBRARY_PATH=$(realpath drift/.dart_tool/sqlite3/minimum) dart test + LD_LIBRARY_PATH=$(realpath ../../drift/.dart_tool/sqlite3/minimum) dart test - name: Check that extracting schema still works working-directory: examples/migrations_example run: dart run drift_dev schema dump lib/database.dart drift_migrations/ From 2ef6c60ba47285eb33bfec6bbf50d7b2bf513f1c Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 10 Oct 2024 22:24:03 +0200 Subject: [PATCH 25/25] Attempt to fix Flutter CI --- .github/workflows/main.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0323019da..3cca82dd4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -280,6 +280,7 @@ jobs: dart test test_flutter: runs-on: ubuntu-latest + needs: [setup] strategy: matrix: # We want to support the two latest stable Flutter versions @@ -299,6 +300,22 @@ jobs: shell: bash - name: Install dependencies in drift/example/app run: melos bootstrap --scope app + + - name: Download sqlite3 + uses: actions/download-artifact@v4 + with: + name: sqlite3 + path: drift/.dart_tool/sqlite3/ + - name: Use downloaded sqlite3 + shell: bash + run: | + chmod a+x drift/.dart_tool/sqlite3/latest/sqlite3 + echo $(realpath drift/.dart_tool/sqlite3/latest) >> $GITHUB_PATH + echo "LD_LIBRARY_PATH=$(realpath drift/.dart_tool/sqlite3/latest)" >> $GITHUB_ENV + - name: Check sqlite3 version + run: sqlite3 --version + shell: bash + - name: Generate code run: dart run build_runner build --delete-conflicting-outputs working-directory: examples/app