mirror of https://github.com/AMT-Cheif/drift.git
Move migration docs into their own section
This commit is contained in:
parent
999c17e19a
commit
ba20f80303
|
@ -16,6 +16,8 @@ dart run build_runner serve web:8080 --live-reload
|
|||
To build the website into a directory `out`, use:
|
||||
|
||||
```
|
||||
dart run drift_dev schema steps lib/snippets/migrations/exported_eschema/ lib/database/schema_versions.dart
|
||||
dart run drift_dev schema steps lib/snippets/migrations/exported_eschema/ lib/snippets/migrations/schema_versions.dart
|
||||
dart run drift_dev schema generate --data-classes --companions lib/snippets/migrations/exported_eschema/ lib/snippets/migrations/tests/generated_migrations/
|
||||
|
||||
dart run build_runner build --release --output web:out
|
||||
```
|
||||
|
|
|
@ -122,6 +122,8 @@ targets:
|
|||
environment: "preview"
|
||||
build_web_compilers:entrypoint:
|
||||
generate_for:
|
||||
include:
|
||||
- "web/**"
|
||||
exclude:
|
||||
- "web/drift_worker.dart"
|
||||
release_options:
|
||||
|
|
|
@ -1,13 +1,5 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
// #docregion stepbystep
|
||||
// This file was generated by `drift_dev schema steps drift_schemas lib/database/schema_versions.dart`
|
||||
import 'schema_versions.dart';
|
||||
|
||||
// #enddocregion stepbystep
|
||||
|
||||
part 'migrations.g.dart';
|
||||
|
||||
const kDebugMode = false;
|
||||
|
@ -25,8 +17,8 @@ class Todos extends Table {
|
|||
// #enddocregion table
|
||||
|
||||
@DriftDatabase(tables: [Todos])
|
||||
class Example extends _$Example {
|
||||
Example(QueryExecutor e) : super(e);
|
||||
class MyDatabase extends _$MyDatabase {
|
||||
MyDatabase(QueryExecutor e) : super(e);
|
||||
|
||||
// #docregion start
|
||||
@override
|
||||
|
@ -99,121 +91,3 @@ class Example extends _$Example {
|
|||
// #enddocregion change_type
|
||||
}
|
||||
}
|
||||
|
||||
class StepByStep {
|
||||
// #docregion stepbystep
|
||||
MigrationStrategy get migration {
|
||||
return MigrationStrategy(
|
||||
onCreate: (Migrator m) async {
|
||||
await m.createAll();
|
||||
},
|
||||
onUpgrade: stepByStep(
|
||||
from1To2: (m, schema) async {
|
||||
// we added the dueDate property in the change from version 1 to
|
||||
// version 2
|
||||
await m.addColumn(schema.todos, schema.todos.dueDate);
|
||||
},
|
||||
from2To3: (m, schema) async {
|
||||
// we added the priority property in the change from version 1 or 2
|
||||
// to version 3
|
||||
await m.addColumn(schema.todos, schema.todos.priority);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
// #enddocregion stepbystep
|
||||
}
|
||||
|
||||
extension StepByStep2 on GeneratedDatabase {
|
||||
MigrationStrategy get migration {
|
||||
return MigrationStrategy(
|
||||
onCreate: (Migrator m) async {
|
||||
await m.createAll();
|
||||
},
|
||||
// #docregion stepbystep2
|
||||
onUpgrade: (m, from, to) async {
|
||||
// Run migration steps without foreign keys and re-enable them later
|
||||
// (https://drift.simonbinder.eu/docs/advanced-features/migrations/#tips)
|
||||
await customStatement('PRAGMA foreign_keys = OFF');
|
||||
|
||||
await m.runMigrationSteps(
|
||||
from: from,
|
||||
to: to,
|
||||
steps: migrationSteps(
|
||||
from1To2: (m, schema) async {
|
||||
// we added the dueDate property in the change from version 1 to
|
||||
// version 2
|
||||
await m.addColumn(schema.todos, schema.todos.dueDate);
|
||||
},
|
||||
from2To3: (m, schema) async {
|
||||
// we added the priority property in the change from version 1 or 2
|
||||
// to version 3
|
||||
await m.addColumn(schema.todos, schema.todos.priority);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (kDebugMode) {
|
||||
// Fail if the migration broke foreign keys
|
||||
final wrongForeignKeys =
|
||||
await customSelect('PRAGMA foreign_key_check').get();
|
||||
assert(wrongForeignKeys.isEmpty,
|
||||
'${wrongForeignKeys.map((e) => e.data)}');
|
||||
}
|
||||
|
||||
await customStatement('PRAGMA foreign_keys = ON;');
|
||||
},
|
||||
// #enddocregion stepbystep2
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension StepByStep3 on Example {
|
||||
MigrationStrategy get migration {
|
||||
return MigrationStrategy(
|
||||
onCreate: (Migrator m) async {
|
||||
await m.createAll();
|
||||
},
|
||||
// #docregion stepbystep3
|
||||
onUpgrade: (m, from, to) async {
|
||||
// Run migration steps without foreign keys and re-enable them later
|
||||
// (https://drift.simonbinder.eu/docs/advanced-features/migrations/#tips)
|
||||
await customStatement('PRAGMA foreign_keys = OFF');
|
||||
|
||||
// Manually running migrations up to schema version 2, after which we've
|
||||
// enabled step-by-step migrations.
|
||||
if (from < 2) {
|
||||
// we added the dueDate property in the change from version 1 to
|
||||
// version 2 - before switching to step-by-step migrations.
|
||||
await m.addColumn(todos, todos.dueDate);
|
||||
}
|
||||
|
||||
// At this point, we should be migrated to schema 3. For future schema
|
||||
// changes, we will "start" at schema 3.
|
||||
await m.runMigrationSteps(
|
||||
from: math.max(2, from),
|
||||
to: to,
|
||||
// ignore: missing_required_argument
|
||||
steps: migrationSteps(
|
||||
from2To3: (m, schema) async {
|
||||
// we added the priority property in the change from version 1 or
|
||||
// 2 to version 3
|
||||
await m.addColumn(schema.todos, schema.todos.priority);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (kDebugMode) {
|
||||
// Fail if the migration broke foreign keys
|
||||
final wrongForeignKeys =
|
||||
await customSelect('PRAGMA foreign_key_check').get();
|
||||
assert(wrongForeignKeys.isEmpty,
|
||||
'${wrongForeignKeys.map((e) => e.data)}');
|
||||
}
|
||||
|
||||
await customStatement('PRAGMA foreign_keys = ON;');
|
||||
},
|
||||
// #enddocregion stepbystep3
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
import 'migrations.dart';
|
||||
|
||||
// #docregion stepbystep
|
||||
// This file was generated by `drift_dev schema steps drift_schemas/ lib/database/schema_versions.dart`
|
||||
import 'schema_versions.dart';
|
||||
|
||||
// #enddocregion stepbystep
|
||||
|
||||
class StepByStep {
|
||||
// #docregion stepbystep
|
||||
MigrationStrategy get migration {
|
||||
return MigrationStrategy(
|
||||
onCreate: (Migrator m) async {
|
||||
await m.createAll();
|
||||
},
|
||||
onUpgrade: stepByStep(
|
||||
from1To2: (m, schema) async {
|
||||
// we added the dueDate property in the change from version 1 to
|
||||
// version 2
|
||||
await m.addColumn(schema.todos, schema.todos.dueDate);
|
||||
},
|
||||
from2To3: (m, schema) async {
|
||||
// we added the priority property in the change from version 1 or 2
|
||||
// to version 3
|
||||
await m.addColumn(schema.todos, schema.todos.priority);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
// #enddocregion stepbystep
|
||||
}
|
||||
|
||||
extension StepByStep2 on GeneratedDatabase {
|
||||
MigrationStrategy get migration {
|
||||
return MigrationStrategy(
|
||||
onCreate: (Migrator m) async {
|
||||
await m.createAll();
|
||||
},
|
||||
// #docregion stepbystep2
|
||||
onUpgrade: (m, from, to) async {
|
||||
// Run migration steps without foreign keys and re-enable them later
|
||||
// (https://drift.simonbinder.eu/docs/advanced-features/migrations/#tips)
|
||||
await customStatement('PRAGMA foreign_keys = OFF');
|
||||
|
||||
await m.runMigrationSteps(
|
||||
from: from,
|
||||
to: to,
|
||||
steps: migrationSteps(
|
||||
from1To2: (m, schema) async {
|
||||
// we added the dueDate property in the change from version 1 to
|
||||
// version 2
|
||||
await m.addColumn(schema.todos, schema.todos.dueDate);
|
||||
},
|
||||
from2To3: (m, schema) async {
|
||||
// we added the priority property in the change from version 1 or 2
|
||||
// to version 3
|
||||
await m.addColumn(schema.todos, schema.todos.priority);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (kDebugMode) {
|
||||
// Fail if the migration broke foreign keys
|
||||
final wrongForeignKeys =
|
||||
await customSelect('PRAGMA foreign_key_check').get();
|
||||
assert(wrongForeignKeys.isEmpty,
|
||||
'${wrongForeignKeys.map((e) => e.data)}');
|
||||
}
|
||||
|
||||
await customStatement('PRAGMA foreign_keys = ON;');
|
||||
},
|
||||
// #enddocregion stepbystep2
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension StepByStep3 on MyDatabase {
|
||||
MigrationStrategy get migration {
|
||||
return MigrationStrategy(
|
||||
onCreate: (Migrator m) async {
|
||||
await m.createAll();
|
||||
},
|
||||
// #docregion stepbystep3
|
||||
onUpgrade: (m, from, to) async {
|
||||
// Run migration steps without foreign keys and re-enable them later
|
||||
// (https://drift.simonbinder.eu/docs/advanced-features/migrations/#tips)
|
||||
await customStatement('PRAGMA foreign_keys = OFF');
|
||||
|
||||
// Manually running migrations up to schema version 2, after which we've
|
||||
// enabled step-by-step migrations.
|
||||
if (from < 2) {
|
||||
// we added the dueDate property in the change from version 1 to
|
||||
// version 2 - before switching to step-by-step migrations.
|
||||
await m.addColumn(todos, todos.dueDate);
|
||||
}
|
||||
|
||||
// At this point, we should be migrated to schema 3. For future schema
|
||||
// changes, we will "start" at schema 3.
|
||||
await m.runMigrationSteps(
|
||||
from: math.max(2, from),
|
||||
to: to,
|
||||
// ignore: missing_required_argument
|
||||
steps: migrationSteps(
|
||||
from2To3: (m, schema) async {
|
||||
// we added the priority property in the change from version 1 or
|
||||
// 2 to version 3
|
||||
await m.addColumn(schema.todos, schema.todos.priority);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (kDebugMode) {
|
||||
// Fail if the migration broke foreign keys
|
||||
final wrongForeignKeys =
|
||||
await customSelect('PRAGMA foreign_key_check').get();
|
||||
assert(wrongForeignKeys.isEmpty,
|
||||
'${wrongForeignKeys.map((e) => e.data)}');
|
||||
}
|
||||
|
||||
await customStatement('PRAGMA foreign_keys = ON;');
|
||||
},
|
||||
// #enddocregion stepbystep3
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// GENERATED CODE, DO NOT EDIT BY HAND.
|
||||
// ignore_for_file: type=lint
|
||||
//@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;
|
||||
|
||||
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);
|
||||
default:
|
||||
throw MissingSchemaException(version, const {1, 2, 3});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,232 @@
|
|||
// GENERATED CODE, DO NOT EDIT BY HAND.
|
||||
// ignore_for_file: type=lint
|
||||
//@dart=2.12
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class Todos extends Table with TableInfo<Todos, TodosData> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
Todos(this.attachedDatabase, [this._alias]);
|
||||
late final GeneratedColumn<int> id = GeneratedColumn<int>(
|
||||
'id', aliasedName, false,
|
||||
hasAutoIncrement: true,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
||||
late final GeneratedColumn<String> title = GeneratedColumn<String>(
|
||||
'title', aliasedName, false,
|
||||
additionalChecks:
|
||||
GeneratedColumn.checkTextLength(minTextLength: 6, maxTextLength: 10),
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true);
|
||||
late final GeneratedColumn<String> content = GeneratedColumn<String>(
|
||||
'body', aliasedName, false,
|
||||
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||
late final GeneratedColumn<int> category = GeneratedColumn<int>(
|
||||
'category', aliasedName, true,
|
||||
type: DriftSqlType.int, requiredDuringInsert: false);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [id, title, content, category];
|
||||
@override
|
||||
String get aliasedName => _alias ?? 'todos';
|
||||
@override
|
||||
String get actualTableName => 'todos';
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
TodosData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return TodosData(
|
||||
id: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||
title: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}title'])!,
|
||||
content: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}body'])!,
|
||||
category: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}category']),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Todos createAlias(String alias) {
|
||||
return Todos(attachedDatabase, alias);
|
||||
}
|
||||
}
|
||||
|
||||
class TodosData extends DataClass implements Insertable<TodosData> {
|
||||
final int id;
|
||||
final String title;
|
||||
final String content;
|
||||
final int? category;
|
||||
const TodosData(
|
||||
{required this.id,
|
||||
required this.title,
|
||||
required this.content,
|
||||
this.category});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['id'] = Variable<int>(id);
|
||||
map['title'] = Variable<String>(title);
|
||||
map['body'] = Variable<String>(content);
|
||||
if (!nullToAbsent || category != null) {
|
||||
map['category'] = Variable<int>(category);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
TodosCompanion toCompanion(bool nullToAbsent) {
|
||||
return TodosCompanion(
|
||||
id: Value(id),
|
||||
title: Value(title),
|
||||
content: Value(content),
|
||||
category: category == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(category),
|
||||
);
|
||||
}
|
||||
|
||||
factory TodosData.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return TodosData(
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
title: serializer.fromJson<String>(json['title']),
|
||||
content: serializer.fromJson<String>(json['content']),
|
||||
category: serializer.fromJson<int?>(json['category']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
'title': serializer.toJson<String>(title),
|
||||
'content': serializer.toJson<String>(content),
|
||||
'category': serializer.toJson<int?>(category),
|
||||
};
|
||||
}
|
||||
|
||||
TodosData copyWith(
|
||||
{int? id,
|
||||
String? title,
|
||||
String? content,
|
||||
Value<int?> category = const Value.absent()}) =>
|
||||
TodosData(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
content: content ?? this.content,
|
||||
category: category.present ? category.value : this.category,
|
||||
);
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('TodosData(')
|
||||
..write('id: $id, ')
|
||||
..write('title: $title, ')
|
||||
..write('content: $content, ')
|
||||
..write('category: $category')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, title, content, category);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is TodosData &&
|
||||
other.id == this.id &&
|
||||
other.title == this.title &&
|
||||
other.content == this.content &&
|
||||
other.category == this.category);
|
||||
}
|
||||
|
||||
class TodosCompanion extends UpdateCompanion<TodosData> {
|
||||
final Value<int> id;
|
||||
final Value<String> title;
|
||||
final Value<String> content;
|
||||
final Value<int?> category;
|
||||
const TodosCompanion({
|
||||
this.id = const Value.absent(),
|
||||
this.title = const Value.absent(),
|
||||
this.content = const Value.absent(),
|
||||
this.category = const Value.absent(),
|
||||
});
|
||||
TodosCompanion.insert({
|
||||
this.id = const Value.absent(),
|
||||
required String title,
|
||||
required String content,
|
||||
this.category = const Value.absent(),
|
||||
}) : title = Value(title),
|
||||
content = Value(content);
|
||||
static Insertable<TodosData> custom({
|
||||
Expression<int>? id,
|
||||
Expression<String>? title,
|
||||
Expression<String>? content,
|
||||
Expression<int>? category,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (title != null) 'title': title,
|
||||
if (content != null) 'body': content,
|
||||
if (category != null) 'category': category,
|
||||
});
|
||||
}
|
||||
|
||||
TodosCompanion copyWith(
|
||||
{Value<int>? id,
|
||||
Value<String>? title,
|
||||
Value<String>? content,
|
||||
Value<int?>? category}) {
|
||||
return TodosCompanion(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
content: content ?? this.content,
|
||||
category: category ?? this.category,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = Variable<int>(id.value);
|
||||
}
|
||||
if (title.present) {
|
||||
map['title'] = Variable<String>(title.value);
|
||||
}
|
||||
if (content.present) {
|
||||
map['body'] = Variable<String>(content.value);
|
||||
}
|
||||
if (category.present) {
|
||||
map['category'] = Variable<int>(category.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('TodosCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('title: $title, ')
|
||||
..write('content: $content, ')
|
||||
..write('category: $category')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseAtV1 extends GeneratedDatabase {
|
||||
DatabaseAtV1(QueryExecutor e) : super(e);
|
||||
late final Todos todos = Todos(this);
|
||||
@override
|
||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||
@override
|
||||
List<DatabaseSchemaEntity> get allSchemaEntities => [todos];
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
}
|
|
@ -0,0 +1,262 @@
|
|||
// GENERATED CODE, DO NOT EDIT BY HAND.
|
||||
// ignore_for_file: type=lint
|
||||
//@dart=2.12
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class Todos extends Table with TableInfo<Todos, TodosData> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
Todos(this.attachedDatabase, [this._alias]);
|
||||
late final GeneratedColumn<int> id = GeneratedColumn<int>(
|
||||
'id', aliasedName, false,
|
||||
hasAutoIncrement: true,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
||||
late final GeneratedColumn<String> title = GeneratedColumn<String>(
|
||||
'title', aliasedName, false,
|
||||
additionalChecks:
|
||||
GeneratedColumn.checkTextLength(minTextLength: 6, maxTextLength: 10),
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true);
|
||||
late final GeneratedColumn<String> content = GeneratedColumn<String>(
|
||||
'body', aliasedName, false,
|
||||
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||
late final GeneratedColumn<int> category = GeneratedColumn<int>(
|
||||
'category', aliasedName, true,
|
||||
type: DriftSqlType.int, requiredDuringInsert: false);
|
||||
late final GeneratedColumn<DateTime> dueDate = GeneratedColumn<DateTime>(
|
||||
'due_date', aliasedName, true,
|
||||
type: DriftSqlType.dateTime, requiredDuringInsert: false);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [id, title, content, category, dueDate];
|
||||
@override
|
||||
String get aliasedName => _alias ?? 'todos';
|
||||
@override
|
||||
String get actualTableName => 'todos';
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
TodosData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return TodosData(
|
||||
id: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||
title: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}title'])!,
|
||||
content: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}body'])!,
|
||||
category: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}category']),
|
||||
dueDate: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.dateTime, data['${effectivePrefix}due_date']),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Todos createAlias(String alias) {
|
||||
return Todos(attachedDatabase, alias);
|
||||
}
|
||||
}
|
||||
|
||||
class TodosData extends DataClass implements Insertable<TodosData> {
|
||||
final int id;
|
||||
final String title;
|
||||
final String content;
|
||||
final int? category;
|
||||
final DateTime? dueDate;
|
||||
const TodosData(
|
||||
{required this.id,
|
||||
required this.title,
|
||||
required this.content,
|
||||
this.category,
|
||||
this.dueDate});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['id'] = Variable<int>(id);
|
||||
map['title'] = Variable<String>(title);
|
||||
map['body'] = Variable<String>(content);
|
||||
if (!nullToAbsent || category != null) {
|
||||
map['category'] = Variable<int>(category);
|
||||
}
|
||||
if (!nullToAbsent || dueDate != null) {
|
||||
map['due_date'] = Variable<DateTime>(dueDate);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
TodosCompanion toCompanion(bool nullToAbsent) {
|
||||
return TodosCompanion(
|
||||
id: Value(id),
|
||||
title: Value(title),
|
||||
content: Value(content),
|
||||
category: category == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(category),
|
||||
dueDate: dueDate == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(dueDate),
|
||||
);
|
||||
}
|
||||
|
||||
factory TodosData.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return TodosData(
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
title: serializer.fromJson<String>(json['title']),
|
||||
content: serializer.fromJson<String>(json['content']),
|
||||
category: serializer.fromJson<int?>(json['category']),
|
||||
dueDate: serializer.fromJson<DateTime?>(json['dueDate']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
'title': serializer.toJson<String>(title),
|
||||
'content': serializer.toJson<String>(content),
|
||||
'category': serializer.toJson<int?>(category),
|
||||
'dueDate': serializer.toJson<DateTime?>(dueDate),
|
||||
};
|
||||
}
|
||||
|
||||
TodosData copyWith(
|
||||
{int? id,
|
||||
String? title,
|
||||
String? content,
|
||||
Value<int?> category = const Value.absent(),
|
||||
Value<DateTime?> dueDate = const Value.absent()}) =>
|
||||
TodosData(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
content: content ?? this.content,
|
||||
category: category.present ? category.value : this.category,
|
||||
dueDate: dueDate.present ? dueDate.value : this.dueDate,
|
||||
);
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('TodosData(')
|
||||
..write('id: $id, ')
|
||||
..write('title: $title, ')
|
||||
..write('content: $content, ')
|
||||
..write('category: $category, ')
|
||||
..write('dueDate: $dueDate')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, title, content, category, dueDate);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is TodosData &&
|
||||
other.id == this.id &&
|
||||
other.title == this.title &&
|
||||
other.content == this.content &&
|
||||
other.category == this.category &&
|
||||
other.dueDate == this.dueDate);
|
||||
}
|
||||
|
||||
class TodosCompanion extends UpdateCompanion<TodosData> {
|
||||
final Value<int> id;
|
||||
final Value<String> title;
|
||||
final Value<String> content;
|
||||
final Value<int?> category;
|
||||
final Value<DateTime?> dueDate;
|
||||
const TodosCompanion({
|
||||
this.id = const Value.absent(),
|
||||
this.title = const Value.absent(),
|
||||
this.content = const Value.absent(),
|
||||
this.category = const Value.absent(),
|
||||
this.dueDate = const Value.absent(),
|
||||
});
|
||||
TodosCompanion.insert({
|
||||
this.id = const Value.absent(),
|
||||
required String title,
|
||||
required String content,
|
||||
this.category = const Value.absent(),
|
||||
this.dueDate = const Value.absent(),
|
||||
}) : title = Value(title),
|
||||
content = Value(content);
|
||||
static Insertable<TodosData> custom({
|
||||
Expression<int>? id,
|
||||
Expression<String>? title,
|
||||
Expression<String>? content,
|
||||
Expression<int>? category,
|
||||
Expression<DateTime>? dueDate,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (title != null) 'title': title,
|
||||
if (content != null) 'body': content,
|
||||
if (category != null) 'category': category,
|
||||
if (dueDate != null) 'due_date': dueDate,
|
||||
});
|
||||
}
|
||||
|
||||
TodosCompanion copyWith(
|
||||
{Value<int>? id,
|
||||
Value<String>? title,
|
||||
Value<String>? content,
|
||||
Value<int?>? category,
|
||||
Value<DateTime?>? dueDate}) {
|
||||
return TodosCompanion(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
content: content ?? this.content,
|
||||
category: category ?? this.category,
|
||||
dueDate: dueDate ?? this.dueDate,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = Variable<int>(id.value);
|
||||
}
|
||||
if (title.present) {
|
||||
map['title'] = Variable<String>(title.value);
|
||||
}
|
||||
if (content.present) {
|
||||
map['body'] = Variable<String>(content.value);
|
||||
}
|
||||
if (category.present) {
|
||||
map['category'] = Variable<int>(category.value);
|
||||
}
|
||||
if (dueDate.present) {
|
||||
map['due_date'] = Variable<DateTime>(dueDate.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('TodosCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('title: $title, ')
|
||||
..write('content: $content, ')
|
||||
..write('category: $category, ')
|
||||
..write('dueDate: $dueDate')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseAtV2 extends GeneratedDatabase {
|
||||
DatabaseAtV2(QueryExecutor e) : super(e);
|
||||
late final Todos todos = Todos(this);
|
||||
@override
|
||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||
@override
|
||||
List<DatabaseSchemaEntity> get allSchemaEntities => [todos];
|
||||
@override
|
||||
int get schemaVersion => 2;
|
||||
}
|
|
@ -0,0 +1,294 @@
|
|||
// GENERATED CODE, DO NOT EDIT BY HAND.
|
||||
// ignore_for_file: type=lint
|
||||
//@dart=2.12
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class Todos extends Table with TableInfo<Todos, TodosData> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
Todos(this.attachedDatabase, [this._alias]);
|
||||
late final GeneratedColumn<int> id = GeneratedColumn<int>(
|
||||
'id', aliasedName, false,
|
||||
hasAutoIncrement: true,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
||||
late final GeneratedColumn<String> title = GeneratedColumn<String>(
|
||||
'title', aliasedName, false,
|
||||
additionalChecks:
|
||||
GeneratedColumn.checkTextLength(minTextLength: 6, maxTextLength: 10),
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true);
|
||||
late final GeneratedColumn<String> content = GeneratedColumn<String>(
|
||||
'body', aliasedName, false,
|
||||
type: DriftSqlType.string, requiredDuringInsert: true);
|
||||
late final GeneratedColumn<int> category = GeneratedColumn<int>(
|
||||
'category', aliasedName, true,
|
||||
type: DriftSqlType.int, requiredDuringInsert: false);
|
||||
late final GeneratedColumn<DateTime> dueDate = GeneratedColumn<DateTime>(
|
||||
'due_date', aliasedName, true,
|
||||
type: DriftSqlType.dateTime, requiredDuringInsert: false);
|
||||
late final GeneratedColumn<int> priority = GeneratedColumn<int>(
|
||||
'priority', aliasedName, true,
|
||||
type: DriftSqlType.int, requiredDuringInsert: false);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns =>
|
||||
[id, title, content, category, dueDate, priority];
|
||||
@override
|
||||
String get aliasedName => _alias ?? 'todos';
|
||||
@override
|
||||
String get actualTableName => 'todos';
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
TodosData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return TodosData(
|
||||
id: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||
title: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}title'])!,
|
||||
content: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}body'])!,
|
||||
category: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}category']),
|
||||
dueDate: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.dateTime, data['${effectivePrefix}due_date']),
|
||||
priority: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}priority']),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Todos createAlias(String alias) {
|
||||
return Todos(attachedDatabase, alias);
|
||||
}
|
||||
}
|
||||
|
||||
class TodosData extends DataClass implements Insertable<TodosData> {
|
||||
final int id;
|
||||
final String title;
|
||||
final String content;
|
||||
final int? category;
|
||||
final DateTime? dueDate;
|
||||
final int? priority;
|
||||
const TodosData(
|
||||
{required this.id,
|
||||
required this.title,
|
||||
required this.content,
|
||||
this.category,
|
||||
this.dueDate,
|
||||
this.priority});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['id'] = Variable<int>(id);
|
||||
map['title'] = Variable<String>(title);
|
||||
map['body'] = Variable<String>(content);
|
||||
if (!nullToAbsent || category != null) {
|
||||
map['category'] = Variable<int>(category);
|
||||
}
|
||||
if (!nullToAbsent || dueDate != null) {
|
||||
map['due_date'] = Variable<DateTime>(dueDate);
|
||||
}
|
||||
if (!nullToAbsent || priority != null) {
|
||||
map['priority'] = Variable<int>(priority);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
TodosCompanion toCompanion(bool nullToAbsent) {
|
||||
return TodosCompanion(
|
||||
id: Value(id),
|
||||
title: Value(title),
|
||||
content: Value(content),
|
||||
category: category == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(category),
|
||||
dueDate: dueDate == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(dueDate),
|
||||
priority: priority == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(priority),
|
||||
);
|
||||
}
|
||||
|
||||
factory TodosData.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return TodosData(
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
title: serializer.fromJson<String>(json['title']),
|
||||
content: serializer.fromJson<String>(json['content']),
|
||||
category: serializer.fromJson<int?>(json['category']),
|
||||
dueDate: serializer.fromJson<DateTime?>(json['dueDate']),
|
||||
priority: serializer.fromJson<int?>(json['priority']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
'title': serializer.toJson<String>(title),
|
||||
'content': serializer.toJson<String>(content),
|
||||
'category': serializer.toJson<int?>(category),
|
||||
'dueDate': serializer.toJson<DateTime?>(dueDate),
|
||||
'priority': serializer.toJson<int?>(priority),
|
||||
};
|
||||
}
|
||||
|
||||
TodosData copyWith(
|
||||
{int? id,
|
||||
String? title,
|
||||
String? content,
|
||||
Value<int?> category = const Value.absent(),
|
||||
Value<DateTime?> dueDate = const Value.absent(),
|
||||
Value<int?> priority = const Value.absent()}) =>
|
||||
TodosData(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
content: content ?? this.content,
|
||||
category: category.present ? category.value : this.category,
|
||||
dueDate: dueDate.present ? dueDate.value : this.dueDate,
|
||||
priority: priority.present ? priority.value : this.priority,
|
||||
);
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('TodosData(')
|
||||
..write('id: $id, ')
|
||||
..write('title: $title, ')
|
||||
..write('content: $content, ')
|
||||
..write('category: $category, ')
|
||||
..write('dueDate: $dueDate, ')
|
||||
..write('priority: $priority')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(id, title, content, category, dueDate, priority);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is TodosData &&
|
||||
other.id == this.id &&
|
||||
other.title == this.title &&
|
||||
other.content == this.content &&
|
||||
other.category == this.category &&
|
||||
other.dueDate == this.dueDate &&
|
||||
other.priority == this.priority);
|
||||
}
|
||||
|
||||
class TodosCompanion extends UpdateCompanion<TodosData> {
|
||||
final Value<int> id;
|
||||
final Value<String> title;
|
||||
final Value<String> content;
|
||||
final Value<int?> category;
|
||||
final Value<DateTime?> dueDate;
|
||||
final Value<int?> priority;
|
||||
const TodosCompanion({
|
||||
this.id = const Value.absent(),
|
||||
this.title = const Value.absent(),
|
||||
this.content = const Value.absent(),
|
||||
this.category = const Value.absent(),
|
||||
this.dueDate = const Value.absent(),
|
||||
this.priority = const Value.absent(),
|
||||
});
|
||||
TodosCompanion.insert({
|
||||
this.id = const Value.absent(),
|
||||
required String title,
|
||||
required String content,
|
||||
this.category = const Value.absent(),
|
||||
this.dueDate = const Value.absent(),
|
||||
this.priority = const Value.absent(),
|
||||
}) : title = Value(title),
|
||||
content = Value(content);
|
||||
static Insertable<TodosData> custom({
|
||||
Expression<int>? id,
|
||||
Expression<String>? title,
|
||||
Expression<String>? content,
|
||||
Expression<int>? category,
|
||||
Expression<DateTime>? dueDate,
|
||||
Expression<int>? priority,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (title != null) 'title': title,
|
||||
if (content != null) 'body': content,
|
||||
if (category != null) 'category': category,
|
||||
if (dueDate != null) 'due_date': dueDate,
|
||||
if (priority != null) 'priority': priority,
|
||||
});
|
||||
}
|
||||
|
||||
TodosCompanion copyWith(
|
||||
{Value<int>? id,
|
||||
Value<String>? title,
|
||||
Value<String>? content,
|
||||
Value<int?>? category,
|
||||
Value<DateTime?>? dueDate,
|
||||
Value<int?>? priority}) {
|
||||
return TodosCompanion(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
content: content ?? this.content,
|
||||
category: category ?? this.category,
|
||||
dueDate: dueDate ?? this.dueDate,
|
||||
priority: priority ?? this.priority,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = Variable<int>(id.value);
|
||||
}
|
||||
if (title.present) {
|
||||
map['title'] = Variable<String>(title.value);
|
||||
}
|
||||
if (content.present) {
|
||||
map['body'] = Variable<String>(content.value);
|
||||
}
|
||||
if (category.present) {
|
||||
map['category'] = Variable<int>(category.value);
|
||||
}
|
||||
if (dueDate.present) {
|
||||
map['due_date'] = Variable<DateTime>(dueDate.value);
|
||||
}
|
||||
if (priority.present) {
|
||||
map['priority'] = Variable<int>(priority.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('TodosCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('title: $title, ')
|
||||
..write('content: $content, ')
|
||||
..write('category: $category, ')
|
||||
..write('dueDate: $dueDate, ')
|
||||
..write('priority: $priority')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseAtV3 extends GeneratedDatabase {
|
||||
DatabaseAtV3(QueryExecutor e) : super(e);
|
||||
late final Todos todos = Todos(this);
|
||||
@override
|
||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||
@override
|
||||
List<DatabaseSchemaEntity> get allSchemaEntities => [todos];
|
||||
@override
|
||||
int get schemaVersion => 3;
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// #docregion setup
|
||||
import 'package:test/test.dart';
|
||||
import 'package:drift_dev/api/migrations.dart';
|
||||
|
||||
// The generated directory from before.
|
||||
import 'generated_migrations/schema.dart';
|
||||
|
||||
// #enddocregion setup
|
||||
import '../migrations.dart';
|
||||
// #docregion setup
|
||||
|
||||
void main() {
|
||||
late SchemaVerifier verifier;
|
||||
|
||||
setUpAll(() {
|
||||
// GeneratedHelper() was generated by drift, the verifier is an api
|
||||
// provided by drift_dev.
|
||||
verifier = SchemaVerifier(GeneratedHelper());
|
||||
});
|
||||
|
||||
test('upgrade from v1 to v2', () async {
|
||||
// Use startAt(1) to obtain a database connection with all tables
|
||||
// from the v1 schema.
|
||||
final connection = await verifier.startAt(1);
|
||||
final db = MyDatabase(connection);
|
||||
|
||||
// Use this to run a migration to v2 and then validate that the
|
||||
// database has the expected schema.
|
||||
await verifier.migrateAndValidate(db, 2);
|
||||
});
|
||||
}
|
||||
// #enddocregion setup
|
|
@ -0,0 +1,49 @@
|
|||
import 'package:test/test.dart';
|
||||
import 'package:drift_dev/api/migrations.dart';
|
||||
|
||||
import '../migrations.dart';
|
||||
import 'generated_migrations/schema.dart';
|
||||
|
||||
// #docregion imports
|
||||
import 'generated_migrations/schema_v1.dart' as v1;
|
||||
import 'generated_migrations/schema_v2.dart' as v2;
|
||||
// #enddocregion imports
|
||||
|
||||
// #docregion main
|
||||
void main() {
|
||||
// #enddocregion main
|
||||
late SchemaVerifier verifier;
|
||||
|
||||
setUpAll(() {
|
||||
// GeneratedHelper() was generated by drift, the verifier is an api
|
||||
// provided by drift_dev.
|
||||
verifier = SchemaVerifier(GeneratedHelper());
|
||||
});
|
||||
|
||||
// #docregion main
|
||||
// ...
|
||||
test('upgrade from v1 to v2', () async {
|
||||
final schema = await verifier.schemaAt(1);
|
||||
|
||||
// Add some data to the table being migrated
|
||||
final oldDb = v1.DatabaseAtV1(schema.newConnection());
|
||||
await oldDb.into(oldDb.todos).insert(v1.TodosCompanion.insert(
|
||||
title: 'my first todo entry',
|
||||
content: 'should still be there after the migration',
|
||||
));
|
||||
await oldDb.close();
|
||||
|
||||
// Run the migration and verify that it adds the name column.
|
||||
final db = MyDatabase(schema.newConnection());
|
||||
await verifier.migrateAndValidate(db, 2);
|
||||
await db.close();
|
||||
|
||||
// Make sure the entry is still here
|
||||
final migratedDb = v2.DatabaseAtV2(schema.newConnection());
|
||||
final entry = await migratedDb.select(migratedDb.todos).getSingle();
|
||||
expect(entry.id, 1);
|
||||
expect(entry.dueDate, isNull); // default from the migration
|
||||
await migratedDb.close();
|
||||
});
|
||||
}
|
||||
// #enddocregion main
|
|
@ -1,528 +0,0 @@
|
|||
---
|
||||
data:
|
||||
title: "Migrations"
|
||||
weight: 10
|
||||
description: Define what happens when your database gets created or updated
|
||||
aliases:
|
||||
- /migrations
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
||||
As your app grows, you may want to change the table structure for your drift database:
|
||||
New features need new columns or tables, and outdated columns may have to be altered or
|
||||
removed altogether.
|
||||
When making changes to your database schema, you need to write migrations enabling users with
|
||||
an old version of your app to convert to the database expected by the latest version.
|
||||
With incorrect migrations, your database ends up in an inconsistent state which can cause crashes
|
||||
or data loss. This is why drift provides dedicated test tools and APIs to make writing migrations
|
||||
easy and safe.
|
||||
|
||||
{% assign snippets = 'package:drift_docs/snippets/migrations/migrations.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
## Manual setup {#basics}
|
||||
|
||||
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`
|
||||
getter.
|
||||
|
||||
Here's an example: Let's say you wanted to add a due date to your todo entries (`v2` of the schema).
|
||||
Later, you decide to also add a priority column (`v3` of the schema).
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'table' %}
|
||||
|
||||
We can now change the `database` class like this:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'start' %}
|
||||
|
||||
You can also add individual tables or drop them - see the reference of [Migrator](https://pub.dev/documentation/drift/latest/drift/Migrator-class.html)
|
||||
for all the available options.
|
||||
|
||||
You can also use higher-level query APIs like `select`, `update` or `delete` inside a migration callback.
|
||||
However, be aware that drift expects the latest schema when creating SQL statements or mapping results.
|
||||
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.
|
||||
|
||||
`sqlite` can feel a bit limiting when it comes to migrations - there only are methods to create tables and columns.
|
||||
Existing columns can't be altered or removed. A workaround is described [here](https://stackoverflow.com/a/805508), it
|
||||
can be used together with `customStatement` to run the statements.
|
||||
Alternatively, [complex migrations](#complex-migrations) described on this page help automating this.
|
||||
|
||||
### 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:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'structured' %}
|
||||
|
||||
## Migration workflow
|
||||
|
||||
While migrations can be written manually without additional help from drift, dedicated tools testing your migrations help
|
||||
to ensure that they are correct and aren't loosing any data.
|
||||
|
||||
Drift's migration tooling consists of the following steps:
|
||||
|
||||
1. After each change to your schema, use a tool to export the current schema into a separate file.
|
||||
2. Use a drift tool to generate test code able to verify that your migrations are bringing the database
|
||||
into the expected schema.
|
||||
3. Use generated code to make writing schema migrations easier.
|
||||
|
||||
### Setup
|
||||
|
||||
As described by the first step, you can export the schema of your database into a JSON file.
|
||||
It is recommended to do this once intially, and then again each time you change your schema
|
||||
and increase the `schemaVersion` getter in the database.
|
||||
|
||||
You should store these exported files in your repository and include them in source control.
|
||||
This guide assumes a top-level `drift_schemas/` folder in your project, like this:
|
||||
|
||||
```
|
||||
my_app
|
||||
.../
|
||||
lib/
|
||||
database/
|
||||
database.dart
|
||||
database.g.dart
|
||||
test/
|
||||
generated_migrations/
|
||||
schema.dart
|
||||
schema_v1.dart
|
||||
schema_v2.dart
|
||||
drift_schemas/
|
||||
drift_schema_v1.json
|
||||
drift_schema_v2.json
|
||||
pubspec.yaml
|
||||
```
|
||||
|
||||
Of course, you can also use another folder or a subfolder somewhere if that suits your workflow
|
||||
better.
|
||||
|
||||
{% block "blocks/alert" title="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).
|
||||
{% endblock %}
|
||||
|
||||
#### Exporting the schema
|
||||
|
||||
To begin, lets create the first schema representation:
|
||||
|
||||
```
|
||||
$ mkdir drift_schemas
|
||||
$ dart run drift_dev schema dump lib/database/database.dart drift_schemas/
|
||||
```
|
||||
|
||||
This instructs the generator to look at the database defined in `lib/database/database.dart` and extract
|
||||
its schema into the new folder.
|
||||
|
||||
After making a change to your database schema, you can run the command again. For instance, let's say we
|
||||
made a change to our tables and increased the `schemaVersion` to `2`. To dump the new schema, just run the
|
||||
command again:
|
||||
|
||||
```
|
||||
$ dart run drift_dev schema dump lib/database/database.dart drift_schemas/
|
||||
```
|
||||
|
||||
You'll need to run this command every time you change the schema of your database and increment the `schemaVersion`.
|
||||
|
||||
Drift will name the files in the folder `drift_schema_vX.json`, where `X` is the current `schemaVersion` of your
|
||||
database.
|
||||
If drift is unable to extract the version from your `schemaVersion` getter, provide the full path explicitly:
|
||||
|
||||
```
|
||||
$ dart run drift_dev schema dump lib/database/database.dart drift_schemas/drift_schema_v3.json
|
||||
```
|
||||
|
||||
{% block "blocks/alert" title='<i class="fas fa-lightbulb"></i> Dumping a database' color="success" %}
|
||||
If, instead of exporting the schema of a database class, you want to export the schema of an existing sqlite3
|
||||
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.
|
||||
{% endblock %}
|
||||
|
||||
### Generating step-by-step migrations {#step-by-step}
|
||||
|
||||
With all your database schemas exported into a folder, drift can generate code that makes it much
|
||||
easier to write schema migrations "step-by-step" (incrementally from each version to the next one).
|
||||
|
||||
This code is stored in a single-file, which you can generate like this:
|
||||
|
||||
```
|
||||
$ dart run drift_dev schema steps drift_schemas/ lib/database/schema_versions.dart
|
||||
```
|
||||
|
||||
The generated code contains a `stepByStep` method which you can use as a callback to the `onUpgrade`
|
||||
parameter of your `MigrationStrategy`.
|
||||
As an example, here is the [initial](#basics) migration shown at the top of this page, but rewritten using
|
||||
the generated `stepByStep` function:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'stepbystep' %}
|
||||
|
||||
`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
|
||||
`OnUpgrade` callback.
|
||||
But you might want to customize the upgrade behavior, for instance by adding foreign key
|
||||
checks afterwards (as described in [tips](#tips)).
|
||||
|
||||
The `Migrator.runMigrationSteps` helper method can be used for that, as this example
|
||||
shows:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'stepbystep2' %}
|
||||
|
||||
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
|
||||
in debug modes.
|
||||
|
||||
#### Moving to step-by-step migrations
|
||||
|
||||
If you've been using drift before `stepByStep` was added to the library, or if you've never exported a schema,
|
||||
you can move to step-by-step migrations by pinning the `from` value in `Migrator.runMigrationSteps` to a known
|
||||
starting point.
|
||||
|
||||
This allows you to perform all prior migration work to get the database to the "starting" point for
|
||||
`stepByStep` migrations, and then use `stepByStep` migrations beyond that schema version.
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'stepbystep3' %}
|
||||
|
||||
Here, we give a "floor" to the `from` value of `2`, since we've performed all other migration work to get to
|
||||
this point. From now on, you can generate step-by-step migrations for each schema change.
|
||||
|
||||
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.
|
||||
|
||||
### Writing tests
|
||||
|
||||
After you've exported the database schemas into a folder, you can generate old versions of your database class
|
||||
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/`.
|
||||
If we wanted to write them to `test/generated_migrations/`, we could use
|
||||
|
||||
```
|
||||
$ dart run drift_dev schema generate drift_schemas/ test/generated_migrations/
|
||||
```
|
||||
|
||||
After that setup, it's finally time to write some tests! For instance, a test could look like this:
|
||||
|
||||
```dart
|
||||
import 'package:my_app/database/database.dart';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:drift_dev/api/migrations.dart';
|
||||
|
||||
// The generated directory from before.
|
||||
import 'generated_migrations/schema.dart';
|
||||
|
||||
void main() {
|
||||
late SchemaVerifier verifier;
|
||||
|
||||
setUpAll(() {
|
||||
// GeneratedHelper() was generated by drift, the verifier is an api
|
||||
// provided by drift_dev.
|
||||
verifier = SchemaVerifier(GeneratedHelper());
|
||||
});
|
||||
|
||||
test('upgrade from v1 to v2', () async {
|
||||
// Use startAt(1) to obtain a database connection with all tables
|
||||
// from the v1 schema.
|
||||
final connection = await verifier.startAt(1);
|
||||
final db = MyDatabase(connection);
|
||||
|
||||
// Use this to run a migration to v2 and then validate that the
|
||||
// database has the expected schema.
|
||||
await verifier.migrateAndValidate(db, 2);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
In general, a test looks like this:
|
||||
|
||||
1. Use `verifier.startAt()` to obtain a [connection](https://drift.simonbinder.eu/api/drift/databaseconnection-class)
|
||||
to a database with an initial schema.
|
||||
This database contains all your tables, indices and triggers from that version, created by using `Migrator.createAll`.
|
||||
2. Create your application database with that connection. For this, create a constructor in your database class that
|
||||
accepts a `QueryExecutor` and forwards it to the super constructor in `GeneratedDatabase`.
|
||||
Then, you can pass the result of calling `newConnection()` to that constructor to create a test instance of your
|
||||
datbaase.
|
||||
3. Call `verifier.migrateAndValidate(db, version)`. This will initiate a migration towards the target version (here, `2`).
|
||||
Unlike the database created by `startAt`, this uses the migration logic you wrote for your database.
|
||||
|
||||
`migrateAndValidate` will extract all `CREATE` statement from the `sqlite_schema` table and semantically compare them.
|
||||
If it sees anything unexpected, it will throw a `SchemaMismatch` exception to fail your test.
|
||||
|
||||
{% block "blocks/alert" title="Writing testable migrations" %}
|
||||
To test migrations _towards_ an old schema version (e.g. from `v1` to `v2` if your current version is `v3`),
|
||||
you're `onUpgrade` handler must be capable of upgrading to a version older than the current `schemaVersion`.
|
||||
For this, check the `to` parameter of the `onUpgrade` callback to run a different migration if necessary.
|
||||
{% endblock %}
|
||||
|
||||
#### 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
|
||||
is still there after it ran.
|
||||
You can use `schemaAt` to obtain a raw `Database` from the `sqlite3` package in addition to a connection.
|
||||
This can be used to insert data before a migration. After the migration ran, you can then check that the data is still there.
|
||||
|
||||
Note that you can't use the regular database class from you app for this, since its data classes always expect the latest
|
||||
schema. However, you can instruct drift to generate older snapshots of your data classes and companions for this purpose.
|
||||
To enable this feature, pass the `--data-classes` and `--companions` command-line arguments to the `drift_dev schema generate`
|
||||
command:
|
||||
|
||||
```
|
||||
$ dart run drift_dev schema generate --data-classes --companions drift_schemas/ test/generated_migrations/
|
||||
```
|
||||
|
||||
Then, you can import the generated classes with an alias:
|
||||
|
||||
```dart
|
||||
import 'generated_migrations/schema_v1.dart' as v1;
|
||||
import 'generated_migrations/schema_v2.dart' as v2;
|
||||
```
|
||||
|
||||
This can then be used to manually create and verify data at a specific version:
|
||||
|
||||
```dart
|
||||
void main() {
|
||||
// ...
|
||||
test('upgrade 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();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Complex migrations
|
||||
|
||||
Sqlite has builtin statements for simple changes, like adding columns or dropping entire tables.
|
||||
More complex migrations require a [12-step procedure](https://www.sqlite.org/lang_altertable.html#otheralter) that
|
||||
involves creating a copy of the table and copying over data from the old table.
|
||||
Drift 3.4 introduced the `TableMigration` api to automate most of this procedure, making it easier and safer to use.
|
||||
|
||||
To start the migration, drift will create a new instance of the table with the current schema. Next, it will copy over
|
||||
rows from the old table.
|
||||
In most cases, for instance when changing column types, we can't just copy over each row without changing its content.
|
||||
Here, you can use a `columnTransformer` to apply a per-row transformation.
|
||||
The `columnTransformer` is a map from columns to the sql expression that will be used to copy the column from the
|
||||
old table.
|
||||
For instance, if we wanted to cast a column before copying it, we could use:
|
||||
|
||||
```dart
|
||||
columnTransformer: {
|
||||
todos.category: todos.category.cast<int>(),
|
||||
}
|
||||
```
|
||||
|
||||
Internally, drift will use a `INSERT INTO SELECT` statement to copy old data. In this case, it would look like
|
||||
`INSERT INTO temporary_todos_copy SELECT id, title, content, CAST(category AS INT) FROM todos`.
|
||||
As you can see, drift will use the expression from the `columnTransformer` map and fall back to just copying the column
|
||||
otherwise.
|
||||
If you're introducing new columns in a table migration, be sure to include them in the `newColumns` parameter of
|
||||
`TableMigration`. Drift will ensure that those columns have a default value or a transformation in `columnTransformer`.
|
||||
Of course, drift won't attempt to copy `newColumns` from the old table either.
|
||||
|
||||
Regardless of whether you're implementing complex migrations with `TableMigration` or by running a custom sequence
|
||||
of statements, we strongly recommend to write integration tests covering your migrations. This helps to avoid data
|
||||
loss caused by errors in a migration.
|
||||
|
||||
Here are some examples demonstrating common usages of the table migration api:
|
||||
|
||||
### Changing the type of a column
|
||||
|
||||
Let's say the `category` column in `Todos` used to be a non-nullable `text()` column that we're now changing to a
|
||||
nullable int. For simplicity, we assume that `category` always contained integers, they were just stored in a text
|
||||
column that we now want to adapt.
|
||||
|
||||
```patch
|
||||
class Todos extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get title => text().withLength(min: 6, max: 10)();
|
||||
TextColumn get content => text().named('body')();
|
||||
- IntColumn get category => text()();
|
||||
+ IntColumn get category => integer().nullable()();
|
||||
}
|
||||
```
|
||||
|
||||
After re-running your build and incrementing the schema version, you can write a migration:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'change_type' %}
|
||||
|
||||
The important part here is the `columnTransformer` - a map from columns to expressions that will
|
||||
be used to copy the old data. The values in that map refer to the old table, so we can use
|
||||
`todos.category.cast<int>()` to copy old rows and transform their `category`.
|
||||
All columns that aren't present in `columnTransformer` will be copied from the old table without
|
||||
any transformation.
|
||||
|
||||
### Changing column constraints
|
||||
|
||||
When you're changing columns constraints in a way that's compatible to existing data (e.g. changing
|
||||
non-nullable columns to nullable columns), you can just copy over data without applying any
|
||||
transformation:
|
||||
|
||||
```dart
|
||||
await m.alterTable(TableMigration(todos));
|
||||
```
|
||||
|
||||
### Deleting columns
|
||||
|
||||
Deleting a column that's not referenced by a foreign key constraint is easy too:
|
||||
|
||||
```dart
|
||||
await m.alterTable(TableMigration(yourTable));
|
||||
```
|
||||
|
||||
To delete a column referenced by a foreign key, you'd have to migrate the referencing
|
||||
tables first.
|
||||
|
||||
### Renaming columns
|
||||
|
||||
If you're renaming a column in Dart, note that the easiest way is to just rename the getter and use
|
||||
`named`: `TextColumn newName => text().named('old_name')()`. That is fully backwards compatible and
|
||||
doesn't require a migration.
|
||||
|
||||
If you know your app runs on sqlite 3.25.0 or later (it does if you're using `sqlite3_flutter_libs`),
|
||||
you can also use the `renameColumn` api in `Migrator`:
|
||||
|
||||
```dart
|
||||
m.renameColumn(yourTable, 'old_column_name', yourTable.newColumn);
|
||||
```
|
||||
|
||||
If you do want to change the actual column name in a table, you can write a `columnTransformer` to
|
||||
use an old column with a different name:
|
||||
|
||||
```dart
|
||||
await m.alterTable(
|
||||
TableMigration(
|
||||
yourTable,
|
||||
columnTransformer: {
|
||||
yourTable.newColumn: const CustomExpression('old_column_name')
|
||||
},
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
## Migrating views, triggers and indices
|
||||
|
||||
When changing the definition of a view, a trigger or an index, the easiest way
|
||||
to update the database schema is to drop and re-create the element.
|
||||
With the `Migrator` API, this is just a matter of calling `await drop(element)`
|
||||
followed by `await create(element)`, where `element` is the trigger, view or index
|
||||
to update.
|
||||
|
||||
Note that the definition of a Dart-defined view might change without modifications
|
||||
to the view class itself. This is because columns from a table are referenced with
|
||||
a getter. When renaming a column through `.named('name')` in a table definition
|
||||
without renaming the getter, the view definition in Dart stays the same but the
|
||||
`CREATE VIEW` statement changes.
|
||||
|
||||
A headache-free solution to this problem is to just re-create all views in a
|
||||
migration, for which the `Migrator` provides the `recreateAllViews` method.
|
||||
|
||||
## 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,
|
||||
regardless of whether a migration actually ran or not. You can use `details.hadUpgrade` or `details.wasCreated` to
|
||||
check whether migrations were necessary:
|
||||
|
||||
```dart
|
||||
beforeOpen: (details) async {
|
||||
if (details.wasCreated) {
|
||||
final workId = await into(categories).insert(Category(description: 'Work'));
|
||||
|
||||
await into(todos).insert(TodoEntry(
|
||||
content: 'A first todo entry',
|
||||
category: null,
|
||||
targetDate: DateTime.now(),
|
||||
));
|
||||
|
||||
await into(todos).insert(
|
||||
TodoEntry(
|
||||
content: 'Rework persistence code',
|
||||
category: workId,
|
||||
targetDate: DateTime.now().add(const Duration(days: 4)),
|
||||
));
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
You could also activate pragma statements that you need:
|
||||
|
||||
```dart
|
||||
beforeOpen: (details) async {
|
||||
if (details.wasCreated) {
|
||||
// ...
|
||||
}
|
||||
await customStatement('PRAGMA foreign_keys = ON');
|
||||
}
|
||||
```
|
||||
|
||||
## 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-migrations) 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.
|
||||
|
||||
{% assign runtime_snippet = 'package:drift_docs/snippets/migrations/runtime_verification.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
{% include "blocks/snippet" snippets = runtime_snippet name = '' %}
|
||||
|
||||
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.
|
|
@ -69,4 +69,4 @@ The generated file (`schema.json` in this case) contains information about all
|
|||
- dependencies thereof
|
||||
|
||||
Exporting a schema can be used to generate test code for your schema migrations. For details,
|
||||
see [the guide]({{ "Advanced Features/migrations.md#verifying-migrations" | pageUrl }}).
|
||||
see [the guide]({{ "Migrations/tests.md" | pageUrl }}).
|
|
@ -232,7 +232,7 @@ should happen when the target row gets updated or deleted.
|
|||
|
||||
Be aware that, in sqlite3, foreign key references aren't enabled by default.
|
||||
They need to be enabled with `PRAGMA foreign_keys = ON`.
|
||||
A suitable place to issue that pragma with drift is in a [post-migration callback]({{ '../Advanced Features/migrations.md#post-migration-callbacks' | pageUrl }}).
|
||||
A suitable place to issue that pragma with drift is in a [post-migration callback]({{ '../Migrations/index.md#post-migration-callbacks' | pageUrl }}).
|
||||
|
||||
## Default values
|
||||
|
||||
|
@ -286,7 +286,7 @@ In Dart, the `check` method on the column builder adds a check constraint to the
|
|||
```
|
||||
|
||||
Note that these `CHECK` constraints are part of the `CREATE TABLE` statement.
|
||||
If you want to change or remove a `check` constraint, write a [schema migration]({{ '../Advanced Features/migrations.md#changing-column-constraints' | pageUrl }}) to re-create the table without the constraint.
|
||||
If you want to change or remove a `check` constraint, write a [schema migration]({{ '../Migrations/api.md#changing-column-constraints' | pageUrl }}) to re-create the table without the constraint.
|
||||
|
||||
### Unique column
|
||||
|
||||
|
|
|
@ -51,5 +51,5 @@ Additional patterns are also shown and explained on this website:
|
|||
[web_worker]: https://github.com/simolus3/drift/tree/develop/examples/web_worker_example
|
||||
[flutter_web_worker]: https://github.com/simolus3/drift/tree/develop/examples/flutter_web_worker_example
|
||||
[migration]: https://github.com/simolus3/drift/tree/develop/examples/migrations_example
|
||||
[migration tooling]: {{ '../Advanced Features/migrations.md#verifying-migrations' | pageUrl }}
|
||||
[migration tooling]: {{ '../Migrations/tests.md#verifying-migrations' | pageUrl }}
|
||||
[with_built_value]: https://github.com/simolus3/drift/tree/develop/examples/with_built_value
|
||||
|
|
|
@ -87,7 +87,7 @@ further guides to help you learn more:
|
|||
|
||||
- The [SQL IDE]({{ "../Using SQL/sql_ide.md" | pageUrl }}) that provides feedback on sql queries right in your editor.
|
||||
- [Transactions]({{ "../Dart API/transactions.md" | pageUrl }})
|
||||
- [Schema migrations]({{ "../Advanced Features/migrations.md" | pageUrl }})
|
||||
- [Schema migrations]({{ "../Migrations/index.md" | pageUrl }})
|
||||
- Writing [queries]({{ "../Dart API/select.md" | pageUrl }}) and
|
||||
[expressions]({{ "../Dart API/expressions.md" | pageUrl }}) in Dart
|
||||
- A more [in-depth guide]({{ "../Using SQL/drift_files.md" | pageUrl }})
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
---
|
||||
data:
|
||||
title: "The migrator API"
|
||||
weight: 50
|
||||
description: How to run `ALTER` statements and complex table migrations.
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
||||
{% assign snippets = 'package:drift_docs/snippets/migrations/migrations.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
You can write migrations manually by using `customStatement()` in a migration
|
||||
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.
|
||||
|
||||
## Migrating views, triggers and indices
|
||||
|
||||
When changing the definition of a view, a trigger or an index, the easiest way
|
||||
to update the database schema is to drop and re-create the element.
|
||||
With the `Migrator` API, this is just a matter of calling `await drop(element)`
|
||||
followed by `await create(element)`, where `element` is the trigger, view or index
|
||||
to update.
|
||||
|
||||
Note that the definition of a Dart-defined view might change without modifications
|
||||
to the view class itself. This is because columns from a table are referenced with
|
||||
a getter. When renaming a column through `.named('name')` in a table definition
|
||||
without renaming the getter, the view definition in Dart stays the same but the
|
||||
`CREATE VIEW` statement changes.
|
||||
|
||||
A headache-free solution to this problem is to just re-create all views in a
|
||||
migration, for which the `Migrator` provides the `recreateAllViews` method.
|
||||
|
||||
## Complex migrations
|
||||
|
||||
Sqlite has builtin statements for simple changes, like adding columns or dropping entire tables.
|
||||
More complex migrations require a [12-step procedure](https://www.sqlite.org/lang_altertable.html#otheralter) that
|
||||
involves creating a copy of the table and copying over data from the old table.
|
||||
Drift 2.4 introduced the `TableMigration` API to automate most of this procedure, making it easier and safer to use.
|
||||
|
||||
To start the migration, drift will create a new instance of the table with the current schema. Next, it will copy over
|
||||
rows from the old table.
|
||||
In most cases, for instance when changing column types, we can't just copy over each row without changing its content.
|
||||
Here, you can use a `columnTransformer` to apply a per-row transformation.
|
||||
The `columnTransformer` is a map from columns to the sql expression that will be used to copy the column from the
|
||||
old table.
|
||||
For instance, if we wanted to cast a column before copying it, we could use:
|
||||
|
||||
```dart
|
||||
columnTransformer: {
|
||||
todos.category: todos.category.cast<int>(),
|
||||
}
|
||||
```
|
||||
|
||||
Internally, drift will use a `INSERT INTO SELECT` statement to copy old data. In this case, it would look like
|
||||
`INSERT INTO temporary_todos_copy SELECT id, title, content, CAST(category AS INT) FROM todos`.
|
||||
As you can see, drift will use the expression from the `columnTransformer` map and fall back to just copying the column
|
||||
otherwise.
|
||||
If you're introducing new columns in a table migration, be sure to include them in the `newColumns` parameter of
|
||||
`TableMigration`. Drift will ensure that those columns have a default value or a transformation in `columnTransformer`.
|
||||
Of course, drift won't attempt to copy `newColumns` from the old table either.
|
||||
|
||||
Regardless of whether you're implementing complex migrations with `TableMigration` or by running a custom sequence
|
||||
of statements, we strongly recommend to write integration tests covering your migrations. This helps to avoid data
|
||||
loss caused by errors in a migration.
|
||||
|
||||
Here are some examples demonstrating common usages of the table migration api:
|
||||
|
||||
### Changing the type of a column
|
||||
|
||||
Let's say the `category` column in `Todos` used to be a non-nullable `text()` column that we're now changing to a
|
||||
nullable int. For simplicity, we assume that `category` always contained integers, they were just stored in a text
|
||||
column that we now want to adapt.
|
||||
|
||||
```patch
|
||||
class Todos extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get title => text().withLength(min: 6, max: 10)();
|
||||
TextColumn get content => text().named('body')();
|
||||
- IntColumn get category => text()();
|
||||
+ IntColumn get category => integer().nullable()();
|
||||
}
|
||||
```
|
||||
|
||||
After re-running your build and incrementing the schema version, you can write a migration:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'change_type' %}
|
||||
|
||||
The important part here is the `columnTransformer` - a map from columns to expressions that will
|
||||
be used to copy the old data. The values in that map refer to the old table, so we can use
|
||||
`todos.category.cast<int>()` to copy old rows and transform their `category`.
|
||||
All columns that aren't present in `columnTransformer` will be copied from the old table without
|
||||
any transformation.
|
||||
|
||||
### Changing column constraints
|
||||
|
||||
When you're changing columns constraints in a way that's compatible to existing data (e.g. changing
|
||||
non-nullable columns to nullable columns), you can just copy over data without applying any
|
||||
transformation:
|
||||
|
||||
```dart
|
||||
await m.alterTable(TableMigration(todos));
|
||||
```
|
||||
|
||||
### Deleting columns
|
||||
|
||||
Deleting a column that's not referenced by a foreign key constraint is easy too:
|
||||
|
||||
```dart
|
||||
await m.alterTable(TableMigration(yourTable));
|
||||
```
|
||||
|
||||
To delete a column referenced by a foreign key, you'd have to migrate the referencing
|
||||
tables first.
|
||||
|
||||
### Renaming columns
|
||||
|
||||
If you're renaming a column in Dart, note that the easiest way is to just rename the getter and use
|
||||
`named`: `TextColumn newName => text().named('old_name')()`. That is fully backwards compatible and
|
||||
doesn't require a migration.
|
||||
|
||||
If you know your app runs on sqlite 3.25.0 or later (it does if you're using `sqlite3_flutter_libs`),
|
||||
you can also use the `renameColumn` api in `Migrator`:
|
||||
|
||||
```dart
|
||||
m.renameColumn(yourTable, 'old_column_name', yourTable.newColumn);
|
||||
```
|
||||
|
||||
If you do want to change the actual column name in a table, you can write a `columnTransformer` to
|
||||
use an old column with a different name:
|
||||
|
||||
```dart
|
||||
await m.alterTable(
|
||||
TableMigration(
|
||||
yourTable,
|
||||
columnTransformer: {
|
||||
yourTable.newColumn: const CustomExpression('old_column_name')
|
||||
},
|
||||
)
|
||||
)
|
||||
```
|
|
@ -0,0 +1,103 @@
|
|||
---
|
||||
data:
|
||||
title: "Exporting schemas"
|
||||
weight: 10
|
||||
description: Store all schema versions of your app for validation.
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
||||
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.
|
||||
Later, drift tools can take a look at all the schema files to validate the migrations
|
||||
you write.
|
||||
|
||||
We recommend exporting the initial schema once. Afterwards, each changed schema version
|
||||
(that is, every time you change the `schemaVersion` in the database) should also be
|
||||
stored.
|
||||
This guide assumes a top-level `drift_schemas/` folder in your project to store these
|
||||
schema files, like this:
|
||||
|
||||
```
|
||||
my_app
|
||||
.../
|
||||
lib/
|
||||
database/
|
||||
database.dart
|
||||
database.g.dart
|
||||
test/
|
||||
generated_migrations/
|
||||
schema.dart
|
||||
schema_v1.dart
|
||||
schema_v2.dart
|
||||
drift_schemas/
|
||||
drift_schema_v1.json
|
||||
drift_schema_v2.json
|
||||
pubspec.yaml
|
||||
```
|
||||
|
||||
Of course, you can also use another folder or a subfolder somewhere if that suits your workflow
|
||||
better.
|
||||
|
||||
{% block "blocks/alert" title="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).
|
||||
{% endblock %}
|
||||
|
||||
## Exporting the schema
|
||||
|
||||
To begin, lets create the first schema representation:
|
||||
|
||||
```
|
||||
$ mkdir drift_schemas
|
||||
$ dart run drift_dev schema dump lib/database/database.dart drift_schemas/
|
||||
```
|
||||
|
||||
This instructs the generator to look at the database defined in `lib/database/database.dart` and extract
|
||||
its schema into the new folder.
|
||||
|
||||
After making a change to your database schema, you can run the command again. For instance, let's say we
|
||||
made a change to our tables and increased the `schemaVersion` to `2`. To dump the new schema, just run the
|
||||
command again:
|
||||
|
||||
```
|
||||
$ dart run drift_dev schema dump lib/database/database.dart drift_schemas/
|
||||
```
|
||||
|
||||
You'll need to run this command every time you change the schema of your database and increment the `schemaVersion`.
|
||||
|
||||
Drift will name the files in the folder `drift_schema_vX.json`, where `X` is the current `schemaVersion` of your
|
||||
database.
|
||||
If drift is unable to extract the version from your `schemaVersion` getter, provide the full path explicitly:
|
||||
|
||||
```
|
||||
$ dart run drift_dev schema dump lib/database/database.dart drift_schemas/drift_schema_v3.json
|
||||
```
|
||||
|
||||
{% block "blocks/alert" title='<i class="fas fa-lightbulb"></i> Dumping a database' color="success" %}
|
||||
If, instead of exporting the schema of a database class, you want to export the schema of an existing sqlite3
|
||||
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.
|
||||
{% endblock %}
|
||||
|
||||
## What now?
|
||||
|
||||
Having exported your schema versions into files like this, drift tools are able
|
||||
to generate code aware of multiple schema versions.
|
||||
|
||||
This enables [step-by-step migrations]({{ 'step_by_step.md' | pageUrl }}): Drift
|
||||
can generate boilerplate code for every schema migration you need to write, so that
|
||||
you only need to fill in what has actually changed. This makes writing migrations
|
||||
much easier.
|
||||
|
||||
By knowing all schema versions, drift can also [generate test code]({{'tests.md' | pageUrl}}),
|
||||
which makes it easy to write unit tests for all your schema migrations.
|
|
@ -1,7 +1,130 @@
|
|||
---
|
||||
data:
|
||||
title: Migrations
|
||||
description: Simple guide to get a drift project up and running.
|
||||
hide_section_index: true
|
||||
description: Tooling and APIs to safely change the schema of your database.
|
||||
template: layouts/docs/list
|
||||
aliases:
|
||||
- /migrations
|
||||
---
|
||||
|
||||
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.
|
||||
|
||||
{% assign snippets = 'package:drift_docs/snippets/migrations/migrations.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
## Manual setup {#basics}
|
||||
|
||||
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`
|
||||
getter.
|
||||
|
||||
Here's an example: Let's say you wanted to add a due date to your todo entries (`v2` of the schema).
|
||||
Later, you decide to also add a priority column (`v3` of the schema).
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'table' %}
|
||||
|
||||
We can now change the `database` class like this:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'start' %}
|
||||
|
||||
You can also add individual tables or drop them - see the reference of [Migrator](https://pub.dev/documentation/drift/latest/drift/Migrator-class.html)
|
||||
for all the available options.
|
||||
|
||||
You can also use higher-level query APIs like `select`, `update` or `delete` inside a migration callback.
|
||||
However, be aware that drift expects the latest schema when creating SQL statements or mapping results.
|
||||
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' | pageUrl }}) to then use easy
|
||||
[step-by-step migrations]({{ 'step_by_step.md' | pageUrl }}) or [tests]({{ 'tests.md' | pageUrl }}).
|
||||
|
||||
## General tips {#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:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'structured' %}
|
||||
|
||||
## 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,
|
||||
regardless of whether a migration actually ran or not. You can use `details.hadUpgrade` or `details.wasCreated` to
|
||||
check whether migrations were necessary:
|
||||
|
||||
```dart
|
||||
beforeOpen: (details) async {
|
||||
if (details.wasCreated) {
|
||||
final workId = await into(categories).insert(Category(description: 'Work'));
|
||||
|
||||
await into(todos).insert(TodoEntry(
|
||||
content: 'A first todo entry',
|
||||
category: null,
|
||||
targetDate: DateTime.now(),
|
||||
));
|
||||
|
||||
await into(todos).insert(
|
||||
TodoEntry(
|
||||
content: 'Rework persistence code',
|
||||
category: workId,
|
||||
targetDate: DateTime.now().add(const Duration(days: 4)),
|
||||
));
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
You could also activate pragma statements that you need:
|
||||
|
||||
```dart
|
||||
beforeOpen: (details) async {
|
||||
if (details.wasCreated) {
|
||||
// ...
|
||||
}
|
||||
await customStatement('PRAGMA foreign_keys = ON');
|
||||
}
|
||||
```
|
||||
|
||||
## 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-migrations) 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.
|
||||
|
||||
{% assign runtime_snippet = 'package:drift_docs/snippets/migrations/runtime_verification.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
{% include "blocks/snippet" snippets = runtime_snippet name = '' %}
|
||||
|
||||
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.
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
---
|
||||
data:
|
||||
title: "Schema migration helpers"
|
||||
weight: 20
|
||||
description: Use generated code reflecting over all schema versions to write migrations step-by-step.
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
||||
{% assign snippets = 'package:drift_docs/snippets/migrations/step_by_step.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
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.
|
||||
|
||||
Reliably writing migrations between app versions isn't easy though. This code needs to be
|
||||
maintained and tested, but the growing complexity of the database schema shouldn't make
|
||||
migrations more complex.
|
||||
Let's take a look at a typical example making the incremental migrations pattern hard:
|
||||
|
||||
1. In the initial database schema, we have a bunch of tables.
|
||||
2. In the migration from 1 to 2, we add a column `birthDate` to one of the table (`Users`).
|
||||
3. In version 3, we realize that we actually don't want to store users at all and delete
|
||||
the table.
|
||||
|
||||
Before version 3, the only migration could have been written as `m.addColumn(users, users.birthDate)`.
|
||||
But now that the `Users` table doesn't exist in the source code anymore, that's no longer possible!
|
||||
Sure, we could remember that the migration from 1 to 2 is now pointless and just skip it if a user
|
||||
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' | pageUrl }}). 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:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'stepbystep' %}
|
||||
|
||||
`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
|
||||
`OnUpgrade` callback.
|
||||
But you might want to customize the upgrade behavior, for instance by adding foreign key
|
||||
checks afterwards (as described in [tips]({{ 'index.md#tips' | pageUrl }})).
|
||||
|
||||
The `Migrator.runMigrationSteps` helper method can be used for that, as this example
|
||||
shows:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'stepbystep2' %}
|
||||
|
||||
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
|
||||
in debug modes.
|
||||
|
||||
## Moving to step-by-step migrations
|
||||
|
||||
If you've been using drift before `stepByStep` was added to the library, or if you've never exported a schema,
|
||||
you can move to step-by-step migrations by pinning the `from` value in `Migrator.runMigrationSteps` to a known
|
||||
starting point.
|
||||
|
||||
This allows you to perform all prior migration work to get the database to the "starting" point for
|
||||
`stepByStep` migrations, and then use `stepByStep` migrations beyond that schema version.
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'stepbystep3' %}
|
||||
|
||||
Here, we give a "floor" to the `from` value of `2`, since we've performed all other migration work to get to
|
||||
this point. From now on, you can generate step-by-step migrations for each schema change.
|
||||
|
||||
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.
|
|
@ -0,0 +1,87 @@
|
|||
---
|
||||
data:
|
||||
title: "Testing migrations"
|
||||
weight: 30
|
||||
description: Generate test code to write unit tests for your migrations.
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
||||
{% assign snippets = 'package:drift_docs/snippets/migrations/tests/schema_test.dart.excerpt.json' | readString | json_decode %}
|
||||
{% assign verify = 'package:drift_docs/snippets/migrations/tests/verify_data_integrity_test.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
While migrations can be written manually without additional help from drift, dedicated tools testing
|
||||
your migrations help to ensure that they are correct and aren't loosing any data.
|
||||
|
||||
Drift's migration tooling consists of the following steps:
|
||||
|
||||
1. After each change to your schema, use a tool to export the current schema into a separate file.
|
||||
2. Use a drift tool to generate test code able to verify that your migrations are bringing the database
|
||||
into the expected schema.
|
||||
3. Use generated code to make writing schema migrations easier.
|
||||
|
||||
This page describes steps 2 and 3. It assumes that you're already following step 1 by
|
||||
[exporting your schema]({{ 'exports.md' }}) when it changes.
|
||||
|
||||
## Writing tests
|
||||
|
||||
After you've exported the database schemas into a folder, you can generate old versions of your database class
|
||||
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/`.
|
||||
If we wanted to write them to `test/generated_migrations/`, we could use
|
||||
|
||||
```
|
||||
$ dart run drift_dev schema generate drift_schemas/ test/generated_migrations/
|
||||
```
|
||||
|
||||
After that setup, it's finally time to write some tests! For instance, a test could look like this:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'setup' %}
|
||||
|
||||
In general, a test looks like this:
|
||||
|
||||
1. Use `verifier.startAt()` to obtain a [connection](https://drift.simonbinder.eu/api/drift/databaseconnection-class)
|
||||
to a database with an initial schema.
|
||||
This database contains all your tables, indices and triggers from that version, created by using `Migrator.createAll`.
|
||||
2. Create your application database with that connection. For this, create a constructor in your database class that
|
||||
accepts a `QueryExecutor` and forwards it to the super constructor in `GeneratedDatabase`.
|
||||
Then, you can pass the result of calling `newConnection()` to that constructor to create a test instance of your
|
||||
datbaase.
|
||||
3. Call `verifier.migrateAndValidate(db, version)`. This will initiate a migration towards the target version (here, `2`).
|
||||
Unlike the database created by `startAt`, this uses the migration logic you wrote for your database.
|
||||
|
||||
`migrateAndValidate` will extract all `CREATE` statement from the `sqlite_schema` table and semantically compare them.
|
||||
If it sees anything unexpected, it will throw a `SchemaMismatch` exception to fail your test.
|
||||
|
||||
{% block "blocks/alert" title="Writing testable migrations" %}
|
||||
To test migrations _towards_ an old schema version (e.g. from `v1` to `v2` if your current version is `v3`),
|
||||
you're `onUpgrade` handler must be capable of upgrading to a version older than the current `schemaVersion`.
|
||||
For this, check the `to` parameter of the `onUpgrade` callback to run a different migration if necessary.
|
||||
Or, use [step-by-step migrations]({{ 'step_by_step.md' | pageUrl }}) which do this automatically.
|
||||
{% endblock %}
|
||||
|
||||
## 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
|
||||
is still there after it ran.
|
||||
You can use `schemaAt` to obtain a raw `Database` from the `sqlite3` package in addition to a connection.
|
||||
This can be used to insert data before a migration. After the migration ran, you can then check that the data is still there.
|
||||
|
||||
Note that you can't use the regular database class from you app for this, since its data classes always expect the latest
|
||||
schema. However, you can instruct drift to generate older snapshots of your data classes and companions for this purpose.
|
||||
To enable this feature, pass the `--data-classes` and `--companions` command-line arguments to the `drift_dev schema generate`
|
||||
command:
|
||||
|
||||
```
|
||||
$ dart run drift_dev schema generate --data-classes --companions drift_schemas/ test/generated_migrations/
|
||||
```
|
||||
|
||||
Then, you can import the generated classes with an alias:
|
||||
|
||||
{% include "blocks/snippet" snippets = verify name = 'imports' %}
|
||||
|
||||
This can then be used to manually create and verify data at a specific version:
|
||||
|
||||
{% include "blocks/snippet" snippets = verify name = 'main' %}
|
|
@ -55,7 +55,7 @@ in your favorite dependency injection framework for flutter hence solves this pr
|
|||
|
||||
## Why am I getting no such table errors?
|
||||
|
||||
If you add another table after your app has already been installed, you need to write a [migration]({{ "Advanced Features/migrations.md" | pageUrl }})
|
||||
If you add another table after your app has already been installed, you need to write a [migration]({{ "Migrations/index.md" | pageUrl }})
|
||||
that covers creating that table. If you're in the process of developing your app and want to use un- and reinstall your app
|
||||
instead of writing migrations, that's fine too. Please note that your apps data might be backed up on Android, so
|
||||
manually deleting your app's data instead of a reinstall is necessary on some devices.
|
||||
|
@ -80,7 +80,7 @@ you can set to `true`. When enabled, drift will print the statements it runs.
|
|||
|
||||
## How do I insert data on the first app start?
|
||||
|
||||
You can populate the database on the first start of your app with a custom [migration strategy]({{ 'Advanced Features/migrations.md' | pageUrl }}).
|
||||
You can populate the database on the first start of your app with a custom [migration strategy]({{ 'Migrations/index.md' | pageUrl }}).
|
||||
To insert data when the database is created (which usually happens when the app is first run), you can use this:
|
||||
|
||||
```dart
|
||||
|
|
|
@ -113,14 +113,11 @@ started with drift:
|
|||
- Writing queries: Drift-generated classes support writing the most common SQL statements, like
|
||||
[selects]({{ 'Dart API/select.md' | pageUrl }}) or [inserts, updates and deletes]({{ 'Dart API/writes.md' | pageUrl }}).
|
||||
- General [notes on how to integrate drift with your app's architecture]({{ 'Dart API/architecture.md' | pageUrl }}).
|
||||
- Something to keep in mind for later: When you change the schema of your database and write migrations, drift can help you make sure they're
|
||||
correct. Use [runtime checks], which don't require additional setup, or more involved [test utilities] if you want to test migrations between
|
||||
any schema versions.
|
||||
- Something to keep in mind for later: When changing the database, for instance by adding new columns
|
||||
or tables, you need to write a migration so that existing databases are transformed to the new
|
||||
format. Drift's extensive [migration tools]({{ 'Migrations/index.md' | pageUrl }}) help with that.
|
||||
|
||||
Once you're familiar with the basics, the [overview here]({{ 'index.md' | pageUrl }}) shows what
|
||||
more drift has to offer.
|
||||
This includes transactions, automated tooling to help with migrations, multi-platform support
|
||||
and more.
|
||||
|
||||
[runtime checks]: {{ 'Advanced Features/migrations.md#verifying-a-database-schema-at-runtime' | pageUrl }}
|
||||
[test utilities]: {{ 'Advanced Features/migrations.md#verifying-migrations' | pageUrl }}
|
||||
|
|
|
@ -116,4 +116,4 @@ test('stream emits a new user when the name updates', () async {
|
|||
## Testing migrations
|
||||
|
||||
Drift can help you generate code for schema migrations. For more details, see
|
||||
[this guide]({{ "Advanced Features/migrations.md#verifying-migrations" | pageUrl }}).
|
||||
[this guide]({{ "Migrations/tests.md" | pageUrl }}).
|
||||
|
|
|
@ -110,7 +110,7 @@ Also, you may have to
|
|||
|
||||
- Format your sources again: Run `dart format .`.
|
||||
- Re-run the build: Run `dart run build_runner build -d`.
|
||||
- If you have been using generated [migration test files]({{ 'Advanced Features/migrations.md#exporting-the-schema' | pageUrl }}),
|
||||
- If you have been using generated [migration test files]({{ 'Migrations/exports.md' | pageUrl }}),
|
||||
re-generate them as well with `dart run drift_dev schema generate drift_schemas/ test/generated_migrations/`
|
||||
(you may have to adapt the command to the directories you use for schemas).
|
||||
- Manually fix the changed order of imports caused by the migration.
|
||||
|
|
|
@ -51,7 +51,7 @@ and easy process.
|
|||
Further, drift provides a complete test toolkit to help you test migrations
|
||||
between all your revisions.
|
||||
|
||||
[All about schema migrations]({{ "docs/Advanced Features/migrations.md" | pageUrl }})
|
||||
[All about schema migrations]({{ "docs/Migrations/index.md" | pageUrl }})
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ dependencies:
|
|||
picocss:
|
||||
hosted: https://simonbinder.eu
|
||||
version: ^1.5.10
|
||||
test: ^1.18.0
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^2.0.0
|
||||
|
@ -43,7 +44,6 @@ dev_dependencies:
|
|||
shelf: ^1.2.0
|
||||
shelf_static: ^1.1.0
|
||||
source_span: ^1.9.1
|
||||
test: ^1.18.0
|
||||
sqlparser:
|
||||
zap_dev: ^0.2.3+1
|
||||
|
||||
|
|
Loading…
Reference in New Issue