diff --git a/example/lib/database.g.dart b/example/lib/database.g.dart index 2635d580..67aefd5c 100644 --- a/example/lib/database.g.dart +++ b/example/lib/database.g.dart @@ -144,11 +144,11 @@ class _$TaskDao extends TaskDao { @override Future findTaskById(int id) async { return _queryAdapter.query('SELECT * FROM task WHERE id = ?1', + arguments: [id], mapper: (Map row) => Task( row['id'] as int?, row['message'] as String, - _dateTimeConverter.decode(row['timestamp'] as int)), - arguments: [id]); + _dateTimeConverter.decode(row['timestamp'] as int))); } @override @@ -171,6 +171,18 @@ class _$TaskDao extends TaskDao { isView: false); } + @override + Stream> rawQueryTasksAsStream(SQLiteQuery query) { + return _queryAdapter.queryListStream(query.query, + arguments: query.arguments, + mapper: (Map row) => Task( + row['id'] as int?, + row['message'] as String, + _dateTimeConverter.decode(row['timestamp'] as int)), + queryableName: 'Task', + isView: false); + } + @override Future insertTask(Task task) async { await _taskInsertionAdapter.insert(task, OnConflictStrategy.abort); diff --git a/example/lib/task_dao.dart b/example/lib/task_dao.dart index b1ce8e6d..483f6c70 100644 --- a/example/lib/task_dao.dart +++ b/example/lib/task_dao.dart @@ -12,6 +12,23 @@ abstract class TaskDao { @Query('SELECT * FROM task') Stream> findAllTasksAsStream(); + @rawQuery + Stream> rawQueryTasksAsStream(SQLiteQuery query); + + Stream> findYesterdaysTasksByMessageAsStream(String message) { + final timestamp = DateTime.now() + .subtract( + const Duration(days: 1), + ) + .millisecondsSinceEpoch; + return rawQueryTasksAsStream(SQLiteQuery( + 'SELECT * FROM task WHERE timestamp > ?1 AND message == ?2', + arguments: [ + timestamp, + message, + ])); + } + @insert Future insertTask(Task task); diff --git a/floor/lib/floor.dart b/floor/lib/floor.dart index f60f694f..b1db581a 100644 --- a/floor/lib/floor.dart +++ b/floor/lib/floor.dart @@ -10,3 +10,4 @@ export 'package:floor/src/database.dart'; export 'package:floor/src/migration.dart'; export 'package:floor/src/sqflite_database_factory.dart'; export 'package:floor_annotation/floor_annotation.dart'; +export 'package:floor_generator/value_object/sqlite_query.dart'; diff --git a/floor_annotation/lib/floor_annotation.dart b/floor_annotation/lib/floor_annotation.dart index c97fef74..82cbd70c 100644 --- a/floor_annotation/lib/floor_annotation.dart +++ b/floor_annotation/lib/floor_annotation.dart @@ -14,6 +14,7 @@ export 'src/insert.dart'; export 'src/on_conflict_strategy.dart'; export 'src/primary_key.dart'; export 'src/query.dart'; +export 'src/raw_query.dart'; export 'src/transaction.dart'; export 'src/type_converter.dart'; export 'src/type_converters.dart'; diff --git a/floor_annotation/lib/src/raw_query.dart b/floor_annotation/lib/src/raw_query.dart new file mode 100644 index 00000000..f0423ca9 --- /dev/null +++ b/floor_annotation/lib/src/raw_query.dart @@ -0,0 +1,5 @@ +class RawQuery { + const RawQuery(); +} + +const rawQuery = RawQuery(); diff --git a/floor_generator/lib/misc/type_utils.dart b/floor_generator/lib/misc/type_utils.dart index 5c4e3ea2..bbdbe075 100644 --- a/floor_generator/lib/misc/type_utils.dart +++ b/floor_generator/lib/misc/type_utils.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; +import 'package:floor_generator/value_object/sqlite_query.dart'; import 'package:source_gen/source_gen.dart'; extension SupportedTypeChecker on DartType { @@ -19,6 +20,7 @@ extension SupportedTypeChecker on DartType { _intTypeChecker, _doubleTypeChecker, _uint8ListTypeChecker, + _sqliteQueryTypeChecker, ]).isExactlyType(this); } } @@ -28,6 +30,11 @@ extension Uint8ListTypeChecker on DartType { getDisplayString(withNullability: false) == 'Uint8List'; } +extension SQLiteQueryTypeChecker on DartType { + bool get isSQLiteQuery => + getDisplayString(withNullability: false) == 'SQLiteQuery'; +} + extension StreamTypeChecker on DartType { bool get isStream => _streamTypeChecker.isExactlyType(this); } @@ -43,6 +50,13 @@ extension AnnotationChecker on Element { return _typeChecker(type).hasAnnotationOfExact(this); } + bool containsAnnotation(final List types) { + return types.firstWhere( + (type) => _typeChecker(type).hasAnnotationOfExact(this), + orElse: () => null.runtimeType) != + null.runtimeType; + } + /// Returns the first annotation object found of [type] /// or `null` if annotation of [type] not found DartObject? getAnnotation(final Type type) { @@ -63,3 +77,5 @@ final _doubleTypeChecker = _typeChecker(double); final _uint8ListTypeChecker = _typeChecker(Uint8List); final _streamTypeChecker = _typeChecker(Stream); + +final _sqliteQueryTypeChecker = _typeChecker(SQLiteQuery); diff --git a/floor_generator/lib/processor/base_query_method_processor.dart b/floor_generator/lib/processor/base_query_method_processor.dart new file mode 100644 index 00000000..b6b1d49f --- /dev/null +++ b/floor_generator/lib/processor/base_query_method_processor.dart @@ -0,0 +1,108 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:collection/collection.dart'; +import 'package:floor_generator/misc/extension/dart_type_extension.dart'; +import 'package:floor_generator/misc/type_utils.dart'; +import 'package:floor_generator/processor/error/base_query_method_processor_error.dart'; +import 'package:floor_generator/processor/processor.dart'; +import 'package:floor_generator/value_object/query_method.dart'; +import 'package:floor_generator/value_object/queryable.dart'; + +abstract class BaseQueryMethodProcessor extends Processor { + final MethodElement _methodElement; + final List _queryables; + final BaseQueryMethodProcessorError _processorError; + + BaseQueryMethodProcessor( + final MethodElement methodElement, + final List queryables, + ) : _methodElement = methodElement, + _queryables = queryables, + _processorError = BaseQueryMethodProcessorError(methodElement); + + @override + QueryMethod process() { + final name = _methodElement.displayName; + final parameters = _methodElement.parameters; + final returnType = _methodElement.returnType; + final returnsStream = returnType.isStream; + _assertReturnsFutureOrStream(returnType, returnsStream); + final returnsList = _getReturnsList(returnType, returnsStream); + + final flattenedReturnType = _getFlattenedReturnType( + returnType, + returnsStream, + returnsList, + ); + + _assertReturnsNullableSingle( + returnsStream, + returnsList, + flattenedReturnType, + ); + + final queryable = _queryables.firstWhereOrNull((queryable) => + queryable.classElement.displayName == + flattenedReturnType.getDisplayString(withNullability: false)); + + return onProcess( + _methodElement, + name, + returnType, + flattenedReturnType, + parameters, + queryable, + ); + } + + DartType _getFlattenedReturnType( + final DartType rawReturnType, + final bool returnsStream, + final bool returnsList, + ) { + final type = returnsStream + ? _methodElement.returnType.flatten() + : _methodElement.library.typeSystem.flatten(rawReturnType); + return returnsList ? type.flatten() : type; + } + + bool _getReturnsList(final DartType returnType, final bool returnsStream) { + final type = returnsStream + ? returnType.flatten() + : _methodElement.library.typeSystem.flatten(returnType); + + return type.isDartCoreList; + } + + void _assertReturnsFutureOrStream( + final DartType rawReturnType, + final bool returnsStream, + ) { + if (!rawReturnType.isDartAsyncFuture && !returnsStream) { + throw _processorError.doesNotReturnFutureNorStream; + } + } + + void _assertReturnsNullableSingle( + final bool returnsStream, + final bool returnsList, + final DartType flattenedReturnType, + ) { + if (!returnsList && + !flattenedReturnType.isVoid && + !flattenedReturnType.isNullable) { + returnsStream + ? throw _processorError.doesNotReturnNullableStream + : throw _processorError.doesNotReturnNullableFuture; + } + } + + QueryMethod onProcess( + MethodElement methodElement, + String name, + DartType returnType, + DartType flattenedReturnType, + List parameters, + Queryable? queryable, + ); +} diff --git a/floor_generator/lib/processor/dao_processor.dart b/floor_generator/lib/processor/dao_processor.dart index 27f62acb..710910d3 100644 --- a/floor_generator/lib/processor/dao_processor.dart +++ b/floor_generator/lib/processor/dao_processor.dart @@ -7,6 +7,7 @@ import 'package:floor_generator/processor/deletion_method_processor.dart'; import 'package:floor_generator/processor/insertion_method_processor.dart'; import 'package:floor_generator/processor/processor.dart'; import 'package:floor_generator/processor/query_method_processor.dart'; +import 'package:floor_generator/processor/raw_query_method_processor.dart'; import 'package:floor_generator/processor/transaction_method_processor.dart'; import 'package:floor_generator/processor/update_method_processor.dart'; import 'package:floor_generator/value_object/dao.dart'; @@ -83,13 +84,24 @@ class DaoProcessor extends Processor { final Set typeConverters, ) { return methods - .where((method) => method.hasAnnotation(annotations.Query)) - .map((method) => QueryMethodProcessor( - method, - [..._entities, ..._views], - typeConverters, - ).process()) - .toList(); + .where((method) => method.containsAnnotation([ + annotations.Query, + annotations.RawQuery, + ])) + .map((method) { + if (method.hasAnnotation(annotations.Query)) { + return QueryMethodProcessor( + method, + [..._entities, ..._views], + typeConverters, + ).process(); + } else { + return RawQueryMethodProcessor( + method, + [..._entities, ..._views], + ).process(); + } + }).toList(); } List _getInsertionMethods( diff --git a/floor_generator/lib/processor/error/base_query_method_processor_error.dart b/floor_generator/lib/processor/error/base_query_method_processor_error.dart new file mode 100644 index 00000000..a17866dd --- /dev/null +++ b/floor_generator/lib/processor/error/base_query_method_processor_error.dart @@ -0,0 +1,36 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:floor_generator/processor/error/processor_error.dart'; +import 'package:source_gen/source_gen.dart'; + +class BaseQueryMethodProcessorError { + final MethodElement _methodElement; + + BaseQueryMethodProcessorError(final MethodElement methodElement) + : _methodElement = methodElement; + + InvalidGenerationSourceError get doesNotReturnFutureNorStream { + return InvalidGenerationSourceError( + 'All queries have to return a Future or Stream.', + todo: 'Define the return type as Future or Stream.', + element: _methodElement, + ); + } + + ProcessorError get doesNotReturnNullableStream { + return ProcessorError( + message: 'Queries returning streams of single elements might emit null.', + todo: + 'Make the method return a Stream of a nullable type e.g. Stream.', + element: _methodElement, + ); + } + + ProcessorError get doesNotReturnNullableFuture { + return ProcessorError( + message: 'Queries returning single elements might return null.', + todo: + 'Make the method return a Future of a nullable type e.g. Future.', + element: _methodElement, + ); + } +} diff --git a/floor_generator/lib/processor/error/query_method_processor_error.dart b/floor_generator/lib/processor/error/query_method_processor_error.dart index 87b30d96..5b0fa09b 100644 --- a/floor_generator/lib/processor/error/query_method_processor_error.dart +++ b/floor_generator/lib/processor/error/query_method_processor_error.dart @@ -1,5 +1,4 @@ import 'package:analyzer/dart/element/element.dart'; -import 'package:floor_generator/processor/error/processor_error.dart'; import 'package:source_gen/source_gen.dart'; class QueryMethodProcessorError { @@ -23,22 +22,4 @@ class QueryMethodProcessorError { element: _methodElement, ); } - - ProcessorError get doesNotReturnNullableStream { - return ProcessorError( - message: 'Queries returning streams of single elements might emit null.', - todo: - 'Make the method return a Stream of a nullable type e.g. Stream.', - element: _methodElement, - ); - } - - ProcessorError get doesNotReturnNullableFuture { - return ProcessorError( - message: 'Queries returning single elements might return null.', - todo: - 'Make the method return a Future of a nullable type e.g. Future.', - element: _methodElement, - ); - } } diff --git a/floor_generator/lib/processor/error/raw_query_method_processor_error.dart b/floor_generator/lib/processor/error/raw_query_method_processor_error.dart new file mode 100644 index 00000000..3728bfa4 --- /dev/null +++ b/floor_generator/lib/processor/error/raw_query_method_processor_error.dart @@ -0,0 +1,30 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:floor_generator/processor/error/processor_error.dart'; +import 'package:source_gen/source_gen.dart'; + +class RawQueryMethodProcessorError { + final MethodElement _methodElement; + + RawQueryMethodProcessorError(final MethodElement methodElement) + : _methodElement = methodElement; + + InvalidGenerationSourceError get queryArgumentsShouldBeSingle { + return InvalidGenerationSourceError( + 'RawQuery methods should have 1 and only 1 parameter with type String or SQLiteQuery', + todo: 'Make sure to supply one parameter per SQL query argument.', + element: _methodElement, + ); + } + + ProcessorError queryMethodParameterIsNullable( + final ParameterElement parameterElement, + ) { + return ProcessorError( + message: 'Query method parameters have to be non-nullable.', + todo: 'Define ${parameterElement.displayName} as non-nullable.' + '\nIf you want to assert null, change your query to use the `IS NULL`/' + '`IS NOT NULL` operator without passing a nullable parameter.', + element: _methodElement, + ); + } +} diff --git a/floor_generator/lib/processor/field_processor.dart b/floor_generator/lib/processor/field_processor.dart index 760b658e..9d7d323e 100644 --- a/floor_generator/lib/processor/field_processor.dart +++ b/floor_generator/lib/processor/field_processor.dart @@ -78,7 +78,7 @@ extension on DartType { return SqlType.integer; } else if (isDartCoreDouble) { return SqlType.real; - } else if (isUint8List) { + } else if (isUint8List || isSQLiteQuery) { return SqlType.blob; } throw StateError('This should really be unreachable'); diff --git a/floor_generator/lib/processor/query_method_processor.dart b/floor_generator/lib/processor/query_method_processor.dart index 3253223c..2832b64c 100644 --- a/floor_generator/lib/processor/query_method_processor.dart +++ b/floor_generator/lib/processor/query_method_processor.dart @@ -1,25 +1,22 @@ 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/constants.dart'; -import 'package:floor_generator/misc/extension/dart_type_extension.dart'; import 'package:floor_generator/misc/extension/iterable_extension.dart'; import 'package:floor_generator/misc/extension/set_extension.dart'; import 'package:floor_generator/misc/extension/type_converter_element_extension.dart'; import 'package:floor_generator/misc/type_utils.dart'; +import 'package:floor_generator/processor/base_query_method_processor.dart'; import 'package:floor_generator/processor/error/query_method_processor_error.dart'; -import 'package:floor_generator/processor/processor.dart'; import 'package:floor_generator/processor/query_processor.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'; -class QueryMethodProcessor extends Processor { +class QueryMethodProcessor extends BaseQueryMethodProcessor { final QueryMethodProcessorError _processorError; final MethodElement _methodElement; - final List _queryables; final Set _typeConverters; QueryMethodProcessor( @@ -27,38 +24,31 @@ class QueryMethodProcessor extends Processor { final List queryables, final Set typeConverters, ) : _methodElement = methodElement, - _queryables = queryables, _typeConverters = typeConverters, - _processorError = QueryMethodProcessorError(methodElement); + _processorError = QueryMethodProcessorError(methodElement), + super(methodElement, queryables); - @override - QueryMethod process() { - final name = _methodElement.displayName; - final parameters = _methodElement.parameters; - final rawReturnType = _methodElement.returnType; - - final query = QueryProcessor(_methodElement, _getQuery()).process(); - - final returnsStream = rawReturnType.isStream; - - _assertReturnsFutureOrStream(rawReturnType, returnsStream); - - final returnsList = _getReturnsList(rawReturnType, returnsStream); - final flattenedReturnType = _getFlattenedReturnType( - rawReturnType, - returnsStream, - returnsList, - ); + String _getQuery() { + final query = _methodElement + .getAnnotation(annotations.Query) + ?.getField(AnnotationField.queryValue) + ?.toStringValue() + ?.trim(); - _assertReturnsNullableSingle( - returnsStream, - returnsList, - flattenedReturnType, - ); + if (query == null || query.isEmpty) throw _processorError.noQueryDefined; + return query; + } - final queryable = _queryables.firstWhereOrNull((queryable) => - queryable.classElement.displayName == - flattenedReturnType.getDisplayString(withNullability: false)); + @override + QueryMethod onProcess( + MethodElement methodElement, + String name, + DartType returnType, + DartType flattenedReturnType, + List parameters, + Queryable? queryable, + ) { + final query = QueryProcessor(_methodElement, _getQuery()).process(); final parameterTypeConverters = parameters .expand((parameter) => @@ -79,66 +69,11 @@ class QueryMethodProcessor extends Processor { _methodElement, name, query, - rawReturnType, + returnType, flattenedReturnType, parameters, queryable, allTypeConverters, ); } - - String _getQuery() { - final query = _methodElement - .getAnnotation(annotations.Query) - ?.getField(AnnotationField.queryValue) - ?.toStringValue() - ?.trim(); - - if (query == null || query.isEmpty) throw _processorError.noQueryDefined; - return query; - } - - DartType _getFlattenedReturnType( - final DartType rawReturnType, - final bool returnsStream, - final bool returnsList, - ) { - final type = returnsStream - ? _methodElement.returnType.flatten() - : _methodElement.library.typeSystem.flatten(rawReturnType); - return returnsList ? type.flatten() : type; - } - - bool _getReturnsList(final DartType returnType, final bool returnsStream) { - final type = returnsStream - ? returnType.flatten() - : _methodElement.library.typeSystem.flatten(returnType); - - return type.isDartCoreList; - } - - void _assertReturnsFutureOrStream( - final DartType rawReturnType, - final bool returnsStream, - ) { - if (!rawReturnType.isDartAsyncFuture && !returnsStream) { - throw _processorError.doesNotReturnFutureNorStream; - } - } - - void _assertReturnsNullableSingle( - final bool returnsStream, - final bool returnsList, - final DartType flattenedReturnType, - ) { - if (!returnsList && - !flattenedReturnType.isVoid && - !flattenedReturnType.isNullable) { - if (returnsStream) { - throw _processorError.doesNotReturnNullableStream; - } else { - throw _processorError.doesNotReturnNullableFuture; - } - } - } } diff --git a/floor_generator/lib/processor/raw_query_method_processor.dart b/floor_generator/lib/processor/raw_query_method_processor.dart new file mode 100644 index 00000000..1ad587f9 --- /dev/null +++ b/floor_generator/lib/processor/raw_query_method_processor.dart @@ -0,0 +1,61 @@ +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:floor_generator/processor/base_query_method_processor.dart'; +import 'package:floor_generator/processor/error/raw_query_method_processor_error.dart'; +import 'package:floor_generator/value_object/query_method.dart'; +import 'package:floor_generator/value_object/queryable.dart'; + +class RawQueryMethodProcessor extends BaseQueryMethodProcessor { + final RawQueryMethodProcessorError _processorError; + final MethodElement _methodElement; + + RawQueryMethodProcessor( + final MethodElement methodElement, + final List queryables, + ) : _methodElement = methodElement, + _processorError = RawQueryMethodProcessorError(methodElement), + super(methodElement, queryables); + + void _assertRawQueryParameters( + final List parameterElements, + ) { + final parametersLength = parameterElements.length; + if (parametersLength > 1 || parametersLength == 0) { + throw _processorError.queryArgumentsShouldBeSingle; + } + + final parameter = parameterElements[0]; + final type = parameter.type; + if (type.isNullable) { + throw _processorError.queryMethodParameterIsNullable(parameter); + } + if (!type.isDartCoreString && !type.isSQLiteQuery) { + throw _processorError.queryArgumentsShouldBeSingle; + } + } + + @override + QueryMethod onProcess( + MethodElement methodElement, + String name, + DartType returnType, + DartType flattenedReturnType, + List parameters, + Queryable? queryable, + ) { + _assertRawQueryParameters(parameters); + + return QueryMethod( + _methodElement, + name, + null, + returnType, + flattenedReturnType, + parameters, + queryable, + {}, + ); + } +} diff --git a/floor_generator/lib/value_object/query_method.dart b/floor_generator/lib/value_object/query_method.dart index 39645eae..9beec2a2 100644 --- a/floor_generator/lib/value_object/query_method.dart +++ b/floor_generator/lib/value_object/query_method.dart @@ -15,7 +15,7 @@ class QueryMethod { final String name; /// Query where the parameter mapping is stored. - final Query query; + final Query? query; final DartType rawReturnType; diff --git a/floor_generator/lib/value_object/sqlite_query.dart b/floor_generator/lib/value_object/sqlite_query.dart new file mode 100644 index 00000000..7530d801 --- /dev/null +++ b/floor_generator/lib/value_object/sqlite_query.dart @@ -0,0 +1,7 @@ +class SQLiteQuery { + SQLiteQuery(this.query, {this.arguments}); + + final String query; + + final List? arguments; +} diff --git a/floor_generator/lib/writer/query_method_writer.dart b/floor_generator/lib/writer/query_method_writer.dart index bb42de25..3f1315a4 100644 --- a/floor_generator/lib/writer/query_method_writer.dart +++ b/floor_generator/lib/writer/query_method_writer.dart @@ -1,7 +1,9 @@ import 'dart:core'; +import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:code_builder/code_builder.dart'; +import 'package:collection/collection.dart'; 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'; @@ -145,11 +147,17 @@ class QueryMethodWriter implements Writer { return parameters.isNotEmpty ? '[${parameters.join(', ')}]' : null; } - String _generateQueryString() { + String? _generateQueryString() { + final query = _queryMethod.query; + if (query == null) { + return null; + } + final code = StringBuffer(); int start = 0; - final originalQuery = _queryMethod.query.sql; - for (final listParameter in _queryMethod.query.listParameters) { + + final originalQuery = query.sql; + for (final listParameter in query.listParameters) { code.write( originalQuery.substring(start, listParameter.position).toLiteral()); code.write(' + _sqliteVariablesFor${listParameter.name.capitalize()} + '); @@ -160,20 +168,20 @@ class QueryMethodWriter implements Writer { return code.toString(); } - String _generateNoReturnQuery(final String query, final String? arguments) { - final parameters = StringBuffer(query); - if (arguments != null) parameters.write(', arguments: $arguments'); - return 'await _queryAdapter.queryNoReturn($parameters);'; + String _generateNoReturnQuery(final String? query, final String? arguments) { + return 'await _queryAdapter.queryNoReturn(${_generateParametersStringBuffer( + query, + arguments, + )});'; } String _generateQuery( - final String query, + final String? query, final String? arguments, final Queryable queryable, ) { - final mapper = _generateMapper(queryable); - final parameters = StringBuffer(query)..write(', mapper: $mapper'); - if (arguments != null) parameters.write(', arguments: $arguments'); + final parameters = _generateParametersStringBuffer(query, arguments) + ..write(', mapper: ${_generateMapper(queryable)}'); if (_queryMethod.returnsStream) { // for streamed queries, we need to provide the queryable to know which @@ -188,9 +196,27 @@ class QueryMethodWriter implements Writer { return 'return _queryAdapter.query$list$stream($parameters);'; } -} -String _generateMapper(Queryable queryable) { - final constructor = queryable.constructor; - return '(Map row) => $constructor'; + StringBuffer _generateParametersStringBuffer( + String? query, String? arguments) { + final parameters = StringBuffer(); + final ParameterElement? parameter = _queryMethod.parameters.firstOrNull; + + if (query != null) { + parameters.write(query); + if (arguments != null) parameters.write(', arguments: $arguments'); + } else if (parameter?.type.isSQLiteQuery == true) { + parameters + ..write('${parameter!.name}.query, ') + ..write('arguments: ${parameter.name}.arguments'); + } else { + throw ArgumentError('Neither @Query nor @RawQuery can be defined.'); + } + return parameters; + } + + String _generateMapper(Queryable queryable) { + final constructor = queryable.constructor; + return '(Map row) => $constructor'; + } } diff --git a/floor_generator/test/processor/query_method_processor_test.dart b/floor_generator/test/processor/query_method_processor_test.dart index 5d1470b5..03b0218d 100644 --- a/floor_generator/test/processor/query_method_processor_test.dart +++ b/floor_generator/test/processor/query_method_processor_test.dart @@ -89,8 +89,8 @@ void main() { final actual = QueryMethodProcessor(methodElement, [], {}).process().query; - expect(actual.sql, equals('SELECT * FROM Person WHERE id = ?1')); - expect(actual.listParameters, equals([])); + expect(actual?.sql, equals('SELECT * FROM Person WHERE id = ?1')); + expect(actual?.listParameters, equals([])); }); test('parse query reusing a single parameter', () async { @@ -100,7 +100,7 @@ void main() { '''); final actual = - QueryMethodProcessor(methodElement, [], {}).process().query.sql; + QueryMethodProcessor(methodElement, [], {}).process().query?.sql; expect(actual, equals('SELECT * FROM Person WHERE id = ?1 AND id = ?1')); }); @@ -112,7 +112,7 @@ void main() { '''); final actual = - QueryMethodProcessor(methodElement, [], {}).process().query.sql; + QueryMethodProcessor(methodElement, [], {}).process().query?.sql; expect( actual, @@ -130,7 +130,7 @@ void main() { """); final actual = - QueryMethodProcessor(methodElement, [], {}).process().query.sql; + QueryMethodProcessor(methodElement, [], {}).process().query?.sql; expect( actual, @@ -147,7 +147,7 @@ void main() { '''); final actual = - QueryMethodProcessor(methodElement, [], {}).process().query.sql; + QueryMethodProcessor(methodElement, [], {}).process().query?.sql; expect( actual, @@ -165,10 +165,10 @@ void main() { QueryMethodProcessor(methodElement, [], {}).process().query; expect( - actual.sql, + actual?.sql, equals(r'update sports set rated = 1 where id in (:varlist)'), ); - expect(actual.listParameters, equals([ListParameter(41, 'ids')])); + expect(actual?.listParameters, equals([ListParameter(41, 'ids')])); }); test('parses IN clause without space after IN', () async { @@ -181,10 +181,10 @@ void main() { QueryMethodProcessor(methodElement, [], {}).process().query; expect( - actual.sql, + actual?.sql, equals(r'update sports set rated = 1 where id in(:varlist)'), ); - expect(actual.listParameters, equals([ListParameter(40, 'ids')])); + expect(actual?.listParameters, equals([ListParameter(40, 'ids')])); }); test('parses IN clause with multiple spaces after IN', () async { @@ -197,10 +197,10 @@ void main() { QueryMethodProcessor(methodElement, [], {}).process().query; expect( - actual.sql, + actual?.sql, equals(r'update sports set rated = 1 where id in (:varlist)'), ); - expect(actual.listParameters, equals([ListParameter(46, 'ids')])); + expect(actual?.listParameters, equals([ListParameter(46, 'ids')])); }); test('Parse query with multiple IN clauses', () async { @@ -213,13 +213,13 @@ void main() { QueryMethodProcessor(methodElement, [], {}).process().query; expect( - actual.sql, + actual?.sql, equals( r'update sports set rated = 1 where id in (:varlist) ' r'and where foo in (:varlist)', ), ); - expect(actual.listParameters, + expect(actual?.listParameters, equals([ListParameter(41, 'ids'), ListParameter(69, 'bar')])); }); @@ -233,13 +233,13 @@ void main() { QueryMethodProcessor(methodElement, [], {}).process().query; expect( - actual.sql, + actual?.sql, equals( r'update sports set rated = 1 where id in (:varlist) ' r'AND foo = ?1', ), ); - expect(actual.listParameters, equals([ListParameter(41, 'ids')])); + expect(actual?.listParameters, equals([ListParameter(41, 'ids')])); }); test('Parse query with mixed IN clauses and other parameters', () async { @@ -252,13 +252,13 @@ void main() { QueryMethodProcessor(methodElement, [], {}).process().query; expect( - actual.sql, + actual?.sql, equals( r'update sports set rated = 1 where id in (:varlist) AND foo = ?2 ' r'AND name in (:varlist) and ?2 = ?1', ), ); - expect(actual.listParameters, + expect(actual?.listParameters, equals([ListParameter(41, 'ids'), ListParameter(77, 'names')])); }); @@ -269,7 +269,7 @@ void main() { '''); final actual = - QueryMethodProcessor(methodElement, [], {}).process().query.sql; + QueryMethodProcessor(methodElement, [], {}).process().query?.sql; expect(actual, equals('SELECT * FROM Persons WHERE name LIKE ?1')); }); @@ -281,7 +281,7 @@ void main() { '''); final actual = - QueryMethodProcessor(methodElement, [], {}).process().query.sql; + QueryMethodProcessor(methodElement, [], {}).process().query?.sql; // note: this will throw an error at runtime, because // sqlite variables can not be used in place of table // names. But the Processor is not aware of this. diff --git a/floor_generator/test/processor/raw_query_method_processor_test.dart b/floor_generator/test/processor/raw_query_method_processor_test.dart new file mode 100644 index 00000000..323a495b --- /dev/null +++ b/floor_generator/test/processor/raw_query_method_processor_test.dart @@ -0,0 +1,146 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:build_test/build_test.dart'; +import 'package:floor_annotation/floor_annotation.dart' as annotations; +import 'package:floor_generator/misc/type_utils.dart'; +import 'package:floor_generator/processor/entity_processor.dart'; +import 'package:floor_generator/processor/raw_query_method_processor.dart'; +import 'package:floor_generator/processor/view_processor.dart'; +import 'package:floor_generator/value_object/entity.dart'; +import 'package:floor_generator/value_object/query_method.dart'; +import 'package:floor_generator/value_object/view.dart'; +import 'package:source_gen/source_gen.dart'; +import 'package:test/test.dart'; + +import '../test_utils.dart'; + +void main() { + late List entities; + late List views; + + setUpAll(() async { + entities = await _getEntities(); + views = await _getViews(); + }); + + test('create raw query method', () async { + final methodElement = await _createQueryMethodElement(''' + @rawQuery + Stream> findAllPersons(SQLiteQuery query); + '''); + + final actual = RawQueryMethodProcessor( + methodElement, + [...entities, ...views], + ).process(); + + expect( + actual, + equals( + QueryMethod( + methodElement, + 'findAllPersons', + null, + await getDartTypeWithPerson('Stream>'), + await getDartTypeWithPerson('Person'), + [], + null, + {}, + ), + ), + ); + }, skip: 'Need to figure out how to pass SQLQuery parameter.'); +} + +Future _createQueryMethodElement( + final String method, +) async { + final library = await resolveSource(''' + library test; + + import 'package:floor_annotation/floor_annotation.dart'; + import 'package:floor_generator/value_object/sqlite_query.dart'; + + @dao + abstract class PersonDao { + $method + } + + @entity + class Person { + @primaryKey + final int id; + + final String name; + + Person(this.id, this.name); + } + + @DatabaseView("SELECT DISTINCT(name) AS name from person") + class Name { + final String name; + + Name(this.name); + } + ''', (resolver) async { + return resolver + .findLibraryByName('test') + .then((value) => ArgumentError.checkNotNull(value)) + .then((value) => LibraryReader(value)); + }); + + return library.classes.first.methods.first; +} + +Future> _getEntities() async { + final library = await resolveSource(''' + library test; + + import 'package:floor_annotation/floor_annotation.dart'; + + @entity + class Person { + @primaryKey + final int id; + + final String name; + + Person(this.id, this.name); + } + ''', (resolver) async { + return resolver + .findLibraryByName('test') + .then((value) => ArgumentError.checkNotNull(value)) + .then((value) => LibraryReader(value)); + }); + + return library.classes + .where((classElement) => classElement.hasAnnotation(annotations.Entity)) + .map((classElement) => EntityProcessor(classElement, {}).process()) + .toList(); +} + +Future> _getViews() async { + final library = await resolveSource(''' + library test; + + import 'package:floor_annotation/floor_annotation.dart'; + + @DatabaseView("SELECT DISTINCT(name) AS name from person") + class Name { + final String name; + + Name(this.name); + } + ''', (resolver) async { + return resolver + .findLibraryByName('test') + .then((value) => ArgumentError.checkNotNull(value)) + .then((value) => LibraryReader(value)); + }); + + return library.classes + .where((classElement) => + classElement.hasAnnotation(annotations.DatabaseView)) + .map((classElement) => ViewProcessor(classElement, {}).process()) + .toList(); +} diff --git a/floor_generator/test/writer/query_method_writer_test.dart b/floor_generator/test/writer/query_method_writer_test.dart index 388ad3aa..78b37878 100644 --- a/floor_generator/test/writer/query_method_writer_test.dart +++ b/floor_generator/test/writer/query_method_writer_test.dart @@ -60,7 +60,7 @@ 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', arguments: [id], mapper: (Map row) => Person(row['id'] as int, row['name'] as String)); } ''')); }); @@ -84,7 +84,7 @@ void main() { expect(actual, equalsDart(r''' @override Future findById(int id) async { - return _queryAdapter.query('SELECT * FROM Order WHERE id = ?1', mapper: (Map row) => Order(row['id'] as int, _dateTimeConverter.decode(row['dateTime'] as int)), arguments: [id]); + return _queryAdapter.query('SELECT * FROM Order WHERE id = ?1', arguments: [id], mapper: (Map row) => Order(row['id'] as int, _dateTimeConverter.decode(row['dateTime'] as int))); } ''')); }); @@ -108,7 +108,7 @@ void main() { expect(actual, equalsDart(r''' @override Future findByDateTime(DateTime dateTime) async { - return _queryAdapter.query('SELECT * FROM Order WHERE dateTime = ?1', mapper: (Map row) => Order(row['id'] as int, _externalTypeConverter.decode(row['dateTime'] as int)), arguments: [_dateTimeConverter.encode(dateTime)]); + return _queryAdapter.query('SELECT * FROM Order WHERE dateTime = ?1', arguments: [_dateTimeConverter.encode(dateTime)], mapper: (Map row) => Order(row['id'] as int, _externalTypeConverter.decode(row['dateTime'] as int))); } ''')); }); @@ -132,7 +132,7 @@ void main() { expect(actual, equalsDart(r''' @override Future findByDateTime(DateTime dateTime) async { - return _queryAdapter.query('SELECT * FROM Order WHERE dateTime = ?1', mapper: (Map row) => Order(row['id'] as int, _externalTypeConverter.decode(row['dateTime'] as int)), arguments: [_dateTimeConverter.encode(dateTime)]); + return _queryAdapter.query('SELECT * FROM Order WHERE dateTime = ?1', arguments: [_dateTimeConverter.encode(dateTime)], mapper: (Map row) => Order(row['id'] as int, _externalTypeConverter.decode(row['dateTime'] as int))); } ''')); }); @@ -158,9 +158,9 @@ void main() { Future> findByDates(List dates) async { const offset = 1; final _sqliteVariablesForDates=Iterable.generate(dates.length, (i)=>'?${i+offset}').join(','); - return _queryAdapter.queryList('SELECT * FROM Order WHERE date IN (' + _sqliteVariablesForDates + ')', - mapper: (Map row) => Order(row['id'] as int, _dateTimeConverter.decode(row['dateTime'] as int)), - arguments: [...dates.map((element) => _dateTimeConverter.encode(element))]); + return _queryAdapter.queryList('SELECT * FROM Order WHERE date IN (' + _sqliteVariablesForDates + ')', + arguments: [...dates.map((element) => _dateTimeConverter.encode(element))], + mapper: (Map row) => Order(row['id'] as int, _dateTimeConverter.decode(row['dateTime'] as int))); } ''')); }); @@ -190,9 +190,9 @@ void main() { final _sqliteVariablesForIds=Iterable.generate(ids.length, (i)=>'?${i+offset}').join(','); offset += ids.length; final _sqliteVariablesForDateTimeList=Iterable.generate(dateTimeList.length, (i)=>'?${i+offset}').join(','); - return _queryAdapter.queryList('SELECT * FROM Order WHERE id IN (' + _sqliteVariablesForIds + ') AND id IN (' + _sqliteVariablesForDateTimeList + ') OR foo in (' + _sqliteVariablesForIds + ') AND bar = ?2 OR name = ?1', - mapper: (Map row) => Order(row['id'] as int, _dateTimeConverter.decode(row['dateTime'] as int)), - arguments: [name, _dateTimeConverter.encode(foo), ...ids, ...dateTimeList.map((element) => _dateTimeConverter.encode(element))]); + return _queryAdapter.queryList('SELECT * FROM Order WHERE id IN (' + _sqliteVariablesForIds + ') AND id IN (' + _sqliteVariablesForDateTimeList + ') OR foo in (' + _sqliteVariablesForIds + ') AND bar = ?2 OR name = ?1', + arguments: [name, _dateTimeConverter.encode(foo), ...ids, ...dateTimeList.map((element) => _dateTimeConverter.encode(element))], + mapper: (Map row) => Order(row['id'] as int, _dateTimeConverter.decode(row['dateTime'] as int))); } ''')); }); @@ -208,7 +208,7 @@ void main() { expect(actual, equalsDart(r''' @override 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), arguments: [flag ? 1 : 0]); + return _queryAdapter.queryList('SELECT * FROM Person WHERE flag = ?1', arguments: [flag ? 1 : 0], mapper: (Map row) => Person(row['id'] as int, row['name'] as String)); } ''')); }); @@ -224,7 +224,7 @@ void main() { expect(actual, equalsDart(r''' @override Future findById(int id, String name) 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), arguments: [id, name]); + return _queryAdapter.query('SELECT * FROM Person WHERE id = ?1 AND name = ?2', arguments: [id, name], mapper: (Map row) => Person(row['id'] as int, row['name'] as String)); } ''')); }); @@ -240,7 +240,7 @@ void main() { expect(actual, equalsDart(r''' @override Future findById(int id, String name, String bar) 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), arguments: [id, name, bar]); + return _queryAdapter.query('SELECT * FROM Person WHERE foo = ?3 AND id = ?1 AND name = ?2 AND name = ?3', arguments: [id, name, bar], mapper: (Map row) => Person(row['id'] as int, row['name'] as String)); } ''')); }); @@ -272,7 +272,7 @@ void main() { expect(actual, equalsDart(r''' @override 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), arguments: [id], queryableName: 'Person', isView: false); + return _queryAdapter.queryStream('SELECT * FROM Person WHERE id = ?1', arguments: [id], mapper: (Map row) => Person(row['id'] as int, row['name'] as String), queryableName: 'Person', isView: false); } ''')); }); @@ -323,8 +323,8 @@ 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), - arguments: [...ids]); + arguments: [...ids], + mapper: (Map row) => Person(row['id'] as int, row['name'] as String)); } ''')); }); @@ -343,8 +343,8 @@ 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), - arguments: [...ids]); + arguments: [...ids], + mapper: (Map row) => Person(row['id'] as int, row['name'] as String)); } ''')); }); @@ -365,8 +365,8 @@ void main() { offset += ids.length; final _sqliteVariablesForIdx=Iterable.generate(idx.length, (i)=>'?${i+offset}').join(','); return _queryAdapter.queryList('SELECT * FROM Person WHERE id IN (' + _sqliteVariablesForIds + ') AND id IN (' + _sqliteVariablesForIdx + ')', - mapper: (Map row) => Person(row['id'] as int, row['name'] as String), - arguments: [...ids, ...idx]); + arguments: [...ids, ...idx], + mapper: (Map row) => Person(row['id'] as int, row['name'] as String)); } ''')); }); @@ -389,8 +389,8 @@ void main() { offset += idx.length; final _sqliteVariablesForIds=Iterable.generate(ids.length, (i)=>'?${i+offset}').join(','); return _queryAdapter.queryList('SELECT * FROM Person WHERE id IN (' + _sqliteVariablesForIds + ') AND id IN (' + _sqliteVariablesForIdx + ') OR foo in (' + _sqliteVariablesForIds + ') AND bar = ?2 OR name = ?1', - mapper: (Map row) => Person(row['id'] as int, row['name'] as String), - arguments: [name, foo, ...idx, ...ids]); + arguments: [name, foo, ...idx, ...ids], + mapper: (Map row) => Person(row['id'] as int, row['name'] as String)); } ''')); });