From a47443cd1bc60683d2b234b6c2301c6b967cd31b Mon Sep 17 00:00:00 2001 From: Markus Richter <8398165+mqus@users.noreply.github.com> Date: Wed, 28 Apr 2021 01:38:22 +0200 Subject: [PATCH] toSetLiteral wip --- .../misc/extension/iterable_extension.dart | 11 +++ floor_generator/lib/writer/dao_writer.dart | 20 +++-- .../extension/iterable_extension_test.dart | 25 ++++++ .../test/writer/dao_writer_test.dart | 90 +++++++++++++++++-- 4 files changed, 132 insertions(+), 14 deletions(-) diff --git a/floor_generator/lib/misc/extension/iterable_extension.dart b/floor_generator/lib/misc/extension/iterable_extension.dart index 7a00c90d..ce2a2593 100644 --- a/floor_generator/lib/misc/extension/iterable_extension.dart +++ b/floor_generator/lib/misc/extension/iterable_extension.dart @@ -1,4 +1,5 @@ import 'dart:collection'; +import 'string_extension.dart'; extension IterableExtension on Iterable { Iterable sortedByDescending(Comparable Function(T element) selector) { @@ -24,3 +25,13 @@ extension IterableExtension on Iterable { } } } + +extension StringIterableExtension on Iterable { + String toSetLiteral({bool withConst = true}) { + final content = distinctBy((element) => element) + .map((e) => e.toLiteral()) + .join(', '); + final cnst = withConst ? 'const ' : ''; + return '$cnst{$content}'; + } +} diff --git a/floor_generator/lib/writer/dao_writer.dart b/floor_generator/lib/writer/dao_writer.dart index 95f1c7ea..17d91347 100644 --- a/floor_generator/lib/writer/dao_writer.dart +++ b/floor_generator/lib/writer/dao_writer.dart @@ -1,5 +1,6 @@ import 'package:code_builder/code_builder.dart'; import 'package:floor_generator/misc/extension/string_extension.dart'; +import 'package:floor_generator/misc/extension/iterable_extension.dart'; import 'package:floor_generator/value_object/dao.dart'; import 'package:floor_generator/value_object/deletion_method.dart'; import 'package:floor_generator/value_object/entity.dart'; @@ -86,7 +87,8 @@ class DaoWriter extends Writer { // create a special change handler which decides case-by-case: // if the insertion happens with onConflict:replace, consider the insertion like a deletion. // if it will not replace (e.g. abort or ignore), only output the single entity at most. - final changeHandler = _generateChangeHandler(foreignKeyRelationships.getAffectedByDelete(entity), entity); + final changeHandler = _generateChangeHandler( + foreignKeyRelationships.getAffectedByDelete(entity), entity); constructorBuilder ..initializers.add(Code( @@ -223,18 +225,26 @@ class DaoWriter extends Writer { /// /// The affected set can be generated with [getAffectedByUpdateEntities] /// and [getAffectedByDeleteEntities] - String _generateChangeHandler(final Set affected ,[Entity? insertionEntity]) { + String _generateChangeHandler(final Set affected, + [Entity? insertionEntity]) { final toNotify = streamEntities.intersection(affected); if (toNotify.isNotEmpty || dbHasViewStreams) { // if there are streaming views, create a new handler even if the set // is empty. This will only trigger a reload of the views. - final set = 'const {${toNotify.map((e) => e.name.toLiteral()).join(', ')}}'; + final set = toNotify.map((e) => e.name).toSetLiteral(); if (insertionEntity == null) { return ', () => changeListener.add($set)'; } else { - final singleSet = 'const {${streamEntities.contains(insertionEntity)?insertionEntity.name.toLiteral():''}}'; - return ', (isReplace) => changeListener.add(isReplace?$set:$singleSet)'; + final singleSet = (streamEntities.contains(insertionEntity) + ? {insertionEntity.name} + : {}) + .toSetLiteral(); + if (singleSet == set) { + return ', (isReplace) => changeListener.add($set)'; + } else { + return ', (isReplace) => changeListener.add(isReplace?$set:$singleSet)'; + } } } else { // do not generate a Handler if the listener doesn't have to be updated diff --git a/floor_generator/test/misc/extension/iterable_extension_test.dart b/floor_generator/test/misc/extension/iterable_extension_test.dart index e2cd58f4..f27ef21a 100644 --- a/floor_generator/test/misc/extension/iterable_extension_test.dart +++ b/floor_generator/test/misc/extension/iterable_extension_test.dart @@ -28,6 +28,31 @@ void main() { expect(actual, equals([_Box(1), _Box(0), _Box(2)])); }); }); + + group('toSetLiteral', () { + test('empty Set from empty set', () { + expect({}.toSetLiteral(), equals('const {}')); + }); + + test('empty Set from empty list', () { + expect([].toSetLiteral(), equals('const {}')); + }); + + test('empty Set from empty set without const', () { + expect({}.toSetLiteral(withConst: false), equals('{}')); + }); + + test('set with elements', () { + expect({'that', 'escaped \'String\''}.toSetLiteral(), + equals("const {'that', 'escaped \'String\''}")); + }); + + test('list with duplicate elements only shows distinct elements', () { + expect( + ['that', 'escaped \'String\'', 'that', 'that'].toSetLiteral(), + equals("const {'that', 'escaped \'String\''}")); + }); + }); } class _Box { diff --git a/floor_generator/test/writer/dao_writer_test.dart b/floor_generator/test/writer/dao_writer_test.dart index 1c6ef459..8c685c2a 100644 --- a/floor_generator/test/writer/dao_writer_test.dart +++ b/floor_generator/test/writer/dao_writer_test.dart @@ -8,6 +8,7 @@ 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/value_object/entity.dart'; +import 'package:floor_generator/value_object/foreign_key.dart'; import 'package:floor_generator/value_object/primary_key.dart'; import 'package:floor_generator/writer/dao_writer.dart'; import 'package:source_gen/source_gen.dart'; @@ -129,7 +130,7 @@ void main() { 'Person', (Person item) => {'id': item.id, 'name': item.name}, - changeListener), + (isReplace) => changeListener.add(const {'Person'})), _personUpdateAdapter = UpdateAdapter( database, 'Person', @@ -180,6 +181,83 @@ void main() { ''')); }); + test('create DAO stream query with onconflict:replace insert', () async { + final dao = await _createDao(''' + @dao + abstract class PersonDao { + @Query('SELECT * FROM person') + Stream> findAllPersonsAsStream(); + + @Insert(onConflict: OnConflictStrategy.replace) + Future insertPerson(Person person); + + @update + Future updatePerson(Person person); + + @delete + Future deletePerson(Person person); + } + '''); + + final streamEntities = { + ...dao.streamEntities, + // add a simulated entity which should be affected if a Person is replaced + Entity( + FakeClassElement(), + 'Dog', + [], + PrimaryKey([], false), + [ + ForeignKey( + 'Person', + ['id'], + ['owner_id'], + annotations.ForeignKeyAction.noAction, + annotations.ForeignKeyAction.setDefault) + ], + [], + false, + '', + '', + null) + }; + + final actual = DaoWriter(dao, streamEntities, dao.streamViews.isNotEmpty, + emptyForeignKeyMap()) + .write(); + + expect(actual, equalsDart(r''' + class _$PersonDao extends PersonDao { + _$PersonDao(this.database, this.changeListener) + : _queryAdapter = QueryAdapter(database, changeListener), + _personInsertionAdapter = InsertionAdapter( + database, + 'Person', + (Person item) => + {'id': item.id, 'name': item.name}, + (isReplace) => changeListener.add(isReplace? const {'Person','Dog'}: const {'Person'})); + + final sqflite.DatabaseExecutor database; + + final StreamController> changeListener; + + final QueryAdapter _queryAdapter; + + final InsertionAdapter _personInsertionAdapter; + + @override + Stream> findAllPersonsAsStream() { + return _queryAdapter.queryListStream('SELECT * FROM person', mapper: (Map row) => Person(row['id'] as int, row['name'] as String), queryableName: 'Person', isView: false); + } + + @override + Future insertPerson(Person person) async { + await _personInsertionAdapter.insert(person, OnConflictStrategy.abort); + } + } + ''')); + }); + test('create DAO aware of other entity stream query', () async { final dao = await _createDao(''' @dao @@ -207,7 +285,7 @@ void main() { 'Person', (Person item) => {'id': item.id, 'name': item.name}, - changeListener), + (isReplace) => changeListener.add(const {'Person'})), _personUpdateAdapter = UpdateAdapter( database, 'Person', @@ -336,12 +414,6 @@ void main() { abstract class PersonDao { @insert Future insertPerson(Person person); - - @update - Future updatePerson(Person person); - - @delete - Future deletePerson(Person person); } '''); // simulate DB is aware of no streamed entity but at least a single View @@ -355,7 +427,7 @@ void main() { 'Person', (Person item) => {'id': item.id, 'name': item.name}, - changeListener), + (isReplace) => changeListener.add(const {})), _personUpdateAdapter = UpdateAdapter( database, 'Person',