Skip to content

Commit

Permalink
Support RealmValue (aka mixed) (#1051)
Browse files Browse the repository at this point in the history
* Support RealmValue (aka Mixed) in generator

* Add generator test of RealmValue

* export RealmValue

* fix core

* RealmValue tests (wip)

* Fix RealmCoreAccessor.get

* Add RealmValue.from factory

* More tests

* RealmValue.value now nullable

* Refactor nullability

* Support RealmList<RealmValue>

* Better test

* Fix generator tests

* Test error message on nullable realm values

* Update CHANGELOG

* Support realm object in RealmValue + extra tests

* cleanup before review

* Address PR feedback

* Add tests for @indexed() and @PrimaryKey() on RealmValue

* Disallow embedded objects in RealmValue (capture in type system)

* Simplify initializer generation

* Remove some dead code

* Add test for unknown object type in RealmValue, but currently disabled as it crashes

* Don't add AsymmetricObjectMarker yet

* Avoid assertion crash on realm_get_object if classKey is unknown to object store
  • Loading branch information
nielsenko authored Dec 19, 2022
1 parent 4bba93a commit 391e4ca
Show file tree
Hide file tree
Showing 22 changed files with 611 additions and 65 deletions.
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: 81 additions & 22 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,95 @@ 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 RealmObjectBaseMarker {}

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

/// @nodoc
class EmbeddedObjectMarker extends RealmObjectBaseMarker {}
abstract class EmbeddedObjectMarker implements 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 {
final Object? value;
Type get type => value.runtimeType;
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;
}
return value == other;
}

@override
int get hashCode => value.hashCode;

@override
String toString() => 'RealmValue.from($value)';
}

/// 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>()))) {
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
10 changes: 9 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 All @@ -65,6 +66,13 @@ class RealmFieldInfo {

String get mappedTypeName => fieldElement.mappedTypeName;

String get initializer {
if (type.isDartCoreList) return ' = const []';
if (isMixed) return ' = const RealmValue.nullValue()';
if (hasDefaultValue) return ' = ${fieldElement.initializerExpression}';
return ''; // no initializer
}

RealmCollectionType get realmCollectionType => type.realmCollectionType;

Iterable<String> toCode() sync* {
Expand Down
6 changes: 3 additions & 3 deletions generator/lib/src/realm_model_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ 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* collections.map((c) => 'Iterable<${c.type.basicMappedName}> ${c.name} = const [],');
yield* notRequired.map((f) => '${f.mappedTypeName} ${f.name}${f.initializer},');
yield* collections.map((c) => 'Iterable<${c.type.basicMappedName}> ${c.name}${c.initializer},');
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
Change type to RealmValue

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'package:realm_common/realm_common.dart';

@RealmModel()
@MapTo('Bad')
class _Foo {
@PrimaryKey()
late RealmValue bad;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Realm only supports the @PrimaryKey() annotation on fields of type
int, String, DateTime, ObjectId, Uuid
as well as their nullable versions

in: asset:pkg/test/error_test_data/realm_value_not_allowed_as_primary_key.dart:7:8
3 │ @RealmModel()
4 │ @MapTo('Bad')
5 │ class _Foo {
│ ━━━━ in realm model for 'Foo'
6 │ @PrimaryKey()
7 │ late RealmValue bad;
│ ^^^^^^^^^^ RealmValue is not a valid type here
Change the type of 'bad', or remove the @PrimaryKey() annotation

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
]);
}
}

Loading

0 comments on commit 391e4ca

Please sign in to comment.