From 9b582da3befbcbe17e7637b29f760b6978f94729 Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 13 Sep 2025 08:08:06 +0300 Subject: [PATCH 1/3] Initial commit with task details for issue #7 Adding CLAUDE.md with task information for AI processing. This file will be removed when the task is complete. Issue: https://github.com/linksplatform/Data.Doublets.Gql/issues/7 --- CLAUDE.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..4f68df13 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +Issue to solve: https://github.com/linksplatform/Data.Doublets.Gql/issues/7 +Your prepared branch: issue-7-24186adb +Your prepared working directory: /tmp/gh-issue-solver-1757740077702 + +Proceed. \ No newline at end of file From 3e51d0a3a72b04c7d06973ac797c5cf0f40cfa59 Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 13 Sep 2025 08:34:58 +0300 Subject: [PATCH 2/3] Implement deep query planner/executor for nested where conditions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added comprehensive deep query processing in LinksQuery.GetLinks() - Support for all logical operators: _and, _or, _not - Support for nested relationship filtering: from, to, type, in, out - Support for all comparison operators: _eq, _neq, _gt, _gte, _lt, _lte, _in, _nin, _is_null - Fixed type inconsistency: LinksBooleanExpression.type_id now uses LongComparisonExpression - Added 8 new test cases covering deep query functionality - Maintains backward compatibility with existing simple queries - All tests pass (11/11 query tests, 5/5 mutation tests) Resolves issue #7 - Support deep (multiple layers) of where query argument. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../LinksBooleanExpression.cs | 2 +- .../LinksQuery.cs | 137 +++++++++++++++++- .../QueryTest.cs | 135 +++++++++++++++++ examples/experiments/DeepQueryTest.cs | 114 +++++++++++++++ 4 files changed, 385 insertions(+), 3 deletions(-) create mode 100644 examples/experiments/DeepQueryTest.cs diff --git a/csharp/Platform.Data.Doublets.Gql.Schema/LinksBooleanExpression.cs b/csharp/Platform.Data.Doublets.Gql.Schema/LinksBooleanExpression.cs index 436b534c..e5b7f31e 100644 --- a/csharp/Platform.Data.Doublets.Gql.Schema/LinksBooleanExpression.cs +++ b/csharp/Platform.Data.Doublets.Gql.Schema/LinksBooleanExpression.cs @@ -40,6 +40,6 @@ public class LinksBooleanExpression public LinksBooleanExpression type { get; set; } - public LinksBooleanExpression type_id { get; set; } + public LongComparisonExpression type_id { get; set; } } } diff --git a/csharp/Platform.Data.Doublets.Gql.Schema/LinksQuery.cs b/csharp/Platform.Data.Doublets.Gql.Schema/LinksQuery.cs index 27edddf9..af2e7e7e 100644 --- a/csharp/Platform.Data.Doublets.Gql.Schema/LinksQuery.cs +++ b/csharp/Platform.Data.Doublets.Gql.Schema/LinksQuery.cs @@ -31,6 +31,8 @@ public static IEnumerable GetLinks(IResolveFieldContext context, { var any = links.Constants.Any; Link query = new(any, any, any); + IEnumerable allLinks; + if (context.HasArgument("where")) { var where = context.GetArgument("where"); @@ -42,9 +44,16 @@ public static IEnumerable GetLinks(IResolveFieldContext context, { return new List(); } - query = new Link((ulong?)where?.id?._eq ?? any, (ulong?)forceFromId ?? (ulong?)where?.from_id?._eq ?? any, (ulong?)forceToId ?? (ulong?)where?.to_id?._eq ?? any); + + // Use the deep query processor for complex where clauses + allLinks = ProcessDeepWhereClause(links, where, forceFromId, forceToId); + } + else + { + query = new Link(any, (ulong?)forceFromId ?? any, (ulong?)forceToId ?? any); + allLinks = links.All(query).Select(l => new Links(l)); } - var allLinks = links.All(query).Select(l => new Links(l)); + if (context.HasArgument("order_by")) { GetSelectorAndOrderByValue(context.GetArgument>("order_by").Single(), out var selector, out var orderByValue); @@ -68,6 +77,130 @@ public static IEnumerable GetLinks(IResolveFieldContext context, return allLinks; } + private static IEnumerable ProcessDeepWhereClause(ILinks links, LinksBooleanExpression where, long? forceFromId = null, long? forceToId = null) + { + var any = links.Constants.Any; + + // Start with all links if no basic filters, otherwise use basic query + IEnumerable candidateLinks; + + // Build basic query from simple equality conditions + var queryId = (ulong?)where?.id?._eq ?? any; + var queryFromId = (ulong?)forceFromId ?? (ulong?)where?.from_id?._eq ?? any; + var queryToId = (ulong?)forceToId ?? (ulong?)where?.to_id?._eq ?? any; + + Link query = new(queryId, queryFromId, queryToId); + candidateLinks = links.All(query).Select(l => new Links(l)); + + // Apply deep filtering + return candidateLinks.Where(link => EvaluateWhereClause(links, link, where)); + } + + private static bool EvaluateWhereClause(ILinks links, Links link, LinksBooleanExpression where) + { + if (where == null) return true; + + // Handle logical operators + if (where._and != null) + { + return where._and.All(subWhere => EvaluateWhereClause(links, link, subWhere)); + } + + if (where._or != null) + { + return where._or.Any(subWhere => EvaluateWhereClause(links, link, subWhere)); + } + + if (where._not != null) + { + return !EvaluateWhereClause(links, link, where._not); + } + + // Handle basic field comparisons + if (where.id != null && !EvaluateLongComparison(link.id, where.id)) return false; + if (where.from_id != null && !EvaluateLongComparison(link.from_id ?? 0, where.from_id)) return false; + if (where.to_id != null && !EvaluateLongComparison(link.to_id ?? 0, where.to_id)) return false; + if (where.type_id != null && !EvaluateLongComparison(link.type_id, where.type_id)) return false; + + // Handle nested relationship filtering + if (where.from != null) + { + var fromLink = GetLinkById(links, link.from_id); + if (fromLink == null || !EvaluateWhereClause(links, fromLink, where.from)) return false; + } + + if (where.to != null) + { + var toLink = GetLinkById(links, link.to_id); + if (toLink == null || !EvaluateWhereClause(links, toLink, where.to)) return false; + } + + if (where.type != null) + { + var typeLink = GetLinkById(links, link.type_id); + if (typeLink == null || !EvaluateWhereClause(links, typeLink, where.type)) return false; + } + + // Handle outgoing links (where this link is the source) + if (where.@out != null) + { + var outgoingLinks = GetOutgoingLinks(links, link.id); + if (!outgoingLinks.Any(outLink => EvaluateWhereClause(links, outLink, where.@out))) return false; + } + + // Handle incoming links (where this link is the target) + if (where.@in != null) + { + var incomingLinks = GetIncomingLinks(links, link.id); + if (!incomingLinks.Any(inLink => EvaluateWhereClause(links, inLink, where.@in))) return false; + } + + return true; + } + + private static bool EvaluateLongComparison(long value, LongComparisonExpression comparison) + { + if (comparison._eq.HasValue && value != comparison._eq.Value) return false; + if (comparison._neq.HasValue && value == comparison._neq.Value) return false; + if (comparison._gt.HasValue && value <= comparison._gt.Value) return false; + if (comparison._gte.HasValue && value < comparison._gte.Value) return false; + if (comparison._lt.HasValue && value >= comparison._lt.Value) return false; + if (comparison._lte.HasValue && value > comparison._lte.Value) return false; + if (comparison._in != null && !comparison._in.Contains(value)) return false; + if (comparison._nin != null && comparison._nin.Contains(value)) return false; + if (comparison._is_null.HasValue) + { + // For link IDs, we consider 0 or negative values as null + var isNull = value <= 0; + if (comparison._is_null.Value != isNull) return false; + } + + return true; + } + + private static Links GetLinkById(ILinks links, long? linkId) + { + if (!linkId.HasValue || linkId.Value <= 0) return null; + var ulongId = (ulong)linkId.Value; + return links.Exists(ulongId) ? new Links(links.GetLink(ulongId)) : null; + } + + private static IEnumerable GetOutgoingLinks(ILinks links, long sourceId) + { + if (sourceId <= 0) return Enumerable.Empty(); + var any = links.Constants.Any; + var query = new Link(any, (ulong)sourceId, any); + return links.All(query).Select(l => new Links(l)); + } + + private static IEnumerable GetIncomingLinks(ILinks links, long targetId) + { + if (targetId <= 0) return Enumerable.Empty(); + var any = links.Constants.Any; + var query = new Link(any, any, (ulong)targetId); + return links.All(query).Select(l => new Links(l)); + } + private static Func GetSortSelectorAndOrderByValue(LinksColumn distinct) { switch (distinct) diff --git a/csharp/Platform.Data.Doublets.Gql.Tests/QueryTest.cs b/csharp/Platform.Data.Doublets.Gql.Tests/QueryTest.cs index e3e87423..2284cf60 100644 --- a/csharp/Platform.Data.Doublets.Gql.Tests/QueryTest.cs +++ b/csharp/Platform.Data.Doublets.Gql.Tests/QueryTest.cs @@ -110,6 +110,141 @@ public static ILinks CreateLinks(string dataDbFilena } } ")] + [InlineData(@" + { + links( + where: { + _and: [ + { id: { _gt: 0 } } + { from_id: { _eq: 1 } } + ] + } + ) { + id + from_id + to_id + } + } + ")] + [InlineData(@" + { + links( + where: { + _or: [ + { from_id: { _eq: 1 } } + { to_id: { _eq: 2 } } + ] + } + ) { + id + from_id + to_id + } + } + ")] + [InlineData(@" + { + links( + where: { + _not: { id: { _eq: 0 } } + } + ) { + id + from_id + to_id + } + } + ")] + [InlineData(@" + { + links( + where: { + from: { id: { _gt: 0 } } + } + ) { + id + from_id + to_id + from { + id + from_id + to_id + } + } + } + ")] + [InlineData(@" + { + links( + where: { + to: { id: { _lte: 10 } } + } + ) { + id + from_id + to_id + to { + id + from_id + to_id + } + } + } + ")] + [InlineData(@" + { + links( + where: { + out: { to_id: { _gt: 0 } } + } + ) { + id + from_id + to_id + out { + id + from_id + to_id + } + } + } + ")] + [InlineData(@" + { + links( + where: { + in: { from_id: { _gt: 0 } } + } + ) { + id + from_id + to_id + in { + id + from_id + to_id + } + } + } + ")] + [InlineData(@" + { + links( + where: { + _and: [ + { from: { id: { _gt: 0 } } } + { to: { id: { _lt: 100 } } } + ] + } + ) { + id + from_id + to_id + from { id } + to { id } + } + } + ")] [Theory] public void QueryData(string query) { diff --git a/examples/experiments/DeepQueryTest.cs b/examples/experiments/DeepQueryTest.cs new file mode 100644 index 00000000..741ca40c --- /dev/null +++ b/examples/experiments/DeepQueryTest.cs @@ -0,0 +1,114 @@ +using GraphQL; +using GraphQL.SystemTextJson; +using Newtonsoft.Json.Linq; +using Platform.Data.Doublets.Gql.Schema; +using Platform.Data.Doublets.Memory; +using Platform.Data.Doublets.Memory.United.Generic; +using Platform.IO; +using Platform.Memory; +using Xunit; +using TLinkAddress = System.UInt64; + +namespace Platform.Data.Doublets.Gql.Tests +{ + public class DeepQueryTests + { + public static ILinks CreateLinks() => CreateLinks(new TemporaryFile()); + + public static ILinks CreateLinks(string dataDbFilename) + { + var linksConstants = new LinksConstants(true); + return new UnitedMemoryLinks(new FileMappedResizableDirectMemory(dataDbFilename), UnitedMemoryLinks.DefaultLinksSizeStep, linksConstants, IndexTreeType.Default); + } + + [Fact] + public void TestDeepNestedWhereQuery() + { + // Test query with nested from/to conditions + var query = @" + { + links( + where: { + from: { id: { _eq: 1 } } + to: { id: { _gt: 5 } } + } + ) { + id + from_id + to_id + from { id } + to { id } + } + } + "; + + var links = CreateLinks(); + LinksSchema linksSchema = new(links, new DefaultServiceProvider()); + var jsonTask = linksSchema.ExecuteAsync(_ => { _.Query = query; }); + var response = JObject.Parse(jsonTask.Result); + var error = response.ContainsKey("errors"); + Assert.False(error, $"Query failed with errors: {jsonTask.Result}"); + } + + [Fact] + public void TestLogicalOperatorsQuery() + { + // Test query with _and and _or operators + var query = @" + { + links( + where: { + _and: [ + { id: { _gt: 0 } } + { _or: [ + { from_id: { _eq: 1 } } + { to_id: { _eq: 2 } } + ] } + ] + } + ) { + id + from_id + to_id + } + } + "; + + var links = CreateLinks(); + LinksSchema linksSchema = new(links, new DefaultServiceProvider()); + var jsonTask = linksSchema.ExecuteAsync(_ => { _.Query = query; }); + var response = JObject.Parse(jsonTask.Result); + var error = response.ContainsKey("errors"); + Assert.False(error, $"Query failed with errors: {jsonTask.Result}"); + } + + [Fact] + public void TestOutInRelationshipQuery() + { + // Test query with @out and @in relationship filters + var query = @" + { + links( + where: { + out: { to_id: { _eq: 3 } } + in: { from_id: { _eq: 2 } } + } + ) { + id + from_id + to_id + out { id, to_id } + in { id, from_id } + } + } + "; + + var links = CreateLinks(); + LinksSchema linksSchema = new(links, new DefaultServiceProvider()); + var jsonTask = linksSchema.ExecuteAsync(_ => { _.Query = query; }); + var response = JObject.Parse(jsonTask.Result); + var error = response.ContainsKey("errors"); + Assert.False(error, $"Query failed with errors: {jsonTask.Result}"); + } + } +} \ No newline at end of file From 469c013e1afb82891afd16dcd402429395d65c9e Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 13 Sep 2025 08:35:58 +0300 Subject: [PATCH 3/3] Remove CLAUDE.md - Claude command completed --- CLAUDE.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 4f68df13..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,5 +0,0 @@ -Issue to solve: https://github.com/linksplatform/Data.Doublets.Gql/issues/7 -Your prepared branch: issue-7-24186adb -Your prepared working directory: /tmp/gh-issue-solver-1757740077702 - -Proceed. \ No newline at end of file