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

RDART-941: Add support for collections in RealmValue #1469

Merged
merged 18 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
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
129 changes: 95 additions & 34 deletions common/lib/src/realm_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
//
////////////////////////////////////////////////////////////////////////////////

import 'dart:ffi';
import 'dart:math';
import 'dart:typed_data';
import 'package:objectid/objectid.dart';
Expand Down Expand Up @@ -157,6 +156,51 @@ abstract class EmbeddedObjectMarker implements RealmObjectBaseMarker {}
/// @nodoc
abstract class AsymmetricObjectMarker implements RealmObjectBaseMarker {}

/// An enum describing the possible types that can be wrapped inside [RealmValue]
enum RealmValueType {
/// The [RealmValue] represents `null`
nullValue,

/// The [RealmValue] represents a [boolean] value
boolean,

/// The [RealmValue] represents a [String] value
string,

/// The [RealmValue] represents an [int] value
int,

/// The [RealmValue] represents a [double] value
double,

/// The [RealmValue] represents a `RealmObject` instance value
object,

/// The [RealmValue] represents an [ObjectId] value
objectId,

/// The [RealmValue] represents a [DateTime] value
dateTime,

/// The [RealmValue] represents a [Decimal128] value
decimal,

/// The [RealmValue] represents an [Uuid] value
uuid,

/// The [RealmValue] represents a binary ([Uint8List]) value
binary,

/// The [RealmValue] represents a `List<RealmValue>`
list,

/// The [RealmValue] represents a `Map<String, RealmValue>`
map;

/// Returns `true` if the enum value represents a collection - i.e. it's [list] or [map].
bool get isCollection => this == RealmValueType.list || this == RealmValueType.map;
}

/// 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,
Expand Down Expand Up @@ -188,60 +232,77 @@ abstract class AsymmetricObjectMarker implements RealmObjectBaseMarker {}
/// ```
class RealmValue {
final Object? value;
Type get type => value.runtimeType;

final RealmValueType type;

/// Casts [value] to [T]. An exception will be thrown if the value is not convertible to [T].
T as<T>() => value as T; // better for code completion

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

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);
const RealmValue.nullValue() : this._(null, RealmValueType.nullValue);
const RealmValue.bool(bool b) : this._(b, RealmValueType.boolean);
const RealmValue.string(String text) : this._(text, RealmValueType.string);
const RealmValue.int(int i) : this._(i, RealmValueType.int);
const RealmValue.double(double d) : this._(d, RealmValueType.double);
// 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 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);
const RealmValue.uuid(Uuid uuid) : this._(uuid);
const RealmValue.uint8List(Uint8List binary) : this._(binary);

/// Will throw [ArgumentError]
const RealmValue.realmObject(RealmObjectMarker o) : this._(o, RealmValueType.object);
const RealmValue.dateTime(DateTime timestamp) : this._(timestamp, RealmValueType.dateTime);
const RealmValue.objectId(ObjectId id) : this._(id, RealmValueType.objectId);
const RealmValue.decimal128(Decimal128 decimal) : this._(decimal, RealmValueType.decimal);
const RealmValue.uuid(Uuid uuid) : this._(uuid, RealmValueType.uuid);
const RealmValue.binary(Uint8List binary) : this._(binary, RealmValueType.binary);
const RealmValue.list(List<RealmValue> list) : this._(list, RealmValueType.list);
const RealmValue.map(Map<String, RealmValue> map) : this._(map, RealmValueType.map);

/// Constructs a RealmValue from an arbitrary object. Collections will be converted recursively as long
/// as all their values are compatible.
///
/// Throws [ArgumentError] if any of the values inside the graph cannot be stored in a [RealmValue].
factory RealmValue.from(Object? object) {
if (object == null ||
object is bool ||
object is String ||
object is int ||
object is Float ||
object is double ||
object is RealmObjectMarker ||
object is DateTime ||
object is ObjectId ||
object is Decimal128 ||
object is Uuid ||
object is Uint8List) {
return RealmValue._(object);
} else {
throw ArgumentError.value(object, 'object', 'Unsupported type');
}
return switch (object) {
null => RealmValue.nullValue(),
bool b => RealmValue.bool(b),
String text => RealmValue.string(text),
int i => RealmValue.int(i),
double d => RealmValue.double(d),
RealmObjectMarker o => RealmValue.realmObject(o),
DateTime d => RealmValue.dateTime(d),
ObjectId id => RealmValue.objectId(id),
Decimal128 decimal => RealmValue.decimal128(decimal),
Uuid uuid => RealmValue.uuid(uuid),
Uint8List binary => RealmValue.binary(binary),
Map<String, RealmValue> d => RealmValue.map(d),
Map<String, dynamic> d => RealmValue.map(d.map((key, value) => MapEntry(key, RealmValue.from(value)))),
List<RealmValue> l => RealmValue.list(l),
List<dynamic> l => RealmValue.list(l.map((o) => RealmValue.from(o)).toList()),
Iterable<RealmValue> i => RealmValue.list(i.toList()),
Iterable<dynamic> i => RealmValue.list(i.map((o) => RealmValue.from(o)).toList()),
_ => throw ArgumentError.value(object.runtimeType, 'object', 'Unsupported type'),
};
}

@override
operator ==(Object? other) {
// We always return false when comparing two RealmValue collections.
if (type.isCollection) {
return false;
}

if (other is RealmValue) {
if (value is Uint8List && other.value is Uint8List) {
return ListEquality().equals(value as Uint8List, other.value as Uint8List);
}

return value == other.value;
return type == other.type && value == other.value;
}

return value == other;
}

@override
int get hashCode => value.hashCode;
int get hashCode => Object.hash(type, value);

@override
String toString() => 'RealmValue($value)';
Expand Down
47 changes: 36 additions & 11 deletions lib/src/list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ class ManagedRealmList<T extends Object?> with RealmEntity, ListMixin<T> impleme
if (element is RealmObjectBase && !element.isManaged) {
throw RealmStateError('Cannot call remove on a managed list with an element that is an unmanaged object');
}

final index = indexOf(element);
if (index < 0) {
return false;
Expand Down Expand Up @@ -173,6 +174,17 @@ class ManagedRealmList<T extends Object?> with RealmEntity, ListMixin<T> impleme
if (element is RealmObjectBase && !element.isManaged) {
throw RealmStateError('Cannot call indexOf on a managed list with an element that is an unmanaged object');
}

if (element is RealmValue) {
if (element.type.isCollection) {
return -1;
}

if (element.value is RealmObjectBase && !(element.value as RealmObjectBase).isManaged) {
return -1;
}
}

if (start < 0) start = 0;
final index = realmCore.listFind(this, element);
return index < start ? -1 : index; // to align with dart list semantics
Expand Down Expand Up @@ -205,7 +217,13 @@ class ManagedRealmList<T extends Object?> with RealmEntity, ListMixin<T> impleme
}

class UnmanagedRealmList<T extends Object?> extends collection.DelegatingList<T> with RealmEntity implements RealmList<T> {
UnmanagedRealmList([Iterable<T>? items]) : super(List<T>.from(items ?? <T>[]));
final List<T> _base;

UnmanagedRealmList([Iterable<T>? items]) : this._(List<T>.from(items ?? <T>[]));

UnmanagedRealmList._(List<T> items)
: _base = items,
super(items);

@override
RealmObjectMetadata? get _metadata => throw RealmException("Unmanaged lists don't have metadata associated with them.");
Expand All @@ -224,6 +242,14 @@ class UnmanagedRealmList<T extends Object?> extends collection.DelegatingList<T>

@override
Stream<RealmListChanges<T>> get changes => throw RealmStateError("Unmanaged lists don't support changes");

@override
bool operator ==(Object? other) {
return _base == other;
}

@override
int get hashCode => _base.hashCode;
}

// The query operations on lists, only work for list of objects (core restriction),
Expand Down Expand Up @@ -264,6 +290,10 @@ extension RealmListInternal<T extends Object?> on RealmList<T> {

RealmObjectMetadata? get metadata => asManaged()._metadata;

static RealmList<T> createFromList<T>(List<T> items) {
return UnmanagedRealmList._(items);
}

static RealmList<T> create<T extends Object?>(RealmListHandle handle, Realm realm, RealmObjectMetadata? metadata) => RealmList<T>._(handle, realm, metadata);

static void setValue(RealmListHandle handle, Realm realm, int index, Object? value, {bool update = false, bool insert = false}) {
Expand All @@ -288,19 +318,14 @@ extension RealmListInternal<T extends Object?> on RealmList<T> {
return;
}

if (value is RealmValue) {
value = value.value;
if (value is RealmValue && value.type.isCollection) {
realmCore.listAddCollectionAt(handle, realm, index, value, insert || index >= length);
return;
}

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

if (insert || index >= length) {
realmCore.listInsertElementAt(handle, index, value);
} else {
realmCore.listSetElementAt(handle, index, value);
}
realmCore.listAddElementAt(handle, index, value, insert || index >= length);
} on Exception catch (e) {
throw RealmException("Error setting value at index $index. Error: $e");
}
Expand Down
37 changes: 29 additions & 8 deletions lib/src/map.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,13 @@ abstract class RealmMap<T extends Object?> with RealmEntity implements MapBase<S
}

class UnmanagedRealmMap<T extends Object?> extends collection.DelegatingMap<String, T> with RealmEntity implements RealmMap<T> {
UnmanagedRealmMap([Map<String, T>? items]) : super(Map<String, T>.from(items ?? <String, T>{}));
final Map<String, T> _base;

UnmanagedRealmMap([Map<String, T>? items]) : this._(Map<String, T>.from(items ?? <String, T>{}));

UnmanagedRealmMap._(Map<String, T> items)
: _base = items,
super(items);

@override
bool get isValid => true;
Expand All @@ -59,6 +65,14 @@ class UnmanagedRealmMap<T extends Object?> extends collection.DelegatingMap<Stri

@override
Stream<RealmMapChanges<T>> get changes => throw RealmStateError("Unmanaged maps don't support changes");

@override
bool operator ==(Object? other) {
return _base == other;
}

@override
int get hashCode => _base.hashCode;
}

class ManagedRealmMap<T extends Object?> with RealmEntity, MapMixin<String, T> implements RealmMap<T> {
Expand Down Expand Up @@ -172,8 +186,14 @@ class ManagedRealmMap<T extends Object?> with RealmEntity, MapMixin<String, T> i
return false;
}

if (value is RealmValue && value.value is RealmObjectBase && !(value.value as RealmObjectBase).isManaged) {
return false;
if (value is RealmValue) {
if (value.value is RealmObjectBase && !(value.value as RealmObjectBase).isManaged) {
return false;
}

if (value.type.isCollection) {
return false;
}
}

return realmCore.mapContainsValue(this, value);
Expand Down Expand Up @@ -246,6 +266,8 @@ extension RealmMapInternal<T extends Object?> on RealmMap<T> {

RealmObjectMetadata? get metadata => asManaged()._metadata;

static RealmMap<T> createFromMap<T>(Map<String, T> map) => UnmanagedRealmMap._(map);

static RealmMap<T> create<T extends Object?>(RealmMapHandle handle, Realm realm, RealmObjectMetadata? metadata) =>
ManagedRealmMap<T>._(handle, realm, metadata);

Expand All @@ -261,13 +283,12 @@ extension RealmMapInternal<T extends Object?> on RealmMap<T> {
return;
}

if (value is RealmValue) {
value = value.value;
if (value is RealmValue && value.type.isCollection) {
realmCore.mapInsertCollection(handle, realm, key, value);
return;
}

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

realmCore.mapInsertValue(handle, key, value);
} on Exception catch (e) {
Expand Down
20 changes: 10 additions & 10 deletions lib/src/native/realm_bindings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8608,7 +8608,7 @@ class RealmLibrary {
late final _realm_set_clear =
_realm_set_clearPtr.asFunction<bool Function(ffi.Pointer<realm_set_t>)>();

bool realm_set_dictionary(
ffi.Pointer<realm_dictionary_t> realm_set_dictionary(
ffi.Pointer<realm_object_t> arg0,
int arg1,
) {
Expand All @@ -8620,10 +8620,11 @@ class RealmLibrary {

late final _realm_set_dictionaryPtr = _lookup<
ffi.NativeFunction<
ffi.Bool Function(ffi.Pointer<realm_object_t>,
ffi.Pointer<realm_dictionary_t> Function(ffi.Pointer<realm_object_t>,
realm_property_key_t)>>('realm_set_dictionary');
late final _realm_set_dictionary = _realm_set_dictionaryPtr
.asFunction<bool Function(ffi.Pointer<realm_object_t>, int)>();
late final _realm_set_dictionary = _realm_set_dictionaryPtr.asFunction<
ffi.Pointer<realm_dictionary_t> Function(
ffi.Pointer<realm_object_t>, int)>();

/// Create an embedded object in a given property.
///
Expand Down Expand Up @@ -8855,7 +8856,7 @@ class RealmLibrary {
bool Function(ffi.Pointer<realm_object_t>, int, ffi.Pointer<ffi.Char>)>();

/// Create a collection in a given Mixed property.
bool realm_set_list(
ffi.Pointer<realm_list_t> realm_set_list(
ffi.Pointer<realm_object_t> arg0,
int arg1,
) {
Expand All @@ -8867,10 +8868,10 @@ class RealmLibrary {

late final _realm_set_listPtr = _lookup<
ffi.NativeFunction<
ffi.Bool Function(ffi.Pointer<realm_object_t>,
ffi.Pointer<realm_list_t> Function(ffi.Pointer<realm_object_t>,
realm_property_key_t)>>('realm_set_list');
late final _realm_set_list = _realm_set_listPtr
.asFunction<bool Function(ffi.Pointer<realm_object_t>, int)>();
late final _realm_set_list = _realm_set_listPtr.asFunction<
ffi.Pointer<realm_list_t> Function(ffi.Pointer<realm_object_t>, int)>();

/// Install the default logger
void realm_set_log_callback(
Expand Down Expand Up @@ -12714,8 +12715,7 @@ abstract class realm_value_type {
static const int RLM_TYPE_LINK = 10;
static const int RLM_TYPE_UUID = 11;
static const int RLM_TYPE_LIST = 12;
static const int RLM_TYPE_SET = 13;
static const int RLM_TYPE_DICTIONARY = 14;
static const int RLM_TYPE_DICTIONARY = 13;
}

final class realm_version_id extends ffi.Struct {
Expand Down
Loading
Loading