Skip to content

Commit dacca00

Browse files
Merge branch '75-query-during-query' into 'main'
Fix incomplete results if query runs while visiting results See merge request objectbox/objectbox-dart!53
2 parents 3e4189a + 44c87ab commit dacca00

File tree

4 files changed

+115
-57
lines changed

4 files changed

+115
-57
lines changed

objectbox/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
## latest
22

3+
* Resolve an issue where not all query results are returned, when an entity constructor or property
4+
setter itself executes a query. [#550](https://github.com/objectbox/objectbox-dart/issues/550)
5+
36
## 2.2.0 (2023-08-08)
47

58
* For Flutter apps running on Android 6 (or older): added `loadObjectBoxLibraryAndroidCompat()` to

objectbox/lib/src/native/bindings/data_visitor.dart

Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,59 @@
11
import 'dart:ffi';
22

3-
import '../../modelinfo/entity_definition.dart';
4-
import '../store.dart';
53
import 'bindings.dart';
4+
import 'helpers.dart';
65

7-
// ignore_for_file: public_member_api_docs
6+
/// Callback for reading data one-by-one, see [visit].
7+
typedef VisitCallback = bool Function(Pointer<Uint8> data, int size);
88

9-
/// When you want to pass a dart callback to a C function you cannot use lambdas
10-
/// and instead the callback must be a static function - giving a lambda to
11-
/// [Pointer.fromFunction()] won't compile:
12-
/// Error: fromFunction expects a static function as parameter.
13-
/// dart:ffi only supports calling static Dart functions from native code.
9+
/// Currently FFI's Pointer.fromFunction only allows to pass a static Dart
10+
/// callback function. When passing a closure it would throw at runtime:
11+
/// Error: fromFunction expects a static function as parameter.
12+
/// dart:ffi only supports calling static Dart functions from native code.
13+
/// Closures and tear-offs are not supported because they can capture context.
1414
///
15-
/// With Dart being all synchronous and not sharing memory at all within a
16-
/// single isolate, we can just alter a single global callback variable.
17-
/// Therefore, let's have a single static function [_forwarder] converted to a
18-
/// native visitor pointer [_nativeVisitor], calling [_callback] in the end.
19-
20-
bool Function(Pointer<Uint8> data, int size) _callback = _callback;
15+
/// So given that and
16+
/// - it is required that each query has its own callback,
17+
/// - it is possible that as part of visiting results another query is created
18+
/// and visits its results (e.g. query run in entity constructor or setter) and
19+
/// - Dart code within an isolate is executed synchronously:
20+
///
21+
/// Create a single static callback function [_callbackWrapper] that wraps
22+
/// the actual Dart callback of the query currently visiting results.
23+
/// Keep callbacks on a [_callbackStack] to restore the callback of an outer
24+
/// query once a nested query is finished visiting results.
25+
List<VisitCallback> _callbackStack = [];
2126

22-
bool _forwarder(Pointer<Uint8> dataPtr, int size, Pointer<Void> _) =>
23-
_callback(dataPtr, size);
27+
bool _callbackWrapper(Pointer<Uint8> dataPtr, int size, Pointer<Void> _) =>
28+
_callbackStack.last(dataPtr, size);
2429

25-
final Pointer<obx_data_visitor> _nativeVisitor =
26-
Pointer.fromFunction(_forwarder, false);
30+
final Pointer<obx_data_visitor> _callbackWrapperPtr =
31+
Pointer.fromFunction(_callbackWrapper, false);
2732

28-
/// The callback for reading data one-by-one.
33+
/// Visits query results.
2934
///
35+
/// Pass a [callback] for reading data one-by-one:
3036
/// - [data] is the read data buffer.
3137
/// - [size] specifies the length of the read data.
3238
/// - Return true to keep going, false to cancel.
3339
@pragma('vm:prefer-inline')
34-
Pointer<obx_data_visitor> dataVisitor(
35-
bool Function(Pointer<Uint8> data, int size) callback) {
36-
_callback = callback;
37-
return _nativeVisitor;
40+
void visit(Pointer<OBX_query> queryPtr, VisitCallback callback) {
41+
// Keep callback in case another query is created and visits results
42+
// within the callback.
43+
_callbackStack.add(callback);
44+
final code = C.query_visit(queryPtr, _callbackWrapperPtr, nullptr);
45+
_callbackStack.removeLast();
46+
// Clean callback from stack before potentially throwing.
47+
checkObx(code);
3848
}
3949

40-
@pragma('vm:prefer-inline')
41-
Pointer<obx_data_visitor> objectCollector<T>(List<T> list, Store store,
42-
EntityDefinition<T> entity, ObjectCollectorError outError) =>
43-
dataVisitor((Pointer<Uint8> data, int size) {
44-
try {
45-
list.add(entity.objectFromData(store, data, size));
46-
return true;
47-
} catch (e) {
48-
outError.error = e;
49-
return false;
50-
}
51-
});
52-
53-
class ObjectCollectorError {
50+
/// Can be used with [visit] to get an error out of the callback.
51+
class ObjectVisitorError {
52+
/// Set this e.g. to an exception that occurred inside the callback.
5453
Object? error;
5554

55+
/// Call once visiting is finished. If an exception is set to [error] will
56+
/// re-throw it.
5657
void throwIfError() {
5758
if (error != null) throw error!;
5859
}

objectbox/lib/src/native/query/query.dart

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -835,17 +835,18 @@ class Query<T> implements Finalizable {
835835
/// results. Note: [offset] and [limit] are respected, if set.
836836
T? findFirst() {
837837
T? result;
838-
Object? error;
839-
final visitor = dataVisitor((Pointer<Uint8> data, int size) {
838+
final errorWrapper = ObjectVisitorError();
839+
visitCallBack(Pointer<Uint8> data, int size) {
840840
try {
841841
result = _entity.objectFromData(_store, data, size);
842842
} catch (e) {
843-
error = e;
843+
errorWrapper.error = e;
844844
}
845845
return false; // we only want to visit the first element
846-
});
847-
checkObx(C.query_visit(_ptr, visitor, nullptr));
848-
if (error != null) throw error!;
846+
}
847+
848+
visit(_ptr, visitCallBack);
849+
errorWrapper.throwIfError();
849850
return result;
850851
}
851852

@@ -868,24 +869,25 @@ class Query<T> implements Finalizable {
868869
/// higher than one, otherwise the check for non-unique result won't work.
869870
T? findUnique() {
870871
T? result;
871-
Object? error;
872-
final visitor = dataVisitor((Pointer<Uint8> data, int size) {
872+
final errorWrapper = ObjectVisitorError();
873+
visitCallback(Pointer<Uint8> data, int size) {
873874
if (result == null) {
874875
try {
875876
result = _entity.objectFromData(_store, data, size);
876877
return true;
877878
} catch (e) {
878-
error = e;
879+
errorWrapper.error = e;
879880
return false;
880881
}
881882
} else {
882-
error = NonUniqueResultException(
883+
errorWrapper.error = NonUniqueResultException(
883884
'Query findUnique() matched more than one object');
884885
return false;
885886
}
886-
});
887-
checkObx(C.query_visit(_ptr, visitor, nullptr));
888-
if (error != null) throw error!;
887+
}
888+
889+
visit(_ptr, visitCallback);
890+
errorWrapper.throwIfError();
889891
return result;
890892
}
891893

@@ -924,9 +926,18 @@ class Query<T> implements Finalizable {
924926
/// Finds Objects matching the query.
925927
List<T> find() {
926928
final result = <T>[];
927-
final errorWrapper = ObjectCollectorError();
928-
final collector = objectCollector(result, _store, _entity, errorWrapper);
929-
checkObx(C.query_visit(_ptr, collector, nullptr));
929+
final errorWrapper = ObjectVisitorError();
930+
visitCallback(Pointer<Uint8> data, int size) {
931+
try {
932+
result.add(_entity.objectFromData(_store, data, size));
933+
return true;
934+
} catch (e) {
935+
errorWrapper.error = e;
936+
return false;
937+
}
938+
}
939+
940+
visit(_ptr, visitCallback);
930941
errorWrapper.throwIfError();
931942
return result;
932943
}
@@ -1174,7 +1185,7 @@ class Query<T> implements Finalizable {
11741185
var dataPtrBatch = List<int>.filled(maxBatchSize, 0);
11751186
var sizeBatch = List<int>.filled(maxBatchSize, 0);
11761187
var batchSize = 0;
1177-
final visitor = dataVisitor((Pointer<Uint8> data, int size) {
1188+
visitCallback(Pointer<Uint8> data, int size) {
11781189
// Currently returning all results, even if the stream has been closed
11791190
// before (e.g. only first element taken). Would need a way to check
11801191
// for exit command on commandPort synchronously.
@@ -1190,11 +1201,12 @@ class Query<T> implements Finalizable {
11901201
batchSize = 0;
11911202
}
11921203
return true;
1193-
});
1204+
}
1205+
11941206
final queryPtr =
11951207
Pointer<OBX_query>.fromAddress(isolateInit.queryPtrAddress);
11961208
try {
1197-
checkObx(C.query_visit(queryPtr, visitor, nullptr));
1209+
visit(queryPtr, visitCallback);
11981210
} catch (e) {
11991211
resultPort.send(e);
12001212
return;

objectbox_test/test/box_test.dart

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1072,7 +1072,7 @@ void main() {
10721072
});
10731073

10741074
// https://github.com/objectbox/objectbox-dart/issues/550
1075-
test("read during object creation", () {
1075+
test("get with nested get reads all properties", () {
10761076
final box = env.store.box<TestEntityReadDuringRead>();
10771077
final id =
10781078
box.put(TestEntityReadDuringRead()..strings2 = ["A2", "B2", "C3"]);
@@ -1086,6 +1086,48 @@ void main() {
10861086
readDuringReadCalledFromSetter = null; // Do not leak the box instance.
10871087
expect(actual.strings2, hasLength(3));
10881088
});
1089+
1090+
// https://github.com/objectbox/objectbox-dart/issues/550
1091+
test("query with nested query returns all results", () {
1092+
final box = env.store.box<TestEntityReadDuringRead>();
1093+
box.put(TestEntityReadDuringRead());
1094+
box.put(TestEntityReadDuringRead());
1095+
// Do a query of another box (to avoid stack overflow)
1096+
// as part of calling a property setter.
1097+
env.box.putMany(simpleItems());
1098+
var nestedCountUnexpected = false;
1099+
readDuringReadCalledFromSetter = () {
1100+
final query = env.box.query().build();
1101+
final count = query.find().length;
1102+
if (count != 6) nestedCountUnexpected = true;
1103+
query.close();
1104+
};
1105+
final query = box.query().build();
1106+
final all = query.find();
1107+
query.close();
1108+
1109+
expect(all.length, 2);
1110+
expect(nestedCountUnexpected, false);
1111+
1112+
readDuringReadCalledFromSetter = null; // Do not leak the box instance.
1113+
});
1114+
1115+
// https://github.com/objectbox/objectbox-dart/issues/550
1116+
test("query with nested throwing query forwards error", () {
1117+
final box = env.store.box<TestEntityReadDuringRead>();
1118+
final id = box.put(TestEntityReadDuringRead());
1119+
// Query for an object that will throw when being read
1120+
// as part of calling a property setter.
1121+
final throwingBox = env.store.box<ThrowingInConverters>();
1122+
throwingBox.put(ThrowingInConverters(throwOnGet: true));
1123+
readDuringReadCalledFromSetter = () {
1124+
throwingBox.query().build().find();
1125+
};
1126+
// The outer query should forward the exception of the nested query.
1127+
expect(() => box.get(id), ThrowingInConverters.throwsIn("Setter"));
1128+
1129+
readDuringReadCalledFromSetter = null; // Do not leak the box instance.
1130+
});
10891131
}
10901132

10911133
List<TestEntity> simpleItems() => ['One', 'Two', 'Three', 'Four', 'Five', 'Six']

0 commit comments

Comments
 (0)