diff --git a/floor_annotation/lib/src/foreign_key.dart b/floor_annotation/lib/src/foreign_key.dart index c774557f..1e76b89c 100644 --- a/floor_annotation/lib/src/foreign_key.dart +++ b/floor_annotation/lib/src/foreign_key.dart @@ -15,13 +15,13 @@ class ForeignKey { /// Action to take when the parent [Entity] is updated from the database. /// /// By default, [ForeignKeyAction.noAction] is used. - final int onUpdate; + final ForeignKeyAction onUpdate; /// [ForeignKeyAction] /// Action to take when the parent [Entity] is deleted from the database. /// /// By default, [ForeignKeyAction.noAction] is used. - final int onDelete; + final ForeignKeyAction onDelete; /// Declares a foreign key on another [Entity]. const ForeignKey({ @@ -35,13 +35,13 @@ class ForeignKey { /// Constants definition for values that can be used in /// [ForeignKey.onDelete] and [ForeignKey.onUpdate] -abstract class ForeignKeyAction { +enum ForeignKeyAction { /// Possible value for [ForeignKey.onDelete] or [ForeignKey.onUpdate]. /// /// When a parent key is modified or deleted from the database, no special /// action is taken. This means that SQLite will not make any effort to fix /// the constraint failure, instead, reject the change. - static const noAction = 1; + noAction, /// Possible value for [ForeignKey.onDelete] or [ForeignKey.onUpdate]. /// @@ -57,7 +57,7 @@ abstract class ForeignKeyAction { /// Even if the foreign key constraint it is attached to is deferred(), /// configuring a RESTRICT action causes SQLite to return an error immediately /// if a parent key with dependent child keys is deleted or modified. - static const restrict = 2; + restrict, /// Possible value for [ForeignKey.onDelete] or [ForeignKey.onUpdate]. /// @@ -65,14 +65,14 @@ abstract class ForeignKeyAction { /// (for [ForeignKey.onDelete]) or modified (for [ForeignKey.onUpdate]), the /// child key columns of all rows in the child table that mapped to the parent /// key are set to contain NULL values. - static const setNull = 3; + setNull, /// Possible value for [ForeignKey.onDelete] or [ForeignKey.onUpdate]. /// /// The 'SET DEFAULT' actions are similar to SET_NULL, except that each of the /// child key columns is set to contain the columns default value instead of /// NULL. - static const setDefault = 4; + setDefault, /// Possible value for [ForeignKey.onDelete] or [ForeignKey.onUpdate]. /// @@ -82,5 +82,5 @@ abstract class ForeignKeyAction { /// deleted parent row is also deleted. For an [ForeignKey.onUpdate] action, /// it means that the values stored in each dependent child key are modified /// to match the new parent key values. - static const cascade = 5; + cascade, } diff --git a/floor_generator/lib/misc/extension/dart_object_extension.dart b/floor_generator/lib/misc/extension/dart_object_extension.dart index 248f611a..12e6f49a 100644 --- a/floor_generator/lib/misc/extension/dart_object_extension.dart +++ b/floor_generator/lib/misc/extension/dart_object_extension.dart @@ -1,15 +1,36 @@ import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/type.dart'; +import 'package:floor_annotation/floor_annotation.dart'; +import 'package:meta/meta.dart'; extension DartObjectExtension on DartObject { - String toEnumValueString() { + /// get the String representation of the enum value, or the result of + /// [orElse] if the enum was not valid. + String toEnumValueString({@required String orElse()}) { final interfaceType = type as InterfaceType; final enumName = interfaceType.getDisplayString(withNullability: false); final enumValue = interfaceType.element.fields .where((element) => element.isEnumConstant) .map((fieldElement) => fieldElement.name) - .singleWhere((valueName) => getField(valueName) != null); + .singleWhere((valueName) => getField(valueName) != null, + orElse: () => null); + if (enumValue == null) { + return orElse(); + } else { + return '$enumName.$enumValue'; + } + } - return '$enumName.$enumValue'; + /// get the ForeignKeyAction this enum represents, or the result of + /// [orElse] if the enum did not contain a valid value. + ForeignKeyAction toForeignKeyAction({@required ForeignKeyAction orElse()}) { + final enumValueString = toEnumValueString(orElse: () => null); + if (enumValueString == null) { + return orElse(); + } else { + return ForeignKeyAction.values.singleWhere( + (foreignKeyAction) => foreignKeyAction.toString() == enumValueString, + orElse: orElse); + } } } diff --git a/floor_generator/lib/misc/foreign_key_action.dart b/floor_generator/lib/misc/extension/foreign_key_action_extension.dart similarity index 56% rename from floor_generator/lib/misc/foreign_key_action.dart rename to floor_generator/lib/misc/extension/foreign_key_action_extension.dart index 4623255c..5351c8c0 100644 --- a/floor_generator/lib/misc/foreign_key_action.dart +++ b/floor_generator/lib/misc/extension/foreign_key_action_extension.dart @@ -1,15 +1,12 @@ +import 'package:floor_annotation/floor_annotation.dart'; import 'package:floor_generator/misc/annotations.dart'; -abstract class ForeignKeyAction { - static const noAction = 1; - static const restrict = 2; - static const setNull = 3; - static const setDefault = 4; - static const cascade = 5; - +extension ForeignKeyActionExtension on ForeignKeyAction { @nonNull - static String getString(final int action) { - switch (action) { + String toSql() { + switch (this) { + case ForeignKeyAction.noAction: + return 'NO ACTION'; case ForeignKeyAction.restrict: return 'RESTRICT'; case ForeignKeyAction.setNull: @@ -18,9 +15,9 @@ abstract class ForeignKeyAction { return 'SET DEFAULT'; case ForeignKeyAction.cascade: return 'CASCADE'; - case ForeignKeyAction.noAction: - default: - return 'NO ACTION'; + default: // can only match null + throw ArgumentError('toSql() should not be called on a null value. ' + 'This is a bug in floor.'); } } } diff --git a/floor_generator/lib/processor/entity_processor.dart b/floor_generator/lib/processor/entity_processor.dart index 8e86bb09..d2591db1 100644 --- a/floor_generator/lib/processor/entity_processor.dart +++ b/floor_generator/lib/processor/entity_processor.dart @@ -4,8 +4,8 @@ import 'package:dartx/dartx.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/extension/dart_object_extension.dart'; import 'package:floor_generator/misc/extension/type_converters_extension.dart'; -import 'package:floor_generator/misc/foreign_key_action.dart'; import 'package:floor_generator/misc/type_utils.dart'; import 'package:floor_generator/processor/error/entity_processor_error.dart'; import 'package:floor_generator/processor/queryable_processor.dart'; @@ -92,13 +92,11 @@ class EntityProcessor extends QueryableProcessor { throw _processorError.missingParentColumns; } - final onUpdateAnnotationValue = - foreignKeyObject.getField(ForeignKeyField.onUpdate)?.toIntValue(); - final onUpdate = ForeignKeyAction.getString(onUpdateAnnotationValue); + final onUpdate = + _getForeignKeyAction(foreignKeyObject, ForeignKeyField.onUpdate); - final onDeleteAnnotationValue = - foreignKeyObject.getField(ForeignKeyField.onDelete)?.toIntValue(); - final onDelete = ForeignKeyAction.getString(onDeleteAnnotationValue); + final onDelete = + _getForeignKeyAction(foreignKeyObject, ForeignKeyField.onDelete); return ForeignKey( parentName, @@ -267,4 +265,18 @@ class EntityProcessor extends QueryableProcessor { return attributeValue; } } + + @nonNull + annotations.ForeignKeyAction _getForeignKeyAction( + DartObject foreignKeyObject, String triggerName) { + final field = foreignKeyObject.getField(triggerName); + if (field == null) { + // field was not defined, return default value + return annotations.ForeignKeyAction.noAction; + } + + return field.toForeignKeyAction( + orElse: () => + throw _processorError.wrongForeignKeyAction(field, triggerName)); + } } diff --git a/floor_generator/lib/processor/error/entity_processor_error.dart b/floor_generator/lib/processor/error/entity_processor_error.dart index f7aabb43..58098520 100644 --- a/floor_generator/lib/processor/error/entity_processor_error.dart +++ b/floor_generator/lib/processor/error/entity_processor_error.dart @@ -1,3 +1,4 @@ +import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:source_gen/source_gen.dart'; @@ -70,6 +71,16 @@ class EntityProcessorError { ); } + InvalidGenerationSourceError wrongForeignKeyAction( + DartObject field, String triggerName) { + return InvalidGenerationSourceError( + 'No ForeignKeyAction with the value $field exists for the $triggerName trigger.', + todo: + 'Make sure to add a correct ForeignKeyAction like `ForeignKeyAction.noAction` or leave it out entirely.', + element: _classElement, + ); + } + InvalidGenerationSourceError get autoIncrementInWithoutRowid { return InvalidGenerationSourceError( 'autoGenerate is not allowed in WITHOUT ROWID tables', diff --git a/floor_generator/lib/processor/insertion_method_processor.dart b/floor_generator/lib/processor/insertion_method_processor.dart index e485f955..873975ec 100644 --- a/floor_generator/lib/processor/insertion_method_processor.dart +++ b/floor_generator/lib/processor/insertion_method_processor.dart @@ -1,7 +1,7 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:floor_annotation/floor_annotation.dart' as annotations - show Insert; + show Insert, OnConflictStrategy; import 'package:floor_generator/misc/annotations.dart'; import 'package:floor_generator/misc/change_method_processor_helper.dart'; import 'package:floor_generator/misc/constants.dart'; @@ -87,7 +87,11 @@ class InsertionMethodProcessor implements Processor { return _methodElement .getAnnotation(annotations.Insert) .getField(AnnotationField.onConflict) - .toEnumValueString(); + .toEnumValueString( + orElse: () => throw InvalidGenerationSourceError( + 'Value of ${AnnotationField.onConflict} must be one of ${annotations.OnConflictStrategy.values.map((e) => e.toString()).join(',')}', + element: _methodElement, + )); } void _assertMethodReturnsFuture(final DartType returnType) { diff --git a/floor_generator/lib/processor/update_method_processor.dart b/floor_generator/lib/processor/update_method_processor.dart index 22e7ca7d..2dface6a 100644 --- a/floor_generator/lib/processor/update_method_processor.dart +++ b/floor_generator/lib/processor/update_method_processor.dart @@ -1,7 +1,7 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:floor_annotation/floor_annotation.dart' as annotations - show Update; + show Update, OnConflictStrategy; import 'package:floor_generator/misc/annotations.dart'; import 'package:floor_generator/misc/change_method_processor_helper.dart'; import 'package:floor_generator/misc/constants.dart'; @@ -69,7 +69,11 @@ class UpdateMethodProcessor implements Processor { return _methodElement .getAnnotation(annotations.Update) .getField(AnnotationField.onConflict) - .toEnumValueString(); + .toEnumValueString( + orElse: () => throw InvalidGenerationSourceError( + 'Value of ${AnnotationField.onConflict} must be one of ${annotations.OnConflictStrategy.values.map((e) => e.toString()).join(',')}', + element: _methodElement, + )); } @nonNull diff --git a/floor_generator/lib/value_object/foreign_key.dart b/floor_generator/lib/value_object/foreign_key.dart index 6c464582..fa8dd6f0 100644 --- a/floor_generator/lib/value_object/foreign_key.dart +++ b/floor_generator/lib/value_object/foreign_key.dart @@ -1,12 +1,14 @@ import 'package:collection/collection.dart'; +import 'package:floor_annotation/floor_annotation.dart' show ForeignKeyAction; import 'package:floor_generator/misc/annotations.dart'; +import 'package:floor_generator/misc/extension/foreign_key_action_extension.dart'; class ForeignKey { final String parentName; final List parentColumns; final List childColumns; - final String onUpdate; - final String onDelete; + final ForeignKeyAction onUpdate; + final ForeignKeyAction onDelete; ForeignKey( this.parentName, @@ -25,8 +27,8 @@ class ForeignKey { return 'FOREIGN KEY ($escapedChildColumns)' ' REFERENCES `$parentName` ($escapedParentColumns)' - ' ON UPDATE $onUpdate' - ' ON DELETE $onDelete'; + ' ON UPDATE ${onUpdate.toSql()}' + ' ON DELETE ${onDelete.toSql()}'; } final _listEquality = const ListEquality(); diff --git a/floor_generator/test/misc/extension/foreign_key_action_extension_test.dart b/floor_generator/test/misc/extension/foreign_key_action_extension_test.dart new file mode 100644 index 00000000..44b79086 --- /dev/null +++ b/floor_generator/test/misc/extension/foreign_key_action_extension_test.dart @@ -0,0 +1,36 @@ +import 'package:floor_annotation/floor_annotation.dart' as annotations; +import 'package:floor_generator/misc/extension/foreign_key_action_extension.dart'; +import 'package:test/test.dart'; + +void main() { + group('foreign key action strings', () { + test('NO ACTION', () { + final actual = annotations.ForeignKeyAction.noAction.toSql(); + expect(actual, equals('NO ACTION')); + }); + + test('RESTRICT', () { + final actual = annotations.ForeignKeyAction.restrict.toSql(); + expect(actual, equals('RESTRICT')); + }); + + test('SET NULL', () { + final actual = annotations.ForeignKeyAction.setNull.toSql(); + expect(actual, equals('SET NULL')); + }); + + test('SET DEFAULT', () { + final actual = annotations.ForeignKeyAction.setDefault.toSql(); + expect(actual, equals('SET DEFAULT')); + }); + + test('CASCADE', () { + final actual = annotations.ForeignKeyAction.cascade.toSql(); + expect(actual, equals('CASCADE')); + }); + + test('null throws ArgumentError', () { + expect(() => null.toSql(), throwsArgumentError); + }); + }); +} diff --git a/floor_generator/test/misc/foreign_key_action_test.dart b/floor_generator/test/misc/foreign_key_action_test.dart deleted file mode 100644 index 43112af2..00000000 --- a/floor_generator/test/misc/foreign_key_action_test.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:floor_generator/misc/foreign_key_action.dart'; -import 'package:test/test.dart'; - -void main() { - group('foreign key action strings', () { - test('NO ACTION', () { - final actual = ForeignKeyAction.getString(ForeignKeyAction.noAction); - - expect(actual, equals('NO ACTION')); - }); - - test('RESTRICT', () { - final actual = ForeignKeyAction.getString(ForeignKeyAction.restrict); - - expect(actual, equals('RESTRICT')); - }); - - test('SET NULL', () { - final actual = ForeignKeyAction.getString(ForeignKeyAction.setNull); - - expect(actual, equals('SET NULL')); - }); - - test('SET DEFAULT', () { - final actual = ForeignKeyAction.getString(ForeignKeyAction.setDefault); - - expect(actual, equals('SET DEFAULT')); - }); - - test('CASCADE', () { - final actual = ForeignKeyAction.getString(ForeignKeyAction.cascade); - - expect(actual, equals('CASCADE')); - }); - - test('falls back to NO ACTION if action not known', () { - final actual = ForeignKeyAction.getString(12345); - - expect(actual, equals('NO ACTION')); - }); - }); -} diff --git a/floor_generator/test/mocks.dart b/floor_generator/test/mocks.dart index 76b45536..e39c0b35 100644 --- a/floor_generator/test/mocks.dart +++ b/floor_generator/test/mocks.dart @@ -1,3 +1,4 @@ +import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:mockito/mockito.dart'; @@ -7,3 +8,8 @@ class MockClassElement extends Mock implements ClassElement {} class MockFieldElement extends Mock implements FieldElement {} class MockDartType extends Mock implements DartType {} + +class MockDartObject extends Mock implements DartObject { + @override + String toString() => 'Null (null)'; +} diff --git a/floor_generator/test/processor/entity_processor_test.dart b/floor_generator/test/processor/entity_processor_test.dart index c16e2a66..ba5f09b2 100644 --- a/floor_generator/test/processor/entity_processor_test.dart +++ b/floor_generator/test/processor/entity_processor_test.dart @@ -1,6 +1,9 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:build_test/build_test.dart'; +import 'package:floor_annotation/floor_annotation.dart' as annotations; +import 'package:floor_generator/misc/constants.dart'; import 'package:floor_generator/processor/entity_processor.dart'; +import 'package:floor_generator/processor/error/entity_processor_error.dart'; import 'package:floor_generator/processor/field_processor.dart'; import 'package:floor_generator/value_object/entity.dart'; import 'package:floor_generator/value_object/foreign_key.dart'; @@ -9,6 +12,7 @@ import 'package:floor_generator/value_object/primary_key.dart'; import 'package:source_gen/source_gen.dart'; import 'package:test/test.dart'; +import '../mocks.dart'; import '../test_utils.dart'; void main() { @@ -131,11 +135,55 @@ void main() { 'Person', ['id'], ['owner_id'], - 'CASCADE', - 'SET NULL', + annotations.ForeignKeyAction.cascade, + annotations.ForeignKeyAction.setNull, ); expect(actual, equals(expected)); }); + + test('error with wrong onUpdate Annotation', () async { + final classElements = await _createClassElements(''' + @entity + class Person { + @primaryKey + final int id; + + final String name; + + Person(this.id, this.name); + } + + @Entity( + foreignKeys: [ + ForeignKey( + childColumns: ['owner_id'], + parentColumns: ['id'], + entity: Person, + onUpdate: null + onDelete: ForeignKeyAction.setNull, + ) + ], + ) + class Dog { + @primaryKey + final int id; + + final String name; + + @ColumnInfo(name: 'owner_id') + final int ownerId; + + Dog(this.id, this.name, this.ownerId); + } + '''); + + final processor = EntityProcessor(classElements[1], {}); + expect( + processor.process, + throwsInvalidGenerationSourceError( + EntityProcessorError(classElements[1]).wrongForeignKeyAction( + MockDartObject(), ForeignKeyField.onUpdate))); + }); }); test('Process entity with "WITHOUT ROWID"', () async { diff --git a/floor_generator/test/processor/insertion_method_processor_test.dart b/floor_generator/test/processor/insertion_method_processor_test.dart index 13068e06..5c62e525 100644 --- a/floor_generator/test/processor/insertion_method_processor_test.dart +++ b/floor_generator/test/processor/insertion_method_processor_test.dart @@ -1,4 +1,5 @@ import 'package:floor_generator/processor/insertion_method_processor.dart'; +import 'package:source_gen/source_gen.dart'; import 'package:test/test.dart'; import '../test_utils.dart'; @@ -18,4 +19,18 @@ void main() { expect(actual, equals('OnConflictStrategy.replace')); }); + + test('Error on wrong onConflict value', () async { + final insertionMethod = await ''' + @Insert(onConflict: OnConflictStrategy.doesnotexist) + Future insertPerson(Person person); + ''' + .asDaoMethodElement(); + final entities = await getPersonEntity(); + + final actual = + () => InsertionMethodProcessor(insertionMethod, [entities]).process(); + + expect(actual, throwsA(const TypeMatcher())); + }); } diff --git a/floor_generator/test/processor/update_method_processor_test.dart b/floor_generator/test/processor/update_method_processor_test.dart index 5eb5cc04..3e6abeb0 100644 --- a/floor_generator/test/processor/update_method_processor_test.dart +++ b/floor_generator/test/processor/update_method_processor_test.dart @@ -1,4 +1,5 @@ import 'package:floor_generator/processor/update_method_processor.dart'; +import 'package:source_gen/source_gen.dart'; import 'package:test/test.dart'; import '../test_utils.dart'; @@ -17,4 +18,18 @@ void main() { expect(actual, equals('OnConflictStrategy.replace')); }); + + test('Error on wrong onConflict value', () async { + final insertionMethod = await ''' + @Update(onConflict: OnConflictStrategy.doesnotexist) + Future updatePerson(Person person); + ''' + .asDaoMethodElement(); + final entities = await getPersonEntity(); + + final actual = + () => UpdateMethodProcessor(insertionMethod, [entities]).process(); + + expect(actual, throwsA(const TypeMatcher())); + }); } diff --git a/floor_generator/test/value_object/entity_test.dart b/floor_generator/test/value_object/entity_test.dart index 42e0ca4d..59404bcb 100644 --- a/floor_generator/test/value_object/entity_test.dart +++ b/floor_generator/test/value_object/entity_test.dart @@ -1,4 +1,6 @@ +import 'package:floor_annotation/floor_annotation.dart' as annotations; import 'package:floor_generator/misc/constants.dart'; +import 'package:floor_generator/misc/extension/foreign_key_action_extension.dart'; import 'package:floor_generator/value_object/entity.dart'; import 'package:floor_generator/value_object/field.dart'; import 'package:floor_generator/value_object/foreign_key.dart'; @@ -119,8 +121,8 @@ void main() { 'parentName', ['parentColumn'], ['childColumn'], - 'foo', - 'bar', + annotations.ForeignKeyAction.cascade, + annotations.ForeignKeyAction.noAction, ); final primaryKey = PrimaryKey([nullableField], true); final entity = Entity( @@ -142,8 +144,8 @@ void main() { 'FOREIGN KEY (`${foreignKey.childColumns[0]}`) ' 'REFERENCES `${foreignKey.parentName}` ' '(`${foreignKey.parentColumns[0]}`) ' - 'ON UPDATE ${foreignKey.onUpdate} ' - 'ON DELETE ${foreignKey.onDelete}' + 'ON UPDATE ${foreignKey.onUpdate.toSql()} ' + 'ON DELETE ${foreignKey.onDelete.toSql()}' ')'; expect(actual, equals(expected)); }); diff --git a/floor_generator/test/value_object/foreign_key_test.dart b/floor_generator/test/value_object/foreign_key_test.dart index 324d9f4b..e07a1514 100644 --- a/floor_generator/test/value_object/foreign_key_test.dart +++ b/floor_generator/test/value_object/foreign_key_test.dart @@ -1,3 +1,4 @@ +import 'package:floor_annotation/floor_annotation.dart' as annotations; import 'package:floor_generator/value_object/foreign_key.dart'; import 'package:test/test.dart'; @@ -8,8 +9,8 @@ void main() { 'Person', ['id'], ['owner_id'], - 'CASCADE', - 'SET NULL', + annotations.ForeignKeyAction.cascade, + annotations.ForeignKeyAction.setNull, ); final actual = foreignKey.getDefinition(); @@ -26,8 +27,8 @@ void main() { 'Person', ['id', 'foo'], ['owner_id', 'bar'], - 'CASCADE', - 'SET NULL', + annotations.ForeignKeyAction.cascade, + annotations.ForeignKeyAction.setNull, ); final actual = foreignKey.getDefinition();