Skip to content

Support indexes, add Index and Unique annotation #123

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Nov 29, 2020
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
2 changes: 1 addition & 1 deletion example/flutter/objectbox_demo/test_driver/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ void main() {
// Call the `main()` function of the app, or call `runApp` with
// any widget you are interested in testing.
app.main();
}
}
7 changes: 7 additions & 0 deletions generator/integration-tests/common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,10 @@ commonModelTests(ModelDefinition defs, ModelInfo jsonModel) {
ModelEntity entity(ModelInfo model, String name) {
return model.entities.firstWhere((ModelEntity e) => e.name == name);
}

ModelProperty property(ModelInfo model, String path) {
final components = path.split('.');
return entity(model, components[0])
.properties
.firstWhere((ModelProperty p) => p.name == components[1]);
}
2 changes: 2 additions & 0 deletions generator/integration-tests/indexes/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# start with an empty project, without a objectbox-model.json
objectbox-model.json
47 changes: 47 additions & 0 deletions generator/integration-tests/indexes/1.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'dart:io';
import 'package:objectbox/objectbox.dart';

import 'lib/lib.dart';
import 'lib/objectbox.g.dart';
import 'package:test/test.dart';
import '../test_env.dart';
import '../common.dart';
import 'package:objectbox/src/bindings/bindings.dart';

void main() {
TestEnv<A> env;
final jsonModel = readModelJson('lib');
final defs = getObjectBoxModel();

setUp(() {
env = TestEnv<A>(defs);
});

tearDown(() {
env.close();
});

commonModelTests(defs, jsonModel);

test('project must be generated properly', () {
expect(TestEnv.dir.existsSync(), true);
expect(File('lib/objectbox.g.dart').existsSync(), true);
expect(File('lib/objectbox-model.json').existsSync(), true);
});

test('property flags', () {
expect(property(jsonModel, 'A.id').flags, equals(OBXPropertyFlags.ID));
expect(property(jsonModel, 'A.indexed').flags,
equals(OBXPropertyFlags.INDEXED));
expect(property(jsonModel, 'A.unique').flags,
equals(OBXPropertyFlags.INDEX_HASH | OBXPropertyFlags.UNIQUE));
expect(property(jsonModel, 'A.uniqueValue').flags,
equals(OBXPropertyFlags.INDEXED | OBXPropertyFlags.UNIQUE));
expect(property(jsonModel, 'A.uniqueHash').flags,
equals(OBXPropertyFlags.INDEX_HASH | OBXPropertyFlags.UNIQUE));
expect(property(jsonModel, 'A.uniqueHash64').flags,
equals(OBXPropertyFlags.INDEX_HASH64 | OBXPropertyFlags.UNIQUE));
expect(property(jsonModel, 'A.uid').flags,
equals(OBXPropertyFlags.INDEXED | OBXPropertyFlags.UNIQUE));
});
}
31 changes: 31 additions & 0 deletions generator/integration-tests/indexes/lib/lib.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import 'package:objectbox/objectbox.dart';
import 'objectbox.g.dart';

@Entity()
class A {
@Id()
int id;

@Index()
int indexed;

@Unique()
String unique;

@Unique()
@Index(type: IndexType.value)
String uniqueValue;

@Unique()
@Index(type: IndexType.hash)
String uniqueHash;

@Unique()
@Index(type: IndexType.hash64)
String uniqueHash64;

@Unique()
int uid;

A();
}
1 change: 1 addition & 0 deletions generator/integration-tests/indexes/pubspec.yaml
55 changes: 30 additions & 25 deletions generator/lib/src/code_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -127,19 +127,24 @@ class CodeBuilder extends Builder {
'Entity ${entity.name}(${entity.id.toString()}) not found in the code, removing from the model');
model.removeEntity(entity);
});

entities.forEach((entity) => mergeEntity(model, entity));
}

void mergeProperty(ModelEntity entity, ModelProperty prop) {
final propInModel = entity.findSameProperty(prop);
var propInModel = entity.findSameProperty(prop);

if (propInModel == null) {
log.info('Found new property ${entity.name}.${prop.name}');
entity.addProperty(prop);
propInModel = entity.createProperty(prop.name, prop.id.uid);
}

propInModel.name = prop.name;
propInModel.type = prop.type;
propInModel.flags = prop.flags;

if (!prop.hasIndexFlag()) {
propInModel.removeIndex();
} else {
propInModel.name = prop.name;
propInModel.type = prop.type;
propInModel.flags = prop.flags;
propInModel.indexId ??= entity.model.createIndexId();
}
}

Expand All @@ -151,26 +156,26 @@ class CodeBuilder extends Builder {
if (entityInModel == null) {
log.info('Found new entity ${entity.name}');
// 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
entityInModel = modelInfo.addEntity(entity);
} else {
entityInModel.name = entity.name;
entityInModel.flags = entity.flags;

// here, the entity was found already and entityInModel and readEntity might differ, i.e. conflicts need to be resolved, so merge all properties first
entity.properties.forEach((p) => mergeProperty(entityInModel, p));

// then remove all properties not present anymore in readEntity
final missingProps = entityInModel.properties
.where((p) => entity.findSameProperty(p) == null)
.toList(growable: false);

missingProps.forEach((p) {
log.warning(
'Property ${entity.name}.${p.name}(${p.id.toString()}) not found in the code, removing from the model');
entityInModel.removeProperty(p);
});
entityInModel = modelInfo.createEntity(entity.name, entity.id.uid);
}

entityInModel.name = entity.name;
entityInModel.flags = entity.flags;

// here, the entity was found already and entityInModel and readEntity might differ, i.e. conflicts need to be resolved, so merge all properties first
entity.properties.forEach((p) => mergeProperty(entityInModel, p));

// then remove all properties not present anymore in readEntity
final missingProps = entityInModel.properties
.where((p) => entity.findSameProperty(p) == null)
.toList(growable: false);

missingProps.forEach((p) {
log.warning(
'Property ${entity.name}.${p.name}(${p.id.toString()}) not found in the code, removing from the model');
entityInModel.removeProperty(p);
});

return entityInModel.id;
}
}
94 changes: 91 additions & 3 deletions generator/lib/src/entity_resolver.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';

import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:build/build.dart';
import 'package:objectbox/objectbox.dart' as obx;
import 'package:objectbox/src/bindings/bindings.dart';
Expand All @@ -22,6 +23,8 @@ class EntityResolver extends Builder {
final _idChecker = const TypeChecker.fromRuntime(obx.Id);
final _transientChecker = const TypeChecker.fromRuntime(obx.Transient);
final _syncChecker = const TypeChecker.fromRuntime(obx.Sync);
final _uniqueChecker = const TypeChecker.fromRuntime(obx.Unique);
final _indexChecker = const TypeChecker.fromRuntime(obx.Index);

@override
FutureOr<void> build(BuildStep buildStep) async {
Expand Down Expand Up @@ -140,13 +143,18 @@ class EntityResolver extends Builder {
}

// create property (do not use readEntity.createProperty in order to avoid generating new ids)
final prop =
ModelProperty(IdUid.empty(), f.name, fieldType, flags, entity);
final prop = ModelProperty(IdUid.empty(), f.name, fieldType,
flags: flags, entity: entity);

// Index and unique annotation.
final indexTypeStr =
processAnnotationIndexUnique(f, fieldType, elementBare, prop);

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

log.info(
' property ${prop.name}(${prop.id}) type:${prop.type} flags:${prop.flags}');
' property ${prop.name}(${prop.id}) type:${prop.type} flags:${prop.flags} ${prop.hasIndexFlag() ? "index:${indexTypeStr}" : ""}');
}

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

return entity;
}

String processAnnotationIndexUnique(FieldElement f, int fieldType,
Element elementBare, obx.ModelProperty prop) {
obx.IndexType indexType;

final indexAnnotation = _indexChecker.firstAnnotationOfExact(f);
final hasUniqueAnnotation = _uniqueChecker.hasAnnotationOfExact(f);
if (indexAnnotation == null && !hasUniqueAnnotation) return null;

// Throw if property type does not support any index.
if (fieldType == OBXPropertyType.Float ||
fieldType == OBXPropertyType.Double ||
fieldType == OBXPropertyType.ByteVector) {
throw InvalidGenerationSourceError(
"in target ${elementBare.name}: @Index/@Unique is not supported for type '${f.type.toString()}' of field '${f.name}'");
}

if (prop.hasFlag(OBXPropertyFlags.ID)) {
throw InvalidGenerationSourceError(
'in target ${elementBare.name}: @Index/@Unique is not supported for ID field ${f.name}. IDs are unique by definition and automatically indexed');
}

// If available use index type from annotation.
if (indexAnnotation != null && !indexAnnotation.isNull) {
// find out @Index(type:) value - its an enum IndexType
final indexTypeField = indexAnnotation.getField('type');
if (!indexTypeField.isNull) {
final indexTypeEnumValues = (indexTypeField.type as InterfaceType)
.element
.fields
.where((f) => f.isEnumConstant)
.toList();

// Find the index of the matching enum constant.
for (var i = 0; i < indexTypeEnumValues.length; i++) {
if (indexTypeEnumValues[i].computeConstantValue() == indexTypeField) {
indexType = obx.IndexType.values[i];
break;
}
}
}
}

// Fall back to index type based on property type.
final supportsHashIndex = fieldType == OBXPropertyType.String;
if (indexType == null) {
if (supportsHashIndex) {
indexType = obx.IndexType.hash;
} else {
indexType = obx.IndexType.value;
}
}

// Throw if HASH or HASH64 is not supported by property type.
if (!supportsHashIndex &&
(indexType == obx.IndexType.hash ||
indexType == obx.IndexType.hash64)) {
throw InvalidGenerationSourceError(
"in target ${elementBare.name}: a hash index is not supported for type '${f.type.toString()}' of field '${f.name}'");
}

if (hasUniqueAnnotation) {
prop.flags |= OBXPropertyFlags.UNIQUE;
}

switch (indexType) {
case obx.IndexType.value:
prop.flags |= OBXPropertyFlags.INDEXED;
return 'value';
case obx.IndexType.hash:
prop.flags |= OBXPropertyFlags.INDEX_HASH;
return 'hash';
case obx.IndexType.hash64:
prop.flags |= OBXPropertyFlags.INDEX_HASH64;
return 'hash64';
default:
throw InvalidGenerationSourceError(
'in target ${elementBare.name}: invalid index type: $indexType');
}
}
}
2 changes: 1 addition & 1 deletion generator/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function runTestFile() {
# build before each step, except for "0.dart"
if [ "${1}" != "0" ]; then
echo "Running build_runner before ${file}"
dart pub run build_runner build
dart pub run build_runner build --verbose
fi
echo "Running ${file}"
dart test "${file}"
Expand Down
4 changes: 2 additions & 2 deletions lib/integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ class IntegrationTest {
static void model() {
// create a model with a single entity and a single property
final modelInfo = ModelInfo();
final property = ModelProperty(
IdUid(1, int64_max - 1), 'id', OBXPropertyType.Long, 0, null);
final property =
ModelProperty(IdUid(1, int64_max - 1), 'id', OBXPropertyType.Long);
final entity = ModelEntity(IdUid(1, int64_max), 'entity', modelInfo);
property.entity = entity;
entity.properties.add(property);
Expand Down
31 changes: 31 additions & 0 deletions lib/src/annotations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,34 @@ class Transient {
// class Sync {
// const Sync();
// }

/// Specifies that the property should be indexed.
///
/// It is highly recommended to index properties that are used in a Query to
/// improve query performance. To fine tune indexing of a property you can
/// override the default index type.
///
/// Note: indexes are currently not supported for ByteVector, Float or Double
/// properties.
class Index {
final IndexType /*?*/ type;
const Index({this.type});
}

enum IndexType {
value,
hash,
hash64,
}

/// 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.
///
/// 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.
class Unique {
const Unique();
}
11 changes: 11 additions & 0 deletions lib/src/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ class Model {
// set last entity id
bindings.obx_model_last_entity_id(
_cModel, model.lastEntityId.id, model.lastEntityId.uid);

// set last index id
if (model.lastIndexId != null) {
bindings.obx_model_last_index_id(
_cModel, model.lastIndexId.id, model.lastIndexId.uid);
}
} catch (e) {
bindings.obx_model_free(_cModel);
rethrow;
Expand Down Expand Up @@ -67,6 +73,11 @@ class Model {
try {
_check(bindings.obx_model_property(
_cModel, name, prop.type, prop.id.id, prop.id.uid));

if (prop.indexId != null) {
_check(bindings.obx_model_property_index_id(
_cModel, prop.indexId.id, prop.indexId.uid));
}
} finally {
free(name);
}
Expand Down
Loading