Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce procedure mode selection for RedisGraph. #162

Merged
merged 6 commits into from
Aug 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 39 additions & 36 deletions src/NRedisStack/Graph/GraphCacheList.cs
Original file line number Diff line number Diff line change
@@ -1,71 +1,74 @@
namespace NRedisStack.Graph
{
internal class GraphCacheList
internal sealed class GraphCacheList
{
protected readonly string GraphName;
protected readonly string Procedure;
private string[] _data;
private readonly string _graphName;
private readonly string _procedure;

protected readonly GraphCommands graph;
protected readonly GraphCommandsAsync asyncGraph;
private bool asyncGraphUsed;
private string[] _data = Array.Empty<string>();

private readonly GraphCommandsAsync _redisGraph;

private readonly object _locker = new object();

internal GraphCacheList(string graphName, string procedure, GraphCommands redisGraph) : this(graphName, procedure)
/// <summary>
/// Constructs a <see cref="GraphCacheList"/> for providing cached information about the graph.
/// </summary>
/// <param name="graphName">The name of the graph to cache information for.</param>
/// <param name="procedure">The saved procedure to call to populate cache. Must be a `read` procedure.</param>
/// <param name="redisGraph">The graph used for the calling the <paramref name="procedure"/>.</param>
internal GraphCacheList(string graphName, string procedure, GraphCommands redisGraph)
{
graph = redisGraph;
asyncGraphUsed = false;
}
_graphName = graphName;
_procedure = procedure;

internal GraphCacheList(string graphName, string procedure, GraphCommandsAsync redisGraph) : this(graphName, procedure)
{
asyncGraph = redisGraph;
asyncGraphUsed = true;
_redisGraph = redisGraph;
}

private GraphCacheList(string graphName, string procedure)
/// <summary>
/// Constructs a <see cref="GraphCacheList"/> for providing cached information about the graph.
/// </summary>
/// <param name="graphName">The name of the graph to cache information for.</param>
/// <param name="procedure">The saved procedure to call to populate cache. Must be a `read` procedure.</param>
/// <param name="redisGraph">The graph used for the calling the <paramref name="procedure"/>.</param>
internal GraphCacheList(string graphName, string procedure, GraphCommandsAsync redisGraphAsync)
{
GraphName = graphName;
Procedure = procedure;
_graphName = graphName;
_procedure = procedure;

_redisGraph = redisGraphAsync;
}

// TODO: Change this to use Lazy<T>?
internal string GetCachedData(int index)
{
if (_data == null || index >= _data.Length)
if (index >= _data.Length)
{
lock (_locker)
{
if (_data == null || index >= _data.Length)
if (index >= _data.Length)
{
GetProcedureInfo();
_data = GetProcedureInfo();
}
}
}

return _data.ElementAtOrDefault(index);
}

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

foreach (var record in resultSet)
{
newData[i++] = record.GetString(0);
}

_data = newData;
var resultSet = CallProcedure();
return resultSet
.Select(r => r.GetString(0))
.ToArray();
}

protected virtual ResultSet CallProcedure(bool asyncGraphUsed = false)
private ResultSet CallProcedure()
{
return asyncGraphUsed
? asyncGraph.CallProcedureAsync(GraphName, Procedure).Result
: graph.CallProcedure(GraphName, Procedure);
return _redisGraph is GraphCommands graphSync
? graphSync.CallProcedure(_graphName, _procedure, ProcedureMode.Read)
: _redisGraph.CallProcedureAsync(_graphName, _procedure, ProcedureMode.Read).Result;
}
}
}
21 changes: 11 additions & 10 deletions src/NRedisStack/Graph/GraphCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace NRedisStack
{
public class GraphCommands : GraphCommandsAsync, IGraphCommands
{
IDatabase _db;
readonly IDatabase _db;
shacharPash marked this conversation as resolved.
Show resolved Hide resolved

public GraphCommands(IDatabase db) : base(db)
{
Expand Down Expand Up @@ -55,22 +55,21 @@ public ResultSet RO_Query(string graphName, string query, long? timeout = null)
return new ResultSet(_db.Execute(GraphCommandBuilder.RO_Query(graphName, query, timeout)), _graphCaches[graphName]);
}

internal static readonly Dictionary<string, List<string>> EmptyKwargsDictionary =
private static readonly Dictionary<string, List<string>> EmptyKwargsDictionary =
new Dictionary<string, List<string>>();

// TODO: Check if this is needed:
/// <inheritdoc/>
public ResultSet CallProcedure(string graphName, string procedure) =>
CallProcedure(graphName, procedure, Enumerable.Empty<string>(), EmptyKwargsDictionary);
public ResultSet CallProcedure(string graphName, string procedure, ProcedureMode procedureMode = ProcedureMode.Write) =>
CallProcedure(graphName, procedure, Enumerable.Empty<string>(), EmptyKwargsDictionary, procedureMode);

/// <inheritdoc/>
public ResultSet CallProcedure(string graphName, string procedure, IEnumerable<string> args) =>
CallProcedure(graphName, procedure, args, EmptyKwargsDictionary);
public ResultSet CallProcedure(string graphName, string procedure, IEnumerable<string> args, ProcedureMode procedureMode = ProcedureMode.Write) =>
CallProcedure(graphName, procedure, args, EmptyKwargsDictionary, procedureMode);

/// <inheritdoc/>
public ResultSet CallProcedure(string graphName, string procedure, IEnumerable<string> args, Dictionary<string, List<string>> kwargs)
public ResultSet CallProcedure(string graphName, string procedure, IEnumerable<string> args, Dictionary<string, List<string>> kwargs, ProcedureMode procedureMode = ProcedureMode.Write)
{
args = args.Select(a => QuoteString(a));
args = args.Select(QuoteString);

var queryBody = new StringBuilder();

Expand All @@ -81,7 +80,9 @@ public ResultSet CallProcedure(string graphName, string procedure, IEnumerable<s
queryBody.Append(string.Join(",", kwargsList));
shacharPash marked this conversation as resolved.
Show resolved Hide resolved
}

return Query(graphName, queryBody.ToString());
return procedureMode is ProcedureMode.Read
? RO_Query(graphName, queryBody.ToString())
: Query(graphName, queryBody.ToString());
}

/// <inheritdoc/>
Expand Down
21 changes: 11 additions & 10 deletions src/NRedisStack/Graph/GraphCommandsAsync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace NRedisStack
{
public class GraphCommandsAsync : IGraphCommandsAsync
{
IDatabaseAsync _db;
readonly IDatabaseAsync _db;
shacharPash marked this conversation as resolved.
Show resolved Hide resolved

public GraphCommandsAsync(IDatabaseAsync db)
{
Expand Down Expand Up @@ -56,22 +56,21 @@ public async Task<ResultSet> RO_QueryAsync(string graphName, string query, long?
return new ResultSet(await _db.ExecuteAsync(GraphCommandBuilder.RO_Query(graphName, query, timeout)), _graphCaches[graphName]);
}

internal static readonly Dictionary<string, List<string>> EmptyKwargsDictionary =
private static readonly Dictionary<string, List<string>> EmptyKwargsDictionary =
new Dictionary<string, List<string>>();

// TODO: Check if this is needed:
/// <inheritdoc/>
public async Task<ResultSet> CallProcedureAsync(string graphName, string procedure) =>
await CallProcedureAsync(graphName, procedure, Enumerable.Empty<string>(), EmptyKwargsDictionary);
public Task<ResultSet> CallProcedureAsync(string graphName, string procedure, ProcedureMode procedureMode = ProcedureMode.Write) =>
shacharPash marked this conversation as resolved.
Show resolved Hide resolved
CallProcedureAsync(graphName, procedure, Enumerable.Empty<string>(), EmptyKwargsDictionary, procedureMode);

/// <inheritdoc/>
public async Task<ResultSet> CallProcedureAsync(string graphName, string procedure, IEnumerable<string> args) =>
await CallProcedureAsync(graphName, procedure, args, EmptyKwargsDictionary);
public Task<ResultSet> CallProcedureAsync(string graphName, string procedure, IEnumerable<string> args, ProcedureMode procedureMode = ProcedureMode.Write) =>
shacharPash marked this conversation as resolved.
Show resolved Hide resolved
CallProcedureAsync(graphName, procedure, args, EmptyKwargsDictionary, procedureMode);

/// <inheritdoc/>
public async Task<ResultSet> CallProcedureAsync(string graphName, string procedure, IEnumerable<string> args, Dictionary<string, List<string>> kwargs)
public async Task<ResultSet> CallProcedureAsync(string graphName, string procedure, IEnumerable<string> args, Dictionary<string, List<string>> kwargs, ProcedureMode procedureMode = ProcedureMode.Write)
{
args = args.Select(a => QuoteString(a));
args = args.Select(QuoteString);
haysch marked this conversation as resolved.
Show resolved Hide resolved

var queryBody = new StringBuilder();

Expand All @@ -82,7 +81,9 @@ public async Task<ResultSet> CallProcedureAsync(string graphName, string procedu
queryBody.Append(string.Join(",", kwargsList));
}

return await QueryAsync(graphName, queryBody.ToString());
return procedureMode is ProcedureMode.Read
? await RO_QueryAsync(graphName, queryBody.ToString())
: await QueryAsync(graphName, queryBody.ToString());
}


Expand Down
9 changes: 6 additions & 3 deletions src/NRedisStack/Graph/IGraphCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,19 @@ public interface IGraphCommands
/// </summary>
/// <param name="graphName">The graph containing the saved procedure.</param>
/// <param name="procedure">The procedure name.</param>
/// <param name="procedureMode">The mode of the saved procedure. Defaults to <see cref="ProcedureMode.Write"/>.</param>
/// <returns>A result set.</returns>
ResultSet CallProcedure(string graphName, string procedure);
ResultSet CallProcedure(string graphName, string procedure, ProcedureMode procedureMode = ProcedureMode.Write);

/// <summary>
/// Call a saved procedure with parameters.
/// </summary>
/// <param name="graphName">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="procedureMode">The mode of the saved procedure. Defaults to <see cref="ProcedureMode.Write"/>.</param>
/// <returns>A result set.</returns>
ResultSet CallProcedure(string graphName, string procedure, IEnumerable<string> args);
ResultSet CallProcedure(string graphName, string procedure, IEnumerable<string> args, ProcedureMode procedureMode = ProcedureMode.Write);

/// <summary>
/// Call a saved procedure with parameters.
Expand All @@ -72,8 +74,9 @@ public interface IGraphCommands
/// <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>
/// <param name="procedureMode">The mode of the saved procedure. Defaults to <see cref="ProcedureMode.Write"/>.</param>
/// <returns>A result set.</returns>
ResultSet CallProcedure(string graphName, string procedure, IEnumerable<string> args, Dictionary<string, List<string>> kwargs);
ResultSet CallProcedure(string graphName, string procedure, IEnumerable<string> args, Dictionary<string, List<string>> kwargs, ProcedureMode procedureMode = ProcedureMode.Write);

/// <summary>
/// Delete an existing graph.
Expand Down
9 changes: 6 additions & 3 deletions src/NRedisStack/Graph/IGraphCommandsAsync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,19 @@ public interface IGraphCommandsAsync
/// </summary>
/// <param name="graphName">The graph containing the saved procedure.</param>
/// <param name="procedure">The procedure name.</param>
/// <param name="procedureMode">The mode of the saved procedure. Defaults to <see cref="ProcedureMode.Write"/>.</param>
/// <returns>A result set.</returns>
Task<ResultSet> CallProcedureAsync(string graphName, string procedure);
Task<ResultSet> CallProcedureAsync(string graphName, string procedure, ProcedureMode procedureMode = ProcedureMode.Write);

/// <summary>
/// Call a saved procedure with parameters.
/// </summary>
/// <param name="graphName">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="procedureMode">The mode of the saved procedure. Defaults to <see cref="ProcedureMode.Write"/>.</param>
/// <returns>A result set.</returns>
Task<ResultSet> CallProcedureAsync(string graphName, string procedure, IEnumerable<string> args);
Task<ResultSet> CallProcedureAsync(string graphName, string procedure, IEnumerable<string> args, ProcedureMode procedureMode = ProcedureMode.Write);

/// <summary>
/// Call a saved procedure with parameters.
Expand All @@ -72,8 +74,9 @@ public interface IGraphCommandsAsync
/// <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>
/// <param name="procedureMode">The mode of the saved procedure. Defaults to <see cref="ProcedureMode.Write"/>.</param>
/// <returns>A result set.</returns>
Task<ResultSet> CallProcedureAsync(string graphName, string procedure, IEnumerable<string> args, Dictionary<string, List<string>> kwargs);
Task<ResultSet> CallProcedureAsync(string graphName, string procedure, IEnumerable<string> args, Dictionary<string, List<string>> kwargs, ProcedureMode procedureMode = ProcedureMode.Write);

/// <summary>
/// Delete an existing graph.
Expand Down
11 changes: 11 additions & 0 deletions src/NRedisStack/Graph/ProcedureMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace NRedisStack.Graph
{
/// <summary>
/// Defines the mode of a saved procedure.
/// </summary>
public enum ProcedureMode
{
Read,
Write
}
}
86 changes: 86 additions & 0 deletions tests/NRedisStack.Tests/Graph/GraphTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,49 @@ public void TestModulePrefixs()
Assert.NotEqual(graph1.GetHashCode(), graph2.GetHashCode());
}

[Fact]
public void TestCallProcedureDbLabels()
{
var db = redisFixture.Redis.GetDatabase();
db.Execute("FLUSHALL");

const string graphName = "social";

var graph = db.GRAPH();
// Create empty graph, otherwise call procedure will throw exception
graph.Query(graphName, "RETURN 1");

var labels0 = graph.CallProcedure(graphName, "db.labels");
Assert.Empty(labels0);

graph.Query(graphName, "CREATE (:Person { name: 'Bob' })");

var labels1 = graph.CallProcedure(graphName, "db.labels");
Assert.Single(labels1);
}

[Fact]
public void TestCallProcedureReadOnly()
{
var db = redisFixture.Redis.GetDatabase();
db.Execute("FLUSHALL");

const string graphName = "social";

var graph = db.GRAPH();
// throws RedisServerException when executing a ReadOnly procedure against non-existing graph.
Assert.Throws<RedisServerException>(() => graph.CallProcedure(graphName, "db.labels", ProcedureMode.Read));

graph.Query(graphName, "CREATE (:Person { name: 'Bob' })");
var procedureArgs = new List<string>
{
"Person",
"name"
};
// throws RedisServerException when executing a Write procedure with Read procedure mode.
Assert.Throws<RedisServerException>(() => graph.CallProcedure(graphName, "db.idx.fulltext.createNodeIndex", procedureArgs, ProcedureMode.Read));
}

#endregion

#region AsyncTests
Expand Down Expand Up @@ -1886,6 +1929,49 @@ public async Task TestModulePrefixsAsync()
Assert.NotEqual(graph1.GetHashCode(), graph2.GetHashCode());
}

[Fact]
public async Task TestCallProcedureDbLabelsAsync()
{
var db = redisFixture.Redis.GetDatabase();
db.Execute("FLUSHALL");

const string graphName = "social";

var graph = db.GRAPH();
// Create empty graph, otherwise call procedure will throw exception
await graph.QueryAsync(graphName, "RETURN 1");

var labels0 = await graph.CallProcedureAsync(graphName, "db.labels");
Assert.Empty(labels0);

await graph.QueryAsync(graphName, "CREATE (:Person { name: 'Bob' })");

var labels1 = await graph.CallProcedureAsync(graphName, "db.labels");
Assert.Single(labels1);
}

[Fact]
public async Task TestCallProcedureReadOnlyAsync()
{
var db = redisFixture.Redis.GetDatabase();
db.Execute("FLUSHALL");

const string graphName = "social";

var graph = db.GRAPH();
// throws RedisServerException when executing a ReadOnly procedure against non-existing graph.
await Assert.ThrowsAsync<RedisServerException>(() => graph.CallProcedureAsync(graphName, "db.labels", ProcedureMode.Read));

await graph.QueryAsync(graphName, "CREATE (:Person { name: 'Bob' })");
var procedureArgs = new List<string>
{
"Person",
"name"
};
// throws RedisServerException when executing a Write procedure with Read procedure mode.
await Assert.ThrowsAsync<RedisServerException>(() => graph.CallProcedureAsync(graphName, "db.idx.fulltext.createNodeIndex", procedureArgs, ProcedureMode.Read));
}

[Fact]
public void TestParseInfinity()
{
Expand Down
Loading