From 457e6974676b454dd234949020ef21753ef41043 Mon Sep 17 00:00:00 2001 From: Markus Richter <8398165+mqus@users.noreply.github.com> Date: Sat, 15 Feb 2020 20:35:43 +0100 Subject: [PATCH] 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. --- floor/pubspec.yaml | 3 +- floor_annotation/lib/floor_annotation.dart | 1 + floor_annotation/lib/src/database.dart | 6 +- floor_annotation/lib/src/database_view.dart | 14 ++ floor_generator/lib/misc/constants.dart | 4 + .../lib/processor/dao_processor.dart | 15 ++- .../lib/processor/database_processor.dart | 33 ++++- .../processor/error/view_processor_error.dart | 19 +++ .../lib/processor/query_method_processor.dart | 23 +++- .../lib/processor/view_processor.dart | 126 ++++++++++++++++++ floor_generator/lib/value_object/dao.dart | 4 +- .../lib/value_object/database.dart | 4 + floor_generator/lib/value_object/entity.dart | 17 +-- .../lib/value_object/query_method.dart | 12 +- .../lib/value_object/queryable.dart | 11 ++ floor_generator/lib/value_object/view.dart | 61 +++++++++ floor_generator/lib/writer/dao_writer.dart | 2 +- .../lib/writer/database_writer.dart | 5 + .../lib/writer/query_method_writer.dart | 4 +- floor_generator/pubspec.yaml | 3 +- .../query_method_processor_test.dart | 93 +++++++++++-- floor_generator/test/test_utils.dart | 31 ++++- .../test/writer/dao_writer_test.dart | 9 +- 23 files changed, 448 insertions(+), 52 deletions(-) create mode 100644 floor_annotation/lib/src/database_view.dart create mode 100644 floor_generator/lib/processor/error/view_processor_error.dart create mode 100644 floor_generator/lib/processor/view_processor.dart create mode 100644 floor_generator/lib/value_object/queryable.dart create mode 100644 floor_generator/lib/value_object/view.dart diff --git a/floor/pubspec.yaml b/floor/pubspec.yaml index 9f04c86d..8e925291 100644 --- a/floor/pubspec.yaml +++ b/floor/pubspec.yaml @@ -12,7 +12,8 @@ environment: dependencies: path: ^1.6.4 sqflite: ^1.2.0 - floor_annotation: ^0.6.0 + floor_annotation: + path: ../floor_annotation meta: ^1.1.8 flutter: sdk: flutter diff --git a/floor_annotation/lib/floor_annotation.dart b/floor_annotation/lib/floor_annotation.dart index 1986d9a3..82bef862 100644 --- a/floor_annotation/lib/floor_annotation.dart +++ b/floor_annotation/lib/floor_annotation.dart @@ -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'; diff --git a/floor_annotation/lib/src/database.dart b/floor_annotation/lib/src/database.dart index 26ccc788..85907e8e 100644 --- a/floor_annotation/lib/src/database.dart +++ b/floor_annotation/lib/src/database.dart @@ -8,6 +8,10 @@ class Database { /// The entities the database manages. final List entities; + /// The views the database manages. + final List 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 []}); } diff --git a/floor_annotation/lib/src/database_view.dart b/floor_annotation/lib/src/database_view.dart new file mode 100644 index 00000000..534cdd0d --- /dev/null +++ b/floor_annotation/lib/src/database_view.dart @@ -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 entity (table). + const DatabaseView( + this.query, { + this.viewName, + }); +} diff --git a/floor_generator/lib/misc/constants.dart b/floor_generator/lib/misc/constants.dart index e84b513b..edde8fae 100644 --- a/floor_generator/lib/misc/constants.dart +++ b/floor_generator/lib/misc/constants.dart @@ -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'; @@ -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 { diff --git a/floor_generator/lib/processor/dao_processor.dart b/floor_generator/lib/processor/dao_processor.dart index 4b371c56..92646969 100644 --- a/floor_generator/lib/processor/dao_processor.dart +++ b/floor_generator/lib/processor/dao_processor.dart @@ -9,10 +9,12 @@ 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'; import 'package:floor_generator/value_object/query_method.dart'; +import 'package:floor_generator/value_object/queryable.dart'; import 'package:floor_generator/value_object/transaction_method.dart'; import 'package:floor_generator/value_object/update_method.dart'; @@ -21,20 +23,24 @@ class DaoProcessor extends Processor { final String _daoGetterName; final String _databaseName; final List _entities; + final List _views; DaoProcessor( final ClassElement classElement, final String daoGetterName, final String databaseName, final List entities, + final List 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() { @@ -64,7 +70,8 @@ class DaoProcessor extends Processor { List _getQueryMethods(final List methods) { return methods .where((method) => method.hasAnnotation(annotations.Query)) - .map((method) => QueryMethodProcessor(method, _entities).process()) + .map((method) => + QueryMethodProcessor(method, _entities, _views).process()) .toList(); } @@ -114,10 +121,10 @@ class DaoProcessor extends Processor { .toList(); } - List _getStreamEntities(final List queryMethods) { + List _getStreamEntities(final List queryMethods) { return queryMethods .where((method) => method.returnsStream) - .map((method) => method.entity) + .map((method) => method.queryable) .toList(); } } diff --git a/floor_generator/lib/processor/database_processor.dart b/floor_generator/lib/processor/database_processor.dart index 6dd36262..ffc5d269 100644 --- a/floor_generator/lib/processor/database_processor.dart +++ b/floor_generator/lib/processor/database_processor.dart @@ -1,6 +1,6 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:floor_annotation/floor_annotation.dart' as annotations - show Database, dao, Entity; + show Database, dao, Entity, DatabaseView; import 'package:floor_generator/misc/annotations.dart'; import 'package:floor_generator/misc/constants.dart'; import 'package:floor_generator/misc/type_utils.dart'; @@ -8,8 +8,10 @@ import 'package:floor_generator/processor/dao_processor.dart'; import 'package:floor_generator/processor/entity_processor.dart'; import 'package:floor_generator/processor/error/database_processor_error.dart'; import 'package:floor_generator/processor/processor.dart'; +import 'package:floor_generator/processor/view_processor.dart'; import 'package:floor_generator/value_object/dao_getter.dart'; import 'package:floor_generator/value_object/database.dart'; +import 'package:floor_generator/value_object/view.dart'; import 'package:floor_generator/value_object/entity.dart'; class DatabaseProcessor extends Processor { @@ -27,10 +29,12 @@ class DatabaseProcessor extends Processor { Database process() { final databaseName = _classElement.displayName; final entities = _getEntities(_classElement); - final daoGetters = _getDaoGetters(databaseName, entities); + final views = _getViews(_classElement); + final daoGetters = _getDaoGetters(databaseName, entities, views); final version = _getDatabaseVersion(); - return Database(_classElement, databaseName, entities, daoGetters, version); + return Database( + _classElement, databaseName, entities, views, daoGetters, version); } @nonNull @@ -50,6 +54,7 @@ class DatabaseProcessor extends Processor { List _getDaoGetters( final String databaseName, final List entities, + final List views, ) { return _classElement.fields.where(_isDao).map((field) { final classElement = field.type.element as ClassElement; @@ -60,6 +65,7 @@ class DatabaseProcessor extends Processor { name, databaseName, entities, + views, ).process(); return DaoGetter(field, name, dao); @@ -97,9 +103,30 @@ class DatabaseProcessor extends Processor { return entities; } + @nonNull + List _getViews(final ClassElement databaseClassElement) { + final views = _classElement + .getAnnotation(annotations.Database) + .getField(AnnotationField.DATABASE_VIEWS) + ?.toListValue() + ?.map((object) => object.toTypeValue().element) + ?.whereType() + ?.where(_isView) + ?.map((classElement) => ViewProcessor(classElement).process()) + ?.toList(); + + return views; + } + @nonNull bool _isEntity(final ClassElement classElement) { return classElement.hasAnnotation(annotations.Entity) && !classElement.isAbstract; } + + @nonNull + bool _isView(final ClassElement classElement) { + return classElement.hasAnnotation(annotations.DatabaseView) && + !classElement.isAbstract; + } } diff --git a/floor_generator/lib/processor/error/view_processor_error.dart b/floor_generator/lib/processor/error/view_processor_error.dart new file mode 100644 index 00000000..40d75410 --- /dev/null +++ b/floor_generator/lib/processor/error/view_processor_error.dart @@ -0,0 +1,19 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:source_gen/source_gen.dart'; + +class ViewProcessorError { + final ClassElement _classElement; + + ViewProcessorError(final ClassElement classElement) + : assert(classElement != null), + _classElement = classElement; + + InvalidGenerationSourceError get MISSING_QUERY { + return InvalidGenerationSourceError( + 'There is no SELECT Query defined on the entity ${_classElement.displayName}.', + todo: + 'Define a SELECT query for this View with @DatabaseView(\'SELECT [...]\') ', + element: _classElement, + ); + } +} diff --git a/floor_generator/lib/processor/query_method_processor.dart b/floor_generator/lib/processor/query_method_processor.dart index 9f69bea4..d05587c3 100644 --- a/floor_generator/lib/processor/query_method_processor.dart +++ b/floor_generator/lib/processor/query_method_processor.dart @@ -7,22 +7,28 @@ import 'package:floor_generator/misc/constants.dart'; 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/value_object/view.dart'; import 'package:floor_generator/value_object/entity.dart'; import 'package:floor_generator/value_object/query_method.dart'; +import 'package:floor_generator/value_object/queryable.dart'; class QueryMethodProcessor extends Processor { final QueryMethodProcessorError _processorError; final MethodElement _methodElement; final List _entities; + final List _views; QueryMethodProcessor( final MethodElement methodElement, final List entities, + final List views, ) : assert(methodElement != null), assert(entities != null), + assert(views != null), _methodElement = methodElement, _entities = entities, + _views = views, _processorError = QueryMethodProcessorError(methodElement); @nonNull @@ -42,11 +48,16 @@ class QueryMethodProcessor extends Processor { returnsStream, ); - final entity = _entities.firstWhere( - (entity) => - entity.classElement.displayName == - flattenedReturnType.getDisplayString(), - orElse: () => null); // doesn't return an entity + final Queryable queryable = _entities.firstWhere( + (entity) => + entity.classElement.displayName == + flattenedReturnType.getDisplayString(), + orElse: () => null) ?? + _views.firstWhere( + (view) => + view.classElement.displayName == + flattenedReturnType.getDisplayString(), + orElse: () => null); return QueryMethod( _methodElement, @@ -55,7 +66,7 @@ class QueryMethodProcessor extends Processor { rawReturnType, flattenedReturnType, parameters, - entity, + queryable, ); } diff --git a/floor_generator/lib/processor/view_processor.dart b/floor_generator/lib/processor/view_processor.dart new file mode 100644 index 00000000..da7b186c --- /dev/null +++ b/floor_generator/lib/processor/view_processor.dart @@ -0,0 +1,126 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:floor_annotation/floor_annotation.dart' as annotations; +import 'package:floor_generator/misc/annotations.dart'; +import 'package:floor_generator/misc/constants.dart'; +import 'package:floor_generator/misc/type_utils.dart'; +import 'package:floor_generator/processor/error/view_processor_error.dart'; +import 'package:floor_generator/processor/field_processor.dart'; +import 'package:floor_generator/processor/processor.dart'; +import 'package:floor_generator/value_object/field.dart'; +import 'package:floor_generator/value_object/view.dart'; + +class ViewProcessor extends Processor { + final ClassElement _classElement; + final ViewProcessorError _processorError; + + ViewProcessor(final ClassElement classElement) + : assert(classElement != null), + _classElement = classElement, + _processorError = ViewProcessorError(classElement); + + @nonNull + @override + View process() { + final name = _getName(); + final fields = _getFields(); + final query = _getQuery(); + return View( + _classElement, + name, + fields, + query, + _getConstructor(fields), + ); + } + + @nonNull + String _getName() { + return _classElement + .getAnnotation(annotations.DatabaseView) + .getField(AnnotationField.VIEW_NAME) + .toStringValue() ?? + _classElement.displayName; + } + + @nonNull + String _getQuery() { + final query = _classElement + .getAnnotation(annotations.DatabaseView) + .getField(AnnotationField.VIEW_QUERY) + .toStringValue(); + + if (query == null || !query.toLowerCase().startsWith('select')) + throw _processorError.MISSING_QUERY; + return query; + } + + @nonNull + List _getFields() { + return _classElement.fields + .where((fieldElement) => fieldElement.shouldBeIncluded()) + .map((field) => FieldProcessor(field).process()) + .toList(); + } + + @nonNull + String _getConstructor(final List fields) { + final constructorParameters = _classElement.constructors.first.parameters; + final parameterValues = constructorParameters + .map((parameterElement) => _getParameterValue(parameterElement, fields)) + .where((parameterValue) => parameterValue != null) + .join(', '); + + return '${_classElement.displayName}($parameterValues)'; + } + + /// Returns `null` whenever field is @ignored + @nullable + String _getParameterValue( + final ParameterElement parameterElement, + final List fields, + ) { + final parameterName = parameterElement.displayName; + final field = fields.firstWhere( + (field) => field.name == parameterName, + orElse: () => null, // whenever field is @ignored + ); + if (field != null) { + final parameterValue = "row['${field.columnName}']"; + final castedParameterValue = + _castParameterValue(parameterElement.type, parameterValue); + if (parameterElement.isNamed) { + return '$parameterName: $castedParameterValue'; + } + return castedParameterValue; // also covers positional parameter + } else { + return null; + } + } + + @nonNull + String _castParameterValue( + final DartType parameterType, + final String parameterValue, + ) { + if (parameterType.isDartCoreBool) { + return '($parameterValue as int) != 0'; // maps int to bool + } else if (parameterType.isDartCoreString) { + return '$parameterValue as String'; + } else if (parameterType.isDartCoreInt) { + return '$parameterValue as int'; + } else if (parameterType.getDisplayString() == 'Uint8List') { + return '$parameterValue'; + } else { + return '$parameterValue as double'; // must be double + } + } +} + +extension on FieldElement { + bool shouldBeIncluded() { + final isIgnored = hasAnnotation(annotations.ignore.runtimeType); + final isHashCode = displayName == 'hashCode'; + return !(isStatic || isHashCode || isIgnored); + } +} diff --git a/floor_generator/lib/value_object/dao.dart b/floor_generator/lib/value_object/dao.dart index 0f715726..4c24cba6 100644 --- a/floor_generator/lib/value_object/dao.dart +++ b/floor_generator/lib/value_object/dao.dart @@ -1,8 +1,8 @@ import 'package:analyzer/dart/element/element.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'; import 'package:floor_generator/value_object/query_method.dart'; +import 'package:floor_generator/value_object/queryable.dart'; import 'package:floor_generator/value_object/transaction_method.dart'; import 'package:floor_generator/value_object/update_method.dart'; @@ -14,7 +14,7 @@ class Dao { final List updateMethods; final List deletionMethods; final List transactionMethods; - final List streamEntities; + final List streamEntities; Dao( this.classElement, diff --git a/floor_generator/lib/value_object/database.dart b/floor_generator/lib/value_object/database.dart index 06c6a7a1..93ddc08c 100644 --- a/floor_generator/lib/value_object/database.dart +++ b/floor_generator/lib/value_object/database.dart @@ -1,5 +1,6 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:floor_generator/value_object/dao_getter.dart'; +import 'package:floor_generator/value_object/view.dart'; import 'package:floor_generator/value_object/entity.dart'; /// Representation of the database component. @@ -7,6 +8,7 @@ class Database { final ClassElement classElement; final String name; final List entities; + final List views; final List daoGetters; final int version; @@ -14,6 +16,7 @@ class Database { this.classElement, this.name, this.entities, + this.views, this.daoGetters, this.version, ); @@ -26,6 +29,7 @@ class Database { classElement == other.classElement && name == other.name && entities == other.entities && + views == other.views && daoGetters == other.daoGetters && version == other.version; diff --git a/floor_generator/lib/value_object/entity.dart b/floor_generator/lib/value_object/entity.dart index cc91af63..38b9d16b 100644 --- a/floor_generator/lib/value_object/entity.dart +++ b/floor_generator/lib/value_object/entity.dart @@ -5,25 +5,22 @@ import 'package:floor_generator/value_object/field.dart'; import 'package:floor_generator/value_object/foreign_key.dart'; import 'package:floor_generator/value_object/index.dart'; import 'package:floor_generator/value_object/primary_key.dart'; +import 'package:floor_generator/value_object/queryable.dart'; -class Entity { - final ClassElement classElement; - final String name; - final List fields; +class Entity extends Queryable { final PrimaryKey primaryKey; final List foreignKeys; final List indices; - final String constructor; Entity( - this.classElement, - this.name, - this.fields, + ClassElement classElement, + String name, + List fields, this.primaryKey, this.foreignKeys, this.indices, - this.constructor, - ); + String constructor, + ) : super(classElement, name, fields, constructor); @nonNull String getCreateTableStatement() { diff --git a/floor_generator/lib/value_object/query_method.dart b/floor_generator/lib/value_object/query_method.dart index 794dc1ce..b2950fb1 100644 --- a/floor_generator/lib/value_object/query_method.dart +++ b/floor_generator/lib/value_object/query_method.dart @@ -2,7 +2,7 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:collection/collection.dart'; import 'package:floor_generator/misc/type_utils.dart'; -import 'package:floor_generator/value_object/entity.dart'; +import 'package:floor_generator/value_object/queryable.dart'; /// Wraps a method annotated with Query /// to enable easy access to code generation relevant data. @@ -28,7 +28,7 @@ class QueryMethod { final List parameters; - final Entity entity; + final Queryable queryable; QueryMethod( this.methodElement, @@ -37,7 +37,7 @@ class QueryMethod { this.rawReturnType, this.flattenedReturnType, this.parameters, - this.entity, + this.queryable, ); bool get returnsList { @@ -64,7 +64,7 @@ class QueryMethod { flattenedReturnType == other.flattenedReturnType && const ListEquality() .equals(parameters, other.parameters) && - entity == other.entity; + queryable == other.queryable; @override int get hashCode => @@ -74,10 +74,10 @@ class QueryMethod { rawReturnType.hashCode ^ flattenedReturnType.hashCode ^ parameters.hashCode ^ - entity.hashCode; + queryable.hashCode; @override String toString() { - return 'QueryMethod{methodElement: $methodElement, name: $name, query: $query, rawReturnType: $rawReturnType, flattenedReturnType: $flattenedReturnType, parameters: $parameters, entity: $entity}'; + return 'QueryMethod{methodElement: $methodElement, name: $name, query: $query, rawReturnType: $rawReturnType, flattenedReturnType: $flattenedReturnType, parameters: $parameters, entity: $queryable}'; } } diff --git a/floor_generator/lib/value_object/queryable.dart b/floor_generator/lib/value_object/queryable.dart new file mode 100644 index 00000000..d3014394 --- /dev/null +++ b/floor_generator/lib/value_object/queryable.dart @@ -0,0 +1,11 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:floor_generator/value_object/field.dart'; + +abstract class Queryable { + final ClassElement classElement; + final String name; + final List fields; + final String constructor; + + Queryable(this.classElement, this.name, this.fields, this.constructor); +} diff --git a/floor_generator/lib/value_object/view.dart b/floor_generator/lib/value_object/view.dart new file mode 100644 index 00000000..4b3814ab --- /dev/null +++ b/floor_generator/lib/value_object/view.dart @@ -0,0 +1,61 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:collection/collection.dart'; +import 'package:floor_generator/misc/annotations.dart'; +import 'package:floor_generator/value_object/field.dart'; +import 'package:floor_generator/value_object/queryable.dart'; + +class View extends Queryable { + final String query; + + View(ClassElement classElement, String name, List fields, this.query, + String constructor) + : super(classElement, name, fields, constructor); + + @nonNull + String getCreateViewStatement() { + return 'CREATE VIEW IF NOT EXISTS `$name` AS $query'; + } + + @nonNull + String getValueMapping() { + final keyValueList = fields.map((field) { + final columnName = field.columnName; + final attributeValue = _getAttributeValue(field.fieldElement); + return "'$columnName': $attributeValue"; + }).toList(); + + return '{${keyValueList.join(', ')}}'; + } + + @nonNull + String _getAttributeValue(final FieldElement fieldElement) { + final parameterName = fieldElement.displayName; + return fieldElement.type.isDartCoreBool + ? 'item.$parameterName ? 1 : 0' + : 'item.$parameterName'; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is View && + runtimeType == other.runtimeType && + classElement == other.classElement && + name == other.name && + const ListEquality().equals(fields, other.fields) && + query == other.query && + constructor == other.constructor; + + @override + int get hashCode => + classElement.hashCode ^ + name.hashCode ^ + fields.hashCode ^ + query.hashCode ^ + constructor.hashCode; + + @override + String toString() { + return 'View{classElement: $classElement, name: $name, fields: $fields, query: $query, constructor: $constructor}'; + } +} diff --git a/floor_generator/lib/writer/dao_writer.dart b/floor_generator/lib/writer/dao_writer.dart index 532137ac..14cad7df 100644 --- a/floor_generator/lib/writer/dao_writer.dart +++ b/floor_generator/lib/writer/dao_writer.dart @@ -59,7 +59,7 @@ class DaoWriter extends Writer { "_queryAdapter = QueryAdapter(database${requiresChangeListener ? ', changeListener' : ''})")); final queryMapperFields = queryMethods - .map((method) => method.entity) + .map((method) => method.queryable) .where((entity) => entity != null) .toSet() .map((entity) { diff --git a/floor_generator/lib/writer/database_writer.dart b/floor_generator/lib/writer/database_writer.dart index 83a818e8..daffee17 100644 --- a/floor_generator/lib/writer/database_writer.dart +++ b/floor_generator/lib/writer/database_writer.dart @@ -85,6 +85,10 @@ class DatabaseWriter implements Writer { .expand((statements) => statements) .map((statement) => "await database.execute('$statement');") .join('\n'); + final createViewStatements = database.views + .map((view) => view.getCreateViewStatement()) + .map((statement) => "await database.execute('$statement');") + .join('\n'); final pathParameter = Parameter((builder) => builder ..name = 'path' @@ -122,6 +126,7 @@ class DatabaseWriter implements Writer { onCreate: (database, version) async { $createTableStatements $createIndexStatements + $createViewStatements await callback?.onCreate?.call(database, version); }, diff --git a/floor_generator/lib/writer/query_method_writer.dart b/floor_generator/lib/writer/query_method_writer.dart index c5a8b4a9..c60b0600 100644 --- a/floor_generator/lib/writer/query_method_writer.dart +++ b/floor_generator/lib/writer/query_method_writer.dart @@ -63,7 +63,7 @@ class QueryMethodWriter implements Writer { return _methodBody.toString(); } - final mapper = '_${_queryMethod.entity.name.decapitalize()}Mapper'; + final mapper = '_${_queryMethod.queryable.name.decapitalize()}Mapper'; if (_queryMethod.returnsStream) { _methodBody.write(_generateStreamQuery(arguments, mapper)); } else { @@ -137,7 +137,7 @@ class QueryMethodWriter implements Writer { @nullable final String arguments, @nonNull final String mapper, ) { - final entityName = _queryMethod.entity.name; + final entityName = _queryMethod.queryable.name; final parameters = StringBuffer()..write("'${_queryMethod.query}', "); if (arguments != null) parameters.write('arguments: $arguments, '); diff --git a/floor_generator/pubspec.yaml b/floor_generator/pubspec.yaml index 1dec15da..0682e8ac 100644 --- a/floor_generator/pubspec.yaml +++ b/floor_generator/pubspec.yaml @@ -17,7 +17,8 @@ dependencies: source_gen: ^0.9.4+7 build_config: ^0.4.1+1 collection: ^1.14.11 - floor_annotation: ^0.6.0 + floor_annotation: + path: ../floor_annotation dev_dependencies: test: ^1.11.0 diff --git a/floor_generator/test/processor/query_method_processor_test.dart b/floor_generator/test/processor/query_method_processor_test.dart index 61867dc9..390f8cad 100644 --- a/floor_generator/test/processor/query_method_processor_test.dart +++ b/floor_generator/test/processor/query_method_processor_test.dart @@ -5,6 +5,8 @@ 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/query_method_processor.dart'; +import 'package:floor_generator/processor/view_processor.dart'; +import 'package:floor_generator/value_object/view.dart'; import 'package:floor_generator/value_object/entity.dart'; import 'package:floor_generator/value_object/query_method.dart'; import 'package:source_gen/source_gen.dart'; @@ -14,8 +16,10 @@ import '../test_utils.dart'; void main() { List entities; + List views; setUpAll(() async => entities = await _getEntities()); + setUpAll(() async => views = await _getViews()); test('create query method', () async { final methodElement = await _createQueryMethodElement(''' @@ -23,7 +27,8 @@ void main() { Future> findAllPersons(); '''); - final actual = QueryMethodProcessor(methodElement, entities).process(); + final actual = + QueryMethodProcessor(methodElement, entities, views).process(); expect( actual, @@ -41,6 +46,31 @@ void main() { ); }); + test('create query method for a view', () async { + final methodElement = await _createQueryMethodElement(''' + @Query('SELECT * FROM name') + Future> findAllNames(); + '''); + + final actual = + QueryMethodProcessor(methodElement, entities, views).process(); + + expect( + actual, + equals( + QueryMethod( + methodElement, + 'findAllNames', + 'SELECT * FROM name', + await getDartTypeWithName('Future>'), + await getDartTypeWithName('Name'), + [], + views.first, + ), + ), + ); + }); + group('query parsing', () { test('parse query', () async { final methodElement = await _createQueryMethodElement(''' @@ -48,7 +78,8 @@ void main() { Future findPerson(int id); '''); - final actual = QueryMethodProcessor(methodElement, []).process().query; + final actual = + QueryMethodProcessor(methodElement, [], []).process().query; expect(actual, equals('SELECT * FROM Person WHERE id = ?')); }); @@ -62,7 +93,8 @@ void main() { Future findPersonByIdAndName(int id, String name); """); - final actual = QueryMethodProcessor(methodElement, []).process().query; + final actual = + QueryMethodProcessor(methodElement, [], []).process().query; expect( actual, @@ -77,7 +109,8 @@ void main() { Future findPersonByIdAndName(int id, String name); '''); - final actual = QueryMethodProcessor(methodElement, []).process().query; + final actual = + QueryMethodProcessor(methodElement, [], []).process().query; expect( actual, @@ -91,7 +124,8 @@ void main() { Future setRated(List ids); '''); - final actual = QueryMethodProcessor(methodElement, []).process().query; + final actual = + QueryMethodProcessor(methodElement, [], []).process().query; expect( actual, @@ -105,7 +139,8 @@ void main() { Future setRated(List ids, List bar); '''); - final actual = QueryMethodProcessor(methodElement, []).process().query; + final actual = + QueryMethodProcessor(methodElement, [], []).process().query; expect( actual, @@ -122,7 +157,8 @@ void main() { Future setRated(List ids, int bar); '''); - final actual = QueryMethodProcessor(methodElement, []).process().query; + final actual = + QueryMethodProcessor(methodElement, [], []).process().query; expect( actual, @@ -139,7 +175,8 @@ void main() { Future> findPersonsWithNamesLike(String name); '''); - final actual = QueryMethodProcessor(methodElement, []).process().query; + final actual = + QueryMethodProcessor(methodElement, [], []).process().query; expect(actual, equals('SELECT * FROM Persons WHERE name LIKE ?')); }); @@ -153,7 +190,7 @@ void main() { '''); final actual = - () => QueryMethodProcessor(methodElement, entities).process(); + () => QueryMethodProcessor(methodElement, entities, views).process(); final error = QueryMethodProcessorError(methodElement) .DOES_NOT_RETURN_FUTURE_NOR_STREAM; @@ -167,7 +204,7 @@ void main() { '''); final actual = - () => QueryMethodProcessor(methodElement, entities).process(); + () => QueryMethodProcessor(methodElement, entities, views).process(); final error = QueryMethodProcessorError(methodElement).NO_QUERY_DEFINED; expect(actual, throwsInvalidGenerationSourceError(error)); @@ -180,7 +217,7 @@ void main() { '''); final actual = - () => QueryMethodProcessor(methodElement, entities).process(); + () => QueryMethodProcessor(methodElement, entities, views).process(); final error = QueryMethodProcessorError(methodElement).NO_QUERY_DEFINED; expect(actual, throwsInvalidGenerationSourceError(error)); @@ -194,7 +231,7 @@ void main() { '''); final actual = - () => QueryMethodProcessor(methodElement, entities).process(); + () => QueryMethodProcessor(methodElement, entities, views).process(); final error = QueryMethodProcessorError(methodElement) .QUERY_ARGUMENTS_AND_METHOD_PARAMETERS_DO_NOT_MATCH; @@ -209,7 +246,7 @@ void main() { '''); final actual = - () => QueryMethodProcessor(methodElement, entities).process(); + () => QueryMethodProcessor(methodElement, entities, views).process(); final error = QueryMethodProcessorError(methodElement) .QUERY_ARGUMENTS_AND_METHOD_PARAMETERS_DO_NOT_MATCH; @@ -240,6 +277,13 @@ Future _createQueryMethodElement( Person(this.id, this.name); } + + @DatabaseView("SELECT DISTINCT(name) AS name from person") + class Name { + final String name; + + Name(this.name); + } ''', (resolver) async { return LibraryReader(await resolver.findLibraryByName('test')); }); @@ -271,3 +315,26 @@ Future> _getEntities() async { .map((classElement) => EntityProcessor(classElement).process()) .toList(); } + +Future> _getViews() async { + final library = await resolveSource(''' + library test; + + import 'package:floor_annotation/floor_annotation.dart'; + + @DatabaseView("SELECT DISTINCT(name) AS name from person") + class Name { + final String name; + + Name(this.name); + } + ''', (resolver) async { + return LibraryReader(await resolver.findLibraryByName('test')); + }); + + return library.classes + .where((classElement) => + classElement.hasAnnotation(annotations.DatabaseView)) + .map((classElement) => ViewProcessor(classElement).process()) + .toList(); +} diff --git a/floor_generator/test/test_utils.dart b/floor_generator/test/test_utils.dart index 4050b8b6..3ed1dc29 100644 --- a/floor_generator/test/test_utils.dart +++ b/floor_generator/test/test_utils.dart @@ -10,6 +10,7 @@ import 'package:floor_annotation/floor_annotation.dart' as annotations; import 'package:floor_generator/misc/type_utils.dart'; import 'package:floor_generator/processor/dao_processor.dart'; import 'package:floor_generator/processor/entity_processor.dart'; +import 'package:floor_generator/processor/view_processor.dart'; import 'package:floor_generator/value_object/dao.dart'; import 'package:path/path.dart' as path; import 'package:source_gen/source_gen.dart'; @@ -72,6 +73,29 @@ Future getDartTypeWithPerson(String value) async { }); } +Future getDartTypeWithName(String value) async { + final source = ''' + library test; + + import 'package:floor_annotation/floor_annotation.dart'; + + $value value; + + @DatabaseView("SELECT DISTINCT(name) AS name from person") + class Name { + final String name; + + Name(this.name); + } + '''; + return resolveSource(source, (item) async { + final libraryReader = LibraryReader(await item.findLibraryByName('test')); + return (libraryReader.allElements.first as PropertyAccessorElement) + .type + .returnType; + }); +} + final _dartfmt = DartFormatter(); String _format(final String source) { @@ -127,7 +151,12 @@ Future createDao(final String methodSignature) async { .where((classElement) => classElement.hasAnnotation(annotations.Entity)) .map((classElement) => EntityProcessor(classElement).process()) .toList(); + final views = library.classes + .where((classElement) => + classElement.hasAnnotation(annotations.DatabaseView)) + .map((classElement) => ViewProcessor(classElement).process()) + .toList(); - return DaoProcessor(daoClass, 'personDao', 'TestDatabase', entities) + return DaoProcessor(daoClass, 'personDao', 'TestDatabase', entities, views) .process(); } diff --git a/floor_generator/test/writer/dao_writer_test.dart b/floor_generator/test/writer/dao_writer_test.dart index 614bda2d..eec642e6 100644 --- a/floor_generator/test/writer/dao_writer_test.dart +++ b/floor_generator/test/writer/dao_writer_test.dart @@ -4,6 +4,7 @@ import 'package:floor_annotation/floor_annotation.dart' as annotations; import 'package:floor_generator/misc/type_utils.dart'; import 'package:floor_generator/processor/dao_processor.dart'; import 'package:floor_generator/processor/entity_processor.dart'; +import 'package:floor_generator/processor/view_processor.dart'; import 'package:floor_generator/value_object/dao.dart'; import 'package:floor_generator/writer/dao_writer.dart'; import 'package:source_gen/source_gen.dart'; @@ -207,6 +208,12 @@ Future _createDao(final String dao) async { .map((classElement) => EntityProcessor(classElement).process()) .toList(); - return DaoProcessor(daoClass, 'personDao', 'TestDatabase', entities) + final views = library.classes + .where((classElement) => + classElement.hasAnnotation(annotations.DatabaseView)) + .map((classElement) => ViewProcessor(classElement).process()) + .toList(); + + return DaoProcessor(daoClass, 'personDao', 'TestDatabase', entities, views) .process(); }