Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Destructive migration #2

Open
wants to merge 18 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion example/lib/database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import 'package:sqflite/sqflite.dart' as sqflite;

part 'database.g.dart';

@Database(version: 1, entities: [Task])
@Database(version: 1, entities: [Task], fallbackToDestructiveMigration: false)
abstract class FlutterDatabase extends FloorDatabase {
TaskDao get taskDao;
}
25 changes: 19 additions & 6 deletions example/lib/database.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions floor/lib/src/adapter/migration_adapter.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'package:floor/src/migration.dart';
import 'package:sqflite/sqflite.dart';

class MissingMigrationException implements Exception {}

abstract class MigrationAdapter {
/// Runs the given [migrations] for migrating the database schema and data.
static Future<void> runMigrations(
Expand All @@ -17,10 +19,7 @@ abstract class MigrationAdapter {

if (relevantMigrations.isEmpty ||
relevantMigrations.last.endVersion != endVersion) {
throw StateError(
'There is no migration supplied to update the database to the current version.'
' Aborting the migration.',
);
throw MissingMigrationException();
}

for (final migration in relevantMigrations) {
Expand Down
27 changes: 25 additions & 2 deletions floor/lib/src/callback.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,30 @@ class Callback {
int endVersion,
)? onUpgrade;

/// Fired when the [database] failed upgrading caused by [exception] from
/// [startVersion] to [endVersion], before dropping all existing data and re-creating the schema.
final FutureOr<void> Function(
Database database,
int startVersion,
int endVersion,
Exception exception,
)? onDestructiveUpgrade;

/// Fired when the [database] is downgrading from [startVersion] to
/// [endVersion], before dropping all existing data and re-creating the schema.
final FutureOr<void> Function(
Database database,
int startVersion,
int endVersion,
)? onDestructiveDowngrade;

/// Constructor.
const Callback(
{this.onConfigure, this.onCreate, this.onOpen, this.onUpgrade});
const Callback({
this.onConfigure,
this.onCreate,
this.onOpen,
this.onUpgrade,
this.onDestructiveUpgrade,
this.onDestructiveDowngrade,
});
}
4 changes: 2 additions & 2 deletions floor/test/adapter/migration_adapter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ void main() {
migrations,
);

expect(actual, throwsStateError);
expect(actual, throwsException);
verifyZeroInteractions(mockDatabase);
});

Expand All @@ -107,7 +107,7 @@ void main() {
migrations,
);

expect(actual, throwsStateError);
expect(actual, throwsException);
verifyZeroInteractions(mockDatabase);
});
}
4 changes: 4 additions & 0 deletions floor_annotation/lib/src/database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ class Database {
/// The views the database manages.
final List<Type> views;

// Re-create the database if migration fails or is missing.
final bool fallbackToDestructiveMigration;

/// Marks a class as a FloorDatabase.
const Database({
required this.version,
required this.entities,
this.fallbackToDestructiveMigration = false,
this.views = const [],
});
}
2 changes: 2 additions & 0 deletions floor_generator/lib/misc/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ abstract class AnnotationField {
static const databaseVersion = 'version';
static const databaseEntities = 'entities';
static const databaseViews = 'views';
static const databaseFallbackToDestructiveMigration =
'fallbackToDestructiveMigration';

static const columnInfoName = 'name';

Expand Down
10 changes: 10 additions & 0 deletions floor_generator/lib/processor/database_processor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class DatabaseProcessor extends Processor<Database> {
daoGetters,
version,
databaseTypeConverters,
_getFallBackToDestruction(),
allTypeConverters,
);
}
Expand Down Expand Up @@ -178,4 +179,13 @@ class DatabaseProcessor extends Processor<Database> {
return classElement.hasAnnotation(annotations.DatabaseView) &&
!classElement.isAbstract;
}

bool _getFallBackToDestruction() {
final isDestructive = _classElement
.getAnnotation(annotations.Database)
?.getField(AnnotationField.databaseFallbackToDestructiveMigration)
?.toBoolValue();

return isDestructive ?? false;
}
}
7 changes: 6 additions & 1 deletion floor_generator/lib/value_object/database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class Database {
final int version;
final Set<TypeConverter> databaseTypeConverters;
final Set<TypeConverter> allTypeConverters;
final bool fallbackToDestructiveMigration;
final bool hasViewStreams;
final Set<Entity> streamEntities;

Expand All @@ -27,6 +28,7 @@ class Database {
this.daoGetters,
this.version,
this.databaseTypeConverters,
this.fallbackToDestructiveMigration,
this.allTypeConverters,
) : streamEntities =
daoGetters.expand((dg) => dg.dao.streamEntities).toSet(),
Expand All @@ -45,6 +47,8 @@ class Database {
version == other.version &&
databaseTypeConverters.equals(other.databaseTypeConverters) &&
allTypeConverters.equals(other.allTypeConverters) &&
fallbackToDestructiveMigration ==
other.fallbackToDestructiveMigration &&
streamEntities.equals(other.streamEntities) &&
hasViewStreams == hasViewStreams;

Expand All @@ -58,11 +62,12 @@ class Database {
version.hashCode ^
databaseTypeConverters.hashCode ^
allTypeConverters.hashCode ^
fallbackToDestructiveMigration.hashCode ^
streamEntities.hashCode ^
hasViewStreams.hashCode;

@override
String toString() {
return 'Database{classElement: $classElement, name: $name, entities: $entities, views: $views, daoGetters: $daoGetters, version: $version, databaseTypeConverters: $databaseTypeConverters, allTypeConverters: $allTypeConverters, streamEntities: $streamEntities, hasViewStreams: $hasViewStreams}';
return 'Database{classElement: $classElement, name: $name, entities: $entities, views: $views, daoGetters: $daoGetters, version: $version, databaseTypeConverters: $databaseTypeConverters, $version,isFallBackToDestruction: $fallbackToDestructiveMigration, allTypeConverters: $allTypeConverters, streamEntities: $streamEntities, hasViewStreams: $hasViewStreams}';
}
}
129 changes: 107 additions & 22 deletions floor_generator/lib/writer/database_writer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ class DatabaseWriter implements Writer {
Class _generateDatabaseImplementation(final Database database) {
final databaseName = database.name;

return Class((builder) => builder
..name = '_\$$databaseName'
..extend = refer(databaseName)
..methods.add(_generateOpenMethod(database))
..methods.addAll(_generateDaoGetters(database))
..fields.addAll(_generateDaoInstances(database))
..constructors.add(_generateConstructor()));
return Class((builder) {
builder
..name = '_\$$databaseName'
..extend = refer(databaseName)
..methods.add(_generateOpenMethod(database))
..methods.add(_generateCreateMethod(database))
..methods.addAll(_generateDaoGetters(database))
..fields.addAll(_generateDaoInstances(database))
..constructors.add(_generateConstructor());
});
}

Constructor _generateConstructor() {
Expand Down Expand Up @@ -68,21 +71,40 @@ class DatabaseWriter implements Writer {
}).toList();
}

Method _generateOpenMethod(final Database database) {
Method _generateCreateMethod(final Database database) {
final databaseParameter = Parameter((builder) => builder
..name = 'database'
..type = refer('sqflite.Database'));

final createTableStatements = _generateCreateTableSqlStatements(
database.entities)
.map((statement) => 'await database.execute(${statement.toLiteral()});')
.join('\n');

final createIndexStatements = database.entities
.map((entity) => entity.indices.map((index) => index.createQuery()))
.expand((statements) => statements)
.map((statement) => 'await database.execute(${statement.toLiteral()});')
.join('\n');

final createViewStatements = database.views
.map((view) => view.getCreateViewStatement().toLiteral())
.map((statement) => 'await database.execute($statement);')
.join('\n');

return Method((builder) => builder
..name = '_create'
..returns = refer('Future<void>')
..requiredParameters.add(databaseParameter)
..modifier = MethodModifier.async
..body = Code('''
$createTableStatements
$createIndexStatements
$createViewStatements
'''));
}

Method _generateOpenMethod(final Database database) {
final pathParameter = Parameter((builder) => builder
..name = 'path'
..type = refer('String'));
Expand All @@ -93,13 +115,59 @@ class DatabaseWriter implements Writer {
..name = 'callback'
..type = refer('Callback?'));

return Method((builder) => builder
..name = 'open'
..returns = refer('Future<sqflite.Database>')
..modifier = MethodModifier.async
..requiredParameters.addAll([pathParameter, migrationsParameter])
..optionalParameters.add(callbackParameter)
..body = Code('''
String body;

if (database.fallbackToDestructiveMigration) {
body = '''
bool shouldDeleteDatabase = false;

final databaseOptions = sqflite.OpenDatabaseOptions(
version: ${database.version},
onConfigure: (database) async {
await database.execute('PRAGMA foreign_keys = ON');
await callback?.onConfigure?.call(database);
},
onOpen: (database) async {
await callback?.onOpen?.call(database);
},
onUpgrade: (database, startVersion, endVersion) async {
try {
await MigrationAdapter.runMigrations(
database,
startVersion,
endVersion,
migrations,
);
await callback?.onUpgrade?.call(database, startVersion, endVersion);
} on Exception catch (e) {
await callback?.onDestructiveUpgrade?.call(database, startVersion, endVersion, e);
shouldDeleteDatabase = true;
}
},
onDowngrade: (database, startVersion, endVersion) async {
await callback?.onDestructiveDowngrade?.call(database, startVersion, endVersion);
shouldDeleteDatabase = true;
},
onCreate: (database, version) async {
await _create(database);
await callback?.onCreate?.call(database, version);
},
);

final database = await sqfliteDatabaseFactory.openDatabase(path,
options: databaseOptions);

if (shouldDeleteDatabase) {
await database.close();
await sqfliteDatabaseFactory.deleteDatabase(path);
return sqfliteDatabaseFactory.openDatabase(path,
options: databaseOptions);
} else {
return database;
}
''';
} else {
body = '''
final databaseOptions = sqflite.OpenDatabaseOptions(
version: ${database.version},
onConfigure: (database) async {
Expand All @@ -110,20 +178,37 @@ class DatabaseWriter implements Writer {
await callback?.onOpen?.call(database);
},
onUpgrade: (database, startVersion, endVersion) async {
await MigrationAdapter.runMigrations(database, startVersion, endVersion, migrations);

try {
await MigrationAdapter.runMigrations(
database,
startVersion,
endVersion,
migrations,
);
} on MissingMigrationException catch (_) {
throw StateError(
'There is no migration supplied to update the database to the current version.'
' Aborting the migration.',
);
}
await callback?.onUpgrade?.call(database, startVersion, endVersion);
},
onCreate: (database, version) async {
$createTableStatements
$createIndexStatements
$createViewStatements

await _create(database);
await callback?.onCreate?.call(database, version);
},
);
return sqfliteDatabaseFactory.openDatabase(path, options: databaseOptions);
'''));
''';
}

return Method((builder) => builder
..name = 'open'
..returns = refer('Future<sqflite.Database>')
..modifier = MethodModifier.async
..requiredParameters.addAll([pathParameter, migrationsParameter])
..optionalParameters.add(callbackParameter)
..body = Code(body));
}

List<String> _generateCreateTableSqlStatements(final List<Entity> entities) {
Expand Down
Loading