Port docs on joins and basic queries

This commit is contained in:
Simon Binder 2023-09-16 19:42:53 +02:00
parent 10725d98fb
commit 999c17e19a
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
24 changed files with 1393 additions and 1006 deletions

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

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

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

@ -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,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,38 +0,0 @@
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).
}
// #enddocregion unique
// #docregion view
abstract class CategoryTodoCount extends View {
// Getters define the tables that this view is reading from.
Todos get todos;
Categories get categories;
// Custom expressions can be given a name by defining them as a getter:.
Expression<int> get itemCount => todos.id.count();
@override
Query as() =>
// Views can select columns defined as expression getters on the class, or
// they can reference columns from other tables.
select([categories.description, itemCount])
.from(categories)
.join([innerJoin(todos, todos.category.equalsExp(categories.id))]);
}
// #enddocregion view

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

@ -44,7 +44,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).

View File

@ -1,262 +0,0 @@
---
data:
title: Expressions
description: Deep-dive into what kind of SQL expressions can be written in Dart
weight: 200
# used to be in the "getting started" section
path: docs/getting-started/expressions-old/
template: layouts/docs/single
---
Expressions are pieces of sql that return a value when the database interprets them.
The Dart API from drift allows you to write most expressions in Dart and then convert
them to sql. Expressions are used in all kinds of situations. For instance, `where`
expects an expression that returns a boolean.
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 %}
## Comparisons
Every expression can be compared to a value by using `equals`. If you want to compare
an expression to another expression, you can use `equalsExpr`. For numeric and datetime
expressions, you can also use a variety of methods like `isSmallerThan`, `isSmallerOrEqual`
and so on to compare them:
```dart
// find all animals with less than 5 legs:
(select(animals)..where((a) => a.amountOfLegs.isSmallerThanValue(5))).get();
// find all animals who's average livespan is shorter than their amount of legs (poor flies)
(select(animals)..where((a) => a.averageLivespan.isSmallerThan(a.amountOfLegs)));
Future<List<Animal>> findAnimalsByLegs(int legCount) {
return (select(animals)..where((a) => a.legs.equals(legCount))).get();
}
```
## Boolean algebra
You can nest boolean expressions by using the `&`, `|` operators and the `not` method
exposed by drift:
```dart
// find all animals that aren't mammals and have 4 legs
select(animals)..where((a) => a.isMammal.not() & a.amountOfLegs.equals(4));
// find all animals that are mammals or have 2 legs
select(animals)..where((a) => a.isMammal | a.amountOfLegs.equals(2));
```
## Arithmetic
For `int` and `double` expressions, you can use the `+`, `-`, `*` and `/` operators. To
run calculations between a sql expression and a Dart value, wrap it in a `Variable`:
```dart
Future<List<Product>> canBeBought(int amount, int price) {
return (select(products)..where((p) {
final totalPrice = p.price * Variable(amount);
return totalPrice.isSmallerOrEqualValue(price);
})).get();
}
```
String expressions define a `+` operator as well. Just like you would expect, it performs
a concatenation in sql.
For integer values, you can use `~`, `bitwiseAnd` and `bitwiseOr` to perform
bitwise operations:
{% include "blocks/snippet" snippets = snippets name = 'bitwise' %}
## Nullability
To check whether an expression evaluates to `NULL` in sql, you can use the `isNull` extension:
```dart
final withoutCategories = select(todos)..where((row) => row.category.isNull());
```
The expression returned will resolve to `true` if the inner expression resolves to null
and `false` otherwise.
As you would expect, `isNotNull` works the other way around.
To use a fallback value when an expression evaluates to `null`, you can use the `coalesce`
function. It takes a list of expressions and evaluates to the first one that isn't `null`:
```dart
final category = coalesce([todos.category, const Constant(1)]);
```
This corresponds to the `??` operator in Dart.
## Date and Time
For columns and expressions that return a `DateTime`, you can use the
`year`, `month`, `day`, `hour`, `minute` and `second` getters to extract individual
fields from that date:
```dart
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`
and `currentDateAndTime` constants provided by drift.
You can also use the `+` and `-` operators to add or subtract a duration from a time column:
```dart
final toNextWeek = TasksCompanion.custom(dueDate: tasks.dueDate + Duration(weeks: 1));
update(tasks).write(toNextWeek);
```
## `IN` and `NOT IN`
You can check whether an expression is in a list of values by using the `isIn` and `isNotIn`
methods:
```dart
select(animals)..where((a) => a.amountOfLegs.isIn([3, 7, 4, 2]);
```
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
from the Dart api. Unlike regular functions, aggregate functions operate on multiple rows at
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 }}).
### Comparing
You can use the `min` and `max` methods on numeric and datetime expressions. They return the smallest
or largest value in the result set, respectively.
### Arithmetic
The `avg`, `sum` and `total` methods are available. For instance, you could watch the average length of
a todo item with this query:
```dart
Stream<double> averageItemLength() {
final avgLength = todos.content.length.avg();
final query = selectOnly(todos)
..addColumns([avgLength]);
return query.map((row) => row.read(avgLength)).watchSingle();
}
```
__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 }})
### Counting
Sometimes, it's useful to count how many rows are present in a group. By using the
[table layout from the example]({{ "../setup.md" | pageUrl }}), this
query will report how many todo entries are associated to each category:
```dart
final amountOfTodos = todos.id.count();
final query = db.select(categories).join([
innerJoin(
todos,
todos.category.equalsExp(categories.id),
useColumns: false,
)
]);
query
..addColumns([amountOfTodos])
..groupBy([categories.id]);
```
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 }})
### group_concat
The `groupConcat` function can be used to join multiple values into a single string:
```dart
Stream<String> allTodoContent() {
final allContent = todos.content.groupConcat();
final query = selectOnly(todos)..addColumns(allContent);
return query.map((row) => row.read(query)).watchSingle();
}
```
The separator defaults to a comma without surrounding whitespace, but it can be changed
with the `separator` argument on `groupConcat`.
## Mathematical functions and regexp
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.
## Subqueries
Drift has basic support for subqueries in expressions.
### Scalar subqueries
A _scalar subquery_ is a select statement that returns exactly one row with exactly one column.
Since it returns exactly one value, it can be used in another query:
```dart
Future<List<Todo>> findTodosInCategory(String description) async {
final groupId = selectOnly(categories)
..addColumns([categories.id])
..where(categories.description.equals(description));
return select(todos)..where((row) => row.category.equalsExp(subqueryExpression(groupId)));
}
```
Here, `groupId` is a regular select statement. By default drift would select all columns, so we use
`selectOnly` to only load the id of the category we care about.
Then, we can use `subqueryExpression` to embed that query into an expression that we're using as
a filter.
### `isInQuery`
Similar to [`isIn` and `isNotIn`](#in-and-not-in) functions, you can use `isInQuery` to pass
a subquery instead of a direct set of values.
The subquery must return exactly one column, but it is allowed to return more than one row.
`isInQuery` returns true if that value is present in the query.
### Exists
The `existsQuery` and `notExistsQuery` functions can be used to check if a subquery contains
any rows. For instance, we could use this to find empty categories:
{% include "blocks/snippet" snippets = snippets name = 'emptyCategories' %}
### Full subqueries
Drift also supports subqueries that appear in `JOIN`s, which are described in the
[documentation for joins]({{ 'joins.md#subqueries' | pageUrl }}).
## Custom expressions
If you want to inline custom sql into Dart queries, you can use a `CustomExpression` class.
It takes a `sql` parameter that lets you write custom expressions:
```dart
const inactive = CustomExpression<bool, BoolType>("julianday('now') - julianday(last_login) > 60");
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.

View File

@ -1,253 +0,0 @@
---
data:
title: "Advanced queries in Dart"
weight: 1
description: Use sql joins or custom expressions from the Dart api
aliases:
- queries/joins/
template: layouts/docs/single
---
{% assign snippets = 'package:drift_docs/snippets/queries.dart.excerpt.json' | readString | json_decode %}
## 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]({{ "../setup.md" | pageUrl }}).
{% include "blocks/snippet" snippets = snippets name = 'joinIntro' %}
Of course, you can also join multiple tables:
{% include "blocks/snippet" snippets = snippets name = 'otherTodosInSameCategory' %}
## Parsing results
Calling `get()` or `watch` on a select statement with join returns a `Future` or `Stream` of
`List<TypedResult>`, respectively. Each `TypedResult` represents a row from which data can be
read. It contains a `rawData` getter to obtain the raw columns. But more importantly, the
`readTable` method can be used to read a data class from a table.
In the example query above, we can read the todo entry and the category from each row like this:
{% include "blocks/snippet" snippets = snippets name = 'results' %}
_Note_: `readTable` will throw an `ArgumentError` when a table is not present in the row. For instance,
todo entries might not be in any category. To account for that, we use `row.readTableOrNull` to load
categories.
## Custom columns
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();
}
```
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.
## Aliases
Sometimes, a query references a table more than once. Consider the following example to store saved routes for a
navigation system:
```dart
class GeoPoints extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
TextColumn get latitude => text()();
TextColumn get longitude => text()();
}
class Routes extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
// contains the id for the start and destination geopoint.
IntColumn get start => integer()();
IntColumn get destination => integer()();
}
```
Now, let's say we wanted to also load the start and destination `GeoPoint` object for each route. We'd have to use
a join on the `geo-points` table twice: For the start and destination point. To express that in a query, aliases
can be used:
```dart
class RouteWithPoints {
final Route route;
final GeoPoint start;
final GeoPoint destination;
RouteWithPoints({this.route, this.start, this.destination});
}
// inside the database class:
Future<List<RouteWithPoints>> loadRoutes() async {
// create aliases for the geoPoints table so that we can reference it twice
final start = alias(geoPoints, 's');
final destination = alias(geoPoints, 'd');
final rows = await select(routes).join([
innerJoin(start, start.id.equalsExp(routes.start)),
innerJoin(destination, destination.id.equalsExp(routes.destination)),
]).get();
return rows.map((resultRow) {
return RouteWithPoints(
route: resultRow.readTable(routes),
start: resultRow.readTable(start),
destination: resultRow.readTable(destination),
);
}).toList();
}
```
The generated statement then looks like this:
```sql
SELECT
routes.id, routes.name, routes.start, routes.destination,
s.id, s.name, s.latitude, s.longitude,
d.id, d.name, d.latitude, d.longitude
FROM routes
INNER JOIN geo_points s ON s.id = routes.start
INNER JOIN geo_points d ON d.id = routes.destination
```
## `ORDER BY` and `WHERE` on joins
Similar to queries on a single table, `orderBy` and `where` can be used on joins too.
The initial example from above is expanded to only include todo entries with a specified
filter and to order results based on the category's id:
```dart
Stream<List<EntryWithCategory>> entriesWithCategory(String entryFilter) {
final query = select(todos).join([
leftOuterJoin(categories, categories.id.equalsExp(todos.category)),
]);
query.where(todos.content.like(entryFilter));
query.orderBy([OrderingTerm.asc(categories.id)]);
// ...
}
```
As a join can have more than one table, all tables in `where` and `orderBy` have to
be specified directly (unlike the callback on single-table queries that gets called
with the right table by default).
## Group by
Sometimes, you need to run queries that _aggregate_ data, meaning that data you're interested in
comes from multiple rows. Common questions include
- how many todo entries are in each category?
- how many entries did a user complete each month?
- what's the average length of a todo entry?
What these queries have in common is that data from multiple rows needs to be combined into a single
row. In sql, this can be achieved with "aggregate functions", for which drift has
[builtin support]({{ "expressions.md#aggregate" | pageUrl }}).
_Additional info_: A good tutorial for group by in sql is available [here](https://www.sqlitetutorial.net/sqlite-group-by/).
To write a query that answers the first question for us, we can use the `count` function.
We're going to select all categories and join each todo entry for each category. What's special is that we set
`useColumns: false` on the join. We do that because we're not interested in the columns of the todo item.
We only care about how many there are. By default, drift would attempt to read each todo item when it appears
in a join.
{% include "blocks/snippet" snippets = snippets name = 'countTodosInCategories' %}
To find the average length of a todo entry, we use `avg`. In this case, we don't even have to use
a `join` since all the data comes from a single table (todos).
That's a problem though - in the join, we used `useColumns: false` because we weren't interested
in the columns of each todo item. Here we don't care about an individual item either, but there's
no join where we could set that flag.
Drift provides a special method for this case - instead of using `select`, we use `selectOnly`.
The "only" means that drift will only report columns we added via "addColumns". In a regular select,
all columns from the table would be selected, which is what you'd usually need.
{% include "blocks/snippet" snippets = snippets name = 'averageItemLength' %}
## Using selects as inserts
In SQL, an `INSERT INTO SELECT` statement can be used to efficiently insert the rows from a `SELECT`
statement into a table.
It is possible to construct these statements in drift with the `insertFromSelect` method.
This example shows how that method is used to construct a statement that creates a new category
for each todo entry that didn't have one assigned before:
{% include "blocks/snippet" snippets = snippets name = 'createCategoryForUnassignedTodoEntries' %}
The first parameter for `insertFromSelect` is the select statement statement to use as a source.
Then, the `columns` map maps columns from the table in which rows are inserted to columns from the
select statement.
In the example, the `newDescription` expression as added as a column to the query.
Then, the map entry `categories.description: newDescription` is used so that the `description` column
for new category rows gets set to that expression.
## Subqueries
Starting from drift 2.11, you can use `Subquery` to use an existing select statement as part of more
complex join.
This snippet uses `Subquery` to count how many of the top-10 todo items (by length of their title) are
in each category.
It does this by first creating a select statement for the top-10 items (but not executing it), and then
joining this select statement onto a larger one grouping by category:
{% include "blocks/snippet" snippets = snippets name = 'subquery' %}
Any statement can be used as a subquery. But be aware that, unlike [subquery expressions]({{ 'expressions.md#scalar-subqueries' | pageUrl }}), full subqueries can't use tables from the outer select statement.
## JSON support
{% assign json_snippet = 'package:drift_docs/snippets/queries/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).
JSON support is helpful when storing a dynamic structure that is best represented with JSON, or when
you have an existing structure (perhaps because you're migrating from a document-based storage)
that you need to support.
As an example, consider a contact book application that started with a JSON structure to store
contacts:
{% include "blocks/snippet" snippets = json_snippet name = 'existing' %}
To easily store this contact representation in a drift database, one could use a JSON column:
{% include "blocks/snippet" snippets = json_snippet name = 'contacts' %}
Note the `name` column as well: It uses `generatedAs` with the `jsonExtract` function to
extract the `name` field from the JSON value on the fly.
The full syntax for JSON path arguments is explained on the [sqlite3 website](https://sqlite.org/json1.html#path_arguments).
To make the example more complex, let's look at another table storing a log of phone calls:
{% include "blocks/snippet" snippets = json_snippet name = 'calls' %}
Let's say we wanted to find the contact for each call, if there is any with a matching phone number.
For this to be expressible in SQL, each `contacts` row would somehow have to be expanded into a row
for each stored phone number.
Luckily, the `json_each` function in sqlite3 can do exactly that, and drift exposes it:
{% include "blocks/snippet" snippets = json_snippet name = 'calls-with-contacts' %}

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.

View File

@ -2,7 +2,7 @@
data:
title: "DAOs"
description: Keep your database code modular with DAOs
path: /docs/advanced-features/daos
path: /docs/advanced-features/daos/
aliases:
- /daos/
template: layouts/docs/single

View File

@ -8,3 +8,255 @@ data:
path: docs/getting-started/expressions/
template: layouts/docs/single
---
Expressions are pieces of sql that return a value when the database interprets them.
The Dart API from drift allows you to write most expressions in Dart and then convert
them to sql. Expressions are used in all kinds of situations. For instance, `where`
expects an expression that returns a boolean.
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/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
an expression to another expression, you can use `equalsExpr`. For numeric and datetime
expressions, you can also use a variety of methods like `isSmallerThan`, `isSmallerOrEqual`
and so on to compare them:
```dart
// find all animals with less than 5 legs:
(select(animals)..where((a) => a.amountOfLegs.isSmallerThanValue(5))).get();
// find all animals who's average livespan is shorter than their amount of legs (poor flies)
(select(animals)..where((a) => a.averageLivespan.isSmallerThan(a.amountOfLegs)));
Future<List<Animal>> findAnimalsByLegs(int legCount) {
return (select(animals)..where((a) => a.legs.equals(legCount))).get();
}
```
## Boolean algebra
You can nest boolean expressions by using the `&`, `|` operators and the `not` method
exposed by drift:
```dart
// find all animals that aren't mammals and have 4 legs
select(animals)..where((a) => a.isMammal.not() & a.amountOfLegs.equals(4));
// find all animals that are mammals or have 2 legs
select(animals)..where((a) => a.isMammal | a.amountOfLegs.equals(2));
```
## Arithmetic
For `int` and `double` expressions, you can use the `+`, `-`, `*` and `/` operators. To
run calculations between a sql expression and a Dart value, wrap it in a `Variable`:
```dart
Future<List<Product>> canBeBought(int amount, int price) {
return (select(products)..where((p) {
final totalPrice = p.price * Variable(amount);
return totalPrice.isSmallerOrEqualValue(price);
})).get();
}
```
String expressions define a `+` operator as well. Just like you would expect, it performs
a concatenation in sql.
For integer values, you can use `~`, `bitwiseAnd` and `bitwiseOr` to perform
bitwise operations:
{% include "blocks/snippet" snippets = snippets name = 'bitwise' %}
## Nullability
To check whether an expression evaluates to `NULL` in sql, you can use the `isNull` extension:
```dart
final withoutCategories = select(todos)..where((row) => row.category.isNull());
```
The expression returned will resolve to `true` if the inner expression resolves to null
and `false` otherwise.
As you would expect, `isNotNull` works the other way around.
To use a fallback value when an expression evaluates to `null`, you can use the `coalesce`
function. It takes a list of expressions and evaluates to the first one that isn't `null`:
```dart
final category = coalesce([todos.category, const Constant(1)]);
```
This corresponds to the `??` operator in Dart.
## Date and Time
For columns and expressions that return a `DateTime`, you can use the
`year`, `month`, `day`, `hour`, `minute` and `second` getters to extract individual
fields from that date:
```dart
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`
and `currentDateAndTime` constants provided by drift.
You can also use the `+` and `-` operators to add or subtract a duration from a time column:
```dart
final toNextWeek = TasksCompanion.custom(dueDate: tasks.dueDate + Duration(weeks: 1));
update(tasks).write(toNextWeek);
```
## `IN` and `NOT IN`
You can check whether an expression is in a list of values by using the `isIn` and `isNotIn`
methods:
```dart
select(animals)..where((a) => a.amountOfLegs.isIn([3, 7, 4, 2]);
```
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
from the Dart api. Unlike regular functions, aggregate functions operate on multiple rows at
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]({{ "select.md#group-by" | pageUrl }}).
### Comparing
You can use the `min` and `max` methods on numeric and datetime expressions. They return the smallest
or largest value in the result set, respectively.
### Arithmetic
The `avg`, `sum` and `total` methods are available. For instance, you could watch the average length of
a todo item with this query:
```dart
Stream<double> averageItemLength() {
final avgLength = todos.content.length.avg();
final query = selectOnly(todos)
..addColumns([avgLength]);
return query.map((row) => row.read(avgLength)).watchSingle();
}
```
__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]({{ "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]({{ "../setup.md" | pageUrl }}), this
query will report how many todo entries are associated to each category:
```dart
final amountOfTodos = todos.id.count();
final query = db.select(categories).join([
innerJoin(
todos,
todos.category.equalsExp(categories.id),
useColumns: false,
)
]);
query
..addColumns([amountOfTodos])
..groupBy([categories.id]);
```
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]({{ "select.md#group-by" | pageUrl }})
### group_concat
The `groupConcat` function can be used to join multiple values into a single string:
```dart
Stream<String> allTodoContent() {
final allContent = todos.content.groupConcat();
final query = selectOnly(todos)..addColumns(allContent);
return query.map((row) => row.read(query)).watchSingle();
}
```
The separator defaults to a comma without surrounding whitespace, but it can be changed
with the `separator` argument on `groupConcat`.
## Mathematical functions and regexp
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.
## Subqueries
Drift has basic support for subqueries in expressions.
### Scalar subqueries
A _scalar subquery_ is a select statement that returns exactly one row with exactly one column.
Since it returns exactly one value, it can be used in another query:
```dart
Future<List<Todo>> findTodosInCategory(String description) async {
final groupId = selectOnly(categories)
..addColumns([categories.id])
..where(categories.description.equals(description));
return select(todos)..where((row) => row.category.equalsExp(subqueryExpression(groupId)));
}
```
Here, `groupId` is a regular select statement. By default drift would select all columns, so we use
`selectOnly` to only load the id of the category we care about.
Then, we can use `subqueryExpression` to embed that query into an expression that we're using as
a filter.
### `isInQuery`
Similar to [`isIn` and `isNotIn`](#in-and-not-in) functions, you can use `isInQuery` to pass
a subquery instead of a direct set of values.
The subquery must return exactly one column, but it is allowed to return more than one row.
`isInQuery` returns true if that value is present in the query.
### Exists
The `existsQuery` and `notExistsQuery` functions can be used to check if a subquery contains
any rows. For instance, we could use this to find empty categories:
{% include "blocks/snippet" snippets = snippets name = 'emptyCategories' %}
### Full subqueries
Drift also supports subqueries that appear in `JOIN`s, which are described in the
[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.
It takes a `sql` parameter that lets you write custom expressions:
```dart
const inactive = CustomExpression<bool, BoolType>("julianday('now') - julianday(last_login) > 60");
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.

View File

@ -5,3 +5,318 @@ data:
weight: 2
template: layouts/docs/single
---
{% 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.
{% include "blocks/snippet" snippets = snippets name = 'joinIntro' %}
Of course, you can also join multiple tables:
{% include "blocks/snippet" snippets = snippets name = 'otherTodosInSameCategory' %}
## Parsing results
Calling `get()` or `watch` on a select statement with join returns a `Future` or `Stream` of
`List<TypedResult>`, respectively. Each `TypedResult` represents a row from which data can be
read. It contains a `rawData` getter to obtain the raw columns. But more importantly, the
`readTable` method can be used to read a data class from a table.
In the example query above, we can read the todo entry and the category from each row like this:
{% include "blocks/snippet" snippets = snippets name = 'results' %}
_Note_: `readTable` will throw an `ArgumentError` when a table is not present in the row. For instance,
todo entries might not be in any category. To account for that, we use `row.readTableOrNull` to load
categories.
## Custom columns
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.
{% 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.
## Aliases
Sometimes, a query references a table more than once. Consider the following example to store saved routes for a
navigation system:
```dart
class GeoPoints extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
TextColumn get latitude => text()();
TextColumn get longitude => text()();
}
class Routes extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text()();
// contains the id for the start and destination geopoint.
IntColumn get start => integer()();
IntColumn get destination => integer()();
}
```
Now, let's say we wanted to also load the start and destination `GeoPoint` object for each route. We'd have to use
a join on the `geo-points` table twice: For the start and destination point. To express that in a query, aliases
can be used:
```dart
class RouteWithPoints {
final Route route;
final GeoPoint start;
final GeoPoint destination;
RouteWithPoints({this.route, this.start, this.destination});
}
// inside the database class:
Future<List<RouteWithPoints>> loadRoutes() async {
// create aliases for the geoPoints table so that we can reference it twice
final start = alias(geoPoints, 's');
final destination = alias(geoPoints, 'd');
final rows = await select(routes).join([
innerJoin(start, start.id.equalsExp(routes.start)),
innerJoin(destination, destination.id.equalsExp(routes.destination)),
]).get();
return rows.map((resultRow) {
return RouteWithPoints(
route: resultRow.readTable(routes),
start: resultRow.readTable(start),
destination: resultRow.readTable(destination),
);
}).toList();
}
```
The generated statement then looks like this:
```sql
SELECT
routes.id, routes.name, routes.start, routes.destination,
s.id, s.name, s.latitude, s.longitude,
d.id, d.name, d.latitude, d.longitude
FROM routes
INNER JOIN geo_points s ON s.id = routes.start
INNER JOIN geo_points d ON d.id = routes.destination
```
## `ORDER BY` and `WHERE` on joins
Similar to queries on a single table, `orderBy` and `where` can be used on joins too.
The initial example from above is expanded to only include todo entries with a specified
filter and to order results based on the category's id:
```dart
Stream<List<EntryWithCategory>> entriesWithCategory(String entryFilter) {
final query = select(todos).join([
leftOuterJoin(categories, categories.id.equalsExp(todos.category)),
]);
query.where(todos.content.like(entryFilter));
query.orderBy([OrderingTerm.asc(categories.id)]);
// ...
}
```
As a join can have more than one table, all tables in `where` and `orderBy` have to
be specified directly (unlike the callback on single-table queries that gets called
with the right table by default).
## Group by
Sometimes, you need to run queries that _aggregate_ data, meaning that data you're interested in
comes from multiple rows. Common questions include
- how many todo entries are in each category?
- how many entries did a user complete each month?
- what's the average length of a todo entry?
What these queries have in common is that data from multiple rows needs to be combined into a single
row. In sql, this can be achieved with "aggregate functions", for which drift has
[builtin support]({{ "expressions.md#aggregate" | pageUrl }}).
_Additional info_: A good tutorial for group by in sql is available [here](https://www.sqlitetutorial.net/sqlite-group-by/).
To write a query that answers the first question for us, we can use the `count` function.
We're going to select all categories and join each todo entry for each category. What's special is that we set
`useColumns: false` on the join. We do that because we're not interested in the columns of the todo item.
We only care about how many there are. By default, drift would attempt to read each todo item when it appears
in a join.
{% include "blocks/snippet" snippets = snippets name = 'countTodosInCategories' %}
To find the average length of a todo entry, we use `avg`. In this case, we don't even have to use
a `join` since all the data comes from a single table (todos).
That's a problem though - in the join, we used `useColumns: false` because we weren't interested
in the columns of each todo item. Here we don't care about an individual item either, but there's
no join where we could set that flag.
Drift provides a special method for this case - instead of using `select`, we use `selectOnly`.
The "only" means that drift will only report columns we added via "addColumns". In a regular select,
all columns from the table would be selected, which is what you'd usually need.
{% include "blocks/snippet" snippets = snippets name = 'averageItemLength' %}
## Using selects as inserts
In SQL, an `INSERT INTO SELECT` statement can be used to efficiently insert the rows from a `SELECT`
statement into a table.
It is possible to construct these statements in drift with the `insertFromSelect` method.
This example shows how that method is used to construct a statement that creates a new category
for each todo entry that didn't have one assigned before:
{% include "blocks/snippet" snippets = snippets name = 'createCategoryForUnassignedTodoEntries' %}
The first parameter for `insertFromSelect` is the select statement statement to use as a source.
Then, the `columns` map maps columns from the table in which rows are inserted to columns from the
select statement.
In the example, the `newDescription` expression as added as a column to the query.
Then, the map entry `categories.description: newDescription` is used so that the `description` column
for new category rows gets set to that expression.
## Subqueries
Starting from drift 2.11, you can use `Subquery` to use an existing select statement as part of more
complex join.
This snippet uses `Subquery` to count how many of the top-10 todo items (by length of their title) are
in each category.
It does this by first creating a select statement for the top-10 items (but not executing it), and then
joining this select statement onto a larger one grouping by category:
{% include "blocks/snippet" snippets = snippets name = 'subquery' %}
Any statement can be used as a subquery. But be aware that, unlike [subquery expressions]({{ 'expressions.md#scalar-subqueries' | pageUrl }}), full subqueries can't use tables from the outer select statement.
## JSON support
{% 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).
JSON support is helpful when storing a dynamic structure that is best represented with JSON, or when
you have an existing structure (perhaps because you're migrating from a document-based storage)
that you need to support.
As an example, consider a contact book application that started with a JSON structure to store
contacts:
{% include "blocks/snippet" snippets = json_snippet name = 'existing' %}
To easily store this contact representation in a drift database, one could use a JSON column:
{% include "blocks/snippet" snippets = json_snippet name = 'contacts' %}
Note the `name` column as well: It uses `generatedAs` with the `jsonExtract` function to
extract the `name` field from the JSON value on the fly.
The full syntax for JSON path arguments is explained on the [sqlite3 website](https://sqlite.org/json1.html#path_arguments).
To make the example more complex, let's look at another table storing a log of phone calls:
{% include "blocks/snippet" snippets = json_snippet name = 'calls' %}
Let's say we wanted to find the contact for each call, if there is any with a matching phone number.
For this to be expressible in SQL, each `contacts` row would somehow have to be expanded into a row
for each stored phone number.
Luckily, the `json_each` function in sqlite3 can do exactly that, and drift exposes it:
{% include "blocks/snippet" snippets = json_snippet name = 'calls-with-contacts' %}

View File

@ -128,7 +128,7 @@ Drift supports two approaches of storing `DateTime` values in SQL:
The mode can be changed with the `store_date_time_values_as_text` [build option]({{ '../Advanced Features/builder_options.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,

View File

@ -10,7 +10,7 @@ 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

@ -5,3 +5,193 @@ data:
weight: 3
template: layouts/docs/single
---
## Updates and deletes
You can use the generated classes to update individual fields of any row:
```dart
Future moveImportantTasksIntoCategory(Category target) {
// for updates, we use the "companion" version of a generated class. This wraps the
// fields in a "Value" type which can be set to be absent using "Value.absent()". This
// allows us to separate between "SET category = NULL" (`category: Value(null)`) and not
// updating the category at all: `category: Value.absent()`.
return (update(todos)
..where((t) => t.title.like('%Important%'))
).write(TodosCompanion(
category: Value(target.id),
),
);
}
Future updateTodo(Todo entry) {
// using replace will update all fields from the entry that are not marked as a primary key.
// it will also make sure that only the entry with the same primary key will be updated.
// Here, this means that the row that has the same id as entry will be updated to reflect
// the entry's title, content and category. As its where clause is set automatically, it
// cannot be used together with where.
return update(todos).replace(entry);
}
Future feelingLazy() {
// delete the oldest nine tasks
return (delete(todos)..where((t) => t.id.isSmallerThanValue(10))).go();
}
```
__⚠ Caution:__ If you don't explicitly add a `where` clause on updates or deletes,
the statement will affect all rows in the table!
{% block "blocks/alert" title="Entries, companions - why do we need all of this?" %}
You might have noticed that we used a `TodosCompanion` for the first update instead of
just passing a `Todo`. Drift generates the `Todo` class (also called _data
class_ for the table) to hold a __full__ row with all its data. For _partial_ data,
prefer to use companions. In the example above, we only set the the `category` column,
so we used a companion.
Why is that necessary? If a field was set to `null`, we wouldn't know whether we need
to set that column back to null in the database or if we should just leave it unchanged.
Fields in the companions have a special `Value.absent()` state which makes this explicit.
Companions also have a special constructor for inserts - all columns which don't have
a default value and aren't nullable are marked `@required` on that constructor. This makes
companions easier to use for inserts because you know which fields to set.
{% endblock %}
## Inserts
You can very easily insert any valid object into tables. As some values can be absent
(like default values that we don't have to set explicitly), we again use the
companion version.
```dart
// returns the generated id
Future<int> addTodo(TodosCompanion entry) {
return into(todos).insert(entry);
}
```
All row classes generated will have a constructor that can be used to create objects:
```dart
addTodo(
TodosCompanion(
title: Value('Important task'),
content: Value('Refactor persistence code'),
),
);
```
If a column is nullable or has a default value (this includes auto-increments), the field
can be omitted. All other fields must be set and non-null. The `insert` method will throw
otherwise.
Multiple insert statements can be run efficiently by using a batch. To do that, you can
use the `insertAll` method inside a `batch`:
```dart
Future<void> insertMultipleEntries() async{
await batch((batch) {
// functions in a batch don't have to be awaited - just
// await the whole batch afterwards.
batch.insertAll(todos, [
TodosCompanion.insert(
title: 'First entry',
content: 'My content',
),
TodosCompanion.insert(
title: 'Another entry',
content: 'More content',
// columns that aren't required for inserts are still wrapped in a Value:
category: Value(3),
),
// ...
]);
});
}
```
Batches are similar to transactions in the sense that all updates are happening atomically,
but they enable further optimizations to avoid preparing the same SQL statement twice.
This makes them suitable for bulk insert or update operations.
### Upserts
Upserts are a feature from newer sqlite3 versions that allows an insert to
behave like an update if a conflicting row already exists.
This allows us to create or override an existing row when its primary key is
part of its data:
```dart
class Users extends Table {
TextColumn get email => text()();
TextColumn get name => text()();
@override
Set<Column> get primaryKey => {email};
}
Future<int> createOrUpdateUser(User user) {
return into(users).insertOnConflictUpdate(user);
}
```
When calling `createOrUpdateUser()` with an email address that already exists,
that user's name will be updated. Otherwise, a new user will be inserted into
the database.
Inserts can also be used with more advanced queries. For instance, let's say
we're building a dictionary and want to keep track of how many times we
encountered a word. A table for that might look like
```dart
class Words extends Table {
TextColumn get word => text()();
IntColumn get usages => integer().withDefault(const Constant(1))();
@override
Set<Column> get primaryKey => {word};
}
```
By using a custom upserts, we can insert a new word or increment its `usages`
counter if it already exists:
```dart
Future<void> trackWord(String word) {
return into(words).insert(
WordsCompanion.insert(word: word),
onConflict: DoUpdate((old) => WordsCompanion.custom(usages: old.usages + Constant(1))),
);
}
```
{% block "blocks/alert" title="Unique constraints and conflict targets" %}
Both `insertOnConflictUpdate` and `onConflict: DoUpdate` use an `DO UPDATE`
upsert in sql. This requires us to provide a so-called "conflict target", a
set of columns to check for uniqueness violations. By default, drift will use
the table's primary key as conflict target. That works in most cases, but if
you have custom `UNIQUE` constraints on some columns, you'll need to use
the `target` parameter on `DoUpdate` in Dart to include those columns.
{% endblock %}
Note that this requires a fairly recent sqlite3 version (3.24.0) that might not
be available on older Android devices when using `drift_sqflite`. `NativeDatabases`
and `sqlite3_flutter_libs` includes the latest sqlite on Android, so consider using
it if you want to support upserts.
Also note that the returned rowid may not be accurate when an upsert took place.
### Returning
You can use `insertReturning` to insert a row or companion and immediately get the row it inserts.
The returned row contains all the default values and incrementing ids that were
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]({{ '../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
set in a row returned by `insertReturning`.

View File

@ -60,8 +60,8 @@ 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 }}).
- Companion versions of these classes. Those are only relevant when
using the Dart apis of drift, you can [learn more here]({{ "../Dart API/writes.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
@ -88,8 +88,8 @@ further guides to help you learn more:
- The [SQL IDE]({{ "../Using SQL/sql_ide.md" | pageUrl }}) that provides feedback on sql queries right in your editor.
- [Transactions]({{ "../Dart API/transactions.md" | pageUrl }})
- [Schema migrations]({{ "../Advanced Features/migrations.md" | pageUrl }})
- Writing [queries]({{ "writing_queries.md" | pageUrl }}) and
[expressions]({{ "../Advanced Features/expressions.md" | pageUrl }}) in Dart
- Writing [queries]({{ "../Dart API/select.md" | pageUrl }}) and
[expressions]({{ "../Dart API/expressions.md" | pageUrl }}) in Dart
- A more [in-depth guide]({{ "../Using SQL/drift_files.md" | pageUrl }})
on `drift` files, which explains `import` statements and the Dart-SQL interop.

View File

@ -9,303 +9,3 @@ aliases:
template: layouts/docs/single
---
{% block "blocks/pageinfo" %}
__Note__: This assumes that you've already completed [the setup]({{ "../setup.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) {
// for updates, we use the "companion" version of a generated class. This wraps the
// fields in a "Value" type which can be set to be absent using "Value.absent()". This
// allows us to separate between "SET category = NULL" (`category: Value(null)`) and not
// updating the category at all: `category: Value.absent()`.
return (update(todos)
..where((t) => t.title.like('%Important%'))
).write(TodosCompanion(
category: Value(target.id),
),
);
}
Future updateTodo(Todo entry) {
// using replace will update all fields from the entry that are not marked as a primary key.
// it will also make sure that only the entry with the same primary key will be updated.
// Here, this means that the row that has the same id as entry will be updated to reflect
// the entry's title, content and category. As its where clause is set automatically, it
// cannot be used together with where.
return update(todos).replace(entry);
}
Future feelingLazy() {
// delete the oldest nine tasks
return (delete(todos)..where((t) => t.id.isSmallerThanValue(10))).go();
}
```
__⚠ Caution:__ If you don't explicitly add a `where` clause on updates or deletes,
the statement will affect all rows in the table!
{% block "blocks/alert" title="Entries, companions - why do we need all of this?" %}
You might have noticed that we used a `TodosCompanion` for the first update instead of
just passing a `Todo`. Drift generates the `Todo` class (also called _data
class_ for the table) to hold a __full__ row with all its data. For _partial_ data,
prefer to use companions. In the example above, we only set the the `category` column,
so we used a companion.
Why is that necessary? If a field was set to `null`, we wouldn't know whether we need
to set that column back to null in the database or if we should just leave it unchanged.
Fields in the companions have a special `Value.absent()` state which makes this explicit.
Companions also have a special constructor for inserts - all columns which don't have
a default value and aren't nullable are marked `@required` on that constructor. This makes
companions easier to use for inserts because you know which fields to set.
{% endblock %}
## Inserts
You can very easily insert any valid object into tables. As some values can be absent
(like default values that we don't have to set explicitly), we again use the
companion version.
```dart
// returns the generated id
Future<int> addTodo(TodosCompanion entry) {
return into(todos).insert(entry);
}
```
All row classes generated will have a constructor that can be used to create objects:
```dart
addTodo(
TodosCompanion(
title: Value('Important task'),
content: Value('Refactor persistence code'),
),
);
```
If a column is nullable or has a default value (this includes auto-increments), the field
can be omitted. All other fields must be set and non-null. The `insert` method will throw
otherwise.
Multiple insert statements can be run efficiently by using a batch. To do that, you can
use the `insertAll` method inside a `batch`:
```dart
Future<void> insertMultipleEntries() async{
await batch((batch) {
// functions in a batch don't have to be awaited - just
// await the whole batch afterwards.
batch.insertAll(todos, [
TodosCompanion.insert(
title: 'First entry',
content: 'My content',
),
TodosCompanion.insert(
title: 'Another entry',
content: 'More content',
// columns that aren't required for inserts are still wrapped in a Value:
category: Value(3),
),
// ...
]);
});
}
```
Batches are similar to transactions in the sense that all updates are happening atomically,
but they enable further optimizations to avoid preparing the same SQL statement twice.
This makes them suitable for bulk insert or update operations.
### Upserts
Upserts are a feature from newer sqlite3 versions that allows an insert to
behave like an update if a conflicting row already exists.
This allows us to create or override an existing row when its primary key is
part of its data:
```dart
class Users extends Table {
TextColumn get email => text()();
TextColumn get name => text()();
@override
Set<Column> get primaryKey => {email};
}
Future<int> createOrUpdateUser(User user) {
return into(users).insertOnConflictUpdate(user);
}
```
When calling `createOrUpdateUser()` with an email address that already exists,
that user's name will be updated. Otherwise, a new user will be inserted into
the database.
Inserts can also be used with more advanced queries. For instance, let's say
we're building a dictionary and want to keep track of how many times we
encountered a word. A table for that might look like
```dart
class Words extends Table {
TextColumn get word => text()();
IntColumn get usages => integer().withDefault(const Constant(1))();
@override
Set<Column> get primaryKey => {word};
}
```
By using a custom upserts, we can insert a new word or increment its `usages`
counter if it already exists:
```dart
Future<void> trackWord(String word) {
return into(words).insert(
WordsCompanion.insert(word: word),
onConflict: DoUpdate((old) => WordsCompanion.custom(usages: old.usages + Constant(1))),
);
}
```
{% block "blocks/alert" title="Unique constraints and conflict targets" %}
Both `insertOnConflictUpdate` and `onConflict: DoUpdate` use an `DO UPDATE`
upsert in sql. This requires us to provide a so-called "conflict target", a
set of columns to check for uniqueness violations. By default, drift will use
the table's primary key as conflict target. That works in most cases, but if
you have custom `UNIQUE` constraints on some columns, you'll need to use
the `target` parameter on `DoUpdate` in Dart to include those columns.
{% endblock %}
Note that this requires a fairly recent sqlite3 version (3.24.0) that might not
be available on older Android devices when using `drift_sqflite`. `NativeDatabases`
and `sqlite3_flutter_libs` includes the latest sqlite on Android, so consider using
it if you want to support upserts.
Also note that the returned rowid may not be accurate when an upsert took place.
### Returning
You can use `insertReturning` to insert a row or companion and immediately get the row it inserts.
The returned row contains all the default values and incrementing ids that were
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]({{ '../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
set in a row returned by `insertReturning`.

View File

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