mirror of https://github.com/AMT-Cheif/drift.git
Port docs on joins and basic queries
This commit is contained in:
parent
10725d98fb
commit
999c17e19a
|
@ -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
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/internal/modular.dart';
|
||||
|
||||
import 'todo_tables.drift.dart';
|
||||
|
||||
// #docregion tables
|
||||
class TodoItems extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get title => text().withLength(min: 6, max: 32)();
|
||||
TextColumn get content => text().named('body')();
|
||||
IntColumn get category => integer().nullable().references(Categories, #id)();
|
||||
}
|
||||
|
||||
@DataClassName('Category')
|
||||
class Categories extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get name => text()();
|
||||
}
|
||||
// #enddocregion tables
|
||||
|
||||
class CanUseCommonTables extends ModularAccessor {
|
||||
CanUseCommonTables(super.attachedDatabase);
|
||||
|
||||
$TodoItemsTable get todoItems => resultSet('todo_items');
|
||||
$CategoriesTable get categories => resultSet('categories');
|
||||
}
|
|
@ -0,0 +1,432 @@
|
|||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:drift_docs/snippets/_shared/todo_tables.drift.dart' as i1;
|
||||
import 'package:drift_docs/snippets/_shared/todo_tables.dart' as i2;
|
||||
|
||||
class $TodoItemsTable extends i2.TodoItems
|
||||
with i0.TableInfo<$TodoItemsTable, i1.TodoItem> {
|
||||
@override
|
||||
final i0.GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$TodoItemsTable(this.attachedDatabase, [this._alias]);
|
||||
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
|
||||
@override
|
||||
late final i0.GeneratedColumn<int> id = i0.GeneratedColumn<int>(
|
||||
'id', aliasedName, false,
|
||||
hasAutoIncrement: true,
|
||||
type: i0.DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
i0.GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
||||
static const i0.VerificationMeta _titleMeta =
|
||||
const i0.VerificationMeta('title');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> title = i0.GeneratedColumn<String>(
|
||||
'title', aliasedName, false,
|
||||
additionalChecks: i0.GeneratedColumn.checkTextLength(
|
||||
minTextLength: 6, maxTextLength: 32),
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: true);
|
||||
static const i0.VerificationMeta _contentMeta =
|
||||
const i0.VerificationMeta('content');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> content = i0.GeneratedColumn<String>(
|
||||
'body', aliasedName, false,
|
||||
type: i0.DriftSqlType.string, requiredDuringInsert: true);
|
||||
static const i0.VerificationMeta _categoryMeta =
|
||||
const i0.VerificationMeta('category');
|
||||
@override
|
||||
late final i0.GeneratedColumn<int> category = i0.GeneratedColumn<int>(
|
||||
'category', aliasedName, true,
|
||||
type: i0.DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
i0.GeneratedColumn.constraintIsAlways('REFERENCES categories (id)'));
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns => [id, title, content, category];
|
||||
@override
|
||||
String get aliasedName => _alias ?? 'todo_items';
|
||||
@override
|
||||
String get actualTableName => 'todo_items';
|
||||
@override
|
||||
i0.VerificationContext validateIntegrity(i0.Insertable<i1.TodoItem> instance,
|
||||
{bool isInserting = false}) {
|
||||
final context = i0.VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('id')) {
|
||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||
}
|
||||
if (data.containsKey('title')) {
|
||||
context.handle(
|
||||
_titleMeta, title.isAcceptableOrUnknown(data['title']!, _titleMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_titleMeta);
|
||||
}
|
||||
if (data.containsKey('body')) {
|
||||
context.handle(_contentMeta,
|
||||
content.isAcceptableOrUnknown(data['body']!, _contentMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_contentMeta);
|
||||
}
|
||||
if (data.containsKey('category')) {
|
||||
context.handle(_categoryMeta,
|
||||
category.isAcceptableOrUnknown(data['category']!, _categoryMeta));
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<i0.GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
i1.TodoItem map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return i1.TodoItem(
|
||||
id: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||
title: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}title'])!,
|
||||
content: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}body'])!,
|
||||
category: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.int, data['${effectivePrefix}category']),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
$TodoItemsTable createAlias(String alias) {
|
||||
return $TodoItemsTable(attachedDatabase, alias);
|
||||
}
|
||||
}
|
||||
|
||||
class TodoItem extends i0.DataClass implements i0.Insertable<i1.TodoItem> {
|
||||
final int id;
|
||||
final String title;
|
||||
final String content;
|
||||
final int? category;
|
||||
const TodoItem(
|
||||
{required this.id,
|
||||
required this.title,
|
||||
required this.content,
|
||||
this.category});
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
map['id'] = i0.Variable<int>(id);
|
||||
map['title'] = i0.Variable<String>(title);
|
||||
map['body'] = i0.Variable<String>(content);
|
||||
if (!nullToAbsent || category != null) {
|
||||
map['category'] = i0.Variable<int>(category);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
i1.TodoItemsCompanion toCompanion(bool nullToAbsent) {
|
||||
return i1.TodoItemsCompanion(
|
||||
id: i0.Value(id),
|
||||
title: i0.Value(title),
|
||||
content: i0.Value(content),
|
||||
category: category == null && nullToAbsent
|
||||
? const i0.Value.absent()
|
||||
: i0.Value(category),
|
||||
);
|
||||
}
|
||||
|
||||
factory TodoItem.fromJson(Map<String, dynamic> json,
|
||||
{i0.ValueSerializer? serializer}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return TodoItem(
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
title: serializer.fromJson<String>(json['title']),
|
||||
content: serializer.fromJson<String>(json['content']),
|
||||
category: serializer.fromJson<int?>(json['category']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
'title': serializer.toJson<String>(title),
|
||||
'content': serializer.toJson<String>(content),
|
||||
'category': serializer.toJson<int?>(category),
|
||||
};
|
||||
}
|
||||
|
||||
i1.TodoItem copyWith(
|
||||
{int? id,
|
||||
String? title,
|
||||
String? content,
|
||||
i0.Value<int?> category = const i0.Value.absent()}) =>
|
||||
i1.TodoItem(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
content: content ?? this.content,
|
||||
category: category.present ? category.value : this.category,
|
||||
);
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('TodoItem(')
|
||||
..write('id: $id, ')
|
||||
..write('title: $title, ')
|
||||
..write('content: $content, ')
|
||||
..write('category: $category')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, title, content, category);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is i1.TodoItem &&
|
||||
other.id == this.id &&
|
||||
other.title == this.title &&
|
||||
other.content == this.content &&
|
||||
other.category == this.category);
|
||||
}
|
||||
|
||||
class TodoItemsCompanion extends i0.UpdateCompanion<i1.TodoItem> {
|
||||
final i0.Value<int> id;
|
||||
final i0.Value<String> title;
|
||||
final i0.Value<String> content;
|
||||
final i0.Value<int?> category;
|
||||
const TodoItemsCompanion({
|
||||
this.id = const i0.Value.absent(),
|
||||
this.title = const i0.Value.absent(),
|
||||
this.content = const i0.Value.absent(),
|
||||
this.category = const i0.Value.absent(),
|
||||
});
|
||||
TodoItemsCompanion.insert({
|
||||
this.id = const i0.Value.absent(),
|
||||
required String title,
|
||||
required String content,
|
||||
this.category = const i0.Value.absent(),
|
||||
}) : title = i0.Value(title),
|
||||
content = i0.Value(content);
|
||||
static i0.Insertable<i1.TodoItem> custom({
|
||||
i0.Expression<int>? id,
|
||||
i0.Expression<String>? title,
|
||||
i0.Expression<String>? content,
|
||||
i0.Expression<int>? category,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (title != null) 'title': title,
|
||||
if (content != null) 'body': content,
|
||||
if (category != null) 'category': category,
|
||||
});
|
||||
}
|
||||
|
||||
i1.TodoItemsCompanion copyWith(
|
||||
{i0.Value<int>? id,
|
||||
i0.Value<String>? title,
|
||||
i0.Value<String>? content,
|
||||
i0.Value<int?>? category}) {
|
||||
return i1.TodoItemsCompanion(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
content: content ?? this.content,
|
||||
category: category ?? this.category,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = i0.Variable<int>(id.value);
|
||||
}
|
||||
if (title.present) {
|
||||
map['title'] = i0.Variable<String>(title.value);
|
||||
}
|
||||
if (content.present) {
|
||||
map['body'] = i0.Variable<String>(content.value);
|
||||
}
|
||||
if (category.present) {
|
||||
map['category'] = i0.Variable<int>(category.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('TodoItemsCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('title: $title, ')
|
||||
..write('content: $content, ')
|
||||
..write('category: $category')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class $CategoriesTable extends i2.Categories
|
||||
with i0.TableInfo<$CategoriesTable, i1.Category> {
|
||||
@override
|
||||
final i0.GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
$CategoriesTable(this.attachedDatabase, [this._alias]);
|
||||
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
|
||||
@override
|
||||
late final i0.GeneratedColumn<int> id = i0.GeneratedColumn<int>(
|
||||
'id', aliasedName, false,
|
||||
hasAutoIncrement: true,
|
||||
type: i0.DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
i0.GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
||||
static const i0.VerificationMeta _nameMeta =
|
||||
const i0.VerificationMeta('name');
|
||||
@override
|
||||
late final i0.GeneratedColumn<String> name = i0.GeneratedColumn<String>(
|
||||
'name', aliasedName, false,
|
||||
type: i0.DriftSqlType.string, requiredDuringInsert: true);
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns => [id, name];
|
||||
@override
|
||||
String get aliasedName => _alias ?? 'categories';
|
||||
@override
|
||||
String get actualTableName => 'categories';
|
||||
@override
|
||||
i0.VerificationContext validateIntegrity(i0.Insertable<i1.Category> instance,
|
||||
{bool isInserting = false}) {
|
||||
final context = i0.VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('id')) {
|
||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||
}
|
||||
if (data.containsKey('name')) {
|
||||
context.handle(
|
||||
_nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_nameMeta);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<i0.GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
i1.Category map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return i1.Category(
|
||||
id: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||
name: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
$CategoriesTable createAlias(String alias) {
|
||||
return $CategoriesTable(attachedDatabase, alias);
|
||||
}
|
||||
}
|
||||
|
||||
class Category extends i0.DataClass implements i0.Insertable<i1.Category> {
|
||||
final int id;
|
||||
final String name;
|
||||
const Category({required this.id, required this.name});
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
map['id'] = i0.Variable<int>(id);
|
||||
map['name'] = i0.Variable<String>(name);
|
||||
return map;
|
||||
}
|
||||
|
||||
i1.CategoriesCompanion toCompanion(bool nullToAbsent) {
|
||||
return i1.CategoriesCompanion(
|
||||
id: i0.Value(id),
|
||||
name: i0.Value(name),
|
||||
);
|
||||
}
|
||||
|
||||
factory Category.fromJson(Map<String, dynamic> json,
|
||||
{i0.ValueSerializer? serializer}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return Category(
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
name: serializer.fromJson<String>(json['name']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
'name': serializer.toJson<String>(name),
|
||||
};
|
||||
}
|
||||
|
||||
i1.Category copyWith({int? id, String? name}) => i1.Category(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
);
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('Category(')
|
||||
..write('id: $id, ')
|
||||
..write('name: $name')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, name);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is i1.Category && other.id == this.id && other.name == this.name);
|
||||
}
|
||||
|
||||
class CategoriesCompanion extends i0.UpdateCompanion<i1.Category> {
|
||||
final i0.Value<int> id;
|
||||
final i0.Value<String> name;
|
||||
const CategoriesCompanion({
|
||||
this.id = const i0.Value.absent(),
|
||||
this.name = const i0.Value.absent(),
|
||||
});
|
||||
CategoriesCompanion.insert({
|
||||
this.id = const i0.Value.absent(),
|
||||
required String name,
|
||||
}) : name = i0.Value(name);
|
||||
static i0.Insertable<i1.Category> custom({
|
||||
i0.Expression<int>? id,
|
||||
i0.Expression<String>? name,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (name != null) 'name': name,
|
||||
});
|
||||
}
|
||||
|
||||
i1.CategoriesCompanion copyWith({i0.Value<int>? id, i0.Value<String>? name}) {
|
||||
return i1.CategoriesCompanion(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = i0.Variable<int>(id.value);
|
||||
}
|
||||
if (name.present) {
|
||||
map['name'] = i0.Variable<String>(name.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('CategoriesCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('name: $name')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
|
@ -1,12 +1,13 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
import 'tables/filename.dart';
|
||||
import '../_shared/todo_tables.dart';
|
||||
import '../_shared/todo_tables.drift.dart';
|
||||
|
||||
extension Expressions on MyDatabase {
|
||||
extension Snippets on CanUseCommonTables {
|
||||
// #docregion emptyCategories
|
||||
Future<List<Category>> emptyCategories() {
|
||||
final hasNoTodo = notExistsQuery(
|
||||
select(todos)..where((row) => row.category.equalsExp(categories.id)));
|
||||
final hasNoTodo = notExistsQuery(select(todoItems)
|
||||
..where((row) => row.category.equalsExp(categories.id)));
|
||||
return (select(categories)..where((row) => hasNoTodo)).get();
|
||||
}
|
||||
// #enddocregion emptyCategories
|
|
@ -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
|
||||
}
|
|
@ -1,13 +1,16 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'tables/filename.dart';
|
||||
|
||||
extension Snippets on MyDatabase {
|
||||
import '../_shared/todo_tables.dart';
|
||||
import '../_shared/todo_tables.drift.dart';
|
||||
|
||||
extension Snippets on CanUseCommonTables {
|
||||
// #docregion deleteCategory
|
||||
Future<void> deleteCategory(Category category) {
|
||||
return transaction(() async {
|
||||
// first, move the affected todo entries back to the default category
|
||||
await (update(todos)..where((row) => row.category.equals(category.id)))
|
||||
.write(const TodosCompanion(category: Value(null)));
|
||||
await (update(todoItems)
|
||||
..where((row) => row.category.equals(category.id)))
|
||||
.write(const TodoItemsCompanion(category: Value(null)));
|
||||
|
||||
// then, delete the category
|
||||
await delete(categories).delete(category);
|
||||
|
@ -18,14 +21,13 @@ extension Snippets on MyDatabase {
|
|||
// #docregion nested
|
||||
Future<void> nestedTransactions() async {
|
||||
await transaction(() async {
|
||||
await into(categories)
|
||||
.insert(CategoriesCompanion.insert(description: 'first'));
|
||||
await into(categories).insert(CategoriesCompanion.insert(name: 'first'));
|
||||
|
||||
// this is a nested transaction:
|
||||
await transaction(() async {
|
||||
// At this point, the first category is visible
|
||||
await into(categories)
|
||||
.insert(CategoriesCompanion.insert(description: 'second'));
|
||||
.insert(CategoriesCompanion.insert(name: 'second'));
|
||||
// Here, the second category is only visible inside this nested
|
||||
// transaction.
|
||||
});
|
||||
|
@ -36,7 +38,7 @@ extension Snippets on MyDatabase {
|
|||
await transaction(() async {
|
||||
// At this point, both categories are visible
|
||||
await into(categories)
|
||||
.insert(CategoriesCompanion.insert(description: 'third'));
|
||||
.insert(CategoriesCompanion.insert(name: 'third'));
|
||||
// The third category is only visible here.
|
||||
throw Exception('Abort in the second nested transaction');
|
||||
});
|
|
@ -1,8 +1,8 @@
|
|||
import 'package:drift/drift.dart';
|
||||
|
||||
import '../tables/filename.dart';
|
||||
|
||||
part 'custom_queries.g.dart';
|
||||
import '../_shared/todo_tables.dart';
|
||||
import '../_shared/todo_tables.drift.dart';
|
||||
import 'custom_queries.drift.dart';
|
||||
|
||||
// #docregion manual
|
||||
class CategoryWithCount {
|
||||
|
@ -16,14 +16,14 @@ class CategoryWithCount {
|
|||
|
||||
// #docregion setup
|
||||
@DriftDatabase(
|
||||
tables: [Todos, Categories],
|
||||
tables: [TodoItems, Categories],
|
||||
queries: {
|
||||
'categoriesWithCount': 'SELECT *, '
|
||||
'(SELECT COUNT(*) FROM todos WHERE category = c.id) AS "amount" '
|
||||
'(SELECT COUNT(*) FROM todo_items WHERE category = c.id) AS "amount" '
|
||||
'FROM categories c;'
|
||||
},
|
||||
)
|
||||
class MyDatabase extends _$MyDatabase {
|
||||
class MyDatabase extends $MyDatabase {
|
||||
// rest of class stays the same
|
||||
// #enddocregion setup
|
||||
@override
|
||||
|
@ -52,7 +52,7 @@ class MyDatabase extends _$MyDatabase {
|
|||
'SELECT *, (SELECT COUNT(*) FROM todos WHERE category = c.id) AS "amount"'
|
||||
' FROM categories c;',
|
||||
// used for the stream: the stream will update when either table changes
|
||||
readsFrom: {todos, categories},
|
||||
readsFrom: {todoItems, categories},
|
||||
).watch().map((rows) {
|
||||
// we get list of rows here. We just have to turn the raw data from the
|
||||
// row into a CategoryWithCount instnace. As we defined the Category table
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:drift_docs/snippets/_shared/todo_tables.drift.dart' as i1;
|
||||
|
||||
abstract class $MyDatabase extends i0.GeneratedDatabase {
|
||||
$MyDatabase(i0.QueryExecutor e) : super(e);
|
||||
late final i1.$CategoriesTable categories = i1.$CategoriesTable(this);
|
||||
late final i1.$TodoItemsTable todoItems = i1.$TodoItemsTable(this);
|
||||
i0.Selectable<CategoriesWithCountResult> categoriesWithCount() {
|
||||
return customSelect(
|
||||
'SELECT *, (SELECT COUNT(*) FROM todo_items WHERE category = c.id) AS amount FROM categories AS c',
|
||||
variables: [],
|
||||
readsFrom: {
|
||||
todoItems,
|
||||
categories,
|
||||
}).map((i0.QueryRow row) => CategoriesWithCountResult(
|
||||
id: row.read<int>('id'),
|
||||
name: row.read<String>('name'),
|
||||
amount: row.read<int>('amount'),
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
||||
@override
|
||||
List<i0.DatabaseSchemaEntity> get allSchemaEntities =>
|
||||
[categories, todoItems];
|
||||
}
|
||||
|
||||
class CategoriesWithCountResult {
|
||||
final int id;
|
||||
final String name;
|
||||
final int amount;
|
||||
CategoriesWithCountResult({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.amount,
|
||||
});
|
||||
}
|
|
@ -1,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
|
|
@ -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
|
|
@ -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).
|
||||
|
|
|
@ -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.
|
|
@ -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' %}
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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' %}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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`
|
||||
|
|
Loading…
Reference in New Issue