Make getSingle() and watchSingle() non-nullable

This commit is contained in:
Simon Binder 2020-12-14 20:22:27 +01:00
parent d9cf6660ec
commit 36edcf0ed6
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
17 changed files with 91 additions and 24 deletions

View File

@ -1,3 +1,4 @@
import 'package:moor/moor.dart';
import 'package:test/test.dart';
import 'package:tests/database/database.dart';
import 'package:tests/suite/suite.dart';

View File

@ -1,3 +1,4 @@
import 'package:moor/moor.dart' hide isNull;
import 'package:test/test.dart';
import 'package:tests/database/database.dart';

View File

@ -1,3 +1,4 @@
import 'package:moor/moor.dart';
import 'package:test/test.dart';
import 'package:tests/data/sample_data.dart' as people;
import 'package:tests/database/database.dart';

View File

@ -1,3 +1,4 @@
import 'package:moor/moor.dart';
import 'package:migrations_example/database.dart';
import 'package:test/test.dart';
import 'package:moor_generator/api/migrations.dart';

View File

@ -1,5 +1,7 @@
## unreleased (breaking - 4.0)
- __Breaking__: `getSingle()` and `watchSingle()` are now non-nullable and throw for empty results.
Use `getSingleOrNull()` and `watchSingleOrNull()` for the old behavior.
- __Breaking__: Changed the `args` parameter in `QueryExecutor` methods to `List<Object?>`
- __Breaking__: Removed the second type parameter from `TypedResult.read`
- Support null safety

View File

@ -265,8 +265,8 @@ mixin QueryEngine on DatabaseConnectionUser {
/// query once, use [Selectable.get]. For an auto-updating streams, set the
/// set of tables the ready [readsFrom] and use [Selectable.watch]. If you
/// know the query will never emit more than one row, you can also use
/// [Selectable.getSingle] and [Selectable.watchSingle] which return the item
/// directly or wrapping it into a list.
/// `getSingle` and `SelectableUtils.watchSingle` which return the item
/// directly without wrapping it into a list.
///
/// If you use variables in your query (for instance with "?"), they will be
/// bound to the [variables] you specify on this query.
@ -280,8 +280,8 @@ mixin QueryEngine on DatabaseConnectionUser {
/// query once, use [Selectable.get]. For an auto-updating streams, set the
/// set of tables the ready [readsFrom] and use [Selectable.watch]. If you
/// know the query will never emit more than one row, you can also use
/// [Selectable.getSingle] and [Selectable.watchSingle] which return the item
/// directly or wrapping it into a list.
/// `getSingle` and `watchSingle` which return the item directly without
/// wrapping it into a list.
///
/// If you use variables in your query (for instance with "?"), they will be
/// bound to the [variables] you specify on this query.

View File

@ -121,7 +121,7 @@ class Migrator {
@experimental
Future<void> alterTable(TableMigration migration) async {
final foreignKeysEnabled =
(await _db.customSelect('PRAGMA foreign_keys').getSingle())!
(await _db.customSelect('PRAGMA foreign_keys').getSingle())
.readBool('foreign_keys');
if (foreignKeysEnabled) {

View File

@ -75,10 +75,14 @@ abstract class Selectable<T> {
/// Creates an auto-updating stream of the result that emits new items
/// whenever any table used in this statement changes.
Stream<List<T>> watch();
}
/// Defines extensions to only get a single element from a query, or to map
/// selectables.
extension SelectableUtils<T> on Selectable<T> {
/// Executes this statement, like [get], but only returns one value. If the
/// result too many values, this method will throw. If no row is returned,
/// `null` will be returned instead.
/// query returns no or too many rows, the returned future will complete with
/// an error.
///
/// {@template moor_single_query_expl}
/// Be aware that this operation won't put a limit clause on this statement,
@ -96,7 +100,22 @@ abstract class Selectable<T> {
/// one row, for instance because you used `limit(1)` or you know the `where`
/// clause will only allow one row.
/// {@endtemplate}
Future<T?> getSingle() async {
///
/// See also: [getSingleOrNull], which returns `null` instead of throwing if
/// the query completes with no rows.
Future<T> getSingle() async {
return (await get()).single;
}
/// Executes this statement, like [get], but only returns one value. If the
/// result too many values, this method will throw. If no row is returned,
/// `null` will be returned instead.
///
/// {@macro moor_single_query_expl}
///
/// See also: [getSingle], which can be used if the query will never evaluate
/// to exactly one row.
Future<T?> getSingleOrNull() async {
final list = await get();
final iterator = list.iterator;
@ -113,14 +132,25 @@ abstract class Selectable<T> {
/// Creates an auto-updating stream of this statement, similar to [watch].
/// However, it is assumed that the query will only emit one result, so
/// instead of returning a [Stream<List<T>>], this returns a [Stream<T>]. If
/// instead of returning a `Stream<List<T>>`, this returns a `Stream<T>`. If,
/// at any point, the query emits no or more than one rows, an error will be
/// added to the stream instead.
///
/// {@macro moor_single_query_expl}
Stream<T> watchSingle() {
return watch().transform(singleElements());
}
/// Creates an auto-updating stream of this statement, similar to [watch].
/// However, it is assumed that the query will only emit one result, so
/// instead of returning a `Stream<List<T>>`, this returns a `Stream<T?>`. If
/// the query emits more than one row at some point, an error will be emitted
/// to the stream instead. If the query emits zero rows at some point, `null`
/// will be added to the stream instead.
///
/// {@macro moor_single_query_expl}
Stream<T?> watchSingle() {
return watch().transform(singleElements());
Stream<T?> watchSingleOrNull() {
return watch().transform(singleElementsOrNull());
}
/// Maps this selectable by the [mapper] function.

View File

@ -1,8 +1,8 @@
import 'dart:async';
/// Transforms a stream of lists into a stream of single elements, assuming
/// that each list is a singleton.
StreamTransformer<List<T>, T?> singleElements<T>() {
/// that each list is a singleton or empty.
StreamTransformer<List<T>, T?> singleElementsOrNull<T>() {
return StreamTransformer.fromHandlers(handleData: (data, sink) {
try {
if (data.isEmpty) {
@ -16,3 +16,16 @@ StreamTransformer<List<T>, T?> singleElements<T>() {
}
});
}
/// Transforms a stream of lists into a stream of single elements, assuming
/// that each list is a singleton.
StreamTransformer<List<T>, T> singleElements<T>() {
return StreamTransformer.fromHandlers(handleData: (data, sink) {
try {
sink.add(data.single);
} catch (e) {
sink.addError(
StateError('Expected exactly one element, but got ${data.length}'));
}
});
}

View File

@ -31,6 +31,6 @@ void main() {
.equalsExp(Variable.withString('bar')));
final resultRow = await query.getSingle();
expect(resultRow!.read(arrayLengthExpr), 3);
expect(resultRow.read(arrayLengthExpr), 3);
}, tags: const ['integration']);
}

View File

@ -42,7 +42,7 @@ void main() {
final result = await (db.selectOnly(db.pureDefaults)..addColumns([expr]))
.getSingle();
return result!.read<bool?>(expr)!;
return result.read<bool?>(expr)!;
}
expect(
@ -72,7 +72,7 @@ void main() {
final result = await (db.selectOnly(db.pureDefaults)..addColumns([expr]))
.getSingle();
return result!.read<bool?>(expr)!;
return result.read<bool?>(expr)!;
}
test('multiLine', () {

View File

@ -1,5 +1,6 @@
@TestOn('vm')
import 'package:moor/ffi.dart';
import 'package:moor/moor.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:test/test.dart';
@ -19,7 +20,7 @@ void main() {
final db = TodoDb(executor);
final row = await db.customSelect('SELECT my_function() AS r;').getSingle();
expect(row!.readString('r'), 'hello from Dart');
expect(row.readString('r'), 'hello from Dart');
await db.close();
});
@ -31,7 +32,7 @@ void main() {
final db = TodoDb(executor);
final row = await db.customSelect('SELECT my_function() AS r;').getSingle();
expect(row!.readString('r'), 'hello from Dart');
expect(row.readString('r'), 'hello from Dart');
final nativeRow = existing.select('SELECT my_function() AS r;').single;
expect(nativeRow['r'], 'hello from Dart');

View File

@ -123,7 +123,7 @@ void _runTests(
test('stream queries work as expected', () async {
final initialCompanion = TodosTableCompanion.insert(content: 'my content');
final stream = database.select(database.todosTable).watchSingle();
final stream = database.select(database.todosTable).watchSingleOrNull();
await expectLater(stream, emits(null));
await database.into(database.todosTable).insert(initialCompanion);

View File

@ -120,7 +120,7 @@ void main() {
});
test('applies default parameter expressions when not set', () async {
await db.readDynamic().getSingle();
await db.readDynamic().getSingleOrNull();
verify(mock.runSelect('SELECT * FROM config WHERE (TRUE)', []));
});

View File

@ -134,10 +134,17 @@ void main() {
test('get once', () {
when(executor.runSelect('SELECT * FROM todos;', any))
.thenAnswer((_) => Future.value([_dataOfTodoEntry]));
expect(db.select(db.todosTable).getSingle(), completion(_todoEntry));
});
test('get once without rows', () {
when(executor.runSelect('SELECT * FROM todos;', any))
.thenAnswer((_) => Future.value([]));
expect(db.select(db.todosTable).getSingle(), throwsA(anything));
expect(db.select(db.todosTable).getSingleOrNull(), completion(isNull));
});
test('get multiple times', () {
final resultRows = <List<Map<String, dynamic>>>[
[_dataOfTodoEntry],
@ -150,7 +157,11 @@ void main() {
return Future.value(resultRows[_currentRow++]);
});
expectLater(db.select(db.todosTable).watchSingle(),
expectLater(
db.select(db.todosTable).watchSingle(),
emitsInOrder(
[_todoEntry, emitsError(anything), emitsError(anything)]));
expectLater(db.select(db.todosTable).watchSingleOrNull(),
emitsInOrder([_todoEntry, isNull, emitsError(anything)]));
db

View File

@ -56,7 +56,7 @@ void main() {
readsFrom: {db.users},
)
.map((r) => r.readInt('_mocked_'))
.watchSingle();
.watchSingleOrNull();
didSetUpStream.complete();
await makeUpdate.future;

View File

@ -18,9 +18,15 @@ void main() {
final controller = StreamController<List<int>>();
final stream = controller.stream.transform(singleElements());
expectLater(stream, emitsInOrder([1, emitsError(anything), 2, null]));
expectLater(stream,
emitsInOrder([1, emitsError(anything), 2, emitsError(anything)]));
controller..add([1])..add([2, 3])..add([2])..add([]);
controller.close();
});
test('singleElementsOrNull() emits null for empty data', () {
final stream = Stream.value([]);
expect(stream.transform(singleElementsOrNull()), emits(isNull));
});
}