diff --git a/Hexastore/Query/ObjectQueryExecutor.cs b/Hexastore/Query/ObjectQueryExecutor.cs index fa1c613..a394280 100644 --- a/Hexastore/Query/ObjectQueryExecutor.cs +++ b/Hexastore/Query/ObjectQueryExecutor.cs @@ -1,12 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Threading; using Hexastore.Errors; using Hexastore.Graph; -using Hexastore.Web.Errors; using Newtonsoft.Json.Linq; namespace Hexastore.Query @@ -23,7 +19,8 @@ public ObjectQueryExecutor() public ObjectQueryResponse Query(ObjectQueryModel query, IStoreGraph graph) { query.PageSize = query.PageSize != 0 ? query.PageSize : Constants.DefaultPageSize; - if (query.Id != null) { + if (query.Id != null) + { var item = graph.S(query.Id).FirstOrDefault(); return new ObjectQueryResponse { @@ -32,12 +29,14 @@ public ObjectQueryResponse Query(ObjectQueryModel query, IStoreGraph graph) }; } - if (query.Filter == null) { + if (query.Filter == null) + { throw _storeErrors.AtLeastOneFilter; } var firstFilter = query.Filter.FirstOrDefault(); - if (firstFilter.Key == null) { + if (firstFilter.Key == null) + { throw _storeErrors.AtLeastOneFilter; } @@ -49,23 +48,27 @@ public ObjectQueryResponse Query(ObjectQueryModel query, IStoreGraph graph) : null; var rsp = CreateConstraint(graph, firstFilter.Key, firstFilter.Value, cTriple); - foreach (var filter in query.Filter.Skip(1)) { + foreach (var filter in query.Filter.Skip(1)) + { rsp = ApplyConstraint(rsp, graph, filter.Key, filter.Value); } - - if (query.HasObject != null) { - foreach (var obj in query.HasObject) { + if (query.HasObject != null) + { + Console.WriteLine($"Apply Outgoing X: {query.HasObject.Length}"); + foreach (var obj in query.HasObject) + { rsp = ApplyOutgoing(rsp, graph, obj); } } - - if (query.HasSubject != null) { - foreach (var sub in query.HasSubject) { + if (query.HasSubject != null) + { + foreach (var sub in query.HasSubject) + { rsp = ApplyIncoming(rsp, graph, sub); } } - - if (query.Aggregates == null || query.Aggregates.Length == 0) { + if (query.Aggregates == null || query.Aggregates.Length == 0) + { var responseTriples = rsp.Take(query.PageSize).ToArray(); var cont = responseTriples.Length < query.PageSize ? null : responseTriples.LastOrDefault(); var queryResponse = new ObjectQueryResponse @@ -92,8 +95,10 @@ public ObjectQueryResponse Query(ObjectQueryModel query, IStoreGraph graph) private ObjectQueryResponse ApplyAggregates(IEnumerable rsp, AggregateQuery[] aggregates) { var responses = new List(); - foreach (var aggregate in aggregates) { - switch (aggregate.Type) { + foreach (var aggregate in aggregates) + { + switch (aggregate.Type) + { case AggregateType.Count: var count = rsp.Count(); responses.Add(count); @@ -114,7 +119,8 @@ private ObjectQueryResponse ApplyAggregates(IEnumerable rsp, AggregateQu private IEnumerable ApplyConstraint(IEnumerable rsp, IGraph graph, string key, QueryUnit value) { var input = new JValue(value.Value); - switch (value.Operator) { + switch (value.Operator) + { case "eq": return rsp.Where(x => graph.Exists(x.Subject, key, TripleObject.FromData(value.Value.ToString()))); case "gt": @@ -135,7 +141,8 @@ private IEnumerable ApplyConstraint(IEnumerable rsp, IGraph grap private IEnumerable CreateConstraint(IStoreGraph graph, string key, QueryUnit value, Triple continuation) { var input = new JValue(value.Value); - switch (value.Operator) { + switch (value.Operator) + { case "eq": return graph.PO(key, TripleObject.FromData(value.Value.ToString()), continuation); case "gt": @@ -152,7 +159,8 @@ private IEnumerable CreateConstraint(IStoreGraph graph, string key, Quer private Func Comparator(QueryUnit value) { var input = new JValue(value.Value); - switch (value.Operator) { + switch (value.Operator) + { case "gt": return (Triple x) => { @@ -190,66 +198,140 @@ private Func Comparator(QueryUnit value) private IEnumerable ApplyOutgoing(IEnumerable source, IStoreGraph graph, LinkQuery link) { - if (link == null) { + if (link == null) + { return source; } - if (string.IsNullOrEmpty(link.Path)) { + if (string.IsNullOrEmpty(link.Path)) + { throw _storeErrors.PathEmpty; } var paths = link.Path.Split(LinkDelimiterArray); - + var matchingTargets = new HashSet(); + var previouslySeenTargets = new HashSet(); var matched = source.Where(x => { - IEnumerable targets; + Dictionary> targets; var segments = new Queue(paths); - if (link.Level == 0) { - targets = GetByLink(graph, new string[] { x.Subject }, segments, (gx, sx, seg) => GetSubjectLink(gx, sx, seg)); - } else { - targets = GetByLevel(graph, new string[] { x.Subject }, link.Level, true); + if (link.Level == 0) + { + targets = GetByLink(graph, new string[] { x.Subject }, segments, GetSubjectLink, new HashSet(), previouslySeenTargets); + } + else + { + targets = GetByLevel(graph, new string[] { x.Subject }, link.Level, true, new HashSet(), previouslySeenTargets); } - // todo: use DP to remember nodes that have matched before - return targets.Any(t => SubjectMatch(t, graph, link.Target)); + foreach (var target in targets) + { + if (matchingTargets.Contains(target.Key)) + { + foreach (var node in target.Value) + { + matchingTargets.Add(node); + previouslySeenTargets.Add(node); + } + return true; + } + if (!SubjectMatch(target.Key, graph, link.Target)) + { + previouslySeenTargets.Add(target.Key); + foreach (var node in target.Value) + { + previouslySeenTargets.Add(node); + } + continue; + }; + matchingTargets.Add(target.Key); + previouslySeenTargets.Add(target.Key); + foreach (var node in target.Value) + { + matchingTargets.Add(node); + previouslySeenTargets.Add(node); + } + return true; + } + + return false; }); return matched; } private IEnumerable ApplyIncoming(IEnumerable source, IStoreGraph graph, LinkQuery link) { - if (link == null) { + if (link == null) + { return source; } - if (string.IsNullOrEmpty(link.Path)) { + if (string.IsNullOrEmpty(link.Path)) + { throw _storeErrors.PathEmpty; } var paths = link.Path.Split(LinkDelimiterArray).Reverse(); - + var matchingTargets = new HashSet(); + var previouslySeenTargets = new HashSet(); var matched = source.Where(x => { - IEnumerable targets; - + Dictionary> targets; var segments = new Queue(paths); - if (link.Level == 0) { - targets = GetByLink(graph, new string[] { x.Subject }, segments, (gx, sx, seg) => GetObjectLink(gx, sx, seg)); - } else { - targets = GetByLevel(graph, new string[] { x.Subject }, link.Level, false); + if (link.Level == 0) + { + targets = GetByLink(graph, new string[] { x.Subject }, segments, GetObjectLink, new HashSet(), previouslySeenTargets); + } + else + { + targets = GetByLevel(graph, new string[] { x.Subject }, link.Level, false, new HashSet(), previouslySeenTargets); } - return targets.Any(t => SubjectMatch(t, graph, link.Target)); + + foreach (var target in targets) + { + if (matchingTargets.Contains(target.Key)) + { + foreach (var node in target.Value) + { + matchingTargets.Add(node); + previouslySeenTargets.Add(node); + } + return true; + } + if (!SubjectMatch(target.Key, graph, link.Target)) + { + previouslySeenTargets.Add(target.Key); + foreach (var node in target.Value) + { + previouslySeenTargets.Add(node); + } + continue; + }; + matchingTargets.Add(target.Key); + previouslySeenTargets.Add(target.Key); + foreach (var node in target.Value) + { + matchingTargets.Add(node); + previouslySeenTargets.Add(node); + } + return true; + } + + return false; }); return matched; } private bool SubjectMatch(string t, IStoreGraph graph, ObjectQueryModel target) { - if (target.Id != null) { + if (target.Id != null) + { return t == target.Id; } var result = true; - foreach (var filter in target.Filter) { - switch (filter.Value.Operator) { + foreach (var filter in target.Filter) + { + switch (filter.Value.Operator) + { case "eq": result &= graph.Exists(t, filter.Key, TripleObject.FromData(filter.Value.Value.ToString())); break; @@ -268,34 +350,89 @@ private bool SubjectMatch(string t, IStoreGraph graph, ObjectQueryModel target) return result; } - private IEnumerable GetByLink(IStoreGraph graph, IEnumerable sources, Queue segments, Func> f) + private Dictionary> GetByLink(IStoreGraph graph, + IEnumerable sources, + Queue segments, Func> f, + HashSet nodesVisited, + HashSet earlyExitNodes) { - if (segments.Count == 0) { - return sources; - } else { + if (segments.Count == 0) + { + var dict = new Dictionary>(); + foreach (var source in sources) + { + dict.Add(source, nodesVisited); + } + return dict; + } + else + { + var nodesVisitedClone = new HashSet(nodesVisited); var segment = segments.Dequeue(); IEnumerable next = new List(); - foreach (var source in sources) { - next = next.Concat(f(graph, source, segment)); + var dict = new Dictionary>(); + foreach (var source in sources) + { + if (earlyExitNodes.Contains(source)) + { + dict.Add(source, nodesVisited); + } + else + { + nodesVisitedClone.Add(source); + next = next.Concat(f(graph, source, segment)); + } + } + + if (next.Any()) + { + var nextNodes = GetByLink(graph, next, segments, f, nodesVisitedClone, earlyExitNodes); + foreach (var node in nextNodes) + { + dict.Add(node.Key, node.Value); + } } - return GetByLink(graph, next, segments, f); + + return dict; } } - private IEnumerable GetByLevel(IStoreGraph graph, IEnumerable sources, int level, bool isOutgoing) + private Dictionary> GetByLevel(IStoreGraph graph, IEnumerable sources, int level, bool isOutgoing, HashSet nodesVisited, HashSet earlyExitNodes) { - if (level == 0) { - return Enumerable.Empty(); + if (level == 0) + { + return new Dictionary>(); } IEnumerable next = new List(); - foreach (var source in sources) { - var items = isOutgoing - ? graph.S(source).Where(x => x.Object.IsID).Select(y => y.Object.Id).Distinct() - : graph.O(source).Select(y => y.Subject).Distinct(); - var targets = GetByLevel(graph, items, level - 1, isOutgoing); - next = next.Concat(targets); + var nodesVisitedClone = new HashSet(nodesVisited); + var dict = new Dictionary>(); + foreach (var source in sources) + { + if (!dict.ContainsKey(source)) + { + dict.Add(source, nodesVisited); + } + if (!earlyExitNodes.Contains(source)) + { + nodesVisitedClone.Add(source); + var items = isOutgoing + ? graph.S(source).Where(x => x.Object.IsID).Select(y => y.Object.Id).Distinct() + : graph.O(source).Select(y => y.Subject).Distinct(); + + // prevent revisiting nodes when circular references exist + var unvisited = items.Where(i => !nodesVisitedClone.Contains(i)); + var targets = GetByLevel(graph, unvisited, level - 1, isOutgoing, nodesVisitedClone, earlyExitNodes); + foreach (var target in targets) + { + if (!dict.ContainsKey(target.Key)) + { + dict.Add(target.Key, target.Value); + } + } + } } - return sources.Concat(next).Distinct(); + + return dict; } private IEnumerable GetSubjectLink(IStoreGraph graph, string source, string segment)