From b369d8c3fa5433023665cb6450eba53702e968b1 Mon Sep 17 00:00:00 2001 From: David Driscoll Date: Tue, 9 Aug 2022 04:11:54 -0400 Subject: [PATCH] Allow empty filter expressions to be visited (#5214) Co-authored-by: Pascal Senn --- .../Expressions/QueryableCombinator.cs | 6 +- .../Visitor/FilterOperationCombinator.cs | 2 +- .../Visitor/FilterOperationCombinator~1.cs | 2 +- .../src/Data/Filters/Visitor/FilterVisitor.cs | 2 +- .../Filters/Visitor/FilterVisitorBase`2.cs | 2 +- .../QueryableFilterCombinatorTests.cs | 58 +++++++++++++++++++ ...mbinatorTests.Create_Empty_Expression.snap | 12 ++++ ...DeepFilterObjectTwoProjections_NET6_0.snap | 17 ++++-- ...DeepFilterObjectTwoProjections_NET7_0.snap | 17 ++++-- ...ObjectDifferentLevelProjection_NET6_0.snap | 17 ++++-- ...ObjectDifferentLevelProjection_NET7_0.snap | 17 ++++-- .../Data/Driver/MongoDbFilterDefinition.cs | 17 ++++++ .../src/Data/Driver/MongoDbFilterOperation.cs | 1 - .../MongoDbFilterScopeExtensions.cs | 17 +----- .../MongoFilterVisitorContextExtensions.cs | 12 ++-- .../List/MongoDbListOperationHandlerBase.cs | 8 +-- .../Data/Filters/MongoDbFilterCombinator.cs | 9 +-- .../src/Data/Filters/MongoDbFilterProvider.cs | 13 ++--- .../MongoDbFilterCombinatorTests.cs | 54 +++++++++++++++++ ...mbinatorTests.Create_Empty_Expression.snap | 12 ++++ .../src/Data/Execution/Neo4JExecutable.cs | 10 +++- .../Extensions/Neo4JFilterScopeExtensions.cs | 16 ++--- .../Neo4JFilterVisitorContextExtensions.cs | 14 ++--- .../List/Neo4JListOperationHandlerBase.cs | 5 +- .../Data/Filtering/Neo4JFilterCombinator.cs | 9 ++- .../src/Data/Filtering/Neo4JFilterProvider.cs | 10 ++-- .../src/Data/Language/CompoundCondition.cs | 4 +- .../Neo4J/src/Data/Language/Where.cs | 4 +- .../Neo4JFilterCombinatorTests.cs | 48 +++++++++++++++ ...mbinatorTests.Create_Empty_Expression.snap | 12 ++++ 30 files changed, 313 insertions(+), 114 deletions(-) create mode 100644 src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterCombinatorTests.cs create mode 100644 src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterCombinatorTests.Create_Empty_Expression.snap create mode 100644 src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/MongoDbFilterCombinatorTests.cs create mode 100644 src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/__snapshots__/MongoDbFilterCombinatorTests.Create_Empty_Expression.snap create mode 100644 src/HotChocolate/Neo4J/test/HotChocolate.Data.Neo4J.Filtering.Tests/Neo4JFilterCombinatorTests.cs create mode 100644 src/HotChocolate/Neo4J/test/HotChocolate.Data.Neo4J.Filtering.Tests/__snapshots__/Neo4JFilterCombinatorTests.Create_Empty_Expression.snap diff --git a/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableCombinator.cs b/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableCombinator.cs index d36c752c46c..25309ca3174 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableCombinator.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableCombinator.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; @@ -12,11 +11,12 @@ public override bool TryCombineOperations( QueryableFilterContext context, Queue operations, FilterCombinator combinator, - [NotNullWhen(true)] out Expression combined) + [NotNullWhen(true)] out Expression? combined) { if (operations.Count == 0) { - throw ThrowHelper.Filtering_QueryableCombinator_QueueEmpty(this); + combined = default; + return false; } combined = operations.Dequeue(); diff --git a/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterOperationCombinator.cs b/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterOperationCombinator.cs index b8274f3c502..51e136448f3 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterOperationCombinator.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterOperationCombinator.cs @@ -29,6 +29,6 @@ public abstract bool TryCombineOperations( TContext context, Queue operations, FilterCombinator combinator, - [NotNullWhen(true)] out T combined) + [NotNullWhen(true)] out T? combined) where TContext : FilterVisitorContext; } diff --git a/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterOperationCombinator~1.cs b/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterOperationCombinator~1.cs index e3d5de0c6a9..cab59cc833c 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterOperationCombinator~1.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterOperationCombinator~1.cs @@ -23,7 +23,7 @@ public abstract bool TryCombineOperations( TContext context, Queue operations, FilterCombinator combinator, - [NotNullWhen(true)] out T combined); + [NotNullWhen(true)] out T? combined); /// public override bool TryCombineOperations( diff --git a/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterVisitor.cs b/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterVisitor.cs index a0df4a52ac0..6285c6b206a 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterVisitor.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterVisitor.cs @@ -54,6 +54,6 @@ protected override bool TryCombineOperations( TContext context, Queue operations, FilterCombinator combinator, - [NotNullWhen(true)] out T combined) => + [NotNullWhen(true)] out T? combined) => _combinator.TryCombineOperations(context, operations, combinator, out combined); } diff --git a/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterVisitorBase`2.cs b/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterVisitorBase`2.cs index 4dc99bdda4d..594db9795fc 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterVisitorBase`2.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Visitor/FilterVisitorBase`2.cs @@ -27,7 +27,7 @@ protected abstract bool TryCombineOperations( TContext context, Queue operations, FilterCombinator combinator, - [NotNullWhen(true)] out T combined); + [NotNullWhen(true)] out T? combined); protected override ISyntaxVisitorAction Leave( ObjectValueNode node, diff --git a/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterCombinatorTests.cs b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterCombinatorTests.cs new file mode 100644 index 00000000000..7ddd19b3c3f --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/QueryableFilterCombinatorTests.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using CookieCrumble; +using HotChocolate.Execution; + +namespace HotChocolate.Data.Filters; + +public class QueryableFilterCombinatorTests +{ + private static readonly Foo[] _fooEntities = + { + new() { Bar = true }, + new() { Bar = false } + }; + + private readonly SchemaCache _cache = new(); + + [Fact] + public async Task Create_Empty_Expression() + { + // arrange + var tester = _cache.CreateSchema(_fooEntities); + + // act + // assert + var res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root(where: { }){ bar }}") + .Create()); + + await Snapshot.Create() + .Add(res1) + .MatchAsync(); + } + + public class Foo + { + public int Id { get; set; } + + public bool Bar { get; set; } + } + + public class FooNullable + { + public int Id { get; set; } + + public bool? Bar { get; set; } + } + + public class FooFilterInput + : FilterInputType + { + } + + public class FooNullableFilterInput + : FilterInputType + { + } +} diff --git a/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterCombinatorTests.Create_Empty_Expression.snap b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterCombinatorTests.Create_Empty_Expression.snap new file mode 100644 index 00000000000..013fbf3214f --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterCombinatorTests.Create_Empty_Expression.snap @@ -0,0 +1,12 @@ +{ + "data": { + "root": [ + { + "bar": true + }, + { + "bar": false + } + ] + } +} \ No newline at end of file diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections_NET6_0.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections_NET6_0.snap index 6b77aeeffa7..c7eb550cf0e 100644 --- a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections_NET6_0.snap +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections_NET6_0.snap @@ -1,19 +1,24 @@ { "errors": [ { - "message": "Unexpected Execution Error", + "message": "The provided value for filter \u0060all\u0060 of type FooNestedFilterInput is invalid. Null values are not supported.", "locations": [ { - "line": 2, - "column": 25 + "line": 1, + "column": 35 } ], "path": [ "root" - ] + ], + "extensions": { + "code": "HC0026", + "expectedType": "FooNestedFilterInput!", + "filterType": "FooNestedFilterInput" + } } ], "data": { - "root": null + "root": [] } -} \ No newline at end of file +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections_NET7_0.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections_NET7_0.snap index 6b77aeeffa7..c7eb550cf0e 100644 --- a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections_NET7_0.snap +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_DeepFilterObjectTwoProjections_NET7_0.snap @@ -1,19 +1,24 @@ { "errors": [ { - "message": "Unexpected Execution Error", + "message": "The provided value for filter \u0060all\u0060 of type FooNestedFilterInput is invalid. Null values are not supported.", "locations": [ { - "line": 2, - "column": 25 + "line": 1, + "column": 35 } ], "path": [ "root" - ] + ], + "extensions": { + "code": "HC0026", + "expectedType": "FooNestedFilterInput!", + "filterType": "FooNestedFilterInput" + } } ], "data": { - "root": null + "root": [] } -} \ No newline at end of file +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_ListObjectDifferentLevelProjection_NET6_0.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_ListObjectDifferentLevelProjection_NET6_0.snap index 6b77aeeffa7..c7eb550cf0e 100644 --- a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_ListObjectDifferentLevelProjection_NET6_0.snap +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_ListObjectDifferentLevelProjection_NET6_0.snap @@ -1,19 +1,24 @@ { "errors": [ { - "message": "Unexpected Execution Error", + "message": "The provided value for filter \u0060all\u0060 of type FooNestedFilterInput is invalid. Null values are not supported.", "locations": [ { - "line": 2, - "column": 25 + "line": 1, + "column": 35 } ], "path": [ "root" - ] + ], + "extensions": { + "code": "HC0026", + "expectedType": "FooNestedFilterInput!", + "filterType": "FooNestedFilterInput" + } } ], "data": { - "root": null + "root": [] } -} \ No newline at end of file +} diff --git a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_ListObjectDifferentLevelProjection_NET7_0.snap b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_ListObjectDifferentLevelProjection_NET7_0.snap index 6b77aeeffa7..c7eb550cf0e 100644 --- a/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_ListObjectDifferentLevelProjection_NET7_0.snap +++ b/src/HotChocolate/Data/test/Data.Projections.SqlServer.Tests/__snapshots__/QueryableFirstOrDefaultTests.Create_ListObjectDifferentLevelProjection_NET7_0.snap @@ -1,19 +1,24 @@ { "errors": [ { - "message": "Unexpected Execution Error", + "message": "The provided value for filter \u0060all\u0060 of type FooNestedFilterInput is invalid. Null values are not supported.", "locations": [ { - "line": 2, - "column": 25 + "line": 1, + "column": 35 } ], "path": [ "root" - ] + ], + "extensions": { + "code": "HC0026", + "expectedType": "FooNestedFilterInput!", + "filterType": "FooNestedFilterInput" + } } ], "data": { - "root": null + "root": [] } -} \ No newline at end of file +} diff --git a/src/HotChocolate/MongoDb/src/Data/Driver/MongoDbFilterDefinition.cs b/src/HotChocolate/MongoDb/src/Data/Driver/MongoDbFilterDefinition.cs index e72f05d91ab..17331ae597a 100644 --- a/src/HotChocolate/MongoDb/src/Data/Driver/MongoDbFilterDefinition.cs +++ b/src/HotChocolate/MongoDb/src/Data/Driver/MongoDbFilterDefinition.cs @@ -7,6 +7,13 @@ namespace HotChocolate.Data.MongoDb; public abstract class MongoDbFilterDefinition : FilterDefinition { + private static readonly MongoDbFilterDefinition _empty = new MongoDbEmptyFilterDefinition(); + + /// + /// Gets an empty filter. An empty filter matches everything. + /// + public static new MongoDbFilterDefinition Empty => MongoDbFilterDefinition._empty; + public abstract BsonDocument Render( IBsonSerializer documentSerializer, IBsonSerializerRegistry serializerRegistry); @@ -52,4 +59,14 @@ public override BsonDocument Render( return Render(documentSerializer, serializerRegistry); } } + + internal sealed class MongoDbEmptyFilterDefinition : MongoDbFilterDefinition + { + public override BsonDocument Render( + IBsonSerializer documentSerializer, + IBsonSerializerRegistry serializerRegistry) + { + return new BsonDocument(); + } + } } diff --git a/src/HotChocolate/MongoDb/src/Data/Driver/MongoDbFilterOperation.cs b/src/HotChocolate/MongoDb/src/Data/Driver/MongoDbFilterOperation.cs index 3f9452f7ba3..8de751f2007 100644 --- a/src/HotChocolate/MongoDb/src/Data/Driver/MongoDbFilterOperation.cs +++ b/src/HotChocolate/MongoDb/src/Data/Driver/MongoDbFilterOperation.cs @@ -1,4 +1,3 @@ -using System; using System.Collections; using MongoDB.Bson; using MongoDB.Bson.IO; diff --git a/src/HotChocolate/MongoDb/src/Data/Extensions/MongoDbFilterScopeExtensions.cs b/src/HotChocolate/MongoDb/src/Data/Extensions/MongoDbFilterScopeExtensions.cs index c9f398bb9ca..e939e789b60 100644 --- a/src/HotChocolate/MongoDb/src/Data/Extensions/MongoDbFilterScopeExtensions.cs +++ b/src/HotChocolate/MongoDb/src/Data/Extensions/MongoDbFilterScopeExtensions.cs @@ -1,8 +1,3 @@ -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using MongoDB.Bson; -using MongoDB.Driver; - namespace HotChocolate.Data.MongoDb.Filters; public static class MongoDbFilterScopeExtensions @@ -10,19 +5,13 @@ public static class MongoDbFilterScopeExtensions public static string GetPath(this MongoDbFilterScope scope) => string.Join(".", scope.Path.Reverse()); - public static bool TryCreateQuery( - this MongoDbFilterScope scope, - [NotNullWhen(true)] out MongoDbFilterDefinition? query) + public static MongoDbFilterDefinition CreateQuery(this MongoDbFilterScope scope) { - query = null; - if (scope.Level.Peek().Count == 0) { - return false; + return MongoDbFilterDefinition.Empty; } - query = new AndFilterDefinition(scope.Level.Peek().ToArray()); - - return true; + return new AndFilterDefinition(scope.Level.Peek().ToArray()); } } diff --git a/src/HotChocolate/MongoDb/src/Data/Extensions/MongoFilterVisitorContextExtensions.cs b/src/HotChocolate/MongoDb/src/Data/Extensions/MongoFilterVisitorContextExtensions.cs index 2519f02b1d2..10f1f76afe6 100644 --- a/src/HotChocolate/MongoDb/src/Data/Extensions/MongoFilterVisitorContextExtensions.cs +++ b/src/HotChocolate/MongoDb/src/Data/Extensions/MongoFilterVisitorContextExtensions.cs @@ -10,20 +10,16 @@ public static class MongoFilterVisitorContextExtensions /// /// The context /// The current scope - public static MongoDbFilterScope GetMongoFilterScope( - this MongoDbFilterVisitorContext context) => - (MongoDbFilterScope)context.GetScope(); + public static MongoDbFilterScope GetMongoFilterScope(this MongoDbFilterVisitorContext context) + => (MongoDbFilterScope)context.GetScope(); /// /// Tries to build the query based on the items that are stored on the scope /// /// the context - /// The query that was build /// True in case the query has been build successfully, otherwise false - public static bool TryCreateQuery( - this MongoDbFilterVisitorContext context, - [NotNullWhen(true)] out MongoDbFilterDefinition? query) + public static MongoDbFilterDefinition CreateQuery(this MongoDbFilterVisitorContext context) { - return context.GetMongoFilterScope().TryCreateQuery(out query); + return context.GetMongoFilterScope().CreateQuery(); } } diff --git a/src/HotChocolate/MongoDb/src/Data/Filters/Handlers/List/MongoDbListOperationHandlerBase.cs b/src/HotChocolate/MongoDb/src/Data/Filters/Handlers/List/MongoDbListOperationHandlerBase.cs index 3e80d281832..49741dc8ed8 100644 --- a/src/HotChocolate/MongoDb/src/Data/Filters/Handlers/List/MongoDbListOperationHandlerBase.cs +++ b/src/HotChocolate/MongoDb/src/Data/Filters/Handlers/List/MongoDbListOperationHandlerBase.cs @@ -1,8 +1,6 @@ -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using HotChocolate.Configuration; using HotChocolate.Data.Filters; -using HotChocolate.Internal; using HotChocolate.Language; using HotChocolate.Language.Visitors; @@ -75,8 +73,7 @@ public override bool TryHandleLeave( { context.RuntimeTypes.Pop(); - if (context.TryCreateQuery(out var query) && - context.Scopes.Pop() is MongoDbFilterScope scope) + if (context.Scopes.Pop() is MongoDbFilterScope scope) { var path = context.GetMongoFilterScope().GetPath(); var combinedOperations = HandleListOperation( @@ -113,8 +110,7 @@ protected abstract MongoDbFilterDefinition HandleListOperation( /// /// The scope where the definitions should be combined /// A with and combined filter definition of all definitions of the scope - protected static MongoDbFilterDefinition CombineOperationsOfScope( - MongoDbFilterScope scope) + protected static MongoDbFilterDefinition CombineOperationsOfScope(MongoDbFilterScope scope) { var level = scope.Level.Peek(); if (level.Count == 1) diff --git a/src/HotChocolate/MongoDb/src/Data/Filters/MongoDbFilterCombinator.cs b/src/HotChocolate/MongoDb/src/Data/Filters/MongoDbFilterCombinator.cs index fa4f187740f..5a45be604ac 100644 --- a/src/HotChocolate/MongoDb/src/Data/Filters/MongoDbFilterCombinator.cs +++ b/src/HotChocolate/MongoDb/src/Data/Filters/MongoDbFilterCombinator.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using HotChocolate.Data.Filters; -using MongoDB.Bson; -using MongoDB.Driver; namespace HotChocolate.Data.MongoDb.Filters; @@ -16,11 +12,12 @@ public override bool TryCombineOperations( MongoDbFilterVisitorContext context, Queue operations, FilterCombinator combinator, - [NotNullWhen(true)] out MongoDbFilterDefinition combined) + [NotNullWhen(true)] out MongoDbFilterDefinition? combined) { if (operations.Count == 0) { - throw ThrowHelper.Filtering_MongoDbCombinator_QueueEmpty(this); + combined = default; + return false; } combined = combinator switch diff --git a/src/HotChocolate/MongoDb/src/Data/Filters/MongoDbFilterProvider.cs b/src/HotChocolate/MongoDb/src/Data/Filters/MongoDbFilterProvider.cs index 494840e850f..033964e0b8e 100644 --- a/src/HotChocolate/MongoDb/src/Data/Filters/MongoDbFilterProvider.cs +++ b/src/HotChocolate/MongoDb/src/Data/Filters/MongoDbFilterProvider.cs @@ -52,8 +52,7 @@ async ValueTask ExecuteAsync( Visitor.Visit(filter, visitorContext); - if (!visitorContext.TryCreateQuery(out var whereQuery) || - visitorContext.Errors.Count > 0) + if (visitorContext.Errors.Count > 0) { context.Result = Array.Empty(); foreach (var error in visitorContext.Errors) @@ -63,16 +62,16 @@ async ValueTask ExecuteAsync( } else { - context.LocalContextData = - context.LocalContextData.SetItem( - nameof(FilterDefinition), - whereQuery); + var query = visitorContext.CreateQuery(); + + context.LocalContextData = context.LocalContextData + .SetItem(nameof(FilterDefinition), query); await next(context).ConfigureAwait(false); if (context.Result is IMongoDbExecutable executable) { - context.Result = executable.WithFiltering(whereQuery); + context.Result = executable.WithFiltering(query); } } } diff --git a/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/MongoDbFilterCombinatorTests.cs b/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/MongoDbFilterCombinatorTests.cs new file mode 100644 index 00000000000..74e19db0aa9 --- /dev/null +++ b/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/MongoDbFilterCombinatorTests.cs @@ -0,0 +1,54 @@ +using CookieCrumble; +using HotChocolate.Data.Filters; +using HotChocolate.Execution; +using MongoDB.Bson.Serialization.Attributes; +using Squadron; + +namespace HotChocolate.Data.MongoDb.Filters; + +public class MongoDbFilterCombinatorTests + : SchemaCache + , IClassFixture +{ + private static readonly Foo[] _fooEntities = + { + new() { Bar = true }, + new() { Bar = false } + }; + + public MongoDbFilterCombinatorTests(MongoResource resource) + { + Init(resource); + } + + [Fact] + public async Task Create_Empty_Expression() + { + // arrange + var tester = CreateSchema(_fooEntities); + + // act + // assert + var res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root(where: { }){ bar }}") + .Create()); + + await Snapshot.Create() + .Add(res1) + .MatchAsync(); + } + + public class Foo + { + [BsonId] + public Guid Id { get; set; } = Guid.NewGuid(); + + public bool Bar { get; set; } + } + + public class FooFilterInput + : FilterInputType + { + } +} diff --git a/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/__snapshots__/MongoDbFilterCombinatorTests.Create_Empty_Expression.snap b/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/__snapshots__/MongoDbFilterCombinatorTests.Create_Empty_Expression.snap new file mode 100644 index 00000000000..013fbf3214f --- /dev/null +++ b/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/__snapshots__/MongoDbFilterCombinatorTests.Create_Empty_Expression.snap @@ -0,0 +1,12 @@ +{ + "data": { + "root": [ + { + "bar": true + }, + { + "bar": false + } + ] + } +} \ No newline at end of file diff --git a/src/HotChocolate/Neo4J/src/Data/Execution/Neo4JExecutable.cs b/src/HotChocolate/Neo4J/src/Data/Execution/Neo4JExecutable.cs index e08b382a45a..fcc6a1411f1 100644 --- a/src/HotChocolate/Neo4J/src/Data/Execution/Neo4JExecutable.cs +++ b/src/HotChocolate/Neo4J/src/Data/Execution/Neo4JExecutable.cs @@ -15,7 +15,7 @@ namespace HotChocolate.Data.Neo4J.Execution; /// public class Neo4JExecutable : INeo4JExecutable - , IExecutable + , IExecutable { private static Node Node => Cypher.NamedNode(typeof(T).Name); @@ -91,7 +91,11 @@ public Neo4JExecutable WithLimit(int limit) /// public INeo4JExecutable WithFiltering(CompoundCondition filters) { - _filters = filters; + if (!filters.IsEmpty) + { + _filters = filters; + } + return this; } @@ -157,4 +161,4 @@ public StatementBuilder Pipeline() return statement; } -} \ No newline at end of file +} diff --git a/src/HotChocolate/Neo4J/src/Data/Filtering/Extensions/Neo4JFilterScopeExtensions.cs b/src/HotChocolate/Neo4J/src/Data/Filtering/Extensions/Neo4JFilterScopeExtensions.cs index 6141189b470..40309ae9a39 100644 --- a/src/HotChocolate/Neo4J/src/Data/Filtering/Extensions/Neo4JFilterScopeExtensions.cs +++ b/src/HotChocolate/Neo4J/src/Data/Filtering/Extensions/Neo4JFilterScopeExtensions.cs @@ -1,5 +1,3 @@ -using System.Diagnostics.CodeAnalysis; -using System.Linq; using HotChocolate.Data.Neo4J.Language; namespace HotChocolate.Data.Neo4J.Filtering; @@ -9,15 +7,11 @@ internal static class Neo4JFilterScopeExtensions public static string GetPath(this Neo4JFilterScope scope) => string.Join(".", scope.Path.Reverse()); - public static bool TryCreateQuery( - this Neo4JFilterScope scope, - [NotNullWhen(true)] out CompoundCondition query) + public static CompoundCondition CreateQuery(this Neo4JFilterScope scope) { - query = null; - if (scope.Level.Peek().Count == 0) { - return false; + return new CompoundCondition(null); } var conditions = new CompoundCondition(Operator.And); @@ -26,8 +20,6 @@ public static bool TryCreateQuery( conditions.And(condition); } - query = conditions; - - return true; + return conditions; } -} \ No newline at end of file +} diff --git a/src/HotChocolate/Neo4J/src/Data/Filtering/Extensions/Neo4JFilterVisitorContextExtensions.cs b/src/HotChocolate/Neo4J/src/Data/Filtering/Extensions/Neo4JFilterVisitorContextExtensions.cs index 320b62eb26a..e41d1b3a94d 100644 --- a/src/HotChocolate/Neo4J/src/Data/Filtering/Extensions/Neo4JFilterVisitorContextExtensions.cs +++ b/src/HotChocolate/Neo4J/src/Data/Filtering/Extensions/Neo4JFilterVisitorContextExtensions.cs @@ -1,5 +1,3 @@ -using System.Diagnostics.CodeAnalysis; -using System.Linq; using HotChocolate.Data.Filters; using HotChocolate.Data.Neo4J.Language; using ServiceStack; @@ -13,8 +11,7 @@ public static class Neo4JFilteringVisitorContextExtensions /// /// The context /// The current scope - public static Neo4JFilterScope GetNeo4JFilterScope( - this Neo4JFilterVisitorContext context) => + public static Neo4JFilterScope GetNeo4JFilterScope(this Neo4JFilterVisitorContext context) => (Neo4JFilterScope)context.GetScope(); public static Node GetNode(this Neo4JFilterVisitorContext context) @@ -28,12 +25,9 @@ public static Node GetNode(this Neo4JFilterVisitorContext context) /// Tries to build the query based on the items that are stored on the scope /// /// The context - /// The query that was build /// True in case the query has been build successfully, otherwise false - public static bool TryCreateQuery( - this Neo4JFilterVisitorContext context, - [NotNullWhen(true)] out CompoundCondition query) + public static CompoundCondition CreateQuery(this Neo4JFilterVisitorContext context) { - return context.GetNeo4JFilterScope().TryCreateQuery(out query); + return context.GetNeo4JFilterScope().CreateQuery(); } -} \ No newline at end of file +} diff --git a/src/HotChocolate/Neo4J/src/Data/Filtering/Handlers/List/Neo4JListOperationHandlerBase.cs b/src/HotChocolate/Neo4J/src/Data/Filtering/Handlers/List/Neo4JListOperationHandlerBase.cs index 13d4e799914..64201860cef 100644 --- a/src/HotChocolate/Neo4J/src/Data/Filtering/Handlers/List/Neo4JListOperationHandlerBase.cs +++ b/src/HotChocolate/Neo4J/src/Data/Filtering/Handlers/List/Neo4JListOperationHandlerBase.cs @@ -76,8 +76,7 @@ public override bool TryHandleLeave( { context.RuntimeTypes.Pop(); - if (context.TryCreateQuery(out var query) && - context.Scopes.Pop() is Neo4JFilterScope scope) + if (context.Scopes.Pop() is Neo4JFilterScope scope) { var path = context.GetNeo4JFilterScope().GetPath(); @@ -128,4 +127,4 @@ protected static Condition CombineOperationsOfScope(Neo4JFilterScope scope) return conditions; } -} \ No newline at end of file +} diff --git a/src/HotChocolate/Neo4J/src/Data/Filtering/Neo4JFilterCombinator.cs b/src/HotChocolate/Neo4J/src/Data/Filtering/Neo4JFilterCombinator.cs index f27d9244878..ddf8a0c3e3f 100644 --- a/src/HotChocolate/Neo4J/src/Data/Filtering/Neo4JFilterCombinator.cs +++ b/src/HotChocolate/Neo4J/src/Data/Filtering/Neo4JFilterCombinator.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using HotChocolate.Data.Filters; using HotChocolate.Data.Neo4J.Language; @@ -14,11 +12,12 @@ public override bool TryCombineOperations( Neo4JFilterVisitorContext context, Queue operations, FilterCombinator combinator, - [NotNullWhen(true)] out Condition combined) + [NotNullWhen(true)] out Condition? combined) { if (operations.Count == 0) { - throw ThrowHelper.Filtering_Neo4JFilterCombinator_QueueEmpty(this); + combined = default; + return false; } combined = combinator switch @@ -63,4 +62,4 @@ private static Condition CombineWithOr(IReadOnlyCollection operations return conditions; } -} \ No newline at end of file +} diff --git a/src/HotChocolate/Neo4J/src/Data/Filtering/Neo4JFilterProvider.cs b/src/HotChocolate/Neo4J/src/Data/Filtering/Neo4JFilterProvider.cs index 7e32d1d97af..01a14316872 100644 --- a/src/HotChocolate/Neo4J/src/Data/Filtering/Neo4JFilterProvider.cs +++ b/src/HotChocolate/Neo4J/src/Data/Filtering/Neo4JFilterProvider.cs @@ -46,8 +46,7 @@ async ValueTask ExecuteAsync( Visitor.Visit(filter, visitorContext); - if (!visitorContext.TryCreateQuery(out var whereQuery) || - visitorContext.Errors.Count > 0) + if (visitorContext.Errors.Count > 0) { context.Result = Array.Empty(); foreach (var error in visitorContext.Errors) @@ -57,14 +56,15 @@ async ValueTask ExecuteAsync( } else { - context.LocalContextData = - context.LocalContextData.SetItem("Filter", whereQuery); + var query = visitorContext.CreateQuery(); + + context.LocalContextData = context.LocalContextData.SetItem("Filter", query); await next(context).ConfigureAwait(false); if (context.Result is INeo4JExecutable executable) { - context.Result = executable.WithFiltering(whereQuery); + context.Result = executable.WithFiltering(query); } } } diff --git a/src/HotChocolate/Neo4J/src/Data/Language/CompoundCondition.cs b/src/HotChocolate/Neo4J/src/Data/Language/CompoundCondition.cs index 6ae6dc4710d..51c12cd4254 100644 --- a/src/HotChocolate/Neo4J/src/Data/Language/CompoundCondition.cs +++ b/src/HotChocolate/Neo4J/src/Data/Language/CompoundCondition.cs @@ -26,6 +26,8 @@ public CompoundCondition(Operator? op) _conditions = new List(); } + public bool IsEmpty => _conditions.Count == 0; + public override ClauseKind Kind => ClauseKind.CompoundCondition; public new void And(Condition condition) => @@ -163,4 +165,4 @@ public static CompoundCondition Create(Condition left, Operator op, Condition ri .Add(op, left) .Add(op, right); } -} \ No newline at end of file +} diff --git a/src/HotChocolate/Neo4J/src/Data/Language/Where.cs b/src/HotChocolate/Neo4J/src/Data/Language/Where.cs index 4a2b01b638d..f6323815177 100644 --- a/src/HotChocolate/Neo4J/src/Data/Language/Where.cs +++ b/src/HotChocolate/Neo4J/src/Data/Language/Where.cs @@ -23,7 +23,7 @@ public Where(bool exists, Condition condition) public override ClauseKind Kind => ClauseKind.Where; - public Exists Exists { get; } + public Exists? Exists { get; } public Condition Condition { get; } @@ -34,4 +34,4 @@ public override void Visit(CypherVisitor cypherVisitor) Condition.Visit(cypherVisitor); cypherVisitor.Leave(this); } -} \ No newline at end of file +} diff --git a/src/HotChocolate/Neo4J/test/HotChocolate.Data.Neo4J.Filtering.Tests/Neo4JFilterCombinatorTests.cs b/src/HotChocolate/Neo4J/test/HotChocolate.Data.Neo4J.Filtering.Tests/Neo4JFilterCombinatorTests.cs new file mode 100644 index 00000000000..8dd77b50d93 --- /dev/null +++ b/src/HotChocolate/Neo4J/test/HotChocolate.Data.Neo4J.Filtering.Tests/Neo4JFilterCombinatorTests.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; +using CookieCrumble; +using HotChocolate.Data.Filters; +using HotChocolate.Execution; + +namespace HotChocolate.Data.Neo4J.Filtering; + +[Collection("Database")] +public class Neo4JFilterCombinatorTests +{ + private readonly Neo4JFixture _fixture; + + public Neo4JFilterCombinatorTests(Neo4JFixture fixture) + { + _fixture = fixture; + } + + private const string _fooEntitiesCypher = + @"CREATE (:FooBool {Bar: true}), (:FooBool {Bar: false})"; + + [Fact] + public async Task Create_Empty_Expression() + { + // arrange + var tester = + await _fixture.GetOrCreateSchema(_fooEntitiesCypher); + + // act + // assert + IExecutionResult res1 = await tester.ExecuteAsync( + QueryRequestBuilder.New() + .SetQuery("{ root(where: { }){ bar }}") + .Create()); + + await Snapshot.Create() + .Add(res1) + .MatchAsync(); + } + + public class FooBool + { + public bool Bar { get; set; } + } + + public class FooBoolFilterType : FilterInputType + { + } +} diff --git a/src/HotChocolate/Neo4J/test/HotChocolate.Data.Neo4J.Filtering.Tests/__snapshots__/Neo4JFilterCombinatorTests.Create_Empty_Expression.snap b/src/HotChocolate/Neo4J/test/HotChocolate.Data.Neo4J.Filtering.Tests/__snapshots__/Neo4JFilterCombinatorTests.Create_Empty_Expression.snap new file mode 100644 index 00000000000..013fbf3214f --- /dev/null +++ b/src/HotChocolate/Neo4J/test/HotChocolate.Data.Neo4J.Filtering.Tests/__snapshots__/Neo4JFilterCombinatorTests.Create_Empty_Expression.snap @@ -0,0 +1,12 @@ +{ + "data": { + "root": [ + { + "bar": true + }, + { + "bar": false + } + ] + } +} \ No newline at end of file