Skip to content

Commit d44badf

Browse files
Flex: for lists/maps with non-null elements/values skip null values
Also explicitly test detection of Map with non-null values. Note about Map: while a map with null values can be cast to one with non-null values, it will error at runtime when iterating over entries as it fails to cast a null value.
1 parent 3170427 commit d44badf

File tree

6 files changed

+113
-38
lines changed

6 files changed

+113
-38
lines changed

generator/lib/src/code_chunks.dart

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,26 @@ class CodeChunks {
449449
);
450450
}
451451

452+
/// Extracts the key and value types by removing "Map<" prefix and ">" suffix.
453+
static String _getMapKeyValueTypes(String typeString) {
454+
// This String operation is very fragile and should be replaced with info
455+
// from ModelProperty created by EntityResolver.
456+
return typeString.substring(
457+
4, // "Map<"
458+
typeString.length - 1, // ">"
459+
);
460+
}
461+
462+
/// Extracts the element type by removing "List<" prefix and ">" suffix.
463+
static String _getListElementType(String typeString) {
464+
// This String operation is very fragile and should be replaced with info
465+
// from ModelProperty created by EntityResolver.
466+
return typeString.substring(
467+
5, // "List<"
468+
typeString.length - 1, // ">"
469+
);
470+
}
471+
452472
static String objectFromFB(ModelEntity entity) {
453473
// collect code for the template at the end of this function
454474
final constructorLines = <String>[]; // used as constructor arguments
@@ -579,50 +599,58 @@ class CodeChunks {
579599
// Check if list needs casting (List<Object> or List<Object?>)
580600
final isListOfObject = p.fieldType.startsWith('List<Object');
581601
final String flexDeserializer;
602+
final bool skipNull;
582603
final String? defaultValue;
583604
final String castSuffix;
584605
if (isValue) {
585606
// dynamic or Object? - can be any FlexBuffer value
586607
flexDeserializer = 'fromFlexBuffer';
608+
skipNull = false;
587609
defaultValue = null; // nullable only, no default needed
588610
castSuffix = '';
589611
} else if (isMap) {
590612
flexDeserializer = 'flexBufferToMap';
591-
defaultValue = '<String, dynamic>{}';
592-
castSuffix = '';
613+
final keyValueTypes = _getMapKeyValueTypes(p.fieldType);
614+
final hasDynamicValueType = keyValueTypes.endsWith('dynamic');
615+
// For maps with non-null values skip nulls (so not
616+
// Map<String, dynamic> or Map<String, Object/List/Map?>).
617+
skipNull = !hasDynamicValueType && !keyValueTypes.endsWith('?');
618+
defaultValue = '<$keyValueTypes>{}';
619+
// Cast to the correct Map type if not Map<String, dynamic>
620+
if (hasDynamicValueType) {
621+
castSuffix = '';
622+
} else {
623+
castSuffix = '?.cast<$keyValueTypes>()';
624+
}
593625
} else if (isListOfMaps) {
594626
flexDeserializer = 'flexBufferToListOfMaps';
595-
// Extract the element type, e.g. "Map<String, dynamic>"
596-
// by removing "List<" prefix and ">" suffix.
597-
final elementType = p.fieldType.substring(
598-
5,
599-
p.fieldType.length - 1,
600-
);
627+
final elementType = _getListElementType(p.fieldType);
628+
// The deserializer already skips nulls
629+
skipNull = false;
601630
defaultValue = '<$elementType>[]';
602-
// Cast needed for Map<String, Object?> (Object not supported)
631+
// Cast needed for Map<String, Object?> and Map<String, Object>
603632
if (elementType == 'Map<String, dynamic>') {
604633
castSuffix = '';
605634
} else {
606635
castSuffix = '?.cast<$elementType>()';
607636
}
608637
} else if (isListOfObject) {
609638
flexDeserializer = 'flexBufferToList';
610-
// Extract the element type, e.g. "Object" or "Object?"
611-
// by removing "List<" prefix and ">" suffix.
612-
final elementType = p.fieldType.substring(
613-
5,
614-
p.fieldType.length - 1,
615-
);
639+
final elementType = _getListElementType(p.fieldType);
640+
// For lists with non-null elements skip nulls
641+
skipNull = !elementType.endsWith('?');
616642
defaultValue = '<$elementType>[]';
617643
castSuffix = '?.cast<$elementType>()';
618644
} else {
619645
// List<dynamic>
620646
flexDeserializer = 'flexBufferToList';
647+
skipNull = false;
621648
defaultValue = '<dynamic>[]';
622649
castSuffix = '';
623650
}
651+
final skipNullArg = skipNull ? ', skipNull: true' : '';
624652
final deserializeExpr =
625-
'$obxInt.$flexDeserializer(buffer, rootOffset, $offset)$castSuffix';
653+
'$obxInt.$flexDeserializer(buffer, rootOffset, $offset$skipNullArg)$castSuffix';
626654
if (p.fieldIsNullable || defaultValue == null) {
627655
// Nullable field or value type (dynamic/Object?) - no default
628656
return deserializeExpr;

generator/test/code_builder_test.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,9 @@ void main() {
825825
826826
// Auto-detected Map<String, Object?> - nullable
827827
Map<String, Object?>? flexObject;
828+
829+
// Auto-detected Map<String, Object> (non-nullable values) - nullable
830+
Map<String, Object>? flexObjectNonNull;
828831
829832
// Non-nullable with default empty map
830833
Map<String, dynamic> flexNonNull = {};
@@ -839,7 +842,7 @@ void main() {
839842
await testEnv.run(source);
840843

841844
var properties = testEnv.model.entities[0].properties;
842-
expectFlexProperties(properties, 5);
845+
expectFlexProperties(properties, 6);
843846
});
844847

845848
test('Flex List type detection', () async {

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

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,36 +19,49 @@ Uint8List toFlexBuffer(Object value) {
1919
/// Returns null if no data is stored for this field. The returned value can be
2020
/// null, bool, int, double, String, `List<dynamic>`, `Map<String, dynamic>`,
2121
/// or nested combinations of these.
22+
///
23+
/// If [skipNullCollectionValues], if a vector element is null or a map value is
24+
/// null the list element or map entry is not returned. This can be used to
25+
/// allow casting the return value of this method to [List] with non-null
26+
/// elements (like `List<Object>`) and [Map] with non-null values (like
27+
/// `Map<String, Object>`).
2228
@pragma('vm:prefer-inline')
23-
dynamic fromFlexBuffer(BufferContext buffer, int offset, int field) {
29+
dynamic fromFlexBuffer(BufferContext buffer, int offset, int field,
30+
{bool skipNullCollectionValues = false}) {
2431
// Note: Uint8ListReader returns a Uint8List? cast to List<int>, so just cast
2532
// it back (if that ever changes, add a custom reader)
2633
final bytes = const Uint8ListReader(lazy: false)
2734
.vTableGetNullable(buffer, offset, field) as Uint8List?;
2835
if (bytes == null) return null;
2936
final ref = flex.Reference.fromBuffer(bytes.buffer);
30-
return _convertReference(ref);
37+
return _convertReference(ref, skipNullCollectionValues);
3138
}
3239

3340
/// Deserializes FlexBuffer bytes to a `Map<String, dynamic>`.
3441
@pragma('vm:prefer-inline')
3542
Map<String, dynamic>? flexBufferToMap(
36-
BufferContext buffer, int offset, int field) =>
37-
fromFlexBuffer(buffer, offset, field) as Map<String, dynamic>?;
43+
BufferContext buffer, int offset, int field, {bool skipNull = false}) =>
44+
fromFlexBuffer(buffer, offset, field, skipNullCollectionValues: skipNull)
45+
as Map<String, dynamic>?;
3846

3947
/// Deserializes FlexBuffer bytes to a `List<dynamic>`.
4048
@pragma('vm:prefer-inline')
41-
List<dynamic>? flexBufferToList(BufferContext buffer, int offset, int field) =>
42-
fromFlexBuffer(buffer, offset, field) as List<dynamic>?;
49+
List<dynamic>? flexBufferToList(BufferContext buffer, int offset, int field,
50+
{bool skipNull = false}) =>
51+
fromFlexBuffer(buffer, offset, field, skipNullCollectionValues: skipNull)
52+
as List<dynamic>?;
4353

4454
/// Deserializes FlexBuffer bytes to a `List<Map<String, dynamic>>`.
4555
@pragma('vm:prefer-inline')
4656
List<Map<String, dynamic>>? flexBufferToListOfMaps(
4757
BufferContext buffer, int offset, int field) =>
48-
flexBufferToList(buffer, offset, field)?.cast<Map<String, dynamic>>();
58+
flexBufferToList(buffer, offset, field, skipNull: true)
59+
?.cast<Map<String, dynamic>>();
4960

5061
/// Recursively converts a FlexBuffer Reference to a Dart object.
51-
dynamic _convertReference(flex.Reference ref) {
62+
///
63+
/// For [skipNullCollectionValues] see [fromFlexBuffer].
64+
dynamic _convertReference(flex.Reference ref, bool skipNullCollectionValues) {
5265
if (ref.isNull) {
5366
return null;
5467
} else if (ref.isBool) {
@@ -68,11 +81,22 @@ dynamic _convertReference(flex.Reference ref) {
6881
final keyIterator = keys.iterator;
6982
final valueIterator = values.iterator;
7083
while (keyIterator.moveNext() && valueIterator.moveNext()) {
71-
map[keyIterator.current] = _convertReference(valueIterator.current);
84+
final value =
85+
_convertReference(valueIterator.current, skipNullCollectionValues);
86+
if (value != null || !skipNullCollectionValues) {
87+
map[keyIterator.current] = value;
88+
}
7289
}
7390
return map;
7491
} else if (ref.isVector) {
75-
return ref.vectorIterable.map(_convertReference).toList();
92+
final list = <dynamic>[];
93+
for (final element in ref.vectorIterable) {
94+
final value = _convertReference(element, skipNullCollectionValues);
95+
if (value != null || !skipNullCollectionValues) {
96+
list.add(value);
97+
}
98+
}
99+
return list;
76100
}
77101

78102
throw UnsupportedError('Unsupported FlexBuffer value type');

objectbox_test/test/entity_flex.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ class FlexMapEntity {
1212
// Auto-detected Map<String, Object?> - nullable
1313
Map<String, Object?>? flexObject;
1414

15+
// Auto-detected Map<String, Object> (non-nullable values) - nullable
16+
Map<String, Object>? flexObjectNonNull;
17+
1518
// Non-nullable with default empty map
1619
Map<String, dynamic> flexNonNull = {};
1720

@@ -22,6 +25,7 @@ class FlexMapEntity {
2225
FlexMapEntity({
2326
this.flexDynamic,
2427
this.flexObject,
28+
this.flexObjectNonNull,
2529
this.flexNonNull = const {},
2630
this.flexExplicit,
2731
});

objectbox_test/test/flex_test.dart

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,31 +43,39 @@ void main() {
4343
'int': 42,
4444
'double': 3.14,
4545
'bool': false,
46-
'null': null,
4746
};
47+
final testMapWithNull = <String, Object?>{
48+
'null': null,
49+
}..addAll(testMap);
4850
final entity = FlexMapEntity(
49-
flexDynamic: testMap,
50-
flexObject: testMap,
51-
flexNonNull: testMap,
52-
flexExplicit: testMap);
51+
flexDynamic: testMapWithNull,
52+
flexObject: testMapWithNull,
53+
flexObjectNonNull: testMap,
54+
flexNonNull: testMapWithNull,
55+
flexExplicit: testMapWithNull);
5356
final id = box.put(entity);
5457

5558
assertTestMap(Map<String, dynamic> map) {
5659
expect(map['string'], 'hello');
5760
expect(map['int'], 42);
5861
expect(map['double'], 3.14);
5962
expect(map['bool'], false);
63+
}
64+
65+
assertTestMapWithNull(Map<String, dynamic> map) {
66+
assertTestMap(map);
6067
expect(map['null'], isNull);
6168
// The map also returns null if it doesn't contain the key,
6269
// so explicitly check it does.
6370
expect(map.containsKey('null'), true);
6471
}
6572

6673
final read = box.get(id)!;
67-
assertTestMap(read.flexDynamic!);
68-
assertTestMap(read.flexObject!);
69-
assertTestMap(read.flexNonNull);
70-
assertTestMap(read.flexExplicit!);
74+
assertTestMapWithNull(read.flexDynamic!);
75+
assertTestMapWithNull(read.flexObject!);
76+
assertTestMap(read.flexObjectNonNull!);
77+
assertTestMapWithNull(read.flexNonNull);
78+
assertTestMapWithNull(read.flexExplicit!);
7179
});
7280

7381
test('put and get nested map', () {
@@ -317,7 +325,10 @@ void main() {
317325
expect(list.length, 2);
318326
final first = list[0];
319327
expect(first['key1'], 'value1');
320-
expect(first['nullable'], isNull);
328+
// Because the List element type of the properties is defined as
329+
// non-null, null values are skipped, which also affects nested maps
330+
// (and lists).
331+
expect(first.containsKey('nullable'), false);
321332
final second = list[1];
322333
expect(second['key2'], 'value2');
323334
expect((second['nested'] as Map)['a'], 1);

objectbox_test/test/objectbox-model.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -877,7 +877,7 @@
877877
},
878878
{
879879
"id": "21:7625885984770420113",
880-
"lastPropertyId": "5:4318338462051520175",
880+
"lastPropertyId": "6:1851577594233191679",
881881
"name": "FlexMapEntity",
882882
"properties": [
883883
{
@@ -905,6 +905,11 @@
905905
"id": "5:4318338462051520175",
906906
"name": "flexExplicit",
907907
"type": 13
908+
},
909+
{
910+
"id": "6:1851577594233191679",
911+
"name": "flexObjectNonNull",
912+
"type": 13
908913
}
909914
],
910915
"relations": []

0 commit comments

Comments
 (0)