mirror of https://github.com/AMT-Cheif/drift.git
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:
parent
799db04daa
commit
b9a605ed25
|
@ -100,4 +100,4 @@ targets:
|
|||
global_options:
|
||||
":api_index":
|
||||
options:
|
||||
packages: ['drift', 'drift_dev']
|
||||
packages: ['drift', 'drift_dev', 'sqlite3']
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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 {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))));
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue