From 3824ae8a3db5f868e1b82f8a5010c7113c9a5c73 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 16 Sep 2023 17:35:44 +0200 Subject: [PATCH 01/12] Show correct annotation name in tables error --- .../lib/src/analysis/resolver/dart/accessor.dart | 5 ++++- .../analysis/resolver/dart/table_parser_test.dart | 13 ++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/drift_dev/lib/src/analysis/resolver/dart/accessor.dart b/drift_dev/lib/src/analysis/resolver/dart/accessor.dart index a52226cbe..1fed02e2c 100644 --- a/drift_dev/lib/src/analysis/resolver/dart/accessor.dart +++ b/drift_dev/lib/src/analysis/resolver/dart/accessor.dart @@ -25,9 +25,12 @@ class DartAccessorResolver final rawTablesOrNull = annotation.getField('tables')?.toListValue(); if (rawTablesOrNull == null) { + final annotationName = + annotation.type?.nameIfInterfaceType ?? 'DriftDatabase'; + reportError(DriftAnalysisError.forDartElement( element, - 'Could not read tables from @DriftDatabase annotation! \n' + 'Could not read tables from @$annotationName annotation! \n' 'Please make sure that all table classes exist.', )); } diff --git a/drift_dev/test/analysis/resolver/dart/table_parser_test.dart b/drift_dev/test/analysis/resolver/dart/table_parser_test.dart index 6c26c0206..4a810ec1b 100644 --- a/drift_dev/test/analysis/resolver/dart/table_parser_test.dart +++ b/drift_dev/test/analysis/resolver/dart/table_parser_test.dart @@ -111,6 +111,9 @@ void main() { @DriftDatabase(tables: [Foo, DoesNotExist]) class Database {} + + @DriftAccessor(tables: [DoesNotExist]) + class Accessor {} ''', 'a|lib/invalid_constraints.dart': ''' import 'package:drift/drift.dart'; @@ -283,18 +286,22 @@ void main() { ); }); - test('reports errors for unknown classes in UseMoor', () async { + test('reports errors for unknown classes', () async { final uri = Uri.parse('package:a/invalid_reference.dart'); final file = await backend.driver.fullyAnalyze(uri); expect( file.allErrors, - contains( + containsAll([ isDriftError(allOf( contains('Could not read tables from @DriftDatabase annotation!'), contains('Please make sure that all table classes exist.'), )), - ), + isDriftError(allOf( + contains('Could not read tables from @DriftAccessor annotation!'), + contains('Please make sure that all table classes exist.'), + )), + ]), ); }); From e0a6b557e74a8cdcb6513e4638d7842aad8d8b89 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 13 Sep 2023 12:01:23 +0200 Subject: [PATCH 02/12] Start with documentation restructuring --- docs/lib/snippets/setup/database.dart | 71 +++++++++++ .../docs/Advanced Features/builder_options.md | 2 +- .../docs/Advanced Features/expressions.md | 2 +- docs/pages/docs/Dart API/architecture.md | 6 + .../{Advanced Features => Dart API}/daos.md | 1 + docs/pages/docs/Dart API/expressions.md | 10 ++ docs/pages/docs/Dart API/index.md | 7 + docs/pages/docs/Dart API/select.md | 7 + docs/pages/docs/Dart API/tables.md | 10 ++ .../pages/docs/{ => Dart API}/transactions.md | 3 +- docs/pages/docs/Dart API/views.md | 6 + docs/pages/docs/Dart API/writes.md | 7 + .../Getting started/advanced_dart_tables.md | 1 + docs/pages/docs/Getting started/index.md | 3 +- .../docs/Getting started/starting_with_sql.md | 2 +- docs/pages/docs/Migrations/index.md | 7 + docs/pages/docs/SQL API/index.md | 7 + docs/pages/docs/Using SQL/custom_queries.md | 2 +- docs/pages/docs/setup.md | 120 ++++++++++++++++++ 19 files changed, 268 insertions(+), 6 deletions(-) create mode 100644 docs/lib/snippets/setup/database.dart create mode 100644 docs/pages/docs/Dart API/architecture.md rename docs/pages/docs/{Advanced Features => Dart API}/daos.md (97%) create mode 100644 docs/pages/docs/Dart API/expressions.md create mode 100644 docs/pages/docs/Dart API/index.md create mode 100644 docs/pages/docs/Dart API/select.md create mode 100644 docs/pages/docs/Dart API/tables.md rename docs/pages/docs/{ => Dart API}/transactions.md (99%) create mode 100644 docs/pages/docs/Dart API/views.md create mode 100644 docs/pages/docs/Dart API/writes.md create mode 100644 docs/pages/docs/Migrations/index.md create mode 100644 docs/pages/docs/SQL API/index.md create mode 100644 docs/pages/docs/setup.md diff --git a/docs/lib/snippets/setup/database.dart b/docs/lib/snippets/setup/database.dart new file mode 100644 index 000000000..c7a94a161 --- /dev/null +++ b/docs/lib/snippets/setup/database.dart @@ -0,0 +1,71 @@ +// #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'; + +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()(); +} +// #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/pages/docs/Advanced Features/builder_options.md b/docs/pages/docs/Advanced Features/builder_options.md index 5a22eef19..42ba760d1 100644 --- a/docs/pages/docs/Advanced Features/builder_options.md +++ b/docs/pages/docs/Advanced Features/builder_options.md @@ -408,5 +408,5 @@ 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. +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/Advanced Features/expressions.md b/docs/pages/docs/Advanced Features/expressions.md index 6e4e36d63..72c71df40 100644 --- a/docs/pages/docs/Advanced Features/expressions.md +++ b/docs/pages/docs/Advanced Features/expressions.md @@ -5,7 +5,7 @@ data: weight: 200 # used to be in the "getting started" section -path: docs/getting-started/expressions/ +path: docs/getting-started/expressions-old/ template: layouts/docs/single --- diff --git a/docs/pages/docs/Dart API/architecture.md b/docs/pages/docs/Dart API/architecture.md new file mode 100644 index 000000000..6f0c61625 --- /dev/null +++ b/docs/pages/docs/Dart API/architecture.md @@ -0,0 +1,6 @@ +--- +data: + title: "Drift and app architecture" + description: Notes on how drift can be integrated into your app's architecture +template: layouts/docs/single +--- diff --git a/docs/pages/docs/Advanced Features/daos.md b/docs/pages/docs/Dart API/daos.md similarity index 97% rename from docs/pages/docs/Advanced Features/daos.md rename to docs/pages/docs/Dart API/daos.md index e4dfe77ec..c24818e75 100644 --- a/docs/pages/docs/Advanced Features/daos.md +++ b/docs/pages/docs/Dart API/daos.md @@ -2,6 +2,7 @@ data: title: "DAOs" description: Keep your database code modular with DAOs +path: /docs/advanced-features/daos aliases: - /daos/ template: layouts/docs/single diff --git a/docs/pages/docs/Dart API/expressions.md b/docs/pages/docs/Dart API/expressions.md new file mode 100644 index 000000000..58e193fbc --- /dev/null +++ b/docs/pages/docs/Dart API/expressions.md @@ -0,0 +1,10 @@ +--- +data: + title: Expressions + description: Deep-dive into what kind of SQL expressions can be written in Dart + weight: 5 + +# used to be in the "getting started" section +path: docs/getting-started/expressions/ +template: layouts/docs/single +--- diff --git a/docs/pages/docs/Dart API/index.md b/docs/pages/docs/Dart API/index.md new file mode 100644 index 000000000..fd86422bf --- /dev/null +++ b/docs/pages/docs/Dart API/index.md @@ -0,0 +1,7 @@ +--- +data: + title: Dart API + description: Drift APIs for your app + weight: 2 +template: layouts/docs/list +--- diff --git a/docs/pages/docs/Dart API/select.md b/docs/pages/docs/Dart API/select.md new file mode 100644 index 000000000..85c0ade4b --- /dev/null +++ b/docs/pages/docs/Dart API/select.md @@ -0,0 +1,7 @@ +--- +data: + title: "Selects" + description: "Select rows or invidiual columns from tables in Dart" + weight: 2 +template: layouts/docs/single +--- diff --git a/docs/pages/docs/Dart API/tables.md b/docs/pages/docs/Dart API/tables.md new file mode 100644 index 000000000..fa615d6b6 --- /dev/null +++ b/docs/pages/docs/Dart API/tables.md @@ -0,0 +1,10 @@ +--- +data: + title: "Dart tables" + 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/ +--- + +In relational databases, diff --git a/docs/pages/docs/transactions.md b/docs/pages/docs/Dart API/transactions.md similarity index 99% rename from docs/pages/docs/transactions.md rename to docs/pages/docs/Dart API/transactions.md index c9b63f617..c16503439 100644 --- a/docs/pages/docs/transactions.md +++ b/docs/pages/docs/Dart API/transactions.md @@ -1,10 +1,11 @@ --- data: title: "Transactions" - weight: 70 + weight: 4 description: Run multiple statements atomically template: layouts/docs/single +path: /docs/transactions/ aliases: - /transactions/ --- diff --git a/docs/pages/docs/Dart API/views.md b/docs/pages/docs/Dart API/views.md new file mode 100644 index 000000000..63e63d141 --- /dev/null +++ b/docs/pages/docs/Dart API/views.md @@ -0,0 +1,6 @@ +--- +data: + title: "Views" + description: How to define SQL views as Dart classes +template: layouts/docs/single +--- diff --git a/docs/pages/docs/Dart API/writes.md b/docs/pages/docs/Dart API/writes.md new file mode 100644 index 000000000..6379703bb --- /dev/null +++ b/docs/pages/docs/Dart API/writes.md @@ -0,0 +1,7 @@ +--- +data: + title: "Writes (update, insert, delete)" + description: "Select rows or invidiual columns from tables in Dart" + weight: 3 +template: layouts/docs/single +--- diff --git a/docs/pages/docs/Getting started/advanced_dart_tables.md b/docs/pages/docs/Getting started/advanced_dart_tables.md index afc7e2673..95220c468 100644 --- a/docs/pages/docs/Getting started/advanced_dart_tables.md +++ b/docs/pages/docs/Getting started/advanced_dart_tables.md @@ -3,6 +3,7 @@ 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 +path: /old-tables template: layouts/docs/single --- diff --git a/docs/pages/docs/Getting started/index.md b/docs/pages/docs/Getting started/index.md index 5833f9145..0fa0b75fd 100644 --- a/docs/pages/docs/Getting started/index.md +++ b/docs/pages/docs/Getting started/index.md @@ -4,9 +4,10 @@ data: description: Simple guide to get a drift project up and running. weight: 1 hide_section_index: true +path: /docs/getting-started-old template: layouts/docs/list aliases: - - /getting-started/ # Used to have this url + - /getting-started-old/ # Used to have this url --- In addition to this document, other resources on how to use drift also exist. diff --git a/docs/pages/docs/Getting started/starting_with_sql.md b/docs/pages/docs/Getting started/starting_with_sql.md index 371414181..7e857e586 100644 --- a/docs/pages/docs/Getting started/starting_with_sql.md +++ b/docs/pages/docs/Getting started/starting_with_sql.md @@ -86,7 +86,7 @@ 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 }}) +- [Transactions]({{ "../Dart API/transactions.md" | pageUrl }}) - [Schema migrations]({{ "../Advanced Features/migrations.md" | pageUrl }}) - Writing [queries]({{ "writing_queries.md" | pageUrl }}) and [expressions]({{ "../Advanced Features/expressions.md" | pageUrl }}) in Dart diff --git a/docs/pages/docs/Migrations/index.md b/docs/pages/docs/Migrations/index.md new file mode 100644 index 000000000..7e5f54f81 --- /dev/null +++ b/docs/pages/docs/Migrations/index.md @@ -0,0 +1,7 @@ +--- +data: + title: Migrations + description: Simple guide to get a drift project up and running. + hide_section_index: true +template: layouts/docs/list +--- diff --git a/docs/pages/docs/SQL API/index.md b/docs/pages/docs/SQL API/index.md new file mode 100644 index 000000000..5d251b0ae --- /dev/null +++ b/docs/pages/docs/SQL API/index.md @@ -0,0 +1,7 @@ +--- +data: + title: SQL API + description: Define your database and queries in SQL instead. + weight: 3 +template: layouts/docs/single +--- diff --git a/docs/pages/docs/Using SQL/custom_queries.md b/docs/pages/docs/Using SQL/custom_queries.md index caa38e5d2..4279956a3 100644 --- a/docs/pages/docs/Using SQL/custom_queries.md +++ b/docs/pages/docs/Using SQL/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. diff --git a/docs/pages/docs/setup.md b/docs/pages/docs/setup.md new file mode 100644 index 000000000..099090050 --- /dev/null +++ b/docs/pages/docs/setup.md @@ -0,0 +1,120 @@ +--- +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.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 }}). +- General [notes on how to integrate drift with your app's architecture]({{ 'Dart API/architecture.md' | pageUrl }}). + +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. From 2403da5b980f8fba8e0cb1085baf8ed72afc6766 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 16 Sep 2023 18:38:22 +0200 Subject: [PATCH 03/12] Write new page on dart tables --- .../datetime_conversion.dart | 0 docs/lib/snippets/dart_api/old_name.dart | 5 + docs/lib/snippets/dart_api/tables.dart | 80 ++++ docs/lib/snippets/setup/database.dart | 2 + docs/pages/docs/Dart API/daos.md | 4 +- docs/pages/docs/Dart API/tables.md | 368 +++++++++++++++++- .../Getting started/advanced_dart_tables.md | 338 ---------------- docs/test/snippet_test.dart | 2 +- drift/lib/src/dsl/table.dart | 2 +- 9 files changed, 459 insertions(+), 342 deletions(-) rename docs/lib/snippets/{migrations => dart_api}/datetime_conversion.dart (100%) create mode 100644 docs/lib/snippets/dart_api/old_name.dart create mode 100644 docs/lib/snippets/dart_api/tables.dart 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/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/dart_api/tables.dart b/docs/lib/snippets/dart_api/tables.dart new file mode 100644 index 000000000..c145748a3 --- /dev/null +++ b/docs/lib/snippets/dart_api/tables.dart @@ -0,0 +1,80 @@ +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 +class Users extends Table {} +// #enddocregion index \ No newline at end of file diff --git a/docs/lib/snippets/setup/database.dart b/docs/lib/snippets/setup/database.dart index c7a94a161..03908ae15 100644 --- a/docs/lib/snippets/setup/database.dart +++ b/docs/lib/snippets/setup/database.dart @@ -17,12 +17,14 @@ import 'package:path/path.dart' as p; // #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]) diff --git a/docs/pages/docs/Dart API/daos.md b/docs/pages/docs/Dart API/daos.md index c24818e75..d20d9ce2e 100644 --- a/docs/pages/docs/Dart API/daos.md +++ b/docs/pages/docs/Dart API/daos.md @@ -9,8 +9,9 @@ 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'; @@ -33,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/Dart API/tables.md b/docs/pages/docs/Dart API/tables.md index fa615d6b6..f873f8aec 100644 --- a/docs/pages/docs/Dart API/tables.md +++ b/docs/pages/docs/Dart API/tables.md @@ -7,4 +7,370 @@ template: layouts/docs/single path: /docs/getting-started/advanced_dart_tables/ --- -In relational databases, +{% 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 %} + +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: + +{% include "blocks/snippet" snippets = setup name = 'table' %} + +This page describes the DSL for tables in more detail. + +## Columns + +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. + +## 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 }}). + +| Dart type | Column | Corresponding SQLite type | +|--------------|---------------|-----------------------------------------------------| +| `int` | `integer()` | `INTEGER` | +| `BigInt` | `int64()` | `INTEGER` (useful for large values on the web) | +| `double` | `real()` | `REAL` | +| `boolean` | `boolean()` | `INTEGER`, which a `CHECK` to only allow `0` or `1` | +| `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 }})). | + +Note that the mapping for `boolean`, `dateTime` and type converters only applies when storing records in +the database. +They don't affect JSON serialization at all. For instance, `boolean` values are expected as `true` or `false` +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). + +### `BigInt` support + +Drift supports the `int64()` column builder to indicate that a column stores +large integers and should be mapped to Dart as a `BigInt`. + +This is mainly useful for Dart apps compiled to JavaScript, where an `int` +really is a `double` that can't store large integers without loosing information. +Here, representing integers as `BigInt` (and passing those to the underlying +database implementation) ensures that you can store large intergers without any +loss of precision. +Be aware that `BigInt`s have a higher overhead than `int`s, so we recommend using +`int64()` only for columns where this is necessary: + +{% block "blocks/alert" title="You might not need this!" color="info" %} +In sqlite3, an `INTEGER` column is stored as a 64-bit integer. +For apps running in the Dart VM (e.g. on everything except for the web), the `int` +type in Dart is the _perfect_ match for that since it's also a 64-bit int. +For those apps, we recommend using the regular `integer()` column builder. + +Essentially, you should use `int64()` if both of these are true: + +- you're building an app that needs to work on the web, _and_ +- the column in question may store values larger than 252. + +In all other cases, using a regular `integer()` column is more efficient. +{% endblock %} + +Here are some more pointers on using `BigInt`s in drift: + +- Since an `integer()` and a `int64()` is the same column in sqlite3, you can + switch between the two without writing a schema migration. +- In addition to large columns, it may also be that you have a complex expression + in a select query that would be better represented as a `BigInt`. You can use + `dartCast()` for this: For an expression + `(table.columnA * table.columnB).dartCast()`, drift will report the + resulting value as a `BigInt` even if `columnA` and `columnB` were defined + as regular integers. +- `BigInt`s are not currently supported by `moor_flutter` and `drift_sqflite`. +- To use `BigInt` support on a `WebDatabase`, set the `readIntsAsBigInt: true` + flag when instantiating it. +- Both `NativeDatabase` and `WasmDatabase` have builtin support for bigints. + +### `DateTime` options + +Drift supports two approaches of storing `DateTime` values in SQL: + +1. __As unix timestamp__ (the default): In this mode, drift stores date time + values as an SQL `INTEGER` containing the unix timestamp (in seconds). + When date times are mapped from SQL back to Dart, drift always returns a + non-UTC value. So even when UTC date times are stored, this information is + lost when retrieving rows. +2. __As ISO 8601 string__: In this mode, datetime values are stored in a + textual format based on `DateTime.toIso8601String()`: UTC values are stored + unchanged (e.g. `2022-07-25 09:28:42.015Z`), while local values have their + UTC offset appended (e.g. `2022-07-25T11:28:42.015 +02:00`). + Most of sqlite3's date and time functions operate on UTC values, but parsing + datetimes in SQL respects the UTC offset added to the value. + + When reading values back from the database, drift will use `DateTime.parse` + as following: + - If the textual value ends with `Z`, drift will use `DateTime.parse` + directly. The `Z` suffix will be recognized and a UTC value is returned. + - If the textual value ends with a UTC offset (e.g. `+02:00`), drift first + uses `DateTime.parse` which respects the modifier but returns a UTC + datetime. Drift then calls `toLocal()` on this intermediate result to + return a local value. + - If the textual value neither has a `Z` suffix nor a UTC offset, drift + will parse it as if it had a `Z` modifier, returning a UTC datetime. + The motivation for this is that the `datetime` function in sqlite3 returns + values in this format and uses UTC by default. + + 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 }}). + +Regardless of the option used, drift's builtin support for +[date and time functions]({{ '../Advanced Features/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, +drift will compare their `julianday` values behind the scenes. + +#### Migrating between the two modes + +While making drift change the date time modes is as simple as changing a build +option, toggling this behavior is not compatible with existing database schemas: + +1. Depending on the build option, drift expects strings or integers for datetime + values. So you need to migrate stored columns to the new format when changing + the option. +2. If you are using SQL statements defined in `.drift` files, use custom SQL + at runtime or manually invoke datetime expressions with a direct + `FunctionCallExpression` instead of using the higher-level date time APIs, you + may have to adapt those usages. + + For instance, comparison operators like `<` work on unix timestamps, but they + will compare textual datetime values lexicographically. So depending on the + mode used, you will have to wrap the value in `unixepoch` or `julianday` to + make them comparable. + +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/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 +unix timestamp in milliseconds. You can change this by creating a +`ValueSerializer.defaults(serializeDateTimeValuesAsString: true)` and assigning +it to `driftRuntimeOptions.defaultSerializer`. + +##### Migrating from unix timestamps to text + +To migrate from using timestamps (the default option) to storing datetimes as +text, follow these steps: + +1. Enable the `store_date_time_values_as_text` build option. +2. Add the following method (or an adaption of it suiting your needs) to your + database class. +3. Increment the `schemaVersion` in your database class. +4. Write a migration step in `onUpgrade` that calls + `migrateFromUnixTimestampsToText` for this schema version increase. + __Remember that triggers, views or other custom SQL entries in your database + will require a custom migration that is not covered by this guide.__ + +{% include "blocks/snippet" snippets = conversion name = "unix-to-text" %} + +##### Migrating from text to unix timestamps + +To migrate from datetimes stored as text back to unix timestamps, follow these +steps: + +1. Disable the `store_date_time_values_as_text` build option. +2. Add the following method (or an adaption of it suiting your needs) to your + database class. +3. Increment the `schemaVersion` in your database class. +4. Write a migration step in `onUpgrade` that calls + `migrateFromTextDateTimesToUnixTimestamps` for this schema version increase. + __Remember that triggers, views or other custom SQL entries in your database + will require a custom migration that is not covered by this guide.__ + +{% include "blocks/snippet" snippets = conversion name = "text-to-unix" %} + +Note that this snippet uses the `unixepoch` sqlite3 function, which has been +added in sqlite 3.38. To support older sqlite3 versions, you can use `strftime` +and cast to an integer instead: + +{% include "blocks/snippet" snippets = conversion name = "text-to-unix-old" %} + +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`. + +### Nullability + +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. + +If you do want to make a column nullable, just use `nullable()`: + +{% include "blocks/snippet" snippets = snippets name = 'nnbd' %} + +## References + +[Foreign key references](https://www.sqlite.org/foreignkeys.html) can be expressed +in Dart tables with the `references()` method when building a column: + +{% 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. + +Optionally, the `onUpdate` and `onDelete` parameters can be used to describe what +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 }}). + +## Default values + +You can set a default value for a column. When not explicitly set, the default value will +be used when inserting a new row. To set a constant default value, use `withDefault`: + +```dart +class Preferences extends Table { + TextColumn get name => text()(); + BoolColumn get enabled => boolean().withDefault(const Constant(false))(); +} +``` + +When you later use `into(preferences).insert(PreferencesCompanion.forInsert(name: 'foo'));`, the new +row will have its `enabled` column set to false (and not to null, as it normally would). +Note that columns with a default value (either through `autoIncrement` or by using a default), are +still marked as `@required` in generated data classes. This is because they are meant to represent a +full row, and every row will have those values. Use companions when representing partial rows, like +for inserts or updates. + +Of course, constants can only be used for static values. But what if you want to generate a dynamic +default value for each column? For that, you can use `clientDefault`. It takes a function returning +the desired default value. The function will be called for each insert. For instance, here's an +example generating a random Uuid using the `uuid` package: +```dart +final _uuid = Uuid(); + +class Users extends Table { + TextColumn get id => text().clientDefault(() => _uuid.v4())(); + // ... +} +``` + +Don't know when to use which? Prefer to use `withDefault` when the default value is constant, or something +simple like `currentDate`. For more complicated values, like a randomly generated id, you need to use +`clientDefault`. Internally, `withDefault` writes the default value into the `CREATE TABLE` statement. This +can be more efficient, but doesn't support dynamic values. + +### 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. + +### 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. + +## Table options + +In addition to the options added to individual columns, some constraints apply to the whole +table. + +### Primary keys + +If your table has an `IntColumn` with an `autoIncrement()` constraint, drift recognizes that as the default +primary key. If you want to specify a custom primary key for your table, you can override the `primaryKey` +getter in your table: + +{% include "blocks/snippet" snippets = snippets name="primary-key" %} + +Note that the primary key must essentially be constant so that the generator can recognize it. That means: + +- it must be defined with the `=>` syntax, function bodies aren't supported +- it must return a set literal without collection elements like `if`, `for` or spread operators + +### Unique columns in table + +When the value of one column must be unique in the table, you can [make that column unique](#unique-column). +When the combined value of multiple columns should be unique, this needs to be declared on the +table by overriding the `uniqueKeys` getter: + +{% include "blocks/snippet" snippets = snippets name="unique-table" %} + +### Custom constraints on tables + +Some table constraints are not directly supported in drift yet. Similar to [custom constraints](#custom-constraints) +on columns, you can add those by overriding `customConstraints`: + +{% include "blocks/snippet" snippets = snippets name="custom-constraint-table" %} + +## Index \ No newline at end of file diff --git a/docs/pages/docs/Getting started/advanced_dart_tables.md b/docs/pages/docs/Getting started/advanced_dart_tables.md index 95220c468..0e8dbe7c9 100644 --- a/docs/pages/docs/Getting started/advanced_dart_tables.md +++ b/docs/pages/docs/Getting started/advanced_dart_tables.md @@ -26,107 +26,6 @@ class Todos extends Table { 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. - -## Default values - -You can set a default value for a column. When not explicitly set, the default value will -be used when inserting a new row. To set a constant default value, use `withDefault`: - -```dart -class Preferences extends Table { - TextColumn get name => text()(); - BoolColumn get enabled => boolean().withDefault(const Constant(false))(); -} -``` - -When you later use `into(preferences).insert(PreferencesCompanion.forInsert(name: 'foo'));`, the new -row will have its `enabled` column set to false (and not to null, as it normally would). -Note that columns with a default value (either through `autoIncrement` or by using a default), are -still marked as `@required` in generated data classes. This is because they are meant to represent a -full row, and every row will have those values. Use companions when representing partial rows, like -for inserts or updates. - -Of course, constants can only be used for static values. But what if you want to generate a dynamic -default value for each column? For that, you can use `clientDefault`. It takes a function returning -the desired default value. The function will be called for each insert. For instance, here's an -example generating a random Uuid using the `uuid` package: -```dart -final _uuid = Uuid(); - -class Users extends Table { - TextColumn get id => text().clientDefault(() => _uuid.v4())(); - // ... -} -``` - -Don't know when to use which? Prefer to use `withDefault` when the default value is constant, or something -simple like `currentDate`. For more complicated values, like a randomly generated id, you need to use -`clientDefault`. Internally, `withDefault` writes the default value into the `CREATE TABLE` statement. This -can be more efficient, but doesn't support dynamic values. - ## Primary keys If your table has an `IntColumn` with an `autoIncrement()` constraint, drift recognizes that as the default @@ -148,243 +47,6 @@ Note that the primary key must essentially be constant so that the generator can - 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 - -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 }}). - -| Dart type | Column | Corresponding SQLite type | -|--------------|---------------|-----------------------------------------------------| -| `int` | `integer()` | `INTEGER` | -| `BigInt` | `int64()` | `INTEGER` (useful for large values on the web) | -| `double` | `real()` | `REAL` | -| `boolean` | `boolean()` | `INTEGER`, which a `CHECK` to only allow `0` or `1` | -| `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 }})). | - -Note that the mapping for `boolean`, `dateTime` and type converters only applies when storing records in -the database. -They don't affect JSON serialization at all. For instance, `boolean` values are expected as `true` or `false` -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). - -### `BigInt` support - -Drift supports the `int64()` column builder to indicate that a column stores -large integers and should be mapped to Dart as a `BigInt`. - -This is mainly useful for Dart apps compiled to JavaScript, where an `int` -really is a `double` that can't store large integers without loosing information. -Here, representing integers as `BigInt` (and passing those to the underlying -database implementation) ensures that you can store large intergers without any -loss of precision. -Be aware that `BigInt`s have a higher overhead than `int`s, so we recommend using -`int64()` only for columns where this is necessary: - -{% block "blocks/alert" title="You might not need this!" color="info" %} -In sqlite3, an `INTEGER` column is stored as a 64-bit integer. -For apps running in the Dart VM (e.g. on everything except for the web), the `int` -type in Dart is the _perfect_ match for that since it's also a 64-bit int. -For those apps, we recommend using the regular `integer()` column builder. - -Essentially, you should use `int64()` if both of these are true: - -- you're building an app that needs to work on the web, _and_ -- the column in question may store values larger than 252. - -In all other cases, using a regular `integer()` column is more efficient. -{% endblock %} - -Here are some more pointers on using `BigInt`s in drift: - -- Since an `integer()` and a `int64()` is the same column in sqlite3, you can - switch between the two without writing a schema migration. -- In addition to large columns, it may also be that you have a complex expression - in a select query that would be better represented as a `BigInt`. You can use - `dartCast()` for this: For an expression - `(table.columnA * table.columnB).dartCast()`, drift will report the - resulting value as a `BigInt` even if `columnA` and `columnB` were defined - as regular integers. -- `BigInt`s are not currently supported by `moor_flutter` and `drift_sqflite`. -- To use `BigInt` support on a `WebDatabase`, set the `readIntsAsBigInt: true` - flag when instantiating it. -- Both `NativeDatabase` and `WasmDatabase` have builtin support for bigints. - -### `DateTime` options - -Drift supports two approaches of storing `DateTime` values in SQL: - -1. __As unix timestamp__ (the default): In this mode, drift stores date time - values as an SQL `INTEGER` containing the unix timestamp (in seconds). - When date times are mapped from SQL back to Dart, drift always returns a - non-UTC value. So even when UTC date times are stored, this information is - lost when retrieving rows. -2. __As ISO 8601 string__: In this mode, datetime values are stored in a - textual format based on `DateTime.toIso8601String()`: UTC values are stored - unchanged (e.g. `2022-07-25 09:28:42.015Z`), while local values have their - UTC offset appended (e.g. `2022-07-25T11:28:42.015 +02:00`). - Most of sqlite3's date and time functions operate on UTC values, but parsing - datetimes in SQL respects the UTC offset added to the value. - - When reading values back from the database, drift will use `DateTime.parse` - as following: - - If the textual value ends with `Z`, drift will use `DateTime.parse` - directly. The `Z` suffix will be recognized and a UTC value is returned. - - If the textual value ends with a UTC offset (e.g. `+02:00`), drift first - uses `DateTime.parse` which respects the modifier but returns a UTC - datetime. Drift then calls `toLocal()` on this intermediate result to - return a local value. - - If the textual value neither has a `Z` suffix nor a UTC offset, drift - will parse it as if it had a `Z` modifier, returning a UTC datetime. - The motivation for this is that the `datetime` function in sqlite3 returns - values in this format and uses UTC by default. - - 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 }}). - -Regardless of the option used, drift's builtin support for -[date and time functions]({{ '../Advanced Features/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, -drift will compare their `julianday` values behind the scenes. - -#### Migrating between the two modes - -While making drift change the date time modes is as simple as changing a build -option, toggling this behavior is not compatible with existing database schemas: - -1. Depending on the build option, drift expects strings or integers for datetime - values. So you need to migrate stored columns to the new format when changing - the option. -2. If you are using SQL statements defined in `.drift` files, use custom SQL - at runtime or manually invoke datetime expressions with a direct - `FunctionCallExpression` instead of using the higher-level date time APIs, you - may have to adapt those usages. - - For instance, comparison operators like `<` work on unix timestamps, but they - will compare textual datetime values lexicographically. So depending on the - mode used, you will have to wrap the value in `unixepoch` or `julianday` to - make them comparable. - -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 %} - -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 -unix timestamp in milliseconds. You can change this by creating a -`ValueSerializer.defaults(serializeDateTimeValuesAsString: true)` and assigning -it to `driftRuntimeOptions.defaultSerializer`. - -##### Migrating from unix timestamps to text - -To migrate from using timestamps (the default option) to storing datetimes as -text, follow these steps: - -1. Enable the `store_date_time_values_as_text` build option. -2. Add the following method (or an adaption of it suiting your needs) to your - database class. -3. Increment the `schemaVersion` in your database class. -4. Write a migration step in `onUpgrade` that calls - `migrateFromUnixTimestampsToText` for this schema version increase. - __Remember that triggers, views or other custom SQL entries in your database - will require a custom migration that is not covered by this guide.__ - -{% include "blocks/snippet" snippets = conversion name = "unix-to-text" %} - -##### Migrating from text to unix timestamps - -To migrate from datetimes stored as text back to unix timestamps, follow these -steps: - -1. Disable the `store_date_time_values_as_text` build option. -2. Add the following method (or an adaption of it suiting your needs) to your - database class. -3. Increment the `schemaVersion` in your database class. -4. Write a migration step in `onUpgrade` that calls - `migrateFromTextDateTimesToUnixTimestamps` for this schema version increase. - __Remember that triggers, views or other custom SQL entries in your database - will require a custom migration that is not covered by this guide.__ - -{% include "blocks/snippet" snippets = conversion name = "text-to-unix" %} - -Note that this snippet uses the `unixepoch` sqlite3 function, which has been -added in sqlite 3.38. To support older sqlite3 versions, you can use `strftime` -and cast to an integer instead: - -{% include "blocks/snippet" snippets = conversion name = "text-to-unix-old" %} - -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`: - -```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)')(); - - @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. - -You can also add table-wide constraints by overriding the `customConstraints` getter in your table class. - -## 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... -} -``` - -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. - -Optionally, the `onUpdate` and `onDelete` parameters can be used to describe what -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 }}). ## Views 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()(); From 10725d98fb53cc5fd3af859cc161be2fd633832e Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 16 Sep 2023 18:54:40 +0200 Subject: [PATCH 04/12] Delete old getting started guide --- docs/lib/snippets/dart_api/tables.dart | 8 +- docs/lib/snippets/dart_api/views.dart | 33 ++++++ .../docs/Advanced Features/builder_options.md | 2 +- .../docs/Advanced Features/expressions.md | 2 +- docs/pages/docs/Advanced Features/isolates.md | 2 +- docs/pages/docs/Advanced Features/joins.md | 2 +- .../Advanced Features/schema_inspection.md | 2 +- docs/pages/docs/Dart API/tables.md | 12 ++- docs/pages/docs/Dart API/views.md | 46 ++++++++ docs/pages/docs/Examples/relationships.md | 2 +- .../Getting started/advanced_dart_tables.md | 97 ----------------- docs/pages/docs/Getting started/index.md | 102 ------------------ .../docs/Getting started/starting_with_sql.md | 2 +- .../docs/Getting started/writing_queries.md | 4 +- docs/pages/docs/Other engines/web.md | 2 +- docs/pages/docs/Using SQL/custom_queries.md | 2 +- docs/pages/docs/Using SQL/drift_files.md | 2 +- docs/pages/docs/faq.md | 2 +- docs/pages/docs/index.md | 2 +- docs/pages/docs/platforms.md | 2 +- docs/pages/docs/setup.md | 6 ++ docs/pages/index.html | 4 +- docs/pages/v2.html | 2 +- 23 files changed, 120 insertions(+), 220 deletions(-) create mode 100644 docs/lib/snippets/dart_api/views.dart delete mode 100644 docs/pages/docs/Getting started/advanced_dart_tables.md delete mode 100644 docs/pages/docs/Getting started/index.md diff --git a/docs/lib/snippets/dart_api/tables.dart b/docs/lib/snippets/dart_api/tables.dart index c145748a3..325560958 100644 --- a/docs/lib/snippets/dart_api/tables.dart +++ b/docs/lib/snippets/dart_api/tables.dart @@ -76,5 +76,9 @@ class TableWithCustomConstraints extends Table { // #enddocregion custom-constraint-table // #docregion index -class Users extends Table {} -// #enddocregion index \ No newline at end of file +@TableIndex(name: 'user_name', columns: {#name}) +class Users extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get name => text()(); +} +// #enddocregion index diff --git a/docs/lib/snippets/dart_api/views.dart b/docs/lib/snippets/dart_api/views.dart new file mode 100644 index 000000000..720bb8198 --- /dev/null +++ b/docs/lib/snippets/dart_api/views.dart @@ -0,0 +1,33 @@ +import 'package:drift/drift.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()(); +} + +@DataClassName('Category') +class Categories extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get description => text()(); +} + +// #docregion view +abstract class CategoryTodoCount extends View { + // Getters define the tables that this view is reading from. + Todos get todos; + Categories get categories; + + // Custom expressions can be given a name by defining them as a getter:. + Expression get itemCount => todos.id.count(); + + @override + Query as() => + // Views can select columns defined as expression getters on the class, or + // they can reference columns from other tables. + select([categories.description, itemCount]) + .from(categories) + .join([innerJoin(todos, todos.category.equalsExp(categories.id))]); +} +// #enddocregion view diff --git a/docs/pages/docs/Advanced Features/builder_options.md b/docs/pages/docs/Advanced Features/builder_options.md index 42ba760d1..f5f875340 100644 --- a/docs/pages/docs/Advanced Features/builder_options.md +++ b/docs/pages/docs/Advanced Features/builder_options.md @@ -71,7 +71,7 @@ At the moment, drift supports these options: 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. diff --git a/docs/pages/docs/Advanced Features/expressions.md b/docs/pages/docs/Advanced Features/expressions.md index 72c71df40..c1e43e53f 100644 --- a/docs/pages/docs/Advanced Features/expressions.md +++ b/docs/pages/docs/Advanced Features/expressions.md @@ -155,7 +155,7 @@ __Note__: We're using `selectOnly` instead of `select` because we're not interes ### 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 diff --git a/docs/pages/docs/Advanced Features/isolates.md b/docs/pages/docs/Advanced Features/isolates.md index 6c4021eca..f313d5566 100644 --- a/docs/pages/docs/Advanced Features/isolates.md +++ b/docs/pages/docs/Advanced Features/isolates.md @@ -10,7 +10,7 @@ As sqlite3 is a synchronous C library, accessing the database from the main isol 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/Advanced Features/joins.md b/docs/pages/docs/Advanced Features/joins.md index 2cca9c654..10e313813 100644 --- a/docs/pages/docs/Advanced Features/joins.md +++ b/docs/pages/docs/Advanced Features/joins.md @@ -15,7 +15,7 @@ template: layouts/docs/single 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 }}). +defined in the [example]({{ "../setup.md" | pageUrl }}). {% include "blocks/snippet" snippets = snippets name = 'joinIntro' %} diff --git a/docs/pages/docs/Advanced Features/schema_inspection.md b/docs/pages/docs/Advanced Features/schema_inspection.md index cbf72ff98..555a3843e 100644 --- a/docs/pages/docs/Advanced Features/schema_inspection.md +++ b/docs/pages/docs/Advanced Features/schema_inspection.md @@ -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/Dart API/tables.md b/docs/pages/docs/Dart API/tables.md index f873f8aec..915acfce1 100644 --- a/docs/pages/docs/Dart API/tables.md +++ b/docs/pages/docs/Dart API/tables.md @@ -373,4 +373,14 @@ on columns, you can add those by overriding `customConstraints`: {% include "blocks/snippet" snippets = snippets name="custom-constraint-table" %} -## Index \ No newline at end of file +## Index + +An [index](https://sqlite.org/lang_createindex.html) on columns in a table allows rows identified +by these columns to be identified more easily. +In drift, you can apply an index to a table with the `@TableIndex` annotation. More than one +index can be applied to the same table by repeating the annotation: + +{% include "blocks/snippet" snippets = snippets name="index" %} + +Each index needs to have its own unique name. Typically, the name of the table is part of the +index' name to ensure unique names. diff --git a/docs/pages/docs/Dart API/views.md b/docs/pages/docs/Dart API/views.md index 63e63d141..d8332c852 100644 --- a/docs/pages/docs/Dart API/views.md +++ b/docs/pages/docs/Dart API/views.md @@ -4,3 +4,49 @@ data: 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]({{ '../Advanced Features/type_converters.md' | pageUrl }}) + used in the column's definition from the table are also applied to the view's column. + + Both kind of columns will be added to the data class for the view when selected. + +Finally, a view needs to be added to a database or accessor by including it in the +`views` parameter: + +```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/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/Getting started/advanced_dart_tables.md b/docs/pages/docs/Getting started/advanced_dart_tables.md deleted file mode 100644 index 0e8dbe7c9..000000000 --- a/docs/pages/docs/Getting started/advanced_dart_tables.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -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 -path: /old-tables -template: layouts/docs/single ---- - -{% 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. - -## 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 - - -## Views - -It is also possible to define [SQL views](https://www.sqlite.org/lang_createview.html) -as Dart classes. -To do so, write an abstract class extending `View`. This example declares a view reading -the amount of todo-items added to a category in the schema from [the example]({{ 'index.md' | pageUrl }}): - -{% include "blocks/snippet" snippets = snippets name = 'view' %} - -Inside a Dart view, use - -- abstract getters to declare tables that you'll read from (e.g. `TodosTable get todos`). -- `Expression` getters to add columns: (e.g. `itemCount => todos.id.count()`). -- the overridden `as` method to define the select statement backing the view. - The columns referenced in `select` may refer to two kinds of columns: - - Columns defined on the view itself (like `itemCount` in the example above). - - Columns defined on referenced tables (like `categories.description` in the example). - For these references, advanced drift features like [type converters]({{ '../Advanced Features/type_converters.md' | pageUrl }}) - used in the column's definition from the table are also applied to the view's column. - - Both kind of columns will be added to the data class for the view when selected. - -Finally, a view needs to be added to a database or accessor by including it in the -`views` parameter: - -```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/index.md b/docs/pages/docs/Getting started/index.md deleted file mode 100644 index 0fa0b75fd..000000000 --- a/docs/pages/docs/Getting started/index.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -data: - title: Getting started - description: Simple guide to get a drift project up and running. - weight: 1 - hide_section_index: true -path: /docs/getting-started-old -template: layouts/docs/list -aliases: - - /getting-started-old/ # 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 index 7e857e586..d9dd5b27d 100644 --- a/docs/pages/docs/Getting started/starting_with_sql.md +++ b/docs/pages/docs/Getting started/starting_with_sql.md @@ -6,7 +6,7 @@ data: template: layouts/docs/single --- -The regular [getting started guide]({{ "index.md" | pageUrl }}) explains how to get started with drift by +The regular [getting started guide]({{ "../setup.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). diff --git a/docs/pages/docs/Getting started/writing_queries.md b/docs/pages/docs/Getting started/writing_queries.md index 314929ace..0fcc486ad 100644 --- a/docs/pages/docs/Getting started/writing_queries.md +++ b/docs/pages/docs/Getting started/writing_queries.md @@ -10,7 +10,7 @@ template: layouts/docs/single --- {% block "blocks/pageinfo" %} -__Note__: This assumes that you've already completed [the setup]({{ "index.md" | pageUrl }}). +__Note__: This assumes that you've already completed [the setup]({{ "../setup.md" | pageUrl }}). {% endblock %} For each table you've specified in the `@DriftDatabase` annotation on your database class, @@ -297,7 +297,7 @@ 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( diff --git a/docs/pages/docs/Other engines/web.md b/docs/pages/docs/Other engines/web.md index c13528b78..cebe8da44 100644 --- a/docs/pages/docs/Other engines/web.md +++ b/docs/pages/docs/Other engines/web.md @@ -128,7 +128,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: diff --git a/docs/pages/docs/Using SQL/custom_queries.md b/docs/pages/docs/Using SQL/custom_queries.md index 4279956a3..5e2304cc7 100644 --- a/docs/pages/docs/Using SQL/custom_queries.md +++ b/docs/pages/docs/Using SQL/custom_queries.md @@ -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/Using SQL/drift_files.md index b6d67d7b4..744cfd17a 100644 --- a/docs/pages/docs/Using SQL/drift_files.md +++ b/docs/pages/docs/Using SQL/drift_files.md @@ -143,7 +143,7 @@ Instead of using an integer mapping enums by their index, you can also store the 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: diff --git a/docs/pages/docs/faq.md b/docs/pages/docs/faq.md index ca2ea531c..b643e4544 100644 --- a/docs/pages/docs/faq.md +++ b/docs/pages/docs/faq.md @@ -7,7 +7,7 @@ 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: diff --git a/docs/pages/docs/index.md b/docs/pages/docs/index.md index 94621b48d..3d9fc6470 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. diff --git a/docs/pages/docs/platforms.md b/docs/pages/docs/platforms.md index f7896fd03..1fc27d45c 100644 --- a/docs/pages/docs/platforms.md +++ b/docs/pages/docs/platforms.md @@ -47,7 +47,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`! diff --git a/docs/pages/docs/setup.md b/docs/pages/docs/setup.md index 099090050..4846181f7 100644 --- a/docs/pages/docs/setup.md +++ b/docs/pages/docs/setup.md @@ -113,8 +113,14 @@ started with drift: - 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 }}). - General [notes on how to integrate drift with your app's architecture]({{ 'Dart API/architecture.md' | pageUrl }}). +- 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. 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. + +[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/index.html b/docs/pages/index.html index 78c66c175..6ae4d48d4 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 %} diff --git a/docs/pages/v2.html b/docs/pages/v2.html index 7278c3fe4..89928f99f 100644 --- a/docs/pages/v2.html +++ b/docs/pages/v2.html @@ -109,7 +109,7 @@ {% 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 }}) From 999c17e19af6f3fc9c481e09c63780001b823b46 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 16 Sep 2023 19:42:53 +0200 Subject: [PATCH 05/12] Port docs on joins and basic queries --- docs/build.yaml | 2 + docs/lib/snippets/_shared/todo_tables.dart | 26 ++ .../snippets/_shared/todo_tables.drift.dart | 432 ++++++++++++++++++ .../snippets/{ => dart_api}/expressions.dart | 9 +- .../snippets/{queries => dart_api}/json.dart | 0 .../{queries.dart => dart_api/select.dart} | 147 ++++-- .../snippets/{ => dart_api}/transactions.dart | 18 +- .../snippets/drift_files/custom_queries.dart | 14 +- .../drift_files/custom_queries.drift.dart | 40 ++ docs/lib/snippets/tables/advanced.dart | 38 -- docs/lib/snippets/tables/filename.dart | 81 ---- .../docs/Advanced Features/builder_options.md | 2 +- .../docs/Advanced Features/expressions.md | 262 ----------- docs/pages/docs/Advanced Features/joins.md | 253 ---------- .../Advanced Features/schema_inspection.md | 2 +- docs/pages/docs/Dart API/daos.md | 2 +- docs/pages/docs/Dart API/expressions.md | 252 ++++++++++ docs/pages/docs/Dart API/select.md | 315 +++++++++++++ docs/pages/docs/Dart API/tables.md | 2 +- docs/pages/docs/Dart API/transactions.md | 2 +- docs/pages/docs/Dart API/writes.md | 190 ++++++++ .../docs/Getting started/starting_with_sql.md | 8 +- .../docs/Getting started/writing_queries.md | 300 ------------ docs/pages/docs/Using SQL/drift_files.md | 2 +- 24 files changed, 1393 insertions(+), 1006 deletions(-) create mode 100644 docs/lib/snippets/_shared/todo_tables.dart create mode 100644 docs/lib/snippets/_shared/todo_tables.drift.dart rename docs/lib/snippets/{ => dart_api}/expressions.dart (62%) rename docs/lib/snippets/{queries => dart_api}/json.dart (100%) rename docs/lib/snippets/{queries.dart => dart_api/select.dart} (55%) rename docs/lib/snippets/{ => dart_api}/transactions.dart (74%) create mode 100644 docs/lib/snippets/drift_files/custom_queries.drift.dart delete mode 100644 docs/lib/snippets/tables/advanced.dart delete mode 100644 docs/lib/snippets/tables/filename.dart delete mode 100644 docs/pages/docs/Advanced Features/expressions.md delete mode 100644 docs/pages/docs/Advanced Features/joins.md diff --git a/docs/build.yaml b/docs/build.yaml index 5b7b9234c..ab3f84e96 100644 --- a/docs/build.yaml +++ b/docs/build.yaml @@ -56,7 +56,9 @@ targets: version: "3.39" generate_for: include: &modular + - "lib/snippets/_shared/**" - "lib/snippets/modular/**" + - "lib/snippets/drift_files/custom_queries.*" drift_dev:modular: enabled: true options: *options diff --git a/docs/lib/snippets/_shared/todo_tables.dart b/docs/lib/snippets/_shared/todo_tables.dart new file mode 100644 index 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/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/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/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/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/tables/advanced.dart b/docs/lib/snippets/tables/advanced.dart deleted file mode 100644 index 99ea50ef1..000000000 --- a/docs/lib/snippets/tables/advanced.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:drift/drift.dart'; - -import 'filename.dart'; - -// #docregion unique -class WithUniqueConstraints extends Table { - IntColumn get a => integer().unique()(); - - IntColumn get b => integer()(); - IntColumn get c => integer()(); - - @override - List> get uniqueKeys => [ - {b, c} - ]; - - // Effectively, this table has two unique key sets: (a) and (b, c). -} -// #enddocregion unique - -// #docregion view -abstract class CategoryTodoCount extends View { - // Getters define the tables that this view is reading from. - Todos get todos; - Categories get categories; - - // Custom expressions can be given a name by defining them as a getter:. - Expression get itemCount => todos.id.count(); - - @override - Query as() => - // Views can select columns defined as expression getters on the class, or - // they can reference columns from other tables. - select([categories.description, itemCount]) - .from(categories) - .join([innerJoin(todos, todos.category.equalsExp(categories.id))]); -} -// #enddocregion view diff --git a/docs/lib/snippets/tables/filename.dart b/docs/lib/snippets/tables/filename.dart deleted file mode 100644 index 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/builder_options.md b/docs/pages/docs/Advanced Features/builder_options.md index f5f875340..8b96eba51 100644 --- a/docs/pages/docs/Advanced Features/builder_options.md +++ b/docs/pages/docs/Advanced Features/builder_options.md @@ -44,7 +44,7 @@ At the moment, drift supports these options: of inserted data and report detailed errors when the integrity is violated. If you're only using inserts with SQL, or don't need this functionality, enabling this flag can help to reduce the amount generated code. -* `use_data_class_name_for_companions`: By default, the name for [companion classes]({{ "../Getting started/writing_queries.md#updates-and-deletes" | pageUrl }}) +* `use_data_class_name_for_companions`: By default, the name for [companion classes]({{ "../Dart API/writes.md#updates-and-deletes" | pageUrl }}) is based on the table name (e.g. a `@DataClassName('Users') class UsersTable extends Table` would generate a `UsersTableCompanion`). With this option, the name is based on the data class (so `UsersCompanion` in this case). diff --git a/docs/pages/docs/Advanced Features/expressions.md b/docs/pages/docs/Advanced Features/expressions.md deleted file mode 100644 index c1e43e53f..000000000 --- a/docs/pages/docs/Advanced Features/expressions.md +++ /dev/null @@ -1,262 +0,0 @@ ---- -data: - title: Expressions - description: Deep-dive into what kind of SQL expressions can be written in Dart - weight: 200 - -# used to be in the "getting started" section -path: docs/getting-started/expressions-old/ -template: layouts/docs/single ---- - -Expressions are pieces of sql that return a value when the database interprets them. -The Dart API from drift allows you to write most expressions in Dart and then convert -them to sql. Expressions are used in all kinds of situations. For instance, `where` -expects an expression that returns a boolean. - -In most cases, you're writing an expression that combines other expressions. Any -column name is a valid expression, so for most `where` clauses you'll be writing -a expression that wraps a column name in some kind of comparison. - -{% assign snippets = 'package:drift_docs/snippets/expressions.dart.excerpt.json' | readString | json_decode %} - -## Comparisons -Every expression can be compared to a value by using `equals`. If you want to compare -an expression to another expression, you can use `equalsExpr`. For numeric and datetime -expressions, you can also use a variety of methods like `isSmallerThan`, `isSmallerOrEqual` -and so on to compare them: -```dart -// find all animals with less than 5 legs: -(select(animals)..where((a) => a.amountOfLegs.isSmallerThanValue(5))).get(); - -// find all animals who's average livespan is shorter than their amount of legs (poor flies) -(select(animals)..where((a) => a.averageLivespan.isSmallerThan(a.amountOfLegs))); - -Future> findAnimalsByLegs(int legCount) { - return (select(animals)..where((a) => a.legs.equals(legCount))).get(); -} -``` - -## Boolean algebra -You can nest boolean expressions by using the `&`, `|` operators and the `not` method -exposed by drift: - -```dart -// find all animals that aren't mammals and have 4 legs -select(animals)..where((a) => a.isMammal.not() & a.amountOfLegs.equals(4)); - -// find all animals that are mammals or have 2 legs -select(animals)..where((a) => a.isMammal | a.amountOfLegs.equals(2)); -``` - -## Arithmetic - -For `int` and `double` expressions, you can use the `+`, `-`, `*` and `/` operators. To -run calculations between a sql expression and a Dart value, wrap it in a `Variable`: -```dart -Future> canBeBought(int amount, int price) { - return (select(products)..where((p) { - final totalPrice = p.price * Variable(amount); - return totalPrice.isSmallerOrEqualValue(price); - })).get(); -} -``` - -String expressions define a `+` operator as well. Just like you would expect, it performs -a concatenation in sql. - -For integer values, you can use `~`, `bitwiseAnd` and `bitwiseOr` to perform -bitwise operations: - -{% include "blocks/snippet" snippets = snippets name = 'bitwise' %} - -## Nullability -To check whether an expression evaluates to `NULL` in sql, you can use the `isNull` extension: - -```dart -final withoutCategories = select(todos)..where((row) => row.category.isNull()); -``` - -The expression returned will resolve to `true` if the inner expression resolves to null -and `false` otherwise. -As you would expect, `isNotNull` works the other way around. - -To use a fallback value when an expression evaluates to `null`, you can use the `coalesce` -function. It takes a list of expressions and evaluates to the first one that isn't `null`: - -```dart -final category = coalesce([todos.category, const Constant(1)]); -``` - -This corresponds to the `??` operator in Dart. - -## Date and Time -For columns and expressions that return a `DateTime`, you can use the -`year`, `month`, `day`, `hour`, `minute` and `second` getters to extract individual -fields from that date: -```dart -select(users)..where((u) => u.birthDate.year.isLessThan(1950)) -``` - -The individual fields like `year`, `month` and so on are expressions themselves. This means -that you can use operators and comparisons on them. -To obtain the current date or the current time as an expression, use the `currentDate` -and `currentDateAndTime` constants provided by drift. - -You can also use the `+` and `-` operators to add or subtract a duration from a time column: - -```dart -final toNextWeek = TasksCompanion.custom(dueDate: tasks.dueDate + Duration(weeks: 1)); -update(tasks).write(toNextWeek); -``` - -## `IN` and `NOT IN` -You can check whether an expression is in a list of values by using the `isIn` and `isNotIn` -methods: -```dart -select(animals)..where((a) => a.amountOfLegs.isIn([3, 7, 4, 2]); -``` - -Again, the `isNotIn` function works the other way around. - -## Aggregate functions (like count and sum) {#aggregate} - -[Aggregate functions](https://www.sqlite.org/lang_aggfunc.html) are available -from the Dart api. Unlike regular functions, aggregate functions operate on multiple rows at -once. -By default, they combine all rows that would be returned by the select statement into a single value. -You can also make them run over different groups in the result by using -[group by]({{ "joins.md#group-by" | pageUrl }}). - -### Comparing - -You can use the `min` and `max` methods on numeric and datetime expressions. They return the smallest -or largest value in the result set, respectively. - -### Arithmetic - -The `avg`, `sum` and `total` methods are available. For instance, you could watch the average length of -a todo item with this query: -```dart -Stream averageItemLength() { - final avgLength = todos.content.length.avg(); - - final query = selectOnly(todos) - ..addColumns([avgLength]); - - return query.map((row) => row.read(avgLength)).watchSingle(); -} -``` - -__Note__: We're using `selectOnly` instead of `select` because we're not interested in any colum that -`todos` provides - we only care about the average length. More details are available -[here]({{ "joins.md#group-by" | pageUrl }}) - -### Counting - -Sometimes, it's useful to count how many rows are present in a group. By using the -[table layout from the example]({{ "../setup.md" | pageUrl }}), this -query will report how many todo entries are associated to each category: - -```dart -final amountOfTodos = todos.id.count(); - -final query = db.select(categories).join([ - innerJoin( - todos, - todos.category.equalsExp(categories.id), - useColumns: false, - ) -]); -query - ..addColumns([amountOfTodos]) - ..groupBy([categories.id]); -``` - -If you don't want to count duplicate values, you can use `count(distinct: true)`. -Sometimes, you only need to count values that match a condition. For that, you can -use the `filter` parameter on `count`. -To count all rows (instead of a single value), you can use the top-level `countAll()` -function. - -More information on how to write aggregate queries with drift's Dart api is available -[here]({{ "joins.md#group-by" | pageUrl }}) - -### group_concat - -The `groupConcat` function can be used to join multiple values into a single string: - -```dart -Stream allTodoContent() { - final allContent = todos.content.groupConcat(); - final query = selectOnly(todos)..addColumns(allContent); - - return query.map((row) => row.read(query)).watchSingle(); -} -``` - -The separator defaults to a comma without surrounding whitespace, but it can be changed -with the `separator` argument on `groupConcat`. - -## Mathematical functions and regexp - -When using a `NativeDatabase`, a basic set of trigonometric functions will be available. -It also defines the `REGEXP` function, which allows you to use `a REGEXP b` in sql queries. -For more information, see the [list of functions]({{ "../Other engines/vm.md#moor-only-functions" | pageUrl }}) here. - -## Subqueries - -Drift has basic support for subqueries in expressions. - -### Scalar subqueries - -A _scalar subquery_ is a select statement that returns exactly one row with exactly one column. -Since it returns exactly one value, it can be used in another query: - -```dart -Future> findTodosInCategory(String description) async { - final groupId = selectOnly(categories) - ..addColumns([categories.id]) - ..where(categories.description.equals(description)); - - return select(todos)..where((row) => row.category.equalsExp(subqueryExpression(groupId))); -} -``` - -Here, `groupId` is a regular select statement. By default drift would select all columns, so we use -`selectOnly` to only load the id of the category we care about. -Then, we can use `subqueryExpression` to embed that query into an expression that we're using as -a filter. - -### `isInQuery` - -Similar to [`isIn` and `isNotIn`](#in-and-not-in) functions, you can use `isInQuery` to pass -a subquery instead of a direct set of values. - -The subquery must return exactly one column, but it is allowed to return more than one row. -`isInQuery` returns true if that value is present in the query. - -### Exists - -The `existsQuery` and `notExistsQuery` functions can be used to check if a subquery contains -any rows. For instance, we could use this to find empty categories: - -{% include "blocks/snippet" snippets = snippets name = 'emptyCategories' %} - -### Full subqueries - -Drift also supports subqueries that appear in `JOIN`s, which are described in the -[documentation for joins]({{ 'joins.md#subqueries' | pageUrl }}). - -## Custom expressions -If you want to inline custom sql into Dart queries, you can use a `CustomExpression` class. -It takes a `sql` parameter that lets you write custom expressions: -```dart -const inactive = CustomExpression("julianday('now') - julianday(last_login) > 60"); -select(users)..where((u) => inactive); -``` - -_Note_: It's easy to write invalid queries by using `CustomExpressions` too much. If you feel like -you need to use them because a feature you use is not available in drift, consider creating an issue -to let us know. If you just prefer sql, you could also take a look at -[compiled sql]({{ "../Using SQL/custom_queries.md" | pageUrl }}) which is typesafe to use. diff --git a/docs/pages/docs/Advanced Features/joins.md b/docs/pages/docs/Advanced Features/joins.md deleted file mode 100644 index 10e313813..000000000 --- a/docs/pages/docs/Advanced Features/joins.md +++ /dev/null @@ -1,253 +0,0 @@ ---- -data: - title: "Advanced queries in Dart" - weight: 1 - description: Use sql joins or custom expressions from the Dart api -aliases: - - queries/joins/ -template: layouts/docs/single ---- - -{% assign snippets = 'package:drift_docs/snippets/queries.dart.excerpt.json' | readString | json_decode %} - -## Joins - -Drift supports sql joins to write queries that operate on more than one table. To use that feature, start -a select regular select statement with `select(table)` and then add a list of joins using `.join()`. For -inner and left outer joins, a `ON` expression needs to be specified. Here's an example using the tables -defined in the [example]({{ "../setup.md" | pageUrl }}). - -{% include "blocks/snippet" snippets = snippets name = 'joinIntro' %} - -Of course, you can also join multiple tables: - -{% include "blocks/snippet" snippets = snippets name = 'otherTodosInSameCategory' %} - -## Parsing results - -Calling `get()` or `watch` on a select statement with join returns a `Future` or `Stream` of -`List`, respectively. Each `TypedResult` represents a row from which data can be -read. It contains a `rawData` getter to obtain the raw columns. But more importantly, the -`readTable` method can be used to read a data class from a table. - -In the example query above, we can read the todo entry and the category from each row like this: - -{% include "blocks/snippet" snippets = snippets name = 'results' %} - -_Note_: `readTable` will throw an `ArgumentError` when a table is not present in the row. For instance, -todo entries might not be in any category. To account for that, we use `row.readTableOrNull` to load -categories. - -## Custom columns - -Select statements aren't limited to columns from tables. You can also include more complex expressions in the -query. For each row in the result, those expressions will be evaluated by the database engine. - -```dart -class EntryWithImportance { - final TodoEntry entry; - final bool important; - - EntryWithImportance(this.entry, this.important); -} - -Future> loadEntries() { - // assume that an entry is important if it has the string "important" somewhere in its content - final isImportant = todos.content.like('%important%'); - - return select(todos).addColumns([isImportant]).map((row) { - final entry = row.readTable(todos); - final entryIsImportant = row.read(isImportant); - - return EntryWithImportance(entry, entryIsImportant); - }).get(); -} -``` - -Note that the `like` check is _not_ performed in Dart - it's sent to the underlying database engine which -can efficiently compute it for all rows. - -## Aliases -Sometimes, a query references a table more than once. Consider the following example to store saved routes for a -navigation system: -```dart -class GeoPoints extends Table { - IntColumn get id => integer().autoIncrement()(); - TextColumn get name => text()(); - TextColumn get latitude => text()(); - TextColumn get longitude => text()(); -} - -class Routes extends Table { - - IntColumn get id => integer().autoIncrement()(); - TextColumn get name => text()(); - - // contains the id for the start and destination geopoint. - IntColumn get start => integer()(); - IntColumn get destination => integer()(); -} -``` - -Now, let's say we wanted to also load the start and destination `GeoPoint` object for each route. We'd have to use -a join on the `geo-points` table twice: For the start and destination point. To express that in a query, aliases -can be used: -```dart -class RouteWithPoints { - final Route route; - final GeoPoint start; - final GeoPoint destination; - - RouteWithPoints({this.route, this.start, this.destination}); -} - -// inside the database class: -Future> loadRoutes() async { - // create aliases for the geoPoints table so that we can reference it twice - final start = alias(geoPoints, 's'); - final destination = alias(geoPoints, 'd'); - - final rows = await select(routes).join([ - innerJoin(start, start.id.equalsExp(routes.start)), - innerJoin(destination, destination.id.equalsExp(routes.destination)), - ]).get(); - - return rows.map((resultRow) { - return RouteWithPoints( - route: resultRow.readTable(routes), - start: resultRow.readTable(start), - destination: resultRow.readTable(destination), - ); - }).toList(); -} -``` -The generated statement then looks like this: -```sql -SELECT - routes.id, routes.name, routes.start, routes.destination, - s.id, s.name, s.latitude, s.longitude, - d.id, d.name, d.latitude, d.longitude -FROM routes - INNER JOIN geo_points s ON s.id = routes.start - INNER JOIN geo_points d ON d.id = routes.destination -``` - -## `ORDER BY` and `WHERE` on joins - -Similar to queries on a single table, `orderBy` and `where` can be used on joins too. -The initial example from above is expanded to only include todo entries with a specified -filter and to order results based on the category's id: - -```dart -Stream> entriesWithCategory(String entryFilter) { - final query = select(todos).join([ - leftOuterJoin(categories, categories.id.equalsExp(todos.category)), - ]); - query.where(todos.content.like(entryFilter)); - query.orderBy([OrderingTerm.asc(categories.id)]); - // ... -} -``` - -As a join can have more than one table, all tables in `where` and `orderBy` have to -be specified directly (unlike the callback on single-table queries that gets called -with the right table by default). - -## Group by - -Sometimes, you need to run queries that _aggregate_ data, meaning that data you're interested in -comes from multiple rows. Common questions include - -- how many todo entries are in each category? -- how many entries did a user complete each month? -- what's the average length of a todo entry? - -What these queries have in common is that data from multiple rows needs to be combined into a single -row. In sql, this can be achieved with "aggregate functions", for which drift has -[builtin support]({{ "expressions.md#aggregate" | pageUrl }}). - -_Additional info_: A good tutorial for group by in sql is available [here](https://www.sqlitetutorial.net/sqlite-group-by/). - -To write a query that answers the first question for us, we can use the `count` function. -We're going to select all categories and join each todo entry for each category. What's special is that we set -`useColumns: false` on the join. We do that because we're not interested in the columns of the todo item. -We only care about how many there are. By default, drift would attempt to read each todo item when it appears -in a join. - -{% include "blocks/snippet" snippets = snippets name = 'countTodosInCategories' %} - -To find the average length of a todo entry, we use `avg`. In this case, we don't even have to use -a `join` since all the data comes from a single table (todos). -That's a problem though - in the join, we used `useColumns: false` because we weren't interested -in the columns of each todo item. Here we don't care about an individual item either, but there's -no join where we could set that flag. -Drift provides a special method for this case - instead of using `select`, we use `selectOnly`. -The "only" means that drift will only report columns we added via "addColumns". In a regular select, -all columns from the table would be selected, which is what you'd usually need. - -{% include "blocks/snippet" snippets = snippets name = 'averageItemLength' %} - -## Using selects as inserts - -In SQL, an `INSERT INTO SELECT` statement can be used to efficiently insert the rows from a `SELECT` -statement into a table. -It is possible to construct these statements in drift with the `insertFromSelect` method. -This example shows how that method is used to construct a statement that creates a new category -for each todo entry that didn't have one assigned before: - -{% include "blocks/snippet" snippets = snippets name = 'createCategoryForUnassignedTodoEntries' %} - -The first parameter for `insertFromSelect` is the select statement statement to use as a source. -Then, the `columns` map maps columns from the table in which rows are inserted to columns from the -select statement. -In the example, the `newDescription` expression as added as a column to the query. -Then, the map entry `categories.description: newDescription` is used so that the `description` column -for new category rows gets set to that expression. - -## Subqueries - -Starting from drift 2.11, you can use `Subquery` to use an existing select statement as part of more -complex join. - -This snippet uses `Subquery` to count how many of the top-10 todo items (by length of their title) are -in each category. -It does this by first creating a select statement for the top-10 items (but not executing it), and then -joining this select statement onto a larger one grouping by category: - -{% include "blocks/snippet" snippets = snippets name = 'subquery' %} - -Any statement can be used as a subquery. But be aware that, unlike [subquery expressions]({{ 'expressions.md#scalar-subqueries' | pageUrl }}), full subqueries can't use tables from the outer select statement. - -## JSON support - -{% assign json_snippet = 'package:drift_docs/snippets/queries/json.dart.excerpt.json' | readString | json_decode %} - -sqlite3 has great support for [JSON operators](https://sqlite.org/json1.html) that are also available -in drift (under the additional `'package:drift/extensions/json1.dart'` import). -JSON support is helpful when storing a dynamic structure that is best represented with JSON, or when -you have an existing structure (perhaps because you're migrating from a document-based storage) -that you need to support. - -As an example, consider a contact book application that started with a JSON structure to store -contacts: - -{% include "blocks/snippet" snippets = json_snippet name = 'existing' %} - -To easily store this contact representation in a drift database, one could use a JSON column: - -{% include "blocks/snippet" snippets = json_snippet name = 'contacts' %} - -Note the `name` column as well: It uses `generatedAs` with the `jsonExtract` function to -extract the `name` field from the JSON value on the fly. -The full syntax for JSON path arguments is explained on the [sqlite3 website](https://sqlite.org/json1.html#path_arguments). - -To make the example more complex, let's look at another table storing a log of phone calls: - -{% include "blocks/snippet" snippets = json_snippet name = 'calls' %} - -Let's say we wanted to find the contact for each call, if there is any with a matching phone number. -For this to be expressible in SQL, each `contacts` row would somehow have to be expanded into a row -for each stored phone number. -Luckily, the `json_each` function in sqlite3 can do exactly that, and drift exposes it: - -{% include "blocks/snippet" snippets = json_snippet name = 'calls-with-contacts' %} diff --git a/docs/pages/docs/Advanced Features/schema_inspection.md b/docs/pages/docs/Advanced Features/schema_inspection.md index 555a3843e..ecdd5e3d6 100644 --- a/docs/pages/docs/Advanced Features/schema_inspection.md +++ b/docs/pages/docs/Advanced Features/schema_inspection.md @@ -7,7 +7,7 @@ template: layouts/docs/single {% assign snippets = 'package:drift_docs/snippets/modular/schema_inspection.dart.excerpt.json' | readString | json_decode %} -Thanks to the typesafe table classes generated by drift, [writing SQL queries]({{ '../Getting started/writing_queries.md' | pageUrl }}) in Dart +Thanks to the typesafe table classes generated by drift, [writing SQL queries]({{ '../Dart API/select.md' | pageUrl }}) in Dart is simple and safe. However, these queries are usually written against a specific table. And while drift supports inheritance for tables, sometimes it is easier to access tables reflectively. Luckily, code generated by drift implements interfaces which can be used to do just that. diff --git a/docs/pages/docs/Dart API/daos.md b/docs/pages/docs/Dart API/daos.md index d20d9ce2e..c3af6405c 100644 --- a/docs/pages/docs/Dart API/daos.md +++ b/docs/pages/docs/Dart API/daos.md @@ -2,7 +2,7 @@ data: title: "DAOs" description: Keep your database code modular with DAOs -path: /docs/advanced-features/daos +path: /docs/advanced-features/daos/ aliases: - /daos/ template: layouts/docs/single diff --git a/docs/pages/docs/Dart API/expressions.md b/docs/pages/docs/Dart API/expressions.md index 58e193fbc..52bf18404 100644 --- a/docs/pages/docs/Dart API/expressions.md +++ b/docs/pages/docs/Dart API/expressions.md @@ -8,3 +8,255 @@ data: path: docs/getting-started/expressions/ template: layouts/docs/single --- + +Expressions are pieces of sql that return a value when the database interprets them. +The Dart API from drift allows you to write most expressions in Dart and then convert +them to sql. Expressions are used in all kinds of situations. For instance, `where` +expects an expression that returns a boolean. + +In most cases, you're writing an expression that combines other expressions. Any +column name is a valid expression, so for most `where` clauses you'll be writing +a expression that wraps a column name in some kind of comparison. + +{% assign snippets = 'package:drift_docs/snippets/dart_api/expressions.dart.excerpt.json' | readString | json_decode %} + +## Comparisons +Every expression can be compared to a value by using `equals`. If you want to compare +an expression to another expression, you can use `equalsExpr`. For numeric and datetime +expressions, you can also use a variety of methods like `isSmallerThan`, `isSmallerOrEqual` +and so on to compare them: +```dart +// find all animals with less than 5 legs: +(select(animals)..where((a) => a.amountOfLegs.isSmallerThanValue(5))).get(); + +// find all animals who's average livespan is shorter than their amount of legs (poor flies) +(select(animals)..where((a) => a.averageLivespan.isSmallerThan(a.amountOfLegs))); + +Future> findAnimalsByLegs(int legCount) { + return (select(animals)..where((a) => a.legs.equals(legCount))).get(); +} +``` + +## Boolean algebra +You can nest boolean expressions by using the `&`, `|` operators and the `not` method +exposed by drift: + +```dart +// find all animals that aren't mammals and have 4 legs +select(animals)..where((a) => a.isMammal.not() & a.amountOfLegs.equals(4)); + +// find all animals that are mammals or have 2 legs +select(animals)..where((a) => a.isMammal | a.amountOfLegs.equals(2)); +``` + +## Arithmetic + +For `int` and `double` expressions, you can use the `+`, `-`, `*` and `/` operators. To +run calculations between a sql expression and a Dart value, wrap it in a `Variable`: +```dart +Future> canBeBought(int amount, int price) { + return (select(products)..where((p) { + final totalPrice = p.price * Variable(amount); + return totalPrice.isSmallerOrEqualValue(price); + })).get(); +} +``` + +String expressions define a `+` operator as well. Just like you would expect, it performs +a concatenation in sql. + +For integer values, you can use `~`, `bitwiseAnd` and `bitwiseOr` to perform +bitwise operations: + +{% include "blocks/snippet" snippets = snippets name = 'bitwise' %} + +## Nullability +To check whether an expression evaluates to `NULL` in sql, you can use the `isNull` extension: + +```dart +final withoutCategories = select(todos)..where((row) => row.category.isNull()); +``` + +The expression returned will resolve to `true` if the inner expression resolves to null +and `false` otherwise. +As you would expect, `isNotNull` works the other way around. + +To use a fallback value when an expression evaluates to `null`, you can use the `coalesce` +function. It takes a list of expressions and evaluates to the first one that isn't `null`: + +```dart +final category = coalesce([todos.category, const Constant(1)]); +``` + +This corresponds to the `??` operator in Dart. + +## Date and Time +For columns and expressions that return a `DateTime`, you can use the +`year`, `month`, `day`, `hour`, `minute` and `second` getters to extract individual +fields from that date: +```dart +select(users)..where((u) => u.birthDate.year.isLessThan(1950)) +``` + +The individual fields like `year`, `month` and so on are expressions themselves. This means +that you can use operators and comparisons on them. +To obtain the current date or the current time as an expression, use the `currentDate` +and `currentDateAndTime` constants provided by drift. + +You can also use the `+` and `-` operators to add or subtract a duration from a time column: + +```dart +final toNextWeek = TasksCompanion.custom(dueDate: tasks.dueDate + Duration(weeks: 1)); +update(tasks).write(toNextWeek); +``` + +## `IN` and `NOT IN` +You can check whether an expression is in a list of values by using the `isIn` and `isNotIn` +methods: +```dart +select(animals)..where((a) => a.amountOfLegs.isIn([3, 7, 4, 2]); +``` + +Again, the `isNotIn` function works the other way around. + +## Aggregate functions (like count and sum) {#aggregate} + +[Aggregate functions](https://www.sqlite.org/lang_aggfunc.html) are available +from the Dart api. Unlike regular functions, aggregate functions operate on multiple rows at +once. +By default, they combine all rows that would be returned by the select statement into a single value. +You can also make them run over different groups in the result by using +[group by]({{ "select.md#group-by" | pageUrl }}). + +### Comparing + +You can use the `min` and `max` methods on numeric and datetime expressions. They return the smallest +or largest value in the result set, respectively. + +### Arithmetic + +The `avg`, `sum` and `total` methods are available. For instance, you could watch the average length of +a todo item with this query: +```dart +Stream averageItemLength() { + final avgLength = todos.content.length.avg(); + + final query = selectOnly(todos) + ..addColumns([avgLength]); + + return query.map((row) => row.read(avgLength)).watchSingle(); +} +``` + +__Note__: We're using `selectOnly` instead of `select` because we're not interested in any colum that +`todos` provides - we only care about the average length. More details are available +[here]({{ "select.md#group-by" | pageUrl }}) + +### Counting + +Sometimes, it's useful to count how many rows are present in a group. By using the +[table layout from the example]({{ "../setup.md" | pageUrl }}), this +query will report how many todo entries are associated to each category: + +```dart +final amountOfTodos = todos.id.count(); + +final query = db.select(categories).join([ + innerJoin( + todos, + todos.category.equalsExp(categories.id), + useColumns: false, + ) +]); +query + ..addColumns([amountOfTodos]) + ..groupBy([categories.id]); +``` + +If you don't want to count duplicate values, you can use `count(distinct: true)`. +Sometimes, you only need to count values that match a condition. For that, you can +use the `filter` parameter on `count`. +To count all rows (instead of a single value), you can use the top-level `countAll()` +function. + +More information on how to write aggregate queries with drift's Dart api is available +[here]({{ "select.md#group-by" | pageUrl }}) + +### group_concat + +The `groupConcat` function can be used to join multiple values into a single string: + +```dart +Stream allTodoContent() { + final allContent = todos.content.groupConcat(); + final query = selectOnly(todos)..addColumns(allContent); + + return query.map((row) => row.read(query)).watchSingle(); +} +``` + +The separator defaults to a comma without surrounding whitespace, but it can be changed +with the `separator` argument on `groupConcat`. + +## Mathematical functions and regexp + +When using a `NativeDatabase`, a basic set of trigonometric functions will be available. +It also defines the `REGEXP` function, which allows you to use `a REGEXP b` in sql queries. +For more information, see the [list of functions]({{ "../Other engines/vm.md#moor-only-functions" | pageUrl }}) here. + +## Subqueries + +Drift has basic support for subqueries in expressions. + +### Scalar subqueries + +A _scalar subquery_ is a select statement that returns exactly one row with exactly one column. +Since it returns exactly one value, it can be used in another query: + +```dart +Future> findTodosInCategory(String description) async { + final groupId = selectOnly(categories) + ..addColumns([categories.id]) + ..where(categories.description.equals(description)); + + return select(todos)..where((row) => row.category.equalsExp(subqueryExpression(groupId))); +} +``` + +Here, `groupId` is a regular select statement. By default drift would select all columns, so we use +`selectOnly` to only load the id of the category we care about. +Then, we can use `subqueryExpression` to embed that query into an expression that we're using as +a filter. + +### `isInQuery` + +Similar to [`isIn` and `isNotIn`](#in-and-not-in) functions, you can use `isInQuery` to pass +a subquery instead of a direct set of values. + +The subquery must return exactly one column, but it is allowed to return more than one row. +`isInQuery` returns true if that value is present in the query. + +### Exists + +The `existsQuery` and `notExistsQuery` functions can be used to check if a subquery contains +any rows. For instance, we could use this to find empty categories: + +{% include "blocks/snippet" snippets = snippets name = 'emptyCategories' %} + +### Full subqueries + +Drift also supports subqueries that appear in `JOIN`s, which are described in the +[documentation for joins]({{ 'select.md#subqueries' | pageUrl }}). + +## Custom expressions +If you want to inline custom sql into Dart queries, you can use a `CustomExpression` class. +It takes a `sql` parameter that lets you write custom expressions: +```dart +const inactive = CustomExpression("julianday('now') - julianday(last_login) > 60"); +select(users)..where((u) => inactive); +``` + +_Note_: It's easy to write invalid queries by using `CustomExpressions` too much. If you feel like +you need to use them because a feature you use is not available in drift, consider creating an issue +to let us know. If you just prefer sql, you could also take a look at +[compiled sql]({{ "../Using SQL/custom_queries.md" | pageUrl }}) which is typesafe to use. diff --git a/docs/pages/docs/Dart API/select.md b/docs/pages/docs/Dart API/select.md index 85c0ade4b..be276460b 100644 --- a/docs/pages/docs/Dart API/select.md +++ b/docs/pages/docs/Dart API/select.md @@ -5,3 +5,318 @@ data: weight: 2 template: layouts/docs/single --- + +{% assign tables = 'package:drift_docs/snippets/_shared/todo_tables.dart.excerpt.json' | readString | json_decode %} +{% assign snippets = 'package:drift_docs/snippets/dart_api/select.dart.excerpt.json' | readString | json_decode %} + +This page describes how to write `SELECT` statements with drift's Dart API. +To make examples easier to grasp, they're referencing two common tables forming +the basis of a todo-list app: + +{% include "blocks/snippet" snippets = tables name = 'tables' %} + +For each table you've specified in the `@DriftDatabase` annotation on your database class, +a corresponding getter for a table will be generated. That getter can be used to +run statements: + +```dart +@DriftDatabase(tables: [TodoItems, Categories]) +class MyDatabase extends _$MyDatabase { + + // the schemaVersion getter and the constructor from the previous page + // have been omitted. + + // loads all todo entries + Future> get allTodoItems => select(todoItems).get(); + + // watches all todo entries in a given category. The stream will automatically + // emit new items whenever the underlying data changes. + Stream> watchEntriesInCategory(Category c) { + return (select(todos)..where((t) => t.category.equals(c.id))).watch(); + } +} +``` + +Drift makes writing queries easy and safe. This page describes how to write basic select +queries, but also explains how to use joins and subqueries for advanced queries. + +## Simple selects + +You can create `select` statements by starting them with `select(tableName)`, where the +table name +is a field generated for you by drift. Each table used in a database will have a matching field +to run queries against. Any query can be run once with `get()` or be turned into an auto-updating +stream using `watch()`. + +### Where +You can apply filters to a query by calling `where()`. The where method takes a function that +should map the given table to an `Expression` of boolean. A common way to create such expression +is by using `equals` on expressions. Integer columns can also be compared with `isBiggerThan` +and `isSmallerThan`. You can compose expressions using `a & b, a | b` and `a.not()`. For more +details on expressions, see [this guide]({{ "../Dart API/expressions.md" | pageUrl }}). + +### Limit +You can limit the amount of results returned by calling `limit` on queries. The method accepts +the amount of rows to return and an optional offset. + +{% include "blocks/snippet" snippets = snippets name = 'limit' %} + + +### Ordering +You can use the `orderBy` method on the select statement. It expects a list of functions that extract the individual +ordering terms from the table. You can use any expression as an ordering term - for more details, see +[this guide]({{ "../Dart API/expressions.md" | pageUrl }}). + +{% include "blocks/snippet" snippets = snippets name = 'order-by' %} + +You can also reverse the order by setting the `mode` property of the `OrderingTerm` to +`OrderingMode.desc`. + +### Single values +If you know a query is never going to return more than one row, wrapping the result in a `List` +can be tedious. Drift lets you work around that with `getSingle` and `watchSingle`: + +{% include "blocks/snippet" snippets = snippets name = 'single' %} + +If an entry with the provided id exists, it will be sent to the stream. Otherwise, +`null` will be added to stream. If a query used with `watchSingle` ever returns +more than one entry (which is impossible in this case), an error will be added +instead. + +### Mapping +Before calling `watch` or `get` (or the single variants), you can use `map` to transform +the result. + +{% include "blocks/snippet" snippets = snippets name = 'mapping' %} + +### Deferring get vs watch +If you want to make your query consumable as either a `Future` or a `Stream`, +you can refine your return type using one of the `Selectable` abstract base classes; + +{% include "blocks/snippet" snippets = snippets name = 'selectable' %} + +These base classes don't have query-building or `map` methods, signaling to the consumer +that they are complete results. + + +## Joins + +Drift supports sql joins to write queries that operate on more than one table. To use that feature, start +a select regular select statement with `select(table)` and then add a list of joins using `.join()`. For +inner and left outer joins, a `ON` expression needs to be specified. + +{% include "blocks/snippet" snippets = snippets name = 'joinIntro' %} + +Of course, you can also join multiple tables: + +{% include "blocks/snippet" snippets = snippets name = 'otherTodosInSameCategory' %} + +## Parsing results + +Calling `get()` or `watch` on a select statement with join returns a `Future` or `Stream` of +`List`, respectively. Each `TypedResult` represents a row from which data can be +read. It contains a `rawData` getter to obtain the raw columns. But more importantly, the +`readTable` method can be used to read a data class from a table. + +In the example query above, we can read the todo entry and the category from each row like this: + +{% include "blocks/snippet" snippets = snippets name = 'results' %} + +_Note_: `readTable` will throw an `ArgumentError` when a table is not present in the row. For instance, +todo entries might not be in any category. To account for that, we use `row.readTableOrNull` to load +categories. + +## Custom columns + +Select statements aren't limited to columns from tables. You can also include more complex expressions in the +query. For each row in the result, those expressions will be evaluated by the database engine. + +{% include "blocks/snippet" snippets = snippets name = 'custom-columns' %} + +Note that the `like` check is _not_ performed in Dart - it's sent to the underlying database engine which +can efficiently compute it for all rows. + +## Aliases +Sometimes, a query references a table more than once. Consider the following example to store saved routes for a +navigation system: +```dart +class GeoPoints extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get name => text()(); + TextColumn get latitude => text()(); + TextColumn get longitude => text()(); +} + +class Routes extends Table { + + IntColumn get id => integer().autoIncrement()(); + TextColumn get name => text()(); + + // contains the id for the start and destination geopoint. + IntColumn get start => integer()(); + IntColumn get destination => integer()(); +} +``` + +Now, let's say we wanted to also load the start and destination `GeoPoint` object for each route. We'd have to use +a join on the `geo-points` table twice: For the start and destination point. To express that in a query, aliases +can be used: +```dart +class RouteWithPoints { + final Route route; + final GeoPoint start; + final GeoPoint destination; + + RouteWithPoints({this.route, this.start, this.destination}); +} + +// inside the database class: +Future> loadRoutes() async { + // create aliases for the geoPoints table so that we can reference it twice + final start = alias(geoPoints, 's'); + final destination = alias(geoPoints, 'd'); + + final rows = await select(routes).join([ + innerJoin(start, start.id.equalsExp(routes.start)), + innerJoin(destination, destination.id.equalsExp(routes.destination)), + ]).get(); + + return rows.map((resultRow) { + return RouteWithPoints( + route: resultRow.readTable(routes), + start: resultRow.readTable(start), + destination: resultRow.readTable(destination), + ); + }).toList(); +} +``` +The generated statement then looks like this: +```sql +SELECT + routes.id, routes.name, routes.start, routes.destination, + s.id, s.name, s.latitude, s.longitude, + d.id, d.name, d.latitude, d.longitude +FROM routes + INNER JOIN geo_points s ON s.id = routes.start + INNER JOIN geo_points d ON d.id = routes.destination +``` + +## `ORDER BY` and `WHERE` on joins + +Similar to queries on a single table, `orderBy` and `where` can be used on joins too. +The initial example from above is expanded to only include todo entries with a specified +filter and to order results based on the category's id: + +```dart +Stream> entriesWithCategory(String entryFilter) { + final query = select(todos).join([ + leftOuterJoin(categories, categories.id.equalsExp(todos.category)), + ]); + query.where(todos.content.like(entryFilter)); + query.orderBy([OrderingTerm.asc(categories.id)]); + // ... +} +``` + +As a join can have more than one table, all tables in `where` and `orderBy` have to +be specified directly (unlike the callback on single-table queries that gets called +with the right table by default). + +## Group by + +Sometimes, you need to run queries that _aggregate_ data, meaning that data you're interested in +comes from multiple rows. Common questions include + +- how many todo entries are in each category? +- how many entries did a user complete each month? +- what's the average length of a todo entry? + +What these queries have in common is that data from multiple rows needs to be combined into a single +row. In sql, this can be achieved with "aggregate functions", for which drift has +[builtin support]({{ "expressions.md#aggregate" | pageUrl }}). + +_Additional info_: A good tutorial for group by in sql is available [here](https://www.sqlitetutorial.net/sqlite-group-by/). + +To write a query that answers the first question for us, we can use the `count` function. +We're going to select all categories and join each todo entry for each category. What's special is that we set +`useColumns: false` on the join. We do that because we're not interested in the columns of the todo item. +We only care about how many there are. By default, drift would attempt to read each todo item when it appears +in a join. + +{% include "blocks/snippet" snippets = snippets name = 'countTodosInCategories' %} + +To find the average length of a todo entry, we use `avg`. In this case, we don't even have to use +a `join` since all the data comes from a single table (todos). +That's a problem though - in the join, we used `useColumns: false` because we weren't interested +in the columns of each todo item. Here we don't care about an individual item either, but there's +no join where we could set that flag. +Drift provides a special method for this case - instead of using `select`, we use `selectOnly`. +The "only" means that drift will only report columns we added via "addColumns". In a regular select, +all columns from the table would be selected, which is what you'd usually need. + +{% include "blocks/snippet" snippets = snippets name = 'averageItemLength' %} + +## Using selects as inserts + +In SQL, an `INSERT INTO SELECT` statement can be used to efficiently insert the rows from a `SELECT` +statement into a table. +It is possible to construct these statements in drift with the `insertFromSelect` method. +This example shows how that method is used to construct a statement that creates a new category +for each todo entry that didn't have one assigned before: + +{% include "blocks/snippet" snippets = snippets name = 'createCategoryForUnassignedTodoEntries' %} + +The first parameter for `insertFromSelect` is the select statement statement to use as a source. +Then, the `columns` map maps columns from the table in which rows are inserted to columns from the +select statement. +In the example, the `newDescription` expression as added as a column to the query. +Then, the map entry `categories.description: newDescription` is used so that the `description` column +for new category rows gets set to that expression. + +## Subqueries + +Starting from drift 2.11, you can use `Subquery` to use an existing select statement as part of more +complex join. + +This snippet uses `Subquery` to count how many of the top-10 todo items (by length of their title) are +in each category. +It does this by first creating a select statement for the top-10 items (but not executing it), and then +joining this select statement onto a larger one grouping by category: + +{% include "blocks/snippet" snippets = snippets name = 'subquery' %} + +Any statement can be used as a subquery. But be aware that, unlike [subquery expressions]({{ 'expressions.md#scalar-subqueries' | pageUrl }}), full subqueries can't use tables from the outer select statement. + +## JSON support + +{% assign json_snippet = 'package:drift_docs/snippets/dart_api/json.dart.excerpt.json' | readString | json_decode %} + +sqlite3 has great support for [JSON operators](https://sqlite.org/json1.html) that are also available +in drift (under the additional `'package:drift/extensions/json1.dart'` import). +JSON support is helpful when storing a dynamic structure that is best represented with JSON, or when +you have an existing structure (perhaps because you're migrating from a document-based storage) +that you need to support. + +As an example, consider a contact book application that started with a JSON structure to store +contacts: + +{% include "blocks/snippet" snippets = json_snippet name = 'existing' %} + +To easily store this contact representation in a drift database, one could use a JSON column: + +{% include "blocks/snippet" snippets = json_snippet name = 'contacts' %} + +Note the `name` column as well: It uses `generatedAs` with the `jsonExtract` function to +extract the `name` field from the JSON value on the fly. +The full syntax for JSON path arguments is explained on the [sqlite3 website](https://sqlite.org/json1.html#path_arguments). + +To make the example more complex, let's look at another table storing a log of phone calls: + +{% include "blocks/snippet" snippets = json_snippet name = 'calls' %} + +Let's say we wanted to find the contact for each call, if there is any with a matching phone number. +For this to be expressible in SQL, each `contacts` row would somehow have to be expanded into a row +for each stored phone number. +Luckily, the `json_each` function in sqlite3 can do exactly that, and drift exposes it: + +{% include "blocks/snippet" snippets = json_snippet name = 'calls-with-contacts' %} diff --git a/docs/pages/docs/Dart API/tables.md b/docs/pages/docs/Dart API/tables.md index 915acfce1..f8820aa6d 100644 --- a/docs/pages/docs/Dart API/tables.md +++ b/docs/pages/docs/Dart API/tables.md @@ -128,7 +128,7 @@ Drift supports two approaches of storing `DateTime` values in SQL: The mode can be changed with the `store_date_time_values_as_text` [build option]({{ '../Advanced Features/builder_options.md' | pageUrl }}). Regardless of the option used, drift's builtin support for -[date and time functions]({{ '../Advanced Features/expressions.md#date-and-time' | pageUrl }}) +[date and time functions]({{ 'expressions.md#date-and-time' | pageUrl }}) return an equivalent values. Drift internally inserts the `unixepoch` [modifier](https://sqlite.org/lang_datefunc.html#modifiers) when unix timestamps are used to make the date functions work. When comparing dates stored as text, diff --git a/docs/pages/docs/Dart API/transactions.md b/docs/pages/docs/Dart API/transactions.md index c16503439..d6b6dd448 100644 --- a/docs/pages/docs/Dart API/transactions.md +++ b/docs/pages/docs/Dart API/transactions.md @@ -10,7 +10,7 @@ aliases: - /transactions/ --- -{% assign snippets = "package:drift_docs/snippets/transactions.dart.excerpt.json" | readString | json_decode %} +{% assign snippets = "package:drift_docs/snippets/dart_api/transactions.dart.excerpt.json" | readString | json_decode %} Drift has support for transactions and allows multiple statements to run atomically, so that none of their changes is visible to the main database until the transaction diff --git a/docs/pages/docs/Dart API/writes.md b/docs/pages/docs/Dart API/writes.md index 6379703bb..51127b0ae 100644 --- a/docs/pages/docs/Dart API/writes.md +++ b/docs/pages/docs/Dart API/writes.md @@ -5,3 +5,193 @@ data: weight: 3 template: layouts/docs/single --- + +## Updates and deletes + +You can use the generated classes to update individual fields of any row: +```dart +Future moveImportantTasksIntoCategory(Category target) { + // for updates, we use the "companion" version of a generated class. This wraps the + // fields in a "Value" type which can be set to be absent using "Value.absent()". This + // allows us to separate between "SET category = NULL" (`category: Value(null)`) and not + // updating the category at all: `category: Value.absent()`. + return (update(todos) + ..where((t) => t.title.like('%Important%')) + ).write(TodosCompanion( + category: Value(target.id), + ), + ); +} + +Future updateTodo(Todo entry) { + // using replace will update all fields from the entry that are not marked as a primary key. + // it will also make sure that only the entry with the same primary key will be updated. + // Here, this means that the row that has the same id as entry will be updated to reflect + // the entry's title, content and category. As its where clause is set automatically, it + // cannot be used together with where. + return update(todos).replace(entry); +} + +Future feelingLazy() { + // delete the oldest nine tasks + return (delete(todos)..where((t) => t.id.isSmallerThanValue(10))).go(); +} +``` +__⚠️ Caution:__ If you don't explicitly add a `where` clause on updates or deletes, +the statement will affect all rows in the table! + +{% block "blocks/alert" title="Entries, companions - why do we need all of this?" %} +You might have noticed that we used a `TodosCompanion` for the first update instead of +just passing a `Todo`. Drift generates the `Todo` class (also called _data +class_ for the table) to hold a __full__ row with all its data. For _partial_ data, +prefer to use companions. In the example above, we only set the the `category` column, +so we used a companion. +Why is that necessary? If a field was set to `null`, we wouldn't know whether we need +to set that column back to null in the database or if we should just leave it unchanged. +Fields in the companions have a special `Value.absent()` state which makes this explicit. + +Companions also have a special constructor for inserts - all columns which don't have +a default value and aren't nullable are marked `@required` on that constructor. This makes +companions easier to use for inserts because you know which fields to set. +{% endblock %} + +## Inserts +You can very easily insert any valid object into tables. As some values can be absent +(like default values that we don't have to set explicitly), we again use the +companion version. +```dart +// returns the generated id +Future addTodo(TodosCompanion entry) { + return into(todos).insert(entry); +} +``` +All row classes generated will have a constructor that can be used to create objects: +```dart +addTodo( + TodosCompanion( + title: Value('Important task'), + content: Value('Refactor persistence code'), + ), +); +``` +If a column is nullable or has a default value (this includes auto-increments), the field +can be omitted. All other fields must be set and non-null. The `insert` method will throw +otherwise. + +Multiple insert statements can be run efficiently by using a batch. To do that, you can +use the `insertAll` method inside a `batch`: + +```dart +Future insertMultipleEntries() async{ + await batch((batch) { + // functions in a batch don't have to be awaited - just + // await the whole batch afterwards. + batch.insertAll(todos, [ + TodosCompanion.insert( + title: 'First entry', + content: 'My content', + ), + TodosCompanion.insert( + title: 'Another entry', + content: 'More content', + // columns that aren't required for inserts are still wrapped in a Value: + category: Value(3), + ), + // ... + ]); + }); +} +``` + +Batches are similar to transactions in the sense that all updates are happening atomically, +but they enable further optimizations to avoid preparing the same SQL statement twice. +This makes them suitable for bulk insert or update operations. + +### Upserts + +Upserts are a feature from newer sqlite3 versions that allows an insert to +behave like an update if a conflicting row already exists. + +This allows us to create or override an existing row when its primary key is +part of its data: + +```dart +class Users extends Table { + TextColumn get email => text()(); + TextColumn get name => text()(); + + @override + Set get primaryKey => {email}; +} + +Future createOrUpdateUser(User user) { + return into(users).insertOnConflictUpdate(user); +} +``` + +When calling `createOrUpdateUser()` with an email address that already exists, +that user's name will be updated. Otherwise, a new user will be inserted into +the database. + +Inserts can also be used with more advanced queries. For instance, let's say +we're building a dictionary and want to keep track of how many times we +encountered a word. A table for that might look like + +```dart +class Words extends Table { + TextColumn get word => text()(); + IntColumn get usages => integer().withDefault(const Constant(1))(); + + @override + Set get primaryKey => {word}; +} +``` + +By using a custom upserts, we can insert a new word or increment its `usages` +counter if it already exists: + +```dart +Future trackWord(String word) { + return into(words).insert( + WordsCompanion.insert(word: word), + onConflict: DoUpdate((old) => WordsCompanion.custom(usages: old.usages + Constant(1))), + ); +} +``` + +{% block "blocks/alert" title="Unique constraints and conflict targets" %} +Both `insertOnConflictUpdate` and `onConflict: DoUpdate` use an `DO UPDATE` +upsert in sql. This requires us to provide a so-called "conflict target", a +set of columns to check for uniqueness violations. By default, drift will use +the table's primary key as conflict target. That works in most cases, but if +you have custom `UNIQUE` constraints on some columns, you'll need to use +the `target` parameter on `DoUpdate` in Dart to include those columns. +{% endblock %} + +Note that this requires a fairly recent sqlite3 version (3.24.0) that might not +be available on older Android devices when using `drift_sqflite`. `NativeDatabases` +and `sqlite3_flutter_libs` includes the latest sqlite on Android, so consider using +it if you want to support upserts. + +Also note that the returned rowid may not be accurate when an upsert took place. + +### Returning + +You can use `insertReturning` to insert a row or companion and immediately get the row it inserts. +The returned row contains all the default values and incrementing ids that were +generated. + +__Note:__ This uses the `RETURNING` syntax added in sqlite3 version 3.35, which is not available on most operating systems by default. When using this method, make sure that you have a recent sqlite3 version available. This is the case with `sqlite3_flutter_libs`. + +For instance, consider this snippet using the tables from the [getting started guide]({{ '../setup.md' | pageUrl }}): + +```dart +final row = await into(todos).insertReturning(TodosCompanion.insert( + title: 'A todo entry', + content: 'A description', +)); +``` + +The `row` returned has the proper `id` set. If a table has further default +values, including dynamic values like `CURRENT_TIME`, then those would also be +set in a row returned by `insertReturning`. diff --git a/docs/pages/docs/Getting started/starting_with_sql.md b/docs/pages/docs/Getting started/starting_with_sql.md index d9dd5b27d..9256184bf 100644 --- a/docs/pages/docs/Getting started/starting_with_sql.md +++ b/docs/pages/docs/Getting started/starting_with_sql.md @@ -60,8 +60,8 @@ Let's take a look at what drift generated during the build: - Generated data classes (`Todo` and `Category`) - these hold a single row from the respective table. -- Companion versions of these classes. Those are only relevant when - using the Dart apis of drift, you can [learn more here]({{ "writing_queries.md#inserts" | pageUrl }}). +- Companion versions of these classes. Those are only relevant when + using the Dart apis of drift, you can [learn more here]({{ "../Dart API/writes.md#inserts" | pageUrl }}). - A `CountEntriesResult` class, it holds the result rows when running the `countEntries` query. - A `_$AppDb` superclass. It takes care of creating the tables when @@ -88,8 +88,8 @@ further guides to help you learn more: - The [SQL IDE]({{ "../Using SQL/sql_ide.md" | pageUrl }}) that provides feedback on sql queries right in your editor. - [Transactions]({{ "../Dart API/transactions.md" | pageUrl }}) - [Schema migrations]({{ "../Advanced Features/migrations.md" | pageUrl }}) -- Writing [queries]({{ "writing_queries.md" | pageUrl }}) and - [expressions]({{ "../Advanced Features/expressions.md" | pageUrl }}) in Dart +- Writing [queries]({{ "../Dart API/select.md" | pageUrl }}) and + [expressions]({{ "../Dart API/expressions.md" | pageUrl }}) in Dart - A more [in-depth guide]({{ "../Using SQL/drift_files.md" | pageUrl }}) on `drift` files, which explains `import` statements and the Dart-SQL interop. diff --git a/docs/pages/docs/Getting started/writing_queries.md b/docs/pages/docs/Getting started/writing_queries.md index 0fcc486ad..75d20a73a 100644 --- a/docs/pages/docs/Getting started/writing_queries.md +++ b/docs/pages/docs/Getting started/writing_queries.md @@ -9,303 +9,3 @@ aliases: template: layouts/docs/single --- -{% block "blocks/pageinfo" %} -__Note__: This assumes that you've already completed [the setup]({{ "../setup.md" | pageUrl }}). -{% endblock %} - -For each table you've specified in the `@DriftDatabase` annotation on your database class, -a corresponding getter for a table will be generated. That getter can be used to -run statements: -```dart -// inside the database class, the `todos` getter has been created by drift. -@DriftDatabase(tables: [Todos, Categories]) -class MyDatabase extends _$MyDatabase { - - // the schemaVersion getter and the constructor from the previous page - // have been omitted. - - // loads all todo entries - Future> get allTodoEntries => select(todos).get(); - - // watches all todo entries in a given category. The stream will automatically - // emit new items whenever the underlying data changes. - Stream> watchEntriesInCategory(Category c) { - return (select(todos)..where((t) => t.category.equals(c.id))).watch(); - } -} -``` -## Select statements -You can create `select` statements by starting them with `select(tableName)`, where the -table name -is a field generated for you by drift. Each table used in a database will have a matching field -to run queries against. Any query can be run once with `get()` or be turned into an auto-updating -stream using `watch()`. -### Where -You can apply filters to a query by calling `where()`. The where method takes a function that -should map the given table to an `Expression` of boolean. A common way to create such expression -is by using `equals` on expressions. Integer columns can also be compared with `isBiggerThan` -and `isSmallerThan`. You can compose expressions using `a & b, a | b` and `a.not()`. For more -details on expressions, see [this guide]({{ "../Advanced Features/expressions.md" | pageUrl }}). - -### Limit -You can limit the amount of results returned by calling `limit` on queries. The method accepts -the amount of rows to return and an optional offset. - -```dart -Future> limitTodos(int limit, {int offset}) { - return (select(todos)..limit(limit, offset: offset)).get(); -} -``` - -### Ordering -You can use the `orderBy` method on the select statement. It expects a list of functions that extract the individual -ordering terms from the table. You can use any expression as an ordering term - for more details, see -[this guide]({{ "../Advanced Features/expressions.md" | pageUrl }}). - -```dart -Future> sortEntriesAlphabetically() { - return (select(todos)..orderBy([(t) => OrderingTerm(expression: t.title)])).get(); -} -``` -You can also reverse the order by setting the `mode` property of the `OrderingTerm` to -`OrderingMode.desc`. - -### Single values -If you know a query is never going to return more than one row, wrapping the result in a `List` -can be tedious. Drift lets you work around that with `getSingle` and `watchSingle`: -```dart -Stream entryById(int id) { - return (select(todos)..where((t) => t.id.equals(id))).watchSingle(); -} -``` -If an entry with the provided id exists, it will be sent to the stream. Otherwise, -`null` will be added to stream. If a query used with `watchSingle` ever returns -more than one entry (which is impossible in this case), an error will be added -instead. - -### Mapping -Before calling `watch` or `get` (or the single variants), you can use `map` to transform -the result. -```dart -Stream> contentWithLongTitles() { - final query = select(todos) - ..where((t) => t.title.length.isBiggerOrEqualValue(16)); - - return query - .map((row) => row.content) - .watch(); -} -``` - -### Deferring get vs watch -If you want to make your query consumable as either a `Future` or a `Stream`, -you can refine your return type using one of the `Selectable` abstract base classes; -```dart -// Exposes `get` and `watch` -MultiSelectable pageOfTodos(int page, {int pageSize = 10}) { - return select(todos)..limit(pageSize, offset: page); -} - -// Exposes `getSingle` and `watchSingle` -SingleSelectable entryById(int id) { - return select(todos)..where((t) => t.id.equals(id)); -} - -// Exposes `getSingleOrNull` and `watchSingleOrNull` -SingleOrNullSelectable entryFromExternalLink(int id) { - return select(todos)..where((t) => t.id.equals(id)); -} -``` -These base classes don't have query-building or `map` methods, signaling to the consumer -that they are complete results. - -If you need more complex queries with joins or custom columns, see [this site]({{ "../Advanced Features/joins.md" | pageUrl }}). - -## Updates and deletes -You can use the generated classes to update individual fields of any row: -```dart -Future moveImportantTasksIntoCategory(Category target) { - // for updates, we use the "companion" version of a generated class. This wraps the - // fields in a "Value" type which can be set to be absent using "Value.absent()". This - // allows us to separate between "SET category = NULL" (`category: Value(null)`) and not - // updating the category at all: `category: Value.absent()`. - return (update(todos) - ..where((t) => t.title.like('%Important%')) - ).write(TodosCompanion( - category: Value(target.id), - ), - ); -} - -Future updateTodo(Todo entry) { - // using replace will update all fields from the entry that are not marked as a primary key. - // it will also make sure that only the entry with the same primary key will be updated. - // Here, this means that the row that has the same id as entry will be updated to reflect - // the entry's title, content and category. As its where clause is set automatically, it - // cannot be used together with where. - return update(todos).replace(entry); -} - -Future feelingLazy() { - // delete the oldest nine tasks - return (delete(todos)..where((t) => t.id.isSmallerThanValue(10))).go(); -} -``` -__⚠️ Caution:__ If you don't explicitly add a `where` clause on updates or deletes, -the statement will affect all rows in the table! - -{% block "blocks/alert" title="Entries, companions - why do we need all of this?" %} -You might have noticed that we used a `TodosCompanion` for the first update instead of -just passing a `Todo`. Drift generates the `Todo` class (also called _data -class_ for the table) to hold a __full__ row with all its data. For _partial_ data, -prefer to use companions. In the example above, we only set the the `category` column, -so we used a companion. -Why is that necessary? If a field was set to `null`, we wouldn't know whether we need -to set that column back to null in the database or if we should just leave it unchanged. -Fields in the companions have a special `Value.absent()` state which makes this explicit. - -Companions also have a special constructor for inserts - all columns which don't have -a default value and aren't nullable are marked `@required` on that constructor. This makes -companions easier to use for inserts because you know which fields to set. -{% endblock %} - -## Inserts -You can very easily insert any valid object into tables. As some values can be absent -(like default values that we don't have to set explicitly), we again use the -companion version. -```dart -// returns the generated id -Future addTodo(TodosCompanion entry) { - return into(todos).insert(entry); -} -``` -All row classes generated will have a constructor that can be used to create objects: -```dart -addTodo( - TodosCompanion( - title: Value('Important task'), - content: Value('Refactor persistence code'), - ), -); -``` -If a column is nullable or has a default value (this includes auto-increments), the field -can be omitted. All other fields must be set and non-null. The `insert` method will throw -otherwise. - -Multiple insert statements can be run efficiently by using a batch. To do that, you can -use the `insertAll` method inside a `batch`: - -```dart -Future insertMultipleEntries() async{ - await batch((batch) { - // functions in a batch don't have to be awaited - just - // await the whole batch afterwards. - batch.insertAll(todos, [ - TodosCompanion.insert( - title: 'First entry', - content: 'My content', - ), - TodosCompanion.insert( - title: 'Another entry', - content: 'More content', - // columns that aren't required for inserts are still wrapped in a Value: - category: Value(3), - ), - // ... - ]); - }); -} -``` - -Batches are similar to transactions in the sense that all updates are happening atomically, -but they enable further optimizations to avoid preparing the same SQL statement twice. -This makes them suitable for bulk insert or update operations. - -### Upserts - -Upserts are a feature from newer sqlite3 versions that allows an insert to -behave like an update if a conflicting row already exists. - -This allows us to create or override an existing row when its primary key is -part of its data: - -```dart -class Users extends Table { - TextColumn get email => text()(); - TextColumn get name => text()(); - - @override - Set get primaryKey => {email}; -} - -Future createOrUpdateUser(User user) { - return into(users).insertOnConflictUpdate(user); -} -``` - -When calling `createOrUpdateUser()` with an email address that already exists, -that user's name will be updated. Otherwise, a new user will be inserted into -the database. - -Inserts can also be used with more advanced queries. For instance, let's say -we're building a dictionary and want to keep track of how many times we -encountered a word. A table for that might look like - -```dart -class Words extends Table { - TextColumn get word => text()(); - IntColumn get usages => integer().withDefault(const Constant(1))(); - - @override - Set get primaryKey => {word}; -} -``` - -By using a custom upserts, we can insert a new word or increment its `usages` -counter if it already exists: - -```dart -Future trackWord(String word) { - return into(words).insert( - WordsCompanion.insert(word: word), - onConflict: DoUpdate((old) => WordsCompanion.custom(usages: old.usages + Constant(1))), - ); -} -``` - -{% block "blocks/alert" title="Unique constraints and conflict targets" %} -Both `insertOnConflictUpdate` and `onConflict: DoUpdate` use an `DO UPDATE` -upsert in sql. This requires us to provide a so-called "conflict target", a -set of columns to check for uniqueness violations. By default, drift will use -the table's primary key as conflict target. That works in most cases, but if -you have custom `UNIQUE` constraints on some columns, you'll need to use -the `target` parameter on `DoUpdate` in Dart to include those columns. -{% endblock %} - -Note that this requires a fairly recent sqlite3 version (3.24.0) that might not -be available on older Android devices when using `drift_sqflite`. `NativeDatabases` -and `sqlite3_flutter_libs` includes the latest sqlite on Android, so consider using -it if you want to support upserts. - -Also note that the returned rowid may not be accurate when an upsert took place. - -### Returning - -You can use `insertReturning` to insert a row or companion and immediately get the row it inserts. -The returned row contains all the default values and incrementing ids that were -generated. - -__Note:__ This uses the `RETURNING` syntax added in sqlite3 version 3.35, which is not available on most operating systems by default. When using this method, make sure that you have a recent sqlite3 version available. This is the case with `sqlite3_flutter_libs`. - -For instance, consider this snippet using the tables from the [getting started guide]({{ '../setup.md' | pageUrl }}): - -```dart -final row = await into(todos).insertReturning(TodosCompanion.insert( - title: 'A todo entry', - content: 'A description', -)); -``` - -The `row` returned has the proper `id` set. If a table has further default -values, including dynamic values like `CURRENT_TIME`, then those would also be -set in a row returned by `insertReturning`. diff --git a/docs/pages/docs/Using SQL/drift_files.md b/docs/pages/docs/Using SQL/drift_files.md index 744cfd17a..23593f8f6 100644 --- a/docs/pages/docs/Using SQL/drift_files.md +++ b/docs/pages/docs/Using SQL/drift_files.md @@ -303,7 +303,7 @@ can be used to construct dynamic filters at runtime: This lets you write a single SQL query and dynamically apply a predicate at runtime! This feature works for -- [expressions]({{ "../Advanced Features/expressions.md" | pageUrl }}), as you've seen in the example above +- [expressions]({{ "../Dart API/expressions.md" | pageUrl }}), as you've seen in the example above - single ordering terms: `SELECT * FROM todos ORDER BY $term, id ASC` will generate a method taking an `OrderingTerm`. - whole order-by clauses: `SELECT * FROM todos ORDER BY $order` From ba20f803032ec2d67ce5def39142d1779630e75f Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 17 Sep 2023 18:10:49 +0200 Subject: [PATCH 06/12] Move migration docs into their own section --- docs/README.md | 4 +- docs/build.yaml | 2 + docs/lib/snippets/migrations/migrations.dart | 130 +---- .../lib/snippets/migrations/step_by_step.dart | 129 +++++ .../tests/generated_migrations/schema.dart | 24 + .../tests/generated_migrations/schema_v1.dart | 232 ++++++++ .../tests/generated_migrations/schema_v2.dart | 262 +++++++++ .../tests/generated_migrations/schema_v3.dart | 294 ++++++++++ .../migrations/tests/schema_test.dart | 32 ++ .../tests/verify_data_integrity_test.dart | 49 ++ .../docs/Advanced Features/migrations.md | 528 ------------------ docs/pages/docs/CLI.md | 2 +- docs/pages/docs/Dart API/tables.md | 4 +- docs/pages/docs/Examples/index.md | 2 +- .../docs/Getting started/starting_with_sql.md | 2 +- docs/pages/docs/Migrations/api.md | 140 +++++ docs/pages/docs/Migrations/exports.md | 103 ++++ docs/pages/docs/Migrations/index.md | 127 ++++- docs/pages/docs/Migrations/step_by_step.md | 87 +++ docs/pages/docs/Migrations/tests.md | 87 +++ docs/pages/docs/faq.md | 4 +- docs/pages/docs/setup.md | 9 +- docs/pages/docs/testing.md | 2 +- docs/pages/docs/upgrading.md | 2 +- docs/pages/index.html | 2 +- docs/pubspec.yaml | 2 +- 26 files changed, 1585 insertions(+), 676 deletions(-) create mode 100644 docs/lib/snippets/migrations/step_by_step.dart create mode 100644 docs/lib/snippets/migrations/tests/generated_migrations/schema.dart create mode 100644 docs/lib/snippets/migrations/tests/generated_migrations/schema_v1.dart create mode 100644 docs/lib/snippets/migrations/tests/generated_migrations/schema_v2.dart create mode 100644 docs/lib/snippets/migrations/tests/generated_migrations/schema_v3.dart create mode 100644 docs/lib/snippets/migrations/tests/schema_test.dart create mode 100644 docs/lib/snippets/migrations/tests/verify_data_integrity_test.dart delete mode 100644 docs/pages/docs/Advanced Features/migrations.md create mode 100644 docs/pages/docs/Migrations/api.md create mode 100644 docs/pages/docs/Migrations/exports.md create mode 100644 docs/pages/docs/Migrations/step_by_step.md create mode 100644 docs/pages/docs/Migrations/tests.md 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 ab3f84e96..cef5ad347 100644 --- a/docs/build.yaml +++ b/docs/build.yaml @@ -122,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/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..fa63835c7 --- /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 \ No newline at end of file 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..cf2f2ff39 100644 --- a/docs/pages/docs/CLI.md +++ b/docs/pages/docs/CLI.md @@ -69,4 +69,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/Dart API/tables.md b/docs/pages/docs/Dart API/tables.md index f8820aa6d..291cddc4a 100644 --- a/docs/pages/docs/Dart API/tables.md +++ b/docs/pages/docs/Dart API/tables.md @@ -232,7 +232,7 @@ 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 }}). ## Default values @@ -286,7 +286,7 @@ In Dart, the `check` method on the column builder adds a check constraint to the ``` 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. +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 diff --git a/docs/pages/docs/Examples/index.md b/docs/pages/docs/Examples/index.md index a06002580..7e64e4a99 100644 --- a/docs/pages/docs/Examples/index.md +++ b/docs/pages/docs/Examples/index.md @@ -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/Getting started/starting_with_sql.md b/docs/pages/docs/Getting started/starting_with_sql.md index 9256184bf..05b4eb3a5 100644 --- a/docs/pages/docs/Getting started/starting_with_sql.md +++ b/docs/pages/docs/Getting started/starting_with_sql.md @@ -87,7 +87,7 @@ further guides to help you learn more: - The [SQL IDE]({{ "../Using SQL/sql_ide.md" | pageUrl }}) that provides feedback on sql queries right in your editor. - [Transactions]({{ "../Dart API/transactions.md" | pageUrl }}) -- [Schema migrations]({{ "../Advanced Features/migrations.md" | pageUrl }}) +- [Schema migrations]({{ "../Migrations/index.md" | pageUrl }}) - Writing [queries]({{ "../Dart API/select.md" | pageUrl }}) and [expressions]({{ "../Dart API/expressions.md" | pageUrl }}) in Dart - A more [in-depth guide]({{ "../Using SQL/drift_files.md" | pageUrl }}) 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 index 7e5f54f81..df435b4db 100644 --- a/docs/pages/docs/Migrations/index.md +++ b/docs/pages/docs/Migrations/index.md @@ -1,7 +1,130 @@ --- data: title: Migrations - description: Simple guide to get a drift project up and running. - hide_section_index: true + description: Tooling and APIs to safely change the schema of your database. 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/faq.md b/docs/pages/docs/faq.md index b643e4544..d65c6f3d1 100644 --- a/docs/pages/docs/faq.md +++ b/docs/pages/docs/faq.md @@ -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 diff --git a/docs/pages/docs/setup.md b/docs/pages/docs/setup.md index 4846181f7..0079cd665 100644 --- a/docs/pages/docs/setup.md +++ b/docs/pages/docs/setup.md @@ -113,14 +113,11 @@ started with drift: - 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 }}). - General [notes on how to integrate drift with your app's architecture]({{ 'Dart API/architecture.md' | pageUrl }}). -- 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. +- 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. - -[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/testing.md b/docs/pages/docs/testing.md index 13e426e7f..cd1141096 100644 --- a/docs/pages/docs/testing.md +++ b/docs/pages/docs/testing.md @@ -116,4 +116,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/upgrading.md b/docs/pages/docs/upgrading.md index 738d681dc..2a6209b6d 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. diff --git a/docs/pages/index.html b/docs/pages/index.html index 6ae4d48d4..15171b0b7 100644 --- a/docs/pages/index.html +++ b/docs/pages/index.html @@ -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 %} 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 From ae2b1b4ddf70292bafff0be9a69c66bd979d5561 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 17 Sep 2023 18:37:09 +0200 Subject: [PATCH 07/12] Move platforms into own section --- .../docs/Advanced Features/builder_options.md | 2 +- docs/pages/docs/Dart API/expressions.md | 2 +- .../docs/Getting started/writing_queries.md | 11 --------- docs/pages/docs/Other engines/index.md | 7 ------ .../encryption.md | 2 +- .../docs/{platforms.md => Platforms/index.md} | 23 ++++++++++--------- .../docs/{Other engines => Platforms}/vm.md | 0 .../docs/{Other engines => Platforms}/web.md | 0 docs/pages/docs/setup.md | 2 +- docs/pages/index.html | 2 +- docs/templates/partials/dependencies.html | 1 - 11 files changed, 17 insertions(+), 35 deletions(-) delete mode 100644 docs/pages/docs/Getting started/writing_queries.md delete mode 100644 docs/pages/docs/Other engines/index.md rename docs/pages/docs/{Other engines => Platforms}/encryption.md (98%) rename docs/pages/docs/{platforms.md => Platforms/index.md} (90%) rename docs/pages/docs/{Other engines => Platforms}/vm.md (100%) rename docs/pages/docs/{Other engines => Platforms}/web.md (100%) diff --git a/docs/pages/docs/Advanced Features/builder_options.md b/docs/pages/docs/Advanced Features/builder_options.md index 8b96eba51..0d665c39a 100644 --- a/docs/pages/docs/Advanced Features/builder_options.md +++ b/docs/pages/docs/Advanced Features/builder_options.md @@ -174,7 +174,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) diff --git a/docs/pages/docs/Dart API/expressions.md b/docs/pages/docs/Dart API/expressions.md index 52bf18404..328aca8dc 100644 --- a/docs/pages/docs/Dart API/expressions.md +++ b/docs/pages/docs/Dart API/expressions.md @@ -202,7 +202,7 @@ with the `separator` argument on `groupConcat`. 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 diff --git a/docs/pages/docs/Getting started/writing_queries.md b/docs/pages/docs/Getting started/writing_queries.md deleted file mode 100644 index 75d20a73a..000000000 --- a/docs/pages/docs/Getting started/writing_queries.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -data: - title: "Writing queries" - linkTitle: "Writing queries" - description: Learn how to write database queries in pure Dart with drift - weight: 100 -aliases: - - /queries/ -template: layouts/docs/single ---- - 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 98% rename from docs/pages/docs/Other engines/encryption.md rename to docs/pages/docs/Platforms/encryption.md index 0cacef60c..fdad3f69c 100644 --- a/docs/pages/docs/Other engines/encryption.md +++ b/docs/pages/docs/Platforms/encryption.md @@ -78,7 +78,7 @@ On iOS and macOS, no additional setup is necessary - simply depend on `sqlcipher 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 90% rename from docs/pages/docs/platforms.md rename to docs/pages/docs/Platforms/index.md index 1fc27d45c..5a4beb0d2 100644 --- a/docs/pages/docs/platforms.md +++ b/docs/pages/docs/Platforms/index.md @@ -2,7 +2,7 @@ data: title: "Supported platforms" description: All platforms supported by drift, and how to use them -template: layouts/docs/single +template: layouts/docs/list --- Being built on top of the sqlite3 database, drift can run on almost every Dart platform. @@ -14,7 +14,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 +24,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]({{ '../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]({{ '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 +48,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]({{ "setup.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 +74,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 @@ -142,7 +143,7 @@ lives next to your application: {% 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]({{ '../Advanced Features/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 100% rename from docs/pages/docs/Other engines/vm.md rename to docs/pages/docs/Platforms/vm.md diff --git a/docs/pages/docs/Other engines/web.md b/docs/pages/docs/Platforms/web.md similarity index 100% rename from docs/pages/docs/Other engines/web.md rename to docs/pages/docs/Platforms/web.md diff --git a/docs/pages/docs/setup.md b/docs/pages/docs/setup.md index 0079cd665..cd1b862c5 100644 --- a/docs/pages/docs/setup.md +++ b/docs/pages/docs/setup.md @@ -52,7 +52,7 @@ If you're wondering why so many packages are necessary, here's a quick overview - `drift`: This is the core package defining the APIs you use to access drift databases. - `sqlite3_flutter_libs`: Ships the latest `sqlite3` version with your Android or iOS app. This is not required when you're _not_ using Flutter, but then you need to take care of including `sqlite3` yourself. - For an overview on other platforms, see [platforms]({{ 'platforms.md' | pageUrl }}). + 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 diff --git a/docs/pages/index.html b/docs/pages/index.html index 15171b0b7..53c2aa1d9 100644 --- a/docs/pages/index.html +++ b/docs/pages/index.html @@ -73,7 +73,7 @@ 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 %} 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 From e0593136492bec16e84d21ea27cc6f18d7f9ba5a Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 17 Sep 2023 19:01:58 +0200 Subject: [PATCH 08/12] Rename "Using SQL" to "SQL API" --- .../snippets/{ => platforms}/encryption.dart | 0 .../{engines => platforms}/new_connect.dart | 0 .../{engines => platforms}/stable_worker.dart | 0 .../snippets/{engines => platforms}/web.dart | 0 .../{engines => platforms}/workers.dart | 0 .../docs/Advanced Features/builder_options.md | 2 +- .../Advanced Features/custom_row_classes.md | 4 +- docs/pages/docs/Dart API/expressions.md | 2 +- .../docs/Getting started/starting_with_sql.md | 107 ------------------ docs/pages/docs/Platforms/encryption.md | 2 +- docs/pages/docs/Platforms/web.md | 8 +- .../{Using SQL => SQL API}/custom_queries.md | 0 .../{Using SQL => SQL API}/drift_files.md | 0 .../docs/{Using SQL => SQL API}/extensions.md | 15 ++- docs/pages/docs/SQL API/index.md | 46 +++++++- .../docs/{Using SQL => SQL API}/sql_ide.md | 0 docs/pages/docs/Using SQL/index.md | 12 -- docs/pages/docs/faq.md | 2 +- docs/pages/index.html | 4 +- docs/pages/v2.html | 10 +- 20 files changed, 72 insertions(+), 142 deletions(-) rename docs/lib/snippets/{ => platforms}/encryption.dart (100%) rename docs/lib/snippets/{engines => platforms}/new_connect.dart (100%) rename docs/lib/snippets/{engines => platforms}/stable_worker.dart (100%) rename docs/lib/snippets/{engines => platforms}/web.dart (100%) rename docs/lib/snippets/{engines => platforms}/workers.dart (100%) delete mode 100644 docs/pages/docs/Getting started/starting_with_sql.md rename docs/pages/docs/{Using SQL => SQL API}/custom_queries.md (100%) rename docs/pages/docs/{Using SQL => SQL API}/drift_files.md (100%) rename docs/pages/docs/{Using SQL => SQL API}/extensions.md (79%) rename docs/pages/docs/{Using SQL => SQL API}/sql_ide.md (100%) delete mode 100644 docs/pages/docs/Using SQL/index.md diff --git a/docs/lib/snippets/encryption.dart b/docs/lib/snippets/platforms/encryption.dart similarity index 100% rename from docs/lib/snippets/encryption.dart rename to docs/lib/snippets/platforms/encryption.dart 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/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/pages/docs/Advanced Features/builder_options.md b/docs/pages/docs/Advanced Features/builder_options.md index 0d665c39a..d8466e98f 100644 --- a/docs/pages/docs/Advanced Features/builder_options.md +++ b/docs/pages/docs/Advanced Features/builder_options.md @@ -67,7 +67,7 @@ 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. diff --git a/docs/pages/docs/Advanced Features/custom_row_classes.md b/docs/pages/docs/Advanced Features/custom_row_classes.md index 51e989adf..93390a8a5 100644 --- a/docs/pages/docs/Advanced Features/custom_row_classes.md +++ b/docs/pages/docs/Advanced Features/custom_row_classes.md @@ -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/Dart API/expressions.md b/docs/pages/docs/Dart API/expressions.md index 328aca8dc..a1cb2e34a 100644 --- a/docs/pages/docs/Dart API/expressions.md +++ b/docs/pages/docs/Dart API/expressions.md @@ -259,4 +259,4 @@ 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. +[compiled sql]({{ "../SQL API/custom_queries.md" | pageUrl }}) which is typesafe to use. 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 05b4eb3a5..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]({{ "../setup.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]({{ "../Dart API/writes.md#inserts" | pageUrl }}). -- A `CountEntriesResult` class, it holds the result rows when running the - `countEntries` query. -- A `_$AppDb` superclass. It takes care of creating the tables when - 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]({{ "../Dart API/transactions.md" | pageUrl }}) -- [Schema migrations]({{ "../Migrations/index.md" | pageUrl }}) -- Writing [queries]({{ "../Dart API/select.md" | pageUrl }}) and - [expressions]({{ "../Dart API/expressions.md" | pageUrl }}) in Dart -- A more [in-depth guide]({{ "../Using SQL/drift_files.md" | pageUrl }}) - on `drift` files, which explains `import` statements and the Dart-SQL interop. - -{% 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/Platforms/encryption.md b/docs/pages/docs/Platforms/encryption.md index fdad3f69c..79b466e15 100644 --- a/docs/pages/docs/Platforms/encryption.md +++ b/docs/pages/docs/Platforms/encryption.md @@ -5,7 +5,7 @@ data: 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 diff --git a/docs/pages/docs/Platforms/web.md b/docs/pages/docs/Platforms/web.md index cebe8da44..93732dcb3 100644 --- a/docs/pages/docs/Platforms/web.md +++ b/docs/pages/docs/Platforms/web.md @@ -12,7 +12,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. @@ -253,7 +253,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 +354,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 +369,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" %} diff --git a/docs/pages/docs/Using SQL/custom_queries.md b/docs/pages/docs/SQL API/custom_queries.md similarity index 100% rename from docs/pages/docs/Using SQL/custom_queries.md rename to docs/pages/docs/SQL API/custom_queries.md diff --git a/docs/pages/docs/Using SQL/drift_files.md b/docs/pages/docs/SQL API/drift_files.md similarity index 100% rename from docs/pages/docs/Using SQL/drift_files.md rename to docs/pages/docs/SQL API/drift_files.md diff --git a/docs/pages/docs/Using SQL/extensions.md b/docs/pages/docs/SQL API/extensions.md similarity index 79% rename from docs/pages/docs/Using SQL/extensions.md rename to docs/pages/docs/SQL API/extensions.md index 934118e7c..1e08cc384 100644 --- a/docs/pages/docs/Using SQL/extensions.md +++ b/docs/pages/docs/SQL API/extensions.md @@ -2,13 +2,18 @@ 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]({{ "../Advanced Features/builder_options.md" | pageUrl }}). ## json1 @@ -42,7 +47,7 @@ class Database extends _$Database { return phoneNumber.equals(number); }) ).get(); - } + } } ``` diff --git a/docs/pages/docs/SQL API/index.md b/docs/pages/docs/SQL API/index.md index 5d251b0ae..ac05ed186 100644 --- a/docs/pages/docs/SQL API/index.md +++ b/docs/pages/docs/SQL API/index.md @@ -3,5 +3,49 @@ data: title: SQL API description: Define your database and queries in SQL instead. weight: 3 -template: layouts/docs/single +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/faq.md b/docs/pages/docs/faq.md index d65c6f3d1..e35251992 100644 --- a/docs/pages/docs/faq.md +++ b/docs/pages/docs/faq.md @@ -142,7 +142,7 @@ 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. diff --git a/docs/pages/index.html b/docs/pages/index.html index 53c2aa1d9..b093a712f 100644 --- a/docs/pages/index.html +++ b/docs/pages/index.html @@ -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 %} @@ -80,7 +80,7 @@ {% 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 89928f99f..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" %} @@ -111,6 +111,6 @@ - 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 From addb457d4a7d03ff553cb462f5f65cb06b4c1c76 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 18 Sep 2023 23:04:47 +0200 Subject: [PATCH 09/12] Move remaining pages to their target destination --- docs/lib/snippets/dart_api/tables.dart | 14 ++ .../snippets/{ => platforms}/platforms.dart | 0 docs/pages/docs/Advanced Features/index.md | 7 - docs/pages/docs/CLI.md | 1 + docs/pages/docs/Dart API/index.md | 2 +- .../schema_inspection.md | 0 docs/pages/docs/Dart API/tables.md | 37 +++- docs/pages/docs/Dart API/views.md | 2 +- docs/pages/docs/Examples/index.md | 2 +- .../Generation options/in_other_builders.md | 63 ++++++ .../index.md} | 199 +----------------- docs/pages/docs/Generation options/modular.md | 125 +++++++++++ docs/pages/docs/Migrations/index.md | 1 + docs/pages/docs/Platforms/index.md | 7 +- docs/pages/docs/Platforms/vm.md | 6 +- docs/pages/docs/Platforms/web.md | 2 +- docs/pages/docs/SQL API/drift_files.md | 14 +- docs/pages/docs/SQL API/extensions.md | 8 +- docs/pages/docs/SQL API/index.md | 4 +- docs/pages/docs/community_tools.md | 1 + .../custom_row_classes.md | 6 +- docs/pages/docs/faq.md | 4 +- docs/pages/docs/index.md | 9 +- .../docs/{Advanced Features => }/isolates.md | 5 +- docs/pages/docs/testing.md | 1 + .../type_converters.md | 4 +- docs/pages/docs/upgrading.md | 2 +- 27 files changed, 291 insertions(+), 235 deletions(-) rename docs/lib/snippets/{ => platforms}/platforms.dart (100%) delete mode 100644 docs/pages/docs/Advanced Features/index.md rename docs/pages/docs/{Advanced Features => Dart API}/schema_inspection.md (100%) create mode 100644 docs/pages/docs/Generation options/in_other_builders.md rename docs/pages/docs/{Advanced Features/builder_options.md => Generation options/index.md} (59%) create mode 100644 docs/pages/docs/Generation options/modular.md rename docs/pages/docs/{Advanced Features => }/custom_row_classes.md (97%) rename docs/pages/docs/{Advanced Features => }/isolates.md (98%) rename docs/pages/docs/{Advanced Features => }/type_converters.md (97%) diff --git a/docs/lib/snippets/dart_api/tables.dart b/docs/lib/snippets/dart_api/tables.dart index 325560958..ba694d53a 100644 --- a/docs/lib/snippets/dart_api/tables.dart +++ b/docs/lib/snippets/dart_api/tables.dart @@ -82,3 +82,17 @@ class Users extends Table { 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/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/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/CLI.md b/docs/pages/docs/CLI.md index cf2f2ff39..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 --- diff --git a/docs/pages/docs/Dart API/index.md b/docs/pages/docs/Dart API/index.md index fd86422bf..6f455410a 100644 --- a/docs/pages/docs/Dart API/index.md +++ b/docs/pages/docs/Dart API/index.md @@ -1,7 +1,7 @@ --- data: title: Dart API - description: Drift APIs for your app + description: Drift's Dart library for declaring tables and writing queries. weight: 2 template: layouts/docs/list --- diff --git a/docs/pages/docs/Advanced Features/schema_inspection.md b/docs/pages/docs/Dart API/schema_inspection.md similarity index 100% rename from docs/pages/docs/Advanced Features/schema_inspection.md rename to docs/pages/docs/Dart API/schema_inspection.md diff --git a/docs/pages/docs/Dart API/tables.md b/docs/pages/docs/Dart API/tables.md index 291cddc4a..05c77db5b 100644 --- a/docs/pages/docs/Dart API/tables.md +++ b/docs/pages/docs/Dart API/tables.md @@ -28,10 +28,10 @@ In the example above, `IntColumn get category => integer().nullable()();` define holding nullable integer values named `category`. This section describes all the options available when declaring columns. -## 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 | |--------------|---------------|-----------------------------------------------------| @@ -42,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. @@ -51,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 @@ -125,7 +136,7 @@ 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]({{ 'expressions.md#date-and-time' | pageUrl }}) @@ -217,7 +228,7 @@ If you do want to make a column nullable, just use `nullable()`: {% 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: @@ -234,7 +245,7 @@ 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]({{ '../Migrations/index.md#post-migration-callbacks' | pageUrl }}). -## Default values +### Default values You can set a default value for a column. When not explicitly set, the default value will be used when inserting a new row. To set a constant default value, use `withDefault`: @@ -340,6 +351,18 @@ That doesn't work in all cases though. With the `EnabledCategories` class from a 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 diff --git a/docs/pages/docs/Dart API/views.md b/docs/pages/docs/Dart API/views.md index d8332c852..238f83b4e 100644 --- a/docs/pages/docs/Dart API/views.md +++ b/docs/pages/docs/Dart API/views.md @@ -20,7 +20,7 @@ Inside a Dart view, use 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 }}) + 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. diff --git a/docs/pages/docs/Examples/index.md b/docs/pages/docs/Examples/index.md index 7e64e4a99..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 --- 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 59% rename from docs/pages/docs/Advanced Features/builder_options.md rename to docs/pages/docs/Generation options/index.md index d8466e98f..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 @@ -75,7 +76,7 @@ At the moment, drift supports these options: * `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 @@ -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]({{ '../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/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/Migrations/index.md b/docs/pages/docs/Migrations/index.md index df435b4db..445092686 100644 --- a/docs/pages/docs/Migrations/index.md +++ b/docs/pages/docs/Migrations/index.md @@ -2,6 +2,7 @@ data: title: Migrations description: Tooling and APIs to safely change the schema of your database. + weight: 4 template: layouts/docs/list aliases: - /migrations diff --git a/docs/pages/docs/Platforms/index.md b/docs/pages/docs/Platforms/index.md index 5a4beb0d2..8da5e63c0 100644 --- a/docs/pages/docs/Platforms/index.md +++ b/docs/pages/docs/Platforms/index.md @@ -2,6 +2,7 @@ data: title: "Supported platforms" description: All platforms supported by drift, and how to use them + weight: 8 template: layouts/docs/list --- @@ -24,7 +25,7 @@ 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`. | +| `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`. | @@ -139,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/Platforms/vm.md b/docs/pages/docs/Platforms/vm.md index 9ec3457b7..7fcccf33c 100644 --- a/docs/pages/docs/Platforms/vm.md +++ b/docs/pages/docs/Platforms/vm.md @@ -112,7 +112,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 +141,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/Platforms/web.md b/docs/pages/docs/Platforms/web.md index 93732dcb3..37e0f43d8 100644 --- a/docs/pages/docs/Platforms/web.md +++ b/docs/pages/docs/Platforms/web.md @@ -382,7 +382,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/SQL API/drift_files.md b/docs/pages/docs/SQL API/drift_files.md index 23593f8f6..63d04409e 100644 --- a/docs/pages/docs/SQL API/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,7 +138,7 @@ 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(...)`. @@ -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/SQL API/extensions.md b/docs/pages/docs/SQL API/extensions.md index 1e08cc384..8af50e742 100644 --- a/docs/pages/docs/SQL API/extensions.md +++ b/docs/pages/docs/SQL API/extensions.md @@ -13,12 +13,12 @@ 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]({{ "../Advanced Features/builder_options.md" | pageUrl }}). +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 @@ -56,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 index ac05ed186..7611c1b18 100644 --- a/docs/pages/docs/SQL API/index.md +++ b/docs/pages/docs/SQL API/index.md @@ -1,7 +1,7 @@ --- data: - title: SQL API - description: Define your database and queries in SQL instead. + title: Verified SQL + description: Define your database and queries in SQL without giving up on type-safety. weight: 3 template: layouts/docs/list --- 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 93390a8a5..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]({{ '../SQL API/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]({{ '../SQL API/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 e35251992..4050a7513 100644 --- a/docs/pages/docs/faq.md +++ b/docs/pages/docs/faq.md @@ -1,7 +1,7 @@ --- data: title: "Frequently asked questions" - + weight: 25 path: faq/ template: layouts/docs/single --- @@ -148,7 +148,7 @@ 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 3d9fc6470..343c15107 100644 --- a/docs/pages/docs/index.md +++ b/docs/pages/docs/index.md @@ -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 f313d5566..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]({{ '../setup.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/testing.md b/docs/pages/docs/testing.md index cd1141096..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 --- 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 2a6209b6d..c0870cc65 100644 --- a/docs/pages/docs/upgrading.md +++ b/docs/pages/docs/upgrading.md @@ -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 | | ------------------------------------------- | ------------------------------ | From dedba63eb4c0f4a1c28e1e39c90b5e1e94490060 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 19 Sep 2023 22:32:10 +0200 Subject: [PATCH 10/12] Recommend `isolateSetup` in docs --- docs/lib/snippets/platforms/encryption.dart | 6 ++++-- docs/pages/docs/Platforms/encryption.md | 4 +++- docs/pages/docs/Platforms/vm.md | 1 + docs/pages/docs/Platforms/web.md | 1 + 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/lib/snippets/platforms/encryption.dart b/docs/lib/snippets/platforms/encryption.dart index 9c3521783..bc117470f 100644 --- a/docs/lib/snippets/platforms/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/pages/docs/Platforms/encryption.md b/docs/pages/docs/Platforms/encryption.md index 79b466e15..7e66ca07e 100644 --- a/docs/pages/docs/Platforms/encryption.md +++ b/docs/pages/docs/Platforms/encryption.md @@ -2,6 +2,7 @@ data: title: Encryption description: Use drift on encrypted databases + weight: 10 template: layouts/docs/single --- @@ -72,7 +73,8 @@ 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`. diff --git a/docs/pages/docs/Platforms/vm.md b/docs/pages/docs/Platforms/vm.md index 7fcccf33c..7064794d4 100644 --- a/docs/pages/docs/Platforms/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 --- diff --git a/docs/pages/docs/Platforms/web.md b/docs/pages/docs/Platforms/web.md index 37e0f43d8..1e315348a 100644 --- a/docs/pages/docs/Platforms/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/ --- From b0f38ebd6b19e893586e14325f99cd3dce651d7f Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 23 Sep 2023 17:55:15 +0200 Subject: [PATCH 11/12] Delete architecture page --- docs/pages/docs/Dart API/architecture.md | 6 ------ docs/pages/docs/Dart API/index.md | 8 ++++++++ docs/pages/docs/setup.md | 1 - 3 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 docs/pages/docs/Dart API/architecture.md diff --git a/docs/pages/docs/Dart API/architecture.md b/docs/pages/docs/Dart API/architecture.md deleted file mode 100644 index 6f0c61625..000000000 --- a/docs/pages/docs/Dart API/architecture.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -data: - title: "Drift and app architecture" - description: Notes on how drift can be integrated into your app's architecture -template: layouts/docs/single ---- diff --git a/docs/pages/docs/Dart API/index.md b/docs/pages/docs/Dart API/index.md index 6f455410a..dcf2fc6db 100644 --- a/docs/pages/docs/Dart API/index.md +++ b/docs/pages/docs/Dart API/index.md @@ -5,3 +5,11 @@ data: 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/setup.md b/docs/pages/docs/setup.md index cd1b862c5..d02021ce4 100644 --- a/docs/pages/docs/setup.md +++ b/docs/pages/docs/setup.md @@ -112,7 +112,6 @@ started with drift: 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 }}). -- General [notes on how to integrate drift with your app's architecture]({{ 'Dart API/architecture.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. From e8c868bd0ac1f18d2d23e90b5ac242329aceb5a1 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 23 Sep 2023 22:41:22 +0200 Subject: [PATCH 12/12] Reformat docs --- .../snippets/migrations/tests/verify_data_integrity_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/lib/snippets/migrations/tests/verify_data_integrity_test.dart b/docs/lib/snippets/migrations/tests/verify_data_integrity_test.dart index fa63835c7..14cf21694 100644 --- a/docs/lib/snippets/migrations/tests/verify_data_integrity_test.dart +++ b/docs/lib/snippets/migrations/tests/verify_data_integrity_test.dart @@ -46,4 +46,4 @@ void main() { await migratedDb.close(); }); } -// #enddocregion main \ No newline at end of file +// #enddocregion main