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