diff --git a/src/NRedisStack/Graph/GraphCacheList.cs b/src/NRedisStack/Graph/GraphCacheList.cs index b490a7fb..551e8476 100644 --- a/src/NRedisStack/Graph/GraphCacheList.cs +++ b/src/NRedisStack/Graph/GraphCacheList.cs @@ -1,45 +1,54 @@ 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(); + + private readonly GraphCommandsAsync _redisGraph; private readonly object _locker = new object(); - internal GraphCacheList(string graphName, string procedure, GraphCommands redisGraph) : this(graphName, procedure) + /// + /// Constructs a for providing cached information about the graph. + /// + /// The name of the graph to cache information for. + /// The saved procedure to call to populate cache. Must be a `read` procedure. + /// The graph used for the calling the . + 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) + /// + /// Constructs a for providing cached information about the graph. + /// + /// The name of the graph to cache information for. + /// The saved procedure to call to populate cache. Must be a `read` procedure. + /// The graph used for the calling the . + internal GraphCacheList(string graphName, string procedure, GraphCommandsAsync redisGraphAsync) { - GraphName = graphName; - Procedure = procedure; + _graphName = graphName; + _procedure = procedure; + + _redisGraph = redisGraphAsync; } // TODO: Change this to use Lazy? 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(); } } } @@ -47,25 +56,19 @@ internal string GetCachedData(int index) 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; } } } \ No newline at end of file diff --git a/src/NRedisStack/Graph/GraphCommands.cs b/src/NRedisStack/Graph/GraphCommands.cs index 59fc4f56..16894221 100644 --- a/src/NRedisStack/Graph/GraphCommands.cs +++ b/src/NRedisStack/Graph/GraphCommands.cs @@ -8,7 +8,7 @@ namespace NRedisStack { public class GraphCommands : GraphCommandsAsync, IGraphCommands { - IDatabase _db; + readonly IDatabase _db; public GraphCommands(IDatabase db) : base(db) { @@ -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> EmptyKwargsDictionary = + private static readonly Dictionary> EmptyKwargsDictionary = new Dictionary>(); - // TODO: Check if this is needed: /// - public ResultSet CallProcedure(string graphName, string procedure) => - CallProcedure(graphName, procedure, Enumerable.Empty(), EmptyKwargsDictionary); + public ResultSet CallProcedure(string graphName, string procedure, ProcedureMode procedureMode = ProcedureMode.Write) => + CallProcedure(graphName, procedure, Enumerable.Empty(), EmptyKwargsDictionary, procedureMode); /// - public ResultSet CallProcedure(string graphName, string procedure, IEnumerable args) => - CallProcedure(graphName, procedure, args, EmptyKwargsDictionary); + public ResultSet CallProcedure(string graphName, string procedure, IEnumerable args, ProcedureMode procedureMode = ProcedureMode.Write) => + CallProcedure(graphName, procedure, args, EmptyKwargsDictionary, procedureMode); /// - public ResultSet CallProcedure(string graphName, string procedure, IEnumerable args, Dictionary> kwargs) + public ResultSet CallProcedure(string graphName, string procedure, IEnumerable args, Dictionary> kwargs, ProcedureMode procedureMode = ProcedureMode.Write) { - args = args.Select(a => QuoteString(a)); + args = args.Select(QuoteString); var queryBody = new StringBuilder(); @@ -81,7 +80,9 @@ public ResultSet CallProcedure(string graphName, string procedure, IEnumerable diff --git a/src/NRedisStack/Graph/GraphCommandsAsync.cs b/src/NRedisStack/Graph/GraphCommandsAsync.cs index cdbb7517..8f06f338 100644 --- a/src/NRedisStack/Graph/GraphCommandsAsync.cs +++ b/src/NRedisStack/Graph/GraphCommandsAsync.cs @@ -9,7 +9,7 @@ namespace NRedisStack { public class GraphCommandsAsync : IGraphCommandsAsync { - IDatabaseAsync _db; + readonly IDatabaseAsync _db; public GraphCommandsAsync(IDatabaseAsync db) { @@ -56,22 +56,21 @@ public async Task 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> EmptyKwargsDictionary = + private static readonly Dictionary> EmptyKwargsDictionary = new Dictionary>(); - // TODO: Check if this is needed: /// - public async Task CallProcedureAsync(string graphName, string procedure) => - await CallProcedureAsync(graphName, procedure, Enumerable.Empty(), EmptyKwargsDictionary); + public Task CallProcedureAsync(string graphName, string procedure, ProcedureMode procedureMode = ProcedureMode.Write) => + CallProcedureAsync(graphName, procedure, Enumerable.Empty(), EmptyKwargsDictionary, procedureMode); /// - public async Task CallProcedureAsync(string graphName, string procedure, IEnumerable args) => - await CallProcedureAsync(graphName, procedure, args, EmptyKwargsDictionary); + public Task CallProcedureAsync(string graphName, string procedure, IEnumerable args, ProcedureMode procedureMode = ProcedureMode.Write) => + CallProcedureAsync(graphName, procedure, args, EmptyKwargsDictionary, procedureMode); /// - public async Task CallProcedureAsync(string graphName, string procedure, IEnumerable args, Dictionary> kwargs) + public async Task CallProcedureAsync(string graphName, string procedure, IEnumerable args, Dictionary> kwargs, ProcedureMode procedureMode = ProcedureMode.Write) { - args = args.Select(a => QuoteString(a)); + args = args.Select(QuoteString); var queryBody = new StringBuilder(); @@ -82,7 +81,9 @@ public async Task 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()); } diff --git a/src/NRedisStack/Graph/IGraphCommands.cs b/src/NRedisStack/Graph/IGraphCommands.cs index 3be792c4..8995d072 100644 --- a/src/NRedisStack/Graph/IGraphCommands.cs +++ b/src/NRedisStack/Graph/IGraphCommands.cs @@ -53,8 +53,9 @@ public interface IGraphCommands /// /// The graph containing the saved procedure. /// The procedure name. + /// The mode of the saved procedure. Defaults to . /// A result set. - ResultSet CallProcedure(string graphName, string procedure); + ResultSet CallProcedure(string graphName, string procedure, ProcedureMode procedureMode = ProcedureMode.Write); /// /// Call a saved procedure with parameters. @@ -62,8 +63,9 @@ public interface IGraphCommands /// The graph containing the saved procedure. /// The procedure name. /// A collection of positional arguments. + /// The mode of the saved procedure. Defaults to . /// A result set. - ResultSet CallProcedure(string graphName, string procedure, IEnumerable args); + ResultSet CallProcedure(string graphName, string procedure, IEnumerable args, ProcedureMode procedureMode = ProcedureMode.Write); /// /// Call a saved procedure with parameters. @@ -72,8 +74,9 @@ public interface IGraphCommands /// The procedure name. /// A collection of positional arguments. /// A collection of keyword arguments. + /// The mode of the saved procedure. Defaults to . /// A result set. - ResultSet CallProcedure(string graphName, string procedure, IEnumerable args, Dictionary> kwargs); + ResultSet CallProcedure(string graphName, string procedure, IEnumerable args, Dictionary> kwargs, ProcedureMode procedureMode = ProcedureMode.Write); /// /// Delete an existing graph. diff --git a/src/NRedisStack/Graph/IGraphCommandsAsync.cs b/src/NRedisStack/Graph/IGraphCommandsAsync.cs index 62ea118f..7ea98ec8 100644 --- a/src/NRedisStack/Graph/IGraphCommandsAsync.cs +++ b/src/NRedisStack/Graph/IGraphCommandsAsync.cs @@ -53,8 +53,9 @@ public interface IGraphCommandsAsync /// /// The graph containing the saved procedure. /// The procedure name. + /// The mode of the saved procedure. Defaults to . /// A result set. - Task CallProcedureAsync(string graphName, string procedure); + Task CallProcedureAsync(string graphName, string procedure, ProcedureMode procedureMode = ProcedureMode.Write); /// /// Call a saved procedure with parameters. @@ -62,8 +63,9 @@ public interface IGraphCommandsAsync /// The graph containing the saved procedure. /// The procedure name. /// A collection of positional arguments. + /// The mode of the saved procedure. Defaults to . /// A result set. - Task CallProcedureAsync(string graphName, string procedure, IEnumerable args); + Task CallProcedureAsync(string graphName, string procedure, IEnumerable args, ProcedureMode procedureMode = ProcedureMode.Write); /// /// Call a saved procedure with parameters. @@ -72,8 +74,9 @@ public interface IGraphCommandsAsync /// The procedure name. /// A collection of positional arguments. /// A collection of keyword arguments. + /// The mode of the saved procedure. Defaults to . /// A result set. - Task CallProcedureAsync(string graphName, string procedure, IEnumerable args, Dictionary> kwargs); + Task CallProcedureAsync(string graphName, string procedure, IEnumerable args, Dictionary> kwargs, ProcedureMode procedureMode = ProcedureMode.Write); /// /// Delete an existing graph. diff --git a/src/NRedisStack/Graph/ProcedureMode.cs b/src/NRedisStack/Graph/ProcedureMode.cs new file mode 100644 index 00000000..092b4d56 --- /dev/null +++ b/src/NRedisStack/Graph/ProcedureMode.cs @@ -0,0 +1,11 @@ +namespace NRedisStack.Graph +{ + /// + /// Defines the mode of a saved procedure. + /// + public enum ProcedureMode + { + Read, + Write + } +} \ No newline at end of file diff --git a/tests/NRedisStack.Tests/Graph/GraphTests.cs b/tests/NRedisStack.Tests/Graph/GraphTests.cs index 6355c15b..d539de78 100644 --- a/tests/NRedisStack.Tests/Graph/GraphTests.cs +++ b/tests/NRedisStack.Tests/Graph/GraphTests.cs @@ -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(() => graph.CallProcedure(graphName, "db.labels", ProcedureMode.Read)); + + graph.Query(graphName, "CREATE (:Person { name: 'Bob' })"); + var procedureArgs = new List + { + "Person", + "name" + }; + // throws RedisServerException when executing a Write procedure with Read procedure mode. + Assert.Throws(() => graph.CallProcedure(graphName, "db.idx.fulltext.createNodeIndex", procedureArgs, ProcedureMode.Read)); + } + #endregion #region AsyncTests @@ -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(() => graph.CallProcedureAsync(graphName, "db.labels", ProcedureMode.Read)); + + await graph.QueryAsync(graphName, "CREATE (:Person { name: 'Bob' })"); + var procedureArgs = new List + { + "Person", + "name" + }; + // throws RedisServerException when executing a Write procedure with Read procedure mode. + await Assert.ThrowsAsync(() => graph.CallProcedureAsync(graphName, "db.idx.fulltext.createNodeIndex", procedureArgs, ProcedureMode.Read)); + } + [Fact] public void TestParseInfinity() {