Allow async mappings from SQL to row classes

When existing, custom row classes are used, drift now supports using a
(potentially asynchronous) static method to load them instead of just
a named constructor like before.
Tables are also changed to support the `map` method being async for
cases where that is needed. The same applies to custom queries which
may have to be async now.
This commit is contained in:
Simon Binder 2022-08-12 22:55:02 +02:00
parent 799db04daa
commit b9a605ed25
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
29 changed files with 377 additions and 109 deletions

View File

@ -100,4 +100,4 @@ targets:
global_options: global_options:
":api_index": ":api_index":
options: options:
packages: ['drift', 'drift_dev'] packages: ['drift', 'drift_dev', 'sqlite3']

View File

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

View File

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

View File

@ -6,6 +6,7 @@ data:
template: layouts/docs/single 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_) 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. 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. 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`. To use a custom row class, simply annotate your table definition with `@UseRowClass`.
```dart {% assign snippets = "package:drift_docs/snippets/custom_row_classes/default.dart.excerpt.json" | readString | json_decode %}
@UseRowClass(User) {% include "blocks/snippet" snippets = snippets name = "start" %}
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});
}
```
A row class must adhere to the following requirements: 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 If you want to use another constructor, set the `constructor` parameter on the
`@UseRowClass` annotation: `@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 ```dart
@UseRowClass(User, constructor: 'fromDb')
class Users extends Table {
// ...
}
class User { class User {
final int id; // ...
final String name;
final DateTime birthDate;
User.fromDb({required this.id, required this.name, required this.birthDate}); static Future<User> load(int id, String name, DateTime birthday) async {
// ...
}
} }
``` ```

View File

@ -17,6 +17,8 @@
- __Breaking__: Remove the `includeJoinedTableColumns` parameter on `selectOnly()`. - __Breaking__: Remove the `includeJoinedTableColumns` parameter on `selectOnly()`.
The method now behaves as if that parameter was turned off. To use columns from a The method now behaves as if that parameter was turned off. To use columns from a
joined table, add them with `addColumns`. 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 - 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). 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` - Consistently handle transaction errors like a failing `BEGIN` or `COMMIT`
@ -25,6 +27,9 @@
to delete statatements. to delete statatements.
- Support nested transactions. - Support nested transactions.
- Support custom collations in the query builder API. - 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 ## 1.7.1

View File

@ -324,6 +324,9 @@ extension BuildGeneralColumn<T extends Object> on _BaseColumnBuilder<T> {
/// ``` /// ```
/// The generated row class will then use a `MyFancyClass` instead of a /// The generated row class will then use a `MyFancyClass` instead of a
/// `String`, which would usually be used for [Table.text] columns. /// `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<T> map<Dart>(TypeConverter<Dart, T?> converter) => ColumnBuilder<T> map<Dart>(TypeConverter<Dart, T?> converter) =>
_isGenerated(); _isGenerated();

View File

@ -10,6 +10,8 @@ import 'package:drift/src/runtime/executor/stream_queries.dart';
import 'package:drift/src/utils/single_transformer.dart'; import 'package:drift/src/utils/single_transformer.dart';
import 'package:meta/meta.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 // New files should not be part of this mega library, which we're trying to
// split up. // split up.
import 'expressions/case_when.dart'; import 'expressions/case_when.dart';

View File

@ -93,7 +93,7 @@ abstract class ResultSetImplementation<Tbl, Row> extends DatabaseSchemaEntity {
List<GeneratedColumn> get $columns; List<GeneratedColumn> get $columns;
/// Maps the given row returned by the database into the fitting data class. /// Maps the given row returned by the database into the fitting data class.
Row map(Map<String, dynamic> data, {String? tablePrefix}); FutureOr<Row> map(Map<String, dynamic> data, {String? tablePrefix});
/// Creates an alias of this table or view that will write the name [alias] /// Creates an alias of this table or view that will write the name [alias]
/// when used in a query. /// when used in a query.
@ -125,7 +125,7 @@ class _AliasResultSet<Tbl, Row> extends ResultSetImplementation<Tbl, Row> {
String get entityName => _inner.entityName; String get entityName => _inner.entityName;
@override @override
Row map(Map<String, dynamic> data, {String? tablePrefix}) { FutureOr<Row> map(Map<String, dynamic> data, {String? tablePrefix}) {
return _inner.map(data, tablePrefix: tablePrefix); return _inner.map(data, tablePrefix: tablePrefix);
} }

View File

@ -75,7 +75,8 @@ mixin TableInfo<TableDsl extends Table, D> on Table
/// The [database] instance is used so that the raw values from the companion /// 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 /// can properly be interpreted as the high-level Dart values exposed by the
/// data class. /// data class.
D mapFromCompanion(Insertable<D> companion, DatabaseConnectionUser database) { Future<D> mapFromCompanion(
Insertable<D> companion, DatabaseConnectionUser database) async {
final asColumnMap = companion.toColumns(false); final asColumnMap = companion.toColumns(false);
if (asColumnMap.values.any((e) => e is! Variable)) { if (asColumnMap.values.any((e) => e is! Variable)) {
@ -122,20 +123,20 @@ mixin VirtualTableInfo<TableDsl extends Table, D> on TableInfo<TableDsl, D> {
/// Most of these are accessed internally by drift or by generated code. /// Most of these are accessed internally by drift or by generated code.
extension TableInfoUtils<TableDsl, D> on ResultSetImplementation<TableDsl, D> { extension TableInfoUtils<TableDsl, D> on ResultSetImplementation<TableDsl, D> {
/// Like [map], but from a [row] instead of the low-level map. /// Like [map], but from a [row] instead of the low-level map.
D mapFromRow(QueryRow row, {String? tablePrefix}) { Future<D> mapFromRow(QueryRow row, {String? tablePrefix}) async {
return map(row.data, tablePrefix: tablePrefix); return map(row.data, tablePrefix: tablePrefix);
} }
/// Like [mapFromRow], but returns null if a non-nullable column of this table /// Like [mapFromRow], but returns null if a non-nullable column of this table
/// is null in [row]. /// is null in [row].
D? mapFromRowOrNull(QueryRow row, {String? tablePrefix}) { Future<D?> mapFromRowOrNull(QueryRow row, {String? tablePrefix}) {
final resolvedPrefix = tablePrefix == null ? '' : '$tablePrefix.'; final resolvedPrefix = tablePrefix == null ? '' : '$tablePrefix.';
final notInRow = $columns final notInRow = $columns
.where((c) => !c.$nullable) .where((c) => !c.$nullable)
.any((e) => row.data['$resolvedPrefix${e.$name}'] == null); .any((e) => row.data['$resolvedPrefix${e.$name}'] == null);
if (notInRow) return null; if (notInRow) return Future.value(null);
return mapFromRow(row, tablePrefix: tablePrefix); return mapFromRow(row, tablePrefix: tablePrefix);
} }
@ -153,7 +154,7 @@ extension TableInfoUtils<TableDsl, D> on ResultSetImplementation<TableDsl, D> {
/// ///
/// Drift would generate code to call this method with `'c1': 'foo'` and /// Drift would generate code to call this method with `'c1': 'foo'` and
/// `'c2': 'bar'` in [alias]. /// `'c2': 'bar'` in [alias].
D mapFromRowWithAlias(QueryRow row, Map<String, String> alias) { Future<D> mapFromRowWithAlias(QueryRow row, Map<String, String> alias) async {
return map({ return map({
for (final entry in row.data.entries) alias[entry.key]!: entry.value, for (final entry in row.data.entries) alias[entry.key]!: entry.value,
}); });

View File

@ -78,7 +78,7 @@ class DeleteStatement<T extends Table, D> extends Query<T, D>
{TableUpdate.onTable(_sourceTable, kind: UpdateKind.delete)}); {TableUpdate.onTable(_sourceTable, kind: UpdateKind.delete)});
} }
return [for (final rawRow in rows) table.map(rawRow)]; return rows.mapAsyncAndAwait(table.map);
}); });
} }
} }

View File

@ -60,7 +60,7 @@ class SimpleSelectStatement<T extends HasResultSet, D> extends Query<T, D>
key: StreamKey(query.sql, query.boundVariables), key: StreamKey(query.sql, query.boundVariables),
); );
return database.createStream(fetcher).map(_mapResponse); return database.createStream(fetcher).asyncMap(_mapResponse);
} }
Future<List<Map<String, Object?>>> _getRaw(GenerationContext ctx) { Future<List<Map<String, Object?>>> _getRaw(GenerationContext ctx) {
@ -69,8 +69,8 @@ class SimpleSelectStatement<T extends HasResultSet, D> extends Query<T, D>
}); });
} }
List<D> _mapResponse(List<Map<String, Object?>> rows) { Future<List<D>> _mapResponse(List<Map<String, Object?>> rows) {
return rows.map(table.map).toList(); return rows.mapAsyncAndAwait(table.map);
} }
/// Creates a select statement that operates on more than one table by /// Creates a select statement that operates on more than one table by

View File

@ -207,7 +207,7 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
return database return database
.createStream(fetcher) .createStream(fetcher)
.map((rows) => _mapResponse(ctx, rows)); .asyncMap((rows) => _mapResponse(ctx, rows));
} }
@override @override
@ -234,9 +234,9 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
}); });
} }
List<TypedResult> _mapResponse( Future<List<TypedResult>> _mapResponse(
GenerationContext ctx, List<Map<String, Object?>> rows) { GenerationContext ctx, List<Map<String, Object?>> rows) {
return rows.map((row) { return Future.wait(rows.map((row) async {
final readTables = <ResultSetImplementation, dynamic>{}; final readTables = <ResultSetImplementation, dynamic>{};
final readColumns = <Expression, dynamic>{}; final readColumns = <Expression, dynamic>{};
@ -244,7 +244,8 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
final prefix = '${table.aliasedName}.'; final prefix = '${table.aliasedName}.';
// if all columns of this table are null, skip the table // if all columns of this table are null, skip the table
if (table.$columns.any((c) => row[prefix + c.$name] != null)) { 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<FirstT extends HasResultSet, FirstD>
} }
return TypedResult(readTables, QueryRow(row, database), readColumns); return TypedResult(readTables, QueryRow(row, database), readColumns);
}).toList(); }));
} }
@alwaysThrows @alwaysThrows

View File

@ -95,7 +95,7 @@ class UpdateStatement<T extends Table, D> extends Query<T, D>
{TableUpdate.onTable(_sourceTable, kind: UpdateKind.update)}); {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 /// Replaces the old version of [entity] that is stored in the database with

View File

@ -0,0 +1,12 @@
@internal
import 'dart:async';
import 'package:meta/meta.dart';
/// Drift-internal utilities to map potentially async operations.
extension MapAndAwait<T> on Iterable<T> {
/// A variant of [Future.wait] that also works for [FutureOr].
Future<List<R>> mapAsyncAndAwait<R>(FutureOr<R> Function(T) mapper) {
return Future.wait(map((e) => Future.sync(() => mapper(e))));
}
}

View File

@ -34,20 +34,20 @@ void main() {
expect(aliasA.hashCode == db.alias(db.users, 'a').hashCode, isTrue); 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( const companion = SharedTodosCompanion(
todo: Value(3), todo: Value(3),
user: Value(4), user: Value(4),
); );
final user = db.sharedTodos.mapFromCompanion(companion, db); final user = await db.sharedTodos.mapFromCompanion(companion, db);
expect( expect(
user, user,
const SharedTodo(todo: 3, user: 4), 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 = { final rowData = {
'id': 1, 'id': 1,
'title': 'some title', 'title': 'some title',
@ -55,7 +55,7 @@ void main() {
'target_date': null, 'target_date': null,
'category': null, 'category': null,
}; };
final todo = db.todosTable.mapFromRowOrNull(QueryRow(rowData, db)); final todo = await db.todosTable.mapFromRowOrNull(QueryRow(rowData, db));
expect( expect(
todo, todo,
const TodoEntry( const TodoEntry(

View File

@ -1596,7 +1596,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
], ],
readsFrom: { readsFrom: {
config, config,
}).map((QueryRow row) => config.mapFromRowWithAlias(row, const { }).asyncMap((QueryRow row) => config.mapFromRowWithAlias(row, const {
'ck': 'config_key', 'ck': 'config_key',
'cf': 'config_value', 'cf': 'config_value',
'cs1': 'sync_state', 'cs1': 'sync_state',
@ -1621,7 +1621,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
readsFrom: { readsFrom: {
config, config,
...generatedclause.watchedTables, ...generatedclause.watchedTables,
}).map(config.mapFromRow); }).asyncMap(config.mapFromRow);
} }
Selectable<Config> readDynamic( Selectable<Config> readDynamic(
@ -1638,7 +1638,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
readsFrom: { readsFrom: {
config, config,
...generatedpredicate.watchedTables, ...generatedpredicate.watchedTables,
}).map(config.mapFromRow); }).asyncMap(config.mapFromRow);
} }
Selectable<String> typeConverterVar(SyncType? var1, List<SyncType?> var2, Selectable<String> typeConverterVar(SyncType? var1, List<SyncType?> var2,
@ -1710,12 +1710,12 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
withDefaults, withDefaults,
withConstraints, withConstraints,
...generatedpredicate.watchedTables, ...generatedpredicate.watchedTables,
}).map((QueryRow row) { }).asyncMap((QueryRow row) async {
return MultipleResult( return MultipleResult(
row: row, row: row,
a: row.readNullable<String>('a'), a: row.readNullable<String>('a'),
b: row.readNullable<int>('b'), b: row.readNullable<int>('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: { readsFrom: {
email, email,
}).map(email.mapFromRow); }).asyncMap(email.mapFromRow);
} }
Selectable<ReadRowIdResult> readRowId( Selectable<ReadRowIdResult> readRowId(
@ -1762,7 +1762,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
Selectable<MyViewData> readView() { Selectable<MyViewData> readView() {
return customSelect('SELECT * FROM my_view', variables: [], readsFrom: { return customSelect('SELECT * FROM my_view', variables: [], readsFrom: {
config, config,
}).map(myView.mapFromRow); }).asyncMap(myView.mapFromRow);
} }
Selectable<int> cfeTest() { Selectable<int> cfeTest() {
@ -1786,9 +1786,10 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
$writeInsertable(this.config, value, startIndex: $arrayStartIndex); $writeInsertable(this.config, value, startIndex: $arrayStartIndex);
$arrayStartIndex += generatedvalue.amountOfVariables; $arrayStartIndex += generatedvalue.amountOfVariables;
return customWriteReturning( return customWriteReturning(
'INSERT INTO config ${generatedvalue.sql} RETURNING *', 'INSERT INTO config ${generatedvalue.sql} RETURNING *',
variables: [...generatedvalue.introducedVariables], variables: [...generatedvalue.introducedVariables],
updates: {config}).then((rows) => rows.map(config.mapFromRow).toList()); updates: {config})
.then((rows) => Future.wait(rows.map(config.mapFromRow)));
} }
Selectable<NestedResult> nested(String? var1) { Selectable<NestedResult> nested(String? var1) {
@ -1803,7 +1804,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
}).asyncMap((QueryRow row) async { }).asyncMap((QueryRow row) async {
return NestedResult( return NestedResult(
row: row, row: row,
defaults: withDefaults.mapFromRow(row, tablePrefix: 'nested_0'), defaults: await withDefaults.mapFromRow(row, tablePrefix: 'nested_0'),
nestedQuery0: await customSelect( nestedQuery0: await customSelect(
'SELECT * FROM with_constraints AS c WHERE c.b = ?1', 'SELECT * FROM with_constraints AS c WHERE c.b = ?1',
variables: [ variables: [
@ -1812,7 +1813,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
readsFrom: { readsFrom: {
withConstraints, withConstraints,
withDefaults, withDefaults,
}).map(withConstraints.mapFromRow).get(), }).asyncMap(withConstraints.mapFromRow).get(),
); );
}); });
} }

View File

@ -1642,7 +1642,7 @@ abstract class _$TodoDb extends GeneratedDatabase {
], ],
readsFrom: { readsFrom: {
todosTable, todosTable,
}).map(todosTable.mapFromRow); }).asyncMap(todosTable.mapFromRow);
} }
Selectable<TodoEntry> search({required int id}) { Selectable<TodoEntry> search({required int id}) {
@ -1653,7 +1653,7 @@ abstract class _$TodoDb extends GeneratedDatabase {
], ],
readsFrom: { readsFrom: {
todosTable, todosTable,
}).map(todosTable.mapFromRow); }).asyncMap(todosTable.mapFromRow);
} }
Selectable<MyCustomObject> findCustom() { Selectable<MyCustomObject> findCustom() {
@ -1750,6 +1750,6 @@ mixin _$SomeDaoMixin on DatabaseAccessor<TodoDb> {
todosTable, todosTable,
sharedTodos, sharedTodos,
users, users,
}).map(todosTable.mapFromRow); }).asyncMap(todosTable.mapFromRow);
} }
} }

View File

@ -53,5 +53,7 @@ class CustomTable extends Table with TableInfo<CustomTable, Null> {
} }
@override @override
Null map(Map<String, dynamic> data, {String? tablePrefix}) => null; Future<Null> map(Map<String, dynamic> data, {String? tablePrefix}) async {
return null;
}
} }

View File

@ -28,10 +28,13 @@ ExistingRowClass? validateExistingClass(
Step step) { Step step) {
final errors = step.errors; final errors = step.errors;
final desiredClass = dartClass.classElement; final desiredClass = dartClass.classElement;
ConstructorElement? ctor; final library = desiredClass.library;
ExecutableElement? ctor;
final InterfaceType instantiation;
if (dartClass.instantiation != null) { if (dartClass.instantiation != null) {
final instantiation = desiredClass.instantiate( instantiation = desiredClass.instantiate(
typeArguments: dartClass.instantiation!, typeArguments: dartClass.instantiation!,
nullabilitySuffix: NullabilitySuffix.none, nullabilitySuffix: NullabilitySuffix.none,
); );
@ -41,6 +44,36 @@ ExistingRowClass? validateExistingClass(
ctor = instantiation.lookUpConstructor(constructor, desiredClass.library); ctor = instantiation.lookUpConstructor(constructor, desiredClass.library);
} else { } else {
ctor = desiredClass.getNamedConstructor(constructor); 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<ThatRowClass>`
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) { if (ctor == null) {

View File

@ -69,15 +69,31 @@ class ExistingRowClass {
/// The Dart types that should be used to instantiate the [targetClass]. /// The Dart types that should be used to instantiate the [targetClass].
final List<DartType> typeInstantiation; final List<DartType> 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<DriftColumn, ParameterElement> mapping; final Map<DriftColumn, ParameterElement> mapping;
/// Generate toCompanion for data class /// Generate toCompanion for data class
final bool generateInsertable; final bool generateInsertable;
ExistingRowClass( ExistingRowClass(
this.targetClass, this.constructor, this.mapping, this.generateInsertable, this.targetClass,
{this.typeInstantiation = const []}); 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()]) { String dartType([GenerationOptions options = const GenerationOptions()]) {
if (typeInstantiation.isEmpty) { if (typeInstantiation.isEmpty) {

View File

@ -118,6 +118,17 @@ abstract class SqlQuery {
placeholders = elements.whereType<FoundDartPlaceholder>().toList(); placeholders = elements.whereType<FoundDartPlaceholder>().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 { String get resultClassName {
final resultSet = this.resultSet; final resultSet = this.resultSet;
if (resultSet == null) { if (resultSet == null) {
@ -194,6 +205,9 @@ class SqlSelectQuery extends SqlQuery {
bool get hasNestedQuery => bool get hasNestedQuery =>
resultSet.nestedResults.any((e) => e is NestedResultQuery); resultSet.nestedResults.any((e) => e is NestedResultQuery);
@override
bool get needsAsyncMapping => hasNestedQuery || super.needsAsyncMapping;
SqlSelectQuery( SqlSelectQuery(
String name, String name,
this.fromContext, this.fromContext,

View File

@ -115,7 +115,7 @@ class QueryWriter {
} }
} else { } else {
_buffer.write('(QueryRow row) '); _buffer.write('(QueryRow row) ');
if (query is SqlSelectQuery && query.hasNestedQuery) { if (query.needsAsyncMapping) {
_buffer.write('async '); _buffer.write('async ');
} }
_buffer.write('{ return ${query.resultClassName}('); _buffer.write('{ return ${query.resultClassName}(');
@ -140,7 +140,7 @@ class QueryWriter {
final mappingMethod = final mappingMethod =
nested.isNullable ? 'mapFromRowOrNull' : 'mapFromRow'; nested.isNullable ? 'mapFromRowOrNull' : 'mapFromRow';
_buffer.write('$fieldName: $tableGetter.$mappingMethod(row, ' _buffer.write('$fieldName: await $tableGetter.$mappingMethod(row, '
'tablePrefix: ${asDartLiteral(prefix)}),'); 'tablePrefix: ${asDartLiteral(prefix)}),');
} else if (nested is NestedResultQuery) { } else if (nested is NestedResultQuery) {
final fieldName = nested.filedName(); final fieldName = nested.filedName();
@ -173,8 +173,8 @@ class QueryWriter {
// The type converter maps non-nullable types, but the column may be // 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 // nullable in SQL => just map null to null and only invoke the type
// converter for non-null values. // converter for non-null values.
code = 'NullAwareTypeConverter.wrapFromSql(${_converter(converter)}, ' code = 'NullAwareTypeConverter.wrapFromSql'
'$code)'; '(${_converter(converter)}, $code)';
} else { } else {
// Just apply the type converter directly. // Just apply the type converter directly.
code = '${_converter(converter)}.fromSql($code)'; code = '${_converter(converter)}.fromSql($code)';
@ -206,7 +206,7 @@ class QueryWriter {
_buffer.write(', '); _buffer.write(', ');
_writeReadsFrom(select); _writeReadsFrom(select);
if (select.hasNestedQuery) { if (select.needsAsyncMapping) {
_buffer.write(').asyncMap('); _buffer.write(').asyncMap(');
} else { } else {
_buffer.write(').map('); _buffer.write(').map(');
@ -259,9 +259,18 @@ class QueryWriter {
_writeExpandedDeclarations(update); _writeExpandedDeclarations(update);
_buffer.write('return customWriteReturning(${_queryCode(update)},'); _buffer.write('return customWriteReturning(${_queryCode(update)},');
_writeCommonUpdateParameters(update); _writeCommonUpdateParameters(update);
_buffer.write(').then((rows) => rows.map(');
_writeMappingLambda(update); _buffer.write(').then((rows) => ');
_buffer.write(').toList());\n}'); 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) { void _writeUpdatingQuery(UpdatingQuery update) {

View File

@ -136,9 +136,13 @@ abstract class TableOrViewWriter {
final dataClassName = tableOrView.dartTypeCode(); final dataClassName = tableOrView.dartTypeCode();
final isAsync = tableOrView.existingRowClass?.isAsyncFactory == true;
final returnType = isAsync ? 'Future<$dataClassName>' : dataClassName;
final asyncModifier = isAsync ? 'async' : '';
buffer buffer
..write('@override\n$dataClassName map(Map<String, dynamic> data, ' ..write('@override $returnType map(Map<String, dynamic> data, '
'{String? tablePrefix}) {\n') '{String? tablePrefix}) $asyncModifier {\n')
..write('final effectivePrefix = ' ..write('final effectivePrefix = '
"tablePrefix != null ? '\$tablePrefix.' : '';"); "tablePrefix != null ? '\$tablePrefix.' : '';");
@ -174,6 +178,7 @@ abstract class TableOrViewWriter {
final ctor = info.constructor; final ctor = info.constructor;
buffer buffer
..write('return ') ..write('return ')
..write(isAsync ? 'await ' : '')
..write(classElement.name); ..write(classElement.name);
if (ctor.name.isNotEmpty) { if (ctor.name.isNotEmpty) {
buffer buffer

View File

@ -148,10 +148,50 @@ class Cls extends HasBar {
Cls(this.foo, int bar): super(bar); 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<MyCustomClass> 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': ''' 'a|lib/custom_parent_class_no_error.dart': '''
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
abstract class BaseModel extends DataClass { abstract class BaseModel extends DataClass {
abstract final String id; abstract final String id;
} }
@ -164,7 +204,7 @@ class Companies extends Table {
''', ''',
'a|lib/custom_parent_class_typed_no_error.dart': ''' 'a|lib/custom_parent_class_typed_no_error.dart': '''
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
abstract class BaseModel<T> extends DataClass { abstract class BaseModel<T> extends DataClass {
abstract final String id; abstract final String id;
} }
@ -177,7 +217,7 @@ class Companies extends Table {
''', ''',
'a|lib/custom_parent_class_no_super.dart': ''' 'a|lib/custom_parent_class_no_super.dart': '''
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
abstract class BaseModel { abstract class BaseModel {
abstract final String id; abstract final String id;
} }
@ -190,7 +230,7 @@ class Companies extends Table {
''', ''',
'a|lib/custom_parent_class_wrong_super.dart': ''' 'a|lib/custom_parent_class_wrong_super.dart': '''
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
class Test { class Test {
} }
@ -206,7 +246,7 @@ class Companies extends Table {
''', ''',
'a|lib/custom_parent_class_typed_wrong_type_arg.dart': ''' 'a|lib/custom_parent_class_typed_wrong_type_arg.dart': '''
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
abstract class BaseModel<T> extends DataClass { abstract class BaseModel<T> extends DataClass {
abstract final String id; abstract final String id;
} }
@ -219,7 +259,7 @@ class Companies extends Table {
''', ''',
'a|lib/custom_parent_class_two_type_argument.dart': ''' 'a|lib/custom_parent_class_two_type_argument.dart': '''
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
abstract class BaseModel<T, D> extends DataClass { abstract class BaseModel<T, D> extends DataClass {
abstract final String id; abstract final String id;
} }
@ -234,7 +274,7 @@ class Companies extends Table {
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
typedef NotClass = void Function(); typedef NotClass = void Function();
@DataClassName('Company', extending: NotClass) @DataClassName('Company', extending: NotClass)
class Companies extends Table { class Companies extends Table {
TextColumn get id => text()(); TextColumn get id => text()();
@ -306,6 +346,35 @@ class Companies extends Table {
contains('but some are missing: bar'))), 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<ErrorInDartCode>()
.having(
(e) => e.message,
'message',
contains('it needs to be static'),
)
.having((e) => e.affectedElement?.name,
'affectedElement.name', 'notStatic'),
),
contains(
isA<ErrorInDartCode>()
.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 { test('supports generic row classes', () async {
@ -361,6 +430,17 @@ class Companies extends Table {
expect(file.errors.errors, isEmpty); 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<ExistingRowClass>()
.having((e) => e.isAsyncFactory, 'isAsyncFactory', isTrue));
});
group('custom data class parent', () { group('custom data class parent', () {
test('check valid', () async { test('check valid', () async {
final file = final file =

View File

@ -9,10 +9,17 @@ import 'package:drift_dev/src/backends/build/drift_builder.dart';
import 'package:pub_semver/pub_semver.dart'; import 'package:pub_semver/pub_semver.dart';
import 'package:test/test.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'; import 'package:drift/drift.dart';
part 'main.moor.dart'; part 'main.drift.dart';
class Users extends Table { class Users extends Table {
IntColumn get id => integer().autoIncrement()(); IntColumn get id => integer().autoIncrement()();
@ -23,18 +30,11 @@ class Users extends Table {
tables: [Users], tables: [Users],
) )
class Database extends _$Database {} 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(), reader: await PackageAssetReader.currentIsolate(),
outputs: const { outputs: const {
'a|lib/main.moor.dart': _GeneratesConstDataClasses( 'a|lib/main.drift.dart': _GeneratesConstDataClasses(
{'User', 'UsersCompanion'}, {'User', 'UsersCompanion'},
), ),
}, },
@ -42,6 +42,56 @@ void main() {
}, },
tags: 'analyzer', 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<MyCustomClass> 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<MyCustomClass> map(Map<String, dynamic> 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 { class _GeneratesConstDataClasses extends Matcher {

View File

@ -651,10 +651,10 @@ abstract class _$AppDatabase extends GeneratedDatabase {
textEntries, textEntries,
todoEntries, todoEntries,
categories, categories,
}).map((QueryRow row) { }).asyncMap((QueryRow row) async {
return SearchResult( return SearchResult(
todos: todoEntries.mapFromRow(row, tablePrefix: 'nested_0'), todos: await todoEntries.mapFromRow(row, tablePrefix: 'nested_0'),
cat: categories.mapFromRowOrNull(row, tablePrefix: 'nested_1'), cat: await categories.mapFromRowOrNull(row, tablePrefix: 'nested_1'),
); );
}); });
} }

View File

@ -182,7 +182,7 @@ abstract class _$MyDatabase extends GeneratedDatabase {
Selectable<Entrie> allEntries() { Selectable<Entrie> allEntries() {
return customSelect('SELECT * FROM entries', variables: [], readsFrom: { return customSelect('SELECT * FROM entries', variables: [], readsFrom: {
entries, entries,
}).map(entries.mapFromRow); }).asyncMap(entries.mapFromRow);
} }
Future<int> addEntry(String var1) { Future<int> addEntry(String var1) {

View File

@ -183,7 +183,7 @@ abstract class _$MyDatabase extends GeneratedDatabase {
Selectable<Entrie> allEntries() { Selectable<Entrie> allEntries() {
return customSelect('SELECT * FROM entries', variables: [], readsFrom: { return customSelect('SELECT * FROM entries', variables: [], readsFrom: {
entries, entries,
}).map(entries.mapFromRow); }).asyncMap(entries.mapFromRow);
} }
Future<int> addEntry(String var1) { Future<int> addEntry(String var1) {

View File

@ -296,7 +296,7 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
@override @override
Set<GeneratedColumn> get $primaryKey => {id}; Set<GeneratedColumn> get $primaryKey => {id};
@override @override
User map(Map<String, dynamic> data, {String? tablePrefix}) { Future<User> map(Map<String, dynamic> data, {String? tablePrefix}) async {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return User( return User(
id: attachedDatabase.options.types id: attachedDatabase.options.types
@ -526,7 +526,8 @@ class $FriendshipsTable extends Friendships
@override @override
Set<GeneratedColumn> get $primaryKey => {firstUser, secondUser}; Set<GeneratedColumn> get $primaryKey => {firstUser, secondUser};
@override @override
Friendship map(Map<String, dynamic> data, {String? tablePrefix}) { Future<Friendship> map(Map<String, dynamic> data,
{String? tablePrefix}) async {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return Friendship( return Friendship(
firstUser: attachedDatabase.options.types firstUser: attachedDatabase.options.types
@ -558,7 +559,7 @@ abstract class _$Database extends GeneratedDatabase {
readsFrom: { readsFrom: {
users, users,
friendships, friendships,
}).map(users.mapFromRow); }).asyncMap(users.mapFromRow);
} }
Selectable<int> amountOfGoodFriends(int user) { Selectable<int> amountOfGoodFriends(int user) {
@ -581,10 +582,10 @@ abstract class _$Database extends GeneratedDatabase {
readsFrom: { readsFrom: {
friendships, friendships,
users, users,
}).map((QueryRow row) { }).asyncMap((QueryRow row) async {
return FriendshipsOfResult( return FriendshipsOfResult(
reallyGoodFriends: row.read<bool>('really_good_friends'), reallyGoodFriends: row.read<bool>('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: { readsFrom: {
users, users,
}).map(users.mapFromRow); }).asyncMap(users.mapFromRow);
} }
Future<List<Friendship>> returning(int var1, int var2, bool var3) { Future<List<Friendship>> returning(int var1, int var2, bool var3) {
@ -631,7 +632,7 @@ abstract class _$Database extends GeneratedDatabase {
], ],
updates: { updates: {
friendships friendships
}).then((rows) => rows.map(friendships.mapFromRow).toList()); }).then((rows) => Future.wait(rows.map(friendships.mapFromRow)));
} }
@override @override