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:
":api_index":
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
---
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<User> load(int id, String name, DateTime birthday) async {
// ...
}
}
```

View File

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

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
/// `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) =>
_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: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';

View File

@ -93,7 +93,7 @@ abstract class ResultSetImplementation<Tbl, Row> extends DatabaseSchemaEntity {
List<GeneratedColumn> get $columns;
/// 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]
/// when used in a query.
@ -125,7 +125,7 @@ class _AliasResultSet<Tbl, Row> extends ResultSetImplementation<Tbl, Row> {
String get entityName => _inner.entityName;
@override
Row map(Map<String, dynamic> data, {String? tablePrefix}) {
FutureOr<Row> map(Map<String, dynamic> data, {String? 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
/// can properly be interpreted as the high-level Dart values exposed by the
/// data class.
D mapFromCompanion(Insertable<D> companion, DatabaseConnectionUser database) {
Future<D> mapFromCompanion(
Insertable<D> companion, DatabaseConnectionUser database) async {
final asColumnMap = companion.toColumns(false);
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.
extension TableInfoUtils<TableDsl, D> on ResultSetImplementation<TableDsl, D> {
/// 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);
}
/// Like [mapFromRow], but returns null if a non-nullable column of this table
/// is null in [row].
D? mapFromRowOrNull(QueryRow row, {String? tablePrefix}) {
Future<D?> 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<TableDsl, D> on ResultSetImplementation<TableDsl, D> {
///
/// Drift would generate code to call this method with `'c1': 'foo'` and
/// `'c2': 'bar'` in [alias].
D mapFromRowWithAlias(QueryRow row, Map<String, String> alias) {
Future<D> mapFromRowWithAlias(QueryRow row, Map<String, String> alias) async {
return map({
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)});
}
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),
);
return database.createStream(fetcher).map(_mapResponse);
return database.createStream(fetcher).asyncMap(_mapResponse);
}
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) {
return rows.map(table.map).toList();
Future<List<D>> _mapResponse(List<Map<String, Object?>> rows) {
return rows.mapAsyncAndAwait(table.map);
}
/// 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
.createStream(fetcher)
.map((rows) => _mapResponse(ctx, rows));
.asyncMap((rows) => _mapResponse(ctx, rows));
}
@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) {
return rows.map((row) {
return Future.wait(rows.map((row) async {
final readTables = <ResultSetImplementation, dynamic>{};
final readColumns = <Expression, dynamic>{};
@ -244,7 +244,8 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
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<FirstT extends HasResultSet, FirstD>
}
return TypedResult(readTables, QueryRow(row, database), readColumns);
}).toList();
}));
}
@alwaysThrows

View File

@ -95,7 +95,7 @@ class UpdateStatement<T extends Table, D> extends Query<T, D>
{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

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);
});
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(

View File

@ -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<Config> readDynamic(
@ -1638,7 +1638,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
readsFrom: {
config,
...generatedpredicate.watchedTables,
}).map(config.mapFromRow);
}).asyncMap(config.mapFromRow);
}
Selectable<String> typeConverterVar(SyncType? var1, List<SyncType?> 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<String>('a'),
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: {
email,
}).map(email.mapFromRow);
}).asyncMap(email.mapFromRow);
}
Selectable<ReadRowIdResult> readRowId(
@ -1762,7 +1762,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
Selectable<MyViewData> readView() {
return customSelect('SELECT * FROM my_view', variables: [], readsFrom: {
config,
}).map(myView.mapFromRow);
}).asyncMap(myView.mapFromRow);
}
Selectable<int> 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<NestedResult> 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(),
);
});
}

View File

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

View File

@ -53,5 +53,7 @@ class CustomTable extends Table with TableInfo<CustomTable, Null> {
}
@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) {
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<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) {

View File

@ -69,15 +69,31 @@ class ExistingRowClass {
/// The Dart types that should be used to instantiate the [targetClass].
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;
/// 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) {

View File

@ -118,6 +118,17 @@ abstract class SqlQuery {
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 {
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,

View File

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

View File

@ -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<String, dynamic> data, '
'{String? tablePrefix}) {\n')
..write('@override $returnType map(Map<String, dynamic> 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

View File

@ -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<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': '''
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<T> 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<T> 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<T, D> 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<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 {
@ -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<ExistingRowClass>()
.having((e) => e.isAsyncFactory, 'isAsyncFactory', isTrue));
});
group('custom data class parent', () {
test('check valid', () async {
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: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<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 {

View File

@ -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'),
);
});
}

View File

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

View File

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

View File

@ -296,7 +296,7 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
User map(Map<String, dynamic> data, {String? tablePrefix}) {
Future<User> map(Map<String, dynamic> 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<GeneratedColumn> get $primaryKey => {firstUser, secondUser};
@override
Friendship map(Map<String, dynamic> data, {String? tablePrefix}) {
Future<Friendship> map(Map<String, dynamic> 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<int> 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<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: {
users,
}).map(users.mapFromRow);
}).asyncMap(users.mapFromRow);
}
Future<List<Friendship>> 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