Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 37 additions & 6 deletions generator/lib/src/entity_resolver.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 ||
Expand All @@ -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];
Expand All @@ -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) {
Expand All @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions objectbox/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +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
Expand Down
28 changes: 22 additions & 6 deletions objectbox/lib/src/annotations.dart
Original file line number Diff line number Diff line change
@@ -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".
///
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions objectbox/lib/src/modelinfo/enums.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
37 changes: 37 additions & 0 deletions objectbox/test/box_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
5 changes: 5 additions & 0 deletions objectbox/test/entity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 9 additions & 2 deletions objectbox/test/objectbox-model.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"entities": [
{
"id": "1:4630700155272683157",
"lastPropertyId": "34:3975438751767916074",
"lastPropertyId": "35:1724663621433823504",
"name": "TestEntity",
"properties": [
{
Expand Down Expand Up @@ -189,6 +189,13 @@
"id": "34:3975438751767916074",
"name": "tDateNano",
"type": 12
},
{
"id": "35:1724663621433823504",
"name": "replaceLong",
"type": 6,
"flags": 32808,
"indexId": "20:4846837430056399798"
}
],
"relations": [
Expand Down Expand Up @@ -523,7 +530,7 @@
}
],
"lastEntityId": "10:8814538095619551454",
"lastIndexId": "19:3009172190024929732",
"lastIndexId": "20:4846837430056399798",
"lastRelationId": "1:2155747579134420981",
"lastSequenceId": "0:0",
"modelVersion": 5,
Expand Down