diff --git a/floor/test/integration/dao/name_dao.dart b/floor/test/integration/dao/name_dao.dart index cb6ccad0..16491d05 100644 --- a/floor/test/integration/dao/name_dao.dart +++ b/floor/test/integration/dao/name_dao.dart @@ -17,6 +17,10 @@ abstract class NameDao { @Query('SELECT * FROM names WHERE name LIKE :suffix ORDER BY name ASC') Future> findNamesLike(String suffix); + @Query( + 'SELECT * FROM names WHERE name LIKE :suffix AND name LIKE :prefix ORDER BY name ASC') + Future> findNamesMatchingBoth(String prefix, String suffix); + @Query('SELECT * FROM multiline_query_names WHERE name = :name') Future findMultilineQueryName(String name); } diff --git a/floor/test/integration/dao/person_dao.dart b/floor/test/integration/dao/person_dao.dart index 70d8ff81..b33d6ab5 100644 --- a/floor/test/integration/dao/person_dao.dart +++ b/floor/test/integration/dao/person_dao.dart @@ -26,6 +26,11 @@ abstract class PersonDao { @Query('SELECT * FROM person WHERE custom_name IN (:names)') Future> findPersonsWithNames(List names); + @Query( + 'SELECT * FROM person WHERE custom_name IN (:names) AND id>=:reference OR custom_name IN (:moreNames) AND id<=:reference') + Future> findPersonsWithNamesComplex( + int reference, List names, List moreNames); + @Query('SELECT * FROM person WHERE custom_name LIKE :name') Future> findPersonsWithNamesLike(String name); diff --git a/floor/test/integration/database_test.dart b/floor/test/integration/database_test.dart index c86e1765..2ad2f0aa 100644 --- a/floor/test/integration/database_test.dart +++ b/floor/test/integration/database_test.dart @@ -322,6 +322,35 @@ void main() { expect(actual, equals([person1, person2])); }); + + test('Find persons with names (complex query)', () async { + final person1 = Person(1, 'Sylvie'); + final person2 = Person(2, 'Simon'); + final person3 = Person(3, 'Paul'); + final person4 = Person(4, 'Albert'); + final person5 = Person(5, 'Louis'); + final person6 = Person(6, 'Chris'); + final person7 = Person(7, 'Maria'); + await personDao.insertPersons( + [person1, person2, person3, person4, person5, person6, person7]); + final names1 = [ + person1.name, + person3.name, + person5.name, + person7.name + ]; + final names2 = [ + person2.name, + person4.name, + person6.name, + person7.name + ]; + + final actual = + await personDao.findPersonsWithNamesComplex(4, names1, names2); + + expect(actual, equals([person5, person7, person4, person2])); + }); }); group('LIKE operator', () { diff --git a/floor/test/integration/view/view_test.dart b/floor/test/integration/view/view_test.dart index 1a923342..997bb1b0 100644 --- a/floor/test/integration/view/view_test.dart +++ b/floor/test/integration/view/view_test.dart @@ -70,6 +70,19 @@ void main() { expect(actual, equals(expected)); }); + test('query view with double LIKE (reordered query params)', () async { + final persons = [Person(1, 'Leo'), Person(2, 'Frank')]; + await personDao.insertPersons(persons); + + final dog = Dog(1, 'Romeo', 'Rome', 1); + await dogDao.insertDog(dog); + + final actual = await nameDao.findNamesMatchingBoth('L%', '%eo'); + + final expected = [Name('Leo')]; + expect(actual, equals(expected)); + }); + test('query view with all values', () async { final persons = [Person(1, 'Leo'), Person(2, 'Frank')]; await personDao.insertPersons(persons); diff --git a/floor_generator/lib/misc/extension/string_extension.dart b/floor_generator/lib/misc/extension/string_extension.dart index 19bde368..b45d26c7 100644 --- a/floor_generator/lib/misc/extension/string_extension.dart +++ b/floor_generator/lib/misc/extension/string_extension.dart @@ -16,4 +16,40 @@ extension StringExtension on String { return this[0].toLowerCase() + substring(1); } } + + /// Returns a copy of this string having its first letter uppercased, or the + /// original string, if it's empty or already starts with a upper case letter. + /// + /// ```dart + /// print('abcd'.capitalize()) // Abcd + /// print('Abcd'.capitalize()) // Abcd + /// ``` + String capitalize() { + switch (length) { + case 0: + return this; + case 1: + return toUpperCase(); + default: + return this[0].toUpperCase() + substring(1); + } + } +} + +extension NullableStringExtension on String? { + /// Converts this string to a literal for + /// embedding it into source code strings. + /// + /// ```dart + /// print(null.toLiteral()) // null + /// print('Abcd'.toLiteral()) // 'Abcd' + /// ``` + String toLiteral() { + if (this == null) { + return 'null'; + } else { + //TODO escape correctly + return "'$this'"; + } + } } 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 2d587dfa..7f5bd8bf 100644 --- a/floor_generator/lib/processor/error/query_method_processor_error.dart +++ b/floor_generator/lib/processor/error/query_method_processor_error.dart @@ -17,14 +17,6 @@ class QueryMethodProcessorError { ); } - InvalidGenerationSourceError get queryArgumentsAndMethodParametersDoNotMatch { - return InvalidGenerationSourceError( - 'SQL query arguments and method parameters have to match.', - todo: 'Make sure to supply one parameter per SQL query argument.', - element: _methodElement, - ); - } - InvalidGenerationSourceError get doesNotReturnFutureNorStream { return InvalidGenerationSourceError( 'All queries have to return a Future or Stream.', @@ -33,18 +25,6 @@ class QueryMethodProcessorError { ); } - 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, - ); - } - ProcessorError get doesNotReturnNullableStream { return ProcessorError( message: 'Queries returning streams of single elements might emit null.', diff --git a/floor_generator/lib/processor/error/query_processor_error.dart b/floor_generator/lib/processor/error/query_processor_error.dart new file mode 100644 index 00000000..0e4f702d --- /dev/null +++ b/floor_generator/lib/processor/error/query_processor_error.dart @@ -0,0 +1,69 @@ +// ignore_for_file: import_of_legacy_library_into_null_safe +import 'package:analyzer/dart/element/element.dart'; +import 'package:floor_generator/processor/error/processor_error.dart'; + +class QueryProcessorError { + final MethodElement _methodElement; + + QueryProcessorError(final MethodElement methodElement) + : _methodElement = methodElement; + + ProcessorError unusedQueryMethodParameter( + final ParameterElement parameterElement, + ) { + return ProcessorError( + message: 'Query method parameters have to be used.', + todo: 'Use ${parameterElement.displayName} in the query or remove it.', + element: parameterElement, + ); + } + + ProcessorError unknownQueryVariable( + final String variableName, + ) { + return ProcessorError( + message: + 'Query variable `$variableName` has to exist as a method parameter.', + todo: + 'Provide $variableName as a method parameter or remove it from the query.', + element: _methodElement, + ); + } + + ProcessorError queryMethodParameterIsListButVariableIsNot( + final String varName, + ) { + final name = varName.substring(1); + return ProcessorError( + message: + 'The parameter $name should be referenced like a list (`x IN ($varName)`)', + todo: 'Change the type of $name to not be a List<> or' + 'reference it with ` IN ($varName)` (including the parentheses).', + element: _methodElement, + ); + } + + ProcessorError queryMethodParameterIsNormalButVariableIsList( + final String varName, + ) { + final name = varName.substring(1); + return ProcessorError( + message: 'The parameter $name should be referenced without `IN`', + todo: 'Change the type of $name to be a List<> or' + 'reference it without `IN`, e.g. `IS $varName`.', + 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: parameterElement, + ); + } +} diff --git a/floor_generator/lib/processor/query_method_processor.dart b/floor_generator/lib/processor/query_method_processor.dart index cf0417d3..52237691 100644 --- a/floor_generator/lib/processor/query_method_processor.dart +++ b/floor_generator/lib/processor/query_method_processor.dart @@ -10,6 +10,7 @@ import 'package:floor_generator/misc/extension/type_converter_element_extension. import 'package:floor_generator/misc/type_utils.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'; @@ -36,7 +37,9 @@ class QueryMethodProcessor extends Processor { final parameters = _methodElement.parameters; final rawReturnType = _methodElement.returnType; - final query = _getQuery(); + final query = QueryProcessor(_methodElement, _getQuery()).process(); + + _getQuery(); final returnsStream = rawReturnType.isStream; _assertReturnsFutureOrStream(rawReturnType, returnsStream); @@ -90,29 +93,10 @@ class QueryMethodProcessor extends Processor { .getAnnotation(annotations.Query) .getField(AnnotationField.queryValue) ?.toStringValue() - ?.replaceAll('\n', ' ') - .replaceAll(RegExp(r'[ ]{2,}'), ' ') - .trim(); + ?.trim(); if (query == null || query.isEmpty) throw _processorError.noQueryDefined; - - final substitutedQuery = query.replaceAll(RegExp(r':[.\w]+'), '?'); - _assertQueryParameters(substitutedQuery, _methodElement.parameters); - return _replaceInClauseArguments(substitutedQuery); - } - - String _replaceInClauseArguments(final String query) { - var index = 0; - return query.replaceAllMapped( - RegExp(r'( in\s*)\([?]\)', caseSensitive: false), - (match) { - final matched = match.input.substring(match.start, match.end); - final replaced = - matched.replaceFirst(RegExp(r'(\?)'), '\$valueList$index'); - index++; - return replaced; - }, - ); + return query; } DartType _getFlattenedReturnType( @@ -143,22 +127,6 @@ class QueryMethodProcessor extends Processor { } } - void _assertQueryParameters( - final String query, - final List parameterElements, - ) { - for (final parameter in parameterElements) { - if (parameter.type.isNullable) { - throw _processorError.queryMethodParameterIsNullable(parameter); - } - } - - final queryParameterCount = RegExp(r'\?').allMatches(query).length; - if (queryParameterCount != parameterElements.length) { - throw _processorError.queryArgumentsAndMethodParametersDoNotMatch; - } - } - void _assertReturnsNullableSingle( final bool returnsStream, final bool returnsList, diff --git a/floor_generator/lib/processor/query_processor.dart b/floor_generator/lib/processor/query_processor.dart new file mode 100644 index 00000000..342a9e54 --- /dev/null +++ b/floor_generator/lib/processor/query_processor.dart @@ -0,0 +1,147 @@ +import 'package:floor_generator/misc/extension/dart_type_extension.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:floor_generator/processor/error/query_processor_error.dart'; +import 'package:floor_generator/processor/processor.dart'; +import 'package:floor_generator/value_object/query.dart'; + +class QueryProcessor extends Processor { + final QueryProcessorError _processorError; + + final String _query; + + final List _parameters; + + QueryProcessor(MethodElement methodElement, this._query) + : _parameters = methodElement.parameters, + _processorError = QueryProcessorError(methodElement); + + @override + Query process() { + _assertNoNullableParameters(); + + final indices = {}; + final fixedParameters = {}; + //map parameters to index (1-based) or 0 (if its a list) + int currentIndex = 1; + for (final parameter in _parameters) { + if (parameter.type.isDartCoreList) { + indices[':${parameter.name}'] = 0; + } else { + fixedParameters.add(parameter.name); + indices[':${parameter.name}'] = currentIndex++; + } + } + + //get List of query variables + final variables = findVariables(_query); + _assertAllParametersAreUsed(variables); + + final newQuery = StringBuffer(); + final listParameters = []; + // iterate over all found variables, replace them with their assigned + // numbered variable (?1,?2,...) or a placeholder if the variable is a list. + // the list variables have to be handled in the writer, so write down their + // positions and names. + int currentLast = 0; + for (final varToken in variables) { + newQuery.write(_query + .substring(currentLast, varToken.startPosition) + .replaceAll('\n', ' ')); + final varIndexInMethod = indices[varToken.name]; + if (varIndexInMethod == null) { + throw _processorError.unknownQueryVariable(varToken.name); + } else if (varIndexInMethod > 0) { + //normal variable/parameter + if (varToken.isListVar) + throw _processorError + .queryMethodParameterIsNormalButVariableIsList(varToken.name); + newQuery.write('?'); + newQuery.write(varIndexInMethod); + } else { + //list variable/parameter + if (!varToken.isListVar) + throw _processorError + .queryMethodParameterIsListButVariableIsNot(varToken.name); + listParameters + .add(ListParameter(newQuery.length, varToken.name.substring(1))); + newQuery.write(varlistPlaceholder); + } + currentLast = varToken.endPosition; + } + newQuery.write(_query.substring(currentLast).replaceAll('\n', ' ')); + + return Query( + newQuery.toString(), + listParameters, + ); + } + + void _assertNoNullableParameters() { + for (final parameter in _parameters) { + if (parameter.type.isNullable) { + throw _processorError.queryMethodParameterIsNullable(parameter); + } + } + } + + void _assertAllParametersAreUsed(List variables) { + final queryVariables = variables.map((e) => e.name.substring(1)).toSet(); + for (final param in _parameters) { + if (!queryVariables.contains(param.displayName)) { + throw _processorError.unusedQueryMethodParameter(param); + } + } + } +} + +/// Treats the incoming String as an Sqlite query and tries to find all used +/// sqlite variables. Also try do identify List variables by looking at their +/// context. +List findVariables(final String query) { + final output = []; + for (final match + in RegExp(r':[\w]+| [iI][nN]\s*\((:[\w]+)\)').allMatches(query)) { + final content = match.group(0)!; + final expectsList = content.toLowerCase().startsWith(' in'); + if (expectsList) { + final varname = match.group(1)!; + output.add( + VariableToken(varname, query.indexOf(varname, match.start), true)); + } else { + output.add(VariableToken(content, match.start, false)); + } + } + return output; +} + +/// Represents a variable within an sqlite query. +class VariableToken { + /// the variable name including `:` (e.g. `:foo`) + final String name; + + /// the offset within the query, where the variable name starts. Useful for + /// splitting the query here. + final int startPosition; + + /// the offset within the query, where the variable name ends. Useful for + /// splitting the query here. + int get endPosition => startPosition + name.length; + + /// denotes if the variable was determined to contain a list + final bool isListVar; + + VariableToken(this.name, this.startPosition, this.isListVar); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is VariableToken && + runtimeType == other.runtimeType && + name == other.name && + startPosition == other.startPosition && + isListVar == other.isListVar; + + @override + int get hashCode => + name.hashCode ^ startPosition.hashCode ^ isListVar.hashCode; +} diff --git a/floor_generator/lib/value_object/query.dart b/floor_generator/lib/value_object/query.dart new file mode 100644 index 00000000..b8e4fdbe --- /dev/null +++ b/floor_generator/lib/value_object/query.dart @@ -0,0 +1,50 @@ +import 'package:collection/collection.dart'; + +const String varlistPlaceholder = ':varlist'; + +class Query { + final String sql; + final List listParameters; + + Query(this.sql, this.listParameters); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Query && + runtimeType == other.runtimeType && + sql == other.sql && + const ListEquality() + .equals(listParameters, other.listParameters); + + @override + int get hashCode => sql.hashCode ^ listParameters.hashCode; + + @override + String toString() { + return 'Query{sql: $sql, listParameters: $listParameters}'; + } +} + +class ListParameter { + final int position; + final String name; + + ListParameter(this.position, this.name); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ListParameter && + runtimeType == other.runtimeType && + position == other.position && + name == other.name; + + @override + int get hashCode => position.hashCode ^ name.hashCode; + + @override + String toString() { + return 'ListParameter{position: $position, name: $name}'; + } +} diff --git a/floor_generator/lib/value_object/query_method.dart b/floor_generator/lib/value_object/query_method.dart index 0a393bd6..39645eae 100644 --- a/floor_generator/lib/value_object/query_method.dart +++ b/floor_generator/lib/value_object/query_method.dart @@ -3,6 +3,7 @@ import 'package:analyzer/dart/element/type.dart'; import 'package:collection/collection.dart'; import 'package:floor_generator/misc/extension/set_equality_extension.dart'; import 'package:floor_generator/misc/type_utils.dart'; +import 'package:floor_generator/value_object/query.dart'; import 'package:floor_generator/value_object/queryable.dart'; import 'package:floor_generator/value_object/type_converter.dart'; @@ -13,8 +14,8 @@ class QueryMethod { final String name; - /// Query where ':' got replaced with '$'. - final String query; + /// Query where the parameter mapping is stored. + final Query query; final DartType rawReturnType; diff --git a/floor_generator/lib/writer/query_method_writer.dart b/floor_generator/lib/writer/query_method_writer.dart index a7e86bee..db33d6d4 100644 --- a/floor_generator/lib/writer/query_method_writer.dart +++ b/floor_generator/lib/writer/query_method_writer.dart @@ -1,13 +1,15 @@ // ignore_for_file: import_of_legacy_library_into_null_safe import 'dart:core'; +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'; import 'package:floor_generator/misc/type_utils.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/view.dart'; import 'package:floor_generator/writer/writer.dart'; @@ -48,66 +50,95 @@ class QueryMethodWriter implements Writer { String _generateMethodBody() { final _methodBody = StringBuffer(); - final valueLists = _generateInClauseValueLists(); - if (valueLists.isNotEmpty) { - _methodBody.write(valueLists.join('')); - } + // generate the variable definitions which will store the sqlite argument + // lists, e.g. '?5,?6,?7,?8'. These have to be generated for each call to + // the query method to accommodate for different list sizes. This is + // necessary to guarantee that each single value is inserted at the right + // place and only via SQLite's escape-mechanism. + // If no [List] parameters are present, Nothing will be written. + _methodBody.write(_generateListConvertersForQuery()); final arguments = _generateArguments(); + final query = _generateQueryString(); + final queryable = _queryMethod.queryable; // null queryable implies void-returning query method if (_queryMethod.returnsVoid || queryable == null) { - _methodBody.write(_generateNoReturnQuery(arguments)); - return _methodBody.toString(); - } - - final constructor = queryable.constructor; - final mapper = '(Map row) => $constructor'; - - if (_queryMethod.returnsStream) { - _methodBody.write(_generateStreamQuery(arguments, mapper)); + _methodBody.write(_generateNoReturnQuery(query, arguments)); } else { - _methodBody.write(_generateQuery(arguments, mapper)); + _methodBody.write(_generateQuery(query, arguments, queryable)); } return _methodBody.toString(); } - List _generateInClauseValueLists() { - return _queryMethod.parameters - .where((parameter) => parameter.type.isDartCoreList) - .mapIndexed((index, parameter) { - // TODO #403 what about type converters that map between e.g. string and list? - final flattenedParameterType = parameter.type.flatten(); - String value; - if (flattenedParameterType.isDefaultSqlType) { - value = '\$value'; + String _generateListConvertersForQuery() { + final code = StringBuffer(); + // because we ultimately want to give a query with numbered variables to sqflite, we have to compute them dynamically when working with lists. + // We establish the conventions that we provide the fixed parameters first and then append the list parameters one by one. + // parameters 1,2,... start-1 are already used by fixed (non-list) parameters. + final start = _queryMethod.parameters + .where((param) => !param.type.isDartCoreList) + .length + + 1; + + String? lastParam; + for (final listParam in _queryMethod.parameters + .where((param) => param.type.isDartCoreList)) { + if (lastParam == null) { + //make start final if it is only used once, fixes a lint + final constInt = + (start == _queryMethod.parameters.length) ? 'const' : 'int'; + code.writeln('$constInt offset = $start;'); } else { - final typeConverter = - _queryMethod.typeConverters.getClosest(flattenedParameterType); - value = '\${_${typeConverter.name.decapitalize()}.encode(value)}'; + code.writeln('offset += $lastParam.length;'); } - return '''final valueList$index = ${parameter.displayName}.map((value) => "'$value'").join(', ');'''; - }).toList(); + final currentParamName = listParam.displayName; + // dynamically generate strings of the form '?4,?5,?6,?7,?8' which we can + // later insert into the query at the marked locations. + code.write('final _sqliteVariablesFor${currentParamName.capitalize()}='); + code.write('Iterable.generate('); + code.write("$currentParamName.length, (i)=>'?\${i+offset}'"); + code.writeln(").join(',');"); + + lastParam = currentParamName; + } + return code.toString(); } List _generateParameters() { - return _queryMethod.parameters - .where((parameter) => !parameter.type.isDartCoreList) - .map((parameter) { - if (parameter.type.isDefaultSqlType) { - if (parameter.type.isDartCoreBool) { - // query method parameters can't be null - return '${parameter.displayName} ? 1 : 0'; + //first, take fixed parameters, then insert list parameters. + return [ + ..._queryMethod.parameters + .where((parameter) => !parameter.type.isDartCoreList) + .map((parameter) { + if (parameter.type.isDefaultSqlType) { + if (parameter.type.isDartCoreBool) { + // query method parameters can't be null + return '${parameter.displayName} ? 1 : 0'; + } else { + return parameter.displayName; + } } else { - return parameter.displayName; + final typeConverter = + _queryMethod.typeConverters.getClosest(parameter.type); + return '_${typeConverter.name.decapitalize()}.encode(${parameter.displayName})'; } - } else { - final typeConverter = - _queryMethod.typeConverters.getClosest(parameter.type); - return '_${typeConverter.name.decapitalize()}.encode(${parameter.displayName})'; - } - }).toList(); + }), + ..._queryMethod.parameters + .where((parameter) => parameter.type.isDartCoreList) + .map((parameter) { + // TODO #403 what about type converters that map between e.g. string and list? + final DartType flatType = parameter.type.flatten(); + if (flatType.isDefaultSqlType) { + return '...${parameter.displayName}'; + } else { + final typeConverter = + _queryMethod.typeConverters.getClosest(flatType); + return '...${parameter.displayName}.map((element) => _${typeConverter.name.decapitalize()}.encode(element))'; + } + }) + ]; } String? _generateArguments() { @@ -115,48 +146,52 @@ class QueryMethodWriter implements Writer { return parameters.isNotEmpty ? '[${parameters.join(', ')}]' : null; } - String _generateNoReturnQuery(final String? arguments) { - final parameters = StringBuffer()..write("'${_queryMethod.query}'"); + String _generateQueryString() { + final code = StringBuffer(); + int start = 0; + final originalQuery = _queryMethod.query.sql; + for (final listParameter in _queryMethod.query.listParameters) { + code.write( + originalQuery.substring(start, listParameter.position).toLiteral()); + code.write(' + _sqliteVariablesFor${listParameter.name.capitalize()} + '); + start = listParameter.position + varlistPlaceholder.length; + } + code.write(originalQuery.substring(start).toLiteral()); + + 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 _generateQuery( + final String query, final String? arguments, - final String mapper, + final Queryable queryable, ) { - final parameters = StringBuffer()..write("'${_queryMethod.query}', "); - if (arguments != null) parameters.write('arguments: $arguments, '); - parameters.write('mapper: $mapper'); + final mapper = _generateMapper(queryable); + final parameters = StringBuffer(query)..write(', mapper: $mapper'); + if (arguments != null) parameters.write(', arguments: $arguments'); - if (_queryMethod.returnsList) { - return 'return _queryAdapter.queryList($parameters);'; - } else { - return 'return _queryAdapter.query($parameters);'; + if (_queryMethod.returnsStream) { + // 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(', isView: ${queryable is View}'); } - } - String _generateStreamQuery( - final String? arguments, - final String mapper, - ) { - final queryable = _queryMethod.queryable; - // can't be null as validated before - if (queryable == null) throw ArgumentError.notNull(); - - final queryableName = queryable.name; - final isView = queryable is View; - final parameters = StringBuffer()..write("'${_queryMethod.query}', "); - if (arguments != null) parameters.write('arguments: $arguments, '); - parameters - ..write("queryableName: '$queryableName', ") - ..write('isView: $isView, ') - ..write('mapper: $mapper'); - - if (_queryMethod.returnsList) { - return 'return _queryAdapter.queryListStream($parameters);'; - } else { - return 'return _queryAdapter.queryStream($parameters);'; - } + final list = _queryMethod.returnsList ? 'List' : ''; + final stream = _queryMethod.returnsStream ? 'Stream' : ''; + + return 'return _queryAdapter.query$list$stream($parameters);'; } } + +String _generateMapper(Queryable queryable) { + final constructor = queryable.constructor; + return '(Map row) => $constructor'; +} diff --git a/floor_generator/test/misc/extension/string_extension_test.dart b/floor_generator/test/misc/extension/string_extension_test.dart index 3cf948c9..234d2a09 100644 --- a/floor_generator/test/misc/extension/string_extension_test.dart +++ b/floor_generator/test/misc/extension/string_extension_test.dart @@ -21,4 +21,41 @@ void main() { expect(actual, equals('fOO')); }); }); + + group('capitalize', () { + test('returns empty string for empty string', () { + expect(''.capitalize(), equals('')); + }); + + test('capitalizes first character for single character string', () { + expect('a'.capitalize(), equals('A')); + }); + + test('does nothing for single capitalized character string', () { + expect('A'.capitalize(), equals('A')); + }); + + test('capitalize word (first letter to lowercase)', () { + expect('fOO'.capitalize(), equals('FOO')); + }); + }); + + group('toLiteral', () { + test('null', () { + expect(null.toLiteral(), equals('null')); + }); + + test('empty string', () { + expect(''.toLiteral(), equals("''")); + }); + + test('Single-character-string', () { + expect('A'.toLiteral(), equals("'A'")); + }); + + test('long-string', () { + final actual = 'The quick brown fox jumps over the lazy dog'.toLiteral(); + expect(actual, equals("'The quick brown fox jumps over the lazy dog'")); + }); + }); } diff --git a/floor_generator/test/processor/query_method_processor_test.dart b/floor_generator/test/processor/query_method_processor_test.dart index 07103606..b3e25f4d 100644 --- a/floor_generator/test/processor/query_method_processor_test.dart +++ b/floor_generator/test/processor/query_method_processor_test.dart @@ -5,9 +5,11 @@ 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/error/query_method_processor_error.dart'; +import 'package:floor_generator/processor/error/query_processor_error.dart'; import 'package:floor_generator/processor/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.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'; @@ -40,7 +42,7 @@ void main() { QueryMethod( methodElement, 'findAllPersons', - 'SELECT * FROM Person', + Query('SELECT * FROM Person', []), await getDartTypeWithPerson('Future>'), await getDartTypeWithPerson('Person'), [], @@ -67,7 +69,7 @@ void main() { QueryMethod( methodElement, 'findAllNames', - 'SELECT * FROM name', + Query('SELECT * FROM name', []), await getDartTypeWithName('Future>'), await getDartTypeWithName('Name'), [], @@ -88,7 +90,35 @@ void main() { final actual = QueryMethodProcessor(methodElement, [], {}).process().query; - expect(actual, equals('SELECT * FROM Person WHERE id = ?')); + expect(actual.sql, equals('SELECT * FROM Person WHERE id = ?1')); + expect(actual.listParameters, equals([])); + }); + + test('parse query reusing a single parameter', () async { + final methodElement = await _createQueryMethodElement(''' + @Query('SELECT * FROM Person WHERE id = :id AND id = :id') + Future findPerson(int id); + '''); + + final actual = + QueryMethodProcessor(methodElement, [], {}).process().query.sql; + + expect(actual, equals('SELECT * FROM Person WHERE id = ?1 AND id = ?1')); + }); + + test('parse query with multiple unordered parameters', () async { + final methodElement = await _createQueryMethodElement(''' + @Query('SELECT * FROM Person WHERE name = :name AND id = :id AND id = :id AND name = :name') + Future findPerson(int id, String name); + '''); + + final actual = + QueryMethodProcessor(methodElement, [], {}).process().query.sql; + + expect( + actual, + equals('SELECT * FROM Person WHERE name = ?2' + ' AND id = ?1 AND id = ?1 AND name = ?2')); }); test('parse multiline query', () async { @@ -101,11 +131,12 @@ void main() { """); final actual = - QueryMethodProcessor(methodElement, [], {}).process().query; + QueryMethodProcessor(methodElement, [], {}).process().query.sql; expect( actual, - equals('SELECT * FROM person WHERE id = ? AND custom_name = ?'), + equals( + 'SELECT * FROM person WHERE id = ?1 AND custom_name = ?2'), ); }); @@ -117,11 +148,11 @@ void main() { '''); final actual = - QueryMethodProcessor(methodElement, [], {}).process().query; + QueryMethodProcessor(methodElement, [], {}).process().query.sql; expect( actual, - equals('SELECT * FROM person WHERE id = ? AND custom_name = ?'), + equals('SELECT * FROM person WHERE id = ?1 AND custom_name = ?2'), ); }); @@ -135,9 +166,10 @@ void main() { QueryMethodProcessor(methodElement, [], {}).process().query; expect( - actual, - equals(r'update sports set rated = 1 where id in ($valueList0)'), + actual.sql, + equals(r'update sports set rated = 1 where id in (:varlist)'), ); + expect(actual.listParameters, equals([ListParameter(41, 'ids')])); }); test('parses IN clause without space after IN', () async { @@ -150,9 +182,10 @@ void main() { QueryMethodProcessor(methodElement, [], {}).process().query; expect( - actual, - equals(r'update sports set rated = 1 where id in($valueList0)'), + actual.sql, + equals(r'update sports set rated = 1 where id in(:varlist)'), ); + expect(actual.listParameters, equals([ListParameter(40, 'ids')])); }); test('parses IN clause with multiple spaces after IN', () async { @@ -165,9 +198,10 @@ void main() { QueryMethodProcessor(methodElement, [], {}).process().query; expect( - actual, - equals(r'update sports set rated = 1 where id in ($valueList0)'), + actual.sql, + equals(r'update sports set rated = 1 where id in (:varlist)'), ); + expect(actual.listParameters, equals([ListParameter(46, 'ids')])); }); test('Parse query with multiple IN clauses', () async { @@ -180,12 +214,14 @@ void main() { QueryMethodProcessor(methodElement, [], {}).process().query; expect( - actual, + actual.sql, equals( - r'update sports set rated = 1 where id in ($valueList0) ' - r'and where foo in ($valueList1)', + r'update sports set rated = 1 where id in (:varlist) ' + r'and where foo in (:varlist)', ), ); + expect(actual.listParameters, + equals([ListParameter(41, 'ids'), ListParameter(69, 'bar')])); }); test('Parse query with IN clause and other parameter', () async { @@ -198,12 +234,33 @@ void main() { QueryMethodProcessor(methodElement, [], {}).process().query; expect( - actual, + actual.sql, + equals( + r'update sports set rated = 1 where id in (:varlist) ' + r'AND foo = ?1', + ), + ); + expect(actual.listParameters, equals([ListParameter(41, 'ids')])); + }); + + test('Parse query with mixed IN clauses and other parameters', () async { + final methodElement = await _createQueryMethodElement(''' + @Query('update sports set rated = 1 where id in (:ids) AND foo = :bar AND name in (:names) and :bar = :foo') + Future setRated(String foo, List names, List ids, int bar); + '''); + + final actual = + QueryMethodProcessor(methodElement, [], {}).process().query; + + expect( + actual.sql, equals( - r'update sports set rated = 1 where id in ($valueList0) ' - r'AND foo = ?', + r'update sports set rated = 1 where id in (:varlist) AND foo = ?2 ' + r'AND name in (:varlist) and ?2 = ?1', ), ); + expect(actual.listParameters, + equals([ListParameter(41, 'ids'), ListParameter(77, 'names')])); }); test('Parse query with LIKE operator', () async { @@ -213,9 +270,9 @@ void main() { '''); final actual = - QueryMethodProcessor(methodElement, [], {}).process().query; + QueryMethodProcessor(methodElement, [], {}).process().query.sql; - expect(actual, equals('SELECT * FROM Persons WHERE name LIKE ?')); + expect(actual, equals('SELECT * FROM Persons WHERE name LIKE ?1')); }); test('Parse query with commas', () async { @@ -225,9 +282,11 @@ void main() { '''); final actual = - QueryMethodProcessor(methodElement, [], {}).process().query; - - expect(actual, equals('SELECT * FROM ?, ?')); + 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. + expect(actual, equals('SELECT * FROM ?1, ?2')); }); }); @@ -275,6 +334,40 @@ void main() { expect(actual, throwsInvalidGenerationSourceError(error)); }); + test( + 'exception when query arguments do not match method parameters, no list vs list', + () async { + final methodElement = await _createQueryMethodElement(''' + @Query('SELECT * FROM Person WHERE id = :id') + Future findPersonByIdAndName(List id); + '''); + + final actual = () => + QueryMethodProcessor(methodElement, [...entities, ...views], {}) + .process(); + + final error = QueryProcessorError(methodElement) + .queryMethodParameterIsListButVariableIsNot(':id'); + expect(actual, throwsProcessorError(error)); + }); + + test( + 'exception when query arguments do not match method parameters, list vs no list', + () async { + final methodElement = await _createQueryMethodElement(''' + @Query('SELECT * FROM Person WHERE id IN (:id)') + Future findPersonByIdAndName(int id); + '''); + + final actual = () => + QueryMethodProcessor(methodElement, [...entities, ...views], {}) + .process(); + + final error = QueryProcessorError(methodElement) + .queryMethodParameterIsNormalButVariableIsList(':id'); + expect(actual, throwsProcessorError(error)); + }); + test('exception when query arguments do not match method parameters', () async { final methodElement = await _createQueryMethodElement(''' @@ -286,9 +379,9 @@ void main() { QueryMethodProcessor(methodElement, [...entities, ...views], {}) .process(); - final error = QueryMethodProcessorError(methodElement) - .queryArgumentsAndMethodParametersDoNotMatch; - expect(actual, throwsInvalidGenerationSourceError(error)); + final error = + QueryProcessorError(methodElement).unknownQueryVariable(':name'); + expect(actual, throwsProcessorError(error)); }); test('exception when passing nullable method parameter to query method', @@ -303,7 +396,7 @@ void main() { .process(); final parameterElement = methodElement.parameters.first; - final error = QueryMethodProcessorError(methodElement) + final error = QueryProcessorError(methodElement) .queryMethodParameterIsNullable(parameterElement); expect(actual, throwsProcessorError(error)); }); @@ -319,9 +412,9 @@ void main() { QueryMethodProcessor(methodElement, [...entities, ...views], {}) .process(); - final error = QueryMethodProcessorError(methodElement) - .queryArgumentsAndMethodParametersDoNotMatch; - expect(actual, throwsInvalidGenerationSourceError(error)); + final error = QueryProcessorError(methodElement) + .unusedQueryMethodParameter(methodElement.parameters[1]); + expect(actual, throwsProcessorError(error)); }); test( diff --git a/floor_generator/test/processor/query_processor_test.dart b/floor_generator/test/processor/query_processor_test.dart new file mode 100644 index 00000000..784a1026 --- /dev/null +++ b/floor_generator/test/processor/query_processor_test.dart @@ -0,0 +1,54 @@ +import 'package:floor_generator/processor/query_processor.dart'; +import 'package:test/test.dart'; + +void main() { + group('variable detection', () { + _testVarFind('empty_query1', '', []); + _testVarFind('empty_query2', ' ', []); + _testVarFind('empty_query3', '\n', []); + + _testVarFind('no_variable1', ':', []); + _testVarFind('no_variable2', ': foo', []); + _testVarFind('no_variable3', ' in (:)', []); + _testVarFind('no_variable4', '(:)', []); + _testVarFind('no_variable5', 'SELECT foo FROM bar WHERE id="more"', []); + + _testVarFind('simple_variable1', ':f', [VariableToken(':f', 0, false)]); + _testVarFind('simple_variable2', 'SELECT * FROM bar WHERE :id="more"', + [VariableToken(':id', 24, false)]); + _testVarFind('simple_variable3', 'SELECT * FROM bar WHERE x in :id', + [VariableToken(':id', 29, false)]); + _testVarFind('simple_variable4', ':id-:id2', + [VariableToken(':id', 0, false), VariableToken(':id2', 4, false)]); + _testVarFind('simple_variable5', ':id-:id2+:id', [ + VariableToken(':id', 0, false), + VariableToken(':id2', 4, false), + VariableToken(':id', 9, false) + ]); + _testVarFind('simple_variable6', ':id-: id2+:id', + [VariableToken(':id', 0, false), VariableToken(':id', 10, false)]); + _testVarFind('simple_variable7', 'SELECT * FROM bar WHERE xin (:id)', + [VariableToken(':id', 29, false)]); + + _testVarFind( + 'list_variable1', 'x in(:foo)', [VariableToken(':foo', 5, true)]); + _testVarFind( + 'list_variable2', 'x in (:foo)', [VariableToken(':foo', 6, true)]); + _testVarFind( + 'list_variable3', 'x IN (:foo)', [VariableToken(':foo', 8, true)]); + _testVarFind( + 'list_variable4', 'x In (:fo2o)', [VariableToken(':fo2o', 6, true)]); + _testVarFind('list_variable5', ':2x iN (:fo2o)', + [VariableToken(':2x', 0, false), VariableToken(':fo2o', 8, true)]); + }); +} + +void _testVarFind( + String testName, String query, List expectedOutput) { + test(testName, () { + expect( + findVariables(query), + orderedEquals(expectedOutput), + ); + }); +} diff --git a/floor_generator/test/writer/dao_writer_test.dart b/floor_generator/test/writer/dao_writer_test.dart index 370bea74..a43c27e7 100644 --- a/floor_generator/test/writer/dao_writer_test.dart +++ b/floor_generator/test/writer/dao_writer_test.dart @@ -159,7 +159,7 @@ void main() { @override Stream> findAllPersonsAsStream() { - return _queryAdapter.queryListStream('SELECT * FROM person', queryableName: 'Person', isView: false, mapper: (Map row) => Person(row['id'] as int, row['name'] as String)); + 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 7890bcd0..893c5829 100644 --- a/floor_generator/test/writer/query_method_writer_test.dart +++ b/floor_generator/test/writer/query_method_writer_test.dart @@ -45,7 +45,7 @@ void main() { expect(actual, equalsDart(r''' @override Future deletePersonById(int id) async { - await _queryAdapter.queryNoReturn('DELETE FROM Person WHERE id = ?', arguments: [id]); + await _queryAdapter.queryNoReturn('DELETE FROM Person WHERE id = ?1', arguments: [id]); } ''')); }); @@ -61,7 +61,7 @@ void main() { expect(actual, equalsDart(r''' @override Future findById(int id) async { - return _queryAdapter.query('SELECT * FROM Person WHERE id = ?', arguments: [id], mapper: (Map row) => Person(row['id'] as int, row['name'] as String)); + return _queryAdapter.query('SELECT * FROM Person WHERE id = ?1', mapper: (Map row) => Person(row['id'] as int, row['name'] as String), arguments: [id]); } ''')); }); @@ -85,7 +85,7 @@ void main() { expect(actual, equalsDart(r''' @override Future findById(int id) async { - return _queryAdapter.query('SELECT * FROM Order WHERE id = ?', arguments: [id], mapper: (Map row) => Order(row['id'] as int, _dateTimeConverter.decode(row['dateTime'] as int))); + 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]); } ''')); }); @@ -109,7 +109,7 @@ void main() { expect(actual, equalsDart(r''' @override Future findByDateTime(DateTime dateTime) async { - return _queryAdapter.query('SELECT * FROM Order WHERE dateTime = ?', arguments: [_dateTimeConverter.encode(dateTime)], mapper: (Map row) => Order(row['id'] as int, _externalTypeConverter.decode(row['dateTime'] as int))); + 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)]); } ''')); }); @@ -133,7 +133,7 @@ void main() { expect(actual, equalsDart(r''' @override Future findByDateTime(DateTime dateTime) async { - return _queryAdapter.query('SELECT * FROM Order WHERE dateTime = ?', arguments: [_dateTimeConverter.encode(dateTime)], mapper: (Map row) => Order(row['id'] as int, _externalTypeConverter.decode(row['dateTime'] as int))); + 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)]); } ''')); }); @@ -157,13 +157,47 @@ void main() { expect(actual, equalsDart(r''' @override Future> findByDates(List dates) async { - final valueList0 = dates.map((value) => "'${_dateTimeConverter.encode(value)}'").join(', '); - return _queryAdapter.queryList('SELECT * FROM Order WHERE date IN ($valueList0)', mapper: (Map row) => Order(row['id'] as int, _dateTimeConverter.decode(row['dateTime'] as int))); + 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))]); } ''')); }); }); + test( + 'Query with multiple IN clauses, reusing and mixing with normal parameters, including converters', + () async { + final typeConverter = TypeConverter( + 'DateTimeConverter', + await dateTimeDartType, + await intDartType, + TypeConverterScope.database, + ); + final queryMethod = await ''' + @Query('SELECT * FROM Order WHERE id IN (:ids) AND id IN (:dateTimeList) OR foo in (:ids) AND bar = :foo OR name = :name') + Future> findWithIds(List ids, String name, @TypeConverters([DateTimeConverter]) List dateTimeList, DateTime foo); + ''' + .asOrderQueryMethod({typeConverter}); + + final actual = QueryMethodWriter(queryMethod).write(); + + expect(actual, equalsDart(r''' + @override + Future> findWithIds(List ids, String name, List dateTimeList, DateTime foo) async { + int offset = 3; + 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))]); + } + ''')); + }); + test('query boolean parameter', () async { final queryMethod = await _createQueryMethod(''' @Query('SELECT * FROM Person WHERE flag = :flag') @@ -175,7 +209,7 @@ void main() { expect(actual, equalsDart(r''' @override Future> findWithFlag(bool flag) async { - return _queryAdapter.queryList('SELECT * FROM Person WHERE flag = ?', arguments: [flag ? 1 : 0], mapper: (Map row) => Person(row['id'] as int, row['name'] as String)); + 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]); } ''')); }); @@ -191,7 +225,23 @@ void main() { expect(actual, equalsDart(r''' @override Future findById(int id, String name) async { - return _queryAdapter.query('SELECT * FROM Person WHERE id = ? AND name = ?', arguments: [id, name], mapper: (Map row) => Person(row['id'] as int, row['name'] as String)); + 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]); + } + ''')); + }); + + test('query item multiple mixed and reused parameters', () async { + final queryMethod = await _createQueryMethod(''' + @Query('SELECT * FROM Person WHERE foo = :bar AND id = :id AND name = :name AND name = :bar') + Future findById(int id, String name, String bar); + '''); + + final actual = QueryMethodWriter(queryMethod).write(); + + 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]); } ''')); }); @@ -223,7 +273,7 @@ void main() { expect(actual, equalsDart(r''' @override Stream findByIdAsStream(int id) { - return _queryAdapter.queryStream('SELECT * FROM Person WHERE id = ?', arguments: [id], queryableName: 'Person', isView: false, mapper: (Map row) => Person(row['id'] as int, row['name'] as String)); + 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); } ''')); }); @@ -239,7 +289,7 @@ void main() { expect(actual, equalsDart(r''' @override Stream> findAllAsStream() { - return _queryAdapter.queryListStream('SELECT * FROM Person', queryableName: 'Person', isView: false, mapper: (Map row) => Person(row['id'] as int, row['name'] as String)); + return _queryAdapter.queryListStream('SELECT * FROM Person', mapper: (Map row) => Person(row['id'] as int, row['name'] as String), queryableName: 'Person', isView: false); } ''')); }); @@ -255,7 +305,7 @@ void main() { expect(actual, equalsDart(r''' @override Stream> findAllAsStream() { - return _queryAdapter.queryListStream('SELECT * FROM Name', queryableName: 'Name', isView: true, mapper: (Map row) => Name(row['name'] as String)); + return _queryAdapter.queryListStream('SELECT * FROM Name', mapper: (Map row) => Name(row['name'] as String), queryableName: 'Name', isView: true); } ''')); }); @@ -271,9 +321,12 @@ void main() { expect(actual, equalsDart(r''' @override Future> findWithIds(List ids) async { - final valueList0 = ids.map((value) => "'$value'").join(', '); - return _queryAdapter.queryList('SELECT * FROM Person WHERE id IN ($valueList0)', mapper: (Map row) => Person(row['id'] as int, row['name'] as String)); - } + 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]); + } ''')); }); @@ -288,8 +341,11 @@ void main() { expect(actual, equalsDart(r''' @override Future> findWithIds(List ids) async { - final valueList0 = ids.map((value) => "'$value'").join(', '); - return _queryAdapter.queryList('SELECT * FROM Person WHERE id IN($valueList0)', mapper: (Map row) => Person(row['id'] as int, row['name'] as String)); + 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]); } ''')); }); @@ -305,9 +361,37 @@ void main() { expect(actual, equalsDart(r''' @override Future> findWithIds(List ids, List idx) async { - final valueList0 = ids.map((value) => "'$value'").join(', '); - final valueList1 = idx.map((value) => "'$value'").join(', '); - return _queryAdapter.queryList('SELECT * FROM Person WHERE id IN ($valueList0) AND id IN ($valueList1)', mapper: (Map row) => Person(row['id'] as int, row['name'] as String)); + int offset = 1; + final _sqliteVariablesForIds=Iterable.generate(ids.length, (i)=>'?${i+offset}').join(','); + 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]); + } + ''')); + }); + + test( + 'Query with multiple IN clauses, reusing and mixing with normal parameters', + () async { + final queryMethod = await _createQueryMethod(''' + @Query('SELECT * FROM Person WHERE id IN (:ids) AND id IN (:idx) OR foo in (:ids) AND bar = :foo OR name = :name') + Future> findWithIds(List idx, String name, List ids, int foo); + '''); + + final actual = QueryMethodWriter(queryMethod).write(); + + expect(actual, equalsDart(r''' + @override + Future> findWithIds(List idx, String name, List ids, int foo) async { + int offset = 3; + final _sqliteVariablesForIdx=Iterable.generate(idx.length, (i)=>'?${i+offset}').join(','); + 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]); } ''')); });