diff --git a/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableCombinator.cs b/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableCombinator.cs index d36c752c46c..4d060cee34d 100644 --- a/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableCombinator.cs +++ b/src/HotChocolate/Data/src/Data/Filters/Expressions/QueryableCombinator.cs @@ -16,7 +16,8 @@ public override bool TryCombineOperations( { if (operations.Count == 0) { - throw ThrowHelper.Filtering_QueryableCombinator_QueueEmpty(this); + combined = default!; + return false; } combined = operations.Dequeue(); diff --git a/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterCombinatorTests.Create_Empty_Expression_true.snap b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterCombinatorTests.Create_Empty_Expression_true.snap new file mode 100644 index 00000000000..e4537587b54 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/__snapshots__/QueryableFilterCombinatorTests.Create_Empty_Expression_true.snap @@ -0,0 +1,12 @@ +{ + "data": { + "root": [ + { + "bar": true + }, + { + "bar": false + } + ] + } +} diff --git a/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/asd.cs b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/asd.cs new file mode 100644 index 00000000000..6e9d7cb7300 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Filters.InMemory.Tests/asd.cs @@ -0,0 +1,54 @@ +using System.Threading.Tasks; +using HotChocolate.Execution; +using Snapshooter; +using Snapshooter.Xunit; +using Xunit; + +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()); + + res1.ToJson().MatchSnapshot(new SnapshotNameExtension("true")); + } + + 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/MongoDb/src/Data/Driver/MongoDbFilterDefinition.cs b/src/HotChocolate/MongoDb/src/Data/Driver/MongoDbFilterDefinition.cs index e20ee73fac6..58fff3c6842 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,5 +59,15 @@ 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/Extensions/MongoDbFilterScopeExtensions.cs b/src/HotChocolate/MongoDb/src/Data/Extensions/MongoDbFilterScopeExtensions.cs index a45ba80f26c..f3cbbbfa678 100644 --- a/src/HotChocolate/MongoDb/src/Data/Extensions/MongoDbFilterScopeExtensions.cs +++ b/src/HotChocolate/MongoDb/src/Data/Extensions/MongoDbFilterScopeExtensions.cs @@ -1,7 +1,5 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; -using MongoDB.Bson; -using MongoDB.Driver; namespace HotChocolate.Data.MongoDb.Filters { @@ -18,7 +16,8 @@ public static bool TryCreateQuery( if (scope.Level.Peek().Count == 0) { - return false; + query = MongoDbFilterDefinition.Empty; + return true; } query = new AndFilterDefinition(scope.Level.Peek().ToArray()); diff --git a/src/HotChocolate/MongoDb/src/Data/Filters/MongoDbFilterCombinator.cs b/src/HotChocolate/MongoDb/src/Data/Filters/MongoDbFilterCombinator.cs index fbfefc34234..c248dbe2a53 100644 --- a/src/HotChocolate/MongoDb/src/Data/Filters/MongoDbFilterCombinator.cs +++ b/src/HotChocolate/MongoDb/src/Data/Filters/MongoDbFilterCombinator.cs @@ -20,7 +20,8 @@ public override bool TryCombineOperations( { 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 f39c1d1e4ad..8471f8aaecc 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 MongoDbFilterDefinition? whereQuery) || - visitorContext.Errors.Count > 0) + if (visitorContext.Errors.Count > 0) { context.Result = Array.Empty(); foreach (IError error in visitorContext.Errors) @@ -61,12 +60,10 @@ async ValueTask ExecuteAsync( context.ReportError(error.WithPath(context.Path)); } } - else + else if (visitorContext.TryCreateQuery(out MongoDbFilterDefinition? whereQuery)) { - context.LocalContextData = - context.LocalContextData.SetItem( - nameof(FilterDefinition), - whereQuery); + context.LocalContextData = context.LocalContextData + .SetItem(nameof(FilterDefinition), whereQuery); await next(context).ConfigureAwait(false); diff --git a/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/HotChocolate.Data.MongoDb.Filters.Tests.csproj b/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/HotChocolate.Data.MongoDb.Filters.Tests.csproj index b400be17245..cabc1ea32f6 100644 --- a/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/HotChocolate.Data.MongoDb.Filters.Tests.csproj +++ b/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/HotChocolate.Data.MongoDb.Filters.Tests.csproj @@ -12,6 +12,6 @@ - + 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..6ba4722e112 --- /dev/null +++ b/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/MongoDbFilterCombinatorTests.cs @@ -0,0 +1,50 @@ +using HotChocolate.Data.Filters; +using HotChocolate.Execution; +using MongoDB.Bson.Serialization.Attributes; +using Squadron; +using Xunit; +using System; +using System.Threading.Tasks; + +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()); + + res1.MatchDocumentSnapshot("res1"); + } + + 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_res1.snap b/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/__snapshots__/MongoDbFilterCombinatorTests.Create_Empty_Expression_res1.snap new file mode 100644 index 00000000000..e4537587b54 --- /dev/null +++ b/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/__snapshots__/MongoDbFilterCombinatorTests.Create_Empty_Expression_res1.snap @@ -0,0 +1,12 @@ +{ + "data": { + "root": [ + { + "bar": true + }, + { + "bar": false + } + ] + } +} diff --git a/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/__snapshots__/MongoDbFilterCombinatorTests.Create_Empty_Expression_res1_query.snap b/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/__snapshots__/MongoDbFilterCombinatorTests.Create_Empty_Expression_res1_query.snap new file mode 100644 index 00000000000..3ac80d01a3e --- /dev/null +++ b/src/HotChocolate/MongoDb/test/Data.MongoDb.Filters.Tests/__snapshots__/MongoDbFilterCombinatorTests.Create_Empty_Expression_res1_query.snap @@ -0,0 +1 @@ +find({ }) diff --git a/src/HotChocolate/Neo4J/src/Data/Execution/Neo4JExecutable.cs b/src/HotChocolate/Neo4J/src/Data/Execution/Neo4JExecutable.cs index 64aef5c4e42..25b7f38b855 100644 --- a/src/HotChocolate/Neo4J/src/Data/Execution/Neo4JExecutable.cs +++ b/src/HotChocolate/Neo4J/src/Data/Execution/Neo4JExecutable.cs @@ -91,7 +91,11 @@ public Neo4JExecutable WithLimit(int limit) /// public INeo4JExecutable WithFiltering(CompoundCondition filters) { - _filters = filters; + if (!filters.IsEmpty) + { + _filters = filters; + } + return this; } diff --git a/src/HotChocolate/Neo4J/src/Data/Filtering/Extensions/Neo4JFilterScopeExtensions.cs b/src/HotChocolate/Neo4J/src/Data/Filtering/Extensions/Neo4JFilterScopeExtensions.cs index d99c52448c0..2383b57329c 100644 --- a/src/HotChocolate/Neo4J/src/Data/Filtering/Extensions/Neo4JFilterScopeExtensions.cs +++ b/src/HotChocolate/Neo4J/src/Data/Filtering/Extensions/Neo4JFilterScopeExtensions.cs @@ -17,7 +17,8 @@ public static bool TryCreateQuery( if (scope.Level.Peek().Count == 0) { - return false; + query = new CompoundCondition(null); + return true; } var conditions = new CompoundCondition(Operator.And); diff --git a/src/HotChocolate/Neo4J/src/Data/Filtering/Neo4JFilterCombinator.cs b/src/HotChocolate/Neo4J/src/Data/Filtering/Neo4JFilterCombinator.cs index 5cfa65c44e9..ecc875a5623 100644 --- a/src/HotChocolate/Neo4J/src/Data/Filtering/Neo4JFilterCombinator.cs +++ b/src/HotChocolate/Neo4J/src/Data/Filtering/Neo4JFilterCombinator.cs @@ -18,7 +18,8 @@ public override bool TryCombineOperations( { if (operations.Count == 0) { - throw ThrowHelper.Filtering_Neo4JFilterCombinator_QueueEmpty(this); + combined = default!; + return false; } combined = combinator switch diff --git a/src/HotChocolate/Neo4J/src/Data/Filtering/Neo4JFilterProvider.cs b/src/HotChocolate/Neo4J/src/Data/Filtering/Neo4JFilterProvider.cs index 885135ee4f9..ed4c84e8184 100644 --- a/src/HotChocolate/Neo4J/src/Data/Filtering/Neo4JFilterProvider.cs +++ b/src/HotChocolate/Neo4J/src/Data/Filtering/Neo4JFilterProvider.cs @@ -49,8 +49,7 @@ async ValueTask ExecuteAsync( Visitor.Visit(filter, visitorContext); - if (!visitorContext.TryCreateQuery(out CompoundCondition whereQuery) || - visitorContext.Errors.Count > 0) + if (visitorContext.Errors.Count > 0) { context.Result = Array.Empty(); foreach (IError error in visitorContext.Errors) @@ -58,7 +57,7 @@ async ValueTask ExecuteAsync( context.ReportError(error.WithPath(context.Path)); } } - else + else if (visitorContext.TryCreateQuery(out CompoundCondition whereQuery)) { context.LocalContextData = context.LocalContextData.SetItem("Filter", whereQuery); diff --git a/src/HotChocolate/Neo4J/src/Data/Language/CompoundCondition.cs b/src/HotChocolate/Neo4J/src/Data/Language/CompoundCondition.cs index 2b3079b06cc..ddd00039439 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) => 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..9c218ee971b --- /dev/null +++ b/src/HotChocolate/Neo4J/test/HotChocolate.Data.Neo4J.Filtering.Tests/Neo4JFilterCombinatorTests.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using HotChocolate.Data.Filters; +using HotChocolate.Data.Neo4J.Sorting; +using HotChocolate.Execution; +using Xunit; + + namespace HotChocolate.Data.Neo4J.Filtering; + + public class Neo4JFilterCombinatorTests + : IClassFixture + { + 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()); + + res1.MatchDocumentSnapshot("ASC"); + } + + 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_ASC.snap b/src/HotChocolate/Neo4J/test/HotChocolate.Data.Neo4J.Filtering.Tests/__snapshots__/Neo4JFilterCombinatorTests.Create_Empty_Expression_ASC.snap new file mode 100644 index 00000000000..e4537587b54 --- /dev/null +++ b/src/HotChocolate/Neo4J/test/HotChocolate.Data.Neo4J.Filtering.Tests/__snapshots__/Neo4JFilterCombinatorTests.Create_Empty_Expression_ASC.snap @@ -0,0 +1,12 @@ +{ + "data": { + "root": [ + { + "bar": true + }, + { + "bar": false + } + ] + } +} diff --git a/src/HotChocolate/Neo4J/test/HotChocolate.Data.Neo4J.Filtering.Tests/__snapshots__/Neo4JFilterCombinatorTests.Create_Empty_Expression_ASC_query.snap b/src/HotChocolate/Neo4J/test/HotChocolate.Data.Neo4J.Filtering.Tests/__snapshots__/Neo4JFilterCombinatorTests.Create_Empty_Expression_ASC_query.snap new file mode 100644 index 00000000000..2ee9a5691f5 --- /dev/null +++ b/src/HotChocolate/Neo4J/test/HotChocolate.Data.Neo4J.Filtering.Tests/__snapshots__/Neo4JFilterCombinatorTests.Create_Empty_Expression_ASC_query.snap @@ -0,0 +1 @@ +MATCH (fooBool:FooBool) RETURN fooBool {.Bar}