@@ -1047,7 +1047,6 @@ export class NamedFragments {
10471047 dependsOn : string [ ] ,
10481048 } ;
10491049 const fragmentsMap = new Map < string , FragmentInfo > ( ) ;
1050-
10511050 const removedFragments = new Set < string > ( ) ;
10521051 for ( const fragment of this . definitions ( ) ) {
10531052 const mappedSelectionSet = mapper ( fragment . selectionSet . expandAllFragments ( ) . trimUnsatisfiableBranches ( fragment . typeCondition ) ) ;
@@ -1527,6 +1526,28 @@ export class SelectionSet {
15271526 // validate this (`canApplyAtType` is not free, and we want to avoid repeating it multiple times).
15281527 diffWithNamedFragmentIfContained ( candidate : NamedFragmentDefinition , parentType : CompositeType ) : { contains : boolean , diff ?: SelectionSet } {
15291528 const that = candidate . selectionSetAtType ( parentType ) ;
1529+ // It's possible that while the fragment technically applies at `parentType`, it's "rebasing" on
1530+ // `parentType` is empty, or contains only `__typename`. For instance, suppose we have
1531+ // a union `U = A | B | C`, and then a fragment:
1532+ // ```graphql
1533+ // fragment F on U {
1534+ // ... on A {
1535+ // x
1536+ // }
1537+ // ... on b {
1538+ // y
1539+ // }
1540+ // }
1541+ // ```
1542+ // It is then possible to apply `F` when the parent type is `C`, but this ends up selecting
1543+ // nothing at all.
1544+ //
1545+ // Returning `contains: true` in those cases is, while not 100% incorrect, at least not productive,
1546+ // and so we skip right away in that case. This is essentially an optimisation.
1547+ if ( that . isEmpty ( ) || ( that . selections ( ) . length === 1 && that . selections ( ) [ 0 ] . isTypenameField ( ) ) ) {
1548+ return { contains : false } ;
1549+ }
1550+
15301551 if ( this . contains ( that ) ) {
15311552 // One subtlety here is that at "this" sub-selections may already have been optimized with some fragments. It's
15321553 // usually ok because `candidate` will also use those fragments, but one fragments that `candidate` can never be
@@ -1554,6 +1575,17 @@ export class SelectionSet {
15541575 const otherSelections = that . triviallyNestedSelectionsForKey ( this . parentType , key ) ;
15551576 const allSelections = thatSelection ? [ thatSelection ] . concat ( otherSelections ) : otherSelections ;
15561577 if ( allSelections . length === 0 ) {
1578+ // If it is a fragment spread, and we didn't find it in `that`, then we try to expand that
1579+ // fragment and see if that result is entirely covered by `that`. If that is the case, then it means
1580+ // `thisSelection` does not need to be in the returned "diff". If it's not entirely covered,
1581+ // we just add the spread itself to the diff: even if some parts of it were covered by `that`,
1582+ // keeping just the fragment is, in a sense, more condensed.
1583+ if ( thisSelection instanceof FragmentSpreadSelection ) {
1584+ const expanded = thisSelection . selectionSet . expandAllFragments ( ) . trimUnsatisfiableBranches ( this . parentType ) ;
1585+ if ( expanded . minus ( that ) . isEmpty ( ) ) {
1586+ continue ;
1587+ }
1588+ }
15571589 updated . add ( thisSelection ) ;
15581590 } else {
15591591 const selectionDiff = allSelections . reduce < Selection | undefined > ( ( prev , val ) => prev ?. minus ( val ) , thisSelection ) ;
@@ -2055,6 +2087,11 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
20552087 return this . element . parentType ;
20562088 }
20572089
2090+ isTypenameField ( ) : boolean {
2091+ // Overridden where appropriate
2092+ return false ;
2093+ }
2094+
20582095 collectVariables ( collector : VariableCollector ) {
20592096 this . element . collectVariables ( collector ) ;
20602097 this . selectionSet ?. collectVariables ( collector )
@@ -2181,6 +2218,10 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
21812218 return this ;
21822219 }
21832220
2221+ isTypenameField ( ) : boolean {
2222+ return this . element . definition . name === typenameFieldName ;
2223+ }
2224+
21842225 withUpdatedComponents ( field : Field < any > , selectionSet : SelectionSet | undefined ) : FieldSelection {
21852226 return new FieldSelection ( field , selectionSet ) ;
21862227 }
@@ -2456,7 +2497,6 @@ export abstract class FragmentSelection extends AbstractSelection<FragmentElemen
24562497 || ( isObjectType ( parentType ) && possibleRuntimeTypes ( this . element . typeCondition ) . some ( ( t ) => t . name === parentType . name ) )
24572498 ) ;
24582499 }
2459-
24602500}
24612501
24622502class InlineFragmentSelection extends FragmentSelection {
@@ -2668,7 +2708,7 @@ class InlineFragmentSelection extends FragmentSelection {
26682708 if ( isObjectType ( thisCondition ) || ! possibleRuntimeTypes ( thisCondition ) . includes ( currentType ) ) {
26692709 return undefined ;
26702710 } else {
2671- const trimmed = this . selectionSet . trimUnsatisfiableBranches ( currentType , options ) ;
2711+ const trimmed = this . selectionSet . trimUnsatisfiableBranches ( currentType , options ) ;
26722712 return trimmed . isEmpty ( ) ? undefined : trimmed ;
26732713 }
26742714 }
@@ -2799,8 +2839,10 @@ class FragmentSpreadSelection extends FragmentSelection {
27992839 assert ( false , `Unsupported` ) ;
28002840 }
28012841
2802- trimUnsatisfiableBranches ( _ : CompositeType ) : FragmentSelection {
2803- return this ;
2842+ trimUnsatisfiableBranches ( parentType : CompositeType ) : FragmentSelection {
2843+ // We must update the spread parent type if necessary since we're not going deeper,
2844+ // or we'll be fundamentally losing context.
2845+ return this . rebaseOn ( parentType ) ;
28042846 }
28052847
28062848 namedFragments ( ) : NamedFragments | undefined {
@@ -2837,18 +2879,30 @@ class FragmentSpreadSelection extends FragmentSelection {
28372879 return this ;
28382880 }
28392881
2840- rebaseOn ( _parentType : CompositeType ) : FragmentSelection {
2841- // This is a little bit iffy, because the fragment could link to a schema (typically the supergraph API one)
2842- // that is different from the one of `_selectionSet` (say, a subgraph fetch selection in which we're trying to
2843- // reuse a user fragment). But in practice, we expand all fragments when we do query planning and only re-add
2844- // fragments back at the very end, so this should be fine. Importantly, we don't want this method to mistakenly
2845- // expand the spread, as that would compromise the code that optimize subgraph fetches to re-use named
2882+ rebaseOn ( parentType : CompositeType ) : FragmentSelection {
2883+ // We preserve the parent type here, to make sure we don't lose context, but we actually don't
2884+ // want to expand the spread as that would compromise the code that optimize subgraph fetches to re-use named
28462885 // fragments.
2847- return this ;
2886+ //
2887+ // This is a little bit iffy, because the fragment may not apply at this parent type, but we
2888+ // currently leave it to the caller to ensure this is not a mistake. But most of the
2889+ // QP code works on selections with fully expanded fragments, so this code (and that of `canAddTo`
2890+ // on come into play in the code for reusing fragments, and that code calls those methods
2891+ // appropriately.
2892+ if ( this . parentType === parentType ) {
2893+ return this ;
2894+ }
2895+ return new FragmentSpreadSelection (
2896+ parentType ,
2897+ this . fragments ,
2898+ this . namedFragment ,
2899+ this . spreadDirectives ,
2900+ ) ;
28482901 }
28492902
28502903 canAddTo ( _ : CompositeType ) : boolean {
2851- // Mimicking the logic of `rebaseOn`.
2904+ // Since `rebaseOn` never fail, we copy the logic here and always return `true`. But as
2905+ // mentioned in `rebaseOn`, this leave it a bit to the caller to know what he is doing.
28522906 return true ;
28532907 }
28542908
0 commit comments