Skip to content

Commit

Permalink
Merge pull request #27 from tombatron/issue-25
Browse files Browse the repository at this point in the history
Expanded support for "read only" queries. 

Closes #25
  • Loading branch information
tombatron committed Jun 13, 2022
2 parents 4aa5593 + bb001b1 commit 6013e78
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 31 deletions.
85 changes: 85 additions & 0 deletions NRedisGraph.Tests/Issues/Number25.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System.Collections.Generic;
using StackExchange.Redis;
using Xunit;

namespace NRedisGraph.Tests.Issues;

public class Number25 : BaseTest
{
private ConnectionMultiplexer _readWriteMultiplexer;
private ConnectionMultiplexer _readonlyMultiplexer;

private RedisGraph _readonlyApi;
private RedisGraph _readWriteApi;

protected override void BeforeTest()
{
// Setup the multiplexers.
_readonlyMultiplexer = ConnectionMultiplexer.Connect("tombaserver.local:10000,tombaserver.local:10001");
_readWriteMultiplexer = ConnectionMultiplexer.Connect("tombaserver.local:10000,tombaserver.local:10001");

// Setup RedisGraph API...
_readonlyApi = new RedisGraph(_readonlyMultiplexer.GetDatabase(0));
_readWriteApi = new RedisGraph(_readWriteMultiplexer.GetDatabase(0));

// Setup the data.
_readWriteApi.GraphQuery("issue25", "CREATE (:Person {username: 'John', age: 20})");
_readWriteApi.GraphQuery("issue25", "CREATE (:Person {username: 'Peter', age: 23})");
_readWriteApi.GraphQuery("issue25", "CREATE (:Person {username: 'Steven', age: 30})");
}

protected override void AfterTest()
{
_readWriteApi.DeleteGraph("issue25");
_readWriteMultiplexer.Dispose();

_readonlyMultiplexer.Dispose();
}

[Fact(Skip="Need a read only node...")]
public void IssueReproduction()
{
// Read from the read-only replica
string readQuery = "MATCH x=(p:Person) RETURN nodes(x) as nodes";

ResultSet records = _readonlyApi.GraphReadOnlyQuery("issue25", readQuery);

var result = new List<Person>();

foreach (Record record in records) // Here it throws the exception (On the iteration)
{
var nodes = record.GetValue<object[]>("nodes");

foreach (var node in nodes)
{
if (node is Node castedNode)
{
Person person = GetPersonInfo(castedNode);

result.Add(new Person(person.Name, person.Age));
}
}
}
}

private static Person GetPersonInfo(Node node)
{
var name = node.PropertyMap["username"];
var age = node.PropertyMap["age"];

return new Person(name.Value.ToString(), (long)age.Value);
}
}

public class Person
{
public string Name { get; }

public long Age { get; }

public Person(string name, long age)
{
Name = name;
Age = age;
}
}
46 changes: 33 additions & 13 deletions NRedisGraph/GraphCache.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,42 @@
namespace NRedisGraph
{
internal sealed class GraphCache
internal interface IGraphCache
{
private readonly GraphCacheList _labels;
private readonly GraphCacheList _propertyNames;
private readonly GraphCacheList _relationshipTypes;
string GetLabel(int index);
string GetRelationshipType(int index);
string GetPropertyName(int index);
}

internal abstract class BaseGraphCache : IGraphCache
{
protected GraphCacheList Labels { get; set; }
protected GraphCacheList PropertyNames { get; set; }
protected GraphCacheList RelationshipTypes { get; set; }

internal GraphCache(string graphId, RedisGraph redisGraph)
{
_labels = new GraphCacheList(graphId, "db.labels", redisGraph);
_propertyNames = new GraphCacheList(graphId, "db.propertyKeys", redisGraph);
_relationshipTypes = new GraphCacheList(graphId, "db.relationshipTypes", redisGraph);
}
public string GetLabel(int index) => Labels.GetCachedData(index);

internal string GetLabel(int index) => _labels.GetCachedData(index);
public string GetRelationshipType(int index) => RelationshipTypes.GetCachedData(index);

internal string GetRelationshipType(int index) => _relationshipTypes.GetCachedData(index);
public string GetPropertyName(int index) => PropertyNames.GetCachedData(index);
}

internal string GetPropertyName(int index) => _propertyNames.GetCachedData(index);
internal sealed class GraphCache : BaseGraphCache
{
public GraphCache(string graphId, RedisGraph redisGraph)
{
Labels = new GraphCacheList(graphId, "db.labels", redisGraph);
PropertyNames = new GraphCacheList(graphId, "db.propertyKeys", redisGraph);
RelationshipTypes = new GraphCacheList(graphId, "db.relationshipTypes", redisGraph);
}
}

internal sealed class ReadOnlyGraphCache : BaseGraphCache
{
public ReadOnlyGraphCache(string graphId, RedisGraph redisGraph)
{
Labels = new ReadOnlyGraphCacheList(graphId, "db.labels", redisGraph);
PropertyNames = new ReadOnlyGraphCacheList(graphId, "db.propertyKeys", redisGraph);
RelationshipTypes = new ReadOnlyGraphCacheList(graphId, "db.relationshipTypes", redisGraph);
}
}
}
32 changes: 24 additions & 8 deletions NRedisGraph/GraphCacheList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@

namespace NRedisGraph
{
internal sealed class GraphCacheList
internal class GraphCacheList
{
protected readonly string GraphId;
protected readonly string Procedure;
protected readonly RedisGraph RedisGraph;

private readonly object _locker = new object();
private readonly string _graphId;
private readonly string _procedure;
private readonly RedisGraph _redisGraph;

private string[] _data;

internal GraphCacheList(string graphId, string procedure, RedisGraph redisGraph)
{
_graphId = graphId;
_procedure = procedure;
_redisGraph = redisGraph;
GraphId = graphId;
Procedure = procedure;
RedisGraph = redisGraph;
}

// TODO: Change this to use Lazy<T>?
Expand All @@ -36,7 +38,7 @@ internal string GetCachedData(int index)

private void GetProcedureInfo()
{
var resultSet = _redisGraph.CallProcedure(_graphId, _procedure);
var resultSet = CallProcedure();
var newData = new string[resultSet.Count];
var i = 0;

Expand All @@ -47,5 +49,19 @@ private void GetProcedureInfo()

_data = newData;
}

protected virtual ResultSet CallProcedure() =>
RedisGraph.CallProcedure(GraphId, Procedure);
}

internal class ReadOnlyGraphCacheList : GraphCacheList
{
internal ReadOnlyGraphCacheList(string graphId, string procedure, RedisGraph redisGraph) :
base(graphId, procedure, redisGraph)
{
}

protected override ResultSet CallProcedure() =>
RedisGraph.CallProcedureReadOnly(GraphId, Procedure);
}
}
5 changes: 3 additions & 2 deletions NRedisGraph/NRedisGraph.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<RepositoryUrl>https://github.com/tombatron/NRedisGraph</RepositoryUrl>
<ProjectRepository>https://github.com/tombatron/NRedisGraph</ProjectRepository>
<LangVersion>7.3</LangVersion>
<PackageVersion>1.6.0</PackageVersion>
<PackageVersion>1.7.0</PackageVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
Expand All @@ -26,7 +26,8 @@

<PropertyGroup>
<PackageLicenseFile>license.txt</PackageLicenseFile>
<PackageReleaseNotes> 1.6.0 - Added support for `GRAPH.RO_QUERY` command.
<PackageReleaseNotes> 1.7.0 - Fixed issue with parsing results from a "read only" query. Introduced read only versions of the "CallProcedure" method.
1.6.0 - Added support for `GRAPH.RO_QUERY` command.
1.5.0 - Sprinkled in a little bit of whimsy.
1.4.0 - Integers are now handled as `int64`.
1.3.0 - Added "Cached execution to the statistics output."
Expand Down
94 changes: 90 additions & 4 deletions NRedisGraph/RedisGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ public sealed class RedisGraph
{
internal static readonly object CompactQueryFlag = "--COMPACT";
private readonly IDatabase _db;
private readonly IDictionary<string, GraphCache> _graphCaches = new Dictionary<string, GraphCache>();
private readonly IDictionary<string, IGraphCache> _graphCaches = new Dictionary<string, IGraphCache>();

private GraphCache GetGraphCache(string graphId)
private IGraphCache GetGraphCache(string graphId)
{
if (!_graphCaches.ContainsKey(graphId))
{
Expand Down Expand Up @@ -153,7 +153,7 @@ public ResultSet GraphReadOnlyQuery(string graphId, string query, IDictionary<st
/// <returns>A result set.</returns>
public ResultSet GraphReadOnlyQuery(string graphId, string query, CommandFlags flags = CommandFlags.None)
{
_graphCaches.PutIfAbsent(graphId, new GraphCache(graphId, this));
_graphCaches.PutIfAbsent(graphId, new ReadOnlyGraphCache(graphId, this));

var parameters = new Collection<object>
{
Expand Down Expand Up @@ -191,7 +191,7 @@ public Task<ResultSet> GraphReadOnlyQueryAsync(string graphId, string query, IDi
/// <returns>A result set.</returns>
public async Task<ResultSet> GraphReadOnlyQueryAsync(string graphId, string query, CommandFlags flags = CommandFlags.None)
{
_graphCaches.PutIfAbsent(graphId, new GraphCache(graphId, this));
_graphCaches.PutIfAbsent(graphId, new ReadOnlyGraphCache(graphId, this));

var parameters = new Collection<object>
{
Expand Down Expand Up @@ -334,5 +334,91 @@ public async Task<ResultSet> DeleteGraphAsync(string graphId)

return processedResult;
}

/// <summary>
/// Call a saved procedure against a read-only node.
/// </summary>
/// <param name="graphId">The graph containing the saved procedure.</param>
/// <param name="procedure">The procedure name.</param>
/// <returns>A result set.</returns>
public ResultSet CallProcedureReadOnly(string graphId, string procedure) =>
CallProcedureReadOnly(graphId, procedure, Enumerable.Empty<string>(), EmptyKwargsDictionary);

/// <summary>
/// Call a saved procedure against a read-only node.
/// </summary>
/// <param name="graphId">The graph containing the saved procedure.</param>
/// <param name="procedure">The procedure name.</param>
/// <returns>A result set.</returns>
public Task<ResultSet> CallProcedureReadOnlyAsync(string graphId, string procedure) =>
CallProcedureReadOnlyAsync(graphId, procedure, Enumerable.Empty<string>(), EmptyKwargsDictionary);

/// <summary>
/// Call a saved procedure with parameters against a read-only node.
/// </summary>
/// <param name="graphId">The graph containing the saved procedure.</param>
/// <param name="procedure">The procedure name.</param>
/// <param name="args">A collection of positional arguments.</param>
/// <returns>A result set.</returns>
public ResultSet CallProcedureReadOnly(string graphId, string procedure, IEnumerable<string> args) =>
CallProcedureReadOnly(graphId, procedure, args, EmptyKwargsDictionary);

/// <summary>
/// Call a saved procedure with parameters against a read-only node.
/// </summary>
/// <param name="graphId">The graph containing the saved procedure.</param>
/// <param name="procedure">The procedure name.</param>
/// <param name="args">A collection of positional arguments.</param>
/// <returns>A result set.</returns>
public Task<ResultSet> CallProcedureReadOnlyAsync(string graphId, string procedure, IEnumerable<string> args) =>
CallProcedureReadOnlyAsync(graphId, procedure, args, EmptyKwargsDictionary);

/// <summary>
/// Call a saved procedure with parameters against a read-only node.
/// </summary>
/// <param name="graphId">The graph containing the saved procedure.</param>
/// <param name="procedure">The procedure name.</param>
/// <param name="args">A collection of positional arguments.</param>
/// <param name="kwargs">A collection of keyword arguments.</param>
/// <returns>A result set.</returns>
public ResultSet CallProcedureReadOnly(string graphId, string procedure, IEnumerable<string> args, Dictionary<string, List<string>> kwargs)
{
args = args.Select(a => QuoteString(a));

var queryBody = new StringBuilder();

queryBody.Append($"CALL {procedure}({string.Join(",", args)})");

if (kwargs.TryGetValue("y", out var kwargsList))
{
queryBody.Append(string.Join(",", kwargsList));
}

return GraphReadOnlyQuery(graphId, queryBody.ToString());
}

/// <summary>
/// Call a saved procedure with parameters against a read-only node.
/// </summary>
/// <param name="graphId">The graph containing the saved procedure.</param>
/// <param name="procedure">The procedure name.</param>
/// <param name="args">A collection of positional arguments.</param>
/// <param name="kwargs">A collection of keyword arguments.</param>
/// <returns>A result set.</returns>
public Task<ResultSet> CallProcedureReadOnlyAsync(string graphId, string procedure, IEnumerable<string> args, Dictionary<string, List<string>> kwargs)
{
args = args.Select(a => QuoteString(a));

var queryBody = new StringBuilder();

queryBody.Append($"CALL {procedure}({string.Join(",", args)})");

if (kwargs.TryGetValue("y", out var kwargsList))
{
queryBody.Append(string.Join(",", kwargsList));
}

return GraphReadOnlyQueryAsync(graphId, queryBody.ToString());
}
}
}
4 changes: 2 additions & 2 deletions NRedisGraph/RedisGraphTransaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ public TransactionResult(string graphId, Task<RedisResult> pendingTask)
}

private readonly ITransaction _transaction;
private readonly IDictionary<string, GraphCache> _graphCaches;
private readonly IDictionary<string, IGraphCache> _graphCaches;
private readonly RedisGraph _redisGraph;
private readonly List<TransactionResult> _pendingTasks = new List<TransactionResult>();
private readonly List<string> _graphCachesToRemove = new List<string>();

internal RedisGraphTransaction(ITransaction transaction, RedisGraph redisGraph, IDictionary<string, GraphCache> graphCaches)
internal RedisGraphTransaction(ITransaction transaction, RedisGraph redisGraph, IDictionary<string, IGraphCache> graphCaches)
{
_graphCaches = graphCaches;
_redisGraph = redisGraph;
Expand Down
4 changes: 2 additions & 2 deletions NRedisGraph/ResultSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ internal enum ResultSetScalarType
}

private readonly RedisResult[] _rawResults;
private readonly GraphCache _graphCache;
private readonly IGraphCache _graphCache;

internal ResultSet(RedisResult result, GraphCache graphCache)
internal ResultSet(RedisResult result, IGraphCache graphCache)
{
if (result.Type == ResultType.MultiBulk)
{
Expand Down

0 comments on commit 6013e78

Please sign in to comment.