From d7a26a6a9a4b7a11ee5896fc178efce8c6bde80e Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 19 Feb 2024 21:28:03 +0100 Subject: [PATCH 01/38] Test variable bindings through isolate dialect Regression test for #2894 --- drift/test/isolate_test.dart | 28 +++++++++++++++++++++++++++ drift/test/test_utils/test_utils.dart | 11 +++++++++++ 2 files changed, 39 insertions(+) diff --git a/drift/test/isolate_test.dart b/drift/test/isolate_test.dart index c27c8b2e..665d8299 100644 --- a/drift/test/isolate_test.dart +++ b/drift/test/isolate_test.dart @@ -240,6 +240,34 @@ void main() { await testWith(DatabaseConnection(NativeDatabase.memory())); }); }); + + test('uses correct dialect', () async { + // Regression test for https://github.com/simolus3/drift/issues/2894 + final isolate = await DriftIsolate.spawn(() { + return NativeDatabase.memory() + .interceptWith(PretendDialectInterceptor(SqlDialect.postgres)); + }); + final database = TodoDb(await isolate.connect(singleClientMode: true)); + addTearDown(database.close); + + await database.transaction(() async { + await expectLater( + database.into(database.users).insertReturning(UsersCompanion.insert( + name: 'test user', profilePicture: Uint8List(0))), + throwsA( + isA().having( + (e) => e.remoteCause, + 'remoteCause', + isA().having( + (e) => e.causingStatement, + 'causingStatement', + contains(r'VALUES ($1, $2)'), + ), + ), + ), + ); + }); + }); } void _runTests(FutureOr Function() spawner, bool terminateIsolate, diff --git a/drift/test/test_utils/test_utils.dart b/drift/test/test_utils/test_utils.dart index 83490aad..85afbb25 100644 --- a/drift/test/test_utils/test_utils.dart +++ b/drift/test/test_utils/test_utils.dart @@ -100,3 +100,14 @@ class CustomTable extends Table with TableInfo { return; } } + +class PretendDialectInterceptor extends QueryInterceptor { + final SqlDialect _dialect; + + PretendDialectInterceptor(this._dialect); + + @override + SqlDialect dialect(QueryExecutor executor) { + return _dialect; + } +} From 4fa75cb30bfbf13356778e3f438eba3cd3f1e5a2 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 19 Feb 2024 21:46:55 +0100 Subject: [PATCH 02/38] Fix new analysis warnings from Dart 3.3 --- .../src/runtime/query_builder/expressions/expression.dart | 2 +- sqlparser/lib/src/analysis/types/data.dart | 6 +++--- sqlparser/lib/src/ast/drift/declared_statement.dart | 4 ++-- sqlparser/lib/src/ast/expressions/aggregate.dart | 2 +- sqlparser/lib/src/ast/statements/create_trigger.dart | 2 +- sqlparser/lib/utils/find_referenced_tables.dart | 2 +- .../test/parser/select/common_table_expression_test.dart | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/drift/lib/src/runtime/query_builder/expressions/expression.dart b/drift/lib/src/runtime/query_builder/expressions/expression.dart index 262fbe2f..9c5a7be3 100644 --- a/drift/lib/src/runtime/query_builder/expressions/expression.dart +++ b/drift/lib/src/runtime/query_builder/expressions/expression.dart @@ -636,7 +636,7 @@ class _SubqueryExpression extends Expression { int get hashCode => statement.hashCode; @override - bool operator ==(Object? other) { + bool operator ==(Object other) { return other is _SubqueryExpression && other.statement == statement; } } diff --git a/sqlparser/lib/src/analysis/types/data.dart b/sqlparser/lib/src/analysis/types/data.dart index 33afe39c..a0cd4933 100644 --- a/sqlparser/lib/src/analysis/types/data.dart +++ b/sqlparser/lib/src/analysis/types/data.dart @@ -81,7 +81,7 @@ class ResolvedType { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || other is ResolvedType && other.type == type && @@ -112,7 +112,7 @@ abstract class TypeHint { @override int get hashCode => runtimeType.hashCode; @override - bool operator ==(dynamic other) => other.runtimeType == runtimeType; + bool operator ==(Object other) => other.runtimeType == runtimeType; } /// Type hint to mark that this type will contain a boolean value. @@ -181,7 +181,7 @@ class ResolveResult { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || other is ResolveResult && other.type == type && diff --git a/sqlparser/lib/src/ast/drift/declared_statement.dart b/sqlparser/lib/src/ast/drift/declared_statement.dart index 40361331..550724ea 100644 --- a/sqlparser/lib/src/ast/drift/declared_statement.dart +++ b/sqlparser/lib/src/ast/drift/declared_statement.dart @@ -64,7 +64,7 @@ class SimpleName extends DeclaredStatementIdentifier { int get hashCode => name.hashCode; @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other is SimpleName && other.name == name); } @@ -87,7 +87,7 @@ class SpecialStatementIdentifier extends DeclaredStatementIdentifier { String get name => specialName; @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other is SpecialStatementIdentifier && other.specialName == specialName); diff --git a/sqlparser/lib/src/ast/expressions/aggregate.dart b/sqlparser/lib/src/ast/expressions/aggregate.dart index 8f365319..feca4f38 100644 --- a/sqlparser/lib/src/ast/expressions/aggregate.dart +++ b/sqlparser/lib/src/ast/expressions/aggregate.dart @@ -241,7 +241,7 @@ class FrameBoundary { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { if (identical(this, other)) return true; if (other.runtimeType != runtimeType) return false; diff --git a/sqlparser/lib/src/ast/statements/create_trigger.dart b/sqlparser/lib/src/ast/statements/create_trigger.dart index 14a555f8..616f372f 100644 --- a/sqlparser/lib/src/ast/statements/create_trigger.dart +++ b/sqlparser/lib/src/ast/statements/create_trigger.dart @@ -56,7 +56,7 @@ abstract class TriggerTarget extends AstNode { int get hashCode => runtimeType.hashCode; @override - bool operator ==(dynamic other) => other.runtimeType == runtimeType; + bool operator ==(Object other) => other.runtimeType == runtimeType; @override Iterable get childNodes => const Iterable.empty(); diff --git a/sqlparser/lib/utils/find_referenced_tables.dart b/sqlparser/lib/utils/find_referenced_tables.dart index e809db4d..3e95ce0d 100644 --- a/sqlparser/lib/utils/find_referenced_tables.dart +++ b/sqlparser/lib/utils/find_referenced_tables.dart @@ -68,7 +68,7 @@ class TableWrite { int get hashCode => 37 * table.hashCode + kind.hashCode; @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return other is TableWrite && other.table == table && other.kind == kind; } } diff --git a/sqlparser/test/parser/select/common_table_expression_test.dart b/sqlparser/test/parser/select/common_table_expression_test.dart index b05b4079..0b504973 100644 --- a/sqlparser/test/parser/select/common_table_expression_test.dart +++ b/sqlparser/test/parser/select/common_table_expression_test.dart @@ -6,7 +6,7 @@ import '../utils.dart'; void main() { test('parses WITH clauses', () { testStatement( - ''' + ''' WITH RECURSIVE cnt(x) AS ( SELECT 1 From a26cc44aaf7a42fa2c3508ae9846ffec21e9bb5a Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Mon, 19 Feb 2024 22:16:19 +0100 Subject: [PATCH 03/38] Add example for reflective update --- .../snippets/modular/schema_inspection.dart | 34 +++++++++++++++++++ docs/pages/docs/Dart API/schema_inspection.md | 16 +++++++++ 2 files changed, 50 insertions(+) diff --git a/docs/lib/snippets/modular/schema_inspection.dart b/docs/lib/snippets/modular/schema_inspection.dart index b7908bfe..f3de4ab4 100644 --- a/docs/lib/snippets/modular/schema_inspection.dart +++ b/docs/lib/snippets/modular/schema_inspection.dart @@ -33,4 +33,38 @@ extension FindTodoEntryById on GeneratedDatabase { return select(todos)..where((row) => row.id.equals(id)); } // #enddocregion findTodoEntryById + + // #docregion updateTitle + Future updateTitle, Row>( + T table, int id, String newTitle) async { + final columnsByName = table.columnsByName; + final stmt = update(table) + ..where((tbl) { + final idColumn = columnsByName['id']; + + if (idColumn == null) { + throw ArgumentError.value( + this, 'this', 'Must be a table with an id column'); + } + + if (idColumn.type != DriftSqlType.int) { + throw ArgumentError('Column `id` is not an integer'); + } + + return idColumn.equals(id); + }); + + final rows = await stmt.writeReturning(RawValuesInsertable({ + 'title': Variable(newTitle), + })); + + return rows.singleOrNull; + } + // #enddocregion updateTitle + + // #docregion updateTodo + Future updateTodoTitle(int id, String newTitle) { + return updateTitle(todos, id, newTitle); + } + // #enddocregion updateTodo } diff --git a/docs/pages/docs/Dart API/schema_inspection.md b/docs/pages/docs/Dart API/schema_inspection.md index 32b5520b..251fe081 100644 --- a/docs/pages/docs/Dart API/schema_inspection.md +++ b/docs/pages/docs/Dart API/schema_inspection.md @@ -49,8 +49,24 @@ To call this extension, `await myDatabase.todos.findById(3).getSingle()` could b A nice thing about defining the method as an extension is that type inference works really well - calling `findById` on `todos` returns a `Todo` instance, the generated data class for this table. +## Updates and inserts + The same approach also works to construct update, delete and insert statements (although those require a [TableInfo] instead of a [ResultSetImplementation] as views are read-only). +Also, updates and inserts use an `Insertable` object which represents a partial row of updated or +inserted columns, respectively. +With a known table, one would use the generated typed `Companion` objects for that. +But this can also be done with schema introspection thanks to the `RawValuesInsertable`, which +can be used as a generic `Insertable` backed by a map of column names to values. + +This example builds on the previous one to update the `title` column of a generic table based on a filter +of the `id` column: + +{% include "blocks/snippet" snippets = snippets name = 'updateTitle' %} + +This method can then be called like this: + +{% include "blocks/snippet" snippets = snippets name = 'updateTodo' %} Hopefully, this page gives you some pointers to start reflectively inspecting your drift databases. The linked Dart documentation also expains the concepts in more detail. From 27877a72e974a62d4f23ea4677f7311827adf404 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 21 Feb 2024 10:49:36 +0100 Subject: [PATCH 04/38] Clarify schema introspection update snippet --- .../snippets/modular/schema_inspection.dart | 24 ++++++++++--------- docs/pages/docs/Dart API/schema_inspection.md | 4 ++-- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/lib/snippets/modular/schema_inspection.dart b/docs/lib/snippets/modular/schema_inspection.dart index f3de4ab4..1bbd75e7 100644 --- a/docs/lib/snippets/modular/schema_inspection.dart +++ b/docs/lib/snippets/modular/schema_inspection.dart @@ -25,16 +25,8 @@ extension FindById } // #enddocregion findById -extension FindTodoEntryById on GeneratedDatabase { - Todos get todos => Todos(this); - - // #docregion findTodoEntryById - Selectable findTodoEntryById(int id) { - return select(todos)..where((row) => row.id.equals(id)); - } - // #enddocregion findTodoEntryById - - // #docregion updateTitle +// #docregion updateTitle +extension UpdateTitle on DatabaseConnectionUser { Future updateTitle, Row>( T table, int id, String newTitle) async { final columnsByName = table.columnsByName; @@ -60,7 +52,17 @@ extension FindTodoEntryById on GeneratedDatabase { return rows.singleOrNull; } - // #enddocregion updateTitle +} +// #enddocregion updateTitle + +extension FindTodoEntryById on GeneratedDatabase { + Todos get todos => Todos(this); + + // #docregion findTodoEntryById + Selectable findTodoEntryById(int id) { + return select(todos)..where((row) => row.id.equals(id)); + } + // #enddocregion findTodoEntryById // #docregion updateTodo Future updateTodoTitle(int id, String newTitle) { diff --git a/docs/pages/docs/Dart API/schema_inspection.md b/docs/pages/docs/Dart API/schema_inspection.md index 251fe081..bc596eaa 100644 --- a/docs/pages/docs/Dart API/schema_inspection.md +++ b/docs/pages/docs/Dart API/schema_inspection.md @@ -64,7 +64,7 @@ of the `id` column: {% include "blocks/snippet" snippets = snippets name = 'updateTitle' %} -This method can then be called like this: +In a database or database accessor class, the method can then be called like this: {% include "blocks/snippet" snippets = snippets name = 'updateTodo' %} @@ -75,4 +75,4 @@ If you have questions about this, or have a suggestion for more examples to incl [ResultSetImplementation]: https://drift.simonbinder.eu/api/drift/resultsetimplementation-class [TableInfo]: https://drift.simonbinder.eu/api/drift/tableinfo-mixin [ViewInfo]: https://drift.simonbinder.eu/api/drift/viewinfo-class -[GeneratedColumn]: https://drift.simonbinder.eu/api/drift/generatedcolumn-class \ No newline at end of file +[GeneratedColumn]: https://drift.simonbinder.eu/api/drift/generatedcolumn-class From f9d5443a8e4083ddfc391acb93a5aa0f7da77072 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 22 Feb 2024 17:23:08 +0100 Subject: [PATCH 05/38] Migrate wasm internals to new interop --- drift/CHANGELOG.md | 1 + .../query_builder/schema/column_impl.dart | 2 +- .../query_builder/statements/insert.dart | 2 +- .../lib/src/web/broadcast_stream_queries.dart | 68 +++--- drift/lib/src/web/new_channel.dart | 50 +++++ drift/lib/src/web/wasm_setup.dart | 99 +++++---- .../src/web/wasm_setup/dedicated_worker.dart | 15 +- drift/lib/src/web/wasm_setup/protocol.dart | 202 +++++++++--------- drift/lib/src/web/wasm_setup/shared.dart | 91 +++++--- .../lib/src/web/wasm_setup/shared_worker.dart | 23 +- drift/lib/wasm.dart | 19 +- drift/lib/web.dart | 2 +- drift/pubspec.yaml | 6 +- drift/test/source_code_test.dart | 56 +++++ drift/test/test_utils/matchers.dart | 2 +- .../integration_tests/web_wasm/web/main.dart | 2 +- .../web_wasm/web/worker.dart | 2 +- 17 files changed, 405 insertions(+), 237 deletions(-) create mode 100644 drift/lib/src/web/new_channel.dart create mode 100644 drift/test/source_code_test.dart diff --git a/drift/CHANGELOG.md b/drift/CHANGELOG.md index f7c301f0..a6ddd65a 100644 --- a/drift/CHANGELOG.md +++ b/drift/CHANGELOG.md @@ -12,6 +12,7 @@ - Improve stack traces for errors happening on drift isolates (which includes usages of `NativeDatabase.createInBackground`). - Don't cache `EXPLAIN` statements, avoiding schema locks. +- Migrate `WasmDatabase` to `dart:js_interop` and `package:web`. ## 2.15.0 diff --git a/drift/lib/src/runtime/query_builder/schema/column_impl.dart b/drift/lib/src/runtime/query_builder/schema/column_impl.dart index 263aef51..ff4a4855 100644 --- a/drift/lib/src/runtime/query_builder/schema/column_impl.dart +++ b/drift/lib/src/runtime/query_builder/schema/column_impl.dart @@ -162,7 +162,7 @@ class GeneratedColumn extends Column { // these custom constraints refer to builtin constraints from drift if (!isSerial && _defaultConstraints != null) { - _defaultConstraints!(into); + _defaultConstraints(into); } } else if ($customConstraints?.isNotEmpty == true) { into.buffer diff --git a/drift/lib/src/runtime/query_builder/statements/insert.dart b/drift/lib/src/runtime/query_builder/statements/insert.dart index 7d22a953..fa473594 100644 --- a/drift/lib/src/runtime/query_builder/statements/insert.dart +++ b/drift/lib/src/runtime/query_builder/statements/insert.dart @@ -348,7 +348,7 @@ class InsertStatement { if (onConflict._where != null) { ctx.writeWhitespace(); - final where = onConflict._where!( + final where = onConflict._where( table.asDslTable, table.createAlias('excluded').asDslTable); where.writeInto(ctx); } diff --git a/drift/lib/src/web/broadcast_stream_queries.dart b/drift/lib/src/web/broadcast_stream_queries.dart index 2b98d388..af855215 100644 --- a/drift/lib/src/web/broadcast_stream_queries.dart +++ b/drift/lib/src/web/broadcast_stream_queries.dart @@ -1,34 +1,47 @@ +@JS() +library; + import 'dart:async'; -import 'dart:html'; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; import 'package:drift/src/runtime/api/runtime_api.dart'; import 'package:drift/src/runtime/executor/stream_queries.dart'; -import 'package:js/js.dart'; -import 'package:js/js_util.dart'; +import 'package:web/web.dart' as web; + +@JS('Array') +extension type _ArrayWrapper._(JSArray _) implements JSObject { + external static JSBoolean isArray(JSAny? value); +} /// A [StreamQueryStore] using [web broadcast] APIs /// /// [web broadcast]: https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API class BroadcastStreamQueryStore extends StreamQueryStore { - final BroadcastChannel _channel; - StreamSubscription? _messageFromChannel; + final web.BroadcastChannel _channel; + StreamSubscription? _messageFromChannel; /// Constructs a broadcast query store with the given [identifier]. /// /// All query stores with the same identifier will share stream query updates. BroadcastStreamQueryStore(String identifier) - : _channel = BroadcastChannel('drift_updates_$identifier') { - _messageFromChannel = _channel.onMessage.listen(_handleMessage); + : _channel = web.BroadcastChannel('drift_updates_$identifier') { + _messageFromChannel = web.EventStreamProviders.messageEvent + .forTarget(_channel) + .listen(_handleMessage); } - void _handleMessage(MessageEvent message) { - // Using getProperty to avoid dart2js structured clone that turns the - // anonymous object into a map. - final data = getProperty(message, 'data'); - if (data is! List || data.isEmpty) return; + void _handleMessage(web.MessageEvent message) { + final data = message.data; + if (!_ArrayWrapper.isArray(data).toDart) { + return; + } + + final asList = (data as JSArray).toDart; + if (asList.isEmpty) return; super.handleTableUpdates({ - for (final entry in data.cast<_SerializedTableUpdate>()) + for (final entry in asList.cast<_SerializedTableUpdate>()) entry.toTableUpdate, }); } @@ -39,7 +52,7 @@ class BroadcastStreamQueryStore extends StreamQueryStore { _channel.postMessage([ for (final update in updates) _SerializedTableUpdate.of(update), - ]); + ].toJS); } @override @@ -50,34 +63,31 @@ class BroadcastStreamQueryStore extends StreamQueryStore { } /// Whether the current JavaScript context supports broadcast channels. - static bool get supported => hasProperty(globalThis, 'BroadcastChannel'); + static bool get supported => globalContext.has('BroadcastChannel'); } @JS() @anonymous -@staticInterop -class _SerializedTableUpdate { +extension type _SerializedTableUpdate._(JSObject _) implements JSObject { external factory _SerializedTableUpdate({ - required String? kind, - required String table, + required JSString? kind, + required JSString table, }); factory _SerializedTableUpdate.of(TableUpdate update) { - return _SerializedTableUpdate(kind: update.kind?.name, table: update.table); + return _SerializedTableUpdate( + kind: update.kind?.name.toJS, + table: update.table.toJS, + ); } -} -extension on _SerializedTableUpdate { - @JS() - external String? get kind; - - @JS() - external String get table; + external JSString? get kind; + external JSString get table; TableUpdate get toTableUpdate { - final updateKind = _updateKindByName[kind]; + final updateKind = _updateKindByName[kind?.toDart]; - return TableUpdate(table, kind: updateKind); + return TableUpdate(table.toDart, kind: updateKind); } static final _updateKindByName = UpdateKind.values.asNameMap(); diff --git a/drift/lib/src/web/new_channel.dart b/drift/lib/src/web/new_channel.dart new file mode 100644 index 00000000..4064d16a --- /dev/null +++ b/drift/lib/src/web/new_channel.dart @@ -0,0 +1,50 @@ +import 'dart:js_interop'; + +import 'package:stream_channel/stream_channel.dart'; + +import 'package:web/web.dart' as web; + +/// Extension to transform a raw [MessagePort] from web workers into a Dart +/// [StreamChannel]. +extension WebPortToChannel on web.MessagePort { + static const _disconnectMessage = '_disconnect'; + + /// Converts this port to a two-way communication channel, exposed as a + /// [StreamChannel]. + /// + /// This can be used to implement a remote database connection over service + /// workers. + /// + /// The [explicitClose] parameter can be used to control whether a close + /// message should be sent through the channel when it is closed. This will + /// cause it to be closed on the other end as well. Note that this is not a + /// reliable way of determining channel closures though, as there is no event + /// for channels being closed due to a tab or worker being closed. + /// Both "ends" of a JS channel calling [channel] on their part must use the + /// value for [explicitClose]. + StreamChannel channel({bool explicitClose = false}) { + final controller = StreamChannelController(); + + onmessage = (web.MessageEvent event) { + final message = event.data; + + if (explicitClose && message == _disconnectMessage.toJS) { + // Other end has closed the connection + controller.local.sink.close(); + } else { + controller.local.sink.add(message.dartify()); + } + }.toJS; + + controller.local.stream.listen((e) => postMessage(e.jsify()), onDone: () { + // Closed locally, inform the other end. + if (explicitClose) { + postMessage(_disconnectMessage.toJS); + } + + close(); + }); + + return controller.foreign; + } +} diff --git a/drift/lib/src/web/wasm_setup.dart b/drift/lib/src/web/wasm_setup.dart index f3d07038..d692766a 100644 --- a/drift/lib/src/web/wasm_setup.dart +++ b/drift/lib/src/web/wasm_setup.dart @@ -7,22 +7,23 @@ /// asynchronous // ignore_for_file: public_member_api_docs @internal +@JS() library; import 'dart:async'; -import 'dart:html'; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; import 'package:async/async.dart'; import 'package:drift/drift.dart'; import 'package:drift/remote.dart'; import 'package:drift/wasm.dart'; -import 'package:js/js.dart'; -import 'package:js/js_util.dart'; import 'package:meta/meta.dart'; import 'package:sqlite3/wasm.dart'; +import 'package:web/web.dart' as web; import 'broadcast_stream_queries.dart'; -import 'channel.dart'; +import 'new_channel.dart'; import 'wasm_setup/shared.dart'; import 'wasm_setup/protocol.dart'; @@ -32,10 +33,10 @@ import 'wasm_setup/protocol.dart'; external bool get crossOriginIsolated; /// Whether shared workers can be constructed in the current context. -bool get supportsSharedWorkers => hasProperty(globalThis, 'SharedWorker'); +bool get supportsSharedWorkers => globalContext.has('SharedWorker'); /// Whether dedicated workers can be constructed in the current context. -bool get supportsWorkers => hasProperty(globalThis, 'Worker'); +bool get supportsWorkers => globalContext.has('Worker'); class WasmDatabaseOpener { final Uri sqlite3WasmUri; @@ -107,7 +108,7 @@ class WasmDatabaseOpener { Future _probeDedicated() async { if (supportsWorkers) { final dedicatedWorker = _dedicatedWorker = - _DriftWorker.dedicated(Worker(driftWorkerUri.toString())); + _DriftWorker.dedicated(web.Worker(driftWorkerUri.toString())); _createCompatibilityCheck().sendTo(dedicatedWorker.send); final status = await dedicatedWorker.workerMessages.nextNoError @@ -133,8 +134,8 @@ class WasmDatabaseOpener { Future _probeShared() async { if (supportsSharedWorkers) { final sharedWorker = - SharedWorker(driftWorkerUri.toString(), 'drift worker'); - final port = sharedWorker.port!; + web.SharedWorker(driftWorkerUri.toString(), 'drift worker'.toJS); + final port = sharedWorker.port; final shared = _sharedWorker = _DriftWorker.shared(sharedWorker, port); // First, the shared worker will tell us which features it supports. @@ -161,40 +162,38 @@ class WasmDatabaseOpener { } final class _DriftWorker { - final AbstractWorker worker; + /// Either a [web.SharedWorker] or a [web.Worker]. + final JSObject worker; ProtocolVersion version = ProtocolVersion.legacy; /// The message port to communicate with the worker, if it's a shared worker. - final MessagePort? portForShared; + final web.MessagePort? portForShared; final StreamQueue workerMessages; - _DriftWorker.dedicated(Worker this.worker) + _DriftWorker.dedicated(web.Worker this.worker) : portForShared = null, - workerMessages = - StreamQueue(_readMessages(worker.onMessage, worker.onError)); + workerMessages = StreamQueue(_readMessages(worker, worker)); - _DriftWorker.shared(SharedWorker this.worker, this.portForShared) - : workerMessages = - StreamQueue(_readMessages(worker.port!.onMessage, worker.onError)); + _DriftWorker.shared(web.SharedWorker this.worker, this.portForShared) + : workerMessages = StreamQueue(_readMessages(worker.port, worker)) { + (worker as web.SharedWorker).port.start(); + } - void send(Object? msg, [List? transfer]) { - switch (worker) { - case final Worker worker: - worker.postMessage(msg, transfer); - case SharedWorker(): - portForShared!.postMessage(msg, transfer); + void send(JSAny? msg, List? transfer) { + if (portForShared case final port?) { + port.postMessage(msg, (transfer ?? const []).toJS); + } else { + (worker as web.Worker).postMessage(msg, (transfer ?? const []).toJS); } } void close() { workerMessages.cancel(); - - switch (worker) { - case final Worker dedicated: - dedicated.terminate(); - case SharedWorker(): - portForShared!.close(); + if (portForShared case final port?) { + port.close(); + } else { + (worker as web.Worker).terminate(); } } } @@ -225,9 +224,9 @@ final class _ProbeResult implements WasmProbeResult { FutureOr Function()? initializeDatabase, WasmDatabaseSetup? localSetup, }) async { - final channel = MessageChannel(); + final channel = web.MessageChannel(); final initializer = initializeDatabase; - final initChannel = initializer != null ? MessageChannel() : null; + final initChannel = initializer != null ? web.MessageChannel() : null; ServeDriftDatabase message; final sharedWorker = opener._sharedWorker; @@ -276,18 +275,24 @@ final class _ProbeResult implements WasmProbeResult { initializeDatabase, localSetup); } - initChannel?.port1.onMessage.listen((event) async { - // The worker hosting the database is asking for the initial blob because - // the database doesn't exist. - Uint8List? result; - try { - result = await initializer?.call(); - } finally { - initChannel.port1 - ..postMessage(result, [if (result != null) result.buffer]) - ..close(); - } - }); + if (initChannel != null) { + initChannel.port1.start(); + web.EventStreamProviders.messageEvent + .forTarget(initChannel.port1) + .listen((event) async { + // The worker hosting the database is asking for the initial blob because + // the database doesn't exist. + Uint8List? result; + try { + result = await initializer?.call(); + } finally { + initChannel.port1 + ..postMessage( + result?.toJS, [if (result != null) result.buffer.toJS].toJS) + ..close(); + } + }); + } final local = channel.port1 .channel(explicitClose: message.protocolVersion >= ProtocolVersion.v1); @@ -350,7 +355,13 @@ final class _ProbeResult implements WasmProbeResult { } Stream _readMessages( - Stream messages, Stream errors) { + web.EventTarget messageTarget, + web.EventTarget errorTarget, +) { + final messages = + web.EventStreamProviders.messageEvent.forTarget(messageTarget); + final errors = web.EventStreamProviders.errorEvent.forTarget(errorTarget); + final mappedMessages = messages.map(WasmInitializationMessage.read); return Stream.multi((listener) { diff --git a/drift/lib/src/web/wasm_setup/dedicated_worker.dart b/drift/lib/src/web/wasm_setup/dedicated_worker.dart index 52246573..3e185c6e 100644 --- a/drift/lib/src/web/wasm_setup/dedicated_worker.dart +++ b/drift/lib/src/web/wasm_setup/dedicated_worker.dart @@ -1,10 +1,12 @@ // ignore_for_file: public_member_api_docs import 'dart:async'; -import 'dart:html'; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; -import 'package:js/js_util.dart'; import 'package:sqlite3/wasm.dart'; +import 'package:web/web.dart' + show DedicatedWorkerGlobalScope, EventStreamProviders; import '../../utils/synchronized.dart'; import 'protocol.dart'; @@ -22,7 +24,7 @@ class DedicatedDriftWorker { : _servers = DriftServerController(setup); void start() { - self.onMessage.listen((event) { + EventStreamProviders.messageEvent.forTarget(self).listen((event) { final message = WasmInitializationMessage.read(event); _handleMessage(message); }); @@ -69,11 +71,10 @@ class DedicatedDriftWorker { } DedicatedWorkerCompatibilityResult( - supportsNestedWorkers: hasProperty(globalThis, 'Worker'), + supportsNestedWorkers: globalContext.has('Worker'), canAccessOpfs: supportsOpfs, supportsIndexedDb: supportsIndexedDb, - supportsSharedArrayBuffers: - hasProperty(globalThis, 'SharedArrayBuffer'), + supportsSharedArrayBuffers: globalContext.has('SharedArrayBuffer'), opfsExists: opfsExists, indexedDbExists: indexedDbExists, existingDatabases: existingDatabases, @@ -83,7 +84,7 @@ class DedicatedDriftWorker { _servers.serve(message); case StartFileSystemServer(sqlite3Options: final options): final worker = await VfsWorker.create(options); - self.postMessage(true); + self.postMessage(true.toJS); await worker.start(); case DeleteDatabase(database: (final storage, final name)): try { diff --git a/drift/lib/src/web/wasm_setup/protocol.dart b/drift/lib/src/web/wasm_setup/protocol.dart index 3d681efc..6bb5b179 100644 --- a/drift/lib/src/web/wasm_setup/protocol.dart +++ b/drift/lib/src/web/wasm_setup/protocol.dart @@ -1,9 +1,9 @@ // ignore_for_file: public_member_api_docs -import 'dart:html'; -import 'dart:js'; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; -import 'package:js/js_util.dart'; +import 'package:web/web.dart' hide WorkerOptions; import 'package:sqlite3/wasm.dart'; import 'types.dart'; @@ -18,8 +18,8 @@ class ProtocolVersion { const ProtocolVersion._(this.versionCode); - void writeToJs(Object object) { - setProperty(object, 'v', versionCode); + void writeToJs(JSObject object) { + object['v'] = versionCode.toJS; } bool operator >=(ProtocolVersion other) { @@ -36,9 +36,9 @@ class ProtocolVersion { }; } - static ProtocolVersion fromJsObject(Object object) { - if (hasProperty(object, 'v')) { - return negotiate(getProperty(object, 'v')); + static ProtocolVersion fromJsObject(JSObject object) { + if (object.has('v')) { + return negotiate((object['v'] as JSNumber).toDartInt); } else { return legacy; } @@ -58,52 +58,56 @@ class ProtocolVersion { static const current = v1; } -typedef PostMessage = void Function(Object? msg, [List? transfer]); +typedef PostMessage = void Function(JSObject? msg, List? transfer); /// Sealed superclass for JavaScript objects exchanged between the UI tab and /// workers spawned by drift to find a suitable database implementation. sealed class WasmInitializationMessage { WasmInitializationMessage(); - factory WasmInitializationMessage.fromJs(Object jsObject) { - final type = getProperty(jsObject, 'type'); - final payload = getProperty(jsObject, 'payload'); + factory WasmInitializationMessage.fromJs(JSObject jsObject) { + final type = (jsObject['type'] as JSString).toDart; + final payload = jsObject['payload']; return switch (type) { - WorkerError.type => WorkerError.fromJsPayload(payload!), - ServeDriftDatabase.type => ServeDriftDatabase.fromJsPayload(payload!), + WorkerError.type => WorkerError.fromJsPayload(payload as JSObject), + ServeDriftDatabase.type => + ServeDriftDatabase.fromJsPayload(payload as JSObject), StartFileSystemServer.type => - StartFileSystemServer.fromJsPayload(payload!), + StartFileSystemServer.fromJsPayload(payload as JSObject), RequestCompatibilityCheck.type => RequestCompatibilityCheck.fromJsPayload(payload), DedicatedWorkerCompatibilityResult.type => - DedicatedWorkerCompatibilityResult.fromJsPayload(payload!), + DedicatedWorkerCompatibilityResult.fromJsPayload(payload as JSObject), SharedWorkerCompatibilityResult.type => - SharedWorkerCompatibilityResult.fromJsPayload(payload!), - DeleteDatabase.type => DeleteDatabase.fromJsPayload(payload!), + SharedWorkerCompatibilityResult.fromJsPayload(payload as JSArray), + DeleteDatabase.type => DeleteDatabase.fromJsPayload(payload as JSAny), _ => throw ArgumentError('Unknown type $type'), }; } factory WasmInitializationMessage.read(MessageEvent event) { - // Not using event.data because we don't want the SDK to dartify the raw JS - // object we're passing around. - final rawData = getProperty(event, 'data'); - return WasmInitializationMessage.fromJs(rawData); + return WasmInitializationMessage.fromJs(event.data as JSObject); } void sendTo(PostMessage sender); void sendToWorker(Worker worker) { - sendTo(worker.postMessage); + sendTo((msg, transfer) { + worker.postMessage(msg, (transfer ?? const []).toJS); + }); } void sendToPort(MessagePort port) { - sendTo(port.postMessage); + sendTo((msg, transfer) { + port.postMessage(msg, (transfer ?? const []).toJS); + }); } void sendToClient(DedicatedWorkerGlobalScope worker) { - sendTo(worker.postMessage); + sendTo((msg, transfer) { + worker.postMessage(msg, (transfer ?? const []).toJS); + }); } } @@ -156,16 +160,15 @@ final class SharedWorkerCompatibilityResult extends CompatibilityResult { required super.version, }); - factory SharedWorkerCompatibilityResult.fromJsPayload(Object payload) { - final asList = payload as List; + factory SharedWorkerCompatibilityResult.fromJsPayload(JSArray payload) { + final asList = payload.toDart; final asBooleans = asList.cast(); final List existingDatabases; var version = ProtocolVersion.legacy; if (asList.length > 5) { - existingDatabases = - EncodeLocations.readFromJs(asList[5] as List); + existingDatabases = EncodeLocations.readFromJs(asList[5] as JSArray); if (asList.length > 6) { version = ProtocolVersion.negotiate(asList[6] as int); @@ -187,15 +190,17 @@ final class SharedWorkerCompatibilityResult extends CompatibilityResult { @override void sendTo(PostMessage sender) { - sender.sendTyped(type, [ - canSpawnDedicatedWorkers, - dedicatedWorkersCanUseOpfs, - canUseIndexedDb, - indexedDbExists, - opfsExists, - existingDatabases.encodeToJs(), - version.versionCode, - ]); + sender.sendTyped( + type, + [ + canSpawnDedicatedWorkers.toJS, + dedicatedWorkersCanUseOpfs.toJS, + canUseIndexedDb.toJS, + indexedDbExists.toJS, + opfsExists.toJS, + existingDatabases.encodeToJs(), + version.versionCode.toJS, + ].toJS); } @override @@ -216,13 +221,13 @@ final class WorkerError extends WasmInitializationMessage implements Exception { WorkerError(this.error); - factory WorkerError.fromJsPayload(Object payload) { - return WorkerError(payload as String); + factory WorkerError.fromJsPayload(JSObject payload) { + return WorkerError((payload as JSString).toDart); } @override void sendTo(PostMessage sender) { - sender.sendTyped(type, error); + sender.sendTyped(type, error.toJS); } @override @@ -252,32 +257,32 @@ final class ServeDriftDatabase extends WasmInitializationMessage { required this.protocolVersion, }); - factory ServeDriftDatabase.fromJsPayload(Object payload) { + factory ServeDriftDatabase.fromJsPayload(JSObject payload) { return ServeDriftDatabase( - sqlite3WasmUri: Uri.parse(getProperty(payload, 'sqlite')), - port: getProperty(payload, 'port'), + sqlite3WasmUri: Uri.parse((payload['sqlite'] as JSString).toDart), + port: payload['port'] as MessagePort, storage: WasmStorageImplementation.values - .byName(getProperty(payload, 'storage')), - databaseName: getProperty(payload, 'database'), - initializationPort: getProperty(payload, 'initPort'), + .byName((payload['storage'] as JSString).toDart), + databaseName: (payload['database'] as JSString).toDart, + initializationPort: payload['initPort'] as MessagePort?, protocolVersion: ProtocolVersion.fromJsObject(payload), ); } @override void sendTo(PostMessage sender) { - final object = newObject(); - setProperty(object, 'sqlite', sqlite3WasmUri.toString()); - setProperty(object, 'port', port); - setProperty(object, 'storage', storage.name); - setProperty(object, 'database', databaseName); - final initPort = initializationPort; - setProperty(object, 'initPort', initPort); + final object = JSObject() + ..['sqlite'] = sqlite3WasmUri.toString().toJS + ..['port'] = port + ..['storage'] = storage.name.toJS + ..['database'] = databaseName.toJS + ..['initPort'] = initializationPort; + protocolVersion.writeToJs(object); sender.sendTyped(type, object, [ port, - if (initPort != null) initPort, + if (initializationPort != null) initializationPort!, ]); } } @@ -293,13 +298,13 @@ final class RequestCompatibilityCheck extends WasmInitializationMessage { RequestCompatibilityCheck(this.databaseName); - factory RequestCompatibilityCheck.fromJsPayload(Object? payload) { - return RequestCompatibilityCheck(payload as String); + factory RequestCompatibilityCheck.fromJsPayload(JSAny? payload) { + return RequestCompatibilityCheck((payload as JSString).toDart); } @override void sendTo(PostMessage sender) { - sender.sendTyped(type, databaseName); + sender.sendTyped(type, databaseName.toJS); } } @@ -322,22 +327,23 @@ final class DedicatedWorkerCompatibilityResult extends CompatibilityResult { required super.version, }); - factory DedicatedWorkerCompatibilityResult.fromJsPayload(Object payload) { + factory DedicatedWorkerCompatibilityResult.fromJsPayload(JSObject payload) { final existingDatabases = []; - if (hasProperty(payload, 'existing')) { + if (payload.has('existing')) { existingDatabases - .addAll(EncodeLocations.readFromJs(getProperty(payload, 'existing'))); + .addAll(EncodeLocations.readFromJs(payload['existing'] as JSArray)); } return DedicatedWorkerCompatibilityResult( - supportsNestedWorkers: getProperty(payload, 'supportsNestedWorkers'), - canAccessOpfs: getProperty(payload, 'canAccessOpfs'), + supportsNestedWorkers: + (payload['supportsNestedWorkers'] as JSBoolean).toDart, + canAccessOpfs: (payload['canAccessOpfs'] as JSBoolean).toDart, supportsSharedArrayBuffers: - getProperty(payload, 'supportsSharedArrayBuffers'), - supportsIndexedDb: getProperty(payload, 'supportsIndexedDb'), - indexedDbExists: getProperty(payload, 'indexedDbExists'), - opfsExists: getProperty(payload, 'opfsExists'), + (payload['supportsSharedArrayBuffers'] as JSBoolean).toDart, + supportsIndexedDb: (payload['supportsIndexedDb'] as JSBoolean).toDart, + indexedDbExists: (payload['indexedDbExists'] as JSBoolean).toDart, + opfsExists: (payload['opfsExists'] as JSBoolean).toDart, existingDatabases: existingDatabases, version: ProtocolVersion.fromJsObject(payload), ); @@ -345,16 +351,14 @@ final class DedicatedWorkerCompatibilityResult extends CompatibilityResult { @override void sendTo(PostMessage sender) { - final object = newObject(); - - setProperty(object, 'supportsNestedWorkers', supportsNestedWorkers); - setProperty(object, 'canAccessOpfs', canAccessOpfs); - setProperty(object, 'supportsIndexedDb', supportsIndexedDb); - setProperty( - object, 'supportsSharedArrayBuffers', supportsSharedArrayBuffers); - setProperty(object, 'indexedDbExists', indexedDbExists); - setProperty(object, 'opfsExists', opfsExists); - setProperty(object, 'existing', existingDatabases.encodeToJs()); + final object = JSObject() + ..['supportsNestedWorkers'] = supportsNestedWorkers.toJS + ..['canAccessOpfs'] = canAccessOpfs.toJS + ..['supportsIndexedDb'] = supportsIndexedDb.toJS + ..['supportsSharedArrayBuffers'] = supportsSharedArrayBuffers.toJS + ..['indexedDbExists'] = indexedDbExists.toJS + ..['opfsExists'] = opfsExists.toJS + ..['existing'] = existingDatabases.encodeToJs(); version.writeToJs(object); sender.sendTyped(type, object); @@ -381,13 +385,13 @@ final class StartFileSystemServer extends WasmInitializationMessage { StartFileSystemServer(this.sqlite3Options); - factory StartFileSystemServer.fromJsPayload(Object payload) { + factory StartFileSystemServer.fromJsPayload(JSObject payload) { return StartFileSystemServer(payload as WorkerOptions); } @override void sendTo(PostMessage sender) { - sender.sendTyped(type, sqlite3Options); + sender.sendTyped(type, sqlite3Options as JSObject); } } @@ -398,53 +402,51 @@ final class DeleteDatabase extends WasmInitializationMessage { DeleteDatabase(this.database); - factory DeleteDatabase.fromJsPayload(Object payload) { - final asList = payload as List; + factory DeleteDatabase.fromJsPayload(JSAny payload) { + final asList = (payload as JSArray).toDart; return DeleteDatabase(( - WebStorageApi.byName[asList[0] as String]!, - asList[1] as String, + WebStorageApi.byName[(asList[0] as JSString).toDart]!, + (asList[1] as JSString).toDart, )); } @override void sendTo(PostMessage sender) { - sender.sendTyped(type, [database.$1.name, database.$2]); + sender.sendTyped(type, [database.$1.name.toJS, database.$2.toJS].toJS); } } extension EncodeLocations on List { - static List readFromJs(List object) { + static List readFromJs(JSArray object) { final existing = []; - for (final entry in object) { + for (final entry in object.toDart.cast()) { existing.add(( - WebStorageApi.byName[getProperty(entry as Object, 'l')]!, - getProperty(entry, 'n'), + WebStorageApi.byName[(entry['l'] as JSString).toDart]!, + (entry['n'] as JSString).toDart, )); } return existing; } - Object encodeToJs() { - final existing = JsArray(); + JSObject encodeToJs() { + final existing = []; for (final entry in this) { - final object = newObject(); - setProperty(object, 'l', entry.$1.name); - setProperty(object, 'n', entry.$2); - - existing.add(object); + existing.add(JSObject() + ..['l'] = entry.$1.name.toJS + ..['n'] = entry.$2.toJS); } - return existing; + return existing.toJS; } } extension on PostMessage { - void sendTyped(String type, Object? payload, [List? transfer]) { - final object = newObject(); - setProperty(object, 'type', type); - setProperty(object, 'payload', payload); + void sendTyped(String type, JSAny? payload, [List? transfer]) { + final object = JSObject() + ..['type'] = type.toJS + ..['payload'] = payload; call(object, transfer); } diff --git a/drift/lib/src/web/wasm_setup/shared.dart b/drift/lib/src/web/wasm_setup/shared.dart index ab582b8a..6b378d90 100644 --- a/drift/lib/src/web/wasm_setup/shared.dart +++ b/drift/lib/src/web/wasm_setup/shared.dart @@ -1,17 +1,25 @@ import 'dart:async'; -import 'dart:html'; -import 'dart:indexed_db'; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; import 'package:drift/drift.dart'; import 'package:drift/remote.dart'; import 'package:drift/wasm.dart'; -import 'package:js/js_util.dart'; +import 'package:web/web.dart' + show + Worker, + IDBFactory, + IDBRequest, + IDBDatabase, + IDBVersionChangeEvent, + EventStreamProviders, + MessageEvent; // ignore: implementation_imports import 'package:sqlite3/src/wasm/js_interop/file_system_access.dart'; import 'package:sqlite3/wasm.dart'; import 'package:stream_channel/stream_channel.dart'; -import '../channel.dart'; +import '../new_channel.dart'; import 'protocol.dart'; /// Checks whether the OPFS API is likely to be correctly implemented in the @@ -38,10 +46,10 @@ Future checkOpfsSupport() async { // In earlier versions of the OPFS standard, some methods like `getSize()` // on a sync file handle have actually been asynchronous. We don't support // Browsers that implement the outdated spec. - final getSizeResult = callMethod(openedFile, 'getSize', []); - if (typeofEquals(getSizeResult, 'object')) { + final getSizeResult = (openedFile as JSObject).callMethod('getSize'.toJS); + if (getSizeResult.typeofEquals('object')) { // Returned a promise, that's no good. - await promiseToFuture(getSizeResult!); + await (getSizeResult as JSPromise).toDart; return false; } @@ -61,18 +69,18 @@ Future checkOpfsSupport() async { /// Checks whether IndexedDB is working in the current browser. Future checkIndexedDbSupport() async { - if (!hasProperty(globalThis, 'indexedDB') || + if (!globalContext.has('indexedDB') || // FileReader needed to read and write blobs efficiently - !hasProperty(globalThis, 'FileReader')) { + !globalContext.has('FileReader')) { return false; } - final idb = getProperty(globalThis, 'indexedDB'); + final idb = globalContext['indexedDB'] as IDBFactory; try { const name = 'drift_mock_db'; - final mockDb = await idb.open(name); + final mockDb = await idb.open(name).complete(); mockDb.close(); idb.deleteDatabase(name); } catch (error) { @@ -87,19 +95,16 @@ Future checkIndexedDbExists(String databaseName) async { bool? indexedDbExists; try { - final idb = getProperty(globalThis, 'indexedDB'); + final idb = globalContext['indexedDB'] as IDBFactory; - final database = await idb.open( - databaseName, - // Current schema version used by the [IndexedDbFileSystem] - version: 1, - onUpgradeNeeded: (event) { - // If there's an upgrade, we're going from 0 to 1 - the database doesn't - // exist! Abort the transaction so that we don't create it here. - event.target.transaction!.abort(); - indexedDbExists = false; - }, - ); + final openRequest = idb.open(databaseName, 1); + openRequest.onupgradeneeded = (IDBVersionChangeEvent event) { + // If there's an upgrade, we're going from 0 to 1 - the database doesn't + // exist! Abort the transaction so that we don't create it here. + openRequest.transaction!.abort(); + indexedDbExists = false; + }.toJS; + final database = await openRequest.complete(); indexedDbExists ??= true; database.close(); @@ -112,9 +117,9 @@ Future checkIndexedDbExists(String databaseName) async { /// Deletes a database from IndexedDb if supported. Future deleteDatabaseInIndexedDb(String databaseName) async { - final idb = window.indexedDB; - if (idb != null) { - await idb.deleteDatabase(databaseName); + if (globalContext.has('indexedDB')) { + final idb = globalContext['indexedDB'] as IDBFactory; + await idb.deleteDatabase(databaseName).complete(); } } @@ -181,12 +186,16 @@ class DriftServerController { final initPort = message.initializationPort; final initializer = initPort != null - ? () async { - initPort.postMessage(true); + ? () { + final completer = Completer(); + initPort.postMessage(true.toJS); - return await initPort.onMessage - .map((e) => e.data as Uint8List?) - .first; + initPort.onmessage = (MessageEvent e) { + final data = (e.data as JSUint8Array?); + completer.complete(data?.toDart); + }.toJS; + + return completer.future; } : null; @@ -269,7 +278,7 @@ class DriftServerController { StartFileSystemServer(options).sendToWorker(worker); // Wait for the server worker to report that it's ready - await worker.onMessage.first; + await EventStreamProviders.messageEvent.forTarget(worker).first; return WasmVfs(workerOptions: options); } @@ -349,3 +358,21 @@ extension StorageClassification on WasmStorageImplementation { this == WasmStorageImplementation.sharedIndexedDb || this == WasmStorageImplementation.unsafeIndexedDb; } + +/// Utilities to complete an IndexedDB request. +extension CompleteIdbRequest on IDBRequest { + /// Turns this request into a Dart future that completes with the first + /// success or error event. + Future complete() { + final completer = Completer.sync(); + + EventStreamProviders.successEvent.forTarget(this).listen((event) { + completer.complete(result as T); + }); + EventStreamProviders.errorEvent.forTarget(this).listen((event) { + completer.completeError(error ?? event); + }); + + return completer.future; + } +} diff --git a/drift/lib/src/web/wasm_setup/shared_worker.dart b/drift/lib/src/web/wasm_setup/shared_worker.dart index a5c32e8c..c1a18387 100644 --- a/drift/lib/src/web/wasm_setup/shared_worker.dart +++ b/drift/lib/src/web/wasm_setup/shared_worker.dart @@ -1,8 +1,8 @@ // ignore_for_file: public_member_api_docs import 'dart:async'; -import 'dart:html'; +import 'dart:js_interop'; -import 'package:js/js_util.dart'; +import 'package:web/web.dart'; import '../wasm_setup.dart'; import 'protocol.dart'; @@ -22,13 +22,15 @@ class SharedDriftWorker { : _servers = DriftServerController(setup); void start() { - const event = EventStreamProvider('connect'); - event.forTarget(self).listen(_newConnection); + const event = EventStreamProviders.connectEvent; + event.forTarget(self).listen((e) => _newConnection(e as MessageEvent)); } void _newConnection(MessageEvent event) async { - final clientPort = event.ports[0]; - clientPort.onMessage + final clientPort = event.ports.toDart[0]; + clientPort.start(); + EventStreamProviders.messageEvent + .forTarget(clientPort) .listen((event) => _messageFromClient(clientPort, event)); } @@ -111,9 +113,9 @@ class SharedDriftWorker { } } - messageSubscription = worker.onMessage.listen((event) { - final data = - WasmInitializationMessage.fromJs(getProperty(event, 'data')); + messageSubscription = + EventStreamProviders.messageEvent.forTarget(worker).listen((event) { + final data = WasmInitializationMessage.read(event); final compatibilityResult = data as DedicatedWorkerCompatibilityResult; result( @@ -124,7 +126,8 @@ class SharedDriftWorker { ); }); - errorSubscription = worker.onError.listen((event) { + errorSubscription = + EventStreamProviders.errorEvent.forTarget(worker).listen((event) { result(false, false, false, const []); worker.terminate(); _dedicatedWorker = null; diff --git a/drift/lib/wasm.dart b/drift/lib/wasm.dart index 4ff8d152..98018f26 100644 --- a/drift/lib/wasm.dart +++ b/drift/lib/wasm.dart @@ -7,15 +7,17 @@ library drift.wasm; import 'dart:async'; -import 'dart:html'; +import 'dart:js_interop'; import 'dart:typed_data'; import 'package:collection/collection.dart'; -import 'package:drift/src/web/wasm_setup.dart'; +import 'package:web/web.dart' + show DedicatedWorkerGlobalScope, SharedWorkerGlobalScope; import 'package:sqlite3/wasm.dart'; import 'backends.dart'; import 'src/sqlite3/database.dart'; +import 'src/web/wasm_setup.dart'; import 'src/web/wasm_setup/dedicated_worker.dart'; import 'src/web/wasm_setup/shared_worker.dart'; import 'src/web/wasm_setup/types.dart'; @@ -205,12 +207,15 @@ class WasmDatabase extends DelegatedDatabase { static void workerMainForOpen({ WasmDatabaseSetup? setupAllDatabases, }) { - final self = WorkerGlobalScope.instance; + final self = globalContext; - if (self is DedicatedWorkerGlobalScope) { - DedicatedDriftWorker(self, setupAllDatabases).start(); - } else if (self is SharedWorkerGlobalScope) { - SharedDriftWorker(self, setupAllDatabases).start(); + if (self.instanceOfString('DedicatedWorkerGlobalScope')) { + DedicatedDriftWorker( + self as DedicatedWorkerGlobalScope, setupAllDatabases) + .start(); + } else if (self.instanceOfString('SharedWorkerGlobalScope')) { + SharedDriftWorker(self as SharedWorkerGlobalScope, setupAllDatabases) + .start(); } } } diff --git a/drift/lib/web.dart b/drift/lib/web.dart index 10546aa4..babc56ca 100644 --- a/drift/lib/web.dart +++ b/drift/lib/web.dart @@ -10,4 +10,4 @@ import 'package:meta/meta.dart'; export 'src/web/sql_js.dart'; export 'src/web/storage.dart' hide CustomSchemaVersionSave; export 'src/web/web_db.dart'; -export 'src/web/channel.dart'; +export 'src/web/channel.dart' show PortToChannel; diff --git a/drift/pubspec.yaml b/drift/pubspec.yaml index 3600131c..304c5db9 100644 --- a/drift/pubspec.yaml +++ b/drift/pubspec.yaml @@ -6,21 +6,23 @@ homepage: https://drift.simonbinder.eu/ issue_tracker: https://github.com/simolus3/drift/issues environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.3.0 <4.0.0' dependencies: async: ^2.5.0 convert: ^3.0.0 collection: ^1.15.0 - js: ^0.6.3 + js: '>=0.6.3 <0.8.0' meta: ^1.3.0 stream_channel: ^2.1.0 sqlite3: ^2.4.0 path: ^1.8.0 stack_trace: ^1.11.1 + web: ^0.5.0 dev_dependencies: archive: ^3.3.1 + analyzer: ^6.4.1 build_test: ^2.0.0 build_runner_core: ^7.0.0 build_verify: ^3.0.0 diff --git a/drift/test/source_code_test.dart b/drift/test/source_code_test.dart new file mode 100644 index 00000000..d7dfdc8b --- /dev/null +++ b/drift/test/source_code_test.dart @@ -0,0 +1,56 @@ +@TestOn('vm') +import 'dart:io'; + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:test/test.dart'; + +void main() { + test('drift does not import legacy JS interop files', () { + // The old web APIs can't be used in dart2wasm, so we shouldn't use them in + // web-specific drift code. + // Legacy APIs (involving `WebDatabase`) are excempt from this. + const allowedLegacyCode = [ + 'lib/web/worker.dart', // Wasm uses a different worker + 'lib/src/web/channel.dart', + 'lib/src/web/storage.dart', + 'lib/src/web/sql_js.dart', + ]; + + final failures = <(String, String)>[]; + + void check(FileSystemEntity e) { + switch (e) { + case File(): + if (allowedLegacyCode.contains(e.path)) return; + + final text = e.readAsStringSync(); + final parsed = parseString(content: text).unit; + + for (final directive in parsed.directives) { + if (directive is ImportDirective) { + final uri = directive.uri.stringValue!; + if (uri.contains('package:js') || + uri == 'dart:js' || + uri == 'dart:js_util' || + uri == 'dart:html' || + uri == 'dart:indexeddb') { + failures.add((e.path, directive.toString())); + } + } + } + + case Directory(): + for (final entry in e.listSync()) { + check(entry); + } + } + } + + final root = Directory('lib/'); + check(root); + + expect(failures, isEmpty, + reason: 'Drift should not import legacy JS code.'); + }); +} diff --git a/drift/test/test_utils/matchers.dart b/drift/test/test_utils/matchers.dart index 1be44d3f..98497716 100644 --- a/drift/test/test_utils/matchers.dart +++ b/drift/test/test_utils/matchers.dart @@ -109,7 +109,7 @@ class _GeneratesSqlMatcher extends Matcher { final argsMatchState = {}; if (_matchVariables != null && - !_matchVariables!.matches(ctx.boundVariables, argsMatchState)) { + !_matchVariables.matches(ctx.boundVariables, argsMatchState)) { matchState['vars'] = ctx.boundVariables; matchState['vars_match'] = argsMatchState; matches = false; diff --git a/extras/integration_tests/web_wasm/web/main.dart b/extras/integration_tests/web_wasm/web/main.dart index b3742a96..c9094787 100644 --- a/extras/integration_tests/web_wasm/web/main.dart +++ b/extras/integration_tests/web_wasm/web/main.dart @@ -150,7 +150,7 @@ Future _open(String? implementationName) async { db.createFunction( functionName: 'database_host', function: (args) => 'document', - argumentCount: const AllowedArgumentCount(1), + argumentCount: const AllowedArgumentCount(0), ); }, ); diff --git a/extras/integration_tests/web_wasm/web/worker.dart b/extras/integration_tests/web_wasm/web/worker.dart index 46ec962b..6db4dc04 100644 --- a/extras/integration_tests/web_wasm/web/worker.dart +++ b/extras/integration_tests/web_wasm/web/worker.dart @@ -6,7 +6,7 @@ void main() { db.createFunction( functionName: 'database_host', function: (args) => 'worker', - argumentCount: const AllowedArgumentCount(1), + argumentCount: const AllowedArgumentCount(0), ); }); } From fe0df6d69cd6f2ad355cce2073efb646924aa9dc Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 24 Feb 2024 12:12:05 +0100 Subject: [PATCH 06/38] Add substrExpr to query builder --- .../query_builder/expressions/text.dart | 25 +++++++++++++++++-- .../expressions_integration_test.dart | 6 ++++- .../test/database/expressions/text_test.dart | 3 +++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/drift/lib/src/runtime/query_builder/expressions/text.dart b/drift/lib/src/runtime/query_builder/expressions/text.dart index 57f345cc..84fca918 100644 --- a/drift/lib/src/runtime/query_builder/expressions/text.dart +++ b/drift/lib/src/runtime/query_builder/expressions/text.dart @@ -137,10 +137,31 @@ extension StringExpressionOperators on Expression { /// and [length] can be negative to return a section of the string before /// [start]. Expression substr(int start, [int? length]) { + return substrExpr( + Constant(start), length != null ? Constant(length) : null); + } + + /// Calls the [`substr`](https://sqlite.org/lang_corefunc.html#substr) + /// function with arbitrary expressions as arguments. + /// + /// For instance, this call uses [substrExpr] to remove the last 5 characters + /// from a column. As this depends on its [StringExpressionOperators.length], + /// it needs to use expressions: + /// + /// ```dart + /// update(table).write(TableCompanion.custom( + /// column: column.substrExpr(Variable(1), column.length - Variable(5)) + /// )); + /// ``` + /// + /// When both [start] and [length] are Dart values (e.g. [Variable]s or + /// [Constant]s), consider using [substr] instead. + Expression substrExpr(Expression start, + [Expression? length]) { return FunctionCallExpression('SUBSTR', [ this, - Constant(start), - if (length != null) Constant(length), + start, + if (length != null) length, ]); } } diff --git a/drift/test/database/expressions/expressions_integration_test.dart b/drift/test/database/expressions/expressions_integration_test.dart index 0b92d8a8..ef3f2403 100644 --- a/drift/test/database/expressions/expressions_integration_test.dart +++ b/drift/test/database/expressions/expressions_integration_test.dart @@ -125,7 +125,11 @@ void main() { }); test('substring', () { - expect(eval(Constant('hello world').substr(7)), completion('world')); + final input = Constant('hello world'); + expect(eval(input.substr(7)), completion('world')); + + expect(eval(input.substrExpr(Variable(1), input.length - Variable(6))), + completion('hello')); }); }); diff --git a/drift/test/database/expressions/text_test.dart b/drift/test/database/expressions/text_test.dart index c63342d0..fdda38b0 100644 --- a/drift/test/database/expressions/text_test.dart +++ b/drift/test/database/expressions/text_test.dart @@ -52,5 +52,8 @@ void main() { test('substr', () { expect(expression.substr(10), generates('SUBSTR(col, 10)')); expect(expression.substr(10, 2), generates('SUBSTR(col, 10, 2)')); + + expect(expression.substrExpr(Variable(1), expression.length - Variable(5)), + generates('SUBSTR(col, ?, LENGTH(col) - ?)', [1, 5])); }); } From d1d2b7ffe584c9ce2b51a814ecd4c1f74c53a58a Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 24 Feb 2024 12:20:09 +0100 Subject: [PATCH 07/38] Add `Value.absentIfNull` --- drift/CHANGELOG.md | 2 ++ drift/lib/src/runtime/data_class.dart | 10 ++++++++++ drift/test/database/data_class_test.dart | 11 ++++++----- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/drift/CHANGELOG.md b/drift/CHANGELOG.md index a6ddd65a..7be8c247 100644 --- a/drift/CHANGELOG.md +++ b/drift/CHANGELOG.md @@ -12,6 +12,8 @@ - Improve stack traces for errors happening on drift isolates (which includes usages of `NativeDatabase.createInBackground`). - Don't cache `EXPLAIN` statements, avoiding schema locks. +- Deprecate `Value.ofNullable` in favor of `Value.absentIfNull`, which is more + explicit about its behavior and allows nullable types too. - Migrate `WasmDatabase` to `dart:js_interop` and `package:web`. ## 2.15.0 diff --git a/drift/lib/src/runtime/data_class.dart b/drift/lib/src/runtime/data_class.dart index 78d90dae..106ec3db 100644 --- a/drift/lib/src/runtime/data_class.dart +++ b/drift/lib/src/runtime/data_class.dart @@ -158,6 +158,7 @@ class Value { /// This constructor should only be used when [T] is not nullable. If [T] were /// nullable, there wouldn't be a clear interpretation for a `null` [value]. /// See the overall documentation on [Value] for details. + @Deprecated('Use Value.absentIfNull instead') const Value.ofNullable(T? value) : assert( value != null || null is! T, @@ -167,6 +168,15 @@ class Value { _value = value, present = value != null; + /// Create a value that is absent if [value] is `null` and [present] if it's + /// not. + /// + /// The functionality is equiavalent to the following: + /// `x != null ? Value(x) : Value.absent()`. + const Value.absentIfNull(T? value) + : _value = value, + present = value != null; + @override String toString() => present ? 'Value($value)' : 'Value.absent()'; diff --git a/drift/test/database/data_class_test.dart b/drift/test/database/data_class_test.dart index e3784421..05afec45 100644 --- a/drift/test/database/data_class_test.dart +++ b/drift/test/database/data_class_test.dart @@ -105,15 +105,16 @@ void main() { expect(entry.toCompanion(true), const PureDefaultsCompanion()); }); - test('nullable values cannot be used with nullOrAbsent', () { + test('utilities to wrap nullable values', () { expect( - // ignore: prefer_const_constructors + // ignore: prefer_const_constructors, deprecated_member_use_from_same_package () => Value.ofNullable(null), throwsA(isA())); - expect(const Value.ofNullable(null).present, isFalse); - expect(const Value.ofNullable(12).present, isTrue); - expect(const Value.ofNullable(23).present, isTrue); + expect(const Value.absentIfNull(null).present, isFalse); + expect(const Value.absentIfNull(null).present, isFalse); + expect(const Value.absentIfNull(12).present, isTrue); + expect(const Value.absentIfNull(23).present, isTrue); }); test('companions support hash and equals', () { From 54e205af5168ea1ede3435e0b5d8cbfb7f8893c6 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 24 Feb 2024 12:45:19 +0100 Subject: [PATCH 08/38] Add integration tests for web migrations --- .../web_wasm/lib/driver.dart | 11 ++++++++++ .../web_wasm/lib/src/database.dart | 10 +++++++++- .../web_wasm/test/drift_wasm_test.dart | 20 ++++++++++++++++++- .../integration_tests/web_wasm/web/main.dart | 8 +++++++- 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/extras/integration_tests/web_wasm/lib/driver.dart b/extras/integration_tests/web_wasm/lib/driver.dart index 6a58ffff..a11dabac 100644 --- a/extras/integration_tests/web_wasm/lib/driver.dart +++ b/extras/integration_tests/web_wasm/lib/driver.dart @@ -162,6 +162,17 @@ class DriftWebDriver { } } + Future setSchemaVersion(int version) async { + final result = await driver.executeAsync( + 'set_schema_version(arguments[0], arguments[1])', + [version.toString()], + ); + + if (result != true) { + throw 'Could not set schema version'; + } + } + Future deleteDatabase(WebStorageApi storageApi, String name) async { await driver.executeAsync('delete_database(arguments[0], arguments[1])', [ json.encode([storageApi.name, name]), diff --git a/extras/integration_tests/web_wasm/lib/src/database.dart b/extras/integration_tests/web_wasm/lib/src/database.dart index c9c64c32..0221ac2d 100644 --- a/extras/integration_tests/web_wasm/lib/src/database.dart +++ b/extras/integration_tests/web_wasm/lib/src/database.dart @@ -12,5 +12,13 @@ class TestDatabase extends _$TestDatabase { TestDatabase(super.e); @override - int get schemaVersion => 1; + MigrationStrategy get migration => MigrationStrategy( + onUpgrade: (m, from, to) async { + await into(testTable).insert( + TestTableCompanion.insert(content: 'from onUpgrade migration')); + }, + ); + + @override + int schemaVersion = 1; } diff --git a/extras/integration_tests/web_wasm/test/drift_wasm_test.dart b/extras/integration_tests/web_wasm/test/drift_wasm_test.dart index 77c1145f..1daf0807 100644 --- a/extras/integration_tests/web_wasm/test/drift_wasm_test.dart +++ b/extras/integration_tests/web_wasm/test/drift_wasm_test.dart @@ -40,7 +40,11 @@ enum Browser { Future spawnDriver() async { return switch (this) { - firefox => Process.start('geckodriver', []), + firefox => Process.start('geckodriver', []).then((result) async { + // geckodriver seems to take a while to initialize + await Future.delayed(const Duration(seconds: 1)); + return result; + }), chrome => Process.start('chromedriver', ['--port=4444', '--url-base=/wd/hub']), }; @@ -156,6 +160,20 @@ void main() { final finalImpls = await driver.probeImplementations(); expect(finalImpls.existing, isEmpty); }); + + test('migrations', () async { + await driver.openDatabase(entry); + await driver.insertIntoDatabase(); + await driver.waitForTableUpdate(); + + await driver.closeDatabase(); + await driver.driver.refresh(); + + await driver.setSchemaVersion(2); + await driver.openDatabase(entry); + // The migration adds a row + expect(await driver.amountOfRows, 2); + }); } group( diff --git a/extras/integration_tests/web_wasm/web/main.dart b/extras/integration_tests/web_wasm/web/main.dart index c9094787..7b91b287 100644 --- a/extras/integration_tests/web_wasm/web/main.dart +++ b/extras/integration_tests/web_wasm/web/main.dart @@ -18,6 +18,7 @@ TestDatabase? openedDatabase; StreamQueue? tableUpdates; InitializationMode initializationMode = InitializationMode.none; +int schemaVersion = 1; void main() { _addCallbackForWebDriver('detectImplementations', _detectImplementations); @@ -32,6 +33,10 @@ void main() { initializationMode = InitializationMode.values.byName(arg!); return true; }); + _addCallbackForWebDriver('set_schema_version', (arg) async { + schemaVersion = int.parse(arg!); + return true; + }); _addCallbackForWebDriver('delete_database', (arg) async { final result = await WasmDatabase.probe( sqlite3Uri: sqlite3WasmUri, @@ -158,7 +163,8 @@ Future _open(String? implementationName) async { connection = result.resolvedExecutor; } - final db = openedDatabase = TestDatabase(connection); + final db = + openedDatabase = TestDatabase(connection)..schemaVersion = schemaVersion; // Make sure it works! await db.customSelect('SELECT database_host()').get(); From f179fe30a03af6b0458973a843febb1d9326d390 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 24 Feb 2024 23:14:27 +0100 Subject: [PATCH 09/38] Update dependencies in devtools extension --- .../drift_devtools_extension/lib/src/remote_database.dart | 3 ++- extras/drift_devtools_extension/pubspec.yaml | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/extras/drift_devtools_extension/lib/src/remote_database.dart b/extras/drift_devtools_extension/lib/src/remote_database.dart index 5a2fb045..109d10a7 100644 --- a/extras/drift_devtools_extension/lib/src/remote_database.dart +++ b/extras/drift_devtools_extension/lib/src/remote_database.dart @@ -120,7 +120,8 @@ class RemoteDatabase { isAlive: isAlive, scope: {'db': database.database.id!}, ); - final value = await eval.retrieveFullValueAsString(stringVal); + final value = await eval.service + .retrieveFullStringValue(eval.isolateRef!.id!, stringVal); final description = DatabaseDescription.fromJson(json.decode(value!)); diff --git a/extras/drift_devtools_extension/pubspec.yaml b/extras/drift_devtools_extension/pubspec.yaml index 3774e060..c724b880 100644 --- a/extras/drift_devtools_extension/pubspec.yaml +++ b/extras/drift_devtools_extension/pubspec.yaml @@ -12,11 +12,11 @@ dependencies: sdk: flutter devtools_extensions: ^0.0.8 - devtools_app_shared: '>=0.0.5 <0.0.6' # 0.0.6 requires unstable Flutter + devtools_app_shared: ^0.0.9 db_viewer: ^1.0.3 rxdart: ^0.27.7 flutter_riverpod: ^3.0.0-dev.0 - vm_service: ^11.10.0 + vm_service: ^13.0.0 path: ^1.8.3 drift: ^2.12.1 logging: ^1.2.0 @@ -29,5 +29,8 @@ dev_dependencies: sdk: flutter flutter_lints: ^3.0.0 +dependency_overrides: + web: ^0.5.0 + flutter: uses-material-design: true From 417c2c12074665a92ddb5c82e1e541ab97f368d0 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 24 Feb 2024 23:17:54 +0100 Subject: [PATCH 10/38] Prepare release --- drift/CHANGELOG.md | 2 +- drift/pubspec.yaml | 2 +- drift_dev/CHANGELOG.md | 2 +- drift_dev/pubspec.yaml | 4 ++-- extras/drift_postgres/pubspec.yaml | 2 +- sqlparser/CHANGELOG.md | 2 +- sqlparser/pubspec.yaml | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/drift/CHANGELOG.md b/drift/CHANGELOG.md index 7be8c247..d875db58 100644 --- a/drift/CHANGELOG.md +++ b/drift/CHANGELOG.md @@ -1,4 +1,4 @@ -## 2.16.0-dev +## 2.16.0 - When a migration throws, the database will now block subsequent operations instead of potentially allowing them to operate on a database in an diff --git a/drift/pubspec.yaml b/drift/pubspec.yaml index 304c5db9..ba3bf91a 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.15.0 +version: 2.16.0 repository: https://github.com/simolus3/drift homepage: https://drift.simonbinder.eu/ issue_tracker: https://github.com/simolus3/drift/issues diff --git a/drift_dev/CHANGELOG.md b/drift_dev/CHANGELOG.md index 1c3946ad..ad3bf9bc 100644 --- a/drift_dev/CHANGELOG.md +++ b/drift_dev/CHANGELOG.md @@ -1,4 +1,4 @@ -## 2.15.1-dev +## 2.16.0 - Keep import alias when referencing existing elements in generated code ([#2845](https://github.com/simolus3/drift/issues/2845)). diff --git a/drift_dev/pubspec.yaml b/drift_dev/pubspec.yaml index 6a6ad592..e4d69927 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.15.0 +version: 2.16.0 repository: https://github.com/simolus3/drift homepage: https://drift.simonbinder.eu/ issue_tracker: https://github.com/simolus3/drift/issues @@ -30,7 +30,7 @@ dependencies: io: ^1.0.3 # Drift-specific analysis and apis - drift: '>=2.15.0 <2.16.0' + drift: '>=2.16.0 <2.17.0' sqlite3: '>=0.1.6 <3.0.0' sqlparser: '^0.34.0' diff --git a/extras/drift_postgres/pubspec.yaml b/extras/drift_postgres/pubspec.yaml index 772fc3a7..ca9b1fc3 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: 1.2.0-dev +version: 1.2.0 repository: https://github.com/simolus3/drift homepage: https://drift.simonbinder.eu/docs/platforms/postgres/ issue_tracker: https://github.com/simolus3/drift/issues diff --git a/sqlparser/CHANGELOG.md b/sqlparser/CHANGELOG.md index b119ab02..aabbd0da 100644 --- a/sqlparser/CHANGELOG.md +++ b/sqlparser/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.34.1-dev +## 0.34.1 - Allow selecting from virtual tables using the table-valued function syntax. diff --git a/sqlparser/pubspec.yaml b/sqlparser/pubspec.yaml index 96b8744e..fba7f470 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.34.1-dev +version: 0.34.1 homepage: https://github.com/simolus3/drift/tree/develop/sqlparser repository: https://github.com/simolus3/drift #homepage: https://drift.simonbinder.eu/ From 98fd6a0ef07bd11fe5595ceaae00417e7fba61c5 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 25 Feb 2024 00:06:12 +0100 Subject: [PATCH 11/38] Add recommended build options for drift postgres --- extras/drift_postgres/README.md | 17 +++++++++++++++++ extras/drift_postgres/build.yaml | 9 +++++++++ 2 files changed, 26 insertions(+) create mode 100644 extras/drift_postgres/build.yaml diff --git a/extras/drift_postgres/README.md b/extras/drift_postgres/README.md index 90f12403..ce0c2dbe 100644 --- a/extras/drift_postgres/README.md +++ b/extras/drift_postgres/README.md @@ -4,8 +4,10 @@ talking to PostgreSQL databases by using the `postgres` package. ## Using this For general notes on using drift, see [this guide](https://drift.simonbinder.eu/getting-started/). +Detailed docs on getting started with `drift_postgres` are available [here](https://drift.simonbinder.eu/docs/platforms/postgres/#setup). To use drift_postgres, add this to your `pubspec.yaml` + ```yaml dependencies: drift: "$latest version" @@ -25,6 +27,21 @@ final database = AppDatabase(PgDatabase( )); ``` +Also, consider adding builder options to make drift generate postgres-specific code: + +```yaml +# build.yaml +targets: + $default: + builders: + drift_dev: + options: + sql: + dialects: + - sqlite # remove this line if you only need postgres + - postgres +``` + ## Running tests To test this package, first run diff --git a/extras/drift_postgres/build.yaml b/extras/drift_postgres/build.yaml new file mode 100644 index 00000000..1303813a --- /dev/null +++ b/extras/drift_postgres/build.yaml @@ -0,0 +1,9 @@ +targets: + $default: + builders: + drift_dev: + options: + sql: + dialects: + - sqlite # remove this line if you only need postgres + - postgres From 863dbb61a9a8b3b1769cb68a81fdffcd7e11fad0 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 29 Feb 2024 18:08:51 +0100 Subject: [PATCH 12/38] Avoid picking wrong import alias Closes #2904 --- drift_dev/CHANGELOG.md | 4 ++++ drift_dev/lib/src/writer/import_manager.dart | 12 +++++++++++- .../test/backends/build/build_integration_test.dart | 13 +++++++------ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/drift_dev/CHANGELOG.md b/drift_dev/CHANGELOG.md index ad3bf9bc..c5ae4216 100644 --- a/drift_dev/CHANGELOG.md +++ b/drift_dev/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.17.0-dev + +- Fix drift using the wrong import alias in generated part files. + ## 2.16.0 - Keep import alias when referencing existing elements in generated code diff --git a/drift_dev/lib/src/writer/import_manager.dart b/drift_dev/lib/src/writer/import_manager.dart index 24e71bab..3a8ec70d 100644 --- a/drift_dev/lib/src/writer/import_manager.dart +++ b/drift_dev/lib/src/writer/import_manager.dart @@ -31,13 +31,23 @@ class ImportManagerForPartFiles extends ImportManager { // Part files can't add their own imports, so try to find the element in an // existing import. for (final MapEntry(:key, :value) in _namedImports.entries) { - if (value.containsKey(elementName)) { + final foundHere = value[elementName]; + if (foundHere != null && _matchingUrl(definitionUri, foundHere)) { return key; } } return null; } + + static bool _matchingUrl(Uri wanted, Element target) { + final targetUri = target.librarySource?.uri; + if (targetUri == null || targetUri.scheme != wanted.scheme) { + return false; + } + + return true; + } } class NullImportManager extends ImportManager { diff --git a/drift_dev/test/backends/build/build_integration_test.dart b/drift_dev/test/backends/build/build_integration_test.dart index 6833f427..d81f397f 100644 --- a/drift_dev/test/backends/build/build_integration_test.dart +++ b/drift_dev/test/backends/build/build_integration_test.dart @@ -38,16 +38,17 @@ CREATE INDEX b_idx /* comment should be stripped */ ON b (foo, upper(foo)); final result = await emulateDriftBuild( inputs: { 'a|lib/main.dart': r''' +import 'dart:io' as io; import 'package:drift/drift.dart' as drift; import 'tables.dart' as tables; -@drift.DriftDatabase(tables: [tables.Texts]) +@drift.DriftDatabase(tables: [tables.Files]) class MyDatabase extends _$MyDatabase {} ''', 'a|lib/tables.dart': ''' import 'package:drift/drift.dart'; -class Texts extends Table { +class Files extends Table { TextColumn get content => text()(); } ''', @@ -59,12 +60,12 @@ class Texts extends Table { 'a|lib/main.drift.dart': decodedMatches( allOf( contains( - r'class $TextsTable extends tables.Texts with ' - r'drift.TableInfo<$TextsTable, Text>', + r'class $FilesTable extends tables.Files with ' + r'drift.TableInfo<$FilesTable, File>', ), contains( - 'class Text extends drift.DataClass implements ' - 'drift.Insertable', + 'class File extends drift.DataClass implements ' + 'drift.Insertable', ), ), ), From 1e7af307a92341e243ea40d0f68555109dacbd6b Mon Sep 17 00:00:00 2001 From: Luna <119916943+lunakoan@users.noreply.github.com> Date: Sun, 3 Mar 2024 23:09:16 +1300 Subject: [PATCH 13/38] Fixes typo in migration.dart --- drift/lib/src/runtime/query_builder/migration.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drift/lib/src/runtime/query_builder/migration.dart b/drift/lib/src/runtime/query_builder/migration.dart index 956f1920..564e471c 100644 --- a/drift/lib/src/runtime/query_builder/migration.dart +++ b/drift/lib/src/runtime/query_builder/migration.dart @@ -132,7 +132,7 @@ class Migrator { return _issueCustomQuery(context.sql, context.boundVariables); } - /// Alter columns of an existing tabe. + /// Alter columns of an existing table. /// /// Since sqlite does not provide a way to alter the type or constraint of an /// individual column, one needs to write a fairly complex migration procedure From e4231571b888e89f7b0223af3350a8f277ed133a Mon Sep 17 00:00:00 2001 From: Dave Hole Date: Tue, 5 Mar 2024 08:44:42 +1100 Subject: [PATCH 14/38] Check for UserDefinedSqlType, not CustomSqlType Variable is expecting UserDefinedSqlType and other packages (ElectricDart) implement UserDefinedSqlType instead of CustomSqlType, so these type checks are missed. --- drift/lib/src/runtime/query_builder/helpers.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drift/lib/src/runtime/query_builder/helpers.dart b/drift/lib/src/runtime/query_builder/helpers.dart index 21466e26..5bb88aa4 100644 --- a/drift/lib/src/runtime/query_builder/helpers.dart +++ b/drift/lib/src/runtime/query_builder/helpers.dart @@ -54,7 +54,7 @@ extension WithTypes on Expression { /// Creates a variable with a matching [driftSqlType]. Variable variable(T? value) { return switch (driftSqlType) { - CustomSqlType custom => Variable(value, custom), + UserDefinedSqlType custom => Variable(value, custom), _ => Variable(value), }; } From ca0c70eac92896cdca4a1623924658455ab586db Mon Sep 17 00:00:00 2001 From: Dave Hole Date: Tue, 5 Mar 2024 11:55:20 +1100 Subject: [PATCH 15/38] Add test to ensure that generated expressions support dialect-aware custom types --- .../query_builder/expressions/custom.dart | 6 +++--- .../database/expressions/custom_types_test.dart | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/drift/lib/src/runtime/query_builder/expressions/custom.dart b/drift/lib/src/runtime/query_builder/expressions/custom.dart index 90e1d526..0f7f6bdc 100644 --- a/drift/lib/src/runtime/query_builder/expressions/custom.dart +++ b/drift/lib/src/runtime/query_builder/expressions/custom.dart @@ -24,14 +24,14 @@ class CustomExpression extends Expression { @override final Precedence precedence; - final CustomSqlType? _customSqlType; + final UserDefinedSqlType? _customSqlType; /// Constructs a custom expression by providing the raw sql [content]. const CustomExpression( this.content, { this.watchedTables = const [], this.precedence = Precedence.unknown, - CustomSqlType? customType, + UserDefinedSqlType? customType, }) : _dialectSpecificContent = null, _customSqlType = customType; @@ -41,7 +41,7 @@ class CustomExpression extends Expression { Map content, { this.watchedTables = const [], this.precedence = Precedence.unknown, - CustomSqlType? customType, + UserDefinedSqlType? customType, }) : _dialectSpecificContent = content, content = '', _customSqlType = customType; diff --git a/drift/test/database/expressions/custom_types_test.dart b/drift/test/database/expressions/custom_types_test.dart index c63bc69a..a72fbdf0 100644 --- a/drift/test/database/expressions/custom_types_test.dart +++ b/drift/test/database/expressions/custom_types_test.dart @@ -44,6 +44,23 @@ void main() { expect(exp, generates('?', [10])); expect(exp.driftSqlType, isA<_NegatedIntType>()); }); + + test('also supports dialect-aware types', () { + const b = CustomExpression( + 'b', + customType: DialectAwareSqlType.via( + fallback: _NegatedIntType(), + overrides: {SqlDialect.postgres: DriftSqlType.int}, + ), + precedence: Precedence.primary, + ); + + expect(b.equals(3), generates('b = ?', [-3])); + expect( + b.equals(3), + generatesWithOptions('b = \$1', + variables: [3], dialect: SqlDialect.postgres)); + }); } class _NegatedIntType implements CustomSqlType { From 707eb72b07f971b5c151d5ffef09549d9f798466 Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Tue, 5 Mar 2024 21:27:32 +0800 Subject: [PATCH 16/38] feat: Add option use_sql_column_name_as_json_key --- drift_dev/lib/src/analysis/options.dart | 10 +++++++++- drift_dev/lib/src/analysis/results/column.dart | 7 ++++--- drift_dev/lib/src/generated/analysis/options.g.dart | 5 +++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/drift_dev/lib/src/analysis/options.dart b/drift_dev/lib/src/analysis/options.dart index fe298795..a4d871cc 100644 --- a/drift_dev/lib/src/analysis/options.dart +++ b/drift_dev/lib/src/analysis/options.dart @@ -45,13 +45,19 @@ class DriftOptions { defaultValue: true) final bool useColumnNameAsJsonKeyWhenDefinedInMoorFile; + /// Uses the sql column name as the json key instead of the name in dart. + /// + /// Overrides [useColumnNameAsJsonKeyWhenDefinedInMoorFile] when set to `true`. + @JsonKey(name: 'use_sql_column_name_as_json_key', defaultValue: false) + final bool useSqlColumnNameAsJsonKey; + /// Generate a `connect` constructor in database superclasses. /// /// This makes drift generate a constructor for database classes that takes a /// `DatabaseConnection` instead of just a `QueryExecutor` - meaning that /// stream queries can also be shared across multiple database instances. /// Starting from drift 2.5, the database connection class implements the - /// `QueryExecutor` interface, making this option unecessary. + /// `QueryExecutor` interface, making this option unnecessary. @JsonKey(name: 'generate_connect_constructor', defaultValue: false) final bool generateConnectConstructor; @@ -120,6 +126,7 @@ class DriftOptions { this.skipVerificationCode = false, this.useDataClassNameForCompanions = false, this.useColumnNameAsJsonKeyWhenDefinedInMoorFile = true, + this.useSqlColumnNameAsJsonKey = false, this.generateConnectConstructor = false, this.dataClassToCompanions = true, this.generateMutableClasses = false, @@ -147,6 +154,7 @@ class DriftOptions { required this.skipVerificationCode, required this.useDataClassNameForCompanions, required this.useColumnNameAsJsonKeyWhenDefinedInMoorFile, + required this.useSqlColumnNameAsJsonKey, required this.generateConnectConstructor, required this.dataClassToCompanions, required this.generateMutableClasses, diff --git a/drift_dev/lib/src/analysis/results/column.dart b/drift_dev/lib/src/analysis/results/column.dart index 93a07786..bbd150aa 100644 --- a/drift_dev/lib/src/analysis/results/column.dart +++ b/drift_dev/lib/src/analysis/results/column.dart @@ -108,12 +108,13 @@ class DriftColumn implements HasType { /// The actual json key to use when serializing a data class of this table /// to json. /// - /// This respectts the [overriddenJsonName], if any, as well as [options]. + /// This respects the [overriddenJsonName], if any, as well as [options]. String getJsonKey([DriftOptions options = const DriftOptions.defaults()]) { if (overriddenJsonName != null) return overriddenJsonName!; - final useColumnName = options.useColumnNameAsJsonKeyWhenDefinedInMoorFile && - declaredInDriftFile; + final useColumnName = options.useSqlColumnNameAsJsonKey || + (options.useColumnNameAsJsonKeyWhenDefinedInMoorFile && + declaredInDriftFile); return useColumnName ? nameInSql : nameInDart; } diff --git a/drift_dev/lib/src/generated/analysis/options.g.dart b/drift_dev/lib/src/generated/analysis/options.g.dart index 4e86b7c9..3872438b 100644 --- a/drift_dev/lib/src/generated/analysis/options.g.dart +++ b/drift_dev/lib/src/generated/analysis/options.g.dart @@ -18,6 +18,7 @@ DriftOptions _$DriftOptionsFromJson(Map json) => $checkedCreate( 'skip_verification_code', 'use_data_class_name_for_companions', 'use_column_name_as_json_key_when_defined_in_moor_file', + 'use_sql_column_name_as_json_key', 'generate_connect_constructor', 'sqlite_modules', 'sqlite', @@ -52,6 +53,8 @@ DriftOptions _$DriftOptionsFromJson(Map json) => $checkedCreate( useColumnNameAsJsonKeyWhenDefinedInMoorFile: $checkedConvert( 'use_column_name_as_json_key_when_defined_in_moor_file', (v) => v as bool? ?? true), + useSqlColumnNameAsJsonKey: $checkedConvert( + 'use_sql_column_name_as_json_key', (v) => v as bool? ?? false), generateConnectConstructor: $checkedConvert( 'generate_connect_constructor', (v) => v as bool? ?? false), dataClassToCompanions: $checkedConvert( @@ -111,6 +114,7 @@ DriftOptions _$DriftOptionsFromJson(Map json) => $checkedCreate( 'useDataClassNameForCompanions': 'use_data_class_name_for_companions', 'useColumnNameAsJsonKeyWhenDefinedInMoorFile': 'use_column_name_as_json_key_when_defined_in_moor_file', + 'useSqlColumnNameAsJsonKey': 'use_sql_column_name_as_json_key', 'generateConnectConstructor': 'generate_connect_constructor', 'dataClassToCompanions': 'data_class_to_companions', 'generateMutableClasses': 'mutable_classes', @@ -143,6 +147,7 @@ Map _$DriftOptionsToJson(DriftOptions instance) => instance.useDataClassNameForCompanions, 'use_column_name_as_json_key_when_defined_in_moor_file': instance.useColumnNameAsJsonKeyWhenDefinedInMoorFile, + 'use_sql_column_name_as_json_key': instance.useSqlColumnNameAsJsonKey, 'generate_connect_constructor': instance.generateConnectConstructor, 'sqlite_modules': instance.modules.map((e) => _$SqlModuleEnumMap[e]!).toList(), From be70907e817abd46aae47abf0ad6cfe4e5bb9903 Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Wed, 6 Mar 2024 20:43:14 +0800 Subject: [PATCH 17/38] test: Add tests --- .../test/writer/data_class_writer_test.dart | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/drift_dev/test/writer/data_class_writer_test.dart b/drift_dev/test/writer/data_class_writer_test.dart index e0bd72fb..220fb98a 100644 --- a/drift_dev/test/writer/data_class_writer_test.dart +++ b/drift_dev/test/writer/data_class_writer_test.dart @@ -267,6 +267,58 @@ extension ItemToInsertable on i1.Item { result.writer, ); }); + + test( + 'generates fromJson and toJson with the sql column names as json keys', + () async { + final writer = await emulateDriftBuild( + options: const BuilderOptions({ + 'use_sql_column_name_as_json_key': true, + }), + inputs: const { + 'a|lib/main.dart': r''' +import 'package:drift/drift.dart'; + +part 'main.drift.dart'; + +class MyTable extends Table { + TextColumn get myFirstColumn => text()(); + IntColumn get mySecondColumn => integer()(); +} + + +@DriftDatabase( + tables: [MyTable], +) +class Database extends _$Database {} +''' + }, + ); + + checkOutputs({ + 'a|lib/main.drift.dart': decodedMatches(contains(r''' + factory MyTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MyTableData( + myFirstColumn: serializer.fromJson(json['my_first_column']), + mySecondColumn: serializer.fromJson(json['my_second_column']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'my_first_column': serializer.toJson(myFirstColumn), + 'my_second_column': serializer.toJson(mySecondColumn), + }; + } + +''')), + }, writer.dartOutputs, writer.writer); + }, + tags: 'analyzer', + ); } class _GeneratesConstDataClasses extends Matcher { From d768ceeb1a82fc29a7ab06f836d0d043806b926c Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Wed, 6 Mar 2024 21:00:13 +0800 Subject: [PATCH 18/38] feat: Name companion class --- drift/lib/src/dsl/table.dart | 12 ++++++++++-- .../lib/src/analysis/resolver/dart/helper.dart | 17 ++++++++++++----- .../lib/src/analysis/resolver/dart/table.dart | 1 + .../lib/src/analysis/results/result_sets.dart | 3 +++ drift_dev/lib/src/analysis/results/table.dart | 4 ++++ drift_dev/lib/src/analysis/results/view.dart | 4 ++++ drift_dev/lib/src/analysis/serializer.dart | 4 ++++ drift_dev/lib/src/writer/writer.dart | 7 ++++--- 8 files changed, 42 insertions(+), 10 deletions(-) diff --git a/drift/lib/src/dsl/table.dart b/drift/lib/src/dsl/table.dart index c0791946..d19bd43a 100644 --- a/drift/lib/src/dsl/table.dart +++ b/drift/lib/src/dsl/table.dart @@ -308,7 +308,11 @@ final class TableIndex { class DataClassName { /// The overridden name to use when generating the data class for a table. /// {@macro drift_custom_data_class} - final String name; + final String? name; + + /// The overridden name to use when generating the companion class for a table. + /// {@macro drift_custom_data_class} + final String? companion; /// The parent type of the data class generated by drift. /// @@ -345,7 +349,11 @@ class DataClassName { /// Customize the data class name for a given table. /// {@macro drift_custom_data_class} - const DataClassName(this.name, {this.extending}); + const DataClassName(this.name, {this.extending, this.companion}); + + /// Customize the data class name for a given table. + /// {@macro drift_custom_data_class} + const DataClassName.custom({this.name, this.extending, this.companion}); } /// An annotation specifying an existing class to be used as a data class. diff --git a/drift_dev/lib/src/analysis/resolver/dart/helper.dart b/drift_dev/lib/src/analysis/resolver/dart/helper.dart index 3d7704e0..7e1f3530 100644 --- a/drift_dev/lib/src/analysis/resolver/dart/helper.dart +++ b/drift_dev/lib/src/analysis/resolver/dart/helper.dart @@ -198,11 +198,13 @@ extension TypeUtils on DartType { class DataClassInformation { final String enforcedName; + final String? companionName; final CustomParentClass? extending; final ExistingRowClass? existingClass; DataClassInformation( this.enforcedName, + this.companionName, this.extending, this.existingClass, ); @@ -233,16 +235,16 @@ class DataClassInformation { )); } - String name; + var name = dataClassName?.getField('name')!.toStringValue() ?? + dataClassNameForClassName(element.name); + final companionName = + dataClassName?.getField('companionName')?.toStringValue(); CustomParentClass? customParentClass; ExistingRowClass? existingClass; if (dataClassName != null) { - name = dataClassName.getField('name')!.toStringValue()!; customParentClass = parseCustomParentClass(name, dataClassName, element, resolver); - } else { - name = dataClassNameForClassName(element.name); } if (useRowClass != null) { @@ -277,7 +279,12 @@ class DataClassInformation { } } - return DataClassInformation(name, customParentClass, existingClass); + return DataClassInformation( + name, + companionName, + customParentClass, + existingClass, + ); } } diff --git a/drift_dev/lib/src/analysis/resolver/dart/table.dart b/drift_dev/lib/src/analysis/resolver/dart/table.dart index e2cdac0c..82917df2 100644 --- a/drift_dev/lib/src/analysis/resolver/dart/table.dart +++ b/drift_dev/lib/src/analysis/resolver/dart/table.dart @@ -55,6 +55,7 @@ class DartTableResolver extends LocalElementResolver { columns: columns, references: references.toList(), nameOfRowClass: dataClassInfo.enforcedName, + nameOfCompanionClass: dataClassInfo.companionName, existingRowClass: dataClassInfo.existingClass, customParentClass: dataClassInfo.extending, baseDartName: element.name, diff --git a/drift_dev/lib/src/analysis/results/result_sets.dart b/drift_dev/lib/src/analysis/results/result_sets.dart index abce72e0..083b4acc 100644 --- a/drift_dev/lib/src/analysis/results/result_sets.dart +++ b/drift_dev/lib/src/analysis/results/result_sets.dart @@ -40,6 +40,9 @@ abstract class DriftElementWithResultSet extends DriftSchemaElement { /// The name for the data class associated with this table or view. String get nameOfRowClass; + /// The name for the companion class associated with this table or view. + String? get nameOfCompanionClass; + /// All [columns] of this table, indexed by their name in SQL. late final Map columnBySqlName = CaseInsensitiveMap.of({ for (final column in columns) column.nameInSql: column, diff --git a/drift_dev/lib/src/analysis/results/table.dart b/drift_dev/lib/src/analysis/results/table.dart index ffa5795d..0af9f9d2 100644 --- a/drift_dev/lib/src/analysis/results/table.dart +++ b/drift_dev/lib/src/analysis/results/table.dart @@ -32,6 +32,9 @@ class DriftTable extends DriftElementWithResultSet { @override final String nameOfRowClass; + @override + final String? nameOfCompanionClass; + final bool withoutRowId; /// Information about the virtual table creating statement backing this table, @@ -69,6 +72,7 @@ class DriftTable extends DriftElementWithResultSet { required this.columns, required this.baseDartName, required this.nameOfRowClass, + this.nameOfCompanionClass, this.references = const [], this.existingRowClass, this.customParentClass, diff --git a/drift_dev/lib/src/analysis/results/view.dart b/drift_dev/lib/src/analysis/results/view.dart index 804096f7..23102b17 100644 --- a/drift_dev/lib/src/analysis/results/view.dart +++ b/drift_dev/lib/src/analysis/results/view.dart @@ -25,6 +25,9 @@ class DriftView extends DriftElementWithResultSet { @override final String nameOfRowClass; + @override + final String? nameOfCompanionClass; + @override List references; @@ -38,6 +41,7 @@ class DriftView extends DriftElementWithResultSet { required this.existingRowClass, required this.nameOfRowClass, required this.references, + this.nameOfCompanionClass, }); @override diff --git a/drift_dev/lib/src/analysis/serializer.dart b/drift_dev/lib/src/analysis/serializer.dart index b27ff839..aba81a2f 100644 --- a/drift_dev/lib/src/analysis/serializer.dart +++ b/drift_dev/lib/src/analysis/serializer.dart @@ -57,6 +57,7 @@ class ElementSerializer { 'fixed_entity_info_name': element.fixedEntityInfoName, 'base_dart_name': element.baseDartName, 'row_class_name': element.nameOfRowClass, + 'companion_class_name': element.nameOfCompanionClass, 'without_rowid': element.withoutRowId, 'strict': element.strict, if (element.isVirtual) @@ -146,6 +147,7 @@ class ElementSerializer { 'custom_parent_class': _serializeCustomParentClass(element.customParentClass), 'name_of_row_class': element.nameOfRowClass, + 'name_of_companion_class': element.nameOfCompanionClass, 'source': serializedSource, }; } else if (element is BaseDriftAccessor) { @@ -536,6 +538,7 @@ class ElementDeserializer { fixedEntityInfoName: json['fixed_entity_info_name'] as String?, baseDartName: json['base_dart_name'] as String, nameOfRowClass: json['row_class_name'] as String, + nameOfCompanionClass: json['companion_class_name'] as String?, withoutRowId: json['without_rowid'] as bool, strict: json['strict'] as bool, virtualTableData: virtualTableData, @@ -675,6 +678,7 @@ class ElementDeserializer { customParentClass: _readCustomParentClass(json['custom_parent_class'] as Map?), nameOfRowClass: json['name_of_row_class'] as String, + nameOfCompanionClass: json['name_of_companion_class'] as String, existingRowClass: json['existing_data_class'] != null ? await _readExistingRowClass( id.libraryUri, json['existing_data_class'] as Map) diff --git a/drift_dev/lib/src/writer/writer.dart b/drift_dev/lib/src/writer/writer.dart index a6b36dfc..70197eb7 100644 --- a/drift_dev/lib/src/writer/writer.dart +++ b/drift_dev/lib/src/writer/writer.dart @@ -72,9 +72,10 @@ abstract class _NodeOrWriter { } AnnotatedDartCode companionType(DriftTable table) { - final baseName = writer.options.useDataClassNameForCompanions - ? table.nameOfRowClass - : table.baseDartName; + final baseName = table.nameOfCompanionClass ?? + (writer.options.useDataClassNameForCompanions + ? table.nameOfRowClass + : table.baseDartName); return generatedElement(table, '${baseName}Companion'); } From d0ff1f29d6e3bf85a472e4b6e0594d4c40906c0f Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Wed, 6 Mar 2024 21:00:26 +0800 Subject: [PATCH 19/38] test: Add a test with overridden companion classes --- .../test/writer/data_class_writer_test.dart | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/drift_dev/test/writer/data_class_writer_test.dart b/drift_dev/test/writer/data_class_writer_test.dart index e0bd72fb..a8245fdc 100644 --- a/drift_dev/test/writer/data_class_writer_test.dart +++ b/drift_dev/test/writer/data_class_writer_test.dart @@ -267,6 +267,56 @@ extension ItemToInsertable on i1.Item { result.writer, ); }); + + test( + 'It should use the provided names for the data classes and the companion class', + () async { + final writer = await emulateDriftBuild( + inputs: const { + 'a|lib/main.dart': r''' +import 'package:drift/drift.dart'; + +part 'main.drift.dart'; + +@DataClassName('FirstDataClass', companion: 'FirstCompanionClass') +class FirstTable extends Table { + TextColumn get foo => text()(); + IntColumn get bar => integer()(); +} + +@DataClassName.custom(name: 'SecondDataClass', companion: 'SecondCompanionClass') +class SecondTable extends Table { + TextColumn get foo => text()(); + IntColumn get bar => integer()(); +} + +@DriftDatabase( + tables: [FirstTable, SecondTable], +) +class Database extends _$Database {} +''' + }, + ); + + checkOutputs({ + 'a|lib/main.drift.dart': decodedMatches(allOf([ + contains( + 'class FirstDataClass extends DataClass implements Insertable {', + ), + contains( + 'class FirstTableCompanion extends UpdateCompanion {', + ), + contains( + 'class SecondDataClass extends DataClass implements Insertable {', + ), + contains( + 'class SecondTableCompanion extends UpdateCompanion {', + ), + ])), + }, writer.dartOutputs, writer.writer); + }, + tags: 'analyzer', + ); } class _GeneratesConstDataClasses extends Matcher { From 914763b47a69ffec5d776e379fc56bce7bf57084 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 6 Mar 2024 20:56:03 +0100 Subject: [PATCH 20/38] Document sql column name builder option --- docs/pages/docs/Generation options/index.md | 2 ++ drift_dev/CHANGELOG.md | 1 + 2 files changed, 3 insertions(+) diff --git a/docs/pages/docs/Generation options/index.md b/docs/pages/docs/Generation options/index.md index ef04b4a5..0246caa1 100644 --- a/docs/pages/docs/Generation options/index.md +++ b/docs/pages/docs/Generation options/index.md @@ -54,6 +54,8 @@ At the moment, drift supports these options: (so a column named `user_name` would also use `user_name` as a json key instead of `userName`). You can always override the json key by using a `JSON KEY` column constraint (e.g. `user_name VARCHAR NOT NULL JSON KEY userName`). +* `use_sql_column_name_as_json_key` (defaults to false): Uses the column name in SQL as the JSON key for serialization, + regardless of whether the table was defined in a drift file or not. * `generate_connect_constructor` (deprecated): Generates a named `connect()` constructor on database classes that takes a `DatabaseConnection` instead of a `QueryExecutor`. This option was deprecated in drift 2.5 because `DatabaseConnection` now implements `QueryExecutor`. diff --git a/drift_dev/CHANGELOG.md b/drift_dev/CHANGELOG.md index c5ae4216..bbcfdcc0 100644 --- a/drift_dev/CHANGELOG.md +++ b/drift_dev/CHANGELOG.md @@ -1,6 +1,7 @@ ## 2.17.0-dev - Fix drift using the wrong import alias in generated part files. +- Add the `use_sql_column_name_as_json_key` builder option. ## 2.16.0 From 6b1f54d6473d529080b589fabacd8f46407732fe Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Thu, 7 Mar 2024 09:52:57 +0800 Subject: [PATCH 21/38] test: Add a test with a custom companion name and a default data name --- drift_dev/test/writer/data_class_writer_test.dart | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/drift_dev/test/writer/data_class_writer_test.dart b/drift_dev/test/writer/data_class_writer_test.dart index a8245fdc..8a07a349 100644 --- a/drift_dev/test/writer/data_class_writer_test.dart +++ b/drift_dev/test/writer/data_class_writer_test.dart @@ -290,8 +290,14 @@ class SecondTable extends Table { IntColumn get bar => integer()(); } +@DataClassName.custom(companion: 'ThirdCompanionClass') +class ThirdTable extends Table { + TextColumn get foo => text()(); + IntColumn get bar => integer()(); +} + @DriftDatabase( - tables: [FirstTable, SecondTable], + tables: [FirstTable, SecondTable, ThirdTable], ) class Database extends _$Database {} ''' @@ -312,6 +318,12 @@ class Database extends _$Database {} contains( 'class SecondTableCompanion extends UpdateCompanion {', ), + contains( + 'class ThirdTableData extends DataClass implements Insertable {', + ), + contains( + 'class ThirdTableCompanion extends UpdateCompanion {', + ), ])), }, writer.dartOutputs, writer.writer); }, From 27488cf3019dc08b373a3bac79d78c7a20b79acc Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Thu, 7 Mar 2024 09:54:54 +0800 Subject: [PATCH 22/38] docs: Remove unrelated macro --- drift/lib/src/dsl/table.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/drift/lib/src/dsl/table.dart b/drift/lib/src/dsl/table.dart index d19bd43a..571716ba 100644 --- a/drift/lib/src/dsl/table.dart +++ b/drift/lib/src/dsl/table.dart @@ -311,7 +311,6 @@ class DataClassName { final String? name; /// The overridden name to use when generating the companion class for a table. - /// {@macro drift_custom_data_class} final String? companion; /// The parent type of the data class generated by drift. From ef274c1ac27d649e0c10d51f3e4283528fe6a70e Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Thu, 7 Mar 2024 09:57:09 +0800 Subject: [PATCH 23/38] docs: Update changelog --- drift/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drift/CHANGELOG.md b/drift/CHANGELOG.md index d875db58..c82b64a9 100644 --- a/drift/CHANGELOG.md +++ b/drift/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.17.0-dev + +- Adds `companion` entry to `DataClassName` to override the name of the + generated companion class. + ## 2.16.0 - When a migration throws, the database will now block subsequent operations From 304d3adafdad17fe7efce593a34ea3ed5f7250c6 Mon Sep 17 00:00:00 2001 From: ValentinVignal Date: Thu, 7 Mar 2024 10:07:30 +0800 Subject: [PATCH 24/38] refactor: Make enforcedName nullable --- drift_dev/lib/src/analysis/resolver/dart/helper.dart | 5 ++--- drift_dev/lib/src/analysis/resolver/dart/table.dart | 4 +++- drift_dev/lib/src/analysis/resolver/dart/view.dart | 4 +++- drift_dev/lib/src/analysis/resolver/shared/data_class.dart | 7 +++++-- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/drift_dev/lib/src/analysis/resolver/dart/helper.dart b/drift_dev/lib/src/analysis/resolver/dart/helper.dart index 7e1f3530..88dfe6c8 100644 --- a/drift_dev/lib/src/analysis/resolver/dart/helper.dart +++ b/drift_dev/lib/src/analysis/resolver/dart/helper.dart @@ -197,7 +197,7 @@ extension TypeUtils on DartType { } class DataClassInformation { - final String enforcedName; + final String? enforcedName; final String? companionName; final CustomParentClass? extending; final ExistingRowClass? existingClass; @@ -235,8 +235,7 @@ class DataClassInformation { )); } - var name = dataClassName?.getField('name')!.toStringValue() ?? - dataClassNameForClassName(element.name); + var name = dataClassName?.getField('name')!.toStringValue(); final companionName = dataClassName?.getField('companionName')?.toStringValue(); CustomParentClass? customParentClass; diff --git a/drift_dev/lib/src/analysis/resolver/dart/table.dart b/drift_dev/lib/src/analysis/resolver/dart/table.dart index 82917df2..d7238e6f 100644 --- a/drift_dev/lib/src/analysis/resolver/dart/table.dart +++ b/drift_dev/lib/src/analysis/resolver/dart/table.dart @@ -2,6 +2,7 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/syntactic_entity.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:collection/collection.dart'; +import 'package:drift_dev/src/analysis/resolver/shared/data_class.dart'; import 'package:sqlparser/sqlparser.dart' as sql; import '../../driver/error.dart'; @@ -54,7 +55,8 @@ class DartTableResolver extends LocalElementResolver { DriftDeclaration.dartElement(element), columns: columns, references: references.toList(), - nameOfRowClass: dataClassInfo.enforcedName, + nameOfRowClass: + dataClassInfo.enforcedName ?? dataClassNameForClassName(element.name), nameOfCompanionClass: dataClassInfo.companionName, existingRowClass: dataClassInfo.existingClass, customParentClass: dataClassInfo.extending, diff --git a/drift_dev/lib/src/analysis/resolver/dart/view.dart b/drift_dev/lib/src/analysis/resolver/dart/view.dart index 9b6142ee..9a8f3b2e 100644 --- a/drift_dev/lib/src/analysis/resolver/dart/view.dart +++ b/drift_dev/lib/src/analysis/resolver/dart/view.dart @@ -9,6 +9,7 @@ import 'package:recase/recase.dart'; import '../../results/results.dart'; import '../intermediate_state.dart'; import '../resolver.dart'; +import '../shared/data_class.dart'; import 'helper.dart'; class DartViewResolver extends LocalElementResolver { @@ -26,7 +27,8 @@ class DartViewResolver extends LocalElementResolver { discovered.ownId, DriftDeclaration.dartElement(discovered.dartElement), columns: columns, - nameOfRowClass: dataClassInfo.enforcedName, + nameOfRowClass: dataClassInfo.enforcedName ?? + dataClassNameForClassName(discovered.dartElement.name), existingRowClass: dataClassInfo.existingClass, customParentClass: dataClassInfo.extending, entityInfoName: '\$${discovered.dartElement.name}View', diff --git a/drift_dev/lib/src/analysis/resolver/shared/data_class.dart b/drift_dev/lib/src/analysis/resolver/shared/data_class.dart index 9a7ca701..a3411bc4 100644 --- a/drift_dev/lib/src/analysis/resolver/shared/data_class.dart +++ b/drift_dev/lib/src/analysis/resolver/shared/data_class.dart @@ -31,7 +31,7 @@ String dataClassNameForClassName(String tableName) { } CustomParentClass? parseCustomParentClass( - String dartTypeName, + String? dartTypeName, DartObject dataClassName, ClassElement element, LocalElementResolver resolver, @@ -87,7 +87,10 @@ CustomParentClass? parseCustomParentClass( code = AnnotatedDartCode([ DartTopLevelSymbol.topLevelElement(extendingType.element), '<', - DartTopLevelSymbol(dartTypeName, null), + DartTopLevelSymbol( + dartTypeName ?? dataClassNameForClassName(element.name), + null, + ), '>', ]); } else { From 29303100b80d41b92f48b4bebff8b4a39075e320 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Fri, 8 Mar 2024 18:13:50 +0100 Subject: [PATCH 25/38] Support custom names for drift views --- .../lib/snippets/modular/drift/row_class.dart | 17 + .../modular/drift/with_existing.drift | 22 ++ .../modular/drift/with_existing.drift.dart | 346 ++++++++++++++++++ docs/pages/docs/SQL API/drift_files.md | 48 +-- sqlparser/CHANGELOG.md | 4 + sqlparser/lib/src/reader/parser.dart | 69 ++-- sqlparser/lib/utils/node_to_text.dart | 1 + sqlparser/test/parser/create_view_test.dart | 19 + sqlparser/test/utils/node_to_text_test.dart | 8 + 9 files changed, 470 insertions(+), 64 deletions(-) create mode 100644 docs/lib/snippets/modular/drift/row_class.dart create mode 100644 docs/lib/snippets/modular/drift/with_existing.drift create mode 100644 docs/lib/snippets/modular/drift/with_existing.drift.dart diff --git a/docs/lib/snippets/modular/drift/row_class.dart b/docs/lib/snippets/modular/drift/row_class.dart new file mode 100644 index 00000000..e9452cca --- /dev/null +++ b/docs/lib/snippets/modular/drift/row_class.dart @@ -0,0 +1,17 @@ +// #docregion user +class User { + final int id; + final String name; + + User(this.id, this.name); +} +// #enddocregion user + +// #docregion userwithfriends +class UserWithFriends { + final User user; + final List friends; + + UserWithFriends(this.user, {this.friends = const []}); +} +// #enddocregion userwithfriends diff --git a/docs/lib/snippets/modular/drift/with_existing.drift b/docs/lib/snippets/modular/drift/with_existing.drift new file mode 100644 index 00000000..ed95d942 --- /dev/null +++ b/docs/lib/snippets/modular/drift/with_existing.drift @@ -0,0 +1,22 @@ +-- #docregion users +import 'row_class.dart'; --import for where the row class is defined + +CREATE TABLE users ( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL +) WITH User; -- This tells drift to use the existing Dart class +-- #enddocregion users + +-- #docregion friends +-- table to demonstrate a more complex select query below. +-- also, remember to add the import for `UserWithFriends` to your drift file. +CREATE TABLE friends ( + user_a INTEGER NOT NULL REFERENCES users(id), + user_b INTEGER NOT NULL REFERENCES users(id), + PRIMARY KEY (user_a, user_b) +); + +allFriendsOf WITH UserWithFriends: SELECT users.** AS user, LIST( + SELECT * FROM users a INNER JOIN friends ON user_a = a.id WHERE user_b = users.id OR user_a = users.id +) AS friends FROM users WHERE id = :id; +-- #enddocregion friends diff --git a/docs/lib/snippets/modular/drift/with_existing.drift.dart b/docs/lib/snippets/modular/drift/with_existing.drift.dart new file mode 100644 index 00000000..a1c42054 --- /dev/null +++ b/docs/lib/snippets/modular/drift/with_existing.drift.dart @@ -0,0 +1,346 @@ +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:drift_docs/snippets/modular/drift/row_class.dart' as i1; +import 'package:drift_docs/snippets/modular/drift/with_existing.drift.dart' + as i2; +import 'package:drift/internal/modular.dart' as i3; + +class Users extends i0.Table with i0.TableInfo { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + Users(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY'); + static const i0.VerificationMeta _nameMeta = + const i0.VerificationMeta('name'); + late final i0.GeneratedColumn name = i0.GeneratedColumn( + 'name', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); + @override + List get $columns => [id, name]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'users'; + @override + i0.VerificationContext validateIntegrity(i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.User map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.User( + attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}id'])!, + attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!, + ); + } + + @override + Users createAlias(String alias) { + return Users(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class UsersCompanion extends i0.UpdateCompanion { + final i0.Value id; + final i0.Value name; + const UsersCompanion({ + this.id = const i0.Value.absent(), + this.name = const i0.Value.absent(), + }); + UsersCompanion.insert({ + this.id = const i0.Value.absent(), + required String name, + }) : name = i0.Value(name); + static i0.Insertable custom({ + i0.Expression? id, + i0.Expression? name, + }) { + return i0.RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + }); + } + + i2.UsersCompanion copyWith({i0.Value? id, i0.Value? name}) { + return i2.UsersCompanion( + id: id ?? this.id, + name: name ?? this.name, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (name.present) { + map['name'] = i0.Variable(name.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('UsersCompanion(') + ..write('id: $id, ') + ..write('name: $name') + ..write(')')) + .toString(); + } +} + +class Friends extends i0.Table with i0.TableInfo { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + Friends(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _userAMeta = + const i0.VerificationMeta('userA'); + late final i0.GeneratedColumn userA = i0.GeneratedColumn( + 'user_a', aliasedName, false, + type: i0.DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES users(id)'); + static const i0.VerificationMeta _userBMeta = + const i0.VerificationMeta('userB'); + late final i0.GeneratedColumn userB = i0.GeneratedColumn( + 'user_b', aliasedName, false, + type: i0.DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES users(id)'); + @override + List get $columns => [userA, userB]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'friends'; + @override + i0.VerificationContext validateIntegrity(i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('user_a')) { + context.handle( + _userAMeta, userA.isAcceptableOrUnknown(data['user_a']!, _userAMeta)); + } else if (isInserting) { + context.missing(_userAMeta); + } + if (data.containsKey('user_b')) { + context.handle( + _userBMeta, userB.isAcceptableOrUnknown(data['user_b']!, _userBMeta)); + } else if (isInserting) { + context.missing(_userBMeta); + } + return context; + } + + @override + Set get $primaryKey => {userA, userB}; + @override + i2.Friend map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i2.Friend( + userA: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}user_a'])!, + userB: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}user_b'])!, + ); + } + + @override + Friends createAlias(String alias) { + return Friends(attachedDatabase, alias); + } + + @override + List get customConstraints => const ['PRIMARY KEY(user_a, user_b)']; + @override + bool get dontWriteConstraints => true; +} + +class Friend extends i0.DataClass implements i0.Insertable { + final int userA; + final int userB; + const Friend({required this.userA, required this.userB}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_a'] = i0.Variable(userA); + map['user_b'] = i0.Variable(userB); + return map; + } + + i2.FriendsCompanion toCompanion(bool nullToAbsent) { + return i2.FriendsCompanion( + userA: i0.Value(userA), + userB: i0.Value(userB), + ); + } + + factory Friend.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return Friend( + userA: serializer.fromJson(json['user_a']), + userB: serializer.fromJson(json['user_b']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'user_a': serializer.toJson(userA), + 'user_b': serializer.toJson(userB), + }; + } + + i2.Friend copyWith({int? userA, int? userB}) => i2.Friend( + userA: userA ?? this.userA, + userB: userB ?? this.userB, + ); + @override + String toString() { + return (StringBuffer('Friend(') + ..write('userA: $userA, ') + ..write('userB: $userB') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(userA, userB); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i2.Friend && + other.userA == this.userA && + other.userB == this.userB); +} + +class FriendsCompanion extends i0.UpdateCompanion { + final i0.Value userA; + final i0.Value userB; + final i0.Value rowid; + const FriendsCompanion({ + this.userA = const i0.Value.absent(), + this.userB = const i0.Value.absent(), + this.rowid = const i0.Value.absent(), + }); + FriendsCompanion.insert({ + required int userA, + required int userB, + this.rowid = const i0.Value.absent(), + }) : userA = i0.Value(userA), + userB = i0.Value(userB); + static i0.Insertable custom({ + i0.Expression? userA, + i0.Expression? userB, + i0.Expression? rowid, + }) { + return i0.RawValuesInsertable({ + if (userA != null) 'user_a': userA, + if (userB != null) 'user_b': userB, + if (rowid != null) 'rowid': rowid, + }); + } + + i2.FriendsCompanion copyWith( + {i0.Value? userA, i0.Value? userB, i0.Value? rowid}) { + return i2.FriendsCompanion( + userA: userA ?? this.userA, + userB: userB ?? this.userB, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userA.present) { + map['user_a'] = i0.Variable(userA.value); + } + if (userB.present) { + map['user_b'] = i0.Variable(userB.value); + } + if (rowid.present) { + map['rowid'] = i0.Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('FriendsCompanion(') + ..write('userA: $userA, ') + ..write('userB: $userB, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class WithExistingDrift extends i3.ModularAccessor { + WithExistingDrift(i0.GeneratedDatabase db) : super(db); + i0.Selectable allFriendsOf(int id) { + return customSelect( + 'SELECT"users"."id" AS "nested_0.id", "users"."name" AS "nested_0.name", users.id AS "\$n_0", users.id AS "\$n_1" FROM users WHERE id = ?1', + variables: [ + i0.Variable(id) + ], + readsFrom: { + users, + friends, + }).asyncMap((i0.QueryRow row) async => i1.UserWithFriends( + await users.mapFromRow(row, tablePrefix: 'nested_0'), + friends: await customSelect( + 'SELECT * FROM users AS a INNER JOIN friends ON user_a = a.id WHERE user_b = ?1 OR user_a = ?2', + variables: [ + i0.Variable(row.read('\$n_0')), + i0.Variable(row.read('\$n_1')) + ], + readsFrom: { + users, + friends, + }) + .map((i0.QueryRow row) => i1.User( + row.read('id'), + row.read('name'), + )) + .get(), + )); + } + + i2.Users get users => this.resultSet('users'); + i2.Friends get friends => this.resultSet('friends'); +} diff --git a/docs/pages/docs/SQL API/drift_files.md b/docs/pages/docs/SQL API/drift_files.md index 2277d3cf..a2ff0a69 100644 --- a/docs/pages/docs/SQL API/drift_files.md +++ b/docs/pages/docs/SQL API/drift_files.md @@ -167,6 +167,8 @@ statement before it runs it. a defined query by appending `WITH YourDartClass` to a `CREATE TABLE` statement. - Alternatively, you may use `AS DesiredRowClassName` to change the name of the row class generated by drift. +- Both custom row classes and custom table names also work for views, e.g. with + `CREATE VIEW my_view AS DartName AS SELECT ...;`. - In a column definition, `MAPPED BY` can be used to [apply a converter](#type-converters) to that column. - Similarly, a `JSON KEY` constraint can be used to define the key drift will @@ -356,28 +358,17 @@ With that option, the variable will be inferred to `Preferences` instead of `Str ### Existing row classes +{% assign existingDrift = "package:drift_docs/snippets/modular/drift/with_existing.drift.excerpt.json" | readString | json_decode %} +{% assign rowClassDart = "package:drift_docs/snippets/modular/drift/row_class.dart.excerpt.json" | readString | json_decode %} + You can use custom row classes instead of having drift generate one for you. For instance, let's say you had a Dart class defined as -```dart -class User { - final int id; - final String name; - - User(this.id, this.name); -} -``` +{% include "blocks/snippet" snippets = rowClassDart name = "user" %} Then, you can instruct drift to use that class as a row class as follows: -```sql -import 'row_class.dart'; --import for where the row class is defined - -CREATE TABLE users ( - id INTEGER NOT NULL PRIMARY KEY, - name TEXT NOT NULL, -) WITH User; -- This tells drift to use the existing Dart class -``` +{% include "blocks/snippet" snippets = existingDrift name = "users" %} When using custom row classes defined in another Dart file, you also need to import that file into the file where you define the database. @@ -388,32 +379,11 @@ can be added after the name of the query. For instance, let's say we expand the existing Dart code in `row_class.dart` by adding another class: -```dart -class UserWithFriends { - final User user; - final List friends; - - UserWithFriends(this.user, {this.friends = const []}); -} -``` +{% include "blocks/snippet" snippets = rowClassDart name = "userwithfriends" %} Now, we can add a corresponding query using the new class for its rows: -```sql --- table to demonstrate a more complex select query below. --- also, remember to add the import for `UserWithFriends` to your drift file. -CREATE TABLE friends ( - user_a INTEGER NOT NULL REFERENCES users(id), - user_b INTEGER NOT NULL REFERENCES users(id), - PRIMARY KEY (user_a, user_b) -); - -allFriendsOf WITH UserWithFriends: SELECT users.**, LIST( - SELECT * FROM users a INNER JOIN friends ON user_a = a.id WHERE user_b = users.id - UNION ALL - SELECT * FROM users b INNER JOIN friends ON user_b = b.id WHERE user_a = users.id -) AS friends FROM users WHERE id = :id; -``` +{% include "blocks/snippet" snippets = existingDrift name = "friends" %} The `WITH UserWithFriends` syntax will make drift consider the `UserWithFriends` class. For every field in the constructor, drift will check the column from the query and diff --git a/sqlparser/CHANGELOG.md b/sqlparser/CHANGELOG.md index aabbd0da..f05cb2f3 100644 --- a/sqlparser/CHANGELOG.md +++ b/sqlparser/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.35.0-dev + +- Drift extensions: Allow custom class names for `CREATE VIEW` statements. + ## 0.34.1 - Allow selecting from virtual tables using the table-valued function diff --git a/sqlparser/lib/src/reader/parser.dart b/sqlparser/lib/src/reader/parser.dart index 63d85a75..758166c1 100644 --- a/sqlparser/lib/src/reader/parser.dart +++ b/sqlparser/lib/src/reader/parser.dart @@ -2294,27 +2294,30 @@ class Parser { supportAs ? const [TokenType.as, TokenType.$with] : [TokenType.$with]; if (enableDriftExtensions && (_match(types))) { - final first = _previous; - final useExisting = _previous.type == TokenType.$with; - final name = - _consumeIdentifier('Expected the name for the data class').identifier; - String? constructorName; - - if (_matchOne(TokenType.dot)) { - constructorName = _consumeIdentifier( - 'Expected name of the constructor to use after the dot') - .identifier; - } - - return DriftTableName( - useExistingDartClass: useExisting, - overriddenDataClassName: name, - constructorName: constructorName, - )..setSpan(first, _previous); + return _startedDriftTableName(_previous); } return null; } + DriftTableName _startedDriftTableName(Token first) { + final useExisting = _previous.type == TokenType.$with; + final name = + _consumeIdentifier('Expected the name for the data class').identifier; + String? constructorName; + + if (_matchOne(TokenType.dot)) { + constructorName = _consumeIdentifier( + 'Expected name of the constructor to use after the dot') + .identifier; + } + + return DriftTableName( + useExistingDartClass: useExisting, + overriddenDataClassName: name, + constructorName: constructorName, + )..setSpan(first, _previous); + } + /// Parses a "CREATE TRIGGER" statement, assuming that the create token has /// already been consumed. CreateTriggerStatement? _createTrigger() { @@ -2409,17 +2412,33 @@ class Parser { final ifNotExists = _ifNotExists(); final name = _consumeIdentifier('Expected a name for this view'); - // Don't allow the "AS ClassName" syntax for views since it causes an - // ambiguity with the regular view syntax. - final driftTableName = _driftTableName(supportAs: false); + DriftTableName? driftTableName; + var skippedToSelect = false; - List? columnNames; - if (_matchOne(TokenType.leftParen)) { - columnNames = _columnNames(); - _consume(TokenType.rightParen, 'Expected closing bracket'); + if (enableDriftExtensions) { + if (_check(TokenType.$with)) { + driftTableName = _driftTableName(); + } else if (_matchOne(TokenType.as)) { + // This can either be a data class name or the beginning of the select + if (_check(TokenType.identifier)) { + // It's a data class name + driftTableName = _startedDriftTableName(_previous); + } else { + // No, we'll expect the SELECT next. + skippedToSelect = true; + } + } } - _consume(TokenType.as, 'Expected AS SELECT'); + List? columnNames; + if (!skippedToSelect) { + if (_matchOne(TokenType.leftParen)) { + columnNames = _columnNames(); + _consume(TokenType.rightParen, 'Expected closing bracket'); + } + + _consume(TokenType.as, 'Expected AS SELECT'); + } final query = _fullSelect(); if (query == null) { diff --git a/sqlparser/lib/utils/node_to_text.dart b/sqlparser/lib/utils/node_to_text.dart index dd2fad7b..59554a36 100644 --- a/sqlparser/lib/utils/node_to_text.dart +++ b/sqlparser/lib/utils/node_to_text.dart @@ -428,6 +428,7 @@ class NodeSqlBuilder extends AstVisitor { _ifNotExists(e.ifNotExists); identifier(e.viewName); + e.driftTableName?.accept(this, arg); if (e.columns != null) { symbol('(', spaceBefore: true); diff --git a/sqlparser/test/parser/create_view_test.dart b/sqlparser/test/parser/create_view_test.dart index de357aae..8aaec48d 100644 --- a/sqlparser/test/parser/create_view_test.dart +++ b/sqlparser/test/parser/create_view_test.dart @@ -36,6 +36,25 @@ void main() { ); }); + test('parses a CREATE VIEW statement with a custom Dart name', () { + testStatement( + 'CREATE VIEW my_view AS DartClass AS SELECT 1', + CreateViewStatement( + viewName: 'my_view', + query: SelectStatement( + columns: [ + ExpressionResultColumn(expression: NumericLiteral(1)), + ], + ), + driftTableName: DriftTableName( + overriddenDataClassName: 'DartClass', + useExistingDartClass: false, + ), + ), + driftMode: true, + ); + }); + test('parses a complex CREATE View statement', () { testStatement( 'CREATE VIEW IF NOT EXISTS my_complex_view (ids, name, count, type) AS ' diff --git a/sqlparser/test/utils/node_to_text_test.dart b/sqlparser/test/utils/node_to_text_test.dart index de0fb44b..9a2fd353 100644 --- a/sqlparser/test/utils/node_to_text_test.dart +++ b/sqlparser/test/utils/node_to_text_test.dart @@ -107,6 +107,14 @@ CREATE VIEW my_view (foo, bar) AS SELECT * FROM t1; testFormat(''' CREATE VIEW my_view AS SELECT * FROM t1; '''); + + testFormat(''' +CREATE VIEW my_view AS Foo (foo, bar) AS SELECT * FROM t1; + '''); + + testFormat(''' +CREATE VIEW my_view WITH Foo.constr (foo, bar) AS SELECT * FROM t1; + '''); }); group('table', () { From c231d25e5980dd702c311af7ec34eb2e5d5c6565 Mon Sep 17 00:00:00 2001 From: Stefano La Terra Date: Sat, 9 Mar 2024 11:08:32 +0100 Subject: [PATCH 26/38] Update select.md Fix small typo in description --- docs/pages/docs/Dart API/select.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pages/docs/Dart API/select.md b/docs/pages/docs/Dart API/select.md index 2bdabea2..b26f3bfe 100644 --- a/docs/pages/docs/Dart API/select.md +++ b/docs/pages/docs/Dart API/select.md @@ -1,7 +1,7 @@ --- data: title: "Selects" - description: "Select rows or invidiual columns from tables in Dart" + description: "Select rows or individual columns from tables in Dart" weight: 2 template: layouts/docs/single From e90c3c33a6087480d44322070af6bd2309c5ad68 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 14 Mar 2024 18:54:47 +0100 Subject: [PATCH 27/38] Add clarifying comment to partial database class --- docs/lib/snippets/setup/database.dart | 5 +++++ docs/pages/docs/setup.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/lib/snippets/setup/database.dart b/docs/lib/snippets/setup/database.dart index fe45fc4d..00d175a5 100644 --- a/docs/lib/snippets/setup/database.dart +++ b/docs/lib/snippets/setup/database.dart @@ -31,7 +31,12 @@ class TodoItems extends Table { @DriftDatabase(tables: [TodoItems]) class AppDatabase extends _$AppDatabase { +// #enddocregion open + // After generating code, this class needs to define a `schemaVersion` getter + // and a constructor telling drift where the database should be stored. + // These are described in the getting started guide: https://drift.simonbinder.eu/getting-started/#open // #enddocregion before_generation +// #docregion open AppDatabase() : super(_openConnection()); @override diff --git a/docs/pages/docs/setup.md b/docs/pages/docs/setup.md index 59947b8f..a1379099 100644 --- a/docs/pages/docs/setup.md +++ b/docs/pages/docs/setup.md @@ -99,7 +99,7 @@ You will now see errors related to missing overrides and a missing constructor. is responsible for telling drift how to open the database. The `schemaVersion` getter is relevant for migrations after changing the database, we can leave it at `1` for now. The database class now looks like this: - + {% include "blocks/snippet" snippets = snippets name = 'open' %} The Android-specific workarounds are necessary because sqlite3 attempts to use `/tmp` to store From 7c0c52cdfebf56463ed34b16fbba57e0052a4e66 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 20 Mar 2024 22:27:09 +0100 Subject: [PATCH 28/38] Apply serialization encoding to result values --- drift/lib/src/remote/protocol.dart | 6 ++++-- drift/test/remote_test.dart | 29 ++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/drift/lib/src/remote/protocol.dart b/drift/lib/src/remote/protocol.dart index d6ddb99f..8a68eefc 100644 --- a/drift/lib/src/remote/protocol.dart +++ b/drift/lib/src/remote/protocol.dart @@ -145,7 +145,9 @@ class DriftProtocol { result.add(rows.length); for (final row in rows) { - result.addAll(row.values); + for (final value in row.values) { + result.add(_encodeDbValue(value)); + } } return result; } @@ -234,7 +236,7 @@ class DriftProtocol { result.add({ for (var c = 0; c < columnCount; c++) - columns[c]: fullMessage[rowOffset + c] + columns[c]: _decodeDbValue(fullMessage[rowOffset + c]) }); } return SelectResult(result); diff --git a/drift/test/remote_test.dart b/drift/test/remote_test.dart index d9845fc4..ce258864 100644 --- a/drift/test/remote_test.dart +++ b/drift/test/remote_test.dart @@ -98,7 +98,7 @@ void main() { ExecuteQuery(StatementMethod.select, 'SELECT ?', [BigInt.one]), ); - final mapped = protocol.deserialize(protocol.serialize(request)!); + final mapped = _checkSimpleRoundtrip(protocol, request); expect( mapped, isA().having((e) => e.id, 'id', 1).having( @@ -109,6 +109,27 @@ void main() { .having((e) => e.args, 'args', [isA()]), ), ); + + final response = SuccessResponse( + 1, + SelectResult([ + {'col': BigInt.one} + ])); + final mappedResponse = _checkSimpleRoundtrip(protocol, response); + expect( + mappedResponse, + isA().having((e) => e.requestId, 'requestId', 1).having( + (e) => e.response, + 'response', + isA().having( + (e) => e.rows, + 'rows', + ([ + {'col': BigInt.one} + ]), + ), + ), + ); }); test('can run protocol without using complex types', () async { @@ -244,6 +265,12 @@ void _checkSimple(Object? object) { } } +Message _checkSimpleRoundtrip(DriftProtocol protocol, Message source) { + final serialized = protocol.serialize(source); + _checkSimple(serialized); + return protocol.deserialize(serialized!); +} + extension on StreamChannel { StreamChannel get expectedToClose { return transformStream(StreamTransformer.fromHandlers( From 044a0f3980e8c0aa392bd375f94b4d7471ea41b7 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 20 Mar 2024 22:47:24 +0100 Subject: [PATCH 29/38] Allow disabling conflict target (#2925) --- .../runtime/query_builder/statements/insert.dart | 11 ++++++++++- drift/test/database/statements/insert_test.dart | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/drift/lib/src/runtime/query_builder/statements/insert.dart b/drift/lib/src/runtime/query_builder/statements/insert.dart index fa473594..78629567 100644 --- a/drift/lib/src/runtime/query_builder/statements/insert.dart +++ b/drift/lib/src/runtime/query_builder/statements/insert.dart @@ -276,8 +276,15 @@ class InsertStatement { if (ctx.dialect == SqlDialect.mariadb) { ctx.buffer.write(' ON DUPLICATE'); } else { - ctx.buffer.write(' ON CONFLICT('); + ctx.buffer.write(' ON CONFLICT'); + if (target != null && target.isEmpty) { + // An empty list indicates that no explicit target should be generated + // by drift, the default rules by the database will apply instead. + return; + } + + ctx.buffer.write('('); final conflictTarget = target ?? table.$primaryKey.toList(); if (conflictTarget.isEmpty) { @@ -473,6 +480,8 @@ class DoUpdate extends UpsertClause { /// specifies the uniqueness constraint that will trigger the upsert. /// /// By default, the primary key of the table will be used. + /// This can be set to an empty list, in which case no explicit conflict + /// target will be generated by drift. final List? target; /// Creates a `DO UPDATE` clause. diff --git a/drift/test/database/statements/insert_test.dart b/drift/test/database/statements/insert_test.dart index a038e255..77b15685 100644 --- a/drift/test/database/statements/insert_test.dart +++ b/drift/test/database/statements/insert_test.dart @@ -263,6 +263,22 @@ void main() { )); }); + test('can ignore conflict target', () async { + await db.into(db.todosTable).insert( + TodosTableCompanion.insert(content: 'my content'), + onConflict: DoUpdate((old) { + return TodosTableCompanion.custom( + content: const Variable('important: ') + old.content); + }, target: []), + ); + + verify(executor.runInsert( + 'INSERT INTO "todos" ("content") VALUES (?) ' + 'ON CONFLICT DO UPDATE SET "content" = ? || "content"', + argThat(equals(['my content', 'important: '])), + )); + }); + test( 'can use multiple upsert targets', () async { From ae40dd6d81863a7fb2be104d9a65fd784b262ea7 Mon Sep 17 00:00:00 2001 From: Jeremiah Ogbomo Date: Fri, 22 Mar 2024 19:43:09 +0100 Subject: [PATCH 30/38] Improve check for if postgres session is still open (#2928) --- extras/drift_postgres/lib/src/pg_database.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extras/drift_postgres/lib/src/pg_database.dart b/extras/drift_postgres/lib/src/pg_database.dart index 7190742b..af2c6689 100644 --- a/extras/drift_postgres/lib/src/pg_database.dart +++ b/extras/drift_postgres/lib/src/pg_database.dart @@ -54,7 +54,8 @@ class _PgDelegate extends DatabaseDelegate { late DbVersionDelegate versionDelegate; @override - Future get isOpen => Future.value(_openedSession != null); + Future get isOpen => + Future.value(_openedSession != null && _openedSession!.isOpen); @override Future open(QueryExecutorUser user) async { From e1bdc6bde5511244c6f55875454b5524e738f8d0 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 24 Mar 2024 14:37:27 +0100 Subject: [PATCH 31/38] Fix missing commas between type arguments --- drift_dev/lib/src/analysis/results/dart.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/drift_dev/lib/src/analysis/results/dart.dart b/drift_dev/lib/src/analysis/results/dart.dart index 5171595d..d5733947 100644 --- a/drift_dev/lib/src/analysis/results/dart.dart +++ b/drift_dev/lib/src/analysis/results/dart.dart @@ -556,4 +556,11 @@ class _AddFromAst extends GeneralizingAstVisitor { _visitCommaSeparated(node.elements); _childEntities([node.rightBracket]); } + + @override + void visitTypeArgumentList(TypeArgumentList node) { + _builder.addText('<'); + _visitCommaSeparated(node.arguments); + _builder.addText('>'); + } } From fc7e2ab1e6d0cb793a78728a48fcb77f7f24335f Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 24 Mar 2024 14:58:58 +0100 Subject: [PATCH 32/38] Add utility for extension type converters --- drift/CHANGELOG.md | 2 + drift/lib/src/runtime/types/converters.dart | 43 +++++ drift/test/database/batch_test.dart | 15 +- drift/test/database/data_class_test.dart | 10 +- drift/test/database/database_test.dart | 2 +- .../statements/custom_queries_test.dart | 2 +- .../test/database/statements/delete_test.dart | 7 +- .../test/database/statements/insert_test.dart | 7 +- drift/test/database/statements/join_test.dart | 12 +- .../test/database/statements/select_test.dart | 8 +- .../test/database/statements/update_test.dart | 10 +- drift/test/database/tables_test.dart | 2 +- .../database/types/type_converter_test.dart | 11 ++ drift/test/generated/todos.dart | 8 +- drift/test/generated/todos.g.dart | 158 ++++++++++-------- .../insert_integration_test.dart | 2 +- .../select_integration_test.dart | 4 +- drift/test/isolate_test.dart | 4 +- drift/test/serialization_test.dart | 2 +- drift/test/test_utils/test_utils.mocks.dart | 2 +- 20 files changed, 195 insertions(+), 116 deletions(-) diff --git a/drift/CHANGELOG.md b/drift/CHANGELOG.md index c82b64a9..e4e7ef3b 100644 --- a/drift/CHANGELOG.md +++ b/drift/CHANGELOG.md @@ -2,6 +2,8 @@ - Adds `companion` entry to `DataClassName` to override the name of the generated companion class. +- Add the `TypeConverter.extensionType` factory to create type converters for + extension types. ## 2.16.0 diff --git a/drift/lib/src/runtime/types/converters.dart b/drift/lib/src/runtime/types/converters.dart index 30a2cda4..a2c0d6e7 100644 --- a/drift/lib/src/runtime/types/converters.dart +++ b/drift/lib/src/runtime/types/converters.dart @@ -47,6 +47,35 @@ abstract class TypeConverter { json: json, ); } + + /// A type converter mapping [extension types] to their underlying + /// representation to store them in databases. + /// + /// Here, [ExtType] is the extension type to use in Dart classes, and [Inner] + /// is the underlying type stored in the database. For instance, if you had + /// a type to represent ids in a database: + /// + /// ```dart + /// extension type IdNumber(int id) {} + /// ``` + /// + /// You could use `TypeConverter.extensionType()` in a column + /// definition: + /// + /// ```dart + /// class Users extends Table { + /// IntColumn get id => integer() + /// .autoIncrement() + /// .map(TypeConverter.extensionType())(); + /// TextColumn get name => text()(); + /// } + /// ``` + /// + /// [extension types]: https://dart.dev/language/extension-types + static JsonTypeConverter + extensionType() { + return _ExtensionTypeConverter(); + } } /// A mixin for [TypeConverter]s that should also apply to drift's builtin @@ -264,3 +293,17 @@ class _NullWrappingTypeConverterWithJson return value == null ? null : requireToJson(value); } } + +class _ExtensionTypeConverter + extends TypeConverter + with JsonTypeConverter { + const _ExtensionTypeConverter(); + + @override + ExtType fromSql(Inner fromDb) { + return fromDb as ExtType; + } + + @override + Inner toSql(ExtType value) => value as Inner; +} diff --git a/drift/test/database/batch_test.dart b/drift/test/database/batch_test.dart index dcf1d51c..1e527289 100644 --- a/drift/test/database/batch_test.dart +++ b/drift/test/database/batch_test.dart @@ -35,14 +35,14 @@ void main() { ); b.replaceAll(db.categories, const [ - CategoriesCompanion(id: Value(1), description: Value('new1')), - CategoriesCompanion(id: Value(2), description: Value('new2')), + CategoriesCompanion(id: Value(RowId(1)), description: Value('new1')), + CategoriesCompanion(id: Value(RowId(2)), description: Value('new2')), ]); b.deleteWhere<$CategoriesTable, Category>( db.categories, (tbl) => tbl.id.equals(1)); b.deleteAll(db.categories); - b.delete(db.todosTable, const TodosTableCompanion(id: Value(3))); + b.delete(db.todosTable, const TodosTableCompanion(id: Value(RowId(3)))); b.update(db.users, const UsersCompanion(name: Value('new name 2'))); @@ -97,7 +97,7 @@ void main() { db.categories, CategoriesCompanion.insert(description: 'description'), onConflict: DoUpdate((old) { - return const CategoriesCompanion(id: Value(42)); + return const CategoriesCompanion(id: Value(RowId(42))); }), ); }); @@ -203,16 +203,17 @@ void main() { test('updates stream queries', () async { await db.batch((b) { - b.insert(db.todosTable, const TodoEntry(id: 3, content: 'content')); + b.insert( + db.todosTable, const TodoEntry(id: RowId(3), content: 'content')); b.update(db.users, const UsersCompanion(name: Value('new user name'))); b.replace( db.todosTable, - const TodosTableCompanion(id: Value(3), content: Value('new')), + const TodosTableCompanion(id: Value(RowId(3)), content: Value('new')), ); b.deleteWhere(db.todosTable, (TodosTable row) => row.id.equals(3)); - b.delete(db.todosTable, const TodosTableCompanion(id: Value(3))); + b.delete(db.todosTable, const TodosTableCompanion(id: Value(RowId(3)))); }); verify( diff --git a/drift/test/database/data_class_test.dart b/drift/test/database/data_class_test.dart index 05afec45..c290bf8d 100644 --- a/drift/test/database/data_class_test.dart +++ b/drift/test/database/data_class_test.dart @@ -6,7 +6,7 @@ import '../generated/todos.dart'; void main() { test('data classes can be serialized', () { final entry = TodoEntry( - id: 13, + id: RowId(13), title: 'Title', content: 'Content', targetDate: DateTime.now(), @@ -36,7 +36,7 @@ void main() { driftRuntimeOptions.defaultSerializer = _MySerializer(); final entry = TodoEntry( - id: 13, + id: RowId(13), title: 'Title', content: 'Content', category: 3, @@ -59,7 +59,7 @@ void main() { test('can serialize and deserialize blob columns', () { final user = User( - id: 3, + id: RowId(3), name: 'Username', isAwesome: true, profilePicture: Uint8List.fromList(const [1, 2, 3, 4]), @@ -79,7 +79,7 @@ void main() { test('generated data classes can be converted to companions', () { const entry = Category( - id: 3, + id: RowId(3), description: 'description', priority: CategoryPriority.low, descriptionInUpperCase: 'ignored', @@ -91,7 +91,7 @@ void main() { companion, equals(CategoriesCompanion.insert( description: 'description', - id: const Value(3), + id: const Value(RowId(3)), priority: const Value(CategoryPriority.low), )), ); diff --git a/drift/test/database/database_test.dart b/drift/test/database/database_test.dart index fe8943d7..e96efd43 100644 --- a/drift/test/database/database_test.dart +++ b/drift/test/database/database_test.dart @@ -72,7 +72,7 @@ void main() { final executor = MockExecutor(); final db = TodoDb(executor); - await db.someDao.todosForUser(user: 1).get(); + await db.someDao.todosForUser(user: RowId(1)).get(); verify(executor.runSelect(argThat(contains('SELECT t.* FROM todos')), [1])); }); diff --git a/drift/test/database/statements/custom_queries_test.dart b/drift/test/database/statements/custom_queries_test.dart index 288a1a31..5759ba1c 100644 --- a/drift/test/database/statements/custom_queries_test.dart +++ b/drift/test/database/statements/custom_queries_test.dart @@ -23,7 +23,7 @@ void main() { group('compiled custom queries', () { // defined query: SELECT * FROM todos WHERE title = ?2 OR id IN ? OR title = ?1 test('work with arrays', () async { - await db.withIn('one', 'two', [1, 2, 3]).get(); + await db.withIn('one', 'two', [RowId(1), RowId(2), RowId(3)]).get(); verify( executor.runSelect( diff --git a/drift/test/database/statements/delete_test.dart b/drift/test/database/statements/delete_test.dart index 24eeb19a..a4db7352 100644 --- a/drift/test/database/statements/delete_test.dart +++ b/drift/test/database/statements/delete_test.dart @@ -58,13 +58,14 @@ void main() { final returnedValue = await db .delete(db.todosTable) - .deleteReturning(const TodosTableCompanion(id: Value(10))); + .deleteReturning(const TodosTableCompanion(id: Value(RowId(10)))); verify(executor.runSelect( 'DELETE FROM "todos" WHERE "id" = ? RETURNING *;', [10])); verify(streamQueries.handleTableUpdates( {TableUpdate.onTable(db.todosTable, kind: UpdateKind.delete)})); - expect(returnedValue, const TodoEntry(id: 10, content: 'Content')); + expect( + returnedValue, const TodoEntry(id: RowId(10), content: 'Content')); }); test('for multiple rows', () async { @@ -112,7 +113,7 @@ void main() { }); test('deleteOne()', () async { - await db.users.deleteOne(const UsersCompanion(id: Value(3))); + await db.users.deleteOne(const UsersCompanion(id: Value(RowId(3)))); verify( executor.runDelete('DELETE FROM "users" WHERE "id" = ?;', const [3])); diff --git a/drift/test/database/statements/insert_test.dart b/drift/test/database/statements/insert_test.dart index 77b15685..971f7ce8 100644 --- a/drift/test/database/statements/insert_test.dart +++ b/drift/test/database/statements/insert_test.dart @@ -56,7 +56,7 @@ void main() { test('generates insert or replace statements', () async { await db.into(db.todosTable).insert( const TodoEntry( - id: 113, + id: RowId(113), content: 'Done', ), mode: InsertMode.insertOrReplace); @@ -405,7 +405,8 @@ void main() { when(executor.runInsert(any, any)).thenAnswer((_) => Future.value(3)); final id = await db.into(db.todosTable).insertOnConflictUpdate( - TodosTableCompanion.insert(content: 'content', id: const Value(3))); + TodosTableCompanion.insert( + content: 'content', id: const Value(RowId(3)))); verify(executor.runInsert( 'INSERT INTO "todos" ("id", "content") VALUES (?, ?) ' @@ -615,7 +616,7 @@ void main() { expect( row, const Category( - id: 1, + id: RowId(1), description: 'description', descriptionInUpperCase: 'DESCRIPTION', priority: CategoryPriority.medium, diff --git a/drift/test/database/statements/join_test.dart b/drift/test/database/statements/join_test.dart index c8d33eae..295f507d 100644 --- a/drift/test/database/statements/join_test.dart +++ b/drift/test/database/statements/join_test.dart @@ -81,7 +81,7 @@ void main() { expect( row.readTable(todos), TodoEntry( - id: 5, + id: RowId(5), title: 'title', content: 'content', targetDate: date, @@ -92,7 +92,7 @@ void main() { expect( row.readTable(categories), const Category( - id: 3, + id: RowId(3), description: 'description', priority: CategoryPriority.high, descriptionInUpperCase: 'DESCRIPTION', @@ -134,7 +134,7 @@ void main() { expect( row.readTable(db.todosTable), const TodoEntry( - id: 5, + id: RowId(5), title: 'title', content: 'content', )); @@ -256,7 +256,7 @@ void main() { result.readTable(categories), equals( const Category( - id: 3, + id: RowId(3), description: 'Description', descriptionInUpperCase: 'DESCRIPTION', priority: CategoryPriority.medium, @@ -306,7 +306,7 @@ void main() { result.readTable(categories), equals( const Category( - id: 3, + id: RowId(3), description: 'Description', descriptionInUpperCase: 'DESCRIPTION', priority: CategoryPriority.medium, @@ -362,7 +362,7 @@ void main() { expect( result.readTable(categories), const Category( - id: 3, + id: RowId(3), description: 'desc', descriptionInUpperCase: 'DESC', priority: CategoryPriority.low, diff --git a/drift/test/database/statements/select_test.dart b/drift/test/database/statements/select_test.dart index 52cc3748..3f1fd1d1 100644 --- a/drift/test/database/statements/select_test.dart +++ b/drift/test/database/statements/select_test.dart @@ -16,7 +16,7 @@ final _dataOfTodoEntry = { }; const _todoEntry = TodoEntry( - id: 10, + id: RowId(10), title: 'A todo title', content: 'Content', category: 3, @@ -126,7 +126,7 @@ void main() { } ]; const resolved = TodoEntry( - id: 10, + id: RowId(10), title: null, content: 'Content', category: null, @@ -198,7 +198,7 @@ void main() { expect( category, const Category( - id: 1, + id: RowId(1), description: 'description', descriptionInUpperCase: 'DESCRIPTION', priority: CategoryPriority.high, @@ -232,7 +232,7 @@ void main() { expect(rows, [ TodoEntry( - id: 10, + id: RowId(10), title: null, content: 'Content', category: null, diff --git a/drift/test/database/statements/update_test.dart b/drift/test/database/statements/update_test.dart index af99b1cc..ce95a733 100644 --- a/drift/test/database/statements/update_test.dart +++ b/drift/test/database/statements/update_test.dart @@ -55,7 +55,7 @@ void main() { group('generates replace statements', () { test('regular', () async { await db.update(db.todosTable).replace(const TodoEntry( - id: 3, + id: RowId(3), title: 'Title', content: 'Updated content', status: TodoStatus.workInProgress, @@ -71,7 +71,7 @@ void main() { test('applies default values', () async { await db.update(db.users).replace( UsersCompanion( - id: const Value(3), + id: const Value(RowId(3)), name: const Value('Hummingbird'), profilePicture: Value(Uint8List(0)), ), @@ -167,14 +167,14 @@ void main() { group('update on table instances', () { test('update()', () async { - await db.users.update().write(const UsersCompanion(id: Value(3))); + await db.users.update().write(const UsersCompanion(id: Value(RowId(3)))); verify(executor.runUpdate('UPDATE "users" SET "id" = ?;', [3])); }); test('replace', () async { await db.categories.replaceOne(const CategoriesCompanion( - id: Value(3), description: Value('new name'))); + id: Value(RowId(3)), description: Value('new name'))); verify(executor.runUpdate( 'UPDATE "categories" SET "desc" = ?, "priority" = 0 WHERE "id" = ?;', @@ -205,7 +205,7 @@ void main() { expect(rows, const [ Category( - id: 3, + id: RowId(3), description: 'test', priority: CategoryPriority.low, descriptionInUpperCase: 'TEST', diff --git a/drift/test/database/tables_test.dart b/drift/test/database/tables_test.dart index 21426964..0f0bb909 100644 --- a/drift/test/database/tables_test.dart +++ b/drift/test/database/tables_test.dart @@ -59,7 +59,7 @@ void main() { expect( todo, const TodoEntry( - id: 1, + id: RowId(1), title: 'some title', content: 'do this', targetDate: null, diff --git a/drift/test/database/types/type_converter_test.dart b/drift/test/database/types/type_converter_test.dart index 4e773e2a..eadf1ef2 100644 --- a/drift/test/database/types/type_converter_test.dart +++ b/drift/test/database/types/type_converter_test.dart @@ -4,6 +4,7 @@ import 'package:drift/drift.dart'; import 'package:test/test.dart'; import '../../generated/converter.dart'; +import '../../generated/todos.dart'; enum _MyEnum { one, two, three } @@ -34,6 +35,16 @@ void main() { }); }); + test('TypeConverter.extensionType', () { + final converter = TypeConverter.extensionType(); + + expect(converter.toSql(RowId(123)), 123); + expect(converter.fromSql(15), RowId(15)); + expect(converter.fromSql(15), 15); + expect(converter.fromJson(16), RowId(16)); + expect(converter.toJson(RowId(124)), 124); + }); + group('enum name', () { const converter = EnumNameConverter(_MyEnum.values); const values = { diff --git a/drift/test/generated/todos.dart b/drift/test/generated/todos.dart index 7d5576a3..a7fa13df 100644 --- a/drift/test/generated/todos.dart +++ b/drift/test/generated/todos.dart @@ -4,8 +4,14 @@ import 'package:uuid/uuid.dart'; part 'todos.g.dart'; +extension type RowId._(int id) { + const RowId(this.id); +} + mixin AutoIncrement on Table { - IntColumn get id => integer().autoIncrement()(); + IntColumn get id => integer() + .autoIncrement() + .map(TypeConverter.extensionType())(); } @DataClassName('TodoEntry') diff --git a/drift/test/generated/todos.g.dart b/drift/test/generated/todos.g.dart index cc61ad3c..4685760b 100644 --- a/drift/test/generated/todos.g.dart +++ b/drift/test/generated/todos.g.dart @@ -11,13 +11,14 @@ class $CategoriesTable extends Categories $CategoriesTable(this.attachedDatabase, [this._alias]); static const VerificationMeta _idMeta = const VerificationMeta('id'); @override - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumnWithTypeConverter id = GeneratedColumn< + int>('id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')) + .withConverter($CategoriesTable.$converterid); static const VerificationMeta _descriptionMeta = const VerificationMeta('description'); @override @@ -56,9 +57,7 @@ class $CategoriesTable extends Categories {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } + context.handle(_idMeta, const VerificationResult.success()); if (data.containsKey('desc')) { context.handle(_descriptionMeta, description.isAcceptableOrUnknown(data['desc']!, _descriptionMeta)); @@ -81,8 +80,8 @@ class $CategoriesTable extends Categories Category map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return Category( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + id: $CategoriesTable.$converterid.fromSql(attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!), description: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}desc'])!, priority: $CategoriesTable.$converterpriority.fromSql(attachedDatabase @@ -99,12 +98,14 @@ class $CategoriesTable extends Categories return $CategoriesTable(attachedDatabase, alias); } + static JsonTypeConverter2 $converterid = + TypeConverter.extensionType(); static JsonTypeConverter2 $converterpriority = const EnumIndexConverter(CategoryPriority.values); } class Category extends DataClass implements Insertable { - final int id; + final RowId id; final String description; final CategoryPriority priority; final String descriptionInUpperCase; @@ -116,7 +117,9 @@ class Category extends DataClass implements Insertable { @override Map toColumns(bool nullToAbsent) { final map = {}; - map['id'] = Variable(id); + { + map['id'] = Variable($CategoriesTable.$converterid.toSql(id)); + } map['desc'] = Variable(description); { map['priority'] = @@ -137,7 +140,8 @@ class Category extends DataClass implements Insertable { {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return Category( - id: serializer.fromJson(json['id']), + id: $CategoriesTable.$converterid + .fromJson(serializer.fromJson(json['id'])), description: serializer.fromJson(json['description']), priority: $CategoriesTable.$converterpriority .fromJson(serializer.fromJson(json['priority'])), @@ -154,7 +158,7 @@ class Category extends DataClass implements Insertable { Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return { - 'id': serializer.toJson(id), + 'id': serializer.toJson($CategoriesTable.$converterid.toJson(id)), 'description': serializer.toJson(description), 'priority': serializer .toJson($CategoriesTable.$converterpriority.toJson(priority)), @@ -164,7 +168,7 @@ class Category extends DataClass implements Insertable { } Category copyWith( - {int? id, + {RowId? id, String? description, CategoryPriority? priority, String? descriptionInUpperCase}) => @@ -200,7 +204,7 @@ class Category extends DataClass implements Insertable { } class CategoriesCompanion extends UpdateCompanion { - final Value id; + final Value id; final Value description; final Value priority; const CategoriesCompanion({ @@ -226,7 +230,7 @@ class CategoriesCompanion extends UpdateCompanion { } CategoriesCompanion copyWith( - {Value? id, + {Value? id, Value? description, Value? priority}) { return CategoriesCompanion( @@ -240,7 +244,7 @@ class CategoriesCompanion extends UpdateCompanion { Map toColumns(bool nullToAbsent) { final map = {}; if (id.present) { - map['id'] = Variable(id.value); + map['id'] = Variable($CategoriesTable.$converterid.toSql(id.value)); } if (description.present) { map['desc'] = Variable(description.value); @@ -271,13 +275,14 @@ class $TodosTableTable extends TodosTable $TodosTableTable(this.attachedDatabase, [this._alias]); static const VerificationMeta _idMeta = const VerificationMeta('id'); @override - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumnWithTypeConverter id = GeneratedColumn< + int>('id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')) + .withConverter($TodosTableTable.$converterid); static const VerificationMeta _titleMeta = const VerificationMeta('title'); @override late final GeneratedColumn title = GeneratedColumn( @@ -328,9 +333,7 @@ class $TodosTableTable extends TodosTable {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } + context.handle(_idMeta, const VerificationResult.success()); if (data.containsKey('title')) { context.handle( _titleMeta, title.isAcceptableOrUnknown(data['title']!, _titleMeta)); @@ -366,8 +369,8 @@ class $TodosTableTable extends TodosTable TodoEntry map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return TodoEntry( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + id: $TodosTableTable.$converterid.fromSql(attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!), title: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}title']), content: attachedDatabase.typeMapping @@ -387,6 +390,8 @@ class $TodosTableTable extends TodosTable return $TodosTableTable(attachedDatabase, alias); } + static JsonTypeConverter2 $converterid = + TypeConverter.extensionType(); static JsonTypeConverter2 $converterstatus = const EnumNameConverter(TodoStatus.values); static JsonTypeConverter2 $converterstatusn = @@ -394,7 +399,7 @@ class $TodosTableTable extends TodosTable } class TodoEntry extends DataClass implements Insertable { - final int id; + final RowId id; final String? title; final String content; final DateTime? targetDate; @@ -410,7 +415,9 @@ class TodoEntry extends DataClass implements Insertable { @override Map toColumns(bool nullToAbsent) { final map = {}; - map['id'] = Variable(id); + { + map['id'] = Variable($TodosTableTable.$converterid.toSql(id)); + } if (!nullToAbsent || title != null) { map['title'] = Variable(title); } @@ -449,7 +456,8 @@ class TodoEntry extends DataClass implements Insertable { {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return TodoEntry( - id: serializer.fromJson(json['id']), + id: $TodosTableTable.$converterid + .fromJson(serializer.fromJson(json['id'])), title: serializer.fromJson(json['title']), content: serializer.fromJson(json['content']), targetDate: serializer.fromJson(json['target_date']), @@ -467,7 +475,7 @@ class TodoEntry extends DataClass implements Insertable { Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return { - 'id': serializer.toJson(id), + 'id': serializer.toJson($TodosTableTable.$converterid.toJson(id)), 'title': serializer.toJson(title), 'content': serializer.toJson(content), 'target_date': serializer.toJson(targetDate), @@ -478,7 +486,7 @@ class TodoEntry extends DataClass implements Insertable { } TodoEntry copyWith( - {int? id, + {RowId? id, Value title = const Value.absent(), String? content, Value targetDate = const Value.absent(), @@ -521,7 +529,7 @@ class TodoEntry extends DataClass implements Insertable { } class TodosTableCompanion extends UpdateCompanion { - final Value id; + final Value id; final Value title; final Value content; final Value targetDate; @@ -562,7 +570,7 @@ class TodosTableCompanion extends UpdateCompanion { } TodosTableCompanion copyWith( - {Value? id, + {Value? id, Value? title, Value? content, Value? targetDate, @@ -582,7 +590,7 @@ class TodosTableCompanion extends UpdateCompanion { Map toColumns(bool nullToAbsent) { final map = {}; if (id.present) { - map['id'] = Variable(id.value); + map['id'] = Variable($TodosTableTable.$converterid.toSql(id.value)); } if (title.present) { map['title'] = Variable(title.value); @@ -624,13 +632,14 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> { $UsersTable(this.attachedDatabase, [this._alias]); static const VerificationMeta _idMeta = const VerificationMeta('id'); @override - late final GeneratedColumn id = GeneratedColumn( - 'id', aliasedName, false, - hasAutoIncrement: true, - type: DriftSqlType.int, - requiredDuringInsert: false, - defaultConstraints: - GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + late final GeneratedColumnWithTypeConverter id = GeneratedColumn< + int>('id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')) + .withConverter($UsersTable.$converterid); static const VerificationMeta _nameMeta = const VerificationMeta('name'); @override late final GeneratedColumn name = GeneratedColumn( @@ -678,9 +687,7 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> { {bool isInserting = false}) { final context = VerificationContext(); final data = instance.toColumns(true); - if (data.containsKey('id')) { - context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); - } + context.handle(_idMeta, const VerificationResult.success()); if (data.containsKey('name')) { context.handle( _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); @@ -714,8 +721,8 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> { User map(Map data, {String? tablePrefix}) { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return User( - id: attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + id: $UsersTable.$converterid.fromSql(attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!), name: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}name'])!, isAwesome: attachedDatabase.typeMapping @@ -731,10 +738,13 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> { $UsersTable createAlias(String alias) { return $UsersTable(attachedDatabase, alias); } + + static JsonTypeConverter2 $converterid = + TypeConverter.extensionType(); } class User extends DataClass implements Insertable { - final int id; + final RowId id; final String name; final bool isAwesome; final Uint8List profilePicture; @@ -748,7 +758,9 @@ class User extends DataClass implements Insertable { @override Map toColumns(bool nullToAbsent) { final map = {}; - map['id'] = Variable(id); + { + map['id'] = Variable($UsersTable.$converterid.toSql(id)); + } map['name'] = Variable(name); map['is_awesome'] = Variable(isAwesome); map['profile_picture'] = Variable(profilePicture); @@ -770,7 +782,8 @@ class User extends DataClass implements Insertable { {ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return User( - id: serializer.fromJson(json['id']), + id: $UsersTable.$converterid + .fromJson(serializer.fromJson(json['id'])), name: serializer.fromJson(json['name']), isAwesome: serializer.fromJson(json['isAwesome']), profilePicture: serializer.fromJson(json['profilePicture']), @@ -785,7 +798,7 @@ class User extends DataClass implements Insertable { Map toJson({ValueSerializer? serializer}) { serializer ??= driftRuntimeOptions.defaultSerializer; return { - 'id': serializer.toJson(id), + 'id': serializer.toJson($UsersTable.$converterid.toJson(id)), 'name': serializer.toJson(name), 'isAwesome': serializer.toJson(isAwesome), 'profilePicture': serializer.toJson(profilePicture), @@ -794,7 +807,7 @@ class User extends DataClass implements Insertable { } User copyWith( - {int? id, + {RowId? id, String? name, bool? isAwesome, Uint8List? profilePicture, @@ -834,7 +847,7 @@ class User extends DataClass implements Insertable { } class UsersCompanion extends UpdateCompanion { - final Value id; + final Value id; final Value name; final Value isAwesome; final Value profilePicture; @@ -871,7 +884,7 @@ class UsersCompanion extends UpdateCompanion { } UsersCompanion copyWith( - {Value? id, + {Value? id, Value? name, Value? isAwesome, Value? profilePicture, @@ -889,7 +902,7 @@ class UsersCompanion extends UpdateCompanion { Map toColumns(bool nullToAbsent) { final map = {}; if (id.present) { - map['id'] = Variable(id.value); + map['id'] = Variable($UsersTable.$converterid.toSql(id.value)); } if (name.present) { map['name'] = Variable(name.value); @@ -1872,7 +1885,7 @@ abstract class _$TodoDb extends GeneratedDatabase { todosTable, }).map((QueryRow row) => AllTodosWithCategoryResult( row: row, - id: row.read('id'), + id: $TodosTableTable.$converterid.fromSql(row.read('id')), title: row.readNullable('title'), content: row.read('content'), targetDate: row.readNullable('target_date'), @@ -1880,21 +1893,21 @@ abstract class _$TodoDb extends GeneratedDatabase { status: NullAwareTypeConverter.wrapFromSql( $TodosTableTable.$converterstatus, row.readNullable('status')), - catId: row.read('catId'), + catId: $CategoriesTable.$converterid.fromSql(row.read('catId')), catDesc: row.read('catDesc'), )); } - Future deleteTodoById(int var1) { + Future deleteTodoById(RowId var1) { return customUpdate( 'DELETE FROM todos WHERE id = ?1', - variables: [Variable(var1)], + variables: [Variable($TodosTableTable.$converterid.toSql(var1))], updates: {todosTable}, updateKind: UpdateKind.delete, ); } - Selectable withIn(String? var1, String? var2, List var3) { + Selectable withIn(String? var1, String? var2, List var3) { var $arrayStartIndex = 3; final expandedvar3 = $expandVar($arrayStartIndex, var3.length); $arrayStartIndex += var3.length; @@ -1903,18 +1916,19 @@ abstract class _$TodoDb extends GeneratedDatabase { variables: [ Variable(var1), Variable(var2), - for (var $ in var3) Variable($) + for (var $ in var3) + Variable($TodosTableTable.$converterid.toSql($)) ], readsFrom: { todosTable, }).asyncMap(todosTable.mapFromRow); } - Selectable search({required int id}) { + Selectable search({required RowId id}) { return customSelect( 'SELECT * FROM todos WHERE CASE WHEN -1 = ?1 THEN 1 ELSE id = ?1 END', variables: [ - Variable(id) + Variable($TodosTableTable.$converterid.toSql(id)) ], readsFrom: { todosTable, @@ -1949,13 +1963,13 @@ abstract class _$TodoDb extends GeneratedDatabase { } class AllTodosWithCategoryResult extends CustomResultSet { - final int id; + final RowId id; final String? title; final String content; final DateTime? targetDate; final int? category; final TodoStatus? status; - final int catId; + final RowId catId; final String catDesc; AllTodosWithCategoryResult({ required QueryRow row, @@ -2006,11 +2020,11 @@ mixin _$SomeDaoMixin on DatabaseAccessor { $SharedTodosTable get sharedTodos => attachedDatabase.sharedTodos; $TodoWithCategoryViewView get todoWithCategoryView => attachedDatabase.todoWithCategoryView; - Selectable todosForUser({required int user}) { + Selectable todosForUser({required RowId user}) { return customSelect( 'SELECT t.* FROM todos AS t INNER JOIN shared_todos AS st ON st.todo = t.id INNER JOIN users AS u ON u.id = st.user WHERE u.id = ?1', variables: [ - Variable(user) + Variable($UsersTable.$converterid.toSql(user)) ], readsFrom: { todosTable, diff --git a/drift/test/integration_tests/insert_integration_test.dart b/drift/test/integration_tests/insert_integration_test.dart index 153c1975..d4e97006 100644 --- a/drift/test/integration_tests/insert_integration_test.dart +++ b/drift/test/integration_tests/insert_integration_test.dart @@ -109,7 +109,7 @@ void main() { expect( entry, const Category( - id: 1, + id: RowId(1), description: 'Description', priority: CategoryPriority.low, descriptionInUpperCase: 'DESCRIPTION', diff --git a/drift/test/integration_tests/select_integration_test.dart b/drift/test/integration_tests/select_integration_test.dart index 09b85241..e2cd8d09 100644 --- a/drift/test/integration_tests/select_integration_test.dart +++ b/drift/test/integration_tests/select_integration_test.dart @@ -26,7 +26,7 @@ void main() { final rows = await (db.select(db.users) ..orderBy([(_) => OrderingTerm.random()])) .get(); - expect(rows.isSorted((a, b) => a.id.compareTo(b.id)), isFalse); + expect(rows.isSorted((a, b) => a.id.id.compareTo(b.id.id)), isFalse); }); test('can select view', () async { @@ -35,7 +35,7 @@ void main() { await db.todosTable.insertOne(TodosTableCompanion.insert( content: 'some content', title: const Value('title'), - category: Value(category.id))); + category: Value(category.id.id))); final result = await db.todoWithCategoryView.select().getSingle(); expect( diff --git a/drift/test/isolate_test.dart b/drift/test/isolate_test.dart index 665d8299..da8cf9ef 100644 --- a/drift/test/isolate_test.dart +++ b/drift/test/isolate_test.dart @@ -190,7 +190,7 @@ void main() { stream, emits([ Category( - id: 1, + id: RowId(1), description: 'From remote isolate!', priority: CategoryPriority.low, descriptionInUpperCase: 'FROM REMOTE ISOLATE!', @@ -318,7 +318,7 @@ void _runTests(FutureOr Function() spawner, bool terminateIsolate, await database.into(database.todosTable).insert(initialCompanion); await expectLater( stream, - emits(const TodoEntry(id: 1, content: 'my content')), + emits(const TodoEntry(id: RowId(1), content: 'my content')), ); }); diff --git a/drift/test/serialization_test.dart b/drift/test/serialization_test.dart index e277c48a..41b1ec79 100644 --- a/drift/test/serialization_test.dart +++ b/drift/test/serialization_test.dart @@ -6,7 +6,7 @@ import 'generated/todos.dart'; final DateTime _someDate = DateTime(2019, 06, 08); final TodoEntry _someTodoEntry = TodoEntry( - id: 3, + id: RowId(3), title: null, content: 'content', targetDate: _someDate, diff --git a/drift/test/test_utils/test_utils.mocks.dart b/drift/test/test_utils/test_utils.mocks.dart index 0d052c05..03d207d3 100644 --- a/drift/test/test_utils/test_utils.mocks.dart +++ b/drift/test/test_utils/test_utils.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.4.3 from annotations // in drift/test/test_utils/test_utils.dart. // Do not manually edit this file. From 54eecf7b4553fa0994e01431593fce2441d70adf Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 2 Apr 2024 13:40:31 +0200 Subject: [PATCH 33/38] Update js dependency for integration test --- extras/integration_tests/web_wasm/pubspec.yaml | 2 +- extras/integration_tests/web_wasm/web/main.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extras/integration_tests/web_wasm/pubspec.yaml b/extras/integration_tests/web_wasm/pubspec.yaml index a7d1acdf..5142225c 100644 --- a/extras/integration_tests/web_wasm/pubspec.yaml +++ b/extras/integration_tests/web_wasm/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: shelf: ^1.4.1 shelf_proxy: ^1.0.4 path: ^1.8.3 - js: ^0.6.7 + js: ^0.7.0 package_config: ^2.1.0 async: ^2.11.0 http: ^1.0.0 diff --git a/extras/integration_tests/web_wasm/web/main.dart b/extras/integration_tests/web_wasm/web/main.dart index 7b91b287..93e52bf6 100644 --- a/extras/integration_tests/web_wasm/web/main.dart +++ b/extras/integration_tests/web_wasm/web/main.dart @@ -90,7 +90,7 @@ Future _initializeDatabase() async { // Let's first open a custom WasmDatabase, the way it would have been // done before WasmDatabase.open. - final sqlite3 = await WasmSqlite3.loadFromUrl(Uri.parse('sqlite3.wasm')); + final sqlite3 = await WasmSqlite3.loadFromUrl(sqlite3WasmUri); final fs = await IndexedDbFileSystem.open(dbName: dbName); sqlite3.registerVirtualFileSystem(fs, makeDefault: true); From 82123e5cc04e8829e979af237a5e9a7bb76b932b Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 4 Apr 2024 22:42:08 +0200 Subject: [PATCH 34/38] Raise language version in examples --- examples/modular/pubspec.yaml | 2 +- examples/with_built_value/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/modular/pubspec.yaml b/examples/modular/pubspec.yaml index 1f170cdc..97e246b7 100644 --- a/examples/modular/pubspec.yaml +++ b/examples/modular/pubspec.yaml @@ -3,7 +3,7 @@ version: 1.0.0 publish_to: none environment: - sdk: '>=2.18.0 <3.0.0' + sdk: '>=3.3.0 <4.0.0' dependencies: drift: diff --git a/examples/with_built_value/pubspec.yaml b/examples/with_built_value/pubspec.yaml index 7a8c0a20..4417f22d 100644 --- a/examples/with_built_value/pubspec.yaml +++ b/examples/with_built_value/pubspec.yaml @@ -3,7 +3,7 @@ publish_to: none version: 1.0.0 environment: - sdk: '>=2.14.0 <4.0.0' + sdk: '>=3.3.0 <4.0.0' dependencies: drift: ^2.11.0 From 37f120d287057c2cd2ad4b79f02a7584957b9f2e Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Fri, 5 Apr 2024 22:39:05 +0200 Subject: [PATCH 35/38] Add setup param to schema verifier --- drift_dev/CHANGELOG.md | 3 +++ drift_dev/lib/api/migrations.dart | 14 ++++++++++++-- .../lib/src/services/schema/verifier_impl.dart | 15 +++++++++++---- .../test/services/schema/verifier_impl_test.dart | 12 +++++++++++- 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/drift_dev/CHANGELOG.md b/drift_dev/CHANGELOG.md index bbcfdcc0..8d6b0e3b 100644 --- a/drift_dev/CHANGELOG.md +++ b/drift_dev/CHANGELOG.md @@ -2,6 +2,9 @@ - Fix drift using the wrong import alias in generated part files. - Add the `use_sql_column_name_as_json_key` builder option. +- Add a `setup` parameter to `SchemaVerifier`. It is called when the verifier + creates database connections (similar to the callback on `NativeDatabase`) + and can be used to register custom functions. ## 2.16.0 diff --git a/drift_dev/lib/api/migrations.dart b/drift_dev/lib/api/migrations.dart index a09cdf15..73047c8b 100644 --- a/drift_dev/lib/api/migrations.dart +++ b/drift_dev/lib/api/migrations.dart @@ -11,8 +11,18 @@ export 'package:drift_dev/src/services/schema/verifier_common.dart' show SchemaMismatch; abstract class SchemaVerifier { - factory SchemaVerifier(SchemaInstantiationHelper helper) = - VerifierImplementation; + /// Creates a schema verifier for the drift-generated [helper]. + /// + /// See [tests] for more information. + /// The optional [setup] parameter is used internally by the verifier for + /// every database connection it opens. This can be used to, for instance, + /// register custom functions expected by your database. + /// + /// [tests]: https://drift.simonbinder.eu/docs/migrations/tests/ + factory SchemaVerifier( + SchemaInstantiationHelper helper, { + void Function(Database raw)? setup, + }) = VerifierImplementation; /// Creates a [DatabaseConnection] that contains empty tables created for the /// known schema [version]. diff --git a/drift_dev/lib/src/services/schema/verifier_impl.dart b/drift_dev/lib/src/services/schema/verifier_impl.dart index ba8b4941..97e54a38 100644 --- a/drift_dev/lib/src/services/schema/verifier_impl.dart +++ b/drift_dev/lib/src/services/schema/verifier_impl.dart @@ -13,8 +13,9 @@ Expando> expectedSchema = Expando(); class VerifierImplementation implements SchemaVerifier { final SchemaInstantiationHelper helper; final Random _random = Random(); + final void Function(Database)? setup; - VerifierImplementation(this.helper); + VerifierImplementation(this.helper, {this.setup}); @override Future migrateAndValidate(GeneratedDatabase db, int expectedVersion, @@ -57,14 +58,20 @@ class VerifierImplementation implements SchemaVerifier { return buffer.toString(); } + Database _setupDatabase(String uri) { + final database = sqlite3.open(uri, uri: true); + setup?.call(database); + return database; + } + @override Future schemaAt(int version) async { // Use distinct executors for setup and use, allowing us to close the helper // db here and avoid creating it twice. // https://www.sqlite.org/inmemorydb.html#sharedmemdb final uri = 'file:mem${_randomString()}?mode=memory&cache=shared'; - final dbForSetup = sqlite3.open(uri, uri: true); - final dbForUse = sqlite3.open(uri, uri: true); + final dbForSetup = _setupDatabase(uri); + final dbForUse = _setupDatabase(uri); final executor = NativeDatabase.opened(dbForSetup); final db = helper.databaseForVersion(executor, version); @@ -74,7 +81,7 @@ class VerifierImplementation implements SchemaVerifier { await db.close(); return InitializedSchema(dbForUse, () { - final db = sqlite3.open(uri, uri: true); + final db = _setupDatabase(uri); return DatabaseConnection(NativeDatabase.opened(db)); }); } diff --git a/drift_dev/test/services/schema/verifier_impl_test.dart b/drift_dev/test/services/schema/verifier_impl_test.dart index b9536fcf..e65862f4 100644 --- a/drift_dev/test/services/schema/verifier_impl_test.dart +++ b/drift_dev/test/services/schema/verifier_impl_test.dart @@ -5,7 +5,11 @@ import 'package:drift_dev/src/services/schema/verifier_impl.dart'; import 'package:test/test.dart'; void main() { - final verifier = SchemaVerifier(_TestHelper()); + final verifier = SchemaVerifier( + _TestHelper(), + setup: (rawDb) => rawDb.createFunction( + functionName: 'test_function', function: (args) => 1), + ); group('startAt', () { test('starts at the requested version', () async { @@ -15,6 +19,12 @@ void main() { expect(details.hadUpgrade, isFalse, reason: 'no upgrade expected'); })); }); + + test('registers custom functions', () async { + final db = (await verifier.startAt(17)).executor; + await db.ensureOpen(_DelegatedUser(17, (_, details) async {})); + await db.runSelect('select test_function()', []); + }); }); group('migrateAndValidate', () { From 039838b2bad11da63b1a2d35fd77c63e1a095e2d Mon Sep 17 00:00:00 2001 From: David Martos Date: Sat, 6 Apr 2024 14:37:28 +0200 Subject: [PATCH 36/38] Support bytea sql literal in postgres (#2943) --- drift/lib/src/runtime/types/mapping.dart | 14 +++++++++--- extras/drift_postgres/test/types_test.dart | 26 ++++++++++++---------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/drift/lib/src/runtime/types/mapping.dart b/drift/lib/src/runtime/types/mapping.dart index 26b1211d..a97218a8 100644 --- a/drift/lib/src/runtime/types/mapping.dart +++ b/drift/lib/src/runtime/types/mapping.dart @@ -115,9 +115,17 @@ final class SqlTypes { return (dart.millisecondsSinceEpoch ~/ 1000).toString(); } } else if (dart is Uint8List) { - // BLOB literals are string literals containing hexadecimal data and - // preceded by a single "x" or "X" character. Example: X'53514C697465' - return "x'${hex.encode(dart)}'"; + final String hexString = hex.encode(dart); + + if (dialect == SqlDialect.postgres) { + // Postgres BYTEA hex format + // https://www.postgresql.org/docs/current/datatype-binary.html#DATATYPE-BINARY-BYTEA-HEX-FORMAT + return "'\\x$hexString'::bytea"; + } else { + // BLOB literals are string literals containing hexadecimal data and + // preceded by a single "x" or "X" character. Example: X'53514C697465' + return "x'${hex.encode(dart)}'"; + } } else if (dart is DriftAny) { return mapToSqlLiteral(dart.rawSqlValue); } diff --git a/extras/drift_postgres/test/types_test.dart b/extras/drift_postgres/test/types_test.dart index 727480d0..a8cabe21 100644 --- a/extras/drift_postgres/test/types_test.dart +++ b/extras/drift_postgres/test/types_test.dart @@ -26,19 +26,19 @@ void main() { return row.read(expression)!; } + void testWith(CustomSqlType? type, T value) { + test('with variable', () async { + final variable = Variable(value, type); + expect(await eval(variable), value); + }); + + test('with constant', () async { + final constant = Constant(value, type); + expect(await eval(constant), value); + }); + } + group('custom types pass through', () { - void testWith(CustomSqlType type, T value) { - test('with variable', () async { - final variable = Variable(value, type); - expect(await eval(variable), value); - }); - - test('with constant', () async { - final constant = Constant(value, type); - expect(await eval(constant), value); - }); - } - group('uuid', () => testWith(PgTypes.uuid, Uuid().v4obj())); group( 'interval', @@ -60,6 +60,8 @@ void main() { ); }); + group('bytea', () => testWith(null, Uint8List.fromList([1, 2, 3, 4, 5]))); + test('compare datetimes', () async { final time = DateTime.now(); final before = Variable( From 43b7f72bad387d6ea0fc97efc55e2bf936ec2aaf Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 6 Apr 2024 14:48:55 +0200 Subject: [PATCH 37/38] sqlparser: Fix parsing binary literals --- drift/CHANGELOG.md | 1 + sqlparser/CHANGELOG.md | 1 + .../lib/src/reader/tokenizer/scanner.dart | 2 +- sqlparser/lib/src/utils/ast_equality.dart | 2 +- sqlparser/lib/utils/node_to_text.dart | 3 +++ ...oken_tests.dart => single_token_test.dart} | 20 +++++++++++++++++++ sqlparser/test/utils/node_to_text_test.dart | 5 +++++ 7 files changed, 32 insertions(+), 2 deletions(-) rename sqlparser/test/scanner/{single_token_tests.dart => single_token_test.dart} (87%) diff --git a/drift/CHANGELOG.md b/drift/CHANGELOG.md index e4e7ef3b..90151da5 100644 --- a/drift/CHANGELOG.md +++ b/drift/CHANGELOG.md @@ -4,6 +4,7 @@ generated companion class. - Add the `TypeConverter.extensionType` factory to create type converters for extension types. +- Fix invalid SQL syntax being generated for `BLOB` literals on postgres. ## 2.16.0 diff --git a/sqlparser/CHANGELOG.md b/sqlparser/CHANGELOG.md index f05cb2f3..f43d53bd 100644 --- a/sqlparser/CHANGELOG.md +++ b/sqlparser/CHANGELOG.md @@ -1,5 +1,6 @@ ## 3.35.0-dev +- Fix parsing binary literals. - Drift extensions: Allow custom class names for `CREATE VIEW` statements. ## 0.34.1 diff --git a/sqlparser/lib/src/reader/tokenizer/scanner.dart b/sqlparser/lib/src/reader/tokenizer/scanner.dart index 4c45b32c..d06096cb 100644 --- a/sqlparser/lib/src/reader/tokenizer/scanner.dart +++ b/sqlparser/lib/src/reader/tokenizer/scanner.dart @@ -290,7 +290,7 @@ class Scanner { } final value = source - .substring(_startOffset + 1, _currentOffset - 1) + .substring(_startOffset + (binary ? 2 : 1), _currentOffset - 1) .replaceAll("''", "'"); tokens.add(StringLiteralToken(value, _currentSpan, binary: binary)); } diff --git a/sqlparser/lib/src/utils/ast_equality.dart b/sqlparser/lib/src/utils/ast_equality.dart index 984d843e..741b7455 100644 --- a/sqlparser/lib/src/utils/ast_equality.dart +++ b/sqlparser/lib/src/utils/ast_equality.dart @@ -642,7 +642,7 @@ class EqualityEnforcingVisitor implements AstVisitor { @override void visitStringLiteral(StringLiteral e, void arg) { final current = _currentAs(e); - _assert(current.value == e.value, e); + _assert(current.value == e.value && current.isBinary == e.isBinary, e); _checkChildren(e); } diff --git a/sqlparser/lib/utils/node_to_text.dart b/sqlparser/lib/utils/node_to_text.dart index 59554a36..8996ed66 100644 --- a/sqlparser/lib/utils/node_to_text.dart +++ b/sqlparser/lib/utils/node_to_text.dart @@ -1114,6 +1114,9 @@ class NodeSqlBuilder extends AstVisitor { @override void visitStringLiteral(StringLiteral e, void arg) { + if (e.isBinary) { + symbol('X', spaceBefore: true); + } _stringLiteral(e.value); } diff --git a/sqlparser/test/scanner/single_token_tests.dart b/sqlparser/test/scanner/single_token_test.dart similarity index 87% rename from sqlparser/test/scanner/single_token_tests.dart rename to sqlparser/test/scanner/single_token_test.dart index 4638890b..3a6df59f 100644 --- a/sqlparser/test/scanner/single_token_tests.dart +++ b/sqlparser/test/scanner/single_token_test.dart @@ -86,6 +86,26 @@ void main() { ); }); + test('binary string literal', () { + final scanner = Scanner("X'1234' x'5678'"); + scanner.scanTokens(); + + expect(scanner.tokens, hasLength(3)); + expect( + scanner.tokens[0], + const TypeMatcher() + .having((token) => token.binary, 'binary', isTrue) + .having((token) => token.value, 'value', '1234'), + ); + expect( + scanner.tokens[1], + const TypeMatcher() + .having((token) => token.binary, 'binary', isTrue) + .having((token) => token.value, 'value', '5678'), + ); + expect(scanner.tokens[2].type, TokenType.eof); + }); + group('parses numeric literals', () { void checkLiteral(String lexeme, NumericToken other, num value) { final scanner = Scanner(lexeme)..scanTokens(); diff --git a/sqlparser/test/utils/node_to_text_test.dart b/sqlparser/test/utils/node_to_text_test.dart index 9a2fd353..f4785446 100644 --- a/sqlparser/test/utils/node_to_text_test.dart +++ b/sqlparser/test/utils/node_to_text_test.dart @@ -568,6 +568,11 @@ CREATE UNIQUE INDEX my_idx ON t1 (c1, c2, c3) WHERE c1 < c3; testFormat('SELECT a -> b'); testFormat('SELECT a ->> b'); }); + + test('blob literal', () { + testFormat( + "select typeof(X'0100000300000000000000000000803F000000000000003F0000803F');"); + }); }); test('identifiers', () { From 74fb2696159904e91ad8a98335b1bb9362363b2b Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 6 Apr 2024 14:59:52 +0200 Subject: [PATCH 38/38] Avoid encoding to hex twice --- drift/lib/src/runtime/types/mapping.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drift/lib/src/runtime/types/mapping.dart b/drift/lib/src/runtime/types/mapping.dart index a97218a8..e6081278 100644 --- a/drift/lib/src/runtime/types/mapping.dart +++ b/drift/lib/src/runtime/types/mapping.dart @@ -124,7 +124,7 @@ final class SqlTypes { } else { // BLOB literals are string literals containing hexadecimal data and // preceded by a single "x" or "X" character. Example: X'53514C697465' - return "x'${hex.encode(dart)}'"; + return "x'$hexString'"; } } else if (dart is DriftAny) { return mapToSqlLiteral(dart.rawSqlValue);