From 067a33adec12384e22ee233dcd03d2c0b9a03892 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sat, 15 Jun 2019 10:56:29 +0200 Subject: [PATCH] Implement single() version for get() and watch() --- moor/lib/src/runtime/database.dart | 5 +- moor/lib/src/runtime/statements/query.dart | 42 ++++++++++++++ moor/lib/src/runtime/statements/select.dart | 43 ++++++++++---- moor/lib/src/utils/single_transformer.dart | 14 +++++ moor/test/select_test.dart | 59 ++++++++++++++++---- moor/test/utils/single_transformer_test.dart | 25 +++++++++ 6 files changed, 161 insertions(+), 27 deletions(-) create mode 100644 moor/lib/src/utils/single_transformer.dart create mode 100644 moor/test/utils/single_transformer_test.dart diff --git a/moor/lib/src/runtime/database.dart b/moor/lib/src/runtime/database.dart index 27b6a3dd..bad9c042 100644 --- a/moor/lib/src/runtime/database.dart +++ b/moor/lib/src/runtime/database.dart @@ -142,8 +142,7 @@ mixin QueryEngine on DatabaseConnectionUser { /// value. Future> customSelect(String query, {List variables = const []}) async { - return CustomSelectStatement(query, variables, {}, this) - .execute(); + return CustomSelectStatement(query, variables, {}, this).get(); } /// Creates a stream from a custom select statement.To use the variables, mark @@ -155,7 +154,7 @@ mixin QueryEngine on DatabaseConnectionUser { {List variables = const [], Set readsFrom}) { final tables = readsFrom ?? {}; final statement = CustomSelectStatement(query, variables, tables, this); - return createStream(statement.constructFetcher()); + return statement.watch(); } /// Executes [action] in a transaction, which means that all its queries and diff --git a/moor/lib/src/runtime/statements/query.dart b/moor/lib/src/runtime/statements/query.dart index 2d6ebe05..94e14a61 100644 --- a/moor/lib/src/runtime/statements/query.dart +++ b/moor/lib/src/runtime/statements/query.dart @@ -10,6 +10,7 @@ import 'package:moor/src/runtime/expressions/custom.dart'; import 'package:moor/src/runtime/expressions/expression.dart'; import 'package:moor/src/types/sql_types.dart'; import 'package:moor/src/runtime/structure/table_info.dart'; +import 'package:moor/src/utils/single_transformer.dart'; /// Statement that operates with data that already exists (select, delete, /// update). @@ -68,6 +69,47 @@ abstract class Query { } } +/// Abstract class for queries which can return one-time values or a stream +/// of values. +abstract class Selectable { + /// Executes this statement and returns the result. + Future> get(); + + /// Creates an auto-updating stream of the result that emits new items + /// whenever any table used in this statement changes. + Stream> watch(); + + /// Executes this statement, like [get], but only returns one value. If the + /// result has no or too many values, this method will throw. + /// + /// Be aware that this operation won't put a limit clause on this statement, + /// if that's needed you would have to do that yourself. + Future getSingle() async { + final list = await get(); + final iterator = list.iterator; + + if (!iterator.moveNext()) { + throw StateError('Expected exactly one result, but actually there were ' + 'none!'); + } + final element = iterator.current; + if (iterator.moveNext()) { + throw StateError('Expected exactly one result, but found more than one!'); + } + + return element; + } + + /// 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>], this returns a [Stream]. If + /// the query emits more than one row at some point, an error will be emitted + /// to the stream instead. + Stream watchSingle() { + return watch().transform(singleElements()); + } +} + mixin SingleTableQueryMixin on Query { void where(Expression filter(T tbl)) { final predicate = filter(table.asDslTable); diff --git a/moor/lib/src/runtime/statements/select.dart b/moor/lib/src/runtime/statements/select.dart index e0d930bf..2200bb83 100644 --- a/moor/lib/src/runtime/statements/select.dart +++ b/moor/lib/src/runtime/statements/select.dart @@ -14,7 +14,8 @@ import 'package:moor/src/runtime/structure/table_info.dart'; typedef OrderingTerm OrderClauseGenerator(T tbl); class JoinedSelectStatement - extends Query with LimitContainerMixin { + extends Query + with LimitContainerMixin, Selectable { JoinedSelectStatement( QueryEngine database, TableInfo table, this._joins) : super(database, table); @@ -97,8 +98,7 @@ class JoinedSelectStatement orderByExpr = OrderBy(terms); } - /// Creates an auto-updating stream of the result that emits new items - /// whenever any table of this statement changes. + @override Stream> watch() { final ctx = constructQuery(); final fetcher = QueryStreamFetcher>( @@ -110,7 +110,7 @@ class JoinedSelectStatement return database.createStream(fetcher); } - /// Executes this statement and returns the result. + @override Future> get() async { final ctx = constructQuery(); return _getWithQuery(ctx); @@ -143,7 +143,7 @@ class JoinedSelectStatement /// A select statement that doesn't use joins class SimpleSelectStatement extends Query - with SingleTableQueryMixin, LimitContainerMixin { + with SingleTableQueryMixin, LimitContainerMixin, Selectable { SimpleSelectStatement(QueryEngine database, TableInfo table) : super(database, table); @@ -155,7 +155,7 @@ class SimpleSelectStatement extends Query ctx.buffer.write('SELECT * FROM ${table.tableWithAlias}'); } - /// Loads and returns all results from this select query. + @override Future> get() async { final ctx = constructQuery(); return _getWithQuery(ctx); @@ -212,8 +212,7 @@ class SimpleSelectStatement extends Query orderByExpr = OrderBy(clauses.map((t) => t(table.asDslTable)).toList()); } - /// Creates an auto-updating stream that emits new items whenever this table - /// changes. + @override Stream> watch() { final query = constructQuery(); final fetcher = QueryStreamFetcher>( @@ -228,7 +227,7 @@ class SimpleSelectStatement extends Query /// A select statement that is constructed with a raw sql prepared statement /// instead of the high-level moor api. -class CustomSelectStatement { +class CustomSelectStatement with Selectable { /// Tables this select statement reads from. When turning this select query /// into an auto-updating stream, that stream will emit new items whenever /// any of these tables changes. @@ -242,12 +241,21 @@ class CustomSelectStatement { final List variables; final QueryEngine _db; - /// Constructs a new + /// Constructs a new custom select statement for the query, the variables, + /// the affected tables and the database. CustomSelectStatement(this.query, this.variables, this.tables, this._db); /// Constructs a fetcher for this query. The fetcher is responsible for /// updating a stream at the right moment. + @Deprecated( + 'There is no need to use this method. Please use watch() directly') QueryStreamFetcher> constructFetcher() { + return _constructFetcher(); + } + + /// Constructs a fetcher for this query. The fetcher is responsible for + /// updating a stream at the right moment. + QueryStreamFetcher> _constructFetcher() { final args = _mapArgs(); return QueryStreamFetcher>( @@ -257,11 +265,22 @@ class CustomSelectStatement { ); } - /// Executes this query and returns the result. - Future> execute() async { + @override + Future> get() async { return _executeWithMappedArgs(_mapArgs()); } + @override + Stream> watch() { + return _db.createStream(_constructFetcher()); + } + + /// Executes this query and returns the result. + @Deprecated('Use get() instead') + Future> execute() async { + return get(); + } + List _mapArgs() { final ctx = GenerationContext.fromDb(_db); return variables.map((v) => v.mapToSimpleValue(ctx)).toList(); diff --git a/moor/lib/src/utils/single_transformer.dart b/moor/lib/src/utils/single_transformer.dart new file mode 100644 index 00000000..63d66fd4 --- /dev/null +++ b/moor/lib/src/utils/single_transformer.dart @@ -0,0 +1,14 @@ +import 'dart:async'; + +/// Transforms a stream of lists into a stream of single elements, assuming +/// that each list is a singleton. +StreamTransformer, T> singleElements() { + return StreamTransformer.fromHandlers(handleData: (data, sink) { + try { + sink.add(data.single); + } catch (e) { + sink.addError( + StateError('Expected exactly one element, but got ${data.length}')); + } + }); +} diff --git a/moor/test/select_test.dart b/moor/test/select_test.dart index d849e191..00e17c46 100644 --- a/moor/test/select_test.dart +++ b/moor/test/select_test.dart @@ -6,6 +6,20 @@ import 'package:test_api/test_api.dart'; import 'data/tables/todos.dart'; import 'data/utils/mocks.dart'; +final _dataOfTodoEntry = { + 'id': 10, + 'title': 'A todo title', + 'content': 'Content', + 'category': 3 +}; + +final _todoEntry = TodoEntry( + id: 10, + title: 'A todo title', + content: 'Content', + category: 3, +); + void main() { TodoDb db; MockExecutor executor; @@ -78,20 +92,10 @@ void main() { group('SELECT results are parsed', () { test('when all fields are non-null', () { - final data = [ - {'id': 10, 'title': 'A todo title', 'content': 'Content', 'category': 3} - ]; - final resolved = TodoEntry( - id: 10, - title: 'A todo title', - content: 'Content', - category: 3, - ); - when(executor.runSelect('SELECT * FROM todos;', any)) - .thenAnswer((_) => Future.value(data)); + .thenAnswer((_) => Future.value([_dataOfTodoEntry])); - expect(db.select(db.todosTable).get(), completion([resolved])); + expect(db.select(db.todosTable).get(), completion([_todoEntry])); }); test('when some fields are null', () { @@ -116,4 +120,35 @@ void main() { expect(db.select(db.todosTable).get(), completion([resolved])); }); }); + + group('queries for a single row', () { + test('get once', () { + when(executor.runSelect('SELECT * FROM todos;', any)) + .thenAnswer((_) => Future.value([_dataOfTodoEntry])); + + expect(db.select(db.todosTable).getSingle(), completion(_todoEntry)); + }); + + test('get multiple times', () { + final resultRows = >>[ + [_dataOfTodoEntry], + [], + [_dataOfTodoEntry, _dataOfTodoEntry], + ]; + var _currentRow = 0; + + when(executor.runSelect('SELECT * FROM todos;', any)).thenAnswer((_) { + return Future.value(resultRows[_currentRow++]); + }); + + expectLater( + db.select(db.todosTable).watchSingle(), + emitsInOrder( + [_todoEntry, emitsError(anything), emitsError(anything)])); + + db + ..markTablesUpdated({db.todosTable}) + ..markTablesUpdated({db.todosTable}); + }); + }); } diff --git a/moor/test/utils/single_transformer_test.dart b/moor/test/utils/single_transformer_test.dart new file mode 100644 index 00000000..b046966b --- /dev/null +++ b/moor/test/utils/single_transformer_test.dart @@ -0,0 +1,25 @@ +import 'dart:async'; + +import 'package:moor/src/utils/single_transformer.dart'; +import 'package:test_api/test_api.dart'; + +void main() { + test('transforms simple values', () { + final controller = StreamController>(); + final stream = controller.stream.transform(singleElements()); + + expectLater(stream, emitsInOrder([1, 2, 3, 4])); + + controller..add([1])..add([2])..add([3])..add([4]); + }); + + test('emits errors for invalid lists', () { + final controller = StreamController>(); + final stream = controller.stream.transform(singleElements()); + + expectLater(stream, + emitsInOrder([1, emitsError(anything), 2, emitsError(anything)])); + + controller..add([1])..add([2, 3])..add([2])..add([]); + }); +}