diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a2fe6ce61..eb991bed0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,7 +20,7 @@ jobs: - name: Check scores shell: bash run: | - if [[ "90" -gt "${{ steps.analysis.outputs.total }}" ]]; then - echo "Score is less then 90, please check the analysis report and resolve the issues" + if [[ "100" -gt "${{ steps.analysis.outputs.total }}" ]]; then + echo "Score is less then 100, please check the analysis report and resolve the issues" exit 1 fi \ No newline at end of file diff --git a/generator/integration-tests/basics/1.dart b/generator/integration-tests/basics/1.dart index 6e5f23d74..34ffc3272 100644 --- a/generator/integration-tests/basics/1.dart +++ b/generator/integration-tests/basics/1.dart @@ -4,7 +4,6 @@ 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 env; diff --git a/generator/integration-tests/indexes/1.dart b/generator/integration-tests/indexes/1.dart index fe84b5a31..4047a5dbb 100644 --- a/generator/integration-tests/indexes/1.dart +++ b/generator/integration-tests/indexes/1.dart @@ -6,7 +6,6 @@ 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 env; diff --git a/generator/lib/src/code_chunks.dart b/generator/lib/src/code_chunks.dart index 2279880df..9ce1f601d 100644 --- a/generator/lib/src/code_chunks.dart +++ b/generator/lib/src/code_chunks.dart @@ -1,6 +1,6 @@ import 'dart:convert'; + import 'package:objectbox/src/modelinfo/index.dart'; -import 'package:objectbox/src/bindings/bindings.dart'; import 'package:source_gen/source_gen.dart' show InvalidGenerationSourceError; class CodeChunks { diff --git a/generator/lib/src/entity_resolver.dart b/generator/lib/src/entity_resolver.dart index 1366cc773..938940064 100644 --- a/generator/lib/src/entity_resolver.dart +++ b/generator/lib/src/entity_resolver.dart @@ -7,8 +7,6 @@ import 'package:analyzer/dart/element/type.dart'; import 'package:build/build.dart'; import 'package:objectbox/objectbox.dart'; import 'package:objectbox/internal.dart'; -import 'package:objectbox/src/bindings/bindings.dart'; -import 'package:objectbox/src/bindings/helpers.dart'; import 'package:objectbox/src/modelinfo/index.dart'; import 'package:source_gen/source_gen.dart'; diff --git a/objectbox/analysis_options.yaml b/objectbox/analysis_options.yaml index 9a1bc5b36..a11934aa9 100644 --- a/objectbox/analysis_options.yaml +++ b/objectbox/analysis_options.yaml @@ -27,7 +27,8 @@ analyzer: - example/** - generator/** - benchmark/** - - lib/src/bindings/objectbox-c.dart + - lib/src/modelinfo/enums.dart + - lib/src/native/bindings/objectbox-c.dart - lib/flatbuffers/** # Not really our code - test/objectbox.g.dart # TODO remove exception after #168 is implemented strong-mode: diff --git a/objectbox/lib/integration_test.dart b/objectbox/lib/integration_test.dart index f54ced1fd..6361f8fbc 100644 --- a/objectbox/lib/integration_test.dart +++ b/objectbox/lib/integration_test.dart @@ -1,7 +1,7 @@ library integration_test; -import './src/bindings/bindings.dart'; -import './src/bindings/helpers.dart'; +import './src/native/bindings/bindings.dart' as native; +import './src/native/bindings/helpers.dart' as native; import 'internal.dart'; // ignore_for_file: public_member_api_docs @@ -31,6 +31,6 @@ class IntegrationTest { modelInfo.validate(); final model = Model(modelInfo); - checkObx(C.model_free(model.ptr)); + native.checkObx(native.C.model_free(model.ptr)); } } diff --git a/objectbox/lib/internal.dart b/objectbox/lib/internal.dart index 6542143aa..8fc0d8318 100644 --- a/objectbox/lib/internal.dart +++ b/objectbox/lib/internal.dart @@ -4,7 +4,7 @@ library objectbox_internal; export 'src/model.dart'; export 'src/modelinfo/index.dart'; -export 'src/query/query.dart' +export 'src/query.dart' // don't export the same things as objectbox.dart to avoid docs conflicts hide Query, diff --git a/objectbox/lib/objectbox.dart b/objectbox/lib/objectbox.dart index 75aeee343..8801bcb61 100644 --- a/objectbox/lib/objectbox.dart +++ b/objectbox/lib/objectbox.dart @@ -9,7 +9,7 @@ export 'src/annotations.dart'; export 'src/box.dart' show Box; export 'src/common.dart'; export 'src/observable.dart'; -export 'src/query/query.dart' +export 'src/query.dart' show Query, QueryBuilder, diff --git a/objectbox/lib/src/box.dart b/objectbox/lib/src/box.dart index 49072063e..05ac67c16 100644 --- a/objectbox/lib/src/box.dart +++ b/objectbox/lib/src/box.dart @@ -1,325 +1 @@ -import 'dart:ffi'; - -import 'package:ffi/ffi.dart' show allocate, free; - -import 'bindings/bindings.dart'; -import 'bindings/flatbuffers.dart'; -import 'bindings/helpers.dart'; -import 'bindings/structs.dart'; -import 'modelinfo/index.dart'; -import 'query/query.dart'; -import 'relations/info.dart'; -import 'relations/to_many.dart'; -import 'relations/to_one.dart'; -import 'store.dart'; -import 'transaction.dart'; - -/// Box put (write) mode. -enum PutMode { - /// Insert (if given object's ID is zero) or update an existing object. - put, - - /// Insert a new object. - insert, - - /// Update an existing object, fails if the given ID doesn't exist. - update, -} - -/// A Box instance gives you access to objects of a particular type. -/// You get Box instances via [Store.box()] or [Box(Store)]. -/// -/// For example, if you have User and Order entities, you need two Box objects -/// to interact with each: -/// ```dart -/// Box userBox = store.box(); -/// Box orderBox = store.box(); -/// ``` -class Box { - final Store _store; - final Pointer _cBox; - final EntityDefinition _entity; - final bool _hasToOneRelations; - final bool _hasToManyRelations; - final _builder = BuilderWithCBuffer(); - - /// Create a box for an Entity. - factory Box(Store store) => store.box(); - - Box._(this._store, this._entity) - : _hasToOneRelations = _entity.model.properties - .any((ModelProperty prop) => prop.isRelation), - _hasToManyRelations = _entity.model.relations.isNotEmpty || - _entity.model.backlinks.isNotEmpty, - _cBox = C.box(_store.ptr, _entity.model.id.id) { - checkObxPtr(_cBox, 'failed to create box'); - } - - bool get _hasRelations => _hasToOneRelations || _hasToManyRelations; - - static int _getOBXPutMode(PutMode mode) { - switch (mode) { - case PutMode.put: - return OBXPutMode.PUT; - case PutMode.insert: - return OBXPutMode.INSERT; - case PutMode.update: - return OBXPutMode.UPDATE; - } - throw Exception('Invalid put mode ' + mode.toString()); - } - - /// Puts the given Object in the box (aka persisting it). - /// - /// If this is a new object (its ID property is 0), a new ID will be assigned - /// to the object (and returned). - /// - /// If the object with given was already in the box, it will be overwritten. - /// - /// Performance note: consider [putMany] to put several objects at once. - int put(T object, {PutMode mode = PutMode.put}) { - if (_hasRelations) { - final tx = Transaction(_store, TxMode.write); - try { - final id = _put(object, mode, tx); - tx.markSuccessful(); - return id; - } finally { - tx.close(); - } - } else { - return _put(object, mode, null); - } - } - - int _put(T object, PutMode mode, Transaction /*?*/ tx) { - if (_hasRelations) { - if (tx == null) { - throw Exception( - 'Invalid state: can only use _put() on an entity with relations when executing from inside a write transaction.'); - } - if (_hasToOneRelations) _putToOneRelFields(object, mode, tx); - } - var id = _entity.objectToFB(object, _builder.fbb); - final newId = C.box_put_object4(_cBox, _builder.bufPtr.cast(), - _builder.fbb.size, _getOBXPutMode(mode)); - id = _handlePutObjectResult(object, id, newId); - if (_hasToManyRelations) _putToManyRelFields(object, mode, tx); - _builder.resetIfLarge(); - return id; - } - - /// Puts the given [objects] into this Box in a single transaction. - /// - /// Returns a list of all IDs of the inserted Objects. - List putMany(List objects, {PutMode mode = PutMode.put}) { - if (objects.isEmpty) return []; - - final putIds = List.filled(objects.length, 0); - - final tx = Transaction(_store, TxMode.write); - try { - if (_hasToOneRelations) { - objects.forEach((object) => _putToOneRelFields(object, mode, tx)); - } - - final cursor = tx.cursor(_entity); - final cMode = _getOBXPutMode(mode); - for (var i = 0; i < objects.length; i++) { - final object = objects[i]; - _builder.fbb.reset(); - final id = _entity.objectToFB(object, _builder.fbb); - final newId = C.cursor_put_object4( - cursor.ptr, _builder.bufPtr.cast(), _builder.fbb.size, cMode); - putIds[i] = _handlePutObjectResult(object, id, newId); - } - - if (_hasToManyRelations) { - objects.forEach((object) => _putToManyRelFields(object, mode, tx)); - } - _builder.resetIfLarge(); - tx.markSuccessful(); - } finally { - tx.close(); - } - - return putIds; - } - - // Checks if native obx_*_put_object() was successful (result is a valid ID). - // Sets the given ID on the object if previous ID was zero (new object). - int _handlePutObjectResult(T object, int prevId, int result) { - if (result == 0) throw latestNativeError(dartMsg: 'object put failed'); - if (prevId == 0) _entity.setId(object, result); - return result; - } - - /// Retrieves the stored object with the ID [id] from this box's database. - /// Returns null if an object with the given ID doesn't exist. - T /*?*/ get(int id) { - final tx = Transaction(_store, TxMode.read); - try { - return tx.cursor(_entity).get(id); - } finally { - tx.close(); - } - } - - /// Returns a list of [ids.length] Objects of type T, each corresponding to - /// the location of its ID in [ids]. Non-existent IDs become null. - /// - /// Pass growableResult: true for the resulting list to be growable. - List getMany(List ids, {bool growableResult = false}) { - final result = List.filled(ids.length, null, growable: growableResult); - if (ids.isEmpty) return result; - final tx = Transaction(_store, TxMode.read); - try { - final cursor = tx.cursor(_entity); - for (var i = 0; i < ids.length; i++) { - final object = cursor.get(ids[i]); - if (object != null) result[i] = object; - } - return result; - } finally { - tx.close(); - } - } - - /// Returns all stored objects in this Box. - List getAll() { - final tx = Transaction(_store, TxMode.read); - try { - final cursor = tx.cursor(_entity); - final result = []; - var code = C.cursor_first(cursor.ptr, cursor.dataPtrPtr, cursor.sizePtr); - while (code != OBX_NOT_FOUND) { - checkObx(code); - result.add(_entity.objectFromFB(_store, cursor.readData)); - code = C.cursor_next(cursor.ptr, cursor.dataPtrPtr, cursor.sizePtr); - } - return result; - } finally { - tx.close(); - } - } - - /// Returns a builder to create queries for Object matching supplied criteria. - QueryBuilder query([Condition /*?*/ qc]) => - QueryBuilder(_store, _entity, qc); - - /// Returns the count of all stored Objects in this box. - /// If [limit] is not zero, stops counting at the given limit. - int count({int limit = 0}) { - final count = allocate(); - try { - checkObx(C.box_count(_cBox, limit, count)); - return count.value; - } finally { - free(count); - } - } - - /// Returns true if no objects are in this box. - bool isEmpty() { - final isEmpty = allocate(); - try { - checkObx(C.box_is_empty(_cBox, isEmpty)); - return isEmpty.value == 1; - } finally { - free(isEmpty); - } - } - - /// Returns true if this box contains an Object with the ID [id]. - bool contains(int id) { - final contains = allocate(); - try { - checkObx(C.box_contains(_cBox, id, contains)); - return contains.value == 1; - } finally { - free(contains); - } - } - - /// Returns true if this box contains objects with all of the given [ids]. - bool containsMany(List ids) { - final contains = allocate(); - try { - return executeWithIdArray(ids, (ptr) { - checkObx(C.box_contains_many(_cBox, ptr, contains)); - return contains.value == 1; - }); - } finally { - free(contains); - } - } - - /// Removes (deletes) the Object with the given [id]. Returns true if the - /// object was present (and thus removed), otherwise returns false. - bool remove(int id) { - final err = C.box_remove(_cBox, id); - if (err == OBX_NOT_FOUND) return false; - checkObx(err); // throws on other errors - return true; - } - - /// Removes (deletes) by ID, returning a list of IDs of all removed Objects. - int removeMany(List ids) { - final countRemoved = allocate(); - try { - return executeWithIdArray(ids, (ptr) { - checkObx(C.box_remove_many(_cBox, ptr, countRemoved)); - return countRemoved.value; - }); - } finally { - free(countRemoved); - } - } - - /// Removes (deletes) ALL Objects in a single transaction. - int removeAll() { - final removedItems = allocate(); - try { - checkObx(C.box_remove_all(_cBox, removedItems)); - return removedItems.value; - } finally { - free(removedItems); - } - } - - /// The low-level pointer to this box. - Pointer get ptr => _cBox; - - void _putToOneRelFields(T object, PutMode mode, Transaction tx) { - _entity.toOneRelations(object).forEach((ToOne rel) { - if (!rel.hasValue) return; - rel.attach(_store); - // put new objects - if (rel.targetId == 0) { - rel.targetId = - InternalToOneAccess.targetBox(rel)._put(rel.target, mode, tx); - } - }); - } - - void _putToManyRelFields(T object, PutMode mode, Transaction tx) { - _entity.toManyRelations(object).forEach((RelInfo info, ToMany rel) { - if (InternalToManyAccess.hasPendingDbChanges(rel)) { - InternalToManyAccess.setRelInfo(rel, _store, info, this); - rel.applyToDb(mode: mode, tx: tx); - } - }); - } -} - -/// Internal only. -// TODO enable annotation once meta:1.3.0 is out -// @internal -class InternalBoxAccess { - /// Create a box in the store for the given entity. - static Box create(Store store, EntityDefinition entity) => - Box._(store, entity); - - /// Close the box, freeing resources. - static void close(Box box) => box._builder.clear(); -} +export 'native/box.dart' if (dart.library.html) 'web/box.dart'; diff --git a/objectbox/lib/src/common.dart b/objectbox/lib/src/common.dart index 29d5825dd..fd3674034 100644 --- a/objectbox/lib/src/common.dart +++ b/objectbox/lib/src/common.dart @@ -1,9 +1,3 @@ -import 'dart:ffi'; - -import 'package:ffi/ffi.dart' show allocate, free; - -import 'bindings/bindings.dart'; - // TODO use pub_semver? /// Wrapper for a semantic version information. class Version { @@ -23,22 +17,6 @@ class Version { String toString() => '$major.$minor.$patch'; } -/// Returns the underlying ObjectBox-C library version. -Version nativeLibraryVersion() { - var majorPtr = allocate(), - minorPtr = allocate(), - patchPtr = allocate(); - - try { - C.version(majorPtr, minorPtr, patchPtr); - return Version(majorPtr.value, minorPtr.value, patchPtr.value); - } finally { - free(majorPtr); - free(minorPtr); - free(patchPtr); - } -} - /// ObjectBox native exception wrapper. class ObjectBoxException implements Exception { /// Dart message related to this native error. diff --git a/objectbox/lib/src/model.dart b/objectbox/lib/src/model.dart index 1f73ef95d..41d940590 100644 --- a/objectbox/lib/src/model.dart +++ b/objectbox/lib/src/model.dart @@ -1,112 +1 @@ -import 'dart:ffi'; - -import 'package:ffi/ffi.dart'; - -import 'bindings/bindings.dart'; -import 'bindings/helpers.dart'; -import 'common.dart'; -import 'modelinfo/index.dart'; - -// ignore_for_file: public_member_api_docs - -class Model { - final Pointer _cModel; - - Pointer get ptr => _cModel; - - Model(ModelInfo model) - : _cModel = checkObxPtr(C.model(), 'failed to create model') { - try { - model.entities.forEach(addEntity); - - // set last entity id - C.model_last_entity_id( - _cModel, model.lastEntityId.id, model.lastEntityId.uid); - - // set last relation id - if (model.lastRelationId != null) { - C.model_last_relation_id( - _cModel, model.lastRelationId.id, model.lastRelationId.uid); - } - - // set last index id - if (model.lastIndexId != null) { - C.model_last_index_id( - _cModel, model.lastIndexId.id, model.lastIndexId.uid); - } - } catch (e) { - C.model_free(_cModel); - rethrow; - } - } - - void _check(int errorCode) { - if (errorCode == OBX_SUCCESS) return; - - throw ObjectBoxException( - dartMsg: 'Model building failed', - nativeCode: C.model_error_code(_cModel), - nativeMsg: cString(C.model_error_message(_cModel))); - } - - void addEntity(ModelEntity entity) { - // start entity - var name = Utf8.toUtf8(entity.name).cast(); - try { - _check(C.model_entity(_cModel, name, entity.id.id, entity.id.uid)); - } finally { - free(name); - } - - if (entity.flags != 0) { - // TODO remove try-catch after upgrading to objectbox-c v0.11 where obx_model_entity_flags() exists. - try { - _check(C.model_entity_flags(_cModel, entity.flags)); - } on ArgumentError { - // flags not supported; don't do anything until objectbox-c v0.11 - // this should only be used from our test code - } - } - - // add all properties - entity.properties.forEach(addProperty); - - // set last property id - _check(C.model_entity_last_property_id( - _cModel, entity.lastPropertyId.id, entity.lastPropertyId.uid)); - - entity.relations.forEach(addRelation); - } - - void addProperty(ModelProperty prop) { - var name = Utf8.toUtf8(prop.name).cast(); - try { - _check( - C.model_property(_cModel, name, prop.type, prop.id.id, prop.id.uid)); - - if (prop.isRelation) { - var relTarget = Utf8.toUtf8(prop.relationTarget /*!*/).cast(); - try { - _check(C.model_property_relation(_cModel, relTarget, - prop.indexId /*!*/ .id, prop.indexId /*!*/ .uid)); - } finally { - free(relTarget); - } - } else if (prop.indexId != null) { - _check(C.model_property_index_id( - _cModel, prop.indexId.id, prop.indexId.uid)); - } - } finally { - free(name); - } - - if (prop.flags != 0) { - _check(C.model_property_flags(_cModel, prop.flags)); - } - } - - void addRelation(ModelRelation rel) { - _check(C.model_relation( - _cModel, rel.id.id, rel.id.uid, rel.targetId.id, rel.targetId.uid)); - } -} +export 'native/model.dart' if (dart.library.html) 'web/model.dart'; diff --git a/objectbox/lib/src/modelinfo/enums.dart b/objectbox/lib/src/modelinfo/enums.dart new file mode 100644 index 000000000..97510e0fa --- /dev/null +++ b/objectbox/lib/src/modelinfo/enums.dart @@ -0,0 +1,160 @@ +// Note: the enums in this file are copied from native/bindings/objectbox-c.dart +// to avoid package:ffi import which would break compatibility with web. + +import '../annotations.dart'; + +/// Maps [OBXPropertyType] to its string representation (name). +String obxPropertyTypeToString(int type) { + switch (type) { + case OBXPropertyType.Bool: + return 'bool'; + case OBXPropertyType.Byte: + return 'byte'; + case OBXPropertyType.Short: + return 'short'; + case OBXPropertyType.Char: + return 'char'; + case OBXPropertyType.Int: + return 'int'; + case OBXPropertyType.Long: + return 'long'; + case OBXPropertyType.Float: + return 'float'; + case OBXPropertyType.Double: + return 'double'; + case OBXPropertyType.String: + return 'string'; + case OBXPropertyType.Date: + return 'date'; + case OBXPropertyType.Relation: + return 'relation'; + case OBXPropertyType.DateNano: + return 'dateNano'; + case OBXPropertyType.ByteVector: + return 'byteVector'; + case OBXPropertyType.StringVector: + return 'stringVector'; + } + + throw Exception('Invalid OBXPropertyType: $type'); +} + +int propertyTypeToOBXPropertyType(PropertyType type) { + switch (type) { + case PropertyType.byte: + return OBXPropertyType.Byte; + case PropertyType.short: + return OBXPropertyType.Short; + case PropertyType.char: + return OBXPropertyType.Char; + case PropertyType.int: + return OBXPropertyType.Int; + case PropertyType.float: + return OBXPropertyType.Float; + case PropertyType.date: + return OBXPropertyType.Date; + case PropertyType.dateNano: + return OBXPropertyType.DateNano; + case PropertyType.byteVector: + return OBXPropertyType.ByteVector; + } + throw Exception('Invalid PropertyType: $type'); +} + +/// /// Bit-flags defining the behavior of entities. +/// /// Note: Numbers indicate the bit position +abstract class OBXEntityFlags { + /// /// Enable "data synchronization" for this entity type: objects will be synced with other stores over the network. + /// /// It's possible to have local-only (non-synced) types and synced types in the same store (schema/data model). + static const int SYNC_ENABLED = 2; +} + +/// /// Bit-flags defining the behavior of properties. +/// /// Note: Numbers indicate the bit position +abstract class OBXPropertyFlags { + /// /// 64 bit long property (internally unsigned) representing the ID of the entity. + /// /// May be combined with: NON_PRIMITIVE_TYPE, ID_MONOTONIC_SEQUENCE, ID_SELF_ASSIGNABLE. + static const int ID = 1; + + /// /// On languages like Java, a non-primitive type is used (aka wrapper types, allowing null) + static const int NON_PRIMITIVE_TYPE = 2; + + /// /// Unused yet + static const int NOT_NULL = 4; + static const int INDEXED = 8; + + /// /// Unused yet + static const int RESERVED = 16; + + /// /// Unique index + static const int UNIQUE = 32; + + /// /// Unused yet: Use a persisted sequence to enforce ID to rise monotonic (no ID reuse) + static const int ID_MONOTONIC_SEQUENCE = 64; + + /// /// Allow IDs to be assigned by the developer + static const int ID_SELF_ASSIGNABLE = 128; + + /// /// Unused yet + static const int INDEX_PARTIAL_SKIP_NULL = 256; + + /// /// Used by References for 1) back-references and 2) to clear references to deleted objects (required for ID reuse) + static const int INDEX_PARTIAL_SKIP_ZERO = 512; + + /// /// Virtual properties may not have a dedicated field in their entity class, e.g. target IDs of to-one relations + static const int VIRTUAL = 1024; + + /// /// Index uses a 32 bit hash instead of the value + /// /// 32 bits is shorter on disk, runs well on 32 bit systems, and should be OK even with a few collisions + static const int INDEX_HASH = 2048; + + /// /// Index uses a 64 bit hash instead of the value + /// /// recommended mostly for 64 bit machines with values longer >200 bytes; small values are faster with a 32 bit hash + static const int INDEX_HASH64 = 4096; + + /// /// The actual type of the variable is unsigned (used in combination with numeric OBXPropertyType_*). + /// /// While our default are signed ints, queries & indexes need do know signing info. + /// /// Note: Don't combine with ID (IDs are always unsigned internally). + static const int UNSIGNED = 8192; + + /// /// By defining an ID companion property, a special ID encoding scheme is activated involving this property. + /// /// + /// /// For Time Series IDs, a companion property of type Date or DateNano represents the exact timestamp. + static const int ID_COMPANION = 16384; +} + +abstract class OBXPropertyType { + /// ///< 1 byte + static const int Bool = 1; + + /// ///< 1 byte + static const int Byte = 2; + + /// ///< 2 bytes + static const int Short = 3; + + /// ///< 1 byte + static const int Char = 4; + + /// ///< 4 bytes + static const int Int = 5; + + /// ///< 8 bytes + static const int Long = 6; + + /// ///< 4 bytes + static const int Float = 7; + + /// ///< 8 bytes + static const int Double = 8; + static const int String = 9; + + /// ///< Unix timestamp (milliseconds since 1970) in 8 bytes + static const int Date = 10; + static const int Relation = 11; + + /// ///< Unix timestamp (nanoseconds since 1970) in 8 bytes + static const int DateNano = 12; + static const int ByteVector = 23; + static const int StringVector = 30; +} diff --git a/objectbox/lib/src/modelinfo/index.dart b/objectbox/lib/src/modelinfo/index.dart index a7a5897d3..8a18272b8 100644 --- a/objectbox/lib/src/modelinfo/index.dart +++ b/objectbox/lib/src/modelinfo/index.dart @@ -1,4 +1,5 @@ export 'entity_definition.dart'; +export 'enums.dart'; export 'iduid.dart'; export 'model_definition.dart'; export 'modelbacklink.dart'; diff --git a/objectbox/lib/src/modelinfo/modelentity.dart b/objectbox/lib/src/modelinfo/modelentity.dart index 7c6ff8da1..f935ff1f5 100644 --- a/objectbox/lib/src/modelinfo/modelentity.dart +++ b/objectbox/lib/src/modelinfo/modelentity.dart @@ -1,4 +1,4 @@ -import '../bindings/bindings.dart'; +import 'enums.dart'; import 'iduid.dart'; import 'modelbacklink.dart'; import 'modelinfo.dart'; diff --git a/objectbox/lib/src/modelinfo/modelproperty.dart b/objectbox/lib/src/modelinfo/modelproperty.dart index 7cb52085a..7f4905774 100644 --- a/objectbox/lib/src/modelinfo/modelproperty.dart +++ b/objectbox/lib/src/modelinfo/modelproperty.dart @@ -1,5 +1,4 @@ -import '../bindings/bindings.dart'; -import '../bindings/helpers.dart'; +import 'enums.dart'; import 'iduid.dart'; import 'modelentity.dart'; diff --git a/objectbox/lib/src/bindings/bindings.dart b/objectbox/lib/src/native/bindings/bindings.dart similarity index 100% rename from objectbox/lib/src/bindings/bindings.dart rename to objectbox/lib/src/native/bindings/bindings.dart diff --git a/objectbox/lib/src/bindings/data_visitor.dart b/objectbox/lib/src/native/bindings/data_visitor.dart similarity index 98% rename from objectbox/lib/src/bindings/data_visitor.dart rename to objectbox/lib/src/native/bindings/data_visitor.dart index 79f39c4da..3866c9f47 100644 --- a/objectbox/lib/src/bindings/data_visitor.dart +++ b/objectbox/lib/src/native/bindings/data_visitor.dart @@ -2,7 +2,7 @@ import 'dart:ffi'; import 'package:ffi/ffi.dart' show allocate, free; -import '../modelinfo/entity_definition.dart'; +import '../../modelinfo/entity_definition.dart'; import '../store.dart'; import 'bindings.dart'; diff --git a/objectbox/lib/src/bindings/flatbuffers.dart b/objectbox/lib/src/native/bindings/flatbuffers.dart similarity index 98% rename from objectbox/lib/src/bindings/flatbuffers.dart rename to objectbox/lib/src/native/bindings/flatbuffers.dart index 658202aa3..3110abd11 100644 --- a/objectbox/lib/src/bindings/flatbuffers.dart +++ b/objectbox/lib/src/native/bindings/flatbuffers.dart @@ -4,7 +4,7 @@ import 'dart:typed_data'; import 'package:ffi/ffi.dart' as f; -import '../../flatbuffers/flat_buffers.dart' as fb; +import '../../../flatbuffers/flat_buffers.dart' as fb; // ignore_for_file: public_member_api_docs diff --git a/objectbox/lib/src/bindings/helpers.dart b/objectbox/lib/src/native/bindings/helpers.dart similarity index 58% rename from objectbox/lib/src/bindings/helpers.dart rename to objectbox/lib/src/native/bindings/helpers.dart index e89ec2740..06cda99c9 100644 --- a/objectbox/lib/src/bindings/helpers.dart +++ b/objectbox/lib/src/native/bindings/helpers.dart @@ -3,9 +3,8 @@ import 'dart:typed_data'; import 'package:ffi/ffi.dart'; -import '../annotations.dart'; -import '../common.dart'; -import '../modelinfo/entity_definition.dart'; +import '../../common.dart'; +import '../../modelinfo/entity_definition.dart'; import '../store.dart'; import 'bindings.dart'; @@ -56,63 +55,6 @@ String cString(Pointer charPtr) { return Utf8.fromUtf8(charPtr.cast()); } -String obxPropertyTypeToString(int type) { - switch (type) { - case OBXPropertyType.Bool: - return 'bool'; - case OBXPropertyType.Byte: - return 'byte'; - case OBXPropertyType.Short: - return 'short'; - case OBXPropertyType.Char: - return 'char'; - case OBXPropertyType.Int: - return 'int'; - case OBXPropertyType.Long: - return 'long'; - case OBXPropertyType.Float: - return 'float'; - case OBXPropertyType.Double: - return 'double'; - case OBXPropertyType.String: - return 'string'; - case OBXPropertyType.Date: - return 'date'; - case OBXPropertyType.Relation: - return 'relation'; - case OBXPropertyType.DateNano: - return 'dateNano'; - case OBXPropertyType.ByteVector: - return 'byteVector'; - case OBXPropertyType.StringVector: - return 'stringVector'; - } - - throw Exception('Invalid OBXPropertyType: $type'); -} - -int propertyTypeToOBXPropertyType(PropertyType type) { - switch (type) { - case PropertyType.byte: - return OBXPropertyType.Byte; - case PropertyType.short: - return OBXPropertyType.Short; - case PropertyType.char: - return OBXPropertyType.Char; - case PropertyType.int: - return OBXPropertyType.Int; - case PropertyType.float: - return OBXPropertyType.Float; - case PropertyType.date: - return OBXPropertyType.Date; - case PropertyType.dateNano: - return OBXPropertyType.DateNano; - case PropertyType.byteVector: - return OBXPropertyType.ByteVector; - } - throw Exception('Invalid PropertyType: $type'); -} - class CursorHelper { final EntityDefinition _entity; final Store _store; diff --git a/objectbox/lib/src/bindings/objectbox-c.dart b/objectbox/lib/src/native/bindings/objectbox-c.dart similarity index 100% rename from objectbox/lib/src/bindings/objectbox-c.dart rename to objectbox/lib/src/native/bindings/objectbox-c.dart diff --git a/objectbox/lib/src/bindings/objectbox.h b/objectbox/lib/src/native/bindings/objectbox.h similarity index 100% rename from objectbox/lib/src/bindings/objectbox.h rename to objectbox/lib/src/native/bindings/objectbox.h diff --git a/objectbox/lib/src/bindings/structs.dart b/objectbox/lib/src/native/bindings/structs.dart similarity index 99% rename from objectbox/lib/src/bindings/structs.dart rename to objectbox/lib/src/native/bindings/structs.dart index 653d63d6c..db18428b4 100644 --- a/objectbox/lib/src/bindings/structs.dart +++ b/objectbox/lib/src/native/bindings/structs.dart @@ -3,7 +3,7 @@ import 'dart:typed_data' show Uint8List; import 'package:ffi/ffi.dart' show allocate, free, Utf8; -import '../common.dart'; +import '../../common.dart'; import 'bindings.dart'; // ignore_for_file: public_member_api_docs diff --git a/objectbox/lib/src/native/box.dart b/objectbox/lib/src/native/box.dart new file mode 100644 index 000000000..4d9f8a3b8 --- /dev/null +++ b/objectbox/lib/src/native/box.dart @@ -0,0 +1,394 @@ +import 'dart:ffi'; + +import 'package:ffi/ffi.dart' show allocate, free; + +import '../modelinfo/index.dart'; +import '../relations/info.dart'; +import '../relations/to_many.dart'; +import '../relations/to_one.dart'; +import '../store.dart'; +import '../transaction.dart'; +import 'bindings/bindings.dart'; +import 'bindings/flatbuffers.dart'; +import 'bindings/helpers.dart'; +import 'bindings/structs.dart'; +import 'query/query.dart'; +import 'transaction.dart'; + +/// Box put (write) mode. +enum PutMode { + /// Insert (if given object's ID is zero) or update an existing object. + put, + + /// Insert a new object. + insert, + + /// Update an existing object, fails if the given ID doesn't exist. + update, +} + +/// A Box instance gives you access to objects of a particular type. +/// You get Box instances via [Store.box()] or [Box(Store)]. +/// +/// For example, if you have User and Order entities, you need two Box objects +/// to interact with each: +/// ```dart +/// Box userBox = store.box(); +/// Box orderBox = store.box(); +/// ``` +class Box { + final Store _store; + final Pointer _cBox; + final EntityDefinition _entity; + final bool _hasToOneRelations; + final bool _hasToManyRelations; + final _builder = BuilderWithCBuffer(); + + /// Create a box for an Entity. + factory Box(Store store) => store.box(); + + Box._(this._store, this._entity) + : _hasToOneRelations = _entity.model.properties + .any((ModelProperty prop) => prop.isRelation), + _hasToManyRelations = _entity.model.relations.isNotEmpty || + _entity.model.backlinks.isNotEmpty, + _cBox = C.box(_store.ptr, _entity.model.id.id) { + checkObxPtr(_cBox, 'failed to create box'); + } + + bool get _hasRelations => _hasToOneRelations || _hasToManyRelations; + + static int _getOBXPutMode(PutMode mode) { + switch (mode) { + case PutMode.put: + return OBXPutMode.PUT; + case PutMode.insert: + return OBXPutMode.INSERT; + case PutMode.update: + return OBXPutMode.UPDATE; + } + throw Exception('Invalid put mode ' + mode.toString()); + } + + /// Puts the given Object in the box (aka persisting it). + /// + /// If this is a new object (its ID property is 0), a new ID will be assigned + /// to the object (and returned). + /// + /// If the object with given was already in the box, it will be overwritten. + /// + /// Performance note: consider [putMany] to put several objects at once. + int put(T object, {PutMode mode = PutMode.put}) { + if (_hasRelations) { + final tx = Transaction(_store, TxMode.write); + try { + final id = _put(object, mode, tx); + tx.markSuccessful(); + return id; + } finally { + tx.close(); + } + } else { + return _put(object, mode, null); + } + } + + int _put(T object, PutMode mode, Transaction /*?*/ tx) { + if (_hasRelations) { + if (tx == null) { + throw Exception( + 'Invalid state: can only use _put() on an entity with relations when executing from inside a write transaction.'); + } + if (_hasToOneRelations) _putToOneRelFields(object, mode, tx); + } + var id = _entity.objectToFB(object, _builder.fbb); + final newId = C.box_put_object4(_cBox, _builder.bufPtr.cast(), + _builder.fbb.size, _getOBXPutMode(mode)); + id = _handlePutObjectResult(object, id, newId); + if (_hasToManyRelations) _putToManyRelFields(object, mode, tx); + _builder.resetIfLarge(); + return id; + } + + /// Puts the given [objects] into this Box in a single transaction. + /// + /// Returns a list of all IDs of the inserted Objects. + List putMany(List objects, {PutMode mode = PutMode.put}) { + if (objects.isEmpty) return []; + + final putIds = List.filled(objects.length, 0); + + final tx = Transaction(_store, TxMode.write); + try { + if (_hasToOneRelations) { + objects.forEach((object) => _putToOneRelFields(object, mode, tx)); + } + + final cursor = tx.cursor(_entity); + final cMode = _getOBXPutMode(mode); + for (var i = 0; i < objects.length; i++) { + final object = objects[i]; + _builder.fbb.reset(); + final id = _entity.objectToFB(object, _builder.fbb); + final newId = C.cursor_put_object4( + cursor.ptr, _builder.bufPtr.cast(), _builder.fbb.size, cMode); + putIds[i] = _handlePutObjectResult(object, id, newId); + } + + if (_hasToManyRelations) { + objects.forEach((object) => _putToManyRelFields(object, mode, tx)); + } + _builder.resetIfLarge(); + tx.markSuccessful(); + } finally { + tx.close(); + } + + return putIds; + } + + // Checks if native obx_*_put_object() was successful (result is a valid ID). + // Sets the given ID on the object if previous ID was zero (new object). + int _handlePutObjectResult(T object, int prevId, int result) { + if (result == 0) throw latestNativeError(dartMsg: 'object put failed'); + if (prevId == 0) _entity.setId(object, result); + return result; + } + + /// Retrieves the stored object with the ID [id] from this box's database. + /// Returns null if an object with the given ID doesn't exist. + T /*?*/ get(int id) { + final tx = Transaction(_store, TxMode.read); + try { + return tx.cursor(_entity).get(id); + } finally { + tx.close(); + } + } + + /// Returns a list of [ids.length] Objects of type T, each corresponding to + /// the location of its ID in [ids]. Non-existent IDs become null. + /// + /// Pass growableResult: true for the resulting list to be growable. + List getMany(List ids, {bool growableResult = false}) { + final result = List.filled(ids.length, null, growable: growableResult); + if (ids.isEmpty) return result; + final tx = Transaction(_store, TxMode.read); + try { + final cursor = tx.cursor(_entity); + for (var i = 0; i < ids.length; i++) { + final object = cursor.get(ids[i]); + if (object != null) result[i] = object; + } + return result; + } finally { + tx.close(); + } + } + + /// Returns all stored objects in this Box. + List getAll() { + final tx = Transaction(_store, TxMode.read); + try { + final cursor = tx.cursor(_entity); + final result = []; + var code = C.cursor_first(cursor.ptr, cursor.dataPtrPtr, cursor.sizePtr); + while (code != OBX_NOT_FOUND) { + checkObx(code); + result.add(_entity.objectFromFB(_store, cursor.readData)); + code = C.cursor_next(cursor.ptr, cursor.dataPtrPtr, cursor.sizePtr); + } + return result; + } finally { + tx.close(); + } + } + + /// Returns a builder to create queries for Object matching supplied criteria. + QueryBuilder query([Condition /*?*/ qc]) => + QueryBuilder(_store, _entity, qc); + + /// Returns the count of all stored Objects in this box. + /// If [limit] is not zero, stops counting at the given limit. + int count({int limit = 0}) { + final count = allocate(); + try { + checkObx(C.box_count(_cBox, limit, count)); + return count.value; + } finally { + free(count); + } + } + + /// Returns true if no objects are in this box. + bool isEmpty() { + final isEmpty = allocate(); + try { + checkObx(C.box_is_empty(_cBox, isEmpty)); + return isEmpty.value == 1; + } finally { + free(isEmpty); + } + } + + /// Returns true if this box contains an Object with the ID [id]. + bool contains(int id) { + final contains = allocate(); + try { + checkObx(C.box_contains(_cBox, id, contains)); + return contains.value == 1; + } finally { + free(contains); + } + } + + /// Returns true if this box contains objects with all of the given [ids]. + bool containsMany(List ids) { + final contains = allocate(); + try { + return executeWithIdArray(ids, (ptr) { + checkObx(C.box_contains_many(_cBox, ptr, contains)); + return contains.value == 1; + }); + } finally { + free(contains); + } + } + + /// Removes (deletes) the Object with the given [id]. Returns true if the + /// object was present (and thus removed), otherwise returns false. + bool remove(int id) { + final err = C.box_remove(_cBox, id); + if (err == OBX_NOT_FOUND) return false; + checkObx(err); // throws on other errors + return true; + } + + /// Removes (deletes) by ID, returning a list of IDs of all removed Objects. + int removeMany(List ids) { + final countRemoved = allocate(); + try { + return executeWithIdArray(ids, (ptr) { + checkObx(C.box_remove_many(_cBox, ptr, countRemoved)); + return countRemoved.value; + }); + } finally { + free(countRemoved); + } + } + + /// Removes (deletes) ALL Objects in a single transaction. + int removeAll() { + final removedItems = allocate(); + try { + checkObx(C.box_remove_all(_cBox, removedItems)); + return removedItems.value; + } finally { + free(removedItems); + } + } + + /// The low-level pointer to this box. + Pointer get ptr => _cBox; + + void _putToOneRelFields(T object, PutMode mode, Transaction tx) { + _entity.toOneRelations(object).forEach((ToOne rel) { + if (!rel.hasValue) return; + rel.attach(_store); + // put new objects + if (rel.targetId == 0) { + rel.targetId = + InternalToOneAccess.targetBox(rel)._put(rel.target, mode, tx); + } + }); + } + + void _putToManyRelFields(T object, PutMode mode, Transaction tx) { + _entity.toManyRelations(object).forEach((RelInfo info, ToMany rel) { + if (InternalToManyAccess.hasPendingDbChanges(rel)) { + InternalToManyAccess.setRelInfo(rel, _store, info, this); + rel.applyToDb(mode: mode, tx: tx); + } + }); + } +} + +/// Internal only. +// TODO enable annotation once meta:1.3.0 is out +// @internal +class InternalBoxAccess { + /// Create a box in the store for the given entity. + static Box create(Store store, EntityDefinition entity) => + Box._(store, entity); + + /// Close the box, freeing resources. + static void close(Box box) => box._builder.clear(); + + /// Put the object in a given transaction. + static int put( + Box box, EntityT object, PutMode mode, Transaction tx) => + box._put(object, mode, tx); + + /// Put a standalone relation. + static void relPut( + Box box, + int relationId, + int sourceId, + int targetId, + ) => + checkObx(C.box_rel_put(box.ptr, relationId, sourceId, targetId)); + + /// Remove a standalone relation entry between two objects. + static void relRemove( + Box box, + int relationId, + int sourceId, + int targetId, + ) => + checkObx(C.box_rel_remove(box.ptr, relationId, sourceId, targetId)); + + /// Read all objects in this Box related to the given object. + /// Similar to box.getMany() but loads the OBX_id_array and reads objects + /// in a single Transaction, ensuring consistency. And it's a little more + /// efficient for not unpacking the id array to a dart list. + static List getRelated(Box box, RelInfo rel) { + final tx = Transaction(box._store, TxMode.read); + try { + Pointer cIdsPtr; + switch (rel.type) { + case RelType.toMany: + cIdsPtr = C.box_rel_get_ids(box.ptr, rel.id, rel.objectId); + break; + case RelType.toOneBacklink: + cIdsPtr = C.box_get_backlink_ids(box.ptr, rel.id, rel.objectId); + break; + case RelType.toManyBacklink: + cIdsPtr = C.box_rel_get_backlink_ids(box.ptr, rel.id, rel.objectId); + break; + default: + throw UnimplementedError(); + } + checkObxPtr(cIdsPtr); + final result = []; + try { + final cIds = cIdsPtr.ref; + if (cIds.count > 0) { + final cursor = tx.cursor(box._entity); + for (var i = 0; i < cIds.count; i++) { + final code = C.cursor_get( + cursor.ptr, cIds.ids[i], cursor.dataPtrPtr, cursor.sizePtr); + if (code != OBX_NOT_FOUND) { + checkObx(code); + result.add(box._entity.objectFromFB(box._store, cursor.readData)); + } + } + } + } finally { + C.id_array_free(cIdsPtr); + } + return result; + } finally { + tx.close(); + } + } +} diff --git a/objectbox/lib/src/native/model.dart b/objectbox/lib/src/native/model.dart new file mode 100644 index 000000000..f23b515c3 --- /dev/null +++ b/objectbox/lib/src/native/model.dart @@ -0,0 +1,112 @@ +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; + +import '../common.dart'; +import '../modelinfo/index.dart'; +import 'bindings/bindings.dart'; +import 'bindings/helpers.dart'; + +// ignore_for_file: public_member_api_docs + +class Model { + final Pointer _cModel; + + Pointer get ptr => _cModel; + + Model(ModelInfo model) + : _cModel = checkObxPtr(C.model(), 'failed to create model') { + try { + model.entities.forEach(addEntity); + + // set last entity id + C.model_last_entity_id( + _cModel, model.lastEntityId.id, model.lastEntityId.uid); + + // set last relation id + if (model.lastRelationId != null) { + C.model_last_relation_id( + _cModel, model.lastRelationId.id, model.lastRelationId.uid); + } + + // set last index id + if (model.lastIndexId != null) { + C.model_last_index_id( + _cModel, model.lastIndexId.id, model.lastIndexId.uid); + } + } catch (e) { + C.model_free(_cModel); + rethrow; + } + } + + void _check(int errorCode) { + if (errorCode == OBX_SUCCESS) return; + + throw ObjectBoxException( + dartMsg: 'Model building failed', + nativeCode: C.model_error_code(_cModel), + nativeMsg: cString(C.model_error_message(_cModel))); + } + + void addEntity(ModelEntity entity) { + // start entity + var name = Utf8.toUtf8(entity.name).cast(); + try { + _check(C.model_entity(_cModel, name, entity.id.id, entity.id.uid)); + } finally { + free(name); + } + + if (entity.flags != 0) { + // TODO remove try-catch after upgrading to objectbox-c v0.11 where obx_model_entity_flags() exists. + try { + _check(C.model_entity_flags(_cModel, entity.flags)); + } on ArgumentError { + // flags not supported; don't do anything until objectbox-c v0.11 + // this should only be used from our test code + } + } + + // add all properties + entity.properties.forEach(addProperty); + + // set last property id + _check(C.model_entity_last_property_id( + _cModel, entity.lastPropertyId.id, entity.lastPropertyId.uid)); + + entity.relations.forEach(addRelation); + } + + void addProperty(ModelProperty prop) { + var name = Utf8.toUtf8(prop.name).cast(); + try { + _check( + C.model_property(_cModel, name, prop.type, prop.id.id, prop.id.uid)); + + if (prop.isRelation) { + var relTarget = Utf8.toUtf8(prop.relationTarget /*!*/).cast(); + try { + _check(C.model_property_relation(_cModel, relTarget, + prop.indexId /*!*/ .id, prop.indexId /*!*/ .uid)); + } finally { + free(relTarget); + } + } else if (prop.indexId != null) { + _check(C.model_property_index_id( + _cModel, prop.indexId.id, prop.indexId.uid)); + } + } finally { + free(name); + } + + if (prop.flags != 0) { + _check(C.model_property_flags(_cModel, prop.flags)); + } + } + + void addRelation(ModelRelation rel) { + _check(C.model_relation( + _cModel, rel.id.id, rel.id.uid, rel.targetId.id, rel.targetId.uid)); + } +} diff --git a/objectbox/lib/src/native/observable.dart b/objectbox/lib/src/native/observable.dart new file mode 100644 index 000000000..e927c8e63 --- /dev/null +++ b/objectbox/lib/src/native/observable.dart @@ -0,0 +1,102 @@ +import 'dart:async'; +import 'dart:ffi'; + +import '../util.dart'; +import 'bindings/bindings.dart'; +import 'query/query.dart'; +import 'store.dart'; + +// ignore_for_file: non_constant_identifier_names + +// dart callback signature +typedef _Any = void Function(Pointer, Pointer, int); + +class _Observable { + static final _anyObserver = >{}; + static final _any = >{}; + + // sync:true -> ObjectBoxException: 10001 TX is not active anymore: #101 + static final controller = StreamController.broadcast(); + + // The user_data is used to pass the store ptr address + // in case there is no consensus on the entity id between stores + static void _anyCallback( + Pointer user_data, Pointer mutated_ids, int mutated_count) { + final storeAddress = user_data.address; + // call schema's callback + final storeCallbacks = _any[storeAddress]; + if (storeCallbacks != null) { + for (var i = 0; i < mutated_count; i++) { + storeCallbacks[mutated_ids[i]] + ?.call(user_data, mutated_ids, mutated_count); + } + } + } + + static void subscribe(Store store) { + syncOrObserversExclusive.mark(store); + + final callback = Pointer.fromFunction(_anyCallback); + final storePtr = store.ptr; + _anyObserver[storePtr.address] = + C.observe(storePtr, callback, storePtr.cast()); + InternalStoreAccess.addCloseListener(store, _anyObserver[storePtr.address], + () { + unsubscribe(store); + }); + } + + // #53 ffi:Pointer finalizer + static void unsubscribe(Store store) { + final storeAddress = store.ptr.address; + if (!_anyObserver.containsKey(storeAddress)) { + return; + } + InternalStoreAccess.removeCloseListener(store, _anyObserver[storeAddress]); + C.observer_close(_anyObserver[storeAddress]); + _anyObserver.remove(storeAddress); + syncOrObserversExclusive.unmark(store); + } + + static bool isSubscribed(Store store) => + _Observable._anyObserver.containsKey(store.ptr.address); +} + +/// Streamable adds stream support to queries. The stream reruns the query +/// whenever there's a change in any of the objects in the queried Box +/// (regardless of the filter conditions). +extension Streamable on Query { + void _setup() { + if (!_Observable.isSubscribed(store)) { + _Observable.subscribe(store); + } + final storeAddress = store.ptr.address; + + _Observable._any[storeAddress] ??= {}; + _Observable._any[storeAddress] /*!*/ [entityId] ??= (u, _, __) { + // dummy value to trigger an event + _Observable.controller.add(entityId); + }; + } + + /// Create a stream, executing [Query.find()] whenever there's a change to any + /// of the objects in the queried Box. + Stream> findStream( + {@Deprecated('Use offset() instead') int offset = 0, + @Deprecated('Use limit() instead') int limit = 0}) { + _setup(); + return _Observable.controller.stream.where((e) => e == entityId).map((_) { + if (offset != 0) this.offset(offset); + if (limit != 0) this.limit(limit); + return find(); + }); + } + + /// Use this for Query Property + Stream> get stream { + _setup(); + return _Observable.controller.stream + .where((e) => e == entityId) + .map((_) => this); + } +} diff --git a/objectbox/lib/src/query/builder.dart b/objectbox/lib/src/native/query/builder.dart similarity index 100% rename from objectbox/lib/src/query/builder.dart rename to objectbox/lib/src/native/query/builder.dart diff --git a/objectbox/lib/src/query/property.dart b/objectbox/lib/src/native/query/property.dart similarity index 100% rename from objectbox/lib/src/query/property.dart rename to objectbox/lib/src/native/query/property.dart diff --git a/objectbox/lib/src/query/query.dart b/objectbox/lib/src/native/query/query.dart similarity index 99% rename from objectbox/lib/src/query/query.dart rename to objectbox/lib/src/native/query/query.dart index b8657d958..e722101a0 100644 --- a/objectbox/lib/src/query/query.dart +++ b/objectbox/lib/src/native/query/query.dart @@ -5,14 +5,14 @@ import 'dart:typed_data'; import 'package:ffi/ffi.dart' show allocate, free, Utf8; +import '../../common.dart'; +import '../../modelinfo/entity_definition.dart'; +import '../../store.dart'; +import '../../transaction.dart'; import '../bindings/bindings.dart'; import '../bindings/data_visitor.dart'; import '../bindings/helpers.dart'; import '../bindings/structs.dart'; -import '../common.dart'; -import '../modelinfo/entity_definition.dart'; -import '../store.dart'; -import '../transaction.dart'; part 'builder.dart'; part 'property.dart'; diff --git a/objectbox/lib/src/native/store.dart b/objectbox/lib/src/native/store.dart new file mode 100644 index 000000000..956ce078c --- /dev/null +++ b/objectbox/lib/src/native/store.dart @@ -0,0 +1,166 @@ +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; + +import '../common.dart'; +import '../modelinfo/index.dart'; +import '../transaction.dart'; +import '../util.dart'; +import 'bindings/bindings.dart'; +import 'bindings/helpers.dart'; +import 'box.dart'; +import 'model.dart'; +import 'sync.dart'; + +/// Represents an ObjectBox database and works together with [Box] to allow +/// getting and putting. +class Store { + /*late final*/ Pointer _cStore; + final _boxes = {}; + final ModelDefinition _defs; + + /// A list of observers of the Store.close() event. + final _onClose = {}; + + /// Creates a BoxStore using the model definition from the generated + /// `objectbox.g.dart` file. + /// + /// For example in a Flutter app: + /// ```dart + /// getApplicationDocumentsDirectory().then((dir) { + /// _store = Store(getObjectBoxModel(), directory: dir.path + "/objectbox"); + /// }); + /// ``` + /// + /// Or for a Dart app: + /// ```dart + /// var store = Store(getObjectBoxModel()); + /// ``` + /// + /// See our examples for more details. + Store(this._defs, + {String /*?*/ directory, + int /*?*/ maxDBSizeInKB, + int /*?*/ fileMode, + int /*?*/ maxReaders}) { + var model = Model(_defs.model); + + var opt = C.opt(); + checkObxPtr(opt, 'failed to create store options'); + + try { + checkObx(C.opt_model(opt, model.ptr)); + if (directory != null && directory.isNotEmpty) { + var cStr = Utf8.toUtf8(directory).cast(); + try { + checkObx(C.opt_directory(opt, cStr)); + } finally { + free(cStr); + } + } + if (maxDBSizeInKB != null && maxDBSizeInKB > 0) { + C.opt_max_db_size_in_kb(opt, maxDBSizeInKB); + } + if (fileMode != null && fileMode >= 0) { + C.opt_file_mode(opt, fileMode); + } + if (maxReaders != null && maxReaders > 0) { + C.opt_max_readers(opt, maxReaders); + } + } catch (e) { + C.opt_free(opt); + rethrow; + } + _cStore = C.store_open(opt); + + try { + checkObxPtr(_cStore, 'failed to create store'); + } on ObjectBoxException catch (e) { + // Recognize common problems when trying to open/create a database + // 10199 = OBX_ERROR_STORAGE_GENERAL + if (e.nativeCode == 10199 && + e.nativeMsg != null && + e.nativeMsg /*!*/ .contains('Dir does not exist')) { + // 13 = permissions denied, 30 = read-only filesystem + if (e.nativeMsg /*!*/ .endsWith(' (13)') || + e.nativeMsg /*!*/ .endsWith(' (30)')) { + final msg = e.nativeMsg /*!*/ + + ' - this usually indicates a problem with permissions; ' + "if you're using Flutter you may need to use " + 'getApplicationDocumentsDirectory() from the path_provider ' + 'package, see example/README.md'; + throw ObjectBoxException( + dartMsg: e.dartMsg, nativeCode: e.nativeCode, nativeMsg: msg); + } + } + rethrow; + } + } + + /// Closes this store. + /// + /// Don't try to call any other ObjectBox methods after the store is closed. + void close() { + _boxes.values.forEach(InternalBoxAccess.close); + _boxes.clear(); + + // Call each "onClose()" event listener. + // Move the list to prevent "Concurrent modification during iteration". + _onClose.values.toList(growable: false).forEach((listener) => listener()); + _onClose.clear(); + + checkObx(C.store_close(_cStore)); + } + + /// Returns a cached Box instance. + Box box() { + if (!_boxes.containsKey(T)) { + return _boxes[T] = InternalBoxAccess.create(this, _entityDef()); + } + return _boxes[T] as Box; + } + + EntityDefinition _entityDef() { + final binding = _defs.bindings[T]; + if (binding == null) { + throw ArgumentError('Unknown entity type ' + T.toString()); + } + return binding /*!*/ as EntityDefinition; + } + + /// Executes a given function inside a transaction. Returns [fn]'s result. + /// Aborts a transaction or rethrows if there's an exception. + /// + /// A transaction can group several operations into a single unit of work that + /// either executes completely or not at all. + /// The advantage of explicit transactions over the bulk put operations is + /// that you can perform any number of operations and use objects of multiple + /// boxes. In addition, you get a consistent (transactional) view on your data + /// while the transaction is in progress. + R runInTransaction(TxMode mode, R Function() fn) => + Transaction.execute(this, mode, fn); + + /// Return an existing SyncClient associated with the store or null if not + /// available. Use [Sync.client()] to create one first. + SyncClient /*?*/ syncClient() => syncClientsStorage[this]; + + /// The low-level pointer to this store. + Pointer get ptr => _cStore; +} + +/// Internal only. +// TODO enable annotation once meta:1.3.0 is out +// @internal +class InternalStoreAccess { + /// Access entity model for the given class (Dart Type). + static EntityDefinition entityDef(Store store) => store._entityDef(); + + /// Adds a listener to the [store.close()] event. + static void addCloseListener( + Store store, dynamic key, void Function() listener) => + store._onClose[key] = listener; + + /// Removes a [store.close()] event listener. + static void removeCloseListener(Store store, dynamic key) => + store._onClose.remove(key); +} diff --git a/objectbox/lib/src/native/sync.dart b/objectbox/lib/src/native/sync.dart new file mode 100644 index 000000000..12c9255d5 --- /dev/null +++ b/objectbox/lib/src/native/sync.dart @@ -0,0 +1,287 @@ +import 'dart:convert' show utf8; +import 'dart:ffi'; +import 'dart:typed_data' show Uint8List; + +import 'package:ffi/ffi.dart'; +import 'package:meta/meta.dart'; + +import '../util.dart'; +import 'bindings/bindings.dart'; +import 'bindings/helpers.dart'; +import 'bindings/structs.dart'; +import 'store.dart'; + +/// Credentials used to authenticate a sync client against a server. +class SyncCredentials { + final int _type; + final Uint8List _data; + + SyncCredentials._(this._type, String data) + : _data = Uint8List.fromList(utf8.encode(data)); + + /// No credentials - usually only for development purposes with a server + /// configured to accept all connections without authentication. + SyncCredentials.none() + : _type = OBXSyncCredentialsType.NONE, + _data = Uint8List(0); + + /// Shared secret authentication. + SyncCredentials.sharedSecretUint8List(this._data) + : _type = OBXSyncCredentialsType.SHARED_SECRET; + + /// Shared secret authentication. + SyncCredentials.sharedSecretString(String data) + : this._(OBXSyncCredentialsType.SHARED_SECRET, data); + + /// Google authentication. + SyncCredentials.googleAuthUint8List(this._data) + : _type = OBXSyncCredentialsType.GOOGLE_AUTH; + + /// Google authentication. + SyncCredentials.googleAuthString(String data) + : this._(OBXSyncCredentialsType.GOOGLE_AUTH, data); +} + +/// Current state of the [SyncClient]. +enum SyncState { + /// State is unknown, e.g. C-API reported a state that's not recognized yet. + unknown, + + /// Client created but not yet started. + created, + + /// Client started and connecting. + started, + + /// Connection with the server established but not authenticated yet. + connected, + + /// Client authenticated and synchronizing. + loggedIn, + + /// Lost connection, will try to reconnect if the credentials are valid. + disconnected, + + /// Client in the process of being closed. + stopped, + + /// Invalid access to the client after it was closed. + dead +} + +/// Configuration of how [SyncClient] fetches remote updates from the server. +enum SyncRequestUpdatesMode { + /// No updates, [SyncClient.requestUpdates()] must be called manually. + manual, + + /// Automatic updates, including subsequent pushes from the server, same as + /// calling [SyncClient.requestUpdates(true)]. This is the default unless + /// changed by [SyncClient.setRequestUpdatesMode()]. + auto, + + /// Automatic update after connection, without subscribing for pushes from the + /// server. Similar to calling [SyncClient.requestUpdates(false)]. + autoNoPushes +} + +/// Sync client is used to connect to an ObjectBox sync server. +class SyncClient { + final Store _store; + + /*late final*/ + Pointer _cSync; + + /// The low-level pointer to this box. + Pointer get ptr => (_cSync.address != 0) + ? _cSync + : throw Exception('SyncClient already closed'); + + /// Creates a sync client associated with the given store and options. + /// This does not initiate any connection attempts yet: call start() to do so. + SyncClient(this._store, String serverUri, SyncCredentials creds) { + if (!Sync.isAvailable()) { + throw Exception( + 'Sync is not available in the loaded ObjectBox runtime library. ' + 'Please visit https://objectbox.io/sync/ for options.'); + } + + final cServerUri = Utf8.toUtf8(serverUri).cast(); + try { + _cSync = checkObxPtr( + C.sync_1(_store.ptr, cServerUri), 'failed to create sync client'); + } finally { + free(cServerUri); + } + + setCredentials(creds); + } + + /// Closes and cleans up all resources used by this sync client. + /// It can no longer be used afterwards, make a new sync client instead. + /// Does nothing if this sync client has already been closed. + void close() { + final err = C.sync_close(_cSync); + _cSync = nullptr; + syncClientsStorage.remove(_store); + InternalStoreAccess.removeCloseListener(_store, this); + syncOrObserversExclusive.unmark(_store); + checkObx(err); + } + + /// Returns if this sync client is closed and can no longer be used. + bool isClosed() => _cSync.address == 0; + + /// Gets the current sync client state. + SyncState state() { + final state = C.sync_state(ptr); + switch (state) { + case OBXSyncState.CREATED: + return SyncState.created; + case OBXSyncState.STARTED: + return SyncState.started; + case OBXSyncState.CONNECTED: + return SyncState.connected; + case OBXSyncState.LOGGED_IN: + return SyncState.loggedIn; + case OBXSyncState.DISCONNECTED: + return SyncState.disconnected; + case OBXSyncState.STOPPED: + return SyncState.stopped; + case OBXSyncState.DEAD: + return SyncState.dead; + default: + return SyncState.unknown; + } + } + + /// Configure authentication credentials, depending on your server config. + void setCredentials(SyncCredentials creds) { + final cCreds = OBX_bytes_wrapper.managedCopyOf(creds._data, align: false); + try { + checkObx(C.sync_credentials( + ptr, + creds._type, + creds._type == OBXSyncCredentialsType.NONE ? nullptr : cCreds.ptr, + cCreds.size)); + } finally { + cCreds.freeManaged(); + } + } + + /// Configures how sync updates are received from the server. If automatic + /// updates are turned off, they will need to be requested manually. + void setRequestUpdatesMode(SyncRequestUpdatesMode mode) { + int cMode; + switch (mode) { + case SyncRequestUpdatesMode.manual: + cMode = OBXRequestUpdatesMode.MANUAL; + break; + case SyncRequestUpdatesMode.auto: + cMode = OBXRequestUpdatesMode.AUTO; + break; + case SyncRequestUpdatesMode.autoNoPushes: + cMode = OBXRequestUpdatesMode.AUTO_NO_PUSHES; + break; + default: + throw Exception('Unknown mode argument: ' + mode.toString()); + } + checkObx(C.sync_request_updates_mode(ptr, cMode)); + } + + /// Once the sync client is configured, you can [start] it to initiate + /// synchronization. + /// + /// This method triggers communication in the background and returns + /// immediately. The background thread will try to connect to the server, + /// log-in and start syncing data (depends on [SyncRequestUpdatesMode]). + /// If the device, network or server is currently offline, connection attempts + /// will be retried later automatically. If you haven't set the credentials in + /// the options during construction, call [setCredentials()] before [start()]. + void start() { + checkObx(C.sync_start(ptr)); + } + + /// Stops this sync client. Does nothing if it is already stopped. + void stop() { + checkObx(C.sync_stop(ptr)); + } + + /// Request updates since we last synchronized our database. + /// + /// Additionally, you can subscribe for future pushes from the server, to let + /// it send us future updates as they come in. + /// Call [cancelUpdates()] to stop the updates. + bool requestUpdates({/*required*/ bool subscribeForFuturePushes}) => + checkObxSuccess(C.sync_updates_request(ptr, subscribeForFuturePushes)); + + /// Cancel updates from the server so that it will stop sending updates. + /// See also [requestUpdates()]. + bool cancelUpdates() => checkObxSuccess(C.sync_updates_cancel(ptr)); + + /// Count the number of messages in the outgoing queue, i.e. those waiting to + /// be sent to the server. + /// + /// Note: This calls uses a (read) transaction internally: + /// 1) It's not just a "cheap" return of a single number. While this will + /// still be fast, avoid calling this function excessively. + /// 2) the result follows transaction view semantics, thus it may not always + /// match the actual value. + int outgoingMessageCount({int limit = 0}) { + final count = allocate(); + try { + checkObx(C.sync_outgoing_message_count(ptr, limit, count)); + return count.value; + } finally { + free(count); + } + } +} + +/// [ObjectBox Sync](https://objectbox.io/sync/) makes data available and +/// synchronized across devices, online and offline. +/// +/// Start a client using [Sync.client()] and connect to a remote server. +class Sync { + /// Create a Sync annotation, enabling synchronization for an entity. + const Sync(); + + static /*late final*/ bool _syncAvailable; + + /// Returns true if the loaded ObjectBox native library supports Sync. + static bool isAvailable() { + // TODO remove try-catch after upgrading to objectbox-c v0.11 where obx_sync_available() exists. + try { + _syncAvailable ??= C.sync_available(); + } catch (_) { + _syncAvailable = false; + } + return _syncAvailable; + } + + /// Creates a sync client associated with the given store and configures it + /// with the given options. This does not initiate any connection attempts + /// yet, call [SyncClient.start()] to do so. + /// + /// Before [SyncClient.start()], you can still configure some aspects of the + /// client, e.g. its [SyncRequestUpdatesMode] mode. + static SyncClient client( + Store store, String serverUri, SyncCredentials creds) { + if (syncClientsStorage.containsKey(store)) { + throw Exception('Only one sync client can be active for a store'); + } + syncOrObserversExclusive.mark(store); + final client = SyncClient(store, serverUri, creds); + syncClientsStorage[store] = client; + InternalStoreAccess.addCloseListener(store, client, client.close); + return client; + } +} + +/// Tests only. +// TODO enable annotation once meta:1.3.0 is out +// @internal +@visibleForTesting +class InternaSyncTestAccess { + /// Access credentials internal data representation. + static Uint8List credentialsData(SyncCredentials creds) => creds._data; +} diff --git a/objectbox/lib/src/native/transaction.dart b/objectbox/lib/src/native/transaction.dart new file mode 100644 index 000000000..4732ffd30 --- /dev/null +++ b/objectbox/lib/src/native/transaction.dart @@ -0,0 +1,110 @@ +import 'dart:ffi'; + +import '../modelinfo/entity_definition.dart'; +import '../store.dart'; +import '../transaction.dart'; +import 'bindings/bindings.dart'; +import 'bindings/helpers.dart'; + +// ignore_for_file: public_member_api_docs + +// TODO enable annotation once meta:1.3.0 is out +// @internal +class Transaction { + final Store _store; + final bool _isWrite; + final Pointer _cTxn; + bool _closed = false; + + // We have two ways of keeping cursors because we usually need just one. + // The variable is faster then the map initialization & access. + /*late final*/ + CursorHelper _firstCursor; + + /*late final*/ + Map _cursors; + + Pointer get ptr => _cTxn; + + Transaction(this._store, TxMode mode) + : _isWrite = mode == TxMode.write, + _cTxn = mode == TxMode.write + ? C.txn_write(_store.ptr) + : C.txn_read(_store.ptr) { + checkObxPtr(_cTxn, 'failed to create transaction'); + } + + void _finish(bool successful) { + if (_isWrite) { + try { + _mark(successful); + } finally { + close(); + } + } else { + close(); + } + } + + void commitAndClose() => _finish(true); + + void abortAndClose() => _finish(false); + + void _mark(bool successful) => + checkObx(C.txn_mark_success(_cTxn, successful)); + + void markSuccessful() => _mark(true); + + void markFailed() => _mark(false); + + void close() { + if (_closed) return; + _closed = true; + if (_firstCursor != null) { + _firstCursor.close(); + if (_cursors != null) { + _cursors.values.forEach((c) => c.close()); + _cursors.clear(); + } + } + checkObx(C.txn_close(_cTxn)); + } + + /// Returns a cursor for the given entity. No need to close it manually. + /// Note: the cursor may have already been used, don't rely on its state! + CursorHelper cursor(EntityDefinition entity) { + if (_firstCursor == null) { + return _firstCursor = + CursorHelper(_store, _cTxn, entity, isWrite: _isWrite); + } else if (_firstCursor.entity == entity) { + return _firstCursor as CursorHelper; + } + _cursors ??= {}; + final entityId = entity.model.id.id; + if (_cursors.containsKey(entityId)) { + return _cursors[entityId] as CursorHelper; + } + return _cursors[entityId] = + CursorHelper(_store, _cTxn, entity, isWrite: _isWrite); + } + + /// Executes a given function inside a transaction. + /// + /// Returns type of [fn] if [return] is called in [fn]. + static R execute(Store store, TxMode mode, R Function() fn) { + final tx = Transaction(store, mode); + try { + // In theory, we should only mark successful after the function finishes. + // In practice, it's safe to assume most functions will be successful and + // thus marking before the call allows us to return directly, before an + // intermediary variable. + if (tx._isWrite) tx.markSuccessful(); + return fn(); + } catch (ex) { + if (tx._isWrite) tx.markFailed(); + rethrow; + } finally { + tx.close(); + } + } +} diff --git a/objectbox/lib/src/native/version.dart b/objectbox/lib/src/native/version.dart new file mode 100644 index 000000000..157322f1c --- /dev/null +++ b/objectbox/lib/src/native/version.dart @@ -0,0 +1,22 @@ +import 'dart:ffi'; + +import 'package:ffi/ffi.dart' show allocate, free; + +import '../common.dart'; +import 'bindings/bindings.dart'; + +/// Returns the underlying ObjectBox-C library version. +Version libraryVersion() { + var majorPtr = allocate(), + minorPtr = allocate(), + patchPtr = allocate(); + + try { + C.version(majorPtr, minorPtr, patchPtr); + return Version(majorPtr.value, minorPtr.value, patchPtr.value); + } finally { + free(majorPtr); + free(minorPtr); + free(patchPtr); + } +} diff --git a/objectbox/lib/src/observable.dart b/objectbox/lib/src/observable.dart index 2f229f095..0d69e5dd7 100644 --- a/objectbox/lib/src/observable.dart +++ b/objectbox/lib/src/observable.dart @@ -1,102 +1 @@ -import 'dart:async'; -import 'dart:ffi'; - -import 'bindings/bindings.dart'; -import 'query/query.dart'; -import 'store.dart'; -import 'util.dart'; - -// ignore_for_file: non_constant_identifier_names - -// dart callback signature -typedef _Any = void Function(Pointer, Pointer, int); - -class _Observable { - static final _anyObserver = >{}; - static final _any = >{}; - - // sync:true -> ObjectBoxException: 10001 TX is not active anymore: #101 - static final controller = StreamController.broadcast(); - - // The user_data is used to pass the store ptr address - // in case there is no consensus on the entity id between stores - static void _anyCallback( - Pointer user_data, Pointer mutated_ids, int mutated_count) { - final storeAddress = user_data.address; - // call schema's callback - final storeCallbacks = _any[storeAddress]; - if (storeCallbacks != null) { - for (var i = 0; i < mutated_count; i++) { - storeCallbacks[mutated_ids[i]] - ?.call(user_data, mutated_ids, mutated_count); - } - } - } - - static void subscribe(Store store) { - syncOrObserversExclusive.mark(store); - - final callback = Pointer.fromFunction(_anyCallback); - final storePtr = store.ptr; - _anyObserver[storePtr.address] = - C.observe(storePtr, callback, storePtr.cast()); - InternalStoreAccess.addCloseListener(store, _anyObserver[storePtr.address], - () { - unsubscribe(store); - }); - } - - // #53 ffi:Pointer finalizer - static void unsubscribe(Store store) { - final storeAddress = store.ptr.address; - if (!_anyObserver.containsKey(storeAddress)) { - return; - } - InternalStoreAccess.removeCloseListener(store, _anyObserver[storeAddress]); - C.observer_close(_anyObserver[storeAddress]); - _anyObserver.remove(storeAddress); - syncOrObserversExclusive.unmark(store); - } - - static bool isSubscribed(Store store) => - _Observable._anyObserver.containsKey(store.ptr.address); -} - -/// Streamable adds stream support to queries. The stream reruns the query -/// whenever there's a change in any of the objects in the queried Box -/// (regardless of the filter conditions). -extension Streamable on Query { - void _setup() { - if (!_Observable.isSubscribed(store)) { - _Observable.subscribe(store); - } - final storeAddress = store.ptr.address; - - _Observable._any[storeAddress] ??= {}; - _Observable._any[storeAddress] /*!*/ [entityId] ??= (u, _, __) { - // dummy value to trigger an event - _Observable.controller.add(entityId); - }; - } - - /// Create a stream, executing [Query.find()] whenever there's a change to any - /// of the objects in the queried Box. - Stream> findStream( - {@Deprecated('Use offset() instead') int offset = 0, - @Deprecated('Use limit() instead') int limit = 0}) { - _setup(); - return _Observable.controller.stream.where((e) => e == entityId).map((_) { - if (offset != 0) this.offset(offset); - if (limit != 0) this.limit(limit); - return find(); - }); - } - - /// Use this for Query Property - Stream> get stream { - _setup(); - return _Observable.controller.stream - .where((e) => e == entityId) - .map((_) => this); - } -} +export 'native/observable.dart' if (dart.library.html) 'web/observable.dart'; diff --git a/objectbox/lib/src/query.dart b/objectbox/lib/src/query.dart new file mode 100644 index 000000000..9fcb2b5ca --- /dev/null +++ b/objectbox/lib/src/query.dart @@ -0,0 +1 @@ +export 'native/query/query.dart' if (dart.library.html) 'web/query.dart'; diff --git a/objectbox/lib/src/relations/to_many.dart b/objectbox/lib/src/relations/to_many.dart index 6230b592d..f2049f686 100644 --- a/objectbox/lib/src/relations/to_many.dart +++ b/objectbox/lib/src/relations/to_many.dart @@ -1,10 +1,7 @@ import 'dart:collection'; -import 'dart:ffi'; import 'package:meta/meta.dart'; -import '../bindings/bindings.dart'; -import '../bindings/helpers.dart'; import '../box.dart'; import '../modelinfo/entity_definition.dart'; import '../store.dart'; @@ -177,13 +174,12 @@ class ToMany extends Object with ListMixin { switch (_rel.type) { case RelType.toMany: if (add) { - if (id == 0) id = _box.put(object, mode: mode); - checkObx( - C.box_rel_put(_otherBox.ptr, _rel.id, _rel.objectId, id)); + if (id == 0) id = InternalBoxAccess.put(_box, object, mode, tx); + InternalBoxAccess.relPut(_otherBox, _rel.id, _rel.objectId, id); } else { if (id == 0) return; - checkObx( - C.box_rel_remove(_otherBox.ptr, _rel.id, _rel.objectId, id)); + InternalBoxAccess.relRemove( + _otherBox, _rel.id, _rel.objectId, id); } break; case RelType.toOneBacklink: @@ -193,11 +189,11 @@ class ToMany extends Object with ListMixin { break; case RelType.toManyBacklink: if (add) { - if (id == 0) id = _box.put(object, mode: mode); - checkObx(C.box_rel_put(_box.ptr, _rel.id, id, _rel.objectId)); + if (id == 0) id = InternalBoxAccess.put(_box, object, mode, tx); + InternalBoxAccess.relPut(_box, _rel.id, id, _rel.objectId); } else { if (id == 0) return; - checkObx(C.box_rel_remove(_box.ptr, _rel.id, id, _rel.objectId)); + InternalBoxAccess.relRemove(_box, _rel.id, id, _rel.objectId); } break; default: @@ -229,22 +225,7 @@ class ToMany extends Object with ListMixin { __items = []; } else { _verifyAttached(); - switch (_rel.type) { - case RelType.toMany: - __items = _getMany( - () => C.box_rel_get_ids(_box.ptr, _rel.id, _rel.objectId)); - break; - case RelType.toOneBacklink: - __items = _getMany( - () => C.box_get_backlink_ids(_box.ptr, _rel.id, _rel.objectId)); - break; - case RelType.toManyBacklink: - __items = _getMany(() => - C.box_rel_get_backlink_ids(_box.ptr, _rel.id, _rel.objectId)); - break; - default: - throw UnimplementedError(); - } + __items = InternalBoxAccess.getRelated(_box, _rel); } if (_addedBeforeLoad.isNotEmpty) { __items.addAll(_addedBeforeLoad); @@ -259,36 +240,6 @@ class ToMany extends Object with ListMixin { "Don't call applyToDb() on new objects, use box.put() instead."); } } - - /// Similar to box.getMany() but loads the OBX_id_array and reads objects - /// in a single Transaction, ensuring consistency. And it's a little more - /// efficient for not unpacking the id array to a dart list. - List _getMany(Pointer Function() cIdsGetterFn) { - final tx = Transaction(_store, TxMode.read); - try { - final result = []; - final cIdsPtr = checkObxPtr(cIdsGetterFn()); - try { - final cIds = cIdsPtr.ref; - if (cIds.count > 0) { - final cursor = tx.cursor(_entity); - for (var i = 0; i < cIds.count; i++) { - final code = C.cursor_get( - cursor.ptr, cIds.ids[i], cursor.dataPtrPtr, cursor.sizePtr); - if (code != OBX_NOT_FOUND) { - checkObx(code); - result.add(_entity.objectFromFB(_store, cursor.readData)); - } - } - } - } finally { - C.id_array_free(cIdsPtr); - } - return result; - } finally { - tx.close(); - } - } } /// Internal only. diff --git a/objectbox/lib/src/store.dart b/objectbox/lib/src/store.dart index 46601d834..7bab24e01 100644 --- a/objectbox/lib/src/store.dart +++ b/objectbox/lib/src/store.dart @@ -1,166 +1 @@ -import 'dart:ffi'; - -import 'package:ffi/ffi.dart'; - -import 'bindings/bindings.dart'; -import 'bindings/helpers.dart'; -import 'box.dart'; -import 'common.dart'; -import 'model.dart'; -import 'modelinfo/index.dart'; -import 'sync.dart'; -import 'transaction.dart'; -import 'util.dart'; - -/// Represents an ObjectBox database and works together with [Box] to allow -/// getting and putting. -class Store { - /*late final*/ Pointer _cStore; - final _boxes = {}; - final ModelDefinition _defs; - - /// A list of observers of the Store.close() event. - final _onClose = {}; - - /// Creates a BoxStore using the model definition from the generated - /// `objectbox.g.dart` file. - /// - /// For example in a Flutter app: - /// ```dart - /// getApplicationDocumentsDirectory().then((dir) { - /// _store = Store(getObjectBoxModel(), directory: dir.path + "/objectbox"); - /// }); - /// ``` - /// - /// Or for a Dart app: - /// ```dart - /// var store = Store(getObjectBoxModel()); - /// ``` - /// - /// See our examples for more details. - Store(this._defs, - {String /*?*/ directory, - int /*?*/ maxDBSizeInKB, - int /*?*/ fileMode, - int /*?*/ maxReaders}) { - var model = Model(_defs.model); - - var opt = C.opt(); - checkObxPtr(opt, 'failed to create store options'); - - try { - checkObx(C.opt_model(opt, model.ptr)); - if (directory != null && directory.isNotEmpty) { - var cStr = Utf8.toUtf8(directory).cast(); - try { - checkObx(C.opt_directory(opt, cStr)); - } finally { - free(cStr); - } - } - if (maxDBSizeInKB != null && maxDBSizeInKB > 0) { - C.opt_max_db_size_in_kb(opt, maxDBSizeInKB); - } - if (fileMode != null && fileMode >= 0) { - C.opt_file_mode(opt, fileMode); - } - if (maxReaders != null && maxReaders > 0) { - C.opt_max_readers(opt, maxReaders); - } - } catch (e) { - C.opt_free(opt); - rethrow; - } - _cStore = C.store_open(opt); - - try { - checkObxPtr(_cStore, 'failed to create store'); - } on ObjectBoxException catch (e) { - // Recognize common problems when trying to open/create a database - // 10199 = OBX_ERROR_STORAGE_GENERAL - if (e.nativeCode == 10199 && - e.nativeMsg != null && - e.nativeMsg /*!*/ .contains('Dir does not exist')) { - // 13 = permissions denied, 30 = read-only filesystem - if (e.nativeMsg /*!*/ .endsWith(' (13)') || - e.nativeMsg /*!*/ .endsWith(' (30)')) { - final msg = e.nativeMsg /*!*/ + - ' - this usually indicates a problem with permissions; ' - "if you're using Flutter you may need to use " - 'getApplicationDocumentsDirectory() from the path_provider ' - 'package, see example/README.md'; - throw ObjectBoxException( - dartMsg: e.dartMsg, nativeCode: e.nativeCode, nativeMsg: msg); - } - } - rethrow; - } - } - - /// Closes this store. - /// - /// Don't try to call any other ObjectBox methods after the store is closed. - void close() { - _boxes.values.forEach(InternalBoxAccess.close); - _boxes.clear(); - - // Call each "onClose()" event listener. - // Move the list to prevent "Concurrent modification during iteration". - _onClose.values.toList(growable: false).forEach((listener) => listener()); - _onClose.clear(); - - checkObx(C.store_close(_cStore)); - } - - /// Returns a cached Box instance. - Box box() { - if (!_boxes.containsKey(T)) { - return _boxes[T] = InternalBoxAccess.create(this, _entityDef()); - } - return _boxes[T] as Box; - } - - EntityDefinition _entityDef() { - final binding = _defs.bindings[T]; - if (binding == null) { - throw ArgumentError('Unknown entity type ' + T.toString()); - } - return binding /*!*/ as EntityDefinition; - } - - /// Executes a given function inside a transaction. Returns [fn]'s result. - /// Aborts a transaction or rethrows if there's an exception. - /// - /// A transaction can group several operations into a single unit of work that - /// either executes completely or not at all. - /// The advantage of explicit transactions over the bulk put operations is - /// that you can perform any number of operations and use objects of multiple - /// boxes. In addition, you get a consistent (transactional) view on your data - /// while the transaction is in progress. - R runInTransaction(TxMode mode, R Function() fn) => - Transaction.execute(this, mode, fn); - - /// Return an existing SyncClient associated with the store or null if not - /// available. Use [Sync.client()] to create one first. - SyncClient /*?*/ syncClient() => syncClientsStorage[this]; - - /// The low-level pointer to this store. - Pointer get ptr => _cStore; -} - -/// Internal only. -// TODO enable annotation once meta:1.3.0 is out -// @internal -class InternalStoreAccess { - /// Access entity model for the given class (Dart Type). - static EntityDefinition entityDef(Store store) => store._entityDef(); - - /// Adds a listener to the [store.close()] event. - static void addCloseListener( - Store store, dynamic key, void Function() listener) => - store._onClose[key] = listener; - - /// Removes a [store.close()] event listener. - static void removeCloseListener(Store store, dynamic key) => - store._onClose.remove(key); -} +export 'native/store.dart' if (dart.library.html) 'web/store.dart'; diff --git a/objectbox/lib/src/sync.dart b/objectbox/lib/src/sync.dart index 0095fd8c8..9f9d9ce3a 100644 --- a/objectbox/lib/src/sync.dart +++ b/objectbox/lib/src/sync.dart @@ -1,287 +1 @@ -import 'dart:convert' show utf8; -import 'dart:ffi'; -import 'dart:typed_data' show Uint8List; - -import 'package:ffi/ffi.dart'; -import 'package:meta/meta.dart'; - -import 'bindings/bindings.dart'; -import 'bindings/helpers.dart'; -import 'bindings/structs.dart'; -import 'store.dart'; -import 'util.dart'; - -/// Credentials used to authenticate a sync client against a server. -class SyncCredentials { - final int _type; - final Uint8List _data; - - SyncCredentials._(this._type, String data) - : _data = Uint8List.fromList(utf8.encode(data)); - - /// No credentials - usually only for development purposes with a server - /// configured to accept all connections without authentication. - SyncCredentials.none() - : _type = OBXSyncCredentialsType.NONE, - _data = Uint8List(0); - - /// Shared secret authentication. - SyncCredentials.sharedSecretUint8List(this._data) - : _type = OBXSyncCredentialsType.SHARED_SECRET; - - /// Shared secret authentication. - SyncCredentials.sharedSecretString(String data) - : this._(OBXSyncCredentialsType.SHARED_SECRET, data); - - /// Google authentication. - SyncCredentials.googleAuthUint8List(this._data) - : _type = OBXSyncCredentialsType.GOOGLE_AUTH; - - /// Google authentication. - SyncCredentials.googleAuthString(String data) - : this._(OBXSyncCredentialsType.GOOGLE_AUTH, data); -} - -/// Current state of the [SyncClient]. -enum SyncState { - /// State is unknown, e.g. C-API reported a state that's not recognized yet. - unknown, - - /// Client created but not yet started. - created, - - /// Client started and connecting. - started, - - /// Connection with the server established but not authenticated yet. - connected, - - /// Client authenticated and synchronizing. - loggedIn, - - /// Lost connection, will try to reconnect if the credentials are valid. - disconnected, - - /// Client in the process of being closed. - stopped, - - /// Invalid access to the client after it was closed. - dead -} - -/// Configuration of how [SyncClient] fetches remote updates from the server. -enum SyncRequestUpdatesMode { - /// No updates, [SyncClient.requestUpdates()] must be called manually. - manual, - - /// Automatic updates, including subsequent pushes from the server, same as - /// calling [SyncClient.requestUpdates(true)]. This is the default unless - /// changed by [SyncClient.setRequestUpdatesMode()]. - auto, - - /// Automatic update after connection, without subscribing for pushes from the - /// server. Similar to calling [SyncClient.requestUpdates(false)]. - autoNoPushes -} - -/// Sync client is used to connect to an ObjectBox sync server. -class SyncClient { - final Store _store; - - /*late final*/ - Pointer _cSync; - - /// The low-level pointer to this box. - Pointer get ptr => (_cSync.address != 0) - ? _cSync - : throw Exception('SyncClient already closed'); - - /// Creates a sync client associated with the given store and options. - /// This does not initiate any connection attempts yet: call start() to do so. - SyncClient(this._store, String serverUri, SyncCredentials creds) { - if (!Sync.isAvailable()) { - throw Exception( - 'Sync is not available in the loaded ObjectBox runtime library. ' - 'Please visit https://objectbox.io/sync/ for options.'); - } - - final cServerUri = Utf8.toUtf8(serverUri).cast(); - try { - _cSync = checkObxPtr( - C.sync_1(_store.ptr, cServerUri), 'failed to create sync client'); - } finally { - free(cServerUri); - } - - setCredentials(creds); - } - - /// Closes and cleans up all resources used by this sync client. - /// It can no longer be used afterwards, make a new sync client instead. - /// Does nothing if this sync client has already been closed. - void close() { - final err = C.sync_close(_cSync); - _cSync = nullptr; - syncClientsStorage.remove(_store); - InternalStoreAccess.removeCloseListener(_store, this); - syncOrObserversExclusive.unmark(_store); - checkObx(err); - } - - /// Returns if this sync client is closed and can no longer be used. - bool isClosed() => _cSync.address == 0; - - /// Gets the current sync client state. - SyncState state() { - final state = C.sync_state(ptr); - switch (state) { - case OBXSyncState.CREATED: - return SyncState.created; - case OBXSyncState.STARTED: - return SyncState.started; - case OBXSyncState.CONNECTED: - return SyncState.connected; - case OBXSyncState.LOGGED_IN: - return SyncState.loggedIn; - case OBXSyncState.DISCONNECTED: - return SyncState.disconnected; - case OBXSyncState.STOPPED: - return SyncState.stopped; - case OBXSyncState.DEAD: - return SyncState.dead; - default: - return SyncState.unknown; - } - } - - /// Configure authentication credentials, depending on your server config. - void setCredentials(SyncCredentials creds) { - final cCreds = OBX_bytes_wrapper.managedCopyOf(creds._data, align: false); - try { - checkObx(C.sync_credentials( - ptr, - creds._type, - creds._type == OBXSyncCredentialsType.NONE ? nullptr : cCreds.ptr, - cCreds.size)); - } finally { - cCreds.freeManaged(); - } - } - - /// Configures how sync updates are received from the server. If automatic - /// updates are turned off, they will need to be requested manually. - void setRequestUpdatesMode(SyncRequestUpdatesMode mode) { - int cMode; - switch (mode) { - case SyncRequestUpdatesMode.manual: - cMode = OBXRequestUpdatesMode.MANUAL; - break; - case SyncRequestUpdatesMode.auto: - cMode = OBXRequestUpdatesMode.AUTO; - break; - case SyncRequestUpdatesMode.autoNoPushes: - cMode = OBXRequestUpdatesMode.AUTO_NO_PUSHES; - break; - default: - throw Exception('Unknown mode argument: ' + mode.toString()); - } - checkObx(C.sync_request_updates_mode(ptr, cMode)); - } - - /// Once the sync client is configured, you can [start] it to initiate - /// synchronization. - /// - /// This method triggers communication in the background and returns - /// immediately. The background thread will try to connect to the server, - /// log-in and start syncing data (depends on [SyncRequestUpdatesMode]). - /// If the device, network or server is currently offline, connection attempts - /// will be retried later automatically. If you haven't set the credentials in - /// the options during construction, call [setCredentials()] before [start()]. - void start() { - checkObx(C.sync_start(ptr)); - } - - /// Stops this sync client. Does nothing if it is already stopped. - void stop() { - checkObx(C.sync_stop(ptr)); - } - - /// Request updates since we last synchronized our database. - /// - /// Additionally, you can subscribe for future pushes from the server, to let - /// it send us future updates as they come in. - /// Call [cancelUpdates()] to stop the updates. - bool requestUpdates({/*required*/ bool subscribeForFuturePushes}) => - checkObxSuccess(C.sync_updates_request(ptr, subscribeForFuturePushes)); - - /// Cancel updates from the server so that it will stop sending updates. - /// See also [requestUpdates()]. - bool cancelUpdates() => checkObxSuccess(C.sync_updates_cancel(ptr)); - - /// Count the number of messages in the outgoing queue, i.e. those waiting to - /// be sent to the server. - /// - /// Note: This calls uses a (read) transaction internally: - /// 1) It's not just a "cheap" return of a single number. While this will - /// still be fast, avoid calling this function excessively. - /// 2) the result follows transaction view semantics, thus it may not always - /// match the actual value. - int outgoingMessageCount({int limit = 0}) { - final count = allocate(); - try { - checkObx(C.sync_outgoing_message_count(ptr, limit, count)); - return count.value; - } finally { - free(count); - } - } -} - -/// [ObjectBox Sync](https://objectbox.io/sync/) makes data available and -/// synchronized across devices, online and offline. -/// -/// Start a client using [Sync.client()] and connect to a remote server. -class Sync { - /// Create a Sync annotation, enabling synchronization for an entity. - const Sync(); - - static /*late final*/ bool _syncAvailable; - - /// Returns true if the loaded ObjectBox native library supports Sync. - static bool isAvailable() { - // TODO remove try-catch after upgrading to objectbox-c v0.11 where obx_sync_available() exists. - try { - _syncAvailable ??= C.sync_available(); - } catch (_) { - _syncAvailable = false; - } - return _syncAvailable; - } - - /// Creates a sync client associated with the given store and configures it - /// with the given options. This does not initiate any connection attempts - /// yet, call [SyncClient.start()] to do so. - /// - /// Before [SyncClient.start()], you can still configure some aspects of the - /// client, e.g. its [SyncRequestUpdatesMode] mode. - static SyncClient client( - Store store, String serverUri, SyncCredentials creds) { - if (syncClientsStorage.containsKey(store)) { - throw Exception('Only one sync client can be active for a store'); - } - syncOrObserversExclusive.mark(store); - final client = SyncClient(store, serverUri, creds); - syncClientsStorage[store] = client; - InternalStoreAccess.addCloseListener(store, client, client.close); - return client; - } -} - -/// Tests only. -// TODO enable annotation once meta:1.3.0 is out -// @internal -@visibleForTesting -class InternaSyncTestAccess { - /// Access credentials internal data representation. - static Uint8List credentialsData(SyncCredentials creds) => creds._data; -} +export 'native/sync.dart' if (dart.library.html) 'web/sync.dart'; diff --git a/objectbox/lib/src/transaction.dart b/objectbox/lib/src/transaction.dart index d83eaa111..07e8e5f55 100644 --- a/objectbox/lib/src/transaction.dart +++ b/objectbox/lib/src/transaction.dart @@ -1,11 +1,4 @@ -import 'dart:ffi'; - -import 'bindings/bindings.dart'; -import 'bindings/helpers.dart'; -import 'modelinfo/entity_definition.dart'; -import 'store.dart'; - -// ignore_for_file: public_member_api_docs +export 'native/transaction.dart' if (dart.library.html) 'web/transaction.dart'; /// Configure transaction mode. Used with [Store.runInTransaction()]. enum TxMode { @@ -21,104 +14,3 @@ enum TxMode { /// write data to the disk at the end. write, } - -// TODO enable annotation once meta:1.3.0 is out -// @internal -class Transaction { - final Store _store; - final bool _isWrite; - final Pointer _cTxn; - bool _closed = false; - - // We have two ways of keeping cursors because we usually need just one. - // The variable is faster then the map initialization & access. - /*late final*/ - CursorHelper _firstCursor; - - /*late final*/ - Map _cursors; - - Pointer get ptr => _cTxn; - - Transaction(this._store, TxMode mode) - : _isWrite = mode == TxMode.write, - _cTxn = mode == TxMode.write - ? C.txn_write(_store.ptr) - : C.txn_read(_store.ptr) { - checkObxPtr(_cTxn, 'failed to create transaction'); - } - - void _finish(bool successful) { - if (_isWrite) { - try { - _mark(successful); - } finally { - close(); - } - } else { - close(); - } - } - - void commitAndClose() => _finish(true); - - void abortAndClose() => _finish(false); - - void _mark(bool successful) => - checkObx(C.txn_mark_success(_cTxn, successful)); - - void markSuccessful() => _mark(true); - - void markFailed() => _mark(false); - - void close() { - if (_closed) return; - _closed = true; - if (_firstCursor != null) { - _firstCursor.close(); - if (_cursors != null) { - _cursors.values.forEach((c) => c.close()); - _cursors.clear(); - } - } - checkObx(C.txn_close(_cTxn)); - } - - /// Returns a cursor for the given entity. No need to close it manually. - /// Note: the cursor may have already been used, don't rely on its state! - CursorHelper cursor(EntityDefinition entity) { - if (_firstCursor == null) { - return _firstCursor = - CursorHelper(_store, _cTxn, entity, isWrite: _isWrite); - } else if (_firstCursor.entity == entity) { - return _firstCursor as CursorHelper; - } - _cursors ??= {}; - final entityId = entity.model.id.id; - if (_cursors.containsKey(entityId)) { - return _cursors[entityId] as CursorHelper; - } - return _cursors[entityId] = - CursorHelper(_store, _cTxn, entity, isWrite: _isWrite); - } - - /// Executes a given function inside a transaction. - /// - /// Returns type of [fn] if [return] is called in [fn]. - static R execute(Store store, TxMode mode, R Function() fn) { - final tx = Transaction(store, mode); - try { - // In theory, we should only mark successful after the function finishes. - // In practice, it's safe to assume most functions will be successful and - // thus marking before the call allows us to return directly, before an - // intermediary variable. - if (tx._isWrite) tx.markSuccessful(); - return fn(); - } catch (ex) { - if (tx._isWrite) tx.markFailed(); - rethrow; - } finally { - tx.close(); - } - } -} diff --git a/objectbox/lib/src/web/box.dart b/objectbox/lib/src/web/box.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/objectbox/lib/src/web/box.dart @@ -0,0 +1 @@ + diff --git a/objectbox/lib/src/web/model.dart b/objectbox/lib/src/web/model.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/objectbox/lib/src/web/model.dart @@ -0,0 +1 @@ + diff --git a/objectbox/lib/src/web/observable.dart b/objectbox/lib/src/web/observable.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/objectbox/lib/src/web/observable.dart @@ -0,0 +1 @@ + diff --git a/objectbox/lib/src/web/query.dart b/objectbox/lib/src/web/query.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/objectbox/lib/src/web/query.dart @@ -0,0 +1 @@ + diff --git a/objectbox/lib/src/web/store.dart b/objectbox/lib/src/web/store.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/objectbox/lib/src/web/store.dart @@ -0,0 +1 @@ + diff --git a/objectbox/lib/src/web/sync.dart b/objectbox/lib/src/web/sync.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/objectbox/lib/src/web/sync.dart @@ -0,0 +1 @@ + diff --git a/objectbox/lib/src/web/transaction.dart b/objectbox/lib/src/web/transaction.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/objectbox/lib/src/web/transaction.dart @@ -0,0 +1 @@ + diff --git a/objectbox/test/basics_test.dart b/objectbox/test/basics_test.dart index b60f3b5c8..c882703d8 100644 --- a/objectbox/test/basics_test.dart +++ b/objectbox/test/basics_test.dart @@ -1,7 +1,7 @@ import 'dart:ffi' as ffi; import 'package:objectbox/internal.dart'; -import 'package:objectbox/src/bindings/bindings.dart'; -import 'package:objectbox/src/bindings/helpers.dart'; +import 'package:objectbox/src/native/bindings/bindings.dart'; +import 'package:objectbox/src/native/bindings/helpers.dart'; import 'package:test/test.dart'; void main() { diff --git a/objectbox/test/flatbuffers_test.dart b/objectbox/test/flatbuffers_test.dart index 2815a44e7..de46fd81e 100644 --- a/objectbox/test/flatbuffers_test.dart +++ b/objectbox/test/flatbuffers_test.dart @@ -3,7 +3,7 @@ import 'dart:typed_data'; import 'package:test/test.dart'; import 'package:flat_buffers/flat_buffers.dart' as fb_upstream; -import 'package:objectbox/src/bindings/flatbuffers.dart'; +import 'package:objectbox/src/native/bindings/flatbuffers.dart'; Uint8List addFbData(dynamic fbb) { fbb.startTable(); diff --git a/objectbox/test/observer_test.dart b/objectbox/test/observer_test.dart index 4d5eea4d0..d1a3c525f 100644 --- a/objectbox/test/observer_test.dart +++ b/objectbox/test/observer_test.dart @@ -1,6 +1,6 @@ import 'dart:ffi'; -import 'package:objectbox/src/bindings/bindings.dart'; +import 'package:objectbox/src/native/bindings/bindings.dart'; import 'package:test/test.dart'; import 'entity.dart'; diff --git a/objectbox/test/sync_test.dart b/objectbox/test/sync_test.dart index f3fadf227..5ae98e1d5 100644 --- a/objectbox/test/sync_test.dart +++ b/objectbox/test/sync_test.dart @@ -1,7 +1,7 @@ import 'dart:math'; import 'dart:typed_data'; -import 'package:objectbox/src/bindings/bindings.dart'; +import 'package:objectbox/src/modelinfo/enums.dart'; import 'package:objectbox/objectbox.dart'; import 'package:objectbox/internal.dart'; import 'package:test/test.dart';