From 7c25a1d68a266a9d2be326487b7186ae47bd4bb2 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 7 Mar 2024 15:15:08 +0100 Subject: [PATCH] Collections of Mixed (#3441) * Added stub * Stubs with comments * Notes * Various fixes * Updated core * Finished stub * Removed comment [skip-ci] * Removed unused * Added stub for lists in lists * Fixed error with null and added basic test with all types * Improved base testing for list, added support for add/insert/set and equality checking * Improved test and removed useless content * Added tests * Small fixes * Small fixes * Added common method for creating a list and adding elements * Fixed error [skip-ci] * Added set and dictionary to primitive value * Various improvements * Added conversion per list * Fixed Operator * Added missing conversion methods * Corrected wrapper * Added test helper method for realm value types and removed unused reflextion extension method * Added missing types for marshaling * Small improvement in operator * Small corrections * Added error for sets adding collections * Trting to uniform access * Created common method for collections * Various improvements [skip-ci] * Fixed dictionary and added more tests * Small corrections * Improved tests for dictionaries * Small fixes * Simplified set collection on object * Added test for dynamic and simplified collection generation * Updated db * Updated changelog [skip-ci] * Updated another db * Updated db * Updated changelog * Test * Fix to generated files * Test * Small improvement * Added field offset * Working version * Small renaming * Reordering * Working version * Solution 1 * Final version of the RealmValue explicit layout * Added changelog * Removed set from RealmValue * Continuing removal of set * Simplified CreateInternalCollectionAndPopulate * Ulterior removal of set and added method parameter name * Removed other set-related things, added implicit method from array of RealmValue * Changed name to method * Added explicit sequence equals * Unified set_collection and add_collection for dictionary * Moved _intValue out of primitiveValue * Removed unnecessary structs * Changed return value * Small corrections * Fixed changelog * Updated core and a couple of fixes * Small corrections * Updated dbs * Updated core to 14.0.1 and changelog * Added test for collection indexes * Updated changelog * Fixed realm studio version * Updated guids to v24 * Corrected methods * Corrected changelog and wrapper * Added comments * Corrected changelog * Corrected changelog * Updated core * Updated changelog * Used correct core version * Fixed changelog * Fixed changelog * Added missing tests * Fixed tests * [RNET-1092] Added support for indexed RealmValue (#3544) * Added support for indexed RealmValue * Fixed changelog * Corrected indexable property types in schema * Added missing newline in the changelog * Testing fix for CI --- CHANGELOG.md | 15 + Realm/Realm.SourceGenerator/InfoClasses.cs | 1 + .../PropertyDefinitionExtensions.cs | 1 + Realm/Realm/DatabaseTypes/RealmDictionary.cs | 17 + Realm/Realm/DatabaseTypes/RealmList.cs | 26 +- Realm/Realm/DatabaseTypes/RealmSet.cs | 5 + Realm/Realm/DatabaseTypes/RealmValue.cs | 156 ++- Realm/Realm/DatabaseTypes/RealmValueType.cs | 15 + .../Realm/Extensions/CollectionExtensions.cs | 11 +- .../Realm/Extensions/RealmValueExtensions.cs | 9 + .../Realm/Extensions/ReflectionExtensions.cs | 2 - Realm/Realm/Handles/CollectionHandleBase.cs | 13 + Realm/Realm/Handles/DictionaryHandle.cs | 41 +- Realm/Realm/Handles/ListHandle.cs | 40 +- Realm/Realm/Handles/ObjectHandle.cs | 23 + Realm/Realm/Helpers/CollectionHelpers.cs | 50 + Realm/Realm/Helpers/Operator.cs | 26 +- Realm/Realm/Helpers/Operator.tt | 32 +- Realm/Realm/Native/PrimitiveValue.cs | 16 + Realm/Realm/Schema/Property.cs | 1 + .../Benchmarks.Android.csproj | 2 +- .../Realm.Tests/Database/ObjectSchemaTests.cs | 6 +- Tests/Realm.Tests/Database/RealmSetTests.cs | 12 +- Tests/Realm.Tests/Database/RealmValueTests.cs | 2 +- .../Database/RealmValueWithCollections.cs | 962 ++++++++++++++++++ Tests/Realm.Tests/Database/TestObjects.cs | 10 + .../IndexedRealmValueObject_generated.cs | 412 ++++++++ Tests/Realm.Tests/TestHelpers.cs | 6 + .../ErrorClasses/UnsupportedIndexableTypes.cs | 6 - .../IndexedClass_generated.cs | 58 ++ .../UnsupportedIndexableTypes.diagnostics.cs | 56 +- .../TestClasses/IndexedClass.cs | 6 + .../RootRealmClass_generated.cs | 276 +---- .../Weaver/AssemblyToProcess/FaultyClasses.cs | 5 +- wrappers/src/dictionary_cs.cpp | 31 +- wrappers/src/list_cs.cpp | 103 +- wrappers/src/marshalling.hpp | 35 +- wrappers/src/object_cs.cpp | 52 +- 38 files changed, 2152 insertions(+), 388 deletions(-) create mode 100644 Realm/Realm/Helpers/CollectionHelpers.cs create mode 100644 Tests/Realm.Tests/Database/RealmValueWithCollections.cs create mode 100644 Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/IndexedRealmValueObject_generated.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index d87f368aed..09565451e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,20 @@ * Opening realm with file format 23 or lower (Realm .NET versions earlier than 12.0.0) in read-only mode will crash. (Core 14.0.0) ### Enhancements +* Added support for list and dictionaries of `RealmValue` (`IList` and `IDictionary`) to be contained in a `RealmValue`. Lists and dictionaries can contain an arbitrary number of collections themselves. It is possible to convert an existing collection to a `RealmValue` using the new static methods `RealmValue.List` and `RealmValue.Dictionary` or using the implicit operators if converting from common types like `List`, `RealmValue[]` or `Dictionary`. Finally, it is possible to obtain the contained collections by using the new conversion method `AsList` and `AsDictionary`. For example: + + ```csharp + var list = new List { 1, true, "stringVal" }; + + var rvo = realm.Write(() => + { + return realm.Add(new RealmValueObject { RealmValueProperty = list}); + }); + + var retrievedList = rvo.RealmValueProperty.AsList(); + ``` + (PR [#3441](https://github.com/realm/realm-dotnet/pull/3441)) +* Reduced memory usage of `RealmValue`. (PR [#3441](https://github.com/realm/realm-dotnet/pull/3441)) * Add support for passing a key paths collection (`KeyPathsCollection`) when using `IRealmCollection.SubscribeForNotifications`. Passing a `KeyPathsCollection` allows to specify which changes in properties should raise a notification. A `KeyPathsCollection` can be obtained by: @@ -63,6 +77,7 @@ var query5 = people.Filter("ListOfDogs[SIZE] = $0", 3) ``` (Core 14.0.0) +* Added support for indexed `RealmValue` properties. (PR [#3544](https://github.com/realm/realm-dotnet/pull/3544)) ### Fixed * Fixed RQL (`.Filter()`) queries like `indexed_property == NONE {x}` which mistakenly matched on only x instead of not x. This only applies when an indexed property with equality (==, or IN) matches with `NONE` on a list of one item. If the constant list contained more than one value then it was working correctly. (Core 13.27.0) diff --git a/Realm/Realm.SourceGenerator/InfoClasses.cs b/Realm/Realm.SourceGenerator/InfoClasses.cs index 2a15ae8ae7..c852de97d2 100644 --- a/Realm/Realm.SourceGenerator/InfoClasses.cs +++ b/Realm/Realm.SourceGenerator/InfoClasses.cs @@ -118,6 +118,7 @@ internal abstract record PropertyTypeInfo ScalarType.ObjectId, ScalarType.Guid, ScalarType.Date, + ScalarType.RealmValue, }; private static readonly HashSet _primaryKeyTypes = new() diff --git a/Realm/Realm.Weaver/Extensions/PropertyDefinitionExtensions.cs b/Realm/Realm.Weaver/Extensions/PropertyDefinitionExtensions.cs index fcdcd23d9b..d467a30f52 100644 --- a/Realm/Realm.Weaver/Extensions/PropertyDefinitionExtensions.cs +++ b/Realm/Realm.Weaver/Extensions/PropertyDefinitionExtensions.cs @@ -41,6 +41,7 @@ internal static class PropertyDefinitionExtensions DateTimeOffsetTypeName, ObjectIdTypeName, GuidTypeName, + RealmValueTypeName, }; internal static bool IsAutomatic(this PropertyDefinition property) diff --git a/Realm/Realm/DatabaseTypes/RealmDictionary.cs b/Realm/Realm/DatabaseTypes/RealmDictionary.cs index 6e7985ab63..fb5a467cd9 100644 --- a/Realm/Realm/DatabaseTypes/RealmDictionary.cs +++ b/Realm/Realm/DatabaseTypes/RealmDictionary.cs @@ -65,6 +65,12 @@ public TValue this[string key] ValidateKey(key); var realmValue = Operator.Convert(value); + if (realmValue.Type.IsCollection()) + { + CollectionHelpers.PopulateCollection(Realm, _dictionaryHandle.SetCollection(key, realmValue.Type), realmValue); + return; + } + if (_isEmbedded && realmValue.Type != RealmValueType.Null) { if (IsDynamic) @@ -124,6 +130,12 @@ public void Add(string key, TValue value) ValidateKey(key); var realmValue = Operator.Convert(value); + if (realmValue.Type.IsCollection()) + { + CollectionHelpers.PopulateCollection(Realm, _dictionaryHandle.AddCollection(key, realmValue.Type), realmValue); + return; + } + if (_isEmbedded && realmValue.Type != RealmValueType.Null) { if (IsDynamic) @@ -163,6 +175,11 @@ public bool Remove(KeyValuePair item) return false; } + if (realmValue.Type.IsCollection()) + { + return false; + } + return _dictionaryHandle.Remove(item.Key, realmValue); } diff --git a/Realm/Realm/DatabaseTypes/RealmList.cs b/Realm/Realm/DatabaseTypes/RealmList.cs index de67a582b5..62bfde4ec7 100644 --- a/Realm/Realm/DatabaseTypes/RealmList.cs +++ b/Realm/Realm/DatabaseTypes/RealmList.cs @@ -23,6 +23,7 @@ using System.Diagnostics.CodeAnalysis; using System.Dynamic; using System.Linq.Expressions; +using System.Reflection; using System.Runtime.CompilerServices; using Realms.Dynamic; using Realms.Helpers; @@ -66,6 +67,12 @@ internal RealmList(Realm realm, ListHandle adoptedList, Metadata? metadata) : ba ValidateIndex(index); var realmValue = ValidateValueToInsert(value); + if (realmValue.Type.IsCollection()) + { + CollectionHelpers.PopulateCollection(Realm, _listHandle.SetCollection(index, realmValue.Type), realmValue); + return; + } + if (_isEmbedded) { if (IsDynamic) @@ -88,6 +95,12 @@ public void Add(T value) { var realmValue = ValidateValueToInsert(value); + if (realmValue.Type.IsCollection()) + { + CollectionHelpers.PopulateCollection(Realm, _listHandle.AddCollection(realmValue.Type), realmValue); + return; + } + if (_isEmbedded) { if (IsDynamic) @@ -112,6 +125,11 @@ public override int IndexOf([AllowNull] T value) return -1; } + if (realmValue.Type.IsCollection()) + { + return -1; + } + return _listHandle.Find(realmValue); } @@ -120,6 +138,12 @@ public void Insert(int index, T value) ValidateIndex(index); var realmValue = ValidateValueToInsert(value); + if (realmValue.Type.IsCollection()) + { + CollectionHelpers.PopulateCollection(Realm, _listHandle.InsertCollection(index, realmValue.Type), realmValue); + return; + } + if (_isEmbedded) { if (IsDynamic) @@ -151,7 +175,7 @@ public override void RemoveAt(int index) { ValidateIndex(index); - _listHandle.Erase((IntPtr)index); + _listHandle.Erase(index); } #endregion diff --git a/Realm/Realm/DatabaseTypes/RealmSet.cs b/Realm/Realm/DatabaseTypes/RealmSet.cs index 50f77ef88e..600888f087 100644 --- a/Realm/Realm/DatabaseTypes/RealmSet.cs +++ b/Realm/Realm/DatabaseTypes/RealmSet.cs @@ -73,6 +73,11 @@ public bool Add(T value) var realmValue = Operator.Convert(value); + if (realmValue.Type.IsCollection()) + { + throw new InvalidOperationException("Set cannot contain other collections."); + } + AddToRealmIfNecessary(realmValue); return _setHandle.Add(realmValue); } diff --git a/Realm/Realm/DatabaseTypes/RealmValue.cs b/Realm/Realm/DatabaseTypes/RealmValue.cs index 5f37c939b7..d2e67a1746 100644 --- a/Realm/Realm/DatabaseTypes/RealmValue.cs +++ b/Realm/Realm/DatabaseTypes/RealmValue.cs @@ -18,6 +18,7 @@ using System; using System.Buffers; +using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Linq; @@ -55,15 +56,46 @@ namespace Realms /// [Preserve(AllMembers = true)] [DebuggerDisplay("Type = {Type}, Value = {ToString(),nq}")] + [StructLayout(LayoutKind.Explicit)] public readonly struct RealmValue : IEquatable { + [FieldOffset(0)] private readonly PrimitiveValue _primitiveValue; + + // _intValue has been extracted here to be more memory efficient, as it needs also _propertyIndex + // and _objectHandle when working with RealmInteger. + [FieldOffset(0)] + private readonly long _intValue; + + // This is only used when PrimitiveValue.Type == Int. + [FieldOffset(8)] + private readonly IntPtr _propertyIndex; + + // This occupies the same memory space as PrimitiveValue.Type. + [FieldOffset(16)] + private readonly RealmValueType _type; + + // Object fields cannot be at the same offset as non-object fields, + // thus all the following values cannot overlap _primitiveValue + // even though some are mutually exclusive. + [FieldOffset(24)] private readonly string? _stringValue; + + [FieldOffset(24)] private readonly byte[]? _dataValue; + + [FieldOffset(24)] private readonly IRealmObjectBase? _objectValue; + [FieldOffset(24)] + private readonly IList? _listValue; + + [FieldOffset(24)] + private readonly IDictionary? _dictionaryValue; + + // This is only used when PrimitiveValue.Type == Int. + [FieldOffset(24)] private readonly ObjectHandle? _objectHandle; - private readonly IntPtr _propertyIndex; /// /// Gets the stored in this value. @@ -76,13 +108,11 @@ namespace Realms /// type of the integral value stored in a field. /// /// The of the current value in the database. - public RealmValueType Type { get; } + public RealmValueType Type => _type; internal RealmValue(PrimitiveValue primitive, Realm? realm = null, ObjectHandle? handle = default, IntPtr propertyIndex = default) : this() { - Type = primitive.Type; - _objectHandle = handle; - _propertyIndex = propertyIndex; + _type = primitive.Type; switch (Type) { @@ -96,30 +126,61 @@ internal RealmValue(PrimitiveValue primitive, Realm? realm = null, ObjectHandle? Argument.NotNull(realm, nameof(realm)); _objectValue = primitive.AsObject(realm!); break; + case RealmValueType.List: + Argument.NotNull(realm, nameof(realm)); + _listValue = primitive.AsList(realm!); + break; + case RealmValueType.Dictionary: + Argument.NotNull(realm, nameof(realm)); + _dictionaryValue = primitive.AsDictionary(realm!); + break; + case RealmValueType.Int: + _intValue = primitive.AsInt(); + _propertyIndex = propertyIndex; + _objectHandle = handle; + break; default: _primitiveValue = primitive; break; } } + private RealmValue(long intValue) : this() + { + _type = RealmValueType.Int; + _intValue = intValue; + } + private RealmValue(byte[] data) : this() { - Type = RealmValueType.Data; + _type = RealmValueType.Data; _dataValue = data; } private RealmValue(string value) : this() { - Type = RealmValueType.String; + _type = RealmValueType.String; _stringValue = value; } private RealmValue(IRealmObjectBase obj) : this() { - Type = RealmValueType.Object; + _type = RealmValueType.Object; _objectValue = obj; } + private RealmValue(IList list) : this() + { + _type = RealmValueType.List; + _listValue = list; + } + + private RealmValue(IDictionary dict) : this() + { + _type = RealmValueType.Dictionary; + _dictionaryValue = dict; + } + /// /// Gets a RealmValue representing null. /// @@ -128,8 +189,6 @@ private RealmValue(IRealmObjectBase obj) : this() private static RealmValue Bool(bool value) => new(PrimitiveValue.Bool(value)); - private static RealmValue Int(long value) => new(PrimitiveValue.Int(value)); - private static RealmValue Float(float value) => new(PrimitiveValue.Float(value)); private static RealmValue Double(double value) => new(PrimitiveValue.Double(value)); @@ -142,6 +201,8 @@ private RealmValue(IRealmObjectBase obj) : this() private static RealmValue Guid(Guid value) => new(PrimitiveValue.Guid(value)); + private static RealmValue Int(long value) => new(value); + private static RealmValue Data(byte[] value) => new(value); private static RealmValue String(string value) => new(value); @@ -149,6 +210,24 @@ private RealmValue(IRealmObjectBase obj) : this() [EditorBrowsable(EditorBrowsableState.Never)] public static RealmValue Object(IRealmObjectBase value) => new(value); + /// + /// Gets a RealmValue representing a list. + /// + /// The input list to copy. + /// A new RealmValue representing the input list. + /// Once created, this RealmValue will just wrap the input collection. + /// After the object containing this RealmValue gets managed this value will be a Realm list. + public static RealmValue List(IList value) => new(value); + + /// + /// Gets a RealmValue representing a dictionary. + /// + /// The input dictionary to copy. + /// A new RealmValue representing the input dictionary. + /// Once created, this RealmValue will just wrap the input collection. + /// After the object containing this RealmValue gets managed this value will be a Realm dictionary. + public static RealmValue Dictionary(IDictionary value) => new(value); + internal static RealmValue Create(T value, RealmValueType type) { if (value is null) @@ -169,6 +248,8 @@ internal static RealmValue Create(T value, RealmValueType type) RealmValueType.ObjectId => ObjectId(Operator.Convert(value)), RealmValueType.Guid => Guid(Operator.Convert(value)), RealmValueType.Object => Object(Operator.Convert(value)), + RealmValueType.List => List(Operator.Convert>(value)), + RealmValueType.Dictionary => Dictionary(Operator.Convert>(value)), _ => throw new NotSupportedException($"RealmValueType {type} is not supported."), }; } @@ -264,7 +345,7 @@ public int AsInt32() public long AsInt64() { EnsureType("long", RealmValueType.Int); - return _primitiveValue.AsInt(); + return _intValue; } /// @@ -417,13 +498,35 @@ public byte[] AsData() /// Returns the stored value as a string. /// /// Thrown if the underlying value is not of type . - /// /// A string representing the value stored in the database. + /// A string representing the value stored in the database. public string AsString() { EnsureType("string", RealmValueType.String); return _stringValue!; } + /// + /// Returns the stored value as a list. + /// + /// Thrown if the underlying value is not of type . + /// A list representing the value stored in the database. + public IList AsList() + { + EnsureType("List", RealmValueType.List); + return _listValue!; + } + + /// + /// Returns the stored value as a dictionary. + /// + /// Thrown if the underlying value is not of type . + /// A dictionary representing the value stored in the database. + public IDictionary AsDictionary() + { + EnsureType("Dictionary", RealmValueType.Dictionary); + return _dictionaryValue!; + } + /// /// Returns the stored value as a . /// @@ -714,6 +817,8 @@ public T As() RealmValueType.ObjectId => Operator.Convert(AsObjectId()), RealmValueType.Guid => Operator.Convert(AsGuid()), RealmValueType.Object => Operator.Convert(AsIRealmObject()), + RealmValueType.List => Operator.Convert, T>(AsList()), + RealmValueType.Dictionary => Operator.Convert, T>(AsDictionary()), _ => throw new NotSupportedException($"RealmValue of type {Type} is not supported."), }; } @@ -738,6 +843,8 @@ public T As() RealmValueType.ObjectId => AsObjectId(), RealmValueType.Guid => AsGuid(), RealmValueType.Object => AsIRealmObject(), + RealmValueType.List => AsList(), + RealmValueType.Dictionary => AsDictionary(), _ => throw new NotSupportedException($"RealmValue of type {Type} is not supported."), }; } @@ -805,6 +912,8 @@ public override int GetHashCode() RealmValueType.Decimal128 => AsDecimal128().GetHashCode(), RealmValueType.ObjectId => AsObjectId().GetHashCode(), RealmValueType.Object => AsIRealmObject().GetHashCode(), + RealmValueType.List => AsList().GetHashCode(), + RealmValueType.Dictionary => AsDictionary().GetHashCode(), _ => 0, }; @@ -1339,6 +1448,27 @@ public override int GetHashCode() /// A containing the supplied . public static implicit operator RealmValue(RealmObjectBase? val) => val == null ? Null : Object(val); + /// + /// Implicitly constructs a from List<RealmValue>?. + /// + /// The value to store in the . + /// A containing the supplied . + public static implicit operator RealmValue(List? val) => val == null ? Null : List(val); + + /// + /// Implicitly constructs a from RealmValue[]?. + /// + /// The value to store in the . + /// A containing the supplied . + public static implicit operator RealmValue(RealmValue[]? val) => val == null ? Null : List(val); + + /// + /// Implicitly constructs a from Dictionary<string, RealmValue>. + /// + /// The value to store in the . + /// A containing the supplied . + public static implicit operator RealmValue(Dictionary? val) => val == null ? Null : Dictionary(val); + private void EnsureType(string target, RealmValueType type) { if (Type != type) @@ -1403,6 +1533,8 @@ public bool Equals(RealmValue other) RealmValueType.ObjectId => AsObjectId() == other.AsObjectId(), RealmValueType.Guid => AsGuid() == other.AsGuid(), RealmValueType.Object => AsIRealmObject().Equals(other.AsIRealmObject()), + RealmValueType.List => AsList().SequenceEqual(other.AsList()), + RealmValueType.Dictionary => AsDictionary().DictionaryEquals(other.AsDictionary()), RealmValueType.Null => true, _ => false, }; diff --git a/Realm/Realm/DatabaseTypes/RealmValueType.cs b/Realm/Realm/DatabaseTypes/RealmValueType.cs index c416932927..ab60b1776c 100644 --- a/Realm/Realm/DatabaseTypes/RealmValueType.cs +++ b/Realm/Realm/DatabaseTypes/RealmValueType.cs @@ -93,5 +93,20 @@ public enum RealmValueType : byte /// The value represents a . /// Guid, + + /// + /// The value represents a . + /// + List, + + /// + /// The value represents a . + /// + Set, + + /// + /// The value represents a . + /// + Dictionary, } } diff --git a/Realm/Realm/Extensions/CollectionExtensions.cs b/Realm/Realm/Extensions/CollectionExtensions.cs index 8cfb7f42d0..d29168138d 100644 --- a/Realm/Realm/Extensions/CollectionExtensions.cs +++ b/Realm/Realm/Extensions/CollectionExtensions.cs @@ -558,15 +558,22 @@ public static async Task> SubscribeAsync(this IQueryable que } [EditorBrowsable(EditorBrowsableState.Never)] - [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "This is only used by the weaver and should not be exposed to users.")] + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", + Justification = "This is only used by the weaver/source generated classes and should not be exposed to users.")] public static void PopulateCollection(ICollection source, ICollection target, bool update, bool skipDefaults) => PopulateCollectionCore(source, target, update, skipDefaults, value => value); [EditorBrowsable(EditorBrowsableState.Never)] - [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "This is only used by the weaver and should not be exposed to users.")] + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", + Justification = "This is only used by the weaver/source generated classes and should not be exposed to users.")] public static void PopulateCollection(IDictionary source, IDictionary target, bool update, bool skipDefaults) => PopulateCollectionCore(source, target, update, skipDefaults, kvp => kvp.Value); + internal static bool DictionaryEquals(this IDictionary first, IDictionary second) + { + return first.Count == second.Count && first.All(fkv => second.TryGetValue(fkv.Key, out RealmValue sVal) && fkv.Value == sVal); + } + private static bool ShouldWaitForSync(WaitForSyncMode mode, Subscription? oldSub, Subscription newSub) { switch (mode) diff --git a/Realm/Realm/Extensions/RealmValueExtensions.cs b/Realm/Realm/Extensions/RealmValueExtensions.cs index 0d5245bc73..956f848195 100644 --- a/Realm/Realm/Extensions/RealmValueExtensions.cs +++ b/Realm/Realm/Extensions/RealmValueExtensions.cs @@ -31,8 +31,17 @@ internal static class RealmValueExtensions RealmValueType.Decimal128 }; + private static readonly HashSet _collectionTypes = new() + { + RealmValueType.List, + RealmValueType.Set, + RealmValueType.Dictionary, + }; + public static bool IsNumeric(this RealmValueType type) => _numericTypes.Contains(type); + public static bool IsCollection(this RealmValueType type) => _collectionTypes.Contains(type); + public static (NativeQueryArgument[] Values, RealmValue.HandlesToCleanup?[] Handles) ToNativeValues(this QueryArgument[] arguments) { var nativeArgs = new NativeQueryArgument[arguments.Length]; diff --git a/Realm/Realm/Extensions/ReflectionExtensions.cs b/Realm/Realm/Extensions/ReflectionExtensions.cs index fafb8da79a..d0b0cce93d 100644 --- a/Realm/Realm/Extensions/ReflectionExtensions.cs +++ b/Realm/Realm/Extensions/ReflectionExtensions.cs @@ -67,7 +67,5 @@ public static ObjectSchema.ObjectType GetRealmSchemaType(this Type type) return ObjectSchema.ObjectType.RealmObject; } - - public static T[] GetEnumValues() => Enum.GetValues(typeof(T)).Cast().ToArray(); } } diff --git a/Realm/Realm/Handles/CollectionHandleBase.cs b/Realm/Realm/Handles/CollectionHandleBase.cs index 366260dd54..b0896280ff 100644 --- a/Realm/Realm/Handles/CollectionHandleBase.cs +++ b/Realm/Realm/Handles/CollectionHandleBase.cs @@ -54,6 +54,19 @@ public ResultsHandle GetFilteredResults(string query, QueryArgument[] arguments) return new ResultsHandle(Root!, ptr); } + protected CollectionHandleBase GetCollectionHandle(IntPtr collectionPtr, RealmValueType collectionType) + { + switch (collectionType) + { + case RealmValueType.List: + return new ListHandle(Root!, collectionPtr); + case RealmValueType.Dictionary: + return new DictionaryHandle(Root!, collectionPtr); + default: + throw new InvalidOperationException("Invalid collection type"); + } + } + public abstract CollectionHandleBase Freeze(SharedRealmHandle frozenRealmHandle); public abstract void Clear(); diff --git a/Realm/Realm/Handles/DictionaryHandle.cs b/Realm/Realm/Handles/DictionaryHandle.cs index 03f6e01c58..cc53737a99 100644 --- a/Realm/Realm/Handles/DictionaryHandle.cs +++ b/Realm/Realm/Handles/DictionaryHandle.cs @@ -75,14 +75,17 @@ public static extern IntPtr add_notification_callback(DictionaryHandle handle, I [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_dictionary_set", CallingConvention = CallingConvention.Cdecl)] public static extern void set_value(DictionaryHandle handle, PrimitiveValue key, PrimitiveValue value, out NativeException ex); + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_dictionary_set_embedded", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr set_embedded(DictionaryHandle handle, PrimitiveValue key, out NativeException ex); + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_dictionary_add", CallingConvention = CallingConvention.Cdecl)] public static extern void add_value(DictionaryHandle handle, PrimitiveValue key, PrimitiveValue value, out NativeException ex); [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_dictionary_add_embedded", CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr add_embedded(DictionaryHandle handle, PrimitiveValue key, out NativeException ex); - [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_dictionary_set_embedded", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr set_embedded(DictionaryHandle handle, PrimitiveValue key, out NativeException ex); + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_dictionary_add_collection", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr add_collection(DictionaryHandle handle, PrimitiveValue key, RealmValueType type, bool allowOverride, out NativeException ex); [DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_dictionary_contains_key", CallingConvention = CallingConvention.Cdecl)] [return: MarshalAs(UnmanagedType.U1)] @@ -238,6 +241,34 @@ public void Set(string key, in RealmValue value) nativeException.ThrowIfNecessary(); } + public ObjectHandle SetEmbedded(string key) + { + EnsureIsOpen(); + + RealmValue keyValue = key; + var (primitiveKey, keyHandles) = keyValue.ToNative(); + + var result = NativeMethods.set_embedded(this, primitiveKey, out var nativeException); + keyHandles?.Dispose(); + nativeException.ThrowIfNecessary(); + + return new ObjectHandle(Root!, result); + } + + public CollectionHandleBase SetCollection(string key, RealmValueType collectionType) + { + EnsureIsOpen(); + + RealmValue keyValue = key; + var (primitiveKey, keyHandles) = keyValue.ToNative(); + + var result = NativeMethods.add_collection(this, primitiveKey, collectionType, allowOverride: true, out var nativeException); + keyHandles?.Dispose(); + nativeException.ThrowIfNecessary(); + + return GetCollectionHandle(result, collectionType); + } + public void Add(string key, in RealmValue value) { EnsureIsOpen(); @@ -267,18 +298,18 @@ public ObjectHandle AddEmbedded(string key) return new ObjectHandle(Root!, result); } - public ObjectHandle SetEmbedded(string key) + public CollectionHandleBase AddCollection(string key, RealmValueType collectionType) { EnsureIsOpen(); RealmValue keyValue = key; var (primitiveKey, keyHandles) = keyValue.ToNative(); - var result = NativeMethods.set_embedded(this, primitiveKey, out var nativeException); + var result = NativeMethods.add_collection(this, primitiveKey, collectionType, allowOverride: false, out var nativeException); keyHandles?.Dispose(); nativeException.ThrowIfNecessary(); - return new ObjectHandle(Root!, result); + return GetCollectionHandle(result, collectionType); } public bool ContainsKey(string key) diff --git a/Realm/Realm/Handles/ListHandle.cs b/Realm/Realm/Handles/ListHandle.cs index 16016980f1..384cc3c927 100644 --- a/Realm/Realm/Handles/ListHandle.cs +++ b/Realm/Realm/Handles/ListHandle.cs @@ -33,18 +33,27 @@ private static class NativeMethods [DllImport(InteropConfig.DLL_NAME, EntryPoint = "list_add_embedded", CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr add_embedded(ListHandle listHandle, out NativeException ex); + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "list_add_collection", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr add_collection(ListHandle listHandle, RealmValueType type, out NativeException ex); + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "list_set_value", CallingConvention = CallingConvention.Cdecl)] public static extern void set_value(ListHandle listHandle, IntPtr targetIndex, PrimitiveValue value, out NativeException ex); [DllImport(InteropConfig.DLL_NAME, EntryPoint = "list_set_embedded", CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr set_embedded(ListHandle listHandle, IntPtr targetIndex, out NativeException ex); + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "list_set_collection", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr set_collection(ListHandle listHandle, IntPtr targetIndex, RealmValueType type, out NativeException ex); + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "list_insert_value", CallingConvention = CallingConvention.Cdecl)] public static extern void insert_value(ListHandle listHandle, IntPtr targetIndex, PrimitiveValue value, out NativeException ex); [DllImport(InteropConfig.DLL_NAME, EntryPoint = "list_insert_embedded", CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr insert_embedded(ListHandle listHandle, IntPtr targetIndex, out NativeException ex); + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "list_insert_collection", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr insert_collection(ListHandle listHandle, IntPtr targetIndex, RealmValueType type, out NativeException ex); + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "list_get_value", CallingConvention = CallingConvention.Cdecl)] public static extern void get_value(ListHandle listHandle, IntPtr link_ndx, out PrimitiveValue value, out NativeException ex); @@ -142,6 +151,15 @@ public ObjectHandle AddEmbedded() return new ObjectHandle(Root!, result); } + public CollectionHandleBase AddCollection(RealmValueType collectionType) + { + EnsureIsOpen(); + + var collectionPtr = NativeMethods.add_collection(this, collectionType, out var nativeException); + nativeException.ThrowIfNecessary(); + return GetCollectionHandle(collectionPtr, collectionType); + } + public void Set(int targetIndex, in RealmValue value) { EnsureIsOpen(); @@ -161,6 +179,15 @@ public ObjectHandle SetEmbedded(int targetIndex) return new ObjectHandle(Root!, result); } + public CollectionHandleBase SetCollection(int targetIndex, RealmValueType collectionType) + { + EnsureIsOpen(); + + var collectionPtr = NativeMethods.set_collection(this, (IntPtr)targetIndex, collectionType, out var nativeException); + nativeException.ThrowIfNecessary(); + return GetCollectionHandle(collectionPtr, collectionType); + } + public void Insert(int targetIndex, in RealmValue value) { EnsureIsOpen(); @@ -180,6 +207,15 @@ public ObjectHandle InsertEmbedded(int targetIndex) return new ObjectHandle(Root!, result); } + public CollectionHandleBase InsertCollection(int targetIndex, RealmValueType collectionType) + { + EnsureIsOpen(); + + var collectionPtr = NativeMethods.insert_collection(this, (IntPtr)targetIndex, collectionType, out var nativeException); + nativeException.ThrowIfNecessary(); + return GetCollectionHandle(collectionPtr, collectionType); + } + public int Find(in RealmValue value) { EnsureIsOpen(); @@ -191,11 +227,11 @@ public int Find(in RealmValue value) return (int)result; } - public void Erase(IntPtr rowIndex) + public void Erase(int targetIndex) { EnsureIsOpen(); - NativeMethods.erase(this, rowIndex, out var nativeException); + NativeMethods.erase(this, (IntPtr)targetIndex, out var nativeException); nativeException.ThrowIfNecessary(); } diff --git a/Realm/Realm/Handles/ObjectHandle.cs b/Realm/Realm/Handles/ObjectHandle.cs index b588838b43..ae8f098cd4 100644 --- a/Realm/Realm/Handles/ObjectHandle.cs +++ b/Realm/Realm/Handles/ObjectHandle.cs @@ -20,6 +20,7 @@ using System.Runtime.InteropServices; using Realms.Exceptions; using Realms.Extensions; +using Realms.Helpers; using Realms.Native; using Realms.Schema; @@ -45,6 +46,9 @@ private static class NativeMethods [DllImport(InteropConfig.DLL_NAME, EntryPoint = "object_set_value", CallingConvention = CallingConvention.Cdecl)] public static extern void set_value(ObjectHandle handle, IntPtr propertyIndex, PrimitiveValue value, out NativeException ex); + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "object_set_collection_value", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr set_collection_value(ObjectHandle handle, IntPtr propertyIndex, RealmValueType type, out NativeException ex); + [DllImport(InteropConfig.DLL_NAME, EntryPoint = "object_create_embedded", CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr create_embedded_link(ObjectHandle handle, IntPtr propertyIndex, out NativeException ex); @@ -219,6 +223,25 @@ public void SetValue(string propertyName, Metadata metadata, in RealmValue value throw new NotSupportedException($"Asymmetric objects cannot be linked to and cannot be contained in a RealmValue. Attempted to set {value} to {metadata.Schema.Name}.{propertyName}"); } } + else if (value.Type.IsCollection()) + { + var collectionPtr = NativeMethods.set_collection_value(this, propertyIndex, value.Type, out var collNativeException); + collNativeException.ThrowIfNecessary(); + + switch (value.Type) + { + case RealmValueType.List: + CollectionHelpers.PopulateCollection(realm, new ListHandle(Root!, collectionPtr), value); + break; + case RealmValueType.Dictionary: + CollectionHelpers.PopulateCollection(realm, new DictionaryHandle(Root!, collectionPtr), value); + break; + default: + break; + } + + return; + } var (primitive, handles) = value.ToNative(); NativeMethods.set_value(this, propertyIndex, primitive, out var nativeException); diff --git a/Realm/Realm/Helpers/CollectionHelpers.cs b/Realm/Realm/Helpers/CollectionHelpers.cs new file mode 100644 index 0000000000..8506f1b9a4 --- /dev/null +++ b/Realm/Realm/Helpers/CollectionHelpers.cs @@ -0,0 +1,50 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +using System.Diagnostics; + +namespace Realms.Helpers; + +internal static class CollectionHelpers +{ + internal static void PopulateCollection(Realm realm, CollectionHandleBase handle, RealmValue content) + { + if (handle is ListHandle listHandle) + { + var newList = new RealmList(realm, listHandle, metadata: null); + + foreach (var item in content.AsList()) + { + newList.Add(item); + } + } + else if (handle is DictionaryHandle dictHandle) + { + var newDict = new RealmDictionary(realm, dictHandle, metadata: null); + + foreach (var item in content.AsDictionary()) + { + newDict.Add(item); + } + } + else + { + Debug.Fail("Invalid collection type"); + } + } +} diff --git a/Realm/Realm/Helpers/Operator.cs b/Realm/Realm/Helpers/Operator.cs index 2595d7a7eb..5f84e63884 100644 --- a/Realm/Realm/Helpers/Operator.cs +++ b/Realm/Realm/Helpers/Operator.cs @@ -1,4 +1,4 @@ -//////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////// // // Copyright 2020 Realm Inc. // @@ -362,6 +362,10 @@ internal static class Operator [(typeof(Decimal128), typeof(decimal))] = new Decimal128DecimalConverter(), [(typeof(RealmValue), typeof(IRealmObjectBase))] = new RealmValueIRealmObjectBaseConverter(), [(typeof(IRealmObjectBase), typeof(RealmValue))] = new IRealmObjectBaseRealmValueConverter(), + [(typeof(IList), typeof(RealmValue))] = new IListRealmValueConverter(), + [(typeof(RealmValue), typeof(IList))] = new RealmValueIListConverter(), + [(typeof(IDictionary), typeof(RealmValue))] = new IDictionaryRealmValueConverter(), + [(typeof(RealmValue), typeof(IDictionary))] = new RealmValueIDictionaryConverter(), }; /// @@ -801,6 +805,16 @@ private class IRealmObjectBaseRealmValueConverter : SpecializedConverterBase value is null ? RealmValue.Null : RealmValue.Object(value); } + + private class IListRealmValueConverter : SpecializedConverterBase, RealmValue> + { + public override RealmValue Convert(IList? value) => value is null ? RealmValue.Null : RealmValue.List(value); + } + + private class IDictionaryRealmValueConverter : SpecializedConverterBase, RealmValue> + { + public override RealmValue Convert(IDictionary? value) => value is null ? RealmValue.Null : RealmValue.Dictionary(value); + } #endregion ToRealmValue Converters #region FromRealmValue Converters @@ -994,6 +1008,16 @@ private class RealmValueIRealmObjectBaseConverter : SpecializedConverterBase value.AsIRealmObject(); } + + private class RealmValueIListConverter : SpecializedConverterBase> + { + public override IList Convert(RealmValue value) => value.AsList(); + } + + private class RealmValueIDictionaryConverter : SpecializedConverterBase> + { + public override IDictionary Convert(RealmValue value) => value.AsDictionary(); + } #endregion FromRealmValue Converters #region Integral Converters diff --git a/Realm/Realm/Helpers/Operator.tt b/Realm/Realm/Helpers/Operator.tt index a4c3dec819..969aef067d 100644 --- a/Realm/Realm/Helpers/Operator.tt +++ b/Realm/Realm/Helpers/Operator.tt @@ -43,7 +43,7 @@ namespace Realms.Helpers { <# var realmValueContents = GetFileContents("../DatabaseTypes/RealmValue.cs"); - var implicitToRealmValueTypes = GetMatchedGroups(realmValueContents, _implicitTypeToRealmValueRegex, "fromType"); + var implicitToRealmValueTypes = GetMatchedGroups(realmValueContents, _implicitTypeToRealmValueRegex, "fromType").Except(_implicitTypeExcludeList); foreach (var type in implicitToRealmValueTypes) { #> @@ -75,6 +75,10 @@ namespace Realms.Helpers #> [(typeof(RealmValue), typeof(IRealmObjectBase))] = new RealmValueIRealmObjectBaseConverter(), [(typeof(IRealmObjectBase), typeof(RealmValue))] = new IRealmObjectBaseRealmValueConverter(), + [(typeof(IList), typeof(RealmValue))] = new IListRealmValueConverter(), + [(typeof(RealmValue), typeof(IList))] = new RealmValueIListConverter(), + [(typeof(IDictionary), typeof(RealmValue))] = new IDictionaryRealmValueConverter(), + [(typeof(RealmValue), typeof(IDictionary))] = new RealmValueIDictionaryConverter(), }; /// @@ -341,6 +345,16 @@ namespace Realms.Helpers { public override RealmValue Convert(IRealmObjectBase? value) => value is null ? RealmValue.Null : RealmValue.Object(value); } + + private class IListRealmValueConverter : SpecializedConverterBase, RealmValue> + { + public override RealmValue Convert(IList? value) => value is null ? RealmValue.Null : RealmValue.List(value); + } + + private class IDictionaryRealmValueConverter : SpecializedConverterBase, RealmValue> + { + public override RealmValue Convert(IDictionary? value) => value is null ? RealmValue.Null : RealmValue.Dictionary(value); + } #endregion ToRealmValue Converters #region FromRealmValue Converters @@ -361,6 +375,16 @@ namespace Realms.Helpers { public override IRealmObjectBase Convert(RealmValue value) => value.AsIRealmObject(); } + + private class RealmValueIListConverter : SpecializedConverterBase> + { + public override IList Convert(RealmValue value) => value.AsList(); + } + + private class RealmValueIDictionaryConverter : SpecializedConverterBase> + { + public override IDictionary Convert(RealmValue value) => value.AsDictionary(); + } #endregion FromRealmValue Converters #region Integral Converters @@ -400,6 +424,12 @@ namespace Realms.Helpers private static readonly Regex _implicitTypeToRealmValueRegex = new Regex(@"public static implicit operator RealmValue\((?\S*)"); private static readonly Regex _explicitRealmValueToTypeRegex = new Regex(@"public static explicit operator (?[^\(]*)\(RealmValue"); + private static readonly string[] _implicitTypeExcludeList = new[] + { + "List?", + "Dictionary AsList(Realm realm) + { + var handle = new ListHandle(realm.SharedRealmHandle, collection_ptr); + return new RealmList(realm, handle, null); + } + + public readonly RealmDictionary AsDictionary(Realm realm) + { + var handle = new DictionaryHandle(realm.SharedRealmHandle, collection_ptr); + return new RealmDictionary(realm, handle, null); + } + public readonly bool TryGetObjectHandle(Realm realm, [NotNullWhen(true)] out ObjectHandle? handle) { if (Type == RealmValueType.Object) diff --git a/Realm/Realm/Schema/Property.cs b/Realm/Realm/Schema/Property.cs index c4b6de3955..b2324b4ca4 100644 --- a/Realm/Realm/Schema/Property.cs +++ b/Realm/Realm/Schema/Property.cs @@ -41,6 +41,7 @@ public readonly struct Property PropertyType.ObjectId, PropertyType.Guid, PropertyType.Date, + PropertyType.RealmValue }; internal static readonly HashSet PrimaryKeyTypes = new() diff --git a/Tests/Benchmarks/Benchmarks.Android/Benchmarks.Android.csproj b/Tests/Benchmarks/Benchmarks.Android/Benchmarks.Android.csproj index 19567ca866..dc7c2eca95 100644 --- a/Tests/Benchmarks/Benchmarks.Android/Benchmarks.Android.csproj +++ b/Tests/Benchmarks/Benchmarks.Android/Benchmarks.Android.csproj @@ -79,7 +79,7 @@ - {BA9ED757-BBE5-486F-BE4A-8D5542504030} + {BECF4F60-9741-49A6-BD79-D59664C2682E} Benchmarks diff --git a/Tests/Realm.Tests/Database/ObjectSchemaTests.cs b/Tests/Realm.Tests/Database/ObjectSchemaTests.cs index 53db7773e7..79b989bec2 100644 --- a/Tests/Realm.Tests/Database/ObjectSchemaTests.cs +++ b/Tests/Realm.Tests/Database/ObjectSchemaTests.cs @@ -313,11 +313,9 @@ public void Property_FromTypeGeneric_Object( } } - public static readonly RealmValueType[] PrimitiveTypes = ReflectionExtensions.GetEnumValues(); - [Test] public void Property_Primitive_Tests( - [ValueSource(nameof(PrimitiveTypes))] RealmValueType type, + [ValueSource(typeof(TestHelpers), nameof(TestHelpers.PrimitiveRealmValueTypes))] RealmValueType type, [ValueSource(nameof(BoolValues))] bool isPrimaryKey, [ValueSource(nameof(IndexTypes))] IndexType indexType, [ValueSource(nameof(BoolValues))] bool isNullable) @@ -369,7 +367,7 @@ public void Property_Primitive_Tests( [Test] public void Property_PrimitiveCollection_Tests( [ValueSource(nameof(CollectionTypes))] PropertyType collectionType, - [ValueSource(nameof(PrimitiveTypes))] RealmValueType type, + [ValueSource(typeof(TestHelpers), nameof(TestHelpers.PrimitiveRealmValueTypes))] RealmValueType type, [ValueSource(nameof(BoolValues))] bool isNullable) { Property getProperty() => collectionType switch diff --git a/Tests/Realm.Tests/Database/RealmSetTests.cs b/Tests/Realm.Tests/Database/RealmSetTests.cs index 7b81e291e8..bdc27b3d87 100644 --- a/Tests/Realm.Tests/Database/RealmSetTests.cs +++ b/Tests/Realm.Tests/Database/RealmSetTests.cs @@ -1092,15 +1092,15 @@ private static IEnumerable> RealmTestValues() var rv10 = RealmValue.Create(new Guid("{F2952191-A847-41C3-8362-497F92CB7D24}"), RealmValueType.Guid); var rv11 = GetRealmValueObject(); - yield return new TestCaseData(new[] { rv0, rv1, rv2, rv3, rv4, rv5, rv6, rv7, rv8, rv9, rv10, rv11 }, new[] { rv0, rv1, rv2, rv3, rv4, rv5, rv6, rv7, rv8, rv9, rv10, rv11 }); + yield return new TestCaseData(new[] { rv0, rv1, rv2, rv3, rv4, rv5, rv6, rv7, rv8, rv9, rv10, rv11 } as IEnumerable, new[] { rv0, rv1, rv2, rv3, rv4, rv5, rv6, rv7, rv8, rv9, rv10, rv11 }); rv11 = GetRealmValueObject(); - yield return new TestCaseData(new[] { rv0, rv1, rv2, rv3, rv4, rv5 }, new[] { rv6, rv7, rv8, rv9, rv10, rv11 }); + yield return new TestCaseData(new[] { rv0, rv1, rv2, rv3, rv4, rv5 } as IEnumerable, new[] { rv6, rv7, rv8, rv9, rv10, rv11 }); rv11 = GetRealmValueObject(); - yield return new TestCaseData(Array.Empty(), new[] { rv0, rv1, rv2, rv3, rv4, rv5, rv6, rv7, rv8, rv9, rv10, rv11 }); + yield return new TestCaseData(Array.Empty() as IEnumerable, new[] { rv0, rv1, rv2, rv3, rv4, rv5, rv6, rv7, rv8, rv9, rv10, rv11 }); static RealmValue GetRealmValueObject() => RealmValue.Create(new IntPropertyObject { Int = 10 }, RealmValueType.Object); @@ -1110,21 +1110,21 @@ private static IEnumerable> RealmTestValues() var i4 = RealmValue.Create(true, RealmValueType.Bool); var i5 = RealmValue.Create(1m, RealmValueType.Decimal128); - yield return new TestCaseData(new[] { i1, i2, i3, i4, i5 }, new[] { i1, i2, i3, i4, i5 }); + yield return new TestCaseData(new[] { i1, i2, i3, i4, i5 } as IEnumerable, new[] { i1, i2, i3, i4, i5 }); var s1 = RealmValue.Create(string.Empty, RealmValueType.String); var s2 = RealmValue.Create(0, RealmValueType.Int); var s3 = RealmValue.Create(Guid.Empty, RealmValueType.Guid); var s4 = RealmValue.Null; - yield return new TestCaseData(new[] { s1, s2, s3, s4 }, new[] { s1, s2, s3, s4 }); + yield return new TestCaseData(new[] { s1, s2, s3, s4 } as IEnumerable, new[] { s1, s2, s3, s4 }); var d1 = RealmValue.Create(1m, RealmValueType.Decimal128); var d2 = RealmValue.Create(1f, RealmValueType.Decimal128); var d3 = RealmValue.Create(1d, RealmValueType.Decimal128); var d4 = RealmValue.Create(1, RealmValueType.Decimal128); - yield return new TestCaseData(new[] { d1, d2, d3, d4 }, new[] { d1, d2, d3, d4 }); + yield return new TestCaseData(new[] { d1, d2, d3, d4 } as IEnumerable, new[] { d1, d2, d3, d4 }); } [TestCaseSource(nameof(RealmTestValues))] diff --git a/Tests/Realm.Tests/Database/RealmValueTests.cs b/Tests/Realm.Tests/Database/RealmValueTests.cs index cfcc7b3bd3..e273d0d1ba 100644 --- a/Tests/Realm.Tests/Database/RealmValueTests.cs +++ b/Tests/Realm.Tests/Database/RealmValueTests.cs @@ -1010,7 +1010,7 @@ public void Query_Generic() Assert.That(f, Is.EquivalentTo(referenceResult)); } - foreach (var type in (RealmValueType[])Enum.GetValues(typeof(RealmValueType))) + foreach (var type in TestHelpers.PrimitiveRealmValueTypes) { // Equality on RealmValueType var referenceResult = rvObjects.Where(r => r.RealmValueProperty.Type == type).OrderBy(r => r.Id).ToList(); diff --git a/Tests/Realm.Tests/Database/RealmValueWithCollections.cs b/Tests/Realm.Tests/Database/RealmValueWithCollections.cs new file mode 100644 index 0000000000..5e76b6fdac --- /dev/null +++ b/Tests/Realm.Tests/Database/RealmValueWithCollections.cs @@ -0,0 +1,962 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2023 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License") +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +using System; +using System.Collections.Generic; +using System.Linq; +using MongoDB.Bson; +using NUnit.Framework; + +namespace Realms.Tests.Database +{ + [TestFixture, Preserve(AllMembers = true)] + internal class RealmValueWithCollections : RealmInstanceTest + { + public static Func> ListGenerator = i => { return new List { $"inner{i}", i }; }; + public static Func> DictGenerator = i => { return new Dictionary { { "s1", i }, { "s2", $"ah{i}" } }; }; + + public static Func[] CollectionGenerators = new Func[] + { + i => (RealmValue)ListGenerator(i), + i => (RealmValue)DictGenerator(i), + }; + + private RealmValueObject PersistAndFind(RealmValue rv) + { + _realm.Write(() => + { + _realm.Add(new RealmValueObject { RealmValueProperty = rv }); + }); + + return _realm.All().First(); + } + + #region List + + [Test] + public void List_WhenRetrieved_WorksWithAllTypes([Values(true, false)] bool isManaged) + { + var innerList = ListGenerator(1); + var innerDict = DictGenerator(1); + + var originalList = new List + { + RealmValue.Null, + 1, + true, + "string", + new byte[] { 0, 1, 2 }, + new DateTimeOffset(1234, 5, 6, 7, 8, 9, TimeSpan.Zero), + 1f, + 2d, + 3m, + new ObjectId("5f63e882536de46d71877979"), + Guid.Parse("3809d6d9-7618-4b3d-8044-2aa35fd02f31"), + new InternalObject { IntProperty = 10, StringProperty = "brown" }, + innerList, + innerDict, + }; + + RealmValue rv = originalList; + + if (isManaged) + { + rv = PersistAndFind(rv).RealmValueProperty; + } + + Assert.That(rv.Type, Is.EqualTo(RealmValueType.List)); + Assert.That(rv != RealmValue.Null); + + Assert.That(rv.AsList(), Is.EqualTo(originalList)); + Assert.That(rv.AsAny(), Is.EqualTo(originalList)); + Assert.That(rv.As>(), Is.EqualTo(originalList)); + + Assert.That(rv == originalList); + Assert.That(rv.Equals(originalList)); + Assert.That(rv.AsList().SequenceEqual(originalList)); + } + + [Test] + public void List_WhenUsingIndexAccessWithCollections_WorkAsExpected([Values(true, false)] bool isManaged) + { + var innerList = ListGenerator(1); + var innerDict = DictGenerator(1); + + var originalList = new List + { + innerList, + innerDict, + }; + + RealmValue rv = originalList; + + if (isManaged) + { + rv = PersistAndFind(rv).RealmValueProperty; + } + + var retrievedList = rv.AsList()[0]; + var retrievedDict = rv.AsList()[1]; + + Assert.That(retrievedList.AsList(), Is.EqualTo(innerList)); + Assert.That(retrievedDict.AsDictionary(), Is.EquivalentTo(innerDict)); + } + + [Test] + public void List_CanBeCopiedFromManagedList([Values(true, false)] bool isManaged) + { + var originalList = ListGenerator(1); + + RealmValue rv = originalList; + + if (isManaged) + { + rv = PersistAndFind(originalList).RealmValueProperty; + } + + var newObj = new RealmValueObject { RealmValueProperty = rv }; + + RealmValue rv2; + + if (isManaged) + { + rv2 = PersistAndFind(rv).RealmValueProperty; + } + else + { + rv2 = newObj.RealmValueProperty; + } + + Assert.That(rv.AsList(), Is.EqualTo(rv2.AsList())); + Assert.That(rv.AsList(), Is.EqualTo(originalList)); + Assert.That(rv2.AsList(), Is.EqualTo(originalList)); + } + + [Test] + public void List_BuiltWithConstructorMethodOrOperatorOrCreateOrArray_WorksTheSame([Values(true, false)] bool isManaged) + { + var originalList = ListGenerator(1); + + RealmValue rvOperator = originalList; + RealmValue rvConstructor = RealmValue.List(originalList); + RealmValue rvCreate = RealmValue.Create(originalList, RealmValueType.List); + RealmValue rvArray = originalList.ToArray(); + + if (isManaged) + { + rvOperator = PersistAndFind(rvOperator).RealmValueProperty; + rvConstructor = PersistAndFind(rvConstructor).RealmValueProperty; + rvCreate = PersistAndFind(rvCreate).RealmValueProperty; + rvArray = PersistAndFind(rvCreate).RealmValueProperty; + } + + Assert.That(rvOperator.AsList(), Is.EqualTo(originalList)); + Assert.That(rvConstructor.AsList(), Is.EqualTo(originalList)); + Assert.That(rvCreate.AsList(), Is.EqualTo(originalList)); + Assert.That(rvArray.AsList(), Is.EqualTo(originalList)); + } + + [Test] + public void List_WhenManaged_IsNotSameReferenceAsOriginalList() + { + var originalList = ListGenerator(1); + + RealmValue rv = originalList; + rv = PersistAndFind(rv).RealmValueProperty; + var retrievedList = rv.AsList(); + + Assert.That(ReferenceEquals(originalList, retrievedList), Is.False); + } + + [Test] + public void List_WhenUnmanaged_IsSameReferenceAsOriginalList() + { + var originalList = ListGenerator(1); + + RealmValue rv = originalList; + var retrievedList = rv.AsList(); + + Assert.That(ReferenceEquals(originalList, retrievedList), Is.True); + } + + [Test] + public void List_AfterCreation_CanBeAssigned([Values(true, false)] bool isManaged) + { + var stringVal = "Mario"; + var rvo = new RealmValueObject { RealmValueProperty = stringVal }; + + if (isManaged) + { + _realm.Write(() => + { + _realm.Add(rvo); + }); + } + + Assert.That(rvo.RealmValueProperty == stringVal); + + var listVal = ListGenerator(1); + + _realm.Write(() => + { + rvo.RealmValueProperty = listVal; + }); + + Assert.That(rvo.RealmValueProperty.AsList(), Is.EqualTo(listVal)); + + var newStringVal = "Luigi"; + + _realm.Write(() => + { + rvo.RealmValueProperty = newStringVal; + }); + + Assert.That(rvo.RealmValueProperty == newStringVal); + } + + [Test] + public void List_WhenManaged_CanBeModified() + { + var listVal = new List { 1, "string", true }; + + var rvo = _realm.Write(() => + { + return _realm.Add(new RealmValueObject { RealmValueProperty = listVal }); + }); + + _realm.Write(() => + { + rvo.RealmValueProperty.AsList()[1] = "Mario"; + listVal[1] = "Mario"; // To keep both list updated + }); + + Assert.That(rvo.RealmValueProperty.AsList(), Is.EqualTo(listVal)); + + _realm.Write(() => + { + rvo.RealmValueProperty.AsList().RemoveAt(2); + listVal.RemoveAt(2); + }); + + Assert.That(rvo.RealmValueProperty.AsList(), Is.EqualTo(listVal)); + + _realm.Write(() => + { + rvo.RealmValueProperty.AsList().Add("newVal"); + listVal.Add("newVal"); + }); + + Assert.That(rvo.RealmValueProperty.AsList(), Is.EqualTo(listVal)); + } + + [TestCaseSource(nameof(CollectionGenerators))] + public void List_AddSetInsertMoveRemoveCollection_WorksAsIntended(Func collectionGenerator) + { + var listVal = new List { 1, "string", true }; + + var rvo = _realm.Write(() => + { + return _realm.Add(new RealmValueObject { RealmValueProperty = listVal }); + }); + + // Indexer + var c1 = collectionGenerator(1); + + _realm.Write(() => + { + rvo.RealmValueProperty.AsList()[1] = c1; + listVal[1] = c1; + }); + + Assert.That(rvo.RealmValueProperty.AsList(), Is.EqualTo(listVal)); + + // Insert + var c2 = collectionGenerator(2); + + _realm.Write(() => + { + rvo.RealmValueProperty.AsList().Insert(1, c2); + listVal.Insert(1, c2); + }); + + Assert.That(rvo.RealmValueProperty.AsList(), Is.EqualTo(listVal)); + + // Add + var c3 = collectionGenerator(3); + + _realm.Write(() => + { + rvo.RealmValueProperty.AsList().Add(c3); + listVal.Add(c3); + }); + + Assert.That(rvo.RealmValueProperty.AsList(), Is.EqualTo(listVal)); + + // Move + _realm.Write(() => + { + rvo.RealmValueProperty.AsList().Move(0, 1); + listVal.Move(0, 1); + }); + + Assert.That(rvo.RealmValueProperty.AsList(), Is.EqualTo(listVal)); + + // Remove + _realm.Write(() => + { + rvo.RealmValueProperty.AsList().RemoveAt(2); + listVal.RemoveAt(2); + }); + + Assert.That(rvo.RealmValueProperty.AsList(), Is.EqualTo(listVal)); + } + + [Test] + public void List_RemoveWithCollectionArgument_ReturnsFalse() + { + var innerList = new List { "inner2", true, 2.0 }; + var innerDict = new Dictionary + { + { "s1", 1 }, + { "s2", "ah" }, + { "s3", true }, + }; + + var listVal = new List { innerList, innerDict }; + + var rvo = _realm.Write(() => + { + return _realm.Add(new RealmValueObject { RealmValueProperty = listVal }); + }); + + _realm.Write(() => + { + Assert.That(rvo.RealmValueProperty.AsList().Remove(innerList), Is.False); + Assert.That(rvo.RealmValueProperty.AsList().Remove(innerDict), Is.False); + }); + } + + [Test] + public void List_WhenManaged_WorksWithDynamicLikeApi() + { + var originalList = ListGenerator(1); + + var rvo = _realm.Write(() => + { + return _realm.Add(new RealmValueObject()); + }); + + _realm.Write(() => + { + rvo.DynamicApi.Set(nameof(RealmValueObject.RealmValueProperty), originalList); + }); + + var rvp = rvo.DynamicApi.Get(nameof(RealmValueObject.RealmValueProperty)); + + Assert.That(rvp.AsList(), Is.EqualTo(originalList)); + } + +#if !UNITY + [Test] +#endif + public void List_WhenManaged_WorksWithDynamic() + { + var originalList = ListGenerator(1); + + dynamic rvo = _realm.Write(() => + { + return _realm.Add(new RealmValueObject()); + }); + + _realm.Write(() => + { + rvo.RealmValueProperty = originalList; + }); + + var rvp = rvo.RealmValueProperty; + + Assert.That(rvp.AsList(), Is.EqualTo(originalList)); + } + + [Test] + public void List_WhenManaged_WorksWithNotifications() + { + var originalList = new List { 1, ListGenerator(1), DictGenerator(1) }; + + var rvo = _realm.Write(() => + { + return _realm.Add(new RealmValueObject { RealmValueProperty = originalList }); + }); + + var callbacks = new List(); + using var token = rvo.RealmValueProperty.AsList().SubscribeForNotifications((collection, changes) => + { + if (changes != null) + { + callbacks.Add(changes); + } + }); + + _realm.Write(() => + { + rvo.RealmValueProperty.AsList()[0] = "mario"; + }); + + _realm.Refresh(); + Assert.That(callbacks.Count, Is.EqualTo(1)); + Assert.That(callbacks[0].ModifiedIndices, Is.EqualTo(new[] { 0 })); + + callbacks.Clear(); + + _realm.Write(() => + { + rvo.RealmValueProperty.AsList()[1].AsList()[0] = "luigi"; + }); + + _realm.Refresh(); + Assert.That(callbacks.Count, Is.EqualTo(1)); + Assert.That(callbacks[0].ModifiedIndices, Is.EqualTo(new[] { 1 })); + + callbacks.Clear(); + + _realm.Write(() => + { + rvo.RealmValueProperty.AsList()[2].AsDictionary()["s1"] = "peach"; + }); + + _realm.Refresh(); + Assert.That(callbacks.Count, Is.EqualTo(1)); + Assert.That(callbacks[0].ModifiedIndices, Is.EqualTo(new[] { 2 })); + + callbacks.Clear(); + } + + #endregion + + #region Dictionary + + [Test] + public void Dictionary_WhenRetrieved_WorksWithAllTypes([Values(true, false)] bool isManaged) + { + var innerList = ListGenerator(1); + var innerDict = DictGenerator(1); + + var originalDict = new Dictionary + { + { "1", RealmValue.Null }, + { "2", 1 }, + { "3", true }, + { "4", "string" }, + { "5", new byte[] { 0, 1, 2 } }, + { "6", new DateTimeOffset(1234, 5, 6, 7, 8, 9, TimeSpan.Zero) }, + { "7", 1f }, + { "8", 2d }, + { "9", 3m }, + { "a", new ObjectId("5f63e882536de46d71877979") }, + { "b", Guid.Parse("3809d6d9-7618-4b3d-8044-2aa35fd02f31") }, + { "c", new InternalObject { IntProperty = 10, StringProperty = "brown" } }, + { "d", innerList }, + { "e", innerDict }, + }; + + RealmValue rv = originalDict; + + if (isManaged) + { + rv = PersistAndFind(rv).RealmValueProperty; + } + + Assert.That(rv.Type, Is.EqualTo(RealmValueType.Dictionary)); + Assert.That(rv != RealmValue.Null, "Different than null"); + + Assert.That(rv.AsDictionary(), Is.EquivalentTo(originalDict)); + Assert.That(rv.AsAny(), Is.EquivalentTo(originalDict)); + Assert.That(rv.As>(), Is.EquivalentTo(originalDict)); + + Assert.That(rv == originalDict, "Equal to original dict"); + Assert.That(rv.Equals(originalDict), "Equal to original dict with Equals"); + } + + [Test] + public void Dictionary_WhenUsingIndexAccessWithCollections_WorkAsExpected([Values(true, false)] bool isManaged) + { + var innerList = ListGenerator(1); + var innerDict = DictGenerator(1); + + var originalDict = new Dictionary + { + { "0", innerList }, + { "1", innerDict }, + }; + + RealmValue rv = originalDict; + + if (isManaged) + { + rv = PersistAndFind(rv).RealmValueProperty; + } + + var retrievedList = rv.AsDictionary().ElementAt(0).Value; + var retrievedDict = rv.AsDictionary().ElementAt(1).Value; + + Assert.That(retrievedList.AsList(), Is.EqualTo(innerList)); + Assert.That(retrievedDict.AsDictionary(), Is.EquivalentTo(innerDict)); + + var retrievedList2 = rv.AsDictionary()["0"]; + var retrievedDict2 = rv.AsDictionary()["1"]; + + Assert.That(retrievedList2.AsList(), Is.EqualTo(innerList)); + Assert.That(retrievedDict2.AsDictionary(), Is.EquivalentTo(innerDict)); + } + + [Test] + public void Dictionary_CanBeCopiedFromManagedDictionary([Values(true, false)] bool isManaged) + { + var originalDict = DictGenerator(1); + + RealmValue rv = originalDict; + + if (isManaged) + { + rv = PersistAndFind(originalDict).RealmValueProperty; + } + + var newObj = new RealmValueObject { RealmValueProperty = rv }; + + RealmValue rv2; + + if (isManaged) + { + rv2 = PersistAndFind(rv).RealmValueProperty; + } + else + { + rv2 = newObj.RealmValueProperty; + } + + Assert.That(rv.AsDictionary(), Is.EqualTo(rv2.AsDictionary())); + Assert.That(rv.AsDictionary(), Is.EqualTo(originalDict)); + Assert.That(rv2.AsDictionary(), Is.EqualTo(originalDict)); + } + + [Test] + public void Dictionary_BuiltWithConstructorMethodOrOperatorOrCreate_WorksTheSame([Values(true, false)] bool isManaged) + { + var originalDict = DictGenerator(1); + + RealmValue rvOperator = originalDict; + RealmValue rvConstructor = RealmValue.Dictionary(originalDict); + RealmValue rvCreate = RealmValue.Create(originalDict, RealmValueType.Dictionary); + + if (isManaged) + { + rvOperator = PersistAndFind(rvOperator).RealmValueProperty; + rvConstructor = PersistAndFind(rvConstructor).RealmValueProperty; + rvCreate = PersistAndFind(rvCreate).RealmValueProperty; + } + + Assert.That(rvOperator.AsDictionary(), Is.EqualTo(originalDict)); + Assert.That(rvConstructor.AsDictionary(), Is.EqualTo(originalDict)); + Assert.That(rvCreate.AsDictionary(), Is.EqualTo(originalDict)); + } + + [Test] + public void Dictionary_WhenManaged_IsNotSameReferenceAsOriginalList() + { + var originalDict = DictGenerator(1); + + RealmValue rv = originalDict; + rv = PersistAndFind(rv).RealmValueProperty; + var retrievedDict = rv.AsDictionary(); + + Assert.That(ReferenceEquals(originalDict, retrievedDict), Is.False); + } + + [Test] + public void Dictionary_WhenUnmanaged_IsSameReferenceAsOriginalList() + { + var originalDict = DictGenerator(1); + + RealmValue rv = originalDict; + var retrievedDict = rv.AsDictionary(); + + Assert.That(ReferenceEquals(originalDict, retrievedDict), Is.True); + } + + [Test] + public void Dictionary_AfterCreation_CanBeAssigned([Values(true, false)] bool isManaged) + { + var stringVal = "Mario"; + var rvo = new RealmValueObject { RealmValueProperty = stringVal }; + + if (isManaged) + { + _realm.Write(() => + { + _realm.Add(rvo); + }); + } + + Assert.That(rvo.RealmValueProperty == stringVal); + + var dictVal = DictGenerator(1); + + _realm.Write(() => + { + rvo.RealmValueProperty = dictVal; + }); + + Assert.That(rvo.RealmValueProperty.AsDictionary(), Is.EquivalentTo(dictVal)); + + var newStringVal = "Luigi"; + + _realm.Write(() => + { + rvo.RealmValueProperty = newStringVal; + }); + + Assert.That(rvo.RealmValueProperty == newStringVal); + } + + [Test] + public void Dictionary_WhenManaged_CanBeModified() + { + var dictVal = DictGenerator(1); + + var rvo = _realm.Write(() => + { + return _realm.Add(new RealmValueObject { RealmValueProperty = dictVal }); + }); + + _realm.Write(() => + { + rvo.RealmValueProperty.AsDictionary()["s1"] = "Mario"; + dictVal["s1"] = "Mario"; // To keep both list updated + }); + + Assert.That(rvo.RealmValueProperty.AsDictionary(), Is.EquivalentTo(dictVal)); + + _realm.Write(() => + { + rvo.RealmValueProperty.AsDictionary().Remove("s2"); + dictVal.Remove("s2"); + }); + + Assert.That(rvo.RealmValueProperty.AsDictionary(), Is.EquivalentTo(dictVal)); + + _realm.Write(() => + { + rvo.RealmValueProperty.AsDictionary().Add("s4", "newVal"); + dictVal.Add("s4", "newVal"); + }); + + Assert.That(rvo.RealmValueProperty.AsDictionary(), Is.EquivalentTo(dictVal)); + } + + [TestCaseSource(nameof(CollectionGenerators))] + public void Dictionary_AddSetInsertMoveRemoveCollection_WorksAsIntended(Func collectionGenerator) + { + var dictVal = DictGenerator(1); + + var rvo = _realm.Write(() => + { + return _realm.Add(new RealmValueObject { RealmValueProperty = dictVal }); + }); + + // Indexer + var c1 = collectionGenerator(1); + + _realm.Write(() => + { + rvo.RealmValueProperty.AsDictionary()["s1"] = c1; + dictVal["s1"] = c1; + }); + + Assert.That(rvo.RealmValueProperty.AsDictionary(), Is.EquivalentTo(dictVal)); + + // Add + var c3 = collectionGenerator(3); + + _realm.Write(() => + { + rvo.RealmValueProperty.AsDictionary().Add("s4", c3); + dictVal.Add("s4", c3); + }); + + Assert.That(rvo.RealmValueProperty.AsDictionary(), Is.EquivalentTo(dictVal)); + + // Remove + _realm.Write(() => + { + rvo.RealmValueProperty.AsDictionary().Remove("s4"); + dictVal.Remove("s4"); + }); + + Assert.That(rvo.RealmValueProperty.AsDictionary(), Is.EquivalentTo(dictVal)); + } + + [Test] + public void Dictionary_RemoveWithCollectionArgument_ReturnsFalse() + { + var innerList = ListGenerator(1); + var innerDict = DictGenerator(1); + + var dictVal = new Dictionary + { + { "s1", innerList }, + { "s3", innerDict }, + }; + + var rvo = _realm.Write(() => + { + return _realm.Add(new RealmValueObject { RealmValueProperty = dictVal }); + }); + + _realm.Write(() => + { + Assert.That(rvo.RealmValueProperty.AsDictionary().Remove(new KeyValuePair("s1", innerList)), Is.False); + Assert.That(rvo.RealmValueProperty.AsDictionary().Remove(new KeyValuePair("s3", innerDict)), Is.False); + }); + } + + [Test] + public void Dictionary_WhenManaged_WorksWithDynamicLikeApi() + { + var dictVal = DictGenerator(1); + + var rvo = _realm.Write(() => + { + return _realm.Add(new RealmValueObject()); + }); + + _realm.Write(() => + { + rvo.DynamicApi.Set(nameof(RealmValueObject.RealmValueProperty), dictVal); + }); + + var rvp = rvo.DynamicApi.Get(nameof(RealmValueObject.RealmValueProperty)); + + Assert.That(rvp.AsDictionary(), Is.EqualTo(dictVal)); + } + +#if !UNITY + [Test] +#endif + public void Dictionary_WhenManaged_WorksWithDynamic() + { + var dictVal = DictGenerator(1); + + dynamic rvo = _realm.Write(() => + { + return _realm.Add(new RealmValueObject()); + }); + + _realm.Write(() => + { + rvo.RealmValueProperty = dictVal; + }); + + var rvp = rvo.RealmValueProperty; + + Assert.That(rvp.AsDictionary(), Is.EqualTo(dictVal)); + } + + [Test] + public void Dictionary_WhenManaged_WorksWithNotifications() + { + var dictVal = new Dictionary { { "s1", 1 }, { "s2", ListGenerator(1) }, { "s3", DictGenerator(1) } }; + + var rvo = _realm.Write(() => + { + return _realm.Add(new RealmValueObject { RealmValueProperty = dictVal }); + }); + + var callbacks = new List(); + using var token = rvo.RealmValueProperty.AsDictionary().SubscribeForNotifications((collection, changes) => + { + if (changes != null) + { + callbacks.Add(changes); + } + }); + + _realm.Write(() => + { + rvo.RealmValueProperty.AsDictionary()["s1"] = "mario"; + }); + + _realm.Refresh(); + Assert.That(callbacks.Count, Is.EqualTo(1)); + + callbacks.Clear(); + + _realm.Write(() => + { + rvo.RealmValueProperty.AsDictionary()["s2"].AsList()[0] = "mario"; + }); + + _realm.Refresh(); + Assert.That(callbacks.Count, Is.EqualTo(1)); + + callbacks.Clear(); + + _realm.Write(() => + { + rvo.RealmValueProperty.AsDictionary()["s3"].AsDictionary()["s1"] = "peach"; + }); + + _realm.Refresh(); + Assert.That(callbacks.Count, Is.EqualTo(1)); + + callbacks.Clear(); + } + + [Test] + public void MixedCollection_Filter_CountSizeType() + { + var ob1 = new RealmValueObject { RealmValueProperty = 2 }; + var ob2 = new RealmValueObject { RealmValueProperty = new List { 1, "string", 23.0 } }; + var ob3 = new RealmValueObject { RealmValueProperty = new List { 1, "string" } }; + var ob4 = new RealmValueObject { RealmValueProperty = new Dictionary { { "s1", 22 } } }; + + _realm.Write(() => + { + _realm.Add(ob1); + _realm.Add(ob2); + _realm.Add(ob3); + _realm.Add(ob4); + }); + + var rvos = _realm.All(); + + var q = rvos.Filter("RealmValueProperty.@size <= 2"); + Assert.That(q.ToList().Select(i => i.Id), Is.EquivalentTo(new[] { ob3.Id, ob4.Id })); + + q = rvos.Filter("RealmValueProperty.@count > 2"); + Assert.That(q.ToList().Select(i => i.Id), Is.EquivalentTo(new[] { ob2.Id })); + + q = rvos.Filter("RealmValueProperty.@type == 'dictionary'"); + Assert.That(q.ToList().Select(i => i.Id), Is.EquivalentTo(new[] { ob4.Id })); + + q = rvos.Filter("RealmValueProperty.@type == 'list'"); + Assert.That(q.ToList().Select(i => i.Id), Is.EquivalentTo(new[] { ob2.Id, ob3.Id })); + + q = rvos.Filter("RealmValueProperty.@type == 'collection'"); + Assert.That(q.ToList().Select(i => i.Id), Is.EquivalentTo(new[] { ob2.Id, ob3.Id, ob4.Id })); + } + + [Test] + public void MixedCollection_Filter_AnyAllNone() + { + var ob1 = new RealmValueObject { RealmValueProperty = 2 }; + var ob2 = new RealmValueObject { RealmValueProperty = new List { "a", "string" } }; + var ob3 = new RealmValueObject { RealmValueProperty = new List { 1, "string" } }; + var ob4 = new RealmValueObject { RealmValueProperty = new List { 1, 23 } }; + + _realm.Write(() => + { + _realm.Add(ob1); + _realm.Add(ob2); + _realm.Add(ob3); + _realm.Add(ob4); + }); + + var rvos = _realm.All(); + + var q = rvos.Filter("ANY RealmValueProperty[*].@type == 'string'"); + Assert.That(q.ToList().Select(i => i.Id), Is.EquivalentTo(new[] { ob2.Id, ob3.Id })); + + // NONE and ALL match both also on "empty lists", that's why they match also on ob1 + q = rvos.Filter("NONE RealmValueProperty[*].@type == 'string'"); + Assert.That(q.ToList().Select(i => i.Id), Is.EquivalentTo(new[] { ob1.Id, ob4.Id })); + + q = rvos.Filter("ALL RealmValueProperty[*].@type == 'string'"); + Assert.That(q.ToList().Select(i => i.Id), Is.EquivalentTo(new[] { ob2.Id, ob1.Id })); + } + + [Test] + public void IndexedRealmValue_WithCollection_BasicTest() + { + var innerList = ListGenerator(1); + var innerDict = DictGenerator(1); + + var originalList = new List + { + RealmValue.Null, + 1, + true, + "string", + new byte[] { 0, 1, 2 }, + new DateTimeOffset(1234, 5, 6, 7, 8, 9, TimeSpan.Zero), + 1f, + 2d, + 3m, + new ObjectId("5f63e882536de46d71877979"), + Guid.Parse("3809d6d9-7618-4b3d-8044-2aa35fd02f31"), + new InternalObject { IntProperty = 10, StringProperty = "brown" }, + innerList, + innerDict, + }; + + var obj = _realm.Write(() => + { + return _realm.Add(new IndexedRealmValueObject { RealmValueProperty = originalList }); + }); + + RealmValue rv = obj.RealmValueProperty; + + Assert.That(rv.Type, Is.EqualTo(RealmValueType.List)); + Assert.That(rv != RealmValue.Null); + Assert.That(rv.AsList(), Is.EqualTo(originalList)); + + var originalDict = new Dictionary + { + { "1", RealmValue.Null }, + { "2", 1 }, + { "3", true }, + { "4", "string" }, + { "5", new byte[] { 0, 1, 2 } }, + { "6", new DateTimeOffset(1234, 5, 6, 7, 8, 9, TimeSpan.Zero) }, + { "7", 1f }, + { "8", 2d }, + { "9", 3m }, + { "a", new ObjectId("5f63e882536de46d71877979") }, + { "b", Guid.Parse("3809d6d9-7618-4b3d-8044-2aa35fd02f31") }, + { "c", new InternalObject { IntProperty = 10, StringProperty = "brown" } }, + { "d", innerList }, + { "e", innerDict }, + }; + + _realm.Write(() => + { + obj.RealmValueProperty = originalDict; + }); + + rv = obj.RealmValueProperty; + + Assert.That(rv.Type, Is.EqualTo(RealmValueType.Dictionary)); + Assert.That(rv != RealmValue.Null, "Different than null"); + + Assert.That(rv.AsDictionary(), Is.EquivalentTo(originalDict)); + Assert.That(rv.AsAny(), Is.EquivalentTo(originalDict)); + Assert.That(rv.As>(), Is.EquivalentTo(originalDict)); + } + + #endregion + } +} diff --git a/Tests/Realm.Tests/Database/TestObjects.cs b/Tests/Realm.Tests/Database/TestObjects.cs index dd77bdbee7..ddfee7695a 100644 --- a/Tests/Realm.Tests/Database/TestObjects.cs +++ b/Tests/Realm.Tests/Database/TestObjects.cs @@ -1127,6 +1127,16 @@ public partial class RealmValueObject : TestRealmObject public IDictionary TestDict { get; } = null!; } + public partial class IndexedRealmValueObject : TestRealmObject + { + [PrimaryKey] + [MapTo("_id")] + public int Id { get; set; } = TestHelpers.Random.Next(); + + [Indexed] + public RealmValue RealmValueProperty { get; set; } + } + public partial class LinksObject : TestRealmObject { [PrimaryKey] diff --git a/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/IndexedRealmValueObject_generated.cs b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/IndexedRealmValueObject_generated.cs new file mode 100644 index 0000000000..01d1413ac1 --- /dev/null +++ b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/IndexedRealmValueObject_generated.cs @@ -0,0 +1,412 @@ +// +#nullable enable + +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using Realms; +using Realms.Schema; +using Realms.Tests; +using Realms.Tests.Database; +using Realms.Weaving; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Xml.Serialization; +using TestEmbeddedObject = Realms.IEmbeddedObject; +using TestRealmObject = Realms.IRealmObject; + +namespace Realms.Tests +{ + [Generated] + [Woven(typeof(IndexedRealmValueObjectObjectHelper)), Realms.Preserve(AllMembers = true)] + public partial class IndexedRealmValueObject : IRealmObject, INotifyPropertyChanged, IReflectableType + { + + [Realms.Preserve] + static IndexedRealmValueObject() + { + Realms.Serialization.RealmObjectSerializer.Register(new IndexedRealmValueObjectSerializer()); + } + + /// + /// Defines the schema for the class. + /// + public static Realms.Schema.ObjectSchema RealmSchema = new Realms.Schema.ObjectSchema.Builder("IndexedRealmValueObject", ObjectSchema.ObjectType.RealmObject) + { + Realms.Schema.Property.Primitive("_id", Realms.RealmValueType.Int, isPrimaryKey: true, indexType: IndexType.None, isNullable: false, managedName: "Id"), + Realms.Schema.Property.RealmValue("RealmValueProperty", managedName: "RealmValueProperty"), + }.Build(); + + #region IRealmObject implementation + + private IIndexedRealmValueObjectAccessor? _accessor; + + Realms.IRealmAccessor Realms.IRealmObjectBase.Accessor => Accessor; + + private IIndexedRealmValueObjectAccessor Accessor => _accessor ??= new IndexedRealmValueObjectUnmanagedAccessor(typeof(IndexedRealmValueObject)); + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsManaged => Accessor.IsManaged; + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsValid => Accessor.IsValid; + + /// + [IgnoreDataMember, XmlIgnore] + public bool IsFrozen => Accessor.IsFrozen; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.Realm? Realm => Accessor.Realm; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.Schema.ObjectSchema ObjectSchema => Accessor.ObjectSchema!; + + /// + [IgnoreDataMember, XmlIgnore] + public Realms.DynamicObjectApi DynamicApi => Accessor.DynamicApi; + + /// + [IgnoreDataMember, XmlIgnore] + public int BacklinksCount => Accessor.BacklinksCount; + + void ISettableManagedAccessor.SetManagedAccessor(Realms.IRealmAccessor managedAccessor, Realms.Weaving.IRealmObjectHelper? helper, bool update, bool skipDefaults) + { + var newAccessor = (IIndexedRealmValueObjectAccessor)managedAccessor; + var oldAccessor = _accessor; + _accessor = newAccessor; + + if (helper != null && oldAccessor != null) + { + if (!skipDefaults || oldAccessor.Id != default(int)) + { + newAccessor.Id = oldAccessor.Id; + } + newAccessor.RealmValueProperty = oldAccessor.RealmValueProperty; + } + + if (_propertyChanged != null) + { + SubscribeForNotifications(); + } + + OnManaged(); + } + + #endregion + + /// + /// Called when the object has been managed by a Realm. + /// + /// + /// This method will be called either when a managed object is materialized or when an unmanaged object has been + /// added to the Realm. It can be useful for providing some initialization logic as when the constructor is invoked, + /// it is not yet clear whether the object is managed or not. + /// + partial void OnManaged(); + + private event PropertyChangedEventHandler? _propertyChanged; + + /// + public event PropertyChangedEventHandler? PropertyChanged + { + add + { + if (_propertyChanged == null) + { + SubscribeForNotifications(); + } + + _propertyChanged += value; + } + + remove + { + _propertyChanged -= value; + + if (_propertyChanged == null) + { + UnsubscribeFromNotifications(); + } + } + } + + /// + /// Called when a property has changed on this class. + /// + /// The name of the property. + /// + /// For this method to be called, you need to have first subscribed to . + /// This can be used to react to changes to the current object, e.g. raising for computed properties. + /// + /// + /// + /// class MyClass : IRealmObject + /// { + /// public int StatusCodeRaw { get; set; } + /// public StatusCodeEnum StatusCode => (StatusCodeEnum)StatusCodeRaw; + /// partial void OnPropertyChanged(string propertyName) + /// { + /// if (propertyName == nameof(StatusCodeRaw)) + /// { + /// RaisePropertyChanged(nameof(StatusCode)); + /// } + /// } + /// } + /// + /// Here, we have a computed property that depends on a persisted one. In order to notify any + /// subscribers that StatusCode has changed, we implement and + /// raise manually by calling . + /// + partial void OnPropertyChanged(string? propertyName); + + private void RaisePropertyChanged([CallerMemberName] string propertyName = "") + { + _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + OnPropertyChanged(propertyName); + } + + private void SubscribeForNotifications() + { + Accessor.SubscribeForNotifications(RaisePropertyChanged); + } + + private void UnsubscribeFromNotifications() + { + Accessor.UnsubscribeFromNotifications(); + } + + /// + /// Converts a to . Equivalent to . + /// + /// The to convert. + /// The stored in the . + public static explicit operator IndexedRealmValueObject?(Realms.RealmValue val) => val.Type == Realms.RealmValueType.Null ? null : val.AsRealmObject(); + + /// + /// Implicitly constructs a from . + /// + /// The value to store in the . + /// A containing the supplied . + public static implicit operator Realms.RealmValue(IndexedRealmValueObject? val) => val == null ? Realms.RealmValue.Null : Realms.RealmValue.Object(val); + + /// + /// Implicitly constructs a from . + /// + /// The value to store in the . + /// A containing the supplied . + public static implicit operator Realms.QueryArgument(IndexedRealmValueObject? val) => (Realms.RealmValue)val; + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public TypeInfo GetTypeInfo() => Accessor.GetTypeInfo(this); + + /// + public override bool Equals(object? obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is InvalidObject) + { + return !IsValid; + } + + if (!(obj is Realms.IRealmObjectBase iro)) + { + return false; + } + + return Accessor.Equals(iro.Accessor); + } + + /// + public override int GetHashCode() => IsManaged ? Accessor.GetHashCode() : base.GetHashCode(); + + /// + public override string? ToString() => Accessor.ToString(); + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class IndexedRealmValueObjectObjectHelper : Realms.Weaving.IRealmObjectHelper + { + public void CopyToRealm(Realms.IRealmObjectBase instance, bool update, bool skipDefaults) + { + throw new InvalidOperationException("This method should not be called for source generated classes."); + } + + public Realms.ManagedAccessor CreateAccessor() => new IndexedRealmValueObjectManagedAccessor(); + + public Realms.IRealmObjectBase CreateInstance() => new IndexedRealmValueObject(); + + public bool TryGetPrimaryKeyValue(Realms.IRealmObjectBase instance, out RealmValue value) + { + value = ((IIndexedRealmValueObjectAccessor)instance.Accessor).Id; + return true; + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + internal interface IIndexedRealmValueObjectAccessor : Realms.IRealmAccessor + { + int Id { get; set; } + + Realms.RealmValue RealmValueProperty { get; set; } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class IndexedRealmValueObjectManagedAccessor : Realms.ManagedAccessor, IIndexedRealmValueObjectAccessor + { + public int Id + { + get => (int)GetValue("_id"); + set => SetValueUnique("_id", value); + } + + public Realms.RealmValue RealmValueProperty + { + get => (Realms.RealmValue)GetValue("RealmValueProperty"); + set => SetValue("RealmValueProperty", value); + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class IndexedRealmValueObjectUnmanagedAccessor : Realms.UnmanagedAccessor, IIndexedRealmValueObjectAccessor + { + public override ObjectSchema ObjectSchema => IndexedRealmValueObject.RealmSchema; + + private int _id = TestHelpers.Random.Next(); + public int Id + { + get => _id; + set + { + _id = value; + RaisePropertyChanged("Id"); + } + } + + private Realms.RealmValue _realmValueProperty; + public Realms.RealmValue RealmValueProperty + { + get => _realmValueProperty; + set + { + _realmValueProperty = value; + RaisePropertyChanged("RealmValueProperty"); + } + } + + public IndexedRealmValueObjectUnmanagedAccessor(Type objectType) : base(objectType) + { + } + + public override Realms.RealmValue GetValue(string propertyName) + { + return propertyName switch + { + "_id" => _id, + "RealmValueProperty" => _realmValueProperty, + _ => throw new MissingMemberException($"The object does not have a gettable Realm property with name {propertyName}"), + }; + } + + public override void SetValue(string propertyName, Realms.RealmValue val) + { + switch (propertyName) + { + case "_id": + throw new InvalidOperationException("Cannot set the value of a primary key property with SetValue. You need to use SetValueUnique"); + case "RealmValueProperty": + RealmValueProperty = (Realms.RealmValue)val; + return; + default: + throw new MissingMemberException($"The object does not have a settable Realm property with name {propertyName}"); + } + } + + public override void SetValueUnique(string propertyName, Realms.RealmValue val) + { + if (propertyName != "_id") + { + throw new InvalidOperationException($"Cannot set the value of non primary key property ({propertyName}) with SetValueUnique"); + } + + Id = (int)val; + } + + public override IList GetListValue(string propertyName) + { + throw new MissingMemberException($"The object does not have a Realm list property with name {propertyName}"); + } + + public override ISet GetSetValue(string propertyName) + { + throw new MissingMemberException($"The object does not have a Realm set property with name {propertyName}"); + } + + public override IDictionary GetDictionaryValue(string propertyName) + { + throw new MissingMemberException($"The object does not have a Realm dictionary property with name {propertyName}"); + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class IndexedRealmValueObjectSerializer : Realms.Serialization.RealmObjectSerializerBase + { + public override string SchemaName => "IndexedRealmValueObject"; + + protected override void SerializeValue(MongoDB.Bson.Serialization.BsonSerializationContext context, BsonSerializationArgs args, IndexedRealmValueObject value) + { + context.Writer.WriteStartDocument(); + + WriteValue(context, args, "_id", value.Id); + WriteValue(context, args, "RealmValueProperty", value.RealmValueProperty); + + context.Writer.WriteEndDocument(); + } + + protected override IndexedRealmValueObject CreateInstance() => new IndexedRealmValueObject(); + + protected override void ReadValue(IndexedRealmValueObject instance, string name, BsonDeserializationContext context) + { + switch (name) + { + case "_id": + instance.Id = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "RealmValueProperty": + instance.RealmValueProperty = BsonSerializer.LookupSerializer().Deserialize(context); + break; + default: + context.Reader.SkipValue(); + break; + } + } + + protected override void ReadArrayElement(IndexedRealmValueObject instance, string name, BsonDeserializationContext context) + { + // No persisted list/set properties to deserialize + } + + protected override void ReadDocumentField(IndexedRealmValueObject instance, string name, string fieldName, BsonDeserializationContext context) + { + // No persisted dictionary properties to deserialize + } + } + } +} diff --git a/Tests/Realm.Tests/TestHelpers.cs b/Tests/Realm.Tests/TestHelpers.cs index 6157b290ce..aa1d5f7f65 100644 --- a/Tests/Realm.Tests/TestHelpers.cs +++ b/Tests/Realm.Tests/TestHelpers.cs @@ -704,6 +704,12 @@ public class StrongBox public static implicit operator StrongBox(T value) => new() { Value = value }; } + public static IEnumerable CollectionRealmValueTypes = + new[] { RealmValueType.List, RealmValueType.Set, RealmValueType.Dictionary }; + + public static IEnumerable PrimitiveRealmValueTypes = ((RealmValueType[])Enum.GetValues(typeof(RealmValueType))) + .Except(CollectionRealmValueTypes); + public static TestCaseData CreateTestCase(string description, T value) => new(description, value); public static TestCaseData CreateTestCase(Property prop) diff --git a/Tests/SourceGenerators/SourceGeneratorAssemblyToProcess/ErrorClasses/UnsupportedIndexableTypes.cs b/Tests/SourceGenerators/SourceGeneratorAssemblyToProcess/ErrorClasses/UnsupportedIndexableTypes.cs index 69138d23a4..f070badd1f 100644 --- a/Tests/SourceGenerators/SourceGeneratorAssemblyToProcess/ErrorClasses/UnsupportedIndexableTypes.cs +++ b/Tests/SourceGenerators/SourceGeneratorAssemblyToProcess/ErrorClasses/UnsupportedIndexableTypes.cs @@ -36,9 +36,6 @@ public partial class UnsupportedIndexableTypes : IRealmObject [Indexed] public RealmObj? ObjectProp { get; set; } - [Indexed] - public RealmValue RealmvalueProp { get; set; } - [Indexed] public decimal DecimalProp { get; set; } @@ -63,9 +60,6 @@ public partial class UnsupportedIndexableTypes : IRealmObject [Indexed(IndexType.None)] public int NoneIndexedInt { get; set; } - [Indexed(IndexType.General)] - public RealmValue GeneralRealmValueProp { get; set; } - [PrimaryKey] [Indexed] public int IndexedPrimaryKeyProp { get; set; } diff --git a/Tests/SourceGenerators/SourceGeneratorAssemblyToProcess/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/IndexedClass_generated.cs b/Tests/SourceGenerators/SourceGeneratorAssemblyToProcess/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/IndexedClass_generated.cs index 0d3414023f..b3b103a98b 100644 --- a/Tests/SourceGenerators/SourceGeneratorAssemblyToProcess/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/IndexedClass_generated.cs +++ b/Tests/SourceGenerators/SourceGeneratorAssemblyToProcess/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/IndexedClass_generated.cs @@ -39,6 +39,8 @@ static IndexedClass() Realms.Schema.Property.Primitive("IntProp", Realms.RealmValueType.Int, isPrimaryKey: false, indexType: IndexType.General, isNullable: false, managedName: "IntProp"), Realms.Schema.Property.Primitive("GuidProp", Realms.RealmValueType.Guid, isPrimaryKey: false, indexType: IndexType.General, isNullable: false, managedName: "GuidProp"), Realms.Schema.Property.Primitive("GeneralGuidProp", Realms.RealmValueType.Guid, isPrimaryKey: false, indexType: IndexType.General, isNullable: false, managedName: "GeneralGuidProp"), + Realms.Schema.Property.RealmValue("RealmValueProp", managedName: "RealmValueProp"), + Realms.Schema.Property.RealmValue("GeneralRealmValueProp", managedName: "GeneralRealmValueProp"), }.Build(); #region IRealmObject implementation @@ -106,6 +108,8 @@ void ISettableManagedAccessor.SetManagedAccessor(Realms.IRealmAccessor managedAc { newAccessor.GeneralGuidProp = oldAccessor.GeneralGuidProp; } + newAccessor.RealmValueProp = oldAccessor.RealmValueProp; + newAccessor.GeneralRealmValueProp = oldAccessor.GeneralRealmValueProp; } if (_propertyChanged != null) @@ -289,6 +293,10 @@ internal interface IIndexedClassAccessor : Realms.IRealmAccessor System.Guid GuidProp { get; set; } System.Guid GeneralGuidProp { get; set; } + + Realms.RealmValue RealmValueProp { get; set; } + + Realms.RealmValue GeneralRealmValueProp { get; set; } } [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] @@ -329,6 +337,18 @@ public System.Guid GeneralGuidProp get => (System.Guid)GetValue("GeneralGuidProp"); set => SetValue("GeneralGuidProp", value); } + + public Realms.RealmValue RealmValueProp + { + get => (Realms.RealmValue)GetValue("RealmValueProp"); + set => SetValue("RealmValueProp", value); + } + + public Realms.RealmValue GeneralRealmValueProp + { + get => (Realms.RealmValue)GetValue("GeneralRealmValueProp"); + set => SetValue("GeneralRealmValueProp", value); + } } [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] @@ -402,6 +422,28 @@ public System.Guid GeneralGuidProp } } + private Realms.RealmValue _realmValueProp; + public Realms.RealmValue RealmValueProp + { + get => _realmValueProp; + set + { + _realmValueProp = value; + RaisePropertyChanged("RealmValueProp"); + } + } + + private Realms.RealmValue _generalRealmValueProp; + public Realms.RealmValue GeneralRealmValueProp + { + get => _generalRealmValueProp; + set + { + _generalRealmValueProp = value; + RaisePropertyChanged("GeneralRealmValueProp"); + } + } + public IndexedClassUnmanagedAccessor(Type objectType) : base(objectType) { } @@ -416,6 +458,8 @@ public override Realms.RealmValue GetValue(string propertyName) "IntProp" => _intProp, "GuidProp" => _guidProp, "GeneralGuidProp" => _generalGuidProp, + "RealmValueProp" => _realmValueProp, + "GeneralRealmValueProp" => _generalRealmValueProp, _ => throw new MissingMemberException($"The object does not have a gettable Realm property with name {propertyName}"), }; } @@ -441,6 +485,12 @@ public override void SetValue(string propertyName, Realms.RealmValue val) case "GeneralGuidProp": GeneralGuidProp = (System.Guid)val; return; + case "RealmValueProp": + RealmValueProp = (Realms.RealmValue)val; + return; + case "GeneralRealmValueProp": + GeneralRealmValueProp = (Realms.RealmValue)val; + return; default: throw new MissingMemberException($"The object does not have a settable Realm property with name {propertyName}"); } @@ -487,6 +537,8 @@ protected override void SerializeValue(MongoDB.Bson.Serialization.BsonSerializat WriteValue(context, args, "IntProp", value.IntProp); WriteValue(context, args, "GuidProp", value.GuidProp); WriteValue(context, args, "GeneralGuidProp", value.GeneralGuidProp); + WriteValue(context, args, "RealmValueProp", value.RealmValueProp); + WriteValue(context, args, "GeneralRealmValueProp", value.GeneralRealmValueProp); context.Writer.WriteEndDocument(); } @@ -515,6 +567,12 @@ protected override void ReadValue(IndexedClass instance, string name, BsonDeseri case "GeneralGuidProp": instance.GeneralGuidProp = BsonSerializer.LookupSerializer().Deserialize(context); break; + case "RealmValueProp": + instance.RealmValueProp = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "GeneralRealmValueProp": + instance.GeneralRealmValueProp = BsonSerializer.LookupSerializer().Deserialize(context); + break; default: context.Reader.SkipValue(); break; diff --git a/Tests/SourceGenerators/SourceGeneratorAssemblyToProcess/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/UnsupportedIndexableTypes.diagnostics.cs b/Tests/SourceGenerators/SourceGeneratorAssemblyToProcess/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/UnsupportedIndexableTypes.diagnostics.cs index 992bd1e435..f165f3a8dd 100644 --- a/Tests/SourceGenerators/SourceGeneratorAssemblyToProcess/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/UnsupportedIndexableTypes.diagnostics.cs +++ b/Tests/SourceGenerators/SourceGeneratorAssemblyToProcess/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/UnsupportedIndexableTypes.diagnostics.cs @@ -57,22 +57,11 @@ { "Id": "RLM007", "Severity": 3, - "Message": "UnsupportedIndexableTypes.RealmvalueProp is marked as [Indexed] which is only allowed on integral types as well as string, bool and DateTimeOffset, not on RealmValue.", + "Message": "UnsupportedIndexableTypes.DecimalProp is marked as [Indexed] which is only allowed on integral types as well as string, bool and DateTimeOffset, not on decimal.", "Location": { "StartLine": 39, "StartColumn": 9, "EndLine": 40, - "EndColumn": 55 - } - }, - { - "Id": "RLM007", - "Severity": 3, - "Message": "UnsupportedIndexableTypes.DecimalProp is marked as [Indexed] which is only allowed on integral types as well as string, bool and DateTimeOffset, not on decimal.", - "Location": { - "StartLine": 42, - "StartColumn": 9, - "EndLine": 43, "EndColumn": 49 } }, @@ -81,9 +70,9 @@ "Severity": 3, "Message": "UnsupportedIndexableTypes.UnsupportedProp is of type 'int[]' which is not yet supported. If that is supposed to be a model class, make sure it implements IRealmObject/IEmbeddedObject/IAsymmetricObject.", "Location": { - "StartLine": 45, + "StartLine": 42, "StartColumn": 9, - "EndLine": 46, + "EndLine": 43, "EndColumn": 60 } }, @@ -92,9 +81,9 @@ "Severity": 3, "Message": "UnsupportedIndexableTypes.FtsIntProp is marked as [Indexed(IndexType.FullText)] which is only allowed on string properties, not on int.", "Location": { - "StartLine": 48, + "StartLine": 45, "StartColumn": 9, - "EndLine": 49, + "EndLine": 46, "EndColumn": 44 } }, @@ -103,9 +92,9 @@ "Severity": 3, "Message": "UnsupportedIndexableTypes.FtsBoolProp is marked as [Indexed(IndexType.FullText)] which is only allowed on string properties, not on bool.", "Location": { - "StartLine": 51, + "StartLine": 48, "StartColumn": 9, - "EndLine": 52, + "EndLine": 49, "EndColumn": 46 } }, @@ -114,9 +103,9 @@ "Severity": 3, "Message": "UnsupportedIndexableTypes.FtsRealmValueProp is marked as [Indexed(IndexType.FullText)] which is only allowed on string properties, not on RealmValue.", "Location": { - "StartLine": 54, + "StartLine": 51, "StartColumn": 9, - "EndLine": 55, + "EndLine": 52, "EndColumn": 58 } }, @@ -125,9 +114,9 @@ "Severity": 3, "Message": "UnsupportedIndexableTypes.FtsObjectProp is marked as [Indexed(IndexType.FullText)] which is only allowed on string properties, not on RealmObj.", "Location": { - "StartLine": 57, + "StartLine": 54, "StartColumn": 9, - "EndLine": 58, + "EndLine": 55, "EndColumn": 53 } }, @@ -136,9 +125,9 @@ "Severity": 3, "Message": "UnsupportedIndexableTypes.FtsDoubleProp is marked as [Indexed(IndexType.FullText)] which is only allowed on string properties, not on double.", "Location": { - "StartLine": 60, + "StartLine": 57, "StartColumn": 9, - "EndLine": 61, + "EndLine": 58, "EndColumn": 50 } }, @@ -147,31 +136,20 @@ "Severity": 3, "Message": "UnsupportedIndexableTypes.NoneIndexedInt is annotated as [Indexed(IndexType.None)] which is not allowed. If you don't wish to index the property, removed the [Indexed] attribute.", "Location": { - "StartLine": 63, + "StartLine": 60, "StartColumn": 9, - "EndLine": 64, + "EndLine": 61, "EndColumn": 48 } }, - { - "Id": "RLM007", - "Severity": 3, - "Message": "UnsupportedIndexableTypes.GeneralRealmValueProp is marked as [Indexed] which is only allowed on integral types as well as string, bool and DateTimeOffset, not on RealmValue.", - "Location": { - "StartLine": 66, - "StartColumn": 9, - "EndLine": 67, - "EndColumn": 62 - } - }, { "Id": "RLM028", "Severity": 3, "Message": "UnsupportedIndexableTypes.IndexedPrimaryKeyProp is marked has both [Indexed] and [PrimaryKey] attributes which is not allowed. PrimaryKey properties are indexed by default so the [Indexed] attribute is redundant.", "Location": { - "StartLine": 69, + "StartLine": 63, "StartColumn": 9, - "EndLine": 71, + "EndLine": 65, "EndColumn": 55 } } diff --git a/Tests/SourceGenerators/SourceGeneratorAssemblyToProcess/TestClasses/IndexedClass.cs b/Tests/SourceGenerators/SourceGeneratorAssemblyToProcess/TestClasses/IndexedClass.cs index adc6991251..38565570bd 100644 --- a/Tests/SourceGenerators/SourceGeneratorAssemblyToProcess/TestClasses/IndexedClass.cs +++ b/Tests/SourceGenerators/SourceGeneratorAssemblyToProcess/TestClasses/IndexedClass.cs @@ -40,5 +40,11 @@ internal partial class IndexedClass : IRealmObject [Indexed(IndexType.General)] public Guid GeneralGuidProp { get; set; } + + [Indexed] + public RealmValue RealmValueProp { get; set; } + + [Indexed(IndexType.General)] + public RealmValue GeneralRealmValueProp { get; set; } } } diff --git a/Tests/Weaver/AnalyticsAssembly/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/RootRealmClass_generated.cs b/Tests/Weaver/AnalyticsAssembly/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/RootRealmClass_generated.cs index b78d7fb1a3..10ee2f3108 100644 --- a/Tests/Weaver/AnalyticsAssembly/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/RootRealmClass_generated.cs +++ b/Tests/Weaver/AnalyticsAssembly/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/RootRealmClass_generated.cs @@ -36,16 +36,7 @@ static RootRealmClass() /// public static Realms.Schema.ObjectSchema RealmSchema = new Realms.Schema.ObjectSchema.Builder("RootRealmClass", ObjectSchema.ObjectType.RealmObject) { - Realms.Schema.Property.Object("JustForRef", "JustForObjectReference", managedName: "JustForRef"), - Realms.Schema.Property.ObjectList("ReferenceList", "JustForObjectReference", managedName: "ReferenceList"), - Realms.Schema.Property.PrimitiveList("PrimitiveList", Realms.RealmValueType.Int, areElementsNullable: false, managedName: "PrimitiveList"), - Realms.Schema.Property.ObjectDictionary("ReferenceDictionary", "JustForObjectReference", managedName: "ReferenceDictionary"), Realms.Schema.Property.PrimitiveDictionary("PrimitiveDictionary", Realms.RealmValueType.Int, areElementsNullable: false, managedName: "PrimitiveDictionary"), - Realms.Schema.Property.ObjectSet("ReferenceSet", "JustForObjectReference", managedName: "ReferenceSet"), - Realms.Schema.Property.PrimitiveSet("PrimitiveSet", Realms.RealmValueType.Int, areElementsNullable: false, managedName: "PrimitiveSet"), - Realms.Schema.Property.Primitive("Counter", Realms.RealmValueType.Int, isPrimaryKey: false, indexType: IndexType.None, isNullable: false, managedName: "Counter"), - Realms.Schema.Property.RealmValue("RealmValue", managedName: "RealmValue"), - Realms.Schema.Property.Backlinks("JustBackLink", "JustForObjectReference", "UseAsBacklink", managedName: "JustBackLink"), }.Build(); #region IRealmObject implementation @@ -94,30 +85,10 @@ void ISettableManagedAccessor.SetManagedAccessor(Realms.IRealmAccessor managedAc { if (!skipDefaults) { - newAccessor.ReferenceList.Clear(); - newAccessor.PrimitiveList.Clear(); - newAccessor.ReferenceDictionary.Clear(); newAccessor.PrimitiveDictionary.Clear(); - newAccessor.ReferenceSet.Clear(); - newAccessor.PrimitiveSet.Clear(); } - if (oldAccessor.JustForRef != null && newAccessor.Realm != null) - { - newAccessor.Realm.Add(oldAccessor.JustForRef, update); - } - newAccessor.JustForRef = oldAccessor.JustForRef; - Realms.CollectionExtensions.PopulateCollection(oldAccessor.ReferenceList, newAccessor.ReferenceList, update, skipDefaults); - Realms.CollectionExtensions.PopulateCollection(oldAccessor.PrimitiveList, newAccessor.PrimitiveList, update, skipDefaults); - Realms.CollectionExtensions.PopulateCollection(oldAccessor.ReferenceDictionary, newAccessor.ReferenceDictionary, update, skipDefaults); Realms.CollectionExtensions.PopulateCollection(oldAccessor.PrimitiveDictionary, newAccessor.PrimitiveDictionary, update, skipDefaults); - Realms.CollectionExtensions.PopulateCollection(oldAccessor.ReferenceSet, newAccessor.ReferenceSet, update, skipDefaults); - Realms.CollectionExtensions.PopulateCollection(oldAccessor.PrimitiveSet, newAccessor.PrimitiveSet, update, skipDefaults); - if (!skipDefaults || oldAccessor.Counter != default(Realms.RealmInteger)) - { - newAccessor.Counter = oldAccessor.Counter; - } - newAccessor.RealmValue = oldAccessor.RealmValue; } if (_propertyChanged != null) @@ -290,78 +261,12 @@ public bool TryGetPrimaryKeyValue(Realms.IRealmObjectBase instance, out RealmVal [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] internal interface IRootRealmClassAccessor : Realms.IRealmAccessor { - JustForObjectReference? JustForRef { get; set; } - - System.Collections.Generic.IList ReferenceList { get; } - - System.Collections.Generic.IList PrimitiveList { get; } - - System.Collections.Generic.IDictionary ReferenceDictionary { get; } - System.Collections.Generic.IDictionary PrimitiveDictionary { get; } - - System.Collections.Generic.ISet ReferenceSet { get; } - - System.Collections.Generic.ISet PrimitiveSet { get; } - - Realms.RealmInteger Counter { get; set; } - - Realms.RealmValue RealmValue { get; set; } - - System.Linq.IQueryable JustBackLink { get; } } [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] private class RootRealmClassManagedAccessor : Realms.ManagedAccessor, IRootRealmClassAccessor { - public JustForObjectReference? JustForRef - { - get => (JustForObjectReference?)GetValue("JustForRef"); - set => SetValue("JustForRef", value); - } - - private System.Collections.Generic.IList _referenceList = null!; - public System.Collections.Generic.IList ReferenceList - { - get - { - if (_referenceList == null) - { - _referenceList = GetListValue("ReferenceList"); - } - - return _referenceList; - } - } - - private System.Collections.Generic.IList _primitiveList = null!; - public System.Collections.Generic.IList PrimitiveList - { - get - { - if (_primitiveList == null) - { - _primitiveList = GetListValue("PrimitiveList"); - } - - return _primitiveList; - } - } - - private System.Collections.Generic.IDictionary _referenceDictionary = null!; - public System.Collections.Generic.IDictionary ReferenceDictionary - { - get - { - if (_referenceDictionary == null) - { - _referenceDictionary = GetDictionaryValue("ReferenceDictionary"); - } - - return _referenceDictionary; - } - } - private System.Collections.Generic.IDictionary _primitiveDictionary = null!; public System.Collections.Generic.IDictionary PrimitiveDictionary { @@ -375,60 +280,6 @@ public System.Collections.Generic.IDictionary PrimitiveDictionary return _primitiveDictionary; } } - - private System.Collections.Generic.ISet _referenceSet = null!; - public System.Collections.Generic.ISet ReferenceSet - { - get - { - if (_referenceSet == null) - { - _referenceSet = GetSetValue("ReferenceSet"); - } - - return _referenceSet; - } - } - - private System.Collections.Generic.ISet _primitiveSet = null!; - public System.Collections.Generic.ISet PrimitiveSet - { - get - { - if (_primitiveSet == null) - { - _primitiveSet = GetSetValue("PrimitiveSet"); - } - - return _primitiveSet; - } - } - - public Realms.RealmInteger Counter - { - get => (Realms.RealmInteger)GetValue("Counter"); - set => SetValue("Counter", value); - } - - public Realms.RealmValue RealmValue - { - get => (Realms.RealmValue)GetValue("RealmValue"); - set => SetValue("RealmValue", value); - } - - private System.Linq.IQueryable _justBackLink = null!; - public System.Linq.IQueryable JustBackLink - { - get - { - if (_justBackLink == null) - { - _justBackLink = GetBacklinks("JustBackLink"); - } - - return _justBackLink; - } - } } [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] @@ -436,85 +287,20 @@ private class RootRealmClassUnmanagedAccessor : Realms.UnmanagedAccessor, IRootR { public override ObjectSchema ObjectSchema => RootRealmClass.RealmSchema; - private JustForObjectReference? _justForRef; - public JustForObjectReference? JustForRef - { - get => _justForRef; - set - { - _justForRef = value; - RaisePropertyChanged("JustForRef"); - } - } - - public System.Collections.Generic.IList ReferenceList { get; } = new List(); - - public System.Collections.Generic.IList PrimitiveList { get; } = new List(); - - public System.Collections.Generic.IDictionary ReferenceDictionary { get; } = new Dictionary(); - public System.Collections.Generic.IDictionary PrimitiveDictionary { get; } = new Dictionary(); - public System.Collections.Generic.ISet ReferenceSet { get; } = new HashSet(RealmSet.Comparer); - - public System.Collections.Generic.ISet PrimitiveSet { get; } = new HashSet(RealmSet.Comparer); - - private Realms.RealmInteger _counter; - public Realms.RealmInteger Counter - { - get => _counter; - set - { - _counter = value; - RaisePropertyChanged("Counter"); - } - } - - private Realms.RealmValue _realmValue; - public Realms.RealmValue RealmValue - { - get => _realmValue; - set - { - _realmValue = value; - RaisePropertyChanged("RealmValue"); - } - } - - public System.Linq.IQueryable JustBackLink => throw new NotSupportedException("Using backlinks is only possible for managed(persisted) objects."); - public RootRealmClassUnmanagedAccessor(Type objectType) : base(objectType) { } public override Realms.RealmValue GetValue(string propertyName) { - return propertyName switch - { - "JustForRef" => _justForRef, - "Counter" => _counter, - "RealmValue" => _realmValue, - "JustBackLink" => throw new NotSupportedException("Using backlinks is only possible for managed(persisted) objects."), - _ => throw new MissingMemberException($"The object does not have a gettable Realm property with name {propertyName}"), - }; + throw new MissingMemberException($"The object does not have a gettable Realm property with name {propertyName}"); } public override void SetValue(string propertyName, Realms.RealmValue val) { - switch (propertyName) - { - case "JustForRef": - JustForRef = (JustForObjectReference?)val; - return; - case "Counter": - Counter = (Realms.RealmInteger)val; - return; - case "RealmValue": - RealmValue = (Realms.RealmValue)val; - return; - default: - throw new MissingMemberException($"The object does not have a settable Realm property with name {propertyName}"); - } + throw new MissingMemberException($"The object does not have a settable Realm property with name {propertyName}"); } public override void SetValueUnique(string propertyName, Realms.RealmValue val) @@ -524,29 +310,18 @@ public override void SetValueUnique(string propertyName, Realms.RealmValue val) public override IList GetListValue(string propertyName) { - return propertyName switch - { - "ReferenceList" => (IList)ReferenceList, - "PrimitiveList" => (IList)PrimitiveList, - _ => throw new MissingMemberException($"The object does not have a Realm list property with name {propertyName}"), - }; + throw new MissingMemberException($"The object does not have a Realm list property with name {propertyName}"); } public override ISet GetSetValue(string propertyName) { - return propertyName switch - { - "ReferenceSet" => (ISet)ReferenceSet, - "PrimitiveSet" => (ISet)PrimitiveSet, - _ => throw new MissingMemberException($"The object does not have a Realm set property with name {propertyName}"), - }; + throw new MissingMemberException($"The object does not have a Realm set property with name {propertyName}"); } public override IDictionary GetDictionaryValue(string propertyName) { return propertyName switch { - "ReferenceDictionary" => (IDictionary)ReferenceDictionary, "PrimitiveDictionary" => (IDictionary)PrimitiveDictionary, _ => throw new MissingMemberException($"The object does not have a Realm dictionary property with name {propertyName}"), }; @@ -562,15 +337,7 @@ protected override void SerializeValue(MongoDB.Bson.Serialization.BsonSerializat { context.Writer.WriteStartDocument(); - WriteValue(context, args, "JustForRef", value.JustForRef); - WriteList(context, args, "ReferenceList", value.ReferenceList); - WriteList(context, args, "PrimitiveList", value.PrimitiveList); - WriteDictionary(context, args, "ReferenceDictionary", value.ReferenceDictionary); WriteDictionary(context, args, "PrimitiveDictionary", value.PrimitiveDictionary); - WriteSet(context, args, "ReferenceSet", value.ReferenceSet); - WriteSet(context, args, "PrimitiveSet", value.PrimitiveSet); - WriteValue(context, args, "Counter", value.Counter); - WriteValue(context, args, "RealmValue", value.RealmValue); context.Writer.WriteEndDocument(); } @@ -581,22 +348,6 @@ protected override void ReadValue(RootRealmClass instance, string name, BsonDese { switch (name) { - case "JustForRef": - instance.JustForRef = Realms.Serialization.RealmObjectSerializer.LookupSerializer()!.DeserializeById(context); - break; - case "Counter": - instance.Counter = BsonSerializer.LookupSerializer>().Deserialize(context); - break; - case "RealmValue": - instance.RealmValue = BsonSerializer.LookupSerializer().Deserialize(context); - break; - case "ReferenceList": - case "PrimitiveList": - case "ReferenceSet": - case "PrimitiveSet": - ReadArray(instance, name, context); - break; - case "ReferenceDictionary": case "PrimitiveDictionary": ReadDictionary(instance, name, context); break; @@ -608,30 +359,13 @@ protected override void ReadValue(RootRealmClass instance, string name, BsonDese protected override void ReadArrayElement(RootRealmClass instance, string name, BsonDeserializationContext context) { - switch (name) - { - case "ReferenceList": - instance.ReferenceList.Add(Realms.Serialization.RealmObjectSerializer.LookupSerializer()!.DeserializeById(context)!); - break; - case "PrimitiveList": - instance.PrimitiveList.Add(BsonSerializer.LookupSerializer().Deserialize(context)); - break; - case "ReferenceSet": - instance.ReferenceSet.Add(Realms.Serialization.RealmObjectSerializer.LookupSerializer()!.DeserializeById(context)!); - break; - case "PrimitiveSet": - instance.PrimitiveSet.Add(BsonSerializer.LookupSerializer().Deserialize(context)); - break; - } + // No persisted list/set properties to deserialize } protected override void ReadDocumentField(RootRealmClass instance, string name, string fieldName, BsonDeserializationContext context) { switch (name) { - case "ReferenceDictionary": - instance.ReferenceDictionary[fieldName] = Realms.Serialization.RealmObjectSerializer.LookupSerializer()!.DeserializeById(context)!; - break; case "PrimitiveDictionary": instance.PrimitiveDictionary[fieldName] = BsonSerializer.LookupSerializer().Deserialize(context); break; diff --git a/Tests/Weaver/AssemblyToProcess/FaultyClasses.cs b/Tests/Weaver/AssemblyToProcess/FaultyClasses.cs index 7adb72e00a..2742094e24 100644 --- a/Tests/Weaver/AssemblyToProcess/FaultyClasses.cs +++ b/Tests/Weaver/AssemblyToProcess/FaultyClasses.cs @@ -98,13 +98,16 @@ public class IndexedProperties : RealmObject [Indexed] public DateTimeOffset DateTimeOffsetProperty { get; set; } + [Indexed] + public RealmValue RealmValueProperty { get; set; } + [Indexed(IndexType.General)] public ObjectId ObjectIdProperty { get; set; } [Indexed(IndexType.FullText)] public string? FullTextStringProperty { get; set; } - // This should cause an error: + // These should cause an error: [Indexed] public float SingleProperty { get; set; } diff --git a/wrappers/src/dictionary_cs.cpp b/wrappers/src/dictionary_cs.cpp index 1909198e71..cec6395d17 100644 --- a/wrappers/src/dictionary_cs.cpp +++ b/wrappers/src/dictionary_cs.cpp @@ -71,6 +71,30 @@ extern "C" { }); } + REALM_EXPORT void* realm_dictionary_add_collection(object_store::Dictionary& dictionary, realm_value_t key, realm_value_type type, bool allow_override, NativeException::Marshallable& ex) + { + return handle_errors(ex, [&]()-> void* { + + auto dict_key = from_capi(key.string); + if (!allow_override && dictionary.contains(dict_key)) + { + throw KeyAlreadyExistsException(dict_key); + } + + switch (type) + { + case realm::binding::realm_value_type::RLM_TYPE_LIST: + dictionary.insert_collection(dict_key, CollectionType::List); + return new List(dictionary.get_list(dict_key)); + case realm::binding::realm_value_type::RLM_TYPE_DICTIONARY: + dictionary.insert_collection(dict_key, CollectionType::Dictionary); + return new object_store::Dictionary(dictionary.get_dictionary(dict_key)); + default: + REALM_TERMINATE("Invalid collection type"); + } + }); + } + REALM_EXPORT void realm_dictionary_set(object_store::Dictionary& dictionary, realm_value_t key, realm_value_t value, NativeException::Marshallable& ex) { handle_errors(ex, [&]() { @@ -90,10 +114,11 @@ extern "C" { REALM_EXPORT bool realm_dictionary_try_get(object_store::Dictionary& dictionary, realm_value_t key, realm_value_t* value, NativeException::Marshallable& ex) { return handle_errors(ex, [&]() { - auto mixed_value = dictionary.try_get_any(from_capi(key.string)); + auto dict_key = from_capi(key.string); + auto mixed_value = dictionary.try_get_any(dict_key); if (mixed_value) { - *value = to_capi(dictionary, *mixed_value); + *value = to_capi(dictionary, *mixed_value, dict_key); return true; } @@ -110,7 +135,7 @@ extern "C" { auto pair = dictionary.get_pair(ndx); *key = to_capi(Mixed(pair.first)); - *value = to_capi(dictionary, pair.second); + *value = to_capi(dictionary, pair.second, pair.first); }); } diff --git a/wrappers/src/list_cs.cpp b/wrappers/src/list_cs.cpp index b5ec43ba2f..c296b7544f 100644 --- a/wrappers/src/list_cs.cpp +++ b/wrappers/src/list_cs.cpp @@ -46,13 +46,6 @@ namespace { extern "C" { -REALM_EXPORT Object* list_add_embedded(List& list, NativeException::Marshallable& ex) -{ - return handle_errors(ex, [&]() { - return new Object(list.get_realm(), list.get_object_schema(), list.add_embedded()); - }); -} - REALM_EXPORT void list_set_value(List& list, size_t list_ndx, realm_value_t value, NativeException::Marshallable& ex) { handle_errors(ex, [&]() { @@ -90,6 +83,28 @@ REALM_EXPORT Object* list_set_embedded(List& list, size_t list_ndx, NativeExcept }); } +REALM_EXPORT void* list_set_collection(List& list, size_t list_ndx, realm_value_type type, NativeException::Marshallable& ex) +{ + return handle_errors(ex, [&]()-> void*{ + + if (list_ndx > list.size()) { + throw IndexOutOfRangeException("Insert into RealmList", list_ndx, list.size()); + } + + switch (type) + { + case realm::binding::realm_value_type::RLM_TYPE_LIST: + list.set_collection(list_ndx, CollectionType::List); + return new List(list.get_list(list_ndx)); + case realm::binding::realm_value_type::RLM_TYPE_DICTIONARY: + list.set_collection(list_ndx, CollectionType::Dictionary); + return new object_store::Dictionary(list.get_dictionary(list_ndx)); + default: + REALM_TERMINATE("Invalid collection type"); + } + }); +} + REALM_EXPORT void list_insert_value(List& list, size_t list_ndx, realm_value_t value, NativeException::Marshallable& ex) { handle_errors(ex, [&]() { @@ -114,11 +129,6 @@ REALM_EXPORT void list_insert_value(List& list, size_t list_ndx, realm_value_t v }); } -REALM_EXPORT void list_add_value(List& list, realm_value_t value, NativeException::Marshallable& ex) -{ - list_insert_value(list, list.size(), value, ex); -} - REALM_EXPORT Object* list_insert_embedded(List& list, size_t list_ndx, NativeException::Marshallable& ex) { return handle_errors(ex, [&]() { @@ -131,6 +141,45 @@ REALM_EXPORT Object* list_insert_embedded(List& list, size_t list_ndx, NativeExc }); } +REALM_EXPORT void* list_insert_collection(List& list, size_t list_ndx, realm_value_type type, NativeException::Marshallable& ex) +{ + return handle_errors(ex, [&]()-> void*{ + + if (list_ndx > list.size()) { + throw IndexOutOfRangeException("Insert into RealmList", list_ndx, list.size()); + } + + switch (type) + { + case realm::binding::realm_value_type::RLM_TYPE_LIST: + list.insert_collection(list_ndx, CollectionType::List); + return new List(list.get_list(list_ndx)); + case realm::binding::realm_value_type::RLM_TYPE_DICTIONARY: + list.insert_collection(list_ndx, CollectionType::Dictionary); + return new object_store::Dictionary(list.get_dictionary(list_ndx)); + default: + REALM_TERMINATE("Invalid collection type"); + } + }); +} + +REALM_EXPORT void list_add_value(List& list, realm_value_t value, NativeException::Marshallable& ex) +{ + list_insert_value(list, list.size(), value, ex); +} + +REALM_EXPORT Object* list_add_embedded(List& list, NativeException::Marshallable& ex) +{ + return handle_errors(ex, [&]() { + return new Object(list.get_realm(), list.get_object_schema(), list.add_embedded()); + }); +} + +REALM_EXPORT void* list_add_collection(List& list, realm_value_type type, NativeException::Marshallable& ex) +{ + return list_insert_collection(list, list.size(), type, ex); +} + REALM_EXPORT void list_get_value(List& list, size_t ndx, realm_value_t* value, NativeException::Marshallable& ex) { handle_errors(ex, [&]() { @@ -140,15 +189,29 @@ REALM_EXPORT void list_get_value(List& list, size_t ndx, realm_value_t* value, N if ((list.get_type() & ~PropertyType::Flags) == PropertyType::Object) { *value = to_capi(list.get(ndx), list.get_realm()); + return; } - else { - auto val = list.get_any(ndx); - if (!val.is_null() && val.get_type() == type_TypedLink) { - *value = to_capi(val.get(), list.get_realm()); - } - else { - *value = to_capi(std::move(val)); - } + + auto val = list.get_any(ndx); + + if (val.is_null()) { + *value = to_capi(std::move(val)); + return; + } + + switch (val.get_type()) { + case type_TypedLink: + *value = to_capi(val.get(), list.get_realm()); + break; + case type_List: + *value = to_capi(new List(list.get_list(ndx))); + break; + case type_Dictionary: + *value = to_capi(new object_store::Dictionary(list.get_dictionary(ndx))); + break; + default: + *value = to_capi(std::move(val)); + break; } }); } diff --git a/wrappers/src/marshalling.hpp b/wrappers/src/marshalling.hpp index c8c6fa69e0..78df9ce1cc 100644 --- a/wrappers/src/marshalling.hpp +++ b/wrappers/src/marshalling.hpp @@ -101,6 +101,9 @@ enum class realm_value_type : uint8_t { RLM_TYPE_OBJECT_ID, RLM_TYPE_LINK, RLM_TYPE_UUID, + RLM_TYPE_LIST, + RLM_TYPE_SET, + RLM_TYPE_DICTIONARY, }; enum class key_path_collection_type : uint8_t { @@ -162,6 +165,7 @@ typedef struct realm_value { realm_uuid_t uuid; realm_link_t link; + object_store::Collection* collection; char data[16]; }; @@ -507,6 +511,25 @@ static inline realm_value_t to_capi(ObjLink obj_link, SharedRealm realm) return to_capi(realm->read_group().get_object(obj_link), realm); } +// Collections need to have their own overload of to_capi, as at the moment there's no API to retrieve a collection value +// from Mixed (like val.get), so we need first to retrieve the collection itself with the specific methods like +// list.get_list, list.get_dictionary and so on +static inline realm_value_t to_capi(List* list) +{ + realm_value_t val{}; + val.type = realm_value_type::RLM_TYPE_LIST; + val.collection = list; + return val; +} + +static inline realm_value_t to_capi(object_store::Dictionary* dictionary) +{ + realm_value_t val{}; + val.type = realm_value_type::RLM_TYPE_DICTIONARY; + val.collection = dictionary; + return val; +} + static inline realm_value_t to_capi(const Mixed& value) { realm_value_t val{}; @@ -569,6 +592,9 @@ static inline realm_value_t to_capi(const Mixed& value) val.uuid = to_capi(value.get()); break; } + case type_List: + case type_Dictionary: + REALM_TERMINATE("Can't use this overload of to_capi on values containing collections, use to_capi(Collection*) instead."); default: REALM_TERMINATE("Invalid Mixed value type"); } @@ -577,7 +603,7 @@ static inline realm_value_t to_capi(const Mixed& value) return val; } -inline realm_value_t to_capi(const object_store::Dictionary& dictionary, const Mixed& val) +inline realm_value_t to_capi(const object_store::Dictionary& dictionary, const Mixed& val, const StringData key) { if (val.is_null()) { return to_capi(std::move(val)); @@ -588,10 +614,15 @@ inline realm_value_t to_capi(const object_store::Dictionary& dictionary, const M if ((dictionary.get_type() & ~PropertyType::Flags) == PropertyType::Object) { return to_capi(ObjLink(dictionary.get_object_schema().table_key, val.get()), dictionary.get_realm()); } - REALM_UNREACHABLE(); case type_TypedLink: return to_capi(val.get_link(), dictionary.get_realm()); + case type_List: + return to_capi(new List(dictionary.get_list(key))); + break; + case type_Dictionary: + return to_capi(new object_store::Dictionary(dictionary.get_dictionary(key))); + break; default: return to_capi(std::move(val)); } diff --git a/wrappers/src/object_cs.cpp b/wrappers/src/object_cs.cpp index d7f7fed587..4ab900189f 100644 --- a/wrappers/src/object_cs.cpp +++ b/wrappers/src/object_cs.cpp @@ -100,15 +100,30 @@ extern "C" { else { value->type = realm_value_type::RLM_TYPE_NULL; } + + return; } - else { - auto val = object.get_obj().get_any(prop.column_key); - if (!val.is_null() && val.get_type() == type_TypedLink) { - *value = to_capi(val.get(), object.realm()); - } - else { - *value = to_capi(std::move(val)); - } + + auto val = object.get_obj().get_any(prop.column_key); + if (val.is_null()) + { + *value = to_capi(std::move(val)); + return; + } + + switch (val.get_type()) { + case type_TypedLink: + *value = to_capi(val.get(), object.realm()); + break; + case type_List: + *value = to_capi(new List(object.realm(), object.get_obj(), prop.column_key)); + break; + case type_Dictionary: + *value = to_capi(new object_store::Dictionary(object.realm(), object.get_obj(), prop.column_key)); + break; + default: + *value = to_capi(std::move(val)); + break; } }); } @@ -159,6 +174,27 @@ extern "C" { }); } + REALM_EXPORT void* object_set_collection_value(Object& object, size_t property_ndx, realm_value_type type, NativeException::Marshallable& ex) + { + return handle_errors(ex, [&]()-> void* { + verify_can_set(object); + + auto prop = get_property(object, property_ndx); + + switch (type) + { + case realm::binding::realm_value_type::RLM_TYPE_LIST: + object.get_obj().set_collection(prop.column_key, CollectionType::List); + return new List(object.realm(), object.get_obj(), prop.column_key); + case realm::binding::realm_value_type::RLM_TYPE_DICTIONARY: + object.get_obj().set_collection(prop.column_key, CollectionType::Dictionary); + return new object_store::Dictionary(object.realm(), object.get_obj(), prop.column_key); + default: + REALM_TERMINATE("Invalid collection type"); + } + }); + } + REALM_EXPORT Results* object_get_backlinks(Object& object, size_t property_ndx, NativeException::Marshallable& ex) { return handle_errors(ex, [&] {