Merge branch 'docs-restructure' into develop

This commit is contained in:
Simon Binder 2023-09-23 22:56:27 +02:00
commit 8c42ce5049
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
80 changed files with 3270 additions and 1720 deletions

View File

@ -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
```

View File

@ -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:

View File

@ -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');
}

View File

@ -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();
}
}

View File

@ -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

View File

@ -0,0 +1,5 @@
import 'package:drift/drift.dart';
class EnabledCategories extends Table {
IntColumn get parentCategory => integer()();
}

View File

@ -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
}

View File

@ -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

View File

@ -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');
});

View File

@ -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 {

View File

@ -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

View File

@ -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,
});
}

View File

@ -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
);
}
}

View File

@ -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
);
}
}

View File

@ -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});
}
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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';");

View File

@ -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

View File

@ -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

View File

@ -1,7 +0,0 @@
---
data:
title: Advanced Features
weight: 20
description: Learn about some advanced features of drift
template: layouts/docs/list
---

View File

@ -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.

View File

@ -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 }}).

View File

@ -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.

View File

@ -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.

View File

@ -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 }}).

View File

@ -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' %}

View File

@ -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).

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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()`.

View File

@ -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()`.

View File

@ -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 }}

View File

@ -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 %}

View File

@ -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')
},
)
)
```

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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' %}

View File

@ -1,7 +0,0 @@
---
data:
title: "Other engines"
description: "Use drift on the web or other platforms"
weight: 100
template: layouts/docs/list
---

View File

@ -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.

View File

@ -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.

View File

@ -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:

View File

@ -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.

View File

@ -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" %}

View File

@ -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

View File

@ -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

View 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.

View File

@ -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.

View File

@ -1,6 +1,7 @@
---
data:
title: "Community"
weight: 50
description: Packages contributed by the community
template: layouts/docs/single
---

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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.

122
docs/pages/docs/setup.md Normal file
View File

@ -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.

View File

@ -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 }}).

View File

@ -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`.

View File

@ -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 |
| ------------------------------------------- | ------------------------------ |

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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

View File

@ -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

View File

@ -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';

View File

@ -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()();