Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
14c832c
Adding the ability to filter queries on nested object fields.
mbhaskar Oct 13, 2022
6c80be8
Fix formatting
Aniruddh25 Oct 27, 2022
97c14a7
Fix merge conflict
Aniruddh25 Oct 27, 2022
d715a9a
Merge remote-tracking branch 'origin/main' into dev/anmunde/nestedFilter
Aniruddh25 Oct 28, 2022
c8360f0
Nested Filter design
Aniruddh25 Nov 8, 2022
2fa812d
Remove addition of launchSettings
Aniruddh25 Nov 8, 2022
71f7c3d
Add a relationship directive on input field node
Aniruddh25 Nov 8, 2022
f1bd4f8
Add 2 options outline for SQL filtering
Aniruddh25 Nov 8, 2022
1729ac9
Merge origin/main
Aniruddh25 Nov 8, 2022
c8751cd
Merged from origin/main
Aniruddh25 Nov 8, 2022
ee64855
Refactoring changes for accommodating nested filters for SQL
Aniruddh25 Nov 9, 2022
03c74aa
Fix build failures
Aniruddh25 Nov 10, 2022
68114a5
Fix further build failures
Aniruddh25 Nov 11, 2022
a46c6dd
Fix remaining build failures
Aniruddh25 Nov 11, 2022
493d5c3
Fix formatting
Aniruddh25 Nov 11, 2022
1ec3932
Revert "Add a relationship directive on input field node"
Aniruddh25 Nov 11, 2022
138918e
Add singleton service GQLFilterParser
Aniruddh25 Nov 11, 2022
b047cca
Remove not yet required function GetDatabaseObjectForGraphQLType
Aniruddh25 Nov 11, 2022
08faa7f
Fix formatting
Aniruddh25 Nov 11, 2022
b5cb065
Fix unnecessary rename
Aniruddh25 Nov 11, 2022
198affb
Fix DatabaseObject.Name for Cosmos
Aniruddh25 Nov 11, 2022
207bcfe
No need for an override for SourceAlias
Aniruddh25 Nov 11, 2022
572b377
Remove unnecessary renames
Aniruddh25 Nov 11, 2022
5e0aec5
Remove SqlMetadataProvider changes for now
Aniruddh25 Nov 11, 2022
dc9e2fa
Fix spacing
Aniruddh25 Nov 11, 2022
5ffdf5e
Remove unused private member
Aniruddh25 Nov 11, 2022
5a3d853
Merge branch 'main' into dev/anmunde/nestedFilterRefactor
Aniruddh25 Nov 12, 2022
1a0f788
Fix constructor call
Aniruddh25 Nov 13, 2022
c89b78f
Fix OdataVisitor unit tests
Aniruddh25 Nov 15, 2022
29caead
Reset queryStructure to the caller's sourceName, sourceAlias after re…
Aniruddh25 Nov 16, 2022
f3076a9
Remove unnecessary constructor
Aniruddh25 Nov 16, 2022
7eb07d7
Fix the mock constructor
Aniruddh25 Nov 16, 2022
9d5db61
Fix namespace
Aniruddh25 Nov 16, 2022
23584ef
Apply suggestions from code review
Aniruddh25 Nov 17, 2022
210d94e
Address review comments
Aniruddh25 Nov 17, 2022
92cd028
Merge branch 'dev/anmunde/nestedFilterRefactor' of https://github.com…
Aniruddh25 Nov 17, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/internals/NestedFilteringForSQL.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ Although scans are costlier than seeks, they are not always bad. The option 2 is


## Implementation Details for Option 1 Exists clause
- When we parse the GraphQL filter arguments, we can identify if it is a nested filter object when the type of filter input is not a scalar i.e. NOT any of String, Boolean, Integer or Id filter input.
- When we parse the GraphQL filter arguments, we can identify if it is a nested filter object when the type of filter input is not a scalar i.e. NOT any of String, Boolean, Integer or Id filter input.
- Once the nested filtering scenario is identified, we need to identify if it is a relational database(SQL) scenario or non-relational. If the source definition of the entity that is being filtered has non-zero primary key count, it is a SQL scenario.
- Create an SqlExistsQueryStructure as the predicate operand of Exists predicate. This query structure has no order by, no limit and selects 1.
- Its predicates are obtained from recursively parsing the nested filter and an additional predicate to reflect the join between main query and this exists subquery.
Expand Down
2 changes: 1 addition & 1 deletion src/Service.Tests/Authorization/AuthorizationHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ public static Dictionary<string, Dictionary<string, string>> CreateColumnMapping
/// Without use of delegate the out param will
/// not be populated with the correct value.
/// This delegate is for the callback used
/// with the mocked SqlMetadataProvider.
/// with the mocked MetadataProvider.
/// </summary>
/// <param name="entity">Name of entity.</param>
/// <param name="exposedField">Exposed field name.</param>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Azure.DataApiBuilder.Service.Authorization;
using Azure.DataApiBuilder.Service.Exceptions;
using Azure.DataApiBuilder.Service.GraphQLBuilder.Mutations;
using Azure.DataApiBuilder.Service.Models;
using Azure.DataApiBuilder.Service.Resolvers;
using Azure.DataApiBuilder.Service.Services;
using HotChocolate.Language;
Expand Down Expand Up @@ -107,6 +108,7 @@ private static SqlMutationEngine SetupTestFixture(bool isAuthorized)
Mock<IHttpContextAccessor> httpContextAccessor = new();
Mock<ILogger<SqlMutationEngine>> _mutationEngineLogger = new();
DefaultHttpContext context = new();
Mock<GQLFilterParser> _gQLFilterParser = new();
httpContextAccessor.Setup(_ => _.HttpContext).Returns(context);

// Creates Mock AuthorizationResolver to return a preset result based on [TestMethod] input.
Expand All @@ -124,6 +126,7 @@ private static SqlMutationEngine SetupTestFixture(bool isAuthorized)
_queryBuilder.Object,
_sqlMetadataProvider.Object,
_authorizationResolver.Object,
_gQLFilterParser.Object,
httpContextAccessor.Object,
_mutationEngineLogger.Object
);
Expand Down
7 changes: 6 additions & 1 deletion src/Service.Tests/SqlTests/SqlTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Azure.DataApiBuilder.Service.Authorization;
using Azure.DataApiBuilder.Service.Configurations;
using Azure.DataApiBuilder.Service.Controllers;
using Azure.DataApiBuilder.Service.Models;
using Azure.DataApiBuilder.Service.Resolvers;
using Azure.DataApiBuilder.Service.Services;
using Microsoft.AspNetCore.Authorization;
Expand Down Expand Up @@ -53,6 +54,7 @@ public abstract class SqlTestBase
protected static ILogger<SqlMutationEngine> _mutationEngineLogger;
protected static ILogger<SqlQueryEngine> _queryEngineLogger;
protected static ILogger<RestController> _restControllerLogger;
protected static GQLFilterParser _gQLFilterParser;
protected const string MSSQL_DEFAULT_DB_NAME = "master";

protected static string DatabaseName { get; set; }
Expand Down Expand Up @@ -102,7 +104,7 @@ protected static async Task InitializeTestFixture(TestContext context, List<stri
// Setup Mock HttpContextAccess to return user as required when calling AuthorizationService.AuthorizeAsync
_httpContextAccessor = new Mock<IHttpContextAccessor>();
_httpContextAccessor.Setup(x => x.HttpContext.User).Returns(new ClaimsPrincipal());

_gQLFilterParser = new();
await ResetDbStateAsync();

// Execute additional queries, if any.
Expand All @@ -126,6 +128,7 @@ protected static async Task InitializeTestFixture(TestContext context, List<stri
{
services.AddHttpContextAccessor();
services.AddSingleton(_runtimeConfigProvider);
services.AddSingleton(_gQLFilterParser);
services.AddSingleton<IQueryEngine>(implementationFactory: (serviceProvider) =>
{
return new SqlQueryEngine(
Expand All @@ -134,6 +137,7 @@ protected static async Task InitializeTestFixture(TestContext context, List<stri
_sqlMetadataProvider,
ActivatorUtilities.GetServiceOrCreateInstance<IHttpContextAccessor>(serviceProvider),
_authorizationResolver,
_gQLFilterParser,
_queryEngineLogger,
_runtimeConfigProvider
);
Expand All @@ -146,6 +150,7 @@ protected static async Task InitializeTestFixture(TestContext context, List<stri
_queryBuilder,
_sqlMetadataProvider,
_authorizationResolver,
_gQLFilterParser,
ActivatorUtilities.GetServiceOrCreateInstance<IHttpContextAccessor>(serviceProvider),
_mutationEngineLogger);
});
Expand Down
14 changes: 12 additions & 2 deletions src/Service.Tests/Unittests/ODataASTVisitorUnitTests.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using System;
using System.Threading.Tasks;
using Azure.DataApiBuilder.Config;
using Azure.DataApiBuilder.Service.Authorization;
using Azure.DataApiBuilder.Service.Exceptions;
using Azure.DataApiBuilder.Service.Models;
using Azure.DataApiBuilder.Service.Parsers;
using Azure.DataApiBuilder.Service.Resolvers;
using Azure.DataApiBuilder.Service.Tests.SqlTests;
using Microsoft.Extensions.Logging;
using Microsoft.OData;
using Microsoft.OData.Edm;
using Microsoft.OData.UriParser;
Expand Down Expand Up @@ -296,8 +298,16 @@ private static ODataASTVisitor CreateVisitor(
Name = tableName
};
FindRequestContext context = new(entityName, dbo, isList);

Mock<SqlQueryStructure> structure = new(context, _sqlMetadataProvider, _runtimeConfigProvider);
AuthorizationResolver authorizationResolver = new(
_runtimeConfigProvider,
_sqlMetadataProvider,
new Mock<ILogger<AuthorizationResolver>>().Object);
Mock<SqlQueryStructure> structure = new(
context,
_sqlMetadataProvider,
authorizationResolver,
_runtimeConfigProvider,
new GQLFilterParser());
return new ODataASTVisitor(structure.Object, _sqlMetadataProvider);
}

Expand Down
2 changes: 1 addition & 1 deletion src/Service.Tests/Unittests/RequestValidatorUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ private static void PerformRequestParserPrimaryKeyTest(
/// Without use of delegate the out param will
/// not be populated with the correct value.
/// This delegate is for the callback used
/// with the mocked SqlMetadataProvider.
/// with the mocked MetadataProvider.
/// </summary>
/// <param name="entity">Name of entity.</param>
/// <param name="exposedField">Exposed field name.</param>
Expand Down
7 changes: 5 additions & 2 deletions src/Service.Tests/Unittests/RestServiceUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Azure.DataApiBuilder.Service.Authorization;
using Azure.DataApiBuilder.Service.Configurations;
using Azure.DataApiBuilder.Service.Exceptions;
using Azure.DataApiBuilder.Service.Models;
using Azure.DataApiBuilder.Service.Resolvers;
using Azure.DataApiBuilder.Service.Services;
using Microsoft.AspNetCore.Authorization;
Expand Down Expand Up @@ -147,13 +148,14 @@ public static void InitializeTest(string path, string entityName)
DefaultHttpContext context = new();
httpContextAccessor.Setup(_ => _.HttpContext).Returns(context);
AuthorizationResolver authorizationResolver = new(runtimeConfigProvider, sqlMetadataProvider.Object, authLogger.Object);

GQLFilterParser gQLFilterParser = new();
SqlQueryEngine queryEngine = new(
queryExecutor,
queryBuilder,
sqlMetadataProvider.Object,
httpContextAccessor.Object,
authorizationResolver,
gQLFilterParser,
queryEngineLogger.Object,
runtimeConfigProvider);

Expand All @@ -164,6 +166,7 @@ public static void InitializeTest(string path, string entityName)
queryBuilder,
sqlMetadataProvider.Object,
authorizationResolver,
gQLFilterParser,
httpContextAccessor.Object,
mutationEngingLogger.Object);

Expand All @@ -184,7 +187,7 @@ public static void InitializeTest(string path, string entityName)
/// Without use of delegate the out param will
/// not be populated with the correct value.
/// This delegate is for the callback used
/// with the mocked SqlMetadataProvider.
/// with the mocked MetadataProvider.
/// </summary>
/// <param name="entityPath">The entity path.</param>
/// <param name="entity">Name of entity.</param>
Expand Down
78 changes: 39 additions & 39 deletions src/Service/Models/GraphQLFilterParsers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using Azure.DataApiBuilder.Config;
using Azure.DataApiBuilder.Service.Resolvers;
using Azure.DataApiBuilder.Service.Services;
using HotChocolate.Language;
using HotChocolate.Resolvers;
Expand All @@ -12,7 +13,7 @@ namespace Azure.DataApiBuilder.Service.Models
/// <summary>
/// Contains methods to parse a GQL filter parameter
/// </summary>
public static class GQLFilterParser
public class GQLFilterParser
{
public static readonly string NullStringValue = "NULL";

Expand All @@ -22,19 +23,21 @@ public static class GQLFilterParser
/// <param name="ctx">The GraphQL context, used to get the query variables</param>
/// <param name="filterArgumentSchema">An IInputField object which describes the schema of the filter argument</param>
/// <param name="fields">The fields in the *FilterInput being processed</param>
/// <param name="sourceAlias">The source alias underlyin the *FilterInput being processed</param>
/// <param name="sourceDefinition">Definition of the table/view underlying the *FilterInput being processed</param>
/// <param name="processLiterals">Parametrizes literals before they are written in string predicate operands</param>
public static Predicate Parse(
/// <param name="queryStructure">The query structure for the entity being filtered providing
/// the source alias of the underlying *FilterInput being processed,
/// source definition of the table/view of the underlying *FilterInput being processed,
/// and the function that parametrizes literals before they are written in string predicate operands.</param>
public Predicate Parse(
IMiddlewareContext ctx,
IInputField filterArgumentSchema,
List<ObjectFieldNode> fields,
string schemaName,
string sourceName,
string sourceAlias,
SourceDefinition sourceDefinition,
Func<object, string> processLiterals)
BaseQueryStructure queryStructure)
{
string schemaName = queryStructure.DatabaseObject.SchemaName;
string sourceName = queryStructure.DatabaseObject.Name;
string sourceAlias = queryStructure.SourceAlias;
SourceDefinition sourceDefinition = queryStructure.GetUnderlyingSourceDefinition();

InputObjectType filterArgumentObject = ResolverMiddleware.InputObjectTypeFromIInputField(filterArgumentSchema);

List<PredicateOperand> predicates = new();
Expand All @@ -56,7 +59,6 @@ public static Predicate Parse(
bool fieldIsOr = string.Equals(name, $"{PredicateOperation.OR}", StringComparison.OrdinalIgnoreCase);

InputObjectType filterInputObjectType = ResolverMiddleware.InputObjectTypeFromIInputField(filterArgumentObject.Fields[name]);

if (fieldIsAnd || fieldIsOr)
{
PredicateOperation op = fieldIsAnd ? PredicateOperation.AND : PredicateOperation.OR;
Expand All @@ -67,39 +69,37 @@ public static Predicate Parse(
argumentSchema: filterArgumentObject.Fields[name],
filterArgumentSchema: filterArgumentSchema,
otherPredicates,
schemaName,
sourceName,
sourceAlias,
sourceDefinition,
op,
processLiterals)));
queryStructure,
op)));
}
else
{
List<ObjectFieldNode> subfields = (List<ObjectFieldNode>)fieldValue;

if (!IsScalarType(filterInputObjectType.Name))
{
queryStructure.DatabaseObject.Name = sourceName + "." + name;
queryStructure.SourceAlias = sourceAlias + "." + name;
predicates.Push(new PredicateOperand(Parse(ctx,
filterArgumentObject.Fields[name],
subfields,
schemaName,
sourceName + "." + name,
sourceAlias + "." + name,
sourceDefinition,
processLiterals)));
queryStructure)));
queryStructure.DatabaseObject.Name = sourceName;
queryStructure.SourceAlias = sourceAlias;
}
else
{
predicates.Push(new PredicateOperand(ParseScalarType(
ctx,
argumentSchema: filterArgumentObject.Fields[name],
name,
subfields,
schemaName,
sourceName,
sourceAlias,
processLiterals)));
predicates.Push(
new PredicateOperand(
ParseScalarType(
ctx,
argumentSchema: filterArgumentObject.Fields[name],
name,
subfields,
schemaName,
sourceName,
sourceAlias,
queryStructure.MakeParamWithValue)));
}
}
}
Expand Down Expand Up @@ -157,17 +157,13 @@ private static Predicate ParseScalarType(
/// <param name="sourceDefinition">Definition of the table/view underlying the *FilterInput being processed</param>
/// <param name="op">The operation (and or or)</param>
/// <param name="processLiterals">Parametrizes literals before they are written in string predicate operands</param>
private static Predicate ParseAndOr(
private Predicate ParseAndOr(
IMiddlewareContext ctx,
IInputField argumentSchema,
IInputField filterArgumentSchema,
List<IValueNode> fields,
string schemaName,
string tableName,
string tableAlias,
SourceDefinition sourceDefinition,
PredicateOperation op,
Func<object, string> processLiterals)
BaseQueryStructure baseQuery,
PredicateOperation op)
{
if (fields.Count == 0)
{
Expand All @@ -188,7 +184,11 @@ private static Predicate ParseAndOr(
}

List<ObjectFieldNode> subfields = (List<ObjectFieldNode>)fieldValue;
operands.Add(new PredicateOperand(Parse(ctx, filterArgumentSchema, subfields, schemaName, tableName, tableAlias, sourceDefinition, processLiterals)));
operands.Add(new PredicateOperand(
Parse(ctx,
filterArgumentSchema,
subfields,
baseQuery)));
}

return MakeChainPredicate(operands, op);
Expand Down
6 changes: 3 additions & 3 deletions src/Service/Parsers/EdmModelBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public IEdmModel GetModel()
/// <summary>
/// Build the model from the provided schema.
/// </summary>
/// <param name="sqlMetadataProvider">The SqlMetadataProvider holds the objects needed
/// <param name="sqlMetadataProvider">The MetadataProvider holds the objects needed
/// to build the correct model.</param>
/// <returns>An EdmModelBuilder that can be used to get a model.</returns>
public EdmModelBuilder BuildModel(ISqlMetadataProvider sqlMetadataProvider)
Expand All @@ -38,7 +38,7 @@ public EdmModelBuilder BuildModel(ISqlMetadataProvider sqlMetadataProvider)
/// <summary>
/// Add the entity types found in the schema to the model
/// </summary>
/// <param name="sqlMetadataProvider">The SqlMetadataProvider holds the objects needed
/// <param name="sqlMetadataProvider">The MetadataProvider holds the objects needed
/// to build the correct model.</param>
/// <returns>this model builder</returns>
private EdmModelBuilder BuildEntityTypes(ISqlMetadataProvider sqlMetadataProvider)
Expand Down Expand Up @@ -146,7 +146,7 @@ SourceDefinition sourceDefinition
/// <summary>
/// Add the entity sets contained within the schema to container.
/// </summary>
/// <param name="sqlMetadataProvider">The SqlMetadataProvider holds the objects needed
/// <param name="sqlMetadataProvider">The MetadataProvider holds the objects needed
/// to build the correct model.</param>
/// <returns>this model builder</returns>
private EdmModelBuilder BuildEntitySets(ISqlMetadataProvider sqlMetadataProvider)
Expand Down
Loading