Skip to content

Commit

Permalink
Support database views (#262)
Browse files Browse the repository at this point in the history
* Feature: Add support for @DatabaseView annotations

This adds initial support for @DatabaseView annotations known from room. One test was added, further testing is needed. Implements #130.

fix linting error

* Clean up Uint8List/Blob<->View interop

* Add unit tests

* Add integration tests for view

* Fix issues from review

- Do not allow queries from views to be streamed
	- throw error if a user tries to do that and test that error
	- remove Stream<> queries from DAO in integration test
	- getStreamEntities will only return entities again

- Create new Superclass for EntityProcessor and ViewProcessor named QueryableProcessor to deduplicate common functions

- Code cleanup
	- clean up integration tests
	- improve code layout and comment wording in annotations, DatabaseProcessor, QueryMethodProcessor
	- fix toString and hashCode methods in Database (value object)
	- improve error message wording in DatabaseViewError

* Adapt to upstream changes,split integrations tests

* Fix from review part 2 (tests & documentation)

- Clean up small details in code, according to the review

- Improve tests
 - Simplify integration tests
 - split off and merge common QueryableProcessor tests into a separate file
 - move createClassElement function, which is now used in three test files to testutils

- Add documentation to README.md
  • Loading branch information
mqus authored Mar 15, 2020
1 parent d0727f8 commit 56a2763
Show file tree
Hide file tree
Showing 33 changed files with 1,221 additions and 411 deletions.
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ This package is still in an early phase and the API will likely change.
1. [Primary Keys](#primary-keys)
1. [Indices](#indices)
1. [Ignoring Fields](#ignoring-fields)
1. [Database Views](#database-views)
1. [Migrations](#migrations)
1. [In-Memory Database](#in-memory-database)
1. [Callback](#callback)
Expand Down Expand Up @@ -275,6 +276,9 @@ StreamBuilder<List<Person>>(
);
```

NOTE: It is currently not possible to return a `Stream` if the function queries a database view. This is mostly due
to the complexity of detecting which entities are involved in a database view.

## Transactions
Whenever you want to perform some operations in a transaction you have to add the `@transaction` annotation to the method.
It's also required to add the `async` modifier. These methods can only return `Future<void>`.
Expand Down Expand Up @@ -414,9 +418,47 @@ class Person {
Person(this.id, this.name);
}
```
## Database Views
If you want to define static `SELECT`-statements which return different types than your entities, your best option is
to use `@DatabaseView`. A database view can be understood as a virtual table, which can be queried like a real table.

A database view in floor is defined and used similarly to entities, with the main difference being that
access is read-only, which means that update, insert and delete functions are not possible. Similarly to
entities, the class name is used if no `viewName` was set.

```dart
@DatabaseView('SELECT distinct(name) as name FROM person', viewName: 'name')
class Name {
final String name;
Name(this.name);
}
```

Database views do not have any foreign/primary keys or indices. Instead, you should manually define indices which fit to
your statement and put them into the `@Entity` annotation of the involved entities.

Setters, getters and static fields are automatically ignored (like in entities), you can specify additional fields
to ignore by annotating them with `@ignore`.

After defining a database view in your code, you have to add it to your database by adding it to the `views` field of
the `@Database` annotation:

```dart
@Database(version: 1, entities: [Person], views:[Name])
abstract class AppDatabase extends FloorDatabase {
PersonDao get personDao;
}
```

You can then query the view via a DAO function like an entity.

NOTE: Be aware that it is currently not possible to return a
`Stream<>` object from a function which queries a database view.

## Migrations
Whenever are doing changes to your entities, you're required to also migrate the old data.
Whenever you are doing changes to your entities, you're required to also migrate the old data.

First, update your entity.
Next, Increase the database version.
Expand Down
44 changes: 43 additions & 1 deletion floor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ This package is still in an early phase and the API will likely change.
1. [Primary Keys](#primary-keys)
1. [Indices](#indices)
1. [Ignoring Fields](#ignoring-fields)
1. [Database Views](#database-views)
1. [Migrations](#migrations)
1. [In-Memory Database](#in-memory-database)
1. [Callback](#callback)
Expand Down Expand Up @@ -275,6 +276,9 @@ StreamBuilder<List<Person>>(
);
```

NOTE: It is currently not possible to return a `Stream` if the function queries a database view. This is mostly due
to the complexity of detecting which entities are involved in a database view.

## Transactions
Whenever you want to perform some operations in a transaction you have to add the `@transaction` annotation to the method.
It's also required to add the `async` modifier. These methods can only return `Future<void>`.
Expand Down Expand Up @@ -414,9 +418,47 @@ class Person {
Person(this.id, this.name);
}
```
## Database Views
If you want to define static `SELECT`-statements which return different types than your entities, your best option is
to use `@DatabaseView`. A database view can be understood as a virtual table, which can be queried like a real table.

A database view in floor is defined and used similarly to entities, with the main difference being that
access is read-only, which means that update, insert and delete functions are not possible. Similarly to
entities, the class name is used if no `viewName` was set.

```dart
@DatabaseView('SELECT distinct(name) as name FROM person', viewName: 'name')
class Name {
final String name;
Name(this.name);
}
```

Database views do not have any foreign/primary keys or indices. Instead, you should manually define indices which fit to
your statement and put them into the `@Entity` annotation of the involved entities.

Setters, getters and static fields are automatically ignored (like in entities), you can specify additional fields
to ignore by annotating them with `@ignore`.

After defining a database view in your code, you have to add it to your database by adding it to the `views` field of
the `@Database` annotation:

```dart
@Database(version: 1, entities: [Person], views:[Name])
abstract class AppDatabase extends FloorDatabase {
PersonDao get personDao;
}
```

You can then query the view via a DAO function like an entity.

NOTE: Be aware that it is currently not possible to return a
`Stream<>` object from a function which queries a database view.

## Migrations
Whenever are doing changes to your entities, you're required to also migrate the old data.
Whenever you are doing changes to your entities, you're required to also migrate the old data.

First, update your entity.
Next, Increase the database version.
Expand Down
15 changes: 15 additions & 0 deletions floor/test/integration/dao/name_dao.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'package:floor/floor.dart';

import '../model/name.dart';

@dao
abstract class NameDao {
@Query('SELECT * FROM names ORDER BY name ASC')
Future<List<Name>> findAllNames();

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

@Query('SELECT * FROM names WHERE name LIKE :suffix ORDER BY name ASC')
Future<List<Name>> findNamesLike(String suffix);
}
23 changes: 23 additions & 0 deletions floor/test/integration/model/name.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:floor/floor.dart';

@DatabaseView(
'SELECT custom_name as name FROM person UNION SELECT name from dog',
viewName: 'names')
class Name {
final String name;

Name(this.name);

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Name && runtimeType == other.runtimeType && name == other.name;

@override
int get hashCode => name.hashCode;

@override
String toString() {
return 'Name{name: $name}';
}
}
87 changes: 87 additions & 0 deletions floor/test/integration/view_test/view_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import 'dart:async';

import 'package:floor/floor.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' hide equals;
import 'package:sqflite/sqflite.dart' as sqflite;
import 'package:sqflite_ffi_test/sqflite_ffi_test.dart';

import '../dao/dog_dao.dart';
import '../dao/name_dao.dart';
import '../dao/person_dao.dart';
import '../model/dog.dart';
import '../model/name.dart';
import '../model/person.dart';

part 'view_test.g.dart';

@Database(version: 1, entities: [Person, Dog], views: [Name])
abstract class ViewTestDatabase extends FloorDatabase {
PersonDao get personDao;

DogDao get dogDao;

NameDao get nameDao;
}

void main() {
TestWidgetsFlutterBinding.ensureInitialized();
sqfliteFfiTestInit();

group('database tests', () {
ViewTestDatabase database;
PersonDao personDao;
DogDao dogDao;
NameDao nameDao;

setUp(() async {
database = await $FloorViewTestDatabase.inMemoryDatabaseBuilder().build();

personDao = database.personDao;
dogDao = database.dogDao;
nameDao = database.nameDao;
});

tearDown(() async {
await database.close();
});

group('Query Views', () {
test('query view with exact value', () async {
final person = Person(1, 'Frank');
await personDao.insertPerson(person);

final actual = await nameDao.findExactName('Frank');

final expected = Name('Frank');
expect(actual, equals(expected));
});

test('query view with LIKE', () 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.findNamesLike('%eo');

final expected = [Name('Leo'), Name('Romeo')];
expect(actual, equals(expected));
});

test('query view with all values', () 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.findAllNames();

final expected = [Name('Frank'), Name('Leo'), Name('Romeo')];
expect(actual, equals(expected));
});
});
});
}
1 change: 1 addition & 0 deletions floor_annotation/lib/floor_annotation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ library floor_annotation;
export 'src/column_info.dart';
export 'src/dao.dart';
export 'src/database.dart';
export 'src/database_view.dart';
export 'src/delete.dart';
export 'src/entity.dart';
export 'src/foreign_key.dart';
Expand Down
9 changes: 8 additions & 1 deletion floor_annotation/lib/src/database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ class Database {
/// The entities the database manages.
final List<Type> entities;

/// The views the database manages.
final List<Type> views;

/// Marks a class as a FloorDatabase.
const Database({@required this.version, @required this.entities});
const Database({
@required this.version,
@required this.entities,
this.views = const [],
});
}
14 changes: 14 additions & 0 deletions floor_annotation/lib/src/database_view.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// Marks a class as a database view (a fixed select statement).
class DatabaseView {
/// The table name of the SQLite view.
final String viewName;

/// The SELECT query on which the view is based on.
final String query;

/// Marks a class as a database view (a fixed select statement).
const DatabaseView(
this.query, {
this.viewName,
});
}
4 changes: 4 additions & 0 deletions floor_generator/lib/misc/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ abstract class AnnotationField {

static const DATABASE_VERSION = 'version';
static const DATABASE_ENTITIES = 'entities';
static const DATABASE_VIEWS = 'views';

static const COLUMN_INFO_NAME = 'name';
static const COLUMN_INFO_NULLABLE = 'nullable';
Expand All @@ -13,6 +14,9 @@ abstract class AnnotationField {
static const ENTITY_FOREIGN_KEYS = 'foreignKeys';
static const ENTITY_INDICES = 'indices';
static const ENTITY_PRIMARY_KEYS = 'primaryKeys';

static const VIEW_NAME = 'viewName';
static const VIEW_QUERY = 'query';
}

abstract class ForeignKeyField {
Expand Down
12 changes: 9 additions & 3 deletions floor_generator/lib/processor/dao_processor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:floor_generator/processor/query_method_processor.dart';
import 'package:floor_generator/processor/transaction_method_processor.dart';
import 'package:floor_generator/processor/update_method_processor.dart';
import 'package:floor_generator/value_object/dao.dart';
import 'package:floor_generator/value_object/view.dart';
import 'package:floor_generator/value_object/deletion_method.dart';
import 'package:floor_generator/value_object/entity.dart';
import 'package:floor_generator/value_object/insertion_method.dart';
Expand All @@ -21,20 +22,24 @@ class DaoProcessor extends Processor<Dao> {
final String _daoGetterName;
final String _databaseName;
final List<Entity> _entities;
final List<View> _views;

DaoProcessor(
final ClassElement classElement,
final String daoGetterName,
final String databaseName,
final List<Entity> entities,
final List<View> views,
) : assert(classElement != null),
assert(daoGetterName != null),
assert(databaseName != null),
assert(entities != null),
assert(views != null),
_classElement = classElement,
_daoGetterName = daoGetterName,
_databaseName = databaseName,
_entities = entities;
_entities = entities,
_views = views;

@override
Dao process() {
Expand Down Expand Up @@ -66,7 +71,8 @@ class DaoProcessor extends Processor<Dao> {
List<QueryMethod> _getQueryMethods(final List<MethodElement> methods) {
return methods
.where((method) => method.hasAnnotation(annotations.Query))
.map((method) => QueryMethodProcessor(method, _entities).process())
.map((method) =>
QueryMethodProcessor(method, _entities, _views).process())
.toList();
}

Expand Down Expand Up @@ -119,7 +125,7 @@ class DaoProcessor extends Processor<Dao> {
List<Entity> _getStreamEntities(final List<QueryMethod> queryMethods) {
return queryMethods
.where((method) => method.returnsStream)
.map((method) => method.entity)
.map((method) => method.queryable as Entity)
.toList();
}
}
Loading

0 comments on commit 56a2763

Please sign in to comment.