@@ -600,13 +600,18 @@ export class NonLocalSelectionsMetadata {
600600 * This calls {@link checkNonLocalSelectionsLimitExceeded} for each of the
601601 * selections in the open branches stack; see that function's doc comment for
602602 * more information.
603+ *
604+ * To support mutations, we allow indicating the initial subgraph is
605+ * constrained, in which case indirect options will be ignored until the first
606+ * field (similar to query planning).
603607 */
604608 checkNonLocalSelectionsLimitExceededAtRoot (
605609 stack : [ Selection , SimultaneousPathsWithLazyIndirectPaths [ ] ] [ ] ,
606610 state : NonLocalSelectionsState ,
607611 supergraphSchema : Schema ,
608612 inconsistentAbstractTypesRuntimes : Set < string > ,
609613 overrideConditions : Map < string , boolean > ,
614+ isInitialSubgraphConstrained : boolean ,
610615 ) : boolean {
611616 for ( const [ selection , simultaneousPaths ] of stack ) {
612617 const tailVertices = new Set < Vertex > ( ) ;
@@ -616,7 +621,10 @@ export class NonLocalSelectionsMetadata {
616621 }
617622 }
618623 const tailVerticesInfo =
619- this . estimateVerticesWithIndirectOptions ( tailVertices ) ;
624+ this . estimateVerticesWithIndirectOptions (
625+ tailVertices ,
626+ isInitialSubgraphConstrained ,
627+ ) ;
620628
621629 // Note that top-level selections aren't avoided via fully-local selection
622630 // set optimization, so we always add them here.
@@ -626,12 +634,16 @@ export class NonLocalSelectionsMetadata {
626634
627635 if ( selection . selectionSet ) {
628636 const selectionHasDefer = selection . hasDefer ( ) ;
637+ const isInitialSubgraphConstrainedAfterElement =
638+ isInitialSubgraphConstrained
639+ && selection . kind === 'FragmentSelection' ;
629640 const nextVertices = this . estimateNextVerticesForSelection (
630641 selection . element ,
631642 tailVerticesInfo ,
632643 state ,
633644 supergraphSchema ,
634645 overrideConditions ,
646+ isInitialSubgraphConstrainedAfterElement ,
635647 ) ;
636648 if ( this . checkNonLocalSelectionsLimitExceeded (
637649 selection . selectionSet ,
@@ -641,6 +653,7 @@ export class NonLocalSelectionsMetadata {
641653 supergraphSchema ,
642654 inconsistentAbstractTypesRuntimes ,
643655 overrideConditions ,
656+ isInitialSubgraphConstrainedAfterElement ,
644657 ) ) {
645658 return true ;
646659 }
@@ -674,6 +687,10 @@ export class NonLocalSelectionsMetadata {
674687 * Note that this function takes in whether the parent selection of the
675688 * selection set has @defer, as that affects whether the optimization is
676689 * disabled for that selection set.
690+ *
691+ * To support mutations, we allow indicating the initial subgraph is
692+ * constrained, in which case indirect options will be ignored until the first
693+ * field (similar to query planning).
677694 */
678695 private checkNonLocalSelectionsLimitExceeded (
679696 selectionSet : SelectionSet ,
@@ -683,6 +700,7 @@ export class NonLocalSelectionsMetadata {
683700 supergraphSchema : Schema ,
684701 inconsistentAbstractTypesRuntimes : Set < string > ,
685702 overrideConditions : Map < string , boolean > ,
703+ isInitialSubgraphConstrained : boolean ,
686704 ) : boolean {
687705 // Compute whether the selection set is non-local, and if so, add its
688706 // selections to the count. Any of the following causes the selection set to
@@ -709,12 +727,16 @@ export class NonLocalSelectionsMetadata {
709727
710728 const oldCount = state . count ;
711729 if ( selection . selectionSet ) {
730+ const isInitialSubgraphConstrainedAfterElement =
731+ isInitialSubgraphConstrained
732+ && selection . kind === 'FragmentSelection' ;
712733 const nextVertices = this . estimateNextVerticesForSelection (
713734 element ,
714735 parentVertices ,
715736 state ,
716737 supergraphSchema ,
717738 overrideConditions ,
739+ isInitialSubgraphConstrainedAfterElement ,
718740 ) ;
719741 if ( this . checkNonLocalSelectionsLimitExceeded (
720742 selection . selectionSet ,
@@ -724,6 +746,7 @@ export class NonLocalSelectionsMetadata {
724746 supergraphSchema ,
725747 inconsistentAbstractTypesRuntimes ,
726748 overrideConditions ,
749+ isInitialSubgraphConstrainedAfterElement ,
727750 ) ) {
728751 return true ;
729752 }
@@ -822,13 +845,20 @@ export class NonLocalSelectionsMetadata {
822845 * selection for a set of parent vertices (including indirect options), this
823846 * function can be used to estimate an upper bound on the next vertices after
824847 * taking the selection (also with indirect options).
848+ *
849+ * To support mutations, we allow indicating the initial subgraph will be
850+ * constrained after taking the element, in which case indirect options will
851+ * be ignored (and caching will be skipped). This is to ensure that top-level
852+ * mutation fields are not executed on a different subgraph than the initial
853+ * one during query planning.
825854 */
826855 private estimateNextVerticesForSelection (
827856 element : OperationElement ,
828857 parentVertices : NextVerticesInfo ,
829858 state : NonLocalSelectionsState ,
830859 supergraphSchema : Schema ,
831860 overrideConditions : Map < string , boolean > ,
861+ isInitialSubgraphConstrainedAfterElement : boolean ,
832862 ) : NextVerticesInfo {
833863 const selectionKey = element . kind === 'Field'
834864 ? element . definition . name
@@ -837,6 +867,28 @@ export class NonLocalSelectionsMetadata {
837867 // For empty type condition, the vertices don't change.
838868 return parentVertices ;
839869 }
870+ if ( isInitialSubgraphConstrainedAfterElement ) {
871+ // When the initial subgraph is constrained, skip caching entirely. Note
872+ // that caching is not skipped when the initial subgraph is constrained
873+ // before this element but not after. Because of that, there may be cache
874+ // entries for remaining vertices that were actually part of a complete
875+ // digraph, but this is only a slight caching inefficiency and doesn't
876+ // affect the computation's result.
877+ assert (
878+ parentVertices . nextVerticesWithIndirectOptions . types . size === 0 ,
879+ ( ) => 'Initial subgraph was constrained which indicates no indirect'
880+ + ' options should be taken, but the parent vertices unexpectedly had'
881+ + ' a complete digraph which indicates indirect options were taken'
882+ + ' upstream in the path.' ,
883+ ) ;
884+ return this . estimateNextVerticesForSelectionWithoutCaching (
885+ element ,
886+ parentVertices . nextVerticesWithIndirectOptions . remainingVertices ,
887+ supergraphSchema ,
888+ overrideConditions ,
889+ true ,
890+ ) ;
891+ }
840892 let cache = state . nextVerticesCache . get ( selectionKey ) ;
841893 if ( ! cache ) {
842894 cache = {
@@ -866,6 +918,7 @@ export class NonLocalSelectionsMetadata {
866918 indirectOptions . sameTypeOptions ,
867919 supergraphSchema ,
868920 overrideConditions ,
921+ false ,
869922 ) ;
870923 cache . typesToNextVertices . set ( typeName , cacheEntry ) ;
871924 }
@@ -879,6 +932,7 @@ export class NonLocalSelectionsMetadata {
879932 [ vertex ] ,
880933 supergraphSchema ,
881934 overrideConditions ,
935+ false ,
882936 ) ;
883937 cache . remainingVerticesToNextVertices . set ( vertex , cacheEntry ) ;
884938 }
@@ -922,16 +976,23 @@ export class NonLocalSelectionsMetadata {
922976 * (We do account for override conditions, which are relatively
923977 * straightforward.)
924978 *
925- * Since we're iterating through next vertices in the process, for efficiency
926- * sake we also compute whether there are any reachable cross-subgraph edges
927- * from the next vertices (without indirect options). This method assumes that
928- * inline fragments have type conditions.
979+ * Since we're iterating through next vertices in the process, for
980+ * efficiency's sake we also compute whether there are any reachable
981+ * cross-subgraph edges from the next vertices (without indirect options).
982+ * This method assumes that inline fragments have type conditions.
983+ *
984+ * To support mutations, we allow indicating the initial subgraph will be
985+ * constrained after taking the element, in which case indirect options will
986+ * be ignored. This is to ensure that top-level mutation fields are not
987+ * executed on a different subgraph than the initial one during query
988+ * planning.
929989 */
930990 private estimateNextVerticesForSelectionWithoutCaching (
931991 element : OperationElement ,
932992 parentVertices : Iterable < Vertex > ,
933993 supergraphSchema : Schema ,
934994 overrideConditions : Map < string , boolean > ,
995+ isInitialSubgraphConstrainedAfterElement : boolean ,
935996 ) : NextVerticesInfo {
936997 const nextVertices = new Set < Vertex > ( ) ;
937998 switch ( element . kind ) {
@@ -955,7 +1016,7 @@ export class NonLocalSelectionsMetadata {
9551016 }
9561017 } ;
9571018 for ( const vertex of parentVertices ) {
958- // As an upper bound for efficiency sake, we consider both
1019+ // As an upper bound for efficiency's sake, we consider both
9591020 // non-type-exploded and type-exploded options.
9601021 processHeadVertex ( vertex ) ;
9611022 const downcasts = this . verticesToObjectTypeDowncasts . get ( vertex ) ;
@@ -1041,16 +1102,33 @@ export class NonLocalSelectionsMetadata {
10411102 assertUnreachable ( element ) ;
10421103 }
10431104
1044- return this . estimateVerticesWithIndirectOptions ( nextVertices ) ;
1105+ return this . estimateVerticesWithIndirectOptions (
1106+ nextVertices ,
1107+ isInitialSubgraphConstrainedAfterElement ,
1108+ ) ;
10451109 }
10461110
10471111 /**
1048- * Estimate the indirect options for the given next vertices, and add them to
1049- * the given vertices. As an upper bound for efficiency's sake, we assume we
1050- * can take any indirect option (i.e. ignore any edge conditions).
1112+ * Estimate the indirect options for the given next vertices, and return the
1113+ * given next vertices along with `nextVerticesWithIndirectOptions` which
1114+ * contains these direct and indirect options. As an upper bound for
1115+ * efficiency's sake, we assume we can take any indirect option (i.e. ignore
1116+ * any edge conditions).
1117+ *
1118+ * Since we're iterating through next vertices in the process, for
1119+ * efficiency's sake we also compute whether there are any reachable
1120+ * cross-subgraph edges from the next vertices (without indirect options).
1121+ *
1122+ * To support mutations, we allow ignoring indirect options, as we don't want
1123+ * top-level mutation fields to be executed on a different subgraph than the
1124+ * initial one. In that case, `nextVerticesWithIndirectOptions` will not have
1125+ * any `types`, and the given vertices will be added to `remainingVertices`
1126+ * (despite them potentially being part of the complete digraph for their
1127+ * type). This is fine, as caching logic accounts for this accordingly.
10511128 */
10521129 private estimateVerticesWithIndirectOptions (
10531130 nextVertices : Set < Vertex > ,
1131+ ignoreIndirectOptions : boolean ,
10541132 ) : NextVerticesInfo {
10551133 const nextVerticesInfo : NextVerticesInfo = {
10561134 nextVertices,
@@ -1063,7 +1141,16 @@ export class NonLocalSelectionsMetadata {
10631141 for ( const nextVertex of nextVertices ) {
10641142 nextVerticesInfo . nextVerticesHaveReachableCrossSubgraphEdges ||=
10651143 nextVertex . hasReachableCrossSubgraphEdges ;
1066-
1144+
1145+ // As noted above, we don't want top-level mutation fields to be executed
1146+ // on a different subgraph than the initial one, so we support ignoring
1147+ // indirect options here.
1148+ if ( ignoreIndirectOptions ) {
1149+ nextVerticesInfo . nextVerticesWithIndirectOptions . remainingVertices
1150+ . add ( nextVertex ) ;
1151+ continue ;
1152+ }
1153+
10671154 const typeName = nextVertex . type . name
10681155 const optionsMetadata = this . typesToIndirectOptions . get ( typeName ) ;
10691156 if ( optionsMetadata ) {
@@ -1085,7 +1172,7 @@ export class NonLocalSelectionsMetadata {
10851172 continue ;
10861173 }
10871174 }
1088- // We need to add the remaining vertex, and if its our first time seeing
1175+ // We need to add the remaining vertex, and if it's our first time seeing
10891176 // it, we also add any of its interface object options.
10901177 if (
10911178 ! nextVerticesInfo . nextVerticesWithIndirectOptions . remainingVertices
0 commit comments