From 0e692524067155a90a90e3b64992e01685c9ff61 Mon Sep 17 00:00:00 2001
From: Simon Binder
Date: Sun, 22 Oct 2023 00:09:19 +0200
Subject: [PATCH 01/37] Prepare for v3 version of pkg:postgres
---
docs/lib/snippets/platforms/postgres.dart | 4 +-
docs/pages/docs/Platforms/postgres.md | 7 +-
docs/pubspec.yaml | 2 +-
extras/drift_postgres/CHANGELOG.md | 4 ++
extras/drift_postgres/example/main.dart | 4 +-
extras/drift_postgres/lib/drift_postgres.dart | 14 ++--
.../drift_postgres/lib/src/pg_database.dart | 69 +++++++++----------
extras/drift_postgres/lib/src/types.dart | 22 +++---
extras/drift_postgres/pubspec.yaml | 4 +-
.../test/drift_postgres_test.dart | 4 +-
extras/drift_postgres/test/types_test.dart | 6 +-
11 files changed, 69 insertions(+), 71 deletions(-)
diff --git a/docs/lib/snippets/platforms/postgres.dart b/docs/lib/snippets/platforms/postgres.dart
index c5ebe691..42edc0d3 100644
--- a/docs/lib/snippets/platforms/postgres.dart
+++ b/docs/lib/snippets/platforms/postgres.dart
@@ -1,6 +1,6 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
-import 'package:postgres/postgres_v3_experimental.dart';
+import 'package:postgres/postgres.dart';
part 'postgres.g.dart';
@@ -20,7 +20,7 @@ class MyDatabase extends _$MyDatabase {
void main() async {
final pgDatabase = PgDatabase(
- endpoint: PgEndpoint(
+ endpoint: Endpoint(
host: 'localhost',
database: 'postgres',
username: 'postgres',
diff --git a/docs/pages/docs/Platforms/postgres.md b/docs/pages/docs/Platforms/postgres.md
index 8d6b9ec9..dd411087 100644
--- a/docs/pages/docs/Platforms/postgres.md
+++ b/docs/pages/docs/Platforms/postgres.md
@@ -1,16 +1,11 @@
---
data:
- title: PostgreSQL support (Alpha)
+ title: PostgreSQL support (Beta)
description: Use drift with PostgreSQL database servers.
weight: 10
template: layouts/docs/single
---
-{% block "blocks/pageinfo" %}
-Postgres support is still in development. In particular, drift is waiting for [version 3](https://github.com/isoos/postgresql-dart/issues/105)
-of the postgres package to stabilize. Minor breaking changes or remaining issues are not unlikely.
-{% endblock %}
-
Thanks to contributions from the community, drift currently has alpha support for postgres with the `drift_postgres` package.
Without having to change your query code, drift can generate Postgres-compatible SQL for most queries,
allowing you to use your drift databases with a Postgres database server.
diff --git a/docs/pubspec.yaml b/docs/pubspec.yaml
index c4aec77b..6bdad56d 100644
--- a/docs/pubspec.yaml
+++ b/docs/pubspec.yaml
@@ -31,7 +31,7 @@ dependencies:
hosted: https://simonbinder.eu
version: ^1.5.10
test: ^1.18.0
- postgres: ^2.6.3
+ postgres: ^3.0.0-0
dev_dependencies:
lints: ^2.0.0
diff --git a/extras/drift_postgres/CHANGELOG.md b/extras/drift_postgres/CHANGELOG.md
index 3325df36..8306822a 100644
--- a/extras/drift_postgres/CHANGELOG.md
+++ b/extras/drift_postgres/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.2.0
+
+- Migrate to the stable v3 version of the `postgres` package.
+
## 0.1.0
- Initial release of `drift_postgres`.
diff --git a/extras/drift_postgres/example/main.dart b/extras/drift_postgres/example/main.dart
index 5ebda50b..00f6795e 100644
--- a/extras/drift_postgres/example/main.dart
+++ b/extras/drift_postgres/example/main.dart
@@ -1,6 +1,6 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
-import 'package:postgres/postgres_v3_experimental.dart';
+import 'package:postgres/postgres.dart' as pg;
import 'package:uuid/uuid.dart';
part 'main.g.dart';
@@ -20,7 +20,7 @@ class DriftPostgresDatabase extends _$DriftPostgresDatabase {
void main() async {
final database = DriftPostgresDatabase(PgDatabase(
- endpoint: PgEndpoint(
+ endpoint: pg.Endpoint(
host: 'localhost',
database: 'postgres',
username: 'postgres',
diff --git a/extras/drift_postgres/lib/drift_postgres.dart b/extras/drift_postgres/lib/drift_postgres.dart
index 27ace103..e2dee938 100644
--- a/extras/drift_postgres/lib/drift_postgres.dart
+++ b/extras/drift_postgres/lib/drift_postgres.dart
@@ -5,7 +5,7 @@
library drift.postgres;
import 'package:drift/drift.dart';
-import 'package:postgres/postgres_v3_experimental.dart';
+import 'package:postgres/postgres.dart' as pg;
import 'package:uuid/uuid.dart';
import 'src/types.dart';
@@ -23,7 +23,7 @@ typedef IntervalColumn = Column;
typedef JsonColumn = Column
Drift is a reactive persistence library for Flutter and Dart, built on top of
-sqlite.
+SQLite.
Drift is
- __Flexible__: Drift lets you write queries in both SQL and Dart,
providing fluent apis for both languages. You can filter and order results
or use joins to run queries on multiple tables. You can even use complex
-sql features like `WITH` and `WINDOW` clauses.
+SQL features like `WITH` and `WINDOW` clauses.
- __🔥 Feature rich__: Drift has builtin support for transactions, schema
migrations, complex filters and expressions, batched updates and joins. We
even have a builtin IDE for SQL!
-- __📦 Modular__: Thanks to builtin support for daos and `import`s in sql files, drift helps you keep your database code simple.
-- __🛡️ Safe__: Drift generates typesafe code based on your tables and queries. If you make a mistake in your queries, drift will find it at compile time and
+- __📦 Modular__: Thanks to builtin support for daos and `import`s in SQL files, drift helps you keep your database code simple.
+- __🛡️ Safe__: Drift generates type-safe code based on your tables and queries. If you make a mistake in your queries, drift will find it at compile time and
provide helpful and descriptive lints.
- __⚡ Fast__: Even though drift lets you write powerful queries, it can keep
up with the performance of key-value stores. Drift is the only major persistence library with builtin threading support, allowing you to run database code across isolates with zero additional effort.
-- __Reactive__: Turn any sql query into an auto-updating stream! This includes complex queries across many tables
+- __Reactive__: Turn any SQL query into an auto-updating stream! This includes complex queries across many tables
- __⚙️ Cross-Platform support__: Drift works on Android, iOS, macOS, Windows, Linux and [the web](https://drift.simonbinder.eu/web). [This template](https://github.com/simolus3/drift/tree/develop/examples/app) is a Flutter todo app that works on all platforms
- __🗡️ Battle tested and production ready__: Drift is stable and well tested with a wide range of unit and integration tests. It powers production Flutter apps.
diff --git a/sqlparser/README.md b/sqlparser/README.md
index 239b5d19..4a505db1 100644
--- a/sqlparser/README.md
+++ b/sqlparser/README.md
@@ -1,16 +1,16 @@
# sqlparser
-Sql parser and static analyzer written in Dart. At the moment, this library targets the
-sqlite dialect only.
+SQL parser and static analyzer written in Dart. At the moment, this library targets the
+SQLite dialect only.
## Features
-This library aims to support every sqlite feature, which includes parsing and detailed
+This library aims to support every SQLite feature, which includes parsing and detailed
static analysis.
We can resolve what type a column in a `SELECT` statement has, infer types for variables,
find semantic errors and more.
-This library supports most sqlite features:
+This library supports most SQLite features:
- DQL: Full support, including joins, `group by`, nested and compound selects, `WITH` clauses
and window functions
- DDL: Supports `CREATE TABLE` statements, including advanced features like foreign keys or
@@ -18,7 +18,7 @@ This library supports most sqlite features:
`CREATE TRIGGER` and `CREATE INDEX` statements.
### Using the parser
-To obtain an abstract syntax tree from an sql statement, use `SqlEngine.parse`.
+To obtain an abstract syntax tree from an SQL statement, use `SqlEngine.parse`.
```dart
import 'package:sqlparser/sqlparser.dart';
@@ -35,11 +35,11 @@ LIMIT 5 OFFSET 5 * 3
```
### Analysis
-Given information about all tables and a sql statement, this library can:
+Given information about all tables and an SQL statement, this library can:
1. Determine which result columns a query is going to have, including types and nullability
2. Make an educated guess about what type the variables in the query should have (it's not really
- possible to be 100% accurate about this because sqlite is very flexible at types, but this library
+ possible to be 100% accurate about this because SQLite is very flexible at types, but this library
gets it mostly right)
3. Issue basic warnings about queries that are syntactically valid but won't run (references unknown
tables / columns, uses undefined functions, etc.)
@@ -70,7 +70,7 @@ resolvedColumns.map((c) => context.typeOf(c).type.type); // int, text, int, text
## But why?
[Drift](https://pub.dev/packages/drift), a persistence library for Dart apps, uses this
-package to generate type-safe methods from sql.
+package to generate type-safe methods from SQL.
## Thanks
- To [Bob Nystrom](https://github.com/munificent) for his amazing ["Crafting Interpreters"](https://craftinginterpreters.com/)
From 398299e6e3d2da8ef5347b3218e9985984b7a25a Mon Sep 17 00:00:00 2001
From: Bruno Bee Nahorny <54868778+Brunobnahorny@users.noreply.github.com>
Date: Sat, 4 Nov 2023 02:45:07 -0300
Subject: [PATCH 17/37] Fix integer nullable cast in protocol serializer
Since the class ExecuteBatchedStatement accepts nullable executorId it looks like it should cast to nullable int.
It occours using Isolate from multiple Flutter engines.
---
drift/lib/src/remote/protocol.dart | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/drift/lib/src/remote/protocol.dart b/drift/lib/src/remote/protocol.dart
index 1c455ac2..d6ddb99f 100644
--- a/drift/lib/src/remote/protocol.dart
+++ b/drift/lib/src/remote/protocol.dart
@@ -191,7 +191,7 @@ class DriftProtocol {
list[0] as int, list.skip(1).toList()));
}
- final executorId = fullMessage.last as int;
+ final executorId = fullMessage.last as int?;
return ExecuteBatchedStatement(
BatchedStatements(sql, args), executorId);
case _tag_RunTransactionAction:
From 5426ad8a40e22259d5d9975b97659c72fd5d37d4 Mon Sep 17 00:00:00 2001
From: brunobnahorny
Date: Sun, 5 Nov 2023 11:07:32 -0300
Subject: [PATCH 18/37] test runBatched with the default executor
---
drift/test/remote_test.dart | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/drift/test/remote_test.dart b/drift/test/remote_test.dart
index fba747e5..d9845fc4 100644
--- a/drift/test/remote_test.dart
+++ b/drift/test/remote_test.dart
@@ -149,6 +149,15 @@ void main() {
(e) => e.remoteCause, 'remoteCause', 'UnimplementedError: error!')),
);
+ final statements =
+ BatchedStatements(['SELECT 1'], [ArgumentsForBatchedStatement(0, [])]);
+ when(executor.runBatched(any)).thenAnswer((i) => Future.value());
+ // Not using db.batch because that starts a transaction, we want to test
+ // this working with the default executor.
+ // Regression test for: https://github.com/simolus3/drift/pull/2707
+ await db.executor.runBatched(statements);
+ verify(executor.runBatched(statements));
+
await db.close();
});
From f57afb0d2726902aeb26010c779d55aa62f6b0f0 Mon Sep 17 00:00:00 2001
From: Simon Binder
Date: Sun, 5 Nov 2023 19:10:46 +0100
Subject: [PATCH 19/37] Delete analyzer plugin to avoid warnings
The plugin is not working at the moment, so there's no point in having
the entrypoint around either.
---
drift/tools/analyzer_plugin/bin/plugin.dart | 38 ---------------------
drift/tools/analyzer_plugin/pubspec.yaml | 18 ----------
2 files changed, 56 deletions(-)
delete mode 100644 drift/tools/analyzer_plugin/bin/plugin.dart
delete mode 100644 drift/tools/analyzer_plugin/pubspec.yaml
diff --git a/drift/tools/analyzer_plugin/bin/plugin.dart b/drift/tools/analyzer_plugin/bin/plugin.dart
deleted file mode 100644
index f2182399..00000000
--- a/drift/tools/analyzer_plugin/bin/plugin.dart
+++ /dev/null
@@ -1,38 +0,0 @@
-import 'dart:convert';
-import 'dart:isolate';
-
-import 'package:drift_dev/integrations/plugin.dart' as plugin;
-import 'package:web_socket_channel/io.dart';
-
-const useDebuggingVariant = false;
-
-void main(List args, SendPort sendPort) {
- if (useDebuggingVariant) {
- _PluginProxy(sendPort).start();
- } else {
- plugin.start(args, sendPort);
- }
-}
-
-// Used during development. See CONTRIBUTING.md in the drift repo on how to
-// debug the plugin.
-class _PluginProxy {
- final SendPort sendToAnalysisServer;
-
- _PluginProxy(this.sendToAnalysisServer);
-
- Future start() async {
- final channel = IOWebSocketChannel.connect('ws://localhost:9999');
- final receive = ReceivePort();
- sendToAnalysisServer.send(receive.sendPort);
-
- receive.listen((data) {
- // the server will send messages as maps, convert to json
- channel.sink.add(json.encode(data));
- });
-
- channel.stream.listen((data) {
- sendToAnalysisServer.send(json.decode(data as String));
- });
- }
-}
diff --git a/drift/tools/analyzer_plugin/pubspec.yaml b/drift/tools/analyzer_plugin/pubspec.yaml
deleted file mode 100644
index ca9d2a2b..00000000
--- a/drift/tools/analyzer_plugin/pubspec.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-name: analyzer_load_drift_plugin
-version: 1.0.0
-description: This pubspec is a part of Drift and determines the version of the analyzer plugin to load
-
-environment:
- sdk: '>=2.17.0 <3.0.0'
-
-dependencies:
- drift_dev: ^2.0.0
- web_socket_channel: ^2.2.0
-
-# These overrides are only needed when working on the plugin with useDebuggingVariant = false (not recommended)
-
-#dependency_overrides:
-# drift_dev:
-# path: /path/to/drift/drift_dev
-# sqlparser:
-# path: /path/to/drift/sqlparser
From 50487ad31719aa9c76428610dba43525ee25d566 Mon Sep 17 00:00:00 2001
From: Simon Binder
Date: Sun, 5 Nov 2023 19:12:46 +0100
Subject: [PATCH 20/37] Prepare patch release
---
drift/CHANGELOG.md | 4 ++++
drift/pubspec.yaml | 2 +-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/drift/CHANGELOG.md b/drift/CHANGELOG.md
index 16c7920f..d98f78bd 100644
--- a/drift/CHANGELOG.md
+++ b/drift/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.13.1
+
+- Fix a bug when running batches over serialized communication channels.
+
## 2.13.0
- Add APIs to setup Wasm databases with custom drift workers.
diff --git a/drift/pubspec.yaml b/drift/pubspec.yaml
index c8a8442f..f764cd42 100644
--- a/drift/pubspec.yaml
+++ b/drift/pubspec.yaml
@@ -1,6 +1,6 @@
name: drift
description: Drift is a reactive library to store relational data in Dart and Flutter applications.
-version: 2.13.0
+version: 2.13.1
repository: https://github.com/simolus3/drift
homepage: https://drift.simonbinder.eu/
issue_tracker: https://github.com/simolus3/drift/issues
From ce554a02e5a2872b151de2a58e87e9fdf295ebea Mon Sep 17 00:00:00 2001
From: Simon Binder
Date: Sun, 5 Nov 2023 19:20:33 +0100
Subject: [PATCH 21/37] Don't resolve dependencies for deleted plugin
---
.github/workflows/main.yml | 8 --------
1 file changed, 8 deletions(-)
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index f0d78d04..0c73e6cf 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -82,14 +82,6 @@ jobs:
dart_version: ${{ needs.setup.outputs.dart_version }}
- run: melos bootstrap --scope drift
working-directory: .
- - name: Get dependencies for plugin
- run: |
- echo "dependency_overrides:" >> pubspec_overrides.yaml
- echo " drift: {path: ../../}" >> pubspec_overrides.yaml
- echo " drift_dev: {path: ../../../drift_dev}" >> pubspec_overrides.yaml
- echo " sqlparser: {path: ../../../sqlparser}" >> pubspec_overrides.yaml
- dart pub get
- working-directory: drift/tools/analyzer_plugin
# analysis
- run: dart format -o none --set-exit-if-changed .
name: dartfmt
From 13d64a955a5818e449b8cc8ad1ea833add3487e1 Mon Sep 17 00:00:00 2001
From: Simon Binder
Date: Tue, 7 Nov 2023 14:40:52 +0100
Subject: [PATCH 22/37] Mention CRDT projects on community page
---
docs/pages/docs/community_tools.md | 10 ++++++++++
drift/lib/src/runtime/types/mapping.dart | 2 +-
2 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/docs/pages/docs/community_tools.md b/docs/pages/docs/community_tools.md
index 3f540158..53dbf7da 100644
--- a/docs/pages/docs/community_tools.md
+++ b/docs/pages/docs/community_tools.md
@@ -11,6 +11,16 @@ Do you have a drift-related package you want to share? Awesome, please let me kn
[Twitter](https://twitter.com/dersimolus) or via email to oss <at>simonbinder<dot>eu.
{% endblock %}
+## Conflict-free replicated datatypes
+
+Conflict-free replicated datatypes (CRDTs) enable synchronization and replication of data
+even when offline.
+The [sql\_crdt](https://pub.dev/packages/sql_crdt) package by Daniel Cachapa uses the
+`sqlparser` package from the drift project transforms SQL queries at runtime to implement
+CRDTs for databases.
+The [drift\_crdt](https://pub.dev/packages/drift_crdt) package by Janez Štupar provides a
+wrapper around this for drift.
+
## Storage inspector
[Nicola Verbeeck](https://github.com/NicolaVerbeeck) wrote the `storage_inspector` packages, which
diff --git a/drift/lib/src/runtime/types/mapping.dart b/drift/lib/src/runtime/types/mapping.dart
index f3c77a0b..7fc5c152 100644
--- a/drift/lib/src/runtime/types/mapping.dart
+++ b/drift/lib/src/runtime/types/mapping.dart
@@ -155,7 +155,7 @@ final class SqlTypes {
// thing.
result = DateTime.parse(rawValue);
} else {
- // Result from complex date tmie transformation. Interpret as UTC,
+ // Result from complex date time transformation. Interpret as UTC,
// which is what sqlite3 does by default.
result = DateTime.parse('${rawValue}Z');
}
From de7e1ce38155aad1ef9f1e5cba3a40aa583d85b3 Mon Sep 17 00:00:00 2001
From: Erlang Parasu
Date: Tue, 7 Nov 2023 08:20:53 +0800
Subject: [PATCH 23/37] Update custom_row_classes.md: fix typo
---
docs/pages/docs/custom_row_classes.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/pages/docs/custom_row_classes.md b/docs/pages/docs/custom_row_classes.md
index 8cad8c1e..b894c311 100644
--- a/docs/pages/docs/custom_row_classes.md
+++ b/docs/pages/docs/custom_row_classes.md
@@ -50,7 +50,7 @@ If you want to use another constructor, set the `constructor` parameter on the
{% assign snippets = "package:drift_docs/snippets/custom_row_classes/named.dart.excerpt.json" | readString | json_decode %}
{% include "blocks/snippet" snippets = snippets name = "named" %}
-### Static and aynchronous factories
+### Static and asynchronous factories
Starting with drift 2.0, the custom constructor set with the `constructor`
parameter on the `@UseRowClass` annotation may also refer to a static method
From 446832c341c3a30bc18fd6e6f5e84fc0dfd6ceb1 Mon Sep 17 00:00:00 2001
From: Simon Binder
Date: Tue, 7 Nov 2023 22:46:29 +0100
Subject: [PATCH 24/37] Add query interceptor API
---
drift/CHANGELOG.md | 4 +
drift/lib/drift.dart | 1 +
.../lib/src/runtime/executor/interceptor.dart | 178 ++++++++++++++++++
drift/pubspec.yaml | 2 +-
drift/test/engines/interceptor_test.dart | 96 ++++++++++
drift_dev/pubspec.yaml | 2 +-
6 files changed, 281 insertions(+), 2 deletions(-)
create mode 100644 drift/lib/src/runtime/executor/interceptor.dart
create mode 100644 drift/test/engines/interceptor_test.dart
diff --git a/drift/CHANGELOG.md b/drift/CHANGELOG.md
index d98f78bd..99e9d7c4 100644
--- a/drift/CHANGELOG.md
+++ b/drift/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.14.0-dev
+
+- Add the `QueryInterceptor` to easily monitor all database calls made by drift.
+
## 2.13.1
- Fix a bug when running batches over serialized communication channels.
diff --git a/drift/lib/drift.dart b/drift/lib/drift.dart
index 51886b96..4843c12f 100644
--- a/drift/lib/drift.dart
+++ b/drift/lib/drift.dart
@@ -15,6 +15,7 @@ export 'src/runtime/data_verification.dart';
export 'src/runtime/exceptions.dart';
export 'src/runtime/executor/connection_pool.dart';
export 'src/runtime/executor/executor.dart';
+export 'src/runtime/executor/interceptor.dart';
export 'src/runtime/query_builder/query_builder.dart'
hide CaseWhenExpressionWithBase, BaseCaseWhenExpression;
export 'src/runtime/types/converters.dart';
diff --git a/drift/lib/src/runtime/executor/interceptor.dart b/drift/lib/src/runtime/executor/interceptor.dart
new file mode 100644
index 00000000..f8f13419
--- /dev/null
+++ b/drift/lib/src/runtime/executor/interceptor.dart
@@ -0,0 +1,178 @@
+import '../api/runtime_api.dart';
+import '../query_builder/query_builder.dart';
+import 'executor.dart';
+
+/// Extension to wrap a [QueryExecutor] with a [QueryInterceptor].
+extension ApplyInterceptor on QueryExecutor {
+ /// Returns a [QueryExecutor] that will use `this` executor internally, but
+ /// with calls intercepted by the given [interceptor].
+ ///
+ /// This can be used to, for instance, write a custom statement logger or to
+ /// retry failing statements automatically.
+ QueryExecutor interceptWith(QueryInterceptor interceptor) {
+ final $this = this;
+
+ if ($this is TransactionExecutor) {
+ return _InterceptedTransactionExecutor($this, interceptor);
+ } else {
+ return _InterceptedExecutor($this, interceptor);
+ }
+ }
+}
+
+/// Extension to wrap a [DatabaseConnection] with a [QueryInterceptor].
+extension ApplyInterceptorConnection on DatabaseConnection {
+ /// Returns a [DatabaseConnection] that will use the same stream queries as
+ /// `this`, but replaces its executor by wrapping it with the [interceptor].
+ ///
+ /// See also: [ApplyInterceptor.interceptWith].
+ DatabaseConnection interceptWith(QueryInterceptor interceptor) {
+ return withExecutor(executor.interceptWith(interceptor));
+ }
+}
+
+/// An interceptor for SQL queries.
+///
+/// This wraps an existing [QueryExecutor] implemented by drift, and by default
+/// does nothing. However, specific methods can be overridden to customize the
+/// behavior of an existing query executor.
+abstract class QueryInterceptor {
+ /// Intercept [QueryExecutor.dialect] calls.
+ SqlDialect dialect(QueryExecutor executor) => executor.dialect;
+
+ /// Intercept [QueryExecutor.beginTransaction] calls.
+ TransactionExecutor beginTransaction(QueryExecutor parent) =>
+ parent.beginTransaction();
+
+ /// Intercept [TransactionExecutor.supportsNestedTransactions] calls.
+ bool transactionCanBeNested(TransactionExecutor inner) {
+ return inner.supportsNestedTransactions;
+ }
+
+ /// Intercept [QueryExecutor.close] calls.
+ Future close(QueryExecutor inner) => inner.close();
+
+ /// Intercept [TransactionExecutor.send] calls.
+ Future commitTransaction(TransactionExecutor inner) {
+ return inner.send();
+ }
+
+ /// Intercept [TransactionExecutor.rollback] calls.
+ Future rollbackTransaction(TransactionExecutor inner) {
+ return inner.rollback();
+ }
+
+ /// Intercept [QueryExecutor.ensureOpen] calls.
+ Future ensureOpen(QueryExecutor executor, QueryExecutorUser user) =>
+ executor.ensureOpen(user);
+
+ /// Intercept [QueryExecutor.runBatched] calls.
+ Future runBatched(
+ QueryExecutor executor, BatchedStatements statements) {
+ return executor.runBatched(statements);
+ }
+
+ /// Intercept [QueryExecutor.runCustom] calls.
+ Future runCustom(
+ QueryExecutor executor, String statement, List args) {
+ return executor.runCustom(statement, args);
+ }
+
+ /// Intercept [QueryExecutor.runInsert] calls.
+ Future runInsert(
+ QueryExecutor executor, String statement, List args) {
+ return executor.runInsert(statement, args);
+ }
+
+ /// Intercept [QueryExecutor.runDelete] calls.
+ Future runDelete(
+ QueryExecutor executor, String statement, List args) {
+ return executor.runDelete(statement, args);
+ }
+
+ /// Intercept [QueryExecutor.runUpdate] calls.
+ Future runUpdate(
+ QueryExecutor executor, String statement, List args) {
+ return executor.runUpdate(statement, args);
+ }
+
+ /// Intercept [QueryExecutor.runSelect] calls.
+ Future>> runSelect(
+ QueryExecutor executor, String statement, List args) {
+ return executor.runSelect(statement, args);
+ }
+}
+
+class _InterceptedExecutor extends QueryExecutor {
+ final QueryExecutor _inner;
+ final QueryInterceptor _interceptor;
+
+ _InterceptedExecutor(this._inner, this._interceptor);
+
+ @override
+ TransactionExecutor beginTransaction() => _InterceptedTransactionExecutor(
+ _interceptor.beginTransaction(_inner), _interceptor);
+
+ @override
+ SqlDialect get dialect => _interceptor.dialect(_inner);
+
+ @override
+ Future ensureOpen(QueryExecutorUser user) {
+ return _interceptor.ensureOpen(_inner, user);
+ }
+
+ @override
+ Future runBatched(BatchedStatements statements) {
+ return _interceptor.runBatched(_inner, statements);
+ }
+
+ @override
+ Future runCustom(String statement, [List? args]) {
+ return _interceptor.runCustom(_inner, statement, args ?? const []);
+ }
+
+ @override
+ Future runDelete(String statement, List args) {
+ return _interceptor.runDelete(_inner, statement, args);
+ }
+
+ @override
+ Future runInsert(String statement, List args) {
+ return _interceptor.runInsert(_inner, statement, args);
+ }
+
+ @override
+ Future>> runSelect(
+ String statement, List args) {
+ return _interceptor.runSelect(_inner, statement, args);
+ }
+
+ @override
+ Future runUpdate(String statement, List args) {
+ return _interceptor.runUpdate(_inner, statement, args);
+ }
+
+ @override
+ Future close() {
+ return _interceptor.close(_inner);
+ }
+}
+
+class _InterceptedTransactionExecutor extends _InterceptedExecutor
+ implements TransactionExecutor {
+ _InterceptedTransactionExecutor(super.inner, super.interceptor);
+
+ @override
+ Future rollback() {
+ return _interceptor.rollbackTransaction(_inner as TransactionExecutor);
+ }
+
+ @override
+ Future send() {
+ return _interceptor.commitTransaction(_inner as TransactionExecutor);
+ }
+
+ @override
+ bool get supportsNestedTransactions =>
+ _interceptor.transactionCanBeNested(_inner as TransactionExecutor);
+}
diff --git a/drift/pubspec.yaml b/drift/pubspec.yaml
index f764cd42..1280fc96 100644
--- a/drift/pubspec.yaml
+++ b/drift/pubspec.yaml
@@ -1,6 +1,6 @@
name: drift
description: Drift is a reactive library to store relational data in Dart and Flutter applications.
-version: 2.13.1
+version: 2.14.0-dev
repository: https://github.com/simolus3/drift
homepage: https://drift.simonbinder.eu/
issue_tracker: https://github.com/simolus3/drift/issues
diff --git a/drift/test/engines/interceptor_test.dart b/drift/test/engines/interceptor_test.dart
new file mode 100644
index 00000000..19666539
--- /dev/null
+++ b/drift/test/engines/interceptor_test.dart
@@ -0,0 +1,96 @@
+import 'dart:async';
+
+import 'package:drift/drift.dart';
+import 'package:test/test.dart';
+
+import '../generated/todos.dart';
+import '../test_utils/test_utils.dart';
+
+void main() {
+ test('calls interceptor methods', () async {
+ final interceptor = EmittingInterceptor();
+ final events = [];
+ interceptor.events.stream.listen(events.add);
+
+ final database = TodoDb(testInMemoryDatabase().interceptWith(interceptor));
+ expect(await database.categories.select().get(), isEmpty);
+ expect(events, ['select']);
+
+ await database.batch((batch) {
+ batch.insert(database.categories,
+ CategoriesCompanion.insert(description: 'from batch'));
+ });
+ expect(events, ['select', 'begin', 'batched', 'commit']);
+ events.clear();
+
+ await database.users.insertOne(
+ UsersCompanion.insert(name: 'Simon B', profilePicture: Uint8List(0)));
+ await database.users.update().write(UsersCompanion(isAwesome: Value(true)));
+ await database.users.delete().go();
+ expect(events, ['insert', 'update', 'delete']);
+ });
+}
+
+class EmittingInterceptor extends QueryInterceptor {
+ final events = StreamController();
+
+ @override
+ TransactionExecutor beginTransaction(QueryExecutor parent) {
+ events.add('begin');
+ return super.beginTransaction(parent);
+ }
+
+ @override
+ Future commitTransaction(TransactionExecutor inner) {
+ events.add('commit');
+ return super.commitTransaction(inner);
+ }
+
+ @override
+ Future rollbackTransaction(TransactionExecutor inner) {
+ events.add('rollback');
+ return super.rollbackTransaction(inner);
+ }
+
+ @override
+ Future runBatched(
+ QueryExecutor executor, BatchedStatements statements) {
+ events.add('batched');
+ return super.runBatched(executor, statements);
+ }
+
+ @override
+ Future runCustom(
+ QueryExecutor executor, String statement, List args) {
+ events.add('custom');
+ return super.runCustom(executor, statement, args);
+ }
+
+ @override
+ Future runInsert(
+ QueryExecutor executor, String statement, List args) {
+ events.add('insert');
+ return super.runInsert(executor, statement, args);
+ }
+
+ @override
+ Future runDelete(
+ QueryExecutor executor, String statement, List args) {
+ events.add('delete');
+ return super.runDelete(executor, statement, args);
+ }
+
+ @override
+ Future runUpdate(
+ QueryExecutor executor, String statement, List args) {
+ events.add('update');
+ return super.runUpdate(executor, statement, args);
+ }
+
+ @override
+ Future>> runSelect(
+ QueryExecutor executor, String statement, List args) {
+ events.add('select');
+ return super.runSelect(executor, statement, args);
+ }
+}
diff --git a/drift_dev/pubspec.yaml b/drift_dev/pubspec.yaml
index bc9f9d98..da691949 100644
--- a/drift_dev/pubspec.yaml
+++ b/drift_dev/pubspec.yaml
@@ -30,7 +30,7 @@ dependencies:
io: ^1.0.3
# Drift-specific analysis and apis
- drift: '>=2.13.0 <2.14.0'
+ drift: '>=2.14.0 <2.15.0'
sqlite3: '>=0.1.6 <3.0.0'
sqlparser: '^0.32.0'
From b096e84fd418c5f3564c12b49d11f32e46ca828b Mon Sep 17 00:00:00 2001
From: Simon Binder
Date: Tue, 7 Nov 2023 23:42:07 +0100
Subject: [PATCH 25/37] Add interceptor example to docs
---
docs/lib/snippets/log_interceptor.dart | 92 ++++++++++++++++++++++++++
docs/pages/docs/Examples/tracing.md | 24 +++++++
docs/test/snippet_test.dart | 31 +++++++++
3 files changed, 147 insertions(+)
create mode 100644 docs/lib/snippets/log_interceptor.dart
create mode 100644 docs/pages/docs/Examples/tracing.md
diff --git a/docs/lib/snippets/log_interceptor.dart b/docs/lib/snippets/log_interceptor.dart
new file mode 100644
index 00000000..979c8aa4
--- /dev/null
+++ b/docs/lib/snippets/log_interceptor.dart
@@ -0,0 +1,92 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:drift/drift.dart';
+import 'package:drift/native.dart';
+
+// #docregion class
+class LogInterceptor extends QueryInterceptor {
+ Future _run(
+ String description, FutureOr Function() operation) async {
+ final stopwatch = Stopwatch()..start();
+ print('Running $description');
+
+ try {
+ final result = await operation();
+ print(' => succeeded after ${stopwatch.elapsedMilliseconds}ms');
+ return result;
+ } on Object catch (e) {
+ print(' => failed after ${stopwatch.elapsedMilliseconds}ms ($e)');
+ rethrow;
+ }
+ }
+
+ @override
+ TransactionExecutor beginTransaction(QueryExecutor parent) {
+ print('begin');
+ return super.beginTransaction(parent);
+ }
+
+ @override
+ Future commitTransaction(TransactionExecutor inner) {
+ return _run('commit', () => inner.send());
+ }
+
+ @override
+ Future rollbackTransaction(TransactionExecutor inner) {
+ return _run('rollback', () => inner.rollback());
+ }
+
+ @override
+ Future runBatched(
+ QueryExecutor executor, BatchedStatements statements) {
+ return _run(
+ 'batch with $statements', () => executor.runBatched(statements));
+ }
+
+ @override
+ Future runInsert(
+ QueryExecutor executor, String statement, List args) {
+ return _run(
+ '$statement with $args', () => executor.runInsert(statement, args));
+ }
+
+ @override
+ Future runUpdate(
+ QueryExecutor executor, String statement, List args) {
+ return _run(
+ '$statement with $args', () => executor.runUpdate(statement, args));
+ }
+
+ @override
+ Future runDelete(
+ QueryExecutor executor, String statement, List args) {
+ return _run(
+ '$statement with $args', () => executor.runDelete(statement, args));
+ }
+
+ @override
+ Future runCustom(
+ QueryExecutor executor, String statement, List args) {
+ return _run(
+ '$statement with $args', () => executor.runCustom(statement, args));
+ }
+
+ @override
+ Future>> runSelect(
+ QueryExecutor executor, String statement, List args) {
+ return _run(
+ '$statement with $args', () => executor.runSelect(statement, args));
+ }
+}
+// #enddocregion class
+
+void use() {
+ final myDatabaseFile = File('/dev/null');
+
+ // #docregion use
+ NativeDatabase.createInBackground(
+ myDatabaseFile,
+ ).interceptWith(LogInterceptor());
+ // #enddocregion use
+}
diff --git a/docs/pages/docs/Examples/tracing.md b/docs/pages/docs/Examples/tracing.md
new file mode 100644
index 00000000..0d234a40
--- /dev/null
+++ b/docs/pages/docs/Examples/tracing.md
@@ -0,0 +1,24 @@
+---
+data:
+ title: "Tracing database operations"
+ description: Using the `QueryInterceptor` API to log details about database operations.
+template: layouts/docs/single
+---
+
+{% assign snippets = 'package:drift_docs/snippets/log_interceptor.dart.excerpt.json' | readString | json_decode %}
+
+Drift provides the relatively simple `logStatements` option to print the statements it
+executes.
+The `QueryInterceptor` API can be used to extend this logging to provide more information,
+which this example will show.
+
+{% include "blocks/snippet" snippets=snippets name="class" %}
+
+Interceptors can be applied with the `interceptWith` extension on `QueryExecutor` and
+`DatabaseConnection`:
+
+{% include "blocks/snippet" snippets=snippets name="use" %}
+
+The `QueryInterceptor` class is pretty powerful, as it allows you to fully control the underlying
+database connection. You could also use it to retry some failing statements or to aggregate
+statistics about query times to an external monitoring service.
diff --git a/docs/test/snippet_test.dart b/docs/test/snippet_test.dart
index 16968961..e8b4f5bb 100644
--- a/docs/test/snippet_test.dart
+++ b/docs/test/snippet_test.dart
@@ -1,6 +1,7 @@
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:drift_docs/snippets/dart_api/datetime_conversion.dart';
+import 'package:drift_docs/snippets/log_interceptor.dart';
import 'package:drift_docs/snippets/modular/schema_inspection.dart';
import 'package:test/test.dart';
@@ -117,4 +118,34 @@ void main() {
expect(row.name, 'bar');
});
});
+
+ test('interceptor', () {
+ expect(
+ () async {
+ final db =
+ Database(NativeDatabase.memory().interceptWith(LogInterceptor()));
+
+ await db.batch((batch) {
+ batch.insert(db.users, UsersCompanion.insert(name: 'foo'));
+ });
+
+ await db.users.all().get();
+ },
+ prints(
+ allOf(
+ stringContainsInOrder(
+ [
+ 'begin',
+ 'Running batch with BatchedStatements',
+ ' => succeeded after ',
+ 'Running commit',
+ ' => succeeded after ',
+ 'Running SELECT * FROM "users"; with []',
+ ' => succeeded after'
+ ],
+ ),
+ ),
+ ),
+ );
+ });
}
From d770c16d8d9790479fde55ef8d870c05beadcd3b Mon Sep 17 00:00:00 2001
From: Simon Binder
Date: Wed, 8 Nov 2023 22:11:28 +0100
Subject: [PATCH 26/37] Fix warnings about missing imports
---
drift_dev/lib/src/analysis/driver/driver.dart | 35 +++++++++++++----
.../build/build_integration_test.dart | 39 ++++++++++++++++++-
.../backends/build/drift_builder_test.dart | 6 +--
drift_dev/test/utils.dart | 8 ++++
4 files changed, 76 insertions(+), 12 deletions(-)
diff --git a/drift_dev/lib/src/analysis/driver/driver.dart b/drift_dev/lib/src/analysis/driver/driver.dart
index 0e9c15e9..c5e7b248 100644
--- a/drift_dev/lib/src/analysis/driver/driver.dart
+++ b/drift_dev/lib/src/analysis/driver/driver.dart
@@ -174,19 +174,40 @@ class DriftAnalysisDriver {
}
Future _warnAboutUnresolvedImportsInDriftFile(FileState known) async {
+ await discoverIfNecessary(known);
+
final state = known.discovery;
if (state is DiscoveredDriftFile) {
for (final import in state.imports) {
final file = await findLocalElements(import.importedUri);
if (file.isValidImport != true) {
- known.errorsDuringDiscovery.add(
- DriftAnalysisError.inDriftFile(
- import.ast,
- 'The imported file, `${import.importedUri}`, does not exist or '
- "can't be imported.",
- ),
+ var crossesPackageBoundaries = false;
+
+ if (import.importedUri.scheme == 'package' &&
+ known.ownUri.scheme == 'package') {
+ final ownPackage = known.ownUri.pathSegments.first;
+ final importedPackage = import.importedUri.pathSegments.first;
+ crossesPackageBoundaries = ownPackage != importedPackage;
+ }
+
+ final message = StringBuffer(
+ 'The imported file, `${import.importedUri}`, does not exist or '
+ "can't be imported.",
);
+ if (crossesPackageBoundaries) {
+ message
+ ..writeln()
+ ..writeln(
+ 'Note: When importing drift files across packages, the '
+ 'imported package needs to apply a drift builder. '
+ 'See https://github.com/simolus3/drift/issues/2719 for '
+ 'details.',
+ );
+ }
+
+ known.errorsDuringDiscovery.add(
+ DriftAnalysisError.inDriftFile(import.ast, message.toString()));
}
}
}
@@ -241,7 +262,7 @@ class DriftAnalysisDriver {
await _warnAboutUnresolvedImportsInDriftFile(known);
// Also make sure elements in transitive imports have been resolved.
- final seen = cache.knownFiles.keys.toSet();
+ final seen = {};
final pending = [known.ownUri];
while (pending.isNotEmpty) {
diff --git a/drift_dev/test/backends/build/build_integration_test.dart b/drift_dev/test/backends/build/build_integration_test.dart
index 26411478..3acf7b06 100644
--- a/drift_dev/test/backends/build/build_integration_test.dart
+++ b/drift_dev/test/backends/build/build_integration_test.dart
@@ -625,8 +625,7 @@ class MyDatabase {
Future runTest(String source, expectedMessage) async {
final build = emulateDriftBuild(
inputs: {'a|lib/a.drift': source},
- logger: loggerThat(emits(isA()
- .having((e) => e.message, 'message', expectedMessage))),
+ logger: loggerThat(emits(record(expectedMessage))),
modularBuild: true,
options: options,
);
@@ -661,4 +660,40 @@ class MyDatabase {
});
}
});
+
+ test('warns about missing imports', () async {
+ await emulateDriftBuild(
+ inputs: {
+ 'a|lib/main.drift': '''
+import 'package:b/b.drift';
+import 'package:a/missing.drift';
+
+CREATE TABLE users (
+ another INTEGER REFERENCES b(foo)
+);
+''',
+ 'b|lib/b.drift': '''
+CREATE TABLE b (foo INTEGER);
+''',
+ },
+ logger: loggerThat(
+ emitsInAnyOrder(
+ [
+ record(
+ allOf(
+ contains(
+ "The imported file, `package:b/b.drift`, does not exist or can't be imported"),
+ contains('Note: When importing drift files across packages'),
+ ),
+ ),
+ record(allOf(
+ contains('package:a/missing.drift'),
+ isNot(contains('Note: When')),
+ )),
+ record(contains('`b` could not be found in any import.')),
+ ],
+ ),
+ ),
+ );
+ });
}
diff --git a/drift_dev/test/backends/build/drift_builder_test.dart b/drift_dev/test/backends/build/drift_builder_test.dart
index 1486dfa3..6519a584 100644
--- a/drift_dev/test/backends/build/drift_builder_test.dart
+++ b/drift_dev/test/backends/build/drift_builder_test.dart
@@ -19,7 +19,7 @@ void main() {
);
await emulateDriftBuild(inputs: {
- 'foo|lib/a.dart': '''
+ 'a|lib/a.dart': '''
// @dart = 2.11
import 'package:drift/drift.dart';
@@ -32,7 +32,7 @@ class Database {}
test('includes version override in part file mode', () async {
final writer = await emulateDriftBuild(inputs: {
- 'foo|lib/a.dart': '''
+ 'a|lib/a.dart': '''
// @dart = 2.13
import 'package:drift/drift.dart';
@@ -44,7 +44,7 @@ class Database {}
checkOutputs(
{
- 'foo|lib/a.drift.dart': decodedMatches(contains('// @dart=2.13')),
+ 'a|lib/a.drift.dart': decodedMatches(contains('// @dart=2.13')),
},
writer.dartOutputs,
writer.writer,
diff --git a/drift_dev/test/utils.dart b/drift_dev/test/utils.dart
index 47c25f21..50063ad5 100644
--- a/drift_dev/test/utils.dart
+++ b/drift_dev/test/utils.dart
@@ -28,6 +28,10 @@ Logger loggerThat(dynamic expectedLogs) {
return logger;
}
+TypeMatcher record(dynamic message) {
+ return isA().having((e) => e.message, 'message', message);
+}
+
final _packageConfig = Future(() async {
final uri = await Isolate.packageConfig;
@@ -77,6 +81,10 @@ Future emulateDriftBuild({
for (final input in inputs.keys) {
final inputId = makeAssetId(input);
+ // Assets from other packages are visible, but we're not running
+ // builders on them.
+ if (inputId.package != 'a') continue;
+
if (expectedOutputs(stage, inputId).isNotEmpty) {
final readerForPhase = _TrackingAssetReader(reader);
From e79124e5afbdd0d4f637a8c6f6073fdb8f44e25b Mon Sep 17 00:00:00 2001
From: Simon Binder
Date: Thu, 9 Nov 2023 19:03:32 +0100
Subject: [PATCH 27/37] Fix order of arguments when reading custom types
---
drift/test/generated/converter.dart | 27 +++++++++++++++++++
drift/test/generated/custom_tables.g.dart | 8 +++---
drift/test/generated/tables.drift | 2 +-
.../integration_tests/drift_files_test.dart | 2 +-
drift_dev/CHANGELOG.md | 4 +++
.../lib/src/writer/queries/query_writer.dart | 2 +-
6 files changed, 38 insertions(+), 7 deletions(-)
diff --git a/drift/test/generated/converter.dart b/drift/test/generated/converter.dart
index a864e9ec..4ab9c425 100644
--- a/drift/test/generated/converter.dart
+++ b/drift/test/generated/converter.dart
@@ -1,5 +1,32 @@
import 'package:drift/drift.dart';
+class CustomTextType implements CustomSqlType {
+ const CustomTextType();
+
+ @override
+ String mapToSqlLiteral(String dartValue) {
+ final escapedChars = dartValue.replaceAll('\'', '\'\'');
+ return "'$escapedChars'";
+ }
+
+ @override
+ Object mapToSqlParameter(String dartValue) {
+ return dartValue;
+ }
+
+ @override
+ String read(Object fromSql) {
+ return fromSql.toString();
+ }
+
+ @override
+ String sqlTypeName(GenerationContext context) {
+ // Still has text column affinity, but can be used to verify that the type
+ // really is used.
+ return 'MY_TEXT';
+ }
+}
+
enum SyncType {
locallyCreated,
locallyUpdated,
diff --git a/drift/test/generated/custom_tables.g.dart b/drift/test/generated/custom_tables.g.dart
index a6018c24..281de529 100644
--- a/drift/test/generated/custom_tables.g.dart
+++ b/drift/test/generated/custom_tables.g.dart
@@ -106,7 +106,7 @@ class WithDefaults extends Table with TableInfo {
static const VerificationMeta _aMeta = const VerificationMeta('a');
late final GeneratedColumn a = GeneratedColumn(
'a', aliasedName, true,
- type: DriftSqlType.string,
+ type: const CustomTextType(),
requiredDuringInsert: false,
$customConstraints: 'DEFAULT \'something\'',
defaultValue: const CustomExpression('\'something\''));
@@ -144,7 +144,7 @@ class WithDefaults extends Table with TableInfo {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return WithDefault(
a: attachedDatabase.typeMapping
- .read(DriftSqlType.string, data['${effectivePrefix}a']),
+ .read(const CustomTextType(), data['${effectivePrefix}a']),
b: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}b']),
);
@@ -267,7 +267,7 @@ class WithDefaultsCompanion extends UpdateCompanion {
Map toColumns(bool nullToAbsent) {
final map = {};
if (a.present) {
- map['a'] = Variable(a.value);
+ map['a'] = Variable(a.value, const CustomTextType());
}
if (b.present) {
map['b'] = Variable(b.value);
@@ -1801,7 +1801,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
...generatedpredicate.watchedTables,
}).asyncMap((QueryRow row) async => MultipleResult(
row: row,
- a: row.readNullable('a'),
+ a: row.readNullableWithType(const CustomTextType(), 'a'),
b: row.readNullable('b'),
c: await withConstraints.mapFromRowOrNull(row,
tablePrefix: 'nested_0'),
diff --git a/drift/test/generated/tables.drift b/drift/test/generated/tables.drift
index 36e7dc1b..f5258057 100644
--- a/drift/test/generated/tables.drift
+++ b/drift/test/generated/tables.drift
@@ -6,7 +6,7 @@ CREATE TABLE no_ids (
) WITHOUT ROWID WITH NoIdRow;
CREATE TABLE with_defaults (
- a TEXT JSON KEY customJsonName DEFAULT 'something',
+ a `const CustomTextType()` JSON KEY customJsonName DEFAULT 'something',
b INT UNIQUE
);
diff --git a/drift/test/integration_tests/drift_files_test.dart b/drift/test/integration_tests/drift_files_test.dart
index fe841632..0b0f2baa 100644
--- a/drift/test/integration_tests/drift_files_test.dart
+++ b/drift/test/integration_tests/drift_files_test.dart
@@ -11,7 +11,7 @@ const _createNoIds =
'WITHOUT ROWID;';
const _createWithDefaults = 'CREATE TABLE IF NOT EXISTS "with_defaults" ('
- "\"a\" TEXT DEFAULT 'something', \"b\" INTEGER UNIQUE);";
+ "\"a\" MY_TEXT DEFAULT 'something', \"b\" INTEGER UNIQUE);";
const _createWithConstraints = 'CREATE TABLE IF NOT EXISTS "with_constraints" ('
'"a" TEXT, "b" INTEGER NOT NULL, "c" REAL, '
diff --git a/drift_dev/CHANGELOG.md b/drift_dev/CHANGELOG.md
index 85403030..6edbbc2b 100644
--- a/drift_dev/CHANGELOG.md
+++ b/drift_dev/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.14.0-dev
+
+- Fix generated queries relying on custom types.
+
## 2.13.1
- Add `has_separate_analyzer` option to optimize builds using the `not_shared` builder.
diff --git a/drift_dev/lib/src/writer/queries/query_writer.dart b/drift_dev/lib/src/writer/queries/query_writer.dart
index b150576a..e370c41d 100644
--- a/drift_dev/lib/src/writer/queries/query_writer.dart
+++ b/drift_dev/lib/src/writer/queries/query_writer.dart
@@ -210,7 +210,7 @@ class QueryWriter {
if (column.sqlType.isCustom) {
final method = isNullable ? 'readNullableWithType' : 'readWithType';
final typeImpl = _emitter.dartCode(column.sqlType.custom!.expression);
- code = 'row.$method<$rawDartType>($dartLiteral, $typeImpl)';
+ code = 'row.$method<$rawDartType>($typeImpl, $dartLiteral)';
} else {
final method = isNullable ? 'readNullable' : 'read';
code = 'row.$method<$rawDartType>($dartLiteral)';
From f9012fc05cb6cb1a6c70db06ab26495dc2f1a6e5 Mon Sep 17 00:00:00 2001
From: Simon Binder
Date: Fri, 10 Nov 2023 21:12:02 +0100
Subject: [PATCH 28/37] Add `count()` as a utility extension
---
drift/CHANGELOG.md | 1 +
.../src/runtime/query_builder/on_table.dart | 17 +++++++++++
.../test/database/statements/select_test.dart | 29 +++++++++++++++++++
3 files changed, 47 insertions(+)
diff --git a/drift/CHANGELOG.md b/drift/CHANGELOG.md
index 99e9d7c4..de636f0b 100644
--- a/drift/CHANGELOG.md
+++ b/drift/CHANGELOG.md
@@ -1,6 +1,7 @@
## 2.14.0-dev
- Add the `QueryInterceptor` to easily monitor all database calls made by drift.
+- Add the `count()` extension on tables to easily count rows in tables or views.
## 2.13.1
diff --git a/drift/lib/src/runtime/query_builder/on_table.dart b/drift/lib/src/runtime/query_builder/on_table.dart
index 156dad3a..b9038e87 100644
--- a/drift/lib/src/runtime/query_builder/on_table.dart
+++ b/drift/lib/src/runtime/query_builder/on_table.dart
@@ -12,6 +12,23 @@ extension TableOrViewStatements
return select();
}
+ /// Counts the rows in this table.
+ ///
+ /// The optional [where] clause can be used to only count rows matching the
+ /// condition, similar to [SimpleSelectStatement.where].
+ ///
+ /// The returned [Selectable] can be run once with [Selectable.getSingle] to
+ /// get the count once, or be watched as a stream with [Selectable.watchSingle].
+ Selectable count({Expression Function(Tbl row)? where}) {
+ final count = countAll();
+ final stmt = selectOnly()..addColumns([count]);
+ if (where != null) {
+ stmt.where(where(asDslTable));
+ }
+
+ return stmt.map((row) => row.read(count)!);
+ }
+
/// Composes a `SELECT` statement on the captured table or view.
///
/// This is equivalent to calling [DatabaseConnectionUser.select].
diff --git a/drift/test/database/statements/select_test.dart b/drift/test/database/statements/select_test.dart
index d8c602b6..52cc3748 100644
--- a/drift/test/database/statements/select_test.dart
+++ b/drift/test/database/statements/select_test.dart
@@ -256,4 +256,33 @@ void main() {
[r'$.foo'],
));
});
+
+ group('count', () {
+ test('all', () async {
+ when(executor.runSelect(any, any)).thenAnswer((_) async => [
+ {'c0': 3}
+ ]);
+
+ final result = await db.todosTable.count().getSingle();
+ expect(result, 3);
+
+ verify(executor.runSelect(
+ 'SELECT COUNT(*) AS "c0" FROM "todos";', argThat(isEmpty)));
+ });
+
+ test('with filter', () async {
+ when(executor.runSelect(any, any)).thenAnswer((_) async => [
+ {'c0': 2}
+ ]);
+
+ final result = await db.todosTable
+ .count(where: (row) => row.id.isBiggerThanValue(12))
+ .getSingle();
+ expect(result, 2);
+
+ verify(executor.runSelect(
+ 'SELECT COUNT(*) AS "c0" FROM "todos" WHERE "todos"."id" > ?;',
+ [12]));
+ });
+ });
}
From a9379a85b1e4c4d40a7c2a68e7d599a1c3dbcf4c Mon Sep 17 00:00:00 2001
From: Simon Binder
Date: Sat, 11 Nov 2023 14:49:51 +0100
Subject: [PATCH 29/37] Validate schema in DevTools extension
---
.../lib/src/runtime/api/connection_user.dart | 12 ++
.../runtime/devtools/service_extension.dart | 64 +++++-
drift/lib/src/runtime/devtools/shared.dart | 1 +
drift/pubspec.yaml | 1 +
.../test/integration_tests/devtools/app.dart | 11 ++
.../devtools/devtools_test.dart | 73 +++++++
drift_dev/lib/api/migrations.dart | 15 +-
.../src/services/schema/sqlite_to_drift.dart | 2 +-
.../src/services/schema/verifier_common.dart | 39 ++++
.../src/services/schema/verifier_impl.dart | 27 +--
.../lib/src/db_viewer/database.dart | 5 +-
.../lib/src/details.dart | 5 +
.../lib/src/remote_database.dart | 5 +
.../lib/src/schema_validator.dart | 184 ++++++++++++++++++
.../lib/src/service.dart | 7 +
extras/drift_devtools_extension/pubspec.yaml | 4 +-
.../drift_devtools_extension/web/sqlite3.wasm | 1 +
17 files changed, 413 insertions(+), 43 deletions(-)
create mode 100644 drift/test/integration_tests/devtools/app.dart
create mode 100644 drift/test/integration_tests/devtools/devtools_test.dart
create mode 100644 drift_dev/lib/src/services/schema/verifier_common.dart
create mode 100644 extras/drift_devtools_extension/lib/src/schema_validator.dart
create mode 120000 extras/drift_devtools_extension/web/sqlite3.wasm
diff --git a/drift/lib/src/runtime/api/connection_user.dart b/drift/lib/src/runtime/api/connection_user.dart
index 9d0c562a..66d194b2 100644
--- a/drift/lib/src/runtime/api/connection_user.dart
+++ b/drift/lib/src/runtime/api/connection_user.dart
@@ -606,3 +606,15 @@ extension on TransactionExecutor {
}
}
}
+
+/// Exposes the private `_runConnectionZoned` method for other parts of drift.
+///
+/// This is only used by the DevTools extension.
+@internal
+extension RunWithEngine on DatabaseConnectionUser {
+ /// Call the private [_runConnectionZoned] method.
+ Future runConnectionZoned(
+ DatabaseConnectionUser user, Future Function() calculation) {
+ return _runConnectionZoned(user, calculation);
+ }
+}
diff --git a/drift/lib/src/runtime/devtools/service_extension.dart b/drift/lib/src/runtime/devtools/service_extension.dart
index 1b4d332b..4b1beb11 100644
--- a/drift/lib/src/runtime/devtools/service_extension.dart
+++ b/drift/lib/src/runtime/devtools/service_extension.dart
@@ -2,9 +2,10 @@ import 'dart:async';
import 'dart:convert';
import 'dart:developer';
+import 'package:drift/drift.dart';
import 'package:drift/src/remote/protocol.dart';
+import 'package:drift/src/runtime/executor/transactions.dart';
-import '../query_builder/query_builder.dart';
import 'devtools.dart';
/// A service extension making asynchronous requests on drift databases
@@ -26,7 +27,7 @@ class DriftServiceExtension {
final stream = tracked.database.tableUpdates();
final id = _subscriptionId++;
- stream.listen((event) {
+ _activeSubscriptions[id] = stream.listen((event) {
postEvent('event', {
'subscription': id,
'payload':
@@ -60,6 +61,16 @@ class DriftServiceExtension {
};
return _protocol.encodePayload(result);
+ case 'collect-expected-schema':
+ final executor = _CollectCreateStatements();
+ await tracked.database.runConnectionZoned(
+ BeforeOpenRunner(tracked.database, executor), () async {
+ // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
+ final migrator = tracked.database.createMigrator();
+ await migrator.createAll();
+ });
+
+ return executor.statements;
default:
throw UnsupportedError('Method $action');
}
@@ -96,3 +107,52 @@ class DriftServiceExtension {
static const _protocol = DriftProtocol();
}
+
+class _CollectCreateStatements extends QueryExecutor {
+ final List statements = [];
+
+ @override
+ TransactionExecutor beginTransaction() {
+ throw UnimplementedError();
+ }
+
+ @override
+ SqlDialect get dialect => SqlDialect.sqlite;
+
+ @override
+ Future ensureOpen(QueryExecutorUser user) {
+ return Future.value(true);
+ }
+
+ @override
+ Future runBatched(BatchedStatements statements) {
+ throw UnimplementedError();
+ }
+
+ @override
+ Future runCustom(String statement, [List? args]) {
+ statements.add(statement);
+ return Future.value();
+ }
+
+ @override
+ Future runDelete(String statement, List args) {
+ throw UnimplementedError();
+ }
+
+ @override
+ Future runInsert(String statement, List args) {
+ throw UnimplementedError();
+ }
+
+ @override
+ Future>> runSelect(
+ String statement, List args) {
+ throw UnimplementedError();
+ }
+
+ @override
+ Future runUpdate(String statement, List args) {
+ throw UnimplementedError();
+ }
+}
diff --git a/drift/lib/src/runtime/devtools/shared.dart b/drift/lib/src/runtime/devtools/shared.dart
index c0757cac..029c1ff4 100644
--- a/drift/lib/src/runtime/devtools/shared.dart
+++ b/drift/lib/src/runtime/devtools/shared.dart
@@ -93,6 +93,7 @@ class EntityDescription {
return EntityDescription(
name: entity.entityName,
type: switch (entity) {
+ VirtualTableInfo() => 'virtual_table',
TableInfo() => 'table',
ViewInfo() => 'view',
Index() => 'index',
diff --git a/drift/pubspec.yaml b/drift/pubspec.yaml
index 1280fc96..fa0472b8 100644
--- a/drift/pubspec.yaml
+++ b/drift/pubspec.yaml
@@ -37,3 +37,4 @@ dev_dependencies:
shelf: ^1.3.0
stack_trace: ^1.10.0
test_descriptor: ^2.0.1
+ vm_service: ^13.0.0
diff --git a/drift/test/integration_tests/devtools/app.dart b/drift/test/integration_tests/devtools/app.dart
new file mode 100644
index 00000000..48e5a7c1
--- /dev/null
+++ b/drift/test/integration_tests/devtools/app.dart
@@ -0,0 +1,11 @@
+import 'package:drift/native.dart';
+
+import '../../generated/todos.dart';
+
+void main() {
+ TodoDb(NativeDatabase.memory());
+ print('database created');
+
+ // Keep the process alive
+ Stream.periodic(const Duration(seconds: 10)).listen(null);
+}
diff --git a/drift/test/integration_tests/devtools/devtools_test.dart b/drift/test/integration_tests/devtools/devtools_test.dart
new file mode 100644
index 00000000..b198ce1e
--- /dev/null
+++ b/drift/test/integration_tests/devtools/devtools_test.dart
@@ -0,0 +1,73 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:test/test.dart';
+import 'package:vm_service/vm_service.dart';
+import 'package:path/path.dart' as p;
+import 'package:vm_service/vm_service_io.dart';
+
+void main() {
+ late Process child;
+ late VmService vm;
+ late String isolateId;
+
+ setUpAll(() async {
+ final socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
+ final port = socket.port;
+ await socket.close();
+
+ String sdk = p.dirname(p.dirname(Platform.resolvedExecutable));
+ child = await Process.start(p.join(sdk, 'bin', 'dart'), [
+ 'run',
+ '--enable-vm-service=$port',
+ '--disable-service-auth-codes',
+ '--enable-asserts',
+ 'test/integration_tests/devtools/app.dart',
+ ]);
+
+ final vmServiceListening = Completer();
+ final databaseOpened = Completer();
+
+ child.stdout
+ .map(utf8.decode)
+ .transform(const LineSplitter())
+ .listen((line) {
+ if (line.startsWith('The Dart VM service is listening')) {
+ vmServiceListening.complete();
+ } else if (line == 'database created') {
+ databaseOpened.complete();
+ } else if (!line.startsWith('The Dart DevTools')) {
+ print('[child]: $line');
+ }
+ });
+
+ await vmServiceListening.future;
+ vm = await vmServiceConnectUri('ws://localhost:$port/ws');
+
+ final state = await vm.getVM();
+ isolateId = state.isolates!.single.id!;
+
+ await databaseOpened.future;
+ });
+
+ tearDownAll(() async {
+ child.kill();
+ });
+
+ test('can list create statements', () async {
+ final response = await vm.callServiceExtension(
+ 'ext.drift.database',
+ args: {'action': 'collect-expected-schema', 'db': '0'},
+ isolateId: isolateId,
+ );
+
+ expect(
+ response.json!['r'],
+ containsAll([
+ 'CREATE TABLE IF NOT EXISTS "categories" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "desc" TEXT NOT NULL UNIQUE, "priority" INTEGER NOT NULL DEFAULT 0, "description_in_upper_case" TEXT NOT NULL GENERATED ALWAYS AS (UPPER("desc")) VIRTUAL);',
+ 'CREATE TABLE IF NOT EXISTS "todos" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "title" TEXT NULL, "content" TEXT NOT NULL, "target_date" INTEGER NULL UNIQUE, "category" INTEGER NULL REFERENCES categories (id), "status" TEXT NULL, UNIQUE ("title", "category"), UNIQUE ("title", "target_date"));',
+ 'CREATE TABLE IF NOT EXISTS "shared_todos" ("todo" INTEGER NOT NULL, "user" INTEGER NOT NULL, PRIMARY KEY ("todo", "user"), FOREIGN KEY (todo) REFERENCES todos(id), FOREIGN KEY (user) REFERENCES users(id));'
+ ]));
+ });
+}
diff --git a/drift_dev/lib/api/migrations.dart b/drift_dev/lib/api/migrations.dart
index b11f2555..a09cdf15 100644
--- a/drift_dev/lib/api/migrations.dart
+++ b/drift_dev/lib/api/migrations.dart
@@ -2,10 +2,13 @@ import 'package:drift/drift.dart';
import 'package:drift/internal/migrations.dart';
import 'package:drift/native.dart';
import 'package:drift_dev/src/services/schema/verifier_impl.dart';
+import 'package:drift_dev/src/services/schema/verifier_common.dart';
import 'package:meta/meta.dart';
import 'package:sqlite3/sqlite3.dart';
export 'package:drift/internal/migrations.dart';
+export 'package:drift_dev/src/services/schema/verifier_common.dart'
+ show SchemaMismatch;
abstract class SchemaVerifier {
factory SchemaVerifier(SchemaInstantiationHelper helper) =
@@ -130,18 +133,6 @@ class _GenerateFromScratch extends GeneratedDatabase {
int get schemaVersion => 1;
}
-/// Thrown when the actual schema differs from the expected schema.
-class SchemaMismatch implements Exception {
- final String explanation;
-
- SchemaMismatch(this.explanation);
-
- @override
- String toString() {
- return 'Schema does not match\n$explanation';
- }
-}
-
/// Contains an initialized schema with all tables, views, triggers and indices.
///
/// You can use the [newConnection] for your database class and the
diff --git a/drift_dev/lib/src/services/schema/sqlite_to_drift.dart b/drift_dev/lib/src/services/schema/sqlite_to_drift.dart
index 753a2e8c..b635e82f 100644
--- a/drift_dev/lib/src/services/schema/sqlite_to_drift.dart
+++ b/drift_dev/lib/src/services/schema/sqlite_to_drift.dart
@@ -1,6 +1,5 @@
import 'package:analyzer/dart/element/element.dart';
import 'package:drift_dev/src/analysis/options.dart';
-import 'package:drift_dev/src/services/schema/verifier_impl.dart';
import 'package:logging/logging.dart';
import 'package:sqlite3/common.dart';
import 'package:sqlparser/sqlparser.dart';
@@ -8,6 +7,7 @@ import 'package:sqlparser/sqlparser.dart';
import '../../analysis/backend.dart';
import '../../analysis/driver/driver.dart';
import '../../analysis/results/results.dart';
+import 'verifier_common.dart';
/// Extracts drift elements from the schema of an existing database.
///
diff --git a/drift_dev/lib/src/services/schema/verifier_common.dart b/drift_dev/lib/src/services/schema/verifier_common.dart
new file mode 100644
index 00000000..1e734619
--- /dev/null
+++ b/drift_dev/lib/src/services/schema/verifier_common.dart
@@ -0,0 +1,39 @@
+import 'find_differences.dart';
+
+/// Attempts to recognize whether [name] is likely the name of an internal
+/// sqlite3 table (like `sqlite3_sequence`) that we should not consider when
+/// comparing schemas.
+bool isInternalElement(String name, List virtualTables) {
+ // Skip sqlite-internal tables, https://www.sqlite.org/fileformat2.html#intschema
+ if (name.startsWith('sqlite_')) return true;
+ if (virtualTables.any((v) => name.startsWith('${v}_'))) return true;
+
+ // This file is added on some Android versions when using the native Android
+ // database APIs, https://github.com/simolus3/drift/discussions/2042
+ if (name == 'android_metadata') return true;
+
+ return false;
+}
+
+void verify(List referenceSchema, List actualSchema,
+ bool validateDropped) {
+ final result =
+ FindSchemaDifferences(referenceSchema, actualSchema, validateDropped)
+ .compare();
+
+ if (!result.noChanges) {
+ throw SchemaMismatch(result.describe());
+ }
+}
+
+/// Thrown when the actual schema differs from the expected schema.
+class SchemaMismatch implements Exception {
+ final String explanation;
+
+ SchemaMismatch(this.explanation);
+
+ @override
+ String toString() {
+ return 'Schema does not match\n$explanation';
+ }
+}
diff --git a/drift_dev/lib/src/services/schema/verifier_impl.dart b/drift_dev/lib/src/services/schema/verifier_impl.dart
index 181b0528..ba8b4941 100644
--- a/drift_dev/lib/src/services/schema/verifier_impl.dart
+++ b/drift_dev/lib/src/services/schema/verifier_impl.dart
@@ -6,6 +6,7 @@ import 'package:drift_dev/api/migrations.dart';
import 'package:sqlite3/sqlite3.dart';
import 'find_differences.dart';
+import 'verifier_common.dart';
Expando> expectedSchema = Expando();
@@ -94,21 +95,6 @@ Input? _parseInputFromSchemaRow(
return Input(name, row['sql'] as String);
}
-/// Attempts to recognize whether [name] is likely the name of an internal
-/// sqlite3 table (like `sqlite3_sequence`) that we should not consider when
-/// comparing schemas.
-bool isInternalElement(String name, List virtualTables) {
- // Skip sqlite-internal tables, https://www.sqlite.org/fileformat2.html#intschema
- if (name.startsWith('sqlite_')) return true;
- if (virtualTables.any((v) => name.startsWith('${v}_'))) return true;
-
- // This file is added on some Android versions when using the native Android
- // database APIs, https://github.com/simolus3/drift/discussions/2042
- if (name == 'android_metadata') return true;
-
- return false;
-}
-
extension CollectSchemaDb on DatabaseConnectionUser {
Future> collectSchemaInput(List virtualTables) async {
final result = await customSelect('SELECT * FROM sqlite_master;').get();
@@ -141,17 +127,6 @@ extension CollectSchema on QueryExecutor {
}
}
-void verify(List referenceSchema, List actualSchema,
- bool validateDropped) {
- final result =
- FindSchemaDifferences(referenceSchema, actualSchema, validateDropped)
- .compare();
-
- if (!result.noChanges) {
- throw SchemaMismatch(result.describe());
- }
-}
-
class _DelegatingUser extends QueryExecutorUser {
@override
final int schemaVersion;
diff --git a/extras/drift_devtools_extension/lib/src/db_viewer/database.dart b/extras/drift_devtools_extension/lib/src/db_viewer/database.dart
index 79a6cf57..b579e76e 100644
--- a/extras/drift_devtools_extension/lib/src/db_viewer/database.dart
+++ b/extras/drift_devtools_extension/lib/src/db_viewer/database.dart
@@ -52,7 +52,10 @@ class ViewerDatabase implements DbViewerDatabase {
@override
List get entityNames => [
for (final entity in database.description.entities)
- if (entity.type == 'table') entity.name,
+ if (entity.type == 'table' ||
+ entity.type == 'virtual_table' ||
+ entity.type == 'view')
+ entity.name,
];
@override
diff --git a/extras/drift_devtools_extension/lib/src/details.dart b/extras/drift_devtools_extension/lib/src/details.dart
index 7b0c0ed1..30b6f2fe 100644
--- a/extras/drift_devtools_extension/lib/src/details.dart
+++ b/extras/drift_devtools_extension/lib/src/details.dart
@@ -1,4 +1,5 @@
import 'package:devtools_app_shared/service.dart';
+import 'package:drift_devtools_extension/src/schema_validator.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -55,6 +56,10 @@ class _DatabaseDetailsState extends ConsumerState {
child: ListView(
controller: controller,
children: [
+ const Padding(
+ padding: EdgeInsets.all(8),
+ child: DatabaseSchemaCheck(),
+ ),
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
diff --git a/extras/drift_devtools_extension/lib/src/remote_database.dart b/extras/drift_devtools_extension/lib/src/remote_database.dart
index 38cb904d..5a2fb045 100644
--- a/extras/drift_devtools_extension/lib/src/remote_database.dart
+++ b/extras/drift_devtools_extension/lib/src/remote_database.dart
@@ -71,6 +71,11 @@ class RemoteDatabase {
await _executeQuery(ExecuteQuery(StatementMethod.custom, sql, args));
}
+ Future> get createStatements async {
+ final res = await _driftRequest('collect-expected-schema');
+ return (res as List).cast();
+ }
+
Future _newTableSubscription() async {
final result = await _driftRequest('subscribe-to-tables');
return result as int;
diff --git a/extras/drift_devtools_extension/lib/src/schema_validator.dart b/extras/drift_devtools_extension/lib/src/schema_validator.dart
new file mode 100644
index 00000000..cd46d74f
--- /dev/null
+++ b/extras/drift_devtools_extension/lib/src/schema_validator.dart
@@ -0,0 +1,184 @@
+import 'package:devtools_app_shared/ui.dart';
+// ignore: implementation_imports
+import 'package:drift_dev/src/services/schema/find_differences.dart';
+// ignore: implementation_imports
+import 'package:drift_dev/src/services/schema/verifier_common.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:sqlite3/wasm.dart' hide Row;
+import 'package:url_launcher/url_launcher.dart';
+
+import 'details.dart';
+import 'remote_database.dart';
+import 'service.dart';
+
+sealed class SchemaStatus {}
+
+final class DidNotValidateYet implements SchemaStatus {
+ const DidNotValidateYet();
+}
+
+final class SchemaComparisonResult implements SchemaStatus {
+ final bool schemaValid;
+ final String message;
+
+ SchemaComparisonResult({required this.schemaValid, required this.message});
+}
+
+final schemaStateProvider =
+ AsyncNotifierProvider.autoDispose(
+ SchemaVerifier._);
+
+class SchemaVerifier extends AutoDisposeAsyncNotifier {
+ RemoteDatabase? _database;
+ CommonSqlite3? _sqlite3;
+
+ SchemaVerifier._();
+
+ @override
+ Future build() async {
+ _database = await ref.read(loadedDatabase.future);
+ _sqlite3 = await ref.read(sqliteProvider.future);
+
+ return const DidNotValidateYet();
+ }
+
+ Future validate() async {
+ state = const AsyncLoading();
+ state = await AsyncValue.guard(() async {
+ final database = _database!;
+
+ final virtualTables = database.description.entities
+ .where((e) => e.type == 'virtual_table')
+ .map((e) => e.name)
+ .toList();
+
+ final expected = await _inputFromNewDatabase(virtualTables);
+ final actual = [];
+
+ for (final row in await database
+ .select('SELECT name, sql FROM sqlite_schema;', [])) {
+ final name = row['name'] as String;
+ final sql = row['sql'] as String;
+
+ if (!isInternalElement(name, virtualTables)) {
+ actual.add(Input(name, sql));
+ }
+ }
+
+ try {
+ verify(expected, actual, true);
+ return SchemaComparisonResult(
+ schemaValid: true,
+ message: 'The schema of the database matches its Dart and .drift '
+ 'definitions, meaning that migrations are likely correct.',
+ );
+ } on SchemaMismatch catch (e) {
+ return SchemaComparisonResult(
+ schemaValid: false,
+ message: e.toString(),
+ );
+ }
+ });
+ }
+
+ Future> _inputFromNewDatabase(List virtuals) async {
+ final expectedStatements = await _database!.createStatements;
+ final newDatabase = _sqlite3!.openInMemory();
+ final inputs = [];
+
+ for (var statement in expectedStatements) {
+ newDatabase.execute(statement);
+ }
+
+ for (final row
+ in newDatabase.select('SELECT name, sql FROM sqlite_schema;', [])) {
+ final name = row['name'] as String;
+ final sql = row['sql'] as String;
+
+ if (!isInternalElement(name, virtuals)) {
+ inputs.add(Input(name, sql));
+ }
+ }
+
+ newDatabase.dispose();
+ return inputs;
+ }
+}
+
+class DatabaseSchemaCheck extends ConsumerWidget {
+ const DatabaseSchemaCheck({super.key});
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final state = ref.watch(schemaStateProvider);
+
+ final description = switch (state) {
+ AsyncData(
+ value: SchemaComparisonResult(schemaValid: true, :var message)
+ ) =>
+ Text.rich(TextSpan(
+ children: [
+ const TextSpan(
+ text: 'Success! ', style: TextStyle(color: Colors.green)),
+ TextSpan(text: message),
+ ],
+ )),
+ AsyncData(
+ value: SchemaComparisonResult(schemaValid: false, :var message)
+ ) =>
+ Text.rich(TextSpan(
+ children: [
+ const TextSpan(
+ text: 'Mismatch detected! ',
+ style: TextStyle(color: Colors.red)),
+ TextSpan(text: message),
+ ],
+ )),
+ AsyncError(:var error) =>
+ Text('The schema could not be validated due to an error: $error'),
+ _ => Text.rich(TextSpan(
+ text: 'By validating your schema, you can ensure that the current '
+ 'state of the database in your app (after migrations ran) '
+ 'matches the expected state of tables as defined in your sources. ',
+ children: [
+ TextSpan(
+ text: 'Learn more',
+ style: const TextStyle(
+ decoration: TextDecoration.underline,
+ ),
+ recognizer: TapGestureRecognizer()
+ ..onTap = () async {
+ await launchUrl(Uri.parse(
+ 'https://drift.simonbinder.eu/docs/migrations/#verifying-a-database-schema-at-runtime'));
+ },
+ ),
+ ],
+ )),
+ };
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(bottom: 8),
+ child: description,
+ ),
+ DevToolsButton(
+ label: switch (state) {
+ AsyncError() ||
+ AsyncData(value: SchemaComparisonResult()) =>
+ 'Validate again',
+ _ => 'Validate schema',
+ },
+ onPressed: () {
+ if (state is! AsyncLoading) {
+ ref.read(schemaStateProvider.notifier).validate();
+ }
+ },
+ )
+ ],
+ );
+ }
+}
diff --git a/extras/drift_devtools_extension/lib/src/service.dart b/extras/drift_devtools_extension/lib/src/service.dart
index b63f07c9..9bbb5c3c 100644
--- a/extras/drift_devtools_extension/lib/src/service.dart
+++ b/extras/drift_devtools_extension/lib/src/service.dart
@@ -5,6 +5,7 @@ import 'package:devtools_extensions/devtools_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:rxdart/transformers.dart';
+import 'package:sqlite3/wasm.dart';
import 'package:vm_service/vm_service.dart';
final _serviceConnection = StreamController.broadcast();
@@ -48,3 +49,9 @@ final hotRestartEventProvider =
return notifier;
});
+
+final sqliteProvider = FutureProvider((ref) async {
+ final sqlite = await WasmSqlite3.loadFromUrl(Uri.parse('sqlite3.wasm'));
+ sqlite.registerVirtualFileSystem(InMemoryFileSystem(), makeDefault: true);
+ return sqlite;
+});
diff --git a/extras/drift_devtools_extension/pubspec.yaml b/extras/drift_devtools_extension/pubspec.yaml
index c90f11ed..5b23e967 100644
--- a/extras/drift_devtools_extension/pubspec.yaml
+++ b/extras/drift_devtools_extension/pubspec.yaml
@@ -15,12 +15,14 @@ dependencies:
devtools_app_shared: '>=0.0.5 <0.0.6' # 0.0.6 requires unstable Flutter
db_viewer: ^1.0.3
rxdart: ^0.27.7
- flutter_riverpod: ^2.4.4
+ flutter_riverpod: ^3.0.0-dev.0
vm_service: ^11.10.0
path: ^1.8.3
drift: ^2.12.1
logging: ^1.2.0
url_launcher: ^6.1.14
+ drift_dev: ^2.13.1
+ sqlite3: ^2.1.0
dev_dependencies:
flutter_test:
diff --git a/extras/drift_devtools_extension/web/sqlite3.wasm b/extras/drift_devtools_extension/web/sqlite3.wasm
new file mode 120000
index 00000000..de4098cb
--- /dev/null
+++ b/extras/drift_devtools_extension/web/sqlite3.wasm
@@ -0,0 +1 @@
+../../assets/sqlite3.wasm
\ No newline at end of file
From 350726417d2fa5607f8401714f5cf42d96a78d64 Mon Sep 17 00:00:00 2001
From: Simon Binder
Date: Sat, 11 Nov 2023 21:14:48 +0100
Subject: [PATCH 30/37] Hide internal member
---
drift/lib/drift.dart | 2 +-
drift/lib/src/runtime/devtools/service_extension.dart | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/drift/lib/drift.dart b/drift/lib/drift.dart
index 4843c12f..6a6b06d2 100644
--- a/drift/lib/drift.dart
+++ b/drift/lib/drift.dart
@@ -8,7 +8,7 @@ export 'dart:typed_data' show Uint8List;
export 'src/dsl/dsl.dart';
export 'src/runtime/api/options.dart';
-export 'src/runtime/api/runtime_api.dart';
+export 'src/runtime/api/runtime_api.dart' hide RunWithEngine;
export 'src/runtime/custom_result_set.dart';
export 'src/runtime/data_class.dart';
export 'src/runtime/data_verification.dart';
diff --git a/drift/lib/src/runtime/devtools/service_extension.dart b/drift/lib/src/runtime/devtools/service_extension.dart
index 4b1beb11..b6ae4bb4 100644
--- a/drift/lib/src/runtime/devtools/service_extension.dart
+++ b/drift/lib/src/runtime/devtools/service_extension.dart
@@ -6,6 +6,7 @@ import 'package:drift/drift.dart';
import 'package:drift/src/remote/protocol.dart';
import 'package:drift/src/runtime/executor/transactions.dart';
+import '../api/runtime_api.dart';
import 'devtools.dart';
/// A service extension making asynchronous requests on drift databases
From fd260edaa3459a3e01188501786e1aebd442fcd1 Mon Sep 17 00:00:00 2001
From: Simon Binder
Date: Sat, 11 Nov 2023 21:23:53 +0100
Subject: [PATCH 31/37] Fix order of arguments when reading custom types
---
drift/test/generated/converter.dart | 27 +++++++++++++++++++
drift/test/generated/custom_tables.g.dart | 8 +++---
drift/test/generated/tables.drift | 2 +-
.../integration_tests/drift_files_test.dart | 2 +-
drift_dev/CHANGELOG.md | 4 +++
.../lib/src/writer/queries/query_writer.dart | 2 +-
drift_dev/pubspec.yaml | 2 +-
7 files changed, 39 insertions(+), 8 deletions(-)
diff --git a/drift/test/generated/converter.dart b/drift/test/generated/converter.dart
index a864e9ec..4ab9c425 100644
--- a/drift/test/generated/converter.dart
+++ b/drift/test/generated/converter.dart
@@ -1,5 +1,32 @@
import 'package:drift/drift.dart';
+class CustomTextType implements CustomSqlType {
+ const CustomTextType();
+
+ @override
+ String mapToSqlLiteral(String dartValue) {
+ final escapedChars = dartValue.replaceAll('\'', '\'\'');
+ return "'$escapedChars'";
+ }
+
+ @override
+ Object mapToSqlParameter(String dartValue) {
+ return dartValue;
+ }
+
+ @override
+ String read(Object fromSql) {
+ return fromSql.toString();
+ }
+
+ @override
+ String sqlTypeName(GenerationContext context) {
+ // Still has text column affinity, but can be used to verify that the type
+ // really is used.
+ return 'MY_TEXT';
+ }
+}
+
enum SyncType {
locallyCreated,
locallyUpdated,
diff --git a/drift/test/generated/custom_tables.g.dart b/drift/test/generated/custom_tables.g.dart
index a6018c24..281de529 100644
--- a/drift/test/generated/custom_tables.g.dart
+++ b/drift/test/generated/custom_tables.g.dart
@@ -106,7 +106,7 @@ class WithDefaults extends Table with TableInfo {
static const VerificationMeta _aMeta = const VerificationMeta('a');
late final GeneratedColumn a = GeneratedColumn(
'a', aliasedName, true,
- type: DriftSqlType.string,
+ type: const CustomTextType(),
requiredDuringInsert: false,
$customConstraints: 'DEFAULT \'something\'',
defaultValue: const CustomExpression('\'something\''));
@@ -144,7 +144,7 @@ class WithDefaults extends Table with TableInfo {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return WithDefault(
a: attachedDatabase.typeMapping
- .read(DriftSqlType.string, data['${effectivePrefix}a']),
+ .read(const CustomTextType(), data['${effectivePrefix}a']),
b: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}b']),
);
@@ -267,7 +267,7 @@ class WithDefaultsCompanion extends UpdateCompanion {
Map toColumns(bool nullToAbsent) {
final map = {};
if (a.present) {
- map['a'] = Variable(a.value);
+ map['a'] = Variable(a.value, const CustomTextType());
}
if (b.present) {
map['b'] = Variable(b.value);
@@ -1801,7 +1801,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
...generatedpredicate.watchedTables,
}).asyncMap((QueryRow row) async => MultipleResult(
row: row,
- a: row.readNullable('a'),
+ a: row.readNullableWithType(const CustomTextType(), 'a'),
b: row.readNullable('b'),
c: await withConstraints.mapFromRowOrNull(row,
tablePrefix: 'nested_0'),
diff --git a/drift/test/generated/tables.drift b/drift/test/generated/tables.drift
index 36e7dc1b..f5258057 100644
--- a/drift/test/generated/tables.drift
+++ b/drift/test/generated/tables.drift
@@ -6,7 +6,7 @@ CREATE TABLE no_ids (
) WITHOUT ROWID WITH NoIdRow;
CREATE TABLE with_defaults (
- a TEXT JSON KEY customJsonName DEFAULT 'something',
+ a `const CustomTextType()` JSON KEY customJsonName DEFAULT 'something',
b INT UNIQUE
);
diff --git a/drift/test/integration_tests/drift_files_test.dart b/drift/test/integration_tests/drift_files_test.dart
index fe841632..0b0f2baa 100644
--- a/drift/test/integration_tests/drift_files_test.dart
+++ b/drift/test/integration_tests/drift_files_test.dart
@@ -11,7 +11,7 @@ const _createNoIds =
'WITHOUT ROWID;';
const _createWithDefaults = 'CREATE TABLE IF NOT EXISTS "with_defaults" ('
- "\"a\" TEXT DEFAULT 'something', \"b\" INTEGER UNIQUE);";
+ "\"a\" MY_TEXT DEFAULT 'something', \"b\" INTEGER UNIQUE);";
const _createWithConstraints = 'CREATE TABLE IF NOT EXISTS "with_constraints" ('
'"a" TEXT, "b" INTEGER NOT NULL, "c" REAL, '
diff --git a/drift_dev/CHANGELOG.md b/drift_dev/CHANGELOG.md
index 85403030..cf0cd861 100644
--- a/drift_dev/CHANGELOG.md
+++ b/drift_dev/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.13.2
+
+- Fix generated queries relying on custom types.
+
## 2.13.1
- Add `has_separate_analyzer` option to optimize builds using the `not_shared` builder.
diff --git a/drift_dev/lib/src/writer/queries/query_writer.dart b/drift_dev/lib/src/writer/queries/query_writer.dart
index b150576a..e370c41d 100644
--- a/drift_dev/lib/src/writer/queries/query_writer.dart
+++ b/drift_dev/lib/src/writer/queries/query_writer.dart
@@ -210,7 +210,7 @@ class QueryWriter {
if (column.sqlType.isCustom) {
final method = isNullable ? 'readNullableWithType' : 'readWithType';
final typeImpl = _emitter.dartCode(column.sqlType.custom!.expression);
- code = 'row.$method<$rawDartType>($dartLiteral, $typeImpl)';
+ code = 'row.$method<$rawDartType>($typeImpl, $dartLiteral)';
} else {
final method = isNullable ? 'readNullable' : 'read';
code = 'row.$method<$rawDartType>($dartLiteral)';
diff --git a/drift_dev/pubspec.yaml b/drift_dev/pubspec.yaml
index bc9f9d98..698fb83b 100644
--- a/drift_dev/pubspec.yaml
+++ b/drift_dev/pubspec.yaml
@@ -1,6 +1,6 @@
name: drift_dev
description: Dev-dependency for users of drift. Contains the generator and development tools.
-version: 2.13.1
+version: 2.13.2
repository: https://github.com/simolus3/drift
homepage: https://drift.simonbinder.eu/
issue_tracker: https://github.com/simolus3/drift/issues
From e19de7ce0a74edbede1e074ffbe9065a8e218953 Mon Sep 17 00:00:00 2001
From: Simon Binder
Date: Wed, 15 Nov 2023 11:35:45 +0100
Subject: [PATCH 32/37] Update to postgres 3.0.0
---
extras/drift_postgres/CHANGELOG.md | 4 +++-
extras/drift_postgres/README.md | 5 -----
extras/drift_postgres/lib/drift_postgres.dart | 2 +-
extras/drift_postgres/lib/src/types.dart | 10 +++++-----
extras/drift_postgres/pubspec.yaml | 8 ++++----
extras/drift_postgres/test/types_test.dart | 6 +++++-
6 files changed, 18 insertions(+), 17 deletions(-)
diff --git a/extras/drift_postgres/CHANGELOG.md b/extras/drift_postgres/CHANGELOG.md
index 8306822a..befd027c 100644
--- a/extras/drift_postgres/CHANGELOG.md
+++ b/extras/drift_postgres/CHANGELOG.md
@@ -1,5 +1,7 @@
-## 0.2.0
+## 1.0.0
+- __Breaking__: The interval type now expects `Interval` types from postgres
+ instead of `Duration` objects.
- Migrate to the stable v3 version of the `postgres` package.
## 0.1.0
diff --git a/extras/drift_postgres/README.md b/extras/drift_postgres/README.md
index ac691bc0..90f12403 100644
--- a/extras/drift_postgres/README.md
+++ b/extras/drift_postgres/README.md
@@ -1,11 +1,6 @@
`package:drift_postgres` extends [drift](https://drift.simonbinder.eu/) to support
talking to PostgreSQL databases by using the `postgres` package.
-This package is currently in alpha. It uses preview APIs from the `postgres` packages,
-which may require this package to be updated if there are breaking changes in that
-package. Once these APIs from `postgres` are stabilized, a stable version of `drift_postgres`
-will be released as well.
-
## Using this
For general notes on using drift, see [this guide](https://drift.simonbinder.eu/getting-started/).
diff --git a/extras/drift_postgres/lib/drift_postgres.dart b/extras/drift_postgres/lib/drift_postgres.dart
index e2dee938..a052c786 100644
--- a/extras/drift_postgres/lib/drift_postgres.dart
+++ b/extras/drift_postgres/lib/drift_postgres.dart
@@ -40,7 +40,7 @@ final class PgTypes {
static const CustomSqlType uuid = UuidType();
/// The `interval` type in Postgres.
- static const CustomSqlType interval = IntervalType();
+ static const CustomSqlType interval = IntervalType();
/// The `date` type in Postgres.
static const CustomSqlType date = DateType(
diff --git a/extras/drift_postgres/lib/src/types.dart b/extras/drift_postgres/lib/src/types.dart
index 304d6f1c..36ee1e20 100644
--- a/extras/drift_postgres/lib/src/types.dart
+++ b/extras/drift_postgres/lib/src/types.dart
@@ -1,7 +1,7 @@
import 'package:drift/drift.dart';
import 'package:postgres/postgres.dart';
// ignore: implementation_imports
-import 'package:postgres/src/text_codec.dart';
+import 'package:postgres/src/types/text_codec.dart';
import 'package:uuid/uuid.dart';
class PostgresType implements CustomSqlType {
@@ -43,7 +43,7 @@ class UuidType extends PostgresType {
@override
UuidValue read(Object fromSql) {
- return UuidValue(fromSql as String);
+ return UuidValue.fromString(fromSql as String);
}
}
@@ -57,12 +57,12 @@ class PointType extends PostgresType {
}
}
-class IntervalType extends PostgresType {
+class IntervalType extends PostgresType {
const IntervalType() : super(type: Type.interval, name: 'interval');
@override
- String mapToSqlLiteral(Duration dartValue) {
- return "'${dartValue.inMicroseconds} microseconds'::interval";
+ String mapToSqlLiteral(Interval dartValue) {
+ return "'$dartValue'::interval";
}
}
diff --git a/extras/drift_postgres/pubspec.yaml b/extras/drift_postgres/pubspec.yaml
index a5241a3e..e94a3727 100644
--- a/extras/drift_postgres/pubspec.yaml
+++ b/extras/drift_postgres/pubspec.yaml
@@ -1,6 +1,6 @@
name: drift_postgres
description: Postgres implementation and APIs for the drift database package.
-version: 0.2.0-dev
+version: 1.0.0
repository: https://github.com/simolus3/drift
homepage: https://drift.simonbinder.eu/docs/platforms/postgres/
issue_tracker: https://github.com/simolus3/drift/issues
@@ -11,12 +11,12 @@ environment:
dependencies:
collection: ^1.16.0
drift: ^2.0.0
- postgres: ^3.0.0-beta.1
+ postgres: ^3.0.0
meta: ^1.8.0
- uuid: ^4.1.0
+ uuid: ^4.2.0
dev_dependencies:
- lints: ^2.0.0
+ lints: ^3.0.0
test: ^1.18.0
drift_dev:
drift_testcases:
diff --git a/extras/drift_postgres/test/types_test.dart b/extras/drift_postgres/test/types_test.dart
index 3693a75b..79ac7189 100644
--- a/extras/drift_postgres/test/types_test.dart
+++ b/extras/drift_postgres/test/types_test.dart
@@ -1,6 +1,7 @@
import 'package:drift/drift.dart';
import 'package:drift_postgres/drift_postgres.dart';
import 'package:postgres/postgres.dart' as pg;
+import 'package:postgres/postgres.dart';
import 'package:test/test.dart';
import 'package:uuid/uuid.dart';
@@ -39,7 +40,10 @@ void main() {
}
group('uuid', () => testWith(PgTypes.uuid, Uuid().v4obj()));
- group('interval', () => testWith(PgTypes.interval, Duration(seconds: 15)));
+ group(
+ 'interval',
+ () => testWith(PgTypes.interval, Interval(months: 2, microseconds: 1234)),
+ );
group('json', () => testWith(PgTypes.json, {'foo': 'bar'}));
group('jsonb', () => testWith(PgTypes.jsonb, {'foo': 'bar'}));
group('point', () => testWith(PgTypes.point, pg.Point(90, -90)));
From 2ac81ddb99e6ef521925295e3d891a1698be2bda Mon Sep 17 00:00:00 2001
From: Simon Binder
Date: Wed, 15 Nov 2023 17:15:50 +0100
Subject: [PATCH 33/37] Remove beta label from postgres docs
---
docs/pages/docs/Platforms/postgres.md | 23 +++++++++++++----------
1 file changed, 13 insertions(+), 10 deletions(-)
diff --git a/docs/pages/docs/Platforms/postgres.md b/docs/pages/docs/Platforms/postgres.md
index dd411087..de10c243 100644
--- a/docs/pages/docs/Platforms/postgres.md
+++ b/docs/pages/docs/Platforms/postgres.md
@@ -1,14 +1,16 @@
---
data:
- title: PostgreSQL support (Beta)
+ title: PostgreSQL support
description: Use drift with PostgreSQL database servers.
weight: 10
template: layouts/docs/single
---
-Thanks to contributions from the community, drift currently has alpha support for postgres with the `drift_postgres` package.
-Without having to change your query code, drift can generate Postgres-compatible SQL for most queries,
-allowing you to use your drift databases with a Postgres database server.
+While drift has originally been designed as a client-side database wrapper for SQLite databases, it can also be used
+with PostgreSQL database servers.
+Without having to change your query code, drift can generate Postgres-compatible SQL for most queries.
+Please keep in mind that some drift APIs, like those for date time modification, are only supported with SQLite.
+Most queries will work without any modification though.
## Setup
@@ -63,11 +65,12 @@ disable migrations in postgres by passing `enableMigrations: false` to the `PgDa
## Current state
-Drift's support for Postgres is still in development, and the integration tests we have for Postgres are
-less extensive than the tests for sqlite3 databases.
-Also, some parts of the core APIs (like the datetime expressions API) are direct wrappers around SQL
-functions only available in sqlite3 and won't work in Postgres.
-However, you can already create tables (or use an existing schema) and most queries should work already.
+Drift's support for PostgreSQL is stable in the sense that the current API is unlikely to break.
+Still, it is a newer implementation and integration tests for PostgreSQL are less extensive than
+the tests for SQLite databases. And while drift offers typed wrappers around most functions supported
+by SQLite, only a tiny subset of PostgreSQL's advanced operators and functions are exposed by
+`drift_postgres`.
If you're running into problems or bugs with the postgres database, please let us know by creating an issue
-or a discussion.
\ No newline at end of file
+or a discussion.
+Contributions expanding wrappers around PosgreSQL functions are also much appreciated.
From 407a40fae1ec5ccb261e62fa215ef405724274ee Mon Sep 17 00:00:00 2001
From: Simon Binder
Date: Thu, 16 Nov 2023 21:16:51 +0100
Subject: [PATCH 34/37] Add example with upsert conflict target to docs
---
docs/lib/snippets/modular/upserts.dart | 54 +++
docs/lib/snippets/modular/upserts.drift.dart | 446 +++++++++++++++++++
docs/pages/docs/Dart API/writes.md | 34 +-
3 files changed, 512 insertions(+), 22 deletions(-)
create mode 100644 docs/lib/snippets/modular/upserts.dart
create mode 100644 docs/lib/snippets/modular/upserts.drift.dart
diff --git a/docs/lib/snippets/modular/upserts.dart b/docs/lib/snippets/modular/upserts.dart
new file mode 100644
index 00000000..3a407a57
--- /dev/null
+++ b/docs/lib/snippets/modular/upserts.dart
@@ -0,0 +1,54 @@
+import 'package:drift/drift.dart';
+import 'package:drift/internal/modular.dart';
+
+import 'upserts.drift.dart';
+
+// #docregion words-table
+class Words extends Table {
+ TextColumn get word => text()();
+ IntColumn get usages => integer().withDefault(const Constant(1))();
+
+ @override
+ Set get primaryKey => {word};
+}
+// #enddocregion words-table
+
+// #docregion upsert-target
+class MatchResults extends Table {
+ IntColumn get id => integer().autoIncrement()();
+ TextColumn get teamA => text()();
+ TextColumn get teamB => text()();
+ BoolColumn get teamAWon => boolean()();
+
+ @override
+ List>>? get uniqueKeys => [
+ {teamA, teamB}
+ ];
+}
+// #enddocregion upsert-target
+
+extension DocumentationSnippets on ModularAccessor {
+ $WordsTable get words => throw 'stub';
+ $MatchResultsTable get matches => throw 'stub';
+
+ // #docregion track-word
+ Future trackWord(String word) {
+ return into(words).insert(
+ WordsCompanion.insert(word: word),
+ onConflict: DoUpdate(
+ (old) => WordsCompanion.custom(usages: old.usages + Constant(1))),
+ );
+ }
+ // #enddocregion track-word
+
+ // #docregion upsert-target
+ Future insertMatch(String teamA, String teamB, bool teamAWon) {
+ final data = MatchResultsCompanion.insert(
+ teamA: teamA, teamB: teamB, teamAWon: teamAWon);
+
+ return into(matches).insert(data,
+ onConflict:
+ DoUpdate((old) => data, target: [matches.teamA, matches.teamB]));
+ }
+ // #enddocregion upsert-target
+}
diff --git a/docs/lib/snippets/modular/upserts.drift.dart b/docs/lib/snippets/modular/upserts.drift.dart
new file mode 100644
index 00000000..977b6e89
--- /dev/null
+++ b/docs/lib/snippets/modular/upserts.drift.dart
@@ -0,0 +1,446 @@
+// ignore_for_file: type=lint
+import 'package:drift/drift.dart' as i0;
+import 'package:drift_docs/snippets/modular/upserts.drift.dart' as i1;
+import 'package:drift_docs/snippets/modular/upserts.dart' as i2;
+import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
+
+class $WordsTable extends i2.Words with i0.TableInfo<$WordsTable, i1.Word> {
+ @override
+ final i0.GeneratedDatabase attachedDatabase;
+ final String? _alias;
+ $WordsTable(this.attachedDatabase, [this._alias]);
+ static const i0.VerificationMeta _wordMeta =
+ const i0.VerificationMeta('word');
+ @override
+ late final i0.GeneratedColumn word = i0.GeneratedColumn(
+ 'word', aliasedName, false,
+ type: i0.DriftSqlType.string, requiredDuringInsert: true);
+ static const i0.VerificationMeta _usagesMeta =
+ const i0.VerificationMeta('usages');
+ @override
+ late final i0.GeneratedColumn usages = i0.GeneratedColumn(
+ 'usages', aliasedName, false,
+ type: i0.DriftSqlType.int,
+ requiredDuringInsert: false,
+ defaultValue: const i3.Constant(1));
+ @override
+ List get $columns => [word, usages];
+ @override
+ String get aliasedName => _alias ?? actualTableName;
+ @override
+ String get actualTableName => $name;
+ static const String $name = 'words';
+ @override
+ i0.VerificationContext validateIntegrity(i0.Insertable instance,
+ {bool isInserting = false}) {
+ final context = i0.VerificationContext();
+ final data = instance.toColumns(true);
+ if (data.containsKey('word')) {
+ context.handle(
+ _wordMeta, word.isAcceptableOrUnknown(data['word']!, _wordMeta));
+ } else if (isInserting) {
+ context.missing(_wordMeta);
+ }
+ if (data.containsKey('usages')) {
+ context.handle(_usagesMeta,
+ usages.isAcceptableOrUnknown(data['usages']!, _usagesMeta));
+ }
+ return context;
+ }
+
+ @override
+ Set get $primaryKey => {word};
+ @override
+ i1.Word map(Map data, {String? tablePrefix}) {
+ final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
+ return i1.Word(
+ word: attachedDatabase.typeMapping
+ .read(i0.DriftSqlType.string, data['${effectivePrefix}word'])!,
+ usages: attachedDatabase.typeMapping
+ .read(i0.DriftSqlType.int, data['${effectivePrefix}usages'])!,
+ );
+ }
+
+ @override
+ $WordsTable createAlias(String alias) {
+ return $WordsTable(attachedDatabase, alias);
+ }
+}
+
+class Word extends i0.DataClass implements i0.Insertable {
+ final String word;
+ final int usages;
+ const Word({required this.word, required this.usages});
+ @override
+ Map toColumns(bool nullToAbsent) {
+ final map = {};
+ map['word'] = i0.Variable(word);
+ map['usages'] = i0.Variable(usages);
+ return map;
+ }
+
+ i1.WordsCompanion toCompanion(bool nullToAbsent) {
+ return i1.WordsCompanion(
+ word: i0.Value(word),
+ usages: i0.Value(usages),
+ );
+ }
+
+ factory Word.fromJson(Map json,
+ {i0.ValueSerializer? serializer}) {
+ serializer ??= i0.driftRuntimeOptions.defaultSerializer;
+ return Word(
+ word: serializer.fromJson(json['word']),
+ usages: serializer.fromJson(json['usages']),
+ );
+ }
+ @override
+ Map toJson({i0.ValueSerializer? serializer}) {
+ serializer ??= i0.driftRuntimeOptions.defaultSerializer;
+ return {
+ 'word': serializer.toJson(word),
+ 'usages': serializer.toJson(usages),
+ };
+ }
+
+ i1.Word copyWith({String? word, int? usages}) => i1.Word(
+ word: word ?? this.word,
+ usages: usages ?? this.usages,
+ );
+ @override
+ String toString() {
+ return (StringBuffer('Word(')
+ ..write('word: $word, ')
+ ..write('usages: $usages')
+ ..write(')'))
+ .toString();
+ }
+
+ @override
+ int get hashCode => Object.hash(word, usages);
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ (other is i1.Word &&
+ other.word == this.word &&
+ other.usages == this.usages);
+}
+
+class WordsCompanion extends i0.UpdateCompanion {
+ final i0.Value word;
+ final i0.Value usages;
+ final i0.Value rowid;
+ const WordsCompanion({
+ this.word = const i0.Value.absent(),
+ this.usages = const i0.Value.absent(),
+ this.rowid = const i0.Value.absent(),
+ });
+ WordsCompanion.insert({
+ required String word,
+ this.usages = const i0.Value.absent(),
+ this.rowid = const i0.Value.absent(),
+ }) : word = i0.Value(word);
+ static i0.Insertable custom({
+ i0.Expression? word,
+ i0.Expression? usages,
+ i0.Expression? rowid,
+ }) {
+ return i0.RawValuesInsertable({
+ if (word != null) 'word': word,
+ if (usages != null) 'usages': usages,
+ if (rowid != null) 'rowid': rowid,
+ });
+ }
+
+ i1.WordsCompanion copyWith(
+ {i0.Value? word, i0.Value? usages, i0.Value? rowid}) {
+ return i1.WordsCompanion(
+ word: word ?? this.word,
+ usages: usages ?? this.usages,
+ rowid: rowid ?? this.rowid,
+ );
+ }
+
+ @override
+ Map toColumns(bool nullToAbsent) {
+ final map = {};
+ if (word.present) {
+ map['word'] = i0.Variable(word.value);
+ }
+ if (usages.present) {
+ map['usages'] = i0.Variable(usages.value);
+ }
+ if (rowid.present) {
+ map['rowid'] = i0.Variable(rowid.value);
+ }
+ return map;
+ }
+
+ @override
+ String toString() {
+ return (StringBuffer('WordsCompanion(')
+ ..write('word: $word, ')
+ ..write('usages: $usages, ')
+ ..write('rowid: $rowid')
+ ..write(')'))
+ .toString();
+ }
+}
+
+class $MatchResultsTable extends i2.MatchResults
+ with i0.TableInfo<$MatchResultsTable, i1.MatchResult> {
+ @override
+ final i0.GeneratedDatabase attachedDatabase;
+ final String? _alias;
+ $MatchResultsTable(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 _teamAMeta =
+ const i0.VerificationMeta('teamA');
+ @override
+ late final i0.GeneratedColumn teamA = i0.GeneratedColumn(
+ 'team_a', aliasedName, false,
+ type: i0.DriftSqlType.string, requiredDuringInsert: true);
+ static const i0.VerificationMeta _teamBMeta =
+ const i0.VerificationMeta('teamB');
+ @override
+ late final i0.GeneratedColumn teamB = i0.GeneratedColumn(
+ 'team_b', aliasedName, false,
+ type: i0.DriftSqlType.string, requiredDuringInsert: true);
+ static const i0.VerificationMeta _teamAWonMeta =
+ const i0.VerificationMeta('teamAWon');
+ @override
+ late final i0.GeneratedColumn teamAWon = i0.GeneratedColumn(
+ 'team_a_won', aliasedName, false,
+ type: i0.DriftSqlType.bool,
+ requiredDuringInsert: true,
+ defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
+ 'CHECK ("team_a_won" IN (0, 1))'));
+ @override
+ List get $columns => [id, teamA, teamB, teamAWon];
+ @override
+ String get aliasedName => _alias ?? actualTableName;
+ @override
+ String get actualTableName => $name;
+ static const String $name = 'match_results';
+ @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('team_a')) {
+ context.handle(
+ _teamAMeta, teamA.isAcceptableOrUnknown(data['team_a']!, _teamAMeta));
+ } else if (isInserting) {
+ context.missing(_teamAMeta);
+ }
+ if (data.containsKey('team_b')) {
+ context.handle(
+ _teamBMeta, teamB.isAcceptableOrUnknown(data['team_b']!, _teamBMeta));
+ } else if (isInserting) {
+ context.missing(_teamBMeta);
+ }
+ if (data.containsKey('team_a_won')) {
+ context.handle(_teamAWonMeta,
+ teamAWon.isAcceptableOrUnknown(data['team_a_won']!, _teamAWonMeta));
+ } else if (isInserting) {
+ context.missing(_teamAWonMeta);
+ }
+ return context;
+ }
+
+ @override
+ Set get $primaryKey => {id};
+ @override
+ List> get uniqueKeys => [
+ {teamA, teamB},
+ ];
+ @override
+ i1.MatchResult map(Map data, {String? tablePrefix}) {
+ final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
+ return i1.MatchResult(
+ id: attachedDatabase.typeMapping
+ .read(i0.DriftSqlType.int, data['${effectivePrefix}id'])!,
+ teamA: attachedDatabase.typeMapping
+ .read(i0.DriftSqlType.string, data['${effectivePrefix}team_a'])!,
+ teamB: attachedDatabase.typeMapping
+ .read(i0.DriftSqlType.string, data['${effectivePrefix}team_b'])!,
+ teamAWon: attachedDatabase.typeMapping
+ .read(i0.DriftSqlType.bool, data['${effectivePrefix}team_a_won'])!,
+ );
+ }
+
+ @override
+ $MatchResultsTable createAlias(String alias) {
+ return $MatchResultsTable(attachedDatabase, alias);
+ }
+}
+
+class MatchResult extends i0.DataClass
+ implements i0.Insertable {
+ final int id;
+ final String teamA;
+ final String teamB;
+ final bool teamAWon;
+ const MatchResult(
+ {required this.id,
+ required this.teamA,
+ required this.teamB,
+ required this.teamAWon});
+ @override
+ Map toColumns(bool nullToAbsent) {
+ final map = {};
+ map['id'] = i0.Variable(id);
+ map['team_a'] = i0.Variable(teamA);
+ map['team_b'] = i0.Variable(teamB);
+ map['team_a_won'] = i0.Variable(teamAWon);
+ return map;
+ }
+
+ i1.MatchResultsCompanion toCompanion(bool nullToAbsent) {
+ return i1.MatchResultsCompanion(
+ id: i0.Value(id),
+ teamA: i0.Value(teamA),
+ teamB: i0.Value(teamB),
+ teamAWon: i0.Value(teamAWon),
+ );
+ }
+
+ factory MatchResult.fromJson(Map json,
+ {i0.ValueSerializer? serializer}) {
+ serializer ??= i0.driftRuntimeOptions.defaultSerializer;
+ return MatchResult(
+ id: serializer.fromJson(json['id']),
+ teamA: serializer.fromJson(json['teamA']),
+ teamB: serializer.fromJson(json['teamB']),
+ teamAWon: serializer.fromJson(json['teamAWon']),
+ );
+ }
+ @override
+ Map toJson({i0.ValueSerializer? serializer}) {
+ serializer ??= i0.driftRuntimeOptions.defaultSerializer;
+ return {
+ 'id': serializer.toJson(id),
+ 'teamA': serializer.toJson(teamA),
+ 'teamB': serializer.toJson(teamB),
+ 'teamAWon': serializer.toJson(teamAWon),
+ };
+ }
+
+ i1.MatchResult copyWith(
+ {int? id, String? teamA, String? teamB, bool? teamAWon}) =>
+ i1.MatchResult(
+ id: id ?? this.id,
+ teamA: teamA ?? this.teamA,
+ teamB: teamB ?? this.teamB,
+ teamAWon: teamAWon ?? this.teamAWon,
+ );
+ @override
+ String toString() {
+ return (StringBuffer('MatchResult(')
+ ..write('id: $id, ')
+ ..write('teamA: $teamA, ')
+ ..write('teamB: $teamB, ')
+ ..write('teamAWon: $teamAWon')
+ ..write(')'))
+ .toString();
+ }
+
+ @override
+ int get hashCode => Object.hash(id, teamA, teamB, teamAWon);
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ (other is i1.MatchResult &&
+ other.id == this.id &&
+ other.teamA == this.teamA &&
+ other.teamB == this.teamB &&
+ other.teamAWon == this.teamAWon);
+}
+
+class MatchResultsCompanion extends i0.UpdateCompanion {
+ final i0.Value id;
+ final i0.Value teamA;
+ final i0.Value teamB;
+ final i0.Value teamAWon;
+ const MatchResultsCompanion({
+ this.id = const i0.Value.absent(),
+ this.teamA = const i0.Value.absent(),
+ this.teamB = const i0.Value.absent(),
+ this.teamAWon = const i0.Value.absent(),
+ });
+ MatchResultsCompanion.insert({
+ this.id = const i0.Value.absent(),
+ required String teamA,
+ required String teamB,
+ required bool teamAWon,
+ }) : teamA = i0.Value(teamA),
+ teamB = i0.Value(teamB),
+ teamAWon = i0.Value(teamAWon);
+ static i0.Insertable custom({
+ i0.Expression? id,
+ i0.Expression? teamA,
+ i0.Expression? teamB,
+ i0.Expression? teamAWon,
+ }) {
+ return i0.RawValuesInsertable({
+ if (id != null) 'id': id,
+ if (teamA != null) 'team_a': teamA,
+ if (teamB != null) 'team_b': teamB,
+ if (teamAWon != null) 'team_a_won': teamAWon,
+ });
+ }
+
+ i1.MatchResultsCompanion copyWith(
+ {i0.Value? id,
+ i0.Value? teamA,
+ i0.Value? teamB,
+ i0.Value? teamAWon}) {
+ return i1.MatchResultsCompanion(
+ id: id ?? this.id,
+ teamA: teamA ?? this.teamA,
+ teamB: teamB ?? this.teamB,
+ teamAWon: teamAWon ?? this.teamAWon,
+ );
+ }
+
+ @override
+ Map toColumns(bool nullToAbsent) {
+ final map = {};
+ if (id.present) {
+ map['id'] = i0.Variable