mirror of https://github.com/AMT-Cheif/drift.git
Merge branch 'docs-restructure' into develop
This commit is contained in:
commit
8c42ce5049
|
@ -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
|
||||
```
|
||||
|
|
|
@ -56,7 +56,9 @@ targets:
|
|||
version: "3.39"
|
||||
generate_for:
|
||||
include: &modular
|
||||
- "lib/snippets/_shared/**"
|
||||
- "lib/snippets/modular/**"
|
||||
- "lib/snippets/drift_files/custom_queries.*"
|
||||
drift_dev:modular:
|
||||
enabled: true
|
||||
options: *options
|
||||
|
@ -120,6 +122,8 @@ targets:
|
|||
environment: "preview"
|
||||
build_web_compilers:entrypoint:
|
||||
generate_for:
|
||||
include:
|
||||
- "web/**"
|
||||
exclude:
|
||||
- "web/drift_worker.dart"
|
||||
release_options:
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/internal/modular.dart';
|
||||
|
||||
import 'todo_tables.drift.dart';
|
||||
|
||||
// #docregion tables
|
||||
class TodoItems extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get title => text().withLength(min: 6, max: 32)();
|
||||
TextColumn get content => text().named('body')();
|
||||
IntColumn get category => integer().nullable().references(Categories, #id)();
|
||||
}
|
||||
|
||||
@DataClassName('Category')
|
||||
class Categories extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get name => text()();
|
||||
}
|
||||
// #enddocregion tables
|
||||
|
||||
class CanUseCommonTables extends ModularAccessor {
|
||||
CanUseCommonTables(super.attachedDatabase);
|
||||
|
||||
$TodoItemsTable get todoItems => resultSet('todo_items');
|
||||
$CategoriesTable get categories => resultSet('categories');
|
||||
}
|
|
@ -0,0 +1,432 @@
|
|||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:drift_docs/snippets/_shared/todo_tables.drift.dart' as i1;
|
||||
import 'package:drift_docs/snippets/_shared/todo_tables.dart' as i2;
|
||||
|
||||
class $TodoItemsTable extends i2.TodoItems
|
||||
with i0.TableInfo<$TodoItemsTable, i1.TodoItem> {
|
||||
@override
|
||||
final i0.GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$TodoItemsTable(this.attachedDatabase, [this._alias]);
|
||||
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
|
||||
@override
|
||||
late final i0.GeneratedColumn<int> id = i0.GeneratedColumn<int>(
|
||||
'id', aliasedName, false,
|
||||
hasAutoIncrement: true,
|
||||
type: i0.DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
i0.GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
||||
static const i0.VerificationMeta _titleMeta =
|
||||
const i0.VerificationMeta('title');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> title = i0.GeneratedColumn<String>(
|
||||
'title', aliasedName, false,
|
||||
additionalChecks: i0.GeneratedColumn.checkTextLength(
|
||||
minTextLength: 6, maxTextLength: 32),
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: true);
|
||||
static const i0.VerificationMeta _contentMeta =
|
||||
const i0.VerificationMeta('content');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> content = i0.GeneratedColumn<String>(
|
||||
'body', aliasedName, false,
|
||||
type: i0.DriftSqlType.string, requiredDuringInsert: true);
|
||||
static const i0.VerificationMeta _categoryMeta =
|
||||
const i0.VerificationMeta('category');
|
||||
@override
|
||||
late final i0.GeneratedColumn<int> category = i0.GeneratedColumn<int>(
|
||||
'category', aliasedName, true,
|
||||
type: i0.DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
i0.GeneratedColumn.constraintIsAlways('REFERENCES categories (id)'));
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns => [id, title, content, category];
|
||||
@override
|
||||
String get aliasedName => _alias ?? 'todo_items';
|
||||
@override
|
||||
String get actualTableName => 'todo_items';
|
||||
@override
|
||||
i0.VerificationContext validateIntegrity(i0.Insertable<i1.TodoItem> instance,
|
||||
{bool isInserting = false}) {
|
||||
final context = i0.VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('id')) {
|
||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||
}
|
||||
if (data.containsKey('title')) {
|
||||
context.handle(
|
||||
_titleMeta, title.isAcceptableOrUnknown(data['title']!, _titleMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_titleMeta);
|
||||
}
|
||||
if (data.containsKey('body')) {
|
||||
context.handle(_contentMeta,
|
||||
content.isAcceptableOrUnknown(data['body']!, _contentMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_contentMeta);
|
||||
}
|
||||
if (data.containsKey('category')) {
|
||||
context.handle(_categoryMeta,
|
||||
category.isAcceptableOrUnknown(data['category']!, _categoryMeta));
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<i0.GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
i1.TodoItem map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return i1.TodoItem(
|
||||
id: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||
title: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}title'])!,
|
||||
content: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}body'])!,
|
||||
category: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.int, data['${effectivePrefix}category']),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
$TodoItemsTable createAlias(String alias) {
|
||||
return $TodoItemsTable(attachedDatabase, alias);
|
||||
}
|
||||
}
|
||||
|
||||
class TodoItem extends i0.DataClass implements i0.Insertable<i1.TodoItem> {
|
||||
final int id;
|
||||
final String title;
|
||||
final String content;
|
||||
final int? category;
|
||||
const TodoItem(
|
||||
{required this.id,
|
||||
required this.title,
|
||||
required this.content,
|
||||
this.category});
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
map['id'] = i0.Variable<int>(id);
|
||||
map['title'] = i0.Variable<String>(title);
|
||||
map['body'] = i0.Variable<String>(content);
|
||||
if (!nullToAbsent || category != null) {
|
||||
map['category'] = i0.Variable<int>(category);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
i1.TodoItemsCompanion toCompanion(bool nullToAbsent) {
|
||||
return i1.TodoItemsCompanion(
|
||||
id: i0.Value(id),
|
||||
title: i0.Value(title),
|
||||
content: i0.Value(content),
|
||||
category: category == null && nullToAbsent
|
||||
? const i0.Value.absent()
|
||||
: i0.Value(category),
|
||||
);
|
||||
}
|
||||
|
||||
factory TodoItem.fromJson(Map<String, dynamic> json,
|
||||
{i0.ValueSerializer? serializer}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return TodoItem(
|
||||
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({i0.ValueSerializer? serializer}) {
|
||||
serializer ??= i0.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),
|
||||
};
|
||||
}
|
||||
|
||||
i1.TodoItem copyWith(
|
||||
{int? id,
|
||||
String? title,
|
||||
String? content,
|
||||
i0.Value<int?> category = const i0.Value.absent()}) =>
|
||||
i1.TodoItem(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
content: content ?? this.content,
|
||||
category: category.present ? category.value : this.category,
|
||||
);
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('TodoItem(')
|
||||
..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 i1.TodoItem &&
|
||||
other.id == this.id &&
|
||||
other.title == this.title &&
|
||||
other.content == this.content &&
|
||||
other.category == this.category);
|
||||
}
|
||||
|
||||
class TodoItemsCompanion extends i0.UpdateCompanion<i1.TodoItem> {
|
||||
final i0.Value<int> id;
|
||||
final i0.Value<String> title;
|
||||
final i0.Value<String> content;
|
||||
final i0.Value<int?> category;
|
||||
const TodoItemsCompanion({
|
||||
this.id = const i0.Value.absent(),
|
||||
this.title = const i0.Value.absent(),
|
||||
this.content = const i0.Value.absent(),
|
||||
this.category = const i0.Value.absent(),
|
||||
});
|
||||
TodoItemsCompanion.insert({
|
||||
this.id = const i0.Value.absent(),
|
||||
required String title,
|
||||
required String content,
|
||||
this.category = const i0.Value.absent(),
|
||||
}) : title = i0.Value(title),
|
||||
content = i0.Value(content);
|
||||
static i0.Insertable<i1.TodoItem> custom({
|
||||
i0.Expression<int>? id,
|
||||
i0.Expression<String>? title,
|
||||
i0.Expression<String>? content,
|
||||
i0.Expression<int>? category,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (title != null) 'title': title,
|
||||
if (content != null) 'body': content,
|
||||
if (category != null) 'category': category,
|
||||
});
|
||||
}
|
||||
|
||||
i1.TodoItemsCompanion copyWith(
|
||||
{i0.Value<int>? id,
|
||||
i0.Value<String>? title,
|
||||
i0.Value<String>? content,
|
||||
i0.Value<int?>? category}) {
|
||||
return i1.TodoItemsCompanion(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
content: content ?? this.content,
|
||||
category: category ?? this.category,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = i0.Variable<int>(id.value);
|
||||
}
|
||||
if (title.present) {
|
||||
map['title'] = i0.Variable<String>(title.value);
|
||||
}
|
||||
if (content.present) {
|
||||
map['body'] = i0.Variable<String>(content.value);
|
||||
}
|
||||
if (category.present) {
|
||||
map['category'] = i0.Variable<int>(category.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('TodoItemsCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('title: $title, ')
|
||||
..write('content: $content, ')
|
||||
..write('category: $category')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class $CategoriesTable extends i2.Categories
|
||||
with i0.TableInfo<$CategoriesTable, i1.Category> {
|
||||
@override
|
||||
final i0.GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$CategoriesTable(this.attachedDatabase, [this._alias]);
|
||||
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
|
||||
@override
|
||||
late final i0.GeneratedColumn<int> id = i0.GeneratedColumn<int>(
|
||||
'id', aliasedName, false,
|
||||
hasAutoIncrement: true,
|
||||
type: i0.DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
i0.GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
||||
static const i0.VerificationMeta _nameMeta =
|
||||
const i0.VerificationMeta('name');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> name = i0.GeneratedColumn<String>(
|
||||
'name', aliasedName, false,
|
||||
type: i0.DriftSqlType.string, requiredDuringInsert: true);
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns => [id, name];
|
||||
@override
|
||||
String get aliasedName => _alias ?? 'categories';
|
||||
@override
|
||||
String get actualTableName => 'categories';
|
||||
@override
|
||||
i0.VerificationContext validateIntegrity(i0.Insertable<i1.Category> instance,
|
||||
{bool isInserting = false}) {
|
||||
final context = i0.VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('id')) {
|
||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||
}
|
||||
if (data.containsKey('name')) {
|
||||
context.handle(
|
||||
_nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_nameMeta);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<i0.GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
i1.Category map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return i1.Category(
|
||||
id: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||
name: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
$CategoriesTable createAlias(String alias) {
|
||||
return $CategoriesTable(attachedDatabase, alias);
|
||||
}
|
||||
}
|
||||
|
||||
class Category extends i0.DataClass implements i0.Insertable<i1.Category> {
|
||||
final int id;
|
||||
final String name;
|
||||
const Category({required this.id, required this.name});
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
map['id'] = i0.Variable<int>(id);
|
||||
map['name'] = i0.Variable<String>(name);
|
||||
return map;
|
||||
}
|
||||
|
||||
i1.CategoriesCompanion toCompanion(bool nullToAbsent) {
|
||||
return i1.CategoriesCompanion(
|
||||
id: i0.Value(id),
|
||||
name: i0.Value(name),
|
||||
);
|
||||
}
|
||||
|
||||
factory Category.fromJson(Map<String, dynamic> json,
|
||||
{i0.ValueSerializer? serializer}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return Category(
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
name: serializer.fromJson<String>(json['name']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
'name': serializer.toJson<String>(name),
|
||||
};
|
||||
}
|
||||
|
||||
i1.Category copyWith({int? id, String? name}) => i1.Category(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
);
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('Category(')
|
||||
..write('id: $id, ')
|
||||
..write('name: $name')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, name);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is i1.Category && other.id == this.id && other.name == this.name);
|
||||
}
|
||||
|
||||
class CategoriesCompanion extends i0.UpdateCompanion<i1.Category> {
|
||||
final i0.Value<int> id;
|
||||
final i0.Value<String> name;
|
||||
const CategoriesCompanion({
|
||||
this.id = const i0.Value.absent(),
|
||||
this.name = const i0.Value.absent(),
|
||||
});
|
||||
CategoriesCompanion.insert({
|
||||
this.id = const i0.Value.absent(),
|
||||
required String name,
|
||||
}) : name = i0.Value(name);
|
||||
static i0.Insertable<i1.Category> custom({
|
||||
i0.Expression<int>? id,
|
||||
i0.Expression<String>? name,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (name != null) 'name': name,
|
||||
});
|
||||
}
|
||||
|
||||
i1.CategoriesCompanion copyWith({i0.Value<int>? id, i0.Value<String>? name}) {
|
||||
return i1.CategoriesCompanion(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = i0.Variable<int>(id.value);
|
||||
}
|
||||
if (name.present) {
|
||||
map['name'] = i0.Variable<String>(name.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('CategoriesCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('name: $name')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
import 'tables/filename.dart';
|
||||
import '../_shared/todo_tables.dart';
|
||||
import '../_shared/todo_tables.drift.dart';
|
||||
|
||||
extension Expressions on MyDatabase {
|
||||
extension Snippets on CanUseCommonTables {
|
||||
// #docregion emptyCategories
|
||||
Future<List<Category>> emptyCategories() {
|
||||
final hasNoTodo = notExistsQuery(
|
||||
select(todos)..where((row) => row.category.equalsExp(categories.id)));
|
||||
final hasNoTodo = notExistsQuery(select(todoItems)
|
||||
..where((row) => row.category.equalsExp(categories.id)));
|
||||
return (select(categories)..where((row) => hasNoTodo)).get();
|
||||
}
|
||||
// #enddocregion emptyCategories
|
|
@ -0,0 +1,5 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
class EnabledCategories extends Table {
|
||||
IntColumn get parentCategory => integer()();
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
import 'tables/filename.dart';
|
||||
import '../_shared/todo_tables.dart';
|
||||
import '../_shared/todo_tables.drift.dart';
|
||||
|
||||
// #docregion joinIntro
|
||||
// We define a data class to contain both a todo entry and the associated
|
||||
|
@ -8,18 +9,64 @@ import 'tables/filename.dart';
|
|||
class EntryWithCategory {
|
||||
EntryWithCategory(this.entry, this.category);
|
||||
|
||||
final Todo entry;
|
||||
final TodoItem entry;
|
||||
final Category? category;
|
||||
}
|
||||
|
||||
// #enddocregion joinIntro
|
||||
|
||||
extension GroupByQueries on MyDatabase {
|
||||
// #docregion joinIntro
|
||||
extension SelectExamples on CanUseCommonTables {
|
||||
// #docregion limit
|
||||
Future<List<TodoItem>> limitTodos(int limit, {int? offset}) {
|
||||
return (select(todoItems)..limit(limit, offset: offset)).get();
|
||||
}
|
||||
// #enddocregion limit
|
||||
|
||||
// #docregion order-by
|
||||
Future<List<TodoItem>> sortEntriesAlphabetically() {
|
||||
return (select(todoItems)
|
||||
..orderBy([(t) => OrderingTerm(expression: t.title)]))
|
||||
.get();
|
||||
}
|
||||
// #enddocregion order-by
|
||||
|
||||
// #docregion single
|
||||
Stream<TodoItem> entryById(int id) {
|
||||
return (select(todoItems)..where((t) => t.id.equals(id))).watchSingle();
|
||||
}
|
||||
// #enddocregion single
|
||||
|
||||
// #docregion mapping
|
||||
Stream<List<String>> contentWithLongTitles() {
|
||||
final query = select(todoItems)
|
||||
..where((t) => t.title.length.isBiggerOrEqualValue(16));
|
||||
|
||||
return query.map((row) => row.content).watch();
|
||||
}
|
||||
// #enddocregion mapping
|
||||
|
||||
// #docregion selectable
|
||||
// Exposes `get` and `watch`
|
||||
MultiSelectable<TodoItem> pageOfTodos(int page, {int pageSize = 10}) {
|
||||
return select(todoItems)..limit(pageSize, offset: page);
|
||||
}
|
||||
|
||||
// Exposes `getSingle` and `watchSingle`
|
||||
SingleSelectable<TodoItem> selectableEntryById(int id) {
|
||||
return select(todoItems)..where((t) => t.id.equals(id));
|
||||
}
|
||||
|
||||
// Exposes `getSingleOrNull` and `watchSingleOrNull`
|
||||
SingleOrNullSelectable<TodoItem> entryFromExternalLink(int id) {
|
||||
return select(todoItems)..where((t) => t.id.equals(id));
|
||||
}
|
||||
// #enddocregion selectable
|
||||
|
||||
// #docregion joinIntro
|
||||
// in the database class, we can then load the category for each entry
|
||||
Stream<List<EntryWithCategory>> entriesWithCategory() {
|
||||
final query = select(todos).join([
|
||||
leftOuterJoin(categories, categories.id.equalsExp(todos.category)),
|
||||
final query = select(todoItems).join([
|
||||
leftOuterJoin(categories, categories.id.equalsExp(todoItems.category)),
|
||||
]);
|
||||
|
||||
// see next section on how to parse the result
|
||||
|
@ -28,7 +75,7 @@ extension GroupByQueries on MyDatabase {
|
|||
return query.watch().map((rows) {
|
||||
return rows.map((row) {
|
||||
return EntryWithCategory(
|
||||
row.readTable(todos),
|
||||
row.readTable(todoItems),
|
||||
row.readTableOrNull(categories),
|
||||
);
|
||||
}).toList();
|
||||
|
@ -38,14 +85,38 @@ extension GroupByQueries on MyDatabase {
|
|||
}
|
||||
// #enddocregion joinIntro
|
||||
|
||||
// #docregion otherTodosInSameCategory
|
||||
/// Searches for todo entries in the same category as the ones having
|
||||
/// `titleQuery` in their titles.
|
||||
Future<List<TodoItem>> otherTodosInSameCategory(String titleQuery) async {
|
||||
// Since we're adding the same table twice (once to filter for the title,
|
||||
// and once to find other todos in same category), we need a way to
|
||||
// distinguish the two tables. So, we're giving one of them a special name:
|
||||
final otherTodos = alias(todoItems, 'inCategory');
|
||||
|
||||
final query = select(otherTodos).join([
|
||||
// In joins, `useColumns: false` tells drift to not add columns of the
|
||||
// joined table to the result set. This is useful here, since we only join
|
||||
// the tables so that we can refer to them in the where clause.
|
||||
innerJoin(categories, categories.id.equalsExp(otherTodos.category),
|
||||
useColumns: false),
|
||||
innerJoin(todoItems, todoItems.category.equalsExp(categories.id),
|
||||
useColumns: false),
|
||||
])
|
||||
..where(todoItems.title.contains(titleQuery));
|
||||
|
||||
return query.map((row) => row.readTable(otherTodos)).get();
|
||||
}
|
||||
// #enddocregion otherTodosInSameCategory
|
||||
|
||||
// #docregion countTodosInCategories
|
||||
Future<void> countTodosInCategories() async {
|
||||
final amountOfTodos = todos.id.count();
|
||||
final amountOfTodos = todoItems.id.count();
|
||||
|
||||
final query = select(categories).join([
|
||||
innerJoin(
|
||||
todos,
|
||||
todos.category.equalsExp(categories.id),
|
||||
todoItems,
|
||||
todoItems.category.equalsExp(categories.id),
|
||||
useColumns: false,
|
||||
)
|
||||
]);
|
||||
|
@ -64,46 +135,22 @@ extension GroupByQueries on MyDatabase {
|
|||
|
||||
// #docregion averageItemLength
|
||||
Stream<double> averageItemLength() {
|
||||
final avgLength = todos.content.length.avg();
|
||||
final query = selectOnly(todos)..addColumns([avgLength]);
|
||||
final avgLength = todoItems.content.length.avg();
|
||||
final query = selectOnly(todoItems)..addColumns([avgLength]);
|
||||
|
||||
return query.map((row) => row.read(avgLength)!).watchSingle();
|
||||
}
|
||||
// #enddocregion averageItemLength
|
||||
|
||||
// #docregion otherTodosInSameCategory
|
||||
/// Searches for todo entries in the same category as the ones having
|
||||
/// `titleQuery` in their titles.
|
||||
Future<List<Todo>> otherTodosInSameCategory(String titleQuery) async {
|
||||
// Since we're adding the same table twice (once to filter for the title,
|
||||
// and once to find other todos in same category), we need a way to
|
||||
// distinguish the two tables. So, we're giving one of them a special name:
|
||||
final otherTodos = alias(todos, 'inCategory');
|
||||
|
||||
final query = select(otherTodos).join([
|
||||
// In joins, `useColumns: false` tells drift to not add columns of the
|
||||
// joined table to the result set. This is useful here, since we only join
|
||||
// the tables so that we can refer to them in the where clause.
|
||||
innerJoin(categories, categories.id.equalsExp(otherTodos.category),
|
||||
useColumns: false),
|
||||
innerJoin(todos, todos.category.equalsExp(categories.id),
|
||||
useColumns: false),
|
||||
])
|
||||
..where(todos.title.contains(titleQuery));
|
||||
|
||||
return query.map((row) => row.readTable(otherTodos)).get();
|
||||
}
|
||||
// #enddocregion otherTodosInSameCategory
|
||||
|
||||
// #docregion createCategoryForUnassignedTodoEntries
|
||||
Future<void> createCategoryForUnassignedTodoEntries() async {
|
||||
final newDescription = Variable<String>('category for: ') + todos.title;
|
||||
final query = selectOnly(todos)
|
||||
..where(todos.category.isNull())
|
||||
final newDescription = Variable<String>('category for: ') + todoItems.title;
|
||||
final query = selectOnly(todoItems)
|
||||
..where(todoItems.category.isNull())
|
||||
..addColumns([newDescription]);
|
||||
|
||||
await into(categories).insertFromSelect(query, columns: {
|
||||
categories.description: newDescription,
|
||||
categories.name: newDescription,
|
||||
});
|
||||
}
|
||||
// #enddocregion createCategoryForUnassignedTodoEntries
|
||||
|
@ -111,7 +158,7 @@ extension GroupByQueries on MyDatabase {
|
|||
// #docregion subquery
|
||||
Future<List<(Category, int)>> amountOfLengthyTodoItemsPerCategory() async {
|
||||
final longestTodos = Subquery(
|
||||
select(todos)
|
||||
select(todoItems)
|
||||
..orderBy([(row) => OrderingTerm.desc(row.title.length)])
|
||||
..limit(10),
|
||||
's',
|
||||
|
@ -121,14 +168,14 @@ extension GroupByQueries on MyDatabase {
|
|||
// found for each category. But we can't access todos.title directly since
|
||||
// we're not selecting from `todos`. Instead, we'll use Subquery.ref to read
|
||||
// from a column in a subquery.
|
||||
final itemCount = longestTodos.ref(todos.title).count();
|
||||
final itemCount = longestTodos.ref(todoItems.title).count();
|
||||
final query = select(categories).join(
|
||||
[
|
||||
innerJoin(
|
||||
longestTodos,
|
||||
// Again using .ref() here to access the category in the outer select
|
||||
// statement.
|
||||
longestTodos.ref(todos.category).equalsExp(categories.id),
|
||||
longestTodos.ref(todoItems.category).equalsExp(categories.id),
|
||||
useColumns: false,
|
||||
)
|
||||
],
|
||||
|
@ -143,4 +190,18 @@ extension GroupByQueries on MyDatabase {
|
|||
];
|
||||
}
|
||||
// #enddocregion subquery
|
||||
|
||||
// #docregion custom-columns
|
||||
Future<List<(TodoItem, bool)>> loadEntries() {
|
||||
// assume that an entry is important if it has the string "important" somewhere in its content
|
||||
final isImportant = todoItems.content.like('%important%');
|
||||
|
||||
return select(todoItems).addColumns([isImportant]).map((row) {
|
||||
final entry = row.readTable(todoItems);
|
||||
final entryIsImportant = row.read(isImportant)!;
|
||||
|
||||
return (entry, entryIsImportant);
|
||||
}).get();
|
||||
}
|
||||
// #enddocregion custom-columns
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
// #docregion nnbd
|
||||
class Items extends Table {
|
||||
IntColumn get category => integer().nullable()();
|
||||
// ...
|
||||
}
|
||||
// #enddocregion nnbd
|
||||
|
||||
// #docregion names
|
||||
@DataClassName('EnabledCategory')
|
||||
class EnabledCategories extends Table {
|
||||
@override
|
||||
String get tableName => 'categories';
|
||||
|
||||
@JsonKey('parent_id')
|
||||
IntColumn get parentCategory => integer().named('parent')();
|
||||
}
|
||||
// #enddocregion names
|
||||
|
||||
// #docregion references
|
||||
class TodoItems extends Table {
|
||||
// ...
|
||||
IntColumn get category =>
|
||||
integer().nullable().references(TodoCategories, #id)();
|
||||
}
|
||||
|
||||
@DataClassName("Category")
|
||||
class TodoCategories extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
// and more columns...
|
||||
}
|
||||
// #enddocregion references
|
||||
|
||||
// #docregion unique-column
|
||||
class TableWithUniqueColumn extends Table {
|
||||
IntColumn get unique => integer().unique()();
|
||||
}
|
||||
// #enddocregion unique-column
|
||||
|
||||
// #docregion primary-key
|
||||
class GroupMemberships extends Table {
|
||||
IntColumn get group => integer()();
|
||||
IntColumn get user => integer()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {group, user};
|
||||
}
|
||||
// #enddocregion primary-key
|
||||
|
||||
// #docregion unique-table
|
||||
class IngredientInRecipes extends Table {
|
||||
@override
|
||||
List<Set<Column>> get uniqueKeys => [
|
||||
{recipe, ingredient},
|
||||
{recipe, amountInGrams}
|
||||
];
|
||||
|
||||
IntColumn get recipe => integer()();
|
||||
IntColumn get ingredient => integer()();
|
||||
|
||||
IntColumn get amountInGrams => integer().named('amount')();
|
||||
}
|
||||
// #enddocregion unique-table
|
||||
|
||||
// #docregion custom-constraint-table
|
||||
class TableWithCustomConstraints extends Table {
|
||||
IntColumn get foo => integer()();
|
||||
IntColumn get bar => integer()();
|
||||
|
||||
@override
|
||||
List<String> get customConstraints => [
|
||||
'FOREIGN KEY (foo, bar) REFERENCES group_memberships ("group", user)',
|
||||
];
|
||||
}
|
||||
// #enddocregion custom-constraint-table
|
||||
|
||||
// #docregion index
|
||||
@TableIndex(name: 'user_name', columns: {#name})
|
||||
class Users extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get name => text()();
|
||||
}
|
||||
// #enddocregion index
|
||||
|
||||
// #docregion custom-type
|
||||
typedef Category = ({int id, String name});
|
||||
|
||||
@UseRowClass(Category)
|
||||
class Categories extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get name => text()();
|
||||
// #enddocregion custom-type
|
||||
@override
|
||||
String get tableName => 'categories2';
|
||||
// #docregion custom-type
|
||||
}
|
||||
// #enddocregion custom-type
|
|
@ -1,13 +1,16 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'tables/filename.dart';
|
||||
|
||||
extension Snippets on MyDatabase {
|
||||
import '../_shared/todo_tables.dart';
|
||||
import '../_shared/todo_tables.drift.dart';
|
||||
|
||||
extension Snippets on CanUseCommonTables {
|
||||
// #docregion deleteCategory
|
||||
Future<void> deleteCategory(Category category) {
|
||||
return transaction(() async {
|
||||
// first, move the affected todo entries back to the default category
|
||||
await (update(todos)..where((row) => row.category.equals(category.id)))
|
||||
.write(const TodosCompanion(category: Value(null)));
|
||||
await (update(todoItems)
|
||||
..where((row) => row.category.equals(category.id)))
|
||||
.write(const TodoItemsCompanion(category: Value(null)));
|
||||
|
||||
// then, delete the category
|
||||
await delete(categories).delete(category);
|
||||
|
@ -18,14 +21,13 @@ extension Snippets on MyDatabase {
|
|||
// #docregion nested
|
||||
Future<void> nestedTransactions() async {
|
||||
await transaction(() async {
|
||||
await into(categories)
|
||||
.insert(CategoriesCompanion.insert(description: 'first'));
|
||||
await into(categories).insert(CategoriesCompanion.insert(name: 'first'));
|
||||
|
||||
// this is a nested transaction:
|
||||
await transaction(() async {
|
||||
// At this point, the first category is visible
|
||||
await into(categories)
|
||||
.insert(CategoriesCompanion.insert(description: 'second'));
|
||||
.insert(CategoriesCompanion.insert(name: 'second'));
|
||||
// Here, the second category is only visible inside this nested
|
||||
// transaction.
|
||||
});
|
||||
|
@ -36,7 +38,7 @@ extension Snippets on MyDatabase {
|
|||
await transaction(() async {
|
||||
// At this point, both categories are visible
|
||||
await into(categories)
|
||||
.insert(CategoriesCompanion.insert(description: 'third'));
|
||||
.insert(CategoriesCompanion.insert(name: 'third'));
|
||||
// The third category is only visible here.
|
||||
throw Exception('Abort in the second nested transaction');
|
||||
});
|
|
@ -1,22 +1,17 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
import 'filename.dart';
|
||||
|
||||
// #docregion unique
|
||||
class WithUniqueConstraints extends Table {
|
||||
IntColumn get a => integer().unique()();
|
||||
|
||||
IntColumn get b => integer()();
|
||||
IntColumn get c => integer()();
|
||||
|
||||
@override
|
||||
List<Set<Column>> get uniqueKeys => [
|
||||
{b, c}
|
||||
];
|
||||
|
||||
// Effectively, this table has two unique key sets: (a) and (b, c).
|
||||
class Todos extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get title => text().withLength(min: 6, max: 32)();
|
||||
TextColumn get content => text().named('body')();
|
||||
IntColumn get category => integer().nullable()();
|
||||
}
|
||||
|
||||
@DataClassName('Category')
|
||||
class Categories extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get description => text()();
|
||||
}
|
||||
// #enddocregion unique
|
||||
|
||||
// #docregion view
|
||||
abstract class CategoryTodoCount extends View {
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
import '../tables/filename.dart';
|
||||
|
||||
part 'custom_queries.g.dart';
|
||||
import '../_shared/todo_tables.dart';
|
||||
import '../_shared/todo_tables.drift.dart';
|
||||
import 'custom_queries.drift.dart';
|
||||
|
||||
// #docregion manual
|
||||
class CategoryWithCount {
|
||||
|
@ -16,14 +16,14 @@ class CategoryWithCount {
|
|||
|
||||
// #docregion setup
|
||||
@DriftDatabase(
|
||||
tables: [Todos, Categories],
|
||||
tables: [TodoItems, Categories],
|
||||
queries: {
|
||||
'categoriesWithCount': 'SELECT *, '
|
||||
'(SELECT COUNT(*) FROM todos WHERE category = c.id) AS "amount" '
|
||||
'(SELECT COUNT(*) FROM todo_items WHERE category = c.id) AS "amount" '
|
||||
'FROM categories c;'
|
||||
},
|
||||
)
|
||||
class MyDatabase extends _$MyDatabase {
|
||||
class MyDatabase extends $MyDatabase {
|
||||
// rest of class stays the same
|
||||
// #enddocregion setup
|
||||
@override
|
||||
|
@ -52,7 +52,7 @@ class MyDatabase extends _$MyDatabase {
|
|||
'SELECT *, (SELECT COUNT(*) FROM todos WHERE category = c.id) AS "amount"'
|
||||
' FROM categories c;',
|
||||
// used for the stream: the stream will update when either table changes
|
||||
readsFrom: {todos, categories},
|
||||
readsFrom: {todoItems, categories},
|
||||
).watch().map((rows) {
|
||||
// we get list of rows here. We just have to turn the raw data from the
|
||||
// row into a CategoryWithCount instnace. As we defined the Category table
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:drift_docs/snippets/_shared/todo_tables.drift.dart' as i1;
|
||||
|
||||
abstract class $MyDatabase extends i0.GeneratedDatabase {
|
||||
$MyDatabase(i0.QueryExecutor e) : super(e);
|
||||
late final i1.$CategoriesTable categories = i1.$CategoriesTable(this);
|
||||
late final i1.$TodoItemsTable todoItems = i1.$TodoItemsTable(this);
|
||||
i0.Selectable<CategoriesWithCountResult> categoriesWithCount() {
|
||||
return customSelect(
|
||||
'SELECT *, (SELECT COUNT(*) FROM todo_items WHERE category = c.id) AS amount FROM categories AS c',
|
||||
variables: [],
|
||||
readsFrom: {
|
||||
todoItems,
|
||||
categories,
|
||||
}).map((i0.QueryRow row) => CategoriesWithCountResult(
|
||||
id: row.read<int>('id'),
|
||||
name: row.read<String>('name'),
|
||||
amount: row.read<int>('amount'),
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
||||
@override
|
||||
List<i0.DatabaseSchemaEntity> get allSchemaEntities =>
|
||||
[categories, todoItems];
|
||||
}
|
||||
|
||||
class CategoriesWithCountResult {
|
||||
final int id;
|
||||
final String name;
|
||||
final int amount;
|
||||
CategoriesWithCountResult({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.amount,
|
||||
});
|
||||
}
|
|
@ -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
|
|
@ -23,8 +23,9 @@ void databases() {
|
|||
final myDatabaseFile = File('/dev/null');
|
||||
|
||||
// #docregion encrypted1
|
||||
NativeDatabase(
|
||||
NativeDatabase.createInBackground(
|
||||
myDatabaseFile,
|
||||
isolateSetup: setupSqlCipher,
|
||||
setup: (rawDb) {
|
||||
rawDb.execute("PRAGMA key = 'passphrase';");
|
||||
},
|
||||
|
@ -32,8 +33,9 @@ void databases() {
|
|||
// #enddocregion encrypted1
|
||||
|
||||
// #docregion encrypted2
|
||||
NativeDatabase(
|
||||
NativeDatabase.createInBackground(
|
||||
myDatabaseFile,
|
||||
isolateSetup: setupSqlCipher,
|
||||
setup: (rawDb) {
|
||||
assert(_debugCheckHasCipher(rawDb));
|
||||
rawDb.execute("PRAGMA key = 'passphrase';");
|
|
@ -0,0 +1,73 @@
|
|||
// #docregion before_generation
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
// #enddocregion before_generation
|
||||
|
||||
// #docregion open
|
||||
// These imports are necessary to open the sqlite3 database
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
// ... the TodoItems table definition stays the same
|
||||
// #enddocregion open
|
||||
|
||||
// #docregion before_generation
|
||||
part 'database.g.dart';
|
||||
|
||||
// #docregion table
|
||||
class TodoItems extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get title => text().withLength(min: 6, max: 32)();
|
||||
TextColumn get content => text().named('body')();
|
||||
IntColumn get category => integer().nullable()();
|
||||
}
|
||||
// #enddocregion table
|
||||
// #docregion open
|
||||
|
||||
@DriftDatabase(tables: [TodoItems])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
// #enddocregion before_generation
|
||||
AppDatabase() : super(_openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
// #docregion before_generation
|
||||
}
|
||||
// #enddocregion before_generation, open
|
||||
|
||||
// #docregion open
|
||||
|
||||
LazyDatabase _openConnection() {
|
||||
// the LazyDatabase util lets us find the right location for the file async.
|
||||
return LazyDatabase(() async {
|
||||
// put the database file, called db.sqlite here, into the documents folder
|
||||
// for your app.
|
||||
final dbFolder = await getApplicationDocumentsDirectory();
|
||||
final file = File(p.join(dbFolder.path, 'db.sqlite'));
|
||||
return NativeDatabase.createInBackground(file);
|
||||
});
|
||||
}
|
||||
// #enddocregion open
|
||||
|
||||
class WidgetsFlutterBinding {
|
||||
static void ensureInitialized() {}
|
||||
}
|
||||
|
||||
// #docregion use
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
final database = AppDatabase();
|
||||
|
||||
await database.into(database.todoItems).insert(TodoItemsCompanion.insert(
|
||||
title: 'todo: finish drift setup',
|
||||
content: 'We can now write queries and define our own tables.',
|
||||
));
|
||||
List<TodoItem> allItems = await database.select(database.todoItems).get();
|
||||
|
||||
print('items in database: $allItems');
|
||||
}
|
||||
// #enddocregion use
|
|
@ -1,81 +0,0 @@
|
|||
// ignore_for_file: directives_ordering
|
||||
|
||||
// #docregion open
|
||||
// To open the database, add these imports to the existing file defining the
|
||||
// database class. They are used to open the database.
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
// #enddocregion open
|
||||
|
||||
// #docregion overview
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
// assuming that your file is called filename.dart. This will give an error at
|
||||
// first, but it's needed for drift to know about the generated code
|
||||
part 'filename.g.dart';
|
||||
|
||||
// this will generate a table called "todos" for us. The rows of that table will
|
||||
// be represented by a class called "Todo".
|
||||
class Todos extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get title => text().withLength(min: 6, max: 32)();
|
||||
TextColumn get content => text().named('body')();
|
||||
IntColumn get category => integer().nullable()();
|
||||
}
|
||||
|
||||
// This will make drift generate a class called "Category" to represent a row in
|
||||
// this table. By default, "Categorie" would have been used because it only
|
||||
//strips away the trailing "s" in the table name.
|
||||
@DataClassName('Category')
|
||||
class Categories extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get description => text()();
|
||||
}
|
||||
|
||||
// this annotation tells drift to prepare a database class that uses both of the
|
||||
// tables we just defined. We'll see how to use that database class in a moment.
|
||||
// #docregion open
|
||||
@DriftDatabase(tables: [Todos, Categories])
|
||||
class MyDatabase extends _$MyDatabase {
|
||||
// #enddocregion overview
|
||||
// we tell the database where to store the data with this constructor
|
||||
MyDatabase() : super(_openConnection());
|
||||
|
||||
// you should bump this number whenever you change or add a table definition.
|
||||
// Migrations are covered later in the documentation.
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
// #docregion overview
|
||||
}
|
||||
// #enddocregion overview
|
||||
|
||||
LazyDatabase _openConnection() {
|
||||
// the LazyDatabase util lets us find the right location for the file async.
|
||||
return LazyDatabase(() async {
|
||||
// put the database file, called db.sqlite here, into the documents folder
|
||||
// for your app.
|
||||
final dbFolder = await getApplicationDocumentsDirectory();
|
||||
final file = File(p.join(dbFolder.path, 'db.sqlite'));
|
||||
return NativeDatabase.createInBackground(file);
|
||||
});
|
||||
}
|
||||
|
||||
// #enddocregion open
|
||||
// #docregion usage
|
||||
Future<void> main() async {
|
||||
final database = MyDatabase();
|
||||
|
||||
// Simple insert:
|
||||
await database
|
||||
.into(database.categories)
|
||||
.insert(CategoriesCompanion.insert(description: 'my first category'));
|
||||
|
||||
// Simple select:
|
||||
final allCategories = await database.select(database.categories).get();
|
||||
print('Categories in database: $allCategories');
|
||||
}
|
||||
// #enddocregion usage
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
data:
|
||||
title: Advanced Features
|
||||
weight: 20
|
||||
description: Learn about some advanced features of drift
|
||||
template: layouts/docs/list
|
||||
---
|
|
@ -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.
|
|
@ -2,6 +2,7 @@
|
|||
data:
|
||||
title: "Command line tools for drift"
|
||||
description: A set of CLI tools to interact with drift projects
|
||||
weight: 20
|
||||
path: /cli/
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
@ -69,4 +70,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 }}).
|
|
@ -2,14 +2,16 @@
|
|||
data:
|
||||
title: "DAOs"
|
||||
description: Keep your database code modular with DAOs
|
||||
path: /docs/advanced-features/daos/
|
||||
aliases:
|
||||
- /daos/
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
||||
When you have a lot of queries, putting them all into one class might become
|
||||
tedious. You can avoid this by extracting some queries into classes that are
|
||||
tedious. You can avoid this by extracting some queries into classes that are
|
||||
available from your main database class. Consider the following code:
|
||||
|
||||
```dart
|
||||
part 'todos_dao.g.dart';
|
||||
|
||||
|
@ -32,5 +34,6 @@ class TodosDao extends DatabaseAccessor<MyDatabase> with _$TodosDaoMixin {
|
|||
}
|
||||
}
|
||||
```
|
||||
|
||||
If we now change the annotation on the `MyDatabase` class to `@DriftDatabase(tables: [Todos, Categories], daos: [TodosDao])`
|
||||
and re-run the code generation, a generated getter `todosDao` can be used to access the instance of that dao.
|
|
@ -2,7 +2,7 @@
|
|||
data:
|
||||
title: Expressions
|
||||
description: Deep-dive into what kind of SQL expressions can be written in Dart
|
||||
weight: 200
|
||||
weight: 5
|
||||
|
||||
# used to be in the "getting started" section
|
||||
path: docs/getting-started/expressions/
|
||||
|
@ -18,7 +18,7 @@ In most cases, you're writing an expression that combines other expressions. Any
|
|||
column name is a valid expression, so for most `where` clauses you'll be writing
|
||||
a expression that wraps a column name in some kind of comparison.
|
||||
|
||||
{% assign snippets = 'package:drift_docs/snippets/expressions.dart.excerpt.json' | readString | json_decode %}
|
||||
{% assign snippets = 'package:drift_docs/snippets/dart_api/expressions.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
## Comparisons
|
||||
Every expression can be compared to a value by using `equals`. If you want to compare
|
||||
|
@ -100,7 +100,7 @@ select(users)..where((u) => u.birthDate.year.isLessThan(1950))
|
|||
|
||||
The individual fields like `year`, `month` and so on are expressions themselves. This means
|
||||
that you can use operators and comparisons on them.
|
||||
To obtain the current date or the current time as an expression, use the `currentDate`
|
||||
To obtain the current date or the current time as an expression, use the `currentDate`
|
||||
and `currentDateAndTime` constants provided by drift.
|
||||
|
||||
You can also use the `+` and `-` operators to add or subtract a duration from a time column:
|
||||
|
@ -121,12 +121,12 @@ Again, the `isNotIn` function works the other way around.
|
|||
|
||||
## Aggregate functions (like count and sum) {#aggregate}
|
||||
|
||||
[Aggregate functions](https://www.sqlite.org/lang_aggfunc.html) are available
|
||||
[Aggregate functions](https://www.sqlite.org/lang_aggfunc.html) are available
|
||||
from the Dart api. Unlike regular functions, aggregate functions operate on multiple rows at
|
||||
once.
|
||||
once.
|
||||
By default, they combine all rows that would be returned by the select statement into a single value.
|
||||
You can also make them run over different groups in the result by using
|
||||
[group by]({{ "joins.md#group-by" | pageUrl }}).
|
||||
You can also make them run over different groups in the result by using
|
||||
[group by]({{ "select.md#group-by" | pageUrl }}).
|
||||
|
||||
### Comparing
|
||||
|
||||
|
@ -149,13 +149,13 @@ Stream<double> averageItemLength() {
|
|||
```
|
||||
|
||||
__Note__: We're using `selectOnly` instead of `select` because we're not interested in any colum that
|
||||
`todos` provides - we only care about the average length. More details are available
|
||||
[here]({{ "joins.md#group-by" | pageUrl }})
|
||||
`todos` provides - we only care about the average length. More details are available
|
||||
[here]({{ "select.md#group-by" | pageUrl }})
|
||||
|
||||
### Counting
|
||||
|
||||
Sometimes, it's useful to count how many rows are present in a group. By using the
|
||||
[table layout from the example]({{ "../Getting started/index.md" | pageUrl }}), this
|
||||
[table layout from the example]({{ "../setup.md" | pageUrl }}), this
|
||||
query will report how many todo entries are associated to each category:
|
||||
|
||||
```dart
|
||||
|
@ -173,14 +173,14 @@ query
|
|||
..groupBy([categories.id]);
|
||||
```
|
||||
|
||||
If you don't want to count duplicate values, you can use `count(distinct: true)`.
|
||||
If you don't want to count duplicate values, you can use `count(distinct: true)`.
|
||||
Sometimes, you only need to count values that match a condition. For that, you can
|
||||
use the `filter` parameter on `count`.
|
||||
To count all rows (instead of a single value), you can use the top-level `countAll()`
|
||||
function.
|
||||
|
||||
More information on how to write aggregate queries with drift's Dart api is available
|
||||
[here]({{ "joins.md#group-by" | pageUrl }})
|
||||
[here]({{ "select.md#group-by" | pageUrl }})
|
||||
|
||||
### group_concat
|
||||
|
||||
|
@ -200,9 +200,9 @@ with the `separator` argument on `groupConcat`.
|
|||
|
||||
## Mathematical functions and regexp
|
||||
|
||||
When using a `NativeDatabase`, a basic set of trigonometric functions will be available.
|
||||
When using a `NativeDatabase`, a basic set of trigonometric functions will be available.
|
||||
It also defines the `REGEXP` function, which allows you to use `a REGEXP b` in sql queries.
|
||||
For more information, see the [list of functions]({{ "../Other engines/vm.md#moor-only-functions" | pageUrl }}) here.
|
||||
For more information, see the [list of functions]({{ "../Platforms/vm.md#moor-only-functions" | pageUrl }}) here.
|
||||
|
||||
## Subqueries
|
||||
|
||||
|
@ -246,7 +246,7 @@ any rows. For instance, we could use this to find empty categories:
|
|||
### Full subqueries
|
||||
|
||||
Drift also supports subqueries that appear in `JOIN`s, which are described in the
|
||||
[documentation for joins]({{ 'joins.md#subqueries' | pageUrl }}).
|
||||
[documentation for joins]({{ 'select.md#subqueries' | pageUrl }}).
|
||||
|
||||
## Custom expressions
|
||||
If you want to inline custom sql into Dart queries, you can use a `CustomExpression` class.
|
||||
|
@ -258,5 +258,5 @@ select(users)..where((u) => inactive);
|
|||
|
||||
_Note_: It's easy to write invalid queries by using `CustomExpressions` too much. If you feel like
|
||||
you need to use them because a feature you use is not available in drift, consider creating an issue
|
||||
to let us know. If you just prefer sql, you could also take a look at
|
||||
[compiled sql]({{ "../Using SQL/custom_queries.md" | pageUrl }}) which is typesafe to use.
|
||||
to let us know. If you just prefer sql, you could also take a look at
|
||||
[compiled sql]({{ "../SQL API/custom_queries.md" | pageUrl }}) which is typesafe to use.
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
data:
|
||||
title: Dart API
|
||||
description: Drift's Dart library for declaring tables and writing queries.
|
||||
weight: 2
|
||||
template: layouts/docs/list
|
||||
---
|
||||
|
||||
After writing a database [as described in the setup page]({{ '../setup.md' | pageUrl }}),
|
||||
drift will generate code enabling you to write SQL statements in Dart.
|
||||
|
||||
The pages on [select statements]({{ 'select.md' | pageUrl }}) and [insert, updates and deletes]({{ 'writes.md' | pageUrl }})
|
||||
explain how to construct statements.
|
||||
|
||||
Advanced features include support for transactions, complex SQL expressions and [database accessor classes]({{ 'daos.md' | pageUrl }}).
|
|
@ -7,7 +7,7 @@ template: layouts/docs/single
|
|||
|
||||
{% assign snippets = 'package:drift_docs/snippets/modular/schema_inspection.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
Thanks to the typesafe table classes generated by drift, [writing SQL queries]({{ '../Getting started/writing_queries.md' | pageUrl }}) in Dart
|
||||
Thanks to the typesafe table classes generated by drift, [writing SQL queries]({{ '../Dart API/select.md' | pageUrl }}) in Dart
|
||||
is simple and safe.
|
||||
However, these queries are usually written against a specific table. And while drift supports inheritance for tables, sometimes it is easier
|
||||
to access tables reflectively. Luckily, code generated by drift implements interfaces which can be used to do just that.
|
||||
|
@ -15,7 +15,7 @@ to access tables reflectively. Luckily, code generated by drift implements inter
|
|||
Since this is a topic that most drift users will not need, this page mostly gives motivating examples and links to the documentation for relevant
|
||||
drift classes.
|
||||
For instance, you might have multiple independent tables that have an `id` column. And you might want to filter rows by their `id` column.
|
||||
When writing this query against a single table, like the `Todos` table as seen in the [getting started]({{'../Getting started/index.md' | pageUrl }}) page,
|
||||
When writing this query against a single table, like the `Todos` table as seen in the [getting started]({{'../setup.md' | pageUrl }}) page,
|
||||
that's pretty straightforward:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'findTodoEntryById' %}
|
|
@ -1,21 +1,109 @@
|
|||
---
|
||||
data:
|
||||
title: "Advanced queries in Dart"
|
||||
weight: 1
|
||||
description: Use sql joins or custom expressions from the Dart api
|
||||
aliases:
|
||||
- queries/joins/
|
||||
title: "Selects"
|
||||
description: "Select rows or invidiual columns from tables in Dart"
|
||||
weight: 2
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
||||
{% assign snippets = 'package:drift_docs/snippets/queries.dart.excerpt.json' | readString | json_decode %}
|
||||
{% assign tables = 'package:drift_docs/snippets/_shared/todo_tables.dart.excerpt.json' | readString | json_decode %}
|
||||
{% assign snippets = 'package:drift_docs/snippets/dart_api/select.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
This page describes how to write `SELECT` statements with drift's Dart API.
|
||||
To make examples easier to grasp, they're referencing two common tables forming
|
||||
the basis of a todo-list app:
|
||||
|
||||
{% include "blocks/snippet" snippets = tables name = 'tables' %}
|
||||
|
||||
For each table you've specified in the `@DriftDatabase` annotation on your database class,
|
||||
a corresponding getter for a table will be generated. That getter can be used to
|
||||
run statements:
|
||||
|
||||
```dart
|
||||
@DriftDatabase(tables: [TodoItems, Categories])
|
||||
class MyDatabase extends _$MyDatabase {
|
||||
|
||||
// the schemaVersion getter and the constructor from the previous page
|
||||
// have been omitted.
|
||||
|
||||
// loads all todo entries
|
||||
Future<List<TodoItem>> get allTodoItems => select(todoItems).get();
|
||||
|
||||
// watches all todo entries in a given category. The stream will automatically
|
||||
// emit new items whenever the underlying data changes.
|
||||
Stream<List<TodoItem>> watchEntriesInCategory(Category c) {
|
||||
return (select(todos)..where((t) => t.category.equals(c.id))).watch();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Drift makes writing queries easy and safe. This page describes how to write basic select
|
||||
queries, but also explains how to use joins and subqueries for advanced queries.
|
||||
|
||||
## Simple selects
|
||||
|
||||
You can create `select` statements by starting them with `select(tableName)`, where the
|
||||
table name
|
||||
is a field generated for you by drift. Each table used in a database will have a matching field
|
||||
to run queries against. Any query can be run once with `get()` or be turned into an auto-updating
|
||||
stream using `watch()`.
|
||||
|
||||
### Where
|
||||
You can apply filters to a query by calling `where()`. The where method takes a function that
|
||||
should map the given table to an `Expression` of boolean. A common way to create such expression
|
||||
is by using `equals` on expressions. Integer columns can also be compared with `isBiggerThan`
|
||||
and `isSmallerThan`. You can compose expressions using `a & b, a | b` and `a.not()`. For more
|
||||
details on expressions, see [this guide]({{ "../Dart API/expressions.md" | pageUrl }}).
|
||||
|
||||
### Limit
|
||||
You can limit the amount of results returned by calling `limit` on queries. The method accepts
|
||||
the amount of rows to return and an optional offset.
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'limit' %}
|
||||
|
||||
|
||||
### Ordering
|
||||
You can use the `orderBy` method on the select statement. It expects a list of functions that extract the individual
|
||||
ordering terms from the table. You can use any expression as an ordering term - for more details, see
|
||||
[this guide]({{ "../Dart API/expressions.md" | pageUrl }}).
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'order-by' %}
|
||||
|
||||
You can also reverse the order by setting the `mode` property of the `OrderingTerm` to
|
||||
`OrderingMode.desc`.
|
||||
|
||||
### Single values
|
||||
If you know a query is never going to return more than one row, wrapping the result in a `List`
|
||||
can be tedious. Drift lets you work around that with `getSingle` and `watchSingle`:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'single' %}
|
||||
|
||||
If an entry with the provided id exists, it will be sent to the stream. Otherwise,
|
||||
`null` will be added to stream. If a query used with `watchSingle` ever returns
|
||||
more than one entry (which is impossible in this case), an error will be added
|
||||
instead.
|
||||
|
||||
### Mapping
|
||||
Before calling `watch` or `get` (or the single variants), you can use `map` to transform
|
||||
the result.
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'mapping' %}
|
||||
|
||||
### Deferring get vs watch
|
||||
If you want to make your query consumable as either a `Future` or a `Stream`,
|
||||
you can refine your return type using one of the `Selectable` abstract base classes;
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'selectable' %}
|
||||
|
||||
These base classes don't have query-building or `map` methods, signaling to the consumer
|
||||
that they are complete results.
|
||||
|
||||
|
||||
## Joins
|
||||
|
||||
Drift supports sql joins to write queries that operate on more than one table. To use that feature, start
|
||||
a select regular select statement with `select(table)` and then add a list of joins using `.join()`. For
|
||||
inner and left outer joins, a `ON` expression needs to be specified. Here's an example using the tables
|
||||
defined in the [example]({{ "../Getting started/index.md" | pageUrl }}).
|
||||
inner and left outer joins, a `ON` expression needs to be specified.
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'joinIntro' %}
|
||||
|
||||
|
@ -43,26 +131,7 @@ categories.
|
|||
Select statements aren't limited to columns from tables. You can also include more complex expressions in the
|
||||
query. For each row in the result, those expressions will be evaluated by the database engine.
|
||||
|
||||
```dart
|
||||
class EntryWithImportance {
|
||||
final TodoEntry entry;
|
||||
final bool important;
|
||||
|
||||
EntryWithImportance(this.entry, this.important);
|
||||
}
|
||||
|
||||
Future<List<EntryWithImportance>> loadEntries() {
|
||||
// assume that an entry is important if it has the string "important" somewhere in its content
|
||||
final isImportant = todos.content.like('%important%');
|
||||
|
||||
return select(todos).addColumns([isImportant]).map((row) {
|
||||
final entry = row.readTable(todos);
|
||||
final entryIsImportant = row.read(isImportant);
|
||||
|
||||
return EntryWithImportance(entry, entryIsImportant);
|
||||
}).get();
|
||||
}
|
||||
```
|
||||
{% include "blocks/snippet" snippets = snippets name = 'custom-columns' %}
|
||||
|
||||
Note that the `like` check is _not_ performed in Dart - it's sent to the underlying database engine which
|
||||
can efficiently compute it for all rows.
|
||||
|
@ -220,7 +289,7 @@ Any statement can be used as a subquery. But be aware that, unlike [subquery exp
|
|||
|
||||
## JSON support
|
||||
|
||||
{% assign json_snippet = 'package:drift_docs/snippets/queries/json.dart.excerpt.json' | readString | json_decode %}
|
||||
{% assign json_snippet = 'package:drift_docs/snippets/dart_api/json.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
sqlite3 has great support for [JSON operators](https://sqlite.org/json1.html) that are also available
|
||||
in drift (under the additional `'package:drift/extensions/json1.dart'` import).
|
|
@ -1,169 +1,37 @@
|
|||
---
|
||||
data:
|
||||
title: "Dart tables"
|
||||
description: "Further information on defining tables in Dart. This page describes advanced features like constraints, nullability, references and views"
|
||||
weight: 150
|
||||
description: "Everything there is to know about defining SQL tables in Dart."
|
||||
weight: 1
|
||||
template: layouts/docs/single
|
||||
path: /docs/getting-started/advanced_dart_tables/
|
||||
---
|
||||
|
||||
{% block "blocks/pageinfo" %}
|
||||
__Prefer sql?__ If you prefer, you can also declare tables via `CREATE TABLE` statements.
|
||||
Drift's sql analyzer will generate matching Dart code. [Details]({{ "starting_with_sql.md" | pageUrl }}).
|
||||
{% endblock %}
|
||||
{% assign snippets = 'package:drift_docs/snippets/dart_api/tables.dart.excerpt.json' | readString | json_decode %}
|
||||
{% assign setup = 'package:drift_docs/snippets/setup/database.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
{% assign snippets = 'package:drift_docs/snippets/tables/advanced.dart.excerpt.json' | readString | json_decode %}
|
||||
In relational databases, tables are used to describe the structure of rows. By
|
||||
adhering to a predefined schema, drift can generate typesafe code for your
|
||||
database.
|
||||
As already shown in the [setup]({{ '../setup.md#database-class' | pageUrl }})
|
||||
page, drift provides APIs to declare tables in Dart:
|
||||
|
||||
As shown in the [getting started guide]({{ "index.md" | pageUrl }}), sql tables can be written in Dart:
|
||||
```dart
|
||||
class Todos extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get title => text().withLength(min: 6, max: 32)();
|
||||
TextColumn get content => text().named('body')();
|
||||
IntColumn get category => integer().nullable()();
|
||||
}
|
||||
```
|
||||
{% include "blocks/snippet" snippets = setup name = 'table' %}
|
||||
|
||||
In this article, we'll cover some advanced features of this syntax.
|
||||
This page describes the DSL for tables in more detail.
|
||||
|
||||
## Names
|
||||
## Columns
|
||||
|
||||
By default, drift uses the `snake_case` name of the Dart getter in the database. For instance, the
|
||||
table
|
||||
```dart
|
||||
class EnabledCategories extends Table {
|
||||
IntColumn get parentCategory => integer()();
|
||||
// ..
|
||||
}
|
||||
```
|
||||
In each table, you define columns by declaring a getter starting with the type of the column,
|
||||
its name in Dart, and the definition mapped to SQL.
|
||||
In the example above, `IntColumn get category => integer().nullable()();` defines a column
|
||||
holding nullable integer values named `category`.
|
||||
This section describes all the options available when declaring columns.
|
||||
|
||||
Would be generated as `CREATE TABLE enabled_categories (parent_category INTEGER NOT NULL)`.
|
||||
|
||||
To override the table name, simply override the `tableName` getter. An explicit name for
|
||||
columns can be provided with the `named` method:
|
||||
```dart
|
||||
class EnabledCategories extends Table {
|
||||
String get tableName => 'categories';
|
||||
|
||||
IntColumn get parentCategory => integer().named('parent')();
|
||||
}
|
||||
```
|
||||
|
||||
The updated class would be generated as `CREATE TABLE categories (parent INTEGER NOT NULL)`.
|
||||
|
||||
To update the name of a column when serializing data to json, annotate the getter with
|
||||
[`@JsonKey`](https://pub.dev/documentation/drift/latest/drift/JsonKey-class.html).
|
||||
|
||||
You can change the name of the generated data class too. By default, drift will stip a trailing
|
||||
`s` from the table name (so a `Users` table would have a `User` data class).
|
||||
That doesn't work in all cases though. With the `EnabledCategories` class from above, we'd get
|
||||
a `EnabledCategorie` data class. In those cases, you can use the [`@DataClassName`](https://pub.dev/documentation/drift/latest/drift/DataClassName-class.html)
|
||||
annotation to set the desired name.
|
||||
|
||||
## Nullability
|
||||
|
||||
By default, columns may not contain null values. When you forgot to set a value in an insert,
|
||||
an exception will be thrown. When using sql, drift also warns about that at compile time.
|
||||
|
||||
If you do want to make a column nullable, just use `nullable()`:
|
||||
```dart
|
||||
class Items {
|
||||
IntColumn get category => integer().nullable()();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Checks
|
||||
|
||||
If you know that a column (or a row) may only contain certain values, you can use a `CHECK` constraint
|
||||
in SQL to enforce custom constraints on data.
|
||||
|
||||
In Dart, the `check` method on the column builder adds a check constraint to the generated column:
|
||||
|
||||
```dart
|
||||
// sqlite3 will enforce that this column only contains timestamps happening after (the beginning of) 1950.
|
||||
DateTimeColumn get creationTime => dateTime()
|
||||
.check(creationTime.isBiggerThan(Constant(DateTime(1950))))
|
||||
.withDefault(currentDateAndTime)();
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Default values
|
||||
|
||||
You can set a default value for a column. When not explicitly set, the default value will
|
||||
be used when inserting a new row. To set a constant default value, use `withDefault`:
|
||||
|
||||
```dart
|
||||
class Preferences extends Table {
|
||||
TextColumn get name => text()();
|
||||
BoolColumn get enabled => boolean().withDefault(const Constant(false))();
|
||||
}
|
||||
```
|
||||
|
||||
When you later use `into(preferences).insert(PreferencesCompanion.forInsert(name: 'foo'));`, the new
|
||||
row will have its `enabled` column set to false (and not to null, as it normally would).
|
||||
Note that columns with a default value (either through `autoIncrement` or by using a default), are
|
||||
still marked as `@required` in generated data classes. This is because they are meant to represent a
|
||||
full row, and every row will have those values. Use companions when representing partial rows, like
|
||||
for inserts or updates.
|
||||
|
||||
Of course, constants can only be used for static values. But what if you want to generate a dynamic
|
||||
default value for each column? For that, you can use `clientDefault`. It takes a function returning
|
||||
the desired default value. The function will be called for each insert. For instance, here's an
|
||||
example generating a random Uuid using the `uuid` package:
|
||||
```dart
|
||||
final _uuid = Uuid();
|
||||
|
||||
class Users extends Table {
|
||||
TextColumn get id => text().clientDefault(() => _uuid.v4())();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Don't know when to use which? Prefer to use `withDefault` when the default value is constant, or something
|
||||
simple like `currentDate`. For more complicated values, like a randomly generated id, you need to use
|
||||
`clientDefault`. Internally, `withDefault` writes the default value into the `CREATE TABLE` statement. This
|
||||
can be more efficient, but doesn't support dynamic values.
|
||||
|
||||
## Primary keys
|
||||
|
||||
If your table has an `IntColumn` with an `autoIncrement()` constraint, drift recognizes that as the default
|
||||
primary key. If you want to specify a custom primary key for your table, you can override the `primaryKey`
|
||||
getter in your table:
|
||||
|
||||
```dart
|
||||
class GroupMemberships extends Table {
|
||||
IntColumn get group => integer()();
|
||||
IntColumn get user => integer()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {group, user};
|
||||
}
|
||||
```
|
||||
|
||||
Note that the primary key must essentially be constant so that the generator can recognize it. That means:
|
||||
|
||||
- it must be defined with the `=>` syntax, function bodies aren't supported
|
||||
- it must return a set literal without collection elements like `if`, `for` or spread operators
|
||||
|
||||
## Unique Constraints
|
||||
|
||||
Starting from version 1.6.0, `UNIQUE` SQL constraints can be defined on Dart tables too.
|
||||
A unique constraint contains one or more columns. The combination of all columns in a constraint
|
||||
must be unique in the table, or the database will report an error on inserts.
|
||||
|
||||
With drift, a unique constraint can be added to a single column by marking it as `.unique()` in
|
||||
the column builder.
|
||||
A unique set spanning multiple columns can be added by overriding the `uniqueKeys` getter in the
|
||||
`Table` class:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'unique' %}
|
||||
|
||||
## Supported column types
|
||||
### Supported column types
|
||||
|
||||
Drift supports a variety of column types out of the box. You can store custom classes in columns by using
|
||||
[type converters]({{ "../Advanced Features/type_converters.md" | pageUrl }}).
|
||||
[type converters]({{ "../type_converters.md" | pageUrl }}).
|
||||
|
||||
| Dart type | Column | Corresponding SQLite type |
|
||||
|--------------|---------------|-----------------------------------------------------|
|
||||
|
@ -174,8 +42,8 @@ Drift supports a variety of column types out of the box. You can store custom cl
|
|||
| `String` | `text()` | `TEXT` |
|
||||
| `DateTime` | `dateTime()` | `INTEGER` (default) or `TEXT` depending on [options](#datetime-options) |
|
||||
| `Uint8List` | `blob()` | `BLOB` |
|
||||
| `Enum` | `intEnum()` | `INTEGER` (more information available [here]({{ "../Advanced Features/type_converters.md#implicit-enum-converters" | pageUrl }})). |
|
||||
| `Enum` | `textEnum()` | `TEXT` (more information available [here]({{ "../Advanced Features/type_converters.md#implicit-enum-converters" | pageUrl }})). |
|
||||
| `Enum` | `intEnum()` | `INTEGER` (more information available [here]({{ "../type_converters.md#implicit-enum-converters" | pageUrl }})). |
|
||||
| `Enum` | `textEnum()` | `TEXT` (more information available [here]({{ "../type_converters.md#implicit-enum-converters" | pageUrl }})). |
|
||||
|
||||
Note that the mapping for `boolean`, `dateTime` and type converters only applies when storing records in
|
||||
the database.
|
||||
|
@ -183,6 +51,17 @@ They don't affect JSON serialization at all. For instance, `boolean` values are
|
|||
in the `fromJson` factory, even though they would be saved as `0` or `1` in the database.
|
||||
If you want a custom mapping for JSON, you need to provide your own [`ValueSerializer`](https://pub.dev/documentation/drift/latest/drift/ValueSerializer-class.html).
|
||||
|
||||
### Custom column types
|
||||
|
||||
While is constrained by the types supported by sqlite3, it supports type converters
|
||||
to store arbitrary Dart types in SQL.
|
||||
|
||||
{% assign type_converters = 'package:drift_docs/snippets/type_converters/converters.dart.excerpt.json' | readString | json_decode %}
|
||||
{% include "blocks/snippet" snippets = type_converters name = 'table' %}
|
||||
|
||||
For more information about type converters, see the page on [type converters]({{ "../type_converters.md#implicit-enum-converters" | pageUrl }})
|
||||
on this website.
|
||||
|
||||
### `BigInt` support
|
||||
|
||||
Drift supports the `int64()` column builder to indicate that a column stores
|
||||
|
@ -257,10 +136,10 @@ Drift supports two approaches of storing `DateTime` values in SQL:
|
|||
This behavior works well with the date functions in sqlite3 while also
|
||||
preserving "UTC-ness" for stored values.
|
||||
|
||||
The mode can be changed with the `store_date_time_values_as_text` [build option]({{ '../Advanced Features/builder_options.md' | pageUrl }}).
|
||||
The mode can be changed with the `store_date_time_values_as_text` [build option]({{ '../Generation options/index.md' | pageUrl }}).
|
||||
|
||||
Regardless of the option used, drift's builtin support for
|
||||
[date and time functions]({{ '../Advanced Features/expressions.md#date-and-time' | pageUrl }})
|
||||
[date and time functions]({{ 'expressions.md#date-and-time' | pageUrl }})
|
||||
return an equivalent values. Drift internally inserts the `unixepoch`
|
||||
[modifier](https://sqlite.org/lang_datefunc.html#modifiers) when unix timestamps
|
||||
are used to make the date functions work. When comparing dates stored as text,
|
||||
|
@ -287,7 +166,7 @@ option, toggling this behavior is not compatible with existing database schemas:
|
|||
As the second point is specific to usages in your app, this documentation only
|
||||
describes how to migrate stored columns between the format:
|
||||
|
||||
{% assign conversion = "package:drift_docs/snippets/migrations/datetime_conversion.dart.excerpt.json" | readString | json_decode %}
|
||||
{% assign conversion = "package:drift_docs/snippets/dart_api/datetime_conversion.dart.excerpt.json" | readString | json_decode %}
|
||||
|
||||
Note that the JSON serialization generated by default is not affected by the
|
||||
datetime mode chosen. By default, drift will serialize `DateTime` values to a
|
||||
|
@ -337,43 +216,24 @@ When using a `NativeDatabase` with a recent dependency on the
|
|||
`sqlite3_flutter_libs` package, you can safely assume that you are on a recent
|
||||
sqlite3 version with support for `unixepoch`.
|
||||
|
||||
## Custom constraints
|
||||
### Nullability
|
||||
|
||||
Some column and table constraints aren't supported through drift's Dart api. This includes `REFERENCES` clauses on columns, which you can set
|
||||
through `customConstraint`:
|
||||
Drift follows Dart's idiom of non-nullable by default types. This means that
|
||||
columns declared on a table defined in Dart can't store null values by default,
|
||||
they are generated with a `NOT NULL` constraint in SQL.
|
||||
When you forget to set a value in an insert, an exception will be thrown.
|
||||
When using sql, drift also warns about that at compile time.
|
||||
|
||||
```dart
|
||||
class GroupMemberships extends Table {
|
||||
IntColumn get group => integer().customConstraint('NOT NULL REFERENCES groups (id)')();
|
||||
IntColumn get user => integer().customConstraint('NOT NULL REFERENCES users (id)')();
|
||||
If you do want to make a column nullable, just use `nullable()`:
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {group, user};
|
||||
}
|
||||
```
|
||||
{% include "blocks/snippet" snippets = snippets name = 'nnbd' %}
|
||||
|
||||
Applying a `customConstraint` will override all other constraints that would be included by default. In
|
||||
particular, that means that we need to also include the `NOT NULL` constraint again.
|
||||
|
||||
You can also add table-wide constraints by overriding the `customConstraints` getter in your table class.
|
||||
|
||||
## References
|
||||
### References
|
||||
|
||||
[Foreign key references](https://www.sqlite.org/foreignkeys.html) can be expressed
|
||||
in Dart tables with the `references()` method when building a column:
|
||||
|
||||
```dart
|
||||
class Todos extends Table {
|
||||
// ...
|
||||
IntColumn get category => integer().nullable().references(Categories, #id)();
|
||||
}
|
||||
|
||||
@DataClassName("Category")
|
||||
class Categories extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
// and more columns...
|
||||
}
|
||||
```
|
||||
{% include "blocks/snippet" snippets = snippets name = 'references' %}
|
||||
|
||||
The first parameter to `references` points to the table on which a reference should be created.
|
||||
The second parameter is a [symbol](https://dart.dev/guides/language/language-tour#symbols) of the column to use for the reference.
|
||||
|
@ -383,52 +243,167 @@ 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 }}).
|
||||
|
||||
## Views
|
||||
### Default values
|
||||
|
||||
It is also possible to define [SQL views](https://www.sqlite.org/lang_createview.html)
|
||||
as Dart classes.
|
||||
To do so, write an abstract class extending `View`. This example declares a view reading
|
||||
the amount of todo-items added to a category in the schema from [the example]({{ 'index.md' | pageUrl }}):
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'view' %}
|
||||
|
||||
Inside a Dart view, use
|
||||
|
||||
- abstract getters to declare tables that you'll read from (e.g. `TodosTable get todos`).
|
||||
- `Expression` getters to add columns: (e.g. `itemCount => todos.id.count()`).
|
||||
- the overridden `as` method to define the select statement backing the view.
|
||||
The columns referenced in `select` may refer to two kinds of columns:
|
||||
- Columns defined on the view itself (like `itemCount` in the example above).
|
||||
- Columns defined on referenced tables (like `categories.description` in the example).
|
||||
For these references, advanced drift features like [type converters]({{ '../Advanced Features/type_converters.md' | pageUrl }})
|
||||
used in the column's definition from the table are also applied to the view's column.
|
||||
|
||||
Both kind of columns will be added to the data class for the view when selected.
|
||||
|
||||
Finally, a view needs to be added to a database or accessor by including it in the
|
||||
`views` parameter:
|
||||
You can set a default value for a column. When not explicitly set, the default value will
|
||||
be used when inserting a new row. To set a constant default value, use `withDefault`:
|
||||
|
||||
```dart
|
||||
@DriftDatabase(tables: [Todos, Categories], views: [CategoryTodoCount])
|
||||
class MyDatabase extends _$MyDatabase {
|
||||
class Preferences extends Table {
|
||||
TextColumn get name => text()();
|
||||
BoolColumn get enabled => boolean().withDefault(const Constant(false))();
|
||||
}
|
||||
```
|
||||
|
||||
### Nullability of columns in a view
|
||||
When you later use `into(preferences).insert(PreferencesCompanion.forInsert(name: 'foo'));`, the new
|
||||
row will have its `enabled` column set to false (and not to null, as it normally would).
|
||||
Note that columns with a default value (either through `autoIncrement` or by using a default), are
|
||||
still marked as `@required` in generated data classes. This is because they are meant to represent a
|
||||
full row, and every row will have those values. Use companions when representing partial rows, like
|
||||
for inserts or updates.
|
||||
|
||||
For a Dart-defined views, expressions defined as an `Expression` getter are
|
||||
_always_ nullable. This behavior matches `TypedResult.read`, the method used to
|
||||
read results from a complex select statement with custom columns.
|
||||
Of course, constants can only be used for static values. But what if you want to generate a dynamic
|
||||
default value for each column? For that, you can use `clientDefault`. It takes a function returning
|
||||
the desired default value. The function will be called for each insert. For instance, here's an
|
||||
example generating a random Uuid using the `uuid` package:
|
||||
```dart
|
||||
final _uuid = Uuid();
|
||||
|
||||
Columns that reference another table's column are nullable if the referenced
|
||||
column is nullable, or if the selected table does not come from an inner join
|
||||
(because the whole table could be `null` in that case).
|
||||
class Users extends Table {
|
||||
TextColumn get id => text().clientDefault(() => _uuid.v4())();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Considering the view from the example above,
|
||||
Don't know when to use which? Prefer to use `withDefault` when the default value is constant, or something
|
||||
simple like `currentDate`. For more complicated values, like a randomly generated id, you need to use
|
||||
`clientDefault`. Internally, `withDefault` writes the default value into the `CREATE TABLE` statement. This
|
||||
can be more efficient, but doesn't support dynamic values.
|
||||
|
||||
- the `itemCount` column is nullable because it is defined as a complex
|
||||
`Expression`
|
||||
- the `description` column, referencing `categories.description`, is non-nullable.
|
||||
This is because it references `categories`, the primary table of the view's
|
||||
select statement.
|
||||
### Checks
|
||||
|
||||
If you know that a column (or a row) may only contain certain values, you can use a `CHECK` constraint
|
||||
in SQL to enforce custom constraints on data.
|
||||
|
||||
In Dart, the `check` method on the column builder adds a check constraint to the generated column:
|
||||
|
||||
```dart
|
||||
// sqlite3 will enforce that this column only contains timestamps happening after (the beginning of) 1950.
|
||||
DateTimeColumn get creationTime => dateTime()
|
||||
.check(creationTime.isBiggerThan(Constant(DateTime(1950))))
|
||||
.withDefault(currentDateAndTime)();
|
||||
```
|
||||
|
||||
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]({{ '../Migrations/api.md#changing-column-constraints' | pageUrl }}) to re-create the table without the constraint.
|
||||
|
||||
### Unique column
|
||||
|
||||
When an individual column must be unique for all rows in the table, it can be declared as `unique()`
|
||||
in its definition:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = "unique-column" %}
|
||||
|
||||
If the combination of more than one column must be unique in the table, you can add a unique
|
||||
[table constraint](#unique-columns-in-table) to the table.
|
||||
|
||||
### Custom constraints
|
||||
|
||||
Some column and table constraints aren't supported through drift's Dart api. This includes the collation
|
||||
of columns, which you can apply using `customConstraint`:
|
||||
|
||||
```dart
|
||||
class Groups extends Table {
|
||||
TextColumn get name => integer().customConstraint('COLLATE BINARY')();
|
||||
}
|
||||
```
|
||||
|
||||
Applying a `customConstraint` will override all other constraints that would be included by default. In
|
||||
particular, that means that we need to also include the `NOT NULL` constraint again.
|
||||
|
||||
You can also add table-wide constraints by overriding the `customConstraints` getter in your table class.
|
||||
|
||||
## Names
|
||||
|
||||
By default, drift uses the `snake_case` name of the Dart getter in the database. For instance, the
|
||||
table
|
||||
|
||||
{% assign name = 'package:drift_docs/snippets/dart_api/old_name.dart.excerpt.json' | readString | json_decode %}
|
||||
{% include "blocks/snippet" snippets = name %}
|
||||
|
||||
Would be generated as `CREATE TABLE enabled_categories (parent_category INTEGER NOT NULL)`.
|
||||
|
||||
To override the table name, simply override the `tableName` getter. An explicit name for
|
||||
columns can be provided with the `named` method:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name="names" %}
|
||||
|
||||
The updated class would be generated as `CREATE TABLE categories (parent INTEGER NOT NULL)`.
|
||||
|
||||
To update the name of a column when serializing data to json, annotate the getter with
|
||||
[`@JsonKey`](https://pub.dev/documentation/drift/latest/drift/JsonKey-class.html).
|
||||
|
||||
You can change the name of the generated data class too. By default, drift will stip a trailing
|
||||
`s` from the table name (so a `Users` table would have a `User` data class).
|
||||
That doesn't work in all cases though. With the `EnabledCategories` class from above, we'd get
|
||||
a `EnabledCategorie` data class. In those cases, you can use the [`@DataClassName`](https://pub.dev/documentation/drift/latest/drift/DataClassName-class.html)
|
||||
annotation to set the desired name.
|
||||
|
||||
## Existing row classes
|
||||
|
||||
By default, drift generates a row class for each table. This row class can be used to access all columns, it also
|
||||
implements `hashCode`, `operator==` and a few other useful operators.
|
||||
When you want to use your own type hierarchy, or have more control over the generated classes, you can
|
||||
also tell drift to your own class or type:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name="custom-type" %}
|
||||
|
||||
Drift verifies that the type is suitable for storing a row of that table.
|
||||
More details about this feature are [described here]({{ '../custom_row_classes.md' | pageUrl }}).
|
||||
|
||||
## Table options
|
||||
|
||||
In addition to the options added to individual columns, some constraints apply to the whole
|
||||
table.
|
||||
|
||||
### Primary keys
|
||||
|
||||
If your table has an `IntColumn` with an `autoIncrement()` constraint, drift recognizes that as the default
|
||||
primary key. If you want to specify a custom primary key for your table, you can override the `primaryKey`
|
||||
getter in your table:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name="primary-key" %}
|
||||
|
||||
Note that the primary key must essentially be constant so that the generator can recognize it. That means:
|
||||
|
||||
- it must be defined with the `=>` syntax, function bodies aren't supported
|
||||
- it must return a set literal without collection elements like `if`, `for` or spread operators
|
||||
|
||||
### Unique columns in table
|
||||
|
||||
When the value of one column must be unique in the table, you can [make that column unique](#unique-column).
|
||||
When the combined value of multiple columns should be unique, this needs to be declared on the
|
||||
table by overriding the `uniqueKeys` getter:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name="unique-table" %}
|
||||
|
||||
### Custom constraints on tables
|
||||
|
||||
Some table constraints are not directly supported in drift yet. Similar to [custom constraints](#custom-constraints)
|
||||
on columns, you can add those by overriding `customConstraints`:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name="custom-constraint-table" %}
|
||||
|
||||
## Index
|
||||
|
||||
An [index](https://sqlite.org/lang_createindex.html) on columns in a table allows rows identified
|
||||
by these columns to be identified more easily.
|
||||
In drift, you can apply an index to a table with the `@TableIndex` annotation. More than one
|
||||
index can be applied to the same table by repeating the annotation:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name="index" %}
|
||||
|
||||
Each index needs to have its own unique name. Typically, the name of the table is part of the
|
||||
index' name to ensure unique names.
|
|
@ -1,15 +1,16 @@
|
|||
---
|
||||
data:
|
||||
title: "Transactions"
|
||||
weight: 70
|
||||
weight: 4
|
||||
description: Run multiple statements atomically
|
||||
|
||||
template: layouts/docs/single
|
||||
path: /docs/transactions/
|
||||
aliases:
|
||||
- /transactions/
|
||||
---
|
||||
|
||||
{% assign snippets = "package:drift_docs/snippets/transactions.dart.excerpt.json" | readString | json_decode %}
|
||||
{% assign snippets = "package:drift_docs/snippets/dart_api/transactions.dart.excerpt.json" | readString | json_decode %}
|
||||
|
||||
Drift has support for transactions and allows multiple statements to run atomically,
|
||||
so that none of their changes is visible to the main database until the transaction
|
|
@ -0,0 +1,52 @@
|
|||
---
|
||||
data:
|
||||
title: "Views"
|
||||
description: How to define SQL views as Dart classes
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
||||
It is also possible to define [SQL views](https://www.sqlite.org/lang_createview.html)
|
||||
as Dart classes.
|
||||
To do so, write an abstract class extending `View`. This example declares a view reading
|
||||
the amount of todo-items added to a category in the schema from [the example]({{ 'index.md' | pageUrl }}):
|
||||
{% assign snippets = 'package:drift_docs/snippets/dart_api/views.dart.excerpt.json' | readString | json_decode %}
|
||||
{% include "blocks/snippet" snippets = snippets name = 'view' %}
|
||||
|
||||
Inside a Dart view, use
|
||||
|
||||
- abstract getters to declare tables that you'll read from (e.g. `TodosTable get todos`).
|
||||
- `Expression` getters to add columns: (e.g. `itemCount => todos.id.count()`).
|
||||
- the overridden `as` method to define the select statement backing the view.
|
||||
The columns referenced in `select` may refer to two kinds of columns:
|
||||
- Columns defined on the view itself (like `itemCount` in the example above).
|
||||
- Columns defined on referenced tables (like `categories.description` in the example).
|
||||
For these references, advanced drift features like [type converters]({{ '../type_converters.md' | pageUrl }})
|
||||
used in the column's definition from the table are also applied to the view's column.
|
||||
|
||||
Both kind of columns will be added to the data class for the view when selected.
|
||||
|
||||
Finally, a view needs to be added to a database or accessor by including it in the
|
||||
`views` parameter:
|
||||
|
||||
```dart
|
||||
@DriftDatabase(tables: [Todos, Categories], views: [CategoryTodoCount])
|
||||
class MyDatabase extends _$MyDatabase {
|
||||
```
|
||||
|
||||
### Nullability of columns in a view
|
||||
|
||||
For a Dart-defined views, expressions defined as an `Expression` getter are
|
||||
_always_ nullable. This behavior matches `TypedResult.read`, the method used to
|
||||
read results from a complex select statement with custom columns.
|
||||
|
||||
Columns that reference another table's column are nullable if the referenced
|
||||
column is nullable, or if the selected table does not come from an inner join
|
||||
(because the whole table could be `null` in that case).
|
||||
|
||||
Considering the view from the example above,
|
||||
|
||||
- the `itemCount` column is nullable because it is defined as a complex
|
||||
`Expression`
|
||||
- the `description` column, referencing `categories.description`, is non-nullable.
|
||||
This is because it references `categories`, the primary table of the view's
|
||||
select statement.
|
|
@ -1,127 +1,13 @@
|
|||
---
|
||||
data:
|
||||
title: "Writing queries"
|
||||
linkTitle: "Writing queries"
|
||||
description: Learn how to write database queries in pure Dart with drift
|
||||
weight: 100
|
||||
aliases:
|
||||
- /queries/
|
||||
title: "Writes (update, insert, delete)"
|
||||
description: "Select rows or invidiual columns from tables in Dart"
|
||||
weight: 3
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
||||
{% block "blocks/pageinfo" %}
|
||||
__Note__: This assumes that you've already completed [the setup]({{ "index.md" | pageUrl }}).
|
||||
{% endblock %}
|
||||
|
||||
For each table you've specified in the `@DriftDatabase` annotation on your database class,
|
||||
a corresponding getter for a table will be generated. That getter can be used to
|
||||
run statements:
|
||||
```dart
|
||||
// inside the database class, the `todos` getter has been created by drift.
|
||||
@DriftDatabase(tables: [Todos, Categories])
|
||||
class MyDatabase extends _$MyDatabase {
|
||||
|
||||
// the schemaVersion getter and the constructor from the previous page
|
||||
// have been omitted.
|
||||
|
||||
// loads all todo entries
|
||||
Future<List<Todo>> get allTodoEntries => select(todos).get();
|
||||
|
||||
// watches all todo entries in a given category. The stream will automatically
|
||||
// emit new items whenever the underlying data changes.
|
||||
Stream<List<Todo>> watchEntriesInCategory(Category c) {
|
||||
return (select(todos)..where((t) => t.category.equals(c.id))).watch();
|
||||
}
|
||||
}
|
||||
```
|
||||
## Select statements
|
||||
You can create `select` statements by starting them with `select(tableName)`, where the
|
||||
table name
|
||||
is a field generated for you by drift. Each table used in a database will have a matching field
|
||||
to run queries against. Any query can be run once with `get()` or be turned into an auto-updating
|
||||
stream using `watch()`.
|
||||
### Where
|
||||
You can apply filters to a query by calling `where()`. The where method takes a function that
|
||||
should map the given table to an `Expression` of boolean. A common way to create such expression
|
||||
is by using `equals` on expressions. Integer columns can also be compared with `isBiggerThan`
|
||||
and `isSmallerThan`. You can compose expressions using `a & b, a | b` and `a.not()`. For more
|
||||
details on expressions, see [this guide]({{ "../Advanced Features/expressions.md" | pageUrl }}).
|
||||
|
||||
### Limit
|
||||
You can limit the amount of results returned by calling `limit` on queries. The method accepts
|
||||
the amount of rows to return and an optional offset.
|
||||
|
||||
```dart
|
||||
Future<List<Todo>> limitTodos(int limit, {int offset}) {
|
||||
return (select(todos)..limit(limit, offset: offset)).get();
|
||||
}
|
||||
```
|
||||
|
||||
### Ordering
|
||||
You can use the `orderBy` method on the select statement. It expects a list of functions that extract the individual
|
||||
ordering terms from the table. You can use any expression as an ordering term - for more details, see
|
||||
[this guide]({{ "../Advanced Features/expressions.md" | pageUrl }}).
|
||||
|
||||
```dart
|
||||
Future<List<Todo>> sortEntriesAlphabetically() {
|
||||
return (select(todos)..orderBy([(t) => OrderingTerm(expression: t.title)])).get();
|
||||
}
|
||||
```
|
||||
You can also reverse the order by setting the `mode` property of the `OrderingTerm` to
|
||||
`OrderingMode.desc`.
|
||||
|
||||
### Single values
|
||||
If you know a query is never going to return more than one row, wrapping the result in a `List`
|
||||
can be tedious. Drift lets you work around that with `getSingle` and `watchSingle`:
|
||||
```dart
|
||||
Stream<Todo> entryById(int id) {
|
||||
return (select(todos)..where((t) => t.id.equals(id))).watchSingle();
|
||||
}
|
||||
```
|
||||
If an entry with the provided id exists, it will be sent to the stream. Otherwise,
|
||||
`null` will be added to stream. If a query used with `watchSingle` ever returns
|
||||
more than one entry (which is impossible in this case), an error will be added
|
||||
instead.
|
||||
|
||||
### Mapping
|
||||
Before calling `watch` or `get` (or the single variants), you can use `map` to transform
|
||||
the result.
|
||||
```dart
|
||||
Stream<List<String>> contentWithLongTitles() {
|
||||
final query = select(todos)
|
||||
..where((t) => t.title.length.isBiggerOrEqualValue(16));
|
||||
|
||||
return query
|
||||
.map((row) => row.content)
|
||||
.watch();
|
||||
}
|
||||
```
|
||||
|
||||
### Deferring get vs watch
|
||||
If you want to make your query consumable as either a `Future` or a `Stream`,
|
||||
you can refine your return type using one of the `Selectable` abstract base classes;
|
||||
```dart
|
||||
// Exposes `get` and `watch`
|
||||
MultiSelectable<Todo> pageOfTodos(int page, {int pageSize = 10}) {
|
||||
return select(todos)..limit(pageSize, offset: page);
|
||||
}
|
||||
|
||||
// Exposes `getSingle` and `watchSingle`
|
||||
SingleSelectable<Todo> entryById(int id) {
|
||||
return select(todos)..where((t) => t.id.equals(id));
|
||||
}
|
||||
|
||||
// Exposes `getSingleOrNull` and `watchSingleOrNull`
|
||||
SingleOrNullSelectable<Todo> entryFromExternalLink(int id) {
|
||||
return select(todos)..where((t) => t.id.equals(id));
|
||||
}
|
||||
```
|
||||
These base classes don't have query-building or `map` methods, signaling to the consumer
|
||||
that they are complete results.
|
||||
|
||||
If you need more complex queries with joins or custom columns, see [this site]({{ "../Advanced Features/joins.md" | pageUrl }}).
|
||||
|
||||
## Updates and deletes
|
||||
|
||||
You can use the generated classes to update individual fields of any row:
|
||||
```dart
|
||||
Future moveImportantTasksIntoCategory(Category target) {
|
||||
|
@ -297,14 +183,14 @@ generated.
|
|||
|
||||
__Note:__ This uses the `RETURNING` syntax added in sqlite3 version 3.35, which is not available on most operating systems by default. When using this method, make sure that you have a recent sqlite3 version available. This is the case with `sqlite3_flutter_libs`.
|
||||
|
||||
For instance, consider this snippet using the tables from the [getting started guide]({{ 'index.md' | pageUrl }}):
|
||||
For instance, consider this snippet using the tables from the [getting started guide]({{ '../setup.md' | pageUrl }}):
|
||||
|
||||
```dart
|
||||
final row = await into(todos).insertReturning(TodosCompanion.insert(
|
||||
title: 'A todo entry',
|
||||
content: 'A description',
|
||||
));
|
||||
```
|
||||
```
|
||||
|
||||
The `row` returned has the proper `id` set. If a table has further default
|
||||
values, including dynamic values like `CURRENT_TIME`, then those would also be
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
data:
|
||||
title: "Examples"
|
||||
weight: 3
|
||||
weight: 30
|
||||
description: Example apps using drift
|
||||
template: layouts/docs/list
|
||||
---
|
||||
|
@ -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
|
||||
|
|
|
@ -16,7 +16,7 @@ queries in drift. First, we need to store some items that can be bought:
|
|||
|
||||
We're going to define two tables for shopping carts: One for the cart
|
||||
itself, and another one to store the entries in the cart.
|
||||
The latter uses [references]({{ '../Getting started/advanced_dart_tables.md#references' | pageUrl }})
|
||||
The latter uses [references]({{ '../Dart API/tables.md#references' | pageUrl }})
|
||||
to express the foreign key constraints of referencing existing shopping
|
||||
carts or product items.
|
||||
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
data:
|
||||
title: "Using drift classes in other builders"
|
||||
description: Configure your build to allow drift dataclasses to be seen by other builders.
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
||||
It is possible to use classes generated by drift in other builders.
|
||||
Due to technicalities related to Dart's build system and `source_gen`, this approach requires a custom configuration
|
||||
and minor code changes. Put this content in a file called `build.yaml` next to your `pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
targets:
|
||||
$default:
|
||||
# disable the default generators, we'll only use the non-shared drift generator here
|
||||
auto_apply_builders: false
|
||||
builders:
|
||||
drift_dev|not_shared:
|
||||
enabled: true
|
||||
# If needed, you can configure the builder like this:
|
||||
# options:
|
||||
# skip_verification_code: true
|
||||
# use_experimental_inference: true
|
||||
# This builder is necessary for drift-file preprocessing. You can disable it if you're not
|
||||
# using .drift files with type converters.
|
||||
drift_dev|preparing_builder:
|
||||
enabled: true
|
||||
|
||||
run_built_value:
|
||||
dependencies: ['your_package_name']
|
||||
builders:
|
||||
# Disable drift builders. By default, those would run on each target
|
||||
drift_dev:
|
||||
enabled: false
|
||||
drift_dev|preparing_builder:
|
||||
enabled: false
|
||||
# we don't need to disable drift|not_shared, because it's disabled by default
|
||||
```
|
||||
|
||||
In all files that use generated drift code, you'll have to replace `part 'filename.g.dart'` with `part 'filename.drift.dart'`.
|
||||
If you use drift _and_ another builder in the same file, you'll need both `.g.dart` and `.drift.dart` as part-files.
|
||||
|
||||
A full example is available as part of [the drift repo](https://github.com/simolus3/drift/tree/develop/examples/with_built_value).
|
||||
|
||||
If you run into any problems with this approach, feel free to open an issue on drift.
|
||||
|
||||
## The technicalities, explained
|
||||
|
||||
Almost all code generation packages use a so called "shared part file" approach provided by `source_gen`.
|
||||
It's a common protocol that allows unrelated builders to write into the same `.g.dart` file.
|
||||
For this to work, each builder first writes a `.part` file with its name. For instance, if you used `drift`
|
||||
and `built_value` in the same project, those part files could be called `.drift.part` and `.built_value.part`.
|
||||
Later, the common `source_gen` package would merge the part files into a single `.g.dart` file.
|
||||
|
||||
This works great for most use cases, but a downside is that each builder can't see the final `.g.dart`
|
||||
file, or use any classes or methods defined in it. To fix that, drift offers an optional builder -
|
||||
`drift_dev|not_shared` - that will generate a separate part file only containing
|
||||
code generated by drift. So most of the work resolves around disabling the default generator of drift
|
||||
and use the non-shared generator instead.
|
||||
|
||||
Finally, we need to the build system to run drift first, and all the other builders otherwise. This is
|
||||
why we split the builders up into multiple targets. The first target will only run drift, the second
|
||||
target has a dependency on the first one and will run all the other builders.
|
|
@ -1,11 +1,12 @@
|
|||
---
|
||||
data:
|
||||
title: "Builder options"
|
||||
description: >-
|
||||
Advanced options applied when writing the generated code
|
||||
template: layouts/docs/single
|
||||
aliases:
|
||||
- "options/"
|
||||
title: Generation options
|
||||
description: Options for `drift_dev` and `build_runner` to change the generated code.
|
||||
weight: 7
|
||||
template: layouts/docs/list
|
||||
#path: docs/advanced-features/builder_options/
|
||||
#aliases:
|
||||
# - "options/"
|
||||
---
|
||||
|
||||
The `drift_dev` package supports a range of options that control how code
|
||||
|
@ -44,7 +45,7 @@ At the moment, drift supports these options:
|
|||
of inserted data and report detailed errors when the integrity is violated. If you're only using
|
||||
inserts with SQL, or don't need this functionality, enabling this flag can help to reduce the amount
|
||||
generated code.
|
||||
* `use_data_class_name_for_companions`: By default, the name for [companion classes]({{ "../Getting started/writing_queries.md#updates-and-deletes" | pageUrl }})
|
||||
* `use_data_class_name_for_companions`: By default, the name for [companion classes]({{ "../Dart API/writes.md#updates-and-deletes" | pageUrl }})
|
||||
is based on the table name (e.g. a `@DataClassName('Users') class UsersTable extends Table` would generate
|
||||
a `UsersTableCompanion`). With this option, the name is based on the data class (so `UsersCompanion` in
|
||||
this case).
|
||||
|
@ -67,15 +68,15 @@ At the moment, drift supports these options:
|
|||
to `null`.
|
||||
* `named_parameters`: Generates named parameters for named variables in SQL queries.
|
||||
* `named_parameters_always_required`: All named parameters (generated if `named_parameters` option is `true`) will be required in Dart.
|
||||
* `scoped_dart_components` (defaults to `true`): Generates a function parameter for [Dart placeholders]({{ '../Using SQL/drift_files.md#dart-components-in-sql' | pageUrl }}) in SQL.
|
||||
* `scoped_dart_components` (defaults to `true`): Generates a function parameter for [Dart placeholders]({{ '../SQL API/drift_files.md#dart-components-in-sql' | pageUrl }}) in SQL.
|
||||
The function has a parameter for each table that is available in the query, making it easier to get aliases right when using
|
||||
Dart placeholders.
|
||||
* `store_date_time_values_as_text`: Whether date-time columns should be stored as ISO 8601 string instead of a unix timestamp.
|
||||
For more information on these modes, see [datetime options]({{ '../Getting started/advanced_dart_tables#datetime-options' | pageUrl }}).
|
||||
For more information on these modes, see [datetime options]({{ '../Dart API/tables.md#datetime-options' | pageUrl }}).
|
||||
* `case_from_dart_to_sql` (defaults to `snake_case`): Controls how the table and column names are re-cased from the Dart identifiers.
|
||||
The possible values are `preserve`, `camelCase`, `CONSTANT_CASE`, `snake_case`, `PascalCase`, `lowercase` and `UPPERCASE` (default: `snake_case`).
|
||||
* `write_to_columns_mixins`: Whether the `toColumns` method should be written as a mixin instead of being added directly to the data class.
|
||||
This is useful when using [existing row classes]({{ 'custom_row_classes.md' | pageUrl }}), as the mixin is generated for those as well.
|
||||
This is useful when using [existing row classes]({{ '../custom_row_classes.md' | pageUrl }}), as the mixin is generated for those as well.
|
||||
* `fatal_warnings`: When enabled (defaults to `false`), warnings found by `drift_dev` in the build process (like syntax errors in SQL queries or
|
||||
unresolved references in your Dart tables) will cause the build to fail.
|
||||
* `preamble`: This option is useful when using drift [as a standalone part builder](#using-drift-classes-in-other-builders) or when running a
|
||||
|
@ -174,7 +175,7 @@ We currently support the following extensions:
|
|||
Enabling this option is safe when using a `NativeDatabase` with `sqlite3_flutter_libs`,
|
||||
which compiles sqlite3 with the R*Tree extension enabled.
|
||||
- `moor_ffi`: Enables support for functions that are only available when using a `NativeDatabase`. This contains `pow`, `sqrt` and a variety
|
||||
of trigonometric functions. Details on those functions are available [here]({{ "../Other engines/vm.md#moor-only-functions" | pageUrl }}).
|
||||
of trigonometric functions. Details on those functions are available [here]({{ "../Platforms/vm.md#moor-only-functions" | pageUrl }}).
|
||||
- `math`: Assumes that sqlite3 was compiled with [math functions](https://www.sqlite.org/lang_mathfunc.html).
|
||||
This module is largely incompatible with the `moor_ffi` module.
|
||||
- `spellfix1`: Assumes that the [spellfix1](https://www.sqlite.org/spellfix1.html)
|
||||
|
@ -226,187 +227,7 @@ We recommend enabling these options.
|
|||
{% endcomment %}
|
||||
However, you can disable some default drift features and reduce the amount of generated code with the following options:
|
||||
|
||||
- `skip_verification_code: true`: You can remove a significant portion of generated code with this option. The
|
||||
downside is that error messages when inserting invalid data will be less specific.
|
||||
- `skip_verification_code: true`: You can remove a significant portion of generated code with this option. The
|
||||
downside is that error messages when inserting invalid data will be less specific.
|
||||
- `data_class_to_companions: false`: Don't generate the `toCompanion` method on data classes. If you don't need that
|
||||
method, you can disable this option.
|
||||
|
||||
## Using drift classes in other builders
|
||||
|
||||
It is possible to use classes generated by drift in other builders.
|
||||
Due to technicalities related to Dart's build system and `source_gen`, this approach requires a custom configuration
|
||||
and minor code changes. Put this content in a file called `build.yaml` next to your `pubspec.yaml`:
|
||||
|
||||
```yaml
|
||||
targets:
|
||||
$default:
|
||||
# disable the default generators, we'll only use the non-shared drift generator here
|
||||
auto_apply_builders: false
|
||||
builders:
|
||||
drift_dev|not_shared:
|
||||
enabled: true
|
||||
# If needed, you can configure the builder like this:
|
||||
# options:
|
||||
# skip_verification_code: true
|
||||
# use_experimental_inference: true
|
||||
# This builder is necessary for drift-file preprocessing. You can disable it if you're not
|
||||
# using .drift files with type converters.
|
||||
drift_dev|preparing_builder:
|
||||
enabled: true
|
||||
|
||||
run_built_value:
|
||||
dependencies: ['your_package_name']
|
||||
builders:
|
||||
# Disable drift builders. By default, those would run on each target
|
||||
drift_dev:
|
||||
enabled: false
|
||||
drift_dev|preparing_builder:
|
||||
enabled: false
|
||||
# we don't need to disable drift|not_shared, because it's disabled by default
|
||||
```
|
||||
|
||||
In all files that use generated drift code, you'll have to replace `part 'filename.g.dart'` with `part 'filename.drift.dart'`.
|
||||
If you use drift _and_ another builder in the same file, you'll need both `.g.dart` and `.drift.dart` as part-files.
|
||||
|
||||
A full example is available as part of [the drift repo](https://github.com/simolus3/drift/tree/develop/examples/with_built_value).
|
||||
|
||||
If you run into any problems with this approach, feel free to open an issue on drift.
|
||||
|
||||
### The technicalities, explained
|
||||
|
||||
Almost all code generation packages use a so called "shared part file" approach provided by `source_gen`.
|
||||
It's a common protocol that allows unrelated builders to write into the same `.g.dart` file.
|
||||
For this to work, each builder first writes a `.part` file with its name. For instance, if you used `drift`
|
||||
and `built_value` in the same project, those part files could be called `.drift.part` and `.built_value.part`.
|
||||
Later, the common `source_gen` package would merge the part files into a single `.g.dart` file.
|
||||
|
||||
This works great for most use cases, but a downside is that each builder can't see the final `.g.dart`
|
||||
file, or use any classes or methods defined in it. To fix that, drift offers an optional builder -
|
||||
`drift_dev|not_shared` - that will generate a separate part file only containing
|
||||
code generated by drift. So most of the work resolves around disabling the default generator of drift
|
||||
and use the non-shared generator instead.
|
||||
|
||||
Finally, we need to the build system to run drift first, and all the other builders otherwise. This is
|
||||
why we split the builders up into multiple targets. The first target will only run drift, the second
|
||||
target has a dependency on the first one and will run all the other builders.
|
||||
|
||||
## Modular code generation
|
||||
|
||||
By default, drift generates code from a single entrypoint - all tables, views
|
||||
and queries for a database are generated into a single part file.
|
||||
For larger projects, this file can become quite large, slowing down builds and
|
||||
the analyzer when it is re-generated.
|
||||
Drift supports an alternative and modular code-generation mode intended as an
|
||||
alternative for larger projects.
|
||||
With this setup, drift generates multiple files and automatically manages
|
||||
imports between them.
|
||||
|
||||
As a motivating example, consider a large drift project with many tables or
|
||||
views being split across different files:
|
||||
|
||||
```
|
||||
lib/src/database/
|
||||
├── database.dart
|
||||
├── tables/
|
||||
│ ├── users.drift
|
||||
│ ├── settings.drift
|
||||
│ ├── groups.drift
|
||||
│ └── search.drift
|
||||
└── views/
|
||||
├── friends.drift
|
||||
└── categories.dart
|
||||
```
|
||||
|
||||
While a modular structure (with `import`s in drift files) is helpful to structure
|
||||
sources, drift still generates everything into a single `database.g.dart` file.
|
||||
With a growing number of tables and queries, drift may need to generate tens of
|
||||
thousands of lines of code for data classes, companions and query results.
|
||||
|
||||
With its modular generation mode, drift instead generates sources for each input
|
||||
file, like this:
|
||||
|
||||
```
|
||||
lib/src/database/
|
||||
├── database.dart
|
||||
├── database.drift.dart
|
||||
├── tables/
|
||||
│ ├── users.drift
|
||||
│ ├── users.drift.dart
|
||||
│ ├── settings.drift
|
||||
│ ├── settings.drift.dart
|
||||
│ └── ...
|
||||
└── views/
|
||||
├── friends.drift
|
||||
├── friends.drift.dart
|
||||
├── categories.dart
|
||||
└── categories.drift.dart
|
||||
```
|
||||
|
||||
### Enabling modular code generation
|
||||
|
||||
_Note_: A small example using modular code generation is also part of [drift's repository](https://github.com/simolus3/drift/tree/develop/examples/modular).
|
||||
|
||||
As drift's modular code generation mode generates different file patterns than
|
||||
the default builder, it needs to be enabled explicitly. For this, create a
|
||||
`build.yaml` file in which you disable the default `drift_dev` build and enable
|
||||
the two builders for modular generation: `drift_dev:analyzer` and
|
||||
`drift_dev:modular`. They should both get the same options:
|
||||
|
||||
```yaml
|
||||
targets:
|
||||
$default:
|
||||
builders:
|
||||
drift_dev:
|
||||
# disable drift's default builder, we're using the modular setup
|
||||
# instead.
|
||||
enabled: false
|
||||
|
||||
# Instead, enable drift_dev:analyzer and drift_dev:modular manually:
|
||||
drift_dev:analyzer:
|
||||
enabled: true
|
||||
options: &options
|
||||
# Drift build options, as per https://drift.simonbinder.eu/docs/advanced-features/builder_options/
|
||||
store_date_time_values_as_text: true
|
||||
named_parameters: true
|
||||
sql:
|
||||
dialect: sqlite
|
||||
options:
|
||||
version: "3.39"
|
||||
modules: [fts5]
|
||||
drift_dev:modular:
|
||||
enabled: true
|
||||
# We use yaml anchors to give the two builders the same options
|
||||
options: *options
|
||||
```
|
||||
|
||||
### What gets generated
|
||||
|
||||
With modular generation, drift generates standalone Dart libraries (Dart files
|
||||
without a `part of` statement). This also means that you no longer need `part`
|
||||
statements in your sources. Instead, you import the generated `.drift.dart`
|
||||
files.
|
||||
|
||||
When it comes to using the generated code, not much is different: The API for
|
||||
the database and DAOs stays mostly the same.
|
||||
A big exception are how `.drift` files are handled in the modular generation
|
||||
mode. In the default builder, all queries in all drift files are generated as
|
||||
methods on the database.
|
||||
With modular code generation, drift generates an implicit database accessor
|
||||
reachable through getters from the database class. Consider a file `user.drift`
|
||||
like this:
|
||||
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id INTEGER NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
name TEXT NOT NULL,
|
||||
is_admin BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
findUsers($predicate = TRUE): SELECT * FROM users WHERE $predicate;
|
||||
```
|
||||
|
||||
If such a `users.drift` file is included from a database, we no longer generate
|
||||
a `findUsers` method for the database itself.
|
||||
Instead, a `users.drift.dart` file contains a [database accessor]({{ 'daos.md' | pageUrl }}) called `UsersDrift` which is implicitly added to the database.
|
||||
To call `findUsers`, you'd now call `database.usersDrift.findUsers()`.
|
|
@ -0,0 +1,125 @@
|
|||
---
|
||||
data:
|
||||
title: "Modular code generation"
|
||||
description: Make drift generate code in multiple files.
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
||||
By default, drift generates code from a single entrypoint - all tables, views
|
||||
and queries for a database are generated into a single part file.
|
||||
For larger projects, this file can become quite large, slowing down builds and
|
||||
the analyzer when it is re-generated.
|
||||
Drift supports an alternative and modular code-generation mode intended as an
|
||||
alternative for larger projects.
|
||||
With this setup, drift generates multiple files and automatically manages
|
||||
imports between them.
|
||||
|
||||
As a motivating example, consider a large drift project with many tables or
|
||||
views being split across different files:
|
||||
|
||||
```
|
||||
lib/src/database/
|
||||
├── database.dart
|
||||
├── tables/
|
||||
│ ├── users.drift
|
||||
│ ├── settings.drift
|
||||
│ ├── groups.drift
|
||||
│ └── search.drift
|
||||
└── views/
|
||||
├── friends.drift
|
||||
└── categories.dart
|
||||
```
|
||||
|
||||
While a modular structure (with `import`s in drift files) is helpful to structure
|
||||
sources, drift still generates everything into a single `database.g.dart` file.
|
||||
With a growing number of tables and queries, drift may need to generate tens of
|
||||
thousands of lines of code for data classes, companions and query results.
|
||||
|
||||
With its modular generation mode, drift instead generates sources for each input
|
||||
file, like this:
|
||||
|
||||
```
|
||||
lib/src/database/
|
||||
├── database.dart
|
||||
├── database.drift.dart
|
||||
├── tables/
|
||||
│ ├── users.drift
|
||||
│ ├── users.drift.dart
|
||||
│ ├── settings.drift
|
||||
│ ├── settings.drift.dart
|
||||
│ └── ...
|
||||
└── views/
|
||||
├── friends.drift
|
||||
├── friends.drift.dart
|
||||
├── categories.dart
|
||||
└── categories.drift.dart
|
||||
```
|
||||
|
||||
## Enabling modular code generation
|
||||
|
||||
_Note_: A small example using modular code generation is also part of [drift's repository](https://github.com/simolus3/drift/tree/develop/examples/modular).
|
||||
|
||||
As drift's modular code generation mode generates different file patterns than
|
||||
the default builder, it needs to be enabled explicitly. For this, create a
|
||||
`build.yaml` file in which you disable the default `drift_dev` build and enable
|
||||
the two builders for modular generation: `drift_dev:analyzer` and
|
||||
`drift_dev:modular`. They should both get the same options:
|
||||
|
||||
```yaml
|
||||
targets:
|
||||
$default:
|
||||
builders:
|
||||
drift_dev:
|
||||
# disable drift's default builder, we're using the modular setup
|
||||
# instead.
|
||||
enabled: false
|
||||
|
||||
# Instead, enable drift_dev:analyzer and drift_dev:modular manually:
|
||||
drift_dev:analyzer:
|
||||
enabled: true
|
||||
options: &options
|
||||
# Drift build options, as per https://drift.simonbinder.eu/docs/advanced-features/builder_options/
|
||||
store_date_time_values_as_text: true
|
||||
named_parameters: true
|
||||
sql:
|
||||
dialect: sqlite
|
||||
options:
|
||||
version: "3.39"
|
||||
modules: [fts5]
|
||||
drift_dev:modular:
|
||||
enabled: true
|
||||
# We use yaml anchors to give the two builders the same options
|
||||
options: *options
|
||||
```
|
||||
|
||||
## What gets generated
|
||||
|
||||
With modular generation, drift generates standalone Dart libraries (Dart files
|
||||
without a `part of` statement). This also means that you no longer need `part`
|
||||
statements in your sources. Instead, you import the generated `.drift.dart`
|
||||
files.
|
||||
|
||||
When it comes to using the generated code, not much is different: The API for
|
||||
the database and DAOs stays mostly the same.
|
||||
A big exception are how `.drift` files are handled in the modular generation
|
||||
mode. In the default builder, all queries in all drift files are generated as
|
||||
methods on the database.
|
||||
With modular code generation, drift generates an implicit database accessor
|
||||
reachable through getters from the database class. Consider a file `user.drift`
|
||||
like this:
|
||||
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id INTEGER NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
name TEXT NOT NULL,
|
||||
is_admin BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
findUsers($predicate = TRUE): SELECT * FROM users WHERE $predicate;
|
||||
```
|
||||
|
||||
If such a `users.drift` file is included from a database, we no longer generate
|
||||
a `findUsers` method for the database itself.
|
||||
Instead, a `users.drift.dart` file contains a [database accessor]({{ '../Dart API/daos.md' | pageUrl }}) called `UsersDrift` which is implicitly added to the database.
|
||||
To call `findUsers`, you'd now call `database.usersDrift.findUsers()`.
|
|
@ -1,101 +0,0 @@
|
|||
---
|
||||
data:
|
||||
title: Getting started
|
||||
description: Simple guide to get a drift project up and running.
|
||||
weight: 1
|
||||
hide_section_index: true
|
||||
template: layouts/docs/list
|
||||
aliases:
|
||||
- /getting-started/ # Used to have this url
|
||||
---
|
||||
|
||||
In addition to this document, other resources on how to use drift also exist.
|
||||
For instance, [this playlist](https://www.youtube.com/watch?v=8ESbEFC0z5Y&list=PLztm2TugcV9Tn6J_H5mtxYIBN40uMAZgO)
|
||||
or [this older video by Reso Coder](https://www.youtube.com/watch?v=zpWsedYMczM&t=281s) might be for you
|
||||
if you prefer a tutorial video.
|
||||
|
||||
If you want to look at an example app instead, a cross-platform Flutter app using drift is available
|
||||
[as part of the drift repository](https://github.com/simolus3/drift/tree/develop/examples/app).
|
||||
|
||||
## Project setup
|
||||
|
||||
{% include "partials/dependencies" %}
|
||||
|
||||
{% assign snippets = 'package:drift_docs/snippets/tables/filename.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
### Declaring tables
|
||||
|
||||
Using drift, you can model the structure of your tables with simple dart code.
|
||||
Let's write a file (simply called `filename.dart` in this snippet) containing
|
||||
two simple tables and a database class using drift to get started:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = "overview" %}
|
||||
|
||||
__⚠️ Note:__ The column definitions, the table name and the primary key must be known at
|
||||
compile time. For column definitions and the primary key, the function must use the `=>`
|
||||
operator and can't contain anything more than what's included in the documentation and the
|
||||
examples. Otherwise, the generator won't be able to know what's going on.
|
||||
|
||||
## Generating the code
|
||||
|
||||
Drift integrates with Dart's `build` system, so you can generate all the code needed with
|
||||
`dart run build_runner build`. If you want to continuously rebuild the generated code
|
||||
where you change your code, run `dart run build_runner watch` instead.
|
||||
After running either command, drift's generator will have created the following classes for
|
||||
you:
|
||||
|
||||
1. The `_$MyDatabase` class that your database is defined to extend. It provides access to all
|
||||
tables and core drift APIs.
|
||||
2. A data class, `Todo` (for `Todos`) and `Category` (for `Categories`) for each table. It is
|
||||
used to hold the result of selecting rows from the table.
|
||||
3. A class which drift calls a "companion" class (`TodosCompanion` and `CategoriesCompanion`
|
||||
in this example here).
|
||||
These classes are used to write inserts and updates into the table. These classes make drift
|
||||
a great match for Dart's null safety feature: In a data class, columns (including those using
|
||||
auto-incremented integers) can be non-nullable since they're coming from a select.
|
||||
Since you don't know the value before running an insert though, the companion class makes these
|
||||
columns optional.
|
||||
|
||||
With the generated code in place, the database can be opened by passing a connection to the superclass,
|
||||
like this:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = "open" %}
|
||||
|
||||
That's it! You can now use drift by creating an instance of `MyDatabase`.
|
||||
In a simple app from a `main` entrypoint, this may look like the following:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = "usage" %}
|
||||
|
||||
The articles linked below explain how to use the database in actual, complete
|
||||
Flutter apps.
|
||||
A complete example for a Flutter app using drift is also available [here](https://github.com/simolus3/drift/tree/develop/examples/app).
|
||||
|
||||
## Next steps
|
||||
|
||||
Congratulations! You're now ready to use all of drift. See the articles below for further reading.
|
||||
The ["Writing queries"]({{ "writing_queries.md" | pageUrl }}) article contains everything you need
|
||||
to know to write selects, updates and inserts in drift!
|
||||
|
||||
{% block "blocks/alert" title="Using the database" %}
|
||||
The database class from this guide is ready to be used with your app.
|
||||
For Flutter apps, a Drift database class is typically instantiated at the top of your widget tree
|
||||
and then passed down with `provider` or `riverpod`.
|
||||
See [using the database]({{ '../faq.md#using-the-database' | pageUrl }}) for ideas on how to integrate
|
||||
Drift into your app's state management.
|
||||
|
||||
The setup in this guide uses [platform channels](https://flutter.dev/docs/development/platform-integration/platform-channels),
|
||||
which are only available after running `runApp` by default.
|
||||
When using drift before your app is initialized, please call `WidgetsFlutterBinding.ensureInitialized()` before using
|
||||
the database to ensure that platform channels are ready.
|
||||
{% endblock %}
|
||||
|
||||
- The articles on [writing queries]({{ 'writing_queries.md' | pageUrl }}) and [Dart tables]({{ 'advanced_dart_tables.md' | pageUrl }}) introduce important concepts of the Dart API used to write queries.
|
||||
- You can use the same drift database on multiple isolates concurrently - see [Isolates]({{ '../Advanced Features/isolates.md' | pageUrl }}) for more on that.
|
||||
- Drift has excellent support for custom SQL statements, including a static analyzer and code-generation tools. See [Getting started with sql]({{ 'starting_with_sql.md' | pageUrl }})
|
||||
or [Using SQL]({{ '../Using SQL/index.md' | pageUrl }}) for everything there is to know about using drift's SQL-based APIs.
|
||||
- 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.
|
||||
|
||||
[runtime checks]: {{ '../Advanced Features/migrations.md#verifying-a-database-schema-at-runtime' | pageUrl }}
|
||||
[test utilities]: {{ '../Advanced Features/migrations.md#verifying-migrations' | pageUrl }}
|
|
@ -1,107 +0,0 @@
|
|||
---
|
||||
data:
|
||||
title: "Getting started with sql"
|
||||
weight: 5
|
||||
description: Learn how to get started with the SQL version of drift, or how to migrate an existing project to drift.
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
||||
The regular [getting started guide]({{ "index.md" | pageUrl }}) explains how to get started with drift by
|
||||
declaring both tables and queries in Dart. This version will focus on how to use drift with SQL instead.
|
||||
|
||||
A complete cross-platform Flutter app using drift is also available [here](https://github.com/simolus3/drift/tree/develop/examples/app).
|
||||
|
||||
## Adding the dependency
|
||||
|
||||
{% include "partials/dependencies" %}
|
||||
|
||||
## Declaring tables and queries
|
||||
|
||||
To declare tables and queries in sql, create a file called `tables.drift`
|
||||
next to your Dart files (for instance in `lib/database/tables.drift`).
|
||||
|
||||
You can put `CREATE TABLE` statements for your queries in there.
|
||||
The following example creates two tables to model a todo-app. If you're
|
||||
migrating an existing project to drift, you can just copy the `CREATE TABLE`
|
||||
statements you've already written into this file.
|
||||
|
||||
{% assign drift_snippets = 'package:drift_docs/snippets/drift_files/getting_started/tables.drift.excerpt.json' | readString | json_decode %}
|
||||
|
||||
{% include "blocks/snippet" snippets = drift_snippets name = '(full)' %}
|
||||
|
||||
{% block "blocks/alert" title="On that AS Category" %}
|
||||
Drift will generate Dart classes for your tables, and the name of those
|
||||
classes is based on the table name. By default, drift just strips away
|
||||
the trailing `s` from your table. That works for most cases, but in some
|
||||
(like the `categories` table above), it doesn't. We'd like to have a
|
||||
`Category` class (and not `Categorie`) generated, so we tell drift to
|
||||
generate a different name with the `AS <name>` declaration at the end.
|
||||
{% endblock %}
|
||||
|
||||
## Generating matching code
|
||||
|
||||
After you declared the tables, lets generate some Dart code to actually
|
||||
run them. Drift needs to know which tables are used in a database, so we
|
||||
have to write a small Dart class that drift will then read. Lets create
|
||||
a file called `database.dart` next to the `tables.drift` file you wrote
|
||||
in the previous step.
|
||||
|
||||
{% assign dart_snippets = 'package:drift_docs/snippets/drift_files/getting_started/database.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
{% include "blocks/snippet" snippets = dart_snippets name = '(full)' %}
|
||||
|
||||
To generate the `database.g.dart` file which contains the `_$AppDb`
|
||||
superclass, run `dart run build_runner build` on the command
|
||||
line.
|
||||
|
||||
## What drift generates
|
||||
|
||||
Let's take a look at what drift generated during the build:
|
||||
|
||||
- Generated data classes (`Todo` and `Category`) - these hold a single
|
||||
row from the respective table.
|
||||
- Companion versions of these classes. Those are only relevant when
|
||||
using the Dart apis of drift, you can [learn more here]({{ "writing_queries.md#inserts" | pageUrl }}).
|
||||
- A `CountEntriesResult` class, it holds the result rows when running the
|
||||
`countEntries` query.
|
||||
- A `_$AppDb` superclass. It takes care of creating the tables when
|
||||
the database file is first opened. It also contains typesafe methods
|
||||
for the queries declared in the `tables.drift` file:
|
||||
- a `Selectable<Todo> todosInCategory(int)` method, which runs the
|
||||
`todosInCategory` query declared above. Drift has determined that the
|
||||
type of the variable in that query is `int`, because that's the type
|
||||
of the `category` column we're comparing it to.
|
||||
The method returns a `Selectable` to indicate that it can both be
|
||||
used as a regular query (`Selectable.get` returns a `Future<List<Todo>>`)
|
||||
or as an auto-updating stream (by using `.watch` instead of `.get()`).
|
||||
- a `Selectable<CountEntriesResult> countEntries()` method, which runs
|
||||
the other query when used.
|
||||
|
||||
By the way, you can also put insert, update and delete statements in
|
||||
a `.drift` file - drift will generate matching code for them as well.
|
||||
|
||||
## Learning more
|
||||
|
||||
Now that you know how to use drift together with sql, here are some
|
||||
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]({{ "../transactions.md" | pageUrl }})
|
||||
- [Schema migrations]({{ "../Advanced Features/migrations.md" | pageUrl }})
|
||||
- Writing [queries]({{ "writing_queries.md" | pageUrl }}) and
|
||||
[expressions]({{ "../Advanced Features/expressions.md" | pageUrl }}) in Dart
|
||||
- A more [in-depth guide]({{ "../Using SQL/drift_files.md" | pageUrl }})
|
||||
on `drift` files, which explains `import` statements and the Dart-SQL interop.
|
||||
|
||||
{% block "blocks/alert" title="Using the database" %}
|
||||
The database class from this guide is ready to be used with your app.
|
||||
For Flutter apps, a Drift database class is typically instantiated at the top of your widget tree
|
||||
and then passed down with `provider` or `riverpod`.
|
||||
See [using the database]({{ '../faq.md#using-the-database' | pageUrl }}) for ideas on how to integrate
|
||||
Drift into your app's state management.
|
||||
|
||||
The setup in this guide uses [platform channels](https://flutter.dev/docs/development/platform-integration/platform-channels),
|
||||
which are only available after running `runApp` by default.
|
||||
When using drift before your app is initialized, please call `WidgetsFlutterBinding.ensureInitialized()` before using
|
||||
the database to ensure that platform channels are ready.
|
||||
{% endblock %}
|
|
@ -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.
|
|
@ -0,0 +1,131 @@
|
|||
---
|
||||
data:
|
||||
title: Migrations
|
||||
description: Tooling and APIs to safely change the schema of your database.
|
||||
weight: 4
|
||||
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' %}
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
data:
|
||||
title: "Other engines"
|
||||
description: "Use drift on the web or other platforms"
|
||||
weight: 100
|
||||
template: layouts/docs/list
|
||||
---
|
|
@ -2,10 +2,11 @@
|
|||
data:
|
||||
title: Encryption
|
||||
description: Use drift on encrypted databases
|
||||
weight: 10
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
||||
{% assign snippets = 'package:drift_docs/snippets/encryption.dart.excerpt.json' | readString | json_decode %}
|
||||
{% assign snippets = 'package:drift_docs/snippets/platforms/encryption.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
There are two ways to use drift on encrypted databases.
|
||||
The `encrypted_drift` package is similar to `drift_sqflite` and uses a platform plugin written in
|
||||
|
@ -72,13 +73,14 @@ of the regular `libsqlite3.so`:
|
|||
{% include "blocks/snippet" snippets = snippets name = "setup" %}
|
||||
|
||||
When using drift on a background database, you need to call `setupSqlCipher` on the background isolate
|
||||
as well.
|
||||
as well. With `NativeDatabase.createInBackground`, which are using isolates internally, you can use
|
||||
the `setupIsolate` callback to do this - the examples on this page use this as well.
|
||||
|
||||
On iOS and macOS, no additional setup is necessary - simply depend on `sqlcipher_flutter_libs`.
|
||||
|
||||
On Windows and Linux, you currently have to include a version of SQLCipher manually when you distribute
|
||||
your app.
|
||||
For more information on this, you can use the documentation [here]({{ '../platforms.md#bundling-sqlite-with-your-app' | pageUrl }}).
|
||||
For more information on this, you can use the documentation [here]({{ '../Platforms/index.md#bundling-sqlite-with-your-app' | pageUrl }}).
|
||||
Instead of including `sqlite3.dll` or `libsqlite3.so`, you'd include the respective versions
|
||||
of SQLCipher.
|
||||
|
|
@ -2,7 +2,8 @@
|
|||
data:
|
||||
title: "Supported platforms"
|
||||
description: All platforms supported by drift, and how to use them
|
||||
template: layouts/docs/single
|
||||
weight: 8
|
||||
template: layouts/docs/list
|
||||
---
|
||||
|
||||
Being built on top of the sqlite3 database, drift can run on almost every Dart platform.
|
||||
|
@ -14,7 +15,8 @@ To achieve platform independence, drift separates its core apis from a platform-
|
|||
database implementation. The core apis are pure-Dart and run on all Dart platforms, even
|
||||
outside of Flutter. When writing drift apps, prefer to mainly use the apis in
|
||||
`package:drift/drift.dart` as they are guaranteed to work across all platforms.
|
||||
Depending on your platform, you can choose a different `QueryExecutor`.
|
||||
Depending on your platform, you can choose a different `QueryExecutor` - the interface
|
||||
binding the core drift library with native databases.
|
||||
|
||||
## Overview
|
||||
|
||||
|
@ -23,8 +25,8 @@ This table list all supported drift implementations and on which platforms they
|
|||
| Implementation | Supported platforms | Notes |
|
||||
|----------------|---------------------|-------|
|
||||
| `SqfliteQueryExecutor` from `package:drift_sqflite` | Android, iOS | Uses platform channels, Flutter only, no isolate support, doesn't support `flutter test`. Formerly known as `moor_flutter` |
|
||||
| `NativeDatabase` from `package:drift/native.dart` | Android, iOS, Windows, Linux, macOS | No further setup is required for Flutter users. For support outside of Flutter, or in `flutter test`, see the [desktop](#desktop) section below. Usage in a [isolate]({{ 'Advanced Features/isolates.md' | pageUrl }}) is recommended. Formerly known as `package:moor/ffi.dart`. |
|
||||
| `WasmDatabase` from `package:drift/wasm.dart` | Web | Works with or without Flutter. A bit of [additional setup]({{ 'Other engines/web.md' | pageUrl }}) is required. |
|
||||
| `NativeDatabase` from `package:drift/native.dart` | Android, iOS, Windows, Linux, macOS | No further setup is required for Flutter users. For support outside of Flutter, or in `flutter test`, see the [desktop](#desktop) section below. Usage in a [isolate]({{ '../isolates.md' | pageUrl }}) is recommended. Formerly known as `package:moor/ffi.dart`. |
|
||||
| `WasmDatabase` from `package:drift/wasm.dart` | Web | Works with or without Flutter. A bit of [additional setup]({{ 'web.md' | pageUrl }}) is required. |
|
||||
| `WebDatabase` from `package:drift/web.dart` | Web | Deprecated in favor of `WasmDatabase`. |
|
||||
|
||||
To support all platforms in a shared codebase, you only need to change how you open your database, all other usages can stay the same.
|
||||
|
@ -47,7 +49,7 @@ is maintaned and supported too.
|
|||
### using `drift/native`
|
||||
|
||||
The new `package:drift/native.dart` implementation uses `dart:ffi` to bind to sqlite3's native C apis.
|
||||
This is the recommended approach for newer projects as described in the [getting started]({{ "Getting started/index.md" | pageUrl }}) guide.
|
||||
This is the recommended approach for newer projects as described in the [getting started]({{ "../setup.md" | pageUrl }}) guide.
|
||||
|
||||
To ensure that your app ships with the latest sqlite3 version, also add a dependency to the `sqlite3_flutter_libs`
|
||||
package when using `package:drift/native.dart`!
|
||||
|
@ -73,12 +75,12 @@ However, there are some smaller issues on some devices that you should be aware
|
|||
|
||||
## Web
|
||||
|
||||
_Main article: [Web]({{ "Other engines/web.md" | pageUrl }})_
|
||||
_Main article: [Web]({{ "web.md" | pageUrl }})_
|
||||
|
||||
For apps that run on the web, you can use drift's experimental web implementation, located
|
||||
in `package:drift/web.dart`.
|
||||
As it binds to [sql.js](https://github.com/sql-js/sql.js), special setup is required. Please
|
||||
read the main article for details.
|
||||
Drift runs on the web by compiling sqlite3 to a WebAssembly module. This database
|
||||
can be accessed using a `WasmDatabase` in `package:drift/wasm.dart`.
|
||||
For optimal support across different browsers, a worker script and some additional
|
||||
setup is required. The main article explains how to set up drift to work on the web.
|
||||
|
||||
## Desktop
|
||||
|
||||
|
@ -138,11 +140,11 @@ install the dynamic library for `sqlite` next to your application executable.
|
|||
This example shows how to do that on Linux, by using a custom `sqlite3.so` that we assume
|
||||
lives next to your application:
|
||||
|
||||
{% assign snippets = 'package:drift_docs/snippets/platforms.dart.excerpt.json' | readString | json_decode %}
|
||||
{% assign snippets = 'package:drift_docs/snippets/platforms/platforms.dart.excerpt.json' | readString | json_decode %}
|
||||
{% include "blocks/snippet" snippets = snippets %}
|
||||
|
||||
Be sure to use drift _after_ you set the platform-specific overrides.
|
||||
When you use drift in [another isolate]({{ 'Advanced Features/isolates.md' | pageUrl }}),
|
||||
When you use drift in [another isolate]({{ '../isolates.md' | pageUrl }}),
|
||||
you'll also need to apply the opening overrides on that background isolate.
|
||||
You can call them in the isolate's entrypoint before using any drift apis.
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
data:
|
||||
title: Native Drift (Desktop support)
|
||||
description: Run drift on both mobile and desktop
|
||||
weight: 1
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
||||
|
@ -112,7 +113,7 @@ The chosen options help reduce binary size by removing features not used by drif
|
|||
- __SQLITE_DQS=0__: This will make sqlite not accept double-quoted strings (and instead parse them as identifiers). This matches
|
||||
the behavior of drift and compiled queries
|
||||
- __SQLITE_THREADSAFE=0__: Since the majority of Flutter apps only use one isolate, thread safety is turned off. Note that you
|
||||
can still use the [isolate api]({{"../Advanced Features/isolates.md" | pageUrl}}) for background operations. As long as all
|
||||
can still use the [isolate api]({{"../isolates.md" | pageUrl}}) for background operations. As long as all
|
||||
database accesses happen from the same thread, there's no problem.
|
||||
- SQLITE_DEFAULT_MEMSTATUS=0: The `sqlite3_status()` interfaces are not exposed by drift, so there's no point of having them.
|
||||
- SQLITE_MAX_EXPR_DEPTH=0: Disables maximum depth when sqlite parses expressions, which can make the parser faster.
|
||||
|
@ -141,8 +142,8 @@ The `NativeDatabase` includes additional sql functions not available in standard
|
|||
|
||||
Note that `NaN`, `-infinity` or `+infinity` are represented as `NULL` in sql.
|
||||
|
||||
When enabling the `moor_ffi` module in your [build options]({{ "../Advanced Features/builder_options.md#available-extensions" | pageUrl }}),
|
||||
the generator will allow you to use those functions in drift files or compiled queries.
|
||||
When enabling the `moor_ffi` module in your [build options]({{ "../Generation options/index.md#available-extensions" | pageUrl }}),
|
||||
the generator will allow you to use those functions in drift files or compiled queries.
|
||||
|
||||
To use those methods from Dart, you need to import `package:drift/extensions/native.dart`.
|
||||
You can then use the additional functions like this:
|
|
@ -2,6 +2,7 @@
|
|||
data:
|
||||
title: Web
|
||||
description: Drift support in Flutter and Dart web apps.
|
||||
weight: 2
|
||||
template: layouts/docs/single
|
||||
path: web/
|
||||
---
|
||||
|
@ -12,7 +13,7 @@ The `WasmDatabase.open` API is the preferred way to run drift on the web. While
|
|||
APIs continue to work, using the stable API will bring performance and safety benefits.
|
||||
{% endblock %}
|
||||
|
||||
{% assign snippets = "package:drift_docs/snippets/engines/web.dart.excerpt.json" | readString | json_decode %}
|
||||
{% assign snippets = "package:drift_docs/snippets/platforms/web.dart.excerpt.json" | readString | json_decode %}
|
||||
|
||||
Using modern browser APIs such as WebAssembly and the Origin-Private File System API,
|
||||
you can use drift databases for the web version of your Flutter and Dart applications.
|
||||
|
@ -128,7 +129,7 @@ to another (potentially slower) implementation in that case.
|
|||
### Setup in Dart
|
||||
|
||||
From a perspective of the Dart code used, drift on the web is similar to drift on other platforms.
|
||||
You can follow the [getting started guide]({{ '../Getting started/index.md' | pageUrl }}) as a general setup guide.
|
||||
You can follow the [getting started guide]({{ '../setup.md' | pageUrl }}) as a general setup guide.
|
||||
|
||||
Instead of using a `NativeDatabase` in your database classes, you can use the `WasmDatabase` optimized for
|
||||
the web:
|
||||
|
@ -253,7 +254,7 @@ If you want to instead compile these yourself, this section describes how to do
|
|||
The web worker is written in Dart - the entrypoint is stable and part of drift's public API.
|
||||
To compile a worker suitable for `WasmDatabase.open`, create a new Dart file that calls `WasmDatabase.workerMainForOpen`:
|
||||
|
||||
{% assign worker = "package:drift_docs/snippets/engines/stable_worker.dart.excerpt.json" | readString | json_decode %}
|
||||
{% assign worker = "package:drift_docs/snippets/platforms/stable_worker.dart.excerpt.json" | readString | json_decode %}
|
||||
{% include "blocks/snippet" snippets = worker %}
|
||||
|
||||
The JavaScript file included in drift releases is compiled with `dart compile js -O4 web/drift_worker.dart`.
|
||||
|
@ -354,7 +355,7 @@ Drift will automatically migrate data from local storage to `IndexedDb` when it
|
|||
|
||||
#### Using web workers
|
||||
|
||||
You can offload the database to a background thread by using
|
||||
You can offload the database to a background thread by using
|
||||
[Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API).
|
||||
Drift also supports [shared workers](https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker),
|
||||
which allows you to seamlessly synchronize query-streams and updates across multiple tabs!
|
||||
|
@ -369,7 +370,7 @@ A Flutter port of this example is [part of the drift repository](https://github.
|
|||
To write a web worker that will serve requests for drift, create a file called `worker.dart` in
|
||||
the `web/` folder of your app. It could have the following content:
|
||||
|
||||
{% assign workers = 'package:drift_docs/snippets/engines/workers.dart.excerpt.json' | readString | json_decode %}
|
||||
{% assign workers = 'package:drift_docs/snippets/platforms/workers.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
{% include "blocks/snippet" snippets = workers name = "worker" %}
|
||||
|
||||
|
@ -382,7 +383,7 @@ you can connect like this:
|
|||
|
||||
You can then open a drift database with that connection.
|
||||
For more information on the `DatabaseConnection` class, see the documentation on
|
||||
[isolates]({{ "../Advanced Features/isolates.md" | pageUrl }}).
|
||||
[isolates]({{ "../isolates.md" | pageUrl }}).
|
||||
|
||||
A small, but working example is available under [examples/web_worker_example](https://github.com/simolus3/drift/tree/develop/examples/web_worker_example)
|
||||
in the drift repository.
|
|
@ -63,7 +63,7 @@ name you specified.
|
|||
{% endblock %}
|
||||
|
||||
You can also use `UPDATE` or `DELETE` statements here. Of course, this feature is also available for
|
||||
[daos]({{ "../Advanced Features/daos.md" | pageUrl }}),
|
||||
[daos]({{ "../Dart API/daos.md" | pageUrl }}),
|
||||
and it perfectly integrates with auto-updating streams by analyzing what tables you're reading from or
|
||||
writing to.
|
||||
|
||||
|
@ -72,7 +72,7 @@ If you don't want to use the statements with an generated api, you can
|
|||
still send custom queries by calling `customSelect` for a one-time query or
|
||||
`customSelectStream` for a query stream that automatically emits a new set of items when
|
||||
the underlying data changes. Using the todo example introduced in the
|
||||
[getting started guide]({{ "../Getting started/index.md" | pageUrl }}), we can
|
||||
[getting started guide]({{ "../setup.md" | pageUrl }}), we can
|
||||
write this query which will load the amount of todo entries in each category:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = "manual" %}
|
|
@ -81,7 +81,7 @@ named parameters. To do so, add a `REQUIRED` keyword:
|
|||
{% include "blocks/snippet" snippets = small name = "q3" %}
|
||||
|
||||
Note that this only has an effect when the `named_parameters`
|
||||
[build option]({{ '../Advanced Features/builder_options.md' | pageUrl }}) is
|
||||
[build option]({{ '../Generation options/index.md' | pageUrl }}) is
|
||||
enabled. Further, non-nullable variables are required by default.
|
||||
|
||||
### Arrays
|
||||
|
@ -138,12 +138,12 @@ CREATE TABLE tasks (
|
|||
);
|
||||
```
|
||||
|
||||
More information on storing enums is available [in the page on type converters]({{ '../Advanced Features/type_converters.md#using-converters-in-moor' | pageUrl }}).
|
||||
More information on storing enums is available [in the page on type converters]({{ '../type_converters.md#using-converters-in-moor' | pageUrl }}).
|
||||
Instead of using an integer mapping enums by their index, you can also store them
|
||||
by their name. For this, use `ENUMNAME(...)` instead of `ENUM(...)`.
|
||||
|
||||
For details on all supported types, and information on how to switch between the
|
||||
datetime modes, see [this section]({{ '../Getting started/advanced_dart_tables.md#supported-column-types' | pageUrl }}).
|
||||
datetime modes, see [this section]({{ '../Dart API/tables.md#supported-column-types' | pageUrl }}).
|
||||
|
||||
The additional drift-specific types (`BOOLEAN`, `DATETIME`, `ENUM` and `ENUMNAME`) are also supported in `CAST`
|
||||
expressions, which is helpful for views:
|
||||
|
@ -303,7 +303,7 @@ can be used to construct dynamic filters at runtime:
|
|||
This lets you write a single SQL query and dynamically apply a predicate at runtime!
|
||||
This feature works for
|
||||
|
||||
- [expressions]({{ "../Advanced Features/expressions.md" | pageUrl }}), as you've seen in the example above
|
||||
- [expressions]({{ "../Dart API/expressions.md" | pageUrl }}), as you've seen in the example above
|
||||
- single ordering terms: `SELECT * FROM todos ORDER BY $term, id ASC`
|
||||
will generate a method taking an `OrderingTerm`.
|
||||
- whole order-by clauses: `SELECT * FROM todos ORDER BY $order`
|
||||
|
@ -320,7 +320,7 @@ default SQL value (here, `TRUE`) when not explicitly set.
|
|||
|
||||
### Type converters
|
||||
|
||||
You can import and use [type converters]({{ "../Advanced Features/type_converters.md" | pageUrl }})
|
||||
You can import and use [type converters]({{ "../type_converters.md" | pageUrl }})
|
||||
written in Dart in a drift file. Importing a Dart file works with a regular `import` statement.
|
||||
To apply a type converter on a column definition, you can use the `MAPPED BY` column constraints:
|
||||
|
||||
|
@ -346,9 +346,9 @@ FROM users;
|
|||
```
|
||||
|
||||
More details on type converts in drift files are available
|
||||
[here]({{ "../Advanced Features/type_converters.md#using-converters-in-moor" | pageUrl }}).
|
||||
[here]({{ "../type_converters.md#using-converters-in-moor" | pageUrl }}).
|
||||
|
||||
When using type converters, we recommend the [`apply_converters_on_variables`]({{ "../Advanced Features/builder_options.md" | pageUrl }})
|
||||
When using type converters, we recommend the [`apply_converters_on_variables`]({{ "../Generation options/index.md" | pageUrl }})
|
||||
build option. This will also apply the converter from Dart to SQL, for instance if used on variables: `SELECT * FROM users WHERE preferences = ?`.
|
||||
With that option, the variable will be inferred to `Preferences` instead of `String`.
|
||||
|
||||
|
@ -380,7 +380,7 @@ CREATE TABLE users (
|
|||
|
||||
When using custom row classes defined in another Dart file, you also need to import that file into the file where you define
|
||||
the database.
|
||||
For more general information on this feature, please check [this page]({{ '../Advanced Features/custom_row_classes.md' | pageUrl }}).
|
||||
For more general information on this feature, please check [this page]({{ '../custom_row_classes.md' | pageUrl }}).
|
||||
|
||||
Custom row classes can be applied to `SELECT` queries defined a `.drift` file. To use a custom row class, the `WITH` syntax
|
||||
can be added after the name of the query.
|
||||
|
@ -421,7 +421,7 @@ Internally, drift will then generate query code to map the row to an instance of
|
|||
`UserWithFriends` class.
|
||||
|
||||
For a more complete overview of using custom row classes for queries, see
|
||||
[the section for queries]({{ '../Advanced Features/custom_row_classes.md#queries' | pageUrl }}).
|
||||
[the section for queries]({{ '../custom_row_classes.md#queries' | pageUrl }}).
|
||||
|
||||
### Dart documentation comments
|
||||
|
|
@ -2,18 +2,23 @@
|
|||
data:
|
||||
title: "Supported sqlite extensions"
|
||||
weight: 10
|
||||
description: Information on json1 and fts5 support in the generator
|
||||
description: Information on json1 and fts5 support in drift files
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
||||
_Note_: Since `drift_sqflite` and `moor_flutter` uses the sqlite version shipped on the device, these extensions might not
|
||||
be available on all devices. When using these extensions, using a `NativeDatabase` is strongly recommended.
|
||||
This enables the extensions listed here on all Android and iOS devices.
|
||||
When analyzing `.drift` files, the generator can consider sqlite3 extensions
|
||||
that might be present.
|
||||
The generator can't know about the sqlite3 library your database is talking to
|
||||
though, so it makes a pessimistic assumption of using an old sqlite3 version
|
||||
without any enabled extensions by default.
|
||||
When using a package like `sqlite3_flutter_libs`, you get the latest sqlite3
|
||||
version with the json1 and fts5 extensions enabled. You can inform the generator
|
||||
about this by using [build options]({{ "../Generation options/index.md" | pageUrl }}).
|
||||
|
||||
## json1
|
||||
|
||||
To enable the json1 extension in drift files and compiled queries, modify your
|
||||
[build options]({{ "../Advanced Features/builder_options.md" | pageUrl }}) to include
|
||||
[build options]({{ "../Generation options/index.md" | pageUrl }}) to include
|
||||
`json1` in the `sqlite_module` section.
|
||||
|
||||
The sqlite extension doesn't require any special tables and works on all text columns. In drift
|
||||
|
@ -42,7 +47,7 @@ class Database extends _$Database {
|
|||
return phoneNumber.equals(number);
|
||||
})
|
||||
).get();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -51,8 +56,8 @@ You can learn more about the json1 extension on [sqlite.org](https://www.sqlite.
|
|||
## fts5
|
||||
|
||||
The fts5 extension provides full-text search capabilities in sqlite tables.
|
||||
To enable the fts5 extension in drift files and compiled queries, modify the
|
||||
[build options]({{ "../Advanced Features/builder_options.md" | pageUrl }}) to include
|
||||
To enable the fts5 extension in drift files and compiled queries, modify the
|
||||
[build options]({{ "../Generation options/index.md" | pageUrl }}) to include
|
||||
`fts5` in the `sqlite_module` section.
|
||||
|
||||
Just like you'd expect when using sqlite, you can create a fts5 table in a drift file
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
data:
|
||||
title: Verified SQL
|
||||
description: Define your database and queries in SQL without giving up on type-safety.
|
||||
weight: 3
|
||||
template: layouts/docs/list
|
||||
---
|
||||
|
||||
Drift provides a [Dart API]({{ '../Dart API/index.md' | pageUrl }}) to define tables and
|
||||
to write SQL queries.
|
||||
Especially when you are already familiar with SQL, it might be easier to define your
|
||||
tables directly in SQL, with `CREATE TABLE` statements.
|
||||
Thanks to a powerful SQL parser and analyzer built into drift, you can still run type-safe
|
||||
SQL queries with support for auto-updating streams and all the other drift features.
|
||||
The validity of your SQL is checked at build time, with drift generating matching methods
|
||||
for each table and SQL statement.
|
||||
|
||||
## Setup
|
||||
|
||||
The basic setup of adding the drift dependencies matches the setup for the Dart APIs. It
|
||||
is described in the [setup page]({{ '../setup.md' | pageUrl }}).
|
||||
|
||||
What's different is how tables and queries are declared. For SQL to be recognized by drift,
|
||||
it needs to be put into a `.drift` file. In this example, we use a `.drift` file next to the
|
||||
database class named `tables.drift`:
|
||||
|
||||
{% assign drift_snippets = 'package:drift_docs/snippets/drift_files/getting_started/tables.drift.excerpt.json' | readString | json_decode %}
|
||||
|
||||
{% include "blocks/snippet" snippets = drift_snippets name = '(full)' %}
|
||||
|
||||
{% block "blocks/alert" title="On that AS Category" %}
|
||||
Drift will generate Dart classes for your tables, and the name of those
|
||||
classes is based on the table name. By default, drift just strips away
|
||||
the trailing `s` from your table. That works for most cases, but in some
|
||||
(like the `categories` table above), it doesn't. We'd like to have a
|
||||
`Category` class (and not `Categorie`) generated, so we tell drift to
|
||||
generate a different name with the `AS <name>` declaration at the end.
|
||||
{% endblock %}
|
||||
|
||||
Integrating drift files into the database simple, they just need to be added to the
|
||||
`include` parameter of the `@DriftDatabase` annotation. The `tables` parameter can
|
||||
be omitted here, since there are no Dart-defined tables to be added to the database.
|
||||
|
||||
{% assign dart_snippets = 'package:drift_docs/snippets/drift_files/getting_started/database.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
{% include "blocks/snippet" snippets = dart_snippets name = '(full)' %}
|
||||
|
||||
To generate the `database.g.dart` file which contains the `_$AppDb`
|
||||
superclass, run `dart run build_runner build` on the command
|
||||
line.
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
---
|
||||
data:
|
||||
title: "Using SQL"
|
||||
weight: 30
|
||||
description: Write typesafe sql with drift
|
||||
template: layouts/docs/list
|
||||
---
|
||||
|
||||
Drift lets you express a variety of queries in pure Dart. However, you don't have to miss out
|
||||
on its features when you need more complex queries or simply prefer sql. Drift has a builtin
|
||||
sql parser and analyzer, so it can generate a typesafe API for sql statements you write.
|
||||
It can also warn about errors in your sql at build time.
|
|
@ -1,6 +1,7 @@
|
|||
---
|
||||
data:
|
||||
title: "Community"
|
||||
weight: 50
|
||||
description: Packages contributed by the community
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
---
|
||||
data:
|
||||
title: "Custom row classes"
|
||||
weight: 6
|
||||
description: >-
|
||||
Use your own classes as data classes for drift tables
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
||||
|
||||
For each table declared in Dart or in a drift file, `drift_dev` generates a row class (sometimes also referred to as _data class_)
|
||||
to hold a full row and a companion class for updates and inserts.
|
||||
This works well for most cases: Drift knows what columns your table has, and it can generate a simple class for all of that.
|
||||
|
@ -167,10 +167,10 @@ For your convenience, drift is using different generation strategies even for qu
|
|||
an existing row class. It is helpful to enumerate them because they affect the allowed type for
|
||||
fields in existing types as well.
|
||||
|
||||
1. Nested tables: When the [`SELECT table.**` syntax]({{ '../Using SQL/drift_files.md#nested-results' | pageUrl }})
|
||||
1. Nested tables: When the [`SELECT table.**` syntax]({{ 'SQL API/drift_files.md#nested-results' | pageUrl }})
|
||||
is used in a query, drift will pack columns from `table` into a nested object instead of generating fields
|
||||
for every column.
|
||||
2. Nested list results: The [`LIST()` macro]({{ '../Using SQL/drift_files.md#list-subqueries' | pageUrl }})
|
||||
2. Nested list results: The [`LIST()` macro]({{ 'SQL API/drift_files.md#list-subqueries' | pageUrl }})
|
||||
can be used to expose results of a subquery as a list.
|
||||
3. Single-table results: When a select statement reads all columns from a table (and no additional columns),
|
||||
like in `SELECT * FROM table`, drift will use the data class of the table instead of generating a new one.
|
|
@ -1,13 +1,13 @@
|
|||
---
|
||||
data:
|
||||
title: "Frequently asked questions"
|
||||
|
||||
weight: 25
|
||||
path: faq/
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
||||
## Using the database
|
||||
If you've created a `MyDatabase` class by following the [getting started guide]({{ "Getting started/index.md" | pageUrl }}), you
|
||||
If you've created a `MyDatabase` class by following the [getting started guide]({{ "setup.md" | pageUrl }}), you
|
||||
still need to somehow obtain an instance of it. It's recommended to only have one (singleton) instance of your database,
|
||||
so you could store that instance in a global variable:
|
||||
|
||||
|
@ -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
|
||||
|
@ -142,13 +142,13 @@ result of your queries.
|
|||
### floor
|
||||
Floor also has a lot of convenience features like auto-updating queries and schema migrations. Similar to drift, you
|
||||
define the structure of your database in Dart. Then, you have write queries in sql - the mapping code if generated
|
||||
by floor. Drift has a [similar feature]({{ "Using SQL/custom_queries.md" | pageUrl }}), but it can also verify that your queries are valid at compile time. Drift
|
||||
by floor. Drift has a [similar feature]({{ "SQL API/custom_queries.md" | pageUrl }}), but it can also verify that your queries are valid at compile time. Drift
|
||||
additionally has an api that lets you write some queries in Dart instead of sql.
|
||||
|
||||
A difference between these two is that Floor lets you write your own classes and generates mapping code around that.
|
||||
By default, drift generates most classes for you, which can make it easier to use, but makes the api less flexible in some
|
||||
instances.
|
||||
Drift can also be used with [custom row classes]({{ 'Advanced Features/custom_row_classes.md' | pageUrl }}) though.
|
||||
Drift can also be used with [custom row classes]({{ 'custom_row_classes.md' | pageUrl }}) though.
|
||||
|
||||
### firebase
|
||||
Both the Realtime Database and Cloud Datastore are easy to use persistence libraries that can sync across devices while
|
||||
|
|
|
@ -11,7 +11,7 @@ Drift is a reactive persistence library for Dart and Flutter applications. It's
|
|||
of database libraries like [the sqlite3 package](https://pub.dev/packages/sqlite3), [sqflite](https://pub.dev/packages/sqflite) or [sql.js](https://github.com/sql-js/sql.js/)
|
||||
and provides additional features, like:
|
||||
|
||||
- __Type safety__: Instead of writing sql queries manually and parsing the `List<Map<String, dynamic>>` that they
|
||||
- __Type safety__: Instead of writing sql queries manually and parsing the `List<Map<String, dynamic>>` that they
|
||||
return, drift turns rows into objects of your choice.
|
||||
- __Stream queries__: Drift lets you "watch" your queries with zero additional effort. Any query can be turned into
|
||||
an auto-updating stream that emits new items when the underlying data changes.
|
||||
|
@ -26,4 +26,11 @@ return, drift turns rows into objects of your choice.
|
|||
And much more! Drift validates data before inserting it, so you can get helpful error messages instead of just an
|
||||
sql error code. Of course, it supports transactions. And DAOs. And efficient batched insert statements. The list goes on.
|
||||
|
||||
Check out these in-depth articles to learn about drift and how to use its features.
|
||||
## Getting started
|
||||
|
||||
To get started with drift, follow the [setup guide]({{ 'setup.md' | pageUrl }}).
|
||||
It explains everything from setting up the dependencies to writing database classes
|
||||
and generating code.
|
||||
|
||||
It also links a few pages intended for developers getting started with drift, so
|
||||
that you can explore the areas you're most interested in first.
|
||||
|
|
|
@ -2,15 +2,18 @@
|
|||
data:
|
||||
title: Isolates
|
||||
description: Accessing drift databases on multiple isolates.
|
||||
weight: 10
|
||||
template: layouts/docs/single
|
||||
path: docs/advanced-features/isolates/
|
||||
---
|
||||
|
||||
{% assign snippets = 'package:drift_docs/snippets/isolates.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
As sqlite3 is a synchronous C library, accessing the database from the main isolate
|
||||
can cause blocking IO operations that lead to reduced responsiveness of your
|
||||
application.
|
||||
To resolve this problem, drift can spawn a long-running isolate to run SQL statements.
|
||||
When following the recommended [getting started guide]({{ '../Getting started/index.md' | pageUrl }})
|
||||
When following the recommended [getting started guide]({{ 'setup.md' | pageUrl }})
|
||||
and using `NativeDatabase.createInBackground`, you automatically benefit from an isolate
|
||||
drift manages for you without needing additional setup.
|
||||
This page describes when advanced isolate setups are necessary, and how to approach them.
|
|
@ -0,0 +1,122 @@
|
|||
---
|
||||
data:
|
||||
title: "Setup"
|
||||
description: All you need to know about adding drift to your project.
|
||||
weight: 1
|
||||
template: layouts/docs/single
|
||||
path: /docs/getting-started/
|
||||
aliases:
|
||||
- /getting-started/ # Used to have this url as well
|
||||
---
|
||||
|
||||
{% assign snippets = 'package:drift_docs/snippets/setup/database.dart.excerpt.json' | readString | json_decode %}
|
||||
|
||||
Drift is a powerful database library for Dart and Flutter applications. To
|
||||
support its advanced capabilities like type-safe SQL queries, verification of
|
||||
your database and migrations, it uses a builder and command-line tooling that
|
||||
runs at compile-time.
|
||||
|
||||
This means that the setup involves a little more than just adding a single
|
||||
dependency to your pubspec. This page explains how to add drift to your project
|
||||
and gives pointers to the next steps.
|
||||
If you're stuck adding drift, or have questions or feedback about the project,
|
||||
please share that with the community by [starting a discussion on GitHub](https://github.com/simolus3/drift/discussions).
|
||||
If you want to look at an example app for inspiration, a cross-platform Flutter app using drift is available
|
||||
[as part of the drift repository](https://github.com/simolus3/drift/tree/develop/examples/app).
|
||||
|
||||
## The dependencies {#adding-dependencies}
|
||||
|
||||
First, lets add drift to your project's `pubspec.yaml`.
|
||||
At the moment, the current version of `drift` is [![Drift version](https://img.shields.io/pub/v/drift.svg)](https://pub.dev/packages/drift)
|
||||
and the latest version of `drift_dev` is [![Generator version](https://img.shields.io/pub/v/drift_dev.svg)](https://pub.dev/packages/drift_dev).
|
||||
In addition to the core drift dependencies, we're also adding packages to find a suitable database
|
||||
location on the device and to include a recent version of `sqlite3`, the database most commonly
|
||||
used with drift.
|
||||
|
||||
{% assign versions = 'package:drift_docs/versions.json' | readString | json_decode %}
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
drift: ^{{ versions.drift }}
|
||||
sqlite3_flutter_libs: ^0.5.0
|
||||
path_provider: ^2.0.0
|
||||
path: ^{{ versions.path }}
|
||||
|
||||
dev_dependencies:
|
||||
drift_dev: ^{{ versions.drift_dev }}
|
||||
build_runner: ^{{ versions.build_runner }}
|
||||
```
|
||||
|
||||
If you're wondering why so many packages are necessary, here's a quick overview over what each package does:
|
||||
|
||||
- `drift`: This is the core package defining the APIs you use to access drift databases.
|
||||
- `sqlite3_flutter_libs`: Ships the latest `sqlite3` version with your Android or iOS app. This is not required when you're _not_ using Flutter,
|
||||
but then you need to take care of including `sqlite3` yourself.
|
||||
For an overview on other platforms, see [platforms]({{ 'Platforms/index.md' | pageUrl }}).
|
||||
Note that the `sqlite3_flutter_libs` package will include the native sqlite3 library for the following
|
||||
architectures: `armv8`, `armv7`, `x86` and `x86_64`.
|
||||
Most Flutter apps don't run on 32-bit x86 devices without further setup, so you should
|
||||
[add a snippet](https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3_flutter_libs#included-platforms)
|
||||
to your `build.gradle` if you don't need `x86` builds.
|
||||
Otherwise, the Play Store might allow users on `x86` devices to install your app even though it is not
|
||||
supported.
|
||||
In Flutter's current native build system, drift unfortunately can't do that for you.
|
||||
- `path_provider` and `path`: Used to find a suitable location to store the database. Maintained by the Flutter and Dart team.
|
||||
- `drift_dev`: This development-only dependency generates query code based on your tables. It will not be included in your final app.
|
||||
- `build_runner`: Common tool for code-generation, maintained by the Dart team.
|
||||
|
||||
## Database class
|
||||
|
||||
Every project using drift needs at least one class to access a database. This class references all the
|
||||
tables you want to use and is the central entrypoint for drift's code generator.
|
||||
In this example, we'll assume that this database class is defined in a file called `database.dart` and
|
||||
somewhere under `lib/`. Of course, you can put this class in any Dart file you like.
|
||||
|
||||
To make the database useful, we'll also add a simple table to it. This table, `TodoItems`, can be used
|
||||
to store todo items for a todo list app.
|
||||
Everything there is to know about defining tables in Dart is described on the [Dart tables]({{'Dart API/tables.md' | pageUrl}}) page.
|
||||
If you prefer using SQL to define your tables, drift supports that too! You can read all about the [SQL API]({{ 'SQL API/index.md' | pageUrl }}) here.
|
||||
|
||||
For now, the contents of `database.dart` are:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'before_generation' %}
|
||||
|
||||
You will get an analyzer warning on the `part` statement and on `extends _$AppDatabase`. This is
|
||||
expected because drift's generator did not run yet.
|
||||
You can do that by invoking [build_runner](https://pub.dev/packages/build_runner):
|
||||
|
||||
- `dart run build_runner build` generates all the required code once.
|
||||
- `dart run build_runner watch` watches for changes in your sources and generates code with
|
||||
incremental rebuilds. This is suitable for development sessions.
|
||||
|
||||
After running either command, the `database.g.dart` file containing the generated `_$AppDatabase`
|
||||
class will have been generated.
|
||||
You will now see errors related to missing overrides and a missing constructor. The constructor
|
||||
is responsible for telling drift how to open the database. The `schemaVersion` getter is relevant
|
||||
for migrations after changing the database, we can leave it at `1` for now. The database class
|
||||
now looks like this:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'open' %}
|
||||
|
||||
## Next steps
|
||||
|
||||
Congratulations! With this setup complete, your project is ready to use drift.
|
||||
This short snippet shows how the database can be opened and how to run inserts and selects:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'use' %}
|
||||
|
||||
But drift can do so much more! These pages provide more information useful when getting
|
||||
started with drift:
|
||||
|
||||
- [Dart tables]({{ 'Dart API/tables.md' | pageUrl }}): This page describes how to write your own
|
||||
Dart tables and which classes drift generates for them.
|
||||
- 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 }}).
|
||||
- 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.
|
|
@ -2,6 +2,7 @@
|
|||
data:
|
||||
title: "Testing"
|
||||
description: Guide on writing unit tests for drift databases
|
||||
weight: 10
|
||||
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
@ -116,4 +117,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 }}).
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
---
|
||||
data:
|
||||
title: "Type converters"
|
||||
weight: 5
|
||||
description: Store more complex data in columns with type converters
|
||||
aliases:
|
||||
- /type_converters
|
||||
path: /docs/advanced_features/type_converters/
|
||||
template: layouts/docs/single
|
||||
---
|
||||
|
||||
|
@ -131,7 +133,7 @@ CREATE TABLE users (
|
|||
);
|
||||
```
|
||||
|
||||
When using type converters in drift files, we recommend the [`apply_converters_on_variables`]({{ "builder_options.md" | pageUrl }})
|
||||
When using type converters in drift files, we recommend the [`apply_converters_on_variables`]({{ "Generation options/index.md" | pageUrl }})
|
||||
build option. This will also apply the converter from Dart to SQL, for instance if used on variables: `SELECT * FROM users WHERE preferences = ?`.
|
||||
With that option, the variable will be inferred to `Preferences` instead of `String`.
|
||||
|
|
@ -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.
|
||||
|
@ -191,7 +191,7 @@ If you opt for a rename, also update your imports and `include:` parameters in d
|
|||
|
||||
#### Build configuration
|
||||
|
||||
When configuring moor builders for [options]({{ 'Advanced Features/builder_options.md' | pageUrl }}), you have to update your `build.yaml` files to reflect the new builder keys:
|
||||
When configuring moor builders for [options]({{ 'Generation options/index.md' | pageUrl }}), you have to update your `build.yaml` files to reflect the new builder keys:
|
||||
|
||||
| Moor builder key | Drift builder key |
|
||||
| ------------------------------------------- | ------------------------------ |
|
||||
|
|
|
@ -14,7 +14,7 @@ data:
|
|||
Write type-safe queries in Dart or SQL, enjoy auto-updating streams, easily managed transactions
|
||||
and so much more to make persistence fun.
|
||||
</p>
|
||||
<a class="btn btn-lg btn-secondary mr-3 mb-4" href="{{ 'docs/Getting started/index.md' | pageUrl }}">
|
||||
<a class="btn btn-lg btn-secondary mr-3 mb-4" href="{{ 'docs/setup.md' | pageUrl }}">
|
||||
Get started <i class="fas fa-code ml-2 "></i>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -28,7 +28,7 @@ With drift, you can declare your database tables and queries in pure Dart withou
|
|||
advanced SQL features. Drift will take care of creating the tables and generate code
|
||||
that allows you run fluent queries on your data.
|
||||
|
||||
[Get started now]({{ "docs/Getting started/index.md" | pageUrl }})
|
||||
[Get started now]({{ "docs/setup.md" | pageUrl }})
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -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 %}
|
||||
|
||||
|
@ -62,7 +62,7 @@ validated and analyzed during build-time, so drift can provide hints about poten
|
|||
code.
|
||||
Of course, you can mix SQL and Dart to your liking.
|
||||
|
||||
[Using SQL with Drift]({{ 'docs/Using SQL/index.md' | pageUrl }})
|
||||
[Using SQL with Drift]({{ 'docs/SQL API/index.md' | pageUrl }})
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -73,14 +73,14 @@ Drift has primary first-class support for Android, iOS, macOS, Linux Windows and
|
|||
|
||||
Other database libraries can easily be integrated into drift as well.
|
||||
|
||||
[All platforms]({{ "docs/platforms.md" | pageUrl }})
|
||||
[All platforms]({{ "docs/Platforms/index.md" | pageUrl }})
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
{% block "blocks/feature.html" icon="fas fa-star" title="And much more!" %}
|
||||
{% block "blocks/markdown.html" %}
|
||||
Drift provides auto-updating streams for all your queries, makes dealing with transactions and migrations easy
|
||||
and lets your write modular database code with DAOs. We even have a [sql IDE]({{ 'docs/Using SQL/sql_ide.md' | pageUrl }}) builtin to the project.
|
||||
and lets your write modular database code with DAOs.
|
||||
When using drift, working with databases in Dart is fun!
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -12,7 +12,7 @@ path: v2
|
|||
<a class="btn btn-lg btn-primary mr-3 mb-4" href="{{ "docs/index.md" | pageUrl }}">
|
||||
Get started <i class="fas fa-arrow-alt-circle-right ml-2"></i>
|
||||
</a>
|
||||
<a class="btn btn-lg btn-secondary mr-3 mb-4" href="{{ "docs/Getting started/starting_with_sql.md" | pageUrl }}">
|
||||
<a class="btn btn-lg btn-secondary mr-3 mb-4" href="{{ "docs/SQL API/index.md" | pageUrl }}">
|
||||
Migrate an existing project <i class="fas fa-code ml-2 "></i>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -25,7 +25,7 @@ path: v2
|
|||
The rewritten compiler is faster than ever, supports more SQL features and gives you
|
||||
more flexibility when writing database code.
|
||||
|
||||
[Check the updated documentation]({{ "docs/Using SQL/drift_files.md" | pageUrl }})
|
||||
[Check the updated documentation]({{ "docs/SQL API/drift_files.md" | pageUrl }})
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -35,7 +35,7 @@ The new `.moor` files have been updated and can now hold both `CREATE TABLE` sta
|
|||
and queries you define. Moor will then generate typesafe Dart APIs based on your tables
|
||||
and statements.
|
||||
|
||||
[Get started with SQL and moor]({{ "docs/Getting started/starting_with_sql.md" | pageUrl }})
|
||||
[Get started with SQL and moor]({{ "docs/SQL API/index.md" | pageUrl }})
|
||||
{% endblock %} {% endblock %}
|
||||
{% block "blocks/feature" icon="fas fa-plus" title="Analyzer improvements" %} {% block "blocks/markdown" %}
|
||||
We now support more advanced features like compound select statements and window functions,
|
||||
|
@ -59,7 +59,7 @@ Moor 2.0 expands the previous sql parser and analyzer, providing real-time feedb
|
|||
SQL queries as you type. Moor plugs right into the Dart analysis server, so you don't have
|
||||
to install any additional extensions.
|
||||
|
||||
[Learn more about the IDE]({{ "docs/Using SQL/sql_ide.md" | pageUrl }})
|
||||
[Learn more about the IDE]({{ "docs/SQL API/sql_ide.md" | pageUrl }})
|
||||
{% endblock %} {% endblock %}
|
||||
|
||||
{% block "blocks/section" color="dark" %}
|
||||
|
@ -109,8 +109,8 @@ _Please not that the package is still in preview_
|
|||
{% block "blocks/section" color="dark" type="section" %} {% block "blocks/markdown" %}
|
||||
## Try moor now
|
||||
|
||||
- To get started with moor, follow our [getting started guide]({{ "docs/Getting started/index.md" | pageUrl }}) here.
|
||||
- To get started with moor, follow our [getting started guide]({{ "docs/setup.md" | pageUrl }}) here.
|
||||
- To get started with SQL in moor, or to migrate an existing project to moor, follow our
|
||||
[migration guide]({{ "docs/Getting started/starting_with_sql.md" | pageUrl }})
|
||||
[migration guide]({{ "docs/SQL API/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
|
||||
|
||||
|
|
|
@ -23,7 +23,6 @@ If you're wondering why so many packages are necessary, here's a quick overview
|
|||
- `drift`: This is the core package defining the APIs you use to access drift databases.
|
||||
- `sqlite3_flutter_libs`: Ships the latest `sqlite3` version with your Android or iOS app. This is not required when you're _not_ using Flutter,
|
||||
but then you need to take care of including `sqlite3` yourself.
|
||||
For an overview on other platforms, see [platforms]({{ '../platforms.md' | pageUrl }}).
|
||||
Note that the `sqlite3_flutter_libs` package will include the native sqlite3 library for the following
|
||||
architectures: `armv8`, `armv7`, `x86` and `x86_64`.
|
||||
Most Flutter apps don't run on 32-bit x86 devices without further setup, so you should
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:drift_docs/snippets/migrations/datetime_conversion.dart';
|
||||
import 'package:drift_docs/snippets/dart_api/datetime_conversion.dart';
|
||||
import 'package:drift_docs/snippets/modular/schema_inspection.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ abstract class Table extends HasResultSet {
|
|||
/// ```dart
|
||||
/// class IngredientInRecipes extends Table {
|
||||
/// @override
|
||||
/// Set<Column> get uniqueKeys =>
|
||||
/// List<Set<Column>> get uniqueKeys =>
|
||||
/// [{recipe, ingredient}, {recipe, amountInGrams}];
|
||||
///
|
||||
/// IntColumn get recipe => integer()();
|
||||
|
|
Loading…
Reference in New Issue