Skip to content

Implemented LINQ query optimization for ObjectListFilter #520

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<PackageReference Update="FsToolkit.ErrorHandling" Version="$(FsToolkitVersion)" />
<PackageReference Update="FsToolkit.ErrorHandling.TaskResult" Version="$(FsToolkitVersion)" />
<PackageReference Update="Giraffe" Version="7.*" />
<PackageReference Update="Linq.Expression.Optimizer" Version="1.0.*" />
<PackageReference Update="Microsoft.Extensions.Http" Version="$(MicrosoftExtensionsVersion)" />
<PackageReference Update="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsVersion)" />
<PackageReference Update="Microsoft.NETCore.Platforms" Version="$(SystemVersion)" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

<ItemGroup>
<PackageReference Include="FSharp.Control.Reactive" />
<PackageReference Include="Linq.Expression.Optimizer" />
<PackageReference Include="System.Reactive" />
</ItemGroup>

Expand Down
38 changes: 25 additions & 13 deletions src/FSharp.Data.GraphQL.Server.Middleware/ObjectListFilter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -60,26 +60,29 @@ open System.Reflection
/// </code></example>
[<Struct>]
type ObjectListFilterLinqOptions<'T, 'D>
([<Optional>] compareDiscriminator : Expression<Func<'T, 'D, bool>> | null, [<Optional>] getDiscriminatorValue : (Type -> 'D) | null) =
([<Optional>] compareDiscriminator : Expression<Func<'T, 'D, bool>> | null, [<Optional>] getDiscriminatorValue : (Type -> 'D) | null, [<Optional; DefaultParameterValue true>] optimize: bool) =

member _.CompareDiscriminator = compareDiscriminator |> ValueOption.ofObj
member _.GetDiscriminatorValue = getDiscriminatorValue |> ValueOption.ofObj
/// Whether perform optimization of a LINQ expression
member _.Optimize = optimize

static member None = ObjectListFilterLinqOptions<'T, 'D> (null, null)
/// Empty options with optimization enabled
static member None = ObjectListFilterLinqOptions<'T, 'D> (null, null, true)

static member GetCompareDiscriminator (getDiscriminatorValue : Expression<Func<'T, 'D>>) =
let tParam = Expression.Parameter (typeof<'T>, "x")
let dParam = Expression.Parameter (typeof<'D>, "d")
let body = Expression.Equal (Expression.Invoke (getDiscriminatorValue, tParam), dParam)
Expression.Lambda<Func<'T, 'D, bool>> (body, tParam, dParam)

new (getDiscriminator : Expression<Func<'T, 'D>>) =
ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, null)
new (compareDiscriminator : Expression<Func<'T, 'D, bool>>) = ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator, null)
new (getDiscriminatorValue : Type -> 'D) =
ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator = null, getDiscriminatorValue = getDiscriminatorValue)
new (getDiscriminator : Expression<Func<'T, 'D>>, getDiscriminatorValue : Type -> 'D) =
ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, getDiscriminatorValue)
new (getDiscriminator : Expression<Func<'T, 'D>>, [<Optional; DefaultParameterValue true>] optimize: bool) =
ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, null, optimize)
new (compareDiscriminator : Expression<Func<'T, 'D, bool>>, [<Optional; DefaultParameterValue true>] optimize: bool) = ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator, null, optimize)
new (getDiscriminatorValue : Type -> 'D, [<Optional; DefaultParameterValue true>] optimize: bool) =
ObjectListFilterLinqOptions<'T, 'D> (compareDiscriminator = null, getDiscriminatorValue = getDiscriminatorValue, optimize = optimize)
new (getDiscriminator : Expression<Func<'T, 'D>>, getDiscriminatorValue : Type -> 'D, [<Optional; DefaultParameterValue true>] optimize: bool) =
ObjectListFilterLinqOptions<'T, 'D> (ObjectListFilterLinqOptions.GetCompareDiscriminator getDiscriminator, getDiscriminatorValue, optimize)

/// Contains tooling for working with ObjectListFilter.
module ObjectListFilter =
Expand Down Expand Up @@ -270,8 +273,12 @@ module ObjectListFilter =
)
let queryExpr =
let param = Expression.Parameter (typeof<'T>, "x")
let body = buildFilterExpr (SourceExpression param) buildTypeDiscriminatorCheck filter
whereExpr<'T> query param body
if options.Optimize then
let body = ExpressionOptimizer.visit(buildFilterExpr (SourceExpression param) buildTypeDiscriminatorCheck filter)
whereExpr<'T> query param body
else
let body = buildFilterExpr (SourceExpression param) buildTypeDiscriminatorCheck filter
whereExpr<'T> query param body
Comment on lines +276 to +281
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if options.Optimize then
let body = ExpressionOptimizer.visit(buildFilterExpr (SourceExpression param) buildTypeDiscriminatorCheck filter)
whereExpr<'T> query param body
else
let body = buildFilterExpr (SourceExpression param) buildTypeDiscriminatorCheck filter
whereExpr<'T> query param body
if options.Optimize then
buildFilterExpr (SourceExpression param) buildTypeDiscriminatorCheck filter
|> ExpressionOptimizer.visit
else
buildFilterExpr (SourceExpression param) buildTypeDiscriminatorCheck filter
|> whereExpr<'T> query param

// Create and execute the final expression
query.Provider.CreateQuery<'T> (queryExpr)

Expand All @@ -282,9 +289,14 @@ module ObjectListFilterExtensions =

type ObjectListFilter with

member inline filter.ApplyTo<'T, 'D> (query : IQueryable<'T>, [<Optional>] options : ObjectListFilterLinqOptions<'T, 'D>) =
member inline filter.Apply<'T, 'D> (query : IQueryable<'T>) =
apply ObjectListFilterLinqOptions<'T, 'D>.None filter query

member inline filter.Apply<'T, 'D> (query : IQueryable<'T>, [<Optional>] options : ObjectListFilterLinqOptions<'T, 'D>) =
apply options filter query

type IQueryable<'T> with

member inline query.Apply (filter : ObjectListFilter, [<Optional>] options : ObjectListFilterLinqOptions<'T, 'D>) = apply options filter query
member inline query.Apply (filter : ObjectListFilter) = apply ObjectListFilterLinqOptions.None filter query

member inline query.Apply (filter : ObjectListFilter, options : ObjectListFilterLinqOptions<'T, 'D>) = apply options filter query
42 changes: 36 additions & 6 deletions tests/FSharp.Data.GraphQL.Tests/ObjectListFilterLinqTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ open System
open System.Linq
open FSharp.Data.GraphQL.Types
open FSharp.Data.GraphQL.Server.Middleware
open FSharp.Data.GraphQL.Server.Middleware.ObjectListFilter.Operators
open FSharp.Data.GraphQL.Tests.LinqTests

[<Fact>]
let ``ObjectListFilter works with Equals operator`` () =
let filter = Equals { FieldName = "firstName"; Value = "Jonathan" } // :> IComparable
let filter = Equals { FieldName = "firstName"; Value = "Jonathan" }
let queryable = data.AsQueryable ()
let filteredData = queryable.Apply (filter) |> Seq.toList
List.length filteredData |> equals 1
Expand All @@ -22,7 +23,7 @@ let ``ObjectListFilter works with Equals operator`` () =

[<Fact>]
let ``ObjectListFilter works with GreaterThan operator`` () =
let filter = GreaterThan { FieldName = "id"; Value = 4 } // :> IComparable
let filter = GreaterThan { FieldName = "id"; Value = 4 }
let queryable = data.AsQueryable ()
let filteredData = queryable.Apply (filter) |> Seq.toList
List.length filteredData |> equals 1
Expand All @@ -35,7 +36,7 @@ let ``ObjectListFilter works with GreaterThan operator`` () =

[<Fact>]
let ``ObjectListFilter works with GreaterThanOrEqual operator`` () =
let filter = GreaterThanOrEqual { FieldName = "id"; Value = 4 } // :> IComparable
let filter = GreaterThanOrEqual { FieldName = "id"; Value = 4 }
let queryable = data.AsQueryable ()
let filteredData = queryable.Apply (filter) |> Seq.toList
List.length filteredData |> equals 2
Expand All @@ -56,7 +57,7 @@ let ``ObjectListFilter works with GreaterThanOrEqual operator`` () =

[<Fact>]
let ``ObjectListFilter works with LessThan operator`` () =
let filter = LessThan { FieldName = "id"; Value = 4 } // :> IComparable
let filter = LessThan { FieldName = "id"; Value = 4 }
let queryable = data.AsQueryable ()
let filteredData = queryable.Apply (filter) |> Seq.toList
List.length filteredData |> equals 1
Expand All @@ -69,7 +70,7 @@ let ``ObjectListFilter works with LessThan operator`` () =

[<Fact>]
let ``ObjectListFilter works with LessThanOrEqual operator`` () =
let filter = LessThanOrEqual { FieldName = "id"; Value = 4 } // :> IComparable
let filter = LessThanOrEqual { FieldName = "id"; Value = 4 }
let queryable = data.AsQueryable ()
let filteredData = queryable.Apply (filter) |> Seq.toList
List.length filteredData |> equals 2
Expand Down Expand Up @@ -155,6 +156,34 @@ let ``ObjectListFilter works with OR operator`` () =
result.Contact |> equals { Email = "[email protected]" }
result.Friends |> equals [ { Email = "[email protected]" }; { Email = "[email protected]" } ]

[<Fact>]
let ``LINQ tree is balanced after multiple usings of OR operator`` () =
let filter =
("firstName" =@@ "J")
||| (("id" ==> 2)
||| (("id" >>> 4)
||| (("lastName" === "Adams")
||| (("lastName" @=@ "e")
||| (("firstName" @=@ "a")
||| ("lastName" @=@ "a"))))))
let queryable = data.AsQueryable ()
let filteredData = queryable.Apply (filter, ObjectListFilterLinqOptions(optimize = true)) |> Seq.toList
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let filteredData = queryable.Apply (filter, ObjectListFilterLinqOptions(optimize = true)) |> Seq.toList
let filteredData = queryable.Apply (filter, ObjectListFilterLinqOptions (optimize = true)) |> Seq.toList

List.length filteredData |> equals 3
do
let result = List.head filteredData
result.ID |> equals 4
result.FirstName |> equals "Ben"
result.LastName |> equals "Adams"
result.Contact |> equals { Email = "[email protected]" }
result.Friends |> equals [ { Email = "[email protected]" }; { Email = "[email protected]" } ]
do
let result = List.last filteredData
result.ID |> equals 7
result.FirstName |> equals "Jeneffer"
result.LastName |> equals "Trif"
result.Contact |> equals { Email = "[email protected]" }
result.Friends |> equals [ { Email = "[email protected]" } ]

[<Fact>]
let ``ObjectListFilter works with IN operator for string type field`` () =
let filter = In { FieldName = "firstName"; Value = [ "Jeneffer"; "Ben" ] }
Expand Down Expand Up @@ -378,7 +407,8 @@ let ``ObjectListFilter works with getDiscriminatorValue for Horse`` () =
(function
| t when t = typeof<Cow> -> t.Name
| t when t = typeof<Horse> -> t.Name
| _ -> raise (NotSupportedException "Type not supported"))
| _ -> raise (NotSupportedException "Type not supported")),
optimize = true
)
let filteredData = queryable.Apply (filter, options) |> Seq.toList
List.length filteredData |> equals 2
Expand Down