From 715afa3827309d4915637135298cb011f762e0b3 Mon Sep 17 00:00:00 2001 From: Uwe Trottmann <13865709+greenrobot-team@users.noreply.github.com> Date: Mon, 15 Nov 2021 14:04:33 +0100 Subject: [PATCH 1/2] Prepare for next version. --- objectbox/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/objectbox/CHANGELOG.md b/objectbox/CHANGELOG.md index 2cd82508c..edf303b37 100644 --- a/objectbox/CHANGELOG.md +++ b/objectbox/CHANGELOG.md @@ -1,3 +1,5 @@ +## latest + ## 1.2.1 (2021-11-09) * Fix Flutter apps crashing on iOS 15 simulator. #313 From c9f5516f64bb56be1d69ec99d4b5bd5c9f672045 Mon Sep 17 00:00:00 2001 From: Uwe Trottmann <13865709+greenrobot-team@users.noreply.github.com> Date: Tue, 9 Nov 2021 15:55:59 +0100 Subject: [PATCH 2/2] Support Unique conflict resolution strategy replace. --- generator/lib/src/entity_resolver.dart | 43 ++++++++++++++++++++++---- objectbox/CHANGELOG.md | 3 ++ objectbox/lib/src/annotations.dart | 28 +++++++++++++---- objectbox/lib/src/modelinfo/enums.dart | 3 ++ objectbox/test/box_test.dart | 37 ++++++++++++++++++++++ objectbox/test/entity.dart | 5 +++ objectbox/test/objectbox-model.json | 11 +++++-- 7 files changed, 116 insertions(+), 14 deletions(-) diff --git a/generator/lib/src/entity_resolver.dart b/generator/lib/src/entity_resolver.dart index d47782149..45d8929ea 100644 --- a/generator/lib/src/entity_resolver.dart +++ b/generator/lib/src/entity_resolver.dart @@ -257,6 +257,11 @@ class EntityResolver extends Builder { } } + // Verify there is at most 1 unique property with REPLACE strategy. + ensureSingleUniqueReplace(entity); + // If sync enabled, verify all unique properties use REPLACE strategy. + ifSyncEnsureAllUniqueAreReplace(entity); + entity.properties.forEach((p) => log.info(' $p')); return entity; @@ -301,9 +306,9 @@ class EntityResolver extends Builder { FieldElement f, int? fieldType, Element elementBare, ModelProperty prop) { IndexType? indexType; - final hasIndexAnnotation = _indexChecker.hasAnnotationOfExact(f); - final hasUniqueAnnotation = _uniqueChecker.hasAnnotationOfExact(f); - if (!hasIndexAnnotation && !hasUniqueAnnotation) return null; + final indexAnnotation = _indexChecker.firstAnnotationOfExact(f); + final uniqueAnnotation = _uniqueChecker.firstAnnotationOfExact(f); + if (indexAnnotation == null && uniqueAnnotation == null) return null; // Throw if property type does not support any index. if (fieldType == OBXPropertyType.Float || @@ -319,8 +324,6 @@ class EntityResolver extends Builder { } // If available use index type from annotation. - final indexAnnotation = - hasIndexAnnotation ? _indexChecker.firstAnnotationOfExact(f) : null; if (indexAnnotation != null && !indexAnnotation.isNull) { final enumValItem = enumValueItem(indexAnnotation.getField('type')!); if (enumValItem != null) indexType = IndexType.values[enumValItem]; @@ -343,8 +346,15 @@ class EntityResolver extends Builder { "entity ${elementBare.name}: a hash index is not supported for type '${f.type}' of field '${f.name}'"); } - if (hasUniqueAnnotation) { + if (uniqueAnnotation != null && !uniqueAnnotation.isNull) { prop.flags |= OBXPropertyFlags.UNIQUE; + // Determine unique conflict resolution. + final onConflictVal = + enumValueItem(uniqueAnnotation.getField('onConflict')!); + if (onConflictVal != null && + ConflictStrategy.values[onConflictVal] == ConflictStrategy.replace) { + prop.flags |= OBXPropertyFlags.UNIQUE_ON_CONFLICT_REPLACE; + } } switch (indexType) { @@ -363,6 +373,27 @@ class EntityResolver extends Builder { } } + void ensureSingleUniqueReplace(ModelEntity entity) { + final uniqueReplaceProps = entity.properties + .where((p) => p.hasFlag(OBXPropertyFlags.UNIQUE_ON_CONFLICT_REPLACE)); + if (uniqueReplaceProps.length > 1) { + throw InvalidGenerationSourceError( + "ConflictStrategy.replace can only be used on a single property, but found multiple in '${entity.name}':\n ${uniqueReplaceProps.join('\n ')}"); + } + } + + void ifSyncEnsureAllUniqueAreReplace(ModelEntity entity) { + if (!entity.hasFlag(OBXEntityFlags.SYNC_ENABLED)) return; + final uniqueButNotReplaceProps = entity.properties.where((p) { + return p.hasFlag(OBXPropertyFlags.UNIQUE) && + !p.hasFlag(OBXPropertyFlags.UNIQUE_ON_CONFLICT_REPLACE); + }); + if (uniqueButNotReplaceProps.isNotEmpty) { + throw InvalidGenerationSourceError( + "Synced entities must use @Unique(onConflict: ConflictStrategy.replace) on all unique properties, but found others in '${entity.name}':\n ${uniqueButNotReplaceProps.join('\n ')}"); + } + } + int? enumValueItem(DartObject typeField) { if (!typeField.isNull) { final enumValues = (typeField.type as InterfaceType) diff --git a/objectbox/CHANGELOG.md b/objectbox/CHANGELOG.md index edf303b37..294c3794c 100644 --- a/objectbox/CHANGELOG.md +++ b/objectbox/CHANGELOG.md @@ -1,5 +1,8 @@ ## latest +* Support annotating a single property with `@Unique(onConflict: ConflictStrategy.replace)` to + replace an existing object if a conflict occurs when doing a put. #297 + ## 1.2.1 (2021-11-09) * Fix Flutter apps crashing on iOS 15 simulator. #313 diff --git a/objectbox/lib/src/annotations.dart b/objectbox/lib/src/annotations.dart index 0979e998e..5f993a1a3 100644 --- a/objectbox/lib/src/annotations.dart +++ b/objectbox/lib/src/annotations.dart @@ -1,3 +1,5 @@ +import 'package:objectbox/objectbox.dart'; + /// Entity annotation is used on a class to let ObjectBox know it should store /// it - making the class a "persistable Entity". /// @@ -190,17 +192,31 @@ enum IndexType { hash64, } -/// Unique annotation forces that the value of a property is unique among all -/// objects stored for the given entity. +/// Enforces that the value of a property is unique among all objects in a box +/// before an object can be put. /// -/// Trying to put an Object with offending values will result in an exception. +/// Trying to put an object with offending values will result in a +/// [UniqueViolationException] (see [ConflictStrategy.fail]). +/// Set [onConflict] to change this strategy. /// -/// Unique properties are based on an [Index], so the same restrictions apply. +/// Note: Unique properties are based on an [Index], so the same restrictions apply. /// It is supported to explicitly add the [Index] annotation to configure the -/// index type. +/// index. class Unique { + /// The strategy to use when a conflict is detected when an object is put. + final ConflictStrategy onConflict; + /// Create a Unique annotation. - const Unique(); + const Unique({this.onConflict = ConflictStrategy.fail}); +} + +/// Used with [Unique] to specify the conflict resolution strategy. +enum ConflictStrategy { + /// Throws [UniqueViolationException] if any property violates a [Unique] constraint. + fail, + + /// Any conflicting objects are deleted before the object is inserted. + replace, } /// Backlink annotation specifies a link in a reverse direction of another diff --git a/objectbox/lib/src/modelinfo/enums.dart b/objectbox/lib/src/modelinfo/enums.dart index eec9f9c90..7e1a0edab 100644 --- a/objectbox/lib/src/modelinfo/enums.dart +++ b/objectbox/lib/src/modelinfo/enums.dart @@ -122,6 +122,9 @@ abstract class OBXPropertyFlags { /// /// /// /// For Time Series IDs, a companion property of type Date or DateNano represents the exact timestamp. static const int ID_COMPANION = 16384; + + /// Unique on-conflict strategy: the object being put replaces any existing conflicting object (deletes it). + static const int UNIQUE_ON_CONFLICT_REPLACE = 32768; } abstract class OBXPropertyType { diff --git a/objectbox/test/box_test.dart b/objectbox/test/box_test.dart index b2c945b05..5281d039b 100644 --- a/objectbox/test/box_test.dart +++ b/objectbox/test/box_test.dart @@ -203,6 +203,43 @@ void main() { e.toString().contains('same property value already exists')))); }); + test('.put() replaces duplicate values on a unique replace field on insert', () { + // insert without conflict + box.putMany([ + TestEntity.uniqueReplace(replaceLong: 1, tString: 'original-1'), + TestEntity.uniqueReplace(replaceLong: 2, tString: 'original-2') + ]); + expect(box.count(), equals(2)); + + // insert with conflict, deletes ID 1 and inserts ID 3 + box.put(TestEntity.uniqueReplace(replaceLong: 1, tString: 'replacement-1')); + expect(box.count(), equals(2)); + final replaced = box.get(3)!; + expect(replaced.replaceLong, equals(1)); + expect(replaced.tString, equals('replacement-1')); + }); + + test('.put() replaces duplicate values on a unique replace field on update', () { + // update without conflict + var first = TestEntity.uniqueReplace(replaceLong: 1, tString: 'first'); + box.put(first); + first.replaceLong = 2; + box.put(first); + expect(box.count(), equals(1)); + final updated = box.get(1)!; + expect(updated.replaceLong, equals(2)); + expect(updated.tString, 'first'); + + // update with conflict, deletes ID 2 and keeps ID 1 + box.put(TestEntity.uniqueReplace(replaceLong: 1, tString: 'second')); + first.replaceLong = 1; + box.put(first); + expect(box.count(), equals(1)); + final updated2 = box.get(1)!; + expect(updated2.replaceLong, equals(1)); + expect(updated2.tString, 'first'); + }); + test('.getAll retrieves all items', () { final int id1 = box.put(TestEntity(tString: 'One')); final int id2 = box.put(TestEntity(tString: 'Two')); diff --git a/objectbox/test/entity.dart b/objectbox/test/entity.dart index 378507304..ba7dcfd8a 100644 --- a/objectbox/test/entity.dart +++ b/objectbox/test/entity.dart @@ -137,6 +137,11 @@ class TestEntity { this.uChar, }) : tString = ''; + @Unique(onConflict: ConflictStrategy.replace) + int? replaceLong; + + TestEntity.uniqueReplace({this.replaceLong, this.tString}); + @Property(type: PropertyType.byte) @Index() int? iByte; diff --git a/objectbox/test/objectbox-model.json b/objectbox/test/objectbox-model.json index f27cc44c4..700838464 100644 --- a/objectbox/test/objectbox-model.json +++ b/objectbox/test/objectbox-model.json @@ -5,7 +5,7 @@ "entities": [ { "id": "1:4630700155272683157", - "lastPropertyId": "34:3975438751767916074", + "lastPropertyId": "35:1724663621433823504", "name": "TestEntity", "properties": [ { @@ -189,6 +189,13 @@ "id": "34:3975438751767916074", "name": "tDateNano", "type": 12 + }, + { + "id": "35:1724663621433823504", + "name": "replaceLong", + "type": 6, + "flags": 32808, + "indexId": "20:4846837430056399798" } ], "relations": [ @@ -523,7 +530,7 @@ } ], "lastEntityId": "10:8814538095619551454", - "lastIndexId": "19:3009172190024929732", + "lastIndexId": "20:4846837430056399798", "lastRelationId": "1:2155747579134420981", "lastSequenceId": "0:0", "modelVersion": 5,