diff --git a/src/NRedisStack/Graph/DataTypes/Edge.cs b/src/NRedisStack/Graph/DataTypes/Edge.cs new file mode 100644 index 00000000..24e7df5b --- /dev/null +++ b/src/NRedisStack/Graph/DataTypes/Edge.cs @@ -0,0 +1,96 @@ +using System.Text; + +namespace NRedisStack.Graph.DataTypes +{ + /// + /// A class reprenting an edge (graph entity). In addition to the base class properties, an edge shows its source, + /// destination, and relationship type. + /// + public class Edge : GraphEntity + { + /// + /// The relationship type. + /// + /// + public string RelationshipType { get; set; } + + /// + /// The ID of the source node. + /// + /// + public long Source { get; set; } + + /// + /// The ID of the desination node. + /// + /// + public long Destination { get; set; } + + /// + /// Overriden from the base `Equals` implementation. In addition to the expected behavior of checking + /// reference equality, we'll also fall back and check to see if the: Source, Destination, and RelationshipType + /// are equal. + /// + /// Another `Edge` object to compare to. + /// True if the two instances are equal, false if not. + public override bool Equals(object? obj) + { + if (obj == null) return this == null; + + if (this == obj) + { + return true; + } + + if (!(obj is Edge that)) + { + return false; + } + + if (!base.Equals(obj)) + { + return false; + } + + return Source == that.Source && Destination == that.Destination && RelationshipType == that.RelationshipType; + } + + /// + /// Overriden from base to compute a deterministic hashcode based on RelationshipType, Source, and Destination. + /// + /// An integer representing the hash code for this instance. + public override int GetHashCode() + { + unchecked + { + int hash = 17; + + hash = hash * 31 + base.GetHashCode(); + hash = hash * 31 + RelationshipType.GetHashCode(); + hash = hash * 31 + Source.GetHashCode(); + hash = hash * 31 + Destination.GetHashCode(); + + return hash; + } + } + + /// + /// Override from base to emit a string that contains: RelationshipType, Source, Destination, Id, and PropertyMap. + /// + /// A string containing a description of the Edge containing a RelationshipType, Source, Destination, Id, and PropertyMap. + public override string ToString() + { + var sb = new StringBuilder(); + + sb.Append("Edge{"); + sb.Append($"relationshipType='{RelationshipType}'"); + sb.Append($", source={Source}"); + sb.Append($", destination={Destination}"); + sb.Append($", id={Id}"); + sb.Append($", {PropertyMapToString()}"); + sb.Append("}"); + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/src/NRedisStack/Graph/DataTypes/GraphEntity.cs b/src/NRedisStack/Graph/DataTypes/GraphEntity.cs new file mode 100644 index 00000000..d902c06b --- /dev/null +++ b/src/NRedisStack/Graph/DataTypes/GraphEntity.cs @@ -0,0 +1,89 @@ +using System.Text; + +namespace NRedisStack.Graph.DataTypes +{ + /// + /// An abstract representation of a graph entity. + /// A graph entity has an ID and a set of properties. The properties are mapped and accessed by their names. + /// + public abstract class GraphEntity + { + public long Id { get; set; } + + public IDictionary PropertyMap = new Dictionary(); + + /// + /// Overriden Equals that considers the equality of the entity ID as well as the equality of the + /// properties that each entity has. + /// + /// + /// + public override bool Equals(object? obj) + { + if (obj == null) return this == null; + + if (this == obj) + { + return true; + } + + if (!(obj is GraphEntity that)) + { + return false; + } + + return Id == that.Id && (PropertyMap.SequenceEqual(that.PropertyMap)); + } + + /// + /// Overriden GetHashCode that computes a deterministic hash code based on the value of the ID + /// and the name/value of each of the associated properties. + /// + /// + public override int GetHashCode() + { + unchecked + { + int hash = 17; + + hash = hash * 31 + Id.GetHashCode(); + + foreach(var prop in PropertyMap) + { + hash = hash * 31 + prop.Key.GetHashCode(); + hash = hash * 31 + prop.Value.GetHashCode(); + } + + return hash; + } + } + + /// + /// Overriden ToString that emits a string containing the ID and property map of the entity. + /// + /// + public override string ToString() + { + var sb = new StringBuilder(); + + sb.Append("GraphEntity{id="); + sb.Append(Id); + sb.Append(", propertyMap="); + sb.Append(PropertyMap); + sb.Append('}'); + + return sb.ToString(); + } + + public string PropertyMapToString() + { + var sb = new StringBuilder(); + + sb.Append("propertyMap={"); + sb.Append(string.Join(", ", PropertyMap.Select(pm => $"{pm.Key}={pm.Value}"))); + sb.Append("}"); + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/src/NRedisStack/Graph/DataTypes/Node.cs b/src/NRedisStack/Graph/DataTypes/Node.cs new file mode 100644 index 00000000..05a30e86 --- /dev/null +++ b/src/NRedisStack/Graph/DataTypes/Node.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NRedisStack.Graph.DataTypes +{ + /// + /// A class representing a node (graph entity). In addition to the base class ID and properties, a node has labels. + /// + public sealed class Node : GraphEntity + { + public List Labels { get; } + + public Node() + { + Labels = new List(); + } + + /// + /// Overriden member that checks to see if the names of the labels of a node are equal + /// (in addition to base `Equals` functionality). + /// + /// + /// + public override bool Equals(object? obj) + { + if (obj == null) return this == null; + + if (this == obj) + { + return true; + } + + if (!(obj is Node that)) + { + return false; + } + + if (!base.Equals(obj)) + { + return false; + } + + return Enumerable.SequenceEqual(Labels, that.Labels); + } + + /// + /// Overridden member that computes a hash code based on the base `GetHashCode` implementation + /// as well as the hash codes of all labels. + /// + /// + public override int GetHashCode() + { + unchecked + { + int hash = 17; + + foreach(var label in Labels) + { + hash = hash * 31 + label.GetHashCode(); + } + + hash = hash * 31 + base.GetHashCode(); + + return hash; + } + } + + /// + /// Overridden member that emits a string containing the labels, ID, and property map of a node. + /// + /// + public override string ToString() + { + var sb = new StringBuilder(); + + sb.Append("Node{labels="); + sb.Append($"[{string.Join(", ", Labels)}]"); + sb.Append($", id={Id}"); + sb.Append($", {PropertyMapToString()}"); + sb.Append("}"); + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/src/NRedisStack/Graph/DataTypes/Path.cs b/src/NRedisStack/Graph/DataTypes/Path.cs new file mode 100644 index 00000000..d3f5a68c --- /dev/null +++ b/src/NRedisStack/Graph/DataTypes/Path.cs @@ -0,0 +1,93 @@ +using System.Collections.ObjectModel; +using System.Runtime.CompilerServices; +using System.Text; + +[assembly: InternalsVisibleTo("NRedisStack.Tests.Graph")] + +namespace NRedisStack.Graph.DataTypes +{ + /// + /// This class represents a path in the graph. + /// + public class Path + { + public ReadOnlyCollection Nodes { get;} + public ReadOnlyCollection Edges { get;} + + public Path(IList nodes, IList edges) + { + Nodes = new ReadOnlyCollection(nodes); + Edges = new ReadOnlyCollection(edges); + } + + + /// + /// How many edges exist on this path. + /// + public int Length => Edges.Count; + + /// + /// Overriden `Equals` method that will consider the equality of the Nodes and Edges between two paths. + /// + /// + /// + public override bool Equals(object? obj) + { + if (obj == null) return this == null; + + if (this == obj) + { + return true; + } + + if (!(obj is Path path)) + { + return false; + } + + return Enumerable.SequenceEqual(Nodes, path.Nodes) && Enumerable.SequenceEqual(Edges, path.Edges); + } + + /// + /// Overridden `GetHashCode` method that will compute a hash code using the hash code of each node and edge on + /// the path. + /// + /// + public override int GetHashCode() + { + unchecked + { + int hash = 17; + + foreach (var node in Nodes) + { + hash = hash * 31 + node.GetHashCode(); + } + + foreach (var edge in Edges) + { + hash = hash * 31 + edge.GetHashCode(); + } + + return hash; + } + } + + /// + /// Overridden `ToString` method that will emit a string based on the string values of the nodes and edges + /// on the path. + /// + /// + public override string ToString() + { + var sb = new StringBuilder(); + + sb.Append("Path{"); + sb.Append($"nodes={Nodes}"); + sb.Append($", edges={Edges}"); + sb.Append("}"); + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/src/NRedisStack/Graph/GraphCache.cs b/src/NRedisStack/Graph/GraphCache.cs new file mode 100644 index 00000000..d8867615 --- /dev/null +++ b/src/NRedisStack/Graph/GraphCache.cs @@ -0,0 +1,23 @@ +namespace NRedisStack.Graph +{ + internal sealed class GraphCache + { + public GraphCacheList Labels { get; set; } + public GraphCacheList PropertyNames { get; set; } + public GraphCacheList RelationshipTypes { get; set; } + + public GraphCache(string graphName, GraphCommands redisGraph) + { + Labels = new GraphCacheList(graphName, "db.labels", redisGraph); + PropertyNames = new GraphCacheList(graphName, "db.propertyKeys", redisGraph); + RelationshipTypes = new GraphCacheList(graphName, "db.relationshipTypes", redisGraph); + } + + public string GetLabel(int index) => Labels.GetCachedData(index); + + public string GetRelationshipType(int index) => RelationshipTypes.GetCachedData(index); + + public string GetPropertyName(int index) => PropertyNames.GetCachedData(index); + + } +} \ No newline at end of file diff --git a/src/NRedisStack/Graph/GraphCacheList.cs b/src/NRedisStack/Graph/GraphCacheList.cs new file mode 100644 index 00000000..74db8abe --- /dev/null +++ b/src/NRedisStack/Graph/GraphCacheList.cs @@ -0,0 +1,66 @@ +namespace NRedisStack.Graph +{ + internal class GraphCacheList + { + protected readonly string GraphName; + protected readonly string Procedure; + private string[] _data; + + protected readonly GraphCommands graph; + + private readonly object _locker = new object(); + + + internal GraphCacheList(string graphName, string procedure, GraphCommands redisGraph) + { + GraphName = graphName; + Procedure = procedure; + graph = redisGraph; + } + + // TODO: Change this to use Lazy? + internal string GetCachedData(int index) + { + if (_data == null || index >= _data.Length) + { + lock(_locker) + { + if (_data == null || index >= _data.Length) + { + GetProcedureInfo(); + } + } + } + + return _data.ElementAtOrDefault(index); + } + + private void GetProcedureInfo() + { + var resultSet = CallProcedure(); + var newData = new string[resultSet.Count]; + var i = 0; + + foreach (var record in resultSet) + { + newData[i++] = record.GetString(0); + } + + _data = newData; + } + + protected virtual ResultSet CallProcedure() => + graph.CallProcedure(GraphName, Procedure); + } + + internal class ReadOnlyGraphCacheList : GraphCacheList + { + internal ReadOnlyGraphCacheList(string graphName, string procedure, GraphCommands redisGraph) : + base(graphName, procedure, redisGraph) + { + } + + protected override ResultSet CallProcedure() => + graph.CallProcedureReadOnly(GraphName, Procedure); + } +} \ No newline at end of file diff --git a/src/NRedisStack/Graph/GraphCommands.cs b/src/NRedisStack/Graph/GraphCommands.cs new file mode 100644 index 00000000..f7e83174 --- /dev/null +++ b/src/NRedisStack/Graph/GraphCommands.cs @@ -0,0 +1,469 @@ +using System.Text; +using NRedisStack.Graph; +using NRedisStack.Literals; +using StackExchange.Redis; +using static NRedisStack.Graph.RedisGraphUtilities; + + +namespace NRedisStack +{ + + public class GraphCommands : IGraphCommands + { + IDatabase _db; + + public GraphCommands(IDatabase db) + { + _db = db; + } + + internal static readonly object CompactQueryFlag = "--COMPACT"; + + private readonly IDictionary _graphCaches = new Dictionary(); + + private GraphCache GetGraphCache(string graphName) + { + if (!_graphCaches.ContainsKey(graphName)) + { + _graphCaches.Add(graphName, new GraphCache(graphName, this)); + } + + return _graphCaches[graphName]; + } + + /// + /// Execute a Cypher query with parameters. + /// + /// A graph to perform the query on. + /// The Cypher query. + /// Parameters map. + /// Timeout (optional). + /// A result set. + /// + public ResultSet Query(string graphName, string query, IDictionary parameters, long? timeout = null) + { + var preparedQuery = PrepareQuery(query, parameters); + + return Query(graphName, preparedQuery, timeout); + } + + /// + /// Execute a Cypher query with parameters. + /// + /// A graph to perform the query on. + /// The Cypher query. + /// Parameters map. + /// Timeout (optional). + /// A result set. + /// + public async Task QueryAsync(string graphName, string query, IDictionary parameters, long? timeout = null) + { + var preparedQuery = PrepareQuery(query, parameters); + + return await QueryAsync(graphName, preparedQuery, timeout); + } + + /// + /// Execute a Cypher query. + /// + /// A graph to perform the query on. + /// The Cypher query. + /// Timeout (optional). + /// A result set. + /// + public ResultSet Query(string graphName, string query, long? timeout = null) + { + if(!_graphCaches.ContainsKey(graphName)) + { + _graphCaches.Add(graphName, new GraphCache(graphName, this)); + } + + var args = (timeout == null) ? new List(3) { graphName, query, CompactQueryFlag } + : new List(5) { graphName, query, CompactQueryFlag, GraphArgs.TIMEOUT, timeout }; + + return new ResultSet(_db.Execute(GRAPH.QUERY, args), _graphCaches[graphName]); + } + + /// + /// Execute a Cypher query. + /// + /// A graph to perform the query on. + /// The Cypher query. + /// Timeout (optional). + /// A result set. + /// + public async Task QueryAsync(string graphName, string query, long? timeout = null) + { + if(!_graphCaches.ContainsKey(graphName)) + { + _graphCaches.Add(graphName, new GraphCache(graphName, this)); + } + + var args = (timeout == null) ? new List(3) { graphName, query, CompactQueryFlag } + : new List(5) { graphName, query, CompactQueryFlag, GraphArgs.TIMEOUT, timeout }; + + return new ResultSet(await _db.ExecuteAsync(GRAPH.QUERY, args), _graphCaches[graphName]); + } + + /// + /// Execute a Cypher query with parameters. + /// + /// A graph to perform the query on. + /// The Cypher query. + /// Parameters map. + /// Timeout (optional). + /// A result set. + /// + public ResultSet RO_Query(string graphName, string query, IDictionary parameters, long? timeout = null) + { + var preparedQuery = PrepareQuery(query, parameters); + + return RO_Query(graphName, preparedQuery, timeout); + } + + /// + /// Execute a Cypher query with parameters. + /// + /// A graph to perform the query on. + /// The Cypher query. + /// Parameters map. + /// Timeout (optional). + /// A result set. + /// + public async Task RO_QueryAsync(string graphName, string query, IDictionary parameters, long? timeout = null) + { + var preparedQuery = PrepareQuery(query, parameters); + + return await RO_QueryAsync(graphName, preparedQuery, timeout); + } + + /// + /// Execute a Cypher query. + /// + /// A graph to perform the query on. + /// The Cypher query. + /// Timeout (optional). + /// A result set. + /// + public ResultSet RO_Query(string graphName, string query, long? timeout = null) + { + if(!_graphCaches.ContainsKey(graphName)) + { + _graphCaches.Add(graphName, new GraphCache(graphName, this)); + } + + var args = (timeout == null) ? new List(3) { graphName, query, CompactQueryFlag } + : new List(5) { graphName, query, CompactQueryFlag, GraphArgs.TIMEOUT, timeout }; + + return new ResultSet(_db.Execute(GRAPH.RO_QUERY, args), _graphCaches[graphName]); + } + + /// + /// Execute a Cypher query. + /// + /// A graph to perform the query on. + /// The Cypher query. + /// Timeout (optional). + /// A result set. + /// + public async Task RO_QueryAsync(string graphName, string query, long? timeout = null) + { + if(!_graphCaches.ContainsKey(graphName)) + { + _graphCaches.Add(graphName, new GraphCache(graphName, this)); + } + + var args = (timeout == null) ? new List(3) { graphName, query, CompactQueryFlag } + : new List(5) { graphName, query, CompactQueryFlag, GraphArgs.TIMEOUT, timeout }; + + return new ResultSet(await _db.ExecuteAsync(GRAPH.RO_QUERY, args), _graphCaches[graphName]); + } + + internal static readonly Dictionary> EmptyKwargsDictionary = + new Dictionary>(); + + // TODO: Check if needed + /// + /// Call a saved procedure. + /// + /// The graph containing the saved procedure. + /// The procedure name. + /// A result set. + public ResultSet CallProcedure(string graphName, string procedure) => + CallProcedure(graphName, procedure, Enumerable.Empty(), EmptyKwargsDictionary); + + /// + /// Call a saved procedure with parameters. + /// + /// The graph containing the saved procedure. + /// The procedure name. + /// A collection of positional arguments. + /// A result set. + public ResultSet CallProcedure(string graphName, string procedure, IEnumerable args) => + CallProcedure(graphName, procedure, args, EmptyKwargsDictionary); + + /// + /// Call a saved procedure with parameters. + /// + /// The graph containing the saved procedure. + /// The procedure name. + /// A collection of positional arguments. + /// A collection of keyword arguments. + /// A result set. + public ResultSet CallProcedure(string graphName, string procedure, IEnumerable args, Dictionary> 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 Query(graphName, queryBody.ToString()); + } + + /// + /// Delete an existing graph. + /// + /// The graph to delete. + /// A result set. + /// + public ResultSet Delete(string graphName) + { + var result = _db.Execute(GRAPH.DELETE, graphName); + + var processedResult = new ResultSet(result, _graphCaches[graphName]); + + _graphCaches.Remove(graphName); + + return processedResult; + } + + /// + /// Delete an existing graph. + /// + /// The graph to delete. + /// A result set. + /// + public async Task DeleteAsync(string graphName) + { + var result = await _db.ExecuteAsync(GRAPH.DELETE, graphName); + + var processedResult = new ResultSet(result, _graphCaches[graphName]); + + _graphCaches.Remove(graphName); + + return processedResult; + } + + // TODO: Check if this (CallProcedure) is needed + /// + /// Call a saved procedure against a read-only node. + /// + /// The graph containing the saved procedure. + /// The procedure name. + /// A result set. + public ResultSet CallProcedureReadOnly(string graphName, string procedure) => + CallProcedureReadOnly(graphName, procedure, Enumerable.Empty(), EmptyKwargsDictionary); + + /// + /// Call a saved procedure with parameters against a read-only node. + /// + /// The graph containing the saved procedure. + /// The procedure name. + /// A collection of positional arguments. + /// A result set. + public ResultSet CallProcedureReadOnly(string graphName, string procedure, IEnumerable args) => + CallProcedureReadOnly(graphName, procedure, args, EmptyKwargsDictionary); + + /// + /// Call a saved procedure with parameters against a read-only node. + /// + /// The graph containing the saved procedure. + /// The procedure name. + /// A collection of positional arguments. + /// A collection of keyword arguments. + /// A result set. + public ResultSet CallProcedureReadOnly(string graphName, string procedure, IEnumerable args, Dictionary> 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 RO_Query(graphName, queryBody.ToString()); + } + + /// + /// Constructs a query execution plan but does not run it. Inspect this execution plan to better understand how your + /// query will get executed. + /// + /// The graph name. + /// The query. + /// String representation of a query execution plan. + /// + public IReadOnlyList Explain(string graphName, string query) + { + return _db.Execute(GRAPH.EXPLAIN, graphName, query).ToStringList(); + } + + /// + /// Constructs a query execution plan but does not run it. Inspect this execution plan to better understand how your + /// query will get executed. + /// + /// The graph name. + /// The query. + /// String representation of a query execution plan. + /// + public async Task> ExplainAsync(string graphName, string query) + { + return (await _db.ExecuteAsync(GRAPH.EXPLAIN, graphName, query)).ToStringList(); + } + + /// + /// Executes a query and produces an execution plan augmented with metrics for each operation's execution. + /// + /// The graph name. + /// The query. + /// Timeout (optional). + /// String representation of a query execution plan, + /// with details on results produced by and time spent in each operation. + /// + public IReadOnlyList Profile(string graphName, string query, long? timeout = null) + { + var args = (timeout == null) ? new List(2) { graphName, query } + : new List(4) { graphName, query, GraphArgs.TIMEOUT, timeout }; + + return _db.Execute(GRAPH.PROFILE, args).ToStringList(); + } + + /// + /// Executes a query and produces an execution plan augmented with metrics for each operation's execution. + /// + /// The graph name. + /// The query. + /// Timeout (optional). + /// String representation of a query execution plan, + /// with details on results produced by and time spent in each operation. + /// + public async Task> ProfileAsync(string graphName, string query, long? timeout = null) + { + var args = (timeout == null) ? new List(2) { graphName, query } + : new List(4) { graphName, query, GraphArgs.TIMEOUT, timeout }; + + return (await _db.ExecuteAsync(GRAPH.PROFILE, args)).ToStringList(); + } + + /// + /// Lists all graph keys in the keyspace. + /// + /// List of all graph keys in the keyspace. + /// + public IReadOnlyList List() + { + return _db.Execute(GRAPH.LIST).ToStringList(); + } + + /// + /// Lists all graph keys in the keyspace. + /// + /// List of all graph keys in the keyspace. + /// + public async Task> ListAsync() + { + return (await _db.ExecuteAsync(GRAPH.LIST)).ToStringList(); + } + + /// + /// Set the value of a RedisGraph configuration parameter. + /// + /// The config name. + /// Value to set. + /// if executed correctly, error otherwise + /// + public bool ConfigSet(string configName, object value) + { + return _db.Execute(GRAPH.CONFIG, GraphArgs.SET, configName, value).OKtoBoolean(); + } + + /// + /// Set the value of a RedisGraph configuration parameter. + /// + /// The config name. + /// Value to set. + /// if executed correctly, error otherwise + /// + public async Task ConfigSetAsync(string configName, object value) + { + return (await _db.ExecuteAsync(GRAPH.CONFIG, GraphArgs.SET, configName, value)).OKtoBoolean(); + } + + /// + /// Set the value of a RedisGraph configuration parameter. + /// + /// The config name. + /// Dictionary of . + /// + public Dictionary ConfigGet(string configName) + { + return _db.Execute(GRAPH.CONFIG, GraphArgs.GET, configName).ToDictionary(); + } + + /// + /// Set the value of a RedisGraph configuration parameter. + /// + /// The config name. + /// Dictionary of . + /// + public async Task> ConfigGetAsync(string configName) + { + return (await _db.ExecuteAsync(GRAPH.CONFIG, GraphArgs.GET, configName)).ToDictionary(); + } + + /// + /// Returns a list containing up to 10 of the slowest queries issued against the given graph Name. + /// + /// The graph name. + /// Dictionary of . + /// + public List> Slowlog(string graphName) + { + var result = _db.Execute(GRAPH.SLOWLOG, graphName).ToArray(); + List> slowlog = new List>(result.Length); + foreach (var item in result) + { + slowlog.Add(item.ToStringList()); + } + + return slowlog; + } + + /// + /// Returns a list containing up to 10 of the slowest queries issued against the given graph Name. + /// + /// The graph name. + /// Dictionary of . + /// + public async Task>> SlowlogAsync(string graphName) + { + var result = (await _db.ExecuteAsync(GRAPH.SLOWLOG, graphName)).ToArray(); + List> slowlog = new List>(result.Length); + foreach (var item in result) + { + slowlog.Add(item.ToStringList()); + } + + return slowlog; + } + } +} \ No newline at end of file diff --git a/src/NRedisStack/Graph/Header.cs b/src/NRedisStack/Graph/Header.cs new file mode 100644 index 00000000..fe8915cb --- /dev/null +++ b/src/NRedisStack/Graph/Header.cs @@ -0,0 +1,72 @@ +using StackExchange.Redis; +namespace NRedisStack.Graph +{ + /// + /// Query response header interface. Represents the response schema (column names and types). + /// + public sealed class Header + { + /// + /// The expected column types. + /// + public enum ResultSetColumnTypes + { + UNKNOWN, + SCALAR, + NODE, + RELATION + } + + /// + /// Collection of the schema types present in the header. + /// + // [Obsolete("SchemaType is no longer supported after RedisGraph 2.1 and will always return COLUMN_SCALAR")] // TODO: it's correct? + public List SchemaTypes { get; } + + /// + /// Collection of the schema names present in the header. + /// + /// + public List SchemaNames { get; } + + internal Header(RedisResult result) + { + SchemaTypes = new List(); + SchemaNames = new List(); + + foreach(RedisResult[] tuple in (RedisResult[])result) + { + SchemaTypes.Add((ResultSetColumnTypes)(int)tuple[0]); + SchemaNames.Add((string)tuple[1]); + } + } + + public override bool Equals(object? obj) + { + if (obj == null) return this == null; + + if (this == obj) + { + return true; + } + + var header = obj as Header; + + if (header is null) + { + return false; + } + + return Object.Equals(SchemaTypes, header.SchemaTypes) + && Object.Equals(SchemaNames, header.SchemaNames); + } + + public override int GetHashCode() + { + return HashCode.Combine(SchemaTypes, SchemaNames); + } + + public override string ToString() => + $"Header{{schemaTypes=[{string.Join(", ", SchemaTypes)}], schemaNames=[{string.Join(", ", SchemaNames)}]}}"; + } +} \ No newline at end of file diff --git a/src/NRedisStack/Graph/IGraphCommands.cs b/src/NRedisStack/Graph/IGraphCommands.cs new file mode 100644 index 00000000..cf374eb8 --- /dev/null +++ b/src/NRedisStack/Graph/IGraphCommands.cs @@ -0,0 +1,270 @@ +using NRedisStack.Graph; +using StackExchange.Redis; + +namespace NRedisStack +{ + public interface IGraphCommands + { + /// + /// Execute a Cypher query with parameters. + /// + /// A graph to perform the query on. + /// The Cypher query. + /// Parameters map. + /// Timeout (optional). + /// A result set. + /// + ResultSet Query(string graphName, string query, IDictionary parameters, long? timeout = null); + + /// + /// Execute a Cypher query with parameters. + /// + /// A graph to perform the query on. + /// The Cypher query. + /// Parameters map. + /// Timeout (optional). + /// A result set. + /// + Task QueryAsync(string graphName, string query, IDictionary parameters, long? timeout = null); + + /// + /// Execute a Cypher query. + /// + /// A graph to perform the query on. + /// The Cypher query. + /// Timeout (optional). + /// A result set. + /// + ResultSet Query(string graphName, string query, long? timeout = null); + + /// + /// Execute a Cypher query. + /// + /// A graph to perform the query on. + /// The Cypher query. + /// Timeout (optional). + /// A result set. + /// + Task QueryAsync(string graphName, string query, long? timeout = null); + + /// + /// Execute a Cypher query with parameters. + /// + /// A graph to perform the query on. + /// The Cypher query. + /// Parameters map. + /// Timeout (optional). + /// A result set. + /// + ResultSet RO_Query(string graphName, string query, IDictionary parameters, long? timeout = null); + + /// + /// Execute a Cypher query with parameters. + /// + /// A graph to perform the query on. + /// The Cypher query. + /// Parameters map. + /// Timeout (optional). + /// A result set. + /// + Task RO_QueryAsync(string graphName, string query, IDictionary parameters, long? timeout = null); + + /// + /// Execute a Cypher query. + /// + /// A graph to perform the query on. + /// The Cypher query. + /// Timeout (optional). + /// A result set. + /// + ResultSet RO_Query(string graphName, string query, long? timeout = null); + + /// + /// Execute a Cypher query. + /// + /// A graph to perform the query on. + /// The Cypher query. + /// Timeout (optional). + /// A result set. + /// + Task RO_QueryAsync(string graphName, string query, long? timeout = null); + + // TODO: Check if needed + /// + /// Call a saved procedure. + /// + /// The graph containing the saved procedure. + /// The procedure name. + /// A result set. + ResultSet CallProcedure(string graphName, string procedure); + + /// + /// Call a saved procedure with parameters. + /// + /// The graph containing the saved procedure. + /// The procedure name. + /// A collection of positional arguments. + /// A result set. + ResultSet CallProcedure(string graphName, string procedure, IEnumerable args); + + /// + /// Call a saved procedure with parameters. + /// + /// The graph containing the saved procedure. + /// The procedure name. + /// A collection of positional arguments. + /// A collection of keyword arguments. + /// A result set. + ResultSet CallProcedure(string graphName, string procedure, IEnumerable args, Dictionary> kwargs); + + /// + /// Delete an existing graph. + /// + /// The graph to delete. + /// A result set. + /// + ResultSet Delete(string graphName); + + /// + /// Delete an existing graph. + /// + /// The graph to delete. + /// A result set. + /// + Task DeleteAsync(string graphName); + + // TODO: Check if this (CallProcedure) is needed + /// + /// Call a saved procedure against a read-only node. + /// + /// The graph containing the saved procedure. + /// The procedure name. + /// A result set. + ResultSet CallProcedureReadOnly(string graphName, string procedure); + + /// + /// Call a saved procedure with parameters against a read-only node. + /// + /// The graph containing the saved procedure. + /// The procedure name. + /// A collection of positional arguments. + /// A result set. + ResultSet CallProcedureReadOnly(string graphName, string procedure, IEnumerable args); + + /// + /// Call a saved procedure with parameters against a read-only node. + /// + /// The graph containing the saved procedure. + /// The procedure name. + /// A collection of positional arguments. + /// A collection of keyword arguments. + /// A result set. + ResultSet CallProcedureReadOnly(string graphName, string procedure, IEnumerable args, Dictionary> kwargs); + + /// + /// Constructs a query execution plan but does not run it. Inspect this execution plan to better understand how your + /// query will get executed. + /// + /// The graph name. + /// The query. + /// String representation of a query execution plan. + /// + IReadOnlyList Explain(string graphName, string query); + + /// + /// Constructs a query execution plan but does not run it. Inspect this execution plan to better understand how your + /// query will get executed. + /// + /// The graph name. + /// The query. + /// String representation of a query execution plan. + /// + Task> ExplainAsync(string graphName, string query); + + /// + /// Executes a query and produces an execution plan augmented with metrics for each operation's execution. + /// + /// The graph name. + /// The query. + /// Timeout (optional). + /// String representation of a query execution plan, + /// with details on results produced by and time spent in each operation. + /// + IReadOnlyList Profile(string graphName, string query, long? timeout = null); + + /// + /// Executes a query and produces an execution plan augmented with metrics for each operation's execution. + /// + /// The graph name. + /// The query. + /// Timeout (optional). + /// String representation of a query execution plan, + /// with details on results produced by and time spent in each operation. + /// + Task> ProfileAsync(string graphName, string query, long? timeout = null); + + /// + /// Lists all graph keys in the keyspace. + /// + /// List of all graph keys in the keyspace. + /// + IReadOnlyList List(); + + /// + /// Lists all graph keys in the keyspace. + /// + /// List of all graph keys in the keyspace. + /// + Task> ListAsync(); + + /// + /// Set the value of a RedisGraph configuration parameter. + /// + /// The config name. + /// Value to set. + /// if executed correctly, error otherwise + /// + bool ConfigSet(string configName, object value); + + /// + /// Set the value of a RedisGraph configuration parameter. + /// + /// The config name. + /// Value to set. + /// if executed correctly, error otherwise + /// + Task ConfigSetAsync(string configName, object value); + + /// + /// Set the value of a RedisGraph configuration parameter. + /// + /// The config name. + /// Dictionary of . + /// + Dictionary ConfigGet(string configName); + + /// + /// Set the value of a RedisGraph configuration parameter. + /// + /// The config name. + /// Dictionary of . + /// + Task> ConfigGetAsync(string configName); + + /// + /// Returns a list containing up to 10 of the slowest queries issued against the given graph Name. + /// + /// The graph name. + /// Dictionary of . + /// + List> Slowlog(string graphName); + + /// + /// Returns a list containing up to 10 of the slowest queries issued against the given graph Name. + /// + /// The graph name. + /// Dictionary of . + /// + Task>> SlowlogAsync(string graphName); + } +} \ No newline at end of file diff --git a/src/NRedisStack/Graph/Literals/CommandArgs.cs b/src/NRedisStack/Graph/Literals/CommandArgs.cs new file mode 100644 index 00000000..d2a18730 --- /dev/null +++ b/src/NRedisStack/Graph/Literals/CommandArgs.cs @@ -0,0 +1,9 @@ +namespace NRedisStack.Literals +{ + internal class GraphArgs + { + public const string TIMEOUT = "TIMEOUT"; + public const string SET = "SET"; + public const string GET = "GET"; + } +} \ No newline at end of file diff --git a/src/NRedisStack/Graph/Literals/Commands.cs b/src/NRedisStack/Graph/Literals/Commands.cs new file mode 100644 index 00000000..10c510a7 --- /dev/null +++ b/src/NRedisStack/Graph/Literals/Commands.cs @@ -0,0 +1,16 @@ +namespace NRedisStack.Literals +{ + internal class GRAPH + { + public const string QUERY = "GRAPH.QUERY"; + public const string RO_QUERY = "GRAPH.RO_QUERY"; + public const string DELETE = "GRAPH.DELETE"; + public const string EXPLAIN = "GRAPH.EXPLAIN"; + public const string PROFILE = "GRAPH.PROFILE"; + public const string SLOWLOG = "GRAPH.SLOWLOG"; + public const string CONFIG = "GRAPH.CONFIG"; + public const string CONFIG_SET = "GRAPH.CONFIG SET"; + public const string CONFIG_GET = "GRAPH.CONFIG GET"; + public const string LIST = "GRAPH.LIST"; + } +} \ No newline at end of file diff --git a/src/NRedisStack/Graph/Point.cs b/src/NRedisStack/Graph/Point.cs new file mode 100644 index 00000000..4bb562fb --- /dev/null +++ b/src/NRedisStack/Graph/Point.cs @@ -0,0 +1,48 @@ +namespace NRedisStack.Graph +{ + public class Point + { + private static readonly double EPSILON = 1e-5; + + private double latitude { get; } + private double longitude { get; } + + public Point(double latitude, double longitude) + { + this.latitude = latitude; + this.longitude = longitude; + } + + public Point(List values) + { + if (values == null || values.Count != 2) + { + throw new ArgumentOutOfRangeException("Point requires two doubles."); + } + this.latitude = values[0]; + this.longitude = values[1]; + } + + public override bool Equals(object? obj) + { + if (obj == null) return this == null; + + if (this == obj) return true; + if (!(obj.GetType() == typeof(Point))) return false; + Point o = (Point)obj; + return Math.Abs(latitude - o.latitude) < EPSILON && + Math.Abs(longitude - o.longitude) < EPSILON; + } + + public override int GetHashCode() + { + return latitude.GetHashCode() ^ longitude.GetHashCode(); + } + + + public override string ToString() + { + return "Point{latitude=" + latitude + ", longitude=" + longitude + "}"; + } + } +} \ No newline at end of file diff --git a/src/NRedisStack/Graph/Record.cs b/src/NRedisStack/Graph/Record.cs new file mode 100644 index 00000000..be2346ec --- /dev/null +++ b/src/NRedisStack/Graph/Record.cs @@ -0,0 +1,110 @@ +using System.Text; + +namespace NRedisStack.Graph +{ + /// + /// Container for RedisGraph result values. + /// + public sealed class Record + { + public List Header { get; } + public List Values { get; } + + internal Record(List header, List values) + { + Header = header; + Values = values; + } + + /// + /// Get a value by index. + /// + /// The index of the value you want to get. + /// The type of the value at the index that you want to get. + /// The value at the index that you specified. + public T GetValue(int index) => (T)Values[index]; + + /// + /// Get a value by key name. + /// + /// The key of the value you want to get. + /// The type of the value that corresponds to the key that you specified. + /// The value that corresponds to the key that you specified. + public T GetValue(string key) => (T)Values[Header.IndexOf(key)]; + + /// + /// Gets the string representation of a value at the given index. + /// + /// The index of the value that you want to get. + /// The string value at the index that you specified. + public string GetString(int index) => Values[index].ToString(); + + /// + /// Gets the string representation of a value by key. + /// + /// The key of the value that you want to get. + /// The string value at the key that you specified. + public string GetString(string key) => Values[Header.IndexOf(key)].ToString(); + + /// + /// Does the key exist in the record? + /// + /// The key to check. + /// + public bool ContainsKey(string key) => Header.Contains(key); + + /// + /// How many keys are in the record? + /// + public int Size => Header.Count; + + public override bool Equals(object? obj) + { + if (obj == null) return this == null; + + if (this == obj) + { + return true; + } + + if (!(obj is Record that)) + { + return false; + } + + return Enumerable.SequenceEqual(Header, that.Header) && Enumerable.SequenceEqual(Values, that.Values); + } + + /// + /// Overridden method that generates a hash code based on the hash codes of the keys and values. + /// + /// + public override int GetHashCode() + { + unchecked + { + int hash = 17; + + hash = hash * 31 + Header.GetHashCode(); + hash = hash * 31 + Values.GetHashCode(); + + return hash; + } + } + + /// + /// Overridden method that emits a string of representing all of the values in a record. + /// + /// + public override string ToString() + { + var sb = new StringBuilder(); + + sb.Append("Record{values="); + sb.Append(string.Join(",", Values)); + sb.Append('}'); + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/src/NRedisStack/Graph/RedisGraphUtilities.cs b/src/NRedisStack/Graph/RedisGraphUtilities.cs new file mode 100644 index 00000000..5f10cd74 --- /dev/null +++ b/src/NRedisStack/Graph/RedisGraphUtilities.cs @@ -0,0 +1,139 @@ +using System.Text; +using System.Collections; +using System.Globalization; + +namespace NRedisStack.Graph +{ + internal static class RedisGraphUtilities + { + internal static string PrepareQuery(string query, IDictionary parms) + { + var preparedQuery = new StringBuilder(); + + preparedQuery.Append("CYPHER "); + + foreach (var param in parms) + { + preparedQuery.Append($"{param.Key}={ValueToString(param.Value)} "); + } + + preparedQuery.Append(query); + + return preparedQuery.ToString(); + } + + public static string ValueToStringNoQuotes(object value) + { + if (value == null) + { + return "null"; + } + + if (value is IConvertible floatValue) + { + return ConvertibleToString(floatValue); + } + + return value.ToString(); + } + + public static string ValueToString(object value) + { + if (value == null) + { + return "null"; + } + + if (value is string stringValue) + { + return QuoteString(stringValue); + } + + if (value is char charValue) + { + return QuoteCharacter(charValue); + } + + if (value.GetType().IsArray) + { + if (value is IEnumerable arrayValue) + { + var values = new List(); + + foreach (var v in arrayValue) + { + values.Add(v); + } + + return ArrayToString(values.ToArray()); + } + } + + if ((value is IList valueList) && value.GetType().IsGenericType) + { + var objectValueList = new List(); + + foreach (var val in valueList) + { + objectValueList.Add((object) val); + } + + return ArrayToString(objectValueList.ToArray()); + } + + if (value is bool boolValue) + { + return boolValue.ToString().ToLowerInvariant(); + } + + if (value is IConvertible floatValue) + { + return ConvertibleToString(floatValue); + } + + return value.ToString(); + } + + private static string ConvertibleToString(IConvertible floatValue) + { + return floatValue.ToString(CultureInfo.InvariantCulture); + } + + private static string ArrayToString(object[] array) + { + var arrayElements = array.Select(x => + { + if (x.GetType().IsArray) + { + return ArrayToString((object[]) x); + } + else + { + return ValueToString(x); + } + }); + + var arrayToString = new StringBuilder(); + + arrayToString.Append('['); + arrayToString.Append(string.Join(", ", arrayElements)); + arrayToString.Append(']'); + + return arrayToString.ToString(); + } + + internal static string QuoteCharacter(char character) => + $"\"{character}\""; + + internal static string QuoteString(string unquotedString) + { + var quotedString = new StringBuilder(unquotedString.Length + 12); + + quotedString.Append('"'); + quotedString.Append(unquotedString.Replace("\"", "\\\"")); + quotedString.Append('"'); + + return quotedString.ToString(); + } + } +} \ No newline at end of file diff --git a/src/NRedisStack/Graph/ResultSet.cs b/src/NRedisStack/Graph/ResultSet.cs new file mode 100644 index 00000000..334f0a8a --- /dev/null +++ b/src/NRedisStack/Graph/ResultSet.cs @@ -0,0 +1,304 @@ +using System.Collections; +using NRedisStack.Graph.DataTypes; +using StackExchange.Redis; + +namespace NRedisStack.Graph +{ + /// + /// Represents the result from a RedisGraph query. + /// + public sealed class ResultSet : IReadOnlyCollection + { + internal enum ResultSetScalarType + { + VALUE_UNKNOWN, + VALUE_NULL, + VALUE_STRING, + VALUE_INT64, + VALUE_BOOLEAN, + VALUE_DOUBLE, + VALUE_ARRAY, + VALUE_EDGE, + VALUE_NODE, + VALUE_PATH, + VALUE_MAP, + VALUE_POINT + } + + private readonly RedisResult[] _rawResults; + private readonly GraphCache _graphCache; + + public Statistics Statistics { get; } + public Header Header { get; } + public int Count { get; } + + internal ResultSet(RedisResult result, GraphCache graphCache) + { + if (result.Type == ResultType.MultiBulk) + { + var resultArray = (RedisResult[])result; + + ScanForErrors(resultArray); + + _graphCache = graphCache; + + if (resultArray.Length == 3) + { + Header = new Header(resultArray[0]); + Statistics = ParseStatistics(resultArray[2]); + + _rawResults = (RedisResult[])resultArray[1]; + + Count = _rawResults.Length; + } + else + { + Statistics = ParseStatistics(resultArray[resultArray.Length - 1]); + Count = 0; + } + } + else + { + if (result.Type == ResultType.Error) + { + throw new RedisServerException(result.ToString()); + } + + Statistics = ParseStatistics(result); + Count = 0; + } + } + + /// + /// Get the enumerator for this result set. + /// + /// + public IEnumerator GetEnumerator() => RecordIterator().GetEnumerator(); + + /// + /// Get the enumerator for this result set. + /// + /// + IEnumerator IEnumerable.GetEnumerator() => RecordIterator().GetEnumerator(); + + private IEnumerable RecordIterator() + { + if (_rawResults == default) + { + yield break; + } + else + { + foreach (RedisResult[] row in _rawResults) + { + var parsedRow = new List(row.Length); + + for (int i = 0; i < row.Length; i++) + { + var obj = (RedisResult[])row[i]; + var objType = Header.SchemaTypes[i]; + + switch (objType) + { + case Header.ResultSetColumnTypes.NODE: + parsedRow.Add(DeserializeNode(obj)); + break; + case Header.ResultSetColumnTypes.RELATION: + parsedRow.Add(DeserializeEdge(obj)); + break; + case Header.ResultSetColumnTypes.SCALAR: + parsedRow.Add(DeserializeScalar(obj)); + break; + default: + parsedRow.Add(null); + break; + } + } + + yield return new Record(Header.SchemaNames, parsedRow); + } + + yield break; + } + } + + private Node DeserializeNode(RedisResult[] rawNodeData) + { + var node = new Node(); + + DeserializeGraphEntityId(node, rawNodeData[0]); + + var labelIndices = (int[])rawNodeData[1]; + + foreach (var labelIndex in labelIndices) + { + var label = _graphCache.GetLabel(labelIndex); + + node.Labels.Add(label); + } + + DeserializeGraphEntityProperties(node, (RedisResult[])rawNodeData[2]); + + return node; + } + + private Edge DeserializeEdge(RedisResult[] rawEdgeData) + { + var edge = new Edge(); + + DeserializeGraphEntityId(edge, rawEdgeData[0]); + + edge.RelationshipType = _graphCache.GetRelationshipType((int)rawEdgeData[1]); + edge.Source = (int)rawEdgeData[2]; + edge.Destination = (int)rawEdgeData[3]; + + DeserializeGraphEntityProperties(edge, (RedisResult[])rawEdgeData[4]); + + return edge; + } + + private object DeserializeScalar(RedisResult[] rawScalarData) + { + var type = GetValueTypeFromObject(rawScalarData[0]); + + switch (type) + { + case ResultSetScalarType.VALUE_NULL: + return null; + case ResultSetScalarType.VALUE_BOOLEAN: + return bool.Parse((string)rawScalarData[1]); + case ResultSetScalarType.VALUE_DOUBLE: + return (double)rawScalarData[1]; + case ResultSetScalarType.VALUE_INT64: + return (long)rawScalarData[1]; + case ResultSetScalarType.VALUE_STRING: + return (string)rawScalarData[1]; + case ResultSetScalarType.VALUE_ARRAY: + return DeserializeArray((RedisResult[])rawScalarData[1]); + case ResultSetScalarType.VALUE_NODE: + return DeserializeNode((RedisResult[])rawScalarData[1]); + case ResultSetScalarType.VALUE_EDGE: + return DeserializeEdge((RedisResult[])rawScalarData[1]); + case ResultSetScalarType.VALUE_PATH: + return DeserializePath((RedisResult[])rawScalarData[1]); + case ResultSetScalarType.VALUE_MAP: + return DeserializeDictionary(rawScalarData[1]); + case ResultSetScalarType.VALUE_POINT: + return DeserializePoint((RedisResult[])rawScalarData[1]); + case ResultSetScalarType.VALUE_UNKNOWN: + default: + return (object)rawScalarData[1]; + } + } + + private static void DeserializeGraphEntityId(GraphEntity graphEntity, RedisResult rawEntityId) => + graphEntity.Id = (int)rawEntityId; + + private void DeserializeGraphEntityProperties(GraphEntity graphEntity, RedisResult[] rawProperties) + { + foreach (RedisResult[] rawProperty in rawProperties) + { + var Key = _graphCache.GetPropertyName((int)rawProperty[0]); + var Value = DeserializeScalar(rawProperty.Skip(1).ToArray()); + + graphEntity.PropertyMap.Add(Key, Value); + + } + } + + private object[] DeserializeArray(RedisResult[] serializedArray) + { + var result = new object[serializedArray.Length]; + + for (var i = 0; i < serializedArray.Length; i++) + { + result[i] = DeserializeScalar((RedisResult[])serializedArray[i]); + } + + return result; + } + + private DataTypes.Path DeserializePath(RedisResult[] rawPath) + { + var deserializedNodes = (object[])DeserializeScalar((RedisResult[])rawPath[0]); + var nodes = Array.ConvertAll(deserializedNodes, n => (Node)n); + + var deserializedEdges = (object[])DeserializeScalar((RedisResult[])rawPath[1]); + var edges = Array.ConvertAll(deserializedEdges, p => (Edge)p); + + return new DataTypes.Path(nodes, edges); + } + + private object DeserializePoint(RedisResult[] rawPath) // Should return Point? + { + if (null == rawPath) + { + return null; + } + // List values = (List)rawPath; + List doubles = new List(rawPath.Count()); + foreach (var value in rawPath) + { + doubles.Add(((double)value)); + } + return new Point(doubles); + } + + // @SuppressWarnings("unchecked") + private Dictionary DeserializeDictionary(RedisResult rawPath) + { + RedisResult[] keyTypeValueEntries = (RedisResult[])rawPath; + + int size = keyTypeValueEntries.Length; + Dictionary dict = new Dictionary(size / 2); // set the capacity to half of the list + + for (int i = 0; i < size; i += 2) + { + string key = keyTypeValueEntries[i].ToString(); + object value = DeserializeScalar((RedisResult[])keyTypeValueEntries[i+1]); + dict.Add(key, value); + } + return dict; + } + + private static ResultSetScalarType GetValueTypeFromObject(RedisResult rawScalarType) => + (ResultSetScalarType)(int)rawScalarType; + + private static void ScanForErrors(RedisResult[] results) + { + foreach (var result in results) + { + if (result.Type == ResultType.Error) + { + throw new RedisServerException(result.ToString()); + } + } + } + + private Statistics ParseStatistics(RedisResult result) + { + RedisResult[] statistics; + + if (result.Type == ResultType.MultiBulk) + { + statistics = (RedisResult[])result; + } + else + { + statistics = new[] { result }; + } + + return new Statistics( + ((RedisResult[])statistics).Select(x => + { + var s = ((string)x).Split(':'); + + return new + { + Label = s[0].Trim(), + Value = s[1].Trim() + }; + }).ToDictionary(k => k.Label, v => v.Value)); + } + } +} \ No newline at end of file diff --git a/src/NRedisStack/Graph/Statistics.cs b/src/NRedisStack/Graph/Statistics.cs new file mode 100644 index 00000000..0991998c --- /dev/null +++ b/src/NRedisStack/Graph/Statistics.cs @@ -0,0 +1,114 @@ +namespace NRedisStack.Graph +{ + /// + /// Query result statistics are encapsulated by this class. + /// + public sealed class Statistics + { + private IDictionary _statistics; + + internal Statistics(Dictionary statistics) + { + _statistics = statistics; + + NodesCreated = GetIntValue("Nodes created"); + NodesDeleted = GetIntValue("Nodes deleted"); + IndicesAdded = GetIntValue("Indices added"); + IndicesCreated = GetIntValue("Indices created"); + IndicesDeleted = GetIntValue("Indices deleted"); + LabelsAdded = GetIntValue("Labels added"); + RelationshipsDeleted = GetIntValue("Relationships deleted"); + RelationshipsCreated = GetIntValue("Relationships created"); + PropertiesSet = GetIntValue("Properties set"); + QueryInternalExecutionTime = GetStringValue("Query internal execution time"); + GraphRemovedInternalExecutionTime = GetStringValue("Graph removed, internal execution time"); + CachedExecution = (GetIntValue("Cached execution") == 1); + + } + + /// + /// Retrieves the relevant statistic. + /// + /// The requested statistic label. + /// A string representation of the specific statistic or null + public string? GetStringValue(string label) => + _statistics.TryGetValue(label, out string? value) ? value : null; + + + private int GetIntValue(string label) + { + var value = GetStringValue(label); + return int.TryParse(value, out var result) ? result : 0; + } + + /// + /// Number of nodes created. + /// + /// + public int NodesCreated { get; } + + /// + /// Number of nodes deleted. + /// + /// + public int NodesDeleted { get; } + + /// + /// Number of indices added. + /// + /// + public int IndicesAdded { get; } + + /// + /// Number of indices created. + /// + /// + public int IndicesCreated { get; } + + /// + /// Number of indices deleted. + /// + public int IndicesDeleted { get; } + + /// + /// Number of labels added. + /// + /// + public int LabelsAdded { get; } + + /// + /// Number of relationships deleted. + /// + /// + public int RelationshipsDeleted { get; } + + /// + /// Number of relationships created. + /// + /// + public int RelationshipsCreated { get; } + + /// + /// Number of properties set. + /// + /// + public int PropertiesSet { get; } + + /// + /// How long the query took to execute. + /// + /// + public string QueryInternalExecutionTime { get; } + + /// + /// How long it took to remove a graph. + /// + /// + public string GraphRemovedInternalExecutionTime { get; } + + /// + /// The execution plan was cached on RedisGraph. + /// + public bool CachedExecution { get; } + } +} \ No newline at end of file diff --git a/src/NRedisStack/ModulPrefixes.cs b/src/NRedisStack/ModulPrefixes.cs index 09e506bf..c9db8358 100644 --- a/src/NRedisStack/ModulPrefixes.cs +++ b/src/NRedisStack/ModulPrefixes.cs @@ -10,6 +10,8 @@ public static class ModulPrefixes static public ICmsCommands CMS(this IDatabase db) => new CmsCommands(db); + static public IGraphCommands GRAPH(this IDatabase db) => new GraphCommands(db); + static public ITopKCommands TOPK(this IDatabase db) => new TopKCommands(db); static public ITdigestCommands TDIGEST(this IDatabase db) => new TdigestCommands(db); diff --git a/src/NRedisStack/ResponseParser.cs b/src/NRedisStack/ResponseParser.cs index 6f161b0e..105fb9d6 100644 --- a/src/NRedisStack/ResponseParser.cs +++ b/src/NRedisStack/ResponseParser.cs @@ -554,7 +554,7 @@ public static TimeSeriesChunck ToTimeSeriesChunk(this RedisResult result) } - public static IReadOnlyList ToStringArray(this RedisResult result) + public static List ToStringList(this RedisResult result) { RedisResult[] redisResults = result.ToArray(); diff --git a/src/NRedisStack/Search/Reducer.cs b/src/NRedisStack/Search/Reducer.cs index 68294250..212de35c 100644 --- a/src/NRedisStack/Search/Reducer.cs +++ b/src/NRedisStack/Search/Reducer.cs @@ -48,9 +48,9 @@ protected virtual void AddOwnArgs(List args) // return this; // } - // public final Reducer as(string alias) { - // return setAlias(alias); - // } + // public final Reducer as(string alias) { + // return setAlias(alias); + // } public Reducer As(string alias) { diff --git a/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesLabel.cs b/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesLabel.cs index 1629c9da..fe6996ff 100644 --- a/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesLabel.cs +++ b/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesLabel.cs @@ -29,7 +29,7 @@ public class TimeSeriesLabel /// /// Object to compare /// If two TimeSeriesLabel objects are equal - public override bool Equals(object obj) => + public override bool Equals(object? obj) => obj is TimeSeriesLabel label && Key == label.Key && Value == label.Value; diff --git a/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesRule.cs b/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesRule.cs index 04e7450e..c14b9fb1 100644 --- a/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesRule.cs +++ b/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesRule.cs @@ -38,7 +38,7 @@ public TimeSeriesRule(string destKey, long timeBucket, TsAggregation aggregation /// /// Object to compare /// If two TimeSeriesRule objects are equal - public override bool Equals(object obj) => + public override bool Equals(object? obj) => obj is TimeSeriesRule rule && DestKey == rule.DestKey && TimeBucket == rule.TimeBucket && diff --git a/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesTuple.cs b/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesTuple.cs index e11e298b..715bbc93 100644 --- a/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesTuple.cs +++ b/src/NRedisStack/TimeSeries/DataTypes/TimeSeriesTuple.cs @@ -29,7 +29,7 @@ public class TimeSeriesTuple /// /// Object to compare /// If two TimeSeriesTuple objects are equal - public override bool Equals(object obj) => + public override bool Equals(object? obj) => obj is TimeSeriesTuple tuple && EqualityComparer.Default.Equals(Time, tuple.Time) && Val == tuple.Val; diff --git a/src/NRedisStack/TimeSeries/DataTypes/TimeStamp.cs b/src/NRedisStack/TimeSeries/DataTypes/TimeStamp.cs index 69b17ba5..8d690bc0 100644 --- a/src/NRedisStack/TimeSeries/DataTypes/TimeStamp.cs +++ b/src/NRedisStack/TimeSeries/DataTypes/TimeStamp.cs @@ -86,7 +86,7 @@ public static implicit operator long(TimeStamp ts) => /// /// Object to compare /// If two TimeStamp objects are equal - public override bool Equals(object obj) => + public override bool Equals(object? obj) => obj is TimeStamp stamp && EqualityComparer.Default.Equals(Value, stamp.Value); /// diff --git a/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs b/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs index 19f17d94..c476037c 100644 --- a/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs +++ b/src/NRedisStack/TimeSeries/TimeSeriesCommands.cs @@ -394,14 +394,14 @@ public async Task InfoAsync(string key, bool debug = fals public IReadOnlyList QueryIndex(IReadOnlyCollection filter) { var args = new List(filter); - return _db.Execute(TS.QUERYINDEX, args).ToStringArray(); + return _db.Execute(TS.QUERYINDEX, args).ToStringList(); } /// public async Task> QueryIndexAsync(IReadOnlyCollection filter) { var args = new List(filter); - return (await _db.ExecuteAsync(TS.QUERYINDEX, args)).ToStringArray(); + return (await _db.ExecuteAsync(TS.QUERYINDEX, args)).ToStringList(); } #endregion diff --git a/tests/NRedisStack.Tests/Graph/GraphTests.cs b/tests/NRedisStack.Tests/Graph/GraphTests.cs new file mode 100644 index 00000000..0a402478 --- /dev/null +++ b/tests/NRedisStack.Tests/Graph/GraphTests.cs @@ -0,0 +1,1932 @@ +using Xunit; +using StackExchange.Redis; +using NRedisStack.RedisStackCommands; +using Moq; +using NRedisStack.Graph; +using NRedisStack.Graph.DataTypes; + +namespace NRedisStack.Tests.Graph; + +public class GraphTests : AbstractNRedisStackTest, IDisposable +{ + Mock _mock = new Mock(); + private readonly string key = "GRAPH_TESTS"; + public GraphTests(RedisFixture redisFixture) : base(redisFixture) { } + + public void Dispose() + { + redisFixture.Redis.GetDatabase().KeyDelete(key); + } + + #region SyncTests + + [Fact] + public void TestReserveBasic() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + } + + [Fact] + public void TestCreateNode() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + // Create a node + ResultSet resultSet = graph.Query("social", "CREATE ({name:'roi',age:32})"); + + Statistics stats = resultSet.Statistics; + Assert.Equal(1, stats.NodesCreated); + Assert.Equal(0, stats.NodesDeleted); + Assert.Equal(0, stats.RelationshipsCreated); + Assert.Equal(0, stats.RelationshipsDeleted); + Assert.Equal(2, stats.PropertiesSet); + Assert.NotNull(stats.QueryInternalExecutionTime); + + Assert.Equal(0, resultSet.Count); + } + + [Fact] + public void TestCreateLabeledNode() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + // Create a node with a label + ResultSet resultSet = graph.Query("social", "CREATE (:human{name:'danny',age:12})"); + + Statistics stats = resultSet.Statistics; + // Assert.Equal("1", stats.getstringValue(Label.NODES_CREATED)); + Assert.Equal(1, stats.NodesCreated); + // Assert.Equal("2", stats.getstringValue(Label.PROPERTIES_SET)); + Assert.Equal(2, stats.PropertiesSet); + // Assert.NotNull(stats.getstringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); + Assert.NotNull(stats.QueryInternalExecutionTime); + + Assert.Equal(0, resultSet.Count); + // Assert.False(resultSet..iterator().MoveNext()); + } + + [Fact] + public void TestConnectNodes() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + // Create both source and destination nodes + Assert.NotNull(graph.Query("social", "CREATE (:person{name:'roi',age:32})")); + Assert.NotNull(graph.Query("social", "CREATE (:person{name:'amit',age:30})")); + + // Connect source and destination nodes. + ResultSet resultSet = graph.Query("social", + "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(b)"); + + Statistics stats = resultSet.Statistics; + // Assert.Null(stats.getstringValue(Label.NODES_CREATED)); + Assert.Equal(0, stats.NodesCreated); + Assert.Equal(1, stats.RelationshipsCreated); + Assert.Equal(0, stats.RelationshipsDeleted); + // Assert.Null(stats.getstringValue(Label.PROPERTIES_SET)); + Assert.Equal(0, stats.PropertiesSet); + // Assert.NotNull(stats.getstringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); + Assert.NotNull(stats.QueryInternalExecutionTime); + + Assert.Equal(0, resultSet.Count); + } + + [Fact] + public void TestDeleteNodes() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + Assert.NotNull(graph.Query("social", "CREATE (:person{name:'roi',age:32})")); + Assert.NotNull(graph.Query("social", "CREATE (:person{name:'amit',age:30})")); + + ResultSet deleteResult = graph.Query("social", "MATCH (a:person) WHERE (a.name = 'roi') DELETE a"); + + Statistics delStats = deleteResult.Statistics; + // Assert.Null(delStats.getstringValue(Label.NODES_CREATED)); + Assert.Equal(0, delStats.NodesCreated); + Assert.Equal(1, delStats.NodesDeleted); + // Assert.Null(delStats.getstringValue(Label.RELATIONSHIPS_CREATED)); + Assert.Equal(0, delStats.RelationshipsCreated); + // Assert.Null(delStats.getstringValue(Label.RELATIONSHIPS_DELETED)); + Assert.Equal(0, delStats.RelationshipsDeleted); + // Assert.Null(delStats.getstringValue(Label.PROPERTIES_SET)); + Assert.Equal(0, delStats.PropertiesSet); + // Assert.NotNull(delStats.getstringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); + Assert.NotNull(delStats.QueryInternalExecutionTime); + Assert.Equal(0, deleteResult.Count); + // Assert.False(deleteResult.iterator().MoveNext()); + + Assert.NotNull(graph.Query("social", "CREATE (:person{name:'roi',age:32})")); + Assert.NotNull(graph.Query("social", + "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(a)")); + + deleteResult = graph.Query("social", "MATCH (a:person) WHERE (a.name = 'roi') DELETE a"); + + // Assert.Null(delStats.getstringValue(Label.NODES_CREATED)); + Assert.Equal(0, delStats.NodesCreated); + Assert.Equal(1, delStats.NodesDeleted); + // Assert.Null(delStats.getstringValue(Label.RELATIONSHIPS_CREATED)); + Assert.Equal(0, delStats.RelationshipsCreated); + // Assert.Equal(1, delStats.RelationshipsDeleted); + Assert.Equal(0, delStats.RelationshipsDeleted); + // Assert.Null(delStats.getstringValue(Label.PROPERTIES_SET)); + Assert.Equal(0, delStats.PropertiesSet); + // Assert.NotNull(delStats.getstringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); + Assert.NotNull(delStats.QueryInternalExecutionTime); + Assert.Equal(0, deleteResult.Count); + // Assert.False(deleteResult.iterator().MoveNext()); + } + + [Fact] + public void TestDeleteRelationship() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + Assert.NotNull(graph.Query("social", "CREATE (:person{name:'roi',age:32})")); + Assert.NotNull(graph.Query("social", "CREATE (:person{name:'amit',age:30})")); + Assert.NotNull(graph.Query("social", + "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(a)")); + + ResultSet deleteResult = graph.Query("social", + "MATCH (a:person)-[e]->() WHERE (a.name = 'roi') DELETE e"); + + Statistics delStats = deleteResult.Statistics; + // Assert.Null(delStats.getstringValue(Label.NODES_CREATED)); + Assert.Equal(0, delStats.NodesCreated); + // Assert.Null(delStats.getstringValue(Label.NODES_DELETED)); + Assert.Equal(0, delStats.NodesDeleted); + // Assert.Null(delStats.getstringValue(Label.RELATIONSHIPS_CREATED)); + Assert.Equal(0, delStats.RelationshipsCreated); + Assert.Equal(1, delStats.RelationshipsDeleted); + // Assert.Null(delStats.getstringValue(Label.PROPERTIES_SET)); + Assert.Equal(0, delStats.PropertiesSet); + // Assert.NotNull(delStats.getstringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); + Assert.NotNull(delStats.QueryInternalExecutionTime); + Assert.Equal(0, deleteResult.Count); + // Assert.False(deleteResult.iterator().MoveNext()); + } + + [Fact] + public void TestIndex() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + // Create both source and destination nodes + Assert.NotNull(graph.Query("social", "CREATE (:person{name:'roi',age:32})")); + + ResultSet createIndexResult = graph.Query("social", "CREATE INDEX ON :person(age)"); + Assert.Empty(createIndexResult); + Assert.Equal(1, createIndexResult.Statistics.IndicesCreated); + + // since RediSearch as index, those action are allowed + ResultSet createNonExistingIndexResult = graph.Query("social", "CREATE INDEX ON :person(age1)"); + Assert.Empty(createNonExistingIndexResult); + Assert.Equal(1, createNonExistingIndexResult.Statistics.IndicesCreated); + + ResultSet createExistingIndexResult = graph.Query("social", "CREATE INDEX ON :person(age)"); + Assert.Empty(createExistingIndexResult); + Assert.Equal(0, createExistingIndexResult.Statistics.IndicesCreated); + + ResultSet deleteExistingIndexResult = graph.Query("social", "DROP INDEX ON :person(age)"); + Assert.Empty(deleteExistingIndexResult); + Assert.Equal(1, deleteExistingIndexResult.Statistics.IndicesDeleted); + } + + [Fact] + public void TestHeader() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + + Assert.NotNull(graph.Query("social", "CREATE (:person{name:'roi',age:32})")); + Assert.NotNull(graph.Query("social", "CREATE (:person{name:'amit',age:30})")); + Assert.NotNull(graph.Query("social", + "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(a)")); + + ResultSet queryResult = graph.Query("social", "MATCH (a:person)-[r:knows]->(b:person) RETURN a,r, a.age"); + + Header header = queryResult.Header; + Assert.NotNull(header); + Assert.Equal("Header{" + // + "schemaTypes=[COLUMN_SCALAR, COLUMN_SCALAR, COLUMN_SCALAR], " + + "schemaTypes=[SCALAR, SCALAR, SCALAR], " + + "schemaNames=[a, r, a.age]}", header.ToString()); + // Assert.Assert.Equal(-1901778507, header.hashCode()); + + List schemaNames = header.SchemaNames; + + Assert.NotNull(schemaNames); + Assert.Equal(3, schemaNames.Count); + Assert.Equal("a", schemaNames[0]); + Assert.Equal("r", schemaNames[1]); + Assert.Equal("a.age", schemaNames[2]); + } + + [Fact] + public void TestRecord() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + string name = "roi"; + int age = 32; + double doubleValue = 3.14; + bool boolValue = true; + + string place = "TLV"; + int since = 2000; + + var nameProperty = new KeyValuePair("name", name); + var ageProperty = new KeyValuePair("age", age); + var doubleProperty = new KeyValuePair("doubleValue", doubleValue); + var trueboolProperty = new KeyValuePair("boolValue", true); + var falseboolProperty = new KeyValuePair("boolValue", false); + var placeProperty = new KeyValuePair("place", place); + var sinceProperty = new KeyValuePair("since", since); + + Node expectedNode = new Node(); + expectedNode.Id = 0; + expectedNode.Labels.Add("person"); + expectedNode.PropertyMap.Add(nameProperty); + expectedNode.PropertyMap.Add(ageProperty); + expectedNode.PropertyMap.Add(doubleProperty); + expectedNode.PropertyMap.Add(trueboolProperty); + Assert.Equal( + "Node{labels=[person], id=0, " + + "propertyMap={name=roi, age=32, doubleValue=3.14, boolValue=True}}", + expectedNode.ToString()); + Edge expectedEdge = new Edge(); + expectedEdge.Id = 0; + expectedEdge.Source = 0; + expectedEdge.Destination = 1; + expectedEdge.RelationshipType = "knows"; + expectedEdge.PropertyMap.Add(placeProperty); + expectedEdge.PropertyMap.Add(sinceProperty); + expectedEdge.PropertyMap.Add(doubleProperty); + expectedEdge.PropertyMap.Add(falseboolProperty); + Assert.Equal("Edge{relationshipType='knows', source=0, destination=1, id=0, " + + "propertyMap={place=TLV, since=2000, doubleValue=3.14, boolValue=False}}", expectedEdge.ToString()); + + Dictionary parameters = new Dictionary(); + parameters.Add("name", name); + parameters.Add("age", age); + parameters.Add("boolValue", boolValue); + parameters.Add("doubleValue", doubleValue); + + Assert.NotNull(graph.Query("social", + "CREATE (:person{name:$name,age:$age, doubleValue:$doubleValue, boolValue:$boolValue})", parameters)); + Assert.NotNull(graph.Query("social", "CREATE (:person{name:'amit',age:30})")); + Assert.NotNull( + graph.Query("social", "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') " + + "CREATE (a)-[:knows{place:'TLV', since:2000,doubleValue:3.14, boolValue:false}]->(b)")); + + ResultSet resultSet = graph.Query("social", "MATCH (a:person)-[r:knows]->(b:person) RETURN a,r, " + + "a.name, a.age, a.doubleValue, a.boolValue, " + + "r.place, r.since, r.doubleValue, r.boolValue"); + Assert.NotNull(resultSet); + + Statistics stats = resultSet.Statistics; + Assert.Equal(0, stats.NodesCreated); + Assert.Equal(0, stats.NodesDeleted); + Assert.Equal(0, stats.LabelsAdded); + Assert.Equal(0, stats.PropertiesSet); + Assert.Equal(0, stats.RelationshipsCreated); + Assert.Equal(0, stats.RelationshipsDeleted); + Assert.NotNull(stats.QueryInternalExecutionTime); + Assert.NotEmpty(stats.QueryInternalExecutionTime); + + Assert.Equal(1, resultSet.Count); + // IReadOnlyCollection iterator = resultSet.GetEnumerator(); + var iterator = resultSet.GetEnumerator(); + Assert.True(iterator.MoveNext()); + var record = iterator.Current; + Assert.False(iterator.MoveNext()); + + Node node = record.GetValue(0); + Assert.NotNull(node); + + Assert.Equal(expectedNode.ToString(), node.ToString()); + //Expected: "Node{labels=[person], id=0, propertyMap={name=Property{name='name', value=roi}, age=Property{name='age', value=32}, doubleValue=Property{name='doubleValue', value=3.14}, boolValue=Property{name='boolValue', value=True}}}" + //Actual :"Node{labels=[person], id=0, propertyMap={name=Property{name='name', value=roi}, age=Property{name='age', value=32}, doubleValue=Property{name='doubleValue', value=3.14}, boolValue=Property{name='boolValue', value=True}}}" + + node = record.GetValue("a"); + Assert.Equal(expectedNode.ToString(), node.ToString()); + + Edge edge = record.GetValue(1); + Assert.NotNull(edge); + Assert.Equal(expectedEdge.ToString(), edge.ToString()); + + edge = record.GetValue("r"); + Assert.Equal(expectedEdge.ToString(), edge.ToString()); + + Assert.Equal(new List(){"a", "r", "a.name", "a.age", "a.doubleValue", "a.boolValue", + "r.place", "r.since", "r.doubleValue", "r.boolValue"}, record.Header); + + List expectedList = new List() {expectedNode, expectedEdge, + name, (long)age, doubleValue, true, + place, (long)since, doubleValue, false}; + + + for (int i = 0; i < expectedList.Count; i++) + { + Assert.Equal(expectedList[i].ToString(), record.Values[i].ToString()); + } + + Node a = record.GetValue("a"); + foreach (string propertyName in expectedNode.PropertyMap.Keys) + { + Assert.Equal(expectedNode.PropertyMap[propertyName].ToString(), a.PropertyMap[propertyName].ToString()); + } + + Assert.Equal("roi", record.GetString(2)); + Assert.Equal("32", record.GetString(3)); + Assert.Equal(32L, (record.GetValue(3))); + Assert.Equal(32L, (record.GetValue("a.age"))); + Assert.Equal("roi", record.GetString("a.name")); + Assert.Equal("32", record.GetString("a.age")); + + } + + [Fact] + public void TestAdditionToProcedures() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + + Assert.NotNull(graph.Query("social", "CREATE (:person{name:'roi',age:32})")); + Assert.NotNull(graph.Query("social", "CREATE (:person{name:'amit',age:30})")); + Assert.NotNull(graph.Query("social", + "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(b)")); + + // expected objects init + var nameProperty = new KeyValuePair("name", "roi"); + var ageProperty = new KeyValuePair("age", 32); + var lastNameProperty = new KeyValuePair("lastName", "a"); + + Node expectedNode = new Node(); + expectedNode.Id = 0; + expectedNode.Labels.Add("person"); + expectedNode.PropertyMap.Add(nameProperty); + expectedNode.PropertyMap.Add(ageProperty); + + Edge expectedEdge = new Edge(); + expectedEdge.Id = 0; + expectedEdge.Source = 0; + expectedEdge.Destination = 1; + expectedEdge.RelationshipType = "knows"; + + ResultSet resultSet = graph.Query("social", "MATCH (a:person)-[r:knows]->(b:person) RETURN a,r"); + Assert.NotNull(resultSet.Header); + Header header = resultSet.Header; + List schemaNames = header.SchemaNames; + Assert.NotNull(schemaNames); + Assert.Equal(2, schemaNames.Count); + Assert.Equal("a", schemaNames[0]); + Assert.Equal("r", schemaNames[1]); + Assert.Equal(1, resultSet.Count); + var iterator = resultSet.GetEnumerator(); + Assert.True(iterator.MoveNext()); + var record = iterator.Current; + Assert.False(iterator.MoveNext()); + Assert.Equal(new List() { "a", "r" }, record.Header); + Assert.Equal(expectedNode.ToString(), record.Values[0].ToString()); + Assert.Equal(expectedEdge.ToString(), record.Values[1].ToString()); + + // test for local cache updates + + expectedNode.PropertyMap.Remove("name"); + expectedNode.PropertyMap.Remove("age"); + expectedNode.PropertyMap.Add(lastNameProperty); + expectedNode.Labels.Remove("person"); + expectedNode.Labels.Add("worker"); + expectedNode.Id = 2; + expectedEdge.RelationshipType = "worksWith"; + expectedEdge.Source = 2; + expectedEdge.Destination = 3; + expectedEdge.Id = 1; + Assert.NotNull(graph.Query("social", "CREATE (:worker{lastName:'a'})")); + Assert.NotNull(graph.Query("social", "CREATE (:worker{lastName:'b'})")); + Assert.NotNull(graph.Query("social", + "MATCH (a:worker), (b:worker) WHERE (a.lastName = 'a' AND b.lastName='b') CREATE (a)-[:worksWith]->(b)")); + resultSet = graph.Query("social", "MATCH (a:worker)-[r:worksWith]->(b:worker) RETURN a,r"); + Assert.NotNull(resultSet.Header); + header = resultSet.Header; + schemaNames = header.SchemaNames; + Assert.NotNull(schemaNames); + Assert.Equal(2, schemaNames.Count); + Assert.Equal("a", schemaNames[0]); + Assert.Equal("r", schemaNames[1]); + Assert.Equal(1, resultSet.Count); + iterator = resultSet.GetEnumerator(); + Assert.True(iterator.MoveNext()); + record = iterator.Current; + Assert.False(iterator.MoveNext()); + Assert.Equal(new List { "a", "r" }, record.Header); + Assert.Equal(expectedNode.ToString(), record.Values[0].ToString()); + Assert.Equal(expectedEdge.ToString(), record.Values[1].ToString()); + } + + [Fact] + public void TestEscapedQuery() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + Dictionary params1 = new Dictionary(); + params1.Add("s1", "S\"'"); + params1.Add("s2", "S'\""); + Assert.NotNull(graph.Query("social", "CREATE (:escaped{s1:$s1,s2:$s2})", params1)); + + Dictionary params2 = new Dictionary(); + params2.Add("s1", "S\"'"); + params2.Add("s2", "S'\""); + Assert.NotNull(graph.Query("social", "MATCH (n) where n.s1=$s1 and n.s2=$s2 RETURN n", params2)); + + Assert.NotNull(graph.Query("social", "MATCH (n) where n.s1='S\"' RETURN n")); + } + + [Fact] + public void TestArraySupport() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + + Node expectedANode = new Node(); + expectedANode.Id = 0; + expectedANode.Labels.Add("person"); + var aNameProperty = new KeyValuePair("name", "a"); + var aAgeProperty = new KeyValuePair("age", 32L); + var aListProperty = new KeyValuePair("array", new object[] { 0L, 1L, 2L }); + expectedANode.PropertyMap.Add(aNameProperty); + expectedANode.PropertyMap.Add(aAgeProperty); + expectedANode.PropertyMap.Add(aListProperty); + + Node expectedBNode = new Node(); + expectedBNode.Id = 1; + expectedBNode.Labels.Add("person"); + var bNameProperty = new KeyValuePair("name", "b"); + var bAgeProperty = new KeyValuePair("age", 30L); + var bListProperty = new KeyValuePair("array", new object[] { 3L, 4L, 5L }); + expectedBNode.PropertyMap.Add(bNameProperty); + expectedBNode.PropertyMap.Add(bAgeProperty); + expectedBNode.PropertyMap.Add(bListProperty); + + Assert.NotNull(graph.Query("social", "CREATE (:person{name:'a',age:32,array:[0,1,2]})")); + Assert.NotNull(graph.Query("social", "CREATE (:person{name:'b',age:30,array:[3,4,5]})")); + + // test array + + ResultSet resultSet = graph.Query("social", "WITH [0,1,2] as x return x"); + + // check header + Assert.NotNull(resultSet.Header); + Header header = resultSet.Header; + + List schemaNames = header.SchemaNames; + Assert.NotNull(schemaNames); + Assert.Equal(1, schemaNames.Count); + Assert.Equal("x", schemaNames[0]); + + // check record + Assert.Equal(1, resultSet.Count); + var iterator = resultSet.GetEnumerator(); + Assert.True(iterator.MoveNext()); + NRedisStack.Graph.Record record = iterator.Current; + Assert.False(iterator.MoveNext()); + Assert.Equal(new List() { "x" }, record.Header); + + var x = record.GetValue("x"); + Assert.Equal(new object[] { 0L, 1L, 2L }, x); + + // test collect + resultSet = graph.Query("social", "MATCH(n) return collect(n) as x"); + + Assert.NotNull(resultSet.Header); + header = resultSet.Header; + + schemaNames = header.SchemaNames; + Assert.NotNull(schemaNames); + Assert.Equal(1, schemaNames.Count); + Assert.Equal("x", schemaNames[0]); + + // check record + Assert.Equal(1, resultSet.Count); + iterator = resultSet.GetEnumerator(); + Assert.True(iterator.MoveNext()); + record = iterator.Current; + Assert.False(iterator.MoveNext()); + Assert.Equal(new List() { "x" }, record.Header); + var x2 = record.GetValue("x"); + + Assert.Equal(expectedANode.ToString(), x2[0].ToString()); + Assert.Equal(expectedBNode.ToString(), x2[1].ToString()); + + // test unwind + resultSet = graph.Query("social", "unwind([0,1,2]) as x return x"); + + Assert.NotNull(resultSet.Header); + header = resultSet.Header; + + schemaNames = header.SchemaNames; + Assert.NotNull(schemaNames); + Assert.Equal(1, schemaNames.Count); + Assert.Equal("x", schemaNames[0]); + + // check record + Assert.Equal(3, resultSet.Count); + iterator = resultSet.GetEnumerator(); + for (long i = 0; i < 3; i++) + { + Assert.True(iterator.MoveNext()); + record = iterator.Current; + Assert.Equal(new List() { "x" }, record.Header); + Assert.Equal(i, (long)record.GetValue("x")); + } + } + + [Fact] + public void TestPath() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + List nodes = new List(3); + for (int i = 0; i < 3; i++) + { + Node node = new Node(); + node.Id = i; + node.Labels.Add("L1"); + nodes.Add(node); + } + + List edges = new List(2); + for (int i = 0; i < 2; i++) + { + Edge edge = new Edge(); + edge.Id = i; + edge.RelationshipType = "R1"; + edge.Source = i; + edge.Destination = i + 1; + edges.Add(edge); + } + + var expectedPaths = new HashSet(); + + NRedisStack.Graph.DataTypes.Path path01 = new PathBuilder(2).Append(nodes[0]).Append(edges[0]).Append(nodes[1]).Build(); + NRedisStack.Graph.DataTypes.Path path12 = new PathBuilder(2).Append(nodes[1]).Append(edges[1]).Append(nodes[2]).Build(); + NRedisStack.Graph.DataTypes.Path path02 = new PathBuilder(3).Append(nodes[0]).Append(edges[0]).Append(nodes[1]) + .Append(edges[1]).Append(nodes[2]).Build(); + + expectedPaths.Add(path01); + expectedPaths.Add(path12); + expectedPaths.Add(path02); + + graph.Query("social", "CREATE (:L1)-[:R1]->(:L1)-[:R1]->(:L1)"); + + ResultSet resultSet = graph.Query("social", "MATCH p = (:L1)-[:R1*]->(:L1) RETURN p"); + + Assert.Equal(expectedPaths.Count, resultSet.Count); + var iterator = resultSet.GetEnumerator(); + + for (int i = 0; i < resultSet.Count; i++) + { + NRedisStack.Graph.DataTypes.Path p = resultSet.ElementAt(i).GetValue("p"); + Assert.Contains(p, expectedPaths); + expectedPaths.Remove(p); + } + } + + [Fact] + public void TestNullGraphEntities() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + // Create two nodes connected by a single outgoing edge. + Assert.NotNull(graph.Query("social", "CREATE (:L)-[:E]->(:L2)")); + // Test a query that produces 1 record with 3 null values. + ResultSet resultSet = graph.Query("social", "OPTIONAL MATCH (a:NONEXISTENT)-[e]->(b) RETURN a, e, b"); + Assert.Equal(1, resultSet.Count); + IEnumerator iterator = resultSet.GetEnumerator(); + Assert.True(iterator.MoveNext()); + NRedisStack.Graph.Record record = iterator.Current; + Assert.False(iterator.MoveNext()); + Assert.Equal(new List() { null, null, null }, record.Values); + + // Test a query that produces 2 records, with 2 null values in the second. + resultSet = graph.Query("social", "MATCH (a) OPTIONAL MATCH (a)-[e]->(b) RETURN a, e, b ORDER BY ID(a)"); + Assert.Equal(2, resultSet.Count); + + // iterator = resultSet.GetEnumerator(); + // record = iterator.Current; + // Assert.Equal(3, record.Size); + record = resultSet.First(); + Assert.Equal(3, record.Values.Count); + + Assert.NotNull(record.Values[0]); + Assert.NotNull(record.Values[1]); + Assert.NotNull(record.Values[2]); + + // record = iterator.Current; + record = resultSet.Skip(1).Take(1).First(); + Assert.Equal(3, record.Size); + + Assert.NotNull(record.Values[0]); + Assert.Null(record.Values[1]); + Assert.Null(record.Values[2]); + + // Test a query that produces 2 records, the first containing a path and the + // second containing a null value. + resultSet = graph.Query("social", "MATCH (a) OPTIONAL MATCH p = (a)-[e]->(b) RETURN p"); + Assert.Equal(2, resultSet.Count); + iterator = resultSet.GetEnumerator(); + + record = resultSet.First(); + Assert.Equal(1, record.Size); + Assert.NotNull(record.Values[0]); + + record = resultSet.Skip(1).First(); + Assert.Equal(1, record.Size); + Assert.Null(record.Values[0]); + } + + [Fact] + public void Test64BitNumber() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + long value = 1L << 40; + Dictionary parameters = new Dictionary(); + parameters.Add("val", value); + ResultSet resultSet = graph.Query("social", "CREATE (n {val:$val}) RETURN n.val", parameters); + Assert.Equal(1, resultSet.Count); + + // NRedisStack.Graph.Record r = resultSet.GetEnumerator().Current; + // Assert.Equal(value, r.Values[0]); + Assert.Equal(value, resultSet.First().GetValue(0)); + + } + + [Fact] + public void TestCachedExecution() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + graph.Query("social", "CREATE (:N {val:1}), (:N {val:2})"); + + // First time should not be loaded from execution cache + Dictionary parameters = new Dictionary(); + parameters.Add("val", 1L); + ResultSet resultSet = graph.Query("social", "MATCH (n:N {val:$val}) RETURN n.val", parameters); + Assert.Equal(1, resultSet.Count); + // NRedisStack.Graph.Record r = resultSet.GetEnumerator().Current; + Assert.Equal(parameters["val"], resultSet.First().Values[0]); + Assert.False(resultSet.Statistics.CachedExecution); + + // Run in loop many times to make sure the query will be loaded + // from cache at least once + for (int i = 0; i < 64; i++) + { + resultSet = graph.Query("social", "MATCH (n:N {val:$val}) RETURN n.val", parameters); + } + Assert.Equal(1, resultSet.Count); + // r = resultSet.GetEnumerator().Current; + // Assert.Equal(parameters["val"], r.Values[0]); + Assert.Equal(parameters["val"], resultSet.First().Values[0]); + + Assert.True(resultSet.Statistics.CachedExecution); + } + + [Fact] + public void TestMapDataType() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + Dictionary expected = new Dictionary(); + expected.Add("a", (long)1); + expected.Add("b", "str"); + expected.Add("c", null); + List d = new List(); + d.Add((long)1); + d.Add((long)2); + d.Add((long)3); + expected.Add("d", d); + expected.Add("e", true); + Dictionary f = new Dictionary(); + f.Add("x", (long)1); + f.Add("y", (long)2); + expected.Add("f", f); + ResultSet res = graph.Query("social", "RETURN {a:1, b:'str', c:NULL, d:[1,2,3], e:True, f:{x:1, y:2}}"); + Assert.Equal(1, res.Count); + + var iterator = res.GetEnumerator(); + iterator.MoveNext(); + NRedisStack.Graph.Record r = iterator.Current; + var actual = r.Values[0]; + Assert.Equal((object)expected, actual); + } + + [Fact] + public void TestGeoPointLatLon() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + ResultSet rs = graph.Query("social", "CREATE (:restaurant" + + " {location: point({latitude:30.27822306, longitude:-97.75134723})})"); + Assert.Equal(1, rs.Statistics.NodesCreated); + Assert.Equal(1, rs.Statistics.PropertiesSet); + + AssertTestGeoPoint(graph); + } + + [Fact] + public void TestGeoPointLonLat() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + ResultSet rs = graph.Query("social", "CREATE (:restaurant" + + " {location: point({longitude:-97.75134723, latitude:30.27822306})})"); + Assert.Equal(1, rs.Statistics.NodesCreated); + Assert.Equal(1, rs.Statistics.PropertiesSet); + + AssertTestGeoPoint(graph); + } + + private void AssertTestGeoPoint(IGraphCommands graph) + { + ResultSet results = graph.Query("social", "MATCH (restaurant) RETURN restaurant"); + Assert.Equal(1, results.Count); + var record = results.GetEnumerator(); + record.MoveNext(); + Assert.Equal(1, record.Current.Size); + Assert.Equal(new List() { "restaurant" }, record.Current.Header); + Node node = record.Current.GetValue(0); + var property = node.PropertyMap["location"]; + + Assert.Equal((object)(new Point(30.27822306, -97.75134723)), property); + } + + [Fact] + public void timeoutArgument() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + ResultSet rs = graph.Query("social", "UNWIND range(0,100) AS x WITH x AS x WHERE x = 100 RETURN x", 1L); + Assert.Equal(1, rs.Count); + var iterator = rs.GetEnumerator(); + iterator.MoveNext(); + var r = iterator.Current; + Assert.Equal(100l, (long)r.Values[0]); + } + + [Fact] + public void TestCachedExecutionReadOnly() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + graph.Query("social", "CREATE (:N {val:1}), (:N {val:2})"); + + // First time should not be loaded from execution cache + Dictionary parameters = new Dictionary(); + parameters.Add("val", 1L); + ResultSet resultSet = graph.RO_Query("social", "MATCH (n:N {val:$val}) RETURN n.val", parameters); + Assert.Equal(1, resultSet.Count); + var iterator = resultSet.GetEnumerator(); + iterator.MoveNext(); + NRedisStack.Graph.Record r = iterator.Current; + Assert.Equal(parameters["val"], r.Values[0]); + Assert.False(resultSet.Statistics.CachedExecution); + + // Run in loop many times to make sure the query will be loaded + // from cache at least once + for (int i = 0; i < 64; i++) + { + resultSet = graph.RO_Query("social", "MATCH (n:N {val:$val}) RETURN n.val", parameters); + } + Assert.Equal(1, resultSet.Count); + iterator = resultSet.GetEnumerator(); + iterator.MoveNext(); + r = iterator.Current; + Assert.Equal(parameters["val"], r.Values[0]); + Assert.True(resultSet.Statistics.CachedExecution); + } + + [Fact] + public void TestSimpleReadOnly() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + graph.Query("social", "CREATE (:person{name:'filipe',age:30})"); + ResultSet rsRo = graph.RO_Query("social", "MATCH (a:person) WHERE (a.name = 'filipe') RETURN a.age"); + Assert.Equal(1, rsRo.Count); + var iterator = rsRo.GetEnumerator(); + iterator.MoveNext(); + var r = iterator.Current; + Assert.Equal("30", r.Values[0].ToString()); + } + + [Fact] + public void TestProfile() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + Assert.NotNull(graph.Query("social", "CREATE (:person{name:'roi',age:32})")); + Assert.NotNull(graph.Query("social", "CREATE (:person{name:'amit',age:30})")); + + var profile = graph.Profile("social", + "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(b)"); + Assert.NotEmpty(profile); + foreach (var p in profile) + { + Assert.NotNull(p); + } + } + + [Fact] + public void TestExplain() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + Assert.NotNull(graph.Profile("social", "CREATE (:person{name:'roi',age:32})")); + Assert.NotNull(graph.Profile("social", "CREATE (:person{name:'amit',age:30})")); + + var explain = graph.Explain("social", + "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(b)"); + Assert.NotEmpty(explain); + foreach (var e in explain) + { + Assert.NotNull(e); + } + } + + [Fact] + public void TestSlowlog() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + Assert.NotNull(graph.Profile("social", "CREATE (:person{name:'roi',age:32})")); + Assert.NotNull(graph.Profile("social", "CREATE (:person{name:'amit',age:30})")); + + List> slowlogs = graph.Slowlog("social"); + Assert.Equal(2, slowlogs.Count); + slowlogs.ForEach(sl => Assert.NotEmpty(sl)); + slowlogs.ForEach(sl => sl.ForEach(s => Assert.NotNull(s))); + } + + [Fact] + public void TestList() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + Assert.Empty(graph.List()); + + graph.Query("social", "CREATE (:person{name:'filipe',age:30})"); + + Assert.Equal(new List() { "social" }, graph.List()); + } + + [Fact] + public void TestConfig() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + graph.Query("social", "CREATE (:person{name:'filipe',age:30})"); + + string name = "RESULTSET_SIZE"; + var existingValue = graph.ConfigGet(name)[name]; + + Assert.True(graph.ConfigSet(name, 250L)); + + var actual = graph.ConfigGet(name); + Assert.Equal(actual.Count, 1); + Assert.Equal("250", actual[name].ToString()); + + graph.ConfigSet(name, existingValue != null ? existingValue.ToString() : -1); + } + + [Fact] + public void TestModulePrefixs() + { + IDatabase db1 = redisFixture.Redis.GetDatabase(); + IDatabase db2 = redisFixture.Redis.GetDatabase(); + + var graph1 = db1.GRAPH(); + var graph2 = db2.GRAPH(); + + Assert.NotEqual(graph1.GetHashCode(), graph2.GetHashCode()); + } + + [Fact] + public void TestModulePrefixs1() + { + { + var conn = ConnectionMultiplexer.Connect("localhost"); + IDatabase db = conn.GetDatabase(); + + var graph = db.GRAPH(); + // ... + conn.Dispose(); + } + + { + var conn = ConnectionMultiplexer.Connect("localhost"); + IDatabase db = conn.GetDatabase(); + + var graph = db.GRAPH(); + // ... + conn.Dispose(); + } + + } + + #endregion + + #region AsyncTests + + [Fact] + public async Task TestReserveBasicAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + } + + [Fact] + public async Task TestCreateNodeAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + // Create a node + ResultSet resultSet = await graph.QueryAsync("social", "CREATE ({name:'roi',age:32})"); + + Statistics stats = resultSet.Statistics; + Assert.Equal(1, stats.NodesCreated); + Assert.Equal(0, stats.NodesDeleted); + Assert.Equal(0, stats.RelationshipsCreated); + Assert.Equal(0, stats.RelationshipsDeleted); + Assert.Equal(2, stats.PropertiesSet); + Assert.NotNull(stats.QueryInternalExecutionTime); + + Assert.Equal(0, resultSet.Count); + } + + [Fact] + public async Task TestCreateLabeledNodeAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + // Create a node with a label + ResultSet resultSet = await graph.QueryAsync("social", "CREATE (:human{name:'danny',age:12})"); + + Statistics stats = resultSet.Statistics; + // Assert.Equal("1", stats.getstringValue(Label.NODES_CREATED)); + Assert.Equal(1, stats.NodesCreated); + // Assert.Equal("2", stats.getstringValue(Label.PROPERTIES_SET)); + Assert.Equal(2, stats.PropertiesSet); + // Assert.NotNull(stats.getstringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); + Assert.NotNull(stats.QueryInternalExecutionTime); + + Assert.Equal(0, resultSet.Count); + // Assert.False(resultSet..iterator().MoveNext()); + } + + [Fact] + public async Task TestConnectNodesAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + // Create both source and destination nodes + Assert.NotNull(await graph.QueryAsync("social", "CREATE (:person{name:'roi',age:32})")); + Assert.NotNull(await graph.QueryAsync("social", "CREATE (:person{name:'amit',age:30})")); + + // Connect source and destination nodes. + ResultSet resultSet = await graph.QueryAsync("social", + "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(b)"); + + Statistics stats = resultSet.Statistics; + // Assert.Null(stats.getstringValue(Label.NODES_CREATED)); + Assert.Equal(0, stats.NodesCreated); + Assert.Equal(1, stats.RelationshipsCreated); + Assert.Equal(0, stats.RelationshipsDeleted); + // Assert.Null(stats.getstringValue(Label.PROPERTIES_SET)); + Assert.Equal(0, stats.PropertiesSet); + // Assert.NotNull(stats.getstringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); + Assert.NotNull(stats.QueryInternalExecutionTime); + + Assert.Equal(0, resultSet.Count); + // Assert.False(resultSet.GetEnumerator().MoveNext()); + } + + [Fact] + public async Task TestDeleteNodesAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + Assert.NotNull(await graph.QueryAsync("social", "CREATE (:person{name:'roi',age:32})")); + Assert.NotNull(await graph.QueryAsync("social", "CREATE (:person{name:'amit',age:30})")); + + ResultSet deleteResult = await graph.QueryAsync("social", "MATCH (a:person) WHERE (a.name = 'roi') DELETE a"); + + Statistics delStats = deleteResult.Statistics; + // Assert.Null(delStats.getstringValue(Label.NODES_CREATED)); + Assert.Equal(0, delStats.NodesCreated); + Assert.Equal(1, delStats.NodesDeleted); + // Assert.Null(delStats.getstringValue(Label.RELATIONSHIPS_CREATED)); + Assert.Equal(0, delStats.RelationshipsCreated); + // Assert.Null(delStats.getstringValue(Label.RELATIONSHIPS_DELETED)); + Assert.Equal(0, delStats.RelationshipsDeleted); + // Assert.Null(delStats.getstringValue(Label.PROPERTIES_SET)); + Assert.Equal(0, delStats.PropertiesSet); + // Assert.NotNull(delStats.getstringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); + Assert.NotNull(delStats.QueryInternalExecutionTime); + Assert.Equal(0, deleteResult.Count); + // Assert.False(deleteResult.iterator().MoveNext()); + + Assert.NotNull(await graph.QueryAsync("social", "CREATE (:person{name:'roi',age:32})")); + Assert.NotNull(await graph.QueryAsync("social", + "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(a)")); + + deleteResult = await graph.QueryAsync("social", "MATCH (a:person) WHERE (a.name = 'roi') DELETE a"); + + // Assert.Null(delStats.getstringValue(Label.NODES_CREATED)); + Assert.Equal(0, delStats.NodesCreated); + Assert.Equal(1, delStats.NodesDeleted); + // Assert.Null(delStats.getstringValue(Label.RELATIONSHIPS_CREATED)); + Assert.Equal(0, delStats.RelationshipsCreated); + // Assert.Equal(1, delStats.RelationshipsDeleted); + Assert.Equal(0, delStats.RelationshipsDeleted); + // Assert.Null(delStats.getstringValue(Label.PROPERTIES_SET)); + Assert.Equal(0, delStats.PropertiesSet); + // Assert.NotNull(delStats.getstringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); + Assert.NotNull(delStats.QueryInternalExecutionTime); + Assert.Equal(0, deleteResult.Count); + // Assert.False(deleteResult.iterator().MoveNext()); + } + + [Fact] + public async Task TestDeleteRelationshipAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + Assert.NotNull(await graph.QueryAsync("social", "CREATE (:person{name:'roi',age:32})")); + Assert.NotNull(await graph.QueryAsync("social", "CREATE (:person{name:'amit',age:30})")); + Assert.NotNull(await graph.QueryAsync("social", + "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(a)")); + + ResultSet deleteResult = await graph.QueryAsync("social", + "MATCH (a:person)-[e]->() WHERE (a.name = 'roi') DELETE e"); + + Statistics delStats = deleteResult.Statistics; + // Assert.Null(delStats.getstringValue(Label.NODES_CREATED)); + Assert.Equal(0, delStats.NodesCreated); + // Assert.Null(delStats.getstringValue(Label.NODES_DELETED)); + Assert.Equal(0, delStats.NodesDeleted); + // Assert.Null(delStats.getstringValue(Label.RELATIONSHIPS_CREATED)); + Assert.Equal(0, delStats.RelationshipsCreated); + Assert.Equal(1, delStats.RelationshipsDeleted); + // Assert.Null(delStats.getstringValue(Label.PROPERTIES_SET)); + Assert.Equal(0, delStats.PropertiesSet); + // Assert.NotNull(delStats.getstringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); + Assert.NotNull(delStats.QueryInternalExecutionTime); + Assert.Equal(0, deleteResult.Count); + // Assert.False(deleteResult.iterator().MoveNext()); + } + + [Fact] + public async Task TestIndexAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + // Create both source and destination nodes + Assert.NotNull(await graph.QueryAsync("social", "CREATE (:person{name:'roi',age:32})")); + + ResultSet createIndexResult = await graph.QueryAsync("social", "CREATE INDEX ON :person(age)"); + Assert.Empty(createIndexResult); + Assert.Equal(1, createIndexResult.Statistics.IndicesCreated); + + // since RediSearch as index, those action are allowed + ResultSet createNonExistingIndexResult = await graph.QueryAsync("social", "CREATE INDEX ON :person(age1)"); + Assert.Empty(createNonExistingIndexResult); + Assert.Equal(1, createNonExistingIndexResult.Statistics.IndicesCreated); + + ResultSet createExistingIndexResult = await graph.QueryAsync("social", "CREATE INDEX ON :person(age)"); + Assert.Empty(createExistingIndexResult); + Assert.Equal(0, createExistingIndexResult.Statistics.IndicesCreated); + + ResultSet deleteExistingIndexResult = await graph.QueryAsync("social", "DROP INDEX ON :person(age)"); + Assert.Empty(deleteExistingIndexResult); + Assert.Equal(1, deleteExistingIndexResult.Statistics.IndicesDeleted); + } + + [Fact] + public async Task TestHeaderAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + + Assert.NotNull(await graph.QueryAsync("social", "CREATE (:person{name:'roi',age:32})")); + Assert.NotNull(await graph.QueryAsync("social", "CREATE (:person{name:'amit',age:30})")); + Assert.NotNull(await graph.QueryAsync("social", + "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(a)")); + + ResultSet queryResult = await graph.QueryAsync("social", "MATCH (a:person)-[r:knows]->(b:person) RETURN a,r, a.age"); + + Header header = queryResult.Header; + Assert.NotNull(header); + Assert.Equal("Header{" + // + "schemaTypes=[COLUMN_SCALAR, COLUMN_SCALAR, COLUMN_SCALAR], " + + "schemaTypes=[SCALAR, SCALAR, SCALAR], " + + "schemaNames=[a, r, a.age]}", header.ToString()); + // Assert.Assert.Equal(-1901778507, header.hashCode()); + + List schemaNames = header.SchemaNames; + + Assert.NotNull(schemaNames); + Assert.Equal(3, schemaNames.Count); + Assert.Equal("a", schemaNames[0]); + Assert.Equal("r", schemaNames[1]); + Assert.Equal("a.age", schemaNames[2]); + } + + [Fact] + public async Task TestRecordAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + string name = "roi"; + int age = 32; + double doubleValue = 3.14; + bool boolValue = true; + + string place = "TLV"; + int since = 2000; + + var nameProperty = new KeyValuePair("name", name); + var ageProperty = new KeyValuePair("age", age); + var doubleProperty = new KeyValuePair("doubleValue", doubleValue); + var trueboolProperty = new KeyValuePair("boolValue", true); + var falseboolProperty = new KeyValuePair("boolValue", false); + var placeProperty = new KeyValuePair("place", place); + var sinceProperty = new KeyValuePair("since", since); + + Node expectedNode = new Node(); + expectedNode.Id = 0; + expectedNode.Labels.Add("person"); + expectedNode.PropertyMap.Add(nameProperty); + expectedNode.PropertyMap.Add(ageProperty); + expectedNode.PropertyMap.Add(doubleProperty); + expectedNode.PropertyMap.Add(trueboolProperty); + Assert.Equal( + "Node{labels=[person], id=0, " + + "propertyMap={name=roi, age=32, doubleValue=3.14, boolValue=True}}", + expectedNode.ToString()); + // "Node{labels=[person], id=0, propertyMap={name=Property{name='name', value=roi}, age=Property{name='age', value=32}, doubleValue=Property{name='doubleValue', value=3.14}, boolValue=Property{name='boolValue', value=True}}}" + // "Node{labels=[person], id=0, propertyMap={name=Property{name='name', value=roi}, boolValue=Property{name='boolValue', value=true}, doubleValue=Property{name='doubleValue', value=3.14}, age=Property{name='age', value=32}}}" + Edge expectedEdge = new Edge(); + expectedEdge.Id = 0; + expectedEdge.Source = 0; + expectedEdge.Destination = 1; + expectedEdge.RelationshipType = "knows"; + expectedEdge.PropertyMap.Add(placeProperty); + expectedEdge.PropertyMap.Add(sinceProperty); + expectedEdge.PropertyMap.Add(doubleProperty); + expectedEdge.PropertyMap.Add(falseboolProperty); + Assert.Equal("Edge{relationshipType='knows', source=0, destination=1, id=0, " + + "propertyMap={place=TLV, since=2000, doubleValue=3.14, boolValue=False}}", expectedEdge.ToString()); + + Dictionary parameters = new Dictionary(); + parameters.Add("name", name); + parameters.Add("age", age); + parameters.Add("boolValue", boolValue); + parameters.Add("doubleValue", doubleValue); + + Assert.NotNull(await graph.QueryAsync("social", + "CREATE (:person{name:$name,age:$age, doubleValue:$doubleValue, boolValue:$boolValue})", parameters)); + Assert.NotNull(await graph.QueryAsync("social", "CREATE (:person{name:'amit',age:30})")); + Assert.NotNull( + await graph.QueryAsync("social", "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') " + + "CREATE (a)-[:knows{place:'TLV', since:2000,doubleValue:3.14, boolValue:false}]->(b)")); + + ResultSet resultSet = await graph.QueryAsync("social", "MATCH (a:person)-[r:knows]->(b:person) RETURN a,r, " + + "a.name, a.age, a.doubleValue, a.boolValue, " + + "r.place, r.since, r.doubleValue, r.boolValue"); + Assert.NotNull(resultSet); + + Statistics stats = resultSet.Statistics; + Assert.Equal(0, stats.NodesCreated); + Assert.Equal(0, stats.NodesDeleted); + Assert.Equal(0, stats.LabelsAdded); + Assert.Equal(0, stats.PropertiesSet); + Assert.Equal(0, stats.RelationshipsCreated); + Assert.Equal(0, stats.RelationshipsDeleted); + Assert.NotNull(stats.QueryInternalExecutionTime); + Assert.NotEmpty(stats.QueryInternalExecutionTime); + + Assert.Equal(1, resultSet.Count); + // IReadOnlyCollection iterator = resultSet.GetEnumerator(); + var iterator = resultSet.GetEnumerator(); + Assert.True(iterator.MoveNext()); + var record = iterator.Current; + Assert.False(iterator.MoveNext()); + + Node node = record.GetValue(0); + Assert.NotNull(node); + + Assert.Equal(expectedNode.ToString(), node.ToString()); + //Expected: "Node{labels=[person], id=0, propertyMap={name=Property{name='name', value=roi}, age=Property{name='age', value=32}, doubleValue=Property{name='doubleValue', value=3.14}, boolValue=Property{name='boolValue', value=True}}}" + //Actual :"Node{labels=[person], id=0, propertyMap={name=Property{name='name', value=roi}, age=Property{name='age', value=32}, doubleValue=Property{name='doubleValue', value=3.14}, boolValue=Property{name='boolValue', value=True}}}" + + node = record.GetValue("a"); + Assert.Equal(expectedNode.ToString(), node.ToString()); + + Edge edge = record.GetValue(1); + Assert.NotNull(edge); + Assert.Equal(expectedEdge.ToString(), edge.ToString()); + + edge = record.GetValue("r"); + Assert.Equal(expectedEdge.ToString(), edge.ToString()); + + Assert.Equal(new List(){"a", "r", "a.name", "a.age", "a.doubleValue", "a.boolValue", + "r.place", "r.since", "r.doubleValue", "r.boolValue"}, record.Header); + + List expectedList = new List() {expectedNode, expectedEdge, + name, (long)age, doubleValue, true, + place, (long)since, doubleValue, false}; + + + for (int i = 0; i < expectedList.Count; i++) + { + Assert.Equal(expectedList[i].ToString(), record.Values[i].ToString()); + } + + Node a = record.GetValue("a"); + foreach (string propertyName in expectedNode.PropertyMap.Keys) + { + Assert.Equal(expectedNode.PropertyMap[propertyName].ToString(), a.PropertyMap[propertyName].ToString()); + } + + Assert.Equal("roi", record.GetString(2)); + Assert.Equal("32", record.GetString(3)); + Assert.Equal(32L, (record.GetValue(3))); + Assert.Equal(32L, (record.GetValue("a.age"))); + Assert.Equal("roi", record.GetString("a.name")); + Assert.Equal("32", record.GetString("a.age")); + + } + + [Fact] + public async Task TestAdditionToProceduresAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + + Assert.NotNull(await graph.QueryAsync("social", "CREATE (:person{name:'roi',age:32})")); + Assert.NotNull(await graph.QueryAsync("social", "CREATE (:person{name:'amit',age:30})")); + Assert.NotNull(await graph.QueryAsync("social", + "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(b)")); + + // expected objects init + var nameProperty = new KeyValuePair("name", "roi"); + var ageProperty = new KeyValuePair("age", 32); + var lastNameProperty = new KeyValuePair("lastName", "a"); + + Node expectedNode = new Node(); + expectedNode.Id = 0; + expectedNode.Labels.Add("person"); + expectedNode.PropertyMap.Add(nameProperty); + expectedNode.PropertyMap.Add(ageProperty); + + Edge expectedEdge = new Edge(); + expectedEdge.Id = 0; + expectedEdge.Source = 0; + expectedEdge.Destination = 1; + expectedEdge.RelationshipType = "knows"; + + ResultSet resultSet = await graph.QueryAsync("social", "MATCH (a:person)-[r:knows]->(b:person) RETURN a,r"); + Assert.NotNull(resultSet.Header); + Header header = resultSet.Header; + List schemaNames = header.SchemaNames; + Assert.NotNull(schemaNames); + Assert.Equal(2, schemaNames.Count); + Assert.Equal("a", schemaNames[0]); + Assert.Equal("r", schemaNames[1]); + Assert.Equal(1, resultSet.Count); + var iterator = resultSet.GetEnumerator(); + Assert.True(iterator.MoveNext()); + var record = iterator.Current; + Assert.False(iterator.MoveNext()); + Assert.Equal(new List() { "a", "r" }, record.Header); + Assert.Equal(expectedNode.ToString(), record.Values[0].ToString()); + Assert.Equal(expectedEdge.ToString(), record.Values[1].ToString()); + + // test for local cache updates + + expectedNode.PropertyMap.Remove("name"); + expectedNode.PropertyMap.Remove("age"); + expectedNode.PropertyMap.Add(lastNameProperty); + expectedNode.Labels.Remove("person"); + expectedNode.Labels.Add("worker"); + expectedNode.Id = 2; + expectedEdge.RelationshipType = "worksWith"; + expectedEdge.Source = 2; + expectedEdge.Destination = 3; + expectedEdge.Id = 1; + Assert.NotNull(await graph.QueryAsync("social", "CREATE (:worker{lastName:'a'})")); + Assert.NotNull(await graph.QueryAsync("social", "CREATE (:worker{lastName:'b'})")); + Assert.NotNull(await graph.QueryAsync("social", + "MATCH (a:worker), (b:worker) WHERE (a.lastName = 'a' AND b.lastName='b') CREATE (a)-[:worksWith]->(b)")); + resultSet = await graph.QueryAsync("social", "MATCH (a:worker)-[r:worksWith]->(b:worker) RETURN a,r"); + Assert.NotNull(resultSet.Header); + header = resultSet.Header; + schemaNames = header.SchemaNames; + Assert.NotNull(schemaNames); + Assert.Equal(2, schemaNames.Count); + Assert.Equal("a", schemaNames[0]); + Assert.Equal("r", schemaNames[1]); + Assert.Equal(1, resultSet.Count); + iterator = resultSet.GetEnumerator(); + Assert.True(iterator.MoveNext()); + record = iterator.Current; + Assert.False(iterator.MoveNext()); + Assert.Equal(new List { "a", "r" }, record.Header); + Assert.Equal(expectedNode.ToString(), record.Values[0].ToString()); + Assert.Equal(expectedEdge.ToString(), record.Values[1].ToString()); + } + + [Fact] + public async Task TestEscapedQueryAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + Dictionary params1 = new Dictionary(); + params1.Add("s1", "S\"'"); + params1.Add("s2", "S'\""); + Assert.NotNull(await graph.QueryAsync("social", "CREATE (:escaped{s1:$s1,s2:$s2})", params1)); + + Dictionary params2 = new Dictionary(); + params2.Add("s1", "S\"'"); + params2.Add("s2", "S'\""); + Assert.NotNull(await graph.QueryAsync("social", "MATCH (n) where n.s1=$s1 and n.s2=$s2 RETURN n", params2)); + + Assert.NotNull(await graph.QueryAsync("social", "MATCH (n) where n.s1='S\"' RETURN n")); + } + + [Fact] + public async Task TestArraySupportAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + + Node expectedANode = new Node(); + expectedANode.Id = 0; + expectedANode.Labels.Add("person"); + var aNameProperty = new KeyValuePair("name", "a"); + var aAgeProperty = new KeyValuePair("age", 32L); + var aListProperty = new KeyValuePair("array", new object[] { 0L, 1L, 2L }); + expectedANode.PropertyMap.Add(aNameProperty); + expectedANode.PropertyMap.Add(aAgeProperty); + expectedANode.PropertyMap.Add(aListProperty); + + Node expectedBNode = new Node(); + expectedBNode.Id = 1; + expectedBNode.Labels.Add("person"); + var bNameProperty = new KeyValuePair("name", "b"); + var bAgeProperty = new KeyValuePair("age", 30L); + var bListProperty = new KeyValuePair("array", new object[] { 3L, 4L, 5L }); + expectedBNode.PropertyMap.Add(bNameProperty); + expectedBNode.PropertyMap.Add(bAgeProperty); + expectedBNode.PropertyMap.Add(bListProperty); + + Assert.NotNull(await graph.QueryAsync("social", "CREATE (:person{name:'a',age:32,array:[0,1,2]})")); + Assert.NotNull(await graph.QueryAsync("social", "CREATE (:person{name:'b',age:30,array:[3,4,5]})")); + + // test array + + ResultSet resultSet = await graph.QueryAsync("social", "WITH [0,1,2] as x return x"); + + // check header + Assert.NotNull(resultSet.Header); + Header header = resultSet.Header; + + List schemaNames = header.SchemaNames; + Assert.NotNull(schemaNames); + Assert.Equal(1, schemaNames.Count); + Assert.Equal("x", schemaNames[0]); + + // check record + Assert.Equal(1, resultSet.Count); + var iterator = resultSet.GetEnumerator(); + Assert.True(iterator.MoveNext()); + NRedisStack.Graph.Record record = iterator.Current; + Assert.False(iterator.MoveNext()); + Assert.Equal(new List() { "x" }, record.Header); + + var x = record.GetValue("x"); + Assert.Equal(new object[] { 0L, 1L, 2L }, x); + + // test collect + resultSet = await graph.QueryAsync("social", "MATCH(n) return collect(n) as x"); + + Assert.NotNull(resultSet.Header); + header = resultSet.Header; + + schemaNames = header.SchemaNames; + Assert.NotNull(schemaNames); + Assert.Equal(1, schemaNames.Count); + Assert.Equal("x", schemaNames[0]); + + // check record + Assert.Equal(1, resultSet.Count); + iterator = resultSet.GetEnumerator(); + Assert.True(iterator.MoveNext()); + record = iterator.Current; + Assert.False(iterator.MoveNext()); + Assert.Equal(new List() { "x" }, record.Header); + var x2 = record.GetValue("x"); + + Assert.Equal(expectedANode.ToString(), x2[0].ToString()); + Assert.Equal(expectedBNode.ToString(), x2[1].ToString()); + + // test unwind + resultSet = await graph.QueryAsync("social", "unwind([0,1,2]) as x return x"); + + Assert.NotNull(resultSet.Header); + header = resultSet.Header; + + schemaNames = header.SchemaNames; + Assert.NotNull(schemaNames); + Assert.Equal(1, schemaNames.Count); + Assert.Equal("x", schemaNames[0]); + + // check record + Assert.Equal(3, resultSet.Count); + iterator = resultSet.GetEnumerator(); + for (long i = 0; i < 3; i++) + { + Assert.True(iterator.MoveNext()); + record = iterator.Current; + Assert.Equal(new List() { "x" }, record.Header); + Assert.Equal(i, (long)record.GetValue("x")); + } + } + + [Fact] + public async Task TestPathAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + List nodes = new List(3); + for (int i = 0; i < 3; i++) + { + Node node = new Node(); + node.Id = i; + node.Labels.Add("L1"); + nodes.Add(node); + } + + List edges = new List(2); + for (int i = 0; i < 2; i++) + { + Edge edge = new Edge(); + edge.Id = i; + edge.RelationshipType = "R1"; + edge.Source = i; + edge.Destination = i + 1; + edges.Add(edge); + } + + var expectedPaths = new HashSet(); + + NRedisStack.Graph.DataTypes.Path path01 = new PathBuilder(2).Append(nodes[0]).Append(edges[0]).Append(nodes[1]).Build(); + NRedisStack.Graph.DataTypes.Path path12 = new PathBuilder(2).Append(nodes[1]).Append(edges[1]).Append(nodes[2]).Build(); + NRedisStack.Graph.DataTypes.Path path02 = new PathBuilder(3).Append(nodes[0]).Append(edges[0]).Append(nodes[1]) + .Append(edges[1]).Append(nodes[2]).Build(); + + expectedPaths.Add(path01); + expectedPaths.Add(path12); + expectedPaths.Add(path02); + + await graph.QueryAsync("social", "CREATE (:L1)-[:R1]->(:L1)-[:R1]->(:L1)"); + + ResultSet resultSet = await graph.QueryAsync("social", "MATCH p = (:L1)-[:R1*]->(:L1) RETURN p"); + + Assert.Equal(expectedPaths.Count, resultSet.Count); + var iterator = resultSet.GetEnumerator(); + + for (int i = 0; i < resultSet.Count; i++) + { + NRedisStack.Graph.DataTypes.Path p = resultSet.ElementAt(i).GetValue("p"); + Assert.Contains(p, expectedPaths); + expectedPaths.Remove(p); + } + } + + [Fact] + public async Task TestNullGraphEntitiesAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + // Create two nodes connected by a single outgoing edge. + Assert.NotNull(await graph.QueryAsync("social", "CREATE (:L)-[:E]->(:L2)")); + // Test a query that produces 1 record with 3 null values. + ResultSet resultSet = await graph.QueryAsync("social", "OPTIONAL MATCH (a:NONEXISTENT)-[e]->(b) RETURN a, e, b"); + Assert.Equal(1, resultSet.Count); + IEnumerator iterator = resultSet.GetEnumerator(); + Assert.True(iterator.MoveNext()); + NRedisStack.Graph.Record record = iterator.Current; + Assert.False(iterator.MoveNext()); + Assert.Equal(new List() { null, null, null }, record.Values); + + // Test a query that produces 2 records, with 2 null values in the second. + resultSet = await graph.QueryAsync("social", "MATCH (a) OPTIONAL MATCH (a)-[e]->(b) RETURN a, e, b ORDER BY ID(a)"); + Assert.Equal(2, resultSet.Count); + + // iterator = resultSet.GetEnumerator(); + // record = iterator.Current; + // Assert.Equal(3, record.Size); + record = resultSet.First(); + Assert.Equal(3, record.Values.Count); + + Assert.NotNull(record.Values[0]); + Assert.NotNull(record.Values[1]); + Assert.NotNull(record.Values[2]); + + // record = iterator.Current; + record = resultSet.Skip(1).Take(1).First(); + Assert.Equal(3, record.Size); + + Assert.NotNull(record.Values[0]); + Assert.Null(record.Values[1]); + Assert.Null(record.Values[2]); + + // Test a query that produces 2 records, the first containing a path and the + // second containing a null value. + resultSet = await graph.QueryAsync("social", "MATCH (a) OPTIONAL MATCH p = (a)-[e]->(b) RETURN p"); + Assert.Equal(2, resultSet.Count); + iterator = resultSet.GetEnumerator(); + + record = resultSet.First(); + Assert.Equal(1, record.Size); + Assert.NotNull(record.Values[0]); + + record = resultSet.Skip(1).First(); + Assert.Equal(1, record.Size); + Assert.Null(record.Values[0]); + } + + [Fact] + public async Task Test64bitnumberAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + long value = 1L << 40; + Dictionary parameters = new Dictionary(); + parameters.Add("val", value); + ResultSet resultSet = await graph.QueryAsync("social", "CREATE (n {val:$val}) RETURN n.val", parameters); + Assert.Equal(1, resultSet.Count); + + // NRedisStack.Graph.Record r = resultSet.GetEnumerator().Current; + // Assert.Equal(value, r.Values[0]); + Assert.Equal(value, resultSet.First().GetValue(0)); + + } + + [Fact] + public async Task TestCachedExecutionAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + await graph.QueryAsync("social", "CREATE (:N {val:1}), (:N {val:2})"); + + // First time should not be loaded from execution cache + Dictionary parameters = new Dictionary(); + parameters.Add("val", 1L); + ResultSet resultSet = await graph.QueryAsync("social", "MATCH (n:N {val:$val}) RETURN n.val", parameters); + Assert.Equal(1, resultSet.Count); + // NRedisStack.Graph.Record r = resultSet.GetEnumerator().Current; + Assert.Equal(parameters["val"], resultSet.First().Values[0]); + Assert.False(resultSet.Statistics.CachedExecution); + + // Run in loop many times to make sure the query will be loaded + // from cache at least once + for (int i = 0; i < 64; i++) + { + resultSet = await graph.QueryAsync("social", "MATCH (n:N {val:$val}) RETURN n.val", parameters); + } + Assert.Equal(1, resultSet.Count); + // r = resultSet.GetEnumerator().Current; + // Assert.Equal(parameters["val"], r.Values[0]); + Assert.Equal(parameters["val"], resultSet.First().Values[0]); + + Assert.True(resultSet.Statistics.CachedExecution); + } + + [Fact] + public async Task TestMapDataTypeAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + Dictionary expected = new Dictionary(); + expected.Add("a", (long)1); + expected.Add("b", "str"); + expected.Add("c", null); + List d = new List(); + d.Add((long)1); + d.Add((long)2); + d.Add((long)3); + expected.Add("d", d); + expected.Add("e", true); + Dictionary f = new Dictionary(); + f.Add("x", (long)1); + f.Add("y", (long)2); + expected.Add("f", f); + ResultSet res = await graph.QueryAsync("social", "RETURN {a:1, b:'str', c:NULL, d:[1,2,3], e:True, f:{x:1, y:2}}"); + Assert.Equal(1, res.Count); + + var iterator = res.GetEnumerator(); + iterator.MoveNext(); + NRedisStack.Graph.Record r = iterator.Current; + var actual = r.Values[0]; + Assert.Equal((object)expected, actual); + } + + [Fact] + public async Task TestGeoPointLatLonAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + ResultSet rs = await graph.QueryAsync("social", "CREATE (:restaurant" + + " {location: point({latitude:30.27822306, longitude:-97.75134723})})"); + Assert.Equal(1, rs.Statistics.NodesCreated); + Assert.Equal(1, rs.Statistics.PropertiesSet); + + AssertTestGeoPoint(graph); + } + + [Fact] + public async Task TestGeoPointLonLatAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + ResultSet rs = await graph.QueryAsync("social", "CREATE (:restaurant" + + " {location: point({longitude:-97.75134723, latitude:30.27822306})})"); + Assert.Equal(1, rs.Statistics.NodesCreated); + Assert.Equal(1, rs.Statistics.PropertiesSet); + + AssertTestGeoPoint(graph); + } + + private async Task AssertTestGeoPointAsync(GraphCommands graph) + { + ResultSet results = await graph.QueryAsync("social", "MATCH (restaurant) RETURN restaurant"); + Assert.Equal(1, results.Count); + var record = results.GetEnumerator(); + record.MoveNext(); + Assert.Equal(1, record.Current.Size); + Assert.Equal(new List() { "restaurant" }, record.Current.Header); + Node node = record.Current.GetValue(0); + var property = node.PropertyMap["location"]; + + Assert.Equal((object)(new Point(30.27822306, -97.75134723)), property); + } + + [Fact] + public async Task timeoutArgumentAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + ResultSet rs = await graph.QueryAsync("social", "UNWIND range(0,100) AS x WITH x AS x WHERE x = 100 RETURN x", 1L); + Assert.Equal(1, rs.Count); + var iterator = rs.GetEnumerator(); + iterator.MoveNext(); + var r = iterator.Current; + Assert.Equal(100l, (long)r.Values[0]); + } + + [Fact] + public async Task TestCachedExecutionReadOnlyAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + await graph.QueryAsync("social", "CREATE (:N {val:1}), (:N {val:2})"); + + // First time should not be loaded from execution cache + Dictionary parameters = new Dictionary(); + parameters.Add("val", 1L); + ResultSet resultSet = await graph.RO_QueryAsync("social", "MATCH (n:N {val:$val}) RETURN n.val", parameters); + Assert.Equal(1, resultSet.Count); + var iterator = resultSet.GetEnumerator(); + iterator.MoveNext(); + NRedisStack.Graph.Record r = iterator.Current; + Assert.Equal(parameters["val"], r.Values[0]); + Assert.False(resultSet.Statistics.CachedExecution); + + // Run in loop many times to make sure the query will be loaded + // from cache at least once + for (int i = 0; i < 64; i++) + { + resultSet = await graph.RO_QueryAsync("social", "MATCH (n:N {val:$val}) RETURN n.val", parameters); + } + Assert.Equal(1, resultSet.Count); + iterator = resultSet.GetEnumerator(); + iterator.MoveNext(); + r = iterator.Current; + Assert.Equal(parameters["val"], r.Values[0]); + Assert.True(resultSet.Statistics.CachedExecution); + } + + [Fact] + public async Task TestSimpleReadOnlyAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + await graph.QueryAsync("social", "CREATE (:person{name:'filipe',age:30})"); + ResultSet rsRo = await graph.RO_QueryAsync("social", "MATCH (a:person) WHERE (a.name = 'filipe') RETURN a.age"); + Assert.Equal(1, rsRo.Count); + var iterator = rsRo.GetEnumerator(); + iterator.MoveNext(); + var r = iterator.Current; + Assert.Equal("30", r.Values[0].ToString()); + } + + [Fact] + public async Task TestProfileAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + Assert.NotNull(await graph.QueryAsync("social", "CREATE (:person{name:'roi',age:32})")); + Assert.NotNull(await graph.QueryAsync("social", "CREATE (:person{name:'amit',age:30})")); + + var profile = await graph.ProfileAsync("social", + "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(b)"); + Assert.NotEmpty(profile); + foreach (var p in profile) + { + Assert.NotNull(p); + } + } + + [Fact] + public async Task TestExplainAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + Assert.NotNull(await graph.ProfileAsync("social", "CREATE (:person{name:'roi',age:32})")); + Assert.NotNull(await graph.ProfileAsync("social", "CREATE (:person{name:'amit',age:30})")); + + var explain = await graph.ExplainAsync("social", + "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(b)"); + Assert.NotEmpty(explain); + foreach (var e in explain) + { + Assert.NotNull(e); + } + } + + [Fact] + public async Task TestSlowlogAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + Assert.NotNull(await graph.ProfileAsync("social", "CREATE (:person{name:'roi',age:32})")); + Assert.NotNull(await graph.ProfileAsync("social", "CREATE (:person{name:'amit',age:30})")); + + List> slowlogs = await graph.SlowlogAsync("social"); + Assert.Equal(2, slowlogs.Count); + slowlogs.ForEach(sl => Assert.NotEmpty(sl)); + slowlogs.ForEach(sl => sl.ForEach(s => Assert.NotNull(s))); + } + + [Fact] + public async Task TestListAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + Assert.Empty(await graph.ListAsync()); + + await graph.QueryAsync("social", "CREATE (:person{name:'filipe',age:30})"); + + Assert.Equal(new List() { "social" }, await graph.ListAsync()); + } + + [Fact] + public async Task TestConfigAsync() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + await graph.QueryAsync("social", "CREATE (:person{name:'filipe',age:30})"); + + string name = "RESULTSET_SIZE"; + var existingValue = (await graph.ConfigGetAsync(name))[name]; + + Assert.True(await graph.ConfigSetAsync(name, 250L)); + + var actual = await graph.ConfigGetAsync(name); + Assert.Equal(actual.Count, 1); + Assert.Equal("250", actual[name].ToString()); + + await graph.ConfigSetAsync(name, existingValue != null ? existingValue.ToString() : -1); + } + + [Fact] + public async Task TestModulePrefixsAsync() + { + IDatabase db1 = redisFixture.Redis.GetDatabase(); + IDatabase db2 = redisFixture.Redis.GetDatabase(); + + var graph1 = db1.GRAPH(); + var graph2 = db2.GRAPH(); + + Assert.NotEqual(graph1.GetHashCode(), graph2.GetHashCode()); + } + + [Fact] + public void TestParseInfinity() + { + IDatabase db = redisFixture.Redis.GetDatabase(); + db.Execute("FLUSHALL"); + var graph = db.GRAPH(); + ResultSet rs = graph.Query("db", "RETURN 10^100000"); + Assert.Equal(1, rs.Count()); + var iterator = rs.GetEnumerator(); + iterator.MoveNext(); + var r = iterator.Current; + Assert.Equal(double.PositiveInfinity, r.Values[0]); + } + + [Fact] + public async Task TestModulePrefixs1Async() + { + { + var conn = ConnectionMultiplexer.Connect("localhost"); + IDatabase db = conn.GetDatabase(); + + var graph = db.GRAPH(); + // ... + conn.Dispose(); + } + + { + var conn = ConnectionMultiplexer.Connect("localhost"); + IDatabase db = conn.GetDatabase(); + + var graph = db.GRAPH(); + // ... + conn.Dispose(); + } + + } + + #endregion + +} \ No newline at end of file diff --git a/tests/NRedisStack.Tests/Graph/Utils/PathBuilder.cs b/tests/NRedisStack.Tests/Graph/Utils/PathBuilder.cs new file mode 100644 index 00000000..070773f4 --- /dev/null +++ b/tests/NRedisStack.Tests/Graph/Utils/PathBuilder.cs @@ -0,0 +1,64 @@ +using NRedisStack.Graph.DataTypes; +namespace NRedisStack.Tests.Graph +{ + public sealed class PathBuilder + { + private readonly List _nodes; + private readonly List _edges; + private Type _currentAppendClass; + + public PathBuilder() + { + _nodes = new List(); + _edges = new List(); + + _currentAppendClass = typeof(Node); + } + + public PathBuilder(int nodesCount) + { + _nodes = new List(nodesCount); + _edges = new List(nodesCount - 1 >= 0 ? nodesCount - 1 : 0); + + _currentAppendClass = typeof(Node); + } + + public PathBuilder Append(Edge edge) + { + if (_currentAppendClass != typeof(Edge)) + { + throw new ArgumentException("Path builder expected Node but was Edge."); + } + + _edges.Add(edge); + + _currentAppendClass = typeof(Node); + + return this; + } + + public PathBuilder Append(Node node) + { + if (_currentAppendClass != typeof(Node)) + { + throw new ArgumentException("Path builder expected Edge but was Node."); + } + + _nodes.Add(node); + + _currentAppendClass = typeof(Edge); + + return this; + } + + public NRedisStack.Graph.DataTypes.Path Build() + { + if (_nodes.Count != _edges.Count + 1) + { + throw new ArgumentException("Path builder nodes count should be edge count + 1"); + } + + return new NRedisStack.Graph.DataTypes.Path(_nodes, _edges); + } + } +} \ No newline at end of file diff --git a/tests/NRedisStack.Tests/Graph/Utils/PathBuilderTest.cs b/tests/NRedisStack.Tests/Graph/Utils/PathBuilderTest.cs new file mode 100644 index 00000000..8dd7a921 --- /dev/null +++ b/tests/NRedisStack.Tests/Graph/Utils/PathBuilderTest.cs @@ -0,0 +1,48 @@ +using NRedisStack.Graph.DataTypes; +using Xunit; + +namespace NRedisStack.Tests.Graph +{ + public class PathBuilderTest + { + [Fact] + public void TestPathBuilderSizeException() + { + var thrownException = Assert.Throws(()=> + { + var pathBuilder = new PathBuilder(0); + + pathBuilder.Build(); + }); + + Assert.Equal("Path builder nodes count should be edge count + 1", thrownException.Message); + } + + [Fact] + public void TestPathBuilderArgumentsExceptionNodeExpected() + { + var thrownException = Assert.Throws(() => + { + var builder = new PathBuilder(0); + + builder.Append(new Edge()); + }); + + Assert.Equal("Path builder expected Node but was Edge.", thrownException.Message); + } + + [Fact] + public void TestPathBuilderArgumentsExceptionPathExpected() + { + var thrownException = Assert.Throws(() => + { + var builder = new PathBuilder(0); + + builder.Append(new Node()); + builder.Append(new Node()); + }); + + Assert.Equal("Path builder expected Edge but was Node.", thrownException.Message); + } + } +} \ No newline at end of file