diff --git a/README.md b/README.md index 72ad92d4..f43cdd9a 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,10 @@ import 'package:floor/floor.dart'; @dao abstract class PersonDao { @Query('SELECT * FROM Person') - Future> findAllPersons(); + Future> findAllPeople(); + + @Query('SELECT name FROM Person') + Stream> findAllPeopleName(); @Query('SELECT * FROM Person WHERE id = :id') Stream findPersonById(int id); diff --git a/docs/daos.md b/docs/daos.md index 992400ed..14d03184 100644 --- a/docs/daos.md +++ b/docs/daos.md @@ -7,7 +7,7 @@ DAO classes can use inherited methods by implementing and extending classes whil @dao abstract class PersonDao { @Query('SELECT * FROM Person') - Future> findAllPersons(); + Future> findAllPeople(); @Query('SELECT * FROM Person WHERE id = :id') Stream findPersonById(int id); @@ -20,8 +20,9 @@ abstract class PersonDao { ## Queries Method signatures turn into query methods by adding the `@Query()` annotation with the query in parenthesis to them. Be mindful about the correctness of your SQL statements as they are only partly validated while generating the code. -These queries have to return either a `Future` or a `Stream` of an entity or `void`. -Returning `Future` comes in handy whenever you want to delete the full content of a table, for instance. +These queries have to return either a `Future` or a `Stream` of an entity, Dart core type or `void`. +Retrieval of Dart Core types such as `String`, `double`, `int`, `double`, `Uint8List` can be used if you want to get all records from a certain column or return `COUNT` records in the table. +Returning `Future` comes in handy whenever you want to delete the full content of a table, for instance. Some query method examples can be seen in the following. A function returning a single item will return `null` when no matching row is found. @@ -36,17 +37,23 @@ Future findPersonById(int id); @Query('SELECT * FROM Person WHERE id = :id AND name = :name') Future findPersonByIdAndName(int id, String name); +@Query('SELECT COUNT(id) FROM Person') +Future getPeopleCount(); // fetch records count + +@Query('SELECT name FROM Person') +Future> getAllPeopleNames(); // fetch all records from one column + @Query('SELECT * FROM Person') -Future> findAllPersons(); // select multiple items +Future> findAllPeople(); // select multiple items @Query('SELECT * FROM Person') -Stream> findAllPersonsAsStream(); // stream return +Stream> findAllPeopleAsStream(); // stream return @Query('DELETE FROM Person') -Future deleteAllPersons(); // query without returning an entity +Future deleteAllPeople(); // query without returning an entity @Query('SELECT * FROM Person WHERE id IN (:ids)') -Future> findPersonsWithIds(List ids); // query with IN clause +Future> findPeopleWithIds(List ids); // query with IN clause ``` Query arguments, when using SQLite's `LIKE` operator, have to be supplied by the input of a method. @@ -55,11 +62,11 @@ It's not possible to define a pattern matching argument like `%foo%` in the quer ```dart // dao @Query('SELECT * FROM Person WHERE name LIKE :name') -Future> findPersonsWithNamesLike(String name); +Future> findPeopleWithNamesLike(String name); // usage final name = '%foo%'; -await dao.findPersonsWithNamesLike(name); +await dao.findPeopleWithNamesLike(name); ``` ## Data Changes @@ -81,7 +88,7 @@ These methods can return a `Future` of either `void`, `int` or `List`. Future insertPerson(Person person); @insert -Future> insertPersons(List persons); +Future> insertPeople(List people); ``` ### Update @@ -98,7 +105,7 @@ These methods can return a `Future` of either `void` or `int`. Future updatePerson(Person person); @update -Future updatePersons(List persons); +Future updatePeople(List people); ``` ### Delete @@ -113,7 +120,7 @@ These methods can return a `Future` of either `void` or `int`. Future deletePerson(Person person); @delete -Future deletePersons(List persons); +Future deletePeople(List people); ``` ## Streams @@ -135,12 +142,12 @@ abstract class PersonDao { Stream findPersonByIdAsStream(int id); @Query('SELECT * FROM Person') - Stream> findAllPersonsAsStream(); + Stream> findAllPeopleAsStream(); } // usage StreamBuilder>( - stream: dao.findAllPersonsAsStream(), + stream: dao.findAllPeopleAsStream(), builder: (BuildContext context, AsyncSnapshot> snapshot) { // do something with the values here }, @@ -160,9 +167,9 @@ It's also required to add the `async` modifier. These methods have to return a ` ```dart @transaction -Future replacePersons(List persons) async { - await deleteAllPersons(); - await insertPersons(persons); +Future replacePeople(List people) async { + await deleteAllPeople(); + await insertPeople(people); } ``` diff --git a/example/lib/database.g.dart b/example/lib/database.g.dart index a86c326f..806f55cd 100644 --- a/example/lib/database.g.dart +++ b/example/lib/database.g.dart @@ -184,7 +184,15 @@ class _$TaskDao extends TaskDao { row['message'] as String, _dateTimeConverter.decode(row['timestamp'] as int), TaskType.values[row['type'] as int]), - queryableName: 'Task', + queryableName: 'task', + isView: false); + } + + @override + Stream findUniqueMessagesCountAsStream() { + return _queryAdapter.queryStream('SELECT DISTINCT COUNT(message) FROM task', + mapper: (Map row) => row.values.first as int, + queryableName: 'task', isView: false); } @@ -198,7 +206,7 @@ class _$TaskDao extends TaskDao { _dateTimeConverter.decode(row['timestamp'] as int), TaskType.values[row['type'] as int]), arguments: [type.index], - queryableName: 'Task', + queryableName: 'task', isView: false); } diff --git a/example/lib/main.dart b/example/lib/main.dart index 3dfd4ff0..d252f654 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -54,6 +54,13 @@ class TasksWidgetState extends State { return Scaffold( appBar: AppBar( title: Text(widget.title), + centerTitle: true, + leading: Align( + alignment: Alignment.center, + child: StreamBuilder( + stream: widget.dao.findUniqueMessagesCountAsStream(), + builder: (_, snapshot) => Text('count: ${snapshot.data ?? 0}')), + ), actions: [ PopupMenuButton( itemBuilder: (context) { diff --git a/example/lib/task_dao.dart b/example/lib/task_dao.dart index 6d4f14c3..9d44e463 100644 --- a/example/lib/task_dao.dart +++ b/example/lib/task_dao.dart @@ -12,6 +12,9 @@ abstract class TaskDao { @Query('SELECT * FROM task') Stream> findAllTasksAsStream(); + @Query('SELECT DISTINCT COUNT(message) FROM task') + Stream findUniqueMessagesCountAsStream(); + @Query('SELECT * FROM task WHERE type = :type') Stream> findAllTasksByTypeAsStream(TaskType type); diff --git a/floor/lib/src/adapter/query_adapter.dart b/floor/lib/src/adapter/query_adapter.dart index d2bbdb11..213bd148 100644 --- a/floor/lib/src/adapter/query_adapter.dart +++ b/floor/lib/src/adapter/query_adapter.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:floor/src/util/string_utils.dart'; import 'package:sqflite/sqflite.dart'; /// This class knows how to execute database queries. @@ -73,7 +74,8 @@ class QueryAdapter { // listen on all updates if the stream is on a view, only listen to the // name of the table if the stream is on a entity. final subscription = changeListener.stream - .where((updatedTable) => updatedTable == queryableName || isView) + .where((updatedTable) => + isView || updatedTable.equals(queryableName, ignoreCase: true)) .listen( (_) async => executeQueryAndNotifyController(), onDone: () => controller.close(), @@ -105,7 +107,8 @@ class QueryAdapter { // Views listen on all events, Entities only on events that changed the same entity. final subscription = changeListener.stream - .where((updatedTable) => isView || updatedTable == queryableName) + .where((updatedTable) => + isView || updatedTable.equals(queryableName, ignoreCase: true)) .listen( (_) async => executeQueryAndNotifyController(), onDone: () => controller.close(), diff --git a/floor/lib/src/util/string_utils.dart b/floor/lib/src/util/string_utils.dart new file mode 100644 index 00000000..82b29660 --- /dev/null +++ b/floor/lib/src/util/string_utils.dart @@ -0,0 +1,5 @@ +extension StringExt on String { + bool equals(String other, {bool ignoreCase = false}) { + return ignoreCase ? toLowerCase() == other.toLowerCase() : this == other; + } +} diff --git a/floor/test/integration/dao/person_dao.dart b/floor/test/integration/dao/person_dao.dart index f4cba460..89373073 100644 --- a/floor/test/integration/dao/person_dao.dart +++ b/floor/test/integration/dao/person_dao.dart @@ -17,6 +17,9 @@ abstract class PersonDao { @Query('SELECT * FROM person WHERE id = :id') Stream findPersonByIdAsStream(int id); + @Query('SELECT DISTINCT COUNT(id) FROM person') + Stream uniqueRecordsCountAsStream(); + @Query('SELECT * FROM person WHERE id = :id AND custom_name = :name') Future findPersonByIdAndName(int id, String name); @@ -89,6 +92,6 @@ abstract class PersonDao { Future deleteAllPersons(); // Used in regression test for Streams on Entities with update methods in other Dao - @Query('SELECT * FROM Dog WHERE owner_id = :id') + @Query('SELECT * FROM dog WHERE owner_id = :id') Stream> findAllDogsOfPersonAsStream(int id); } diff --git a/floor/test/integration/stream_query_test.dart b/floor/test/integration/stream_query_test.dart index 26d3b99e..bb4f39b7 100644 --- a/floor/test/integration/stream_query_test.dart +++ b/floor/test/integration/stream_query_test.dart @@ -60,6 +60,18 @@ void main() { await personDao.insertPersons(persons2); expect(actual, emits(persons + persons2)); }); + + test('unique records count', () async { + final persons = [Person(1, 'Simon'), Person(2, 'Frank')]; + final persons2 = [Person(3, 'Paul'), Person(4, 'George')]; + await personDao.insertPersons(persons); + + final actual = personDao.uniqueRecordsCountAsStream(); + expect(actual, emits(persons.length)); + + await personDao.insertPersons(persons2); + expect(actual, emits(persons.length + persons2.length)); + }); }); group('update change', () { diff --git a/floor_generator/lib/misc/extension/string_extension.dart b/floor_generator/lib/misc/extension/string_extension.dart index fdd80c4d..e7d87f95 100644 --- a/floor_generator/lib/misc/extension/string_extension.dart +++ b/floor_generator/lib/misc/extension/string_extension.dart @@ -1,3 +1,8 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:floor_generator/misc/extension/dart_type_extension.dart'; +import 'package:floor_generator/misc/type_utils.dart'; +import 'package:source_gen/source_gen.dart'; import 'package:strings/strings.dart'; extension StringExtension on String { @@ -54,3 +59,41 @@ extension NullableStringExtension on String? { } } } + +extension CastStringExtension on String { + String cast(DartType dartType, Element? parameterElement, + {bool withNullability = true}) { + if (dartType.isDartCoreBool) { + final booleanDeserializer = '($this as int) != 0'; + if (dartType.isNullable && withNullability) { + // if the value is null, return null + // if the value is not null, interpret 1 as true and 0 as false + return '$this == null ? null : $booleanDeserializer'; + } else { + return booleanDeserializer; + } + } else if (dartType.isEnumType) { + final typeString = dartType.getDisplayString(withNullability: false); + final enumDeserializer = '$typeString.values[$this as int]'; + if (dartType.isNullable && withNullability) { + return '$this == null ? null : $enumDeserializer'; + } else { + return enumDeserializer; + } + } else if (dartType.isDartCoreString || + dartType.isDartCoreInt || + dartType.isUint8List || + dartType.isDartCoreDouble) { + final typeString = dartType.getDisplayString( + withNullability: withNullability, + ); + return '$this as $typeString'; + } else { + throw InvalidGenerationSourceError( + 'Trying to convert unsupported type $dartType.', + todo: 'Consider adding a type converter.', + element: parameterElement, + ); + } + } +} diff --git a/floor_generator/lib/processor/error/query_method_writer_error.dart b/floor_generator/lib/processor/error/query_method_writer_error.dart new file mode 100644 index 00000000..aa304aac --- /dev/null +++ b/floor_generator/lib/processor/error/query_method_writer_error.dart @@ -0,0 +1,18 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:source_gen/source_gen.dart'; + +class QueryMethodWriterError { + final MethodElement _methodElement; + + QueryMethodWriterError(final MethodElement methodElement) + : _methodElement = methodElement; + + InvalidGenerationSourceError queryMethodReturnType() { + return InvalidGenerationSourceError( + 'Can not define return type', + todo: + 'Add supported return type to your query. https://pinchbv.github.io/floor/daos/#queries', + element: _methodElement, + ); + } +} diff --git a/floor_generator/lib/processor/queryable_processor.dart b/floor_generator/lib/processor/queryable_processor.dart index 8716369d..b6447717 100644 --- a/floor_generator/lib/processor/queryable_processor.dart +++ b/floor_generator/lib/processor/queryable_processor.dart @@ -1,8 +1,6 @@ import 'package:analyzer/dart/element/element.dart'; -import 'package:analyzer/dart/element/type.dart'; import 'package:collection/collection.dart'; import 'package:floor_annotation/floor_annotation.dart' as annotations; -import 'package:floor_generator/misc/extension/dart_type_extension.dart'; import 'package:floor_generator/misc/extension/set_extension.dart'; import 'package:floor_generator/misc/extension/string_extension.dart'; import 'package:floor_generator/misc/extension/type_converter_element_extension.dart'; @@ -15,7 +13,6 @@ import 'package:floor_generator/value_object/field.dart'; import 'package:floor_generator/value_object/queryable.dart'; import 'package:floor_generator/value_object/type_converter.dart'; import 'package:meta/meta.dart'; -import 'package:source_gen/source_gen.dart'; abstract class QueryableProcessor extends Processor { final QueryableProcessorError _queryableProcessorError; @@ -106,41 +103,6 @@ abstract class QueryableProcessor extends Processor { } } -extension on String { - String cast(DartType dartType, ParameterElement parameterElement) { - if (dartType.isDartCoreBool) { - final booleanDeserializer = '($this as int) != 0'; - if (dartType.isNullable) { - // if the value is null, return null - // if the value is not null, interpret 1 as true and 0 as false - return '$this == null ? null : $booleanDeserializer'; - } else { - return booleanDeserializer; - } - } else if (dartType.isEnumType) { - final typeString = dartType.getDisplayString(withNullability: false); - final enumDeserializer = '$typeString.values[$this as int]'; - if (dartType.isNullable) { - return '$this == null ? null : $enumDeserializer'; - } else { - return enumDeserializer; - } - } else if (dartType.isDartCoreString || - dartType.isDartCoreInt || - dartType.isUint8List || - dartType.isDartCoreDouble) { - final typeString = dartType.getDisplayString(withNullability: true); - return '$this as $typeString'; - } else { - throw InvalidGenerationSourceError( - 'Trying to convert unsupported type $dartType.', - todo: 'Consider adding a type converter.', - element: parameterElement, - ); - } - } -} - extension on FieldElement { bool shouldBeIncluded() { final isIgnored = hasAnnotation(annotations.ignore.runtimeType); diff --git a/floor_generator/lib/writer/query_method_writer.dart b/floor_generator/lib/writer/query_method_writer.dart index f8f72b2a..a1a20fa6 100644 --- a/floor_generator/lib/writer/query_method_writer.dart +++ b/floor_generator/lib/writer/query_method_writer.dart @@ -6,9 +6,11 @@ import 'package:floor_generator/misc/annotation_expression.dart'; import 'package:floor_generator/misc/extension/string_extension.dart'; import 'package:floor_generator/misc/extension/type_converters_extension.dart'; import 'package:floor_generator/misc/type_utils.dart'; +import 'package:floor_generator/processor/error/query_method_writer_error.dart'; import 'package:floor_generator/value_object/query.dart'; import 'package:floor_generator/value_object/query_method.dart'; import 'package:floor_generator/value_object/queryable.dart'; +import 'package:floor_generator/value_object/type_converter.dart'; import 'package:floor_generator/value_object/view.dart'; import 'package:floor_generator/writer/writer.dart'; @@ -60,12 +62,10 @@ class QueryMethodWriter implements Writer { final arguments = _generateArguments(); final query = _generateQueryString(); - final queryable = _queryMethod.queryable; - // null queryable implies void-returning query method - if (_queryMethod.returnsVoid || queryable == null) { + if (_queryMethod.returnsVoid) { _methodBody.write(_generateNoReturnQuery(query, arguments)); } else { - _methodBody.write(_generateQuery(query, arguments, queryable)); + _methodBody.write(_generateQuery(query, arguments)); } return _methodBody.toString(); @@ -169,12 +169,23 @@ class QueryMethodWriter implements Writer { return 'await _queryAdapter.queryNoReturn($parameters);'; } - String _generateQuery( - final String query, - final String? arguments, - final Queryable queryable, - ) { - final mapper = _generateMapper(queryable); + String _generateQuery(final String query, final String? arguments) { + final queryable = _queryMethod.queryable; + final returnType = _queryMethod.flattenedReturnType; + final converter = _queryMethod.typeConverters.getClosestOrNull(returnType); + + String? mapper; + if (queryable != null) { + mapper = _generateMapper(queryable); + } else if (returnType.isDefaultSqlType || returnType.isEnumType) { + mapper = _generateDartCoreMapper(returnType); + } else if (converter != null) { + mapper = _generateConverterMapper(converter); + } else { + throw QueryMethodWriterError(_queryMethod.methodElement) + .queryMethodReturnType(); + } + final parameters = StringBuffer(query)..write(', mapper: $mapper'); if (arguments != null) parameters.write(', arguments: $arguments'); @@ -182,7 +193,7 @@ class QueryMethodWriter implements Writer { // for streamed queries, we need to provide the queryable to know which // entity to monitor. For views, we monitor all entities. parameters - ..write(", queryableName: '${queryable.name}'") + ..write(", queryableName: '${_parseTableName(query)}'") ..write(', isView: ${queryable is View}'); } @@ -191,6 +202,30 @@ class QueryMethodWriter implements Writer { return 'return _queryAdapter.query$list$stream($parameters);'; } + + String _generateDartCoreMapper(final DartType returnType) { + final castedDatabaseValue = 'row.values.first'.cast( + returnType, + returnType.element, + withNullability: false, + ); + return '(Map row) => $castedDatabaseValue'; + } + + String _generateConverterMapper(final TypeConverter typeConverter) { + final castedDatabaseValue = 'row.values.first'.cast( + typeConverter.databaseType, + typeConverter.fieldType.element, + ); + return '(Map row) => _${typeConverter.name.decapitalize()}.decode($castedDatabaseValue)'; + } + + String _parseTableName(String query) { + return RegExp(r'(?<=FROM )\w+', caseSensitive: false) + .firstMatch(query) + ?.group(0) ?? + 'no_table_name'; + } } String _generateMapper(Queryable queryable) { diff --git a/floor_generator/test/processor/processor_error_test.dart b/floor_generator/test/processor/processor_error_test.dart index 84eedd39..405cd889 100644 --- a/floor_generator/test/processor/processor_error_test.dart +++ b/floor_generator/test/processor/processor_error_test.dart @@ -16,9 +16,9 @@ void main() { expect( error.toString(), equals('mymessage mytodo\n' - 'package:_resolve_source/_resolve_source.dart:8:20\n' + 'package:_resolve_source/_resolve_source.dart:9:20\n' ' ╷\n' - '8 │ Future insertPerson(Person person);\n' + '9 │ Future insertPerson(Person person);\n' ' │ ^^^^^^^^^^^^\n' ' ╵')); }); diff --git a/floor_generator/test/test_utils.dart b/floor_generator/test/test_utils.dart index fc19215f..5858f00d 100644 --- a/floor_generator/test/test_utils.dart +++ b/floor_generator/test/test_utils.dart @@ -162,6 +162,7 @@ Future createDao(final String methodSignature) async { library test; import 'package:floor_annotation/floor_annotation.dart'; + import 'dart:typed_data'; @dao abstract class PersonDao { @@ -243,6 +244,7 @@ Future getPersonEntity() async { library test; import 'package:floor_annotation/floor_annotation.dart'; + import 'dart:typed_data'; $_personEntity ''', (resolver) async { @@ -264,6 +266,7 @@ extension StringExtension on String { library test; import 'package:floor_annotation/floor_annotation.dart'; + import 'dart:typed_data'; @dao abstract class PersonDao { @@ -289,8 +292,14 @@ const _personEntity = ''' final int id; final String name; + + final double weight; + + final bool admin; + + final Uint8List avatar; - Person(this.id, this.name); + Person(this.id, this.name, this.weight, this.admin, this.avatar); } '''; diff --git a/floor_generator/test/writer/dao_writer_test.dart b/floor_generator/test/writer/dao_writer_test.dart index 38154e44..cbf940ce 100644 --- a/floor_generator/test/writer/dao_writer_test.dart +++ b/floor_generator/test/writer/dao_writer_test.dart @@ -164,7 +164,7 @@ void main() { @override Stream> findAllPersonsAsStream() { - return _queryAdapter.queryListStream('SELECT * FROM person', mapper: (Map row) => Person(row['id'] as int, row['name'] as String), queryableName: 'Person', isView: false); + return _queryAdapter.queryListStream('SELECT * FROM person', mapper: (Map row) => Person(row['id'] as int, row['name'] as String), queryableName: 'person', isView: false); } @override diff --git a/floor_generator/test/writer/query_method_writer_test.dart b/floor_generator/test/writer/query_method_writer_test.dart index ce479025..caac2306 100644 --- a/floor_generator/test/writer/query_method_writer_test.dart +++ b/floor_generator/test/writer/query_method_writer_test.dart @@ -60,7 +60,295 @@ void main() { expect(actual, equalsDart(r''' @override Future findById(int id) async { - return _queryAdapter.query('SELECT * FROM Person WHERE id = ?1', mapper: (Map row) => Person(row['id'] as int, row['name'] as String), arguments: [id]); + return _queryAdapter.query('SELECT * FROM Person WHERE id = ?1', mapper: (Map row) => Person(row['id'] as int, row['name'] as String, row['weight'] as double, (row['admin'] as int) != 0, row['avatar'] as Uint8List), arguments: [id]); + } + ''')); + }); + + test('query return int type', () async { + final queryMethod = await _createQueryMethod(''' + @Query('SELECT COUNT(id) FROM Person') + Future getUnique(); + '''); + + final actual = QueryMethodWriter(queryMethod).write(); + + expect(actual, equalsDart(r''' + @override + Future getUnique() async { + return _queryAdapter.query('SELECT COUNT(id) FROM Person', mapper: (Map row) => row.values.first as int); + } + ''')); + }); + + test('query return List type', () async { + final queryMethod = await _createQueryMethod(''' + @Query('SELECT id FROM Person') + Future> getPeopleIdList(); + '''); + + final actual = QueryMethodWriter(queryMethod).write(); + + expect(actual, equalsDart(r''' + @override + Future> getPeopleIdList() async { + return _queryAdapter.queryList('SELECT id FROM Person', mapper: (Map row) => row.values.first as int); + } + ''')); + }); + + test('query return List type stream', () async { + final queryMethod = await _createQueryMethod(''' + @Query('SELECT id FROM Person') + Stream> getPeopleIdListAsStream(); + '''); + + final actual = QueryMethodWriter(queryMethod).write(); + + expect(actual, equalsDart(r''' + @override + Stream> getPeopleIdListAsStream() { + return _queryAdapter.queryListStream('SELECT id FROM Person', mapper: (Map row) => row.values.first as int, queryableName: 'Person', isView: false); + } + ''')); + }); + + test('query return double type', () async { + final queryMethod = await _createQueryMethod(''' + @Query('SELECT weight FROM Person LIMIT 1') + Future getFirstPersonWeight(); + '''); + + final actual = QueryMethodWriter(queryMethod).write(); + + expect(actual, equalsDart(r''' + @override + Future getFirstPersonWeight() async { + return _queryAdapter.query('SELECT weight FROM Person LIMIT 1', mapper: (Map row) => row.values.first as double); + } + ''')); + }); + + test('query return List type', () async { + final queryMethod = await _createQueryMethod(''' + @Query('SELECT weight FROM Person') + Future> getPeopleWeightList(); + '''); + + final actual = QueryMethodWriter(queryMethod).write(); + + expect(actual, equalsDart(r''' + @override + Future> getPeopleWeightList() async { + return _queryAdapter.queryList('SELECT weight FROM Person', mapper: (Map row) => row.values.first as double); + } + ''')); + }); + + test('query return List type stream', () async { + final queryMethod = await _createQueryMethod(''' + @Query('SELECT weight FROM Person') + Stream> getPeopleWeightListAsStream(); + '''); + + final actual = QueryMethodWriter(queryMethod).write(); + + expect(actual, equalsDart(r''' + @override + Stream> getPeopleWeightListAsStream() { + return _queryAdapter.queryListStream('SELECT weight FROM Person', mapper: (Map row) => row.values.first as double, queryableName: 'Person', isView: false); + } + ''')); + }); + + test('query return bool type', () async { + final queryMethod = await _createQueryMethod(''' + @Query('SELECT admin FROM Person LIMIT 1') + Future getAdminValue(); + '''); + + final actual = QueryMethodWriter(queryMethod).write(); + + expect(actual, equalsDart(r''' + @override + Future getAdminValue() async { + return _queryAdapter.query('SELECT admin FROM Person LIMIT 1', mapper: (Map row) => (row.values.first as int) != 0); + } + ''')); + }); + + test('query return List type', () async { + final queryMethod = await _createQueryMethod(''' + @Query('SELECT admin FROM Person') + Future> getAdminValueList(); + '''); + + final actual = QueryMethodWriter(queryMethod).write(); + + expect(actual, equalsDart(r''' + @override + Future> getAdminValueList() async { + return _queryAdapter.queryList('SELECT admin FROM Person', mapper: (Map row) => (row.values.first as int) != 0); + } + ''')); + }); + + test('query return List type stream', () async { + final queryMethod = await _createQueryMethod(''' + @Query('SELECT admin FROM Person') + Stream> getAdminValueListAsStream(); + '''); + + final actual = QueryMethodWriter(queryMethod).write(); + + expect(actual, equalsDart(r''' + @override + Stream> getAdminValueListAsStream() { + return _queryAdapter.queryListStream('SELECT admin FROM Person', mapper: (Map row) => (row.values.first as int) != 0, queryableName: 'Person', isView: false); + } + ''')); + }); + + test('query return String type', () async { + final queryMethod = await _createQueryMethod(''' + @Query('SELECT name FROM Person LIMIT 1') + Future getFirstPersonName(); + '''); + + final actual = QueryMethodWriter(queryMethod).write(); + + expect(actual, equalsDart(r''' + @override + Future getFirstPersonName() async { + return _queryAdapter.query('SELECT name FROM Person LIMIT 1', mapper: (Map row) => row.values.first as String); + } + ''')); + }); + + test('query return List type', () async { + final queryMethod = await _createQueryMethod(''' + @Query('SELECT name FROM Person') + Future> getPeopleNameList(); + '''); + + final actual = QueryMethodWriter(queryMethod).write(); + + expect(actual, equalsDart(r''' + @override + Future> getPeopleNameList() async { + return _queryAdapter.queryList('SELECT name FROM Person', mapper: (Map row) => row.values.first as String); + } + ''')); + }); + + test('query return List type stream', () async { + final queryMethod = await _createQueryMethod(''' + @Query('SELECT name FROM Person') + Stream> getPeopleNameListAsStream(); + '''); + + final actual = QueryMethodWriter(queryMethod).write(); + + expect(actual, equalsDart(r''' + @override + Stream> getPeopleNameListAsStream() { + return _queryAdapter.queryListStream('SELECT name FROM Person', mapper: (Map row) => row.values.first as String, queryableName: 'Person', isView: false); + } + ''')); + }); + + test('query return Uint8List type', () async { + final queryMethod = await _createQueryMethod(''' + @Query('SELECT avatar FROM Person LIMIT 1') + Future getFirstPersonAvatar(); + '''); + + final actual = QueryMethodWriter(queryMethod).write(); + + expect(actual, equalsDart(r''' + @override + Future getFirstPersonAvatar() async { + return _queryAdapter.query('SELECT avatar FROM Person LIMIT 1', mapper: (Map row) => row.values.first as Uint8List); + } + ''')); + }); + + test('query return List type', () async { + final queryMethod = await _createQueryMethod(''' + @Query('SELECT avatar FROM Person') + Future> getPeopleAvatarList(); + '''); + + final actual = QueryMethodWriter(queryMethod).write(); + + expect(actual, equalsDart(r''' + @override + Future> getPeopleAvatarList() async { + return _queryAdapter.queryList('SELECT avatar FROM Person', mapper: (Map row) => row.values.first as Uint8List); + } + ''')); + }); + + test('query return List type stream', () async { + final queryMethod = await _createQueryMethod(''' + @Query('SELECT avatar FROM Person') + Stream> getPeopleAvatarAsStream(); + '''); + + final actual = QueryMethodWriter(queryMethod).write(); + + expect(actual, equalsDart(r''' + @override + Stream> getPeopleAvatarAsStream() { + return _queryAdapter.queryListStream('SELECT avatar FROM Person', mapper: (Map row) => row.values.first as Uint8List, queryableName: 'Person', isView: false); + } + ''')); + }); + + test('query return CharacterType type', () async { + final queryMethod = await _createQueryMethod(''' + @Query('SELECT someType FROM Person LIMIT 1') + Future getFirstPersonCharacter(); + '''); + + final actual = QueryMethodWriter(queryMethod).write(); + + expect(actual, equalsDart(r''' + @override + Future getFirstPersonCharacter() async { + return _queryAdapter.query('SELECT someType FROM Person LIMIT 1', mapper: (Map row) => CharacterType.values[row.values.first as int]); + } + ''')); + }); + + test('query return List type', () async { + final queryMethod = await _createQueryMethod(''' + @Query('SELECT someType FROM Person') + Future> getPeopleCharacterList(); + '''); + + final actual = QueryMethodWriter(queryMethod).write(); + + expect(actual, equalsDart(r''' + @override + Future> getPeopleCharacterList() async { + return _queryAdapter.queryList('SELECT someType FROM Person', mapper: (Map row) => CharacterType.values[row.values.first as int]); + } + ''')); + }); + + test('query return List type stream', () async { + final queryMethod = await _createQueryMethod(''' + @Query('SELECT someType FROM Person') + Stream> getPeopleCharacterAsStream(); + '''); + + final actual = QueryMethodWriter(queryMethod).write(); + + expect(actual, equalsDart(r''' + @override + Stream> getPeopleCharacterAsStream() { + return _queryAdapter.queryListStream('SELECT someType FROM Person', mapper: (Map row) => CharacterType.values[row.values.first as int], queryableName: 'Person', isView: false); } ''')); }); @@ -164,6 +452,30 @@ void main() { } ''')); }); + + test('generates method with the type converted return type', () async { + final typeConverter = TypeConverter( + 'DateTimeConverter', + await dateTimeDartType, + await intDartType, + TypeConverterScope.database, + ); + + final queryMethod = await ''' + @Query('SELECT timestamp FROM Person') + Future> findTimestampList(); + ''' + .asOrderQueryMethod({typeConverter}); + + final actual = QueryMethodWriter(queryMethod).write(); + + expect(actual, equalsDart(r''' + @override + Future> findTimestampList() async { + return _queryAdapter.queryList('SELECT timestamp FROM Person', mapper: (Map row) => _dateTimeConverter.decode(row.values.first as int)); + } + ''')); + }); }); test( @@ -231,7 +543,7 @@ void main() { Future> findWithFlag(bool flag) async { return _queryAdapter.queryList('SELECT * FROM Person WHERE flag = ?1', mapper: (Map row) => - Person(row['id'] as int, row['name'] as String), + Person(row['id'] as int, row['name'] as String, row['weight'] as double, (row['admin'] as int) != 0, row['avatar'] as Uint8List), arguments: [flag ? 1 : 0]); } ''')); @@ -251,7 +563,7 @@ void main() { return _queryAdapter.queryList( 'SELECT * FROM Person WHERE characterType = ?1', mapper: (Map row) => - Person(row['id'] as int, row['name'] as String), + Person(row['id'] as int, row['name'] as String, row['weight'] as double, (row['admin'] as int) != 0, row['avatar'] as Uint8List), arguments: [type.index]); } ''')); @@ -273,7 +585,7 @@ void main() { ) async { return _queryAdapter.query('SELECT * FROM Person WHERE id = ?1 AND name = ?2', mapper: (Map row) => - Person(row['id'] as int, row['name'] as String), + Person(row['id'] as int, row['name'] as String, row['weight'] as double, (row['admin'] as int) != 0, row['avatar'] as Uint8List), arguments: [id, name]); } ''')); @@ -296,7 +608,7 @@ void main() { ) async { return _queryAdapter.query( 'SELECT * FROM Person WHERE foo = ?3 AND id = ?1 AND name = ?2 AND name = ?3', - mapper: (Map row) => Person(row['id'] as int, row['name'] as String), + mapper: (Map row) => Person(row['id'] as int, row['name'] as String, row['weight'] as double, (row['admin'] as int) != 0, row['avatar'] as Uint8List), arguments: [id, name, bar]); } ''')); @@ -315,7 +627,7 @@ void main() { Future> findAll() async { return _queryAdapter.queryList('SELECT * FROM Person', mapper: (Map row) => - Person(row['id'] as int, row['name'] as String)); + Person(row['id'] as int, row['name'] as String, row['weight'] as double, (row['admin'] as int) != 0, row['avatar'] as Uint8List)); } ''')); }); @@ -333,7 +645,7 @@ void main() { Stream findByIdAsStream(int id) { return _queryAdapter.queryStream('SELECT * FROM Person WHERE id = ?1', mapper: (Map row) => - Person(row['id'] as int, row['name'] as String), + Person(row['id'] as int, row['name'] as String, row['weight'] as double, (row['admin'] as int) != 0, row['avatar'] as Uint8List), arguments: [id], queryableName: 'Person', isView: false); @@ -354,7 +666,7 @@ void main() { Stream> findAllAsStream() { return _queryAdapter.queryListStream( 'SELECT * FROM Person', - mapper: (Map row) => Person(row['id'] as int, row['name'] as String), + mapper: (Map row) => Person(row['id'] as int, row['name'] as String, row['weight'] as double, (row['admin'] as int) != 0, row['avatar'] as Uint8List), queryableName: 'Person', isView: false); } ''')); @@ -393,7 +705,7 @@ void main() { const offset = 1; final _sqliteVariablesForIds=Iterable.generate(ids.length, (i)=>'?${i+offset}').join(','); return _queryAdapter.queryList('SELECT * FROM Person WHERE id IN (' + _sqliteVariablesForIds + ')', - mapper: (Map row) => Person(row['id'] as int, row['name'] as String), + mapper: (Map row) => Person(row['id'] as int, row['name'] as String, row['weight'] as double, (row['admin'] as int) != 0, row['avatar'] as Uint8List), arguments: [...ids]); } ''')); @@ -413,7 +725,7 @@ void main() { const offset = 1; final _sqliteVariablesForIds=Iterable.generate(ids.length, (i)=>'?${i+offset}').join(','); return _queryAdapter.queryList('SELECT * FROM Person WHERE id IN(' + _sqliteVariablesForIds + ')', - mapper: (Map row) => Person(row['id'] as int, row['name'] as String), + mapper: (Map row) => Person(row['id'] as int, row['name'] as String, row['weight'] as double, (row['admin'] as int) != 0, row['avatar'] as Uint8List), arguments: [...ids]); } ''')); @@ -446,7 +758,7 @@ void main() { _sqliteVariablesForIdx + ')', mapper: (Map row) => - Person(row['id'] as int, row['name'] as String), + Person(row['id'] as int, row['name'] as String, row['weight'] as double, (row['admin'] as int) != 0, row['avatar'] as Uint8List), arguments: [...ids, ...idx]); } ''')); @@ -485,7 +797,7 @@ void main() { _sqliteVariablesForIds + ') AND bar = ?2 OR name = ?1', mapper: (Map row) => - Person(row['id'] as int, row['name'] as String), + Person(row['id'] as int, row['name'] as String, row['weight'] as double, (row['admin'] as int) != 0, row['avatar'] as Uint8List), arguments: [name, foo, ...idx, ...ids]); } ''')); @@ -502,7 +814,7 @@ void main() { expect(actual, throwsA(const TypeMatcher())); }); - test('query with unsupported return type throws', () async { + test('query with unsupported void return type throws', () async { final queryMethod = () => _createQueryMethod(''' @Query('DELETE * FROM Person WHERE name = :name') void deleteByName(String name); @@ -511,6 +823,17 @@ void main() { expect(queryMethod, throwsA(const TypeMatcher())); }); + + test('query without TypeConverter for return type throws', () async { + final queryMethod = await _createQueryMethod(''' + @Query('SELECT timestamp FROM Person') + Future> findTimestampList(); + '''); + + final actual = () => QueryMethodWriter(queryMethod).write(); + + expect(actual, throwsA(const TypeMatcher())); + }); } Future _createQueryMethod(final String methodSignature) async {