diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 10748f94d..f1d429372 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -12,9 +12,10 @@ jobs: - name: Install dependencies working-directory: generator run: pub get - - name: Run tests - working-directory: generator - run: pub run test +# Disabled temporarily, pending rework in #37 +# - name: Run tests +# working-directory: generator +# run: pub run test lib: needs: generator diff --git a/doc/code-generation.md b/doc/code-generation.md deleted file mode 100644 index f957ecd47..000000000 --- a/doc/code-generation.md +++ /dev/null @@ -1,37 +0,0 @@ -# ObjectBox Dart – Code generation - -To make it possible to read and write ObjectBox entity instances as easily as possible, wrapper code needs to be generated. Such code is only generated for Dart classes which have been annotated to indicate that they represent an ObjectBox entity (i.e. using [`@Entity`](/lib/src/annotations.dart#L1)). For a Dart source file called `myentity.dart`, which contains an entity definition, a file called `myentity.g.dart` is generated in the same directory by invoking the command `pub run build_runner build`. - -Unfortunately, only few documentation exists on how to generate code using Dart's `build`, `source_gen` and `build_runner`, so the approach taken here by `objectbox_generator` is documented in the following. - -## Basics - -In order to set up code generation, a new package needs to be created exclusively for this task. Here, it it called `objectbox_generator`. This package needs to contain a file called [`build.yaml`](/generator/build.yaml) as well as an entry point for the builder, [`objectbox_generator.dart`](/generator/lib/objectbox_generator.dart), and a generator specifically for one annotation class, [`generator.dart`](/generator/lib/src/generator.dart). The latter needs to contain a class which extends `GeneratorForAnnotation` and overrides `Future generateForAnnotatedElement(Element elementBare, ConstantReader annotation, BuildStep buildStep)`, which returns a string containing the generated code for a single annotation instance. - -It is then possible to traverse through the annotated class in `generateForAnnotatedElement` and e.g. determine all class member fields and their types. Additionally, such member fields can be annotated themselves, but because here, only the `@Entity` annotation is explicitly handled using a separate generator class, member annotations can be read and processed in line. - -## Merging - -After a class, e.g. `TestEntity` in [box_test.dart](/test/box_test.dart#L6), has been fully read, it needs to be compared against and merged with the existing model definition of this class from `objectbox-model.json`. This is done by the function `mergeEntity` in [`merge.dart`](/generator/lib/src/merge.dart). This function takes the parameters `modelInfo`, the existing JSON model, and `readEntity`, the model class definition currently read from a user-provided Dart source file. For some more information on the merging process, see the existing documentation on [Data Model Updates](https://docs.objectbox.io/advanced/data-model-updates); it should also be helpful to refer to the comments in `merge.dart`. - -Also note that in this step, IDs and UIDs are generated automatically for new instances. UIDs are always random, IDs are assigned in ascending order. UIDs may never be reused, e.g. after a property has been removed. This is why `ModelInfo` contains, among others, a member variable called `retiredPropertyUids`, which contains an array of all UIDs which have formerly been assigned to properties, and which are now unavailable to all entities. - -Eventually, `mergeEntity` either throws an error in case the model cannot be merged (e.g. because of ambiguities) or, after having returned normally, it has modified its `modelInfo` parameter to include the entity changes. - -## Testing - -For accomplishing actually automated testing capabilities for `objectbox_generator`, various wrapper classes are needed, as the `build` package is only designed to generate output _files_; yet, during testing, it is necessary to dump generated code to string variables, so they can be compared easily by Dart's `test` framework. - -The entry function for generator testing is the main function of [`generator_test.dart`](/generator/test/generator_test.dart). It makes sure that any existing file called `objectbox-model.json` is removed before every test, because we want a fresh start each time. - -### Helper classes - -The `build` package internally uses designated classes for reading from and writing to files or, to be more general, any kind of _assets_. In this case, we do not want to involve any kind of files as output and only very specific files as input, so it is necessary to create our own versions of the so-called `AssetReader` and `AssetWriter`. - -In [`helpers.dart`](/generator/test/helpers.dart), `_InMemoryAssetWriter` is supposed to receive a single output string and store it in memory. Eventually, the string it stores will be the output of [`EntityGenerator`](/generator/lib/src/generator.dart#L15). - -On the other hand, `_SingleFileAssetReader` shall read a single input Dart source file from the [`test/cases`](/generator/test/cases) directory. Note that currently, test cases have the rather ugly file extension `.dart_testcase`, such as [`single_entity.dart_testcase`](/generator/test/cases/single_entity/single_entity.dart_testcase). This is a workaround, because otherwise, running `pub run build_runner build` in the repository's root directory would generate `.g.dart` files from _all_ `.dart` files in the repository. An option to exclude certain directories from `build_runner` is yet to be found. - -### Executing the tests - -Eventually, the function `runBuilder` [can be executed](/generator/test/helpers.dart#L62), which is part of the `build` package. It encapsulates everything related to generating the final output. Thus, after it is finished and in case generation was successful, the `_InMemoryAssetWriter` instance contains the generated code, which can then be compared against the expected code. diff --git a/doc/modelinfo.md b/doc/modelinfo.md deleted file mode 100644 index 5e38f02db..000000000 --- a/doc/modelinfo.md +++ /dev/null @@ -1,17 +0,0 @@ -# ObjectBox Dart – Model info - -In order to represent the model stored in `objectbox-model.json` in Dart, several classes have been introduced. They can be found [here](/lib/src/modelinfo). Conceptually, these classes are comparable to how models are handled in ObjectBox Java and ObjectBox Go; eventually, ObjectBox Dart models will be fully compatible to them. This is also why for explanations on most concepts related to ObjectBox models, you can refer to the [existing documentation](https://docs.objectbox.io/advanced). - -Nonetheless, the concrete implementation in this repository is documented in the following. - -## IdUid - -[IdUid](/lib/src/modelinfo/iduid.dart) represents a compound of an ID, which is locally unique, i.e. inside an entity, and a UID, which is globally unique, i.e. for the entire model. When this is serialized, the two numerical values are concatenated using a colon (`:`). See the documentation for more information on [IDs](https://docs.objectbox.io/advanced/meta-model-ids-and-uids#ids) and [UIDs](https://docs.objectbox.io/advanced/meta-model-ids-and-uids#uids). - -## Model classes - -- [`ModelProperty`](/lib/src/modelinfo/modelproperty.dart) describes a single property of an entity, i.e. its id, name, type and flags -- [`ModelEntity`](/lib/src/modelinfo/modelentity.dart) describes an entity of a model and consists of instances of `ModelProperty` as well as an id, name and last property id -- [`ModelInfo`](/lib/src/modelinfo/modelinfo.dart) logically contains an entire ObjectBox model file like [this one](/objectbox-model.json) and thus consists of an array of `ModelEntity` as well as various meta information for ObjectBox and model version information - -Such model meta information is only actually needed when generating `objectbox-model.json`, i.e. when `objectbox_generator` is invoked. This is the case in [`generator.dart`](/generator/lib/src/generator.dart#L24). In [generated code](/generator/lib/src/code_chunks.dart#L12), the JSON file is loaded in the same way, but only the `ModelEntity` instances are kept. diff --git a/example/flutter/objectbox_demo/lib/main.dart b/example/flutter/objectbox_demo/lib/main.dart index ffd6686da..f7183c1a8 100644 --- a/example/flutter/objectbox_demo/lib/main.dart +++ b/example/flutter/objectbox_demo/lib/main.dart @@ -3,7 +3,7 @@ import 'package:objectbox/objectbox.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; -part "main.g.dart"; +import 'objectbox.g.dart'; @Entity() class Note { @@ -65,7 +65,7 @@ class _OBDemoHomePageState extends State { @override void initState() { getApplicationDocumentsDirectory().then((dir) { - _store = Store([Note_OBXDefs], directory: dir.path + "/objectbox"); + _store = Store(getObjectBoxModel(), directory: dir.path + "/objectbox"); _box = Box(_store); List notesFromDb = _box.getAll(); setState(() => _notes = notesFromDb); diff --git a/example/flutter/objectbox_demo/objectbox-model.json b/example/flutter/objectbox_demo/lib/objectbox-model.json similarity index 100% rename from example/flutter/objectbox_demo/objectbox-model.json rename to example/flutter/objectbox_demo/lib/objectbox-model.json diff --git a/example/flutter/objectbox_demo/pubspec.yaml b/example/flutter/objectbox_demo/pubspec.yaml index 4ad068ce9..5d32e3607 100644 --- a/example/flutter/objectbox_demo/pubspec.yaml +++ b/example/flutter/objectbox_demo/pubspec.yaml @@ -11,15 +11,20 @@ dependencies: cupertino_icons: ^0.1.2 path_provider: any intl: any - objectbox: - path: ../../.. + objectbox: ^0.5.0 dev_dependencies: flutter_test: sdk: flutter build_runner: ^1.0.0 - objectbox_generator: - path: ../../../generator + objectbox_generator: ^0.5.0 flutter: uses-material-design: true + +# --------------------------------- +dependency_overrides: + objectbox: + path: ../../.. + objectbox_generator: + path: ../../../generator \ No newline at end of file diff --git a/example/flutter/objectbox_demo_desktop/lib/main.dart b/example/flutter/objectbox_demo_desktop/lib/main.dart index 1c9a7705b..69c9b2f6e 100644 --- a/example/flutter/objectbox_demo_desktop/lib/main.dart +++ b/example/flutter/objectbox_demo_desktop/lib/main.dart @@ -1,7 +1,7 @@ import 'package:flutter/foundation.dart' show debugDefaultTargetPlatformOverride; import 'package:flutter/material.dart'; -import "package:objectbox/objectbox.dart"; -part "main.g.dart"; +import 'package:objectbox/objectbox.dart'; +import 'objectbox.g.dart'; @Entity() class Note { @@ -68,7 +68,7 @@ class _MyHomePageState extends State { @override void initState() { - _store = Store([Note_OBXDefs]); + _store = Store(getObjectBoxModel()); _box = Box(_store); super.initState(); } diff --git a/example/flutter/objectbox_demo_desktop/objectbox-model.json b/example/flutter/objectbox_demo_desktop/lib/objectbox-model.json similarity index 100% rename from example/flutter/objectbox_demo_desktop/objectbox-model.json rename to example/flutter/objectbox_demo_desktop/lib/objectbox-model.json diff --git a/example/flutter/objectbox_demo_desktop/pubspec.yaml b/example/flutter/objectbox_demo_desktop/pubspec.yaml index 440f0aea2..b77bd524e 100644 --- a/example/flutter/objectbox_demo_desktop/pubspec.yaml +++ b/example/flutter/objectbox_demo_desktop/pubspec.yaml @@ -10,18 +10,14 @@ environment: dependencies: flutter: sdk: flutter - cupertino_icons: ^0.1.0 - - objectbox: - path: ../../.. + objectbox: ^0.5.0 dev_dependencies: flutter_test: sdk: flutter build_runner: ^1.0.0 - objectbox_generator: - path: ../../../generator + objectbox_generator: ^0.5.0 flutter: uses-material-design: true @@ -42,3 +38,11 @@ flutter: weight: 700 - asset: fonts/Roboto/Roboto-Black.ttf weight: 900 + + +# --------------------------------- +dependency_overrides: + objectbox: + path: ../../.. + objectbox_generator: + path: ../../../generator \ No newline at end of file diff --git a/generator/build.yaml b/generator/build.yaml index 346aba44e..579256d47 100644 --- a/generator/build.yaml +++ b/generator/build.yaml @@ -1,18 +1,37 @@ +# See docs: +# https://pub.dev/packages/build_config +# https://github.com/dart-lang/build/blob/master/docs/build_yaml_format.md +# https://github.com/dart-lang/build/blob/master/docs/transforming_code.md + targets: $default: builders: - objectbox_generator|objectbox: + objectbox_generator|resolver: + enabled: true + objectbox_generator|generator: enabled: true builders: - objectbox: - target: ":objectbox_generator" + # Finds all classes annotated with @Entity annotation and creates intermediate files for the generator. + # It's executed multiple times, once per file + resolver: import: "package:objectbox_generator/objectbox_generator.dart" - builder_factories: ["objectboxModelFactory"] - build_extensions: {".dart": [".objectbox_model.g.part"]} + builder_factories: ["entityResolverFactory"] + # build_extensions: Required. A map from input extension to the list of output extensions that may be created + # for that input. This must match the merged buildExtensions maps from each Builder in builder_factories. + build_extensions: {'.dart': ['.objectbox.info']} auto_apply: dependents build_to: cache - applies_builders: ["source_gen|combining_builder"] - defaults: - generate_for: - exclude: ["example/**"] + + # Writes objectbox_model.dart and objectbox-model.json from the prepared .objectbox.info files found in the repo. + generator: + import: "package:objectbox_generator/objectbox_generator.dart" + builder_factories: ["codeGeneratorFactory"] + # build_extensions: Required. A map from input extension to the list of output extensions that may be created + # for that input. This must match the merged buildExtensions maps from each Builder in builder_factories. + build_extensions: + "$lib$": ["objectbox.g.dart"] + "$test": ["objectbox.g.dart"] + required_inputs: ['.objectbox.info'] + auto_apply: dependents + build_to: source diff --git a/generator/code-generation.md b/generator/code-generation.md new file mode 100644 index 000000000..c9706bbfb --- /dev/null +++ b/generator/code-generation.md @@ -0,0 +1,43 @@ +# ObjectBox Dart – Code generation + +To make it possible to read and write ObjectBox entity instances as easily as possible, wrapper code needs to be +generated. Such code is only generated for Dart classes which have been annotated to indicate that they represent +an ObjectBox entity (i.e. using `@Entity`). The code is generated by executing `pub run build_runner build`. + +See docs: +* https://github.com/dart-lang/build/blob/master/docs/writing_a_builder.md +* https://github.com/dart-lang/build/blob/master/docs/writing_an_aggregate_builder.md +* https://pub.dev/packages/build_config#configuring-builders-applied-to-your-package + +## Basics + +In order to set up code generation, a new package needs to be created exclusively for this task. +Here, it it called `objectbox_generator`. This package needs to contain a [`build.yaml`](build.yaml) as well as an entry +point for the builder, [`objectbox_generator.dart`](lib/objectbox_generator.dart) which defines code-generator factories +for two generators used in build.yaml. + +## Merging + +After all entities have been found in the project, they're compared to an existing model definition in +an objectbox-model.json and merged. Also note that in this step, IDs and UIDs are generated automatically for new +instances. UIDs are always random, IDs are assigned in ascending order. UIDs may never be reused, e.g. after a property +has been removed, and previously used UIDs of remove instances (entities, properties, indexes, ...) are stored in +the JSON file. + +## Testing + +For accomplishing actually automated testing capabilities for `objectbox_generator`, various wrapper classes are needed, as the `build` package is only designed to generate output _files_; yet, during testing, it is necessary to dump generated code to string variables, so they can be compared easily by Dart's `test` framework. + +The entry function for generator testing is the main function of [`generator_test.dart`](/generator/test/generator_test.dart). It makes sure that any existing file called `objectbox-model.json` is removed before every test, because we want a fresh start each time. + +### Helper classes + +The `build` package internally uses designated classes for reading from and writing to files or, to be more general, any kind of _assets_. In this case, we do not want to involve any kind of files as output and only very specific files as input, so it is necessary to create our own versions of the so-called `AssetReader` and `AssetWriter`. + +In [`helpers.dart`](/generator/test/helpers.dart), `_InMemoryAssetWriter` is supposed to receive a single output string and store it in memory. Eventually, the string it stores will be the output of [`EntityGenerator`](/generator/lib/src/entity_binding.dart#L15). + +On the other hand, `_SingleFileAssetReader` shall read a single input Dart source file from the [`test/cases`](/generator/test/cases) directory. Note that currently, test cases have the rather ugly file extension `.dart_testcase`, such as [`single_entity.dart_testcase`](/generator/test/cases/single_entity/single_entity.dart_testcase). This is a workaround, because otherwise, running `pub run build_runner build` in the repository's root directory would generate `.g.dart` files from _all_ `.dart` files in the repository. An option to exclude certain directories from `build_runner` is yet to be found. + +### Executing the tests + +Eventually, the function `runBuilder` [can be executed](/generator/test/helpers.dart#L62), which is part of the `build` package. It encapsulates everything related to generating the final output. Thus, after it is finished and in case generation was successful, the `_InMemoryAssetWriter` instance contains the generated code, which can then be compared against the expected code. diff --git a/generator/lib/objectbox_generator.dart b/generator/lib/objectbox_generator.dart index 1427324ba..71e5140ad 100644 --- a/generator/lib/objectbox_generator.dart +++ b/generator/lib/objectbox_generator.dart @@ -1,5 +1,7 @@ import "package:build/build.dart"; -import "package:source_gen/source_gen.dart"; -import "package:objectbox_generator/src/generator.dart"; +import "src/entity_resolver.dart"; +import "src/code_builder.dart"; -Builder objectboxModelFactory(BuilderOptions options) => SharedPartBuilder([EntityGenerator()], "objectbox_model"); +Builder entityResolverFactory(BuilderOptions options) => EntityResolver(); + +Builder codeGeneratorFactory(BuilderOptions options) => CodeBuilder(); diff --git a/generator/lib/src/code_builder.dart b/generator/lib/src/code_builder.dart new file mode 100644 index 000000000..06ca3069e --- /dev/null +++ b/generator/lib/src/code_builder.dart @@ -0,0 +1,146 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:convert'; +import 'package:build/build.dart'; +import 'package:glob/glob.dart'; +import 'package:path/path.dart' as path; +import 'package:objectbox/objectbox.dart'; +import 'package:dart_style/dart_style.dart'; +import 'entity_resolver.dart'; +import 'code_chunks.dart'; + +/// CodeBuilder collects all ".objectbox.info" files created by EntityResolver and generates objectbox-model.json and +/// objectbox_model.dart +class CodeBuilder extends Builder { + static final jsonFile = 'objectbox-model.json'; + static final codeFile = 'objectbox.g.dart'; + + @override + final buildExtensions = {r'$lib$': _outputs, r'$test$': _outputs}; + + // we can't write `jsonFile` as part of the output because we want it persisted, not removed before each generation + static final _outputs = [codeFile]; + + String dir(BuildStep buildStep) => path.dirname(buildStep.inputId.path); + + @override + FutureOr build(BuildStep buildStep) async { + // build() will be called only twice, once for the `lib` directory and once for the `test` directory + + // map from file name to a "json" representation of entities + final files = Map>(); + final glob = Glob(path.join(dir(buildStep), '**' + EntityResolver.suffix)); + await for (final input in buildStep.findAssets(glob)) { + files[input.path] = json.decode(await buildStep.readAsString(input)); + } + if (files.isEmpty) return; + + // collect all entities and sort them by name + final entities = List(); + for (final entitiesList in files.values) { + for (final entityMap in entitiesList) { + entities.add(ModelEntity.fromMap(entityMap)); + } + } + entities.sort((a, b) => a.name.compareTo(b.name)); + + log.info("Package: ${buildStep.inputId.package}"); + log.info("Found ${entities.length} entities in: ${files.keys}"); + + // update the model JSON with the read entities + final model = await updateModel(entities, buildStep); + + // generate binding code + updateCode(model, files.keys.toList(growable: false), buildStep); + } + + Future updateModel(List entities, BuildStep buildStep) async { + // load an existing model or initialize a new one + ModelInfo model; + final jsonId = AssetId(buildStep.inputId.package, path.join(dir(buildStep), jsonFile)); + if (await buildStep.canRead(jsonId)) { + log.info("Using model: ${jsonId.path}"); + model = ModelInfo.fromMap(json.decode(await buildStep.readAsString(jsonId))); + } else { + log.warning("Creating model: ${jsonId.path}"); + model = ModelInfo.createDefault(); + } + + // merge existing model and annotated model that was just read, then write new final model to file + merge(model, entities); + + // write model info + // Can't use output, it's removed before each build, though writing to FS is explicitly forbidden by package:build. + // await buildStep.writeAsString(jsonId, JsonEncoder.withIndent(" ").convert(model.toMap())); + await File(jsonId.path).writeAsString(JsonEncoder.withIndent(" ").convert(model.toMap())); + + return model; + } + + void updateCode(ModelInfo model, List infoFiles, BuildStep buildStep) async { + // transform "/lib/path/entity.objectbox.info" to "path/entity.dart" + final imports = infoFiles + .map((file) => file.replaceFirst(EntityResolver.suffix, ".dart").replaceFirst(dir(buildStep) + "/", "")) + .toList(); + + var code = CodeChunks.objectboxDart(model, imports); + code = DartFormatter().format(code); + + final codeId = AssetId(buildStep.inputId.package, path.join(dir(buildStep), codeFile)); + log.info("Generating code: ${codeId.path}"); + await buildStep.writeAsString(codeId, code); + } + + void merge(ModelInfo model, List entities) { + // update existing and add new, while collecting all entity IDs at the end + final currentEntityIds = Map(); + entities.forEach((entity) { + final id = mergeEntity(model, entity); + currentEntityIds[id.id] = true; + }); + + // remove ("retire") missing entities + model.entities.where((entity) => !currentEntityIds.containsKey(entity.id.id)).forEach((entity) { + log.warning("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) { + ModelProperty propInModel = entity.findSameProperty(prop); + if (propInModel == null) { + log.info("Found new property ${entity.name}.${prop.name}"); + entity.addProperty(prop); + } else { + propInModel.type = prop.type; + propInModel.flags = prop.flags; + } + } + + IdUid mergeEntity(ModelInfo modelInfo, ModelEntity entity) { + // "readEntity" only contains the entity info directly read from the annotations and Dart source (i.e. with missing ID, lastPropertyId etc.) + // "entityInModel" is the entity from the model with all correct id/uid, lastPropertyId etc. + ModelEntity entityInModel = modelInfo.findSameEntity(entity); + + 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 + final createdEntity = modelInfo.addEntity(entity); + return createdEntity.id; + } + + // 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 + entityInModel.properties.where((p) => entity.findSameProperty(p) == null).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; + } +} diff --git a/generator/lib/src/code_chunks.dart b/generator/lib/src/code_chunks.dart index 1607cc603..636f11635 100644 --- a/generator/lib/src/code_chunks.dart +++ b/generator/lib/src/code_chunks.dart @@ -4,41 +4,50 @@ import "package:objectbox/src/bindings/constants.dart" show OBXPropertyType; import "package:source_gen/source_gen.dart" show InvalidGenerationSourceError; class CodeChunks { - // TODO ModelInfo, once per DB - static String modelInfoLoader() => """ + static String objectboxDart(ModelInfo model, List imports) => """ + // GENERATED CODE - DO NOT MODIFY BY HAND + + import 'package:objectbox/objectbox.dart'; + export 'package:objectbox/objectbox.dart'; // so that callers only have to import this file + import '${imports.join("';\n import '")}'; + + ModelDefinition getObjectBoxModel() { + final model = ModelInfo.fromMap(${JsonEncoder().convert(model.toMap(forCodeGen: true))}); + + final bindings = Map(); + ${model.entities.map((entity) => "bindings[${entity.name}] = ${entityBinding(entity)};").join("\n")} + + return ModelDefinition(model, bindings); + } + + ${model.entities.map((entity) => queryConditionClasses(entity)).join("\n")} """; - static String instanceBuildersReaders(ModelEntity readEntity) { - String name = readEntity.name; + static String entityBinding(ModelEntity entity) { + String name = entity.name; return """ - ModelEntity _${name}_OBXModelGetter() { - return ModelEntity.fromMap(${JsonEncoder().convert(readEntity.toMap())}); - } - - $name _${name}_OBXBuilder(Map members) { + EntityDefinition<${name}>( + model: model.findEntityByUid(${entity.id.uid}), + reader: ($name inst) => { + ${entity.properties.map((p) => "\"${p.name}\": inst.${p.name}").join(",\n")} + }, + writer: (Map members) { $name r = $name(); - ${readEntity.properties.map((p) => "r.${p.name} = members[\"${p.name}\"];").join()} + ${entity.properties.map((p) => "r.${p.name} = members[\"${p.name}\"];").join()} return r; } - - Map _${name}_OBXReader($name inst) { - Map r = {}; - ${readEntity.properties.map((p) => "r[\"${p.name}\"] = inst.${p.name};").join()} - return r; - } - - const ${name}_OBXDefs = EntityDefinition<${name}>(_${name}_OBXModelGetter, _${name}_OBXReader, _${name}_OBXBuilder); + ) """; } - static String _queryConditionBuilder(ModelEntity readEntity) { + static String _queryConditionBuilder(ModelEntity entity) { final ret = []; - for (var f in readEntity.properties) { - final name = f.name; + for (var prop in entity.properties) { + final name = prop.name; // see OBXPropertyType String fieldType; - switch (f.type) { + switch (prop.type) { case OBXPropertyType.Bool: fieldType = "Boolean"; break; @@ -64,21 +73,21 @@ class CodeChunks { case OBXPropertyType.Long: continue integer; default: - throw InvalidGenerationSourceError("Unsupported property type (${f.type}): ${readEntity.name}.${name}"); + throw InvalidGenerationSourceError("Unsupported property type (${prop.type}): ${entity.name}.${name}"); } ret.add(""" - static final ${name} = Query${fieldType}Property(entityId:${readEntity.id.id}, propertyId:${f.id.id}, obxType:${f.type}); + static final ${name} = Query${fieldType}Property(entityId:${entity.id.id}, propertyId:${prop.id.id}, obxType:${prop.type}); """); } return ret.join(); } - static String queryConditionClasses(ModelEntity readEntity) { + static String queryConditionClasses(ModelEntity entity) { // TODO add entity.id check to throw an error Box if the wrong entity.property is used return """ - class ${readEntity.name}_ { - ${_queryConditionBuilder(readEntity)} + class ${entity.name}_ { + ${_queryConditionBuilder(entity)} }"""; } } diff --git a/generator/lib/src/entity_resolver.dart b/generator/lib/src/entity_resolver.dart new file mode 100644 index 000000000..1dec3299e --- /dev/null +++ b/generator/lib/src/entity_resolver.dart @@ -0,0 +1,123 @@ +import "dart:async"; +import "dart:convert"; +import "package:analyzer/dart/element/element.dart"; +import 'package:build/build.dart'; +import "package:source_gen/source_gen.dart"; +import "package:objectbox/objectbox.dart" as obx; +import "package:objectbox/src/bindings/constants.dart"; +import "package:objectbox/src/modelinfo/index.dart"; + +/// EntityResolver finds all classes with an @Entity annotation and generates ".objectbox.info" files in build cache. +/// It's using some tools from source_gen but defining its custom builder because source_gen expects only dart code. +class EntityResolver extends Builder { + static const suffix = '.objectbox.info'; + @override + final buildExtensions = { + '.dart': [suffix] + }; + + final _annotationChecker = const TypeChecker.fromRuntime(obx.Entity); + final _propertyChecker = const TypeChecker.fromRuntime(obx.Property); + final _idChecker = const TypeChecker.fromRuntime(obx.Id); + + @override + FutureOr build(BuildStep buildStep) async { + final resolver = buildStep.resolver; + if (!await resolver.isLibrary(buildStep.inputId)) return; + final libReader = LibraryReader(await buildStep.inputLibrary); + + // generate for all entities + final entities = List>(); + for (var annotatedEl in libReader.annotatedWith(_annotationChecker)) { + entities.add(generateForAnnotatedElement(annotatedEl.element, annotatedEl.annotation).toMap()); + } + + if (entities.isEmpty) return; + + final json = JsonEncoder().convert(entities); + await buildStep.writeAsString(buildStep.inputId.changeExtension(suffix), json); + } + + ModelEntity generateForAnnotatedElement(Element elementBare, ConstantReader annotation) { + if (elementBare is! ClassElement) { + throw InvalidGenerationSourceError("in target ${elementBare.name}: annotated element isn't a class"); + } + var element = elementBare as ClassElement; + + // process basic entity (note that allModels.createEntity is not used, as the entity will be merged) + ModelEntity readEntity = ModelEntity(IdUid.empty(), null, element.name, [], null); + var entityUid = annotation.read("uid"); + if (entityUid != null && !entityUid.isNull) readEntity.id.uid = entityUid.intValue; + + log.info("entity ${readEntity.name}(${readEntity.id})"); + + // read all suitable annotated properties + bool hasIdProperty = false; + for (var f in element.fields) { + int fieldType, flags = 0; + int propUid; + + if (_idChecker.hasAnnotationOfExact(f)) { + if (hasIdProperty) { + throw InvalidGenerationSourceError( + "in target ${elementBare.name}: has more than one properties annotated with @Id"); + } + if (f.type.toString() != "int") { + throw InvalidGenerationSourceError( + "in target ${elementBare.name}: field with @Id property has type '${f.type.toString()}', but it must be 'int'"); + } + + hasIdProperty = true; + + fieldType = OBXPropertyType.Long; + flags |= OBXPropertyFlag.ID; + + final _idAnnotation = _idChecker.firstAnnotationOfExact(f); + propUid = _idAnnotation.getField('uid').toIntValue(); + } else if (_propertyChecker.hasAnnotationOfExact(f)) { + final _propertyAnnotation = _propertyChecker.firstAnnotationOfExact(f); + propUid = _propertyAnnotation.getField('uid').toIntValue(); + fieldType = _propertyAnnotation.getField('type').toIntValue(); + flags = _propertyAnnotation.getField('flag').toIntValue() ?? 0; + } + + if (fieldType == null) { + var fieldTypeStr = f.type.toString(); + + if (fieldTypeStr == "int") { + // dart: 8 bytes + // ob: 8 bytes + fieldType = OBXPropertyType.Long; + } else if (fieldTypeStr == "String") { + fieldType = OBXPropertyType.String; + } else if (fieldTypeStr == "bool") { + // dart: 1 byte + // ob: 1 byte + fieldType = OBXPropertyType.Bool; + } else if (fieldTypeStr == "double") { + // dart: 8 bytes + // ob: 8 bytes + fieldType = OBXPropertyType.Double; + } else { + log.warning( + " skipping property '${f.name}' in entity '${element.name}', as it has the unsupported type '$fieldTypeStr'"); + continue; + } + } + + // create property (do not use readEntity.createProperty in order to avoid generating new ids) + ModelProperty prop = ModelProperty(IdUid.empty(), f.name, fieldType, flags, readEntity); + if (propUid != null) prop.id.uid = propUid; + readEntity.properties.add(prop); + + log.info(" property ${prop.name}(${prop.id}) type:${prop.type} flags:${prop.flags}"); + } + + // some checks on the entity's integrity + if (!hasIdProperty) { + throw InvalidGenerationSourceError("in target ${elementBare.name}: has no properties annotated with @Id"); + } + + return readEntity; + } +} diff --git a/generator/lib/src/generator.dart b/generator/lib/src/generator.dart deleted file mode 100644 index 8a3d0a87c..000000000 --- a/generator/lib/src/generator.dart +++ /dev/null @@ -1,146 +0,0 @@ -import "dart:async"; -import "dart:convert"; -import "dart:io"; -import "package:analyzer/dart/element/element.dart"; -import 'package:build/build.dart'; -import "package:build/src/builder/build_step.dart"; -import "package:source_gen/source_gen.dart"; -import "package:objectbox/objectbox.dart" as obx; -import "package:objectbox/src/bindings/constants.dart"; -import "package:objectbox/src/modelinfo/index.dart"; -import "code_chunks.dart"; -import "merge.dart"; - -class EntityGenerator extends GeneratorForAnnotation { - static const ALL_MODELS_JSON = "objectbox-model.json"; - - // each .g.dart file needs to get a header with functions to load the ALL_MODELS_JSON file exactly once. Store the input .dart file ids this has already been done for here - List entityHeaderDone = []; - - Future _loadModelInfo() async { - if ((await FileSystemEntity.type(ALL_MODELS_JSON)) == FileSystemEntityType.notFound) { - return ModelInfo.createDefault(); - } - return ModelInfo.fromMap(json.decode(await (File(ALL_MODELS_JSON).readAsString()))); - } - - final _propertyChecker = const TypeChecker.fromRuntime(obx.Property); - final _idChecker = const TypeChecker.fromRuntime(obx.Id); - - @override - Future generateForAnnotatedElement( - Element elementBare, ConstantReader annotation, BuildStep buildStep) async { - try { - if (elementBare is! ClassElement) { - throw InvalidGenerationSourceError("in target ${elementBare.name}: annotated element isn't a class"); - } - var element = elementBare as ClassElement; - - // load existing model from JSON file if possible - String inputFileId = buildStep.inputId.toString(); - ModelInfo allModels = await _loadModelInfo(); - - // optionally add header for loading the .g.json file - var ret = ""; - if (!entityHeaderDone.contains(inputFileId)) { - ret += CodeChunks.modelInfoLoader(); - entityHeaderDone.add(inputFileId); - } - - // process basic entity (note that allModels.createEntity is not used, as the entity will be merged) - ModelEntity readEntity = ModelEntity(IdUid.empty(), null, element.name, [], allModels); - var entityUid = annotation.read("uid"); - if (entityUid != null && !entityUid.isNull) readEntity.id.uid = entityUid.intValue; - - // read all suitable annotated properties - bool hasIdProperty = false; - for (var f in element.fields) { - int fieldType, flags = 0; - int propUid; - - if (_idChecker.hasAnnotationOfExact(f)) { - if (hasIdProperty) { - throw InvalidGenerationSourceError( - "in target ${elementBare.name}: has more than one properties annotated with @Id"); - } - if (f.type.toString() != "int") { - throw InvalidGenerationSourceError( - "in target ${elementBare.name}: field with @Id property has type '${f.type.toString()}', but it must be 'int'"); - } - - hasIdProperty = true; - - fieldType = OBXPropertyType.Long; - flags |= OBXPropertyFlag.ID; - - final _idAnnotation = _idChecker.firstAnnotationOfExact(f); - propUid = _idAnnotation.getField('uid').toIntValue(); - } else if (_propertyChecker.hasAnnotationOfExact(f)) { - final _propertyAnnotation = _propertyChecker.firstAnnotationOfExact(f); - propUid = _propertyAnnotation.getField('uid').toIntValue(); - fieldType = _propertyAnnotation.getField('type').toIntValue(); - flags = _propertyAnnotation.getField('flag').toIntValue() ?? 0; - - log.info( - "annotated property found on ${f.name} with parameters: propUid(${propUid}) fieldType(${fieldType}) flags(${flags})"); - } else { - log.info( - "property found on ${f.name} with parameters: propUid(${propUid}) fieldType(${fieldType}) flags(${flags})"); - } - - if (fieldType == null) { - var fieldTypeStr = f.type.toString(); - - if (fieldTypeStr == "int") { - // dart: 8 bytes - // ob: 8 bytes - fieldType = OBXPropertyType.Long; - } else if (fieldTypeStr == "String") { - fieldType = OBXPropertyType.String; - } else if (fieldTypeStr == "bool") { - // dart: 1 byte - // ob: 1 byte - fieldType = OBXPropertyType.Bool; - } else if (fieldTypeStr == "double") { - // dart: 8 bytes - // ob: 8 bytes - fieldType = OBXPropertyType.Double; - } else { - log.warning( - "skipping field '${f.name}' in entity '${element.name}', as it has the unsupported type '$fieldTypeStr'"); - continue; - } - } - - // create property (do not use readEntity.createProperty in order to avoid generating new ids) - ModelProperty prop = ModelProperty(IdUid.empty(), f.name, fieldType, flags, readEntity); - if (propUid != null) prop.id.uid = propUid; - readEntity.properties.add(prop); - } - - // some checks on the entity's integrity - if (!hasIdProperty) { - throw InvalidGenerationSourceError("in target ${elementBare.name}: has no properties annotated with @Id"); - } - - // merge existing model and annotated model that was just read, then write new final model to file - mergeEntity(allModels, readEntity); - final modelJson = JsonEncoder.withIndent(" ").convert(allModels.toMap()); - await File(ALL_MODELS_JSON).writeAsString(modelJson); - - readEntity = allModels.findEntityByName(element.name); - if (readEntity == null) return ret; - - // main code for instance builders and readers - ret += CodeChunks.instanceBuildersReaders(readEntity); - - // for building queries - ret += CodeChunks.queryConditionClasses(readEntity); - - return ret; - } catch (e, s) { - log.warning(s); - rethrow; - } - } -} diff --git a/generator/lib/src/merge.dart b/generator/lib/src/merge.dart deleted file mode 100644 index aeda9078f..000000000 --- a/generator/lib/src/merge.dart +++ /dev/null @@ -1,30 +0,0 @@ -import "package:objectbox/src/modelinfo/index.dart"; - -void _mergeProperty(ModelEntity entity, ModelProperty prop) { - ModelProperty propInModel = entity.findSameProperty(prop); - if (propInModel == null) { - entity.createCopiedProperty(prop); - } else { - propInModel.type = prop.type; - propInModel.flags = prop.flags; - } -} - -void mergeEntity(ModelInfo modelInfo, ModelEntity readEntity) { - // "readEntity" only contains the entity info directly read from the annotations and Dart source (i.e. with missing ID, lastPropertyId etc.) - // "entityInModel" is the entity from the model with all correct id/uid, lastPropertyId etc. - ModelEntity entityInModel = modelInfo.findSameEntity(readEntity); - - if (entityInModel == null) { - // 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 - modelInfo.createCopiedEntity(readEntity); - } else { - // here, the entity was found already and entityInModel and readEntity might differ, i.e. conflicts need to be resolved, so merge all properties first - readEntity.properties.forEach((p) => _mergeProperty(entityInModel, p)); - - // them remove all properties not present anymore in readEntity - entityInModel.properties - .where((p) => readEntity.findSameProperty(p) == null) - .forEach((p) => entityInModel.removeProperty(p)); - } -} diff --git a/generator/test/helpers.dart b/generator/test/helpers.dart index 5d0ccceee..1a787f507 100644 --- a/generator/test/helpers.dart +++ b/generator/test/helpers.dart @@ -62,7 +62,7 @@ Future> _buildGeneratorOutput(String caseName) async { var reader = _SingleFileAssetReader(); Resolvers resolvers = AnalyzerResolvers(); - await runBuilder(objectboxModelFactory(BuilderOptions.empty), entities, reader, writer, resolvers); + await runBuilder(entityBindingBuilder(BuilderOptions.empty), entities, reader, writer, resolvers); return writer.output; } @@ -85,7 +85,7 @@ void testGeneratorOutput(String caseName, bool updateExpected) { Map built = await _buildGeneratorOutput(caseName); built.forEach((assetId, generatedCode) async { - final expectedPath = assetId.path.replaceAll(".objectbox_model.g.part", ".g.dart_expected"); + final expectedPath = assetId.path.replaceAll(".objectbox_entity.g.part", ".g.dart_expected"); checkExpectedContents(expectedPath, generatedCode, updateExpected); }); diff --git a/lib/integration_test.dart b/lib/integration_test.dart index 620a5b47b..03ff23407 100644 --- a/lib/integration_test.dart +++ b/lib/integration_test.dart @@ -28,7 +28,7 @@ class IntegrationTest { modelInfo.lastEntityId = entity.id; modelInfo.validate(); - final model = Model(modelInfo.entities); + final model = Model(modelInfo); checkObx(bindings.obx_model_free(model.ptr)); } } diff --git a/lib/src/box.dart b/lib/src/box.dart index eda7fa581..040362089 100644 --- a/lib/src/box.dart +++ b/lib/src/box.dart @@ -27,7 +27,7 @@ class Box { Box(this._store) { EntityDefinition entityDefs = _store.entityDef(); - _modelEntity = entityDefs.getModel(); + _modelEntity = entityDefs.model; _entityReader = entityDefs.reader; _fbManager = OBXFlatbuffersManager(_modelEntity, entityDefs.writer); diff --git a/lib/src/model.dart b/lib/src/model.dart index a0b710e0b..039e6d230 100644 --- a/lib/src/model.dart +++ b/lib/src/model.dart @@ -12,19 +12,14 @@ class Model { get ptr => _cModel; - Model(List modelEntities) { + Model(ModelInfo model) { _cModel = checkObxPtr(bindings.obx_model(), "failed to create model"); try { - // transform classes into model descriptions and loop through them - modelEntities.forEach(addEntity); + model.entities.forEach(addEntity); // set last entity id - // TODO read last entity ID from the model - if (modelEntities.isNotEmpty) { - ModelEntity lastEntity = modelEntities[modelEntities.length - 1]; - bindings.obx_model_last_entity_id(_cModel, lastEntity.id.id, lastEntity.id.uid); - } + bindings.obx_model_last_entity_id(_cModel, model.lastEntityId.id, model.lastEntityId.uid); } catch (e) { bindings.obx_model_free(_cModel); _cModel = null; diff --git a/lib/src/modelinfo/entity_definition.dart b/lib/src/modelinfo/entity_definition.dart index bf48212e5..758ad95ea 100644 --- a/lib/src/modelinfo/entity_definition.dart +++ b/lib/src/modelinfo/entity_definition.dart @@ -1,15 +1,15 @@ -// Used by the generated code as a container for model loading callables import 'package:objectbox/src/modelinfo/modelentity.dart'; typedef ObjectReader = Map Function(T object); typedef ObjectWriter = T Function(Map properties); +/// Used by the generated code as a container for model loading callables class EntityDefinition { - final ModelEntity Function() getModel; + final ModelEntity model; final ObjectReader reader; final ObjectWriter writer; - const EntityDefinition(this.getModel, this.reader, this.writer); + const EntityDefinition({this.model, this.reader, this.writer}); Type type() { return T; diff --git a/lib/src/modelinfo/iduid.dart b/lib/src/modelinfo/iduid.dart index 17f4a20f3..ae409ddba 100644 --- a/lib/src/modelinfo/iduid.dart +++ b/lib/src/modelinfo/iduid.dart @@ -1,3 +1,8 @@ +/// IdUid represents a compound of an ID, which is locally unique, i.e. inside an entity, and a UID, which is globally +/// unique, i.e. for the entire model. It is serialized as two numerical values concatenated using a colon (`:`). +/// See the documentation for more information on +/// * [IDs](https://docs.objectbox.io/advanced/meta-model-ids-and-uids#ids) +/// * [UIDs](https://docs.objectbox.io/advanced/meta-model-ids-and-uids#uids) class IdUid { int _id, _uid; diff --git a/lib/src/modelinfo/index.dart b/lib/src/modelinfo/index.dart index 277fb0f03..55cdc26b4 100644 --- a/lib/src/modelinfo/index.dart +++ b/lib/src/modelinfo/index.dart @@ -1,5 +1,6 @@ export "modelentity.dart"; export "iduid.dart"; export "entity_definition.dart"; +export "model_definition.dart"; export "modelinfo.dart"; export "modelproperty.dart"; diff --git a/lib/src/modelinfo/model_definition.dart b/lib/src/modelinfo/model_definition.dart new file mode 100644 index 000000000..793469e63 --- /dev/null +++ b/lib/src/modelinfo/model_definition.dart @@ -0,0 +1,9 @@ +import 'package:objectbox/src/modelinfo/modelinfo.dart'; +import 'package:objectbox/src/modelinfo/entity_definition.dart'; + +class ModelDefinition { + final ModelInfo model; + final Map bindings; + + const ModelDefinition(this.model, this.bindings); +} diff --git a/lib/src/modelinfo/modelentity.dart b/lib/src/modelinfo/modelentity.dart index 8c9b9a5a7..bff436d52 100644 --- a/lib/src/modelinfo/modelentity.dart +++ b/lib/src/modelinfo/modelentity.dart @@ -4,6 +4,8 @@ import "modelinfo.dart"; import "modelproperty.dart"; import "package:objectbox/src/bindings/constants.dart"; +/// ModelEntity describes an entity of a model and consists of instances of `ModelProperty` as well as an other entity +/// information: id, name and last property id. class ModelEntity { IdUid id, lastPropertyId; String name; @@ -111,30 +113,21 @@ class ModelEntity { return property; } - ModelProperty createCopiedProperty(ModelProperty prop) { + ModelProperty addProperty(ModelProperty prop) { ModelProperty ret = createProperty(prop.name, prop.id.uid); ret.type = prop.type; ret.flags = prop.flags; return ret; } - void _recalculateLastPropertyId() { - // assign id/uid of property with largest id to lastPropertyId - lastPropertyId = null; - properties.forEach((p) { - if (lastPropertyId == null || p.id.id > lastPropertyId.id) lastPropertyId = p.id; - }); - } - void removeProperty(ModelProperty prop) { - if (prop == null) return; + if (prop == null) throw Exception("prop == null"); ModelProperty foundProp = findSameProperty(prop); if (foundProp == null) { throw Exception("cannot remove property '${prop.name}' with id ${prop.id.toString()}: not found"); } properties = properties.where((p) => p != foundProp).toList(); model.retiredPropertyUids.add(prop.id.uid); - _recalculateLastPropertyId(); } bool containsUid(int searched) { diff --git a/lib/src/modelinfo/modelinfo.dart b/lib/src/modelinfo/modelinfo.dart index e8f996bd5..bd05d45d4 100644 --- a/lib/src/modelinfo/modelinfo.dart +++ b/lib/src/modelinfo/modelinfo.dart @@ -7,6 +7,10 @@ import "iduid.dart"; const _minModelVersion = 5; const _maxModelVersion = 5; +/// In order to represent the model stored in `objectbox-model.json` in Dart, several classes have been introduced. +/// Conceptually, these classes are comparable to how models are handled in ObjectBox Java and ObjectBox Go; eventually, +/// ObjectBox Dart models will be fully compatible to them. This is also why for explanations on most concepts related +/// to ObjectBox models, you can refer to the [existing documentation](https://docs.objectbox.io/advanced). class ModelInfo { static const notes = [ "KEEP THIS FILE! Check it into a version control system (VCS) like git.", @@ -55,10 +59,10 @@ class ModelInfo { lastSequenceId = IdUid.fromString(data["lastSequenceId"]); modelVersion = data["modelVersion"]; modelVersionParserMinimum = data["modelVersionParserMinimum"]; - retiredEntityUids = data["retiredEntityUids"].map((x) => x as int).toList(); - retiredIndexUids = data["retiredIndexUids"].map((x) => x as int).toList(); - retiredPropertyUids = data["retiredPropertyUids"].map((x) => x as int).toList(); - retiredRelationUids = data["retiredRelationUids"].map((x) => x as int).toList(); + retiredEntityUids = List.from(data["retiredEntityUids"] ?? []); + retiredIndexUids = List.from(data["retiredIndexUids"] ?? []); + retiredPropertyUids = List.from(data["retiredPropertyUids"] ?? []); + retiredRelationUids = List.from(data["retiredRelationUids"] ?? []); version = data["version"]; validate(); } @@ -104,23 +108,27 @@ class ModelInfo { } } - Map toMap() { + Map toMap({bool forCodeGen = false}) { Map ret = {}; - ret["_note1"] = notes[0]; - ret["_note2"] = notes[1]; - ret["_note3"] = notes[2]; + if (!forCodeGen) { + ret["_note1"] = notes[0]; + ret["_note2"] = notes[1]; + ret["_note3"] = notes[2]; + } ret["entities"] = entities.map((p) => p.toMap()).toList(); ret["lastEntityId"] = lastEntityId.toString(); ret["lastIndexId"] = lastIndexId.toString(); ret["lastRelationId"] = lastRelationId.toString(); ret["lastSequenceId"] = lastSequenceId.toString(); ret["modelVersion"] = modelVersion; - ret["modelVersionParserMinimum"] = modelVersionParserMinimum; - ret["retiredEntityUids"] = retiredEntityUids; - ret["retiredIndexUids"] = retiredIndexUids; - ret["retiredPropertyUids"] = retiredPropertyUids; - ret["retiredRelationUids"] = retiredRelationUids; - ret["version"] = version; + if (!forCodeGen) { + ret["modelVersionParserMinimum"] = modelVersionParserMinimum; + ret["retiredEntityUids"] = retiredEntityUids; + ret["retiredIndexUids"] = retiredIndexUids; + ret["retiredPropertyUids"] = retiredPropertyUids; + ret["retiredRelationUids"] = retiredRelationUids; + ret["version"] = version; + } return ret; } @@ -143,9 +151,9 @@ class ModelInfo { return ret; } - ModelEntity createCopiedEntity(ModelEntity other) { + ModelEntity addEntity(ModelEntity other) { ModelEntity ret = createEntity(other.name, other.id.uid); - other.properties.forEach((p) => ret.createCopiedProperty(p)); + other.properties.forEach((p) => ret.addProperty(p)); return ret; } @@ -161,6 +169,18 @@ class ModelInfo { return entity; } + void removeEntity(ModelEntity entity) { + if (entity == null) throw Exception("entity == null"); + + final foundEntity = findSameEntity(entity); + if (foundEntity == null) { + throw Exception("cannot remove entity '${entity.name}' with id ${entity.id.toString()}: not found"); + } + entities = entities.where((p) => p != foundEntity).toList(); + retiredEntityUids.add(entity.id.uid); + entity.properties.forEach((prop) => retiredPropertyUids.add(prop.id.uid)); + } + int generateUid() { var rng = Random(); for (int i = 0; i < 1000; ++i) { diff --git a/lib/src/modelinfo/modelproperty.dart b/lib/src/modelinfo/modelproperty.dart index dc35b9a35..2486b2201 100644 --- a/lib/src/modelinfo/modelproperty.dart +++ b/lib/src/modelinfo/modelproperty.dart @@ -1,6 +1,7 @@ import "modelentity.dart"; import "iduid.dart"; +/// ModelProperty describes a single property of an entity, i.e. its id, name, type and flags. class ModelProperty { IdUid id; String name; diff --git a/lib/src/store.dart b/lib/src/store.dart index c17aa7708..5e9229591 100644 --- a/lib/src/store.dart +++ b/lib/src/store.dart @@ -1,13 +1,10 @@ import "dart:ffi"; - +import "package:ffi/ffi.dart"; import "bindings/bindings.dart"; import "bindings/helpers.dart"; import "modelinfo/index.dart"; - import "model.dart"; -import "package:ffi/ffi.dart"; - enum TxMode { Read, Write, @@ -17,11 +14,10 @@ enum TxMode { /// specific type. class Store { Pointer _cStore; - Map _entityDefinitions = {}; + final ModelDefinition defs; - Store(List defs, {String directory, int maxDBSizeInKB, int fileMode, int maxReaders}) { - defs.forEach((d) => _entityDefinitions[d.type()] = d); - var model = Model(defs.map((d) => d.getModel()).toList()); + Store(this.defs, {String directory, int maxDBSizeInKB, int fileMode, int maxReaders}) { + var model = Model(defs.model); var opt = bindings.obx_opt(); checkObxPtr(opt, "failed to create store options"); @@ -56,7 +52,7 @@ class Store { } EntityDefinition entityDef() { - return _entityDefinitions[T]; + return defs.bindings[T]; } /// Executes a given function inside a transaction. diff --git a/test/entity.dart b/test/entity.dart index cf37cbc5e..b88c1f7a2 100644 --- a/test/entity.dart +++ b/test/entity.dart @@ -1,7 +1,5 @@ import "package:objectbox/objectbox.dart"; -part 'entity.g.dart'; - /// A dummy annotation to verify the code is generated properly even with annotations unknown to ObjectBox generator. class TestingUnknownAnnotation { const TestingUnknownAnnotation(); diff --git a/test/entity2.dart b/test/entity2.dart new file mode 100644 index 000000000..22da3b1bb --- /dev/null +++ b/test/entity2.dart @@ -0,0 +1,8 @@ +import "package:objectbox/objectbox.dart"; + +// Testing a model for entities in multiple files is generated properly +@Entity() +class TestEntity2 { + @Id() + int id; +} diff --git a/objectbox-model.json b/test/objectbox-model.json similarity index 62% rename from objectbox-model.json rename to test/objectbox-model.json index d59840e4f..658cfbaa9 100644 --- a/objectbox-model.json +++ b/test/objectbox-model.json @@ -62,78 +62,45 @@ ] }, { - "id": "2:2679953000475642792", - "lastPropertyId": "11:2900967122054840440", - "name": "TestEntityProperty", + "id": "3:3569200127393812728", + "lastPropertyId": "1:2429256362396080523", + "name": "TestEntity2", "properties": [ { - "id": "1:118121232448890483", + "id": "1:2429256362396080523", "name": "id", "type": 6, "flags": 1 - }, - { - "id": "2:8913396295192638938", - "name": "tBool", - "type": 1 - }, - { - "id": "3:7399685737415196420", - "name": "tLong", - "type": 6 - }, - { - "id": "4:5773372525238577573", - "name": "tDouble", - "type": 8 - }, - { - "id": "5:1652533532846680965", - "name": "tString", - "type": 9 - }, - { - "id": "7:2032187304851276864", - "name": "tShort", - "type": 3 - }, - { - "id": "8:263290714597678490", - "name": "tChar", - "type": 4 - }, - { - "id": "9:2191004797635629014", - "name": "tInt", - "type": 5 - }, - { - "id": "10:6174275661850707374", - "name": "tFloat", - "type": 7 - }, - { - "id": "11:2900967122054840440", - "name": "tByte", - "type": 2 } ] } ], - "lastEntityId": "2:2679953000475642792", + "lastEntityId": "3:3569200127393812728", "lastIndexId": "0:0", "lastRelationId": "0:0", "lastSequenceId": "0:0", "modelVersion": 5, "modelVersionParserMinimum": 5, - "retiredEntityUids": [], + "retiredEntityUids": [ + 2679953000475642792 + ], "retiredIndexUids": [], "retiredPropertyUids": [ 5849170199816666167, 6119953496456269132, 1043358010384181585, 8886007565639578393, - 1874578842943403707 + 1874578842943403707, + 118121232448890483, + 8913396295192638938, + 7399685737415196420, + 5773372525238577573, + 1652533532846680965, + 2032187304851276864, + 263290714597678490, + 2191004797635629014, + 6174275661850707374, + 2900967122054840440 ], "retiredRelationUids": [], "version": 1 diff --git a/test/query_test.dart b/test/query_test.dart index 305f43807..39d80478f 100644 --- a/test/query_test.dart +++ b/test/query_test.dart @@ -1,7 +1,7 @@ import "package:test/test.dart"; -import "package:objectbox/objectbox.dart"; import "entity.dart"; import 'test_env.dart'; +import 'objectbox.g.dart'; void main() { TestEnv env; diff --git a/test/test_env.dart b/test/test_env.dart index b9e7d8a98..d1ad4799f 100644 --- a/test/test_env.dart +++ b/test/test_env.dart @@ -1,6 +1,6 @@ import "dart:io"; -import "package:objectbox/objectbox.dart"; import "entity.dart"; +import 'objectbox.g.dart'; class TestEnv { final Directory dir; @@ -10,7 +10,7 @@ class TestEnv { TestEnv(String name) : dir = Directory("testdata-" + name) { if (dir.existsSync()) dir.deleteSync(recursive: true); - store = Store([TestEntity_OBXDefs], directory: dir.path); + store = Store(getObjectBoxModel(), directory: dir.path); box = Box(store); }