From c6bb604c8fd0f9b620bb53daa39b15738e556ce3 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Sat, 13 Apr 2024 21:40:20 +0200 Subject: [PATCH] RNET-1132: Add tests for sync schema migration (#3570) * Add tests for sync schema migration * Make nullable schemas explicit --- Realm.sln.DotSettings | 3 + .../Database/RealmValueWithCollections.cs | 3 +- .../NullablesV0_generated.cs | 701 +++++++++++++++++ .../NullablesV1_generated.cs | 724 ++++++++++++++++++ Tests/Realm.Tests/Sync/SyncMigrationTests.cs | 365 +++++++++ Tools/DeployApps/BaasClient.cs | 116 ++- 6 files changed, 1897 insertions(+), 15 deletions(-) create mode 100644 Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/NullablesV0_generated.cs create mode 100644 Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/NullablesV1_generated.cs create mode 100644 Tests/Realm.Tests/Sync/SyncMigrationTests.cs diff --git a/Realm.sln.DotSettings b/Realm.sln.DotSettings index c84c41511a..40903ac831 100644 --- a/Realm.sln.DotSettings +++ b/Realm.sln.DotSettings @@ -9,6 +9,7 @@ True True True + True True True True @@ -64,6 +65,7 @@ True True True + True True True True @@ -72,6 +74,7 @@ True True True + True True True True diff --git a/Tests/Realm.Tests/Database/RealmValueWithCollections.cs b/Tests/Realm.Tests/Database/RealmValueWithCollections.cs index 738387d1cf..9a2d318d5c 100644 --- a/Tests/Realm.Tests/Database/RealmValueWithCollections.cs +++ b/Tests/Realm.Tests/Database/RealmValueWithCollections.cs @@ -1,4 +1,4 @@ -//////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////// // // Copyright 2023 Realm Inc. // @@ -532,6 +532,7 @@ public void List_WhenManaged_WorksWithNotifications() callbacks.Clear(); } + #endregion #region Dictionary diff --git a/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/NullablesV0_generated.cs b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/NullablesV0_generated.cs new file mode 100644 index 0000000000..cef4fc9ffc --- /dev/null +++ b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/NullablesV0_generated.cs @@ -0,0 +1,701 @@ +// +#nullable enable + +using Baas; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using NUnit.Framework; +using Realms; +using Realms.Schema; +using Realms.Sync; +using Realms.Sync.Exceptions; +using Realms.Tests.Sync; +using Realms.Weaving; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using System.Xml.Serialization; + +namespace Realms.Tests.Sync +{ + [Generated] + [Woven(typeof(NullablesV0ObjectHelper)), Realms.Preserve(AllMembers = true)] + public partial class NullablesV0 : IRealmObject, INotifyPropertyChanged, IReflectableType + { + + [Realms.Preserve] + static NullablesV0() + { + Realms.Serialization.RealmObjectSerializer.Register(new NullablesV0Serializer()); + } + + /// + /// Defines the schema for the class. + /// + public static Realms.Schema.ObjectSchema RealmSchema = new Realms.Schema.ObjectSchema.Builder("Nullables", ObjectSchema.ObjectType.RealmObject) + { + Realms.Schema.Property.Primitive("_id", Realms.RealmValueType.ObjectId, isPrimaryKey: true, indexType: IndexType.None, isNullable: false, managedName: "Id"), + Realms.Schema.Property.Primitive("Differentiator", Realms.RealmValueType.ObjectId, isPrimaryKey: false, indexType: IndexType.None, isNullable: false, managedName: "Differentiator"), + Realms.Schema.Property.Primitive("BoolValue", Realms.RealmValueType.Bool, isPrimaryKey: false, indexType: IndexType.None, isNullable: true, managedName: "BoolValue"), + Realms.Schema.Property.Primitive("IntValue", Realms.RealmValueType.Int, isPrimaryKey: false, indexType: IndexType.None, isNullable: true, managedName: "IntValue"), + Realms.Schema.Property.Primitive("DoubleValue", Realms.RealmValueType.Double, isPrimaryKey: false, indexType: IndexType.None, isNullable: true, managedName: "DoubleValue"), + Realms.Schema.Property.Primitive("DecimalValue", Realms.RealmValueType.Decimal128, isPrimaryKey: false, indexType: IndexType.None, isNullable: true, managedName: "DecimalValue"), + Realms.Schema.Property.Primitive("DateValue", Realms.RealmValueType.Date, isPrimaryKey: false, indexType: IndexType.None, isNullable: true, managedName: "DateValue"), + Realms.Schema.Property.Primitive("StringValue", Realms.RealmValueType.String, isPrimaryKey: false, indexType: IndexType.None, isNullable: true, managedName: "StringValue"), + Realms.Schema.Property.Primitive("ObjectIdValue", Realms.RealmValueType.ObjectId, isPrimaryKey: false, indexType: IndexType.None, isNullable: true, managedName: "ObjectIdValue"), + Realms.Schema.Property.Primitive("UuidValue", Realms.RealmValueType.Guid, isPrimaryKey: false, indexType: IndexType.None, isNullable: true, managedName: "UuidValue"), + Realms.Schema.Property.Primitive("BinaryValue", Realms.RealmValueType.Data, isPrimaryKey: false, indexType: IndexType.None, isNullable: true, managedName: "BinaryValue"), + }.Build(); + + #region IRealmObject implementation + + private INullablesV0Accessor? _accessor; + + Realms.IRealmAccessor Realms.IRealmObjectBase.Accessor => Accessor; + + private INullablesV0Accessor Accessor => _accessor ??= new NullablesV0UnmanagedAccessor(typeof(NullablesV0)); + + /// + [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 = (INullablesV0Accessor)managedAccessor; + var oldAccessor = _accessor; + _accessor = newAccessor; + + if (helper != null && oldAccessor != null) + { + if (!skipDefaults || oldAccessor.Id != default(MongoDB.Bson.ObjectId)) + { + newAccessor.Id = oldAccessor.Id; + } + if (!skipDefaults || oldAccessor.Differentiator != default(MongoDB.Bson.ObjectId)) + { + newAccessor.Differentiator = oldAccessor.Differentiator; + } + if (!skipDefaults || oldAccessor.BoolValue != default(bool?)) + { + newAccessor.BoolValue = oldAccessor.BoolValue; + } + if (!skipDefaults || oldAccessor.IntValue != default(int?)) + { + newAccessor.IntValue = oldAccessor.IntValue; + } + if (!skipDefaults || oldAccessor.DoubleValue != default(double?)) + { + newAccessor.DoubleValue = oldAccessor.DoubleValue; + } + if (!skipDefaults || oldAccessor.DecimalValue != default(MongoDB.Bson.Decimal128?)) + { + newAccessor.DecimalValue = oldAccessor.DecimalValue; + } + newAccessor.DateValue = oldAccessor.DateValue; + if (!skipDefaults || oldAccessor.StringValue != default(string?)) + { + newAccessor.StringValue = oldAccessor.StringValue; + } + if (!skipDefaults || oldAccessor.ObjectIdValue != default(MongoDB.Bson.ObjectId?)) + { + newAccessor.ObjectIdValue = oldAccessor.ObjectIdValue; + } + if (!skipDefaults || oldAccessor.UuidValue != default(System.Guid?)) + { + newAccessor.UuidValue = oldAccessor.UuidValue; + } + if (!skipDefaults || oldAccessor.BinaryValue != default(byte[]?)) + { + newAccessor.BinaryValue = oldAccessor.BinaryValue; + } + } + + 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 NullablesV0?(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(NullablesV0? 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(NullablesV0? 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 NullablesV0ObjectHelper : 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 NullablesV0ManagedAccessor(); + + public Realms.IRealmObjectBase CreateInstance() => new NullablesV0(); + + public bool TryGetPrimaryKeyValue(Realms.IRealmObjectBase instance, out RealmValue value) + { + value = ((INullablesV0Accessor)instance.Accessor).Id; + return true; + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + internal interface INullablesV0Accessor : Realms.IRealmAccessor + { + MongoDB.Bson.ObjectId Id { get; set; } + + MongoDB.Bson.ObjectId Differentiator { get; set; } + + bool? BoolValue { get; set; } + + int? IntValue { get; set; } + + double? DoubleValue { get; set; } + + MongoDB.Bson.Decimal128? DecimalValue { get; set; } + + System.DateTimeOffset? DateValue { get; set; } + + string? StringValue { get; set; } + + MongoDB.Bson.ObjectId? ObjectIdValue { get; set; } + + System.Guid? UuidValue { get; set; } + + byte[]? BinaryValue { get; set; } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class NullablesV0ManagedAccessor : Realms.ManagedAccessor, INullablesV0Accessor + { + public MongoDB.Bson.ObjectId Id + { + get => (MongoDB.Bson.ObjectId)GetValue("_id"); + set => SetValueUnique("_id", value); + } + + public MongoDB.Bson.ObjectId Differentiator + { + get => (MongoDB.Bson.ObjectId)GetValue("Differentiator"); + set => SetValue("Differentiator", value); + } + + public bool? BoolValue + { + get => (bool?)GetValue("BoolValue"); + set => SetValue("BoolValue", value); + } + + public int? IntValue + { + get => (int?)GetValue("IntValue"); + set => SetValue("IntValue", value); + } + + public double? DoubleValue + { + get => (double?)GetValue("DoubleValue"); + set => SetValue("DoubleValue", value); + } + + public MongoDB.Bson.Decimal128? DecimalValue + { + get => (MongoDB.Bson.Decimal128?)GetValue("DecimalValue"); + set => SetValue("DecimalValue", value); + } + + public System.DateTimeOffset? DateValue + { + get => (System.DateTimeOffset?)GetValue("DateValue"); + set => SetValue("DateValue", value); + } + + public string? StringValue + { + get => (string?)GetValue("StringValue"); + set => SetValue("StringValue", value); + } + + public MongoDB.Bson.ObjectId? ObjectIdValue + { + get => (MongoDB.Bson.ObjectId?)GetValue("ObjectIdValue"); + set => SetValue("ObjectIdValue", value); + } + + public System.Guid? UuidValue + { + get => (System.Guid?)GetValue("UuidValue"); + set => SetValue("UuidValue", value); + } + + public byte[]? BinaryValue + { + get => (byte[]?)GetValue("BinaryValue"); + set => SetValue("BinaryValue", value); + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class NullablesV0UnmanagedAccessor : Realms.UnmanagedAccessor, INullablesV0Accessor + { + public override ObjectSchema ObjectSchema => NullablesV0.RealmSchema; + + private MongoDB.Bson.ObjectId _id = ObjectId.GenerateNewId(); + public MongoDB.Bson.ObjectId Id + { + get => _id; + set + { + _id = value; + RaisePropertyChanged("Id"); + } + } + + private MongoDB.Bson.ObjectId _differentiator; + public MongoDB.Bson.ObjectId Differentiator + { + get => _differentiator; + set + { + _differentiator = value; + RaisePropertyChanged("Differentiator"); + } + } + + private bool? _boolValue; + public bool? BoolValue + { + get => _boolValue; + set + { + _boolValue = value; + RaisePropertyChanged("BoolValue"); + } + } + + private int? _intValue; + public int? IntValue + { + get => _intValue; + set + { + _intValue = value; + RaisePropertyChanged("IntValue"); + } + } + + private double? _doubleValue; + public double? DoubleValue + { + get => _doubleValue; + set + { + _doubleValue = value; + RaisePropertyChanged("DoubleValue"); + } + } + + private MongoDB.Bson.Decimal128? _decimalValue; + public MongoDB.Bson.Decimal128? DecimalValue + { + get => _decimalValue; + set + { + _decimalValue = value; + RaisePropertyChanged("DecimalValue"); + } + } + + private System.DateTimeOffset? _dateValue; + public System.DateTimeOffset? DateValue + { + get => _dateValue; + set + { + _dateValue = value; + RaisePropertyChanged("DateValue"); + } + } + + private string? _stringValue; + public string? StringValue + { + get => _stringValue; + set + { + _stringValue = value; + RaisePropertyChanged("StringValue"); + } + } + + private MongoDB.Bson.ObjectId? _objectIdValue; + public MongoDB.Bson.ObjectId? ObjectIdValue + { + get => _objectIdValue; + set + { + _objectIdValue = value; + RaisePropertyChanged("ObjectIdValue"); + } + } + + private System.Guid? _uuidValue; + public System.Guid? UuidValue + { + get => _uuidValue; + set + { + _uuidValue = value; + RaisePropertyChanged("UuidValue"); + } + } + + private byte[]? _binaryValue; + public byte[]? BinaryValue + { + get => _binaryValue; + set + { + _binaryValue = value; + RaisePropertyChanged("BinaryValue"); + } + } + + public NullablesV0UnmanagedAccessor(Type objectType) : base(objectType) + { + } + + public override Realms.RealmValue GetValue(string propertyName) + { + return propertyName switch + { + "_id" => _id, + "Differentiator" => _differentiator, + "BoolValue" => _boolValue, + "IntValue" => _intValue, + "DoubleValue" => _doubleValue, + "DecimalValue" => _decimalValue, + "DateValue" => _dateValue, + "StringValue" => _stringValue, + "ObjectIdValue" => _objectIdValue, + "UuidValue" => _uuidValue, + "BinaryValue" => _binaryValue, + _ => 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 "Differentiator": + Differentiator = (MongoDB.Bson.ObjectId)val; + return; + case "BoolValue": + BoolValue = (bool?)val; + return; + case "IntValue": + IntValue = (int?)val; + return; + case "DoubleValue": + DoubleValue = (double?)val; + return; + case "DecimalValue": + DecimalValue = (MongoDB.Bson.Decimal128?)val; + return; + case "DateValue": + DateValue = (System.DateTimeOffset?)val; + return; + case "StringValue": + StringValue = (string?)val; + return; + case "ObjectIdValue": + ObjectIdValue = (MongoDB.Bson.ObjectId?)val; + return; + case "UuidValue": + UuidValue = (System.Guid?)val; + return; + case "BinaryValue": + BinaryValue = (byte[]?)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 = (MongoDB.Bson.ObjectId)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 NullablesV0Serializer : Realms.Serialization.RealmObjectSerializerBase + { + public override string SchemaName => "Nullables"; + + protected override void SerializeValue(MongoDB.Bson.Serialization.BsonSerializationContext context, BsonSerializationArgs args, NullablesV0 value) + { + context.Writer.WriteStartDocument(); + + WriteValue(context, args, "_id", value.Id); + WriteValue(context, args, "Differentiator", value.Differentiator); + WriteValue(context, args, "BoolValue", value.BoolValue); + WriteValue(context, args, "IntValue", value.IntValue); + WriteValue(context, args, "DoubleValue", value.DoubleValue); + WriteValue(context, args, "DecimalValue", value.DecimalValue); + WriteValue(context, args, "DateValue", value.DateValue); + WriteValue(context, args, "StringValue", value.StringValue); + WriteValue(context, args, "ObjectIdValue", value.ObjectIdValue); + WriteValue(context, args, "UuidValue", value.UuidValue); + WriteValue(context, args, "BinaryValue", value.BinaryValue); + + context.Writer.WriteEndDocument(); + } + + protected override NullablesV0 CreateInstance() => new NullablesV0(); + + protected override void ReadValue(NullablesV0 instance, string name, BsonDeserializationContext context) + { + switch (name) + { + case "_id": + instance.Id = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "Differentiator": + instance.Differentiator = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "BoolValue": + instance.BoolValue = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "IntValue": + instance.IntValue = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "DoubleValue": + instance.DoubleValue = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "DecimalValue": + instance.DecimalValue = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "DateValue": + instance.DateValue = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "StringValue": + instance.StringValue = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "ObjectIdValue": + instance.ObjectIdValue = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "UuidValue": + instance.UuidValue = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "BinaryValue": + instance.BinaryValue = BsonSerializer.LookupSerializer().Deserialize(context); + break; + default: + context.Reader.SkipValue(); + break; + } + } + + protected override void ReadArrayElement(NullablesV0 instance, string name, BsonDeserializationContext context) + { + // No persisted list/set properties to deserialize + } + + protected override void ReadDocumentField(NullablesV0 instance, string name, string fieldName, BsonDeserializationContext context) + { + // No persisted dictionary properties to deserialize + } + } + } +} diff --git a/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/NullablesV1_generated.cs b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/NullablesV1_generated.cs new file mode 100644 index 0000000000..eb08403524 --- /dev/null +++ b/Tests/Realm.Tests/Generated/Realm.SourceGenerator/Realms.SourceGenerator.RealmGenerator/NullablesV1_generated.cs @@ -0,0 +1,724 @@ +// +#nullable enable + +using Baas; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using NUnit.Framework; +using Realms; +using Realms.Schema; +using Realms.Sync; +using Realms.Sync.Exceptions; +using Realms.Tests.Sync; +using Realms.Weaving; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using System.Xml.Serialization; + +namespace Realms.Tests.Sync +{ + [Generated] + [Woven(typeof(NullablesV1ObjectHelper)), Realms.Preserve(AllMembers = true)] + public partial class NullablesV1 : IRealmObject, INotifyPropertyChanged, IReflectableType + { + + [Realms.Preserve] + static NullablesV1() + { + Realms.Serialization.RealmObjectSerializer.Register(new NullablesV1Serializer()); + } + + /// + /// Defines the schema for the class. + /// + public static Realms.Schema.ObjectSchema RealmSchema = new Realms.Schema.ObjectSchema.Builder("Nullables", ObjectSchema.ObjectType.RealmObject) + { + Realms.Schema.Property.Primitive("_id", Realms.RealmValueType.ObjectId, isPrimaryKey: true, indexType: IndexType.None, isNullable: false, managedName: "Id"), + Realms.Schema.Property.Primitive("Differentiator", Realms.RealmValueType.ObjectId, isPrimaryKey: false, indexType: IndexType.None, isNullable: false, managedName: "Differentiator"), + Realms.Schema.Property.Primitive("BoolValue", Realms.RealmValueType.Bool, isPrimaryKey: false, indexType: IndexType.None, isNullable: false, managedName: "BoolValue"), + Realms.Schema.Property.Primitive("IntValue", Realms.RealmValueType.Int, isPrimaryKey: false, indexType: IndexType.None, isNullable: false, managedName: "IntValue"), + Realms.Schema.Property.Primitive("DoubleValue", Realms.RealmValueType.Double, isPrimaryKey: false, indexType: IndexType.None, isNullable: false, managedName: "DoubleValue"), + Realms.Schema.Property.Primitive("DecimalValue", Realms.RealmValueType.Decimal128, isPrimaryKey: false, indexType: IndexType.None, isNullable: false, managedName: "DecimalValue"), + Realms.Schema.Property.Primitive("DateValue", Realms.RealmValueType.Date, isPrimaryKey: false, indexType: IndexType.None, isNullable: false, managedName: "DateValue"), + Realms.Schema.Property.Primitive("StringValue", Realms.RealmValueType.String, isPrimaryKey: false, indexType: IndexType.None, isNullable: false, managedName: "StringValue"), + Realms.Schema.Property.Primitive("ObjectIdValue", Realms.RealmValueType.ObjectId, isPrimaryKey: false, indexType: IndexType.None, isNullable: false, managedName: "ObjectIdValue"), + Realms.Schema.Property.Primitive("UuidValue", Realms.RealmValueType.Guid, isPrimaryKey: false, indexType: IndexType.None, isNullable: false, managedName: "UuidValue"), + Realms.Schema.Property.Primitive("BinaryValue", Realms.RealmValueType.Data, isPrimaryKey: false, indexType: IndexType.None, isNullable: false, managedName: "BinaryValue"), + Realms.Schema.Property.Primitive("WillBeRemoved", Realms.RealmValueType.String, isPrimaryKey: false, indexType: IndexType.None, isNullable: false, managedName: "WillBeRemoved"), + }.Build(); + + #region IRealmObject implementation + + private INullablesV1Accessor? _accessor; + + Realms.IRealmAccessor Realms.IRealmObjectBase.Accessor => Accessor; + + private INullablesV1Accessor Accessor => _accessor ??= new NullablesV1UnmanagedAccessor(typeof(NullablesV1)); + + /// + [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 = (INullablesV1Accessor)managedAccessor; + var oldAccessor = _accessor; + _accessor = newAccessor; + + if (helper != null && oldAccessor != null) + { + if (!skipDefaults || oldAccessor.Id != default(MongoDB.Bson.ObjectId)) + { + newAccessor.Id = oldAccessor.Id; + } + if (!skipDefaults || oldAccessor.Differentiator != default(MongoDB.Bson.ObjectId)) + { + newAccessor.Differentiator = oldAccessor.Differentiator; + } + if (!skipDefaults || oldAccessor.BoolValue != default(bool)) + { + newAccessor.BoolValue = oldAccessor.BoolValue; + } + if (!skipDefaults || oldAccessor.IntValue != default(int)) + { + newAccessor.IntValue = oldAccessor.IntValue; + } + if (!skipDefaults || oldAccessor.DoubleValue != default(double)) + { + newAccessor.DoubleValue = oldAccessor.DoubleValue; + } + if (!skipDefaults || oldAccessor.DecimalValue != default(MongoDB.Bson.Decimal128)) + { + newAccessor.DecimalValue = oldAccessor.DecimalValue; + } + newAccessor.DateValue = oldAccessor.DateValue; + newAccessor.StringValue = oldAccessor.StringValue; + if (!skipDefaults || oldAccessor.ObjectIdValue != default(MongoDB.Bson.ObjectId)) + { + newAccessor.ObjectIdValue = oldAccessor.ObjectIdValue; + } + if (!skipDefaults || oldAccessor.UuidValue != default(System.Guid)) + { + newAccessor.UuidValue = oldAccessor.UuidValue; + } + newAccessor.BinaryValue = oldAccessor.BinaryValue; + newAccessor.WillBeRemoved = oldAccessor.WillBeRemoved; + } + + 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 NullablesV1?(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(NullablesV1? 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(NullablesV1? 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 NullablesV1ObjectHelper : 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 NullablesV1ManagedAccessor(); + + public Realms.IRealmObjectBase CreateInstance() => new NullablesV1(); + + public bool TryGetPrimaryKeyValue(Realms.IRealmObjectBase instance, out RealmValue value) + { + value = ((INullablesV1Accessor)instance.Accessor).Id; + return true; + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + internal interface INullablesV1Accessor : Realms.IRealmAccessor + { + MongoDB.Bson.ObjectId Id { get; set; } + + MongoDB.Bson.ObjectId Differentiator { get; set; } + + bool BoolValue { get; set; } + + int IntValue { get; set; } + + double DoubleValue { get; set; } + + MongoDB.Bson.Decimal128 DecimalValue { get; set; } + + System.DateTimeOffset DateValue { get; set; } + + string StringValue { get; set; } + + MongoDB.Bson.ObjectId ObjectIdValue { get; set; } + + System.Guid UuidValue { get; set; } + + byte[] BinaryValue { get; set; } + + string WillBeRemoved { get; set; } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class NullablesV1ManagedAccessor : Realms.ManagedAccessor, INullablesV1Accessor + { + public MongoDB.Bson.ObjectId Id + { + get => (MongoDB.Bson.ObjectId)GetValue("_id"); + set => SetValueUnique("_id", value); + } + + public MongoDB.Bson.ObjectId Differentiator + { + get => (MongoDB.Bson.ObjectId)GetValue("Differentiator"); + set => SetValue("Differentiator", value); + } + + public bool BoolValue + { + get => (bool)GetValue("BoolValue"); + set => SetValue("BoolValue", value); + } + + public int IntValue + { + get => (int)GetValue("IntValue"); + set => SetValue("IntValue", value); + } + + public double DoubleValue + { + get => (double)GetValue("DoubleValue"); + set => SetValue("DoubleValue", value); + } + + public MongoDB.Bson.Decimal128 DecimalValue + { + get => (MongoDB.Bson.Decimal128)GetValue("DecimalValue"); + set => SetValue("DecimalValue", value); + } + + public System.DateTimeOffset DateValue + { + get => (System.DateTimeOffset)GetValue("DateValue"); + set => SetValue("DateValue", value); + } + + public string StringValue + { + get => (string)GetValue("StringValue")!; + set => SetValue("StringValue", value); + } + + public MongoDB.Bson.ObjectId ObjectIdValue + { + get => (MongoDB.Bson.ObjectId)GetValue("ObjectIdValue"); + set => SetValue("ObjectIdValue", value); + } + + public System.Guid UuidValue + { + get => (System.Guid)GetValue("UuidValue"); + set => SetValue("UuidValue", value); + } + + public byte[] BinaryValue + { + get => (byte[])GetValue("BinaryValue")!; + set => SetValue("BinaryValue", value); + } + + public string WillBeRemoved + { + get => (string)GetValue("WillBeRemoved")!; + set => SetValue("WillBeRemoved", value); + } + } + + [EditorBrowsable(EditorBrowsableState.Never), Realms.Preserve(AllMembers = true)] + private class NullablesV1UnmanagedAccessor : Realms.UnmanagedAccessor, INullablesV1Accessor + { + public override ObjectSchema ObjectSchema => NullablesV1.RealmSchema; + + private MongoDB.Bson.ObjectId _id = ObjectId.GenerateNewId(); + public MongoDB.Bson.ObjectId Id + { + get => _id; + set + { + _id = value; + RaisePropertyChanged("Id"); + } + } + + private MongoDB.Bson.ObjectId _differentiator; + public MongoDB.Bson.ObjectId Differentiator + { + get => _differentiator; + set + { + _differentiator = value; + RaisePropertyChanged("Differentiator"); + } + } + + private bool _boolValue; + public bool BoolValue + { + get => _boolValue; + set + { + _boolValue = value; + RaisePropertyChanged("BoolValue"); + } + } + + private int _intValue; + public int IntValue + { + get => _intValue; + set + { + _intValue = value; + RaisePropertyChanged("IntValue"); + } + } + + private double _doubleValue; + public double DoubleValue + { + get => _doubleValue; + set + { + _doubleValue = value; + RaisePropertyChanged("DoubleValue"); + } + } + + private MongoDB.Bson.Decimal128 _decimalValue; + public MongoDB.Bson.Decimal128 DecimalValue + { + get => _decimalValue; + set + { + _decimalValue = value; + RaisePropertyChanged("DecimalValue"); + } + } + + private System.DateTimeOffset _dateValue; + public System.DateTimeOffset DateValue + { + get => _dateValue; + set + { + _dateValue = value; + RaisePropertyChanged("DateValue"); + } + } + + private string _stringValue = string.Empty; + public string StringValue + { + get => _stringValue; + set + { + _stringValue = value; + RaisePropertyChanged("StringValue"); + } + } + + private MongoDB.Bson.ObjectId _objectIdValue; + public MongoDB.Bson.ObjectId ObjectIdValue + { + get => _objectIdValue; + set + { + _objectIdValue = value; + RaisePropertyChanged("ObjectIdValue"); + } + } + + private System.Guid _uuidValue; + public System.Guid UuidValue + { + get => _uuidValue; + set + { + _uuidValue = value; + RaisePropertyChanged("UuidValue"); + } + } + + private byte[] _binaryValue = Array.Empty(); + public byte[] BinaryValue + { + get => _binaryValue; + set + { + _binaryValue = value; + RaisePropertyChanged("BinaryValue"); + } + } + + private string _willBeRemoved = string.Empty; + public string WillBeRemoved + { + get => _willBeRemoved; + set + { + _willBeRemoved = value; + RaisePropertyChanged("WillBeRemoved"); + } + } + + public NullablesV1UnmanagedAccessor(Type objectType) : base(objectType) + { + } + + public override Realms.RealmValue GetValue(string propertyName) + { + return propertyName switch + { + "_id" => _id, + "Differentiator" => _differentiator, + "BoolValue" => _boolValue, + "IntValue" => _intValue, + "DoubleValue" => _doubleValue, + "DecimalValue" => _decimalValue, + "DateValue" => _dateValue, + "StringValue" => _stringValue, + "ObjectIdValue" => _objectIdValue, + "UuidValue" => _uuidValue, + "BinaryValue" => _binaryValue, + "WillBeRemoved" => _willBeRemoved, + _ => 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 "Differentiator": + Differentiator = (MongoDB.Bson.ObjectId)val; + return; + case "BoolValue": + BoolValue = (bool)val; + return; + case "IntValue": + IntValue = (int)val; + return; + case "DoubleValue": + DoubleValue = (double)val; + return; + case "DecimalValue": + DecimalValue = (MongoDB.Bson.Decimal128)val; + return; + case "DateValue": + DateValue = (System.DateTimeOffset)val; + return; + case "StringValue": + StringValue = (string)val!; + return; + case "ObjectIdValue": + ObjectIdValue = (MongoDB.Bson.ObjectId)val; + return; + case "UuidValue": + UuidValue = (System.Guid)val; + return; + case "BinaryValue": + BinaryValue = (byte[])val!; + return; + case "WillBeRemoved": + WillBeRemoved = (string)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 = (MongoDB.Bson.ObjectId)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 NullablesV1Serializer : Realms.Serialization.RealmObjectSerializerBase + { + public override string SchemaName => "Nullables"; + + protected override void SerializeValue(MongoDB.Bson.Serialization.BsonSerializationContext context, BsonSerializationArgs args, NullablesV1 value) + { + context.Writer.WriteStartDocument(); + + WriteValue(context, args, "_id", value.Id); + WriteValue(context, args, "Differentiator", value.Differentiator); + WriteValue(context, args, "BoolValue", value.BoolValue); + WriteValue(context, args, "IntValue", value.IntValue); + WriteValue(context, args, "DoubleValue", value.DoubleValue); + WriteValue(context, args, "DecimalValue", value.DecimalValue); + WriteValue(context, args, "DateValue", value.DateValue); + WriteValue(context, args, "StringValue", value.StringValue); + WriteValue(context, args, "ObjectIdValue", value.ObjectIdValue); + WriteValue(context, args, "UuidValue", value.UuidValue); + WriteValue(context, args, "BinaryValue", value.BinaryValue); + WriteValue(context, args, "WillBeRemoved", value.WillBeRemoved); + + context.Writer.WriteEndDocument(); + } + + protected override NullablesV1 CreateInstance() => new NullablesV1(); + + protected override void ReadValue(NullablesV1 instance, string name, BsonDeserializationContext context) + { + switch (name) + { + case "_id": + instance.Id = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "Differentiator": + instance.Differentiator = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "BoolValue": + instance.BoolValue = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "IntValue": + instance.IntValue = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "DoubleValue": + instance.DoubleValue = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "DecimalValue": + instance.DecimalValue = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "DateValue": + instance.DateValue = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "StringValue": + instance.StringValue = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "ObjectIdValue": + instance.ObjectIdValue = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "UuidValue": + instance.UuidValue = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "BinaryValue": + instance.BinaryValue = BsonSerializer.LookupSerializer().Deserialize(context); + break; + case "WillBeRemoved": + instance.WillBeRemoved = BsonSerializer.LookupSerializer().Deserialize(context); + break; + default: + context.Reader.SkipValue(); + break; + } + } + + protected override void ReadArrayElement(NullablesV1 instance, string name, BsonDeserializationContext context) + { + // No persisted list/set properties to deserialize + } + + protected override void ReadDocumentField(NullablesV1 instance, string name, string fieldName, BsonDeserializationContext context) + { + // No persisted dictionary properties to deserialize + } + } + } +} diff --git a/Tests/Realm.Tests/Sync/SyncMigrationTests.cs b/Tests/Realm.Tests/Sync/SyncMigrationTests.cs new file mode 100644 index 0000000000..1ab64c0b84 --- /dev/null +++ b/Tests/Realm.Tests/Sync/SyncMigrationTests.cs @@ -0,0 +1,365 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 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.Linq; +using System.Threading.Tasks; +using Baas; +using MongoDB.Bson; +using NUnit.Framework; +using Realms.Sync; +using Realms.Sync.Exceptions; + +namespace Realms.Tests.Sync +{ + // The model must match BaasClient.Schemas.Nullables + [MapTo("Nullables"), Explicit] + public partial class NullablesV0 : IRealmObject + { + [PrimaryKey, MapTo("_id")] + public ObjectId Id { get; set; } = ObjectId.GenerateNewId(); + + public ObjectId Differentiator { get; set; } + + public bool? BoolValue { get; set; } + + public int? IntValue { get; set; } + + public double? DoubleValue { get; set; } + + public Decimal128? DecimalValue { get; set; } + + public DateTimeOffset? DateValue { get; set; } + + public string? StringValue { get; set; } + + public ObjectId? ObjectIdValue { get; set; } + + public Guid? UuidValue { get; set; } + + public byte[]? BinaryValue { get; set; } + } + + [MapTo("Nullables"), Explicit] + public partial class NullablesV1 : IRealmObject + { + [PrimaryKey, MapTo("_id")] + public ObjectId Id { get; set; } = ObjectId.GenerateNewId(); + + public ObjectId Differentiator { get; set; } + + public bool BoolValue { get; set; } + + public int IntValue { get; set; } + + public double DoubleValue { get; set; } + + public Decimal128 DecimalValue { get; set; } + + public DateTimeOffset DateValue { get; set; } + + public string StringValue { get; set; } = string.Empty; + + public ObjectId ObjectIdValue { get; set; } + + public Guid UuidValue { get; set; } + + public byte[] BinaryValue { get; set; } = Array.Empty(); + + public string WillBeRemoved { get; set; } = string.Empty; + } + + [TestFixture, Preserve(AllMembers = true)] + public class SyncMigrationTests : SyncTestBase + { + private async Task OpenRealm(ObjectId differentiator, Type schema, ulong schemaVersion) + { + var app = App.Create(SyncTestHelpers.GetAppConfig(AppConfigType.StaticSchema)); + var config = await GetFLXIntegrationConfigAsync(app); + config.SchemaVersion = schemaVersion; + config.Schema = new[] + { + schema + }; + + config.PopulateInitialSubscriptions = (r) => + { + r.Subscriptions.Add(r.DynamicApi.All("Nullables").Filter("Differentiator == $0", differentiator)); + }; + + var realm = GetRealm(config); + + await WaitForSubscriptionsAsync(realm); + + return realm; + } + + [Test] + public void Model_CanMigratePropertyOptionality() + { + SyncTestHelpers.RunBaasTestAsync(async () => + { + var differentiator = ObjectId.GenerateNewId(); + var date = new DateTimeOffset(2015, 12, 13, 11, 24, 59, TimeSpan.Zero); + var oid = ObjectId.GenerateNewId(); + var uuid = Guid.NewGuid(); + var binary = new byte[] + { + 1, 2, 3 + }; + + var realmv0 = await OpenRealm(differentiator, typeof(NullablesV0), schemaVersion: 0); + var objv0 = realmv0.Write(() => realmv0.Add(new NullablesV0 + { + BoolValue = true, + DateValue = date, + DecimalValue = 324.987m, + DoubleValue = -999.87654321, + IntValue = 42, + ObjectIdValue = oid, + StringValue = "bla bla", + UuidValue = uuid, + BinaryValue = binary, + Differentiator = differentiator, + })); + + await WaitForUploadAsync(realmv0); + + var realmv1 = await OpenRealm(differentiator, typeof(NullablesV1), schemaVersion: 1); + var objv1 = realmv1.All().Single(); + + Assert.That(objv1.BoolValue, Is.EqualTo(true)); + Assert.That(objv1.DateValue, Is.EqualTo(date)); + Assert.That(objv1.DecimalValue, Is.EqualTo(new Decimal128(324.987m))); + Assert.That(objv1.DoubleValue, Is.EqualTo(-999.87654321)); + Assert.That(objv1.IntValue, Is.EqualTo(42)); + Assert.That(objv1.ObjectIdValue, Is.EqualTo(oid)); + Assert.That(objv1.StringValue, Is.EqualTo("bla bla")); + Assert.That(objv1.UuidValue, Is.EqualTo(uuid)); + Assert.That(objv1.BinaryValue, Is.EqualTo(binary)); + Assert.That(objv1.WillBeRemoved, Is.EqualTo(string.Empty)); + + var realmv2 = await OpenRealm(differentiator, typeof(NullablesV0), schemaVersion: 2); + var objv2 = realmv2.All().Single(); + + Assert.That(objv2.BoolValue, Is.EqualTo(true)); + Assert.That(objv2.DateValue, Is.EqualTo(date)); + Assert.That(objv2.DecimalValue, Is.EqualTo(new Decimal128(324.987m))); + Assert.That(objv2.DoubleValue, Is.EqualTo(-999.87654321)); + Assert.That(objv2.IntValue, Is.EqualTo(42)); + Assert.That(objv2.ObjectIdValue, Is.EqualTo(oid)); + Assert.That(objv2.StringValue, Is.EqualTo("bla bla")); + Assert.That(objv2.UuidValue, Is.EqualTo(uuid)); + Assert.That(objv2.BinaryValue, Is.EqualTo(binary)); + + realmv0.Write(() => + { + objv0.BoolValue = null; + objv0.DateValue = null; + objv0.DecimalValue = null; + objv0.DoubleValue = null; + objv0.IntValue = null; + objv0.ObjectIdValue = null; + objv0.StringValue = null; + objv0.UuidValue = null; + objv0.BinaryValue = null; + }); + + await WaitForUploadAsync(realmv0); + await WaitForDownloadAsync(realmv1); + await WaitForDownloadAsync(realmv2); + + Assert.That(objv1.BoolValue, Is.EqualTo(false)); + Assert.That(objv1.DateValue, Is.EqualTo(new DateTimeOffset(1, 1, 1, 0, 0, 0, TimeSpan.Zero))); + Assert.That(objv1.DecimalValue, Is.EqualTo(Decimal128.Zero)); + Assert.That(objv1.DoubleValue, Is.EqualTo(0)); + Assert.That(objv1.IntValue, Is.EqualTo(0)); + Assert.That(objv1.ObjectIdValue, Is.EqualTo(ObjectId.Empty)); + Assert.That(objv1.StringValue, Is.EqualTo(string.Empty)); + Assert.That(objv1.UuidValue, Is.EqualTo(Guid.Empty)); + Assert.That(objv1.BinaryValue, Is.EqualTo(Array.Empty())); + Assert.That(objv1.WillBeRemoved, Is.EqualTo(string.Empty)); + + Assert.That(objv2.BoolValue, Is.Null); + Assert.That(objv2.DateValue, Is.Null); + Assert.That(objv2.DecimalValue, Is.Null); + Assert.That(objv2.DoubleValue, Is.Null); + Assert.That(objv2.IntValue, Is.Null); + Assert.That(objv2.ObjectIdValue, Is.Null); + Assert.That(objv2.StringValue, Is.Null); + Assert.That(objv2.UuidValue, Is.Null); + Assert.That(objv2.BinaryValue, Is.Null); + }); + } + + [Test] + public void Model_CanRemoveField() + { + SyncTestHelpers.RunBaasTestAsync(async () => + { + var differentiator = ObjectId.GenerateNewId(); + var realmv1 = await OpenRealm(differentiator, typeof(NullablesV1), schemaVersion: 1); + + var objv1 = realmv1.Write(() => realmv1.Add(new NullablesV1 + { + Differentiator = differentiator, + BoolValue = true, + DateValue = DateTimeOffset.UtcNow, + DecimalValue = Decimal128.MaxValue, + DoubleValue = 5.555, + IntValue = 123, + ObjectIdValue = ObjectId.GenerateNewId(), + StringValue = "foo bar", + UuidValue = Guid.NewGuid(), + BinaryValue = Array.Empty(), + WillBeRemoved = "this should go away!" + })); + + Assert.That(objv1.WillBeRemoved, Is.EqualTo("this should go away!")); + + await WaitForUploadAsync(realmv1); + + var realmv2 = await OpenRealm(differentiator, typeof(NullablesV0), schemaVersion: 2); + var id2 = realmv2.Write(() => realmv2.Add(new NullablesV0 + { + Differentiator = differentiator + })).Id; + + await WaitForUploadAsync(realmv2); + await WaitForDownloadAsync(realmv1); + + var objv2 = realmv1.Find(id2)!; + Assert.That(objv2.WillBeRemoved, Is.EqualTo(string.Empty)); + }); + } + + [Test] + public void Migration_FailsWithFutureVersion() + { + SyncTestHelpers.RunBaasTestAsync(async () => + { + var ex = await TestHelpers.AssertThrows(() => OpenRealm(ObjectId.GenerateNewId(), typeof(NullablesV0), schemaVersion: 3)); + Assert.That(ex.Message, Does.Contain("schema version in BIND 3 is greater than latest schema version 2")); + }); + } + + [Test] + public void SameRealm_CanBeMigratedThroughConsecutiveVersions() + { + SyncTestHelpers.RunBaasTestAsync(async () => + { + var differentiator = ObjectId.GenerateNewId(); + var realm = await OpenRealm(differentiator, typeof(NullablesV0), schemaVersion: 0); + var id = ObjectId.GenerateNewId(); + realm.Write(() => realm.Add(new NullablesV0 + { + Id = id, + Differentiator = differentiator + })); + + realm.Dispose(); + + var configv1 = realm.Config.Clone(); + + configv1.SchemaVersion = 1; + configv1.Schema = new[] + { + typeof(NullablesV1) + }; + + realm = await GetRealmAsync(configv1); + + var objv1 = realm.All().Single(); + + Assert.That(objv1.Id, Is.EqualTo(id)); + Assert.That(objv1.Differentiator, Is.EqualTo(differentiator)); + Assert.That(objv1.BoolValue, Is.EqualTo(false)); + Assert.That(objv1.DateValue, Is.EqualTo(new DateTimeOffset(1, 1, 1, 0, 0, 0, TimeSpan.Zero))); + Assert.That(objv1.DecimalValue, Is.EqualTo(Decimal128.Zero)); + Assert.That(objv1.DoubleValue, Is.EqualTo(0)); + Assert.That(objv1.IntValue, Is.EqualTo(0)); + Assert.That(objv1.ObjectIdValue, Is.EqualTo(ObjectId.Empty)); + Assert.That(objv1.StringValue, Is.EqualTo(string.Empty)); + Assert.That(objv1.UuidValue, Is.EqualTo(Guid.Empty)); + Assert.That(objv1.BinaryValue, Is.EqualTo(Array.Empty())); + Assert.That(objv1.WillBeRemoved, Is.EqualTo(string.Empty)); + + realm.Dispose(); + + var configv2 = realm.Config.Clone(); + + configv2.SchemaVersion = 2; + configv2.Schema = new[] + { + typeof(NullablesV0) + }; + + realm = await GetRealmAsync(configv2); + var objv2 = realm.All().Single(); + + Assert.That(objv2.Id, Is.EqualTo(id)); + Assert.That(objv2.Differentiator, Is.EqualTo(differentiator)); + Assert.That(objv2.BoolValue, Is.Null); + Assert.That(objv2.DateValue, Is.Null); + Assert.That(objv2.DecimalValue, Is.Null); + Assert.That(objv2.DoubleValue, Is.Null); + Assert.That(objv2.IntValue, Is.Null); + Assert.That(objv2.ObjectIdValue, Is.Null); + Assert.That(objv2.StringValue, Is.Null); + Assert.That(objv2.UuidValue, Is.Null); + Assert.That(objv2.BinaryValue, Is.Null); + }); + } + + [Test] + public void SameRealm_CanBeMigratedSkippingVersions() + { + SyncTestHelpers.RunBaasTestAsync(async () => + { + var differentiator = ObjectId.GenerateNewId(); + var realm = await OpenRealm(differentiator, typeof(NullablesV0), schemaVersion: 0); + var id = ObjectId.GenerateNewId(); + realm.Write(() => realm.Add(new NullablesV0 + { + Id = id, + Differentiator = differentiator + })); + + realm.Dispose(); + + var configv2 = realm.Config.Clone(); + configv2.SchemaVersion = 2; + + realm = await GetRealmAsync(configv2); + var objv2 = realm.All().Single(); + + Assert.That(objv2.Id, Is.EqualTo(id)); + Assert.That(objv2.Differentiator, Is.EqualTo(differentiator)); + Assert.That(objv2.BoolValue, Is.Null); + Assert.That(objv2.DateValue, Is.Null); + Assert.That(objv2.DecimalValue, Is.Null); + Assert.That(objv2.DoubleValue, Is.Null); + Assert.That(objv2.IntValue, Is.Null); + Assert.That(objv2.ObjectIdValue, Is.Null); + Assert.That(objv2.StringValue, Is.Null); + Assert.That(objv2.UuidValue, Is.Null); + Assert.That(objv2.BinaryValue, Is.Null); + }); + } + } +} diff --git a/Tools/DeployApps/BaasClient.cs b/Tools/DeployApps/BaasClient.cs index e300f0359a..714a45bee8 100644 --- a/Tools/DeployApps/BaasClient.cs +++ b/Tools/DeployApps/BaasClient.cs @@ -43,6 +43,7 @@ public static class AppConfigType public const string ObjectIdPartitionKey = "pbs-oid"; public const string UUIDPartitionKey = "pbs-uuid"; public const string FlexibleSync = "flx"; + public const string StaticSchema = "schema"; } public class BaasClient @@ -316,7 +317,8 @@ public async Task> GetOrCreateApps() var result = new Dictionary(); await GetOrCreateApp(result, AppConfigType.Default, apps, CreateDefaultApp); - await GetOrCreateApp(result, AppConfigType.FlexibleSync, apps, CreateFlxApp); + await GetOrCreateApp(result, AppConfigType.FlexibleSync, apps, name => CreateFlxApp(name, enableDevMode: true)); + await GetOrCreateApp(result, AppConfigType.StaticSchema, apps, name => CreateFlxApp(name, enableDevMode: false)); await GetOrCreateApp(result, AppConfigType.IntPartitionKey, apps, name => CreatePbsApp(name, "long")); await GetOrCreateApp(result, AppConfigType.UUIDPartitionKey, apps, name => CreatePbsApp(name, "uuid")); await GetOrCreateApp(result, AppConfigType.ObjectIdPartitionKey, apps, name => CreatePbsApp(name, "objectId")); @@ -463,7 +465,7 @@ private async Task CreatePbsApp(string name, string partitionKeyType, b return app; } - private async Task CreateFlxApp(string name) + private async Task CreateFlxApp(string name, bool enableDevMode) { _output.WriteLine($"Creating FLX app {name}..."); @@ -473,9 +475,9 @@ private async Task CreateFlxApp(string name) { state = "enabled", database_name = GetSyncDatabaseName(name), - queryable_fields_names = new[] { "Int64Property", "GuidProperty", "DoubleProperty", "Int", "Guid", "Id", "PartitionLike" }, + queryable_fields_names = new[] { "Int64Property", "GuidProperty", "DoubleProperty", "Int", "Guid", "Id", "PartitionLike", "Differentiator" }, } - }); + }, enableDevMode); await PostAsync($"groups/{_groupId}/apps/{app}/services/{mongoServiceId}/default_rule", new { @@ -500,6 +502,20 @@ private async Task CreateFlxApp(string name) await CreateFunction(app, "triggerClientResetOnSyncServer", TriggerClientResetOnSyncServerFuncSource, runAsSystem: true); + if (!enableDevMode) + { + var schemaV0 = Schemas.Nullables(Differentiator, required: false); + var schemaId = await CreateSchema(app, mongoServiceId, schemaV0, null); + + var schemaV1 = Schemas.Nullables(Differentiator, required: true); + await UpdateSchema(app, schemaId, schemaV1); + + // Revert to schema_v0 + await UpdateSchema(app, schemaId, schemaV0); + + await WaitForSchemaVersion(app, 2); + } + return app; } @@ -523,7 +539,7 @@ public async Task SetAutomaticRecoveryEnabled(BaasApp app, bool enabled) await PatchAsync($"groups/{_groupId}/apps/{app}/services/{mongoServiceId}/config", fragment); } - private async Task<(BaasApp App, string MongoServiceId)> CreateAppCore(string name, object syncConfig) + private async Task<(BaasApp App, string MongoServiceId)> CreateAppCore(string name, object syncConfig, bool enableDeveloperMode = true) { var doc = await PostAsync($"groups/{_groupId}/apps", new { name = $"{name}{_appSuffix}" }); var appId = doc!["_id"].AsString; @@ -551,10 +567,14 @@ public async Task SetAutomaticRecoveryEnabled(BaasApp app, bool enabled) var mongoServiceId = await CreateMongodbService(app, syncConfig); - await PutAsync($"groups/{_groupId}/apps/{app}/sync/config", new + if (enableDeveloperMode) { - development_mode_enabled = true, - }); + await PutAsync($"groups/{_groupId}/apps/{app}/sync/config", new + { + development_mode_enabled = true, + }); + } + return (app, mongoServiceId); } @@ -616,7 +636,6 @@ private async Task GetApps() return new BaasApp(doc["_id"].AsString, doc["client_app_id"].AsString, appName); }) .Where(a => a != null) - .Select(a => a!) .ToArray(); } @@ -671,12 +690,36 @@ private async Task CreateMongodbService(BaasApp app, object syncConfig) return mongoServiceId; } - private async Task CreateSchema(BaasApp app, string mongoServiceId, object schema, object rule) + private async Task CreateSchema(BaasApp app, string mongoServiceId, object schema, object? rule) { _output.WriteLine($"Creating schema for {app.Name}..."); - await PostAsync($"groups/{_groupId}/apps/{app}/schemas", schema); - await PostAsync($"groups/{_groupId}/apps/{app}/services/{mongoServiceId}/rules", rule); + var createResponse = await PostAsync($"groups/{_groupId}/apps/{app}/schemas", schema); + if (rule != null) + { + await PostAsync($"groups/{_groupId}/apps/{app}/services/{mongoServiceId}/rules", rule); + } + + return createResponse!["_id"].AsString; + } + + private async Task UpdateSchema(BaasApp app, string schemaId, object schema) + { + _output.WriteLine($"Creating schema for {app.Name}..."); + + await PutAsync($"groups/{_groupId}/apps/{app}/schemas/{schemaId}?bypass_service_change=SyncSchemaVersionIncrease", schema); + } + + private async Task WaitForSchemaVersion(BaasApp app, int expectedVersion) + { + while (true) + { + var response = await GetAsync($"groups/{_groupId}/apps/{app}/sync/schemas/versions"); + if (response!["versions"].AsBsonArray.Any(version => version.AsBsonDocument["version_major"].AsInt32 >= expectedVersion)) + { + return; + } + } } private async Task RefreshAccessTokenAsync() @@ -917,6 +960,51 @@ public static (object Schema, object Rules) Foos(string partitionKeyType, string } }, GenericBaasRule(differentiator, "foos")); + + public static object Nullables(string differentiator, bool required) + { + var schema = new + { + title = "Nullables", + bsonType = "object", + properties = new Dictionary + { + ["_id"] = new { bsonType = "objectId" }, + ["Differentiator"] = new { bsonType = "objectId" }, + ["BoolValue"] = new { bsonType = "bool" }, + ["IntValue"] = new { bsonType = "long" }, + ["FloatValue"] = new { bsonType = "float" }, + ["DoubleValue"] = new { bsonType = "double" }, + ["DecimalValue"] = new { bsonType = "decimal" }, + ["DateValue"] = new { bsonType = "date" }, + ["StringValue"] = new { bsonType = "string" }, + ["ObjectIdValue"] = new { bsonType = "objectId" }, + ["UuidValue"] = new { bsonType = "uuid" }, + ["BinaryValue"] = new { bsonType = "binData" }, + }, + required = new List(), + }; + + if (required) + { + // For schema v1, we add an extra property + schema.properties["WillBeRemoved"] = new + { + bsonType = "string" + }; + } + + schema.required.AddRange(required ? schema.properties.Keys : new[] + { + "_id", "Differentiator" + }); + + return new + { + metadata = Metadata(differentiator, "Nullables"), + schema, + }; + } } private class BaasaasClient @@ -956,7 +1044,7 @@ public async Task GetOrDeployContainer(string differentiator, TextWriter } } - output.WriteLine($"No container found, starting a new one."); + output.WriteLine("No container found, starting a new one."); var containerId = await StartContainer(differentiator); output.WriteLine($"Container with id {containerId} started, waiting for it to be running."); @@ -975,7 +1063,7 @@ public async Task StopContainersForDifferentiator(string differentiator, TextWri var containers = await GetContainers(); var userId = await GetCurrentUserId(); - var existingContainers = containers! + var existingContainers = containers .Where(c => c.CreatorId == userId && c.Tags.Any(t => t.Key == "DIFFERENTIATOR" && t.Value == differentiator)); foreach (var container in existingContainers)