Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved Parameter mapping for query methods #509

Merged
merged 7 commits into from
Mar 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions floor/test/integration/dao/name_dao.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ abstract class NameDao {
@Query('SELECT * FROM names WHERE name LIKE :suffix ORDER BY name ASC')
Future<List<Name>> findNamesLike(String suffix);

@Query(
'SELECT * FROM names WHERE name LIKE :suffix AND name LIKE :prefix ORDER BY name ASC')
Future<List<Name>> findNamesMatchingBoth(String prefix, String suffix);

@Query('SELECT * FROM multiline_query_names WHERE name = :name')
Future<MultilineQueryName?> findMultilineQueryName(String name);
}
5 changes: 5 additions & 0 deletions floor/test/integration/dao/person_dao.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ abstract class PersonDao {
@Query('SELECT * FROM person WHERE custom_name IN (:names)')
Future<List<Person>> findPersonsWithNames(List<String> names);

@Query(
'SELECT * FROM person WHERE custom_name IN (:names) AND id>=:reference OR custom_name IN (:moreNames) AND id<=:reference')
Future<List<Person>> findPersonsWithNamesComplex(
int reference, List<String> names, List<String> moreNames);

@Query('SELECT * FROM person WHERE custom_name LIKE :name')
Future<List<Person>> findPersonsWithNamesLike(String name);

Expand Down
29 changes: 29 additions & 0 deletions floor/test/integration/database_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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', () {
Expand Down
13 changes: 13 additions & 0 deletions floor/test/integration/view/view_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
36 changes: 36 additions & 0 deletions floor_generator/lib/misc/extension/string_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
vitusortner marked this conversation as resolved.
Show resolved Hide resolved
if (this == null) {
return 'null';
} else {
//TODO escape correctly
vitusortner marked this conversation as resolved.
Show resolved Hide resolved
return "'$this'";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand All @@ -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.',
Expand Down
69 changes: 69 additions & 0 deletions floor_generator/lib/processor/error/query_processor_error.dart
Original file line number Diff line number Diff line change
@@ -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,
);
}
}
44 changes: 6 additions & 38 deletions floor_generator/lib/processor/query_method_processor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -36,7 +37,9 @@ class QueryMethodProcessor extends Processor<QueryMethod> {
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);
Expand Down Expand Up @@ -90,29 +93,10 @@ class QueryMethodProcessor extends Processor<QueryMethod> {
.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(
Expand Down Expand Up @@ -143,22 +127,6 @@ class QueryMethodProcessor extends Processor<QueryMethod> {
}
}

void _assertQueryParameters(
final String query,
final List<ParameterElement> 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,
Expand Down
Loading