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; /// Type for columns storing [PgPoint]s. -typedef PointColumn = Column; +typedef PointColumn = Column; /// Type for columns storing dates directly as ([PgDateTime]). typedef TimestampColumn = Column; @@ -44,28 +44,28 @@ final class PgTypes { /// The `date` type in Postgres. static const CustomSqlType date = DateType( - PgDataType.date, + pg.Type.date, 'date', PgDate.fromDateTime, ); /// The `timestamp without time zone` type in Postgres. static const CustomSqlType timestampNoTimezone = DateType( - PgDataType.timestampWithoutTimezone, + pg.Type.timestampWithoutTimezone, 'timestamp without time zone', PgDateTime.new, ); /// The `json` type in Postgres. static const CustomSqlType json = - PostgresType(type: PgDataType.json, name: 'json'); + PostgresType(type: pg.Type.json, name: 'json'); /// The `jsonb` type in Postgres. static const CustomSqlType jsonb = - PostgresType(type: PgDataType.json, name: 'jsonb'); + PostgresType(type: pg.Type.json, name: 'jsonb'); /// The `point` type in Postgres. - static const CustomSqlType point = PointType(); + static const CustomSqlType point = PointType(); } /// A wrapper for values with the Postgres `timestamp without timezone` and diff --git a/extras/drift_postgres/lib/src/pg_database.dart b/extras/drift_postgres/lib/src/pg_database.dart index 4f6206d2..bdebb372 100644 --- a/extras/drift_postgres/lib/src/pg_database.dart +++ b/extras/drift_postgres/lib/src/pg_database.dart @@ -2,18 +2,18 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:drift/backends.dart'; -import 'package:postgres/postgres_v3_experimental.dart'; +import 'package:postgres/postgres.dart'; /// A drift database implementation that talks to a postgres database. class PgDatabase extends DelegatedDatabase { PgDatabase({ - required PgEndpoint endpoint, - PgSessionSettings? sessionSettings, + required Endpoint endpoint, + SessionSettings? sessionSettings, bool logStatements = false, bool enableMigrations = true, }) : super( _PgDelegate( - () => PgConnection.open(endpoint, sessionSettings: sessionSettings), + () => Connection.open(endpoint, sessionSettings: sessionSettings), true, enableMigrations, ), @@ -24,7 +24,7 @@ class PgDatabase extends DelegatedDatabase { /// Creates a drift database implementation from a postgres database /// [connection]. PgDatabase.opened( - PgSession connection, { + Session connection, { bool logStatements = false, bool enableMigrations = true, }) : super(_PgDelegate(() => connection, false, enableMigrations), @@ -43,9 +43,9 @@ class _PgDelegate extends DatabaseDelegate { final bool closeUnderlyingWhenClosed; final bool enableMigrations; - final FutureOr Function() _open; + final FutureOr Function() _open; - PgSession? _openedSession; + Session? _openedSession; @override TransactionDelegate get transactionDelegate => const NoTransactionDelegate(); @@ -76,7 +76,7 @@ class _PgDelegate extends DatabaseDelegate { Future runBatched(BatchedStatements statements) async { final session = _openedSession!; final prepared = - List.filled(statements.statements.length, null); + List.filled(statements.statements.length, null); try { for (final instantation in statements.arguments) { @@ -89,7 +89,7 @@ class _PgDelegate extends DatabaseDelegate { if (stmt == null) { final sql = statements.statements[stmtIndex]; stmt = prepared[stmtIndex] = - await session.prepare(PgSql(sql, types: pgArgs.types)); + await session.prepare(Sql(sql, types: pgArgs.types)); } await stmt.run(pgArgs.parameters); @@ -106,7 +106,7 @@ class _PgDelegate extends DatabaseDelegate { final pgArgs = _BoundArguments.ofDartArgs(args); final result = await session.execute( - PgSql(statement, types: pgArgs.types), + Sql(statement, types: pgArgs.types), parameters: pgArgs.parameters, ); return result.affectedRows; @@ -121,7 +121,7 @@ class _PgDelegate extends DatabaseDelegate { Future runInsert(String statement, List args) async { final session = _openedSession!; final pgArgs = _BoundArguments.ofDartArgs(args); - final result = await session.execute(PgSql(statement, types: pgArgs.types), + final result = await session.execute(Sql(statement, types: pgArgs.types), parameters: pgArgs.parameters); return result.firstOrNull?[0] as int? ?? 0; } @@ -135,7 +135,7 @@ class _PgDelegate extends DatabaseDelegate { Future runSelect(String statement, List args) async { final session = _openedSession!; final pgArgs = _BoundArguments.ofDartArgs(args); - final result = await session.execute(PgSql(statement, types: pgArgs.types), + final result = await session.execute(Sql(statement, types: pgArgs.types), parameters: pgArgs.parameters); return QueryResult([ @@ -146,35 +146,37 @@ class _PgDelegate extends DatabaseDelegate { @override Future close() async { if (closeUnderlyingWhenClosed) { - await _openedSession?.close(); + if (_openedSession case final Connection c) { + await c.close(); + } } } } class _BoundArguments { - final List types; - final List parameters; + final List types; + final List parameters; _BoundArguments(this.types, this.parameters); factory _BoundArguments.ofDartArgs(List args) { - final types = []; - final parameters = []; + final types = []; + final parameters = []; - void add(PgTypedParameter param) { + void add(TypedValue param) { types.add(param.type); parameters.add(param); } for (final value in args) { add(switch (value) { - PgTypedParameter() => value, - null => PgTypedParameter(PgDataType.text, null), - int() || BigInt() => PgTypedParameter(PgDataType.bigInteger, value), - String() => PgTypedParameter(PgDataType.text, value), - bool() => PgTypedParameter(PgDataType.boolean, value), - double() => PgTypedParameter(PgDataType.double, value), - List() => PgTypedParameter(PgDataType.byteArray, value), + TypedValue() => value, + null => TypedValue(Type.text, null), + int() || BigInt() => TypedValue(Type.bigInteger, value), + String() => TypedValue(Type.text, value), + bool() => TypedValue(Type.boolean, value), + double() => TypedValue(Type.double, value), + List() => TypedValue(Type.byteArray, value), _ => throw ArgumentError.value(value, 'value', 'Unsupported type'), }); } @@ -184,35 +186,32 @@ class _BoundArguments { } class _PgVersionDelegate extends DynamicVersionDelegate { - final PgSession database; + final Session database; _PgVersionDelegate(this.database); @override Future get schemaVersion async { - final result = - await database.execute(PgSql('SELECT version FROM __schema')); + final result = await database.execute(Sql('SELECT version FROM __schema')); return result[0][0] as int; } Future init() async { - await database.execute(PgSql('CREATE TABLE IF NOT EXISTS __schema (' + await database.execute(Sql('CREATE TABLE IF NOT EXISTS __schema (' 'version integer NOT NULL DEFAULT 0)')); - final count = - await database.execute(PgSql('SELECT COUNT(*) FROM __schema')); + final count = await database.execute(Sql('SELECT COUNT(*) FROM __schema')); if (count[0][0] as int == 0) { - await database - .execute(PgSql('INSERT INTO __schema (version) VALUES (0)')); + await database.execute(Sql('INSERT INTO __schema (version) VALUES (0)')); } } @override Future setSchemaVersion(int version) async { await database.execute( - PgSql(r'UPDATE __schema SET version = $1', types: [PgDataType.integer]), + Sql(r'UPDATE __schema SET version = $1', types: [Type.integer]), parameters: [ - PgTypedParameter(PgDataType.integer, version), + TypedValue(Type.integer, version), ], ); } diff --git a/extras/drift_postgres/lib/src/types.dart b/extras/drift_postgres/lib/src/types.dart index 8661c062..304d6f1c 100644 --- a/extras/drift_postgres/lib/src/types.dart +++ b/extras/drift_postgres/lib/src/types.dart @@ -1,5 +1,5 @@ import 'package:drift/drift.dart'; -import 'package:postgres/postgres_v3_experimental.dart'; +import 'package:postgres/postgres.dart'; // ignore: implementation_imports import 'package:postgres/src/text_codec.dart'; import 'package:uuid/uuid.dart'; @@ -7,7 +7,7 @@ import 'package:uuid/uuid.dart'; class PostgresType implements CustomSqlType { static final _encoder = PostgresTextEncoder(); - final PgDataType type; + final Type type; final String name; const PostgresType({required this.type, required this.name}); @@ -18,7 +18,7 @@ class PostgresType implements CustomSqlType { } @override - Object mapToSqlParameter(T dartValue) => PgTypedParameter(type, dartValue); + Object mapToSqlParameter(T dartValue) => TypedValue(type, dartValue); @override T read(Object fromSql) => fromSql as T; @@ -28,7 +28,7 @@ class PostgresType implements CustomSqlType { } class UuidType extends PostgresType { - const UuidType() : super(type: PgDataType.uuid, name: 'uuid'); + const UuidType() : super(type: Type.uuid, name: 'uuid'); @override String mapToSqlLiteral(UuidValue dartValue) { @@ -38,7 +38,7 @@ class UuidType extends PostgresType { @override Object mapToSqlParameter(UuidValue dartValue) { - return PgTypedParameter(PgDataType.uuid, dartValue.uuid); + return TypedValue(Type.uuid, dartValue.uuid); } @override @@ -48,17 +48,17 @@ class UuidType extends PostgresType { } // override because the text encoder doesn't properly encode PgPoint values -class PointType extends PostgresType { - const PointType() : super(type: PgDataType.point, name: 'point'); +class PointType extends PostgresType { + const PointType() : super(type: Type.point, name: 'point'); @override - String mapToSqlLiteral(PgPoint dartValue) { + String mapToSqlLiteral(Point dartValue) { return "'(${dartValue.latitude}, ${dartValue.longitude})'::point"; } } class IntervalType extends PostgresType { - const IntervalType() : super(type: PgDataType.interval, name: 'interval'); + const IntervalType() : super(type: Type.interval, name: 'interval'); @override String mapToSqlLiteral(Duration dartValue) { @@ -74,7 +74,7 @@ class DateType extends PostgresType { final T Function(DateTime) _fromDateTime; const DateType( - PgDataType type, + Type type, String name, this._fromDateTime, ) : super(type: type, name: name); @@ -86,7 +86,7 @@ class DateType extends PostgresType { @override Object mapToSqlParameter(T dartValue) { - return PgTypedParameter(type, dartValue.toDateTime()); + return TypedValue(type, dartValue.toDateTime()); } @override diff --git a/extras/drift_postgres/pubspec.yaml b/extras/drift_postgres/pubspec.yaml index 5f250b68..7a2e270a 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.1.0 +version: 0.2.0-dev repository: https://github.com/simolus3/drift homepage: https://drift.simonbinder.eu/docs/platforms/postgres/ issue_tracker: https://github.com/simolus3/drift/issues @@ -11,7 +11,7 @@ environment: dependencies: collection: ^1.16.0 drift: ^2.0.0 - postgres: ^2.6.3 + postgres: ^3.0.0-0 meta: ^1.8.0 uuid: ^4.1.0 diff --git a/extras/drift_postgres/test/drift_postgres_test.dart b/extras/drift_postgres/test/drift_postgres_test.dart index 50c07ab3..f1cebc39 100644 --- a/extras/drift_postgres/test/drift_postgres_test.dart +++ b/extras/drift_postgres/test/drift_postgres_test.dart @@ -1,6 +1,6 @@ import 'package:drift_postgres/drift_postgres.dart'; import 'package:drift_testcases/tests.dart'; -import 'package:postgres/postgres_v3_experimental.dart'; +import 'package:postgres/postgres.dart' as pg; class PgExecutor extends TestExecutor { @override @@ -12,7 +12,7 @@ class PgExecutor extends TestExecutor { @override DatabaseConnection createConnection() { return DatabaseConnection(PgDatabase( - endpoint: PgEndpoint( + endpoint: pg.Endpoint( host: 'localhost', database: 'postgres', username: 'postgres', diff --git a/extras/drift_postgres/test/types_test.dart b/extras/drift_postgres/test/types_test.dart index d8fde73a..82724d3f 100644 --- a/extras/drift_postgres/test/types_test.dart +++ b/extras/drift_postgres/test/types_test.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:test/test.dart'; import 'package:uuid/uuid.dart'; @@ -8,7 +8,7 @@ import '../example/main.dart'; void main() { final database = DriftPostgresDatabase(PgDatabase( - endpoint: PgEndpoint( + endpoint: pg.Endpoint( host: 'localhost', database: 'postgres', username: 'postgres', @@ -48,7 +48,7 @@ void main() { group('interval', () => testWith(PgTypes.interval, Duration(seconds: 15))); group('json', () => testWith(PgTypes.json, {'foo': 'bar'})); group('jsonb', () => testWith(PgTypes.jsonb, {'foo': 'bar'})); - group('point', () => testWith(PgTypes.point, PgPoint(90, -90))); + group('point', () => testWith(PgTypes.point, pg.Point(90, -90))); group( 'timestamp without timezone', () => testWith(PgTypes.timestampNoTimezone, From 61bd7f5ec23d025c1ca8c93a2b429aa4e28be618 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 22 Oct 2023 00:36:07 +0200 Subject: [PATCH 02/37] Fix postgres tests --- .github/workflows/main.yml | 2 +- docs/lib/snippets/platforms/postgres.dart | 5 +++++ extras/drift_postgres/README.md | 2 +- extras/drift_postgres/example/main.dart | 5 +++++ .../drift_postgres/test/drift_postgres_test.dart | 3 ++- extras/drift_postgres/test/types_test.dart | 14 ++++---------- 6 files changed, 18 insertions(+), 13 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 643088ed..f0d78d04 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -188,7 +188,7 @@ jobs: working-directory: extras/drift_postgres run: | dart pub upgrade - dart test + dart test -j 1 - name: MariaDB integration tests working-directory: extras/drift_mariadb continue-on-error: true diff --git a/docs/lib/snippets/platforms/postgres.dart b/docs/lib/snippets/platforms/postgres.dart index 42edc0d3..b7060003 100644 --- a/docs/lib/snippets/platforms/postgres.dart +++ b/docs/lib/snippets/platforms/postgres.dart @@ -26,6 +26,11 @@ void main() async { username: 'postgres', password: 'postgres', ), + sessionSettings: SessionSettings( + // If you expect to talk to a Postgres database over a public connection, + // please use SslMode.verifyFull instead. + sslMode: SslMode.disable, + ), ); final driftDatabase = MyDatabase(pgDatabase); diff --git a/extras/drift_postgres/README.md b/extras/drift_postgres/README.md index 19a91b38..ac691bc0 100644 --- a/extras/drift_postgres/README.md +++ b/extras/drift_postgres/README.md @@ -38,4 +38,4 @@ To test this package, first run docker run -p 5432:5432 -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres postgres ``` -It can then be tested with `dart test`. +It can then be tested with `dart test -j 1` (concurrency needs to be disabled since tests are using the same database). diff --git a/extras/drift_postgres/example/main.dart b/extras/drift_postgres/example/main.dart index 00f6795e..b21447e8 100644 --- a/extras/drift_postgres/example/main.dart +++ b/extras/drift_postgres/example/main.dart @@ -26,6 +26,11 @@ void main() async { username: 'postgres', password: 'postgres', ), + sessionSettings: pg.SessionSettings( + // If you expect to talk to a Postgres database over a public connection, + // please use SslMode.verifyFull instead. + sslMode: pg.SslMode.disable, + ), logStatements: true, )); diff --git a/extras/drift_postgres/test/drift_postgres_test.dart b/extras/drift_postgres/test/drift_postgres_test.dart index f1cebc39..1f416a94 100644 --- a/extras/drift_postgres/test/drift_postgres_test.dart +++ b/extras/drift_postgres/test/drift_postgres_test.dart @@ -18,11 +18,12 @@ class PgExecutor extends TestExecutor { username: 'postgres', password: 'postgres', ), + sessionSettings: pg.SessionSettings(sslMode: pg.SslMode.disable), )); } @override - Future clearDatabaseAndClose(Database db) async { + Future clearDatabaseAndClose(GeneratedDatabase db) async { await db.customStatement('DROP SCHEMA public CASCADE;'); await db.customStatement('CREATE SCHEMA public;'); await db.customStatement('GRANT ALL ON SCHEMA public TO postgres;'); diff --git a/extras/drift_postgres/test/types_test.dart b/extras/drift_postgres/test/types_test.dart index 82724d3f..3693a75b 100644 --- a/extras/drift_postgres/test/types_test.dart +++ b/extras/drift_postgres/test/types_test.dart @@ -5,24 +5,18 @@ import 'package:test/test.dart'; import 'package:uuid/uuid.dart'; import '../example/main.dart'; +import 'drift_postgres_test.dart'; void main() { - final database = DriftPostgresDatabase(PgDatabase( - endpoint: pg.Endpoint( - host: 'localhost', - database: 'postgres', - username: 'postgres', - password: 'postgres', - ), - )); + final executor = PgExecutor(); + final database = DriftPostgresDatabase(executor.createConnection()); setUpAll(() async { await database.users.insertOne(UsersCompanion.insert(name: 'test user')); }); tearDownAll(() async { - await database.users.deleteAll(); - await database.close(); + await executor.clearDatabaseAndClose(database); }); group('custom types pass through', () { From d70de95382c7cd63fe66ef7008301ec2dc00566d Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 22 Oct 2023 12:33:03 +0200 Subject: [PATCH 03/37] Make `sum()` nullable --- .../src/analysis/types/graph/relationships.dart | 9 ++++++++- .../lib/src/analysis/types/graph/type_graph.dart | 8 ++++++-- .../lib/src/analysis/types/resolving_visitor.dart | 14 +++++++++++--- .../errors/invalid_parameter_count_test.dart | 10 ++++++++++ .../test/analysis/types2/misc_cases_test.dart | 3 ++- 5 files changed, 37 insertions(+), 7 deletions(-) diff --git a/sqlparser/lib/src/analysis/types/graph/relationships.dart b/sqlparser/lib/src/analysis/types/graph/relationships.dart index aa524e00..ee0c2c9d 100644 --- a/sqlparser/lib/src/analysis/types/graph/relationships.dart +++ b/sqlparser/lib/src/analysis/types/graph/relationships.dart @@ -91,6 +91,13 @@ class CopyAndCast extends TypeRelation implements DirectedRelation { final Typeable other; final CastMode cast; final bool dropTypeHint; + final bool makeNullable; - CopyAndCast(this.target, this.other, this.cast, {this.dropTypeHint = false}); + CopyAndCast( + this.target, + this.other, + this.cast, { + this.dropTypeHint = false, + this.makeNullable = false, + }); } diff --git a/sqlparser/lib/src/analysis/types/graph/type_graph.dart b/sqlparser/lib/src/analysis/types/graph/type_graph.dart index 2e2b211d..f3b848a3 100644 --- a/sqlparser/lib/src/analysis/types/graph/type_graph.dart +++ b/sqlparser/lib/src/analysis/types/graph/type_graph.dart @@ -112,8 +112,12 @@ class TypeGraph { _copyType(resolved, t, other); } } else if (edge is CopyAndCast) { - _copyType(resolved, t, edge.target, - this[t]!.cast(edge.cast, edge.dropTypeHint)); + var copied = this[t]!.cast(edge.cast, edge.dropTypeHint); + if (edge.makeNullable) { + copied = copied.withNullable(true); + } + + _copyType(resolved, t, edge.target, copied); } else if (edge is MultiSourceRelation) { // handle many-to-one changes, if all targets have been resolved or // lax handling is enabled. diff --git a/sqlparser/lib/src/analysis/types/resolving_visitor.dart b/sqlparser/lib/src/analysis/types/resolving_visitor.dart index 2914d890..c67b7639 100644 --- a/sqlparser/lib/src/analysis/types/resolving_visitor.dart +++ b/sqlparser/lib/src/analysis/types/resolving_visitor.dart @@ -549,10 +549,18 @@ class TypeResolver extends RecursiveVisitor { // ignore: dead_code throw AssertionError(); // required so that this switch compiles case 'sum': + checkArgumentCount(1); + + // The result of `sum()` is `NULL` if there are no input rows. + session._addRelation(CopyAndCast( + e, + params.first, + CastMode.numeric, + dropTypeHint: true, + makeNullable: true, + )); session._addRelation( - CopyAndCast(e, params.first, CastMode.numeric, dropTypeHint: true)); - session._addRelation(DefaultType(e, defaultType: _realType)); - nullableIfChildIs(); + DefaultType(e, defaultType: _realType.withNullable(true))); return null; case 'lower': case 'ltrim': diff --git a/sqlparser/test/analysis/errors/invalid_parameter_count_test.dart b/sqlparser/test/analysis/errors/invalid_parameter_count_test.dart index 94600f6a..195b5ccc 100644 --- a/sqlparser/test/analysis/errors/invalid_parameter_count_test.dart +++ b/sqlparser/test/analysis/errors/invalid_parameter_count_test.dart @@ -15,4 +15,14 @@ void main() { message: 'iif expects 3 arguments, got 2.'), ]); }); + + test('sum', () { + final engine = SqlEngine(); + final result = engine.analyze('SELECT sum(1, 2, 3)'); + + result.expectError( + '1, 2, 3', + type: AnalysisErrorType.invalidAmountOfParameters, + ); + }); } diff --git a/sqlparser/test/analysis/types2/misc_cases_test.dart b/sqlparser/test/analysis/types2/misc_cases_test.dart index bdaa69df..322b920a 100644 --- a/sqlparser/test/analysis/types2/misc_cases_test.dart +++ b/sqlparser/test/analysis/types2/misc_cases_test.dart @@ -53,7 +53,8 @@ const Map _types = { "SELECT 'a' -> ? = 'b'": ResolvedType(type: BasicType.text, nullable: false), "SELECT 'a' ->> ? = 'b'": ResolvedType(type: BasicType.text, nullable: false), 'SELECT MAX(id, ?) FROM demo': ResolvedType(type: BasicType.int), - 'SELECT SUM(id = 2) = ? FROM demo': ResolvedType(type: BasicType.int), + 'SELECT SUM(id = 2) = ? FROM demo': + ResolvedType(type: BasicType.int, nullable: true), "SELECT unixepoch('now') = ?": ResolvedType(type: BasicType.int, nullable: true, hints: [IsDateTime()]), "SELECT datetime('now') = ?": From 86c071a965d766557ce479aef29c19eb4a211143 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 22 Oct 2023 12:33:10 +0200 Subject: [PATCH 04/37] Re-run build --- drift/lib/src/runtime/devtools/shared.g.dart | 77 ------------------- drift/test/test_utils/test_utils.mocks.dart | 19 +++++ examples/app/lib/database/database.g.dart | 16 ++-- examples/encryption/lib/database.g.dart | 5 +- examples/modular/lib/src/users.drift.dart | 1 + .../lib/database/database.g.dart | 1 + 6 files changed, 34 insertions(+), 85 deletions(-) delete mode 100644 drift/lib/src/runtime/devtools/shared.g.dart diff --git a/drift/lib/src/runtime/devtools/shared.g.dart b/drift/lib/src/runtime/devtools/shared.g.dart deleted file mode 100644 index f74fcb71..00000000 --- a/drift/lib/src/runtime/devtools/shared.g.dart +++ /dev/null @@ -1,77 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'shared.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -TypeDescription _$TypeDescriptionFromJson(Map json) => - TypeDescription( - type: $enumDecodeNullable(_$DriftSqlTypeEnumMap, json['type']), - customTypeName: json['customTypeName'] as String?, - ); - -Map _$TypeDescriptionToJson(TypeDescription instance) => - { - 'type': _$DriftSqlTypeEnumMap[instance.type], - 'customTypeName': instance.customTypeName, - }; - -const _$DriftSqlTypeEnumMap = { - DriftSqlType.bool: 'bool', - DriftSqlType.string: 'string', - DriftSqlType.bigInt: 'bigInt', - DriftSqlType.int: 'int', - DriftSqlType.dateTime: 'dateTime', - DriftSqlType.blob: 'blob', - DriftSqlType.double: 'double', - DriftSqlType.any: 'any', -}; - -ColumnDescription _$ColumnDescriptionFromJson(Map json) => - ColumnDescription( - name: json['name'] as String, - type: json['type'] == null - ? null - : TypeDescription.fromJson(json['type'] as Map), - isNullable: json['isNullable'] as bool, - ); - -Map _$ColumnDescriptionToJson(ColumnDescription instance) => - { - 'name': instance.name, - 'type': instance.type, - 'isNullable': instance.isNullable, - }; - -EntityDescription _$EntityDescriptionFromJson(Map json) => - EntityDescription( - name: json['name'] as String, - type: json['type'] as String, - columns: (json['columns'] as List?) - ?.map((e) => ColumnDescription.fromJson(e as Map)) - .toList(), - ); - -Map _$EntityDescriptionToJson(EntityDescription instance) => - { - 'name': instance.name, - 'type': instance.type, - 'columns': instance.columns, - }; - -DatabaseDescription _$DatabaseDescriptionFromJson(Map json) => - DatabaseDescription( - dateTimeAsText: json['dateTimeAsText'] as bool, - entities: (json['entities'] as List) - .map((e) => EntityDescription.fromJson(e as Map)) - .toList(), - ); - -Map _$DatabaseDescriptionToJson( - DatabaseDescription instance) => - { - 'dateTimeAsText': instance.dateTimeAsText, - 'entities': instance.entities, - }; diff --git a/drift/test/test_utils/test_utils.mocks.dart b/drift/test/test_utils/test_utils.mocks.dart index fad373d8..0e96ecb1 100644 --- a/drift/test/test_utils/test_utils.mocks.dart +++ b/drift/test/test_utils/test_utils.mocks.dart @@ -64,6 +64,7 @@ class MockDatabaseDelegate extends _i1.Mock implements _i2.DatabaseDelegate { returnValue: false, returnValueForMissingStub: false, ) as bool); + @override set isInTransaction(bool? _isInTransaction) => super.noSuchMethod( Invocation.setter( @@ -72,6 +73,7 @@ class MockDatabaseDelegate extends _i1.Mock implements _i2.DatabaseDelegate { ), returnValueForMissingStub: null, ); + @override _i2.DbVersionDelegate get versionDelegate => (super.noSuchMethod( Invocation.getter(#versionDelegate), @@ -84,6 +86,7 @@ class MockDatabaseDelegate extends _i1.Mock implements _i2.DatabaseDelegate { Invocation.getter(#versionDelegate), ), ) as _i2.DbVersionDelegate); + @override _i2.TransactionDelegate get transactionDelegate => (super.noSuchMethod( Invocation.getter(#transactionDelegate), @@ -96,12 +99,14 @@ class MockDatabaseDelegate extends _i1.Mock implements _i2.DatabaseDelegate { Invocation.getter(#transactionDelegate), ), ) as _i2.TransactionDelegate); + @override _i4.FutureOr get isOpen => (super.noSuchMethod( Invocation.getter(#isOpen), returnValue: _i4.Future.value(false), returnValueForMissingStub: _i4.Future.value(false), ) as _i4.FutureOr); + @override _i4.Future open(_i5.QueryExecutorUser? db) => (super.noSuchMethod( Invocation.method( @@ -111,6 +116,7 @@ class MockDatabaseDelegate extends _i1.Mock implements _i2.DatabaseDelegate { returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); + @override _i4.Future close() => (super.noSuchMethod( Invocation.method( @@ -120,6 +126,7 @@ class MockDatabaseDelegate extends _i1.Mock implements _i2.DatabaseDelegate { returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); + @override void notifyDatabaseOpened(_i5.OpeningDetails? details) => super.noSuchMethod( Invocation.method( @@ -128,6 +135,7 @@ class MockDatabaseDelegate extends _i1.Mock implements _i2.DatabaseDelegate { ), returnValueForMissingStub: null, ); + @override _i4.Future<_i3.QueryResult> runSelect( String? statement, @@ -163,6 +171,7 @@ class MockDatabaseDelegate extends _i1.Mock implements _i2.DatabaseDelegate { ), )), ) as _i4.Future<_i3.QueryResult>); + @override _i4.Future runUpdate( String? statement, @@ -179,6 +188,7 @@ class MockDatabaseDelegate extends _i1.Mock implements _i2.DatabaseDelegate { returnValue: _i4.Future.value(0), returnValueForMissingStub: _i4.Future.value(0), ) as _i4.Future); + @override _i4.Future runInsert( String? statement, @@ -195,6 +205,7 @@ class MockDatabaseDelegate extends _i1.Mock implements _i2.DatabaseDelegate { returnValue: _i4.Future.value(0), returnValueForMissingStub: _i4.Future.value(0), ) as _i4.Future); + @override _i4.Future runCustom( String? statement, @@ -211,6 +222,7 @@ class MockDatabaseDelegate extends _i1.Mock implements _i2.DatabaseDelegate { returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); + @override _i4.Future runBatched(_i5.BatchedStatements? statements) => (super.noSuchMethod( @@ -234,6 +246,7 @@ class MockDynamicVersionDelegate extends _i1.Mock returnValue: _i4.Future.value(0), returnValueForMissingStub: _i4.Future.value(0), ) as _i4.Future); + @override _i4.Future setSchemaVersion(int? version) => (super.noSuchMethod( Invocation.method( @@ -256,6 +269,7 @@ class MockSupportedTransactionDelegate extends _i1.Mock returnValue: false, returnValueForMissingStub: false, ) as bool); + @override _i4.FutureOr startTransaction( _i4.Future Function(_i2.QueryDelegate)? run) => @@ -284,6 +298,7 @@ class MockStreamQueries extends _i1.Mock implements _i6.StreamQueryStore { returnValueForMissingStub: _i4.Stream>>.empty(), ) as _i4.Stream>>); + @override _i4.Stream> updatesForSync( _i5.TableUpdateQuery? query) => @@ -295,6 +310,7 @@ class MockStreamQueries extends _i1.Mock implements _i6.StreamQueryStore { returnValue: _i4.Stream>.empty(), returnValueForMissingStub: _i4.Stream>.empty(), ) as _i4.Stream>); + @override void handleTableUpdates(Set<_i5.TableUpdate>? updates) => super.noSuchMethod( Invocation.method( @@ -303,6 +319,7 @@ class MockStreamQueries extends _i1.Mock implements _i6.StreamQueryStore { ), returnValueForMissingStub: null, ); + @override void markAsClosed( _i6.QueryStream? stream, @@ -318,6 +335,7 @@ class MockStreamQueries extends _i1.Mock implements _i6.StreamQueryStore { ), returnValueForMissingStub: null, ); + @override void markAsOpened(_i6.QueryStream? stream) => super.noSuchMethod( Invocation.method( @@ -326,6 +344,7 @@ class MockStreamQueries extends _i1.Mock implements _i6.StreamQueryStore { ), returnValueForMissingStub: null, ); + @override _i4.Future close() => (super.noSuchMethod( Invocation.method( diff --git a/examples/app/lib/database/database.g.dart b/examples/app/lib/database/database.g.dart index 8a412b58..52aebbf6 100644 --- a/examples/app/lib/database/database.g.dart +++ b/examples/app/lib/database/database.g.dart @@ -32,9 +32,10 @@ class $CategoriesTable extends Categories @override List get $columns => [id, name, color]; @override - String get aliasedName => _alias ?? 'categories'; + String get aliasedName => _alias ?? actualTableName; @override - String get actualTableName => 'categories'; + String get actualTableName => $name; + static const String $name = 'categories'; @override VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { @@ -194,6 +195,7 @@ class CategoriesCompanion extends UpdateCompanion { } if (color.present) { final converter = $CategoriesTable.$convertercolor; + map['color'] = Variable(converter.toSql(color.value)); } return map; @@ -249,9 +251,10 @@ class $TodoEntriesTable extends TodoEntries @override List get $columns => [id, description, category, dueDate]; @override - String get aliasedName => _alias ?? 'todo_entries'; + String get aliasedName => _alias ?? actualTableName; @override - String get actualTableName => 'todo_entries'; + String get actualTableName => $name; + static const String $name = 'todo_entries'; @override VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { @@ -486,9 +489,10 @@ class TextEntries extends Table @override List get $columns => [description]; @override - String get aliasedName => _alias ?? 'text_entries'; + String get aliasedName => _alias ?? actualTableName; @override - String get actualTableName => 'text_entries'; + String get actualTableName => $name; + static const String $name = 'text_entries'; @override VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { diff --git a/examples/encryption/lib/database.g.dart b/examples/encryption/lib/database.g.dart index 123a6dce..bec75774 100644 --- a/examples/encryption/lib/database.g.dart +++ b/examples/encryption/lib/database.g.dart @@ -26,9 +26,10 @@ class $NotesTable extends Notes with TableInfo<$NotesTable, Note> { @override List get $columns => [id, content]; @override - String get aliasedName => _alias ?? 'notes'; + String get aliasedName => _alias ?? actualTableName; @override - String get actualTableName => 'notes'; + String get actualTableName => $name; + static const String $name = 'notes'; @override VerificationContext validateIntegrity(Insertable instance, {bool isInserting = false}) { diff --git a/examples/modular/lib/src/users.drift.dart b/examples/modular/lib/src/users.drift.dart index a2153113..f76dc728 100644 --- a/examples/modular/lib/src/users.drift.dart +++ b/examples/modular/lib/src/users.drift.dart @@ -292,6 +292,7 @@ class UsersCompanion extends i0.UpdateCompanion { } if (preferences.present) { final converter = i1.Users.$converterpreferencesn; + map['preferences'] = i0.Variable(converter.toSql(preferences.value)); } diff --git a/extras/integration_tests/drift_testcases/lib/database/database.g.dart b/extras/integration_tests/drift_testcases/lib/database/database.g.dart index 44742d05..0f25f1a7 100644 --- a/extras/integration_tests/drift_testcases/lib/database/database.g.dart +++ b/extras/integration_tests/drift_testcases/lib/database/database.g.dart @@ -295,6 +295,7 @@ class UsersCompanion extends UpdateCompanion { } if (preferences.present) { final converter = $UsersTable.$converterpreferences; + map['preferences'] = Variable(converter.toSql(preferences.value)); } return map; From bf8dff088777be3f9b61be8fa92184f46f2221ed Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 22 Oct 2023 12:38:31 +0200 Subject: [PATCH 05/37] Add changelog entry for sum change --- sqlparser/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sqlparser/CHANGELOG.md b/sqlparser/CHANGELOG.md index 700e27d4..67e179a4 100644 --- a/sqlparser/CHANGELOG.md +++ b/sqlparser/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.32.1-dev + +- Treat the result of `sum()` as nullable when inferring types. + ## 0.32.0 - Turn `ResolvedType.hints` into a list, supporting multiple type hints. From ca84c194bd214f17306965a288c161633904ae98 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 23 Oct 2023 18:35:29 +0200 Subject: [PATCH 06/37] Add shared analyzer option (closes #2688) --- .../Generation options/in_other_builders.md | 52 ++++++++++++++++++- docs/pages/docs/Generation options/index.md | 3 ++ drift_dev/CHANGELOG.md | 4 ++ drift_dev/build.yaml | 1 + drift_dev/lib/src/analysis/options.dart | 5 ++ .../lib/src/backends/build/drift_builder.dart | 6 ++- .../lib/src/generated/analysis/options.g.dart | 5 ++ drift_dev/pubspec.yaml | 2 +- sqlparser/pubspec.yaml | 2 +- 9 files changed, 75 insertions(+), 5 deletions(-) diff --git a/docs/pages/docs/Generation options/in_other_builders.md b/docs/pages/docs/Generation options/in_other_builders.md index 418ba85b..4653fcda 100644 --- a/docs/pages/docs/Generation options/in_other_builders.md +++ b/docs/pages/docs/Generation options/in_other_builders.md @@ -15,8 +15,21 @@ targets: drift: auto_apply_builders: false builders: + drift_dev:analyzer: + enabled: true + options: &options + # Drift build options, as per https://drift.simonbinder.eu/docs/advanced-features/builder_options/ + store_date_time_values_as_text: true + named_parameters: true + sql: + dialect: sqlite + options: + version: "3.39" + modules: [fts5] drift_dev:modular: enabled: true + # We use yaml anchors to give the two builders the same options + options: *options $default: dependencies: @@ -27,7 +40,6 @@ targets: # its own target instead. drift_dev: enabled: false - ``` With modular generation, you'll have to replace the `part` statement in the database file with an @@ -59,3 +71,41 @@ and use the non-shared generator instead. Finally, we need to the build system to run drift first, and all the other builders otherwise. This is why we split the builders up into multiple targets. The first target will only run drift, the second target has a dependency on the first one and will run all the other builders. + +## Using `drift_dev:not_shared` + +For complex build setups like those requiring other builders to see drift code, the `drift_dev:modular` +builder is recommended. +However, enabling the modular builder requires other code modifications like replacing `part` statements +with imports. A simpler change may be the `not_shared` builder offered by `drift_dev`. It works like the +default setup, except that it emits a `.drift.dart` part file instead of a shared `.g.dart` file - so you +only have to change a single `part` statement to migrate. + +To enable this builder, also enable the `drift_dev:analyzer` builder and the `has_separate_analyzer` +option: + +```yaml +targets: + drift: + auto_apply_builders: false + builders: + drift_dev:analyzer: + enabled: true + options: &options + has_separate_analyzer: true # always enable this option when using `not_shared` + # remaining options... + drift_dev:not_shared: + enabled: true + # We use yaml anchors to give the two builders the same options + options: *options + + $default: + dependencies: + # run drift's builder first + - ":drift" + builders: + # This builder is enabled by default, but we're using the modular builder in + # its own target instead. + drift_dev: + enabled: false +``` diff --git a/docs/pages/docs/Generation options/index.md b/docs/pages/docs/Generation options/index.md index b2d77e09..ef04b4a5 100644 --- a/docs/pages/docs/Generation options/index.md +++ b/docs/pages/docs/Generation options/index.md @@ -77,6 +77,9 @@ At the moment, drift supports these options: The possible values are `preserve`, `camelCase`, `CONSTANT_CASE`, `snake_case`, `PascalCase`, `lowercase` and `UPPERCASE` (default: `snake_case`). * `write_to_columns_mixins`: Whether the `toColumns` method should be written as a mixin instead of being added directly to the data class. This is useful when using [existing row classes]({{ '../custom_row_classes.md' | pageUrl }}), as the mixin is generated for those as well. +* `has_separate_analyzer`: This option is only relevant when using the `drift_dev:not_shared` builder, which needs to use a less efficient + analysis implementation than the other builders by default. After also applying `drift_dev:analyzer` to the same build target, this option + can be enabled to speed up builds. This option has no effect with the default or the modular builder. * `fatal_warnings`: When enabled (defaults to `false`), warnings found by `drift_dev` in the build process (like syntax errors in SQL queries or unresolved references in your Dart tables) will cause the build to fail. * `preamble`: This option is useful when using drift [as a standalone part builder](#using-drift-classes-in-other-builders) or when running a diff --git a/drift_dev/CHANGELOG.md b/drift_dev/CHANGELOG.md index c2608f37..df13dbef 100644 --- a/drift_dev/CHANGELOG.md +++ b/drift_dev/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.13.1-dev + +- Add `has_separate_analyzer` option to optimize builds using the `not_shared` builder. + ## 2.13.0 - Fix indices not being created for Dart tables from different files. diff --git a/drift_dev/build.yaml b/drift_dev/build.yaml index 0fd1d4f9..d3194109 100644 --- a/drift_dev/build.yaml +++ b/drift_dev/build.yaml @@ -62,6 +62,7 @@ builders: auto_apply: none required_inputs: [".drift_prep.json"] applies_builders: [":preparing_builder"] + runs_before: [":not_shared"] post_process_builders: cleanup: diff --git a/drift_dev/lib/src/analysis/options.dart b/drift_dev/lib/src/analysis/options.dart index bc7fba4d..f4b36be5 100644 --- a/drift_dev/lib/src/analysis/options.dart +++ b/drift_dev/lib/src/analysis/options.dart @@ -102,6 +102,9 @@ class DriftOptions { @JsonKey(name: 'write_to_columns_mixins', defaultValue: false) final bool writeToColumnsMixins; + @JsonKey(name: 'has_separate_analyzer', defaultValue: false) + final bool hasDriftAnalyzer; + final String? preamble; @JsonKey(name: 'fatal_warnings', defaultValue: false) @@ -131,6 +134,7 @@ class DriftOptions { this.preamble, this.writeToColumnsMixins = false, this.fatalWarnings = false, + this.hasDriftAnalyzer = false, }); DriftOptions({ @@ -155,6 +159,7 @@ class DriftOptions { required this.writeToColumnsMixins, required this.fatalWarnings, required this.preamble, + required this.hasDriftAnalyzer, this.dialect, }) { // ignore: deprecated_member_use_from_same_package diff --git a/drift_dev/lib/src/backends/build/drift_builder.dart b/drift_dev/lib/src/backends/build/drift_builder.dart index 37a6b925..bf47bdf7 100644 --- a/drift_dev/lib/src/backends/build/drift_builder.dart +++ b/drift_dev/lib/src/backends/build/drift_builder.dart @@ -140,8 +140,10 @@ class _DriftBuildRun { // The discovery and analyzer builders will have emitted IR for // every relevant file in a previous build step that this builder // has a dependency on. - findsResolvedElementsReliably: !mode.embeddedAnalyzer, - findsLocalElementsReliably: !mode.embeddedAnalyzer, + findsResolvedElementsReliably: + !mode.embeddedAnalyzer || options.hasDriftAnalyzer, + findsLocalElementsReliably: + !mode.embeddedAnalyzer || options.hasDriftAnalyzer, ); Future run() async { diff --git a/drift_dev/lib/src/generated/analysis/options.g.dart b/drift_dev/lib/src/generated/analysis/options.g.dart index c9a0974d..74576168 100644 --- a/drift_dev/lib/src/generated/analysis/options.g.dart +++ b/drift_dev/lib/src/generated/analysis/options.g.dart @@ -33,6 +33,7 @@ DriftOptions _$DriftOptionsFromJson(Map json) => $checkedCreate( 'store_date_time_values_as_text', 'case_from_dart_to_sql', 'write_to_columns_mixins', + 'has_separate_analyzer', 'preamble', 'fatal_warnings' ], @@ -91,6 +92,8 @@ DriftOptions _$DriftOptionsFromJson(Map json) => $checkedCreate( fatalWarnings: $checkedConvert('fatal_warnings', (v) => v as bool? ?? false), preamble: $checkedConvert('preamble', (v) => v as String?), + hasDriftAnalyzer: $checkedConvert( + 'has_separate_analyzer', (v) => v as bool? ?? false), dialect: $checkedConvert('sql', (v) => v == null ? null : DialectOptions.fromJson(v as Map)), ); @@ -120,6 +123,7 @@ DriftOptions _$DriftOptionsFromJson(Map json) => $checkedCreate( 'caseFromDartToSql': 'case_from_dart_to_sql', 'writeToColumnsMixins': 'write_to_columns_mixins', 'fatalWarnings': 'fatal_warnings', + 'hasDriftAnalyzer': 'has_separate_analyzer', 'dialect': 'sql' }, ); @@ -153,6 +157,7 @@ Map _$DriftOptionsToJson(DriftOptions instance) => 'case_from_dart_to_sql': _$CaseFromDartToSqlEnumMap[instance.caseFromDartToSql]!, 'write_to_columns_mixins': instance.writeToColumnsMixins, + 'has_separate_analyzer': instance.hasDriftAnalyzer, 'preamble': instance.preamble, 'fatal_warnings': instance.fatalWarnings, }; diff --git a/drift_dev/pubspec.yaml b/drift_dev/pubspec.yaml index a32f2afa..67b5a501 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.0 +version: 2.13.1-dev repository: https://github.com/simolus3/drift homepage: https://drift.simonbinder.eu/ issue_tracker: https://github.com/simolus3/drift/issues diff --git a/sqlparser/pubspec.yaml b/sqlparser/pubspec.yaml index d3a674d2..07077868 100644 --- a/sqlparser/pubspec.yaml +++ b/sqlparser/pubspec.yaml @@ -1,6 +1,6 @@ name: sqlparser description: Parses sqlite statements and performs static analysis on them -version: 0.32.0 +version: 0.32.1-dev homepage: https://github.com/simolus3/drift/tree/develop/sqlparser repository: https://github.com/simolus3/drift #homepage: https://drift.simonbinder.eu/ From 8077c564d76cd4861c54915138e1fd5c6d25b8c1 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 24 Oct 2023 21:14:57 +0200 Subject: [PATCH 07/37] Highlight Android workarounds more --- docs/assets/path_provider/lib/path_provider.dart | 4 ++++ .../lib/sqlite3_flutter_libs.dart | 3 +++ docs/assets/sqlite3_flutter_libs/pubspec.yaml | 6 ++++++ docs/lib/snippets/setup/database.dart | 14 ++++++++++++++ docs/pages/docs/setup.md | 5 +++++ docs/pubspec.yaml | 4 +++- examples/app/android/app/build.gradle | 4 ++++ examples/app/lib/database/connection/native.dart | 16 +++++++++++++++- 8 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 docs/assets/sqlite3_flutter_libs/lib/sqlite3_flutter_libs.dart create mode 100644 docs/assets/sqlite3_flutter_libs/pubspec.yaml diff --git a/docs/assets/path_provider/lib/path_provider.dart b/docs/assets/path_provider/lib/path_provider.dart index 73d86f30..b26b594b 100644 --- a/docs/assets/path_provider/lib/path_provider.dart +++ b/docs/assets/path_provider/lib/path_provider.dart @@ -3,3 +3,7 @@ import 'dart:io'; Future getApplicationDocumentsDirectory() { throw UnsupportedError('stub!'); } + +Future getTemporaryDirectory() { + throw UnsupportedError('stub!'); +} diff --git a/docs/assets/sqlite3_flutter_libs/lib/sqlite3_flutter_libs.dart b/docs/assets/sqlite3_flutter_libs/lib/sqlite3_flutter_libs.dart new file mode 100644 index 00000000..a43cf6da --- /dev/null +++ b/docs/assets/sqlite3_flutter_libs/lib/sqlite3_flutter_libs.dart @@ -0,0 +1,3 @@ +Future applyWorkaroundToOpenSqlite3OnOldAndroidVersions() async { + throw 'stub!'; +} diff --git a/docs/assets/sqlite3_flutter_libs/pubspec.yaml b/docs/assets/sqlite3_flutter_libs/pubspec.yaml new file mode 100644 index 00000000..de9e42be --- /dev/null +++ b/docs/assets/sqlite3_flutter_libs/pubspec.yaml @@ -0,0 +1,6 @@ +name: sqlite3_flutter_libs +publish_to: none +description: Fake "sqlite3_flutter_libs" package so that we can import it in snippets without depending on Flutter. + +environment: + sdk: ^2.16.0 diff --git a/docs/lib/snippets/setup/database.dart b/docs/lib/snippets/setup/database.dart index 03908ae1..58661c33 100644 --- a/docs/lib/snippets/setup/database.dart +++ b/docs/lib/snippets/setup/database.dart @@ -10,6 +10,8 @@ import 'dart:io'; import 'package:drift/native.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as p; +import 'package:sqlite3/sqlite3.dart'; +import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; // ... the TodoItems table definition stays the same // #enddocregion open @@ -47,6 +49,18 @@ LazyDatabase _openConnection() { // for your app. final dbFolder = await getApplicationDocumentsDirectory(); final file = File(p.join(dbFolder.path, 'db.sqlite')); + + // Also work around limitations on old Android versions + if (Platform.isAndroid) { + await applyWorkaroundToOpenSqlite3OnOldAndroidVersions(); + + final cachebase = (await getTemporaryDirectory()).path; + + // We can't access /tmp on Android, which sqlite3 would try by default. + // Explicitly tell it about the correct temporary directory. + sqlite3.tempDirectory = cachebase; + } + return NativeDatabase.createInBackground(file); }); } diff --git a/docs/pages/docs/setup.md b/docs/pages/docs/setup.md index 81c9836f..6142f1df 100644 --- a/docs/pages/docs/setup.md +++ b/docs/pages/docs/setup.md @@ -96,6 +96,11 @@ now looks like this: {% include "blocks/snippet" snippets = snippets name = 'open' %} +The Android-specific workarounds are necessary because sqlite3 attempts to use `/tmp` to store +private data on unix-like systems, which is forbidden on Android. We also use this opportunity +to work around a problem some older Android devices have with loading custom libraries through +`dart:ffi`. + ## Next steps Congratulations! With this setup complete, your project is ready to use drift. diff --git a/docs/pubspec.yaml b/docs/pubspec.yaml index 6bdad56d..a6b01865 100644 --- a/docs/pubspec.yaml +++ b/docs/pubspec.yaml @@ -19,9 +19,11 @@ dependencies: # used in snippets http: ^1.1.0 sqlite3: ^2.0.0 - # Fake path_provider for snippets + # Fake flutter packages for snippets path_provider: path: assets/path_provider + sqlite3_flutter_libs: + path: assets/sqlite3_flutter_libs # Used in examples rxdart: ^0.27.3 yaml: ^3.1.1 diff --git a/examples/app/android/app/build.gradle b/examples/app/android/app/build.gradle index 2a6af6eb..afc95aa0 100644 --- a/examples/app/android/app/build.gradle +++ b/examples/app/android/app/build.gradle @@ -48,6 +48,10 @@ android { targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName + + ndk { + abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64' + } } buildTypes { diff --git a/examples/app/lib/database/connection/native.dart b/examples/app/lib/database/connection/native.dart index bf5e2149..7e16a4fc 100644 --- a/examples/app/lib/database/connection/native.dart +++ b/examples/app/lib/database/connection/native.dart @@ -6,6 +6,8 @@ import 'package:drift_dev/api/migrations.dart'; import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as p; +import 'package:sqlite3/sqlite3.dart'; +import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; Future get databaseFile async { // We use `path_provider` to find a suitable path to store our data in. @@ -17,7 +19,19 @@ Future get databaseFile async { /// Obtains a database connection for running drift in a Dart VM. DatabaseConnection connect() { return DatabaseConnection.delayed(Future(() async { - return NativeDatabase.createBackgroundConnection(await databaseFile); + if (Platform.isAndroid) { + await applyWorkaroundToOpenSqlite3OnOldAndroidVersions(); + + final cachebase = (await getTemporaryDirectory()).path; + + // We can't access /tmp on Android, which sqlite3 would try by default. + // Explicitly tell it about the correct temporary directory. + sqlite3.tempDirectory = cachebase; + } + + return NativeDatabase.createBackgroundConnection( + await databaseFile, + ); })); } From 559cf986a1f2182bcfab7a8a8d7b7a25f59f1fb0 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 24 Oct 2023 22:32:39 +0200 Subject: [PATCH 08/37] sqlparser: Support `ORDER BY` in functions --- sqlparser/CHANGELOG.md | 2 ++ .../lib/src/analysis/steps/prepare_ast.dart | 32 +++++++++++++++++++ .../lib/src/ast/expressions/aggregate.dart | 13 +++++--- sqlparser/lib/src/ast/visitor.dart | 2 +- sqlparser/lib/src/engine/options.dart | 6 +++- sqlparser/lib/src/reader/parser.dart | 17 ++++++++-- sqlparser/lib/utils/node_to_text.dart | 1 + .../analysis/errors/syntax_error_test.dart | 10 ++++++ .../analysis/errors/unsupported_test.dart | 8 +++++ sqlparser/test/parser/partition_test.dart | 13 ++++++++ sqlparser/test/utils/node_to_text_test.dart | 11 ++++++- 11 files changed, 104 insertions(+), 11 deletions(-) diff --git a/sqlparser/CHANGELOG.md b/sqlparser/CHANGELOG.md index 67e179a4..73ebef5e 100644 --- a/sqlparser/CHANGELOG.md +++ b/sqlparser/CHANGELOG.md @@ -1,6 +1,8 @@ ## 0.32.1-dev - Treat the result of `sum()` as nullable when inferring types. +- Support features added in sqlite 3.44: + - `ORDER BY` clauses as part of aggregate functions. ## 0.32.0 diff --git a/sqlparser/lib/src/analysis/steps/prepare_ast.dart b/sqlparser/lib/src/analysis/steps/prepare_ast.dart index 4f56235a..493350b9 100644 --- a/sqlparser/lib/src/analysis/steps/prepare_ast.dart +++ b/sqlparser/lib/src/analysis/steps/prepare_ast.dart @@ -19,6 +19,22 @@ class AstPreparingVisitor extends RecursiveVisitor { resolveIndexOfVariables(_foundVariables); } + @override + void visitAggregateFunctionInvocation( + AggregateFunctionInvocation e, void arg) { + if (e.orderBy != null && + context.engineOptions.version < SqliteVersion.v3_44) { + context.reportError(AnalysisError( + type: AnalysisErrorType.notSupportedInDesiredVersion, + message: + 'ORDER BY in aggregate functions require sqlite 3.44 or later.', + relevantNode: e.orderBy, + )); + } + + super.visitAggregateFunctionInvocation(e, arg); + } + @override void defaultInsertSource(InsertSource e, void arg) { e.scope = SourceScope(e.parent!.statementScope); @@ -225,4 +241,20 @@ class AstPreparingVisitor extends RecursiveVisitor { super.visitDriftSpecificNode(e, arg); } + + @override + void visitWindowFunctionInvocation(WindowFunctionInvocation e, void arg) { + // Window functions can't use ORDER BY in their arguments: + // https://github.com/sqlite/sqlite/blob/85b1f5c2f6a05ba151496122fc62b10d560498ca/src/expr.c#L1231-L1235 + if (e.orderBy != null) { + context.reportError(AnalysisError( + type: AnalysisErrorType.synctactic, + message: + 'Window functions may not use `ORDER BY` in their parameter list', + relevantNode: e.orderBy, + )); + } + + super.visitWindowFunctionInvocation(e, arg); + } } diff --git a/sqlparser/lib/src/ast/expressions/aggregate.dart b/sqlparser/lib/src/ast/expressions/aggregate.dart index b107cce0..8f365319 100644 --- a/sqlparser/lib/src/ast/expressions/aggregate.dart +++ b/sqlparser/lib/src/ast/expressions/aggregate.dart @@ -12,6 +12,7 @@ class AggregateFunctionInvocation extends Expression @override FunctionParameters parameters; + OrderByBase? orderBy; Expression? filter; @override @@ -20,6 +21,7 @@ class AggregateFunctionInvocation extends Expression AggregateFunctionInvocation({ required this.function, required this.parameters, + this.orderBy, this.filter, }) : nameToken = function; @@ -31,6 +33,7 @@ class AggregateFunctionInvocation extends Expression @override void transformChildren(Transformer transformer, A arg) { parameters = transformer.transformChild(parameters, this, arg); + orderBy = transformer.transformNullableChild(orderBy, this, arg); filter = transformer.transformNullableChild(filter, this, arg); } @@ -61,14 +64,14 @@ class WindowFunctionInvocation extends AggregateFunctionInvocation { final String? windowName; WindowFunctionInvocation( - {required IdentifierToken function, - required FunctionParameters parameters, - Expression? filter, + {required super.function, + required super.parameters, + super.orderBy, + super.filter, this.windowDefinition, this.windowName}) // one of window definition or name must be null - : assert((windowDefinition == null) || (windowName == null)), - super(function: function, parameters: parameters, filter: filter); + : assert((windowDefinition == null) || (windowName == null)); @override R accept(AstVisitor visitor, A arg) { diff --git a/sqlparser/lib/src/ast/visitor.dart b/sqlparser/lib/src/ast/visitor.dart index 48adfb97..3e0f4232 100644 --- a/sqlparser/lib/src/ast/visitor.dart +++ b/sqlparser/lib/src/ast/visitor.dart @@ -505,7 +505,7 @@ class RecursiveVisitor implements AstVisitor { @override R? visitWindowFunctionInvocation(WindowFunctionInvocation e, A arg) { - return visitExpressionInvocation(e, arg); + return visitAggregateFunctionInvocation(e, arg); } @override diff --git a/sqlparser/lib/src/engine/options.dart b/sqlparser/lib/src/engine/options.dart index 8ef86010..edcfe26b 100644 --- a/sqlparser/lib/src/engine/options.dart +++ b/sqlparser/lib/src/engine/options.dart @@ -93,6 +93,10 @@ class SqliteVersion implements Comparable { /// can't provide analysis warnings when using recent sqlite3 features. static const SqliteVersion minimum = SqliteVersion.v3(34); + /// Version `3.44.0` has added `ORDER BY` clauses as parameters for aggregate + /// functions and more SQL functions. + static const SqliteVersion v3_44 = SqliteVersion.v3(44); + /// Version `3.43.0` added the built-in `timediff` and `octet_length` /// functions. static const SqliteVersion v3_43 = SqliteVersion.v3(43); @@ -118,7 +122,7 @@ class SqliteVersion implements Comparable { /// The highest sqlite version supported by this `sqlparser` package. /// /// Newer features in `sqlite3` may not be recognized by this library. - static const SqliteVersion current = v3_43; + static const SqliteVersion current = v3_44; /// The major version of sqlite. /// diff --git a/sqlparser/lib/src/reader/parser.dart b/sqlparser/lib/src/reader/parser.dart index 919eb6ee..51f74f84 100644 --- a/sqlparser/lib/src/reader/parser.dart +++ b/sqlparser/lib/src/reader/parser.dart @@ -922,11 +922,17 @@ class Parser { } else if (_matchOne(TokenType.leftParen)) { // We have something like "foo(" -> that's a function! final parameters = _functionParameters(); + + // Aggregate functions can use `ORDER BY` in their argument list. + final orderBy = _orderBy(); + final rightParen = _consume( TokenType.rightParen, 'Expected closing bracket after argument list'); - if (_peek.type == TokenType.filter || _peek.type == TokenType.over) { - return _aggregate(first, parameters); + if (orderBy != null || + _peek.type == TokenType.filter || + _peek.type == TokenType.over) { + return _aggregate(first, parameters, orderBy); } return FunctionExpression(name: first.identifier, parameters: parameters) @@ -980,7 +986,10 @@ class Parser { } AggregateFunctionInvocation _aggregate( - IdentifierToken name, FunctionParameters params) { + IdentifierToken name, + FunctionParameters params, + OrderByBase? orderBy, + ) { Expression? filter; // https://www.sqlite.org/syntax/filter.html (it's optional) @@ -1005,6 +1014,7 @@ class Parser { return WindowFunctionInvocation( function: name, parameters: params, + orderBy: orderBy, filter: filter, windowDefinition: window, windowName: windowName, @@ -1013,6 +1023,7 @@ class Parser { return AggregateFunctionInvocation( function: name, parameters: params, + orderBy: orderBy, filter: filter, )..setSpan(name, _previous); } diff --git a/sqlparser/lib/utils/node_to_text.dart b/sqlparser/lib/utils/node_to_text.dart index c54ac376..229a9211 100644 --- a/sqlparser/lib/utils/node_to_text.dart +++ b/sqlparser/lib/utils/node_to_text.dart @@ -39,6 +39,7 @@ class NodeSqlBuilder extends AstVisitor { symbol('('); visit(e.parameters, arg); + visitNullable(e.orderBy, arg); symbol(')'); if (e.filter != null) { diff --git a/sqlparser/test/analysis/errors/syntax_error_test.dart b/sqlparser/test/analysis/errors/syntax_error_test.dart index e9ba8041..0b29c60e 100644 --- a/sqlparser/test/analysis/errors/syntax_error_test.dart +++ b/sqlparser/test/analysis/errors/syntax_error_test.dart @@ -128,4 +128,14 @@ END; }); }); }); + + test('window function with order by', () { + final engine = SqlEngine(EngineOptions(version: SqliteVersion.v3_44)) + ..registerTable(demoTable); + + engine + .analyze('SELECT group_concat(content ORDER BY id DESC) ' + 'OVER (ROWS UNBOUNDED PRECEDING) FROM demo;') + .expectError('ORDER BY id DESC'); + }); } diff --git a/sqlparser/test/analysis/errors/unsupported_test.dart b/sqlparser/test/analysis/errors/unsupported_test.dart index f5d54bdf..123831fe 100644 --- a/sqlparser/test/analysis/errors/unsupported_test.dart +++ b/sqlparser/test/analysis/errors/unsupported_test.dart @@ -155,4 +155,12 @@ void main() { currentEngine.analyze(right).expectNoError(); currentEngine.analyze(full).expectNoError(); }); + + test('warns about aggregate functions with order by', () { + const sql = "SELECT group_concat(content ORDER BY id) FROM demo"; + + minimumEngine.analyze(sql).expectError('ORDER BY id', + type: AnalysisErrorType.notSupportedInDesiredVersion); + currentEngine.analyze(sql).expectNoError(); + }); } diff --git a/sqlparser/test/parser/partition_test.dart b/sqlparser/test/parser/partition_test.dart index 2c595cd0..ce3ebbf7 100644 --- a/sqlparser/test/parser/partition_test.dart +++ b/sqlparser/test/parser/partition_test.dart @@ -65,6 +65,19 @@ final Map _testCases = { BooleanLiteral(true), ), ), + "string_agg(foo, ', ' ORDER BY foo DESC)": AggregateFunctionInvocation( + function: identifier('string_agg'), + parameters: ExprFunctionParameters(parameters: [ + Reference(columnName: 'foo'), + StringLiteral(', '), + ]), + orderBy: OrderBy(terms: [ + OrderingTerm( + expression: Reference(columnName: 'foo'), + orderingMode: OrderingMode.descending, + ), + ]), + ), }; void main() { diff --git a/sqlparser/test/utils/node_to_text_test.dart b/sqlparser/test/utils/node_to_text_test.dart index 184f9c6e..9c221cbf 100644 --- a/sqlparser/test/utils/node_to_text_test.dart +++ b/sqlparser/test/utils/node_to_text_test.dart @@ -82,7 +82,7 @@ END; test('after update of when', () { testFormat(''' -CREATE TRIGGER IF NOT EXISTS my_trigger AFTER UPDATE OF c1, c2 ON t1 +CREATE TRIGGER IF NOT EXISTS my_trigger AFTER UPDATE OF c1, c2 ON t1 WHEN foo = bar BEGIN SELECT * FROM t2; @@ -309,6 +309,15 @@ CREATE UNIQUE INDEX my_idx ON t1 (c1, c2, c3) WHERE c1 < c3; '''); }); + test('aggregate with order by', () { + testFormat(''' + SELECT + string_agg(foo, ',' ORDER BY foo DESC, bar), + string_agg(foo, ',' ORDER BY foo DESC) FILTER (WHERE foo > 10) + FROM bar + '''); + }); + group('joins', () { for (final kind in ['LEFT', 'RIGHT', 'FULL']) { test(kind, () { From 222dc3063e75095939cbd0502746bf89461ac4ce Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 24 Oct 2023 22:56:12 +0200 Subject: [PATCH 09/37] sqlparser: Support new SQL functions --- sqlparser/CHANGELOG.md | 1 + sqlparser/lib/src/analysis/steps/linting_visitor.dart | 1 + sqlparser/lib/src/analysis/types/resolving_visitor.dart | 9 +++++++++ sqlparser/test/analysis/types2/misc_cases_test.dart | 6 ++++++ 4 files changed, 17 insertions(+) diff --git a/sqlparser/CHANGELOG.md b/sqlparser/CHANGELOG.md index 73ebef5e..55a727d8 100644 --- a/sqlparser/CHANGELOG.md +++ b/sqlparser/CHANGELOG.md @@ -3,6 +3,7 @@ - Treat the result of `sum()` as nullable when inferring types. - Support features added in sqlite 3.44: - `ORDER BY` clauses as part of aggregate functions. + - Support `concat`, `concat_ws` and `string_agg`. ## 0.32.0 diff --git a/sqlparser/lib/src/analysis/steps/linting_visitor.dart b/sqlparser/lib/src/analysis/steps/linting_visitor.dart index 93e01add..8fa725b0 100644 --- a/sqlparser/lib/src/analysis/steps/linting_visitor.dart +++ b/sqlparser/lib/src/analysis/steps/linting_visitor.dart @@ -278,6 +278,7 @@ class LintingVisitor extends RecursiveVisitor { 'format' || 'unixepoch' => SqliteVersion.v3_38, 'unhex' => SqliteVersion.v3_41, 'timediff' || 'octet_length' => SqliteVersion.v3_43, + 'concat' || 'concat_ws' || 'string_agg' => SqliteVersion.v3_44, _ => null, }; diff --git a/sqlparser/lib/src/analysis/types/resolving_visitor.dart b/sqlparser/lib/src/analysis/types/resolving_visitor.dart index c67b7639..1c0454f9 100644 --- a/sqlparser/lib/src/analysis/types/resolving_visitor.dart +++ b/sqlparser/lib/src/analysis/types/resolving_visitor.dart @@ -573,7 +573,16 @@ class TypeResolver extends RecursiveVisitor { case 'upper': nullableIfChildIs(); return _textType.withoutNullabilityInfo; + case 'concat': + return _textType; + case 'concat_ws': + // null if the first argument is null + if (params.isNotEmpty) { + session._addRelation(NullableIfSomeOtherIs(e, [params.first])); + } + return _textType.withoutNullabilityInfo; case 'group_concat': + case 'string_agg': return _textType.withNullable(true); case 'date': case 'time': diff --git a/sqlparser/test/analysis/types2/misc_cases_test.dart b/sqlparser/test/analysis/types2/misc_cases_test.dart index 322b920a..a871f4b3 100644 --- a/sqlparser/test/analysis/types2/misc_cases_test.dart +++ b/sqlparser/test/analysis/types2/misc_cases_test.dart @@ -66,6 +66,12 @@ const Map _types = { "SELECT unhex('ab') = ?": ResolvedType(type: BasicType.blob, nullable: true), 'SELECT unhex(?)': ResolvedType(type: BasicType.text), 'SELECT 1 GROUP BY 1 HAVING ? ': ResolvedType.bool(), + "SELECT concat(1, NULL, 2) = ?": + ResolvedType(type: BasicType.text, nullable: false), + "SELECT concat_ws(',', 1, 2) = ?": + ResolvedType(type: BasicType.text, nullable: false), + "SELECT concat_ws(NULL, 1, 2) = ?": + ResolvedType(type: BasicType.text, nullable: true), }; SqlEngine _spawnEngine() { From 0973aa982cc0c8f7549377f1e761bad2ce103351 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 25 Oct 2023 22:08:55 +0200 Subject: [PATCH 10/37] Don't use invalid nullable converter fields --- drift/test/test_utils/test_utils.mocks.dart | 19 ------- drift_dev/CHANGELOG.md | 2 + .../lib/src/analysis/results/column.dart | 6 +- .../lib/src/writer/tables/table_writer.dart | 2 +- drift_dev/lib/src/writer/writer.dart | 55 ++++++++++--------- drift_dev/test/writer/writer_test.dart | 32 +++++++++++ 6 files changed, 70 insertions(+), 46 deletions(-) diff --git a/drift/test/test_utils/test_utils.mocks.dart b/drift/test/test_utils/test_utils.mocks.dart index 0e96ecb1..fad373d8 100644 --- a/drift/test/test_utils/test_utils.mocks.dart +++ b/drift/test/test_utils/test_utils.mocks.dart @@ -64,7 +64,6 @@ class MockDatabaseDelegate extends _i1.Mock implements _i2.DatabaseDelegate { returnValue: false, returnValueForMissingStub: false, ) as bool); - @override set isInTransaction(bool? _isInTransaction) => super.noSuchMethod( Invocation.setter( @@ -73,7 +72,6 @@ class MockDatabaseDelegate extends _i1.Mock implements _i2.DatabaseDelegate { ), returnValueForMissingStub: null, ); - @override _i2.DbVersionDelegate get versionDelegate => (super.noSuchMethod( Invocation.getter(#versionDelegate), @@ -86,7 +84,6 @@ class MockDatabaseDelegate extends _i1.Mock implements _i2.DatabaseDelegate { Invocation.getter(#versionDelegate), ), ) as _i2.DbVersionDelegate); - @override _i2.TransactionDelegate get transactionDelegate => (super.noSuchMethod( Invocation.getter(#transactionDelegate), @@ -99,14 +96,12 @@ class MockDatabaseDelegate extends _i1.Mock implements _i2.DatabaseDelegate { Invocation.getter(#transactionDelegate), ), ) as _i2.TransactionDelegate); - @override _i4.FutureOr get isOpen => (super.noSuchMethod( Invocation.getter(#isOpen), returnValue: _i4.Future.value(false), returnValueForMissingStub: _i4.Future.value(false), ) as _i4.FutureOr); - @override _i4.Future open(_i5.QueryExecutorUser? db) => (super.noSuchMethod( Invocation.method( @@ -116,7 +111,6 @@ class MockDatabaseDelegate extends _i1.Mock implements _i2.DatabaseDelegate { returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); - @override _i4.Future close() => (super.noSuchMethod( Invocation.method( @@ -126,7 +120,6 @@ class MockDatabaseDelegate extends _i1.Mock implements _i2.DatabaseDelegate { returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); - @override void notifyDatabaseOpened(_i5.OpeningDetails? details) => super.noSuchMethod( Invocation.method( @@ -135,7 +128,6 @@ class MockDatabaseDelegate extends _i1.Mock implements _i2.DatabaseDelegate { ), returnValueForMissingStub: null, ); - @override _i4.Future<_i3.QueryResult> runSelect( String? statement, @@ -171,7 +163,6 @@ class MockDatabaseDelegate extends _i1.Mock implements _i2.DatabaseDelegate { ), )), ) as _i4.Future<_i3.QueryResult>); - @override _i4.Future runUpdate( String? statement, @@ -188,7 +179,6 @@ class MockDatabaseDelegate extends _i1.Mock implements _i2.DatabaseDelegate { returnValue: _i4.Future.value(0), returnValueForMissingStub: _i4.Future.value(0), ) as _i4.Future); - @override _i4.Future runInsert( String? statement, @@ -205,7 +195,6 @@ class MockDatabaseDelegate extends _i1.Mock implements _i2.DatabaseDelegate { returnValue: _i4.Future.value(0), returnValueForMissingStub: _i4.Future.value(0), ) as _i4.Future); - @override _i4.Future runCustom( String? statement, @@ -222,7 +211,6 @@ class MockDatabaseDelegate extends _i1.Mock implements _i2.DatabaseDelegate { returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); - @override _i4.Future runBatched(_i5.BatchedStatements? statements) => (super.noSuchMethod( @@ -246,7 +234,6 @@ class MockDynamicVersionDelegate extends _i1.Mock returnValue: _i4.Future.value(0), returnValueForMissingStub: _i4.Future.value(0), ) as _i4.Future); - @override _i4.Future setSchemaVersion(int? version) => (super.noSuchMethod( Invocation.method( @@ -269,7 +256,6 @@ class MockSupportedTransactionDelegate extends _i1.Mock returnValue: false, returnValueForMissingStub: false, ) as bool); - @override _i4.FutureOr startTransaction( _i4.Future Function(_i2.QueryDelegate)? run) => @@ -298,7 +284,6 @@ class MockStreamQueries extends _i1.Mock implements _i6.StreamQueryStore { returnValueForMissingStub: _i4.Stream>>.empty(), ) as _i4.Stream>>); - @override _i4.Stream> updatesForSync( _i5.TableUpdateQuery? query) => @@ -310,7 +295,6 @@ class MockStreamQueries extends _i1.Mock implements _i6.StreamQueryStore { returnValue: _i4.Stream>.empty(), returnValueForMissingStub: _i4.Stream>.empty(), ) as _i4.Stream>); - @override void handleTableUpdates(Set<_i5.TableUpdate>? updates) => super.noSuchMethod( Invocation.method( @@ -319,7 +303,6 @@ class MockStreamQueries extends _i1.Mock implements _i6.StreamQueryStore { ), returnValueForMissingStub: null, ); - @override void markAsClosed( _i6.QueryStream? stream, @@ -335,7 +318,6 @@ class MockStreamQueries extends _i1.Mock implements _i6.StreamQueryStore { ), returnValueForMissingStub: null, ); - @override void markAsOpened(_i6.QueryStream? stream) => super.noSuchMethod( Invocation.method( @@ -344,7 +326,6 @@ class MockStreamQueries extends _i1.Mock implements _i6.StreamQueryStore { ), returnValueForMissingStub: null, ); - @override _i4.Future close() => (super.noSuchMethod( Invocation.method( diff --git a/drift_dev/CHANGELOG.md b/drift_dev/CHANGELOG.md index df13dbef..36f4c17e 100644 --- a/drift_dev/CHANGELOG.md +++ b/drift_dev/CHANGELOG.md @@ -1,6 +1,8 @@ ## 2.13.1-dev - Add `has_separate_analyzer` option to optimize builds using the `not_shared` builder. +- Avoid illegal references to implicitly nullable variant of type converter when no such + field exists. ## 2.13.0 diff --git a/drift_dev/lib/src/analysis/results/column.dart b/drift_dev/lib/src/analysis/results/column.dart index d34c3ad8..880de9b9 100644 --- a/drift_dev/lib/src/analysis/results/column.dart +++ b/drift_dev/lib/src/analysis/results/column.dart @@ -192,7 +192,11 @@ class AppliedTypeConverter { /// column, drift generates a new wrapped type converter which will deal with /// `null` values. /// That converter is stored in this field. - String get nullableFieldName => '${fieldName}n'; + String get nullableFieldName { + assert(canBeSkippedForNulls && owningColumn?.nullable == true); + + return '${fieldName}n'; + } AppliedTypeConverter({ required this.expression, diff --git a/drift_dev/lib/src/writer/tables/table_writer.dart b/drift_dev/lib/src/writer/tables/table_writer.dart index 73d9f761..545ce6a8 100644 --- a/drift_dev/lib/src/writer/tables/table_writer.dart +++ b/drift_dev/lib/src/writer/tables/table_writer.dart @@ -49,7 +49,7 @@ abstract class TableOrViewWriter { buffer.write('static $typeName ${converter.fieldName} = $code;'); // Generate wrappers for non-nullable type converters that are applied to - // nullable converters. + // nullable columns. final column = converter.owningColumn!; if (converter.canBeSkippedForNulls && column.nullable) { final nullableTypeName = emitter.dartCode( diff --git a/drift_dev/lib/src/writer/writer.dart b/drift_dev/lib/src/writer/writer.dart index e64b8953..d3f00c4d 100644 --- a/drift_dev/lib/src/writer/writer.dart +++ b/drift_dev/lib/src/writer/writer.dart @@ -99,34 +99,39 @@ abstract class _NodeOrWriter { /// Returns a Dart expression evaluating to the [converter]. AnnotatedDartCode readConverter(AppliedTypeConverter converter, {bool forNullable = false}) { - if (converter.owningColumn == null) { - // Type converters applied to individual columns in the result set of a - // `SELECT` query don't have an owning table. We instead write the - // expression here. - return AnnotatedDartCode.build((b) { - final implicitlyNullable = - converter.canBeSkippedForNulls && forNullable; + return AnnotatedDartCode.build((b) { + final owningColumn = converter.owningColumn; + final needsImplicitNullableVersion = + forNullable && converter.canBeSkippedForNulls; + final hasNullableVariantInField = owningColumn != null && + converter.canBeSkippedForNulls && + owningColumn.nullable; - if (implicitlyNullable) { + void addRegularConverter() { + if (owningColumn != null) { + b + ..addCode(entityInfoType(owningColumn.owner)) + ..addText('.${converter.fieldName}'); + } else { + // There's no field storing this converter, so evaluate it every time + // it is used. + b.addCode(converter.expression); + } + } + + switch ((needsImplicitNullableVersion, hasNullableVariantInField)) { + case (false, _): + addRegularConverter(); + case (true, false): b.addSymbol('NullAwareTypeConverter.wrap(', AnnotatedDartCode.drift); - } - - b.addCode(converter.expression); - - if (implicitlyNullable) { + b.addCode(converter.expression); b.addText(')'); - } - }); - } else { - final fieldName = forNullable && converter.canBeSkippedForNulls - ? converter.nullableFieldName - : converter.fieldName; - - return AnnotatedDartCode([ - ...entityInfoType(converter.owningColumn!.owner).elements, - '.$fieldName', - ]); - } + case (true, true): + b + ..addCode(entityInfoType(converter.owningColumn!.owner)) + ..addText('.${converter.nullableFieldName}'); + } + }); } /// A suitable typename to store an instance of the type converter used here. diff --git a/drift_dev/test/writer/writer_test.dart b/drift_dev/test/writer/writer_test.dart index 39807073..8b945da9 100644 --- a/drift_dev/test/writer/writer_test.dart +++ b/drift_dev/test/writer/writer_test.dart @@ -88,4 +88,36 @@ class Database {} )) }, result.dartOutputs, result.writer); }, skip: requireDart('3.0.0-dev')); + + test( + 'references nullable variant of converter on non-nullable column', + () async { + final result = await emulateDriftBuild( + inputs: { + 'a|lib/converter.dart': ''' +import 'package:drift/drift.dart'; + +TypeConverter get testConverter => throw ''; +''', + 'a|lib/a.drift': ''' +import 'converter.dart'; + +CREATE TABLE foo ( + bar TEXT MAPPED BY `testConverter` NOT NULL +); + +CREATE VIEW a AS SELECT nullif(bar, '') FROM foo; +''', + }, + modularBuild: true, + logger: loggerThat(neverEmits(anything)), + ); + + checkOutputs({ + 'a|lib/a.drift.dart': decodedMatches( + allOf(isNot(contains('converterbarn'))), + ), + }, result.dartOutputs, result.writer); + }, + ); } From ca4e376c8b35d666424928fea43dc1cc2ef65196 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 25 Oct 2023 22:17:32 +0200 Subject: [PATCH 11/37] Fix invalid snippet on website (closes #2698) --- docs/lib/snippets/drift_files/custom_queries.dart | 10 ++++++++++ docs/pages/docs/SQL API/custom_queries.md | 11 ++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/lib/snippets/drift_files/custom_queries.dart b/docs/lib/snippets/drift_files/custom_queries.dart index 3af53333..852e9b60 100644 --- a/docs/lib/snippets/drift_files/custom_queries.dart +++ b/docs/lib/snippets/drift_files/custom_queries.dart @@ -31,6 +31,16 @@ class MyDatabase extends $MyDatabase { MyDatabase(QueryExecutor e) : super(e); + // #docregion amountOfTodosInCategory + Stream amountOfTodosInCategory(int id) { + return customSelect( + 'SELECT COUNT(*) AS c FROM todo_items WHERE category = ?', + variables: [Variable.withInt(id)], + readsFrom: {todoItems}, + ).map((row) => row.read('c')).watchSingle(); + } + // #enddocregion amountOfTodosInCategory + // #docregion run Future useGeneratedQuery() async { // The generated query can be run once as a future: diff --git a/docs/pages/docs/SQL API/custom_queries.md b/docs/pages/docs/SQL API/custom_queries.md index 167cb77a..422f15ed 100644 --- a/docs/pages/docs/SQL API/custom_queries.md +++ b/docs/pages/docs/SQL API/custom_queries.md @@ -84,15 +84,8 @@ items. You can also bind SQL variables by using question-mark placeholders and the `variables` parameter: -```dart -Stream amountOfTodosInCategory(int id) { - return customSelect( - 'SELECT COUNT(*) AS c FROM todos WHERE category = ?', - variables: [Variable.withInt(id)], - readsFrom: {todos}, - ).map((row) => row.read('c')).watch(); -} -``` +{% include "blocks/snippet" snippets = snippets name = "amountOfTodosInCategory" %} + Of course, you can also use indexed variables (like `?12`) - for more information on them, see [the sqlite3 documentation](https://sqlite.org/lang_expr.html#varparam). From a4938abb191331ab564d4cb69c7f7a311b6262f2 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 25 Oct 2023 22:19:04 +0200 Subject: [PATCH 12/37] Also apply temp directory workaround on iOS --- docs/lib/snippets/setup/database.dart | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/lib/snippets/setup/database.dart b/docs/lib/snippets/setup/database.dart index 58661c33..fe45fc4d 100644 --- a/docs/lib/snippets/setup/database.dart +++ b/docs/lib/snippets/setup/database.dart @@ -53,14 +53,15 @@ LazyDatabase _openConnection() { // Also work around limitations on old Android versions if (Platform.isAndroid) { await applyWorkaroundToOpenSqlite3OnOldAndroidVersions(); - - final cachebase = (await getTemporaryDirectory()).path; - - // We can't access /tmp on Android, which sqlite3 would try by default. - // Explicitly tell it about the correct temporary directory. - sqlite3.tempDirectory = cachebase; } + // Make sqlite3 pick a more suitable location for temporary files - the + // one from the system may be inaccessible due to sandboxing. + final cachebase = (await getTemporaryDirectory()).path; + // We can't access /tmp on Android, which sqlite3 would try by default. + // Explicitly tell it about the correct temporary directory. + sqlite3.tempDirectory = cachebase; + return NativeDatabase.createInBackground(file); }); } From ddc864a7c21c0b3845f8b1493ab64e121058c0b1 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 29 Oct 2023 19:32:23 +0100 Subject: [PATCH 13/37] Prepare drift_dev and sqlparser release --- drift_dev/CHANGELOG.md | 2 +- drift_dev/pubspec.yaml | 2 +- sqlparser/CHANGELOG.md | 2 +- sqlparser/pubspec.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/drift_dev/CHANGELOG.md b/drift_dev/CHANGELOG.md index 36f4c17e..85403030 100644 --- a/drift_dev/CHANGELOG.md +++ b/drift_dev/CHANGELOG.md @@ -1,4 +1,4 @@ -## 2.13.1-dev +## 2.13.1 - Add `has_separate_analyzer` option to optimize builds using the `not_shared` builder. - Avoid illegal references to implicitly nullable variant of type converter when no such diff --git a/drift_dev/pubspec.yaml b/drift_dev/pubspec.yaml index 67b5a501..bc9f9d98 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-dev +version: 2.13.1 repository: https://github.com/simolus3/drift homepage: https://drift.simonbinder.eu/ issue_tracker: https://github.com/simolus3/drift/issues diff --git a/sqlparser/CHANGELOG.md b/sqlparser/CHANGELOG.md index 55a727d8..a7c28abd 100644 --- a/sqlparser/CHANGELOG.md +++ b/sqlparser/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.32.1-dev +## 0.32.1 - Treat the result of `sum()` as nullable when inferring types. - Support features added in sqlite 3.44: diff --git a/sqlparser/pubspec.yaml b/sqlparser/pubspec.yaml index 07077868..56ab6dfb 100644 --- a/sqlparser/pubspec.yaml +++ b/sqlparser/pubspec.yaml @@ -1,6 +1,6 @@ name: sqlparser description: Parses sqlite statements and performs static analysis on them -version: 0.32.1-dev +version: 0.32.1 homepage: https://github.com/simolus3/drift/tree/develop/sqlparser repository: https://github.com/simolus3/drift #homepage: https://drift.simonbinder.eu/ From 7106dfc519fc7b4d981b722f2bfa16534dbdcf70 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 31 Oct 2023 22:10:51 +0100 Subject: [PATCH 14/37] Update to postgres 3.0 beta --- docs/lib/snippets/platforms/postgres.dart | 2 +- extras/drift_postgres/example/main.dart | 2 +- extras/drift_postgres/lib/src/pg_database.dart | 4 ++-- extras/drift_postgres/pubspec.yaml | 2 +- extras/drift_postgres/test/drift_postgres_test.dart | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/lib/snippets/platforms/postgres.dart b/docs/lib/snippets/platforms/postgres.dart index b7060003..24365e4e 100644 --- a/docs/lib/snippets/platforms/postgres.dart +++ b/docs/lib/snippets/platforms/postgres.dart @@ -26,7 +26,7 @@ void main() async { username: 'postgres', password: 'postgres', ), - sessionSettings: SessionSettings( + settings: ConnectionSettings( // If you expect to talk to a Postgres database over a public connection, // please use SslMode.verifyFull instead. sslMode: SslMode.disable, diff --git a/extras/drift_postgres/example/main.dart b/extras/drift_postgres/example/main.dart index b21447e8..4927c4d5 100644 --- a/extras/drift_postgres/example/main.dart +++ b/extras/drift_postgres/example/main.dart @@ -26,7 +26,7 @@ void main() async { username: 'postgres', password: 'postgres', ), - sessionSettings: pg.SessionSettings( + settings: pg.ConnectionSettings( // If you expect to talk to a Postgres database over a public connection, // please use SslMode.verifyFull instead. sslMode: pg.SslMode.disable, diff --git a/extras/drift_postgres/lib/src/pg_database.dart b/extras/drift_postgres/lib/src/pg_database.dart index bdebb372..afc30f14 100644 --- a/extras/drift_postgres/lib/src/pg_database.dart +++ b/extras/drift_postgres/lib/src/pg_database.dart @@ -8,12 +8,12 @@ import 'package:postgres/postgres.dart'; class PgDatabase extends DelegatedDatabase { PgDatabase({ required Endpoint endpoint, - SessionSettings? sessionSettings, + ConnectionSettings? settings, bool logStatements = false, bool enableMigrations = true, }) : super( _PgDelegate( - () => Connection.open(endpoint, sessionSettings: sessionSettings), + () => Connection.open(endpoint, settings: settings), true, enableMigrations, ), diff --git a/extras/drift_postgres/pubspec.yaml b/extras/drift_postgres/pubspec.yaml index 7a2e270a..a5241a3e 100644 --- a/extras/drift_postgres/pubspec.yaml +++ b/extras/drift_postgres/pubspec.yaml @@ -11,7 +11,7 @@ environment: dependencies: collection: ^1.16.0 drift: ^2.0.0 - postgres: ^3.0.0-0 + postgres: ^3.0.0-beta.1 meta: ^1.8.0 uuid: ^4.1.0 diff --git a/extras/drift_postgres/test/drift_postgres_test.dart b/extras/drift_postgres/test/drift_postgres_test.dart index 1f416a94..44474ac4 100644 --- a/extras/drift_postgres/test/drift_postgres_test.dart +++ b/extras/drift_postgres/test/drift_postgres_test.dart @@ -18,7 +18,7 @@ class PgExecutor extends TestExecutor { username: 'postgres', password: 'postgres', ), - sessionSettings: pg.SessionSettings(sslMode: pg.SslMode.disable), + settings: pg.ConnectionSettings(sslMode: pg.SslMode.disable), )); } From f043e2997aff4270b9bb1718524debdaae87b078 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 31 Oct 2023 22:16:54 +0100 Subject: [PATCH 15/37] Set up automated publishing for drift_postgres --- .github/workflows/publish.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1ef25d54..2ab19489 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,6 +6,7 @@ on: - 'drift-[0-9]+.[0-9]+.[0-9]+*' - 'drift_dev-[0-9]+.[0-9]+.[0-9]+*' - 'sqlparser-[0-9]+.[0-9]+.[0-9]+*' + - 'drift_postgres-[0-9]+.[0-9]+.[0-9]+*' jobs: setup: @@ -87,3 +88,22 @@ jobs: - run: dart pub get - run: dart pub lish --dry-run - run: dart pub lish -f + + publish_drift_postgres: + if: "${{ startsWith(github.ref_name, 'drift_postgres-') }}" + needs: [setup] + runs-on: ubuntu-latest + environment: pub.dev + permissions: + id-token: write # Required for authentication using OIDC + defaults: + run: + working-directory: extras/drift_postgres + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/prepare + with: + dart_version: ${{ needs.setup.outputs.dart_version }} + - run: dart pub get + - run: dart pub lish --dry-run + - run: dart pub lish -f From 0b0a721164dfebce1cf9e7f1563da6416eb67132 Mon Sep 17 00:00:00 2001 From: Simon Friis Vindum Date: Wed, 1 Nov 2023 10:27:01 +0100 Subject: [PATCH 16/37] Fix grammar mistakes in documentation --- CONTRIBUTING.md | 10 +++++----- README.md | 16 +++++++-------- docs/lib/snippets/drift_files/tables.drift | 2 +- docs/pages/docs/Dart API/expressions.md | 20 +++++++++---------- docs/pages/docs/Dart API/schema_inspection.md | 2 +- docs/pages/docs/Dart API/tables.md | 2 +- docs/pages/docs/Examples/relationships.md | 12 +++++------ docs/pages/docs/SQL API/custom_queries.md | 4 ++-- docs/pages/docs/SQL API/drift_files.md | 6 +++--- docs/pages/docs/faq.md | 2 +- docs/pages/docs/index.md | 8 ++++---- docs/pages/docs/setup.md | 4 ++-- docs/pages/index.html | 4 ++-- docs/pages/v2.html | 6 +++--- drift/README.md | 10 +++++----- sqlparser/README.md | 16 +++++++-------- 16 files changed, 62 insertions(+), 62 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ccdc8af3..32e45879 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,7 +37,7 @@ The project is divided into multiple modules: - `moor_flutter/`: Contains a Flutter implementation for the database. - `drift_dev/`: Creates table, database and dao classes from the table structure and compiled queries. -- `sqlparser/`: Contains an sql parser and analyzer that is mostly independent of drift, +- `sqlparser/`: Contains an SQL parser and analyzer that is mostly independent of drift, but used by the generator for compiled custom queries. ## Concepts @@ -46,7 +46,7 @@ we generate three classes: 1. A class that inherits from `TableInfo` (we call this the "table class"). It contains a structural representation of the table, which includes columns (including name, type, constraints...), the primary key and so on. The idea is - that, if we have a `TableInfo` instance, we can create all kinds of sql statements. + that, if we have a `TableInfo` instance, we can create all kinds of SQL statements. 2. A class to represent a fully loaded row of a table. We call this a "data class" and it inherits from `DataClass`. 3. A class to represent partial data (e.g. for inserts or updates, where not all columns are set). This class was introduced in moor 1.5 and is called a "companion". @@ -55,7 +55,7 @@ This approach lets us write a higher-level api that uses the generated `TableInf write. For instance, the `Migrator` can write `CREATE TABLE` statements from these classes, an `UpdateStatement` will write `UPDATE` statements and so on. To write the query, we construct a `GenerationContext`, which contains a string buffer to write the query, keeps track of the introduced variables and so on. The idea is that everything that can -appear anywhere in a sql statement inherits from `Component` (for instance, `Query`, `Expression`, `Variable`, `Where`, +appear anywhere in an SQL statement inherits from `Component` (for instance, `Query`, `Expression`, `Variable`, `Where`, `OrderBy`). We can then recursively create the query by calling `Component.writeInto` for all subparts of a component. This query is then sent to a `QueryExecutor`, which is responsible for executing it and returning its result. The `QueryExecutor` is the only part that is platform specific, everything else is pure Dart that doesn't import any @@ -63,7 +63,7 @@ restricted libraries. ### Important classes A `DatabaseConnectionUser` is the central piece of a drift database instance. It contains an `SqlTypeSystem` (responsible -for mapping simple Dart objects from and to sql), the `QueryExecutor` discussed above and a `StreamQueryStore` +for mapping simple Dart objects from and to SQL), the `QueryExecutor` discussed above and a `StreamQueryStore` (responsible for keeping active queries and re-running them when a table updates). It is also the super class of `GeneratedDatabase` and `DatabaseAccessor`, which are the classes a `@UseMoor` and `@UseDao` class inherits from. Finally, the `QueryEngine` is a mixin in `DatabaseConnectionUser` that provides the `select`, `update`, `delete` methods @@ -119,7 +119,7 @@ updates that span multiple versions, we should follow these steps 2. `drift_dev` 3. (optional) `moor_flutter` -The `sqlparser` library can be published independently from drift. +The `sqlparser` library can be published independently of drift. ### Building the documentation diff --git a/README.md b/README.md index d4216d7b..45f5d78f 100644 --- a/README.md +++ b/README.md @@ -26,22 +26,22 @@ _Note: Moor has been renamed to drift_ | [![Main version](https://img.shields.io/pub/v/drift.svg)](https://pub.dev/packages/drift) | [![Generator version](https://img.shields.io/pub/v/drift_dev.svg)](https://pub.dev/packages/drift_dev) | 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 like shared preferences and Hive. 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. [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. @@ -58,10 +58,10 @@ project, I'd appreciate your [🌟 on GitHub](https://github.com/simolus3/drift/ This repository contains a number of packages making up the drift project, most notably: -- `drift`: The main runtime for drift, which provides most apis +- `drift`: The main runtime for drift, which provides most APIs. - `drift_dev`: The compiler for drift tables, databases and daos. It - also contains a fully-featured sql ide for the Dart analyzer. -- `sqlparser`: A sql parser and static analyzer, written in pure Dart. This package can be used without drift to perform analysis on sql statements. + also contains a fully-featured SQL IDE for the Dart analyzer. +- `sqlparser`: A SQL parser and static analyzer, written in pure Dart. This package can be used without drift to perform analysis on SQL statements. It's on pub at [![sqlparser](https://img.shields.io/pub/v/sqlparser.svg)](https://pub.dev/packages/sqlparser) diff --git a/docs/lib/snippets/drift_files/tables.drift b/docs/lib/snippets/drift_files/tables.drift index 7b654d2e..c3de6d49 100644 --- a/docs/lib/snippets/drift_files/tables.drift +++ b/docs/lib/snippets/drift_files/tables.drift @@ -13,7 +13,7 @@ CREATE TABLE categories ( -- You can also create an index or triggers with drift files CREATE INDEX categories_description ON categories(description); --- we can put named sql queries in here as well: +-- we can put named SQL queries in here as well: createEntry: INSERT INTO todos (title, content) VALUES (:title, :content); deleteById: DELETE FROM todos WHERE id = :id; allTodos: SELECT * FROM todos; diff --git a/docs/pages/docs/Dart API/expressions.md b/docs/pages/docs/Dart API/expressions.md index c628e182..f6784374 100644 --- a/docs/pages/docs/Dart API/expressions.md +++ b/docs/pages/docs/Dart API/expressions.md @@ -9,9 +9,9 @@ path: docs/getting-started/expressions/ template: layouts/docs/single --- -Expressions are pieces of sql that return a value when the database interprets them. +Expressions are pieces of SQL that return a value when the database interprets them. The Dart API from drift allows you to write most expressions in Dart and then convert -them to sql. Expressions are used in all kinds of situations. For instance, `where` +them to SQL. Expressions are used in all kinds of situations. For instance, `where` expects an expression that returns a boolean. In most cases, you're writing an expression that combines other expressions. Any @@ -62,7 +62,7 @@ Expression.and([ ## Arithmetic For `int` and `double` expressions, you can use the `+`, `-`, `*` and `/` operators. To -run calculations between a sql expression and a Dart value, wrap it in a `Variable`: +run calculations between an SQL expression and a Dart value, wrap it in a `Variable`: ```dart Future> canBeBought(int amount, int price) { return (select(products)..where((p) { @@ -73,7 +73,7 @@ Future> canBeBought(int amount, int price) { ``` String expressions define a `+` operator as well. Just like you would expect, it performs -a concatenation in sql. +a concatenation in SQL. For integer values, you can use `~`, `bitwiseAnd` and `bitwiseOr` to perform bitwise operations: @@ -81,7 +81,7 @@ bitwise operations: {% include "blocks/snippet" snippets = snippets name = 'bitwise' %} ## Nullability -To check whether an expression evaluates to `NULL` in sql, you can use the `isNull` extension: +To check whether an expression evaluates to `NULL` in SQL, you can use the `isNull` extension: ```dart final withoutCategories = select(todos)..where((row) => row.category.isNull()); @@ -213,7 +213,7 @@ with the `separator` argument on `groupConcat`. ## Mathematical functions and regexp When using a `NativeDatabase`, a basic set of trigonometric functions will be available. -It also defines the `REGEXP` function, which allows you to use `a REGEXP b` in sql queries. +It also defines the `REGEXP` function, which allows you to use `a REGEXP b` in SQL queries. For more information, see the [list of functions]({{ "../Platforms/vm.md#moor-only-functions" | pageUrl }}) here. ## Subqueries @@ -261,8 +261,8 @@ Drift also supports subqueries that appear in `JOIN`s, which are described in th [documentation for joins]({{ 'select.md#subqueries' | pageUrl }}). ## Custom expressions -If you want to inline custom sql into Dart queries, you can use a `CustomExpression` class. -It takes a `sql` parameter that lets you write custom expressions: +If you want to inline custom SQL into Dart queries, you can use a `CustomExpression` class. +It takes an `sql` parameter that lets you write custom expressions: ```dart const inactive = CustomExpression("julianday('now') - julianday(last_login) > 60"); select(users)..where((u) => inactive); @@ -270,5 +270,5 @@ select(users)..where((u) => inactive); _Note_: It's easy to write invalid queries by using `CustomExpressions` too much. If you feel like you need to use them because a feature you use is not available in drift, consider creating an issue -to let us know. If you just prefer sql, you could also take a look at -[compiled sql]({{ "../SQL API/custom_queries.md" | pageUrl }}) which is typesafe to use. +to let us know. If you just prefer SQL, you could also take a look at +[compiled SQL]({{ "../SQL API/custom_queries.md" | pageUrl }}) which is type-safe to use. diff --git a/docs/pages/docs/Dart API/schema_inspection.md b/docs/pages/docs/Dart API/schema_inspection.md index e3f7f26c..32b5520b 100644 --- a/docs/pages/docs/Dart API/schema_inspection.md +++ b/docs/pages/docs/Dart API/schema_inspection.md @@ -10,7 +10,7 @@ aliases: {% assign snippets = 'package:drift_docs/snippets/modular/schema_inspection.dart.excerpt.json' | readString | json_decode %} -Thanks to the typesafe table classes generated by drift, [writing SQL queries]({{ '../Dart API/select.md' | pageUrl }}) in Dart +Thanks to the type-safe table classes generated by drift, [writing SQL queries]({{ '../Dart API/select.md' | pageUrl }}) in Dart is simple and safe. However, these queries are usually written against a specific table. And while drift supports inheritance for tables, sometimes it is easier to access tables reflectively. Luckily, code generated by drift implements interfaces which can be used to do just that. diff --git a/docs/pages/docs/Dart API/tables.md b/docs/pages/docs/Dart API/tables.md index 05c77db5..659acbcc 100644 --- a/docs/pages/docs/Dart API/tables.md +++ b/docs/pages/docs/Dart API/tables.md @@ -11,7 +11,7 @@ path: /docs/getting-started/advanced_dart_tables/ {% assign setup = 'package:drift_docs/snippets/setup/database.dart.excerpt.json' | readString | json_decode %} In relational databases, tables are used to describe the structure of rows. By -adhering to a predefined schema, drift can generate typesafe code for your +adhering to a predefined schema, drift can generate type-safe code for your database. As already shown in the [setup]({{ '../setup.md#database-class' | pageUrl }}) page, drift provides APIs to declare tables in Dart: diff --git a/docs/pages/docs/Examples/relationships.md b/docs/pages/docs/Examples/relationships.md index 6927836d..7d213dd1 100644 --- a/docs/pages/docs/Examples/relationships.md +++ b/docs/pages/docs/Examples/relationships.md @@ -1,6 +1,6 @@ --- data: - title: "Many to many relationships" + title: "Many-to-many relationships" description: An example that models a shopping cart system with drift. template: layouts/docs/single --- @@ -19,16 +19,16 @@ A product can be in many shopping carts at the same time, and carts can of cours contain more than one product too. In sqlite3, there are two good ways to model many-to-many relationships -betweeen tables: +between tables: 1. The traditional way of using a third table storing every combination of products and carts. -2. A more modern way might be to store product ids in a shopping cart as a JSON +2. A more modern way might be to store product IDs in a shopping cart as a JSON array. The two approaches have different upsides and downsides. With the traditional relational way, it's easier to ensure data integrity (by, for instance, deleting -product references out of shopping carts when they a product is deleted). +product references out of shopping carts when a product is deleted). On the other hand, queries are easier to write with JSON structures. Especially when the order of products in the shopping cart is important as well, a JSON list is very helpful since rows in a table are unordered. @@ -109,11 +109,11 @@ in a single table: To select a single cart, we can use the [`json_each`](https://sqlite.org/json1.html#jeach) function from sqlite3 to "join" each item stored in the JSON array as if it were a separate -row. That way, we can efficiently lookup all items in a cart: +row. That way, we can efficiently look up all items in a cart: {% include "blocks/snippet" snippets=json name="watchCart" %} Watching all carts isn't that much harder, we just remove the `where` clause and combine all rows into a map from carts to their items: -{% include "blocks/snippet" snippets=json name="watchAllCarts" %} \ No newline at end of file +{% include "blocks/snippet" snippets=json name="watchAllCarts" %} diff --git a/docs/pages/docs/SQL API/custom_queries.md b/docs/pages/docs/SQL API/custom_queries.md index 422f15ed..3775c3a3 100644 --- a/docs/pages/docs/SQL API/custom_queries.md +++ b/docs/pages/docs/SQL API/custom_queries.md @@ -25,7 +25,7 @@ the second section gives an example for a custom query defined at runtime. ## Statements with a generated api -You can instruct drift to automatically generate a typesafe +You can instruct drift to automatically generate a type-safe API for your select, update and delete statements. Of course, you can still write custom sql manually. See the sections below for details. @@ -92,6 +92,6 @@ Of course, you can also use indexed variables (like `?12`) - for more informatio ## Custom update statements For update and delete statements, you can use `customUpdate`. Just like `customSelect`, that method -also takes a sql statement and optional variables. You can also tell drift which tables will be +also takes an SQL statement and optional variables. You can also tell drift which tables will be affected by your query using the optional `updates` parameter. That will help with other select streams, which will then update automatically. diff --git a/docs/pages/docs/SQL API/drift_files.md b/docs/pages/docs/SQL API/drift_files.md index d02f6feb..2277d3cf 100644 --- a/docs/pages/docs/SQL API/drift_files.md +++ b/docs/pages/docs/SQL API/drift_files.md @@ -22,7 +22,7 @@ template: layouts/docs/single Drift files are a new feature that lets you write all your database code in SQL. But unlike raw SQL strings you might pass to simple database clients, everything in a drift file is verified by drift's powerful SQL analyzer. -This allows you to write SQL queries safer: Drift will find mistakes in them during builds, and it will generate typesafe +This allows you to write SQL queries safer: Drift will find mistakes in them during builds, and it will generate type-safe Dart APIs for them so that you don't have to read back results manually. ## Getting started @@ -63,7 +63,7 @@ Inside of named queries, you can use variables just like you would expect with sql. We support regular variables (`?`), explicitly indexed variables (`?123`) and colon-named variables (`:id`). We don't support variables declared with @ or $. The compiler will attempt to infer the variable's type by -looking at its context. This lets drift generate typesafe apis for your +looking at its context. This lets drift generate type-safe APIs for your queries, the variables will be written as parameters to your method. When it's ambiguous, the analyzer might be unable to resolve the type of @@ -476,4 +476,4 @@ At the moment, the following statements can appear in a `.drift` file. All imports must come before DDL statements, and those must come before named queries. If you need support for another statement, or if drift rejects a query you think is valid, please -create an issue! \ No newline at end of file +create an issue! diff --git a/docs/pages/docs/faq.md b/docs/pages/docs/faq.md index 4050a751..e5d6b87e 100644 --- a/docs/pages/docs/faq.md +++ b/docs/pages/docs/faq.md @@ -124,7 +124,7 @@ Sqflite is a Flutter package that provides bindings to the sqlite api for both i and has stable api. In fact, the `moor_flutter` or `drift_sqflite` variants are built on top of sqflite. But even though sqflite has an api to construct some simple queries in Dart, drift goes a bit further by -* Generating typesafe mapping code for your queries +* Generating type-safe mapping code for your queries * Providing auto-updating streams for queries * Managing `CREATE TABLE` statements and most schema migrations * A more fluent api to compose queries diff --git a/docs/pages/docs/index.md b/docs/pages/docs/index.md index 343c1510..7b2cce12 100644 --- a/docs/pages/docs/index.md +++ b/docs/pages/docs/index.md @@ -11,20 +11,20 @@ Drift is a reactive persistence library for Dart and Flutter applications. It's of database libraries like [the sqlite3 package](https://pub.dev/packages/sqlite3), [sqflite](https://pub.dev/packages/sqflite) or [sql.js](https://github.com/sql-js/sql.js/) and provides additional features, like: -- __Type safety__: Instead of writing sql queries manually and parsing the `List>` that they +- __Type safety__: Instead of writing SQL queries manually and parsing the `List>` that they return, drift turns rows into objects of your choice. - __Stream queries__: Drift lets you "watch" your queries with zero additional effort. Any query can be turned into an auto-updating stream that emits new items when the underlying data changes. - __Fluent queries__: Drift generates a Dart api that you can use to write queries and automatically get their results. - Keep an updated list of all users with `select(users).watch()`. That's it! No sql to write, no rows to parse. -- __Typesafe sql__: If you prefer to write sql, that's fine! Drift has an sql parser and analyzer built in. It can parse + Keep an updated list of all users with `select(users).watch()`. That's it! No SQL to write, no rows to parse. +- __Type-safe SQL__: If you prefer to write SQL, that's fine! Drift has an SQL parser and analyzer built in. It can parse your queries at compile time, figure out what columns they're going to return and generate Dart code to represent your rows. - __Migration utils__: Drift makes writing migrations easier thanks to utility functions like `.createAllTables()`. You don't need to manually write your `CREATE TABLE` statements and keep them updated. And much more! Drift validates data before inserting it, so you can get helpful error messages instead of just an -sql error code. Of course, it supports transactions. And DAOs. And efficient batched insert statements. The list goes on. +SQL error code. Of course, it supports transactions. And DAOs. And efficient batched insert statements. The list goes on. ## Getting started diff --git a/docs/pages/docs/setup.md b/docs/pages/docs/setup.md index 6142f1df..37d76ace 100644 --- a/docs/pages/docs/setup.md +++ b/docs/pages/docs/setup.md @@ -26,7 +26,7 @@ If you want to look at an example app for inspiration, a cross-platform Flutter ## The dependencies {#adding-dependencies} -First, lets add drift to your project's `pubspec.yaml`. +First, let's add drift to your project's `pubspec.yaml`. In addition to the core drift dependencies, we're also adding packages to find a suitable database location on the device and to include a recent version of `sqlite3`, the database most commonly used with drift. @@ -66,7 +66,7 @@ If you're wondering why so many packages are necessary, here's a quick overview ## Database class Every project using drift needs at least one class to access a database. This class references all the -tables you want to use and is the central entrypoint for drift's code generator. +tables you want to use and is the central entry point for drift's code generator. In this example, we'll assume that this database class is defined in a file called `database.dart` and somewhere under `lib/`. Of course, you can put this class in any Dart file you like. diff --git a/docs/pages/index.html b/docs/pages/index.html index b093a712..c10c6536 100644 --- a/docs/pages/index.html +++ b/docs/pages/index.html @@ -1,6 +1,6 @@ --- page: - title: "Drift - Reactive, typesafe persistence library for Dart" + title: "Drift - Reactive, type-safe persistence library for Dart" template: layouts/home.html path: "" data: @@ -57,7 +57,7 @@ between all your revisions. {% block "blocks/feature.html" icon="fas fa-database" title="Prefer SQL? Drift's got you covered!" %} {% block "blocks/markdown.html" %} -Drift ships a powerful sql parser and analyzer, allowing it to create typesafe methods for all your sql queries. All sql queries are +Drift ships a powerful SQL parser and analyzer, allowing it to create type-safe methods for all your SQL queries. All SQL queries are validated and analyzed during build-time, so drift can provide hints about potential errors quickly and generate efficient mapping code. Of course, you can mix SQL and Dart to your liking. diff --git a/docs/pages/v2.html b/docs/pages/v2.html index 5409bbb2..5433951b 100644 --- a/docs/pages/v2.html +++ b/docs/pages/v2.html @@ -32,7 +32,7 @@ more flexibility when writing database code. {% block "blocks/section" color="light" %} {% block "blocks/feature" icon="fas fa-database" title="Pure SQL API" %} {% block "blocks/markdown" %} The new `.moor` files have been updated and can now hold both `CREATE TABLE` statements -and queries you define. Moor will then generate typesafe Dart APIs based on your tables +and queries you define. Moor will then generate type-safe Dart APIs based on your tables and statements. [Get started with SQL and moor]({{ "docs/SQL API/index.md" | pageUrl }}) @@ -55,7 +55,7 @@ worlds. {% block "blocks/lead" color="green" %} {% block "blocks/markdown" %} ## Builtin SQL IDE -Moor 2.0 expands the previous sql parser and analyzer, providing real-time feedback on your +Moor 2.0 expands the previous SQL parser and analyzer, providing real-time feedback on your SQL queries as you type. Moor plugs right into the Dart analysis server, so you don't have to install any additional extensions. @@ -113,4 +113,4 @@ _Please not that the package is still in preview_ - To get started with SQL in moor, or to migrate an existing project to moor, follow our [migration guide]({{ "docs/SQL API/index.md" | pageUrl }}) -{% endblock %} {% endblock %} \ No newline at end of file +{% endblock %} {% endblock %} diff --git a/drift/README.md b/drift/README.md index b4dd2775..8cb7e152 100644 --- a/drift/README.md +++ b/drift/README.md @@ -15,22 +15,22 @@

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(id.value); + } + if (teamA.present) { + map['team_a'] = i0.Variable(teamA.value); + } + if (teamB.present) { + map['team_b'] = i0.Variable(teamB.value); + } + if (teamAWon.present) { + map['team_a_won'] = i0.Variable(teamAWon.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MatchResultsCompanion(') + ..write('id: $id, ') + ..write('teamA: $teamA, ') + ..write('teamB: $teamB, ') + ..write('teamAWon: $teamAWon') + ..write(')')) + .toString(); + } +} diff --git a/docs/pages/docs/Dart API/writes.md b/docs/pages/docs/Dart API/writes.md index 51127b0a..69a79c44 100644 --- a/docs/pages/docs/Dart API/writes.md +++ b/docs/pages/docs/Dart API/writes.md @@ -109,7 +109,9 @@ This makes them suitable for bulk insert or update operations. ### Upserts -Upserts are a feature from newer sqlite3 versions that allows an insert to +{% assign upserts = "package:drift_docs/snippets/modular/upserts.dart.excerpt.json" | readString | json_decode %} + +Upserts are a feature from newer sqlite3 versions that allows an insert to behave like an update if a conflicting row already exists. This allows us to create or override an existing row when its primary key is @@ -129,35 +131,20 @@ Future createOrUpdateUser(User user) { } ``` -When calling `createOrUpdateUser()` with an email address that already exists, +When calling `createOrUpdateUser()` with an email address that already exists, that user's name will be updated. Otherwise, a new user will be inserted into the database. -Inserts can also be used with more advanced queries. For instance, let's say -we're building a dictionary and want to keep track of how many times we +Inserts can also be used with more advanced queries. For instance, let's say +we're building a dictionary and want to keep track of how many times we encountered a word. A table for that might look like -```dart -class Words extends Table { - TextColumn get word => text()(); - IntColumn get usages => integer().withDefault(const Constant(1))(); - - @override - Set get primaryKey => {word}; -} -``` +{% include "blocks/snippet" snippets = upserts name = "words-table" %} By using a custom upserts, we can insert a new word or increment its `usages` counter if it already exists: -```dart -Future trackWord(String word) { - return into(words).insert( - WordsCompanion.insert(word: word), - onConflict: DoUpdate((old) => WordsCompanion.custom(usages: old.usages + Constant(1))), - ); -} -``` +{% include "blocks/snippet" snippets = upserts name = "track-word" %} {% block "blocks/alert" title="Unique constraints and conflict targets" %} Both `insertOnConflictUpdate` and `onConflict: DoUpdate` use an `DO UPDATE` @@ -165,7 +152,10 @@ upsert in sql. This requires us to provide a so-called "conflict target", a set of columns to check for uniqueness violations. By default, drift will use the table's primary key as conflict target. That works in most cases, but if you have custom `UNIQUE` constraints on some columns, you'll need to use -the `target` parameter on `DoUpdate` in Dart to include those columns. +the `target` parameter on `DoUpdate` in Dart to include those columns: + +{% include "blocks/snippet" snippets = upserts name = "upsert-target" %} + {% endblock %} Note that this requires a fairly recent sqlite3 version (3.24.0) that might not From 0a457e92e586bb95ec55bc58a50dadfd89e2e3ab Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 16 Nov 2023 21:19:37 +0100 Subject: [PATCH 35/37] Fix dartdoc templates --- drift/lib/src/dsl/table.dart | 2 +- drift/lib/src/runtime/query_builder/expressions/datetimes.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/drift/lib/src/dsl/table.dart b/drift/lib/src/dsl/table.dart index 1ca952c7..dd6c4bb1 100644 --- a/drift/lib/src/dsl/table.dart +++ b/drift/lib/src/dsl/table.dart @@ -395,7 +395,7 @@ class DriftView { /// By default, drift will attempt to use the view name followed by "Data" /// when naming data classes (e.g. a view named "UserView" will generate a /// data class called "UserViewData"). - /// {@macro drift_custom_data_class} + /// {@endtemplate} final String? dataClassName; /// The parent class of generated data class. Class must extends [DataClass]! diff --git a/drift/lib/src/runtime/query_builder/expressions/datetimes.dart b/drift/lib/src/runtime/query_builder/expressions/datetimes.dart index ae4c8794..3887285f 100644 --- a/drift/lib/src/runtime/query_builder/expressions/datetimes.dart +++ b/drift/lib/src/runtime/query_builder/expressions/datetimes.dart @@ -71,7 +71,7 @@ extension DateTimeExpressions on Expression { /// ```dart /// Variable(DateTime.now()).modify(DateTimeModifier.localTime()).hour /// ``` - /// {@template} + /// {@endtemplate} Expression get year => _StrftimeSingleFieldExpression('%Y', this); /// Extracts the month from `this` datetime expression. From 220c7125a33c79ce9a7220e7819c00f0d0cdf736 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 16 Nov 2023 21:25:21 +0100 Subject: [PATCH 36/37] Update to lints 3.0.0 --- .../snippets/drift_files/custom_queries.dart | 2 +- docs/lib/snippets/migrations/migrations.dart | 2 +- .../migrations/runtime_verification.dart | 4 +-- .../snippets/modular/drift/dart_example.dart | 3 +-- .../snippets/modular/many_to_many/json.dart | 2 +- .../modular/many_to_many/relational.dart | 2 +- docs/lib/snippets/platforms/web.dart | 2 +- docs/pubspec.yaml | 2 +- docs/test/generated/database.dart | 2 +- docs/tool/snippets.dart | 2 +- drift/example/main.dart | 2 +- drift/lib/internal/versioned_schema.dart | 4 +-- drift/lib/native.dart | 13 ++++------ drift/lib/src/remote/client_impl.dart | 6 ++--- drift/lib/src/runtime/api/db_base.dart | 7 +++--- .../query_builder/statements/delete.dart | 3 +-- .../statements/select/select.dart | 5 +--- .../statements/select/select_with_join.dart | 6 ++--- .../query_builder/statements/update.dart | 3 +-- drift/lib/wasm.dart | 4 +-- drift/pubspec.yaml | 2 +- drift/test/database/database_test.dart | 2 +- .../test/database/statements/schema_test.dart | 2 +- drift/test/generated/custom_tables.dart | 2 +- drift/test/generated/todos.dart | 2 +- .../cancellation_test_support.dart | 2 +- .../migrations_integration_test.dart | 3 +-- .../integration_tests/regress_1589_test.dart | 2 +- drift_dev/lib/src/cli/commands/analyze.dart | 2 +- .../src/cli/commands/identify_databases.dart | 2 +- drift_dev/lib/src/cli/commands/migrate.dart | 2 +- .../lib/src/cli/commands/schema/dump.dart | 3 ++- drift_dev/lib/src/writer/modules.dart | 2 +- drift_dev/pubspec.yaml | 2 +- .../services/schema/validate_schema_test.dart | 4 +-- .../services/schema/verifier_impl_test.dart | 2 +- drift_sqflite/pubspec.yaml | 2 +- examples/encryption/lib/main.dart | 6 ++--- examples/encryption/pubspec.yaml | 2 +- examples/migrations_example/lib/database.dart | 2 +- examples/migrations_example/pubspec.yaml | 2 +- examples/modular/pubspec.yaml | 2 +- examples/web_worker_example/pubspec.yaml | 2 +- extras/drift_devtools_extension/pubspec.yaml | 2 +- extras/drift_mariadb/pubspec.yaml | 2 +- .../lib/database/database.dart | 2 +- .../drift_testcases/pubspec.yaml | 2 +- .../integration_tests/web_wasm/pubspec.yaml | 2 +- sqlparser/lib/src/analysis/error.dart | 5 ++-- .../lib/src/ast/schema/column_definition.dart | 25 +++++++++---------- .../lib/src/ast/schema/table_definition.dart | 12 ++++----- sqlparser/lib/src/ast/statements/select.dart | 2 +- sqlparser/lib/src/engine/module/fts5.dart | 6 ++--- sqlparser/lib/src/reader/tokenizer/token.dart | 4 +-- sqlparser/pubspec.yaml | 2 +- 55 files changed, 88 insertions(+), 108 deletions(-) diff --git a/docs/lib/snippets/drift_files/custom_queries.dart b/docs/lib/snippets/drift_files/custom_queries.dart index 852e9b60..1ac5b730 100644 --- a/docs/lib/snippets/drift_files/custom_queries.dart +++ b/docs/lib/snippets/drift_files/custom_queries.dart @@ -29,7 +29,7 @@ class MyDatabase extends $MyDatabase { @override int get schemaVersion => 1; - MyDatabase(QueryExecutor e) : super(e); + MyDatabase(super.e); // #docregion amountOfTodosInCategory Stream amountOfTodosInCategory(int id) { diff --git a/docs/lib/snippets/migrations/migrations.dart b/docs/lib/snippets/migrations/migrations.dart index 49b43259..d025c213 100644 --- a/docs/lib/snippets/migrations/migrations.dart +++ b/docs/lib/snippets/migrations/migrations.dart @@ -18,7 +18,7 @@ class Todos extends Table { @DriftDatabase(tables: [Todos]) class MyDatabase extends _$MyDatabase { - MyDatabase(QueryExecutor e) : super(e); + MyDatabase(super.e); // #docregion start @override diff --git a/docs/lib/snippets/migrations/runtime_verification.dart b/docs/lib/snippets/migrations/runtime_verification.dart index deff357d..5ad716fa 100644 --- a/docs/lib/snippets/migrations/runtime_verification.dart +++ b/docs/lib/snippets/migrations/runtime_verification.dart @@ -8,14 +8,14 @@ import 'package:drift_dev/api/migrations.dart'; const kDebugMode = true; abstract class _$MyDatabase extends GeneratedDatabase { - _$MyDatabase(QueryExecutor executor) : super(executor); + _$MyDatabase(super.executor); } // #docregion class MyDatabase extends _$MyDatabase { // #enddocregion - MyDatabase(QueryExecutor executor) : super(executor); + MyDatabase(super.executor); @override Iterable> get allTables => diff --git a/docs/lib/snippets/modular/drift/dart_example.dart b/docs/lib/snippets/modular/drift/dart_example.dart index b373a6ec..e16360fc 100644 --- a/docs/lib/snippets/modular/drift/dart_example.dart +++ b/docs/lib/snippets/modular/drift/dart_example.dart @@ -1,9 +1,8 @@ -import 'package:drift/drift.dart'; import 'example.drift.dart'; class DartExample extends ExampleDrift { - DartExample(GeneratedDatabase attachedDatabase) : super(attachedDatabase); + DartExample(super.attachedDatabase); // #docregion watchInCategory Stream> watchInCategory(int category) { diff --git a/docs/lib/snippets/modular/many_to_many/json.dart b/docs/lib/snippets/modular/many_to_many/json.dart index 92a9db49..af15e3eb 100644 --- a/docs/lib/snippets/modular/many_to_many/json.dart +++ b/docs/lib/snippets/modular/many_to_many/json.dart @@ -48,7 +48,7 @@ class ShoppingCartEntries { @DriftDatabase(tables: [BuyableItems, ShoppingCarts]) class JsonBasedDatabase extends $JsonBasedDatabase { - JsonBasedDatabase(QueryExecutor e) : super(e); + JsonBasedDatabase(super.e); @override int get schemaVersion => 1; diff --git a/docs/lib/snippets/modular/many_to_many/relational.dart b/docs/lib/snippets/modular/many_to_many/relational.dart index e25f1be8..186358a9 100644 --- a/docs/lib/snippets/modular/many_to_many/relational.dart +++ b/docs/lib/snippets/modular/many_to_many/relational.dart @@ -31,7 +31,7 @@ class ShoppingCartEntries extends Table { @DriftDatabase(tables: [BuyableItems, ShoppingCarts, ShoppingCartEntries]) class RelationalDatabase extends $RelationalDatabase { - RelationalDatabase(QueryExecutor e) : super(e); + RelationalDatabase(super.e); @override int get schemaVersion => 1; diff --git a/docs/lib/snippets/platforms/web.dart b/docs/lib/snippets/platforms/web.dart index 7c63c41a..499e0ff5 100644 --- a/docs/lib/snippets/platforms/web.dart +++ b/docs/lib/snippets/platforms/web.dart @@ -28,7 +28,7 @@ DatabaseConnection connectOnWeb() { // You can then use this method to open your database: class MyWebDatabase extends _$MyWebDatabase { - MyWebDatabase._(QueryExecutor e) : super(e); + MyWebDatabase._(super.e); factory MyWebDatabase() => MyWebDatabase._(connectOnWeb()); // ... diff --git a/docs/pubspec.yaml b/docs/pubspec.yaml index a6b01865..ffe71a9d 100644 --- a/docs/pubspec.yaml +++ b/docs/pubspec.yaml @@ -36,7 +36,7 @@ dependencies: postgres: ^3.0.0-0 dev_dependencies: - lints: ^2.0.0 + lints: ^3.0.0 build: ^2.1.0 build_runner: ^2.0.5 build_runner_core: ^7.2.7 diff --git a/docs/test/generated/database.dart b/docs/test/generated/database.dart index c283cb79..a3ce3974 100644 --- a/docs/test/generated/database.dart +++ b/docs/test/generated/database.dart @@ -11,7 +11,7 @@ class Users extends Table { @DriftDatabase(tables: [Users]) class Database extends _$Database { - Database(QueryExecutor c) : super(c); + Database(super.c); @override int get schemaVersion => 1; diff --git a/docs/tool/snippets.dart b/docs/tool/snippets.dart index bbace20e..21f984b2 100644 --- a/docs/tool/snippets.dart +++ b/docs/tool/snippets.dart @@ -35,7 +35,7 @@ class SnippetsBuilder extends CodeExcerptBuilder { } class _DriftHighlighter extends Highlighter { - _DriftHighlighter(SourceFile file) : super(file); + _DriftHighlighter(super.file); @override void highlight() { diff --git a/drift/example/main.dart b/drift/example/main.dart index a6893780..de746c83 100644 --- a/drift/example/main.dart +++ b/drift/example/main.dart @@ -64,7 +64,7 @@ abstract class TodoItemWithCategoryNameView extends View { TodoItemWithCategoryNameView, ]) class Database extends _$Database { - Database(QueryExecutor e) : super(e); + Database(super.e); @override int get schemaVersion => 2; diff --git a/drift/lib/internal/versioned_schema.dart b/drift/lib/internal/versioned_schema.dart index 7c905f48..c240e91f 100644 --- a/drift/lib/internal/versioned_schema.dart +++ b/drift/lib/internal/versioned_schema.dart @@ -223,9 +223,9 @@ class VersionedVirtualTable extends VersionedTable /// Create a virtual table by copying fields from [source] and applying a /// [alias] to columns. VersionedVirtualTable.aliased( - {required VersionedVirtualTable source, required String? alias}) + {required VersionedVirtualTable super.source, required super.alias}) : moduleAndArgs = source.moduleAndArgs, - super.aliased(source: source, alias: alias); + super.aliased(); @override VersionedVirtualTable createAlias(String alias) { diff --git a/drift/lib/native.dart b/drift/lib/native.dart index cdb1570a..e523df69 100644 --- a/drift/lib/native.dart +++ b/drift/lib/native.dart @@ -48,8 +48,8 @@ class NativeDatabase extends DelegatedDatabase { // when changing this, also update the documentation in `drift_vm_database_factory`. static const _cacheStatementsByDefault = false; - NativeDatabase._(DatabaseDelegate delegate, bool logStatements) - : super(delegate, isSequential: false, logStatements: logStatements); + NativeDatabase._(super.delegate, bool logStatements) + : super(isSequential: false, logStatements: logStatements); /// Creates a database that will store its result in the [file], creating it /// if it doesn't exist. @@ -253,15 +253,12 @@ class _NativeDelegate extends Sqlite3Delegate { ); _NativeDelegate.opened( - Database db, - DatabaseSetup? setup, - bool closeUnderlyingWhenClosed, + Database super.db, + super.setup, + super.closeUnderlyingWhenClosed, bool cachePreparedStatements, ) : file = null, super.opened( - db, - setup, - closeUnderlyingWhenClosed, cachePreparedStatements: cachePreparedStatements, ); diff --git a/drift/lib/src/remote/client_impl.dart b/drift/lib/src/remote/client_impl.dart index 48852955..12459f40 100644 --- a/drift/lib/src/remote/client_impl.dart +++ b/drift/lib/src/remote/client_impl.dart @@ -135,8 +135,7 @@ abstract class _BaseExecutor extends QueryExecutor { } class _RemoteQueryExecutor extends _BaseExecutor { - _RemoteQueryExecutor(DriftClient client, [int? executorId]) - : super(client, executorId); + _RemoteQueryExecutor(super.client, [super.executorId]); Completer? _setSchemaVersion; Future? _serverIsOpen; @@ -181,8 +180,7 @@ class _RemoteTransactionExecutor extends _BaseExecutor implements TransactionExecutor { final int? _outerExecutorId; - _RemoteTransactionExecutor(DriftClient client, this._outerExecutorId) - : super(client); + _RemoteTransactionExecutor(super.client, this._outerExecutorId); Completer? _pendingOpen; bool _done = false; diff --git a/drift/lib/src/runtime/api/db_base.dart b/drift/lib/src/runtime/api/db_base.dart index f13158ce..d0d8d43c 100644 --- a/drift/lib/src/runtime/api/db_base.dart +++ b/drift/lib/src/runtime/api/db_base.dart @@ -59,14 +59,13 @@ abstract class GeneratedDatabase extends DatabaseConnectionUser final Type _$dontSendThisOverIsolates = Null; /// Used by generated code - GeneratedDatabase(QueryExecutor executor, {StreamQueryStore? streamStore}) - : super(executor, streamQueries: streamStore) { + GeneratedDatabase(super.executor, {StreamQueryStore? streamStore}) + : super(streamQueries: streamStore) { _whenConstructed(); } /// Used by generated code to connect to a database that is already open. - GeneratedDatabase.connect(DatabaseConnection connection) - : super.fromConnection(connection) { + GeneratedDatabase.connect(super.connection) : super.fromConnection() { _whenConstructed(); } diff --git a/drift/lib/src/runtime/query_builder/statements/delete.dart b/drift/lib/src/runtime/query_builder/statements/delete.dart index 732ff60d..978c8430 100644 --- a/drift/lib/src/runtime/query_builder/statements/delete.dart +++ b/drift/lib/src/runtime/query_builder/statements/delete.dart @@ -5,8 +5,7 @@ class DeleteStatement extends Query with SingleTableQueryMixin { /// This constructor should be called by [DatabaseConnectionUser.delete] for /// you. - DeleteStatement(DatabaseConnectionUser database, TableInfo table) - : super(database, table); + DeleteStatement(super.database, TableInfo super.table); @override void writeStartPart(GenerationContext ctx) { diff --git a/drift/lib/src/runtime/query_builder/statements/select/select.dart b/drift/lib/src/runtime/query_builder/statements/select/select.dart index b73b873f..3b834b93 100644 --- a/drift/lib/src/runtime/query_builder/statements/select/select.dart +++ b/drift/lib/src/runtime/query_builder/statements/select/select.dart @@ -30,10 +30,7 @@ class SimpleSelectStatement extends Query /// Used internally by drift, users will want to call /// [DatabaseConnectionUser.select] instead. - SimpleSelectStatement( - DatabaseConnectionUser database, ResultSetImplementation table, - {this.distinct = false}) - : super(database, table); + SimpleSelectStatement(super.database, super.table, {this.distinct = false}); /// The tables this select statement reads from. @visibleForOverriding diff --git a/drift/lib/src/runtime/query_builder/statements/select/select_with_join.dart b/drift/lib/src/runtime/query_builder/statements/select/select_with_join.dart index 4df6e637..ac7a6edc 100644 --- a/drift/lib/src/runtime/query_builder/statements/select/select_with_join.dart +++ b/drift/lib/src/runtime/query_builder/statements/select/select_with_join.dart @@ -10,12 +10,10 @@ class JoinedSelectStatement implements BaseSelectStatement { /// Used internally by drift, users should use [SimpleSelectStatement.join] /// instead. - JoinedSelectStatement(DatabaseConnectionUser database, - ResultSetImplementation table, this._joins, + JoinedSelectStatement(super.database, super.table, this._joins, [this.distinct = false, this._includeMainTableInResult = true, - this._includeJoinedTablesInResult = true]) - : super(database, table); + this._includeJoinedTablesInResult = true]); /// Whether to generate a `SELECT DISTINCT` query that will remove duplicate /// rows from the result set. diff --git a/drift/lib/src/runtime/query_builder/statements/update.dart b/drift/lib/src/runtime/query_builder/statements/update.dart index f1b35989..d87b0d9d 100644 --- a/drift/lib/src/runtime/query_builder/statements/update.dart +++ b/drift/lib/src/runtime/query_builder/statements/update.dart @@ -4,8 +4,7 @@ part of '../query_builder.dart'; class UpdateStatement extends Query with SingleTableQueryMixin { /// Used internally by drift, construct an update statement - UpdateStatement(DatabaseConnectionUser database, TableInfo table) - : super(database, table); + UpdateStatement(super.database, TableInfo super.table); late Map _updatedFields; diff --git a/drift/lib/wasm.dart b/drift/lib/wasm.dart index c66893a1..4bf9924d 100644 --- a/drift/lib/wasm.dart +++ b/drift/lib/wasm.dart @@ -31,8 +31,8 @@ export 'src/web/wasm_setup/types.dart'; /// how to obtain this file. A [working example](https://github.com/simolus3/drift/blob/04539882330d80519128fec1ceb120fb1623a831/examples/app/lib/database/connection/web.dart#L27-L36) /// is also available in the drift repository. class WasmDatabase extends DelegatedDatabase { - WasmDatabase._(DatabaseDelegate delegate, bool logStatements) - : super(delegate, isSequential: true, logStatements: logStatements); + WasmDatabase._(super.delegate, bool logStatements) + : super(isSequential: true, logStatements: logStatements); /// Creates a wasm database at [path] in the virtual file system of the /// [sqlite3] module. diff --git a/drift/pubspec.yaml b/drift/pubspec.yaml index fa0472b8..3744cb6c 100644 --- a/drift/pubspec.yaml +++ b/drift/pubspec.yaml @@ -28,7 +28,7 @@ dev_dependencies: drift_testcases: path: ../extras/integration_tests/drift_testcases http: ^0.13.4 - lints: ^2.0.0 + lints: ^3.0.0 uuid: ^4.0.0 build_runner: ^2.0.0 test: ^1.17.0 diff --git a/drift/test/database/database_test.dart b/drift/test/database/database_test.dart index c5c18a44..fe8943d7 100644 --- a/drift/test/database/database_test.dart +++ b/drift/test/database/database_test.dart @@ -6,7 +6,7 @@ import '../generated/todos.dart'; import '../test_utils/test_utils.dart'; class _FakeDb extends GeneratedDatabase { - _FakeDb(QueryExecutor executor) : super(executor); + _FakeDb(super.executor); @override MigrationStrategy get migration { diff --git a/drift/test/database/statements/schema_test.dart b/drift/test/database/statements/schema_test.dart index bdb0606a..73ee2f56 100644 --- a/drift/test/database/statements/schema_test.dart +++ b/drift/test/database/statements/schema_test.dart @@ -321,7 +321,7 @@ final class _FakeSchemaVersion extends VersionedSchema { } class _DefaultDb extends GeneratedDatabase { - _DefaultDb(QueryExecutor executor) : super(executor); + _DefaultDb(super.executor); @override List> get allTables => []; diff --git a/drift/test/generated/custom_tables.dart b/drift/test/generated/custom_tables.dart index 6c565ebd..6504bc72 100644 --- a/drift/test/generated/custom_tables.dart +++ b/drift/test/generated/custom_tables.dart @@ -15,7 +15,7 @@ part 'custom_tables.g.dart'; }, ) class CustomTablesDb extends _$CustomTablesDb { - CustomTablesDb(QueryExecutor e) : super(e) { + CustomTablesDb(super.e) { driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; } diff --git a/drift/test/generated/todos.dart b/drift/test/generated/todos.dart index 43509b3e..e5c4e0a8 100644 --- a/drift/test/generated/todos.dart +++ b/drift/test/generated/todos.dart @@ -257,7 +257,7 @@ class TodoDb extends _$TodoDb { }, ) class SomeDao extends DatabaseAccessor with _$SomeDaoMixin { - SomeDao(TodoDb db) : super(db); + SomeDao(super.db); } QueryExecutor get _nullExecutor => diff --git a/drift/test/integration_tests/cancellation_test_support.dart b/drift/test/integration_tests/cancellation_test_support.dart index f49808f5..924bfab4 100644 --- a/drift/test/integration_tests/cancellation_test_support.dart +++ b/drift/test/integration_tests/cancellation_test_support.dart @@ -35,7 +35,7 @@ DatabaseConnection createConnection() { } class EmptyDb extends GeneratedDatabase { - EmptyDb(DatabaseConnection c) : super(c); + EmptyDb(DatabaseConnection super.c); @override final List allTables = const []; @override diff --git a/drift/test/integration_tests/migrations_integration_test.dart b/drift/test/integration_tests/migrations_integration_test.dart index 76ea82ef..80d19881 100644 --- a/drift/test/integration_tests/migrations_integration_test.dart +++ b/drift/test/integration_tests/migrations_integration_test.dart @@ -495,8 +495,7 @@ void main() { } class _TestDatabase extends GeneratedDatabase { - _TestDatabase(QueryExecutor executor, this.schemaVersion, this.migration) - : super(executor); + _TestDatabase(super.executor, this.schemaVersion, this.migration); @override Iterable> get allTables => const Iterable.empty(); diff --git a/drift/test/integration_tests/regress_1589_test.dart b/drift/test/integration_tests/regress_1589_test.dart index 6baa30ea..b0f2fc2b 100644 --- a/drift/test/integration_tests/regress_1589_test.dart +++ b/drift/test/integration_tests/regress_1589_test.dart @@ -61,7 +61,7 @@ CREATE TABLE IF NOT EXISTS todo_categories ( } class _Database extends GeneratedDatabase { - _Database(QueryExecutor executor) : super(executor); + _Database(super.executor); @override Iterable> get allTables => const Iterable.empty(); diff --git a/drift_dev/lib/src/cli/commands/analyze.dart b/drift_dev/lib/src/cli/commands/analyze.dart index 7aef845b..fde0663c 100644 --- a/drift_dev/lib/src/cli/commands/analyze.dart +++ b/drift_dev/lib/src/cli/commands/analyze.dart @@ -3,7 +3,7 @@ import 'dart:io'; import '../cli.dart'; class AnalyzeCommand extends MoorCommand { - AnalyzeCommand(DriftDevCli cli) : super(cli); + AnalyzeCommand(super.cli); @override String get description => 'Analyze and lint drift database code'; diff --git a/drift_dev/lib/src/cli/commands/identify_databases.dart b/drift_dev/lib/src/cli/commands/identify_databases.dart index 065e31f9..0ef0ea5b 100644 --- a/drift_dev/lib/src/cli/commands/identify_databases.dart +++ b/drift_dev/lib/src/cli/commands/identify_databases.dart @@ -7,7 +7,7 @@ import '../../analysis/results/results.dart'; import '../cli.dart'; class IdentifyDatabases extends MoorCommand { - IdentifyDatabases(DriftDevCli cli) : super(cli); + IdentifyDatabases(super.cli); @override String get description => diff --git a/drift_dev/lib/src/cli/commands/migrate.dart b/drift_dev/lib/src/cli/commands/migrate.dart index 2c446bf4..771eac1b 100644 --- a/drift_dev/lib/src/cli/commands/migrate.dart +++ b/drift_dev/lib/src/cli/commands/migrate.dart @@ -26,7 +26,7 @@ class MigrateCommand extends MoorCommand { late final AnalysisContext context; - MigrateCommand(DriftDevCli cli) : super(cli); + MigrateCommand(super.cli); @override String get description => 'Migrate a project from moor to drift'; diff --git a/drift_dev/lib/src/cli/commands/schema/dump.dart b/drift_dev/lib/src/cli/commands/schema/dump.dart index feedd5d4..2c530a54 100644 --- a/drift_dev/lib/src/cli/commands/schema/dump.dart +++ b/drift_dev/lib/src/cli/commands/schema/dump.dart @@ -86,7 +86,8 @@ class DumpSchemaCommand extends Command { try { final elements = await extractDriftElementsFromDatabase(opened); - final userVersion = opened.select('pragma user_version').single[0] as int; + final userVersion = + opened.select('pragma user_version').single.columnAt(0) as int; return _AnalyzedDatabase(elements, userVersion); } finally { diff --git a/drift_dev/lib/src/writer/modules.dart b/drift_dev/lib/src/writer/modules.dart index b06464cb..63de81fc 100644 --- a/drift_dev/lib/src/writer/modules.dart +++ b/drift_dev/lib/src/writer/modules.dart @@ -85,7 +85,7 @@ class ModularAccessorWriter { // Also make imports available final imports = file.discovery?.importDependencies ?? const []; for (final import in imports) { - final file = driver.cache.knownFiles[import]; + final file = driver.cache.knownFiles[import.uri]; if (file != null && file.needsModularAccessor(driver)) { final moduleClass = restOfClass.modularAccessor(import.uri); diff --git a/drift_dev/pubspec.yaml b/drift_dev/pubspec.yaml index fa4ac25c..b0dd5684 100644 --- a/drift_dev/pubspec.yaml +++ b/drift_dev/pubspec.yaml @@ -50,7 +50,7 @@ dependencies: string_scanner: ^1.1.1 dev_dependencies: - lints: ^2.0.0 + lints: ^3.0.0 checked_yaml: ^2.0.1 test: ^1.16.0 test_descriptor: ^2.0.0 diff --git a/drift_dev/test/services/schema/validate_schema_test.dart b/drift_dev/test/services/schema/validate_schema_test.dart index df568e06..578976d9 100644 --- a/drift_dev/test/services/schema/validate_schema_test.dart +++ b/drift_dev/test/services/schema/validate_schema_test.dart @@ -42,8 +42,8 @@ class _TestDatabase extends GeneratedDatabase { @override DriftDatabaseOptions options = DriftDatabaseOptions(); - _TestDatabase.connect(DatabaseConnection connection) - : super.connect(connection); + _TestDatabase.connect(super.connection) + : super.connect(); } void main() { diff --git a/drift_dev/test/services/schema/verifier_impl_test.dart b/drift_dev/test/services/schema/verifier_impl_test.dart index 7f2e2814..b9536fcf 100644 --- a/drift_dev/test/services/schema/verifier_impl_test.dart +++ b/drift_dev/test/services/schema/verifier_impl_test.dart @@ -80,7 +80,7 @@ class _TestDatabase extends GeneratedDatabase { @override MigrationStrategy migration = MigrationStrategy(); - _TestDatabase(QueryExecutor executor, this.schemaVersion) : super(executor); + _TestDatabase(super.executor, this.schemaVersion); @override Iterable> get allTables { diff --git a/drift_sqflite/pubspec.yaml b/drift_sqflite/pubspec.yaml index e9818ee7..7b112370 100644 --- a/drift_sqflite/pubspec.yaml +++ b/drift_sqflite/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: dev_dependencies: integration_test: sdk: flutter - flutter_lints: ^2.0.0 + flutter_lints: ^3.0.0 flutter_test: sdk: flutter drift_testcases: diff --git a/examples/encryption/lib/main.dart b/examples/encryption/lib/main.dart index 25c38d12..6445b5b1 100644 --- a/examples/encryption/lib/main.dart +++ b/examples/encryption/lib/main.dart @@ -19,7 +19,7 @@ void main() { } class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); + const MyApp({super.key}); @override Widget build(BuildContext context) { @@ -35,7 +35,7 @@ class MyApp extends StatelessWidget { } class HomePage extends StatefulWidget { - const HomePage({Key? key}) : super(key: key); + const HomePage({super.key}); @override State createState() => _HomePageState(); @@ -95,7 +95,7 @@ class _AddEntryDialog extends StatefulWidget { // database around, but tiny example only wants to show how to use encryption. final MyEncryptedDatabase database; - const _AddEntryDialog({Key? key, required this.database}) : super(key: key); + const _AddEntryDialog({required this.database}); @override State<_AddEntryDialog> createState() => _AddEntryDialogState(); diff --git a/examples/encryption/pubspec.yaml b/examples/encryption/pubspec.yaml index 5455c1fa..e0b4e254 100644 --- a/examples/encryption/pubspec.yaml +++ b/examples/encryption/pubspec.yaml @@ -19,7 +19,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.0 + flutter_lints: ^3.0.0 drift_dev: ^2.0.2 build_runner: ^2.2.0 diff --git a/examples/migrations_example/lib/database.dart b/examples/migrations_example/lib/database.dart index 2476b51d..8d630666 100644 --- a/examples/migrations_example/lib/database.dart +++ b/examples/migrations_example/lib/database.dart @@ -21,7 +21,7 @@ class Database extends _$Database { @override int get schemaVersion => latestSchemaVersion; - Database(DatabaseConnection connection) : super(connection); + Database(DatabaseConnection super.connection); @override MigrationStrategy get migration { diff --git a/examples/migrations_example/pubspec.yaml b/examples/migrations_example/pubspec.yaml index e3a9427b..87d9a475 100644 --- a/examples/migrations_example/pubspec.yaml +++ b/examples/migrations_example/pubspec.yaml @@ -10,6 +10,6 @@ dependencies: drift_dev: dev_dependencies: - lints: ^2.0.0 + lints: ^3.0.0 build_runner: ^2.0.0 test: ^1.15.4 diff --git a/examples/modular/pubspec.yaml b/examples/modular/pubspec.yaml index cc254f05..1f170cdc 100644 --- a/examples/modular/pubspec.yaml +++ b/examples/modular/pubspec.yaml @@ -11,5 +11,5 @@ dependencies: dev_dependencies: build_runner: ^2.3.2 drift_dev: - lints: ^2.0.0 + lints: ^3.0.0 test: ^1.16.0 diff --git a/examples/web_worker_example/pubspec.yaml b/examples/web_worker_example/pubspec.yaml index 18c689f8..01f9e3a9 100644 --- a/examples/web_worker_example/pubspec.yaml +++ b/examples/web_worker_example/pubspec.yaml @@ -14,5 +14,5 @@ dependencies: dev_dependencies: build_runner: ^2.1.11 build_web_compilers: ^4.0.0 - lints: ^2.0.0 + lints: ^3.0.0 drift_dev: diff --git a/extras/drift_devtools_extension/pubspec.yaml b/extras/drift_devtools_extension/pubspec.yaml index 5b23e967..3774e060 100644 --- a/extras/drift_devtools_extension/pubspec.yaml +++ b/extras/drift_devtools_extension/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.0 + flutter_lints: ^3.0.0 flutter: uses-material-design: true diff --git a/extras/drift_mariadb/pubspec.yaml b/extras/drift_mariadb/pubspec.yaml index 939c8497..40aa1fe7 100644 --- a/extras/drift_mariadb/pubspec.yaml +++ b/extras/drift_mariadb/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: mysql_client: ^0.0.27 dev_dependencies: - lints: ^2.0.0 + lints: ^3.0.0 test: ^1.21.0 drift_testcases: path: ../integration_tests/drift_testcases diff --git a/extras/integration_tests/drift_testcases/lib/database/database.dart b/extras/integration_tests/drift_testcases/lib/database/database.dart index b285842c..4a884c5a 100644 --- a/extras/integration_tests/drift_testcases/lib/database/database.dart +++ b/extras/integration_tests/drift_testcases/lib/database/database.dart @@ -88,7 +88,7 @@ class Database extends _$Database { @override final int schemaVersion; - Database(DatabaseConnection e, {this.schemaVersion = 2}) : super(e); + Database(DatabaseConnection super.e, {this.schemaVersion = 2}); Database.executor(QueryExecutor db) : this(DatabaseConnection(db)); diff --git a/extras/integration_tests/drift_testcases/pubspec.yaml b/extras/integration_tests/drift_testcases/pubspec.yaml index 9bec7f09..b28407a5 100644 --- a/extras/integration_tests/drift_testcases/pubspec.yaml +++ b/extras/integration_tests/drift_testcases/pubspec.yaml @@ -15,4 +15,4 @@ dev_dependencies: build_runner: ^2.1.11 drift_dev: ^2.0.0 json_serializable: ^6.2.0 - lints: ^2.0.0 + lints: ^3.0.0 diff --git a/extras/integration_tests/web_wasm/pubspec.yaml b/extras/integration_tests/web_wasm/pubspec.yaml index 36f25afb..a7d1acdf 100644 --- a/extras/integration_tests/web_wasm/pubspec.yaml +++ b/extras/integration_tests/web_wasm/pubspec.yaml @@ -23,5 +23,5 @@ dev_dependencies: build_runner: ^2.4.5 build_web_compilers: ^4.0.0 drift_dev: - lints: ^2.0.0 + lints: ^3.0.0 test: ^1.24.3 diff --git a/sqlparser/lib/src/analysis/error.dart b/sqlparser/lib/src/analysis/error.dart index 9f742903..9bcd8e3e 100644 --- a/sqlparser/lib/src/analysis/error.dart +++ b/sqlparser/lib/src/analysis/error.dart @@ -44,11 +44,10 @@ class UnresolvedReferenceError extends AnalysisError { final Iterable available; UnresolvedReferenceError( - {required AnalysisErrorType type, + {required super.type, required this.reference, required this.available, - AstNode? relevantNode}) - : super(type: type, relevantNode: relevantNode); + AstNode? super.relevantNode}); @override String get message { diff --git a/sqlparser/lib/src/ast/schema/column_definition.dart b/sqlparser/lib/src/ast/schema/column_definition.dart index dab8b7e3..d9ce13c7 100644 --- a/sqlparser/lib/src/ast/schema/column_definition.dart +++ b/sqlparser/lib/src/ast/schema/column_definition.dart @@ -95,7 +95,7 @@ class NullColumnConstraint extends ColumnConstraint { /// The `NULL` token forming this constraint. Token? $null; - NullColumnConstraint(String? name, {this.$null}) : super(name); + NullColumnConstraint(super.name, {this.$null}); @override Iterable get childNodes => const Iterable.empty(); @@ -110,7 +110,7 @@ class NotNull extends ColumnConstraint { Token? not; Token? $null; - NotNull(String? name, {this.onConflict}) : super(name); + NotNull(super.name, {this.onConflict}); @override final Iterable childNodes = const []; @@ -124,9 +124,8 @@ class PrimaryKeyColumn extends ColumnConstraint { final ConflictClause? onConflict; final OrderingMode? mode; - PrimaryKeyColumn(String? name, - {this.autoIncrement = false, this.mode, this.onConflict}) - : super(name); + PrimaryKeyColumn(super.name, + {this.autoIncrement = false, this.mode, this.onConflict}); @override Iterable get childNodes => const []; @@ -138,7 +137,7 @@ class PrimaryKeyColumn extends ColumnConstraint { class UniqueColumn extends ColumnConstraint { final ConflictClause? onConflict; - UniqueColumn(String? name, this.onConflict) : super(name); + UniqueColumn(super.name, this.onConflict); @override Iterable get childNodes => const []; @@ -150,7 +149,7 @@ class UniqueColumn extends ColumnConstraint { class CheckColumn extends ColumnConstraint { Expression expression; - CheckColumn(String? name, this.expression) : super(name); + CheckColumn(super.name, this.expression); @override Iterable get childNodes => [expression]; @@ -164,7 +163,7 @@ class CheckColumn extends ColumnConstraint { class Default extends ColumnConstraint { Expression expression; - Default(String? name, this.expression) : super(name); + Default(super.name, this.expression); @override Iterable get childNodes => [expression]; @@ -178,7 +177,7 @@ class Default extends ColumnConstraint { class CollateConstraint extends ColumnConstraint { final String collation; - CollateConstraint(String? name, this.collation) : super(name); + CollateConstraint(super.name, this.collation); @override final Iterable childNodes = const []; @@ -190,7 +189,7 @@ class CollateConstraint extends ColumnConstraint { class ForeignKeyColumnConstraint extends ColumnConstraint { ForeignKeyClause clause; - ForeignKeyColumnConstraint(String? name, this.clause) : super(name); + ForeignKeyColumnConstraint(super.name, this.clause); @override Iterable get childNodes => [clause]; @@ -223,7 +222,7 @@ class MappedBy extends ColumnConstraint { /// The Dart expression creating the type converter we use to map this token. final InlineDartToken mapper; - MappedBy(String? name, this.mapper) : super(name); + MappedBy(super.name, this.mapper); @override final Iterable childNodes = const []; @@ -243,7 +242,7 @@ class JsonKey extends ColumnConstraint { String get jsonKey => jsonNameToken.identifier; - JsonKey(String? name, this.jsonNameToken) : super(name); + JsonKey(super.name, this.jsonNameToken); @override void transformChildren
(Transformer transformer, A arg) {} @@ -259,7 +258,7 @@ class DriftDartName extends ColumnConstraint { String get dartName => identifier.identifier; - DriftDartName(String? name, this.identifier) : super(name); + DriftDartName(super.name, this.identifier); @override void transformChildren(Transformer transformer, A arg) {} diff --git a/sqlparser/lib/src/ast/schema/table_definition.dart b/sqlparser/lib/src/ast/schema/table_definition.dart index 1a00c68a..4fb6944e 100644 --- a/sqlparser/lib/src/ast/schema/table_definition.dart +++ b/sqlparser/lib/src/ast/schema/table_definition.dart @@ -97,9 +97,8 @@ class KeyClause extends TableConstraint { ]; } - KeyClause(String? name, - {required this.isPrimaryKey, required this.columns, this.onConflict}) - : super(name); + KeyClause(super.name, + {required this.isPrimaryKey, required this.columns, this.onConflict}); @override bool constraintEquals(KeyClause other) { @@ -118,7 +117,7 @@ class KeyClause extends TableConstraint { class CheckTable extends TableConstraint { Expression expression; - CheckTable(String? name, this.expression) : super(name); + CheckTable(super.name, this.expression); @override bool constraintEquals(CheckTable other) => true; @@ -136,9 +135,8 @@ class ForeignKeyTableConstraint extends TableConstraint { List columns; ForeignKeyClause clause; - ForeignKeyTableConstraint(String? name, - {required this.columns, required this.clause}) - : super(name); + ForeignKeyTableConstraint(super.name, + {required this.columns, required this.clause}); @override bool constraintEquals(ForeignKeyTableConstraint other) => true; diff --git a/sqlparser/lib/src/ast/statements/select.dart b/sqlparser/lib/src/ast/statements/select.dart index dc06d047..1786d21c 100644 --- a/sqlparser/lib/src/ast/statements/select.dart +++ b/sqlparser/lib/src/ast/statements/select.dart @@ -8,7 +8,7 @@ abstract class BaseSelectStatement extends CrudStatement with ResultSet { @override List? resolvedColumns; - BaseSelectStatement._(WithClause? withClause) : super(withClause); + BaseSelectStatement._(super.withClause); } /// Marker interface for classes that are a [BaseSelectStatement] but aren't a diff --git a/sqlparser/lib/src/engine/module/fts5.dart b/sqlparser/lib/src/engine/module/fts5.dart index a73e082a..deb5f6f2 100644 --- a/sqlparser/lib/src/engine/module/fts5.dart +++ b/sqlparser/lib/src/engine/module/fts5.dart @@ -114,20 +114,18 @@ class Fts5Table extends Table { final String? contentRowId; Fts5Table({ - required String name, + required super.name, required List columns, this.contentTable, this.contentRowId, - CreateVirtualTableStatement? definition, + CreateVirtualTableStatement? super.definition, }) : super( - name: name, resolvedColumns: [ if (contentTable != null && contentRowId != null) RowId(), ...columns, _Fts5RankColumn(), _Fts5TableColumn(name), ], - definition: definition, isVirtual: true, ); } diff --git a/sqlparser/lib/src/reader/tokenizer/token.dart b/sqlparser/lib/src/reader/tokenizer/token.dart index b7a1f5ef..072447ea 100644 --- a/sqlparser/lib/src/reader/tokenizer/token.dart +++ b/sqlparser/lib/src/reader/tokenizer/token.dart @@ -508,7 +508,7 @@ class IdentifierToken extends Token { } abstract class VariableToken extends Token { - VariableToken(TokenType type, FileSpan span) : super(type, span); + VariableToken(super.type, super.span); } class QuestionMarkVariableToken extends Token { @@ -559,7 +559,7 @@ class KeywordToken extends Token { /// Whether this token has been used as an identifier while parsing. bool isIdentifier = false; - KeywordToken(TokenType type, FileSpan span) : super(type, span); + KeywordToken(super.type, super.span); bool canConvertToIdentifier() { // https://stackoverflow.com/a/45775719, but we don't parse indexed yet. diff --git a/sqlparser/pubspec.yaml b/sqlparser/pubspec.yaml index 56ab6dfb..81b78adf 100644 --- a/sqlparser/pubspec.yaml +++ b/sqlparser/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: charcode: ^1.2.0 dev_dependencies: - lints: ^2.0.0 + lints: ^3.0.0 test: ^1.17.4 path: ^1.8.0 ffi: ^2.0.0 From 0b537b50c14c0233c6227ab5dd3f73a5baf5e2da Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 16 Nov 2023 21:30:41 +0100 Subject: [PATCH 37/37] Reformat --- docs/lib/snippets/modular/drift/dart_example.dart | 1 - drift_dev/test/services/schema/validate_schema_test.dart | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/lib/snippets/modular/drift/dart_example.dart b/docs/lib/snippets/modular/drift/dart_example.dart index e16360fc..98ec96f6 100644 --- a/docs/lib/snippets/modular/drift/dart_example.dart +++ b/docs/lib/snippets/modular/drift/dart_example.dart @@ -1,4 +1,3 @@ - import 'example.drift.dart'; class DartExample extends ExampleDrift { diff --git a/drift_dev/test/services/schema/validate_schema_test.dart b/drift_dev/test/services/schema/validate_schema_test.dart index 578976d9..30d70430 100644 --- a/drift_dev/test/services/schema/validate_schema_test.dart +++ b/drift_dev/test/services/schema/validate_schema_test.dart @@ -42,8 +42,7 @@ class _TestDatabase extends GeneratedDatabase { @override DriftDatabaseOptions options = DriftDatabaseOptions(); - _TestDatabase.connect(super.connection) - : super.connect(); + _TestDatabase.connect(super.connection) : super.connect(); } void main() {