Skip to content

Commit 859ba5d

Browse files
committed
index/unique annotations cleanup, fixes and tests
1 parent 80769f5 commit 859ba5d

19 files changed

+311
-189
lines changed

example/flutter/objectbox_demo/test_driver/app.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ void main() {
88
// Call the `main()` function of the app, or call `runApp` with
99
// any widget you are interested in testing.
1010
app.main();
11-
}
11+
}

generator/integration-tests/common.dart

+7
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,10 @@ commonModelTests(ModelDefinition defs, ModelInfo jsonModel) {
7373
ModelEntity entity(ModelInfo model, String name) {
7474
return model.entities.firstWhere((ModelEntity e) => e.name == name);
7575
}
76+
77+
ModelProperty property(ModelInfo model, String path) {
78+
final components = path.split('.');
79+
return entity(model, components[0])
80+
.properties
81+
.firstWhere((ModelProperty p) => p.name == components[1]);
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# start with an empty project, without a objectbox-model.json
2+
objectbox-model.json
+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import 'dart:io';
2+
import 'package:objectbox/objectbox.dart';
3+
4+
import 'lib/lib.dart';
5+
import 'lib/objectbox.g.dart';
6+
import 'package:test/test.dart';
7+
import '../test_env.dart';
8+
import '../common.dart';
9+
import 'package:objectbox/src/bindings/bindings.dart';
10+
11+
void main() {
12+
TestEnv<A> env;
13+
final jsonModel = readModelJson('lib');
14+
final defs = getObjectBoxModel();
15+
16+
setUp(() {
17+
env = TestEnv<A>(defs);
18+
});
19+
20+
tearDown(() {
21+
env.close();
22+
});
23+
24+
commonModelTests(defs, jsonModel);
25+
26+
test('project must be generated properly', () {
27+
expect(TestEnv.dir.existsSync(), true);
28+
expect(File('lib/objectbox.g.dart').existsSync(), true);
29+
expect(File('lib/objectbox-model.json').existsSync(), true);
30+
});
31+
32+
test('property flags', () {
33+
expect(property(jsonModel, 'A.id').flags, equals(OBXPropertyFlags.ID));
34+
expect(property(jsonModel, 'A.indexed').flags,
35+
equals(OBXPropertyFlags.INDEXED));
36+
expect(property(jsonModel, 'A.unique').flags,
37+
equals(OBXPropertyFlags.INDEX_HASH | OBXPropertyFlags.UNIQUE));
38+
expect(property(jsonModel, 'A.uniqueValue').flags,
39+
equals(OBXPropertyFlags.INDEXED | OBXPropertyFlags.UNIQUE));
40+
expect(property(jsonModel, 'A.uniqueHash').flags,
41+
equals(OBXPropertyFlags.INDEX_HASH | OBXPropertyFlags.UNIQUE));
42+
expect(property(jsonModel, 'A.uniqueHash64').flags,
43+
equals(OBXPropertyFlags.INDEX_HASH64 | OBXPropertyFlags.UNIQUE));
44+
expect(property(jsonModel, 'A.uid').flags,
45+
equals(OBXPropertyFlags.INDEXED | OBXPropertyFlags.UNIQUE));
46+
});
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import 'package:objectbox/objectbox.dart';
2+
import 'objectbox.g.dart';
3+
4+
@Entity()
5+
class A {
6+
@Id()
7+
int id;
8+
9+
@Index()
10+
int indexed;
11+
12+
@Unique()
13+
String unique;
14+
15+
@Unique()
16+
@Index(type: IndexType.value)
17+
String uniqueValue;
18+
19+
@Unique()
20+
@Index(type: IndexType.hash)
21+
String uniqueHash;
22+
23+
@Unique()
24+
@Index(type: IndexType.hash64)
25+
String uniqueHash64;
26+
27+
@Unique()
28+
int uid;
29+
30+
A();
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../shared-pubspec.yaml

generator/lib/src/code_builder.dart

+30-29
Original file line numberDiff line numberDiff line change
@@ -130,20 +130,21 @@ class CodeBuilder extends Builder {
130130
}
131131

132132
void mergeProperty(ModelEntity entity, ModelProperty prop) {
133-
final propInModel = entity.findSameProperty(prop);
133+
var propInModel = entity.findSameProperty(prop);
134+
134135
if (propInModel == null) {
135136
log.info('Found new property ${entity.name}.${prop.name}');
136-
entity.addProperty(prop);
137+
propInModel = entity.createProperty(prop.name, prop.id.uid);
138+
}
139+
140+
propInModel.name = prop.name;
141+
propInModel.type = prop.type;
142+
propInModel.flags = prop.flags;
143+
144+
if (!prop.hasIndexFlag()) {
145+
propInModel.removeIndex();
137146
} else {
138-
if (propInModel.name != prop.name) {
139-
log.warning('The name of the property(${prop.name}) changed.');
140-
}
141-
if (propInModel.flags != prop.flags) {
142-
log.warning('The flags of the property(${prop.name}) changed.');
143-
}
144-
if (propInModel.type != prop.type) {
145-
log.warning('The type of the property(${prop.name}) changed.');
146-
}
147+
propInModel.indexId ??= entity.model.createIndexId();
147148
}
148149
}
149150

@@ -155,26 +156,26 @@ class CodeBuilder extends Builder {
155156
if (entityInModel == null) {
156157
log.info('Found new entity ${entity.name}');
157158
// in case the entity is created (i.e. when its given UID or name that does not yet exist), we are done, as nothing needs to be merged
158-
entityInModel = modelInfo.addEntity(entity);
159-
} else {
160-
entityInModel.name = entity.name;
161-
entityInModel.flags = entity.flags;
162-
163-
// here, the entity was found already and entityInModel and readEntity might differ, i.e. conflicts need to be resolved, so merge all properties first
164-
entity.properties.forEach((p) => mergeProperty(entityInModel, p));
165-
166-
// then remove all properties not present anymore in readEntity
167-
final missingProps = entityInModel.properties
168-
.where((p) => entity.findSameProperty(p) == null)
169-
.toList(growable: false);
170-
171-
missingProps.forEach((p) {
172-
log.warning(
173-
'Property ${entity.name}.${p.name}(${p.id.toString()}) not found in the code, removing from the model');
174-
entityInModel.removeProperty(p);
175-
});
159+
entityInModel = modelInfo.createEntity(entity.name, entity.id.uid);
176160
}
177161

162+
entityInModel.name = entity.name;
163+
entityInModel.flags = entity.flags;
164+
165+
// here, the entity was found already and entityInModel and readEntity might differ, i.e. conflicts need to be resolved, so merge all properties first
166+
entity.properties.forEach((p) => mergeProperty(entityInModel, p));
167+
168+
// then remove all properties not present anymore in readEntity
169+
final missingProps = entityInModel.properties
170+
.where((p) => entity.findSameProperty(p) == null)
171+
.toList(growable: false);
172+
173+
missingProps.forEach((p) {
174+
log.warning(
175+
'Property ${entity.name}.${p.name}(${p.id.toString()}) not found in the code, removing from the model');
176+
entityInModel.removeProperty(p);
177+
});
178+
178179
return entityInModel.id;
179180
}
180181
}

generator/lib/src/entity_resolver.dart

+88-60
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:async';
22
import 'dart:convert';
33

44
import 'package:analyzer/dart/element/element.dart';
5+
import 'package:analyzer/dart/element/type.dart';
56
import 'package:build/build.dart';
67
import 'package:objectbox/objectbox.dart' as obx;
78
import 'package:objectbox/src/bindings/bindings.dart';
@@ -91,7 +92,7 @@ class EntityResolver extends Builder {
9192
}
9293

9394
int fieldType, flags = 0;
94-
int propUid, indexUid;
95+
int propUid;
9596

9697
if (_idChecker.hasAnnotationOfExact(f)) {
9798
if (hasIdProperty) {
@@ -141,72 +142,19 @@ class EntityResolver extends Builder {
141142
}
142143
}
143144

144-
// Index and unique index.
145-
final indexAnnotation = _indexChecker.firstAnnotationOfExact(f);
146-
final hasUniqueAnnotation = _uniqueChecker.hasAnnotationOfExact(f);
147-
if (indexAnnotation != null || hasUniqueAnnotation) {
148-
// Throw if property type does not support any index.
149-
if (fieldType == OBXPropertyType.Float || fieldType == OBXPropertyType.Double || fieldType == OBXPropertyType.ByteVector) {
150-
throw InvalidGenerationSourceError(
151-
"in target ${elementBare.name}: @Index/@Unique is not supported for type '${f.type.toString()}' of field '${f.name}'");
152-
}
153-
154-
// TODO Throw if index used on Id property.
155-
156-
157-
// If available use index type from annotation.
158-
var indexFlag;
159-
if (indexAnnotation != null) {
160-
indexFlag = indexAnnotation.getField('flag').toIntValue();
161-
if (indexFlag != null &&
162-
(indexFlag != OBXPropertyFlag.INDEXED
163-
|| indexFlag != OBXPropertyFlag.INDEX_HASH
164-
|| indexFlag != OBXPropertyFlag.INDEX_HASH64)) {
165-
throw InvalidGenerationSourceError(
166-
'in target ${elementBare.name}: @Index flag must be one of [OBXPropertyFlag.INDEXED, OBXPropertyFlag.INDEX_HASH, OBXPropertyFlag.INDEX_HASH64] or none for auto-detection');
167-
}
168-
}
169-
170-
// Fall back to index type based on property type.
171-
final supportsHashIndex = fieldType == OBXPropertyType.String;
172-
if (indexFlag == null) {
173-
if (supportsHashIndex) {
174-
indexFlag = OBXPropertyFlag.INDEX_HASH;
175-
} else {
176-
indexFlag = OBXPropertyFlag.INDEXED;
177-
}
178-
}
179-
180-
// Throw if HASH or HASH64 is not supported by property type.
181-
if (!supportsHashIndex &&
182-
(indexFlag == OBXPropertyFlag.INDEX_HASH ||
183-
indexFlag == OBXPropertyFlag.INDEX_HASH64)) {
184-
throw InvalidGenerationSourceError(
185-
"in target ${elementBare.name}: a hash index is not supported for type '${f.type.toString()}' of field '${f.name}'");
186-
}
187-
188-
flags |= indexFlag;
189-
if (hasUniqueAnnotation) {
190-
flags |= OBXPropertyFlag.UNIQUE;
191-
}
192-
}
193-
194145
// create property (do not use readEntity.createProperty in order to avoid generating new ids)
195-
final prop =
196-
ModelProperty(IdUid.empty(), f.name, fieldType, flags, entity);
146+
final prop = ModelProperty(IdUid.empty(), f.name, fieldType,
147+
flags: flags, entity: entity);
197148

198-
// TODO test on @Property(...) that uses the proper flags for index
199-
final isIndexer = flags.isIndexer;
200-
201-
if (isIndexer) {
202-
prop.indexId = indexUid == null ? IdUid.empty() : IdUid(0, indexUid);
203-
}
149+
// Index and unique annotation.
150+
final indexTypeStr =
151+
processAnnotationIndexUnique(f, fieldType, elementBare, prop);
204152

205153
if (propUid != null) prop.id.uid = propUid;
206154
entity.properties.add(prop);
207155

208156
log.info(
209-
' ${isIndexer ? "index " : ""}property ${prop.name}(${prop.id}) type:${prop.type} flags:${prop.flags}');
157+
' property ${prop.name}(${prop.id}) type:${prop.type} flags:${prop.flags} ${prop.hasIndexFlag() ? "index:${indexTypeStr}" : ""}');
210158
}
211159

212160
// some checks on the entity's integrity
@@ -217,4 +165,84 @@ class EntityResolver extends Builder {
217165

218166
return entity;
219167
}
168+
169+
String processAnnotationIndexUnique(FieldElement f, int fieldType,
170+
Element elementBare, obx.ModelProperty prop) {
171+
obx.IndexType indexType;
172+
173+
final indexAnnotation = _indexChecker.firstAnnotationOfExact(f);
174+
final hasUniqueAnnotation = _uniqueChecker.hasAnnotationOfExact(f);
175+
if (indexAnnotation == null && !hasUniqueAnnotation) return null;
176+
177+
// Throw if property type does not support any index.
178+
if (fieldType == OBXPropertyType.Float ||
179+
fieldType == OBXPropertyType.Double ||
180+
fieldType == OBXPropertyType.ByteVector) {
181+
throw InvalidGenerationSourceError(
182+
"in target ${elementBare.name}: @Index/@Unique is not supported for type '${f.type.toString()}' of field '${f.name}'");
183+
}
184+
185+
if (prop.hasFlag(OBXPropertyFlags.ID)) {
186+
throw InvalidGenerationSourceError(
187+
'in target ${elementBare.name}: @Index/@Unique is not supported for ID field ${f.name}. IDs are unique by definition and automatically indexed');
188+
}
189+
190+
// If available use index type from annotation.
191+
if (indexAnnotation != null && !indexAnnotation.isNull) {
192+
// find out @Index(type:) value - its an enum IndexType
193+
final indexTypeField = indexAnnotation.getField('type');
194+
if (!indexTypeField.isNull) {
195+
final indexTypeEnumValues = (indexTypeField.type as InterfaceType)
196+
.element
197+
.fields
198+
.where((f) => f.isEnumConstant)
199+
.toList();
200+
201+
// Find the index of the matching enum constant.
202+
for (var i = 0; i < indexTypeEnumValues.length; i++) {
203+
if (indexTypeEnumValues[i].computeConstantValue() == indexTypeField) {
204+
indexType = obx.IndexType.values[i];
205+
break;
206+
}
207+
}
208+
}
209+
}
210+
211+
// Fall back to index type based on property type.
212+
final supportsHashIndex = fieldType == OBXPropertyType.String;
213+
if (indexType == null) {
214+
if (supportsHashIndex) {
215+
indexType = obx.IndexType.hash;
216+
} else {
217+
indexType = obx.IndexType.value;
218+
}
219+
}
220+
221+
// Throw if HASH or HASH64 is not supported by property type.
222+
if (!supportsHashIndex &&
223+
(indexType == obx.IndexType.hash ||
224+
indexType == obx.IndexType.hash64)) {
225+
throw InvalidGenerationSourceError(
226+
"in target ${elementBare.name}: a hash index is not supported for type '${f.type.toString()}' of field '${f.name}'");
227+
}
228+
229+
if (hasUniqueAnnotation) {
230+
prop.flags |= OBXPropertyFlags.UNIQUE;
231+
}
232+
233+
switch (indexType) {
234+
case obx.IndexType.value:
235+
prop.flags |= OBXPropertyFlags.INDEXED;
236+
return 'value';
237+
case obx.IndexType.hash:
238+
prop.flags |= OBXPropertyFlags.INDEX_HASH;
239+
return 'hash';
240+
case obx.IndexType.hash64:
241+
prop.flags |= OBXPropertyFlags.INDEX_HASH64;
242+
return 'hash64';
243+
default:
244+
throw InvalidGenerationSourceError(
245+
'in target ${elementBare.name}: invalid index type: $indexType');
246+
}
247+
}
220248
}

generator/test.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ function runTestFile() {
1515
# build before each step, except for "0.dart"
1616
if [ "${1}" != "0" ]; then
1717
echo "Running build_runner before ${file}"
18-
dart pub run build_runner build
18+
dart pub run build_runner build --verbose
1919
fi
2020
echo "Running ${file}"
2121
dart test "${file}"

lib/integration_test.dart

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ class IntegrationTest {
1818
static void model() {
1919
// create a model with a single entity and a single property
2020
final modelInfo = ModelInfo();
21-
final property = ModelProperty(
22-
IdUid(1, int64_max - 1), 'id', OBXPropertyType.Long, 0, null);
21+
final property =
22+
ModelProperty(IdUid(1, int64_max - 1), 'id', OBXPropertyType.Long);
2323
final entity = ModelEntity(IdUid(1, int64_max), 'entity', modelInfo);
2424
property.entity = entity;
2525
entity.properties.add(property);

0 commit comments

Comments
 (0)