Skip to content
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
14 changes: 14 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,17 @@
* **Breaking Change** `Parser.parse` and `Parser.tryParse` now reject a document whose braces, brackets and parentheses, outside of strings and comments, are nested deeper than `DocumentLimitsDefaults.MaxNestingDepth` (128), with a syntax error giving the line and column of the first one too deep. Deeper documents used to overflow the stack, which terminated the process
* Fixed parsing time growing exponentially with the nesting of list types in variable definitions: a 100-deep `[[…Int…]]` never finished parsing
* Fixed `Parser.tryParse` throwing `OverflowException`, and so an HTTP server answering with an unhandled exception, for an integer out of the range of 64-bit integers; it is now a syntax error
* **Breaking Change** `SelectionInfoContext.FragmentDefinitions` was replaced by `Fragments`, the fragment definitions that validation inlines, by name
* Fixed query planning time growing exponentially with the nesting of fields under an interface or a union, with the number of its possible types as the base. The selection set of a field is now planned once for each type it can return, instead of once for each possible type of every abstract field above it, and the plans of a field selected more than once are merged once, however many possible types share them. The same merge, run when a variable disables `@defer` on fragments that select the same nested fields, is no longer exponential at execution either
* Fixed query planning time growing quadratically with the number of fields, fragment spreads and deferred fragments of a selection set, and with the number of selections of a field selected many times with different selections
* Fixed execution time growing quadratically with the number of fields of an object
* Fixed execution time growing with the number of objects of a list times the size of the deferred fragments a variable disables on them: the fields of the fragments were merged into the selection again for every object, and are now merged once per request
* Fixed `QueryWeightMiddleware` and `ObjectListFilterMiddleware` taking time exponential in the nesting of fields under an interface or a union, measuring or searching the plans shared by its possible types once for every path to them
* Fixed `ObjectListFilterMiddleware` reporting only the filters of the last list field it found in a selection, dropping the filters of the list fields before it. The filters are reported by path, so when a field of an interface or a union has different filters under different types, one of them is reported
* Fixed `ObjectListFilterMiddleware` failing with a `NullReferenceException` on a deferred fragment at the root of an operation
* Fixed the field selection merging rule taking exponential time on nested fields that share a response name. Like graphql-js, it now compares each pair of fields once, whichever comes first, and so reports each conflict once, and it stops after the number of errors validation reports
* Fixed the input value rule checking the argument values of a fragment again for every operation that spreads it, searching the variable definitions for every use of a variable, and reporting every invalid value of every such operation: a 380 KB document took more than a minute to validate. A value is now checked once, the variables used in it once per operation, and the rule stops after the number of errors validation reports
* Fixed validation never finishing on a variable whose default value is or holds a variable, such as `query ($a: Int = $a)`, which the input value rule followed forever. Default values must be constants, so a variable in one is now a validation error
* Fixed the unused variable rule searching the document once for every variable, and walking the values of a fragment again for every operation that spreads it
* Fixed validation time growing quadratically with the number of operations and of fragment spreads
* Fixed the operation name uniqueness rule counting a fragment with the same name as an operation
255 changes: 185 additions & 70 deletions src/FSharp.Data.GraphQL.Server.Middleware/MiddlewareDefinitions.fs

Large diffs are not rendered by default.

38 changes: 27 additions & 11 deletions src/FSharp.Data.GraphQL.Server/Execution.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
module FSharp.Data.GraphQL.Execution

open System
open System.Collections.Concurrent
open System.Collections.Generic
open System.Collections.Immutable
open System.Diagnostics
open System.Runtime.CompilerServices
open System.Text.Json
open System.Threading
open FSharp.Control.Reactive
Expand Down Expand Up @@ -357,20 +359,34 @@ let private deferredFragmentEvents
| ValueSome _ -> AnnouncedEvents.announced (DeferredFragmentPending (fragmentPath, label, fragmentId)) events
| ValueNone -> events

/// <summary>
/// The selection sets of each request with the deferred fragments its variables disable inlined, by the fields of the
/// selection set, compared by reference.
/// </summary>
/// <remarks>
/// Every object of a list resolves the same selection set, and inlining it for each of them would merge the fields of
/// the fragments once per object: the time would grow with the number of objects times the size of the fragments. The
/// table holds the inlined selection sets for as long as the execution context of the request is alive.
/// </remarks>
let private inlinedSelectionSets =
ConditionalWeakTable<ExecutionContext, ConcurrentDictionary<ExecutionInfo list, ExecutionInfo list>> ()

/// <summary>
/// The fields with every deferred fragment whose <c>if</c> argument is <see langword="false"/> with the variables of
/// the request replaced by its own fields, merged into the selection as if the directive were absent; a fragment
/// still deferred stays among the fields as it is.
/// </summary>
let private inlineDisabledFragments (variables : ImmutableDictionary<string, obj>) (fields : ExecutionInfo list) =
let rec inlineDisabled (fields : ExecutionInfo list) =
([], fields)
||> List.fold (fun fields field ->
match field.Kind with
| ResolveDeferredFragment (_, _, enabled, fragmentFields) when enabled variables = Ok false ->
Planning.deepMerge fields (inlineDisabled fragmentFields)
| _ -> [ yield! fields; yield field ])
inlineDisabled fields
let private inlineDisabledFragments (ctx : ExecutionContext) (fields : ExecutionInfo list) =
let isDisabled (fragment : ExecutionInfo) =
match fragment.Kind with
| ResolveDeferredFragment (_, _, enabled, _) -> enabled ctx.Variables = Ok false
| _ -> false
if not (fields |> List.exists isDisabled) then
fields
else
let inlined =
inlinedSelectionSets.GetValue (ctx, fun _ -> ConcurrentDictionary<ExecutionInfo list, ExecutionInfo list> (HashIdentity.Reference))
inlined.GetOrAdd (fields, Planning.inlineDeferredFragments isDisabled)

/// The root fields a deferred fragment at the operation's root selects, those of the fragments nested in it included.
let rec private rootFieldsOfFragment (info : ExecutionInfo) =
Expand Down Expand Up @@ -871,7 +887,7 @@ and executeObjectFields
// on the fragment excludes it
let ownFields, deferredFragments =
fields
|> inlineDisabledFragments ctx.Variables
|> inlineDisabledFragments ctx.Context
|> List.partition (fun field ->
match field.Kind with
| ResolveDeferredFragment _ -> false
Expand Down Expand Up @@ -1231,7 +1247,7 @@ let internal executeOperation (ctx : ExecutionContext) : AsyncVal<GQLExecutionRe
let includeResults =
ctx.ExecutionPlan.Fields
// A root fragment disabled with `if: false` through a variable contributes its fields to the root selection
|> inlineDisabledFragments ctx.Variables
|> inlineDisabledFragments ctx
|> List.map (fun info ->
info.Include ctx.Variables
|> Result.map (fun include -> struct (info, include)))
Expand Down
Loading
Loading