diff --git a/docs/build.yaml b/docs/build.yaml index 31c45627..d3a0f7e1 100644 --- a/docs/build.yaml +++ b/docs/build.yaml @@ -100,4 +100,4 @@ targets: global_options: ":api_index": options: - packages: ['drift', 'drift_dev'] + packages: ['drift', 'drift_dev', 'sqlite3'] diff --git a/docs/lib/snippets/custom_row_classes/default.dart b/docs/lib/snippets/custom_row_classes/default.dart new file mode 100644 index 00000000..2a8d6806 --- /dev/null +++ b/docs/lib/snippets/custom_row_classes/default.dart @@ -0,0 +1,18 @@ +import 'package:drift/drift.dart'; + +// #docregion start +@UseRowClass(User) +class Users extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get name => text()(); + DateTimeColumn get birthday => dateTime()(); +} + +class User { + final int id; + final String name; + final DateTime birthday; + + User({required this.id, required this.name, required this.birthday}); +} +// #enddocregion start \ No newline at end of file diff --git a/docs/lib/snippets/custom_row_classes/named.dart b/docs/lib/snippets/custom_row_classes/named.dart new file mode 100644 index 00000000..cbdb8d39 --- /dev/null +++ b/docs/lib/snippets/custom_row_classes/named.dart @@ -0,0 +1,21 @@ +import 'package:drift/drift.dart'; + +// #docregion named +@UseRowClass(User, constructor: 'fromDb') +class Users extends Table { + // ... + // #enddocregion named + IntColumn get id => integer().autoIncrement()(); + TextColumn get name => text()(); + DateTimeColumn get birthday => dateTime()(); + // #docregion named +} + +class User { + final int id; + final String name; + final DateTime birthday; + + User.fromDb({required this.id, required this.name, required this.birthday}); +} +// #enddocregion named \ No newline at end of file diff --git a/docs/pages/docs/Advanced Features/custom_row_classes.md b/docs/pages/docs/Advanced Features/custom_row_classes.md index f4adf426..eea7bc69 100644 --- a/docs/pages/docs/Advanced Features/custom_row_classes.md +++ b/docs/pages/docs/Advanced Features/custom_row_classes.md @@ -6,6 +6,7 @@ data: template: layouts/docs/single --- + For each table declared in Dart or in a drift file, `drift_dev` generates a row class (sometimes also referred to as _data class_) to hold a full row and a companion class for updates and inserts. This works well for most cases: Drift knows what columns your table has, and it can generate a simple class for all of that. @@ -19,22 +20,8 @@ Starting from moor version 4.3 (and in drift), it is possible to use your own cl To use a custom row class, simply annotate your table definition with `@UseRowClass`. -```dart -@UseRowClass(User) -class Users extends Table { - IntColumn get id => integer().autoIncrement()(); - TextColumn get name => text()(); - DateTimeColumn get birthday => dateTime()(); -} - -class User { - final int id; - final String name; - final DateTime birthDate; - - User({required this.id, required this.name, required this.birthDate}); -} -``` +{% assign snippets = "package:drift_docs/snippets/custom_row_classes/default.dart.excerpt.json" | readString | json_decode %} +{% include "blocks/snippet" snippets = snippets name = "start" %} A row class must adhere to the following requirements: @@ -58,18 +45,25 @@ By default, drift will use the default, unnamed constructor to map a row to the If you want to use another constructor, set the `constructor` parameter on the `@UseRowClass` annotation: +{% assign snippets = "package:drift_docs/snippets/custom_row_classes/named.dart.excerpt.json" | readString | json_decode %} +{% include "blocks/snippet" snippets = snippets name = "named" %} + +### Static and aynchronous factories + +Starting with drift 2.0, the custom constructor set with the `constructor` +parameter on the `@UseRowClass` annotation may also refer to a static method +defined on the class to load. +That method must either return the row class or a `Future` of that type. +Unlike a named constructor or a factory, this can be useful in case the mapping +from SQL to Dart needs to be asynchronous: + ```dart -@UseRowClass(User, constructor: 'fromDb') -class Users extends Table { - // ... -} - class User { - final int id; - final String name; - final DateTime birthDate; + // ... - User.fromDb({required this.id, required this.name, required this.birthDate}); + static Future load(int id, String name, DateTime birthday) async { + // ... + } } ``` diff --git a/drift/CHANGELOG.md b/drift/CHANGELOG.md index 0a47bb23..f26ee8cd 100644 --- a/drift/CHANGELOG.md +++ b/drift/CHANGELOG.md @@ -17,6 +17,8 @@ - __Breaking__: Remove the `includeJoinedTableColumns` parameter on `selectOnly()`. The method now behaves as if that parameter was turned off. To use columns from a joined table, add them with `addColumns`. +- __Breaking__: Remove the `fromData` factory on generated data classes. Use the + `map` method on tables instead. - Add support for storing date times as (ISO-8601) strings. For details on how to use this, see [the documentation](https://drift.simonbinder.eu/docs/getting-started/advanced_dart_tables/#supported-column-types). - Consistently handle transaction errors like a failing `BEGIN` or `COMMIT` @@ -25,6 +27,9 @@ to delete statatements. - Support nested transactions. - Support custom collations in the query builder API. +- [Custom row classes](https://drift.simonbinder.eu/docs/advanced-features/custom_row_classes/) + can now be constructed with static methods too. + These static factories can also be asynchronous. ## 1.7.1 diff --git a/drift/lib/src/dsl/columns.dart b/drift/lib/src/dsl/columns.dart index 7ea6c39f..0960adc3 100644 --- a/drift/lib/src/dsl/columns.dart +++ b/drift/lib/src/dsl/columns.dart @@ -324,6 +324,9 @@ extension BuildGeneralColumn on _BaseColumnBuilder { /// ``` /// The generated row class will then use a `MyFancyClass` instead of a /// `String`, which would usually be used for [Table.text] columns. + /// + /// The type [T] of the type converter may only be nullable if this column + /// was declared [nullable] too. Otherwise, `drift_dev` will emit an error. ColumnBuilder map(TypeConverter converter) => _isGenerated(); diff --git a/drift/lib/src/runtime/query_builder/query_builder.dart b/drift/lib/src/runtime/query_builder/query_builder.dart index 8298f791..245d740f 100644 --- a/drift/lib/src/runtime/query_builder/query_builder.dart +++ b/drift/lib/src/runtime/query_builder/query_builder.dart @@ -10,6 +10,8 @@ import 'package:drift/src/runtime/executor/stream_queries.dart'; import 'package:drift/src/utils/single_transformer.dart'; import 'package:meta/meta.dart'; +import '../../utils/async.dart'; + // New files should not be part of this mega library, which we're trying to // split up. import 'expressions/case_when.dart'; diff --git a/drift/lib/src/runtime/query_builder/schema/entities.dart b/drift/lib/src/runtime/query_builder/schema/entities.dart index 3e8ebe0f..089b07f2 100644 --- a/drift/lib/src/runtime/query_builder/schema/entities.dart +++ b/drift/lib/src/runtime/query_builder/schema/entities.dart @@ -93,7 +93,7 @@ abstract class ResultSetImplementation extends DatabaseSchemaEntity { List get $columns; /// Maps the given row returned by the database into the fitting data class. - Row map(Map data, {String? tablePrefix}); + FutureOr map(Map data, {String? tablePrefix}); /// Creates an alias of this table or view that will write the name [alias] /// when used in a query. @@ -125,7 +125,7 @@ class _AliasResultSet extends ResultSetImplementation { String get entityName => _inner.entityName; @override - Row map(Map data, {String? tablePrefix}) { + FutureOr map(Map data, {String? tablePrefix}) { return _inner.map(data, tablePrefix: tablePrefix); } diff --git a/drift/lib/src/runtime/query_builder/schema/table_info.dart b/drift/lib/src/runtime/query_builder/schema/table_info.dart index 8cd4a025..4a653ca5 100644 --- a/drift/lib/src/runtime/query_builder/schema/table_info.dart +++ b/drift/lib/src/runtime/query_builder/schema/table_info.dart @@ -75,7 +75,8 @@ mixin TableInfo on Table /// The [database] instance is used so that the raw values from the companion /// can properly be interpreted as the high-level Dart values exposed by the /// data class. - D mapFromCompanion(Insertable companion, DatabaseConnectionUser database) { + Future mapFromCompanion( + Insertable companion, DatabaseConnectionUser database) async { final asColumnMap = companion.toColumns(false); if (asColumnMap.values.any((e) => e is! Variable)) { @@ -122,20 +123,20 @@ mixin VirtualTableInfo on TableInfo { /// Most of these are accessed internally by drift or by generated code. extension TableInfoUtils on ResultSetImplementation { /// Like [map], but from a [row] instead of the low-level map. - D mapFromRow(QueryRow row, {String? tablePrefix}) { + Future mapFromRow(QueryRow row, {String? tablePrefix}) async { return map(row.data, tablePrefix: tablePrefix); } /// Like [mapFromRow], but returns null if a non-nullable column of this table /// is null in [row]. - D? mapFromRowOrNull(QueryRow row, {String? tablePrefix}) { + Future mapFromRowOrNull(QueryRow row, {String? tablePrefix}) { final resolvedPrefix = tablePrefix == null ? '' : '$tablePrefix.'; final notInRow = $columns .where((c) => !c.$nullable) .any((e) => row.data['$resolvedPrefix${e.$name}'] == null); - if (notInRow) return null; + if (notInRow) return Future.value(null); return mapFromRow(row, tablePrefix: tablePrefix); } @@ -153,7 +154,7 @@ extension TableInfoUtils on ResultSetImplementation { /// /// Drift would generate code to call this method with `'c1': 'foo'` and /// `'c2': 'bar'` in [alias]. - D mapFromRowWithAlias(QueryRow row, Map alias) { + Future mapFromRowWithAlias(QueryRow row, Map alias) async { return map({ for (final entry in row.data.entries) alias[entry.key]!: entry.value, }); diff --git a/drift/lib/src/runtime/query_builder/statements/delete.dart b/drift/lib/src/runtime/query_builder/statements/delete.dart index 8df4e9b9..732ff60d 100644 --- a/drift/lib/src/runtime/query_builder/statements/delete.dart +++ b/drift/lib/src/runtime/query_builder/statements/delete.dart @@ -78,7 +78,7 @@ class DeleteStatement extends Query {TableUpdate.onTable(_sourceTable, kind: UpdateKind.delete)}); } - return [for (final rawRow in rows) table.map(rawRow)]; + return rows.mapAsyncAndAwait(table.map); }); } } diff --git a/drift/lib/src/runtime/query_builder/statements/select/select.dart b/drift/lib/src/runtime/query_builder/statements/select/select.dart index 6b1fe0d2..4ee4e9ee 100644 --- a/drift/lib/src/runtime/query_builder/statements/select/select.dart +++ b/drift/lib/src/runtime/query_builder/statements/select/select.dart @@ -60,7 +60,7 @@ class SimpleSelectStatement extends Query key: StreamKey(query.sql, query.boundVariables), ); - return database.createStream(fetcher).map(_mapResponse); + return database.createStream(fetcher).asyncMap(_mapResponse); } Future>> _getRaw(GenerationContext ctx) { @@ -69,8 +69,8 @@ class SimpleSelectStatement extends Query }); } - List _mapResponse(List> rows) { - return rows.map(table.map).toList(); + Future> _mapResponse(List> rows) { + return rows.mapAsyncAndAwait(table.map); } /// Creates a select statement that operates on more than one table by diff --git a/drift/lib/src/runtime/query_builder/statements/select/select_with_join.dart b/drift/lib/src/runtime/query_builder/statements/select/select_with_join.dart index dcd4e172..ebf5bbad 100644 --- a/drift/lib/src/runtime/query_builder/statements/select/select_with_join.dart +++ b/drift/lib/src/runtime/query_builder/statements/select/select_with_join.dart @@ -207,7 +207,7 @@ class JoinedSelectStatement return database .createStream(fetcher) - .map((rows) => _mapResponse(ctx, rows)); + .asyncMap((rows) => _mapResponse(ctx, rows)); } @override @@ -234,9 +234,9 @@ class JoinedSelectStatement }); } - List _mapResponse( + Future> _mapResponse( GenerationContext ctx, List> rows) { - return rows.map((row) { + return Future.wait(rows.map((row) async { final readTables = {}; final readColumns = {}; @@ -244,7 +244,8 @@ class JoinedSelectStatement final prefix = '${table.aliasedName}.'; // if all columns of this table are null, skip the table if (table.$columns.any((c) => row[prefix + c.$name] != null)) { - readTables[table] = table.map(row, tablePrefix: table.aliasedName); + readTables[table] = + await table.map(row, tablePrefix: table.aliasedName); } } @@ -256,7 +257,7 @@ class JoinedSelectStatement } return TypedResult(readTables, QueryRow(row, database), readColumns); - }).toList(); + })); } @alwaysThrows diff --git a/drift/lib/src/runtime/query_builder/statements/update.dart b/drift/lib/src/runtime/query_builder/statements/update.dart index 2f530586..fd541e2d 100644 --- a/drift/lib/src/runtime/query_builder/statements/update.dart +++ b/drift/lib/src/runtime/query_builder/statements/update.dart @@ -95,7 +95,7 @@ class UpdateStatement extends Query {TableUpdate.onTable(_sourceTable, kind: UpdateKind.update)}); } - return [for (final rawRow in rows) table.map(rawRow)]; + return rows.mapAsyncAndAwait(table.map); } /// Replaces the old version of [entity] that is stored in the database with diff --git a/drift/lib/src/utils/async.dart b/drift/lib/src/utils/async.dart new file mode 100644 index 00000000..0ec1726f --- /dev/null +++ b/drift/lib/src/utils/async.dart @@ -0,0 +1,12 @@ +@internal +import 'dart:async'; + +import 'package:meta/meta.dart'; + +/// Drift-internal utilities to map potentially async operations. +extension MapAndAwait on Iterable { + /// A variant of [Future.wait] that also works for [FutureOr]. + Future> mapAsyncAndAwait(FutureOr Function(T) mapper) { + return Future.wait(map((e) => Future.sync(() => mapper(e)))); + } +} diff --git a/drift/test/database/tables_test.dart b/drift/test/database/tables_test.dart index de949b04..084a30a5 100644 --- a/drift/test/database/tables_test.dart +++ b/drift/test/database/tables_test.dart @@ -34,20 +34,20 @@ void main() { expect(aliasA.hashCode == db.alias(db.users, 'a').hashCode, isTrue); }); - test('can convert a companion to a row class', () { + test('can convert a companion to a row class', () async { const companion = SharedTodosCompanion( todo: Value(3), user: Value(4), ); - final user = db.sharedTodos.mapFromCompanion(companion, db); + final user = await db.sharedTodos.mapFromCompanion(companion, db); expect( user, const SharedTodo(todo: 3, user: 4), ); }); - test('can map from row without table prefix', () { + test('can map from row without table prefix', () async { final rowData = { 'id': 1, 'title': 'some title', @@ -55,7 +55,7 @@ void main() { 'target_date': null, 'category': null, }; - final todo = db.todosTable.mapFromRowOrNull(QueryRow(rowData, db)); + final todo = await db.todosTable.mapFromRowOrNull(QueryRow(rowData, db)); expect( todo, const TodoEntry( diff --git a/drift/test/generated/custom_tables.g.dart b/drift/test/generated/custom_tables.g.dart index 1382260e..95d63ab4 100644 --- a/drift/test/generated/custom_tables.g.dart +++ b/drift/test/generated/custom_tables.g.dart @@ -1596,7 +1596,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase { ], readsFrom: { config, - }).map((QueryRow row) => config.mapFromRowWithAlias(row, const { + }).asyncMap((QueryRow row) => config.mapFromRowWithAlias(row, const { 'ck': 'config_key', 'cf': 'config_value', 'cs1': 'sync_state', @@ -1621,7 +1621,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase { readsFrom: { config, ...generatedclause.watchedTables, - }).map(config.mapFromRow); + }).asyncMap(config.mapFromRow); } Selectable readDynamic( @@ -1638,7 +1638,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase { readsFrom: { config, ...generatedpredicate.watchedTables, - }).map(config.mapFromRow); + }).asyncMap(config.mapFromRow); } Selectable typeConverterVar(SyncType? var1, List var2, @@ -1710,12 +1710,12 @@ abstract class _$CustomTablesDb extends GeneratedDatabase { withDefaults, withConstraints, ...generatedpredicate.watchedTables, - }).map((QueryRow row) { + }).asyncMap((QueryRow row) async { return MultipleResult( row: row, a: row.readNullable('a'), b: row.readNullable('b'), - c: withConstraints.mapFromRowOrNull(row, tablePrefix: 'nested_0'), + c: await withConstraints.mapFromRowOrNull(row, tablePrefix: 'nested_0'), ); }); } @@ -1728,7 +1728,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase { ], readsFrom: { email, - }).map(email.mapFromRow); + }).asyncMap(email.mapFromRow); } Selectable readRowId( @@ -1762,7 +1762,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase { Selectable readView() { return customSelect('SELECT * FROM my_view', variables: [], readsFrom: { config, - }).map(myView.mapFromRow); + }).asyncMap(myView.mapFromRow); } Selectable cfeTest() { @@ -1786,9 +1786,10 @@ abstract class _$CustomTablesDb extends GeneratedDatabase { $writeInsertable(this.config, value, startIndex: $arrayStartIndex); $arrayStartIndex += generatedvalue.amountOfVariables; return customWriteReturning( - 'INSERT INTO config ${generatedvalue.sql} RETURNING *', - variables: [...generatedvalue.introducedVariables], - updates: {config}).then((rows) => rows.map(config.mapFromRow).toList()); + 'INSERT INTO config ${generatedvalue.sql} RETURNING *', + variables: [...generatedvalue.introducedVariables], + updates: {config}) + .then((rows) => Future.wait(rows.map(config.mapFromRow))); } Selectable nested(String? var1) { @@ -1803,7 +1804,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase { }).asyncMap((QueryRow row) async { return NestedResult( row: row, - defaults: withDefaults.mapFromRow(row, tablePrefix: 'nested_0'), + defaults: await withDefaults.mapFromRow(row, tablePrefix: 'nested_0'), nestedQuery0: await customSelect( 'SELECT * FROM with_constraints AS c WHERE c.b = ?1', variables: [ @@ -1812,7 +1813,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase { readsFrom: { withConstraints, withDefaults, - }).map(withConstraints.mapFromRow).get(), + }).asyncMap(withConstraints.mapFromRow).get(), ); }); } diff --git a/drift/test/generated/todos.g.dart b/drift/test/generated/todos.g.dart index cc698b90..45d615ab 100644 --- a/drift/test/generated/todos.g.dart +++ b/drift/test/generated/todos.g.dart @@ -1642,7 +1642,7 @@ abstract class _$TodoDb extends GeneratedDatabase { ], readsFrom: { todosTable, - }).map(todosTable.mapFromRow); + }).asyncMap(todosTable.mapFromRow); } Selectable search({required int id}) { @@ -1653,7 +1653,7 @@ abstract class _$TodoDb extends GeneratedDatabase { ], readsFrom: { todosTable, - }).map(todosTable.mapFromRow); + }).asyncMap(todosTable.mapFromRow); } Selectable findCustom() { @@ -1750,6 +1750,6 @@ mixin _$SomeDaoMixin on DatabaseAccessor { todosTable, sharedTodos, users, - }).map(todosTable.mapFromRow); + }).asyncMap(todosTable.mapFromRow); } } diff --git a/drift/test/test_utils/test_utils.dart b/drift/test/test_utils/test_utils.dart index eb04bf29..47a73281 100644 --- a/drift/test/test_utils/test_utils.dart +++ b/drift/test/test_utils/test_utils.dart @@ -53,5 +53,7 @@ class CustomTable extends Table with TableInfo { } @override - Null map(Map data, {String? tablePrefix}) => null; + Future map(Map data, {String? tablePrefix}) async { + return null; + } } diff --git a/drift_dev/lib/src/analyzer/dart_types.dart b/drift_dev/lib/src/analyzer/dart_types.dart index ea5f97d6..f3647dfb 100644 --- a/drift_dev/lib/src/analyzer/dart_types.dart +++ b/drift_dev/lib/src/analyzer/dart_types.dart @@ -28,10 +28,13 @@ ExistingRowClass? validateExistingClass( Step step) { final errors = step.errors; final desiredClass = dartClass.classElement; - ConstructorElement? ctor; + final library = desiredClass.library; + + ExecutableElement? ctor; + final InterfaceType instantiation; if (dartClass.instantiation != null) { - final instantiation = desiredClass.instantiate( + instantiation = desiredClass.instantiate( typeArguments: dartClass.instantiation!, nullabilitySuffix: NullabilitySuffix.none, ); @@ -41,6 +44,36 @@ ExistingRowClass? validateExistingClass( ctor = instantiation.lookUpConstructor(constructor, desiredClass.library); } else { ctor = desiredClass.getNamedConstructor(constructor); + instantiation = library.typeSystem.instantiateInterfaceToBounds( + element: desiredClass, nullabilitySuffix: NullabilitySuffix.none); + } + + if (ctor == null) { + final fallback = desiredClass.getMethod(constructor); + + if (fallback != null) { + if (!fallback.isStatic) { + errors.report(ErrorInDartCode( + affectedElement: fallback, + message: 'To use this method as a factory for the custom row class, ' + 'it needs to be static.', + )); + } + + // The static factory must return a subtype of `FutureOr` + final expectedReturnType = + library.typeProvider.futureOrType(instantiation); + if (!library.typeSystem + .isAssignableTo(fallback.returnType, expectedReturnType)) { + errors.report(ErrorInDartCode( + affectedElement: fallback, + message: 'To be used as a factory for the custom row class, this ' + 'method needs to return an instance of it.', + )); + } + + ctor = fallback; + } } if (ctor == null) { diff --git a/drift_dev/lib/src/model/base_entity.dart b/drift_dev/lib/src/model/base_entity.dart index 1e73cc4d..5359261b 100644 --- a/drift_dev/lib/src/model/base_entity.dart +++ b/drift_dev/lib/src/model/base_entity.dart @@ -69,15 +69,31 @@ class ExistingRowClass { /// The Dart types that should be used to instantiate the [targetClass]. final List typeInstantiation; - final ConstructorElement constructor; + + /// The method to use when instantiating the row class. + /// + /// This may either be a constructor or a static method on the row class. + final ExecutableElement constructor; + final Map mapping; /// Generate toCompanion for data class final bool generateInsertable; ExistingRowClass( - this.targetClass, this.constructor, this.mapping, this.generateInsertable, - {this.typeInstantiation = const []}); + this.targetClass, + this.constructor, + this.mapping, + this.generateInsertable, { + this.typeInstantiation = const [], + }); + + /// Whether the [constructor] returns a future and thus needs to be awaited + /// to create an instance of the custom row class. + bool get isAsyncFactory { + final typeSystem = targetClass.library.typeSystem; + return typeSystem.flatten(constructor.returnType) != constructor.returnType; + } String dartType([GenerationOptions options = const GenerationOptions()]) { if (typeInstantiation.isEmpty) { diff --git a/drift_dev/lib/src/model/sql_query.dart b/drift_dev/lib/src/model/sql_query.dart index 16cf7783..d42e2db4 100644 --- a/drift_dev/lib/src/model/sql_query.dart +++ b/drift_dev/lib/src/model/sql_query.dart @@ -118,6 +118,17 @@ abstract class SqlQuery { placeholders = elements.whereType().toList(); } + bool get needsAsyncMapping { + final result = resultSet; + if (result != null) { + // Mapping to tables is asynchronous + if (result.matchingTable != null) return true; + if (result.nestedResults.any((e) => e is NestedResultTable)) return true; + } + + return false; + } + String get resultClassName { final resultSet = this.resultSet; if (resultSet == null) { @@ -194,6 +205,9 @@ class SqlSelectQuery extends SqlQuery { bool get hasNestedQuery => resultSet.nestedResults.any((e) => e is NestedResultQuery); + @override + bool get needsAsyncMapping => hasNestedQuery || super.needsAsyncMapping; + SqlSelectQuery( String name, this.fromContext, diff --git a/drift_dev/lib/src/writer/queries/query_writer.dart b/drift_dev/lib/src/writer/queries/query_writer.dart index eb5edbd1..c54ff793 100644 --- a/drift_dev/lib/src/writer/queries/query_writer.dart +++ b/drift_dev/lib/src/writer/queries/query_writer.dart @@ -115,7 +115,7 @@ class QueryWriter { } } else { _buffer.write('(QueryRow row) '); - if (query is SqlSelectQuery && query.hasNestedQuery) { + if (query.needsAsyncMapping) { _buffer.write('async '); } _buffer.write('{ return ${query.resultClassName}('); @@ -140,7 +140,7 @@ class QueryWriter { final mappingMethod = nested.isNullable ? 'mapFromRowOrNull' : 'mapFromRow'; - _buffer.write('$fieldName: $tableGetter.$mappingMethod(row, ' + _buffer.write('$fieldName: await $tableGetter.$mappingMethod(row, ' 'tablePrefix: ${asDartLiteral(prefix)}),'); } else if (nested is NestedResultQuery) { final fieldName = nested.filedName(); @@ -173,8 +173,8 @@ class QueryWriter { // The type converter maps non-nullable types, but the column may be // nullable in SQL => just map null to null and only invoke the type // converter for non-null values. - code = 'NullAwareTypeConverter.wrapFromSql(${_converter(converter)}, ' - '$code)'; + code = 'NullAwareTypeConverter.wrapFromSql' + '(${_converter(converter)}, $code)'; } else { // Just apply the type converter directly. code = '${_converter(converter)}.fromSql($code)'; @@ -206,7 +206,7 @@ class QueryWriter { _buffer.write(', '); _writeReadsFrom(select); - if (select.hasNestedQuery) { + if (select.needsAsyncMapping) { _buffer.write(').asyncMap('); } else { _buffer.write(').map('); @@ -259,9 +259,18 @@ class QueryWriter { _writeExpandedDeclarations(update); _buffer.write('return customWriteReturning(${_queryCode(update)},'); _writeCommonUpdateParameters(update); - _buffer.write(').then((rows) => rows.map('); - _writeMappingLambda(update); - _buffer.write(').toList());\n}'); + + _buffer.write(').then((rows) => '); + if (update.needsAsyncMapping) { + _buffer.write('Future.wait(rows.map('); + _writeMappingLambda(update); + _buffer.write('))'); + } else { + _buffer.write('rows.map('); + _writeMappingLambda(update); + _buffer.write(')'); + } + _buffer.write(');\n}'); } void _writeUpdatingQuery(UpdatingQuery update) { diff --git a/drift_dev/lib/src/writer/tables/table_writer.dart b/drift_dev/lib/src/writer/tables/table_writer.dart index f789e47a..16f15c26 100644 --- a/drift_dev/lib/src/writer/tables/table_writer.dart +++ b/drift_dev/lib/src/writer/tables/table_writer.dart @@ -136,9 +136,13 @@ abstract class TableOrViewWriter { final dataClassName = tableOrView.dartTypeCode(); + final isAsync = tableOrView.existingRowClass?.isAsyncFactory == true; + final returnType = isAsync ? 'Future<$dataClassName>' : dataClassName; + final asyncModifier = isAsync ? 'async' : ''; + buffer - ..write('@override\n$dataClassName map(Map data, ' - '{String? tablePrefix}) {\n') + ..write('@override $returnType map(Map data, ' + '{String? tablePrefix}) $asyncModifier {\n') ..write('final effectivePrefix = ' "tablePrefix != null ? '\$tablePrefix.' : '';"); @@ -174,6 +178,7 @@ abstract class TableOrViewWriter { final ctor = info.constructor; buffer ..write('return ') + ..write(isAsync ? 'await ' : '') ..write(classElement.name); if (ctor.name.isNotEmpty) { buffer diff --git a/drift_dev/test/analyzer/dart/custom_row_classes_test.dart b/drift_dev/test/analyzer/dart/custom_row_classes_test.dart index 431fe218..06e2df92 100644 --- a/drift_dev/test/analyzer/dart/custom_row_classes_test.dart +++ b/drift_dev/test/analyzer/dart/custom_row_classes_test.dart @@ -148,10 +148,50 @@ class Cls extends HasBar { Cls(this.foo, int bar): super(bar); } +''', + 'a|lib/async_factory.dart': ''' +import 'package:drift/drift.dart'; + +@UseRowClass(MyCustomClass, constructor: 'load') +class Tbl extends Table { + TextColumn get foo => text()(); + IntColumn get bar => integer()(); +} + +class MyCustomClass { + static Future load(String foo, int bar) async { + throw 'stub'; + } +} +''', + 'a|lib/invalid_static_factory.dart': ''' +import 'package:drift/drift.dart'; + +@UseRowClass(MyCustomClass, constructor: 'invalidReturn') +class Tbl extends Table { + TextColumn get foo => text()(); + IntColumn get bar => integer()(); +} + +@UseRowClass(MyCustomClass, constructor: 'notStatic') +class Tbl2 extends Table { + TextColumn get foo => text()(); + IntColumn get bar => integer()(); +} + +class MyCustomClass { + static String invalidReturn(String foo, int bar) async { + throw 'stub'; + } + + MyRowClass notStatic() { + throw 'stub'; + } +} ''', 'a|lib/custom_parent_class_no_error.dart': ''' import 'package:drift/drift.dart'; - + abstract class BaseModel extends DataClass { abstract final String id; } @@ -164,7 +204,7 @@ class Companies extends Table { ''', 'a|lib/custom_parent_class_typed_no_error.dart': ''' import 'package:drift/drift.dart'; - + abstract class BaseModel extends DataClass { abstract final String id; } @@ -177,7 +217,7 @@ class Companies extends Table { ''', 'a|lib/custom_parent_class_no_super.dart': ''' import 'package:drift/drift.dart'; - + abstract class BaseModel { abstract final String id; } @@ -190,7 +230,7 @@ class Companies extends Table { ''', 'a|lib/custom_parent_class_wrong_super.dart': ''' import 'package:drift/drift.dart'; - + class Test { } @@ -206,7 +246,7 @@ class Companies extends Table { ''', 'a|lib/custom_parent_class_typed_wrong_type_arg.dart': ''' import 'package:drift/drift.dart'; - + abstract class BaseModel extends DataClass { abstract final String id; } @@ -219,7 +259,7 @@ class Companies extends Table { ''', 'a|lib/custom_parent_class_two_type_argument.dart': ''' import 'package:drift/drift.dart'; - + abstract class BaseModel extends DataClass { abstract final String id; } @@ -234,7 +274,7 @@ class Companies extends Table { import 'package:drift/drift.dart'; typedef NotClass = void Function(); - + @DataClassName('Company', extending: NotClass) class Companies extends Table { TextColumn get id => text()(); @@ -306,6 +346,35 @@ class Companies extends Table { contains('but some are missing: bar'))), ); }); + + test('for invalid static factories', () async { + final file = await state.analyze('package:a/invalid_static_factory.dart'); + + expect( + file.errors.errors, + allOf( + contains( + isA() + .having( + (e) => e.message, + 'message', + contains('it needs to be static'), + ) + .having((e) => e.affectedElement?.name, + 'affectedElement.name', 'notStatic'), + ), + contains( + isA() + .having( + (e) => e.message, + 'message', + contains('needs to return an instance of it'), + ) + .having((e) => e.affectedElement?.name, + 'affectedElement.name', 'invalidReturn'), + ), + )); + }); }); test('supports generic row classes', () async { @@ -361,6 +430,17 @@ class Companies extends Table { expect(file.errors.errors, isEmpty); }); + test('supports async factories for existing row classes', () async { + final file = await state.analyze('package:a/async_factory.dart'); + expect(file.errors.errors, isEmpty); + + final table = file.currentResult!.declaredTables.single; + expect( + table.existingRowClass, + isA() + .having((e) => e.isAsyncFactory, 'isAsyncFactory', isTrue)); + }); + group('custom data class parent', () { test('check valid', () async { final file = diff --git a/drift_dev/test/writer/data_class_writer_test.dart b/drift_dev/test/writer/data_class_writer_test.dart index 6d670ad9..5f33f87e 100644 --- a/drift_dev/test/writer/data_class_writer_test.dart +++ b/drift_dev/test/writer/data_class_writer_test.dart @@ -9,10 +9,17 @@ import 'package:drift_dev/src/backends/build/drift_builder.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:test/test.dart'; -const _testInput = r''' +void main() { + test( + 'generates const constructor for data classes can companion classes', + () async { + await testBuilder( + DriftPartBuilder(const BuilderOptions({}), isForNewDriftPackage: true), + const { + 'a|lib/main.dart': r''' import 'package:drift/drift.dart'; -part 'main.moor.dart'; +part 'main.drift.dart'; class Users extends Table { IntColumn get id => integer().autoIncrement()(); @@ -23,18 +30,11 @@ class Users extends Table { tables: [Users], ) class Database extends _$Database {} -'''; - -void main() { - test( - 'generates const constructor for data classes can companion classes', - () async { - await testBuilder( - DriftPartBuilder(const BuilderOptions({})), - const {'a|lib/main.dart': _testInput}, +''' + }, reader: await PackageAssetReader.currentIsolate(), outputs: const { - 'a|lib/main.moor.dart': _GeneratesConstDataClasses( + 'a|lib/main.drift.dart': _GeneratesConstDataClasses( {'User', 'UsersCompanion'}, ), }, @@ -42,6 +42,56 @@ void main() { }, tags: 'analyzer', ); + + test( + 'generates async mapping code for existing row class with async factory', + () async { + await testBuilder( + DriftPartBuilder(const BuilderOptions({}), isForNewDriftPackage: true), + const { + 'a|lib/main.dart': r''' +import 'package:drift/drift.dart'; + +part 'main.drift.dart'; + +@UseRowClass(MyCustomClass, constructor: 'load') +class Tbl extends Table { + TextColumn get foo => text()(); + IntColumn get bar => integer()(); +} + +class MyCustomClass { + static Future load(String foo, int bar) async { + throw 'stub'; + } +} + +@DriftDatabase( + tables: [Tbl], +) +class Database extends _$Database {} +''' + }, + reader: await PackageAssetReader.currentIsolate(), + outputs: { + 'a|lib/main.drift.dart': decodedMatches(contains(r''' + @override + Future map(Map data, + {String? tablePrefix}) async { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return await MyCustomClass.load( + attachedDatabase.options.types + .read(DriftSqlType.string, data['${effectivePrefix}foo'])!, + attachedDatabase.options.types + .read(DriftSqlType.int, data['${effectivePrefix}bar'])!, + ); + } +''')), + }, + ); + }, + tags: 'analyzer', + ); } class _GeneratesConstDataClasses extends Matcher { diff --git a/examples/app/lib/database/database.g.dart b/examples/app/lib/database/database.g.dart index f9281e7b..785429d1 100644 --- a/examples/app/lib/database/database.g.dart +++ b/examples/app/lib/database/database.g.dart @@ -651,10 +651,10 @@ abstract class _$AppDatabase extends GeneratedDatabase { textEntries, todoEntries, categories, - }).map((QueryRow row) { + }).asyncMap((QueryRow row) async { return SearchResult( - todos: todoEntries.mapFromRow(row, tablePrefix: 'nested_0'), - cat: categories.mapFromRowOrNull(row, tablePrefix: 'nested_1'), + todos: await todoEntries.mapFromRow(row, tablePrefix: 'nested_0'), + cat: await categories.mapFromRowOrNull(row, tablePrefix: 'nested_1'), ); }); } diff --git a/examples/flutter_web_worker_example/lib/src/database/database.g.dart b/examples/flutter_web_worker_example/lib/src/database/database.g.dart index f77b50d8..eefe72ae 100644 --- a/examples/flutter_web_worker_example/lib/src/database/database.g.dart +++ b/examples/flutter_web_worker_example/lib/src/database/database.g.dart @@ -182,7 +182,7 @@ abstract class _$MyDatabase extends GeneratedDatabase { Selectable allEntries() { return customSelect('SELECT * FROM entries', variables: [], readsFrom: { entries, - }).map(entries.mapFromRow); + }).asyncMap(entries.mapFromRow); } Future addEntry(String var1) { diff --git a/examples/web_worker_example/lib/database.g.dart b/examples/web_worker_example/lib/database.g.dart index 48afe32d..9a7521eb 100644 --- a/examples/web_worker_example/lib/database.g.dart +++ b/examples/web_worker_example/lib/database.g.dart @@ -183,7 +183,7 @@ abstract class _$MyDatabase extends GeneratedDatabase { Selectable allEntries() { return customSelect('SELECT * FROM entries', variables: [], readsFrom: { entries, - }).map(entries.mapFromRow); + }).asyncMap(entries.mapFromRow); } Future addEntry(String var1) { diff --git a/extras/integration_tests/drift_testcases/lib/database/database.g.dart b/extras/integration_tests/drift_testcases/lib/database/database.g.dart index 1572397c..f2873746 100644 --- a/extras/integration_tests/drift_testcases/lib/database/database.g.dart +++ b/extras/integration_tests/drift_testcases/lib/database/database.g.dart @@ -296,7 +296,7 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> { @override Set get $primaryKey => {id}; @override - User map(Map data, {String? tablePrefix}) { + Future map(Map data, {String? tablePrefix}) async { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return User( id: attachedDatabase.options.types @@ -526,7 +526,8 @@ class $FriendshipsTable extends Friendships @override Set get $primaryKey => {firstUser, secondUser}; @override - Friendship map(Map data, {String? tablePrefix}) { + Future map(Map data, + {String? tablePrefix}) async { final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; return Friendship( firstUser: attachedDatabase.options.types @@ -558,7 +559,7 @@ abstract class _$Database extends GeneratedDatabase { readsFrom: { users, friendships, - }).map(users.mapFromRow); + }).asyncMap(users.mapFromRow); } Selectable amountOfGoodFriends(int user) { @@ -581,10 +582,10 @@ abstract class _$Database extends GeneratedDatabase { readsFrom: { friendships, users, - }).map((QueryRow row) { + }).asyncMap((QueryRow row) async { return FriendshipsOfResult( reallyGoodFriends: row.read('really_good_friends'), - user: users.mapFromRow(row, tablePrefix: 'nested_0'), + user: await users.mapFromRow(row, tablePrefix: 'nested_0'), ); }); } @@ -618,7 +619,7 @@ abstract class _$Database extends GeneratedDatabase { ], readsFrom: { users, - }).map(users.mapFromRow); + }).asyncMap(users.mapFromRow); } Future> returning(int var1, int var2, bool var3) { @@ -631,7 +632,7 @@ abstract class _$Database extends GeneratedDatabase { ], updates: { friendships - }).then((rows) => rows.map(friendships.mapFromRow).toList()); + }).then((rows) => Future.wait(rows.map(friendships.mapFromRow))); } @override