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

Allow Dart core types in Query return type. #691

Merged
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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ import 'package:floor/floor.dart';
@dao
abstract class PersonDao {
@Query('SELECT * FROM Person')
Future<List<Person>> findAllPersons();
Future<List<Person>> findAllPeople();

@Query('SELECT name FROM Person')
Stream<List<String>> findAllPeopleName();

@Query('SELECT * FROM Person WHERE id = :id')
Stream<Person?> findPersonById(int id);
Expand Down
41 changes: 24 additions & 17 deletions docs/daos.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ DAO classes can use inherited methods by implementing and extending classes whil
@dao
abstract class PersonDao {
@Query('SELECT * FROM Person')
Future<List<Person>> findAllPersons();
Future<List<Person>> findAllPeople();

@Query('SELECT * FROM Person WHERE id = :id')
Stream<Person?> findPersonById(int id);
Expand All @@ -20,8 +20,9 @@ abstract class PersonDao {
## Queries
Method signatures turn into query methods by adding the `@Query()` annotation with the query in parenthesis to them.
Be mindful about the correctness of your SQL statements as they are only partly validated while generating the code.
These queries have to return either a `Future` or a `Stream` of an entity or `void`.
Returning `Future<void>` comes in handy whenever you want to delete the full content of a table, for instance.
These queries have to return either a `Future` or a `Stream` of an entity, Dart core type or `void`.
Retrieval of Dart Core types such as `String`, `double`, `int`, `double`, `Uint8List` can be used if you want to get all records from a certain column or return `COUNT` records in the table.
Returning `Future<void>` comes in handy whenever you want to delete the full content of a table, for instance.
Some query method examples can be seen in the following.

A function returning a single item will return `null` when no matching row is found.
Expand All @@ -36,17 +37,23 @@ Future<Person?> findPersonById(int id);
@Query('SELECT * FROM Person WHERE id = :id AND name = :name')
Future<Person?> findPersonByIdAndName(int id, String name);

@Query('SELECT COUNT(id) FROM Person')
Future<int?> getPeopleCount(); // fetch records count

@Query('SELECT name FROM Person')
Future<List<String>> getAllPeopleNames(); // fetch all records from one column

@Query('SELECT * FROM Person')
Future<List<Person>> findAllPersons(); // select multiple items
Future<List<Person>> findAllPeople(); // select multiple items

@Query('SELECT * FROM Person')
Stream<List<Person>> findAllPersonsAsStream(); // stream return
Stream<List<Person>> findAllPeopleAsStream(); // stream return

@Query('DELETE FROM Person')
Future<void> deleteAllPersons(); // query without returning an entity
Future<void> deleteAllPeople(); // query without returning an entity

@Query('SELECT * FROM Person WHERE id IN (:ids)')
Future<List<Person>> findPersonsWithIds(List<int> ids); // query with IN clause
Future<List<Person>> findPeopleWithIds(List<int> ids); // query with IN clause
```

Query arguments, when using SQLite's `LIKE` operator, have to be supplied by the input of a method.
Expand All @@ -55,11 +62,11 @@ It's not possible to define a pattern matching argument like `%foo%` in the quer
```dart
// dao
@Query('SELECT * FROM Person WHERE name LIKE :name')
Future<List<Person>> findPersonsWithNamesLike(String name);
Future<List<Person>> findPeopleWithNamesLike(String name);

// usage
final name = '%foo%';
await dao.findPersonsWithNamesLike(name);
await dao.findPeopleWithNamesLike(name);
```

## Data Changes
Expand All @@ -81,7 +88,7 @@ These methods can return a `Future` of either `void`, `int` or `List<int>`.
Future<void> insertPerson(Person person);

@insert
Future<List<int>> insertPersons(List<Person> persons);
Future<List<int>> insertPeople(List<Person> people);
```

### Update
Expand All @@ -98,7 +105,7 @@ These methods can return a `Future` of either `void` or `int`.
Future<void> updatePerson(Person person);

@update
Future<int> updatePersons(List<Person> persons);
Future<int> updatePeople(List<Person> people);
```

### Delete
Expand All @@ -113,7 +120,7 @@ These methods can return a `Future` of either `void` or `int`.
Future<void> deletePerson(Person person);

@delete
Future<int> deletePersons(List<Person> persons);
Future<int> deletePeople(List<Person> people);
```

## Streams
Expand All @@ -135,12 +142,12 @@ abstract class PersonDao {
Stream<Person?> findPersonByIdAsStream(int id);

@Query('SELECT * FROM Person')
Stream<List<Person>> findAllPersonsAsStream();
Stream<List<Person>> findAllPeopleAsStream();
}

// usage
StreamBuilder<List<Person>>(
stream: dao.findAllPersonsAsStream(),
stream: dao.findAllPeopleAsStream(),
builder: (BuildContext context, AsyncSnapshot<List<Person>> snapshot) {
// do something with the values here
},
Expand All @@ -160,9 +167,9 @@ It's also required to add the `async` modifier. These methods have to return a `

```dart
@transaction
Future<void> replacePersons(List<Person> persons) async {
await deleteAllPersons();
await insertPersons(persons);
Future<void> replacePeople(List<Person> people) async {
await deleteAllPeople();
await insertPeople(people);
}
```

Expand Down
12 changes: 10 additions & 2 deletions example/lib/database.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ class TasksWidgetState extends State<TasksWidget> {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
centerTitle: true,
leading: Align(
alignment: Alignment.center,
child: StreamBuilder(
stream: widget.dao.findUniqueMessagesCountAsStream(),
builder: (_, snapshot) => Text('count: ${snapshot.data ?? 0}')),
),
actions: <Widget>[
PopupMenuButton<int>(
itemBuilder: (context) {
Expand Down
3 changes: 3 additions & 0 deletions example/lib/task_dao.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ abstract class TaskDao {
@Query('SELECT * FROM task')
Stream<List<Task>> findAllTasksAsStream();

@Query('SELECT DISTINCT COUNT(message) FROM task')
Stream<int?> findUniqueMessagesCountAsStream();

@Query('SELECT * FROM task WHERE type = :type')
Stream<List<Task>> findAllTasksByTypeAsStream(TaskType type);

Expand Down
7 changes: 5 additions & 2 deletions floor/lib/src/adapter/query_adapter.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:async';

import 'package:floor/src/util/string_utils.dart';
import 'package:sqflite/sqflite.dart';

/// This class knows how to execute database queries.
Expand Down Expand Up @@ -73,7 +74,8 @@ class QueryAdapter {
// listen on all updates if the stream is on a view, only listen to the
// name of the table if the stream is on a entity.
final subscription = changeListener.stream
.where((updatedTable) => updatedTable == queryableName || isView)
.where((updatedTable) =>
isView || updatedTable.equals(queryableName, ignoreCase: true))
.listen(
(_) async => executeQueryAndNotifyController(),
onDone: () => controller.close(),
Expand Down Expand Up @@ -105,7 +107,8 @@ class QueryAdapter {

// Views listen on all events, Entities only on events that changed the same entity.
final subscription = changeListener.stream
.where((updatedTable) => isView || updatedTable == queryableName)
.where((updatedTable) =>
isView || updatedTable.equals(queryableName, ignoreCase: true))
.listen(
(_) async => executeQueryAndNotifyController(),
onDone: () => controller.close(),
Expand Down
5 changes: 5 additions & 0 deletions floor/lib/src/util/string_utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
extension StringExt on String {
bool equals(String other, {bool ignoreCase = false}) {
return ignoreCase ? toLowerCase() == other.toLowerCase() : this == other;
}
}
5 changes: 4 additions & 1 deletion floor/test/integration/dao/person_dao.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ abstract class PersonDao {
@Query('SELECT * FROM person WHERE id = :id')
Stream<Person?> findPersonByIdAsStream(int id);

@Query('SELECT DISTINCT COUNT(id) FROM person')
Stream<int?> uniqueRecordsCountAsStream();

@Query('SELECT * FROM person WHERE id = :id AND custom_name = :name')
Future<Person?> findPersonByIdAndName(int id, String name);

Expand Down Expand Up @@ -89,6 +92,6 @@ abstract class PersonDao {
Future<void> deleteAllPersons();

// Used in regression test for Streams on Entities with update methods in other Dao
@Query('SELECT * FROM Dog WHERE owner_id = :id')
@Query('SELECT * FROM dog WHERE owner_id = :id')
Stream<List<Dog>> findAllDogsOfPersonAsStream(int id);
}
12 changes: 12 additions & 0 deletions floor/test/integration/stream_query_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ void main() {
await personDao.insertPersons(persons2);
expect(actual, emits(persons + persons2));
});

test('unique records count', () async {
final persons = [Person(1, 'Simon'), Person(2, 'Frank')];
final persons2 = [Person(3, 'Paul'), Person(4, 'George')];
await personDao.insertPersons(persons);

final actual = personDao.uniqueRecordsCountAsStream();
expect(actual, emits(persons.length));

await personDao.insertPersons(persons2);
expect(actual, emits(persons.length + persons2.length));
});
});

group('update change', () {
Expand Down
43 changes: 43 additions & 0 deletions floor_generator/lib/misc/extension/string_extension.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:floor_generator/misc/extension/dart_type_extension.dart';
import 'package:floor_generator/misc/type_utils.dart';
import 'package:source_gen/source_gen.dart';
import 'package:strings/strings.dart';

extension StringExtension on String {
Expand Down Expand Up @@ -54,3 +59,41 @@ extension NullableStringExtension on String? {
}
}
}

extension CastStringExtension on String {
String cast(DartType dartType, Element? parameterElement,
{bool withNullability = true}) {
if (dartType.isDartCoreBool) {
final booleanDeserializer = '($this as int) != 0';
if (dartType.isNullable && withNullability) {
// if the value is null, return null
// if the value is not null, interpret 1 as true and 0 as false
return '$this == null ? null : $booleanDeserializer';
} else {
return booleanDeserializer;
}
} else if (dartType.isEnumType) {
final typeString = dartType.getDisplayString(withNullability: false);
final enumDeserializer = '$typeString.values[$this as int]';
if (dartType.isNullable && withNullability) {
return '$this == null ? null : $enumDeserializer';
} else {
return enumDeserializer;
}
} else if (dartType.isDartCoreString ||
dartType.isDartCoreInt ||
dartType.isUint8List ||
dartType.isDartCoreDouble) {
final typeString = dartType.getDisplayString(
withNullability: withNullability,
);
return '$this as $typeString';
} else {
throw InvalidGenerationSourceError(
'Trying to convert unsupported type $dartType.',
todo: 'Consider adding a type converter.',
element: parameterElement,
);
}
}
}
18 changes: 18 additions & 0 deletions floor_generator/lib/processor/error/query_method_writer_error.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:analyzer/dart/element/element.dart';
import 'package:source_gen/source_gen.dart';

class QueryMethodWriterError {
final MethodElement _methodElement;

QueryMethodWriterError(final MethodElement methodElement)
: _methodElement = methodElement;

InvalidGenerationSourceError queryMethodReturnType() {
return InvalidGenerationSourceError(
'Can not define return type',
todo:
'Add supported return type to your query. https://pinchbv.github.io/floor/daos/#queries',
element: _methodElement,
);
}
}
38 changes: 0 additions & 38 deletions floor_generator/lib/processor/queryable_processor.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:collection/collection.dart';
import 'package:floor_annotation/floor_annotation.dart' as annotations;
import 'package:floor_generator/misc/extension/dart_type_extension.dart';
import 'package:floor_generator/misc/extension/set_extension.dart';
import 'package:floor_generator/misc/extension/string_extension.dart';
import 'package:floor_generator/misc/extension/type_converter_element_extension.dart';
Expand All @@ -15,7 +13,6 @@ import 'package:floor_generator/value_object/field.dart';
import 'package:floor_generator/value_object/queryable.dart';
import 'package:floor_generator/value_object/type_converter.dart';
import 'package:meta/meta.dart';
import 'package:source_gen/source_gen.dart';

abstract class QueryableProcessor<T extends Queryable> extends Processor<T> {
final QueryableProcessorError _queryableProcessorError;
Expand Down Expand Up @@ -106,41 +103,6 @@ abstract class QueryableProcessor<T extends Queryable> extends Processor<T> {
}
}

extension on String {
String cast(DartType dartType, ParameterElement parameterElement) {
if (dartType.isDartCoreBool) {
final booleanDeserializer = '($this as int) != 0';
if (dartType.isNullable) {
// if the value is null, return null
// if the value is not null, interpret 1 as true and 0 as false
return '$this == null ? null : $booleanDeserializer';
} else {
return booleanDeserializer;
}
} else if (dartType.isEnumType) {
final typeString = dartType.getDisplayString(withNullability: false);
final enumDeserializer = '$typeString.values[$this as int]';
if (dartType.isNullable) {
return '$this == null ? null : $enumDeserializer';
} else {
return enumDeserializer;
}
} else if (dartType.isDartCoreString ||
dartType.isDartCoreInt ||
dartType.isUint8List ||
dartType.isDartCoreDouble) {
final typeString = dartType.getDisplayString(withNullability: true);
return '$this as $typeString';
} else {
throw InvalidGenerationSourceError(
'Trying to convert unsupported type $dartType.',
todo: 'Consider adding a type converter.',
element: parameterElement,
);
}
}
}

extension on FieldElement {
bool shouldBeIncluded() {
final isIgnored = hasAnnotation(annotations.ignore.runtimeType);
Expand Down
Loading