From 6586fabc4c37153e64e4e5b4f70bac4e42778a98 Mon Sep 17 00:00:00 2001 From: Vitus Ortner Date: Sun, 3 Mar 2019 22:32:52 +0100 Subject: [PATCH] Add database adapters --- analysis_options.yaml | 2 +- floor/lib/floor.dart | 5 + floor/lib/src/adapter/deletion_adapter.dart | 72 +++++++++ floor/lib/src/adapter/insertion_adapter.dart | 77 +++++++++ floor/lib/src/adapter/migration_adapter.dart | 30 ++++ floor/lib/src/adapter/query_adapter.dart | 37 +++++ floor/lib/src/adapter/update_adapter.dart | 94 +++++++++++ floor/lib/src/database.dart | 28 ---- floor/test/adapter/deletion_adapter.dart | 126 +++++++++++++++ .../test/adapter/insertion_adapter_test.dart | 136 ++++++++++++++++ .../migration_adapter_test.dart} | 37 ++--- floor/test/adapter/query_adapter_test.dart | 93 +++++++++++ floor/test/adapter/update_adapter_test.dart | 150 ++++++++++++++++++ floor/test/util/mocks.dart | 8 + floor/test/util/person.dart | 17 ++ floor_generator/lib/model/change_method.dart | 2 +- floor_generator/lib/model/entity.dart | 115 +++++++++++++- .../adapter/deletion_adapters_writer.dart | 44 +++++ .../adapter/insertion_adapters_writer.dart | 44 +++++ .../writer/adapter/query_adapter_writer.dart | 45 ++++++ .../adapter/update_adapters_writer.dart | 44 +++++ .../lib/writer/database_writer.dart | 44 ++++- .../lib/writer/delete_method_body_writer.dart | 36 +---- .../lib/writer/insert_method_body_writer.dart | 67 +------- .../lib/writer/query_method_writer.dart | 84 +++------- .../lib/writer/update_method_body_writer.dart | 75 +-------- .../test/database_writer_test.dart | 4 +- .../test/insert_method_writer_test.dart | 58 ++----- 28 files changed, 1226 insertions(+), 348 deletions(-) create mode 100644 floor/lib/src/adapter/deletion_adapter.dart create mode 100644 floor/lib/src/adapter/insertion_adapter.dart create mode 100644 floor/lib/src/adapter/migration_adapter.dart create mode 100644 floor/lib/src/adapter/query_adapter.dart create mode 100644 floor/lib/src/adapter/update_adapter.dart create mode 100644 floor/test/adapter/deletion_adapter.dart create mode 100644 floor/test/adapter/insertion_adapter_test.dart rename floor/test/{migration_test.dart => adapter/migration_adapter_test.dart} (73%) create mode 100644 floor/test/adapter/query_adapter_test.dart create mode 100644 floor/test/adapter/update_adapter_test.dart create mode 100644 floor/test/util/mocks.dart create mode 100644 floor/test/util/person.dart create mode 100644 floor_generator/lib/writer/adapter/deletion_adapters_writer.dart create mode 100644 floor_generator/lib/writer/adapter/insertion_adapters_writer.dart create mode 100644 floor_generator/lib/writer/adapter/query_adapter_writer.dart create mode 100644 floor_generator/lib/writer/adapter/update_adapters_writer.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index 5c862220..f128d48e 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -15,7 +15,7 @@ linter: # the Dart Lint rules page to make maintenance easier # https://github.com/dart-lang/linter/blob/master/example/all.yaml - always_declare_return_types - - always_put_control_body_on_new_line +# - always_put_control_body_on_new_line # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 # - always_require_non_null_named_parameters # - always_specify_types diff --git a/floor/lib/floor.dart b/floor/lib/floor.dart index 51ac629a..3c350f9a 100644 --- a/floor/lib/floor.dart +++ b/floor/lib/floor.dart @@ -1,5 +1,10 @@ library floor; +export 'package:floor/src/adapter/deletion_adapter.dart'; +export 'package:floor/src/adapter/insertion_adapter.dart'; +export 'package:floor/src/adapter/migration_adapter.dart'; +export 'package:floor/src/adapter/query_adapter.dart'; +export 'package:floor/src/adapter/update_adapter.dart'; export 'package:floor/src/database.dart'; export 'package:floor/src/migration.dart'; export 'package:floor_annotation/floor_annotation.dart'; diff --git a/floor/lib/src/adapter/deletion_adapter.dart b/floor/lib/src/adapter/deletion_adapter.dart new file mode 100644 index 00000000..2ca3a28e --- /dev/null +++ b/floor/lib/src/adapter/deletion_adapter.dart @@ -0,0 +1,72 @@ +import 'package:sqflite/sqflite.dart'; + +class DeletionAdapter { + final DatabaseExecutor _database; + final String _entityName; + final String _primaryKeyColumnName; + final Map Function(T) _valueMapper; + + DeletionAdapter( + final DatabaseExecutor database, + final String entityName, + final String primaryKeyColumnName, + final Map Function(T) valueMapper, + ) : assert(database != null), + assert(entityName != null), + assert(entityName.isNotEmpty), + assert(primaryKeyColumnName != null), + assert(primaryKeyColumnName.isNotEmpty), + assert(valueMapper != null), + _database = database, + _entityName = entityName, + _primaryKeyColumnName = primaryKeyColumnName, + _valueMapper = valueMapper; + + Future delete(final T item) async { + await _delete(item); + } + + Future deleteList(final List items) async { + if (items.isEmpty) return; + + final batch = _database.batch(); + _deleteList(batch, items); + await batch.commit(noResult: true); + } + + Future deleteAndReturnChangedRows(final T item) { + return _delete(item); + } + + Future deleteListAndReturnChangedRows(final List items) async { + if (items.isEmpty) return 0; + + final batch = _database.batch(); + _deleteList(batch, items); + return (await batch.commit(noResult: false)) + .cast() + .reduce((sum, element) => sum + element); + } + + Future _delete(final T item) { + final int primaryKey = _valueMapper(item)[_primaryKeyColumnName]; + + return _database.delete( + _entityName, + where: '$_primaryKeyColumnName = ?', + whereArgs: [primaryKey], + ); + } + + void _deleteList(final Batch batch, final List items) { + for (final item in items) { + final int primaryKey = _valueMapper(item)[_primaryKeyColumnName]; + + batch.delete( + _entityName, + where: '$_primaryKeyColumnName = ?', + whereArgs: [primaryKey], + ); + } + } +} diff --git a/floor/lib/src/adapter/insertion_adapter.dart b/floor/lib/src/adapter/insertion_adapter.dart new file mode 100644 index 00000000..2fbcc7dd --- /dev/null +++ b/floor/lib/src/adapter/insertion_adapter.dart @@ -0,0 +1,77 @@ +import 'package:sqflite/sqflite.dart'; + +class InsertionAdapter { + final DatabaseExecutor _database; + final String _entityName; + final Map Function(T) _valueMapper; + + InsertionAdapter( + final DatabaseExecutor database, + final String entityName, + final Map Function(T) valueMapper, + ) : assert(database != null), + assert(entityName != null), + assert(entityName.isNotEmpty), + assert(valueMapper != null), + _database = database, + _entityName = entityName, + _valueMapper = valueMapper; + + Future insert( + final T item, + final ConflictAlgorithm conflictAlgorithm, + ) async { + await _insert(item, conflictAlgorithm); + } + + Future insertList( + final List items, + final ConflictAlgorithm conflictAlgorithm, + ) async { + if (items.isEmpty) return; + + final batch = _database.batch(); + _insertList(batch, items, conflictAlgorithm); + await batch.commit(noResult: true); + } + + Future insertAndReturnId( + final T item, + final ConflictAlgorithm conflictAlgorithm, + ) { + return _insert(item, conflictAlgorithm); + } + + Future> insertListAndReturnIds( + final List items, + final ConflictAlgorithm conflictAlgorithm, + ) async { + if (items.isEmpty) return []; + + final batch = _database.batch(); + _insertList(batch, items, conflictAlgorithm); + return (await batch.commit(noResult: false)).cast(); + } + + Future _insert(final T item, final ConflictAlgorithm conflictAlgorithm) { + return _database.insert( + _entityName, + _valueMapper(item), + conflictAlgorithm: conflictAlgorithm, + ); + } + + void _insertList( + final Batch batch, + final List items, + final ConflictAlgorithm conflictAlgorithm, + ) { + for (final item in items) { + batch.insert( + _entityName, + _valueMapper(item), + conflictAlgorithm: conflictAlgorithm, + ); + } + } +} diff --git a/floor/lib/src/adapter/migration_adapter.dart b/floor/lib/src/adapter/migration_adapter.dart new file mode 100644 index 00000000..396e3a24 --- /dev/null +++ b/floor/lib/src/adapter/migration_adapter.dart @@ -0,0 +1,30 @@ +import 'package:floor/src/migration.dart'; +import 'package:sqflite/sqflite.dart'; + +abstract class MigrationAdapter { + /// Runs the given [migrations] for migrating the database schema and data. + static void runMigrations( + final Database migrationDatabase, + final int startVersion, + final int endVersion, + final List migrations, + ) { + final relevantMigrations = migrations + .where((migration) => migration.startVersion >= startVersion) + .toList() + ..sort((first, second) => + first.startVersion.compareTo(second.startVersion)); + + if (relevantMigrations.isEmpty || + relevantMigrations.last.endVersion != endVersion) { + throw StateError( + 'There is no migration supplied to update the database to the current version.' + ' Aborting the migration.', + ); + } + + for (final migration in relevantMigrations) { + migration.migrate(migrationDatabase); + } + } +} diff --git a/floor/lib/src/adapter/query_adapter.dart b/floor/lib/src/adapter/query_adapter.dart new file mode 100644 index 00000000..bb4b3a43 --- /dev/null +++ b/floor/lib/src/adapter/query_adapter.dart @@ -0,0 +1,37 @@ +import 'package:sqflite/sqflite.dart'; + +/// This class knows how to execute database queries. +class QueryAdapter { + final DatabaseExecutor _database; + + QueryAdapter(final DatabaseExecutor database) + : assert(database != null), + _database = database; + + Future query( + final String sql, + final T Function(Map) mapper, + ) async { + final rows = await _database.rawQuery(sql); + + if (rows.isEmpty) { + return null; + } else if (rows.length > 1) { + throw StateError("Query returned more than one row for '$sql'"); + } + + return mapper(rows.first); + } + + Future> queryList( + final String sql, + final T Function(Map) mapper, + ) async { + final rows = await _database.rawQuery(sql); + return rows.map((row) => mapper(row)).toList(); + } + + Future queryNoReturn(final String sql) async { + await _database.rawQuery(sql); + } +} diff --git a/floor/lib/src/adapter/update_adapter.dart b/floor/lib/src/adapter/update_adapter.dart new file mode 100644 index 00000000..f8446196 --- /dev/null +++ b/floor/lib/src/adapter/update_adapter.dart @@ -0,0 +1,94 @@ +import 'package:sqflite/sqflite.dart'; + +class UpdateAdapter { + final DatabaseExecutor _database; + final String _entityName; + final String _primaryKeyColumnName; + final Map Function(T) _valueMapper; + + UpdateAdapter( + final DatabaseExecutor database, + final String entityName, + final String primaryKeyColumnName, + final Map Function(T) valueMapper, + ) : assert(database != null), + assert(entityName != null), + assert(entityName.isNotEmpty), + assert(primaryKeyColumnName != null), + assert(primaryKeyColumnName.isNotEmpty), + assert(valueMapper != null), + _database = database, + _entityName = entityName, + _valueMapper = valueMapper, + _primaryKeyColumnName = primaryKeyColumnName; + + Future update( + final T item, + final ConflictAlgorithm conflictAlgorithm, + ) async { + await _update(item, conflictAlgorithm); + } + + Future updateList( + final List items, + final ConflictAlgorithm conflictAlgorithm, + ) async { + if (items.isEmpty) return; + + final batch = _database.batch(); + _updateList(batch, items, conflictAlgorithm); + await batch.commit(noResult: true); + } + + Future updateAndReturnChangedRows( + final T item, + final ConflictAlgorithm conflictAlgorithm, + ) { + return _update(item, conflictAlgorithm); + } + + Future updateListAndReturnChangedRows( + final List items, + final ConflictAlgorithm conflictAlgorithm, + ) async { + if (items.isEmpty) return 0; + + final batch = _database.batch(); + _updateList(batch, items, conflictAlgorithm); + return (await batch.commit(noResult: false)) + .cast() + .reduce((sum, element) => sum + element); + } + + Future _update(final T item, final ConflictAlgorithm conflictAlgorithm) { + final values = _valueMapper(item); + final int primaryKey = values[_primaryKeyColumnName]; + + return _database.update( + _entityName, + values, + where: '$_primaryKeyColumnName = ?', + whereArgs: [primaryKey], + conflictAlgorithm: conflictAlgorithm, + ); + } + + void _updateList( + final Batch batch, + final List items, + final ConflictAlgorithm conflictAlgorithm, + ) { + for (final item in items) { + final values = _valueMapper(item); + final int primaryKey = values[_primaryKeyColumnName]; + + batch.update( + _entityName, + values, + where: '$_primaryKeyColumnName = ?', + whereArgs: [primaryKey], + conflictAlgorithm: conflictAlgorithm, + ); + } + } +} diff --git a/floor/lib/src/database.dart b/floor/lib/src/database.dart index bdf9ebb8..755555cc 100644 --- a/floor/lib/src/database.dart +++ b/floor/lib/src/database.dart @@ -1,5 +1,4 @@ import 'package:floor/floor.dart'; -import 'package:meta/meta.dart'; import 'package:sqflite/sqflite.dart' as sqflite; /// Extend this class to enable database functionality. @@ -19,31 +18,4 @@ abstract class FloorDatabase { await immutableDatabase.close(); } } - - /// Runs the given [migrations] for migrating the database schema and data. - @protected - void runMigrations( - final sqflite.Database migrationDatabase, - final int startVersion, - final int endVersion, - final List migrations, - ) { - final relevantMigrations = migrations - .where((migration) => migration.startVersion >= startVersion) - .toList() - ..sort((first, second) => - first.startVersion.compareTo(second.startVersion)); - - if (relevantMigrations.isEmpty || - relevantMigrations.last.endVersion != endVersion) { - throw StateError( - 'There is no migration supplied to update the database to the current version.' - ' Aborting the migration.', - ); - } - - for (final migration in relevantMigrations) { - migration.migrate(migrationDatabase); - } - } } diff --git a/floor/test/adapter/deletion_adapter.dart b/floor/test/adapter/deletion_adapter.dart new file mode 100644 index 00000000..7634b10f --- /dev/null +++ b/floor/test/adapter/deletion_adapter.dart @@ -0,0 +1,126 @@ +import 'package:floor/src/adapter/deletion_adapter.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../util/mocks.dart'; +import '../util/person.dart'; + + +void main() { + final mockDatabaseExecutor = MockDatabaseExecutor(); + final mockDatabaseBatch = MockDatabaseBatch(); + + const entityName = 'person'; + const primaryKeyColumnName = 'id'; + final valueMapper = (Person person) => + {'id': person.id, 'name': person.name}; + + final underTest = DeletionAdapter( + mockDatabaseExecutor, + entityName, + primaryKeyColumnName, + valueMapper, + ); + + tearDown(() { + clearInteractions(mockDatabaseExecutor); + }); + + group('delete without return', () { + test('delete item', () async { + final person = Person(1, 'Simon'); + + await underTest.delete(person); + + verify(mockDatabaseExecutor.delete( + entityName, + where: '$primaryKeyColumnName = ?', + whereArgs: [person.id], + )); + }); + + test('delete list', () async { + final person1 = Person(1, 'Simon'); + final person2 = Person(2, 'Frank'); + final persons = [person1, person2]; + when(mockDatabaseExecutor.batch()).thenReturn(mockDatabaseBatch); + + await underTest.deleteList(persons); + + verifyInOrder([ + mockDatabaseExecutor.batch(), + mockDatabaseBatch.delete( + entityName, + where: '$primaryKeyColumnName = ?', + whereArgs: [person1.id], + ), + mockDatabaseBatch.delete( + entityName, + where: '$primaryKeyColumnName = ?', + whereArgs: [person2.id], + ), + mockDatabaseBatch.commit(noResult: true), + ]); + }); + + test('delete list but supply empty list', () async { + await underTest.deleteList([]); + + verifyZeroInteractions(mockDatabaseExecutor); + }); + }); + + group('delete with return', () { + test('delete item and return changed rows (1)', () async { + final person = Person(1, 'Simon'); + when(mockDatabaseExecutor.delete( + entityName, + where: '$primaryKeyColumnName = ?', + whereArgs: [person.id], + )).thenAnswer((_) => Future(() => 1)); + + final actual = await underTest.deleteAndReturnChangedRows(person); + + verify(mockDatabaseExecutor.delete( + entityName, + where: '$primaryKeyColumnName = ?', + whereArgs: [person.id], + )); + expect(actual, equals(1)); + }); + + test('delete items and return changed rows', () async { + final person1 = Person(1, 'Simon'); + final person2 = Person(2, 'Frank'); + final persons = [person1, person2]; + when(mockDatabaseExecutor.batch()).thenReturn(mockDatabaseBatch); + when(mockDatabaseBatch.commit(noResult: false)) + .thenAnswer((_) => Future(() => [1, 1])); + + final actual = await underTest.deleteListAndReturnChangedRows(persons); + + verifyInOrder([ + mockDatabaseExecutor.batch(), + mockDatabaseBatch.delete( + entityName, + where: '$primaryKeyColumnName = ?', + whereArgs: [person1.id], + ), + mockDatabaseBatch.delete( + entityName, + where: '$primaryKeyColumnName = ?', + whereArgs: [person2.id], + ), + mockDatabaseBatch.commit(noResult: false), + ]); + expect(actual, equals(2)); + }); + + test('delete items but supply empty list', () async { + final actual = await underTest.deleteListAndReturnChangedRows([]); + + verifyZeroInteractions(mockDatabaseExecutor); + expect(actual, equals(0)); + }); + }); +} diff --git a/floor/test/adapter/insertion_adapter_test.dart b/floor/test/adapter/insertion_adapter_test.dart new file mode 100644 index 00000000..398fa9e7 --- /dev/null +++ b/floor/test/adapter/insertion_adapter_test.dart @@ -0,0 +1,136 @@ +import 'package:floor/src/adapter/insertion_adapter.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:sqflite/sqflite.dart'; + +import '../util/mocks.dart'; +import '../util/person.dart'; + + +void main() { + final mockDatabaseExecutor = MockDatabaseExecutor(); + final mockDatabaseBatch = MockDatabaseBatch(); + + const entityName = 'person'; + final valueMapper = (Person person) => + {'id': person.id, 'name': person.name}; + const conflictAlgorithm = ConflictAlgorithm.ignore; + + final underTest = InsertionAdapter( + mockDatabaseExecutor, + entityName, + valueMapper, + ); + + tearDown(() { + clearInteractions(mockDatabaseExecutor); + }); + + group('insertion without return', () { + test('insert item', () async { + final person = Person(1, 'Simon'); + + await underTest.insert(person, conflictAlgorithm); + + final values = {'id': person.id, 'name': person.name}; + verify(mockDatabaseExecutor.insert( + entityName, + values, + conflictAlgorithm: conflictAlgorithm, + )); + }); + + test('insert list', () async { + final person1 = Person(1, 'Simon'); + final person2 = Person(2, 'Frank'); + final persons = [person1, person2]; + when(mockDatabaseExecutor.batch()).thenReturn(mockDatabaseBatch); + + await underTest.insertList(persons, conflictAlgorithm); + + final values1 = {'id': person1.id, 'name': person1.name}; + final values2 = {'id': person2.id, 'name': person2.name}; + verifyInOrder([ + mockDatabaseExecutor.batch(), + mockDatabaseBatch.insert( + entityName, + values1, + conflictAlgorithm: conflictAlgorithm, + ), + mockDatabaseBatch.insert( + entityName, + values2, + conflictAlgorithm: conflictAlgorithm, + ), + mockDatabaseBatch.commit(noResult: true), + ]); + }); + + test('insert empty list', () async { + await underTest.insertList([], conflictAlgorithm); + + verifyZeroInteractions(mockDatabaseExecutor); + }); + }); + + group('insertion with return', () { + test('insert item and return primary key', () async { + final person = Person(1, 'Simon'); + final values = {'id': person.id, 'name': person.name}; + when(mockDatabaseExecutor.insert( + entityName, + values, + conflictAlgorithm: conflictAlgorithm, + )).thenAnswer((_) => Future(() => person.id)); + + final actual = + await underTest.insertAndReturnId(person, conflictAlgorithm); + + verify(mockDatabaseExecutor.insert( + entityName, + values, + conflictAlgorithm: conflictAlgorithm, + )); + expect(actual, equals(person.id)); + }); + + test('insert items and return primary keys', () async { + final person1 = Person(1, 'Simon'); + final person2 = Person(2, 'Frank'); + final persons = [person1, person2]; + final primaryKeys = persons.map((person) => person.id).toList(); + when(mockDatabaseExecutor.batch()).thenReturn(mockDatabaseBatch); + when(mockDatabaseBatch.commit(noResult: false)) + .thenAnswer((_) => Future(() => primaryKeys)); + + final actual = + await underTest.insertListAndReturnIds(persons, conflictAlgorithm); + + final values1 = {'id': person1.id, 'name': person1.name}; + final values2 = {'id': person2.id, 'name': person2.name}; + verifyInOrder([ + mockDatabaseExecutor.batch(), + mockDatabaseBatch.insert( + entityName, + values1, + conflictAlgorithm: conflictAlgorithm, + ), + mockDatabaseBatch.insert( + entityName, + values2, + conflictAlgorithm: conflictAlgorithm, + ), + mockDatabaseBatch.commit(noResult: false), + ]); + expect(actual, equals(primaryKeys)); + }); + + test('insert empty list', () async { + final actual = + await underTest.insertListAndReturnIds([], conflictAlgorithm); + + verifyZeroInteractions(mockDatabaseExecutor); + expect(actual, equals([])); + }); + }); +} diff --git a/floor/test/migration_test.dart b/floor/test/adapter/migration_adapter_test.dart similarity index 73% rename from floor/test/migration_test.dart rename to floor/test/adapter/migration_adapter_test.dart index ed082b9a..5fb5405e 100644 --- a/floor/test/migration_test.dart +++ b/floor/test/adapter/migration_adapter_test.dart @@ -1,11 +1,11 @@ -import 'package:floor/src/database.dart'; +import 'package:floor/src/adapter/migration_adapter.dart'; import 'package:floor/src/migration.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; -import 'package:sqflite/sqlite_api.dart'; + +import '../util/mocks.dart'; void main() { - final floorDatabase = TestFloorDatabase(); final mockMigrationDatabase = MockSqfliteDatabase(); tearDown(() { @@ -22,8 +22,7 @@ void main() { }) ]; - // ignore: invalid_use_of_protected_member - floorDatabase.runMigrations( + MigrationAdapter.runMigrations( mockMigrationDatabase, startVersion, endVersion, @@ -51,8 +50,7 @@ void main() { }), ]; - // ignore: invalid_use_of_protected_member - floorDatabase.runMigrations( + MigrationAdapter.runMigrations( mockMigrationDatabase, startVersion, endVersion, @@ -76,8 +74,7 @@ void main() { }) ]; - // ignore: invalid_use_of_protected_member - final actual = () => floorDatabase.runMigrations( + final actual = () => MigrationAdapter.runMigrations( mockMigrationDatabase, startVersion, endVersion, @@ -98,24 +95,14 @@ void main() { }) ]; - // ignore: invalid_use_of_protected_member - final actual = () => floorDatabase.runMigrations( - mockMigrationDatabase, - startVersion, - endVersion, - migrations, - ); + final actual = () => MigrationAdapter.runMigrations( + mockMigrationDatabase, + startVersion, + endVersion, + migrations, + ); expect(actual, throwsStateError); verifyZeroInteractions(mockMigrationDatabase); }); } - -class TestFloorDatabase extends FloorDatabase { - @override - Future open(List migrations) { - return null; - } -} - -class MockSqfliteDatabase extends Mock implements Database {} diff --git a/floor/test/adapter/query_adapter_test.dart b/floor/test/adapter/query_adapter_test.dart new file mode 100644 index 00000000..191e087a --- /dev/null +++ b/floor/test/adapter/query_adapter_test.dart @@ -0,0 +1,93 @@ +import 'package:floor/src/adapter/query_adapter.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +import '../util/mocks.dart'; +import '../util/person.dart'; + +void main() { + final mockDatabaseExecutor = MockDatabaseExecutor(); + + const sql = 'abcd'; + final mapper = (Map row) => Person(row['id'], row['name']); + + final underTest = QueryAdapter(mockDatabaseExecutor); + + tearDown(() { + clearInteractions(mockDatabaseExecutor); + }); + + group('query item', () { + test('returns item', () async { + final person = Person(1, 'Frank'); + final queryResult = Future(() => [ + {'id': person.id, 'name': person.name} + ]); + when(mockDatabaseExecutor.rawQuery(sql)).thenAnswer((_) => queryResult); + + final actual = await underTest.query(sql, mapper); + + expect(actual, equals(person)); + verify(mockDatabaseExecutor.rawQuery(sql)); + }); + + test('null when query returns nothing', () async { + final queryResult = Future(() => >[]); + when(mockDatabaseExecutor.rawQuery(sql)).thenAnswer((_) => queryResult); + + final actual = await underTest.query(sql, mapper); + + expect(actual, isNull); + verify(mockDatabaseExecutor.rawQuery(sql)); + }); + + test('exception because query returns multiple items', () async { + final person = Person(1, 'Frank'); + final queryResult = Future(() => [ + {'id': person.id, 'name': person.name}, + {'id': 2, 'name': 'Peter'}, + ]); + when(mockDatabaseExecutor.rawQuery(sql)).thenAnswer((_) => queryResult); + + final actual = () => underTest.query(sql, mapper); + + expect(actual, throwsStateError); + verify(mockDatabaseExecutor.rawQuery(sql)); + }); + }); + + group('query list', () { + test('returns items', () async { + final person = Person(1, 'Frank'); + final person2 = Person(2, 'Peter'); + final queryResult = Future(() => [ + {'id': person.id, 'name': person.name}, + {'id': person2.id, 'name': person2.name}, + ]); + when(mockDatabaseExecutor.rawQuery(sql)).thenAnswer((_) => queryResult); + + final actual = await underTest.queryList(sql, mapper); + + expect(actual, equals([person, person2])); + verify(mockDatabaseExecutor.rawQuery(sql)); + }); + + test('returns emtpy list when query returns nothing', () async { + final queryResult = Future(() => >[]); + when(mockDatabaseExecutor.rawQuery(sql)).thenAnswer((_) => queryResult); + + final actual = await underTest.queryList(sql, mapper); + + expect(actual, isEmpty); + verify(mockDatabaseExecutor.rawQuery(sql)); + }); + }); + + group('query no return', () { + test('executes query', () async { + await underTest.queryNoReturn(sql); + + verify(mockDatabaseExecutor.rawQuery(sql)); + }); + }); +} diff --git a/floor/test/adapter/update_adapter_test.dart b/floor/test/adapter/update_adapter_test.dart new file mode 100644 index 00000000..61e890de --- /dev/null +++ b/floor/test/adapter/update_adapter_test.dart @@ -0,0 +1,150 @@ +import 'package:floor/src/adapter/update_adapter.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:sqflite/sqflite.dart'; + +import '../util/mocks.dart'; +import '../util/person.dart'; + +void main() { + final mockDatabaseExecutor = MockDatabaseExecutor(); + final mockDatabaseBatch = MockDatabaseBatch(); + + const entityName = 'person'; + const primaryKeyColumnName = 'id'; + final valueMapper = (Person person) => + {'id': person.id, 'name': person.name}; + const conflictAlgorithm = ConflictAlgorithm.abort; + + final underTest = UpdateAdapter( + mockDatabaseExecutor, + entityName, + primaryKeyColumnName, + valueMapper, + ); + + tearDown(() { + clearInteractions(mockDatabaseExecutor); + }); + + group('update without return', () { + test('update item', () async { + final person = Person(1, 'Simon'); + + await underTest.update(person, conflictAlgorithm); + + final values = {'id': person.id, 'name': person.name}; + verify(mockDatabaseExecutor.update( + entityName, + values, + where: '$primaryKeyColumnName = ?', + whereArgs: [person.id], + conflictAlgorithm: conflictAlgorithm, + )); + }); + + test('update items', () async { + final person1 = Person(1, 'Simon'); + final person2 = Person(2, 'Frank'); + final persons = [person1, person2]; + when(mockDatabaseExecutor.batch()).thenReturn(mockDatabaseBatch); + + await underTest.updateList(persons, conflictAlgorithm); + + final values1 = {'id': person1.id, 'name': person1.name}; + final values2 = {'id': person2.id, 'name': person2.name}; + verifyInOrder([ + mockDatabaseExecutor.batch(), + mockDatabaseBatch.update( + entityName, + values1, + where: '$primaryKeyColumnName = ?', + whereArgs: [person1.id], + conflictAlgorithm: conflictAlgorithm, + ), + mockDatabaseBatch.update( + entityName, + values2, + where: '$primaryKeyColumnName = ?', + whereArgs: [person2.id], + conflictAlgorithm: conflictAlgorithm, + ), + mockDatabaseBatch.commit(noResult: true), + ]); + }); + + test('update items but supply empty list', () async { + await underTest.updateList([], conflictAlgorithm); + + verifyZeroInteractions(mockDatabaseExecutor); + }); + }); + + group('update with return', () { + test('update item and return changed rows (1)', () async { + final person = Person(1, 'Simon'); + final values = {'id': person.id, 'name': person.name}; + when(mockDatabaseExecutor.update( + entityName, + values, + where: '$primaryKeyColumnName = ?', + whereArgs: [person.id], + conflictAlgorithm: conflictAlgorithm, + )).thenAnswer((_) => Future(() => 1)); + + final actual = + await underTest.updateAndReturnChangedRows(person, conflictAlgorithm); + + verify(mockDatabaseExecutor.update( + entityName, + values, + where: '$primaryKeyColumnName = ?', + whereArgs: [person.id], + conflictAlgorithm: conflictAlgorithm, + )); + expect(actual, equals(1)); + }); + + test('update items and return changed rows', () async { + final person1 = Person(1, 'Simon'); + final person2 = Person(2, 'Frank'); + final persons = [person1, person2]; + when(mockDatabaseExecutor.batch()).thenReturn(mockDatabaseBatch); + when(mockDatabaseBatch.commit(noResult: false)) + .thenAnswer((_) => Future(() => [1, 1])); + + final actual = await underTest.updateListAndReturnChangedRows( + persons, conflictAlgorithm); + + final values1 = {'id': person1.id, 'name': person1.name}; + final values2 = {'id': person2.id, 'name': person2.name}; + verifyInOrder([ + mockDatabaseExecutor.batch(), + mockDatabaseBatch.update( + entityName, + values1, + where: '$primaryKeyColumnName = ?', + whereArgs: [person1.id], + conflictAlgorithm: conflictAlgorithm, + ), + mockDatabaseBatch.update( + entityName, + values2, + where: '$primaryKeyColumnName = ?', + whereArgs: [person2.id], + conflictAlgorithm: conflictAlgorithm, + ), + mockDatabaseBatch.commit(noResult: false), + ]); + expect(actual, equals(2)); + }); + + test('update items but supply empty list', () async { + final actual = + await underTest.updateListAndReturnChangedRows([], conflictAlgorithm); + + verifyZeroInteractions(mockDatabaseExecutor); + expect(actual, equals(0)); + }); + }); +} diff --git a/floor/test/util/mocks.dart b/floor/test/util/mocks.dart new file mode 100644 index 00000000..63fdb75b --- /dev/null +++ b/floor/test/util/mocks.dart @@ -0,0 +1,8 @@ +import 'package:mockito/mockito.dart'; +import 'package:sqflite/sqflite.dart'; + +class MockDatabaseExecutor extends Mock implements DatabaseExecutor {} + +class MockDatabaseBatch extends Mock implements Batch {} + +class MockSqfliteDatabase extends Mock implements Database {} diff --git a/floor/test/util/person.dart b/floor/test/util/person.dart new file mode 100644 index 00000000..b5ba7ea8 --- /dev/null +++ b/floor/test/util/person.dart @@ -0,0 +1,17 @@ +class Person { + final int id; + final String name; + + Person(this.id, this.name); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Person && + runtimeType == other.runtimeType && + id == other.id && + name == other.name; + + @override + int get hashCode => id.hashCode ^ name.hashCode; +} diff --git a/floor_generator/lib/model/change_method.dart b/floor_generator/lib/model/change_method.dart index f952f8a5..c9a89948 100644 --- a/floor_generator/lib/model/change_method.dart +++ b/floor_generator/lib/model/change_method.dart @@ -57,7 +57,7 @@ class ChangeMethod { .any((entity) => entity == _flattenedParameterType.displayName); } - bool get requiresAsyncModifier => returnsVoid || changesMultipleItems; + bool get requiresAsyncModifier => returnsVoid; bool get returnsList { final type = method.returnType.flattenFutures(method.context.typeSystem); diff --git a/floor_generator/lib/model/entity.dart b/floor_generator/lib/model/entity.dart index daa7f5b0..5c3447b5 100644 --- a/floor_generator/lib/model/entity.dart +++ b/floor_generator/lib/model/entity.dart @@ -1,4 +1,5 @@ import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; import 'package:floor_generator/misc/constants.dart'; import 'package:floor_generator/misc/type_utils.dart'; import 'package:floor_generator/model/column.dart'; @@ -10,8 +11,10 @@ class Entity { Entity(final this.clazz); + String _nameCache; + String get name { - return clazz.metadata + return _nameCache ??= clazz.metadata .firstWhere(isEntityAnnotation) .computeConstantValue() .getField(AnnotationField.ENTITY_TABLE_NAME) @@ -19,18 +22,23 @@ class Entity { clazz.displayName; } + List _fieldsCache; + List get fields { - return clazz.fields - .where((field) => field.displayName != 'hashCode') - .toList(); + return _fieldsCache ??= + clazz.fields.where((field) => field.displayName != 'hashCode').toList(); } + List _columnsCache; + List get columns { - return fields.map((field) => Column(field)).toList(); + return _columnsCache ??= fields.map((field) => Column(field)).toList(); } + Column _primaryKeyColumnCache; + Column get primaryKeyColumn { - return columns.firstWhere( + return _primaryKeyColumnCache ??= columns.firstWhere( (column) => column.isPrimaryKey, orElse: () => throw InvalidGenerationSourceError( 'There is no primary key defined on the entity $name.', @@ -39,8 +47,10 @@ class Entity { ); } + List _foreignKeysCache; + List get foreignKeys { - return clazz.metadata + return _foreignKeysCache ??= clazz.metadata .firstWhere(isEntityAnnotation) .computeConstantValue() .getField(AnnotationField.ENTITY_FOREIGN_KEYS) @@ -50,7 +60,13 @@ class Entity { []; } + String _createTableStatementsCache; + String getCreateTableStatement(final LibraryReader library) { + if (_createTableStatementsCache != null) { + return _createTableStatementsCache; + } + final databaseDefinition = columns.map((column) => column.definition).toList(); @@ -60,6 +76,89 @@ class Entity { databaseDefinition.addAll(foreignKeyDefinitions); - return "'CREATE TABLE IF NOT EXISTS `$name` (${databaseDefinition.join(', ')})'"; + return _createTableStatementsCache ??= + "'CREATE TABLE IF NOT EXISTS `$name` (${databaseDefinition.join(', ')})'"; + } + + String _constructorCache; + + String getConstructor(final LibraryReader library) { + if (_constructorCache != null) { + return _constructorCache; + } + + final columnNames = columns.map((column) => column.name).toList(); + final constructorParameters = clazz.constructors.first.parameters; + + final parameterValues = []; + + for (var i = 0; i < constructorParameters.length; i++) { + final parameterValue = "row['${columnNames[i]}']"; + final castedParameterValue = + _castParameterValue(constructorParameters[i].type, parameterValue); + + if (castedParameterValue != null) { + parameterValues.add(castedParameterValue); + } + } + + return _constructorCache ??= + '${clazz.displayName}(${parameterValues.join(', ')})'; + } + + String _castParameterValue( + final DartType parameterType, + final String parameterValue, + ) { + if (isBool(parameterType)) { + return '($parameterValue as int) != 0'; // maps int to bool + } else if (isString(parameterType)) { + return '$parameterValue as String'; + } else if (isInt(parameterType)) { + return '$parameterValue as int'; + } else if (isDouble(parameterType)) { + return '$parameterValue as double'; + } else { + return null; + } } + + String _valueMappingCache; + + String getValueMapping(final LibraryReader library) { + if (_valueMappingCache != null) { + return _valueMappingCache; + } + + final columnNames = columns.map((column) => column.name).toList(); + final constructorParameters = clazz.constructors.first.parameters; + + final keyValueList = []; + + for (var i = 0; i < constructorParameters.length; i++) { + final valueMapping = _getValueMapping(constructorParameters[i]); + keyValueList.add("'${columnNames[i]}': $valueMapping"); + } + + return _valueMappingCache ??= + '{${keyValueList.join(', ')}}'; + } + + String _getValueMapping(final ParameterElement parameter) { + final parameterName = parameter.displayName; + + return isBool(parameter.type) + ? 'item.$parameterName ? 1 : 0' + : 'item.$parameterName'; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Entity && + runtimeType == other.runtimeType && + clazz == other.clazz; + + @override + int get hashCode => clazz.hashCode; } diff --git a/floor_generator/lib/writer/adapter/deletion_adapters_writer.dart b/floor_generator/lib/writer/adapter/deletion_adapters_writer.dart new file mode 100644 index 00000000..7d7d1cfb --- /dev/null +++ b/floor_generator/lib/writer/adapter/deletion_adapters_writer.dart @@ -0,0 +1,44 @@ +import 'package:code_builder/code_builder.dart'; +import 'package:floor_generator/model/delete_method.dart'; +import 'package:source_gen/source_gen.dart'; + +class DeletionAdaptersWriter { + final LibraryReader library; + final ClassBuilder builder; + final List deleteMethods; + + DeletionAdaptersWriter(this.library, this.builder, this.deleteMethods); + + void write() { + final deleteEntities = deleteMethods + .map((method) => method.getEntity(library)) + .where((entity) => entity != null) + .toSet(); + + for (final entity in deleteEntities) { + final entityName = entity.name; + + final cacheName = '_${entityName}DeletionAdapterCache'; + final type = refer('DeletionAdapter<${entity.clazz.displayName}>'); + + final adapterCache = Field((builder) => builder + ..name = cacheName + ..type = type); + + builder..fields.add(adapterCache); + + final valueMapper = + '(${entity.clazz.displayName} item) => ${entity.getValueMapping(library)}'; + + final getAdapter = Method((builder) => builder + ..type = MethodType.getter + ..name = '_${entityName}DeletionAdapter' + ..returns = type + ..body = Code(''' + return $cacheName ??= DeletionAdapter(database, '$entityName', '${entity.primaryKeyColumn.name}', $valueMapper); + ''')); + + builder..methods.add(getAdapter); + } + } +} diff --git a/floor_generator/lib/writer/adapter/insertion_adapters_writer.dart b/floor_generator/lib/writer/adapter/insertion_adapters_writer.dart new file mode 100644 index 00000000..fee7bc30 --- /dev/null +++ b/floor_generator/lib/writer/adapter/insertion_adapters_writer.dart @@ -0,0 +1,44 @@ +import 'package:code_builder/code_builder.dart'; +import 'package:floor_generator/model/insert_method.dart'; +import 'package:source_gen/source_gen.dart'; + +class InsertionAdaptersWriter { + final LibraryReader library; + final ClassBuilder builder; + final List insertMethods; + + InsertionAdaptersWriter(this.library, this.builder, this.insertMethods); + + void write() { + final insertEntities = insertMethods + .map((method) => method.getEntity(library)) + .where((entity) => entity != null) + .toSet(); + + for (final entity in insertEntities) { + final entityName = entity.name; + + final cacheName = '_${entityName}InsertionAdapterCache'; + final type = refer('InsertionAdapter<${entity.clazz.displayName}>'); + + final adapterCache = Field((builder) => builder + ..name = cacheName + ..type = type); + + builder..fields.add(adapterCache); + + final valueMapper = + '(${entity.clazz.displayName} item) => ${entity.getValueMapping(library)}'; + + final getAdapter = Method((builder) => builder + ..type = MethodType.getter + ..name = '_${entityName}InsertionAdapter' + ..returns = type + ..body = Code(''' + return $cacheName ??= InsertionAdapter(database, '$entityName', $valueMapper); + ''')); + + builder..methods.add(getAdapter); + } + } +} diff --git a/floor_generator/lib/writer/adapter/query_adapter_writer.dart b/floor_generator/lib/writer/adapter/query_adapter_writer.dart new file mode 100644 index 00000000..4f34d4ef --- /dev/null +++ b/floor_generator/lib/writer/adapter/query_adapter_writer.dart @@ -0,0 +1,45 @@ +import 'package:code_builder/code_builder.dart'; +import 'package:floor_generator/model/query_method.dart'; +import 'package:source_gen/source_gen.dart'; + +class QueryAdapterWriter { + final LibraryReader library; + final ClassBuilder builder; + final List queryMethods; + + QueryAdapterWriter(this.library, this.builder, this.queryMethods); + + void write() { + final queryMappers = queryMethods + .map((method) => method.getEntity(library)) + .where((entity) => entity != null) + .toSet() + .map((entity) { + final constructor = entity.getConstructor(library); + final name = '_${entity.name}Mapper'; + + return Field((builder) => builder + ..name = name + ..modifier = FieldModifier.final$ + ..assignment = Code('(Map row) => $constructor')); + }); + + const cacheName = '_queryAdapterCache'; + + final queryAdapterSingleton = Field((builder) => builder + ..name = cacheName + ..type = refer('QueryAdapter')); + + final getQueryAdapter = Method((builder) => builder + ..name = '_queryAdapter' + ..returns = refer('QueryAdapter') + ..type = MethodType.getter + ..lambda = true + ..body = const Code('$cacheName ??= QueryAdapter(database)')); + + builder..fields.addAll(queryMappers); + builder..fields.add(queryAdapterSingleton); + builder..methods.add(getQueryAdapter); + } + +} diff --git a/floor_generator/lib/writer/adapter/update_adapters_writer.dart b/floor_generator/lib/writer/adapter/update_adapters_writer.dart new file mode 100644 index 00000000..90e4c91c --- /dev/null +++ b/floor_generator/lib/writer/adapter/update_adapters_writer.dart @@ -0,0 +1,44 @@ +import 'package:code_builder/code_builder.dart'; +import 'package:floor_generator/model/update_method.dart'; +import 'package:source_gen/source_gen.dart'; + +class UpdateAdaptersWriter { + final LibraryReader library; + final ClassBuilder builder; + final List updateMethods; + + UpdateAdaptersWriter(this.library, this.builder, this.updateMethods); + + void write() { + final updateEntities = updateMethods + .map((method) => method.getEntity(library)) + .where((entity) => entity != null) + .toSet(); + + for (final entity in updateEntities) { + final entityName = entity.name; + + final cacheName = '_${entityName}UpdateAdapterCache'; + final type = refer('UpdateAdapter<${entity.clazz.displayName}>'); + + final adapterCache = Field((builder) => builder + ..name = cacheName + ..type = type); + + builder..fields.add(adapterCache); + + final valueMapper = + '(${entity.clazz.displayName} item) => ${entity.getValueMapping(library)}'; + + final getAdapter = Method((builder) => builder + ..type = MethodType.getter + ..name = '_${entityName}UpdateAdapter' + ..returns = type + ..body = Code(''' + return $cacheName ??= UpdateAdapter(database, '$entityName', '${entity.primaryKeyColumn.name}', $valueMapper); + ''')); + + builder..methods.add(getAdapter); + } + } +} diff --git a/floor_generator/lib/writer/database_writer.dart b/floor_generator/lib/writer/database_writer.dart index 1981040f..f50d3df3 100644 --- a/floor_generator/lib/writer/database_writer.dart +++ b/floor_generator/lib/writer/database_writer.dart @@ -8,8 +8,12 @@ import 'package:floor_generator/model/insert_method.dart'; import 'package:floor_generator/model/query_method.dart'; import 'package:floor_generator/model/transaction_method.dart'; import 'package:floor_generator/model/update_method.dart'; +import 'package:floor_generator/writer/adapter/insertion_adapters_writer.dart'; +import 'package:floor_generator/writer/adapter/query_adapter_writer.dart'; +import 'package:floor_generator/writer/adapter/update_adapters_writer.dart'; import 'package:floor_generator/writer/change_method_writer.dart'; import 'package:floor_generator/writer/delete_method_body_writer.dart'; +import 'package:floor_generator/writer/adapter/deletion_adapters_writer.dart'; import 'package:floor_generator/writer/insert_method_body_writer.dart'; import 'package:floor_generator/writer/query_method_writer.dart'; import 'package:floor_generator/writer/transaction_method_writer.dart'; @@ -87,16 +91,40 @@ class DatabaseWriter implements Writer { final databaseName = database.name; - return Class((builder) => builder + final builder = ClassBuilder() ..name = '_\$$databaseName' - ..extend = refer(databaseName) + ..extend = refer(databaseName); + + final queryMethods = database.queryMethods; + if (queryMethods.isNotEmpty) { + QueryAdapterWriter(library, builder, queryMethods).write(); + } + + final insertMethods = database.insertMethods; + if (insertMethods.isNotEmpty) { + InsertionAdaptersWriter(library, builder, insertMethods).write(); + } + + final updateMethods = database.updateMethods; + if (updateMethods.isNotEmpty) { + UpdateAdaptersWriter(library, builder, updateMethods).write(); + } + + final deleteMethods = database.deleteMethods; + if (deleteMethods.isNotEmpty) { + DeletionAdaptersWriter(library, builder, deleteMethods).write(); + } + + builder ..methods.add(_generateOpenMethod(database, createTableStatements)) - ..methods.addAll(_generateQueryMethods(database.queryMethods)) - ..methods.addAll(_generateInsertMethods(database.insertMethods)) - ..methods.addAll(_generateUpdateMethods(database.updateMethods)) - ..methods.addAll(_generateDeleteMethods(database.deleteMethods)) + ..methods.addAll(_generateQueryMethods(queryMethods)) + ..methods.addAll(_generateInsertMethods(insertMethods)) + ..methods.addAll(_generateUpdateMethods(updateMethods)) + ..methods.addAll(_generateDeleteMethods(deleteMethods)) ..methods - .addAll(_generateTransactionMethods(database.transactionMethods))); + .addAll(_generateTransactionMethods(database.transactionMethods)); + + return builder.build(); } Method _generateOpenMethod( @@ -123,7 +151,7 @@ class DatabaseWriter implements Writer { await database.execute('PRAGMA foreign_keys = ON'); }, onUpgrade: (database, startVersion, endVersion) async { - runMigrations(database, startVersion, endVersion, migrations); + MigrationAdapter.runMigrations(database, startVersion, endVersion, migrations); }, onCreate: (database, version) async { $createTableStatements diff --git a/floor_generator/lib/writer/delete_method_body_writer.dart b/floor_generator/lib/writer/delete_method_body_writer.dart index 9b6140c6..c560d1bb 100644 --- a/floor_generator/lib/writer/delete_method_body_writer.dart +++ b/floor_generator/lib/writer/delete_method_body_writer.dart @@ -18,22 +18,18 @@ class DeleteMethodBodyWriter implements Writer { String _generateMethodBody() { _assertMethodReturnsNoList(); - final entity = method.getEntity(library); - final entityName = entity.name; - final primaryKeyColumn = entity.primaryKeyColumn; + final entityName = method.getEntity(library).name; final methodSignatureParameterName = method.parameter.name; if (method.returnsInt) { return _generateIntReturnMethodBody( methodSignatureParameterName, entityName, - primaryKeyColumn, ); } else if (method.returnsVoid) { return _generateVoidReturnMethodBody( methodSignatureParameterName, entityName, - primaryKeyColumn, ); } else { throw InvalidGenerationSourceError( @@ -46,44 +42,22 @@ class DeleteMethodBodyWriter implements Writer { String _generateVoidReturnMethodBody( final String methodSignatureParameterName, final String entityName, - final Column primaryKeyColumn, ) { if (method.changesMultipleItems) { - return ''' - final batch = database.batch(); - for (final item in $methodSignatureParameterName) { - batch.delete('$entityName', where: '${primaryKeyColumn.name} = ?', whereArgs: [item.${primaryKeyColumn.field.displayName}]); - } - await batch.commit(noResult: true); - '''; + return 'await _${entityName}DeletionAdapter.deleteList($methodSignatureParameterName);'; } else { - return ''' - final item = $methodSignatureParameterName; - await database.delete('$entityName', where: '${primaryKeyColumn.name} = ?', whereArgs: [item.${primaryKeyColumn.field.displayName}]); - '''; + return 'await _${entityName}DeletionAdapter.delete($methodSignatureParameterName);'; } } String _generateIntReturnMethodBody( final String methodSignatureParameterName, final String entityName, - final Column primaryKeyColumn, ) { if (method.changesMultipleItems) { - return ''' - final batch = database.batch(); - for (final item in $methodSignatureParameterName) { - batch.delete('$entityName', where: '${primaryKeyColumn.name} = ?', whereArgs: [item.${primaryKeyColumn.field.displayName}]); - } - return (await batch.commit(noResult: false)) - .cast() - .reduce((first, second) => first + second); - '''; + return 'return _${entityName}DeletionAdapter.deleteListAndReturnChangedRows($methodSignatureParameterName);'; } else { - return ''' - final item = $methodSignatureParameterName; - return database.delete('$entityName', where: '${primaryKeyColumn.name} = ?', whereArgs: [item.${primaryKeyColumn.field.displayName}]); - '''; + return 'return _${entityName}DeletionAdapter.deleteAndReturnChangedRows($methodSignatureParameterName);'; } } diff --git a/floor_generator/lib/writer/insert_method_body_writer.dart b/floor_generator/lib/writer/insert_method_body_writer.dart index 62eb8ca9..76d031f0 100644 --- a/floor_generator/lib/writer/insert_method_body_writer.dart +++ b/floor_generator/lib/writer/insert_method_body_writer.dart @@ -1,6 +1,4 @@ -import 'package:analyzer/dart/element/element.dart'; import 'package:code_builder/code_builder.dart'; -import 'package:floor_generator/misc/type_utils.dart'; import 'package:floor_generator/model/insert_method.dart'; import 'package:floor_generator/writer/writer.dart'; import 'package:source_gen/source_gen.dart'; @@ -17,32 +15,17 @@ class InsertMethodBodyWriter implements Writer { } String _generateMethodBody() { - final entity = method.getEntity(library); - - final columnNames = entity.columns.map((column) => column.name).toList(); - final constructorParameters = - method.flattenedParameterClass.constructors.first.parameters; - - final keyValueList = []; - - for (var i = 0; i < constructorParameters.length; i++) { - final valueMapping = _getValueMapping(constructorParameters[i]); - keyValueList.add("'${columnNames[i]}': $valueMapping"); - } - - final entityName = entity.name; + final entityName = method.getEntity(library).name; final methodSignatureParameterName = method.parameter.displayName; if (method.returnsInt) { return _generateIntReturnMethodBody( methodSignatureParameterName, - keyValueList, entityName, ); } else if (method.returnsVoid) { return _generateVoidReturnMethodBody( methodSignatureParameterName, - keyValueList, entityName, ); } else { @@ -55,63 +38,23 @@ class InsertMethodBodyWriter implements Writer { String _generateVoidReturnMethodBody( final String methodSignatureParameterName, - final List keyValueList, final String entityName, ) { if (method.changesMultipleItems) { - return ''' - final batch = database.batch(); - for (final item in $methodSignatureParameterName) { - final values = { - ${keyValueList.join(', ')} - }; - batch.insert('$entityName', values, conflictAlgorithm: ${method.onConflict}); - } - await batch.commit(noResult: true); - '''; + return 'await _${entityName}InsertionAdapter.insertList($methodSignatureParameterName, ${method.onConflict});'; } else { - return ''' - final item = $methodSignatureParameterName; - final values = { - ${keyValueList.join(', ')} - }; - await database.insert('$entityName', values, conflictAlgorithm: ${method.onConflict}); - '''; + return 'await _${entityName}InsertionAdapter.insert($methodSignatureParameterName, ${method.onConflict});'; } } String _generateIntReturnMethodBody( final String methodSignatureParameterName, - final List keyValueList, final String entityName, ) { if (method.changesMultipleItems) { - return ''' - final batch = database.batch(); - for (final item in $methodSignatureParameterName) { - final values = { - ${keyValueList.join(', ')} - }; - batch.insert('$entityName', values, conflictAlgorithm: ${method.onConflict}); - } - return (await batch.commit(noResult: false)).cast(); - '''; + return 'return _${entityName}InsertionAdapter.insertListAndReturnIds($methodSignatureParameterName, ${method.onConflict});'; } else { - return ''' - final item = $methodSignatureParameterName; - final values = { - ${keyValueList.join(', ')} - }; - return database.insert('$entityName', values, conflictAlgorithm: ${method.onConflict}); - '''; + return 'return _${entityName}InsertionAdapter.insertAndReturnId($methodSignatureParameterName, ${method.onConflict});'; } } - - String _getValueMapping(final ParameterElement parameter) { - final parameterName = parameter.displayName; - - return isBool(parameter.type) - ? 'item.$parameterName ? 1 : 0' - : 'item.$parameterName'; - } } diff --git a/floor_generator/lib/writer/query_method_writer.dart b/floor_generator/lib/writer/query_method_writer.dart index 9e398246..ca0098e8 100644 --- a/floor_generator/lib/writer/query_method_writer.dart +++ b/floor_generator/lib/writer/query_method_writer.dart @@ -22,13 +22,18 @@ class QueryMethodWriter implements Writer { _assertReturnsFuture(); _assertQueryParameters(); - return Method((builder) => builder + final builder = MethodBuilder() ..annotations.add(overrideAnnotationExpression) ..returns = refer(queryMethod.rawReturnType.displayName) ..name = queryMethod.name ..requiredParameters.addAll(_generateMethodParameters()) - ..modifier = MethodModifier.async - ..body = Code(_generateMethodBody())); + ..body = Code(_generateMethodBody()); + + if (queryMethod.returnsVoid) { + builder..modifier = MethodModifier.async; + } + + return builder.build(); } List _generateMethodParameters() { @@ -46,74 +51,23 @@ class QueryMethodWriter implements Writer { }).toList(); } - String _generateMapping() { - final constructorCall = - _generateConstructorCall(queryMethod.flattenedReturnType); + String _generateMethodBody() { + if (queryMethod.returnsVoid) { + return "await _queryAdapter.queryNoReturn('${queryMethod.query}');"; + } + + _assertReturnsEntity(); + final mapper = '_${queryMethod.getEntity(library).name}Mapper'; if (queryMethod.returnsList) { - return 'return rows.map((row) => $constructorCall).toList();'; - } else { return ''' - if (rows.isEmpty) { - return null; - } - final row = rows.first; - return $constructorCall; + return _queryAdapter.queryList('${queryMethod.query}', $mapper); '''; - } - } - - String _generateConstructorCall(final DartType type) { - final columnNames = queryMethod - .getEntity(library) - .columns - .map((column) => column.name) - .toList(); - final constructorParameters = - (type.element as ClassElement).constructors.first.parameters; - - final parameterValues = []; - - for (var i = 0; i < constructorParameters.length; i++) { - final parameterValue = "row['${columnNames[i]}']"; - final castedParameterValue = - _castParameterValue(constructorParameters[i].type, parameterValue); - - if (castedParameterValue != null) { - parameterValues.add(castedParameterValue); - } - } - - return '${type.displayName}(${parameterValues.join(', ')})'; - } - - String _castParameterValue( - final DartType parameterType, - final String parameterValue, - ) { - if (isBool(parameterType)) { - return '($parameterValue as int) != 0'; // maps int to bool - } else if (isString(parameterType)) { - return '$parameterValue as String'; - } else if (isInt(parameterType)) { - return '$parameterValue as int'; - } else if (isDouble(parameterType)) { - return '$parameterValue as double'; } else { - return null; - } - } - - String _generateMethodBody() { - if (queryMethod.returnsVoid) { - return "await database.rawQuery('${queryMethod.query}');"; + return ''' + return _queryAdapter.query('${queryMethod.query}', $mapper); + '''; } - - _assertReturnsEntity(); - return ''' - final rows = await database.rawQuery('${queryMethod.query}'); - ${_generateMapping()} - '''; } void _assertQueryParameters() { diff --git a/floor_generator/lib/writer/update_method_body_writer.dart b/floor_generator/lib/writer/update_method_body_writer.dart index 41703e6e..165c6aeb 100644 --- a/floor_generator/lib/writer/update_method_body_writer.dart +++ b/floor_generator/lib/writer/update_method_body_writer.dart @@ -1,7 +1,4 @@ -import 'package:analyzer/dart/element/element.dart'; import 'package:code_builder/code_builder.dart'; -import 'package:floor_generator/misc/type_utils.dart'; -import 'package:floor_generator/model/column.dart'; import 'package:floor_generator/model/update_method.dart'; import 'package:floor_generator/writer/writer.dart'; import 'package:source_gen/source_gen.dart'; @@ -20,36 +17,18 @@ class UpdateMethodBodyWriter implements Writer { String _generateMethodBody() { _assertMethodReturnsNoList(); - final entity = method.getEntity(library); - - final columnNames = entity.columns.map((column) => column.name).toList(); - final constructorParameters = - method.flattenedParameterClass.constructors.first.parameters; - - final keyValueList = []; - - for (var i = 0; i < constructorParameters.length; i++) { - final valueMapping = _getValueMapping(constructorParameters[i]); - keyValueList.add("'${columnNames[i]}': $valueMapping"); - } - - final entityName = entity.name; + final entityName = method.getEntity(library).name; final methodSignatureParameterName = method.parameter.displayName; - final primaryKeyColumn = entity.primaryKeyColumn; if (method.returnsInt) { return _generateIntReturnMethodBody( methodSignatureParameterName, - keyValueList, entityName, - primaryKeyColumn, ); } else if (method.returnsVoid) { return _generateVoidReturnMethodBody( methodSignatureParameterName, - keyValueList, entityName, - primaryKeyColumn, ); } else { throw InvalidGenerationSourceError( @@ -61,70 +40,26 @@ class UpdateMethodBodyWriter implements Writer { String _generateIntReturnMethodBody( final String methodSignatureParameterName, - final List keyValueList, final String entityName, - final Column primaryKeyColumn, ) { if (method.changesMultipleItems) { - return ''' - final batch = database.batch(); - for (final item in $methodSignatureParameterName) { - final values = { - ${keyValueList.join(', ')} - }; - batch.update('$entityName', values, where: '${primaryKeyColumn.name} = ?', whereArgs: [item.${primaryKeyColumn.field.displayName}], conflictAlgorithm: ${method.onConflict}); - } - return (await batch.commit(noResult: false)) - .cast() - .reduce((first, second) => first + second); - '''; + return 'return _${entityName}UpdateAdapter.updateListAndReturnChangedRows($methodSignatureParameterName, ${method.onConflict});'; } else { - return ''' - final item = $methodSignatureParameterName; - final values = { - ${keyValueList.join(', ')} - }; - return database.update('$entityName', values, where: '${primaryKeyColumn.name} = ?', whereArgs: [item.${primaryKeyColumn.field.displayName}], conflictAlgorithm: ${method.onConflict}); - '''; + return 'return _${entityName}UpdateAdapter.updateAndReturnChangedRows($methodSignatureParameterName, ${method.onConflict});'; } } String _generateVoidReturnMethodBody( final String methodSignatureParameterName, - final List keyValueList, final String entityName, - final Column primaryKeyColumn, ) { if (method.changesMultipleItems) { - return ''' - final batch = database.batch(); - for (final item in $methodSignatureParameterName) { - final values = { - ${keyValueList.join(', ')} - }; - batch.update('$entityName', values, where: '${primaryKeyColumn.name} = ?', whereArgs: [item.${primaryKeyColumn.field.displayName}], conflictAlgorithm: ${method.onConflict}); - } - await batch.commit(noResult: true); - '''; + return 'await _${entityName}UpdateAdapter.updateList($methodSignatureParameterName, ${method.onConflict});'; } else { - return ''' - final item = $methodSignatureParameterName; - final values = { - ${keyValueList.join(', ')} - }; - await database.update('$entityName', values, where: '${primaryKeyColumn.name} = ?', whereArgs: [item.${primaryKeyColumn.field.displayName}], conflictAlgorithm: ${method.onConflict}); - '''; + return 'await _${entityName}UpdateAdapter.update($methodSignatureParameterName, ${method.onConflict});'; } } - String _getValueMapping(final ParameterElement parameter) { - final parameterName = parameter.displayName; - - return isBool(parameter.type) - ? 'item.$parameterName ? 1 : 0' - : 'item.$parameterName'; - } - void _assertMethodReturnsNoList() { if (method.returnsList) { throw InvalidGenerationSourceError( diff --git a/floor_generator/test/database_writer_test.dart b/floor_generator/test/database_writer_test.dart index 1c0aa0c9..aeed65f9 100644 --- a/floor_generator/test/database_writer_test.dart +++ b/floor_generator/test/database_writer_test.dart @@ -41,7 +41,7 @@ void main() { await database.execute('PRAGMA foreign_keys = ON'); }, onUpgrade: (database, startVersion, endVersion) async { - runMigrations(database, startVersion, endVersion, migrations); + MigrationAdapter.runMigrations(database, startVersion, endVersion, migrations); }, onCreate: (database, version) async { await database.execute( @@ -86,7 +86,7 @@ void main() { await database.execute('PRAGMA foreign_keys = ON'); }, onUpgrade: (database, startVersion, endVersion) async { - runMigrations(database, startVersion, endVersion, migrations); + MigrationAdapter.runMigrations(database, startVersion, endVersion, migrations); }, onCreate: (database, version) async { await database.execute( diff --git a/floor_generator/test/insert_method_writer_test.dart b/floor_generator/test/insert_method_writer_test.dart index 15f1fa1e..d8f0134f 100644 --- a/floor_generator/test/insert_method_writer_test.dart +++ b/floor_generator/test/insert_method_writer_test.dart @@ -22,10 +22,7 @@ void main() { expect(actual, equalsDart(r''' @override Future insertPerson(Person person) async { - final item = person; - final values = {'id': item.id, 'custom_name': item.name}; - await database.insert('person', values, - conflictAlgorithm: sqflite.ConflictAlgorithm.abort); + await _personInsertionAdapter.insert(person, sqflite.ConflictAlgorithm.abort); } ''')); }); @@ -39,13 +36,7 @@ void main() { expect(actual, equalsDart(''' @override Future insertPersons(List persons) async { - final batch = database.batch(); - for (final item in persons) { - final values = {'id': item.id, 'custom_name': item.name}; - batch.insert('person', values, - conflictAlgorithm: sqflite.ConflictAlgorithm.abort); - } - await batch.commit(noResult: true); + await _personInsertionAdapter.insertList(persons, sqflite.ConflictAlgorithm.abort); } ''')); }); @@ -61,10 +52,7 @@ void main() { expect(actual, equalsDart(''' @override Future insertPersonWithReturn(Person person) { - final item = person; - final values = {'id': item.id, 'custom_name': item.name}; - return database.insert('person', values, - conflictAlgorithm: sqflite.ConflictAlgorithm.abort); + return _personInsertionAdapter.insertAndReturnId(person, sqflite.ConflictAlgorithm.abort); } ''')); }); @@ -77,14 +65,8 @@ void main() { expect(actual, equalsDart(''' @override - Future> insertPersonsWithReturn(List persons) async { - final batch = database.batch(); - for (final item in persons) { - final values = {'id': item.id, 'custom_name': item.name}; - batch.insert('person', values, - conflictAlgorithm: sqflite.ConflictAlgorithm.abort); - } - return (await batch.commit(noResult: false)).cast(); + Future> insertPersonsWithReturn(List persons) { + return _personInsertionAdapter.insertListAndReturnIds(persons, sqflite.ConflictAlgorithm.abort); } ''')); }); @@ -100,10 +82,7 @@ void main() { expect(actual, equalsDart(r''' @override Future insertPerson(Person person) async { - final item = person; - final values = {'id': item.id, 'custom_name': item.name}; - await database.insert('person', values, - conflictAlgorithm: sqflite.ConflictAlgorithm.abort); + await _personInsertionAdapter.insert(person, sqflite.ConflictAlgorithm.abort); } ''')); }); @@ -117,10 +96,7 @@ void main() { expect(actual, equalsDart(r''' @override Future insertPerson(Person person) async { - final item = person; - final values = {'id': item.id, 'custom_name': item.name}; - await database.insert('person', values, - conflictAlgorithm: sqflite.ConflictAlgorithm.replace); + await _personInsertionAdapter.insert(person, sqflite.ConflictAlgorithm.replace); } ''')); }); @@ -134,10 +110,7 @@ void main() { expect(actual, equalsDart(r''' @override Future insertPerson(Person person) async { - final item = person; - final values = {'id': item.id, 'custom_name': item.name}; - await database.insert('person', values, - conflictAlgorithm: sqflite.ConflictAlgorithm.rollback); + await _personInsertionAdapter.insert(person, sqflite.ConflictAlgorithm.rollback); } ''')); }); @@ -151,10 +124,7 @@ void main() { expect(actual, equalsDart(r''' @override Future insertPerson(Person person) async { - final item = person; - final values = {'id': item.id, 'custom_name': item.name}; - await database.insert('person', values, - conflictAlgorithm: sqflite.ConflictAlgorithm.abort); + await _personInsertionAdapter.insert(person, sqflite.ConflictAlgorithm.abort); } ''')); }); @@ -168,10 +138,7 @@ void main() { expect(actual, equalsDart(r''' @override Future insertPerson(Person person) async { - final item = person; - final values = {'id': item.id, 'custom_name': item.name}; - await database.insert('person', values, - conflictAlgorithm: sqflite.ConflictAlgorithm.fail); + await _personInsertionAdapter.insert(person, sqflite.ConflictAlgorithm.fail); } ''')); }); @@ -185,10 +152,7 @@ void main() { expect(actual, equalsDart(r''' @override Future insertPerson(Person person) async { - final item = person; - final values = {'id': item.id, 'custom_name': item.name}; - await database.insert('person', values, - conflictAlgorithm: sqflite.ConflictAlgorithm.ignore); + await _personInsertionAdapter.insert(person, sqflite.ConflictAlgorithm.ignore); } ''')); });