Skip to content

Commit

Permalink
Implement simple Streams on DatabaseViews, fix multi-dao changelisten…
Browse files Browse the repository at this point in the history
…er (#320)

* Implement simple Streams on DatabaseViews, fix multi-dao changelistener

* clean up

* update generated db of example project

* apply suggestions from review

* Add tests, fix errors

* fix generated example code and run dartfmt in example/

* add tests

* Simplify QueryMethodProcessor

* Apply suggestions from code review

some quick fixes

Co-authored-by: Vitus <vitusortner.dev@gmail.com>

* Apply suggestions from second review, part 2

Co-authored-by: Vitus <vitusortner.dev@gmail.com>
  • Loading branch information
mqus and vitusortner authored May 23, 2020
1 parent 49fbff2 commit 527b0cb
Show file tree
Hide file tree
Showing 23 changed files with 565 additions and 149 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,9 @@ abstract class AppDatabase extends FloorDatabase {
You can then query the view via a DAO function like an entity.

#### Limitations
- Be aware it is currently not possible to return a `Stream` object from a function which queries a database view.
- It is now possible to return a `Stream` object from a DAO method which queries a database view. But it will fire on **any**
`@update`, `@insert`, `@delete` events in the whole database, which can get quite taxing on the runtime. Please add it only if you know what you are doing!
This is mostly due to the complexity of detecting which entities are involved in a database view.

## Data Access Objects
These components are responsible for managing access to the underlying SQLite database and are defined as abstract classes with method signatures and query statements.
Expand Down Expand Up @@ -475,7 +477,8 @@ StreamBuilder<List<Person>>(
#### Limitations
- Only methods annotated with `@insert`, `@update` and `@delete` trigger `Stream` emissions.
Inserting data by using the `@Query()` annotation doesn't.
- It is not possible to return a `Stream` if the function queries a database view.
- It is now possible to return a `Stream` if the function queries a database view. But it will fire on **any**
`@update`, `@insert`, `@delete` events in the whole database, which can get quite taxing on the runtime. Please add it only if you know what you are doing!
This is mostly due to the complexity of detecting which entities are involved in a database view.

### Transactions
Expand Down
2 changes: 1 addition & 1 deletion example/lib/database.g.dart

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

8 changes: 4 additions & 4 deletions example/lib/task.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ class Task {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Task &&
runtimeType == other.runtimeType &&
id == other.id &&
message == other.message;
other is Task &&
runtimeType == other.runtimeType &&
id == other.id &&
message == other.message;

@override
int get hashCode => id.hashCode ^ message.hashCode;
Expand Down
7 changes: 5 additions & 2 deletions floor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,9 @@ abstract class AppDatabase extends FloorDatabase {
You can then query the view via a DAO function like an entity.

#### Limitations
- Be aware it is currently not possible to return a `Stream` object from a function which queries a database view.
- It is now possible to return a `Stream` object from a DAO method which queries a database view. But it will fire on **any**
`@update`, `@insert`, `@delete` events in the whole database, which can get quite taxing on the runtime. Please add it only if you know what you are doing!
This is mostly due to the complexity of detecting which entities are involved in a database view.

## Data Access Objects
These components are responsible for managing access to the underlying SQLite database and are defined as abstract classes with method signatures and query statements.
Expand Down Expand Up @@ -475,7 +477,8 @@ StreamBuilder<List<Person>>(
#### Limitations
- Only methods annotated with `@insert`, `@update` and `@delete` trigger `Stream` emissions.
Inserting data by using the `@Query()` annotation doesn't.
- It is not possible to return a `Stream` if the function queries a database view.
- It is now possible to return a `Stream` if the function queries a database view. But it will fire on **any**
`@update`, `@insert`, `@delete` events in the whole database, which can get quite taxing on the runtime. Please add it only if you know what you are doing!
This is mostly due to the complexity of detecting which entities are involved in a database view.

### Transactions
Expand Down
13 changes: 9 additions & 4 deletions floor/lib/src/adapter/query_adapter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ class QueryAdapter {
Stream<T> queryStream<T>(
final String sql, {
final List<dynamic> arguments,
@required final String tableName,
@required final String queryableName,
@required final bool isView,
@required final T Function(Map<String, dynamic>) mapper,
}) {
assert(_changeListener != null);
Expand All @@ -70,8 +71,10 @@ class QueryAdapter {

controller.onListen = () async => executeQueryAndNotifyController();

// 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 == tableName)
.where((updatedTable) => updatedTable == queryableName || isView)
.listen(
(_) async => executeQueryAndNotifyController(),
onDone: () => controller.close(),
Expand All @@ -86,7 +89,8 @@ class QueryAdapter {
Stream<List<T>> queryListStream<T>(
final String sql, {
final List<dynamic> arguments,
@required final String tableName,
@required final String queryableName,
@required final bool isView,
@required final T Function(Map<String, dynamic>) mapper,
}) {
assert(_changeListener != null);
Expand All @@ -100,8 +104,9 @@ class QueryAdapter {

controller.onListen = () async => executeQueryAndNotifyController();

// Views listen on all events, Entities only on events that changed the same entity.
final subscription = _changeListener.stream
.where((updatedTable) => updatedTable == tableName)
.where((updatedTable) => isView || updatedTable == queryableName)
.listen(
(_) async => executeQueryAndNotifyController(),
onDone: () => controller.close(),
Expand Down
47 changes: 37 additions & 10 deletions floor/test/adapter/query_adapter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ void main() {
]);
when(mockDatabaseExecutor.rawQuery(sql)).thenAnswer((_) => queryResult);

final actual =
underTest.queryStream(sql, tableName: entityName, mapper: mapper);
final actual = underTest.queryStream(sql,
queryableName: entityName, isView: false, mapper: mapper);

expect(actual, emits(person));
});
Expand All @@ -179,7 +179,8 @@ void main() {
final actual = underTest.queryStream(
sql,
arguments: arguments,
tableName: entityName,
queryableName: entityName,
isView: false,
mapper: mapper,
);

Expand All @@ -193,8 +194,8 @@ void main() {
]);
when(mockDatabaseExecutor.rawQuery(sql)).thenAnswer((_) => queryResult);

final actual =
underTest.queryStream(sql, tableName: entityName, mapper: mapper);
final actual = underTest.queryStream(sql,
queryableName: entityName, isView: false, mapper: mapper);
streamController.add(entityName);

expect(actual, emitsInOrder(<Person>[person, person]));
Expand All @@ -209,8 +210,8 @@ void main() {
]);
when(mockDatabaseExecutor.rawQuery(sql)).thenAnswer((_) => queryResult);

final actual =
underTest.queryListStream(sql, tableName: entityName, mapper: mapper);
final actual = underTest.queryListStream(sql,
queryableName: entityName, isView: false, mapper: mapper);

expect(actual, emits([person, person2]));
});
Expand All @@ -229,7 +230,8 @@ void main() {
final actual = underTest.queryListStream(
sql,
arguments: arguments,
tableName: entityName,
queryableName: entityName,
isView: false,
mapper: mapper,
);

Expand All @@ -245,8 +247,8 @@ void main() {
]);
when(mockDatabaseExecutor.rawQuery(sql)).thenAnswer((_) => queryResult);

final actual =
underTest.queryListStream(sql, tableName: entityName, mapper: mapper);
final actual = underTest.queryListStream(sql,
queryableName: entityName, isView: false, mapper: mapper);
streamController.add(entityName);

expect(
Expand All @@ -257,5 +259,30 @@ void main() {
]),
);
});

test('query stream from view with same and different triggering entity',
() async {
final person = Person(1, 'Frank');
final person2 = Person(2, 'Peter');
final queryResult = Future(() => [
<String, dynamic>{'id': person.id, 'name': person.name},
<String, dynamic>{'id': person2.id, 'name': person2.name},
]);
when(mockDatabaseExecutor.rawQuery(sql)).thenAnswer((_) => queryResult);

final actual = underTest.queryListStream(sql,
queryableName: entityName, isView: true, mapper: mapper);
expect(
actual,
emitsInOrder(<List<Person>>[
<Person>[person, person2],
<Person>[person, person2],
<Person>[person, person2]
]),
);

streamController.add(entityName);
streamController.add('otherEntity');
});
});
}
3 changes: 3 additions & 0 deletions floor/test/integration/dao/name_dao.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ abstract class NameDao {
@Query('SELECT * FROM names ORDER BY name ASC')
Future<List<Name>> findAllNames();

@Query('SELECT * FROM names ORDER BY name ASC')
Stream<List<Name>> findAllNamesAsStream();

@Query('SELECT * FROM names WHERE name = :name')
Future<Name> findExactName(String name);

Expand Down
5 changes: 5 additions & 0 deletions floor/test/integration/dao/person_dao.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:floor/floor.dart';

import '../model/dog.dart';
import '../model/person.dart';

@dao
Expand Down Expand Up @@ -72,4 +73,8 @@ abstract class PersonDao {

@Query('DELETE FROM person')
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')
Stream<List<Dog>> findAllDogsOfPersonAsStream(int id);
}
32 changes: 32 additions & 0 deletions floor/test/integration/stream_query_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter_test/flutter_test.dart';
import '../test_util/extensions.dart';
import 'dao/person_dao.dart';
import 'database.dart';
import 'model/dog.dart';
import 'model/person.dart';

void main() {
Expand Down Expand Up @@ -126,5 +127,36 @@ void main() {
expect(actual, emits(<Person>[]));
});
});

test('regression test streaming updates from other Dao', () async {
final person1 = Person(1, 'Simon');
final person2 = Person(2, 'Frank');
final dog1 = Dog(1, 'Dog', 'Doggie', person1.id);
final dog2 = Dog(2, 'OtherDog', 'Doggo', person2.id);

final actual = personDao.findAllDogsOfPersonAsStream(person1.id);
expect(
actual,
emitsInOrder(<List<Dog>>[
[], // initial state,
[dog1], // after inserting dog1
[dog1], // after inserting dog2
//[], // after removing person1. Does not work because
// ForeignKey-relations are not considered yet (#321)
]));

await personDao.insertPerson(person1);
await personDao.insertPerson(person2);

await database.dogDao.insertDog(dog1);

await database.dogDao.insertDog(dog2);

// avoid that delete happens before the re-execution of
// the select query for the stream
await Future<void>.delayed(const Duration(milliseconds: 100));

await database.personDao.deletePerson(person1);
});
});
}
42 changes: 42 additions & 0 deletions floor/test/integration/view/view_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,48 @@ void main() {
expect(actual, equals(expected));
});

test('query view with all values as stream', () async {
final actual = nameDao.findAllNamesAsStream();
expect(
actual,
emitsInOrder(<List<Name>>[
[], // initial state
[Name('Frank'), Name('Leo')], // after inserting Persons
[
// after inserting Dog:
Name('Frank'),
Name('Leo'),
Name('Romeo')
],
[
// after updating Leo:
Name('Frank'),
Name('Leonhard'),
Name('Romeo')
],
[Name('Frank')], // after removing Person (and associated Dog)
]));

final persons = [Person(1, 'Leo'), Person(2, 'Frank')];
await personDao.insertPersons(persons);

await Future<void>.delayed(const Duration(milliseconds: 100));

final dog = Dog(1, 'Romeo', 'Rome', 1);
await dogDao.insertDog(dog);

await Future<void>.delayed(const Duration(milliseconds: 100));

final renamedPerson = Person(1, 'Leonhard');
await personDao.updatePerson(renamedPerson);

await Future<void>.delayed(const Duration(milliseconds: 100));

// Also removes the dog which belonged to
// Leonhard through ForeignKey relations
await personDao.deletePerson(renamedPerson);
});

test('query multiline query view to find name', () async {
final person = Person(1, 'Frank');
await personDao.insertPerson(person);
Expand Down
5 changes: 3 additions & 2 deletions floor_generator/lib/generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ class FloorGenerator extends GeneratorForAnnotation<annotations.Database> {
final daoGetters = database.daoGetters;

final databaseClass = DatabaseWriter(database).write();
final daoClasses =
daoGetters.map((daoGetter) => DaoWriter(daoGetter.dao).write());
final daoClasses = daoGetters.map((daoGetter) => DaoWriter(
daoGetter.dao, database.streamEntities, database.hasViewStreams)
.write());

final library = Library((builder) => builder
..body.add(FloorWriter(database.name).write())
Expand Down
16 changes: 7 additions & 9 deletions floor_generator/lib/processor/dao_processor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ class DaoProcessor extends Processor<Dao> {
final deletionMethods = _getDeletionMethods(methods);
final transactionMethods = _getTransactionMethods(methods);

final streamEntities = _getStreamEntities(queryMethods);
final streamQueryables = queryMethods
.where((method) => method.returnsStream)
.map((method) => method.queryable);
final streamEntities = streamQueryables.whereType<Entity>().toSet();
final streamViews = streamQueryables.whereType<View>().toSet();

return Dao(
_classElement,
Expand All @@ -66,14 +70,15 @@ class DaoProcessor extends Processor<Dao> {
deletionMethods,
transactionMethods,
streamEntities,
streamViews,
);
}

List<QueryMethod> _getQueryMethods(final List<MethodElement> methods) {
return methods
.where((method) => method.hasAnnotation(annotations.Query))
.map((method) =>
QueryMethodProcessor(method, _entities, _views).process())
QueryMethodProcessor(method, [..._entities, ..._views]).process())
.toList();
}

Expand Down Expand Up @@ -122,11 +127,4 @@ class DaoProcessor extends Processor<Dao> {
).process())
.toList();
}

List<Entity> _getStreamEntities(final List<QueryMethod> queryMethods) {
return queryMethods
.where((method) => method.returnsStream)
.map((method) => method.queryable as Entity)
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,4 @@ class QueryMethodProcessorError {
element: _methodElement,
);
}

InvalidGenerationSourceError get viewNotStreamable {
return InvalidGenerationSourceError(
'Queries on a view can not be returned as a Stream yet.',
todo: 'Don\'t use Stream as the return type of a Query on a View.',
element: _methodElement,
);
}
}
Loading

0 comments on commit 527b0cb

Please sign in to comment.