diff --git a/docs/build.yaml b/docs/build.yaml index 5b7b9234..ab3f84e9 100644 --- a/docs/build.yaml +++ b/docs/build.yaml @@ -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 diff --git a/docs/lib/snippets/_shared/todo_tables.dart b/docs/lib/snippets/_shared/todo_tables.dart new file mode 100644 index 00000000..695e3c31 --- /dev/null +++ b/docs/lib/snippets/_shared/todo_tables.dart @@ -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'); +} diff --git a/docs/lib/snippets/_shared/todo_tables.drift.dart b/docs/lib/snippets/_shared/todo_tables.drift.dart new file mode 100644 index 00000000..3e22c747 --- /dev/null +++ b/docs/lib/snippets/_shared/todo_tables.drift.dart @@ -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 id = i0.GeneratedColumn( + '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 title = i0.GeneratedColumn( + '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 content = i0.GeneratedColumn( + 'body', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _categoryMeta = + const i0.VerificationMeta('category'); + @override + late final i0.GeneratedColumn category = i0.GeneratedColumn( + 'category', aliasedName, true, + type: i0.DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + i0.GeneratedColumn.constraintIsAlways('REFERENCES categories (id)')); + @override + List 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 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 get $primaryKey => {id}; + @override + i1.TodoItem map(Map 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 { + 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 toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = i0.Variable(id); + map['title'] = i0.Variable(title); + map['body'] = i0.Variable(content); + if (!nullToAbsent || category != null) { + map['category'] = i0.Variable(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 json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return TodoItem( + id: serializer.fromJson(json['id']), + title: serializer.fromJson(json['title']), + content: serializer.fromJson(json['content']), + category: serializer.fromJson(json['category']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'title': serializer.toJson(title), + 'content': serializer.toJson(content), + 'category': serializer.toJson(category), + }; + } + + i1.TodoItem copyWith( + {int? id, + String? title, + String? content, + i0.Value 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 { + final i0.Value id; + final i0.Value title; + final i0.Value content; + final i0.Value 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 custom({ + i0.Expression? id, + i0.Expression? title, + i0.Expression? content, + i0.Expression? 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? id, + i0.Value? title, + i0.Value? content, + i0.Value? category}) { + return i1.TodoItemsCompanion( + id: id ?? this.id, + title: title ?? this.title, + content: content ?? this.content, + category: category ?? this.category, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (title.present) { + map['title'] = i0.Variable(title.value); + } + if (content.present) { + map['body'] = i0.Variable(content.value); + } + if (category.present) { + map['category'] = i0.Variable(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 id = i0.GeneratedColumn( + '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 name = i0.GeneratedColumn( + 'name', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, name]; + @override + String get aliasedName => _alias ?? 'categories'; + @override + String get actualTableName => 'categories'; + @override + i0.VerificationContext validateIntegrity(i0.Insertable 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 get $primaryKey => {id}; + @override + i1.Category map(Map 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 { + final int id; + final String name; + const Category({required this.id, required this.name}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = i0.Variable(id); + map['name'] = i0.Variable(name); + return map; + } + + i1.CategoriesCompanion toCompanion(bool nullToAbsent) { + return i1.CategoriesCompanion( + id: i0.Value(id), + name: i0.Value(name), + ); + } + + factory Category.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return Category( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(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 { + final i0.Value id; + final i0.Value 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 custom({ + i0.Expression? id, + i0.Expression? name, + }) { + return i0.RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + }); + } + + i1.CategoriesCompanion copyWith({i0.Value? id, i0.Value? name}) { + return i1.CategoriesCompanion( + id: id ?? this.id, + name: name ?? this.name, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (name.present) { + map['name'] = i0.Variable(name.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('CategoriesCompanion(') + ..write('id: $id, ') + ..write('name: $name') + ..write(')')) + .toString(); + } +} diff --git a/docs/lib/snippets/expressions.dart b/docs/lib/snippets/dart_api/expressions.dart similarity index 62% rename from docs/lib/snippets/expressions.dart rename to docs/lib/snippets/dart_api/expressions.dart index 20619403..604055cd 100644 --- a/docs/lib/snippets/expressions.dart +++ b/docs/lib/snippets/dart_api/expressions.dart @@ -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> 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 diff --git a/docs/lib/snippets/queries/json.dart b/docs/lib/snippets/dart_api/json.dart similarity index 100% rename from docs/lib/snippets/queries/json.dart rename to docs/lib/snippets/dart_api/json.dart diff --git a/docs/lib/snippets/queries.dart b/docs/lib/snippets/dart_api/select.dart similarity index 55% rename from docs/lib/snippets/queries.dart rename to docs/lib/snippets/dart_api/select.dart index c763c30b..937f9a09 100644 --- a/docs/lib/snippets/queries.dart +++ b/docs/lib/snippets/dart_api/select.dart @@ -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> limitTodos(int limit, {int? offset}) { + return (select(todoItems)..limit(limit, offset: offset)).get(); + } + // #enddocregion limit + + // #docregion order-by + Future> sortEntriesAlphabetically() { + return (select(todoItems) + ..orderBy([(t) => OrderingTerm(expression: t.title)])) + .get(); + } + // #enddocregion order-by + + // #docregion single + Stream entryById(int id) { + return (select(todoItems)..where((t) => t.id.equals(id))).watchSingle(); + } + // #enddocregion single + + // #docregion mapping + Stream> 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 pageOfTodos(int page, {int pageSize = 10}) { + return select(todoItems)..limit(pageSize, offset: page); + } + + // Exposes `getSingle` and `watchSingle` + SingleSelectable selectableEntryById(int id) { + return select(todoItems)..where((t) => t.id.equals(id)); + } + + // Exposes `getSingleOrNull` and `watchSingleOrNull` + SingleOrNullSelectable 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> 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> 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 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 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> 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 createCategoryForUnassignedTodoEntries() async { - final newDescription = Variable('category for: ') + todos.title; - final query = selectOnly(todos) - ..where(todos.category.isNull()) + final newDescription = Variable('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> 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> 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 } diff --git a/docs/lib/snippets/transactions.dart b/docs/lib/snippets/dart_api/transactions.dart similarity index 74% rename from docs/lib/snippets/transactions.dart rename to docs/lib/snippets/dart_api/transactions.dart index 31da3f57..ab0d1aa7 100644 --- a/docs/lib/snippets/transactions.dart +++ b/docs/lib/snippets/dart_api/transactions.dart @@ -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 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 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'); }); diff --git a/docs/lib/snippets/drift_files/custom_queries.dart b/docs/lib/snippets/drift_files/custom_queries.dart index 34925112..3af53333 100644 --- a/docs/lib/snippets/drift_files/custom_queries.dart +++ b/docs/lib/snippets/drift_files/custom_queries.dart @@ -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 diff --git a/docs/lib/snippets/drift_files/custom_queries.drift.dart b/docs/lib/snippets/drift_files/custom_queries.drift.dart new file mode 100644 index 00000000..6bcbe9e7 --- /dev/null +++ b/docs/lib/snippets/drift_files/custom_queries.drift.dart @@ -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 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('id'), + name: row.read('name'), + amount: row.read('amount'), + )); + } + + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List 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, + }); +} diff --git a/docs/lib/snippets/tables/advanced.dart b/docs/lib/snippets/tables/advanced.dart deleted file mode 100644 index 99ea50ef..00000000 --- a/docs/lib/snippets/tables/advanced.dart +++ /dev/null @@ -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> 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 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 diff --git a/docs/lib/snippets/tables/filename.dart b/docs/lib/snippets/tables/filename.dart deleted file mode 100644 index 355f4f85..00000000 --- a/docs/lib/snippets/tables/filename.dart +++ /dev/null @@ -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 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 diff --git a/docs/pages/docs/Advanced Features/builder_options.md b/docs/pages/docs/Advanced Features/builder_options.md index f5f87534..8b96eba5 100644 --- a/docs/pages/docs/Advanced Features/builder_options.md +++ b/docs/pages/docs/Advanced Features/builder_options.md @@ -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). diff --git a/docs/pages/docs/Advanced Features/expressions.md b/docs/pages/docs/Advanced Features/expressions.md deleted file mode 100644 index c1e43e53..00000000 --- a/docs/pages/docs/Advanced Features/expressions.md +++ /dev/null @@ -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> 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> 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 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 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> 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("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. diff --git a/docs/pages/docs/Advanced Features/joins.md b/docs/pages/docs/Advanced Features/joins.md deleted file mode 100644 index 10e31381..00000000 --- a/docs/pages/docs/Advanced Features/joins.md +++ /dev/null @@ -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`, 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> 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> 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> 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' %} diff --git a/docs/pages/docs/Advanced Features/schema_inspection.md b/docs/pages/docs/Advanced Features/schema_inspection.md index 555a3843..ecdd5e3d 100644 --- a/docs/pages/docs/Advanced Features/schema_inspection.md +++ b/docs/pages/docs/Advanced Features/schema_inspection.md @@ -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. diff --git a/docs/pages/docs/Dart API/daos.md b/docs/pages/docs/Dart API/daos.md index d20d9ce2..c3af6405 100644 --- a/docs/pages/docs/Dart API/daos.md +++ b/docs/pages/docs/Dart API/daos.md @@ -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 diff --git a/docs/pages/docs/Dart API/expressions.md b/docs/pages/docs/Dart API/expressions.md index 58e193fb..52bf1840 100644 --- a/docs/pages/docs/Dart API/expressions.md +++ b/docs/pages/docs/Dart API/expressions.md @@ -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> 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> 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 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 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> 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("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. diff --git a/docs/pages/docs/Dart API/select.md b/docs/pages/docs/Dart API/select.md index 85c0ade4..be276460 100644 --- a/docs/pages/docs/Dart API/select.md +++ b/docs/pages/docs/Dart API/select.md @@ -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> 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> 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`, 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> 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> 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' %} diff --git a/docs/pages/docs/Dart API/tables.md b/docs/pages/docs/Dart API/tables.md index 915acfce..f8820aa6 100644 --- a/docs/pages/docs/Dart API/tables.md +++ b/docs/pages/docs/Dart API/tables.md @@ -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, diff --git a/docs/pages/docs/Dart API/transactions.md b/docs/pages/docs/Dart API/transactions.md index c1650343..d6b6dd44 100644 --- a/docs/pages/docs/Dart API/transactions.md +++ b/docs/pages/docs/Dart API/transactions.md @@ -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 diff --git a/docs/pages/docs/Dart API/writes.md b/docs/pages/docs/Dart API/writes.md index 6379703b..51127b0a 100644 --- a/docs/pages/docs/Dart API/writes.md +++ b/docs/pages/docs/Dart API/writes.md @@ -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 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 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 get primaryKey => {email}; +} + +Future 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 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 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`. diff --git a/docs/pages/docs/Getting started/starting_with_sql.md b/docs/pages/docs/Getting started/starting_with_sql.md index d9dd5b27..9256184b 100644 --- a/docs/pages/docs/Getting started/starting_with_sql.md +++ b/docs/pages/docs/Getting started/starting_with_sql.md @@ -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. diff --git a/docs/pages/docs/Getting started/writing_queries.md b/docs/pages/docs/Getting started/writing_queries.md index 0fcc486a..75d20a73 100644 --- a/docs/pages/docs/Getting started/writing_queries.md +++ b/docs/pages/docs/Getting started/writing_queries.md @@ -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> 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> 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> 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> 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 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> 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 pageOfTodos(int page, {int pageSize = 10}) { - return select(todos)..limit(pageSize, offset: page); -} - -// Exposes `getSingle` and `watchSingle` -SingleSelectable entryById(int id) { - return select(todos)..where((t) => t.id.equals(id)); -} - -// Exposes `getSingleOrNull` and `watchSingleOrNull` -SingleOrNullSelectable 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 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 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 get primaryKey => {email}; -} - -Future 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 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 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`. diff --git a/docs/pages/docs/Using SQL/drift_files.md b/docs/pages/docs/Using SQL/drift_files.md index 744cfd17..23593f8f 100644 --- a/docs/pages/docs/Using SQL/drift_files.md +++ b/docs/pages/docs/Using SQL/drift_files.md @@ -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`