Skip to content

Commit

Permalink
Add support for subscription inclusions
Browse files Browse the repository at this point in the history
  • Loading branch information
nirinchev committed Apr 24, 2019
1 parent b94c290 commit acdb1c3
Show file tree
Hide file tree
Showing 9 changed files with 249 additions and 38 deletions.
22 changes: 18 additions & 4 deletions Realm/Realm/Handles/SubscriptionHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
////////////////////////////////////////////////////////////////////////////

using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Realms.Exceptions;
Expand All @@ -37,10 +38,10 @@ private static class NativeMethods
[DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_subscription_create", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr subscribe(
ResultsHandle results,
[MarshalAs(UnmanagedType.LPWStr)] string name,
int name_len,
[MarshalAs(UnmanagedType.LPWStr)] string name, int name_len,
long time_to_live,
[MarshalAs(UnmanagedType.I1)] bool update,
[MarshalAs(UnmanagedType.LPArray), In] Native.StringValue[] inclusions, int inclusions_length,
out NativeException ex);

[DllImport(InteropConfig.DLL_NAME, EntryPoint = "realm_subscription_get_state", CallingConvention = CallingConvention.Cdecl)]
Expand All @@ -64,10 +65,23 @@ private SubscriptionHandle(IntPtr handle) : base(null, handle)
{
}

public static SubscriptionHandle Create(ResultsHandle results, string name, long? timeToLive, bool update)
public static SubscriptionHandle Create(ResultsHandle results, string name, long? timeToLive, bool update, string[] inclusions)
{
var nativeInclusions = new Native.StringValue[0];
if (inclusions != null)
{
nativeInclusions = inclusions.Select(i => new Native.StringValue { Value = i }).ToArray();
}

// We use -1 to signal "no value"
var handle = NativeMethods.subscribe(results, name, name?.Length ?? -1, timeToLive ?? -1, update, out var ex);
var handle = NativeMethods.subscribe(
results,
name, name?.Length ?? -1,
timeToLive ?? -1,
update,
nativeInclusions, inclusions?.Length ?? -1,
out var ex);

ex.ThrowIfNecessary();

return new SubscriptionHandle(handle);
Expand Down
58 changes: 58 additions & 0 deletions Realm/Realm/Helpers/LinqHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2018 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 System.Linq.Expressions;
using System.Reflection;

namespace Realms.Helpers
{
internal static class LinqHelper
{
public static string[] ToStringPaths<TSource, TProperty>(this Expression<Func<TSource, TProperty>>[] expressions)
{
return expressions.Select(ToStringPath).ToArray();
}

public static string ToStringPath<TSource, TResult>(this Expression<Func<TSource, TResult>> expression)
{
var visitor = new PropertyVisitor();
visitor.Visit(expression.Body);
visitor.Path.Reverse();
return string.Join(".", visitor.Path);
}

private class PropertyVisitor : ExpressionVisitor
{
public List<string> Path { get; } = new List<string>();

protected override Expression VisitMember(MemberExpression node)
{
if (!(node.Member is PropertyInfo))
{
throw new ArgumentException("The path can only contain properties", nameof(node));
}

this.Path.Add(node.Member.Name);
return base.VisitMember(node);
}
}
}
}
31 changes: 31 additions & 0 deletions Realm/Realm/Native/StringValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2016 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.Runtime.InteropServices;

namespace Realms.Sync.Native
{
[StructLayout(LayoutKind.Sequential)]
internal struct StringValue
{
internal static readonly int Size = Marshal.SizeOf<StringValue>();

[MarshalAs(UnmanagedType.LPStr)]
public string Value;
}
}
26 changes: 21 additions & 5 deletions Realm/Realm/Sync/Subscription.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Realms.Exceptions;
Expand Down Expand Up @@ -85,6 +86,11 @@ public static Subscription<T> Subscribe<T>(this IQueryable<T> query, string name
/// <param name="options">
/// Options that configure some metadata of the subscription, such as its name or time to live.
/// </param>
/// <param name="includedBacklinks">
/// An array of property expressions which specifies which linkingObjects relationships should be included in
/// the subscription. Subscriptions already include link and list properties (in the forward direction)
/// automatically by default.
/// </param>
/// <returns>
/// A <see cref="Subscription{T}"/> instance that contains information and methods for monitoring
/// the state of the subscription.
Expand All @@ -93,17 +99,27 @@ public static Subscription<T> Subscribe<T>(this IQueryable<T> query, string name
/// <exception cref="ArgumentException">
/// Thrown if the <c>query</c> was not obtained from a query-based synchronized Realm.
/// </exception>
public static Subscription<T> Subscribe<T>(this IQueryable<T> query, SubscriptionOptions options = null)
public static Subscription<T> Subscribe<T>(
this IQueryable<T> query,
SubscriptionOptions options = null,
params Expression<Func<T, IQueryable>>[] includedBacklinks)
{
Argument.NotNull(query, nameof(query));

var results = query as RealmResults<T>;
Argument.Ensure(results != null, $"{nameof(query)} must be an instance of IRealmCollection<{typeof(T).Name}>.", nameof(query));

options = options ?? new SubscriptionOptions();

var syncConfig = results.Realm.Config as SyncConfigurationBase;
Argument.Ensure(syncConfig?.IsFullSync == false, $"{nameof(query)} must be obtained from a synchronized Realm using query-based synchronization.", nameof(query));

var handle = SubscriptionHandle.Create(results.ResultsHandle, options.Name, (long?)options.TimeToLive?.TotalMilliseconds, options.ShouldUpdate);
var handle = SubscriptionHandle.Create(
results.ResultsHandle,
options.Name,
(long?)options.TimeToLive?.TotalMilliseconds,
options.ShouldUpdate,
includedBacklinks.ToStringPaths());
return new Subscription<T>(handle, results);
}

Expand All @@ -122,7 +138,7 @@ public static IRealmCollection<NamedSubscription> GetAllSubscriptions(this Realm
}

/// <summary>
/// Cancel a named subscription that was created by calling <see cref="Subscribe{T}(IQueryable{T}, SubscriptionOptions)"/>.
/// Cancel a named subscription that was created by calling <see cref="Subscribe{T}(IQueryable{T}, SubscriptionOptions, Expression{Func{T, IQueryable}}[])"/>.
/// <para />
/// Removing a subscription will delete all objects from the local Realm that were matched
/// only by that subscription and not any remaining subscriptions. The deletion is performed
Expand Down Expand Up @@ -161,7 +177,7 @@ public static Task UnsubscribeAsync(this Realm realm, string subscriptionName)
}

/// <summary>
/// Cancel a subscription that was created by calling <see cref="Subscribe{T}(IQueryable{T}, SubscriptionOptions)"/>.
/// Cancel a subscription that was created by calling <see cref="Subscribe{T}(IQueryable{T}, SubscriptionOptions, Expression{Func{T, IQueryable}}[])"/>.
/// <para />
/// Removing a subscription will delete all objects from the local Realm that were matched
/// only by that subscription and not any remaining subscriptions. The deletion is performed
Expand Down Expand Up @@ -229,7 +245,7 @@ private static void SubscriptionCallbackImpl(IntPtr managedHandle)
/// <para/>
/// The state of the subscription can be observed by subscribing to the <see cref="PropertyChanged"/> event handler.
/// <para/>
/// Subscriptions are created by calling <see cref="Subscription.Subscribe{T}(IQueryable{T}, SubscriptionOptions)"/>.
/// Subscriptions are created by calling <see cref="Subscription.Subscribe{T}(IQueryable{T}, SubscriptionOptions, Expression{Func{T, IQueryable}}[])"/>.
/// </summary>
/// <typeparam name="T">The type of the objects that make up the subscription query.</typeparam>
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleClass")]
Expand Down
68 changes: 63 additions & 5 deletions Tests/Realm.Tests/Sync/QueryBasedSyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,7 @@ public void Subscribe_UpdatesQuery(bool openAsync)
var updatedSub = realm.All<ObjectA>()
.Where(o => o.IntValue < 3)
.Subscribe(new SubscriptionOptions { Name = "foo", ShouldUpdate = true });
await updatedSub.WaitForSynchronizationAsync().Timeout(2000);
Assert.That(subscription.Results.Count(), Is.EqualTo(3));
Expand Down Expand Up @@ -271,7 +270,7 @@ public void Subscribe_UpdatesTtl(bool openAsync, bool changeTtlValue)
var updatedSub = realm.All<ObjectA>()
.Where(o => o.IntValue < 5)
.Subscribe(new SubscriptionOptions { Name = "foo", ShouldUpdate = true, TimeToLive = updatedTtl });
await updatedSub.WaitForSynchronizationAsync().Timeout(2000);
// NamedSub is a Realm object so it should have updated itself.
Expand Down Expand Up @@ -455,12 +454,55 @@ public void WaitForSynchronization_OnBackgroundThread_Throws(bool openAsync)
});
}

[TestCase(true)]
[TestCase(false)]
public void Subscribe_WithInclusions(bool openAsync)
{
SyncTestHelpers.RunRosTestAsync(async () =>
{
using (var realm = await GetQueryBasedRealm(openAsync))
{
var subscription = realm.All<ObjectB>()
.Where(b => b.BoolValue)
.Subscribe(includedBacklinks: b => b.As);
await subscription.WaitForSynchronizationAsync().Timeout(2000);
Assert.That(realm.All<ObjectB>().Count(), Is.EqualTo(5));
Assert.That(realm.All<ObjectA>().Count(), Is.EqualTo(5));
Assert.That(realm.All<ObjectA>().ToArray().All(a => a.IntValue % 2 == 0));
}
});
}

[TestCase(true)]
[TestCase(false)]
public void Subscribe_WithInclusions_ExtraHop(bool openAsync)
{
SyncTestHelpers.RunRosTestAsync(async () =>
{
using (var realm = await GetQueryBasedRealm(openAsync))
{
var subscription = realm.All<ObjectC>()
.Filter("B.BoolValue == true")
.Subscribe(includedBacklinks: c => c.B.As);
await subscription.WaitForSynchronizationAsync().Timeout(2000);
Assert.That(realm.All<ObjectC>().Count(), Is.EqualTo(5));
Assert.That(realm.All<ObjectB>().Count(), Is.EqualTo(5));
Assert.That(realm.All<ObjectA>().Count(), Is.EqualTo(5));
Assert.That(realm.All<ObjectA>().ToArray().All(a => a.IntValue % 2 == 0));
}
});
}

private async Task<Realm> GetQueryBasedRealm(bool openAsync, [CallerMemberName] string realmPath = null)
{
var user = await SyncTestHelpers.GetUserAsync();
var config = new QueryBasedSyncConfiguration(SyncTestHelpers.RealmUri($"~/{realmPath}_{openAsync}"), user, Guid.NewGuid().ToString())
{
ObjectClasses = new[] { typeof(ObjectA), typeof(ObjectB) }
ObjectClasses = new[] { typeof(ObjectA), typeof(ObjectB), typeof(ObjectC) }
};

using (var original = GetRealm(config))
Expand All @@ -469,7 +511,7 @@ private async Task<Realm> GetQueryBasedRealm(bool openAsync, [CallerMemberName]
{
for (var i = 0; i < 10; i++)
{
original.Add(new ObjectA
var a = original.Add(new ObjectA
{
StringValue = "A #" + i,
IntValue = i,
Expand All @@ -479,6 +521,12 @@ private async Task<Realm> GetQueryBasedRealm(bool openAsync, [CallerMemberName]
BoolValue = i % 2 == 0,
}
});
original.Add(new ObjectC
{
IntValue = i,
B = a.B
});
}
});

Expand Down Expand Up @@ -522,6 +570,16 @@ public class ObjectB : RealmObject
public string StringValue { get; set; }

public bool BoolValue { get; set; }

[Backlink(nameof(ObjectA.B))]
public IQueryable<ObjectA> As { get; }
}

public class ObjectC : RealmObject
{
public int IntValue { get; set; }

public ObjectB B { get; set; }
}
}
}
5 changes: 5 additions & 0 deletions wrappers/src/marshalling.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ struct PrimitiveValue
} value;
};

struct StringValue
{
const char* value;
};

template<typename Collection>
void collection_get_primitive(Collection* collection, size_t ndx, PrimitiveValue& value, NativeException::Marshallable& ex)
{
Expand Down
17 changes: 1 addition & 16 deletions wrappers/src/results_cs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "notifications_cs.hpp"
#include "wrapper_exceptions.hpp"
#include "timestamp_helpers.hpp"
#include "schema_cs.hpp"
#include <realm/parser/parser.hpp>
#include <realm/parser/query_builder.hpp>

Expand All @@ -44,22 +45,6 @@ inline T get(Results* results, size_t ndx, NativeException::Marshallable& ex)
});
}

inline void alias_backlinks(parser::KeyPathMapping &mapping, const realm::SharedRealm &realm)
{
const realm::Schema &schema = realm->schema();
for (auto it = schema.begin(); it != schema.end(); ++it) {
for (const Property &property : it->computed_properties) {
if (property.type == realm::PropertyType::LinkingObjects) {
auto target_object_schema = schema.find(property.object_type);
const TableRef table = ObjectStore::table_for_object_type(realm->read_group(), it->name);
const TableRef target_table = ObjectStore::table_for_object_type(realm->read_group(), target_object_schema->name);
std::string native_name = "@links." + std::string(target_table->get_name()) + "." + property.link_origin_property_name;
mapping.add_mapping(table, property.name, native_name);
}
}
}
}

extern "C" {

REALM_EXPORT void results_destroy(Results* results_ptr)
Expand Down
Loading

0 comments on commit acdb1c3

Please sign in to comment.