Skip to content

Commit 6c9ea15

Browse files
authored
Introduce procedure mode selection for RedisGraph. (#162)
* Introduce procedure mode selection for RedisGraph. * Add option to specify procedure mode as specified by `dbms.procedures()`. * Refactor `GraphCacheList` to use READ mode for `db.labels()`, `db.propertyKeys()`, and `db.relationshipTypes()` in `GraphCache`. * Document constructor and initialize data array to avoid nulls. * Minor formatting changes. * Add tests for CallProcedure and CallProcedureAsync * Replace lambda with method call * Trim trailing whitespace
1 parent 78be574 commit 6c9ea15

File tree

7 files changed

+170
-62
lines changed

7 files changed

+170
-62
lines changed
+39-36
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,74 @@
11
namespace NRedisStack.Graph
22
{
3-
internal class GraphCacheList
3+
internal sealed class GraphCacheList
44
{
5-
protected readonly string GraphName;
6-
protected readonly string Procedure;
7-
private string[] _data;
5+
private readonly string _graphName;
6+
private readonly string _procedure;
87

9-
protected readonly GraphCommands graph;
10-
protected readonly GraphCommandsAsync asyncGraph;
11-
private bool asyncGraphUsed;
8+
private string[] _data = Array.Empty<string>();
9+
10+
private readonly GraphCommandsAsync _redisGraph;
1211

1312
private readonly object _locker = new object();
1413

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

21-
internal GraphCacheList(string graphName, string procedure, GraphCommandsAsync redisGraph) : this(graphName, procedure)
22-
{
23-
asyncGraph = redisGraph;
24-
asyncGraphUsed = true;
25+
_redisGraph = redisGraph;
2526
}
2627

27-
private GraphCacheList(string graphName, string procedure)
28+
/// <summary>
29+
/// Constructs a <see cref="GraphCacheList"/> for providing cached information about the graph.
30+
/// </summary>
31+
/// <param name="graphName">The name of the graph to cache information for.</param>
32+
/// <param name="procedure">The saved procedure to call to populate cache. Must be a `read` procedure.</param>
33+
/// <param name="redisGraph">The graph used for the calling the <paramref name="procedure"/>.</param>
34+
internal GraphCacheList(string graphName, string procedure, GraphCommandsAsync redisGraphAsync)
2835
{
29-
GraphName = graphName;
30-
Procedure = procedure;
36+
_graphName = graphName;
37+
_procedure = procedure;
38+
39+
_redisGraph = redisGraphAsync;
3140
}
3241

3342
// TODO: Change this to use Lazy<T>?
3443
internal string GetCachedData(int index)
3544
{
36-
if (_data == null || index >= _data.Length)
45+
if (index >= _data.Length)
3746
{
3847
lock (_locker)
3948
{
40-
if (_data == null || index >= _data.Length)
49+
if (index >= _data.Length)
4150
{
42-
GetProcedureInfo();
51+
_data = GetProcedureInfo();
4352
}
4453
}
4554
}
4655

4756
return _data.ElementAtOrDefault(index);
4857
}
4958

50-
private void GetProcedureInfo()
59+
private string[] GetProcedureInfo()
5160
{
52-
var resultSet = CallProcedure(asyncGraphUsed);
53-
var newData = new string[resultSet.Count];
54-
var i = 0;
55-
56-
foreach (var record in resultSet)
57-
{
58-
newData[i++] = record.GetString(0);
59-
}
60-
61-
_data = newData;
61+
var resultSet = CallProcedure();
62+
return resultSet
63+
.Select(r => r.GetString(0))
64+
.ToArray();
6265
}
6366

64-
protected virtual ResultSet CallProcedure(bool asyncGraphUsed = false)
67+
private ResultSet CallProcedure()
6568
{
66-
return asyncGraphUsed
67-
? asyncGraph.CallProcedureAsync(GraphName, Procedure).Result
68-
: graph.CallProcedure(GraphName, Procedure);
69+
return _redisGraph is GraphCommands graphSync
70+
? graphSync.CallProcedure(_graphName, _procedure, ProcedureMode.Read)
71+
: _redisGraph.CallProcedureAsync(_graphName, _procedure, ProcedureMode.Read).Result;
6972
}
7073
}
7174
}

src/NRedisStack/Graph/GraphCommands.cs

+11-10
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace NRedisStack
88
{
99
public class GraphCommands : GraphCommandsAsync, IGraphCommands
1010
{
11-
IDatabase _db;
11+
readonly IDatabase _db;
1212

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

58-
internal static readonly Dictionary<string, List<string>> EmptyKwargsDictionary =
58+
private static readonly Dictionary<string, List<string>> EmptyKwargsDictionary =
5959
new Dictionary<string, List<string>>();
6060

61-
// TODO: Check if this is needed:
6261
/// <inheritdoc/>
63-
public ResultSet CallProcedure(string graphName, string procedure) =>
64-
CallProcedure(graphName, procedure, Enumerable.Empty<string>(), EmptyKwargsDictionary);
62+
public ResultSet CallProcedure(string graphName, string procedure, ProcedureMode procedureMode = ProcedureMode.Write) =>
63+
CallProcedure(graphName, procedure, Enumerable.Empty<string>(), EmptyKwargsDictionary, procedureMode);
6564

6665
/// <inheritdoc/>
67-
public ResultSet CallProcedure(string graphName, string procedure, IEnumerable<string> args) =>
68-
CallProcedure(graphName, procedure, args, EmptyKwargsDictionary);
66+
public ResultSet CallProcedure(string graphName, string procedure, IEnumerable<string> args, ProcedureMode procedureMode = ProcedureMode.Write) =>
67+
CallProcedure(graphName, procedure, args, EmptyKwargsDictionary, procedureMode);
6968

7069
/// <inheritdoc/>
71-
public ResultSet CallProcedure(string graphName, string procedure, IEnumerable<string> args, Dictionary<string, List<string>> kwargs)
70+
public ResultSet CallProcedure(string graphName, string procedure, IEnumerable<string> args, Dictionary<string, List<string>> kwargs, ProcedureMode procedureMode = ProcedureMode.Write)
7271
{
73-
args = args.Select(a => QuoteString(a));
72+
args = args.Select(QuoteString);
7473

7574
var queryBody = new StringBuilder();
7675

@@ -81,7 +80,9 @@ public ResultSet CallProcedure(string graphName, string procedure, IEnumerable<s
8180
queryBody.Append(string.Join(",", kwargsList));
8281
}
8382

84-
return Query(graphName, queryBody.ToString());
83+
return procedureMode is ProcedureMode.Read
84+
? RO_Query(graphName, queryBody.ToString())
85+
: Query(graphName, queryBody.ToString());
8586
}
8687

8788
/// <inheritdoc/>

src/NRedisStack/Graph/GraphCommandsAsync.cs

+11-10
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace NRedisStack
99
{
1010
public class GraphCommandsAsync : IGraphCommandsAsync
1111
{
12-
IDatabaseAsync _db;
12+
readonly IDatabaseAsync _db;
1313

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

59-
internal static readonly Dictionary<string, List<string>> EmptyKwargsDictionary =
59+
private static readonly Dictionary<string, List<string>> EmptyKwargsDictionary =
6060
new Dictionary<string, List<string>>();
6161

62-
// TODO: Check if this is needed:
6362
/// <inheritdoc/>
64-
public async Task<ResultSet> CallProcedureAsync(string graphName, string procedure) =>
65-
await CallProcedureAsync(graphName, procedure, Enumerable.Empty<string>(), EmptyKwargsDictionary);
63+
public Task<ResultSet> CallProcedureAsync(string graphName, string procedure, ProcedureMode procedureMode = ProcedureMode.Write) =>
64+
CallProcedureAsync(graphName, procedure, Enumerable.Empty<string>(), EmptyKwargsDictionary, procedureMode);
6665

6766
/// <inheritdoc/>
68-
public async Task<ResultSet> CallProcedureAsync(string graphName, string procedure, IEnumerable<string> args) =>
69-
await CallProcedureAsync(graphName, procedure, args, EmptyKwargsDictionary);
67+
public Task<ResultSet> CallProcedureAsync(string graphName, string procedure, IEnumerable<string> args, ProcedureMode procedureMode = ProcedureMode.Write) =>
68+
CallProcedureAsync(graphName, procedure, args, EmptyKwargsDictionary, procedureMode);
7069

7170
/// <inheritdoc/>
72-
public async Task<ResultSet> CallProcedureAsync(string graphName, string procedure, IEnumerable<string> args, Dictionary<string, List<string>> kwargs)
71+
public async Task<ResultSet> CallProcedureAsync(string graphName, string procedure, IEnumerable<string> args, Dictionary<string, List<string>> kwargs, ProcedureMode procedureMode = ProcedureMode.Write)
7372
{
74-
args = args.Select(a => QuoteString(a));
73+
args = args.Select(QuoteString);
7574

7675
var queryBody = new StringBuilder();
7776

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

85-
return await QueryAsync(graphName, queryBody.ToString());
84+
return procedureMode is ProcedureMode.Read
85+
? await RO_QueryAsync(graphName, queryBody.ToString())
86+
: await QueryAsync(graphName, queryBody.ToString());
8687
}
8788

8889

src/NRedisStack/Graph/IGraphCommands.cs

+6-3
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,19 @@ public interface IGraphCommands
5353
/// </summary>
5454
/// <param name="graphName">The graph containing the saved procedure.</param>
5555
/// <param name="procedure">The procedure name.</param>
56+
/// <param name="procedureMode">The mode of the saved procedure. Defaults to <see cref="ProcedureMode.Write"/>.</param>
5657
/// <returns>A result set.</returns>
57-
ResultSet CallProcedure(string graphName, string procedure);
58+
ResultSet CallProcedure(string graphName, string procedure, ProcedureMode procedureMode = ProcedureMode.Write);
5859

5960
/// <summary>
6061
/// Call a saved procedure with parameters.
6162
/// </summary>
6263
/// <param name="graphName">The graph containing the saved procedure.</param>
6364
/// <param name="procedure">The procedure name.</param>
6465
/// <param name="args">A collection of positional arguments.</param>
66+
/// <param name="procedureMode">The mode of the saved procedure. Defaults to <see cref="ProcedureMode.Write"/>.</param>
6567
/// <returns>A result set.</returns>
66-
ResultSet CallProcedure(string graphName, string procedure, IEnumerable<string> args);
68+
ResultSet CallProcedure(string graphName, string procedure, IEnumerable<string> args, ProcedureMode procedureMode = ProcedureMode.Write);
6769

6870
/// <summary>
6971
/// Call a saved procedure with parameters.
@@ -72,8 +74,9 @@ public interface IGraphCommands
7274
/// <param name="procedure">The procedure name.</param>
7375
/// <param name="args">A collection of positional arguments.</param>
7476
/// <param name="kwargs">A collection of keyword arguments.</param>
77+
/// <param name="procedureMode">The mode of the saved procedure. Defaults to <see cref="ProcedureMode.Write"/>.</param>
7578
/// <returns>A result set.</returns>
76-
ResultSet CallProcedure(string graphName, string procedure, IEnumerable<string> args, Dictionary<string, List<string>> kwargs);
79+
ResultSet CallProcedure(string graphName, string procedure, IEnumerable<string> args, Dictionary<string, List<string>> kwargs, ProcedureMode procedureMode = ProcedureMode.Write);
7780

7881
/// <summary>
7982
/// Delete an existing graph.

src/NRedisStack/Graph/IGraphCommandsAsync.cs

+6-3
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,19 @@ public interface IGraphCommandsAsync
5353
/// </summary>
5454
/// <param name="graphName">The graph containing the saved procedure.</param>
5555
/// <param name="procedure">The procedure name.</param>
56+
/// <param name="procedureMode">The mode of the saved procedure. Defaults to <see cref="ProcedureMode.Write"/>.</param>
5657
/// <returns>A result set.</returns>
57-
Task<ResultSet> CallProcedureAsync(string graphName, string procedure);
58+
Task<ResultSet> CallProcedureAsync(string graphName, string procedure, ProcedureMode procedureMode = ProcedureMode.Write);
5859

5960
/// <summary>
6061
/// Call a saved procedure with parameters.
6162
/// </summary>
6263
/// <param name="graphName">The graph containing the saved procedure.</param>
6364
/// <param name="procedure">The procedure name.</param>
6465
/// <param name="args">A collection of positional arguments.</param>
66+
/// <param name="procedureMode">The mode of the saved procedure. Defaults to <see cref="ProcedureMode.Write"/>.</param>
6567
/// <returns>A result set.</returns>
66-
Task<ResultSet> CallProcedureAsync(string graphName, string procedure, IEnumerable<string> args);
68+
Task<ResultSet> CallProcedureAsync(string graphName, string procedure, IEnumerable<string> args, ProcedureMode procedureMode = ProcedureMode.Write);
6769

6870
/// <summary>
6971
/// Call a saved procedure with parameters.
@@ -72,8 +74,9 @@ public interface IGraphCommandsAsync
7274
/// <param name="procedure">The procedure name.</param>
7375
/// <param name="args">A collection of positional arguments.</param>
7476
/// <param name="kwargs">A collection of keyword arguments.</param>
77+
/// <param name="procedureMode">The mode of the saved procedure. Defaults to <see cref="ProcedureMode.Write"/>.</param>
7578
/// <returns>A result set.</returns>
76-
Task<ResultSet> CallProcedureAsync(string graphName, string procedure, IEnumerable<string> args, Dictionary<string, List<string>> kwargs);
79+
Task<ResultSet> CallProcedureAsync(string graphName, string procedure, IEnumerable<string> args, Dictionary<string, List<string>> kwargs, ProcedureMode procedureMode = ProcedureMode.Write);
7780

7881
/// <summary>
7982
/// Delete an existing graph.
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace NRedisStack.Graph
2+
{
3+
/// <summary>
4+
/// Defines the mode of a saved procedure.
5+
/// </summary>
6+
public enum ProcedureMode
7+
{
8+
Read,
9+
Write
10+
}
11+
}

tests/NRedisStack.Tests/Graph/GraphTests.cs

+86
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,49 @@ public void TestModulePrefixs()
956956
Assert.NotEqual(graph1.GetHashCode(), graph2.GetHashCode());
957957
}
958958

959+
[Fact]
960+
public void TestCallProcedureDbLabels()
961+
{
962+
var db = redisFixture.Redis.GetDatabase();
963+
db.Execute("FLUSHALL");
964+
965+
const string graphName = "social";
966+
967+
var graph = db.GRAPH();
968+
// Create empty graph, otherwise call procedure will throw exception
969+
graph.Query(graphName, "RETURN 1");
970+
971+
var labels0 = graph.CallProcedure(graphName, "db.labels");
972+
Assert.Empty(labels0);
973+
974+
graph.Query(graphName, "CREATE (:Person { name: 'Bob' })");
975+
976+
var labels1 = graph.CallProcedure(graphName, "db.labels");
977+
Assert.Single(labels1);
978+
}
979+
980+
[Fact]
981+
public void TestCallProcedureReadOnly()
982+
{
983+
var db = redisFixture.Redis.GetDatabase();
984+
db.Execute("FLUSHALL");
985+
986+
const string graphName = "social";
987+
988+
var graph = db.GRAPH();
989+
// throws RedisServerException when executing a ReadOnly procedure against non-existing graph.
990+
Assert.Throws<RedisServerException>(() => graph.CallProcedure(graphName, "db.labels", ProcedureMode.Read));
991+
992+
graph.Query(graphName, "CREATE (:Person { name: 'Bob' })");
993+
var procedureArgs = new List<string>
994+
{
995+
"Person",
996+
"name"
997+
};
998+
// throws RedisServerException when executing a Write procedure with Read procedure mode.
999+
Assert.Throws<RedisServerException>(() => graph.CallProcedure(graphName, "db.idx.fulltext.createNodeIndex", procedureArgs, ProcedureMode.Read));
1000+
}
1001+
9591002
#endregion
9601003

9611004
#region AsyncTests
@@ -1886,6 +1929,49 @@ public async Task TestModulePrefixsAsync()
18861929
Assert.NotEqual(graph1.GetHashCode(), graph2.GetHashCode());
18871930
}
18881931

1932+
[Fact]
1933+
public async Task TestCallProcedureDbLabelsAsync()
1934+
{
1935+
var db = redisFixture.Redis.GetDatabase();
1936+
db.Execute("FLUSHALL");
1937+
1938+
const string graphName = "social";
1939+
1940+
var graph = db.GRAPH();
1941+
// Create empty graph, otherwise call procedure will throw exception
1942+
await graph.QueryAsync(graphName, "RETURN 1");
1943+
1944+
var labels0 = await graph.CallProcedureAsync(graphName, "db.labels");
1945+
Assert.Empty(labels0);
1946+
1947+
await graph.QueryAsync(graphName, "CREATE (:Person { name: 'Bob' })");
1948+
1949+
var labels1 = await graph.CallProcedureAsync(graphName, "db.labels");
1950+
Assert.Single(labels1);
1951+
}
1952+
1953+
[Fact]
1954+
public async Task TestCallProcedureReadOnlyAsync()
1955+
{
1956+
var db = redisFixture.Redis.GetDatabase();
1957+
db.Execute("FLUSHALL");
1958+
1959+
const string graphName = "social";
1960+
1961+
var graph = db.GRAPH();
1962+
// throws RedisServerException when executing a ReadOnly procedure against non-existing graph.
1963+
await Assert.ThrowsAsync<RedisServerException>(() => graph.CallProcedureAsync(graphName, "db.labels", ProcedureMode.Read));
1964+
1965+
await graph.QueryAsync(graphName, "CREATE (:Person { name: 'Bob' })");
1966+
var procedureArgs = new List<string>
1967+
{
1968+
"Person",
1969+
"name"
1970+
};
1971+
// throws RedisServerException when executing a Write procedure with Read procedure mode.
1972+
await Assert.ThrowsAsync<RedisServerException>(() => graph.CallProcedureAsync(graphName, "db.idx.fulltext.createNodeIndex", procedureArgs, ProcedureMode.Read));
1973+
}
1974+
18891975
[Fact]
18901976
public void TestParseInfinity()
18911977
{

0 commit comments

Comments
 (0)