Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Support RealmValue (aka mixed) #1051

Merged
merged 25 commits into from
Dec 19, 2022
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
570d453
Support RealmValue (aka Mixed) in generator
nielsenko Nov 18, 2022
1089abc
Add generator test of RealmValue
nielsenko Nov 18, 2022
635d5b5
export RealmValue
nielsenko Nov 18, 2022
7863668
fix core
nielsenko Nov 18, 2022
b162451
RealmValue tests (wip)
nielsenko Nov 19, 2022
dfbf3b0
Fix RealmCoreAccessor.get
nielsenko Nov 30, 2022
f49b875
Add RealmValue.from factory
nielsenko Nov 30, 2022
f3ea39f
More tests
nielsenko Nov 30, 2022
8101c46
RealmValue.value now nullable
nielsenko Nov 30, 2022
2151339
Refactor nullability
nielsenko Dec 8, 2022
6b3cd60
Support RealmList<RealmValue>
nielsenko Dec 8, 2022
95204e2
Better test
nielsenko Dec 9, 2022
abc10fb
Fix generator tests
nielsenko Dec 9, 2022
c500274
Test error message on nullable realm values
nielsenko Dec 12, 2022
a1463cf
Update CHANGELOG
nielsenko Dec 12, 2022
b392671
Support realm object in RealmValue + extra tests
nielsenko Dec 13, 2022
502d5c2
cleanup before review
nielsenko Dec 13, 2022
0dcdb77
Address PR feedback
nielsenko Dec 14, 2022
8530dc7
Add tests for @Indexed() and @PrimaryKey() on RealmValue
nielsenko Dec 15, 2022
d2298ea
Disallow embedded objects in RealmValue (capture in type system)
nielsenko Dec 15, 2022
6ab7a1c
Simplify initializer generation
nielsenko Dec 15, 2022
4d8bf20
Remove some dead code
nielsenko Dec 15, 2022
634a8f1
Add test for unknown object type in RealmValue, but currently disable…
nielsenko Dec 15, 2022
9acd3a2
Don't add AsymmetricObjectMarker yet
nielsenko Dec 15, 2022
cd47f1d
Avoid assertion crash on realm_get_object if classKey is unknown to o…
nielsenko Dec 16, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* Add List.move extension method that moves an element from one index to another. Delegates to ManagedRealmList.move for managed lists. This allows notifications to correctly report moves, as opposed to reporting moves as deletes + inserts. ([#1037](https://github.com/realm/realm-dart/issues/1037))
* Support setting `shouldDeleteIfMigrationNeeded` when creating a `Configuration.local`. ([#1049](https://github.com/realm/realm-dart/issues/1049))
* Add `unknown` error code to all SyncErrors: `SyncSessionErrorCode.unknown`, `SyncConnectionErrorCode.unknown`, `SyncClientErrorCode.unknown`, `GeneralSyncErrorCode.unknown`. Use `unknown` error code instead of throwing a RealmError. ([#1052](https://github.com/realm/realm-dart/pull/1052))
* Add support for `RealmValue` data type. This new type can represent any valid Realm data type, including objects. Lists of `RealmValue` are also supported, but `RealmValue` itself cannot contain collections. Please note that a property of type `RealmValue` cannot be nullable, but can contain null, represented by the value `RealmValue.nullValue()`. ([#1051](https://github.com/realm/realm-dart/pull/1051))

### Fixed
* Support mapping into `SyncSessionErrorCode` for "Compensating write" with error code 231. ([#1022](https://github.com/realm/realm-dart/pull/1022))
Expand Down
103 changes: 77 additions & 26 deletions common/lib/src/realm_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
////////////////////////////////////////////////////////////////////////////////

import 'dart:ffi';
import 'dart:typed_data';
import 'package:objectid/objectid.dart';
import 'package:sane_uuid/uuid.dart';

Expand Down Expand Up @@ -46,7 +45,7 @@ enum RealmPropertyType {
_3, // ignore: unused_field, constant_identifier_names
binary,
_5, // ignore: unused_field, constant_identifier_names
mixed,
mixed(Mapping<RealmValue>(indexable: true)),
_7, // ignore: unused_field, constant_identifier_names
timestamp(Mapping<DateTime>(indexable: true)),
float,
Expand Down Expand Up @@ -104,35 +103,87 @@ class RealmStateError extends StateError implements RealmError {
class Decimal128 {} // TODO Support decimal128 datatype https://github.com/realm/realm-dart/issues/725

/// @nodoc
class RealmObjectBaseMarker {}
abstract class RealmObjectMarker {}

/// @nodoc
class RealmObjectMarker extends RealmObjectBaseMarker {}

/// @nodoc
class EmbeddedObjectMarker extends RealmObjectBaseMarker {}

// Union type
/// @nodoc
class RealmAny {
final dynamic value;
/// A type that can represent any valid realm data type, except collections and embedded objects.
///
/// You can use [RealmValue] to declare fields on realm models, in which case it must be non-nullable,
/// but it can wrap a null-value. List of [RealmValue] (`List<RealmValue>`) are also legal.
///
/// [RealmValue] fields can be [Indexed]
///
/// ```dart
/// @RealmModel()
/// class _AnythingGoes {
/// @Indexed()
/// late RealmValue any;
/// late List<RealmValue> manyAny;
/// }
///
/// void main() {
/// final realm = Realm(Configuration.local([AnythingGoes.schema]));
/// realm.write(() {
/// final something = realm.add(AnythingGoes(any: RealmValue.string('text')));
/// something.manyAny.addAll([
/// null,
/// true,
/// 'text',
/// 42,
/// 3.14,
/// ].map(RealmValue.from));
/// });
/// }
/// ```
class RealmValue {
nielsenko marked this conversation as resolved.
Show resolved Hide resolved
final Object? value;
Type get type => value.runtimeType;
nirinchev marked this conversation as resolved.
Show resolved Hide resolved
T as<T>() => value as T; // better for code completion

// This is private, so user cannot accidentally construct an invalid instance
const RealmAny._(this.value);

const RealmAny.bool(bool b) : this._(b);
const RealmAny.string(String text) : this._(text);
const RealmAny.int(int i) : this._(i);
const RealmAny.float(Float f) : this._(f);
const RealmAny.double(double d) : this._(d);
const RealmAny.uint8List(Uint8List data) : this._(data);
const RealmValue._(this.value);

const RealmValue.nullValue() : this._(null);
const RealmValue.bool(bool b) : this._(b);
const RealmValue.string(String text) : this._(text);
const RealmValue.int(int i) : this._(i);
const RealmValue.double(double d) : this._(d);
// TODO: RealmObjectMarker introduced to avoid dependency inversion. It would be better if we could use RealmObject directly. https://github.com/realm/realm-dart/issues/701
const RealmAny.realmObject(RealmObjectBaseMarker o) : this._(o);
const RealmAny.dateTime(DateTime timestamp) : this._(timestamp);
const RealmAny.objectId(ObjectId id) : this._(id);
const RealmAny.decimal128(Decimal128 decimal) : this._(decimal);
const RealmAny.uuid(Uuid uuid) : this._(uuid);
const RealmValue.realmObject(RealmObjectMarker o) : this._(o);
const RealmValue.dateTime(DateTime timestamp) : this._(timestamp);
const RealmValue.objectId(ObjectId id) : this._(id);
// const RealmValue.decimal128(Decimal128 decimal) : this._(decimal); // not supported yet
const RealmValue.uuid(Uuid uuid) : this._(uuid);

/// Will throw [ArgumentError]
factory RealmValue.from(Object? o) {
if (o == null ||
o is bool ||
o is String ||
o is int ||
o is Float ||
o is double ||
o is RealmObjectMarker ||
o is DateTime ||
o is ObjectId ||
// o is Decimal128 || // not supported yet
o is Uuid) {
return RealmValue._(o);
} else {
throw ArgumentError.value(o, 'o', 'Unsupported type');
}
}

@override
operator ==(Object? other) {
if (other is RealmValue) {
return value == other.value;
} else {
nielsenko marked this conversation as resolved.
Show resolved Hide resolved
return value == other;
blagoev marked this conversation as resolved.
Show resolved Hide resolved
}
}

@override
int get hashCode => value.hashCode;
nielsenko marked this conversation as resolved.
Show resolved Hide resolved
}

/// The category of a [SyncError].
Expand Down
4 changes: 2 additions & 2 deletions generator/lib/src/dart_type_ex.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import 'type_checkers.dart';
extension DartTypeEx on DartType {
bool isExactly<T>() => TypeChecker.fromRuntime(T).isExactlyType(this);

bool get isRealmAny => const TypeChecker.fromRuntime(RealmAny).isAssignableFromType(this);
bool get isRealmValue => const TypeChecker.fromRuntime(RealmValue).isAssignableFromType(this);
bool get isRealmCollection => realmCollectionType != RealmCollectionType.none;
bool get isRealmModel => element2 != null ? realmModelChecker.annotationsOfExact(element2!).isNotEmpty : false;

Expand Down Expand Up @@ -108,7 +108,7 @@ extension DartTypeEx on DartType {
if (isDartCoreBool) return RealmPropertyType.bool;
if (isDartCoreString) return RealmPropertyType.string;
if (isExactly<Uint8List>()) return RealmPropertyType.binary;
if (isRealmAny) return RealmPropertyType.mixed;
if (isRealmValue) return RealmPropertyType.mixed;
if (isExactly<DateTime>()) return RealmPropertyType.timestamp;
if (isExactly<Float>()) return RealmPropertyType.float;
if (isDartCoreNum || isDartCoreDouble) return RealmPropertyType.double;
Expand Down
16 changes: 13 additions & 3 deletions generator/lib/src/field_element_ex.dart
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ extension FieldElementEx on FieldElement {
// Check for as-of-yet unsupported type
if (type.isDartCoreSet || //
type.isDartCoreMap ||
type.isRealmAny ||
type.isExactly<Decimal128>()) {
throw RealmInvalidGenerationSourceError(
'Field type not supported yet',
Expand Down Expand Up @@ -134,12 +133,12 @@ extension FieldElementEx on FieldElement {

// Validate indexes
if ((((primaryKey ?? indexed) != null) && !(type.realmType?.mapping.indexable ?? false)) || //
(primaryKey != null && type.isDartCoreBool)) {
(primaryKey != null && (type.isDartCoreBool || type.isExactly<RealmValue>()))) {
nielsenko marked this conversation as resolved.
Show resolved Hide resolved
final file = span!.file;
final annotation = (primaryKey ?? indexed)!.annotation;
final listOfValidTypes = RealmPropertyType.values //
.map((t) => t.mapping)
.where((m) => m.indexable && (m.type != bool || primaryKey == null))
.where((m) => m.indexable && ((m.type != bool && m.type != RealmValue) || primaryKey == null))
.map((m) => m.type);

throw RealmInvalidGenerationSourceError(
Expand Down Expand Up @@ -262,6 +261,17 @@ extension FieldElementEx on FieldElement {
);
}
}

// Validate mixed (RealmValue)
else if (realmType == RealmPropertyType.mixed && type.isNullable) {
throw RealmInvalidGenerationSourceError(
'RealmValue fields cannot be nullable',
primarySpan: typeSpan(file),
primaryLabel: '$modelTypeName is nullable',
todo: 'Change type to ${modelType.asNonNullable}',
element: this,
);
}
}

return RealmFieldInfo(
Expand Down
3 changes: 2 additions & 1 deletion generator/lib/src/realm_field_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ class RealmFieldInfo {
bool get isRealmCollection => type.isRealmCollection;
bool get isLate => fieldElement.isLate;
bool get hasDefaultValue => fieldElement.hasInitializer;
bool get optional => type.basicType.isNullable;
bool get optional => type.basicType.isNullable || realmType == RealmPropertyType.mixed;
bool get isRequired => !(hasDefaultValue || optional);
bool get isRealmBacklink => realmType == RealmPropertyType.linkingObjects;
bool get isMixed => realmType == RealmPropertyType.mixed;
bool get isComputed => isRealmBacklink; // only computed, so far

String get name => fieldElement.name;
Expand Down
7 changes: 5 additions & 2 deletions generator/lib/src/realm_model_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,13 @@ class RealmModelInfo {
yield* required.map((f) => '${f.mappedTypeName} ${f.name},');

final notRequired = allSettable.where((f) => !f.isRequired && !f.isPrimaryKey);
final collections = fields.where((f) => f.type.isRealmCollection).toList();
final collections = fields.where((f) => f.isRealmCollection).toList();
if (notRequired.isNotEmpty || collections.isNotEmpty) {
yield '{';
yield* notRequired.map((f) => '${f.mappedTypeName} ${f.name}${f.hasDefaultValue ? ' = ${f.fieldElement.initializerExpression}' : ''},');
yield* notRequired.map(
(f) =>
'${f.mappedTypeName} ${f.name}${f.hasDefaultValue ? ' = ${f.fieldElement.initializerExpression}' : (f.isMixed ? ' = const RealmValue.nullValue()' : '')},',
nielsenko marked this conversation as resolved.
Show resolved Hide resolved
);
yield* collections.map((c) => 'Iterable<${c.type.basicMappedName}> ${c.name} = const [],');
yield '}';
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Realm only supports the @Indexed() annotation on fields of type
int, bool, String, DateTime, ObjectId, Uuid
int, bool, String, RealmValue, DateTime, ObjectId, Uuid
as well as their nullable versions

in: asset:pkg/test/error_test_data/not_an_indexable_type.dart:8:8
Expand Down
8 changes: 8 additions & 0 deletions generator/test/error_test_data/nullable_realm_value.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'package:realm_common/realm_common.dart';

//part 'nullable_list.g.dart';

@RealmModel()
class _Bad {
RealmValue? wrong;
}
12 changes: 12 additions & 0 deletions generator/test/error_test_data/nullable_realm_value.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
RealmValue fields cannot be nullable

in: asset:pkg/test/error_test_data/nullable_realm_value.dart:7:3
5 │ @RealmModel()
6 │ class _Bad {
│ ━━━━ in realm model for 'Bad'
7 │ RealmValue? wrong;
│ ^^^^^^^^^^^ RealmValue? is nullable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be more descriptive with these error messages? In my experience people don't understand short errors like these and reach out to support to ask them what they mean. We could rephrase that to something like: 'wrong' is declared as 'RealmValue?' which is not allowed. 'RealmValue' can already represent null with 'RealmValue.nullValue()', so you don't need to declare the property itself as nullable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general I agree that we should have long descriptive error messages, but we need to agree on the team. Previously I was asked to simplify them.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would vote for short and clear messages. We could reword this to be more clear, but long error messages stand in the way after a while. So imo, this could be

`RealmValue?` is declared nullable. Use `RealmValue` or `RealmValue myfield = RealmValue.nullValue()` to assign a default null value.

Copy link
Contributor Author

@nielsenko nielsenko Dec 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it gets in the way. The short version is at the top, the rest you can read if you are confused.

But I don't think this is specific to this PR. If we want more verbose error messages, I think we should change a lot of them.

Change type to RealmValue

4 changes: 3 additions & 1 deletion generator/test/good_test_data/all_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ class _Bar {
@Indexed()
late bool aBool, another; // both are indexed!
var data = Uint8List(16);
// late RealmAny any; // not supported yet
@MapTo('tidspunkt')
@Indexed()
var timestamp = DateTime.now();
Expand All @@ -39,6 +38,9 @@ class _Bar {

@Backlink(#bar)
late Iterable<_Foo> foos;

late RealmValue any;
late List<RealmValue> manyAny;
}

@RealmModel()
Expand Down
29 changes: 24 additions & 5 deletions generator/test/good_test_data/all_types.expected
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,16 @@ class Bar extends _Bar with RealmEntity, RealmObjectBase, RealmObject {
String name,
bool aBool,
bool another,
ObjectId objectId,
ObjectId objectId,
Uuid uuid, {
Uint8List data = Uint8List(16),
DateTime timestamp = DateTime.now(),
double aDouble = 0.0,
Foo? foo,
String? anOptionalString,
RealmValue any = const RealmValue.nullValue(),
Iterable<int> list = const [],
Iterable<RealmValue> manyAny = const [],
}) {
if (!_defaultsSet) {
_defaultsSet = RealmObjectBase.setDefaults<Bar>({
Expand All @@ -82,7 +84,10 @@ class Bar extends _Bar with RealmEntity, RealmObjectBase, RealmObject {
RealmObjectBase.set(this, 'objectId', objectId);
RealmObjectBase.set(this, 'uuid', uuid);
RealmObjectBase.set(this, 'anOptionalString', anOptionalString);
RealmObjectBase.set(this, 'any', any);
RealmObjectBase.set<RealmList<int>>(this, 'list', RealmList<int>(list));
RealmObjectBase.set<RealmList<RealmValue>>(
this, 'manyAny', RealmList<RealmValue>(manyAny));
}

Bar._();
Expand Down Expand Up @@ -132,8 +137,7 @@ class Bar extends _Bar with RealmEntity, RealmObjectBase, RealmObject {
set objectId(ObjectId value) => RealmObjectBase.set(this, 'objectId', value);

@override
Uuid get uuid =>
RealmObjectBase.get<Uuid>(this, 'uuid') as Uuid;
Uuid get uuid => RealmObjectBase.get<Uuid>(this, 'uuid') as Uuid;
@override
set uuid(Uuid value) => RealmObjectBase.set(this, 'uuid', value);

Expand All @@ -150,11 +154,24 @@ class Bar extends _Bar with RealmEntity, RealmObjectBase, RealmObject {
set anOptionalString(String? value) =>
RealmObjectBase.set(this, 'anOptionalString', value);

@override
RealmValue get any =>
RealmObjectBase.get<RealmValue>(this, 'any') as RealmValue;
@override
set any(RealmValue value) => RealmObjectBase.set(this, 'any', value);

@override
RealmList<RealmValue> get manyAny =>
RealmObjectBase.get<RealmValue>(this, 'manyAny') as RealmList<RealmValue>;
@override
set manyAny(covariant RealmList<RealmValue> value) =>
throw RealmUnsupportedSetError();

@override
RealmResults<Foo> get foos =>
RealmObjectBase.get<Foo>(this, 'foos') as RealmResults<Foo>;
@override
set foos(covariant RealmResults<Foo> value) =>
set foos(covariant RealmResults<Foo> value) =>
throw RealmUnsupportedSetError();

@override
Expand Down Expand Up @@ -184,6 +201,9 @@ class Bar extends _Bar with RealmEntity, RealmObjectBase, RealmObject {
collectionType: RealmCollectionType.list),
SchemaProperty('anOptionalString', RealmPropertyType.string,
optional: true, indexed: true),
SchemaProperty('any', RealmPropertyType.mixed, optional: true),
SchemaProperty('manyAny', RealmPropertyType.mixed,
optional: true, collectionType: RealmCollectionType.list),
SchemaProperty('foos', RealmPropertyType.linkingObjects,
linkOriginProperty: 'bar',
collectionType: RealmCollectionType.list,
Expand Down Expand Up @@ -263,4 +283,3 @@ class PrimitiveTypes extends _PrimitiveTypes
]);
}
}

23 changes: 19 additions & 4 deletions lib/src/list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,23 @@ class ManagedRealmList<T extends Object?> with RealmEntity, ListMixin<T> impleme
}

try {
final value = realmCore.listGetElementAt(this, index);

var value = realmCore.listGetElementAt(this, index);
if (value is RealmObjectHandle) {
return realm.createObject(T, value, _metadata!) as T;
late RealmObjectMetadata targetMetadata;
late Type type;
if (T == RealmValue) {
final tuple = realm.metadata.getByClassKey(realmCore.getClassKey(value));
type = tuple.item1;
targetMetadata = tuple.item2;
Comment on lines +119 to +121
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this handles the case where you open a Realm with an incomplete schema that contains RealmValue properties linking to tables not in the user schema.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add a test for this case.

What should be the expected outcome? Do we error out, or use dynamic realm objects?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should return dynamic objects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm .. added a test, but it crashes in

realmCore.getProperty(object, propertyMeta.key);

At least the c-api don't support this

} else {
targetMetadata = _metadata!;
type = T;
}
value = realm.createObject(type, value, targetMetadata);
}
if (T == RealmValue) {
value = RealmValue.from(value);
}

return value as T;
} on Exception catch (e) {
throw RealmException("Error getting value at index $index. Error: $e");
Expand Down Expand Up @@ -277,6 +288,10 @@ extension RealmListInternal<T extends Object?> on RealmList<T> {
return;
}

if (value is RealmValue) {
value = value.value;
}

if (value is RealmObject && !value.isManaged) {
realm.add<RealmObject>(value, update: update);
}
Expand Down
Loading