Skip to content

Commit 89bf491

Browse files
authored
Merge pull request #123 from objectbox/Buggaboo-feature/annotation-candy
Support indexes, add Index and Unique annotation
2 parents 4faf387 + 859ba5d commit 89bf491

File tree

19 files changed

+470
-57
lines changed

19 files changed

+470
-57
lines changed

example/flutter/objectbox_demo/test_driver/app.dart

Lines changed: 1 addition & 1 deletion
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

Lines changed: 7 additions & 0 deletions
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+
}
Lines changed: 2 additions & 0 deletions
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
Lines changed: 47 additions & 0 deletions
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+
}
Lines changed: 31 additions & 0 deletions
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+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../shared-pubspec.yaml

generator/lib/src/code_builder.dart

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -127,19 +127,24 @@ class CodeBuilder extends Builder {
127127
'Entity ${entity.name}(${entity.id.toString()}) not found in the code, removing from the model');
128128
model.removeEntity(entity);
129129
});
130-
131-
entities.forEach((entity) => mergeEntity(model, entity));
132130
}
133131

134132
void mergeProperty(ModelEntity entity, ModelProperty prop) {
135-
final propInModel = entity.findSameProperty(prop);
133+
var propInModel = entity.findSameProperty(prop);
134+
136135
if (propInModel == null) {
137136
log.info('Found new property ${entity.name}.${prop.name}');
138-
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();
139146
} else {
140-
propInModel.name = prop.name;
141-
propInModel.type = prop.type;
142-
propInModel.flags = prop.flags;
147+
propInModel.indexId ??= entity.model.createIndexId();
143148
}
144149
}
145150

@@ -151,26 +156,26 @@ class CodeBuilder extends Builder {
151156
if (entityInModel == null) {
152157
log.info('Found new entity ${entity.name}');
153158
// 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
154-
entityInModel = modelInfo.addEntity(entity);
155-
} else {
156-
entityInModel.name = entity.name;
157-
entityInModel.flags = entity.flags;
158-
159-
// here, the entity was found already and entityInModel and readEntity might differ, i.e. conflicts need to be resolved, so merge all properties first
160-
entity.properties.forEach((p) => mergeProperty(entityInModel, p));
161-
162-
// then remove all properties not present anymore in readEntity
163-
final missingProps = entityInModel.properties
164-
.where((p) => entity.findSameProperty(p) == null)
165-
.toList(growable: false);
166-
167-
missingProps.forEach((p) {
168-
log.warning(
169-
'Property ${entity.name}.${p.name}(${p.id.toString()}) not found in the code, removing from the model');
170-
entityInModel.removeProperty(p);
171-
});
159+
entityInModel = modelInfo.createEntity(entity.name, entity.id.uid);
172160
}
173161

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+
174179
return entityInModel.id;
175180
}
176181
}

generator/lib/src/entity_resolver.dart

Lines changed: 91 additions & 3 deletions
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';
@@ -22,6 +23,8 @@ class EntityResolver extends Builder {
2223
final _idChecker = const TypeChecker.fromRuntime(obx.Id);
2324
final _transientChecker = const TypeChecker.fromRuntime(obx.Transient);
2425
final _syncChecker = const TypeChecker.fromRuntime(obx.Sync);
26+
final _uniqueChecker = const TypeChecker.fromRuntime(obx.Unique);
27+
final _indexChecker = const TypeChecker.fromRuntime(obx.Index);
2528

2629
@override
2730
FutureOr<void> build(BuildStep buildStep) async {
@@ -140,13 +143,18 @@ class EntityResolver extends Builder {
140143
}
141144

142145
// create property (do not use readEntity.createProperty in order to avoid generating new ids)
143-
final prop =
144-
ModelProperty(IdUid.empty(), f.name, fieldType, flags, entity);
146+
final prop = ModelProperty(IdUid.empty(), f.name, fieldType,
147+
flags: flags, entity: entity);
148+
149+
// Index and unique annotation.
150+
final indexTypeStr =
151+
processAnnotationIndexUnique(f, fieldType, elementBare, prop);
152+
145153
if (propUid != null) prop.id.uid = propUid;
146154
entity.properties.add(prop);
147155

148156
log.info(
149-
' 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}" : ""}');
150158
}
151159

152160
// some checks on the entity's integrity
@@ -157,4 +165,84 @@ class EntityResolver extends Builder {
157165

158166
return entity;
159167
}
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+
}
160248
}

generator/test.sh

Lines changed: 1 addition & 1 deletion
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

Lines changed: 2 additions & 2 deletions
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);

lib/src/annotations.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,34 @@ class Transient {
3333
// class Sync {
3434
// const Sync();
3535
// }
36+
37+
/// Specifies that the property should be indexed.
38+
///
39+
/// It is highly recommended to index properties that are used in a Query to
40+
/// improve query performance. To fine tune indexing of a property you can
41+
/// override the default index type.
42+
///
43+
/// Note: indexes are currently not supported for ByteVector, Float or Double
44+
/// properties.
45+
class Index {
46+
final IndexType /*?*/ type;
47+
const Index({this.type});
48+
}
49+
50+
enum IndexType {
51+
value,
52+
hash,
53+
hash64,
54+
}
55+
56+
/// Enforces that the value of a property is unique among all Objects in a Box
57+
/// before an Object can be put.
58+
///
59+
/// Trying to put an Object with offending values will result in an exception.
60+
///
61+
/// Unique properties are based on an [Index], so the same restrictions apply.
62+
/// It is supported to explicitly add the [Index] annotation to configure the
63+
/// index type.
64+
class Unique {
65+
const Unique();
66+
}

lib/src/model.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ class Model {
1919
// set last entity id
2020
bindings.obx_model_last_entity_id(
2121
_cModel, model.lastEntityId.id, model.lastEntityId.uid);
22+
23+
// set last index id
24+
if (model.lastIndexId != null) {
25+
bindings.obx_model_last_index_id(
26+
_cModel, model.lastIndexId.id, model.lastIndexId.uid);
27+
}
2228
} catch (e) {
2329
bindings.obx_model_free(_cModel);
2430
rethrow;
@@ -67,6 +73,11 @@ class Model {
6773
try {
6874
_check(bindings.obx_model_property(
6975
_cModel, name, prop.type, prop.id.id, prop.id.uid));
76+
77+
if (prop.indexId != null) {
78+
_check(bindings.obx_model_property_index_id(
79+
_cModel, prop.indexId.id, prop.indexId.uid));
80+
}
7081
} finally {
7182
free(name);
7283
}

0 commit comments

Comments
 (0)