diff --git a/docs/README.md b/docs/README.md index c44fa3cef..38ba660e1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,6 +16,8 @@ dart run build_runner serve web:8080 --live-reload To build the website into a directory `out`, use: ``` -dart run drift_dev schema steps lib/snippets/migrations/exported_eschema/ lib/database/schema_versions.dart +dart run drift_dev schema steps lib/snippets/migrations/exported_eschema/ lib/snippets/migrations/schema_versions.dart +dart run drift_dev schema generate --data-classes --companions lib/snippets/migrations/exported_eschema/ lib/snippets/migrations/tests/generated_migrations/ + dart run build_runner build --release --output web:out ``` diff --git a/docs/build.yaml b/docs/build.yaml index 5b7b9234c..cef5ad347 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 @@ -120,6 +122,8 @@ targets: environment: "preview" build_web_compilers:entrypoint: generate_for: + include: + - "web/**" exclude: - "web/drift_worker.dart" release_options: diff --git a/docs/lib/snippets/_shared/todo_tables.dart b/docs/lib/snippets/_shared/todo_tables.dart new file mode 100644 index 000000000..695e3c31c --- /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 000000000..3e22c7473 --- /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/migrations/datetime_conversion.dart b/docs/lib/snippets/dart_api/datetime_conversion.dart similarity index 100% rename from docs/lib/snippets/migrations/datetime_conversion.dart rename to docs/lib/snippets/dart_api/datetime_conversion.dart 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 20619403c..604055cd6 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/dart_api/old_name.dart b/docs/lib/snippets/dart_api/old_name.dart new file mode 100644 index 000000000..a078ce274 --- /dev/null +++ b/docs/lib/snippets/dart_api/old_name.dart @@ -0,0 +1,5 @@ +import 'package:drift/drift.dart'; + +class EnabledCategories extends Table { + IntColumn get parentCategory => integer()(); +} 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 c763c30b1..937f9a09c 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/dart_api/tables.dart b/docs/lib/snippets/dart_api/tables.dart new file mode 100644 index 000000000..ba694d53a --- /dev/null +++ b/docs/lib/snippets/dart_api/tables.dart @@ -0,0 +1,98 @@ +import 'package:drift/drift.dart'; + +// #docregion nnbd +class Items extends Table { + IntColumn get category => integer().nullable()(); + // ... +} +// #enddocregion nnbd + +// #docregion names +@DataClassName('EnabledCategory') +class EnabledCategories extends Table { + @override + String get tableName => 'categories'; + + @JsonKey('parent_id') + IntColumn get parentCategory => integer().named('parent')(); +} +// #enddocregion names + +// #docregion references +class TodoItems extends Table { + // ... + IntColumn get category => + integer().nullable().references(TodoCategories, #id)(); +} + +@DataClassName("Category") +class TodoCategories extends Table { + IntColumn get id => integer().autoIncrement()(); + // and more columns... +} +// #enddocregion references + +// #docregion unique-column +class TableWithUniqueColumn extends Table { + IntColumn get unique => integer().unique()(); +} +// #enddocregion unique-column + +// #docregion primary-key +class GroupMemberships extends Table { + IntColumn get group => integer()(); + IntColumn get user => integer()(); + + @override + Set get primaryKey => {group, user}; +} +// #enddocregion primary-key + +// #docregion unique-table +class IngredientInRecipes extends Table { + @override + List> get uniqueKeys => [ + {recipe, ingredient}, + {recipe, amountInGrams} + ]; + + IntColumn get recipe => integer()(); + IntColumn get ingredient => integer()(); + + IntColumn get amountInGrams => integer().named('amount')(); +} +// #enddocregion unique-table + +// #docregion custom-constraint-table +class TableWithCustomConstraints extends Table { + IntColumn get foo => integer()(); + IntColumn get bar => integer()(); + + @override + List get customConstraints => [ + 'FOREIGN KEY (foo, bar) REFERENCES group_memberships ("group", user)', + ]; +} +// #enddocregion custom-constraint-table + +// #docregion index +@TableIndex(name: 'user_name', columns: {#name}) +class Users extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get name => text()(); +} +// #enddocregion index + +// #docregion custom-type +typedef Category = ({int id, String name}); + +@UseRowClass(Category) +class Categories extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get name => text()(); + // #enddocregion custom-type + @override + String get tableName => 'categories2'; + // #docregion custom-type +} +// #enddocregion custom-type 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 31da3f578..ab0d1aa70 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/tables/advanced.dart b/docs/lib/snippets/dart_api/views.dart similarity index 63% rename from docs/lib/snippets/tables/advanced.dart rename to docs/lib/snippets/dart_api/views.dart index 99ea50ef1..720bb8198 100644 --- a/docs/lib/snippets/tables/advanced.dart +++ b/docs/lib/snippets/dart_api/views.dart @@ -1,22 +1,17 @@ import 'package:drift/drift.dart'; -import 'filename.dart'; - -// #docregion unique -class WithUniqueConstraints extends Table { - IntColumn get a => integer().unique()(); - - IntColumn get b => integer()(); - IntColumn get c => integer()(); - - @override - List> get uniqueKeys => [ - {b, c} - ]; +class Todos extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get title => text().withLength(min: 6, max: 32)(); + TextColumn get content => text().named('body')(); + IntColumn get category => integer().nullable()(); +} - // Effectively, this table has two unique key sets: (a) and (b, c). +@DataClassName('Category') +class Categories extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get description => text()(); } -// #enddocregion unique // #docregion view abstract class CategoryTodoCount extends View { diff --git a/docs/lib/snippets/drift_files/custom_queries.dart b/docs/lib/snippets/drift_files/custom_queries.dart index 34925112d..3af533338 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 000000000..6bcbe9e70 --- /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/migrations/migrations.dart b/docs/lib/snippets/migrations/migrations.dart index 062e7b740..49b432591 100644 --- a/docs/lib/snippets/migrations/migrations.dart +++ b/docs/lib/snippets/migrations/migrations.dart @@ -1,13 +1,5 @@ -import 'dart:math' as math; - import 'package:drift/drift.dart'; -// #docregion stepbystep -// This file was generated by `drift_dev schema steps drift_schemas lib/database/schema_versions.dart` -import 'schema_versions.dart'; - -// #enddocregion stepbystep - part 'migrations.g.dart'; const kDebugMode = false; @@ -25,8 +17,8 @@ class Todos extends Table { // #enddocregion table @DriftDatabase(tables: [Todos]) -class Example extends _$Example { - Example(QueryExecutor e) : super(e); +class MyDatabase extends _$MyDatabase { + MyDatabase(QueryExecutor e) : super(e); // #docregion start @override @@ -99,121 +91,3 @@ class Example extends _$Example { // #enddocregion change_type } } - -class StepByStep { - // #docregion stepbystep - MigrationStrategy get migration { - return MigrationStrategy( - onCreate: (Migrator m) async { - await m.createAll(); - }, - onUpgrade: stepByStep( - from1To2: (m, schema) async { - // we added the dueDate property in the change from version 1 to - // version 2 - await m.addColumn(schema.todos, schema.todos.dueDate); - }, - from2To3: (m, schema) async { - // we added the priority property in the change from version 1 or 2 - // to version 3 - await m.addColumn(schema.todos, schema.todos.priority); - }, - ), - ); - } - // #enddocregion stepbystep -} - -extension StepByStep2 on GeneratedDatabase { - MigrationStrategy get migration { - return MigrationStrategy( - onCreate: (Migrator m) async { - await m.createAll(); - }, - // #docregion stepbystep2 - onUpgrade: (m, from, to) async { - // Run migration steps without foreign keys and re-enable them later - // (https://drift.simonbinder.eu/docs/advanced-features/migrations/#tips) - await customStatement('PRAGMA foreign_keys = OFF'); - - await m.runMigrationSteps( - from: from, - to: to, - steps: migrationSteps( - from1To2: (m, schema) async { - // we added the dueDate property in the change from version 1 to - // version 2 - await m.addColumn(schema.todos, schema.todos.dueDate); - }, - from2To3: (m, schema) async { - // we added the priority property in the change from version 1 or 2 - // to version 3 - await m.addColumn(schema.todos, schema.todos.priority); - }, - ), - ); - - if (kDebugMode) { - // Fail if the migration broke foreign keys - final wrongForeignKeys = - await customSelect('PRAGMA foreign_key_check').get(); - assert(wrongForeignKeys.isEmpty, - '${wrongForeignKeys.map((e) => e.data)}'); - } - - await customStatement('PRAGMA foreign_keys = ON;'); - }, - // #enddocregion stepbystep2 - ); - } -} - -extension StepByStep3 on Example { - MigrationStrategy get migration { - return MigrationStrategy( - onCreate: (Migrator m) async { - await m.createAll(); - }, - // #docregion stepbystep3 - onUpgrade: (m, from, to) async { - // Run migration steps without foreign keys and re-enable them later - // (https://drift.simonbinder.eu/docs/advanced-features/migrations/#tips) - await customStatement('PRAGMA foreign_keys = OFF'); - - // Manually running migrations up to schema version 2, after which we've - // enabled step-by-step migrations. - if (from < 2) { - // we added the dueDate property in the change from version 1 to - // version 2 - before switching to step-by-step migrations. - await m.addColumn(todos, todos.dueDate); - } - - // At this point, we should be migrated to schema 3. For future schema - // changes, we will "start" at schema 3. - await m.runMigrationSteps( - from: math.max(2, from), - to: to, - // ignore: missing_required_argument - steps: migrationSteps( - from2To3: (m, schema) async { - // we added the priority property in the change from version 1 or - // 2 to version 3 - await m.addColumn(schema.todos, schema.todos.priority); - }, - ), - ); - - if (kDebugMode) { - // Fail if the migration broke foreign keys - final wrongForeignKeys = - await customSelect('PRAGMA foreign_key_check').get(); - assert(wrongForeignKeys.isEmpty, - '${wrongForeignKeys.map((e) => e.data)}'); - } - - await customStatement('PRAGMA foreign_keys = ON;'); - }, - // #enddocregion stepbystep3 - ); - } -} diff --git a/docs/lib/snippets/migrations/step_by_step.dart b/docs/lib/snippets/migrations/step_by_step.dart new file mode 100644 index 000000000..7c569a8cd --- /dev/null +++ b/docs/lib/snippets/migrations/step_by_step.dart @@ -0,0 +1,129 @@ +import 'dart:math' as math; + +import 'package:drift/drift.dart'; + +import 'migrations.dart'; + +// #docregion stepbystep +// This file was generated by `drift_dev schema steps drift_schemas/ lib/database/schema_versions.dart` +import 'schema_versions.dart'; + +// #enddocregion stepbystep + +class StepByStep { + // #docregion stepbystep + MigrationStrategy get migration { + return MigrationStrategy( + onCreate: (Migrator m) async { + await m.createAll(); + }, + onUpgrade: stepByStep( + from1To2: (m, schema) async { + // we added the dueDate property in the change from version 1 to + // version 2 + await m.addColumn(schema.todos, schema.todos.dueDate); + }, + from2To3: (m, schema) async { + // we added the priority property in the change from version 1 or 2 + // to version 3 + await m.addColumn(schema.todos, schema.todos.priority); + }, + ), + ); + } + // #enddocregion stepbystep +} + +extension StepByStep2 on GeneratedDatabase { + MigrationStrategy get migration { + return MigrationStrategy( + onCreate: (Migrator m) async { + await m.createAll(); + }, + // #docregion stepbystep2 + onUpgrade: (m, from, to) async { + // Run migration steps without foreign keys and re-enable them later + // (https://drift.simonbinder.eu/docs/advanced-features/migrations/#tips) + await customStatement('PRAGMA foreign_keys = OFF'); + + await m.runMigrationSteps( + from: from, + to: to, + steps: migrationSteps( + from1To2: (m, schema) async { + // we added the dueDate property in the change from version 1 to + // version 2 + await m.addColumn(schema.todos, schema.todos.dueDate); + }, + from2To3: (m, schema) async { + // we added the priority property in the change from version 1 or 2 + // to version 3 + await m.addColumn(schema.todos, schema.todos.priority); + }, + ), + ); + + if (kDebugMode) { + // Fail if the migration broke foreign keys + final wrongForeignKeys = + await customSelect('PRAGMA foreign_key_check').get(); + assert(wrongForeignKeys.isEmpty, + '${wrongForeignKeys.map((e) => e.data)}'); + } + + await customStatement('PRAGMA foreign_keys = ON;'); + }, + // #enddocregion stepbystep2 + ); + } +} + +extension StepByStep3 on MyDatabase { + MigrationStrategy get migration { + return MigrationStrategy( + onCreate: (Migrator m) async { + await m.createAll(); + }, + // #docregion stepbystep3 + onUpgrade: (m, from, to) async { + // Run migration steps without foreign keys and re-enable them later + // (https://drift.simonbinder.eu/docs/advanced-features/migrations/#tips) + await customStatement('PRAGMA foreign_keys = OFF'); + + // Manually running migrations up to schema version 2, after which we've + // enabled step-by-step migrations. + if (from < 2) { + // we added the dueDate property in the change from version 1 to + // version 2 - before switching to step-by-step migrations. + await m.addColumn(todos, todos.dueDate); + } + + // At this point, we should be migrated to schema 3. For future schema + // changes, we will "start" at schema 3. + await m.runMigrationSteps( + from: math.max(2, from), + to: to, + // ignore: missing_required_argument + steps: migrationSteps( + from2To3: (m, schema) async { + // we added the priority property in the change from version 1 or + // 2 to version 3 + await m.addColumn(schema.todos, schema.todos.priority); + }, + ), + ); + + if (kDebugMode) { + // Fail if the migration broke foreign keys + final wrongForeignKeys = + await customSelect('PRAGMA foreign_key_check').get(); + assert(wrongForeignKeys.isEmpty, + '${wrongForeignKeys.map((e) => e.data)}'); + } + + await customStatement('PRAGMA foreign_keys = ON;'); + }, + // #enddocregion stepbystep3 + ); + } +} diff --git a/docs/lib/snippets/migrations/tests/generated_migrations/schema.dart b/docs/lib/snippets/migrations/tests/generated_migrations/schema.dart new file mode 100644 index 000000000..1c9347e90 --- /dev/null +++ b/docs/lib/snippets/migrations/tests/generated_migrations/schema.dart @@ -0,0 +1,24 @@ +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +//@dart=2.12 +import 'package:drift/drift.dart'; +import 'package:drift/internal/migrations.dart'; +import 'schema_v1.dart' as v1; +import 'schema_v2.dart' as v2; +import 'schema_v3.dart' as v3; + +class GeneratedHelper implements SchemaInstantiationHelper { + @override + GeneratedDatabase databaseForVersion(QueryExecutor db, int version) { + switch (version) { + case 1: + return v1.DatabaseAtV1(db); + case 2: + return v2.DatabaseAtV2(db); + case 3: + return v3.DatabaseAtV3(db); + default: + throw MissingSchemaException(version, const {1, 2, 3}); + } + } +} diff --git a/docs/lib/snippets/migrations/tests/generated_migrations/schema_v1.dart b/docs/lib/snippets/migrations/tests/generated_migrations/schema_v1.dart new file mode 100644 index 000000000..9c383740f --- /dev/null +++ b/docs/lib/snippets/migrations/tests/generated_migrations/schema_v1.dart @@ -0,0 +1,232 @@ +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +//@dart=2.12 +import 'package:drift/drift.dart'; + +class Todos extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Todos(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn title = GeneratedColumn( + 'title', aliasedName, false, + additionalChecks: + GeneratedColumn.checkTextLength(minTextLength: 6, maxTextLength: 10), + type: DriftSqlType.string, + requiredDuringInsert: true); + late final GeneratedColumn content = GeneratedColumn( + 'body', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn category = GeneratedColumn( + 'category', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + @override + List get $columns => [id, title, content, category]; + @override + String get aliasedName => _alias ?? 'todos'; + @override + String get actualTableName => 'todos'; + @override + Set get $primaryKey => {id}; + @override + TodosData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TodosData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + title: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}title'])!, + content: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}body'])!, + category: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}category']), + ); + } + + @override + Todos createAlias(String alias) { + return Todos(attachedDatabase, alias); + } +} + +class TodosData extends DataClass implements Insertable { + final int id; + final String title; + final String content; + final int? category; + const TodosData( + {required this.id, + required this.title, + required this.content, + this.category}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['title'] = Variable(title); + map['body'] = Variable(content); + if (!nullToAbsent || category != null) { + map['category'] = Variable(category); + } + return map; + } + + TodosCompanion toCompanion(bool nullToAbsent) { + return TodosCompanion( + id: Value(id), + title: Value(title), + content: Value(content), + category: category == null && nullToAbsent + ? const Value.absent() + : Value(category), + ); + } + + factory TodosData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TodosData( + id: serializer.fromJson(json['id']), + title: serializer.fromJson(json['title']), + content: serializer.fromJson(json['content']), + category: serializer.fromJson(json['category']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'title': serializer.toJson(title), + 'content': serializer.toJson(content), + 'category': serializer.toJson(category), + }; + } + + TodosData copyWith( + {int? id, + String? title, + String? content, + Value category = const Value.absent()}) => + TodosData( + id: id ?? this.id, + title: title ?? this.title, + content: content ?? this.content, + category: category.present ? category.value : this.category, + ); + @override + String toString() { + return (StringBuffer('TodosData(') + ..write('id: $id, ') + ..write('title: $title, ') + ..write('content: $content, ') + ..write('category: $category') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, title, content, category); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TodosData && + other.id == this.id && + other.title == this.title && + other.content == this.content && + other.category == this.category); +} + +class TodosCompanion extends UpdateCompanion { + final Value id; + final Value title; + final Value content; + final Value category; + const TodosCompanion({ + this.id = const Value.absent(), + this.title = const Value.absent(), + this.content = const Value.absent(), + this.category = const Value.absent(), + }); + TodosCompanion.insert({ + this.id = const Value.absent(), + required String title, + required String content, + this.category = const Value.absent(), + }) : title = Value(title), + content = Value(content); + static Insertable custom({ + Expression? id, + Expression? title, + Expression? content, + Expression? category, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (title != null) 'title': title, + if (content != null) 'body': content, + if (category != null) 'category': category, + }); + } + + TodosCompanion copyWith( + {Value? id, + Value? title, + Value? content, + Value? category}) { + return TodosCompanion( + 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'] = Variable(id.value); + } + if (title.present) { + map['title'] = Variable(title.value); + } + if (content.present) { + map['body'] = Variable(content.value); + } + if (category.present) { + map['category'] = Variable(category.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TodosCompanion(') + ..write('id: $id, ') + ..write('title: $title, ') + ..write('content: $content, ') + ..write('category: $category') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV1 extends GeneratedDatabase { + DatabaseAtV1(QueryExecutor e) : super(e); + late final Todos todos = Todos(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [todos]; + @override + int get schemaVersion => 1; +} diff --git a/docs/lib/snippets/migrations/tests/generated_migrations/schema_v2.dart b/docs/lib/snippets/migrations/tests/generated_migrations/schema_v2.dart new file mode 100644 index 000000000..b4a27a3d2 --- /dev/null +++ b/docs/lib/snippets/migrations/tests/generated_migrations/schema_v2.dart @@ -0,0 +1,262 @@ +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +//@dart=2.12 +import 'package:drift/drift.dart'; + +class Todos extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Todos(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn title = GeneratedColumn( + 'title', aliasedName, false, + additionalChecks: + GeneratedColumn.checkTextLength(minTextLength: 6, maxTextLength: 10), + type: DriftSqlType.string, + requiredDuringInsert: true); + late final GeneratedColumn content = GeneratedColumn( + 'body', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn category = GeneratedColumn( + 'category', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn dueDate = GeneratedColumn( + 'due_date', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + @override + List get $columns => [id, title, content, category, dueDate]; + @override + String get aliasedName => _alias ?? 'todos'; + @override + String get actualTableName => 'todos'; + @override + Set get $primaryKey => {id}; + @override + TodosData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TodosData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + title: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}title'])!, + content: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}body'])!, + category: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}category']), + dueDate: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}due_date']), + ); + } + + @override + Todos createAlias(String alias) { + return Todos(attachedDatabase, alias); + } +} + +class TodosData extends DataClass implements Insertable { + final int id; + final String title; + final String content; + final int? category; + final DateTime? dueDate; + const TodosData( + {required this.id, + required this.title, + required this.content, + this.category, + this.dueDate}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['title'] = Variable(title); + map['body'] = Variable(content); + if (!nullToAbsent || category != null) { + map['category'] = Variable(category); + } + if (!nullToAbsent || dueDate != null) { + map['due_date'] = Variable(dueDate); + } + return map; + } + + TodosCompanion toCompanion(bool nullToAbsent) { + return TodosCompanion( + id: Value(id), + title: Value(title), + content: Value(content), + category: category == null && nullToAbsent + ? const Value.absent() + : Value(category), + dueDate: dueDate == null && nullToAbsent + ? const Value.absent() + : Value(dueDate), + ); + } + + factory TodosData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TodosData( + id: serializer.fromJson(json['id']), + title: serializer.fromJson(json['title']), + content: serializer.fromJson(json['content']), + category: serializer.fromJson(json['category']), + dueDate: serializer.fromJson(json['dueDate']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'title': serializer.toJson(title), + 'content': serializer.toJson(content), + 'category': serializer.toJson(category), + 'dueDate': serializer.toJson(dueDate), + }; + } + + TodosData copyWith( + {int? id, + String? title, + String? content, + Value category = const Value.absent(), + Value dueDate = const Value.absent()}) => + TodosData( + id: id ?? this.id, + title: title ?? this.title, + content: content ?? this.content, + category: category.present ? category.value : this.category, + dueDate: dueDate.present ? dueDate.value : this.dueDate, + ); + @override + String toString() { + return (StringBuffer('TodosData(') + ..write('id: $id, ') + ..write('title: $title, ') + ..write('content: $content, ') + ..write('category: $category, ') + ..write('dueDate: $dueDate') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, title, content, category, dueDate); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TodosData && + other.id == this.id && + other.title == this.title && + other.content == this.content && + other.category == this.category && + other.dueDate == this.dueDate); +} + +class TodosCompanion extends UpdateCompanion { + final Value id; + final Value title; + final Value content; + final Value category; + final Value dueDate; + const TodosCompanion({ + this.id = const Value.absent(), + this.title = const Value.absent(), + this.content = const Value.absent(), + this.category = const Value.absent(), + this.dueDate = const Value.absent(), + }); + TodosCompanion.insert({ + this.id = const Value.absent(), + required String title, + required String content, + this.category = const Value.absent(), + this.dueDate = const Value.absent(), + }) : title = Value(title), + content = Value(content); + static Insertable custom({ + Expression? id, + Expression? title, + Expression? content, + Expression? category, + Expression? dueDate, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (title != null) 'title': title, + if (content != null) 'body': content, + if (category != null) 'category': category, + if (dueDate != null) 'due_date': dueDate, + }); + } + + TodosCompanion copyWith( + {Value? id, + Value? title, + Value? content, + Value? category, + Value? dueDate}) { + return TodosCompanion( + id: id ?? this.id, + title: title ?? this.title, + content: content ?? this.content, + category: category ?? this.category, + dueDate: dueDate ?? this.dueDate, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (title.present) { + map['title'] = Variable(title.value); + } + if (content.present) { + map['body'] = Variable(content.value); + } + if (category.present) { + map['category'] = Variable(category.value); + } + if (dueDate.present) { + map['due_date'] = Variable(dueDate.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TodosCompanion(') + ..write('id: $id, ') + ..write('title: $title, ') + ..write('content: $content, ') + ..write('category: $category, ') + ..write('dueDate: $dueDate') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV2 extends GeneratedDatabase { + DatabaseAtV2(QueryExecutor e) : super(e); + late final Todos todos = Todos(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [todos]; + @override + int get schemaVersion => 2; +} diff --git a/docs/lib/snippets/migrations/tests/generated_migrations/schema_v3.dart b/docs/lib/snippets/migrations/tests/generated_migrations/schema_v3.dart new file mode 100644 index 000000000..0aab5157d --- /dev/null +++ b/docs/lib/snippets/migrations/tests/generated_migrations/schema_v3.dart @@ -0,0 +1,294 @@ +// GENERATED CODE, DO NOT EDIT BY HAND. +// ignore_for_file: type=lint +//@dart=2.12 +import 'package:drift/drift.dart'; + +class Todos extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Todos(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumn title = GeneratedColumn( + 'title', aliasedName, false, + additionalChecks: + GeneratedColumn.checkTextLength(minTextLength: 6, maxTextLength: 10), + type: DriftSqlType.string, + requiredDuringInsert: true); + late final GeneratedColumn content = GeneratedColumn( + 'body', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + late final GeneratedColumn category = GeneratedColumn( + 'category', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + late final GeneratedColumn dueDate = GeneratedColumn( + 'due_date', aliasedName, true, + type: DriftSqlType.dateTime, requiredDuringInsert: false); + late final GeneratedColumn priority = GeneratedColumn( + 'priority', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + @override + List get $columns => + [id, title, content, category, dueDate, priority]; + @override + String get aliasedName => _alias ?? 'todos'; + @override + String get actualTableName => 'todos'; + @override + Set get $primaryKey => {id}; + @override + TodosData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TodosData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + title: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}title'])!, + content: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}body'])!, + category: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}category']), + dueDate: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}due_date']), + priority: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}priority']), + ); + } + + @override + Todos createAlias(String alias) { + return Todos(attachedDatabase, alias); + } +} + +class TodosData extends DataClass implements Insertable { + final int id; + final String title; + final String content; + final int? category; + final DateTime? dueDate; + final int? priority; + const TodosData( + {required this.id, + required this.title, + required this.content, + this.category, + this.dueDate, + this.priority}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['title'] = Variable(title); + map['body'] = Variable(content); + if (!nullToAbsent || category != null) { + map['category'] = Variable(category); + } + if (!nullToAbsent || dueDate != null) { + map['due_date'] = Variable(dueDate); + } + if (!nullToAbsent || priority != null) { + map['priority'] = Variable(priority); + } + return map; + } + + TodosCompanion toCompanion(bool nullToAbsent) { + return TodosCompanion( + id: Value(id), + title: Value(title), + content: Value(content), + category: category == null && nullToAbsent + ? const Value.absent() + : Value(category), + dueDate: dueDate == null && nullToAbsent + ? const Value.absent() + : Value(dueDate), + priority: priority == null && nullToAbsent + ? const Value.absent() + : Value(priority), + ); + } + + factory TodosData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TodosData( + id: serializer.fromJson(json['id']), + title: serializer.fromJson(json['title']), + content: serializer.fromJson(json['content']), + category: serializer.fromJson(json['category']), + dueDate: serializer.fromJson(json['dueDate']), + priority: serializer.fromJson(json['priority']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'title': serializer.toJson(title), + 'content': serializer.toJson(content), + 'category': serializer.toJson(category), + 'dueDate': serializer.toJson(dueDate), + 'priority': serializer.toJson(priority), + }; + } + + TodosData copyWith( + {int? id, + String? title, + String? content, + Value category = const Value.absent(), + Value dueDate = const Value.absent(), + Value priority = const Value.absent()}) => + TodosData( + id: id ?? this.id, + title: title ?? this.title, + content: content ?? this.content, + category: category.present ? category.value : this.category, + dueDate: dueDate.present ? dueDate.value : this.dueDate, + priority: priority.present ? priority.value : this.priority, + ); + @override + String toString() { + return (StringBuffer('TodosData(') + ..write('id: $id, ') + ..write('title: $title, ') + ..write('content: $content, ') + ..write('category: $category, ') + ..write('dueDate: $dueDate, ') + ..write('priority: $priority') + ..write(')')) + .toString(); + } + + @override + int get hashCode => + Object.hash(id, title, content, category, dueDate, priority); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TodosData && + other.id == this.id && + other.title == this.title && + other.content == this.content && + other.category == this.category && + other.dueDate == this.dueDate && + other.priority == this.priority); +} + +class TodosCompanion extends UpdateCompanion { + final Value id; + final Value title; + final Value content; + final Value category; + final Value dueDate; + final Value priority; + const TodosCompanion({ + this.id = const Value.absent(), + this.title = const Value.absent(), + this.content = const Value.absent(), + this.category = const Value.absent(), + this.dueDate = const Value.absent(), + this.priority = const Value.absent(), + }); + TodosCompanion.insert({ + this.id = const Value.absent(), + required String title, + required String content, + this.category = const Value.absent(), + this.dueDate = const Value.absent(), + this.priority = const Value.absent(), + }) : title = Value(title), + content = Value(content); + static Insertable custom({ + Expression? id, + Expression? title, + Expression? content, + Expression? category, + Expression? dueDate, + Expression? priority, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (title != null) 'title': title, + if (content != null) 'body': content, + if (category != null) 'category': category, + if (dueDate != null) 'due_date': dueDate, + if (priority != null) 'priority': priority, + }); + } + + TodosCompanion copyWith( + {Value? id, + Value? title, + Value? content, + Value? category, + Value? dueDate, + Value? priority}) { + return TodosCompanion( + id: id ?? this.id, + title: title ?? this.title, + content: content ?? this.content, + category: category ?? this.category, + dueDate: dueDate ?? this.dueDate, + priority: priority ?? this.priority, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (title.present) { + map['title'] = Variable(title.value); + } + if (content.present) { + map['body'] = Variable(content.value); + } + if (category.present) { + map['category'] = Variable(category.value); + } + if (dueDate.present) { + map['due_date'] = Variable(dueDate.value); + } + if (priority.present) { + map['priority'] = Variable(priority.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TodosCompanion(') + ..write('id: $id, ') + ..write('title: $title, ') + ..write('content: $content, ') + ..write('category: $category, ') + ..write('dueDate: $dueDate, ') + ..write('priority: $priority') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV3 extends GeneratedDatabase { + DatabaseAtV3(QueryExecutor e) : super(e); + late final Todos todos = Todos(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [todos]; + @override + int get schemaVersion => 3; +} diff --git a/docs/lib/snippets/migrations/tests/schema_test.dart b/docs/lib/snippets/migrations/tests/schema_test.dart new file mode 100644 index 000000000..81fb9bbd1 --- /dev/null +++ b/docs/lib/snippets/migrations/tests/schema_test.dart @@ -0,0 +1,32 @@ +// #docregion setup +import 'package:test/test.dart'; +import 'package:drift_dev/api/migrations.dart'; + +// The generated directory from before. +import 'generated_migrations/schema.dart'; + +// #enddocregion setup +import '../migrations.dart'; +// #docregion setup + +void main() { + late SchemaVerifier verifier; + + setUpAll(() { + // GeneratedHelper() was generated by drift, the verifier is an api + // provided by drift_dev. + verifier = SchemaVerifier(GeneratedHelper()); + }); + + test('upgrade from v1 to v2', () async { + // Use startAt(1) to obtain a database connection with all tables + // from the v1 schema. + final connection = await verifier.startAt(1); + final db = MyDatabase(connection); + + // Use this to run a migration to v2 and then validate that the + // database has the expected schema. + await verifier.migrateAndValidate(db, 2); + }); +} +// #enddocregion setup diff --git a/docs/lib/snippets/migrations/tests/verify_data_integrity_test.dart b/docs/lib/snippets/migrations/tests/verify_data_integrity_test.dart new file mode 100644 index 000000000..14cf21694 --- /dev/null +++ b/docs/lib/snippets/migrations/tests/verify_data_integrity_test.dart @@ -0,0 +1,49 @@ +import 'package:test/test.dart'; +import 'package:drift_dev/api/migrations.dart'; + +import '../migrations.dart'; +import 'generated_migrations/schema.dart'; + +// #docregion imports +import 'generated_migrations/schema_v1.dart' as v1; +import 'generated_migrations/schema_v2.dart' as v2; +// #enddocregion imports + +// #docregion main +void main() { +// #enddocregion main + late SchemaVerifier verifier; + + setUpAll(() { + // GeneratedHelper() was generated by drift, the verifier is an api + // provided by drift_dev. + verifier = SchemaVerifier(GeneratedHelper()); + }); + +// #docregion main + // ... + test('upgrade from v1 to v2', () async { + final schema = await verifier.schemaAt(1); + + // Add some data to the table being migrated + final oldDb = v1.DatabaseAtV1(schema.newConnection()); + await oldDb.into(oldDb.todos).insert(v1.TodosCompanion.insert( + title: 'my first todo entry', + content: 'should still be there after the migration', + )); + await oldDb.close(); + + // Run the migration and verify that it adds the name column. + final db = MyDatabase(schema.newConnection()); + await verifier.migrateAndValidate(db, 2); + await db.close(); + + // Make sure the entry is still here + final migratedDb = v2.DatabaseAtV2(schema.newConnection()); + final entry = await migratedDb.select(migratedDb.todos).getSingle(); + expect(entry.id, 1); + expect(entry.dueDate, isNull); // default from the migration + await migratedDb.close(); + }); +} +// #enddocregion main diff --git a/docs/lib/snippets/encryption.dart b/docs/lib/snippets/platforms/encryption.dart similarity index 87% rename from docs/lib/snippets/encryption.dart rename to docs/lib/snippets/platforms/encryption.dart index 9c3521783..bc117470f 100644 --- a/docs/lib/snippets/encryption.dart +++ b/docs/lib/snippets/platforms/encryption.dart @@ -23,8 +23,9 @@ void databases() { final myDatabaseFile = File('/dev/null'); // #docregion encrypted1 - NativeDatabase( + NativeDatabase.createInBackground( myDatabaseFile, + isolateSetup: setupSqlCipher, setup: (rawDb) { rawDb.execute("PRAGMA key = 'passphrase';"); }, @@ -32,8 +33,9 @@ void databases() { // #enddocregion encrypted1 // #docregion encrypted2 - NativeDatabase( + NativeDatabase.createInBackground( myDatabaseFile, + isolateSetup: setupSqlCipher, setup: (rawDb) { assert(_debugCheckHasCipher(rawDb)); rawDb.execute("PRAGMA key = 'passphrase';"); diff --git a/docs/lib/snippets/engines/new_connect.dart b/docs/lib/snippets/platforms/new_connect.dart similarity index 100% rename from docs/lib/snippets/engines/new_connect.dart rename to docs/lib/snippets/platforms/new_connect.dart diff --git a/docs/lib/snippets/platforms.dart b/docs/lib/snippets/platforms/platforms.dart similarity index 100% rename from docs/lib/snippets/platforms.dart rename to docs/lib/snippets/platforms/platforms.dart diff --git a/docs/lib/snippets/engines/stable_worker.dart b/docs/lib/snippets/platforms/stable_worker.dart similarity index 100% rename from docs/lib/snippets/engines/stable_worker.dart rename to docs/lib/snippets/platforms/stable_worker.dart diff --git a/docs/lib/snippets/engines/web.dart b/docs/lib/snippets/platforms/web.dart similarity index 100% rename from docs/lib/snippets/engines/web.dart rename to docs/lib/snippets/platforms/web.dart diff --git a/docs/lib/snippets/engines/workers.dart b/docs/lib/snippets/platforms/workers.dart similarity index 100% rename from docs/lib/snippets/engines/workers.dart rename to docs/lib/snippets/platforms/workers.dart diff --git a/docs/lib/snippets/setup/database.dart b/docs/lib/snippets/setup/database.dart new file mode 100644 index 000000000..03908ae15 --- /dev/null +++ b/docs/lib/snippets/setup/database.dart @@ -0,0 +1,73 @@ +// #docregion before_generation +import 'package:drift/drift.dart'; + +// #enddocregion before_generation + +// #docregion open +// These imports are necessary to open the sqlite3 database +import 'dart:io'; + +import 'package:drift/native.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as p; + +// ... the TodoItems table definition stays the same +// #enddocregion open + +// #docregion before_generation +part 'database.g.dart'; + +// #docregion table +class TodoItems extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get title => text().withLength(min: 6, max: 32)(); + TextColumn get content => text().named('body')(); + IntColumn get category => integer().nullable()(); +} +// #enddocregion table +// #docregion open + +@DriftDatabase(tables: [TodoItems]) +class AppDatabase extends _$AppDatabase { +// #enddocregion before_generation + AppDatabase() : super(_openConnection()); + + @override + int get schemaVersion => 1; +// #docregion before_generation +} +// #enddocregion before_generation, open + +// #docregion open + +LazyDatabase _openConnection() { + // the LazyDatabase util lets us find the right location for the file async. + return LazyDatabase(() async { + // put the database file, called db.sqlite here, into the documents folder + // for your app. + final dbFolder = await getApplicationDocumentsDirectory(); + final file = File(p.join(dbFolder.path, 'db.sqlite')); + return NativeDatabase.createInBackground(file); + }); +} +// #enddocregion open + +class WidgetsFlutterBinding { + static void ensureInitialized() {} +} + +// #docregion use +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + final database = AppDatabase(); + + await database.into(database.todoItems).insert(TodoItemsCompanion.insert( + title: 'todo: finish drift setup', + content: 'We can now write queries and define our own tables.', + )); + List allItems = await database.select(database.todoItems).get(); + + print('items in database: $allItems'); +} +// #enddocregion use diff --git a/docs/lib/snippets/tables/filename.dart b/docs/lib/snippets/tables/filename.dart deleted file mode 100644 index 355f4f85e..000000000 --- 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/index.md b/docs/pages/docs/Advanced Features/index.md deleted file mode 100644 index b4c045e6b..000000000 --- a/docs/pages/docs/Advanced Features/index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -data: - title: Advanced Features - weight: 20 - description: Learn about some advanced features of drift -template: layouts/docs/list ---- diff --git a/docs/pages/docs/Advanced Features/migrations.md b/docs/pages/docs/Advanced Features/migrations.md deleted file mode 100644 index 07bc0767e..000000000 --- a/docs/pages/docs/Advanced Features/migrations.md +++ /dev/null @@ -1,528 +0,0 @@ ---- -data: - title: "Migrations" - weight: 10 - description: Define what happens when your database gets created or updated -aliases: - - /migrations -template: layouts/docs/single ---- - -As your app grows, you may want to change the table structure for your drift database: -New features need new columns or tables, and outdated columns may have to be altered or -removed altogether. -When making changes to your database schema, you need to write migrations enabling users with -an old version of your app to convert to the database expected by the latest version. -With incorrect migrations, your database ends up in an inconsistent state which can cause crashes -or data loss. This is why drift provides dedicated test tools and APIs to make writing migrations -easy and safe. - -{% assign snippets = 'package:drift_docs/snippets/migrations/migrations.dart.excerpt.json' | readString | json_decode %} - -## Manual setup {#basics} - -Drift provides a migration API that can be used to gradually apply schema changes after bumping -the `schemaVersion` getter inside the `Database` class. To use it, override the `migration` -getter. - -Here's an example: Let's say you wanted to add a due date to your todo entries (`v2` of the schema). -Later, you decide to also add a priority column (`v3` of the schema). - -{% include "blocks/snippet" snippets = snippets name = 'table' %} - -We can now change the `database` class like this: - -{% include "blocks/snippet" snippets = snippets name = 'start' %} - -You can also add individual tables or drop them - see the reference of [Migrator](https://pub.dev/documentation/drift/latest/drift/Migrator-class.html) -for all the available options. - -You can also use higher-level query APIs like `select`, `update` or `delete` inside a migration callback. -However, be aware that drift expects the latest schema when creating SQL statements or mapping results. -For instance, when adding a new column to your database, you shouldn't run a `select` on that table before -you've actually added the column. In general, try to avoid running queries in migration callbacks if possible. - -`sqlite` can feel a bit limiting when it comes to migrations - there only are methods to create tables and columns. -Existing columns can't be altered or removed. A workaround is described [here](https://stackoverflow.com/a/805508), it -can be used together with `customStatement` to run the statements. -Alternatively, [complex migrations](#complex-migrations) described on this page help automating this. - -### Tips - -To ensure your schema stays consistent during a migration, you can wrap it in a `transaction` block. -However, be aware that some pragmas (including `foreign_keys`) can't be changed inside transactions. -Still, it can be useful to: - -- always re-enable foreign keys before using the database, by enabling them in [`beforeOpen`](#post-migration-callbacks). -- disable foreign-keys before migrations -- run migrations inside a transaction -- make sure your migrations didn't introduce any inconsistencies with `PRAGMA foreign_key_check`. - -With all of this combined, a migration callback can look like this: - -{% include "blocks/snippet" snippets = snippets name = 'structured' %} - -## Migration workflow - -While migrations can be written manually without additional help from drift, dedicated tools testing your migrations help -to ensure that they are correct and aren't loosing any data. - -Drift's migration tooling consists of the following steps: - -1. After each change to your schema, use a tool to export the current schema into a separate file. -2. Use a drift tool to generate test code able to verify that your migrations are bringing the database - into the expected schema. -3. Use generated code to make writing schema migrations easier. - -### Setup - -As described by the first step, you can export the schema of your database into a JSON file. -It is recommended to do this once intially, and then again each time you change your schema -and increase the `schemaVersion` getter in the database. - -You should store these exported files in your repository and include them in source control. -This guide assumes a top-level `drift_schemas/` folder in your project, like this: - -``` -my_app - .../ - lib/ - database/ - database.dart - database.g.dart - test/ - generated_migrations/ - schema.dart - schema_v1.dart - schema_v2.dart - drift_schemas/ - drift_schema_v1.json - drift_schema_v2.json - pubspec.yaml -``` - -Of course, you can also use another folder or a subfolder somewhere if that suits your workflow -better. - -{% block "blocks/alert" title="Examples available" %} -Exporting schemas and generating code for them can't be done with `build_runner` alone, which is -why this setup described here is necessary. - -We hope it's worth it though! Verifying migrations can give you confidence that you won't run -into issues after changing your database. -If you get stuck along the way, don't hesitate to [open a discussion about it](https://github.com/simolus3/drift/discussions). - -Also there are two examples in the drift repository which may be useful as a reference: - -- A [Flutter app](https://github.com/simolus3/drift/tree/latest-release/examples/app) -- An [example specific to migrations](https://github.com/simolus3/drift/tree/latest-release/examples/migrations_example). -{% endblock %} - -#### Exporting the schema - -To begin, lets create the first schema representation: - -``` -$ mkdir drift_schemas -$ dart run drift_dev schema dump lib/database/database.dart drift_schemas/ -``` - -This instructs the generator to look at the database defined in `lib/database/database.dart` and extract -its schema into the new folder. - -After making a change to your database schema, you can run the command again. For instance, let's say we -made a change to our tables and increased the `schemaVersion` to `2`. To dump the new schema, just run the -command again: - -``` -$ dart run drift_dev schema dump lib/database/database.dart drift_schemas/ -``` - -You'll need to run this command every time you change the schema of your database and increment the `schemaVersion`. - -Drift will name the files in the folder `drift_schema_vX.json`, where `X` is the current `schemaVersion` of your -database. -If drift is unable to extract the version from your `schemaVersion` getter, provide the full path explicitly: - -``` -$ dart run drift_dev schema dump lib/database/database.dart drift_schemas/drift_schema_v3.json -``` - -{% block "blocks/alert" title=' Dumping a database' color="success" %} -If, instead of exporting the schema of a database class, you want to export the schema of an existing sqlite3 -database file, you can do that as well! `drift_dev schema dump` recognizes a sqlite3 database file as its first -argument and can extract the relevant schema from there. -{% endblock %} - -### Generating step-by-step migrations {#step-by-step} - -With all your database schemas exported into a folder, drift can generate code that makes it much -easier to write schema migrations "step-by-step" (incrementally from each version to the next one). - -This code is stored in a single-file, which you can generate like this: - -``` -$ dart run drift_dev schema steps drift_schemas/ lib/database/schema_versions.dart -``` - -The generated code contains a `stepByStep` method which you can use as a callback to the `onUpgrade` -parameter of your `MigrationStrategy`. -As an example, here is the [initial](#basics) migration shown at the top of this page, but rewritten using -the generated `stepByStep` function: - -{% include "blocks/snippet" snippets = snippets name = 'stepbystep' %} - -`stepByStep` expects a callback for each schema upgrade responsible for running the partial migration. -That callback receives two parameters: A migrator `m` (similar to the regular migrator you'd get for -`onUpgrade` callbacks) and a `schema` parameter that gives you access to the schema at the version you're -migrating to. -For instance, in the `from1To2` function, `schema` provides getters for the database schema at version 2. -The migrator passed to the function is also set up to consider that specific version by default. -A call to `m.recreateAllViews()` would re-create views at the expected state of schema version 2, for instance. - -#### Customizing step-by-step migrations - -The `stepByStep` function generated by the `drift_dev schema steps` command gives you an -`OnUpgrade` callback. -But you might want to customize the upgrade behavior, for instance by adding foreign key -checks afterwards (as described in [tips](#tips)). - -The `Migrator.runMigrationSteps` helper method can be used for that, as this example -shows: - -{% include "blocks/snippet" snippets = snippets name = 'stepbystep2' %} - -Here, foreign keys are disabled before runnign the migration and re-enabled afterwards. -A check ensuring no inconsistencies occurred helps catching issues with the migration -in debug modes. - -#### Moving to step-by-step migrations - -If you've been using drift before `stepByStep` was added to the library, or if you've never exported a schema, -you can move to step-by-step migrations by pinning the `from` value in `Migrator.runMigrationSteps` to a known -starting point. - -This allows you to perform all prior migration work to get the database to the "starting" point for -`stepByStep` migrations, and then use `stepByStep` migrations beyond that schema version. - -{% include "blocks/snippet" snippets = snippets name = 'stepbystep3' %} - -Here, we give a "floor" to the `from` value of `2`, since we've performed all other migration work to get to -this point. From now on, you can generate step-by-step migrations for each schema change. - -If you did not do this, a user migrating from schema 1 directly to schema 3 would not properly walk migrations -and apply all migration changes required. - -### Writing tests - -After you've exported the database schemas into a folder, you can generate old versions of your database class -based on those schema files. -For verifications, drift will generate a much smaller database implementation that can only be used to -test migrations. - -You can put this test code whereever you want, but it makes sense to put it in a subfolder of `test/`. -If we wanted to write them to `test/generated_migrations/`, we could use - -``` -$ dart run drift_dev schema generate drift_schemas/ test/generated_migrations/ -``` - -After that setup, it's finally time to write some tests! For instance, a test could look like this: - -```dart -import 'package:my_app/database/database.dart'; - -import 'package:test/test.dart'; -import 'package:drift_dev/api/migrations.dart'; - -// The generated directory from before. -import 'generated_migrations/schema.dart'; - -void main() { - late SchemaVerifier verifier; - - setUpAll(() { - // GeneratedHelper() was generated by drift, the verifier is an api - // provided by drift_dev. - verifier = SchemaVerifier(GeneratedHelper()); - }); - - test('upgrade from v1 to v2', () async { - // Use startAt(1) to obtain a database connection with all tables - // from the v1 schema. - final connection = await verifier.startAt(1); - final db = MyDatabase(connection); - - // Use this to run a migration to v2 and then validate that the - // database has the expected schema. - await verifier.migrateAndValidate(db, 2); - }); -} -``` - -In general, a test looks like this: - -1. Use `verifier.startAt()` to obtain a [connection](https://drift.simonbinder.eu/api/drift/databaseconnection-class) - to a database with an initial schema. - This database contains all your tables, indices and triggers from that version, created by using `Migrator.createAll`. -2. Create your application database with that connection. For this, create a constructor in your database class that - accepts a `QueryExecutor` and forwards it to the super constructor in `GeneratedDatabase`. - Then, you can pass the result of calling `newConnection()` to that constructor to create a test instance of your - datbaase. -3. Call `verifier.migrateAndValidate(db, version)`. This will initiate a migration towards the target version (here, `2`). - Unlike the database created by `startAt`, this uses the migration logic you wrote for your database. - -`migrateAndValidate` will extract all `CREATE` statement from the `sqlite_schema` table and semantically compare them. -If it sees anything unexpected, it will throw a `SchemaMismatch` exception to fail your test. - -{% block "blocks/alert" title="Writing testable migrations" %} -To test migrations _towards_ an old schema version (e.g. from `v1` to `v2` if your current version is `v3`), -you're `onUpgrade` handler must be capable of upgrading to a version older than the current `schemaVersion`. -For this, check the `to` parameter of the `onUpgrade` callback to run a different migration if necessary. -{% endblock %} - -#### Verifying data integrity - -In addition to the changes made in your table structure, its useful to ensure that data that was present before a migration -is still there after it ran. -You can use `schemaAt` to obtain a raw `Database` from the `sqlite3` package in addition to a connection. -This can be used to insert data before a migration. After the migration ran, you can then check that the data is still there. - -Note that you can't use the regular database class from you app for this, since its data classes always expect the latest -schema. However, you can instruct drift to generate older snapshots of your data classes and companions for this purpose. -To enable this feature, pass the `--data-classes` and `--companions` command-line arguments to the `drift_dev schema generate` -command: - -``` -$ dart run drift_dev schema generate --data-classes --companions drift_schemas/ test/generated_migrations/ -``` - -Then, you can import the generated classes with an alias: - -```dart -import 'generated_migrations/schema_v1.dart' as v1; -import 'generated_migrations/schema_v2.dart' as v2; -``` - -This can then be used to manually create and verify data at a specific version: - -```dart -void main() { - // ... - test('upgrade from v1 to v2', () async { - final schema = await verifier.schemaAt(1); - - // Add some data to the users table, which only has an id column at v1 - final oldDb = v1.DatabaseAtV1(schema.newConnection()); - await oldDb.into(oldDb.users).insert(const v1.UsersCompanion(id: Value(1))); - await oldDb.close(); - - // Run the migration and verify that it adds the name column. - final db = Database(schema.newConnection()); - await verifier.migrateAndValidate(db, 2); - await db.close(); - - // Make sure the user is still here - final migratedDb = v2.DatabaseAtV2(schema.newConnection()); - final user = await migratedDb.select(migratedDb.users).getSingle(); - expect(user.id, 1); - expect(user.name, 'no name'); // default from the migration - await migratedDb.close(); - }); -} -``` - -## Complex migrations - -Sqlite has builtin statements for simple changes, like adding columns or dropping entire tables. -More complex migrations require a [12-step procedure](https://www.sqlite.org/lang_altertable.html#otheralter) that -involves creating a copy of the table and copying over data from the old table. -Drift 3.4 introduced the `TableMigration` api to automate most of this procedure, making it easier and safer to use. - -To start the migration, drift will create a new instance of the table with the current schema. Next, it will copy over -rows from the old table. -In most cases, for instance when changing column types, we can't just copy over each row without changing its content. -Here, you can use a `columnTransformer` to apply a per-row transformation. -The `columnTransformer` is a map from columns to the sql expression that will be used to copy the column from the -old table. -For instance, if we wanted to cast a column before copying it, we could use: - -```dart -columnTransformer: { - todos.category: todos.category.cast(), -} -``` - -Internally, drift will use a `INSERT INTO SELECT` statement to copy old data. In this case, it would look like -`INSERT INTO temporary_todos_copy SELECT id, title, content, CAST(category AS INT) FROM todos`. -As you can see, drift will use the expression from the `columnTransformer` map and fall back to just copying the column -otherwise. -If you're introducing new columns in a table migration, be sure to include them in the `newColumns` parameter of -`TableMigration`. Drift will ensure that those columns have a default value or a transformation in `columnTransformer`. -Of course, drift won't attempt to copy `newColumns` from the old table either. - -Regardless of whether you're implementing complex migrations with `TableMigration` or by running a custom sequence -of statements, we strongly recommend to write integration tests covering your migrations. This helps to avoid data -loss caused by errors in a migration. - -Here are some examples demonstrating common usages of the table migration api: - -### Changing the type of a column - -Let's say the `category` column in `Todos` used to be a non-nullable `text()` column that we're now changing to a -nullable int. For simplicity, we assume that `category` always contained integers, they were just stored in a text -column that we now want to adapt. - -```patch -class Todos extends Table { - IntColumn get id => integer().autoIncrement()(); - TextColumn get title => text().withLength(min: 6, max: 10)(); - TextColumn get content => text().named('body')(); -- IntColumn get category => text()(); -+ IntColumn get category => integer().nullable()(); -} -``` - -After re-running your build and incrementing the schema version, you can write a migration: - -{% include "blocks/snippet" snippets = snippets name = 'change_type' %} - -The important part here is the `columnTransformer` - a map from columns to expressions that will -be used to copy the old data. The values in that map refer to the old table, so we can use -`todos.category.cast()` to copy old rows and transform their `category`. -All columns that aren't present in `columnTransformer` will be copied from the old table without -any transformation. - -### Changing column constraints - -When you're changing columns constraints in a way that's compatible to existing data (e.g. changing -non-nullable columns to nullable columns), you can just copy over data without applying any -transformation: - -```dart -await m.alterTable(TableMigration(todos)); -``` - -### Deleting columns - -Deleting a column that's not referenced by a foreign key constraint is easy too: - -```dart -await m.alterTable(TableMigration(yourTable)); -``` - -To delete a column referenced by a foreign key, you'd have to migrate the referencing -tables first. - -### Renaming columns - -If you're renaming a column in Dart, note that the easiest way is to just rename the getter and use -`named`: `TextColumn newName => text().named('old_name')()`. That is fully backwards compatible and -doesn't require a migration. - -If you know your app runs on sqlite 3.25.0 or later (it does if you're using `sqlite3_flutter_libs`), -you can also use the `renameColumn` api in `Migrator`: - -```dart -m.renameColumn(yourTable, 'old_column_name', yourTable.newColumn); -``` - -If you do want to change the actual column name in a table, you can write a `columnTransformer` to -use an old column with a different name: - -```dart -await m.alterTable( - TableMigration( - yourTable, - columnTransformer: { - yourTable.newColumn: const CustomExpression('old_column_name') - }, - ) -) -``` - -## Migrating views, triggers and indices - -When changing the definition of a view, a trigger or an index, the easiest way -to update the database schema is to drop and re-create the element. -With the `Migrator` API, this is just a matter of calling `await drop(element)` -followed by `await create(element)`, where `element` is the trigger, view or index -to update. - -Note that the definition of a Dart-defined view might change without modifications -to the view class itself. This is because columns from a table are referenced with -a getter. When renaming a column through `.named('name')` in a table definition -without renaming the getter, the view definition in Dart stays the same but the -`CREATE VIEW` statement changes. - -A headache-free solution to this problem is to just re-create all views in a -migration, for which the `Migrator` provides the `recreateAllViews` method. - -## Post-migration callbacks - -The `beforeOpen` parameter in `MigrationStrategy` can be used to populate data after the database has been created. -It runs after migrations, but before any other query. Note that it will be called whenever the database is opened, -regardless of whether a migration actually ran or not. You can use `details.hadUpgrade` or `details.wasCreated` to -check whether migrations were necessary: - -```dart -beforeOpen: (details) async { - if (details.wasCreated) { - final workId = await into(categories).insert(Category(description: 'Work')); - - await into(todos).insert(TodoEntry( - content: 'A first todo entry', - category: null, - targetDate: DateTime.now(), - )); - - await into(todos).insert( - TodoEntry( - content: 'Rework persistence code', - category: workId, - targetDate: DateTime.now().add(const Duration(days: 4)), - )); - } -}, -``` - -You could also activate pragma statements that you need: - -```dart -beforeOpen: (details) async { - if (details.wasCreated) { - // ... - } - await customStatement('PRAGMA foreign_keys = ON'); -} -``` - -## During development - -During development, you might be changing your schema very often and don't want to write migrations for that -yet. You can just delete your apps' data and reinstall the app - the database will be deleted and all tables -will be created again. Please note that uninstalling is not enough sometimes - Android might have backed up -the database file and will re-create it when installing the app again. - -You can also delete and re-create all tables every time your app is opened, see [this comment](https://github.com/simolus3/drift/issues/188#issuecomment-542682912) -on how that can be achieved. - -## Verifying a database schema at runtime - -Instead (or in addition to) [writing tests](#verifying-migrations) to ensure your migrations work as they should, -you can use a new API from `drift_dev` 1.5.0 to verify the current schema without any additional setup. - -{% assign runtime_snippet = 'package:drift_docs/snippets/migrations/runtime_verification.dart.excerpt.json' | readString | json_decode %} - -{% include "blocks/snippet" snippets = runtime_snippet name = '' %} - -When you use `validateDatabaseSchema`, drift will transparently: - -- collect information about your database by reading from `sqlite3_schema`. -- create a fresh in-memory instance of your database and create a reference schema with `Migrator.createAll()`. -- compare the two. Ideally, your actual schema at runtime should be identical to the fresh one even though it - grew through different versions of your app. - -When a mismatch is found, an exception with a message explaining exactly where another value was expected will -be thrown. -This allows you to find issues with your schema migrations quickly. diff --git a/docs/pages/docs/CLI.md b/docs/pages/docs/CLI.md index 2d5717032..121d870d6 100644 --- a/docs/pages/docs/CLI.md +++ b/docs/pages/docs/CLI.md @@ -2,6 +2,7 @@ data: title: "Command line tools for drift" description: A set of CLI tools to interact with drift projects + weight: 20 path: /cli/ template: layouts/docs/single --- @@ -69,4 +70,4 @@ The generated file (`schema.json` in this case) contains information about all - dependencies thereof Exporting a schema can be used to generate test code for your schema migrations. For details, -see [the guide]({{ "Advanced Features/migrations.md#verifying-migrations" | pageUrl }}). \ No newline at end of file +see [the guide]({{ "Migrations/tests.md" | pageUrl }}). \ No newline at end of file diff --git a/docs/pages/docs/Advanced Features/daos.md b/docs/pages/docs/Dart API/daos.md similarity index 96% rename from docs/pages/docs/Advanced Features/daos.md rename to docs/pages/docs/Dart API/daos.md index e4dfe77ec..c3af6405c 100644 --- a/docs/pages/docs/Advanced Features/daos.md +++ b/docs/pages/docs/Dart API/daos.md @@ -2,14 +2,16 @@ data: title: "DAOs" description: Keep your database code modular with DAOs +path: /docs/advanced-features/daos/ aliases: - /daos/ template: layouts/docs/single --- When you have a lot of queries, putting them all into one class might become -tedious. You can avoid this by extracting some queries into classes that are +tedious. You can avoid this by extracting some queries into classes that are available from your main database class. Consider the following code: + ```dart part 'todos_dao.g.dart'; @@ -32,5 +34,6 @@ class TodosDao extends DatabaseAccessor with _$TodosDaoMixin { } } ``` + If we now change the annotation on the `MyDatabase` class to `@DriftDatabase(tables: [Todos, Categories], daos: [TodosDao])` and re-run the code generation, a generated getter `todosDao` can be used to access the instance of that dao. diff --git a/docs/pages/docs/Advanced Features/expressions.md b/docs/pages/docs/Dart API/expressions.md similarity index 92% rename from docs/pages/docs/Advanced Features/expressions.md rename to docs/pages/docs/Dart API/expressions.md index 6e4e36d63..a1cb2e34a 100644 --- a/docs/pages/docs/Advanced Features/expressions.md +++ b/docs/pages/docs/Dart API/expressions.md @@ -2,7 +2,7 @@ data: title: Expressions description: Deep-dive into what kind of SQL expressions can be written in Dart - weight: 200 + weight: 5 # used to be in the "getting started" section path: docs/getting-started/expressions/ @@ -18,7 +18,7 @@ In most cases, you're writing an expression that combines other expressions. Any column name is a valid expression, so for most `where` clauses you'll be writing a expression that wraps a column name in some kind of comparison. -{% assign snippets = 'package:drift_docs/snippets/expressions.dart.excerpt.json' | readString | json_decode %} +{% assign snippets = 'package:drift_docs/snippets/dart_api/expressions.dart.excerpt.json' | readString | json_decode %} ## Comparisons Every expression can be compared to a value by using `equals`. If you want to compare @@ -100,7 +100,7 @@ select(users)..where((u) => u.birthDate.year.isLessThan(1950)) The individual fields like `year`, `month` and so on are expressions themselves. This means that you can use operators and comparisons on them. -To obtain the current date or the current time as an expression, use the `currentDate` +To obtain the current date or the current time as an expression, use the `currentDate` and `currentDateAndTime` constants provided by drift. You can also use the `+` and `-` operators to add or subtract a duration from a time column: @@ -121,12 +121,12 @@ Again, the `isNotIn` function works the other way around. ## Aggregate functions (like count and sum) {#aggregate} -[Aggregate functions](https://www.sqlite.org/lang_aggfunc.html) are available +[Aggregate functions](https://www.sqlite.org/lang_aggfunc.html) are available from the Dart api. Unlike regular functions, aggregate functions operate on multiple rows at -once. +once. By default, they combine all rows that would be returned by the select statement into a single value. -You can also make them run over different groups in the result by using -[group by]({{ "joins.md#group-by" | pageUrl }}). +You can also make them run over different groups in the result by using +[group by]({{ "select.md#group-by" | pageUrl }}). ### Comparing @@ -149,13 +149,13 @@ Stream averageItemLength() { ``` __Note__: We're using `selectOnly` instead of `select` because we're not interested in any colum that -`todos` provides - we only care about the average length. More details are available -[here]({{ "joins.md#group-by" | pageUrl }}) +`todos` provides - we only care about the average length. More details are available +[here]({{ "select.md#group-by" | pageUrl }}) ### Counting Sometimes, it's useful to count how many rows are present in a group. By using the -[table layout from the example]({{ "../Getting started/index.md" | pageUrl }}), this +[table layout from the example]({{ "../setup.md" | pageUrl }}), this query will report how many todo entries are associated to each category: ```dart @@ -173,14 +173,14 @@ query ..groupBy([categories.id]); ``` -If you don't want to count duplicate values, you can use `count(distinct: true)`. +If you don't want to count duplicate values, you can use `count(distinct: true)`. Sometimes, you only need to count values that match a condition. For that, you can use the `filter` parameter on `count`. To count all rows (instead of a single value), you can use the top-level `countAll()` function. More information on how to write aggregate queries with drift's Dart api is available -[here]({{ "joins.md#group-by" | pageUrl }}) +[here]({{ "select.md#group-by" | pageUrl }}) ### group_concat @@ -200,9 +200,9 @@ with the `separator` argument on `groupConcat`. ## Mathematical functions and regexp -When using a `NativeDatabase`, a basic set of trigonometric functions will be available. +When using a `NativeDatabase`, a basic set of trigonometric functions will be available. It also defines the `REGEXP` function, which allows you to use `a REGEXP b` in sql queries. -For more information, see the [list of functions]({{ "../Other engines/vm.md#moor-only-functions" | pageUrl }}) here. +For more information, see the [list of functions]({{ "../Platforms/vm.md#moor-only-functions" | pageUrl }}) here. ## Subqueries @@ -246,7 +246,7 @@ any rows. For instance, we could use this to find empty categories: ### Full subqueries Drift also supports subqueries that appear in `JOIN`s, which are described in the -[documentation for joins]({{ 'joins.md#subqueries' | pageUrl }}). +[documentation for joins]({{ 'select.md#subqueries' | pageUrl }}). ## Custom expressions If you want to inline custom sql into Dart queries, you can use a `CustomExpression` class. @@ -258,5 +258,5 @@ select(users)..where((u) => inactive); _Note_: It's easy to write invalid queries by using `CustomExpressions` too much. If you feel like you need to use them because a feature you use is not available in drift, consider creating an issue -to let us know. If you just prefer sql, you could also take a look at -[compiled sql]({{ "../Using SQL/custom_queries.md" | pageUrl }}) which is typesafe to use. +to let us know. If you just prefer sql, you could also take a look at +[compiled sql]({{ "../SQL API/custom_queries.md" | pageUrl }}) which is typesafe to use. diff --git a/docs/pages/docs/Dart API/index.md b/docs/pages/docs/Dart API/index.md new file mode 100644 index 000000000..dcf2fc6db --- /dev/null +++ b/docs/pages/docs/Dart API/index.md @@ -0,0 +1,15 @@ +--- +data: + title: Dart API + description: Drift's Dart library for declaring tables and writing queries. + weight: 2 +template: layouts/docs/list +--- + +After writing a database [as described in the setup page]({{ '../setup.md' | pageUrl }}), +drift will generate code enabling you to write SQL statements in Dart. + +The pages on [select statements]({{ 'select.md' | pageUrl }}) and [insert, updates and deletes]({{ 'writes.md' | pageUrl }}) +explain how to construct statements. + +Advanced features include support for transactions, complex SQL expressions and [database accessor classes]({{ 'daos.md' | pageUrl }}). diff --git a/docs/pages/docs/Advanced Features/schema_inspection.md b/docs/pages/docs/Dart API/schema_inspection.md similarity index 96% rename from docs/pages/docs/Advanced Features/schema_inspection.md rename to docs/pages/docs/Dart API/schema_inspection.md index cbf72ff98..ecdd5e3d6 100644 --- a/docs/pages/docs/Advanced Features/schema_inspection.md +++ b/docs/pages/docs/Dart API/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. @@ -15,7 +15,7 @@ to access tables reflectively. Luckily, code generated by drift implements inter Since this is a topic that most drift users will not need, this page mostly gives motivating examples and links to the documentation for relevant drift classes. For instance, you might have multiple independent tables that have an `id` column. And you might want to filter rows by their `id` column. -When writing this query against a single table, like the `Todos` table as seen in the [getting started]({{'../Getting started/index.md' | pageUrl }}) page, +When writing this query against a single table, like the `Todos` table as seen in the [getting started]({{'../setup.md' | pageUrl }}) page, that's pretty straightforward: {% include "blocks/snippet" snippets = snippets name = 'findTodoEntryById' %} diff --git a/docs/pages/docs/Advanced Features/joins.md b/docs/pages/docs/Dart API/select.md similarity index 69% rename from docs/pages/docs/Advanced Features/joins.md rename to docs/pages/docs/Dart API/select.md index 2cca9c654..be276460b 100644 --- a/docs/pages/docs/Advanced Features/joins.md +++ b/docs/pages/docs/Dart API/select.md @@ -1,21 +1,109 @@ --- data: - title: "Advanced queries in Dart" - weight: 1 - description: Use sql joins or custom expressions from the Dart api -aliases: - - queries/joins/ + title: "Selects" + description: "Select rows or invidiual columns from tables in Dart" + weight: 2 template: layouts/docs/single --- -{% assign snippets = 'package:drift_docs/snippets/queries.dart.excerpt.json' | readString | json_decode %} +{% assign tables = 'package:drift_docs/snippets/_shared/todo_tables.dart.excerpt.json' | readString | json_decode %} +{% assign snippets = 'package:drift_docs/snippets/dart_api/select.dart.excerpt.json' | readString | json_decode %} + +This page describes how to write `SELECT` statements with drift's Dart API. +To make examples easier to grasp, they're referencing two common tables forming +the basis of a todo-list app: + +{% include "blocks/snippet" snippets = tables name = 'tables' %} + +For each table you've specified in the `@DriftDatabase` annotation on your database class, +a corresponding getter for a table will be generated. That getter can be used to +run statements: + +```dart +@DriftDatabase(tables: [TodoItems, Categories]) +class MyDatabase extends _$MyDatabase { + + // the schemaVersion getter and the constructor from the previous page + // have been omitted. + + // loads all todo entries + Future> 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. Here's an example using the tables -defined in the [example]({{ "../Getting started/index.md" | pageUrl }}). +inner and left outer joins, a `ON` expression needs to be specified. {% include "blocks/snippet" snippets = snippets name = 'joinIntro' %} @@ -43,26 +131,7 @@ categories. Select statements aren't limited to columns from tables. You can also include more complex expressions in the query. For each row in the result, those expressions will be evaluated by the database engine. -```dart -class EntryWithImportance { - final TodoEntry entry; - final bool important; - - EntryWithImportance(this.entry, this.important); -} - -Future> loadEntries() { - // assume that an entry is important if it has the string "important" somewhere in its content - final isImportant = todos.content.like('%important%'); - - return select(todos).addColumns([isImportant]).map((row) { - final entry = row.readTable(todos); - final entryIsImportant = row.read(isImportant); - - return EntryWithImportance(entry, entryIsImportant); - }).get(); -} -``` +{% include "blocks/snippet" snippets = snippets name = 'custom-columns' %} Note that the `like` check is _not_ performed in Dart - it's sent to the underlying database engine which can efficiently compute it for all rows. @@ -220,7 +289,7 @@ Any statement can be used as a subquery. But be aware that, unlike [subquery exp ## JSON support -{% assign json_snippet = 'package:drift_docs/snippets/queries/json.dart.excerpt.json' | readString | json_decode %} +{% assign json_snippet = 'package:drift_docs/snippets/dart_api/json.dart.excerpt.json' | readString | json_decode %} sqlite3 has great support for [JSON operators](https://sqlite.org/json1.html) that are also available in drift (under the additional `'package:drift/extensions/json1.dart'` import). diff --git a/docs/pages/docs/Getting started/advanced_dart_tables.md b/docs/pages/docs/Dart API/tables.md similarity index 70% rename from docs/pages/docs/Getting started/advanced_dart_tables.md rename to docs/pages/docs/Dart API/tables.md index afc7e2673..05c77db5b 100644 --- a/docs/pages/docs/Getting started/advanced_dart_tables.md +++ b/docs/pages/docs/Dart API/tables.md @@ -1,169 +1,37 @@ --- data: title: "Dart tables" - description: "Further information on defining tables in Dart. This page describes advanced features like constraints, nullability, references and views" - weight: 150 + description: "Everything there is to know about defining SQL tables in Dart." + weight: 1 template: layouts/docs/single +path: /docs/getting-started/advanced_dart_tables/ --- -{% block "blocks/pageinfo" %} -__Prefer sql?__ If you prefer, you can also declare tables via `CREATE TABLE` statements. -Drift's sql analyzer will generate matching Dart code. [Details]({{ "starting_with_sql.md" | pageUrl }}). -{% endblock %} - -{% assign snippets = 'package:drift_docs/snippets/tables/advanced.dart.excerpt.json' | readString | json_decode %} - -As shown in the [getting started guide]({{ "index.md" | pageUrl }}), sql tables can be written in Dart: -```dart -class Todos extends Table { - IntColumn get id => integer().autoIncrement()(); - TextColumn get title => text().withLength(min: 6, max: 32)(); - TextColumn get content => text().named('body')(); - IntColumn get category => integer().nullable()(); -} -``` - -In this article, we'll cover some advanced features of this syntax. - -## Names - -By default, drift uses the `snake_case` name of the Dart getter in the database. For instance, the -table -```dart -class EnabledCategories extends Table { - IntColumn get parentCategory => integer()(); - // .. -} -``` - -Would be generated as `CREATE TABLE enabled_categories (parent_category INTEGER NOT NULL)`. - -To override the table name, simply override the `tableName` getter. An explicit name for -columns can be provided with the `named` method: -```dart -class EnabledCategories extends Table { - String get tableName => 'categories'; - - IntColumn get parentCategory => integer().named('parent')(); -} -``` - -The updated class would be generated as `CREATE TABLE categories (parent INTEGER NOT NULL)`. - -To update the name of a column when serializing data to json, annotate the getter with -[`@JsonKey`](https://pub.dev/documentation/drift/latest/drift/JsonKey-class.html). - -You can change the name of the generated data class too. By default, drift will stip a trailing -`s` from the table name (so a `Users` table would have a `User` data class). -That doesn't work in all cases though. With the `EnabledCategories` class from above, we'd get -a `EnabledCategorie` data class. In those cases, you can use the [`@DataClassName`](https://pub.dev/documentation/drift/latest/drift/DataClassName-class.html) -annotation to set the desired name. - -## Nullability - -By default, columns may not contain null values. When you forgot to set a value in an insert, -an exception will be thrown. When using sql, drift also warns about that at compile time. - -If you do want to make a column nullable, just use `nullable()`: -```dart -class Items { - IntColumn get category => integer().nullable()(); - // ... -} -``` - -## Checks - -If you know that a column (or a row) may only contain certain values, you can use a `CHECK` constraint -in SQL to enforce custom constraints on data. - -In Dart, the `check` method on the column builder adds a check constraint to the generated column: - -```dart - // sqlite3 will enforce that this column only contains timestamps happening after (the beginning of) 1950. - DateTimeColumn get creationTime => dateTime() - .check(creationTime.isBiggerThan(Constant(DateTime(1950)))) - .withDefault(currentDateAndTime)(); -``` - -Note that these `CHECK` constraints are part of the `CREATE TABLE` statement. -If you want to change or remove a `check` constraint, write a [schema migration]({{ '../Advanced Features/migrations.md#changing-column-constraints' | pageUrl }}) to re-create the table without the constraint. +{% assign snippets = 'package:drift_docs/snippets/dart_api/tables.dart.excerpt.json' | readString | json_decode %} +{% assign setup = 'package:drift_docs/snippets/setup/database.dart.excerpt.json' | readString | json_decode %} -## Default values +In relational databases, tables are used to describe the structure of rows. By +adhering to a predefined schema, drift can generate typesafe code for your +database. +As already shown in the [setup]({{ '../setup.md#database-class' | pageUrl }}) +page, drift provides APIs to declare tables in Dart: -You can set a default value for a column. When not explicitly set, the default value will -be used when inserting a new row. To set a constant default value, use `withDefault`: +{% include "blocks/snippet" snippets = setup name = 'table' %} -```dart -class Preferences extends Table { - TextColumn get name => text()(); - BoolColumn get enabled => boolean().withDefault(const Constant(false))(); -} -``` +This page describes the DSL for tables in more detail. -When you later use `into(preferences).insert(PreferencesCompanion.forInsert(name: 'foo'));`, the new -row will have its `enabled` column set to false (and not to null, as it normally would). -Note that columns with a default value (either through `autoIncrement` or by using a default), are -still marked as `@required` in generated data classes. This is because they are meant to represent a -full row, and every row will have those values. Use companions when representing partial rows, like -for inserts or updates. +## Columns -Of course, constants can only be used for static values. But what if you want to generate a dynamic -default value for each column? For that, you can use `clientDefault`. It takes a function returning -the desired default value. The function will be called for each insert. For instance, here's an -example generating a random Uuid using the `uuid` package: -```dart -final _uuid = Uuid(); +In each table, you define columns by declaring a getter starting with the type of the column, +its name in Dart, and the definition mapped to SQL. +In the example above, `IntColumn get category => integer().nullable()();` defines a column +holding nullable integer values named `category`. +This section describes all the options available when declaring columns. -class Users extends Table { - TextColumn get id => text().clientDefault(() => _uuid.v4())(); - // ... -} -``` - -Don't know when to use which? Prefer to use `withDefault` when the default value is constant, or something -simple like `currentDate`. For more complicated values, like a randomly generated id, you need to use -`clientDefault`. Internally, `withDefault` writes the default value into the `CREATE TABLE` statement. This -can be more efficient, but doesn't support dynamic values. - -## Primary keys - -If your table has an `IntColumn` with an `autoIncrement()` constraint, drift recognizes that as the default -primary key. If you want to specify a custom primary key for your table, you can override the `primaryKey` -getter in your table: - -```dart -class GroupMemberships extends Table { - IntColumn get group => integer()(); - IntColumn get user => integer()(); - - @override - Set get primaryKey => {group, user}; -} -``` - -Note that the primary key must essentially be constant so that the generator can recognize it. That means: - -- it must be defined with the `=>` syntax, function bodies aren't supported -- it must return a set literal without collection elements like `if`, `for` or spread operators - -## Unique Constraints - -Starting from version 1.6.0, `UNIQUE` SQL constraints can be defined on Dart tables too. -A unique constraint contains one or more columns. The combination of all columns in a constraint -must be unique in the table, or the database will report an error on inserts. - -With drift, a unique constraint can be added to a single column by marking it as `.unique()` in -the column builder. -A unique set spanning multiple columns can be added by overriding the `uniqueKeys` getter in the -`Table` class: - -{% include "blocks/snippet" snippets = snippets name = 'unique' %} - -## Supported column types +### Supported column types Drift supports a variety of column types out of the box. You can store custom classes in columns by using -[type converters]({{ "../Advanced Features/type_converters.md" | pageUrl }}). +[type converters]({{ "../type_converters.md" | pageUrl }}). | Dart type | Column | Corresponding SQLite type | |--------------|---------------|-----------------------------------------------------| @@ -174,8 +42,8 @@ Drift supports a variety of column types out of the box. You can store custom cl | `String` | `text()` | `TEXT` | | `DateTime` | `dateTime()` | `INTEGER` (default) or `TEXT` depending on [options](#datetime-options) | | `Uint8List` | `blob()` | `BLOB` | -| `Enum` | `intEnum()` | `INTEGER` (more information available [here]({{ "../Advanced Features/type_converters.md#implicit-enum-converters" | pageUrl }})). | -| `Enum` | `textEnum()` | `TEXT` (more information available [here]({{ "../Advanced Features/type_converters.md#implicit-enum-converters" | pageUrl }})). | +| `Enum` | `intEnum()` | `INTEGER` (more information available [here]({{ "../type_converters.md#implicit-enum-converters" | pageUrl }})). | +| `Enum` | `textEnum()` | `TEXT` (more information available [here]({{ "../type_converters.md#implicit-enum-converters" | pageUrl }})). | Note that the mapping for `boolean`, `dateTime` and type converters only applies when storing records in the database. @@ -183,6 +51,17 @@ They don't affect JSON serialization at all. For instance, `boolean` values are in the `fromJson` factory, even though they would be saved as `0` or `1` in the database. If you want a custom mapping for JSON, you need to provide your own [`ValueSerializer`](https://pub.dev/documentation/drift/latest/drift/ValueSerializer-class.html). +### Custom column types + +While is constrained by the types supported by sqlite3, it supports type converters +to store arbitrary Dart types in SQL. + +{% assign type_converters = 'package:drift_docs/snippets/type_converters/converters.dart.excerpt.json' | readString | json_decode %} +{% include "blocks/snippet" snippets = type_converters name = 'table' %} + +For more information about type converters, see the page on [type converters]({{ "../type_converters.md#implicit-enum-converters" | pageUrl }}) +on this website. + ### `BigInt` support Drift supports the `int64()` column builder to indicate that a column stores @@ -257,10 +136,10 @@ Drift supports two approaches of storing `DateTime` values in SQL: This behavior works well with the date functions in sqlite3 while also preserving "UTC-ness" for stored values. -The mode can be changed with the `store_date_time_values_as_text` [build option]({{ '../Advanced Features/builder_options.md' | pageUrl }}). +The mode can be changed with the `store_date_time_values_as_text` [build option]({{ '../Generation options/index.md' | pageUrl }}). Regardless of the option used, drift's builtin support for -[date and time functions]({{ '../Advanced Features/expressions.md#date-and-time' | pageUrl }}) +[date and time functions]({{ 'expressions.md#date-and-time' | pageUrl }}) return an equivalent values. Drift internally inserts the `unixepoch` [modifier](https://sqlite.org/lang_datefunc.html#modifiers) when unix timestamps are used to make the date functions work. When comparing dates stored as text, @@ -287,7 +166,7 @@ option, toggling this behavior is not compatible with existing database schemas: As the second point is specific to usages in your app, this documentation only describes how to migrate stored columns between the format: -{% assign conversion = "package:drift_docs/snippets/migrations/datetime_conversion.dart.excerpt.json" | readString | json_decode %} +{% assign conversion = "package:drift_docs/snippets/dart_api/datetime_conversion.dart.excerpt.json" | readString | json_decode %} Note that the JSON serialization generated by default is not affected by the datetime mode chosen. By default, drift will serialize `DateTime` values to a @@ -337,43 +216,24 @@ When using a `NativeDatabase` with a recent dependency on the `sqlite3_flutter_libs` package, you can safely assume that you are on a recent sqlite3 version with support for `unixepoch`. -## Custom constraints - -Some column and table constraints aren't supported through drift's Dart api. This includes `REFERENCES` clauses on columns, which you can set -through `customConstraint`: +### Nullability -```dart -class GroupMemberships extends Table { - IntColumn get group => integer().customConstraint('NOT NULL REFERENCES groups (id)')(); - IntColumn get user => integer().customConstraint('NOT NULL REFERENCES users (id)')(); +Drift follows Dart's idiom of non-nullable by default types. This means that +columns declared on a table defined in Dart can't store null values by default, +they are generated with a `NOT NULL` constraint in SQL. +When you forget to set a value in an insert, an exception will be thrown. +When using sql, drift also warns about that at compile time. - @override - Set get primaryKey => {group, user}; -} -``` - -Applying a `customConstraint` will override all other constraints that would be included by default. In -particular, that means that we need to also include the `NOT NULL` constraint again. +If you do want to make a column nullable, just use `nullable()`: -You can also add table-wide constraints by overriding the `customConstraints` getter in your table class. +{% include "blocks/snippet" snippets = snippets name = 'nnbd' %} -## References +### References [Foreign key references](https://www.sqlite.org/foreignkeys.html) can be expressed in Dart tables with the `references()` method when building a column: -```dart -class Todos extends Table { - // ... - IntColumn get category => integer().nullable().references(Categories, #id)(); -} - -@DataClassName("Category") -class Categories extends Table { - IntColumn get id => integer().autoIncrement()(); - // and more columns... -} -``` +{% include "blocks/snippet" snippets = snippets name = 'references' %} The first parameter to `references` points to the table on which a reference should be created. The second parameter is a [symbol](https://dart.dev/guides/language/language-tour#symbols) of the column to use for the reference. @@ -383,52 +243,167 @@ should happen when the target row gets updated or deleted. Be aware that, in sqlite3, foreign key references aren't enabled by default. They need to be enabled with `PRAGMA foreign_keys = ON`. -A suitable place to issue that pragma with drift is in a [post-migration callback]({{ '../Advanced Features/migrations.md#post-migration-callbacks' | pageUrl }}). +A suitable place to issue that pragma with drift is in a [post-migration callback]({{ '../Migrations/index.md#post-migration-callbacks' | pageUrl }}). -## Views +### Default values -It is also possible to define [SQL views](https://www.sqlite.org/lang_createview.html) -as Dart classes. -To do so, write an abstract class extending `View`. This example declares a view reading -the amount of todo-items added to a category in the schema from [the example]({{ 'index.md' | pageUrl }}): +You can set a default value for a column. When not explicitly set, the default value will +be used when inserting a new row. To set a constant default value, use `withDefault`: -{% include "blocks/snippet" snippets = snippets name = 'view' %} +```dart +class Preferences extends Table { + TextColumn get name => text()(); + BoolColumn get enabled => boolean().withDefault(const Constant(false))(); +} +``` -Inside a Dart view, use +When you later use `into(preferences).insert(PreferencesCompanion.forInsert(name: 'foo'));`, the new +row will have its `enabled` column set to false (and not to null, as it normally would). +Note that columns with a default value (either through `autoIncrement` or by using a default), are +still marked as `@required` in generated data classes. This is because they are meant to represent a +full row, and every row will have those values. Use companions when representing partial rows, like +for inserts or updates. -- abstract getters to declare tables that you'll read from (e.g. `TodosTable get todos`). -- `Expression` getters to add columns: (e.g. `itemCount => todos.id.count()`). -- the overridden `as` method to define the select statement backing the view. - The columns referenced in `select` may refer to two kinds of columns: - - Columns defined on the view itself (like `itemCount` in the example above). - - Columns defined on referenced tables (like `categories.description` in the example). - For these references, advanced drift features like [type converters]({{ '../Advanced Features/type_converters.md' | pageUrl }}) - used in the column's definition from the table are also applied to the view's column. +Of course, constants can only be used for static values. But what if you want to generate a dynamic +default value for each column? For that, you can use `clientDefault`. It takes a function returning +the desired default value. The function will be called for each insert. For instance, here's an +example generating a random Uuid using the `uuid` package: +```dart +final _uuid = Uuid(); + +class Users extends Table { + TextColumn get id => text().clientDefault(() => _uuid.v4())(); + // ... +} +``` + +Don't know when to use which? Prefer to use `withDefault` when the default value is constant, or something +simple like `currentDate`. For more complicated values, like a randomly generated id, you need to use +`clientDefault`. Internally, `withDefault` writes the default value into the `CREATE TABLE` statement. This +can be more efficient, but doesn't support dynamic values. - Both kind of columns will be added to the data class for the view when selected. +### Checks -Finally, a view needs to be added to a database or accessor by including it in the -`views` parameter: +If you know that a column (or a row) may only contain certain values, you can use a `CHECK` constraint +in SQL to enforce custom constraints on data. + +In Dart, the `check` method on the column builder adds a check constraint to the generated column: ```dart -@DriftDatabase(tables: [Todos, Categories], views: [CategoryTodoCount]) -class MyDatabase extends _$MyDatabase { + // sqlite3 will enforce that this column only contains timestamps happening after (the beginning of) 1950. + DateTimeColumn get creationTime => dateTime() + .check(creationTime.isBiggerThan(Constant(DateTime(1950)))) + .withDefault(currentDateAndTime)(); ``` -### Nullability of columns in a view +Note that these `CHECK` constraints are part of the `CREATE TABLE` statement. +If you want to change or remove a `check` constraint, write a [schema migration]({{ '../Migrations/api.md#changing-column-constraints' | pageUrl }}) to re-create the table without the constraint. + +### Unique column + +When an individual column must be unique for all rows in the table, it can be declared as `unique()` +in its definition: + +{% include "blocks/snippet" snippets = snippets name = "unique-column" %} + +If the combination of more than one column must be unique in the table, you can add a unique +[table constraint](#unique-columns-in-table) to the table. + +### Custom constraints + +Some column and table constraints aren't supported through drift's Dart api. This includes the collation +of columns, which you can apply using `customConstraint`: + +```dart +class Groups extends Table { + TextColumn get name => integer().customConstraint('COLLATE BINARY')(); +} +``` + +Applying a `customConstraint` will override all other constraints that would be included by default. In +particular, that means that we need to also include the `NOT NULL` constraint again. + +You can also add table-wide constraints by overriding the `customConstraints` getter in your table class. + +## Names + +By default, drift uses the `snake_case` name of the Dart getter in the database. For instance, the +table + +{% assign name = 'package:drift_docs/snippets/dart_api/old_name.dart.excerpt.json' | readString | json_decode %} +{% include "blocks/snippet" snippets = name %} + +Would be generated as `CREATE TABLE enabled_categories (parent_category INTEGER NOT NULL)`. + +To override the table name, simply override the `tableName` getter. An explicit name for +columns can be provided with the `named` method: + +{% include "blocks/snippet" snippets = snippets name="names" %} + +The updated class would be generated as `CREATE TABLE categories (parent INTEGER NOT NULL)`. + +To update the name of a column when serializing data to json, annotate the getter with +[`@JsonKey`](https://pub.dev/documentation/drift/latest/drift/JsonKey-class.html). + +You can change the name of the generated data class too. By default, drift will stip a trailing +`s` from the table name (so a `Users` table would have a `User` data class). +That doesn't work in all cases though. With the `EnabledCategories` class from above, we'd get +a `EnabledCategorie` data class. In those cases, you can use the [`@DataClassName`](https://pub.dev/documentation/drift/latest/drift/DataClassName-class.html) +annotation to set the desired name. + +## Existing row classes + +By default, drift generates a row class for each table. This row class can be used to access all columns, it also +implements `hashCode`, `operator==` and a few other useful operators. +When you want to use your own type hierarchy, or have more control over the generated classes, you can +also tell drift to your own class or type: + +{% include "blocks/snippet" snippets = snippets name="custom-type" %} + +Drift verifies that the type is suitable for storing a row of that table. +More details about this feature are [described here]({{ '../custom_row_classes.md' | pageUrl }}). + +## Table options + +In addition to the options added to individual columns, some constraints apply to the whole +table. + +### Primary keys + +If your table has an `IntColumn` with an `autoIncrement()` constraint, drift recognizes that as the default +primary key. If you want to specify a custom primary key for your table, you can override the `primaryKey` +getter in your table: + +{% include "blocks/snippet" snippets = snippets name="primary-key" %} + +Note that the primary key must essentially be constant so that the generator can recognize it. That means: + +- it must be defined with the `=>` syntax, function bodies aren't supported +- it must return a set literal without collection elements like `if`, `for` or spread operators + +### Unique columns in table + +When the value of one column must be unique in the table, you can [make that column unique](#unique-column). +When the combined value of multiple columns should be unique, this needs to be declared on the +table by overriding the `uniqueKeys` getter: + +{% include "blocks/snippet" snippets = snippets name="unique-table" %} + +### Custom constraints on tables + +Some table constraints are not directly supported in drift yet. Similar to [custom constraints](#custom-constraints) +on columns, you can add those by overriding `customConstraints`: + +{% include "blocks/snippet" snippets = snippets name="custom-constraint-table" %} -For a Dart-defined views, expressions defined as an `Expression` getter are -_always_ nullable. This behavior matches `TypedResult.read`, the method used to -read results from a complex select statement with custom columns. +## Index -Columns that reference another table's column are nullable if the referenced -column is nullable, or if the selected table does not come from an inner join -(because the whole table could be `null` in that case). +An [index](https://sqlite.org/lang_createindex.html) on columns in a table allows rows identified +by these columns to be identified more easily. +In drift, you can apply an index to a table with the `@TableIndex` annotation. More than one +index can be applied to the same table by repeating the annotation: -Considering the view from the example above, +{% include "blocks/snippet" snippets = snippets name="index" %} -- the `itemCount` column is nullable because it is defined as a complex - `Expression` -- the `description` column, referencing `categories.description`, is non-nullable. - This is because it references `categories`, the primary table of the view's - select statement. +Each index needs to have its own unique name. Typically, the name of the table is part of the +index' name to ensure unique names. diff --git a/docs/pages/docs/transactions.md b/docs/pages/docs/Dart API/transactions.md similarity index 96% rename from docs/pages/docs/transactions.md rename to docs/pages/docs/Dart API/transactions.md index c9b63f617..d6b6dd448 100644 --- a/docs/pages/docs/transactions.md +++ b/docs/pages/docs/Dart API/transactions.md @@ -1,15 +1,16 @@ --- data: title: "Transactions" - weight: 70 + weight: 4 description: Run multiple statements atomically template: layouts/docs/single +path: /docs/transactions/ aliases: - /transactions/ --- -{% assign snippets = "package:drift_docs/snippets/transactions.dart.excerpt.json" | readString | json_decode %} +{% assign snippets = "package:drift_docs/snippets/dart_api/transactions.dart.excerpt.json" | readString | json_decode %} Drift has support for transactions and allows multiple statements to run atomically, so that none of their changes is visible to the main database until the transaction diff --git a/docs/pages/docs/Dart API/views.md b/docs/pages/docs/Dart API/views.md new file mode 100644 index 000000000..238f83b4e --- /dev/null +++ b/docs/pages/docs/Dart API/views.md @@ -0,0 +1,52 @@ +--- +data: + title: "Views" + description: How to define SQL views as Dart classes +template: layouts/docs/single +--- + +It is also possible to define [SQL views](https://www.sqlite.org/lang_createview.html) +as Dart classes. +To do so, write an abstract class extending `View`. This example declares a view reading +the amount of todo-items added to a category in the schema from [the example]({{ 'index.md' | pageUrl }}): +{% assign snippets = 'package:drift_docs/snippets/dart_api/views.dart.excerpt.json' | readString | json_decode %} +{% include "blocks/snippet" snippets = snippets name = 'view' %} + +Inside a Dart view, use + +- abstract getters to declare tables that you'll read from (e.g. `TodosTable get todos`). +- `Expression` getters to add columns: (e.g. `itemCount => todos.id.count()`). +- the overridden `as` method to define the select statement backing the view. + The columns referenced in `select` may refer to two kinds of columns: + - Columns defined on the view itself (like `itemCount` in the example above). + - Columns defined on referenced tables (like `categories.description` in the example). + For these references, advanced drift features like [type converters]({{ '../type_converters.md' | pageUrl }}) + used in the column's definition from the table are also applied to the view's column. + + Both kind of columns will be added to the data class for the view when selected. + +Finally, a view needs to be added to a database or accessor by including it in the +`views` parameter: + +```dart +@DriftDatabase(tables: [Todos, Categories], views: [CategoryTodoCount]) +class MyDatabase extends _$MyDatabase { +``` + +### Nullability of columns in a view + +For a Dart-defined views, expressions defined as an `Expression` getter are +_always_ nullable. This behavior matches `TypedResult.read`, the method used to +read results from a complex select statement with custom columns. + +Columns that reference another table's column are nullable if the referenced +column is nullable, or if the selected table does not come from an inner join +(because the whole table could be `null` in that case). + +Considering the view from the example above, + +- the `itemCount` column is nullable because it is defined as a complex + `Expression` +- the `description` column, referencing `categories.description`, is non-nullable. + This is because it references `categories`, the primary table of the view's + select statement. diff --git a/docs/pages/docs/Getting started/writing_queries.md b/docs/pages/docs/Dart API/writes.md similarity index 60% rename from docs/pages/docs/Getting started/writing_queries.md rename to docs/pages/docs/Dart API/writes.md index 314929ace..51127b0ae 100644 --- a/docs/pages/docs/Getting started/writing_queries.md +++ b/docs/pages/docs/Dart API/writes.md @@ -1,127 +1,13 @@ --- data: - title: "Writing queries" - linkTitle: "Writing queries" - description: Learn how to write database queries in pure Dart with drift - weight: 100 -aliases: - - /queries/ + title: "Writes (update, insert, delete)" + description: "Select rows or invidiual columns from tables in Dart" + weight: 3 template: layouts/docs/single --- -{% block "blocks/pageinfo" %} -__Note__: This assumes that you've already completed [the setup]({{ "index.md" | pageUrl }}). -{% endblock %} - -For each table you've specified in the `@DriftDatabase` annotation on your database class, -a corresponding getter for a table will be generated. That getter can be used to -run statements: -```dart -// inside the database class, the `todos` getter has been created by drift. -@DriftDatabase(tables: [Todos, Categories]) -class MyDatabase extends _$MyDatabase { - - // the schemaVersion getter and the constructor from the previous page - // have been omitted. - - // loads all todo entries - Future> 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) { @@ -297,14 +183,14 @@ generated. __Note:__ This uses the `RETURNING` syntax added in sqlite3 version 3.35, which is not available on most operating systems by default. When using this method, make sure that you have a recent sqlite3 version available. This is the case with `sqlite3_flutter_libs`. -For instance, consider this snippet using the tables from the [getting started guide]({{ 'index.md' | pageUrl }}): +For instance, consider this snippet using the tables from the [getting started guide]({{ '../setup.md' | pageUrl }}): ```dart final row = await into(todos).insertReturning(TodosCompanion.insert( title: 'A todo entry', content: 'A description', )); -``` +``` The `row` returned has the proper `id` set. If a table has further default values, including dynamic values like `CURRENT_TIME`, then those would also be diff --git a/docs/pages/docs/Examples/index.md b/docs/pages/docs/Examples/index.md index a06002580..695666002 100644 --- a/docs/pages/docs/Examples/index.md +++ b/docs/pages/docs/Examples/index.md @@ -1,7 +1,7 @@ --- data: title: "Examples" - weight: 3 + weight: 30 description: Example apps using drift template: layouts/docs/list --- @@ -51,5 +51,5 @@ Additional patterns are also shown and explained on this website: [web_worker]: https://github.com/simolus3/drift/tree/develop/examples/web_worker_example [flutter_web_worker]: https://github.com/simolus3/drift/tree/develop/examples/flutter_web_worker_example [migration]: https://github.com/simolus3/drift/tree/develop/examples/migrations_example -[migration tooling]: {{ '../Advanced Features/migrations.md#verifying-migrations' | pageUrl }} +[migration tooling]: {{ '../Migrations/tests.md#verifying-migrations' | pageUrl }} [with_built_value]: https://github.com/simolus3/drift/tree/develop/examples/with_built_value diff --git a/docs/pages/docs/Examples/relationships.md b/docs/pages/docs/Examples/relationships.md index 57c111742..1f5eba751 100644 --- a/docs/pages/docs/Examples/relationships.md +++ b/docs/pages/docs/Examples/relationships.md @@ -16,7 +16,7 @@ queries in drift. First, we need to store some items that can be bought: We're going to define two tables for shopping carts: One for the cart itself, and another one to store the entries in the cart. -The latter uses [references]({{ '../Getting started/advanced_dart_tables.md#references' | pageUrl }}) +The latter uses [references]({{ '../Dart API/tables.md#references' | pageUrl }}) to express the foreign key constraints of referencing existing shopping carts or product items. diff --git a/docs/pages/docs/Generation options/in_other_builders.md b/docs/pages/docs/Generation options/in_other_builders.md new file mode 100644 index 000000000..0ff83548e --- /dev/null +++ b/docs/pages/docs/Generation options/in_other_builders.md @@ -0,0 +1,63 @@ +--- +data: + title: "Using drift classes in other builders" + description: Configure your build to allow drift dataclasses to be seen by other builders. +template: layouts/docs/single +--- + +It is possible to use classes generated by drift in other builders. +Due to technicalities related to Dart's build system and `source_gen`, this approach requires a custom configuration +and minor code changes. Put this content in a file called `build.yaml` next to your `pubspec.yaml`: + +```yaml +targets: + $default: + # disable the default generators, we'll only use the non-shared drift generator here + auto_apply_builders: false + builders: + drift_dev|not_shared: + enabled: true + # If needed, you can configure the builder like this: + # options: + # skip_verification_code: true + # use_experimental_inference: true + # This builder is necessary for drift-file preprocessing. You can disable it if you're not + # using .drift files with type converters. + drift_dev|preparing_builder: + enabled: true + + run_built_value: + dependencies: ['your_package_name'] + builders: + # Disable drift builders. By default, those would run on each target + drift_dev: + enabled: false + drift_dev|preparing_builder: + enabled: false + # we don't need to disable drift|not_shared, because it's disabled by default +``` + +In all files that use generated drift code, you'll have to replace `part 'filename.g.dart'` with `part 'filename.drift.dart'`. +If you use drift _and_ another builder in the same file, you'll need both `.g.dart` and `.drift.dart` as part-files. + +A full example is available as part of [the drift repo](https://github.com/simolus3/drift/tree/develop/examples/with_built_value). + +If you run into any problems with this approach, feel free to open an issue on drift. + +## The technicalities, explained + +Almost all code generation packages use a so called "shared part file" approach provided by `source_gen`. +It's a common protocol that allows unrelated builders to write into the same `.g.dart` file. +For this to work, each builder first writes a `.part` file with its name. For instance, if you used `drift` +and `built_value` in the same project, those part files could be called `.drift.part` and `.built_value.part`. +Later, the common `source_gen` package would merge the part files into a single `.g.dart` file. + +This works great for most use cases, but a downside is that each builder can't see the final `.g.dart` +file, or use any classes or methods defined in it. To fix that, drift offers an optional builder - +`drift_dev|not_shared` - that will generate a separate part file only containing +code generated by drift. So most of the work resolves around disabling the default generator of drift +and use the non-shared generator instead. + +Finally, we need to the build system to run drift first, and all the other builders otherwise. This is +why we split the builders up into multiple targets. The first target will only run drift, the second +target has a dependency on the first one and will run all the other builders. diff --git a/docs/pages/docs/Advanced Features/builder_options.md b/docs/pages/docs/Generation options/index.md similarity index 57% rename from docs/pages/docs/Advanced Features/builder_options.md rename to docs/pages/docs/Generation options/index.md index 5a22eef19..33ca33f7c 100644 --- a/docs/pages/docs/Advanced Features/builder_options.md +++ b/docs/pages/docs/Generation options/index.md @@ -1,11 +1,12 @@ --- data: - title: "Builder options" - description: >- - Advanced options applied when writing the generated code -template: layouts/docs/single -aliases: - - "options/" + title: Generation options + description: Options for `drift_dev` and `build_runner` to change the generated code. + weight: 7 +template: layouts/docs/list +#path: docs/advanced-features/builder_options/ +#aliases: +# - "options/" --- The `drift_dev` package supports a range of options that control how code @@ -44,7 +45,7 @@ At the moment, drift supports these options: of inserted data and report detailed errors when the integrity is violated. If you're only using inserts with SQL, or don't need this functionality, enabling this flag can help to reduce the amount generated code. -* `use_data_class_name_for_companions`: By default, the name for [companion classes]({{ "../Getting started/writing_queries.md#updates-and-deletes" | pageUrl }}) +* `use_data_class_name_for_companions`: By default, the name for [companion classes]({{ "../Dart API/writes.md#updates-and-deletes" | pageUrl }}) is based on the table name (e.g. a `@DataClassName('Users') class UsersTable extends Table` would generate a `UsersTableCompanion`). With this option, the name is based on the data class (so `UsersCompanion` in this case). @@ -67,15 +68,15 @@ At the moment, drift supports these options: to `null`. * `named_parameters`: Generates named parameters for named variables in SQL queries. * `named_parameters_always_required`: All named parameters (generated if `named_parameters` option is `true`) will be required in Dart. -* `scoped_dart_components` (defaults to `true`): Generates a function parameter for [Dart placeholders]({{ '../Using SQL/drift_files.md#dart-components-in-sql' | pageUrl }}) in SQL. +* `scoped_dart_components` (defaults to `true`): Generates a function parameter for [Dart placeholders]({{ '../SQL API/drift_files.md#dart-components-in-sql' | pageUrl }}) in SQL. The function has a parameter for each table that is available in the query, making it easier to get aliases right when using Dart placeholders. * `store_date_time_values_as_text`: Whether date-time columns should be stored as ISO 8601 string instead of a unix timestamp. - For more information on these modes, see [datetime options]({{ '../Getting started/advanced_dart_tables#datetime-options' | pageUrl }}). + For more information on these modes, see [datetime options]({{ '../Dart API/tables.md#datetime-options' | pageUrl }}). * `case_from_dart_to_sql` (defaults to `snake_case`): Controls how the table and column names are re-cased from the Dart identifiers. The possible values are `preserve`, `camelCase`, `CONSTANT_CASE`, `snake_case`, `PascalCase`, `lowercase` and `UPPERCASE` (default: `snake_case`). * `write_to_columns_mixins`: Whether the `toColumns` method should be written as a mixin instead of being added directly to the data class. - This is useful when using [existing row classes]({{ 'custom_row_classes.md' | pageUrl }}), as the mixin is generated for those as well. + This is useful when using [existing row classes]({{ '../custom_row_classes.md' | pageUrl }}), as the mixin is generated for those as well. * `fatal_warnings`: When enabled (defaults to `false`), warnings found by `drift_dev` in the build process (like syntax errors in SQL queries or unresolved references in your Dart tables) will cause the build to fail. * `preamble`: This option is useful when using drift [as a standalone part builder](#using-drift-classes-in-other-builders) or when running a @@ -174,7 +175,7 @@ We currently support the following extensions: Enabling this option is safe when using a `NativeDatabase` with `sqlite3_flutter_libs`, which compiles sqlite3 with the R*Tree extension enabled. - `moor_ffi`: Enables support for functions that are only available when using a `NativeDatabase`. This contains `pow`, `sqrt` and a variety - of trigonometric functions. Details on those functions are available [here]({{ "../Other engines/vm.md#moor-only-functions" | pageUrl }}). + of trigonometric functions. Details on those functions are available [here]({{ "../Platforms/vm.md#moor-only-functions" | pageUrl }}). - `math`: Assumes that sqlite3 was compiled with [math functions](https://www.sqlite.org/lang_mathfunc.html). This module is largely incompatible with the `moor_ffi` module. - `spellfix1`: Assumes that the [spellfix1](https://www.sqlite.org/spellfix1.html) @@ -226,187 +227,7 @@ We recommend enabling these options. {% endcomment %} However, you can disable some default drift features and reduce the amount of generated code with the following options: -- `skip_verification_code: true`: You can remove a significant portion of generated code with this option. The - downside is that error messages when inserting invalid data will be less specific. +- `skip_verification_code: true`: You can remove a significant portion of generated code with this option. The + downside is that error messages when inserting invalid data will be less specific. - `data_class_to_companions: false`: Don't generate the `toCompanion` method on data classes. If you don't need that method, you can disable this option. - -## Using drift classes in other builders - -It is possible to use classes generated by drift in other builders. -Due to technicalities related to Dart's build system and `source_gen`, this approach requires a custom configuration -and minor code changes. Put this content in a file called `build.yaml` next to your `pubspec.yaml`: - -```yaml -targets: - $default: - # disable the default generators, we'll only use the non-shared drift generator here - auto_apply_builders: false - builders: - drift_dev|not_shared: - enabled: true - # If needed, you can configure the builder like this: - # options: - # skip_verification_code: true - # use_experimental_inference: true - # This builder is necessary for drift-file preprocessing. You can disable it if you're not - # using .drift files with type converters. - drift_dev|preparing_builder: - enabled: true - - run_built_value: - dependencies: ['your_package_name'] - builders: - # Disable drift builders. By default, those would run on each target - drift_dev: - enabled: false - drift_dev|preparing_builder: - enabled: false - # we don't need to disable drift|not_shared, because it's disabled by default -``` - -In all files that use generated drift code, you'll have to replace `part 'filename.g.dart'` with `part 'filename.drift.dart'`. -If you use drift _and_ another builder in the same file, you'll need both `.g.dart` and `.drift.dart` as part-files. - -A full example is available as part of [the drift repo](https://github.com/simolus3/drift/tree/develop/examples/with_built_value). - -If you run into any problems with this approach, feel free to open an issue on drift. - -### The technicalities, explained - -Almost all code generation packages use a so called "shared part file" approach provided by `source_gen`. -It's a common protocol that allows unrelated builders to write into the same `.g.dart` file. -For this to work, each builder first writes a `.part` file with its name. For instance, if you used `drift` -and `built_value` in the same project, those part files could be called `.drift.part` and `.built_value.part`. -Later, the common `source_gen` package would merge the part files into a single `.g.dart` file. - -This works great for most use cases, but a downside is that each builder can't see the final `.g.dart` -file, or use any classes or methods defined in it. To fix that, drift offers an optional builder - -`drift_dev|not_shared` - that will generate a separate part file only containing -code generated by drift. So most of the work resolves around disabling the default generator of drift -and use the non-shared generator instead. - -Finally, we need to the build system to run drift first, and all the other builders otherwise. This is -why we split the builders up into multiple targets. The first target will only run drift, the second -target has a dependency on the first one and will run all the other builders. - -## Modular code generation - -By default, drift generates code from a single entrypoint - all tables, views -and queries for a database are generated into a single part file. -For larger projects, this file can become quite large, slowing down builds and -the analyzer when it is re-generated. -Drift supports an alternative and modular code-generation mode intended as an -alternative for larger projects. -With this setup, drift generates multiple files and automatically manages -imports between them. - -As a motivating example, consider a large drift project with many tables or -views being split across different files: - -``` -lib/src/database/ -├── database.dart -├── tables/ -│ ├── users.drift -│ ├── settings.drift -│ ├── groups.drift -│ └── search.drift -└── views/ - ├── friends.drift - └── categories.dart -``` - -While a modular structure (with `import`s in drift files) is helpful to structure -sources, drift still generates everything into a single `database.g.dart` file. -With a growing number of tables and queries, drift may need to generate tens of -thousands of lines of code for data classes, companions and query results. - -With its modular generation mode, drift instead generates sources for each input -file, like this: - -``` -lib/src/database/ -├── database.dart -├── database.drift.dart -├── tables/ -│ ├── users.drift -│ ├── users.drift.dart -│ ├── settings.drift -│ ├── settings.drift.dart -│ └── ... -└── views/ - ├── friends.drift - ├── friends.drift.dart - ├── categories.dart - └── categories.drift.dart -``` - -### Enabling modular code generation - -_Note_: A small example using modular code generation is also part of [drift's repository](https://github.com/simolus3/drift/tree/develop/examples/modular). - -As drift's modular code generation mode generates different file patterns than -the default builder, it needs to be enabled explicitly. For this, create a -`build.yaml` file in which you disable the default `drift_dev` build and enable -the two builders for modular generation: `drift_dev:analyzer` and -`drift_dev:modular`. They should both get the same options: - -```yaml -targets: - $default: - builders: - drift_dev: - # disable drift's default builder, we're using the modular setup - # instead. - enabled: false - - # Instead, enable drift_dev:analyzer and drift_dev:modular manually: - drift_dev:analyzer: - enabled: true - options: &options - # Drift build options, as per https://drift.simonbinder.eu/docs/advanced-features/builder_options/ - store_date_time_values_as_text: true - named_parameters: true - sql: - dialect: sqlite - options: - version: "3.39" - modules: [fts5] - drift_dev:modular: - enabled: true - # We use yaml anchors to give the two builders the same options - options: *options -``` - -### What gets generated - -With modular generation, drift generates standalone Dart libraries (Dart files -without a `part of` statement). This also means that you no longer need `part` -statements in your sources. Instead, you import the generated `.drift.dart` -files. - -When it comes to using the generated code, not much is different: The API for -the database and DAOs stays mostly the same. -A big exception are how `.drift` files are handled in the modular generation -mode. In the default builder, all queries in all drift files are generated as -methods on the database. -With modular code generation, drift generates an implicit database accessor -reachable through getters from the database class. Consider a file `user.drift` -like this: - -```sql -CREATE TABLE users ( - id INTEGER NOT NULL, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - name TEXT NOT NULL, - is_admin BOOLEAN NOT NULL DEFAULT FALSE -); - -findUsers($predicate = TRUE): SELECT * FROM users WHERE $predicate; -``` - -If such a `users.drift` file is included from a database, we no longer generate -a `findUsers` method for the database itself. -Instead, a `users.drift.dart` file contains a [database accessor]({{ 'daos.md' | pageUrl }}) called `UsersDrift` which is implicitly added to the database. -To call `findUsers`, you'd now call `database.usersDrift.findUsers()`. diff --git a/docs/pages/docs/Generation options/modular.md b/docs/pages/docs/Generation options/modular.md new file mode 100644 index 000000000..2b23f9581 --- /dev/null +++ b/docs/pages/docs/Generation options/modular.md @@ -0,0 +1,125 @@ +--- +data: + title: "Modular code generation" + description: Make drift generate code in multiple files. +template: layouts/docs/single +--- + +By default, drift generates code from a single entrypoint - all tables, views +and queries for a database are generated into a single part file. +For larger projects, this file can become quite large, slowing down builds and +the analyzer when it is re-generated. +Drift supports an alternative and modular code-generation mode intended as an +alternative for larger projects. +With this setup, drift generates multiple files and automatically manages +imports between them. + +As a motivating example, consider a large drift project with many tables or +views being split across different files: + +``` +lib/src/database/ +├── database.dart +├── tables/ +│ ├── users.drift +│ ├── settings.drift +│ ├── groups.drift +│ └── search.drift +└── views/ + ├── friends.drift + └── categories.dart +``` + +While a modular structure (with `import`s in drift files) is helpful to structure +sources, drift still generates everything into a single `database.g.dart` file. +With a growing number of tables and queries, drift may need to generate tens of +thousands of lines of code for data classes, companions and query results. + +With its modular generation mode, drift instead generates sources for each input +file, like this: + +``` +lib/src/database/ +├── database.dart +├── database.drift.dart +├── tables/ +│ ├── users.drift +│ ├── users.drift.dart +│ ├── settings.drift +│ ├── settings.drift.dart +│ └── ... +└── views/ + ├── friends.drift + ├── friends.drift.dart + ├── categories.dart + └── categories.drift.dart +``` + +## Enabling modular code generation + +_Note_: A small example using modular code generation is also part of [drift's repository](https://github.com/simolus3/drift/tree/develop/examples/modular). + +As drift's modular code generation mode generates different file patterns than +the default builder, it needs to be enabled explicitly. For this, create a +`build.yaml` file in which you disable the default `drift_dev` build and enable +the two builders for modular generation: `drift_dev:analyzer` and +`drift_dev:modular`. They should both get the same options: + +```yaml +targets: + $default: + builders: + drift_dev: + # disable drift's default builder, we're using the modular setup + # instead. + enabled: false + + # Instead, enable drift_dev:analyzer and drift_dev:modular manually: + drift_dev:analyzer: + enabled: true + options: &options + # Drift build options, as per https://drift.simonbinder.eu/docs/advanced-features/builder_options/ + store_date_time_values_as_text: true + named_parameters: true + sql: + dialect: sqlite + options: + version: "3.39" + modules: [fts5] + drift_dev:modular: + enabled: true + # We use yaml anchors to give the two builders the same options + options: *options +``` + +## What gets generated + +With modular generation, drift generates standalone Dart libraries (Dart files +without a `part of` statement). This also means that you no longer need `part` +statements in your sources. Instead, you import the generated `.drift.dart` +files. + +When it comes to using the generated code, not much is different: The API for +the database and DAOs stays mostly the same. +A big exception are how `.drift` files are handled in the modular generation +mode. In the default builder, all queries in all drift files are generated as +methods on the database. +With modular code generation, drift generates an implicit database accessor +reachable through getters from the database class. Consider a file `user.drift` +like this: + +```sql +CREATE TABLE users ( + id INTEGER NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + name TEXT NOT NULL, + is_admin BOOLEAN NOT NULL DEFAULT FALSE +); + +findUsers($predicate = TRUE): SELECT * FROM users WHERE $predicate; +``` + +If such a `users.drift` file is included from a database, we no longer generate +a `findUsers` method for the database itself. +Instead, a `users.drift.dart` file contains a [database accessor]({{ '../Dart API/daos.md' | pageUrl }}) called `UsersDrift` which is implicitly added to the database. +To call `findUsers`, you'd now call `database.usersDrift.findUsers()`. diff --git a/docs/pages/docs/Getting started/index.md b/docs/pages/docs/Getting started/index.md deleted file mode 100644 index 5833f9145..000000000 --- a/docs/pages/docs/Getting started/index.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -data: - title: Getting started - description: Simple guide to get a drift project up and running. - weight: 1 - hide_section_index: true -template: layouts/docs/list -aliases: - - /getting-started/ # Used to have this url ---- - -In addition to this document, other resources on how to use drift also exist. -For instance, [this playlist](https://www.youtube.com/watch?v=8ESbEFC0z5Y&list=PLztm2TugcV9Tn6J_H5mtxYIBN40uMAZgO) -or [this older video by Reso Coder](https://www.youtube.com/watch?v=zpWsedYMczM&t=281s) might be for you -if you prefer a tutorial video. - -If you want to look at an example app instead, a cross-platform Flutter app using drift is available -[as part of the drift repository](https://github.com/simolus3/drift/tree/develop/examples/app). - -## Project setup - -{% include "partials/dependencies" %} - -{% assign snippets = 'package:drift_docs/snippets/tables/filename.dart.excerpt.json' | readString | json_decode %} - -### Declaring tables - -Using drift, you can model the structure of your tables with simple dart code. -Let's write a file (simply called `filename.dart` in this snippet) containing -two simple tables and a database class using drift to get started: - -{% include "blocks/snippet" snippets = snippets name = "overview" %} - -__⚠️ Note:__ The column definitions, the table name and the primary key must be known at -compile time. For column definitions and the primary key, the function must use the `=>` -operator and can't contain anything more than what's included in the documentation and the -examples. Otherwise, the generator won't be able to know what's going on. - -## Generating the code - -Drift integrates with Dart's `build` system, so you can generate all the code needed with -`dart run build_runner build`. If you want to continuously rebuild the generated code -where you change your code, run `dart run build_runner watch` instead. -After running either command, drift's generator will have created the following classes for -you: - -1. The `_$MyDatabase` class that your database is defined to extend. It provides access to all - tables and core drift APIs. -2. A data class, `Todo` (for `Todos`) and `Category` (for `Categories`) for each table. It is - used to hold the result of selecting rows from the table. -3. A class which drift calls a "companion" class (`TodosCompanion` and `CategoriesCompanion` - in this example here). - These classes are used to write inserts and updates into the table. These classes make drift - a great match for Dart's null safety feature: In a data class, columns (including those using - auto-incremented integers) can be non-nullable since they're coming from a select. - Since you don't know the value before running an insert though, the companion class makes these - columns optional. - -With the generated code in place, the database can be opened by passing a connection to the superclass, -like this: - -{% include "blocks/snippet" snippets = snippets name = "open" %} - -That's it! You can now use drift by creating an instance of `MyDatabase`. -In a simple app from a `main` entrypoint, this may look like the following: - -{% include "blocks/snippet" snippets = snippets name = "usage" %} - -The articles linked below explain how to use the database in actual, complete -Flutter apps. -A complete example for a Flutter app using drift is also available [here](https://github.com/simolus3/drift/tree/develop/examples/app). - -## Next steps - -Congratulations! You're now ready to use all of drift. See the articles below for further reading. -The ["Writing queries"]({{ "writing_queries.md" | pageUrl }}) article contains everything you need -to know to write selects, updates and inserts in drift! - -{% block "blocks/alert" title="Using the database" %} -The database class from this guide is ready to be used with your app. -For Flutter apps, a Drift database class is typically instantiated at the top of your widget tree -and then passed down with `provider` or `riverpod`. -See [using the database]({{ '../faq.md#using-the-database' | pageUrl }}) for ideas on how to integrate -Drift into your app's state management. - -The setup in this guide uses [platform channels](https://flutter.dev/docs/development/platform-integration/platform-channels), -which are only available after running `runApp` by default. -When using drift before your app is initialized, please call `WidgetsFlutterBinding.ensureInitialized()` before using -the database to ensure that platform channels are ready. -{% endblock %} - -- The articles on [writing queries]({{ 'writing_queries.md' | pageUrl }}) and [Dart tables]({{ 'advanced_dart_tables.md' | pageUrl }}) introduce important concepts of the Dart API used to write queries. -- You can use the same drift database on multiple isolates concurrently - see [Isolates]({{ '../Advanced Features/isolates.md' | pageUrl }}) for more on that. -- Drift has excellent support for custom SQL statements, including a static analyzer and code-generation tools. See [Getting started with sql]({{ 'starting_with_sql.md' | pageUrl }}) - or [Using SQL]({{ '../Using SQL/index.md' | pageUrl }}) for everything there is to know about using drift's SQL-based APIs. -- Something to keep in mind for later: When you change the schema of your database and write migrations, drift can help you make sure they're - correct. Use [runtime checks], which don't require additional setup, or more involved [test utilities] if you want to test migrations between - any schema versions. - -[runtime checks]: {{ '../Advanced Features/migrations.md#verifying-a-database-schema-at-runtime' | pageUrl }} -[test utilities]: {{ '../Advanced Features/migrations.md#verifying-migrations' | pageUrl }} diff --git a/docs/pages/docs/Getting started/starting_with_sql.md b/docs/pages/docs/Getting started/starting_with_sql.md deleted file mode 100644 index 371414181..000000000 --- a/docs/pages/docs/Getting started/starting_with_sql.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -data: - title: "Getting started with sql" - weight: 5 - description: Learn how to get started with the SQL version of drift, or how to migrate an existing project to drift. -template: layouts/docs/single ---- - -The regular [getting started guide]({{ "index.md" | pageUrl }}) explains how to get started with drift by -declaring both tables and queries in Dart. This version will focus on how to use drift with SQL instead. - -A complete cross-platform Flutter app using drift is also available [here](https://github.com/simolus3/drift/tree/develop/examples/app). - -## Adding the dependency - -{% include "partials/dependencies" %} - -## Declaring tables and queries - -To declare tables and queries in sql, create a file called `tables.drift` -next to your Dart files (for instance in `lib/database/tables.drift`). - -You can put `CREATE TABLE` statements for your queries in there. -The following example creates two tables to model a todo-app. If you're -migrating an existing project to drift, you can just copy the `CREATE TABLE` -statements you've already written into this file. - -{% assign drift_snippets = 'package:drift_docs/snippets/drift_files/getting_started/tables.drift.excerpt.json' | readString | json_decode %} - -{% include "blocks/snippet" snippets = drift_snippets name = '(full)' %} - -{% block "blocks/alert" title="On that AS Category" %} -Drift will generate Dart classes for your tables, and the name of those -classes is based on the table name. By default, drift just strips away -the trailing `s` from your table. That works for most cases, but in some -(like the `categories` table above), it doesn't. We'd like to have a -`Category` class (and not `Categorie`) generated, so we tell drift to -generate a different name with the `AS ` declaration at the end. -{% endblock %} - -## Generating matching code - -After you declared the tables, lets generate some Dart code to actually -run them. Drift needs to know which tables are used in a database, so we -have to write a small Dart class that drift will then read. Lets create -a file called `database.dart` next to the `tables.drift` file you wrote -in the previous step. - -{% assign dart_snippets = 'package:drift_docs/snippets/drift_files/getting_started/database.dart.excerpt.json' | readString | json_decode %} - -{% include "blocks/snippet" snippets = dart_snippets name = '(full)' %} - -To generate the `database.g.dart` file which contains the `_$AppDb` -superclass, run `dart run build_runner build` on the command -line. - -## What drift generates - -Let's take a look at what drift generated during the build: - -- Generated data classes (`Todo` and `Category`) - these hold a single - row from the respective table. -- Companion versions of these classes. Those are only relevant when - using the Dart apis of drift, you can [learn more here]({{ "writing_queries.md#inserts" | pageUrl }}). -- A `CountEntriesResult` class, it holds the result rows when running the - `countEntries` query. -- A `_$AppDb` superclass. It takes care of creating the tables when - the database file is first opened. It also contains typesafe methods - for the queries declared in the `tables.drift` file: - - a `Selectable todosInCategory(int)` method, which runs the - `todosInCategory` query declared above. Drift has determined that the - type of the variable in that query is `int`, because that's the type - of the `category` column we're comparing it to. - The method returns a `Selectable` to indicate that it can both be - used as a regular query (`Selectable.get` returns a `Future>`) - or as an auto-updating stream (by using `.watch` instead of `.get()`). - - a `Selectable countEntries()` method, which runs - the other query when used. - -By the way, you can also put insert, update and delete statements in -a `.drift` file - drift will generate matching code for them as well. - -## Learning more - -Now that you know how to use drift together with sql, here are some -further guides to help you learn more: - -- The [SQL IDE]({{ "../Using SQL/sql_ide.md" | pageUrl }}) that provides feedback on sql queries right in your editor. -- [Transactions]({{ "../transactions.md" | pageUrl }}) -- [Schema migrations]({{ "../Advanced Features/migrations.md" | pageUrl }}) -- Writing [queries]({{ "writing_queries.md" | pageUrl }}) and - [expressions]({{ "../Advanced Features/expressions.md" | pageUrl }}) in Dart -- A more [in-depth guide]({{ "../Using SQL/drift_files.md" | pageUrl }}) - on `drift` files, which explains `import` statements and the Dart-SQL interop. - -{% block "blocks/alert" title="Using the database" %} -The database class from this guide is ready to be used with your app. -For Flutter apps, a Drift database class is typically instantiated at the top of your widget tree -and then passed down with `provider` or `riverpod`. -See [using the database]({{ '../faq.md#using-the-database' | pageUrl }}) for ideas on how to integrate -Drift into your app's state management. - -The setup in this guide uses [platform channels](https://flutter.dev/docs/development/platform-integration/platform-channels), -which are only available after running `runApp` by default. -When using drift before your app is initialized, please call `WidgetsFlutterBinding.ensureInitialized()` before using -the database to ensure that platform channels are ready. -{% endblock %} diff --git a/docs/pages/docs/Migrations/api.md b/docs/pages/docs/Migrations/api.md new file mode 100644 index 000000000..a9e788980 --- /dev/null +++ b/docs/pages/docs/Migrations/api.md @@ -0,0 +1,140 @@ +--- +data: + title: "The migrator API" + weight: 50 + description: How to run `ALTER` statements and complex table migrations. +template: layouts/docs/single +--- + +{% assign snippets = 'package:drift_docs/snippets/migrations/migrations.dart.excerpt.json' | readString | json_decode %} + +You can write migrations manually by using `customStatement()` in a migration +callback. However, the callbacks also give you an instance of `Migrator` as a +parameter. This class knows about the target schema of the database and can be +used to create, drop and alter most elements in your schema. + +## Migrating views, triggers and indices + +When changing the definition of a view, a trigger or an index, the easiest way +to update the database schema is to drop and re-create the element. +With the `Migrator` API, this is just a matter of calling `await drop(element)` +followed by `await create(element)`, where `element` is the trigger, view or index +to update. + +Note that the definition of a Dart-defined view might change without modifications +to the view class itself. This is because columns from a table are referenced with +a getter. When renaming a column through `.named('name')` in a table definition +without renaming the getter, the view definition in Dart stays the same but the +`CREATE VIEW` statement changes. + +A headache-free solution to this problem is to just re-create all views in a +migration, for which the `Migrator` provides the `recreateAllViews` method. + +## Complex migrations + +Sqlite has builtin statements for simple changes, like adding columns or dropping entire tables. +More complex migrations require a [12-step procedure](https://www.sqlite.org/lang_altertable.html#otheralter) that +involves creating a copy of the table and copying over data from the old table. +Drift 2.4 introduced the `TableMigration` API to automate most of this procedure, making it easier and safer to use. + +To start the migration, drift will create a new instance of the table with the current schema. Next, it will copy over +rows from the old table. +In most cases, for instance when changing column types, we can't just copy over each row without changing its content. +Here, you can use a `columnTransformer` to apply a per-row transformation. +The `columnTransformer` is a map from columns to the sql expression that will be used to copy the column from the +old table. +For instance, if we wanted to cast a column before copying it, we could use: + +```dart +columnTransformer: { + todos.category: todos.category.cast(), +} +``` + +Internally, drift will use a `INSERT INTO SELECT` statement to copy old data. In this case, it would look like +`INSERT INTO temporary_todos_copy SELECT id, title, content, CAST(category AS INT) FROM todos`. +As you can see, drift will use the expression from the `columnTransformer` map and fall back to just copying the column +otherwise. +If you're introducing new columns in a table migration, be sure to include them in the `newColumns` parameter of +`TableMigration`. Drift will ensure that those columns have a default value or a transformation in `columnTransformer`. +Of course, drift won't attempt to copy `newColumns` from the old table either. + +Regardless of whether you're implementing complex migrations with `TableMigration` or by running a custom sequence +of statements, we strongly recommend to write integration tests covering your migrations. This helps to avoid data +loss caused by errors in a migration. + +Here are some examples demonstrating common usages of the table migration api: + +### Changing the type of a column + +Let's say the `category` column in `Todos` used to be a non-nullable `text()` column that we're now changing to a +nullable int. For simplicity, we assume that `category` always contained integers, they were just stored in a text +column that we now want to adapt. + +```patch +class Todos extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get title => text().withLength(min: 6, max: 10)(); + TextColumn get content => text().named('body')(); +- IntColumn get category => text()(); ++ IntColumn get category => integer().nullable()(); +} +``` + +After re-running your build and incrementing the schema version, you can write a migration: + +{% include "blocks/snippet" snippets = snippets name = 'change_type' %} + +The important part here is the `columnTransformer` - a map from columns to expressions that will +be used to copy the old data. The values in that map refer to the old table, so we can use +`todos.category.cast()` to copy old rows and transform their `category`. +All columns that aren't present in `columnTransformer` will be copied from the old table without +any transformation. + +### Changing column constraints + +When you're changing columns constraints in a way that's compatible to existing data (e.g. changing +non-nullable columns to nullable columns), you can just copy over data without applying any +transformation: + +```dart +await m.alterTable(TableMigration(todos)); +``` + +### Deleting columns + +Deleting a column that's not referenced by a foreign key constraint is easy too: + +```dart +await m.alterTable(TableMigration(yourTable)); +``` + +To delete a column referenced by a foreign key, you'd have to migrate the referencing +tables first. + +### Renaming columns + +If you're renaming a column in Dart, note that the easiest way is to just rename the getter and use +`named`: `TextColumn newName => text().named('old_name')()`. That is fully backwards compatible and +doesn't require a migration. + +If you know your app runs on sqlite 3.25.0 or later (it does if you're using `sqlite3_flutter_libs`), +you can also use the `renameColumn` api in `Migrator`: + +```dart +m.renameColumn(yourTable, 'old_column_name', yourTable.newColumn); +``` + +If you do want to change the actual column name in a table, you can write a `columnTransformer` to +use an old column with a different name: + +```dart +await m.alterTable( + TableMigration( + yourTable, + columnTransformer: { + yourTable.newColumn: const CustomExpression('old_column_name') + }, + ) +) +``` diff --git a/docs/pages/docs/Migrations/exports.md b/docs/pages/docs/Migrations/exports.md new file mode 100644 index 000000000..7b7fc0a8a --- /dev/null +++ b/docs/pages/docs/Migrations/exports.md @@ -0,0 +1,103 @@ +--- +data: + title: "Exporting schemas" + weight: 10 + description: Store all schema versions of your app for validation. +template: layouts/docs/single +--- + +By design, drift's code generator can only see the current state of your database +schema. When you change it, it can be helpful to store a snapshot of the older +schema in a file. +Later, drift tools can take a look at all the schema files to validate the migrations +you write. + +We recommend exporting the initial schema once. Afterwards, each changed schema version +(that is, every time you change the `schemaVersion` in the database) should also be +stored. +This guide assumes a top-level `drift_schemas/` folder in your project to store these +schema files, like this: + +``` +my_app + .../ + lib/ + database/ + database.dart + database.g.dart + test/ + generated_migrations/ + schema.dart + schema_v1.dart + schema_v2.dart + drift_schemas/ + drift_schema_v1.json + drift_schema_v2.json + pubspec.yaml +``` + +Of course, you can also use another folder or a subfolder somewhere if that suits your workflow +better. + +{% block "blocks/alert" title="Examples available" %} +Exporting schemas and generating code for them can't be done with `build_runner` alone, which is +why this setup described here is necessary. + +We hope it's worth it though! Verifying migrations can give you confidence that you won't run +into issues after changing your database. +If you get stuck along the way, don't hesitate to [open a discussion about it](https://github.com/simolus3/drift/discussions). + +Also there are two examples in the drift repository which may be useful as a reference: + +- A [Flutter app](https://github.com/simolus3/drift/tree/latest-release/examples/app) +- An [example specific to migrations](https://github.com/simolus3/drift/tree/latest-release/examples/migrations_example). +{% endblock %} + +## Exporting the schema + +To begin, lets create the first schema representation: + +``` +$ mkdir drift_schemas +$ dart run drift_dev schema dump lib/database/database.dart drift_schemas/ +``` + +This instructs the generator to look at the database defined in `lib/database/database.dart` and extract +its schema into the new folder. + +After making a change to your database schema, you can run the command again. For instance, let's say we +made a change to our tables and increased the `schemaVersion` to `2`. To dump the new schema, just run the +command again: + +``` +$ dart run drift_dev schema dump lib/database/database.dart drift_schemas/ +``` + +You'll need to run this command every time you change the schema of your database and increment the `schemaVersion`. + +Drift will name the files in the folder `drift_schema_vX.json`, where `X` is the current `schemaVersion` of your +database. +If drift is unable to extract the version from your `schemaVersion` getter, provide the full path explicitly: + +``` +$ dart run drift_dev schema dump lib/database/database.dart drift_schemas/drift_schema_v3.json +``` + +{% block "blocks/alert" title=' Dumping a database' color="success" %} +If, instead of exporting the schema of a database class, you want to export the schema of an existing sqlite3 +database file, you can do that as well! `drift_dev schema dump` recognizes a sqlite3 database file as its first +argument and can extract the relevant schema from there. +{% endblock %} + +## What now? + +Having exported your schema versions into files like this, drift tools are able +to generate code aware of multiple schema versions. + +This enables [step-by-step migrations]({{ 'step_by_step.md' | pageUrl }}): Drift +can generate boilerplate code for every schema migration you need to write, so that +you only need to fill in what has actually changed. This makes writing migrations +much easier. + +By knowing all schema versions, drift can also [generate test code]({{'tests.md' | pageUrl}}), +which makes it easy to write unit tests for all your schema migrations. diff --git a/docs/pages/docs/Migrations/index.md b/docs/pages/docs/Migrations/index.md new file mode 100644 index 000000000..445092686 --- /dev/null +++ b/docs/pages/docs/Migrations/index.md @@ -0,0 +1,131 @@ +--- +data: + title: Migrations + description: Tooling and APIs to safely change the schema of your database. + weight: 4 +template: layouts/docs/list +aliases: + - /migrations +--- + +The strict schema of tables and columns is what enables type-safe queries to +the database. +But since the schema is stored in the database too, changing it needs to happen +through migrations developed as part of your app. Drift provides APIs to make most +migrations easy to write, as well as command-line and testing tools to ensure +the migrations are correct. + +{% assign snippets = 'package:drift_docs/snippets/migrations/migrations.dart.excerpt.json' | readString | json_decode %} + +## Manual setup {#basics} + +Drift provides a migration API that can be used to gradually apply schema changes after bumping +the `schemaVersion` getter inside the `Database` class. To use it, override the `migration` +getter. + +Here's an example: Let's say you wanted to add a due date to your todo entries (`v2` of the schema). +Later, you decide to also add a priority column (`v3` of the schema). + +{% include "blocks/snippet" snippets = snippets name = 'table' %} + +We can now change the `database` class like this: + +{% include "blocks/snippet" snippets = snippets name = 'start' %} + +You can also add individual tables or drop them - see the reference of [Migrator](https://pub.dev/documentation/drift/latest/drift/Migrator-class.html) +for all the available options. + +You can also use higher-level query APIs like `select`, `update` or `delete` inside a migration callback. +However, be aware that drift expects the latest schema when creating SQL statements or mapping results. +For instance, when adding a new column to your database, you shouldn't run a `select` on that table before +you've actually added the column. In general, try to avoid running queries in migration callbacks if possible. + +Writing migrations without any tooling support isn't easy. Since correct migrations are +essential for app updates to work smoothly, we strongly recommend using the tools and testing +framework provided by drift to ensure your migrations are correct. +To do that, [export old versions]({{ 'exports.md' | pageUrl }}) to then use easy +[step-by-step migrations]({{ 'step_by_step.md' | pageUrl }}) or [tests]({{ 'tests.md' | pageUrl }}). + +## General tips {#tips} + +To ensure your schema stays consistent during a migration, you can wrap it in a `transaction` block. +However, be aware that some pragmas (including `foreign_keys`) can't be changed inside transactions. +Still, it can be useful to: + +- always re-enable foreign keys before using the database, by enabling them in [`beforeOpen`](#post-migration-callbacks). +- disable foreign-keys before migrations +- run migrations inside a transaction +- make sure your migrations didn't introduce any inconsistencies with `PRAGMA foreign_key_check`. + +With all of this combined, a migration callback can look like this: + +{% include "blocks/snippet" snippets = snippets name = 'structured' %} + +## Post-migration callbacks + +The `beforeOpen` parameter in `MigrationStrategy` can be used to populate data after the database has been created. +It runs after migrations, but before any other query. Note that it will be called whenever the database is opened, +regardless of whether a migration actually ran or not. You can use `details.hadUpgrade` or `details.wasCreated` to +check whether migrations were necessary: + +```dart +beforeOpen: (details) async { + if (details.wasCreated) { + final workId = await into(categories).insert(Category(description: 'Work')); + + await into(todos).insert(TodoEntry( + content: 'A first todo entry', + category: null, + targetDate: DateTime.now(), + )); + + await into(todos).insert( + TodoEntry( + content: 'Rework persistence code', + category: workId, + targetDate: DateTime.now().add(const Duration(days: 4)), + )); + } +}, +``` + +You could also activate pragma statements that you need: + +```dart +beforeOpen: (details) async { + if (details.wasCreated) { + // ... + } + await customStatement('PRAGMA foreign_keys = ON'); +} +``` + +## During development + +During development, you might be changing your schema very often and don't want to write migrations for that +yet. You can just delete your apps' data and reinstall the app - the database will be deleted and all tables +will be created again. Please note that uninstalling is not enough sometimes - Android might have backed up +the database file and will re-create it when installing the app again. + +You can also delete and re-create all tables every time your app is opened, see [this comment](https://github.com/simolus3/drift/issues/188#issuecomment-542682912) +on how that can be achieved. + +## Verifying a database schema at runtime + +Instead (or in addition to) [writing tests](#verifying-migrations) to ensure your migrations work as they should, +you can use a new API from `drift_dev` 1.5.0 to verify the current schema without any additional setup. + +{% assign runtime_snippet = 'package:drift_docs/snippets/migrations/runtime_verification.dart.excerpt.json' | readString | json_decode %} + +{% include "blocks/snippet" snippets = runtime_snippet name = '' %} + +When you use `validateDatabaseSchema`, drift will transparently: + +- collect information about your database by reading from `sqlite3_schema`. +- create a fresh in-memory instance of your database and create a reference schema with `Migrator.createAll()`. +- compare the two. Ideally, your actual schema at runtime should be identical to the fresh one even though it + grew through different versions of your app. + +When a mismatch is found, an exception with a message explaining exactly where another value was expected will +be thrown. +This allows you to find issues with your schema migrations quickly. diff --git a/docs/pages/docs/Migrations/step_by_step.md b/docs/pages/docs/Migrations/step_by_step.md new file mode 100644 index 000000000..cf1eb3abe --- /dev/null +++ b/docs/pages/docs/Migrations/step_by_step.md @@ -0,0 +1,87 @@ +--- +data: + title: "Schema migration helpers" + weight: 20 + description: Use generated code reflecting over all schema versions to write migrations step-by-step. +template: layouts/docs/single +--- + +{% assign snippets = 'package:drift_docs/snippets/migrations/step_by_step.dart.excerpt.json' | readString | json_decode %} + +Database migrations are typically written incrementally, with one piece of code transforming +the database schema to the next version. By chaining these migrations, you can write +schema migrations even for very old app versions. + +Reliably writing migrations between app versions isn't easy though. This code needs to be +maintained and tested, but the growing complexity of the database schema shouldn't make +migrations more complex. +Let's take a look at a typical example making the incremental migrations pattern hard: + +1. In the initial database schema, we have a bunch of tables. +2. In the migration from 1 to 2, we add a column `birthDate` to one of the table (`Users`). +3. In version 3, we realize that we actually don't want to store users at all and delete + the table. + +Before version 3, the only migration could have been written as `m.addColumn(users, users.birthDate)`. +But now that the `Users` table doesn't exist in the source code anymore, that's no longer possible! +Sure, we could remember that the migration from 1 to 2 is now pointless and just skip it if a user +upgrades from 1 to 3 directly, but this adds a lot of complexity. For more complex migration scripts +spanning many versions, this can quickly lead to code that's hard to understand and maintain. + +## Generating step-by-step code + +Drift provides tools to [export old schema versions]({{ 'exports.md' | pageUrl }}). After exporting all +your schema versions, you can use the following command to generate code aiding with the implementation +of step-by-step migrations: + +``` +$ dart run drift_dev schema steps drift_schemas/ lib/database/schema_versions.dart +``` + +The first argument (`drift_schemas/`) is the folder storing exported schemas, the second argument is +the path of the file to generate. Typically, you'd generate a file next to your database class. + +The generated file contains a `stepByStep` method which can be used to write migrations easily: + +{% include "blocks/snippet" snippets = snippets name = 'stepbystep' %} + +`stepByStep` expects a callback for each schema upgrade responsible for running the partial migration. +That callback receives two parameters: A migrator `m` (similar to the regular migrator you'd get for +`onUpgrade` callbacks) and a `schema` parameter that gives you access to the schema at the version you're +migrating to. +For instance, in the `from1To2` function, `schema` provides getters for the database schema at version 2. +The migrator passed to the function is also set up to consider that specific version by default. +A call to `m.recreateAllViews()` would re-create views at the expected state of schema version 2, for instance. + +## Customizing step-by-step migrations + +The `stepByStep` function generated by the `drift_dev schema steps` command gives you an +`OnUpgrade` callback. +But you might want to customize the upgrade behavior, for instance by adding foreign key +checks afterwards (as described in [tips]({{ 'index.md#tips' | pageUrl }})). + +The `Migrator.runMigrationSteps` helper method can be used for that, as this example +shows: + +{% include "blocks/snippet" snippets = snippets name = 'stepbystep2' %} + +Here, foreign keys are disabled before runnign the migration and re-enabled afterwards. +A check ensuring no inconsistencies occurred helps catching issues with the migration +in debug modes. + +## Moving to step-by-step migrations + +If you've been using drift before `stepByStep` was added to the library, or if you've never exported a schema, +you can move to step-by-step migrations by pinning the `from` value in `Migrator.runMigrationSteps` to a known +starting point. + +This allows you to perform all prior migration work to get the database to the "starting" point for +`stepByStep` migrations, and then use `stepByStep` migrations beyond that schema version. + +{% include "blocks/snippet" snippets = snippets name = 'stepbystep3' %} + +Here, we give a "floor" to the `from` value of `2`, since we've performed all other migration work to get to +this point. From now on, you can generate step-by-step migrations for each schema change. + +If you did not do this, a user migrating from schema 1 directly to schema 3 would not properly walk migrations +and apply all migration changes required. diff --git a/docs/pages/docs/Migrations/tests.md b/docs/pages/docs/Migrations/tests.md new file mode 100644 index 000000000..78c538ea8 --- /dev/null +++ b/docs/pages/docs/Migrations/tests.md @@ -0,0 +1,87 @@ +--- +data: + title: "Testing migrations" + weight: 30 + description: Generate test code to write unit tests for your migrations. +template: layouts/docs/single +--- + +{% assign snippets = 'package:drift_docs/snippets/migrations/tests/schema_test.dart.excerpt.json' | readString | json_decode %} +{% assign verify = 'package:drift_docs/snippets/migrations/tests/verify_data_integrity_test.dart.excerpt.json' | readString | json_decode %} + +While migrations can be written manually without additional help from drift, dedicated tools testing +your migrations help to ensure that they are correct and aren't loosing any data. + +Drift's migration tooling consists of the following steps: + +1. After each change to your schema, use a tool to export the current schema into a separate file. +2. Use a drift tool to generate test code able to verify that your migrations are bringing the database + into the expected schema. +3. Use generated code to make writing schema migrations easier. + +This page describes steps 2 and 3. It assumes that you're already following step 1 by +[exporting your schema]({{ 'exports.md' }}) when it changes. + +## Writing tests + +After you've exported the database schemas into a folder, you can generate old versions of your database class +based on those schema files. +For verifications, drift will generate a much smaller database implementation that can only be used to +test migrations. + +You can put this test code whereever you want, but it makes sense to put it in a subfolder of `test/`. +If we wanted to write them to `test/generated_migrations/`, we could use + +``` +$ dart run drift_dev schema generate drift_schemas/ test/generated_migrations/ +``` + +After that setup, it's finally time to write some tests! For instance, a test could look like this: + +{% include "blocks/snippet" snippets = snippets name = 'setup' %} + +In general, a test looks like this: + +1. Use `verifier.startAt()` to obtain a [connection](https://drift.simonbinder.eu/api/drift/databaseconnection-class) + to a database with an initial schema. + This database contains all your tables, indices and triggers from that version, created by using `Migrator.createAll`. +2. Create your application database with that connection. For this, create a constructor in your database class that + accepts a `QueryExecutor` and forwards it to the super constructor in `GeneratedDatabase`. + Then, you can pass the result of calling `newConnection()` to that constructor to create a test instance of your + datbaase. +3. Call `verifier.migrateAndValidate(db, version)`. This will initiate a migration towards the target version (here, `2`). + Unlike the database created by `startAt`, this uses the migration logic you wrote for your database. + +`migrateAndValidate` will extract all `CREATE` statement from the `sqlite_schema` table and semantically compare them. +If it sees anything unexpected, it will throw a `SchemaMismatch` exception to fail your test. + +{% block "blocks/alert" title="Writing testable migrations" %} +To test migrations _towards_ an old schema version (e.g. from `v1` to `v2` if your current version is `v3`), +you're `onUpgrade` handler must be capable of upgrading to a version older than the current `schemaVersion`. +For this, check the `to` parameter of the `onUpgrade` callback to run a different migration if necessary. +Or, use [step-by-step migrations]({{ 'step_by_step.md' | pageUrl }}) which do this automatically. +{% endblock %} + +## Verifying data integrity + +In addition to the changes made in your table structure, its useful to ensure that data that was present before a migration +is still there after it ran. +You can use `schemaAt` to obtain a raw `Database` from the `sqlite3` package in addition to a connection. +This can be used to insert data before a migration. After the migration ran, you can then check that the data is still there. + +Note that you can't use the regular database class from you app for this, since its data classes always expect the latest +schema. However, you can instruct drift to generate older snapshots of your data classes and companions for this purpose. +To enable this feature, pass the `--data-classes` and `--companions` command-line arguments to the `drift_dev schema generate` +command: + +``` +$ dart run drift_dev schema generate --data-classes --companions drift_schemas/ test/generated_migrations/ +``` + +Then, you can import the generated classes with an alias: + +{% include "blocks/snippet" snippets = verify name = 'imports' %} + +This can then be used to manually create and verify data at a specific version: + +{% include "blocks/snippet" snippets = verify name = 'main' %} diff --git a/docs/pages/docs/Other engines/index.md b/docs/pages/docs/Other engines/index.md deleted file mode 100644 index 4cb529880..000000000 --- a/docs/pages/docs/Other engines/index.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -data: - title: "Other engines" - description: "Use drift on the web or other platforms" - weight: 100 -template: layouts/docs/list ---- diff --git a/docs/pages/docs/Other engines/encryption.md b/docs/pages/docs/Platforms/encryption.md similarity index 92% rename from docs/pages/docs/Other engines/encryption.md rename to docs/pages/docs/Platforms/encryption.md index 0cacef60c..7e66ca07e 100644 --- a/docs/pages/docs/Other engines/encryption.md +++ b/docs/pages/docs/Platforms/encryption.md @@ -2,10 +2,11 @@ data: title: Encryption description: Use drift on encrypted databases + weight: 10 template: layouts/docs/single --- -{% assign snippets = 'package:drift_docs/snippets/encryption.dart.excerpt.json' | readString | json_decode %} +{% assign snippets = 'package:drift_docs/snippets/platforms/encryption.dart.excerpt.json' | readString | json_decode %} There are two ways to use drift on encrypted databases. The `encrypted_drift` package is similar to `drift_sqflite` and uses a platform plugin written in @@ -72,13 +73,14 @@ of the regular `libsqlite3.so`: {% include "blocks/snippet" snippets = snippets name = "setup" %} When using drift on a background database, you need to call `setupSqlCipher` on the background isolate -as well. +as well. With `NativeDatabase.createInBackground`, which are using isolates internally, you can use +the `setupIsolate` callback to do this - the examples on this page use this as well. On iOS and macOS, no additional setup is necessary - simply depend on `sqlcipher_flutter_libs`. On Windows and Linux, you currently have to include a version of SQLCipher manually when you distribute your app. -For more information on this, you can use the documentation [here]({{ '../platforms.md#bundling-sqlite-with-your-app' | pageUrl }}). +For more information on this, you can use the documentation [here]({{ '../Platforms/index.md#bundling-sqlite-with-your-app' | pageUrl }}). Instead of including `sqlite3.dll` or `libsqlite3.so`, you'd include the respective versions of SQLCipher. diff --git a/docs/pages/docs/platforms.md b/docs/pages/docs/Platforms/index.md similarity index 89% rename from docs/pages/docs/platforms.md rename to docs/pages/docs/Platforms/index.md index f7896fd03..8da5e63c0 100644 --- a/docs/pages/docs/platforms.md +++ b/docs/pages/docs/Platforms/index.md @@ -2,7 +2,8 @@ data: title: "Supported platforms" description: All platforms supported by drift, and how to use them -template: layouts/docs/single + weight: 8 +template: layouts/docs/list --- Being built on top of the sqlite3 database, drift can run on almost every Dart platform. @@ -14,7 +15,8 @@ To achieve platform independence, drift separates its core apis from a platform- database implementation. The core apis are pure-Dart and run on all Dart platforms, even outside of Flutter. When writing drift apps, prefer to mainly use the apis in `package:drift/drift.dart` as they are guaranteed to work across all platforms. -Depending on your platform, you can choose a different `QueryExecutor`. +Depending on your platform, you can choose a different `QueryExecutor` - the interface +binding the core drift library with native databases. ## Overview @@ -23,8 +25,8 @@ This table list all supported drift implementations and on which platforms they | Implementation | Supported platforms | Notes | |----------------|---------------------|-------| | `SqfliteQueryExecutor` from `package:drift_sqflite` | Android, iOS | Uses platform channels, Flutter only, no isolate support, doesn't support `flutter test`. Formerly known as `moor_flutter` | -| `NativeDatabase` from `package:drift/native.dart` | Android, iOS, Windows, Linux, macOS | No further setup is required for Flutter users. For support outside of Flutter, or in `flutter test`, see the [desktop](#desktop) section below. Usage in a [isolate]({{ 'Advanced Features/isolates.md' | pageUrl }}) is recommended. Formerly known as `package:moor/ffi.dart`. | -| `WasmDatabase` from `package:drift/wasm.dart` | Web | Works with or without Flutter. A bit of [additional setup]({{ 'Other engines/web.md' | pageUrl }}) is required. | +| `NativeDatabase` from `package:drift/native.dart` | Android, iOS, Windows, Linux, macOS | No further setup is required for Flutter users. For support outside of Flutter, or in `flutter test`, see the [desktop](#desktop) section below. Usage in a [isolate]({{ '../isolates.md' | pageUrl }}) is recommended. Formerly known as `package:moor/ffi.dart`. | +| `WasmDatabase` from `package:drift/wasm.dart` | Web | Works with or without Flutter. A bit of [additional setup]({{ 'web.md' | pageUrl }}) is required. | | `WebDatabase` from `package:drift/web.dart` | Web | Deprecated in favor of `WasmDatabase`. | To support all platforms in a shared codebase, you only need to change how you open your database, all other usages can stay the same. @@ -47,7 +49,7 @@ is maintaned and supported too. ### using `drift/native` The new `package:drift/native.dart` implementation uses `dart:ffi` to bind to sqlite3's native C apis. -This is the recommended approach for newer projects as described in the [getting started]({{ "Getting started/index.md" | pageUrl }}) guide. +This is the recommended approach for newer projects as described in the [getting started]({{ "../setup.md" | pageUrl }}) guide. To ensure that your app ships with the latest sqlite3 version, also add a dependency to the `sqlite3_flutter_libs` package when using `package:drift/native.dart`! @@ -73,12 +75,12 @@ However, there are some smaller issues on some devices that you should be aware ## Web -_Main article: [Web]({{ "Other engines/web.md" | pageUrl }})_ +_Main article: [Web]({{ "web.md" | pageUrl }})_ -For apps that run on the web, you can use drift's experimental web implementation, located -in `package:drift/web.dart`. -As it binds to [sql.js](https://github.com/sql-js/sql.js), special setup is required. Please -read the main article for details. +Drift runs on the web by compiling sqlite3 to a WebAssembly module. This database +can be accessed using a `WasmDatabase` in `package:drift/wasm.dart`. +For optimal support across different browsers, a worker script and some additional +setup is required. The main article explains how to set up drift to work on the web. ## Desktop @@ -138,11 +140,11 @@ install the dynamic library for `sqlite` next to your application executable. This example shows how to do that on Linux, by using a custom `sqlite3.so` that we assume lives next to your application: -{% assign snippets = 'package:drift_docs/snippets/platforms.dart.excerpt.json' | readString | json_decode %} +{% assign snippets = 'package:drift_docs/snippets/platforms/platforms.dart.excerpt.json' | readString | json_decode %} {% include "blocks/snippet" snippets = snippets %} Be sure to use drift _after_ you set the platform-specific overrides. -When you use drift in [another isolate]({{ 'Advanced Features/isolates.md' | pageUrl }}), +When you use drift in [another isolate]({{ '../isolates.md' | pageUrl }}), you'll also need to apply the opening overrides on that background isolate. You can call them in the isolate's entrypoint before using any drift apis. diff --git a/docs/pages/docs/Other engines/vm.md b/docs/pages/docs/Platforms/vm.md similarity index 97% rename from docs/pages/docs/Other engines/vm.md rename to docs/pages/docs/Platforms/vm.md index 9ec3457b7..7064794d4 100644 --- a/docs/pages/docs/Other engines/vm.md +++ b/docs/pages/docs/Platforms/vm.md @@ -2,6 +2,7 @@ data: title: Native Drift (Desktop support) description: Run drift on both mobile and desktop + weight: 1 template: layouts/docs/single --- @@ -112,7 +113,7 @@ The chosen options help reduce binary size by removing features not used by drif - __SQLITE_DQS=0__: This will make sqlite not accept double-quoted strings (and instead parse them as identifiers). This matches the behavior of drift and compiled queries - __SQLITE_THREADSAFE=0__: Since the majority of Flutter apps only use one isolate, thread safety is turned off. Note that you - can still use the [isolate api]({{"../Advanced Features/isolates.md" | pageUrl}}) for background operations. As long as all + can still use the [isolate api]({{"../isolates.md" | pageUrl}}) for background operations. As long as all database accesses happen from the same thread, there's no problem. - SQLITE_DEFAULT_MEMSTATUS=0: The `sqlite3_status()` interfaces are not exposed by drift, so there's no point of having them. - SQLITE_MAX_EXPR_DEPTH=0: Disables maximum depth when sqlite parses expressions, which can make the parser faster. @@ -141,8 +142,8 @@ The `NativeDatabase` includes additional sql functions not available in standard Note that `NaN`, `-infinity` or `+infinity` are represented as `NULL` in sql. -When enabling the `moor_ffi` module in your [build options]({{ "../Advanced Features/builder_options.md#available-extensions" | pageUrl }}), -the generator will allow you to use those functions in drift files or compiled queries. +When enabling the `moor_ffi` module in your [build options]({{ "../Generation options/index.md#available-extensions" | pageUrl }}), +the generator will allow you to use those functions in drift files or compiled queries. To use those methods from Dart, you need to import `package:drift/extensions/native.dart`. You can then use the additional functions like this: diff --git a/docs/pages/docs/Other engines/web.md b/docs/pages/docs/Platforms/web.md similarity index 97% rename from docs/pages/docs/Other engines/web.md rename to docs/pages/docs/Platforms/web.md index c13528b78..1e315348a 100644 --- a/docs/pages/docs/Other engines/web.md +++ b/docs/pages/docs/Platforms/web.md @@ -2,6 +2,7 @@ data: title: Web description: Drift support in Flutter and Dart web apps. + weight: 2 template: layouts/docs/single path: web/ --- @@ -12,7 +13,7 @@ The `WasmDatabase.open` API is the preferred way to run drift on the web. While APIs continue to work, using the stable API will bring performance and safety benefits. {% endblock %} -{% assign snippets = "package:drift_docs/snippets/engines/web.dart.excerpt.json" | readString | json_decode %} +{% assign snippets = "package:drift_docs/snippets/platforms/web.dart.excerpt.json" | readString | json_decode %} Using modern browser APIs such as WebAssembly and the Origin-Private File System API, you can use drift databases for the web version of your Flutter and Dart applications. @@ -128,7 +129,7 @@ to another (potentially slower) implementation in that case. ### Setup in Dart From a perspective of the Dart code used, drift on the web is similar to drift on other platforms. -You can follow the [getting started guide]({{ '../Getting started/index.md' | pageUrl }}) as a general setup guide. +You can follow the [getting started guide]({{ '../setup.md' | pageUrl }}) as a general setup guide. Instead of using a `NativeDatabase` in your database classes, you can use the `WasmDatabase` optimized for the web: @@ -253,7 +254,7 @@ If you want to instead compile these yourself, this section describes how to do The web worker is written in Dart - the entrypoint is stable and part of drift's public API. To compile a worker suitable for `WasmDatabase.open`, create a new Dart file that calls `WasmDatabase.workerMainForOpen`: -{% assign worker = "package:drift_docs/snippets/engines/stable_worker.dart.excerpt.json" | readString | json_decode %} +{% assign worker = "package:drift_docs/snippets/platforms/stable_worker.dart.excerpt.json" | readString | json_decode %} {% include "blocks/snippet" snippets = worker %} The JavaScript file included in drift releases is compiled with `dart compile js -O4 web/drift_worker.dart`. @@ -354,7 +355,7 @@ Drift will automatically migrate data from local storage to `IndexedDb` when it #### Using web workers -You can offload the database to a background thread by using +You can offload the database to a background thread by using [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API). Drift also supports [shared workers](https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker), which allows you to seamlessly synchronize query-streams and updates across multiple tabs! @@ -369,7 +370,7 @@ A Flutter port of this example is [part of the drift repository](https://github. To write a web worker that will serve requests for drift, create a file called `worker.dart` in the `web/` folder of your app. It could have the following content: -{% assign workers = 'package:drift_docs/snippets/engines/workers.dart.excerpt.json' | readString | json_decode %} +{% assign workers = 'package:drift_docs/snippets/platforms/workers.dart.excerpt.json' | readString | json_decode %} {% include "blocks/snippet" snippets = workers name = "worker" %} @@ -382,7 +383,7 @@ you can connect like this: You can then open a drift database with that connection. For more information on the `DatabaseConnection` class, see the documentation on -[isolates]({{ "../Advanced Features/isolates.md" | pageUrl }}). +[isolates]({{ "../isolates.md" | pageUrl }}). A small, but working example is available under [examples/web_worker_example](https://github.com/simolus3/drift/tree/develop/examples/web_worker_example) in the drift repository. diff --git a/docs/pages/docs/Using SQL/custom_queries.md b/docs/pages/docs/SQL API/custom_queries.md similarity index 97% rename from docs/pages/docs/Using SQL/custom_queries.md rename to docs/pages/docs/SQL API/custom_queries.md index caa38e5d2..5e2304cc7 100644 --- a/docs/pages/docs/Using SQL/custom_queries.md +++ b/docs/pages/docs/SQL API/custom_queries.md @@ -63,7 +63,7 @@ name you specified. {% endblock %} You can also use `UPDATE` or `DELETE` statements here. Of course, this feature is also available for -[daos]({{ "../Advanced Features/daos.md" | pageUrl }}), +[daos]({{ "../Dart API/daos.md" | pageUrl }}), and it perfectly integrates with auto-updating streams by analyzing what tables you're reading from or writing to. @@ -72,7 +72,7 @@ If you don't want to use the statements with an generated api, you can still send custom queries by calling `customSelect` for a one-time query or `customSelectStream` for a query stream that automatically emits a new set of items when the underlying data changes. Using the todo example introduced in the -[getting started guide]({{ "../Getting started/index.md" | pageUrl }}), we can +[getting started guide]({{ "../setup.md" | pageUrl }}), we can write this query which will load the amount of todo entries in each category: {% include "blocks/snippet" snippets = snippets name = "manual" %} diff --git a/docs/pages/docs/Using SQL/drift_files.md b/docs/pages/docs/SQL API/drift_files.md similarity index 95% rename from docs/pages/docs/Using SQL/drift_files.md rename to docs/pages/docs/SQL API/drift_files.md index b6d67d7b4..63d04409e 100644 --- a/docs/pages/docs/Using SQL/drift_files.md +++ b/docs/pages/docs/SQL API/drift_files.md @@ -81,7 +81,7 @@ named parameters. To do so, add a `REQUIRED` keyword: {% include "blocks/snippet" snippets = small name = "q3" %} Note that this only has an effect when the `named_parameters` -[build option]({{ '../Advanced Features/builder_options.md' | pageUrl }}) is +[build option]({{ '../Generation options/index.md' | pageUrl }}) is enabled. Further, non-nullable variables are required by default. ### Arrays @@ -138,12 +138,12 @@ CREATE TABLE tasks ( ); ``` -More information on storing enums is available [in the page on type converters]({{ '../Advanced Features/type_converters.md#using-converters-in-moor' | pageUrl }}). +More information on storing enums is available [in the page on type converters]({{ '../type_converters.md#using-converters-in-moor' | pageUrl }}). Instead of using an integer mapping enums by their index, you can also store them by their name. For this, use `ENUMNAME(...)` instead of `ENUM(...)`. For details on all supported types, and information on how to switch between the -datetime modes, see [this section]({{ '../Getting started/advanced_dart_tables.md#supported-column-types' | pageUrl }}). +datetime modes, see [this section]({{ '../Dart API/tables.md#supported-column-types' | pageUrl }}). The additional drift-specific types (`BOOLEAN`, `DATETIME`, `ENUM` and `ENUMNAME`) are also supported in `CAST` expressions, which is helpful for views: @@ -303,7 +303,7 @@ can be used to construct dynamic filters at runtime: This lets you write a single SQL query and dynamically apply a predicate at runtime! This feature works for -- [expressions]({{ "../Advanced Features/expressions.md" | pageUrl }}), as you've seen in the example above +- [expressions]({{ "../Dart API/expressions.md" | pageUrl }}), as you've seen in the example above - single ordering terms: `SELECT * FROM todos ORDER BY $term, id ASC` will generate a method taking an `OrderingTerm`. - whole order-by clauses: `SELECT * FROM todos ORDER BY $order` @@ -320,7 +320,7 @@ default SQL value (here, `TRUE`) when not explicitly set. ### Type converters -You can import and use [type converters]({{ "../Advanced Features/type_converters.md" | pageUrl }}) +You can import and use [type converters]({{ "../type_converters.md" | pageUrl }}) written in Dart in a drift file. Importing a Dart file works with a regular `import` statement. To apply a type converter on a column definition, you can use the `MAPPED BY` column constraints: @@ -346,9 +346,9 @@ FROM users; ``` More details on type converts in drift files are available -[here]({{ "../Advanced Features/type_converters.md#using-converters-in-moor" | pageUrl }}). +[here]({{ "../type_converters.md#using-converters-in-moor" | pageUrl }}). -When using type converters, we recommend the [`apply_converters_on_variables`]({{ "../Advanced Features/builder_options.md" | pageUrl }}) +When using type converters, we recommend the [`apply_converters_on_variables`]({{ "../Generation options/index.md" | pageUrl }}) build option. This will also apply the converter from Dart to SQL, for instance if used on variables: `SELECT * FROM users WHERE preferences = ?`. With that option, the variable will be inferred to `Preferences` instead of `String`. @@ -380,7 +380,7 @@ CREATE TABLE users ( When using custom row classes defined in another Dart file, you also need to import that file into the file where you define the database. -For more general information on this feature, please check [this page]({{ '../Advanced Features/custom_row_classes.md' | pageUrl }}). +For more general information on this feature, please check [this page]({{ '../custom_row_classes.md' | pageUrl }}). Custom row classes can be applied to `SELECT` queries defined a `.drift` file. To use a custom row class, the `WITH` syntax can be added after the name of the query. @@ -421,7 +421,7 @@ Internally, drift will then generate query code to map the row to an instance of `UserWithFriends` class. For a more complete overview of using custom row classes for queries, see -[the section for queries]({{ '../Advanced Features/custom_row_classes.md#queries' | pageUrl }}). +[the section for queries]({{ '../custom_row_classes.md#queries' | pageUrl }}). ### Dart documentation comments diff --git a/docs/pages/docs/Using SQL/extensions.md b/docs/pages/docs/SQL API/extensions.md similarity index 73% rename from docs/pages/docs/Using SQL/extensions.md rename to docs/pages/docs/SQL API/extensions.md index 934118e7c..8af50e742 100644 --- a/docs/pages/docs/Using SQL/extensions.md +++ b/docs/pages/docs/SQL API/extensions.md @@ -2,18 +2,23 @@ data: title: "Supported sqlite extensions" weight: 10 - description: Information on json1 and fts5 support in the generator + description: Information on json1 and fts5 support in drift files template: layouts/docs/single --- -_Note_: Since `drift_sqflite` and `moor_flutter` uses the sqlite version shipped on the device, these extensions might not -be available on all devices. When using these extensions, using a `NativeDatabase` is strongly recommended. -This enables the extensions listed here on all Android and iOS devices. +When analyzing `.drift` files, the generator can consider sqlite3 extensions +that might be present. +The generator can't know about the sqlite3 library your database is talking to +though, so it makes a pessimistic assumption of using an old sqlite3 version +without any enabled extensions by default. +When using a package like `sqlite3_flutter_libs`, you get the latest sqlite3 +version with the json1 and fts5 extensions enabled. You can inform the generator +about this by using [build options]({{ "../Generation options/index.md" | pageUrl }}). ## json1 To enable the json1 extension in drift files and compiled queries, modify your -[build options]({{ "../Advanced Features/builder_options.md" | pageUrl }}) to include +[build options]({{ "../Generation options/index.md" | pageUrl }}) to include `json1` in the `sqlite_module` section. The sqlite extension doesn't require any special tables and works on all text columns. In drift @@ -42,7 +47,7 @@ class Database extends _$Database { return phoneNumber.equals(number); }) ).get(); - } + } } ``` @@ -51,8 +56,8 @@ You can learn more about the json1 extension on [sqlite.org](https://www.sqlite. ## fts5 The fts5 extension provides full-text search capabilities in sqlite tables. -To enable the fts5 extension in drift files and compiled queries, modify the -[build options]({{ "../Advanced Features/builder_options.md" | pageUrl }}) to include +To enable the fts5 extension in drift files and compiled queries, modify the +[build options]({{ "../Generation options/index.md" | pageUrl }}) to include `fts5` in the `sqlite_module` section. Just like you'd expect when using sqlite, you can create a fts5 table in a drift file diff --git a/docs/pages/docs/SQL API/index.md b/docs/pages/docs/SQL API/index.md new file mode 100644 index 000000000..7611c1b18 --- /dev/null +++ b/docs/pages/docs/SQL API/index.md @@ -0,0 +1,51 @@ +--- +data: + title: Verified SQL + description: Define your database and queries in SQL without giving up on type-safety. + weight: 3 +template: layouts/docs/list +--- + +Drift provides a [Dart API]({{ '../Dart API/index.md' | pageUrl }}) to define tables and +to write SQL queries. +Especially when you are already familiar with SQL, it might be easier to define your +tables directly in SQL, with `CREATE TABLE` statements. +Thanks to a powerful SQL parser and analyzer built into drift, you can still run type-safe +SQL queries with support for auto-updating streams and all the other drift features. +The validity of your SQL is checked at build time, with drift generating matching methods +for each table and SQL statement. + +## Setup + +The basic setup of adding the drift dependencies matches the setup for the Dart APIs. It +is described in the [setup page]({{ '../setup.md' | pageUrl }}). + +What's different is how tables and queries are declared. For SQL to be recognized by drift, +it needs to be put into a `.drift` file. In this example, we use a `.drift` file next to the +database class named `tables.drift`: + +{% assign drift_snippets = 'package:drift_docs/snippets/drift_files/getting_started/tables.drift.excerpt.json' | readString | json_decode %} + +{% include "blocks/snippet" snippets = drift_snippets name = '(full)' %} + +{% block "blocks/alert" title="On that AS Category" %} +Drift will generate Dart classes for your tables, and the name of those +classes is based on the table name. By default, drift just strips away +the trailing `s` from your table. That works for most cases, but in some +(like the `categories` table above), it doesn't. We'd like to have a +`Category` class (and not `Categorie`) generated, so we tell drift to +generate a different name with the `AS ` declaration at the end. +{% endblock %} + +Integrating drift files into the database simple, they just need to be added to the +`include` parameter of the `@DriftDatabase` annotation. The `tables` parameter can +be omitted here, since there are no Dart-defined tables to be added to the database. + +{% assign dart_snippets = 'package:drift_docs/snippets/drift_files/getting_started/database.dart.excerpt.json' | readString | json_decode %} + +{% include "blocks/snippet" snippets = dart_snippets name = '(full)' %} + +To generate the `database.g.dart` file which contains the `_$AppDb` +superclass, run `dart run build_runner build` on the command +line. + diff --git a/docs/pages/docs/Using SQL/sql_ide.md b/docs/pages/docs/SQL API/sql_ide.md similarity index 100% rename from docs/pages/docs/Using SQL/sql_ide.md rename to docs/pages/docs/SQL API/sql_ide.md diff --git a/docs/pages/docs/Using SQL/index.md b/docs/pages/docs/Using SQL/index.md deleted file mode 100644 index 2be59fde3..000000000 --- a/docs/pages/docs/Using SQL/index.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -data: - title: "Using SQL" - weight: 30 - description: Write typesafe sql with drift -template: layouts/docs/list ---- - -Drift lets you express a variety of queries in pure Dart. However, you don't have to miss out -on its features when you need more complex queries or simply prefer sql. Drift has a builtin -sql parser and analyzer, so it can generate a typesafe API for sql statements you write. -It can also warn about errors in your sql at build time. \ No newline at end of file diff --git a/docs/pages/docs/community_tools.md b/docs/pages/docs/community_tools.md index 0e2d6f256..57c0d0184 100644 --- a/docs/pages/docs/community_tools.md +++ b/docs/pages/docs/community_tools.md @@ -1,6 +1,7 @@ --- data: title: "Community" + weight: 50 description: Packages contributed by the community template: layouts/docs/single --- diff --git a/docs/pages/docs/Advanced Features/custom_row_classes.md b/docs/pages/docs/custom_row_classes.md similarity index 97% rename from docs/pages/docs/Advanced Features/custom_row_classes.md rename to docs/pages/docs/custom_row_classes.md index 51e989adf..4d57e10fa 100644 --- a/docs/pages/docs/Advanced Features/custom_row_classes.md +++ b/docs/pages/docs/custom_row_classes.md @@ -1,12 +1,12 @@ --- data: title: "Custom row classes" + weight: 6 description: >- Use your own classes as data classes for drift tables template: layouts/docs/single --- - For each table declared in Dart or in a drift file, `drift_dev` generates a row class (sometimes also referred to as _data class_) to hold a full row and a companion class for updates and inserts. This works well for most cases: Drift knows what columns your table has, and it can generate a simple class for all of that. @@ -167,10 +167,10 @@ For your convenience, drift is using different generation strategies even for qu an existing row class. It is helpful to enumerate them because they affect the allowed type for fields in existing types as well. -1. Nested tables: When the [`SELECT table.**` syntax]({{ '../Using SQL/drift_files.md#nested-results' | pageUrl }}) +1. Nested tables: When the [`SELECT table.**` syntax]({{ 'SQL API/drift_files.md#nested-results' | pageUrl }}) is used in a query, drift will pack columns from `table` into a nested object instead of generating fields for every column. -2. Nested list results: The [`LIST()` macro]({{ '../Using SQL/drift_files.md#list-subqueries' | pageUrl }}) +2. Nested list results: The [`LIST()` macro]({{ 'SQL API/drift_files.md#list-subqueries' | pageUrl }}) can be used to expose results of a subquery as a list. 3. Single-table results: When a select statement reads all columns from a table (and no additional columns), like in `SELECT * FROM table`, drift will use the data class of the table instead of generating a new one. diff --git a/docs/pages/docs/faq.md b/docs/pages/docs/faq.md index ca2ea531c..4050a7513 100644 --- a/docs/pages/docs/faq.md +++ b/docs/pages/docs/faq.md @@ -1,13 +1,13 @@ --- data: title: "Frequently asked questions" - + weight: 25 path: faq/ template: layouts/docs/single --- ## Using the database -If you've created a `MyDatabase` class by following the [getting started guide]({{ "Getting started/index.md" | pageUrl }}), you +If you've created a `MyDatabase` class by following the [getting started guide]({{ "setup.md" | pageUrl }}), you still need to somehow obtain an instance of it. It's recommended to only have one (singleton) instance of your database, so you could store that instance in a global variable: @@ -55,7 +55,7 @@ in your favorite dependency injection framework for flutter hence solves this pr ## Why am I getting no such table errors? -If you add another table after your app has already been installed, you need to write a [migration]({{ "Advanced Features/migrations.md" | pageUrl }}) +If you add another table after your app has already been installed, you need to write a [migration]({{ "Migrations/index.md" | pageUrl }}) that covers creating that table. If you're in the process of developing your app and want to use un- and reinstall your app instead of writing migrations, that's fine too. Please note that your apps data might be backed up on Android, so manually deleting your app's data instead of a reinstall is necessary on some devices. @@ -80,7 +80,7 @@ you can set to `true`. When enabled, drift will print the statements it runs. ## How do I insert data on the first app start? -You can populate the database on the first start of your app with a custom [migration strategy]({{ 'Advanced Features/migrations.md' | pageUrl }}). +You can populate the database on the first start of your app with a custom [migration strategy]({{ 'Migrations/index.md' | pageUrl }}). To insert data when the database is created (which usually happens when the app is first run), you can use this: ```dart @@ -142,13 +142,13 @@ result of your queries. ### floor Floor also has a lot of convenience features like auto-updating queries and schema migrations. Similar to drift, you define the structure of your database in Dart. Then, you have write queries in sql - the mapping code if generated -by floor. Drift has a [similar feature]({{ "Using SQL/custom_queries.md" | pageUrl }}), but it can also verify that your queries are valid at compile time. Drift +by floor. Drift has a [similar feature]({{ "SQL API/custom_queries.md" | pageUrl }}), but it can also verify that your queries are valid at compile time. Drift additionally has an api that lets you write some queries in Dart instead of sql. A difference between these two is that Floor lets you write your own classes and generates mapping code around that. By default, drift generates most classes for you, which can make it easier to use, but makes the api less flexible in some instances. -Drift can also be used with [custom row classes]({{ 'Advanced Features/custom_row_classes.md' | pageUrl }}) though. +Drift can also be used with [custom row classes]({{ 'custom_row_classes.md' | pageUrl }}) though. ### firebase Both the Realtime Database and Cloud Datastore are easy to use persistence libraries that can sync across devices while diff --git a/docs/pages/docs/index.md b/docs/pages/docs/index.md index 94621b48d..343c15107 100644 --- a/docs/pages/docs/index.md +++ b/docs/pages/docs/index.md @@ -11,7 +11,7 @@ Drift is a reactive persistence library for Dart and Flutter applications. It's of database libraries like [the sqlite3 package](https://pub.dev/packages/sqlite3), [sqflite](https://pub.dev/packages/sqflite) or [sql.js](https://github.com/sql-js/sql.js/) and provides additional features, like: -- __Type safety__: Instead of writing sql queries manually and parsing the `List>` that they +- __Type safety__: Instead of writing sql queries manually and parsing the `List>` that they return, drift turns rows into objects of your choice. - __Stream queries__: Drift lets you "watch" your queries with zero additional effort. Any query can be turned into an auto-updating stream that emits new items when the underlying data changes. @@ -26,4 +26,11 @@ return, drift turns rows into objects of your choice. And much more! Drift validates data before inserting it, so you can get helpful error messages instead of just an sql error code. Of course, it supports transactions. And DAOs. And efficient batched insert statements. The list goes on. -Check out these in-depth articles to learn about drift and how to use its features. +## Getting started + +To get started with drift, follow the [setup guide]({{ 'setup.md' | pageUrl }}). +It explains everything from setting up the dependencies to writing database classes +and generating code. + +It also links a few pages intended for developers getting started with drift, so +that you can explore the areas you're most interested in first. diff --git a/docs/pages/docs/Advanced Features/isolates.md b/docs/pages/docs/isolates.md similarity index 98% rename from docs/pages/docs/Advanced Features/isolates.md rename to docs/pages/docs/isolates.md index 6c4021eca..d15d6fb90 100644 --- a/docs/pages/docs/Advanced Features/isolates.md +++ b/docs/pages/docs/isolates.md @@ -2,15 +2,18 @@ data: title: Isolates description: Accessing drift databases on multiple isolates. + weight: 10 template: layouts/docs/single +path: docs/advanced-features/isolates/ --- + {% assign snippets = 'package:drift_docs/snippets/isolates.dart.excerpt.json' | readString | json_decode %} As sqlite3 is a synchronous C library, accessing the database from the main isolate can cause blocking IO operations that lead to reduced responsiveness of your application. To resolve this problem, drift can spawn a long-running isolate to run SQL statements. -When following the recommended [getting started guide]({{ '../Getting started/index.md' | pageUrl }}) +When following the recommended [getting started guide]({{ 'setup.md' | pageUrl }}) and using `NativeDatabase.createInBackground`, you automatically benefit from an isolate drift manages for you without needing additional setup. This page describes when advanced isolate setups are necessary, and how to approach them. diff --git a/docs/pages/docs/setup.md b/docs/pages/docs/setup.md new file mode 100644 index 000000000..d02021ce4 --- /dev/null +++ b/docs/pages/docs/setup.md @@ -0,0 +1,122 @@ +--- +data: + title: "Setup" + description: All you need to know about adding drift to your project. + weight: 1 +template: layouts/docs/single +path: /docs/getting-started/ +aliases: + - /getting-started/ # Used to have this url as well +--- + +{% assign snippets = 'package:drift_docs/snippets/setup/database.dart.excerpt.json' | readString | json_decode %} + +Drift is a powerful database library for Dart and Flutter applications. To +support its advanced capabilities like type-safe SQL queries, verification of +your database and migrations, it uses a builder and command-line tooling that +runs at compile-time. + +This means that the setup involves a little more than just adding a single +dependency to your pubspec. This page explains how to add drift to your project +and gives pointers to the next steps. +If you're stuck adding drift, or have questions or feedback about the project, +please share that with the community by [starting a discussion on GitHub](https://github.com/simolus3/drift/discussions). +If you want to look at an example app for inspiration, a cross-platform Flutter app using drift is available +[as part of the drift repository](https://github.com/simolus3/drift/tree/develop/examples/app). + +## The dependencies {#adding-dependencies} + +First, lets add drift to your project's `pubspec.yaml`. +At the moment, the current version of `drift` is [![Drift version](https://img.shields.io/pub/v/drift.svg)](https://pub.dev/packages/drift) +and the latest version of `drift_dev` is [![Generator version](https://img.shields.io/pub/v/drift_dev.svg)](https://pub.dev/packages/drift_dev). +In addition to the core drift dependencies, we're also adding packages to find a suitable database +location on the device and to include a recent version of `sqlite3`, the database most commonly +used with drift. + +{% assign versions = 'package:drift_docs/versions.json' | readString | json_decode %} + +```yaml +dependencies: + drift: ^{{ versions.drift }} + sqlite3_flutter_libs: ^0.5.0 + path_provider: ^2.0.0 + path: ^{{ versions.path }} + +dev_dependencies: + drift_dev: ^{{ versions.drift_dev }} + build_runner: ^{{ versions.build_runner }} +``` + +If you're wondering why so many packages are necessary, here's a quick overview over what each package does: + +- `drift`: This is the core package defining the APIs you use to access drift databases. +- `sqlite3_flutter_libs`: Ships the latest `sqlite3` version with your Android or iOS app. This is not required when you're _not_ using Flutter, + but then you need to take care of including `sqlite3` yourself. + For an overview on other platforms, see [platforms]({{ 'Platforms/index.md' | pageUrl }}). + Note that the `sqlite3_flutter_libs` package will include the native sqlite3 library for the following + architectures: `armv8`, `armv7`, `x86` and `x86_64`. + Most Flutter apps don't run on 32-bit x86 devices without further setup, so you should + [add a snippet](https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3_flutter_libs#included-platforms) + to your `build.gradle` if you don't need `x86` builds. + Otherwise, the Play Store might allow users on `x86` devices to install your app even though it is not + supported. + In Flutter's current native build system, drift unfortunately can't do that for you. +- `path_provider` and `path`: Used to find a suitable location to store the database. Maintained by the Flutter and Dart team. +- `drift_dev`: This development-only dependency generates query code based on your tables. It will not be included in your final app. +- `build_runner`: Common tool for code-generation, maintained by the Dart team. + +## Database class + +Every project using drift needs at least one class to access a database. This class references all the +tables you want to use and is the central entrypoint for drift's code generator. +In this example, we'll assume that this database class is defined in a file called `database.dart` and +somewhere under `lib/`. Of course, you can put this class in any Dart file you like. + +To make the database useful, we'll also add a simple table to it. This table, `TodoItems`, can be used +to store todo items for a todo list app. +Everything there is to know about defining tables in Dart is described on the [Dart tables]({{'Dart API/tables.md' | pageUrl}}) page. +If you prefer using SQL to define your tables, drift supports that too! You can read all about the [SQL API]({{ 'SQL API/index.md' | pageUrl }}) here. + +For now, the contents of `database.dart` are: + +{% include "blocks/snippet" snippets = snippets name = 'before_generation' %} + +You will get an analyzer warning on the `part` statement and on `extends _$AppDatabase`. This is +expected because drift's generator did not run yet. +You can do that by invoking [build_runner](https://pub.dev/packages/build_runner): + + - `dart run build_runner build` generates all the required code once. + - `dart run build_runner watch` watches for changes in your sources and generates code with + incremental rebuilds. This is suitable for development sessions. + +After running either command, the `database.g.dart` file containing the generated `_$AppDatabase` +class will have been generated. +You will now see errors related to missing overrides and a missing constructor. The constructor +is responsible for telling drift how to open the database. The `schemaVersion` getter is relevant +for migrations after changing the database, we can leave it at `1` for now. The database class +now looks like this: + +{% include "blocks/snippet" snippets = snippets name = 'open' %} + +## Next steps + +Congratulations! With this setup complete, your project is ready to use drift. +This short snippet shows how the database can be opened and how to run inserts and selects: + +{% include "blocks/snippet" snippets = snippets name = 'use' %} + +But drift can do so much more! These pages provide more information useful when getting +started with drift: + +- [Dart tables]({{ 'Dart API/tables.md' | pageUrl }}): This page describes how to write your own + Dart tables and which classes drift generates for them. +- Writing queries: Drift-generated classes support writing the most common SQL statements, like + [selects]({{ 'Dart API/select.md' | pageUrl }}) or [inserts, updates and deletes]({{ 'Dart API/writes.md' | pageUrl }}). +- Something to keep in mind for later: When changing the database, for instance by adding new columns + or tables, you need to write a migration so that existing databases are transformed to the new + format. Drift's extensive [migration tools]({{ 'Migrations/index.md' | pageUrl }}) help with that. + +Once you're familiar with the basics, the [overview here]({{ 'index.md' | pageUrl }}) shows what +more drift has to offer. +This includes transactions, automated tooling to help with migrations, multi-platform support +and more. diff --git a/docs/pages/docs/testing.md b/docs/pages/docs/testing.md index 13e426e7f..26399eef3 100644 --- a/docs/pages/docs/testing.md +++ b/docs/pages/docs/testing.md @@ -2,6 +2,7 @@ data: title: "Testing" description: Guide on writing unit tests for drift databases + weight: 10 template: layouts/docs/single --- @@ -116,4 +117,4 @@ test('stream emits a new user when the name updates', () async { ## Testing migrations Drift can help you generate code for schema migrations. For more details, see -[this guide]({{ "Advanced Features/migrations.md#verifying-migrations" | pageUrl }}). +[this guide]({{ "Migrations/tests.md" | pageUrl }}). diff --git a/docs/pages/docs/Advanced Features/type_converters.md b/docs/pages/docs/type_converters.md similarity index 97% rename from docs/pages/docs/Advanced Features/type_converters.md rename to docs/pages/docs/type_converters.md index 3c2f344fc..18bd6b951 100644 --- a/docs/pages/docs/Advanced Features/type_converters.md +++ b/docs/pages/docs/type_converters.md @@ -1,9 +1,11 @@ --- data: title: "Type converters" + weight: 5 description: Store more complex data in columns with type converters aliases: - /type_converters +path: /docs/advanced_features/type_converters/ template: layouts/docs/single --- @@ -131,7 +133,7 @@ CREATE TABLE users ( ); ``` -When using type converters in drift files, we recommend the [`apply_converters_on_variables`]({{ "builder_options.md" | pageUrl }}) +When using type converters in drift files, we recommend the [`apply_converters_on_variables`]({{ "Generation options/index.md" | pageUrl }}) build option. This will also apply the converter from Dart to SQL, for instance if used on variables: `SELECT * FROM users WHERE preferences = ?`. With that option, the variable will be inferred to `Preferences` instead of `String`. diff --git a/docs/pages/docs/upgrading.md b/docs/pages/docs/upgrading.md index 738d681dc..c0870cc65 100644 --- a/docs/pages/docs/upgrading.md +++ b/docs/pages/docs/upgrading.md @@ -110,7 +110,7 @@ Also, you may have to - Format your sources again: Run `dart format .`. - Re-run the build: Run `dart run build_runner build -d`. - - If you have been using generated [migration test files]({{ 'Advanced Features/migrations.md#exporting-the-schema' | pageUrl }}), + - If you have been using generated [migration test files]({{ 'Migrations/exports.md' | pageUrl }}), re-generate them as well with `dart run drift_dev schema generate drift_schemas/ test/generated_migrations/` (you may have to adapt the command to the directories you use for schemas). - Manually fix the changed order of imports caused by the migration. @@ -191,7 +191,7 @@ If you opt for a rename, also update your imports and `include:` parameters in d #### Build configuration -When configuring moor builders for [options]({{ 'Advanced Features/builder_options.md' | pageUrl }}), you have to update your `build.yaml` files to reflect the new builder keys: +When configuring moor builders for [options]({{ 'Generation options/index.md' | pageUrl }}), you have to update your `build.yaml` files to reflect the new builder keys: | Moor builder key | Drift builder key | | ------------------------------------------- | ------------------------------ | diff --git a/docs/pages/index.html b/docs/pages/index.html index 78c66c175..b093a712f 100644 --- a/docs/pages/index.html +++ b/docs/pages/index.html @@ -14,7 +14,7 @@ Write type-safe queries in Dart or SQL, enjoy auto-updating streams, easily managed transactions and so much more to make persistence fun.

- + Get started @@ -28,7 +28,7 @@ advanced SQL features. Drift will take care of creating the tables and generate code that allows you run fluent queries on your data. -[Get started now]({{ "docs/Getting started/index.md" | pageUrl }}) +[Get started now]({{ "docs/setup.md" | pageUrl }}) {% endblock %} {% endblock %} @@ -51,7 +51,7 @@ Further, drift provides a complete test toolkit to help you test migrations between all your revisions. -[All about schema migrations]({{ "docs/Advanced Features/migrations.md" | pageUrl }}) +[All about schema migrations]({{ "docs/Migrations/index.md" | pageUrl }}) {% endblock %} {% endblock %} @@ -62,7 +62,7 @@ code. Of course, you can mix SQL and Dart to your liking. -[Using SQL with Drift]({{ 'docs/Using SQL/index.md' | pageUrl }}) +[Using SQL with Drift]({{ 'docs/SQL API/index.md' | pageUrl }}) {% endblock %} {% endblock %} @@ -73,14 +73,14 @@ Other database libraries can easily be integrated into drift as well. -[All platforms]({{ "docs/platforms.md" | pageUrl }}) +[All platforms]({{ "docs/Platforms/index.md" | pageUrl }}) {% endblock %} {% endblock %} {% block "blocks/feature.html" icon="fas fa-star" title="And much more!" %} {% block "blocks/markdown.html" %} Drift provides auto-updating streams for all your queries, makes dealing with transactions and migrations easy -and lets your write modular database code with DAOs. We even have a [sql IDE]({{ 'docs/Using SQL/sql_ide.md' | pageUrl }}) builtin to the project. +and lets your write modular database code with DAOs. When using drift, working with databases in Dart is fun! {% endblock %} {% endblock %} diff --git a/docs/pages/v2.html b/docs/pages/v2.html index 7278c3fe4..5409bbb23 100644 --- a/docs/pages/v2.html +++ b/docs/pages/v2.html @@ -12,7 +12,7 @@ Get started - + Migrate an existing project @@ -25,7 +25,7 @@ The rewritten compiler is faster than ever, supports more SQL features and gives you more flexibility when writing database code. -[Check the updated documentation]({{ "docs/Using SQL/drift_files.md" | pageUrl }}) +[Check the updated documentation]({{ "docs/SQL API/drift_files.md" | pageUrl }}) {% endblock %} {% endblock %} @@ -35,7 +35,7 @@ and queries you define. Moor will then generate typesafe Dart APIs based on your tables and statements. -[Get started with SQL and moor]({{ "docs/Getting started/starting_with_sql.md" | pageUrl }}) +[Get started with SQL and moor]({{ "docs/SQL API/index.md" | pageUrl }}) {% endblock %} {% endblock %} {% block "blocks/feature" icon="fas fa-plus" title="Analyzer improvements" %} {% block "blocks/markdown" %} We now support more advanced features like compound select statements and window functions, @@ -59,7 +59,7 @@ SQL queries as you type. Moor plugs right into the Dart analysis server, so you don't have to install any additional extensions. -[Learn more about the IDE]({{ "docs/Using SQL/sql_ide.md" | pageUrl }}) +[Learn more about the IDE]({{ "docs/SQL API/sql_ide.md" | pageUrl }}) {% endblock %} {% endblock %} {% block "blocks/section" color="dark" %} @@ -109,8 +109,8 @@ {% block "blocks/section" color="dark" type="section" %} {% block "blocks/markdown" %} ## Try moor now -- To get started with moor, follow our [getting started guide]({{ "docs/Getting started/index.md" | pageUrl }}) here. +- To get started with moor, follow our [getting started guide]({{ "docs/setup.md" | pageUrl }}) here. - To get started with SQL in moor, or to migrate an existing project to moor, follow our - [migration guide]({{ "docs/Getting started/starting_with_sql.md" | pageUrl }}) + [migration guide]({{ "docs/SQL API/index.md" | pageUrl }}) {% endblock %} {% endblock %} \ No newline at end of file diff --git a/docs/pubspec.yaml b/docs/pubspec.yaml index 645e8e8d6..7c19be4d9 100644 --- a/docs/pubspec.yaml +++ b/docs/pubspec.yaml @@ -29,6 +29,7 @@ dependencies: picocss: hosted: https://simonbinder.eu version: ^1.5.10 + test: ^1.18.0 dev_dependencies: lints: ^2.0.0 @@ -43,7 +44,6 @@ dev_dependencies: shelf: ^1.2.0 shelf_static: ^1.1.0 source_span: ^1.9.1 - test: ^1.18.0 sqlparser: zap_dev: ^0.2.3+1 diff --git a/docs/templates/partials/dependencies.html b/docs/templates/partials/dependencies.html index 534f8bd09..1bb3fdc6a 100644 --- a/docs/templates/partials/dependencies.html +++ b/docs/templates/partials/dependencies.html @@ -23,7 +23,6 @@ - `drift`: This is the core package defining the APIs you use to access drift databases. - `sqlite3_flutter_libs`: Ships the latest `sqlite3` version with your Android or iOS app. This is not required when you're _not_ using Flutter, but then you need to take care of including `sqlite3` yourself. - For an overview on other platforms, see [platforms]({{ '../platforms.md' | pageUrl }}). Note that the `sqlite3_flutter_libs` package will include the native sqlite3 library for the following architectures: `armv8`, `armv7`, `x86` and `x86_64`. Most Flutter apps don't run on 32-bit x86 devices without further setup, so you should diff --git a/docs/test/snippet_test.dart b/docs/test/snippet_test.dart index 6b996b7bb..16968961a 100644 --- a/docs/test/snippet_test.dart +++ b/docs/test/snippet_test.dart @@ -1,6 +1,6 @@ import 'package:drift/drift.dart'; import 'package:drift/native.dart'; -import 'package:drift_docs/snippets/migrations/datetime_conversion.dart'; +import 'package:drift_docs/snippets/dart_api/datetime_conversion.dart'; import 'package:drift_docs/snippets/modular/schema_inspection.dart'; import 'package:test/test.dart'; diff --git a/drift/lib/src/dsl/table.dart b/drift/lib/src/dsl/table.dart index ac358366b..072f12f6c 100644 --- a/drift/lib/src/dsl/table.dart +++ b/drift/lib/src/dsl/table.dart @@ -72,7 +72,7 @@ abstract class Table extends HasResultSet { /// ```dart /// class IngredientInRecipes extends Table { /// @override - /// Set get uniqueKeys => + /// List> get uniqueKeys => /// [{recipe, ingredient}, {recipe, amountInGrams}]; /// /// IntColumn get recipe => integer()();