From 3e2c845c74407a136b9e0066e44c1ad1467d3013 Mon Sep 17 00:00:00 2001 From: Jeremy Lempereur Date: Fri, 12 Apr 2024 14:15:20 +0200 Subject: [PATCH] Type conditioned fetching (#2949) Type conditioned fetching Fixes #2938 When querying a field that is in a path of 2 or more unions, the query planner was not able to handle different selections and would aggressively collapse selections in fetches yielding an incorrect plan. This change introduces new syntax to express type conditions in (key and flatten) paths. Type conditioned fetching can be enabled through a flag, and execution is supported in the router only. --- .changeset/olive-papayas-act.md | 9 + gateway-js/src/index.ts | 8 +- .../src/__tests__/typeConditions.test.ts | 763 ++++++++++++++++++ query-planner-js/src/buildPlan.ts | 133 ++- query-planner-js/src/config.ts | 11 + 5 files changed, 898 insertions(+), 26 deletions(-) create mode 100644 .changeset/olive-papayas-act.md create mode 100644 query-planner-js/src/__tests__/typeConditions.test.ts diff --git a/.changeset/olive-papayas-act.md b/.changeset/olive-papayas-act.md new file mode 100644 index 000000000..6a845e28e --- /dev/null +++ b/.changeset/olive-papayas-act.md @@ -0,0 +1,9 @@ +--- +"@apollo/query-planner": patch +--- + +Type conditioned fetching + +When querying a field that is in a path of 2 or more unions, the query planner was not able to handle different selections and would aggressively collapse selections in fetches yielding an incorrect plan. + +This change introduces new syntax to express type conditions in (key and flatten) paths. Type conditioned fetching can be enabled through a flag, and execution is supported in the router only. (#2938) diff --git a/gateway-js/src/index.ts b/gateway-js/src/index.ts index 4254947db..786614356 100644 --- a/gateway-js/src/index.ts +++ b/gateway-js/src/index.ts @@ -57,6 +57,7 @@ import { LocalCompose, } from './supergraphManagers'; import { + assert, operationFromDocument, Schema, ServiceDefinition, @@ -186,7 +187,7 @@ export class ApolloGateway implements GatewayInterface { this.pollIntervalInMs = this.config?.pollIntervalInMs; } - this.issueConfigurationWarningsIfApplicable(); + this.validateConfigAndEmitWarnings(); this.logger.debug('Gateway successfully initialized (but not yet loaded)'); this.state = { phase: 'initialized' }; @@ -213,7 +214,9 @@ export class ApolloGateway implements GatewayInterface { }); } - private issueConfigurationWarningsIfApplicable() { + private validateConfigAndEmitWarnings() { + assert(!this.config.queryPlannerConfig?.typeConditionedFetching, "Type conditions are not supported in the gateway"); + // Warn against using the pollInterval and a serviceList simultaneously // TODO(trevor:removeServiceList) if (this.pollIntervalInMs && isServiceListConfig(this.config)) { @@ -568,6 +571,7 @@ export class ApolloGateway implements GatewayInterface { this.queryPlanStore.clear(); this.apiSchema = supergraph.apiSchema(); this.schema = addExtensions(this.apiSchema.toGraphQLJSSchema()); + this.queryPlanner = new QueryPlanner(supergraph, this.config.queryPlannerConfig); // Notify onSchemaChange listeners of the updated schema diff --git a/query-planner-js/src/__tests__/typeConditions.test.ts b/query-planner-js/src/__tests__/typeConditions.test.ts new file mode 100644 index 000000000..7908bf6b3 --- /dev/null +++ b/query-planner-js/src/__tests__/typeConditions.test.ts @@ -0,0 +1,763 @@ +import { operationFromDocument } from '@apollo/federation-internals'; +import gql from 'graphql-tag'; +import { + composeAndCreatePlanner, + composeAndCreatePlannerWithOptions, +} from './testHelper'; + +describe('Type Condition field merging', () => { + const subgraph1 = { + name: 's1', + typeDefs: gql` + type Query { + f1: [U1] + } + + union U1 = T1 | T2 + union U2 = T3 | T4 + + type T1 @key(fields: "id") { + id: ID! + f2: [U2] + } + + type T2 @key(fields: "id") { + id: ID! + f2: [U2] + } + + type T3 @key(fields: "id") { + id: ID! + } + + type T4 @key(fields: "id") { + id: ID! + } + `, + }; + + const subgraph2 = { + name: 's2', + typeDefs: gql` + type Query { + me: String + } + + type T3 @key(fields: "id") { + id: ID! + f2: String + f3(params: String): String + } + + type T4 @key(fields: "id") { + id: ID! + f3(params: String): String + } + `, + }; + + const [api, queryPlanner] = composeAndCreatePlanner(subgraph1, subgraph2); + const [_, queryPlannerWithTypeConditionedFetching] = + composeAndCreatePlannerWithOptions( + [subgraph1, subgraph2], + { typeConditionedFetching: true }, + false, + ); + + test('does eagerly merge fields on different type conditions if flag is absent', () => { + const operation = operationFromDocument( + api, + gql` + query f1($p1: String, $p2: String) { + f1 { + __typename + ... on T1 { + id + f2 { + ... on T3 { + id + f3(params: $p1) + } + } + } + ... on T2 { + id + f2 { + ... on T3 { + id + f3(params: $p2) + f2 + } + } + } + } + } + `, + ); + + const plan = queryPlanner.buildQueryPlan(operation); + expect(plan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "s1") { + { + f1 { + __typename + ... on T1 { + id + f2 { + __typename + ... on T3 { + __typename + id + } + } + } + ... on T2 { + id + f2 { + __typename + ... on T3 { + __typename + id + } + } + } + } + } + }, + Flatten(path: "f1.@.f2.@") { + Fetch(service: "s2") { + { + ... on T3 { + __typename + id + } + } => + { + ... on T3 { + f3(params: $p1) + f2 + } + } + }, + }, + }, + } + `); + }); + + test('does not eagerly merge fields on different type conditions if flag is present', () => { + const operation = operationFromDocument( + api, + gql` + query f1($p1: String, $p2: String) { + f1 { + ... on T1 { + f2 { + ... on T3 { + id + f2 + f3(params: $p1) + } + ... on T4 { + f3(params: $p1) + id + } + } + id + } + ... on T2 { + id + f2 { + ... on T4 { + f3(params: $p2) + } + ... on T3 { + f3(params: $p2) + } + } + } + } + } + `, + ); + + const plan = + queryPlannerWithTypeConditionedFetching.buildQueryPlan(operation); + expect(plan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "s1") { + { + f1 { + __typename + ... on T1 { + f2 { + __typename + ... on T3 { + __typename + id + } + ... on T4 { + __typename + id + } + } + id + } + ... on T2 { + id + f2 { + __typename + ... on T4 { + __typename + id + } + ... on T3 { + __typename + id + } + } + } + } + } + }, + Parallel { + Flatten(path: ".f1.@|[T1].f2.@") { + Fetch(service: "s2") { + { + ... on T3 { + __typename + id + } + ... on T4 { + __typename + id + } + } => + { + ... on T3 { + f2 + f3(params: $p1) + } + ... on T4 { + f3(params: $p1) + } + } + }, + }, + Flatten(path: ".f1.@|[T2].f2.@") { + Fetch(service: "s2") { + { + ... on T4 { + __typename + id + } + ... on T3 { + __typename + id + } + } => + { + ... on T4 { + f3(params: $p2) + } + ... on T3 { + f3(params: $p2) + } + } + }, + }, + }, + }, + } + `); + }); + + const subgraph3 = { + name: 's1', + typeDefs: gql` + type Query { + f1: [I1] + } + + union U1 = T3 | T4 + + interface I1 @key(fields: "id") { + id: ID! + f2: [U1] + } + + interface I2 implements I1 @key(fields: "id") { + id: ID! + f2: [U1] + } + + type T1 implements I2 & I1 @key(fields: "id") { + id: ID! + f2: [U1] + } + + type T5 implements I2 & I1 @key(fields: "id") { + id: ID! + f2: [U1] + } + + type T2 implements I1 @key(fields: "id") { + id: ID! + f2: [U1] + } + + type T3 @key(fields: "id") { + id: ID! + } + + type T4 @key(fields: "id") { + id: ID! + } + `, + }; + + test('does not eagerly merge fields on different type conditions if flag is present with interface', () => { + const [api2, queryPlanner2] = composeAndCreatePlannerWithOptions( + [subgraph3, subgraph2], + { typeConditionedFetching: true }, + false, + ); + + const operation = operationFromDocument( + api2, + gql` + query f1($p1: String, $p2: String) { + f1 { + __typename + ... on T1 { + id + f2 { + ... on T3 { + id + f3(params: $p1) + } + } + } + ... on T2 { + id + f2 { + ... on T3 { + id + f3(params: $p2) + f2 + } + } + } + } + } + `, + ); + + const plan = queryPlanner2.buildQueryPlan(operation); + expect(plan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "s1") { + { + f1 { + __typename + ... on T1 { + id + f2 { + __typename + ... on T3 { + __typename + id + } + } + } + ... on T2 { + id + f2 { + __typename + ... on T3 { + __typename + id + } + } + } + } + } + }, + Parallel { + Flatten(path: ".f1.@|[T1].f2.@") { + Fetch(service: "s2") { + { + ... on T3 { + __typename + id + } + } => + { + ... on T3 { + f3(params: $p1) + } + } + }, + }, + Flatten(path: ".f1.@|[T2].f2.@") { + Fetch(service: "s2") { + { + ... on T3 { + __typename + id + } + } => + { + ... on T3 { + f3(params: $p2) + f2 + } + } + }, + }, + }, + }, + } + `); + }); + + test('does generate type conditions with interface fragment', () => { + const [api2, queryPlanner2] = composeAndCreatePlannerWithOptions( + [subgraph3, subgraph2], + { typeConditionedFetching: true }, + ); + + const operation = operationFromDocument( + api2, + gql` + query f1($p1: String, $p2: String) { + f1 { + __typename + ... on I1 { + id + f2 { + ... on T3 { + id + f3(params: $p1) + } + } + } + ... on T2 { + id + f2 { + ... on T3 { + id + f3(params: $p2) + f2 + } + } + } + } + } + `, + ); + + const plan = queryPlanner2.buildQueryPlan(operation); + expect(plan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "s1") { + { + f1 { + __typename + id + f2 { + __typename + ... on T3 { + __typename + id + } + } + ... on T2 { + id + f2 { + __typename + ... on T3 { + __typename + id + } + } + } + } + } + }, + Parallel { + Flatten(path: ".f1.@.f2.@") { + Fetch(service: "s2") { + { + ... on T3 { + __typename + id + } + } => + { + ... on T3 { + f3(params: $p1) + } + } + }, + }, + Flatten(path: ".f1.@|[T2].f2.@") { + Fetch(service: "s2") { + { + ... on T3 { + __typename + id + } + } => + { + ... on T3 { + f3(params: $p2) + f2 + } + } + }, + }, + }, + }, + } + `); + }); + + test('does generate type conditions with interface fragment', () => { + const [api, queryPlanner] = composeAndCreatePlannerWithOptions( + [ + { + name: 's2', + typeDefs: gql` + type Query { + me: String + } + + type T3 @key(fields: "id") { + id: ID! + f2: String + f3(params: String): String + } + + type T4 @key(fields: "id") { + id: ID! + f3(params: String): String + } + `, + }, + { + name: 's1', + typeDefs: gql` + type Query { + f1: [U1] + oneF1: U1 + } + + union U1 = T1 | T2 + union U2 = T3 | T4 + + type T1 @key(fields: "id") { + id: ID! + f2: [U2] + oneU2: U2 + } + + type T2 @key(fields: "id") { + id: ID! + f2: [U2] + oneU2: U2 + } + + type T3 @key(fields: "id") { + id: ID! + } + + type T4 @key(fields: "id") { + id: ID! + } + `, + }, + ], + { typeConditionedFetching: true }, + ); + + const operation = operationFromDocument( + api, + gql` + query f1($p1: String, $p2: String) { + oneF1 { + __typename + ... on T1 { + id + f2 { + ... on T3 { + id + f3(params: $p1) + } + } + } + ... on T2 { + id + f2 { + ... on T3 { + id + f3(params: $p2) + f2 + } + } + } + } + f1 { + __typename + ... on T1 { + id + f2 { + ... on T3 { + id + f3(params: $p1) + } + } + } + ... on T2 { + id + f2 { + ... on T3 { + id + f3(params: $p2) + f2 + } + } + } + } + } + `, + ); + + const plan = queryPlanner.buildQueryPlan(operation); + expect(plan).toMatchInlineSnapshot(` + QueryPlan { + Sequence { + Fetch(service: "s1") { + { + oneF1 { + __typename + ... on T1 { + id + f2 { + __typename + ... on T3 { + __typename + id + } + } + } + ... on T2 { + id + f2 { + __typename + ... on T3 { + __typename + id + } + } + } + } + f1 { + __typename + ... on T1 { + id + f2 { + __typename + ... on T3 { + __typename + id + } + } + } + ... on T2 { + id + f2 { + __typename + ... on T3 { + __typename + id + } + } + } + } + } + }, + Parallel { + Flatten(path: ".oneF1|[T1].f2.@") { + Fetch(service: "s2") { + { + ... on T3 { + __typename + id + } + } => + { + ... on T3 { + f3(params: $p1) + } + } + }, + }, + Flatten(path: ".oneF1|[T2].f2.@") { + Fetch(service: "s2") { + { + ... on T3 { + __typename + id + } + } => + { + ... on T3 { + f3(params: $p2) + f2 + } + } + }, + }, + Flatten(path: ".f1.@|[T1].f2.@") { + Fetch(service: "s2") { + { + ... on T3 { + __typename + id + } + } => + { + ... on T3 { + f3(params: $p1) + } + } + }, + }, + Flatten(path: ".f1.@|[T2].f2.@") { + Fetch(service: "s2") { + { + ... on T3 { + __typename + id + } + } => + { + ... on T3 { + f3(params: $p2) + f2 + } + } + }, + }, + }, + }, + } + `); + }); +}); diff --git a/query-planner-js/src/buildPlan.ts b/query-planner-js/src/buildPlan.ts index 823e7be52..bf1fe75b4 100644 --- a/query-planner-js/src/buildPlan.ts +++ b/query-planner-js/src/buildPlan.ts @@ -57,10 +57,11 @@ import { isDefined, InterfaceType, FragmentSelection, - possibleRuntimeTypes, typesCanBeMerged, Supergraph, sameType, + possibleRuntimeTypes, + NamedType, } from "@apollo/federation-internals"; import { advanceSimultaneousPathsWithOperation, @@ -378,6 +379,7 @@ class QueryPlanningTraversal { private stack: [Selection, SimultaneousPathsWithLazyIndirectPaths[]][]; private readonly closedBranches: ClosedBranch[] = []; private readonly optionsLimit: number | null; + private readonly typeConditionedFetching: boolean; constructor( readonly parameters: PlanningParameters, @@ -387,10 +389,12 @@ class QueryPlanningTraversal { private readonly rootKind: SchemaRootKind, readonly costFunction: CostFunction, initialContext: PathContext, + typeConditionedFetching: boolean, excludedDestinations: ExcludedDestinations = [], excludedConditions: ExcludedConditions = [], ) { const { root, federatedQueryGraph } = parameters; + this.typeConditionedFetching = typeConditionedFetching || false; this.isTopLevel = isRootVertex(root); this.optionsLimit = parameters.config.debug?.pathsLimit; this.conditionResolver = cachingConditionResolver( @@ -747,8 +751,8 @@ class QueryPlanningTraversal { private updatedDependencyGraph(dependencyGraph: FetchDependencyGraph, tree: OpPathTree): FetchDependencyGraph { return isRootPathTree(tree) - ? computeRootFetchGroups(dependencyGraph, tree, this.rootKind) - : computeNonRootFetchGroups(dependencyGraph, tree, this.rootKind); + ? computeRootFetchGroups(dependencyGraph, tree, this.rootKind, this.typeConditionedFetching) + : computeNonRootFetchGroups(dependencyGraph, tree, this.rootKind, this.typeConditionedFetching); } private resolveConditionPlan(edge: Edge, context: PathContext, excludedDestinations: ExcludedDestinations, excludedConditions: ExcludedConditions): ConditionResolution { @@ -763,6 +767,7 @@ class QueryPlanningTraversal { 'query', this.costFunction, context, + this.typeConditionedFetching, excludedDestinations, addConditionExclusion(excludedConditions, edge.conditions), ).findBestPlan(); @@ -1784,11 +1789,16 @@ class GroupPath { private readonly fullPath: OperationPath, private readonly pathInGroup: OperationPath, private readonly responsePath: ResponsePath, + private readonly typeConditionedFetching: boolean, + private readonly possibleTypes: ObjectType[], + private readonly possibleTypesAfterLastField: ObjectType[], ) { } - static empty(): GroupPath { - return new GroupPath([], [], []); + static empty(typeConditionedFetching: boolean, rootType: CompositeType): GroupPath { + const rootPossibleRuntimeTypes = typeConditionedFetching ? Array.from(possibleRuntimeTypes(rootType)): []; + rootPossibleRuntimeTypes.sort(); + return new GroupPath([], [], [], typeConditionedFetching, rootPossibleRuntimeTypes, rootPossibleRuntimeTypes); } inGroup(): OperationPath { @@ -1808,6 +1818,9 @@ class GroupPath { this.fullPath, newGroupContext, this.responsePath, + this.typeConditionedFetching, + this.possibleTypes, + this.possibleTypesAfterLastField, ); } @@ -1820,35 +1833,94 @@ class GroupPath { // implements it (in the supergraph). concatOperationPaths(pathOfGroupInParent, filterOperationPath(this.pathInGroup, parentSchema)), this.responsePath, + this.typeConditionedFetching, + this.possibleTypes, + this.possibleTypesAfterLastField ); } - private updatedResponsePath(element: OperationElement) { - if (element.kind !== 'Field') { - return this.responsePath; - } + private updatedResponsePath(element: OperationElement): ResponsePath { + switch (element.kind){ + case 'FragmentElement': + return this.responsePath; + case 'Field': + let newPath = this.responsePath; + if (this.possibleTypesAfterLastField.length !== this.possibleTypes.length) { + const conditions = `|[${this.possibleTypes.join(',')}]`; + const previousLastElement = newPath[newPath.length -1] as string || ''; - let type = element.definition.type!; - const newPath = this.responsePath.concat(element.responseName()); - while (!isNamedType(type)) { - if (isListType(type)) { - newPath.push('@'); - } - type = type.ofType; + if (previousLastElement.startsWith('|[')) { + newPath = [...newPath.slice(0, -1), conditions]; + } else { + newPath = [...newPath.slice(0, -1), `${previousLastElement}${conditions}`]; + } + } + let type = element.definition.type!; + if (newPath.length === 0 && this.typeConditionedFetching) { + newPath = newPath.concat(''); + } + newPath = newPath.concat(`${element.responseName()}`); + while (!isNamedType(type)) { + if (isListType(type)) { + newPath.push('@'); + } + type = type.ofType; + } + return newPath; } - return newPath; } add(element: OperationElement): GroupPath { + const responsePath = this.updatedResponsePath(element); + const newPossibleTypes = this.computeNewPossibleTypes(element); return new GroupPath( this.fullPath.concat(element), this.pathInGroup.concat(element), - this.updatedResponsePath(element), + responsePath, + this.typeConditionedFetching, + newPossibleTypes, + element.kind === 'Field'? newPossibleTypes: this.possibleTypesAfterLastField ); } toString() { - return `[${this.fullPath}]:[${this.pathInGroup}]`; + return this.inResponse().join('.'); + } + + computeNewPossibleTypes(element: OperationElement): ObjectType[] { + if (!this.typeConditionedFetching) { + return []; + } + switch (element.kind){ + case 'FragmentElement': + if (!element.typeCondition) { + return this.possibleTypes; + } + const elementPossibleTypes = possibleRuntimeTypes(element.typeCondition); + return this.possibleTypes.filter((pt) => elementPossibleTypes.some((ept) => ept.name === pt.name)); + case 'Field': + return this.advanceFieldType(element); + } + } + + + advanceFieldType(element: Field): ObjectType[] { + if (!isCompositeType(element.baseType())) { + return []; + } + + const res = Array.from( + new Set( + this.possibleTypes.map( + (pt) => possibleRuntimeTypes( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + baseType(pt.field(element.name)!.type!) as CompositeType + ) + ).flat() + ) + ); + res.sort(); + return res; } } @@ -2138,6 +2210,11 @@ class FetchDependencyGraph { return cloned; } + + supergraphSchemaType(typeName: string): NamedType | undefined { + return this.supergraphSchema.type(typeName) + } + getOrCreateRootFetchGroup({ subgraphName, rootKind, @@ -3495,6 +3572,7 @@ function computeRootParallelBestPlan( parameters.root.rootKind, defaultCostFunction, emptyContext, + parameters.config.typeConditionedFetching, ); const plan = planningTraversal.findBestPlan(); // Getting no plan means the query is essentially unsatisfiable (it's a valid query, but we can prove it will never return a result), @@ -3554,6 +3632,7 @@ function computeRootSerialDependencyGraph( ), prevPaths, root.rootKind, + parameters.config.typeConditionedFetching ); } else { startingFetchId = prevDepGraph.nextFetchId(); @@ -3817,7 +3896,7 @@ function samePathsInParents(first: OperationPath | undefined, second: OperationP return !!second && sameOperationPaths(first, second); } -function computeRootFetchGroups(dependencyGraph: FetchDependencyGraph, pathTree: OpRootPathTree, rootKind: SchemaRootKind): FetchDependencyGraph { +function computeRootFetchGroups(dependencyGraph: FetchDependencyGraph, pathTree: OpRootPathTree, rootKind: SchemaRootKind, typeConditionedFetching: boolean): FetchDependencyGraph { // The root of the pathTree is one of the "fake" root of the subgraphs graph, which belongs to no subgraph but points to each ones. // So we "unpack" the first level of the tree to find out our top level groups (and initialize our stack). // Note that we can safely ignore the triggers of that first level as it will all be free transition, and we know we cannot have conditions. @@ -3827,18 +3906,24 @@ function computeRootFetchGroups(dependencyGraph: FetchDependencyGraph, pathTree: // The edge tail type is one of the subgraph root type, so it has to be an ObjectType. const rootType = edge.tail.type as ObjectType; const group = dependencyGraph.getOrCreateRootFetchGroup({ subgraphName, rootKind, parentType: rootType }); - computeGroupsForTree(dependencyGraph, child, group, GroupPath.empty(), emptyDeferContext); + // If a type is in a subgraph, it has to be in the supergraph. + // A root type has to be a Composite type. + const rootTypeInSupergraph = dependencyGraph.supergraphSchemaType(rootType.name) as CompositeType; + computeGroupsForTree(dependencyGraph, child, group, GroupPath.empty(typeConditionedFetching, rootTypeInSupergraph), emptyDeferContext); } return dependencyGraph; } -function computeNonRootFetchGroups(dependencyGraph: FetchDependencyGraph, pathTree: OpPathTree, rootKind: SchemaRootKind): FetchDependencyGraph { +function computeNonRootFetchGroups(dependencyGraph: FetchDependencyGraph, pathTree: OpPathTree, rootKind: SchemaRootKind, typeConditionedFetching: boolean): FetchDependencyGraph { const subgraphName = pathTree.vertex.source; // The edge tail type is one of the subgraph root type, so it has to be an ObjectType. const rootType = pathTree.vertex.type; assert(isCompositeType(rootType), () => `Should not have condition on non-selectable type ${rootType}`); - const group = dependencyGraph.getOrCreateRootFetchGroup({ subgraphName, rootKind, parentType: rootType } ); - computeGroupsForTree(dependencyGraph, pathTree, group, GroupPath.empty(), emptyDeferContext); + const group = dependencyGraph.getOrCreateRootFetchGroup({ subgraphName, rootKind, parentType: rootType} ); + // If a type is in a subgraph, it has to be in the supergraph. + // A root type has to be a Composite type. + const rootTypeInSupergraph = dependencyGraph.supergraphSchemaType(rootType.name) as CompositeType; + computeGroupsForTree(dependencyGraph, pathTree, group, GroupPath.empty(typeConditionedFetching, rootTypeInSupergraph), emptyDeferContext); return dependencyGraph; } diff --git a/query-planner-js/src/config.ts b/query-planner-js/src/config.ts index 1cf014c62..db460174f 100644 --- a/query-planner-js/src/config.ts +++ b/query-planner-js/src/config.ts @@ -108,6 +108,16 @@ export type QueryPlannerConfig = { */ pathsLimit?: number | null }, + + /** + * Enables type conditioned fetching. + * This flag is a workaround, which may yield significant + * performance degradation when computing query plans, + * and increase query plan size. + * + * If you aren't aware of this flag, you probably don't need it. + */ + typeConditionedFetching?: boolean, } export function enforceQueryPlannerConfigDefaults( @@ -132,6 +142,7 @@ export function enforceQueryPlannerConfigDefaults( pathsLimit: null, ...config?.debug, }, + typeConditionedFetching: config?.typeConditionedFetching || false, }; }