Skip to content

Commit cab383b

Browse files
author
Sylvain Lebresne
authored
Optim local value types (#2449)
Often times, value types only references either "leaf" types or other value types, but no entity type or root types. When a sub-part of a query arrives to such a type, then we know that the rest of the subselection is going to also be part of whichever fetch we're currently building. We can use that knowledge to save work. This is what this commit does. First, it pre-computes when building the query planner which types have no reachable entity or root type starting from them. Then, when computing query plans, it checks when such types is reached, and when it is, it short-cuts the building of the `GraphPath`s and `PathTree`s for the remaining sub-selections. When subgraphs have a large number of value types (especially some deeply nested ones), this can measurably speed up query plan generation. This is particularly true when one federate either a single subgraph or one subgraph is much large, which is a corner cases, but can happen in the process of migrating an existin monolith to federation).
1 parent 260c357 commit cab383b

9 files changed

Lines changed: 569 additions & 274 deletions

File tree

.changeset/twenty-shirts-battle.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"apollo-federation-integration-testsuite": patch
3+
"@apollo/query-planner": patch
4+
"@apollo/query-graphs": patch
5+
"@apollo/composition": patch
6+
"@apollo/federation-internals": patch
7+
"@apollo/subgraph": patch
8+
"@apollo/gateway": patch
9+
---
10+
11+
Optimises query plan generation for parts of queries that can statically be known to not cross across subgraphs
12+

gateway-js/src/__tests__/executeQueryPlan.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3165,7 +3165,8 @@ describe('executeQueryPlan', () => {
31653165
data {
31663166
__typename
31673167
foo
3168-
... on Data {
3168+
... on Bar {
3169+
__typename
31693170
bar
31703171
}
31713172
}

internals-js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"dependencies": {
2626
"chalk": "^4.1.0",
2727
"js-levenshtein": "^1.1.6",
28-
"@types/uuid": "^8.3.4",
28+
"@types/uuid": "^9.0.0",
2929
"uuid": "^9.0.0"
3030
},
3131
"publishConfig": {

internals-js/src/operations.ts

Lines changed: 176 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
sameDirectiveApplication,
4747
isLeafType,
4848
Variables,
49+
isObjectType,
4950
} from "./definitions";
5051
import { ERRORS } from "./error";
5152
import { isDirectSubtype, sameType } from "./types";
@@ -750,6 +751,21 @@ export class Operation {
750751
);
751752
}
752753

754+
trimUnsatisfiableBranches(): Operation {
755+
const trimmedSelections = this.selectionSet.trimUnsatisfiableBranches(this.selectionSet.parentType);
756+
if (trimmedSelections === this.selectionSet) {
757+
return this;
758+
}
759+
760+
return new Operation(
761+
this.schema,
762+
this.rootKind,
763+
trimmedSelections,
764+
this.variableDefinitions,
765+
this.name
766+
);
767+
}
768+
753769
/**
754770
* Returns this operation but potentially modified so all/some of the @defer applications have been removed.
755771
*
@@ -971,6 +987,14 @@ export class NamedFragments {
971987
return this.fragments.values();
972988
}
973989

990+
map(mapper: (def: NamedFragmentDefinition) => NamedFragmentDefinition): NamedFragments {
991+
const mapped = new NamedFragments();
992+
for (const def of this.fragments.values()) {
993+
mapped.fragments.set(def.name, mapper(def));
994+
}
995+
return mapped;
996+
}
997+
974998
validate(variableDefinitions: VariableDefinitions) {
975999
for (const fragment of this.fragments.values()) {
9761000
fragment.selectionSet.validate(variableDefinitions);
@@ -1125,19 +1149,23 @@ export class SelectionSet {
11251149
// without any fragments, we don't bother handling this more complex case.
11261150
assert(!this.fragments || this.fragments.isEmpty(), `Should not be called on selection that already has named fragments, but got ${this.fragments}`)
11271151

1128-
return this.lazyMap((selection) => selection.optimize(fragments), fragments);
1152+
return this.lazyMap((selection) => selection.optimize(fragments), { fragments });
11291153
}
11301154

11311155
expandAllFragments(): SelectionSet {
1132-
return this.lazyMap((selection) => selection.expandAllFragments(), null);
1156+
return this.lazyMap((selection) => selection.expandAllFragments(), { fragments: null });
11331157
}
11341158

11351159
expandFragments(names: string[], updatedFragments: NamedFragments | undefined): SelectionSet {
11361160
if (names.length === 0) {
11371161
return this;
11381162
}
11391163

1140-
return this.lazyMap((selection) => selection.expandFragments(names, updatedFragments), updatedFragments ?? null);
1164+
return this.lazyMap((selection) => selection.expandFragments(names, updatedFragments), { fragments: updatedFragments ?? null });
1165+
}
1166+
1167+
trimUnsatisfiableBranches(parentType: CompositeType): SelectionSet {
1168+
return this.lazyMap((selection) => selection.trimUnsatisfiableBranches(parentType), { parentType });
11411169
}
11421170

11431171
/**
@@ -1150,9 +1178,13 @@ export class SelectionSet {
11501178
*/
11511179
lazyMap(
11521180
mapper: (selection: Selection) => Selection | readonly Selection[] | SelectionSet | undefined,
1153-
updatedFragments?: NamedFragments | null,
1181+
options?: {
1182+
fragments?: NamedFragments | null,
1183+
parentType?: CompositeType,
1184+
}
11541185
): SelectionSet {
11551186
const selections = this.selections();
1187+
const updatedFragments = options?.fragments;
11561188
const newFragments = updatedFragments === undefined ? this.fragments : (updatedFragments ?? undefined);
11571189

11581190
let updatedSelections: SelectionSetUpdates | undefined = undefined;
@@ -1172,7 +1204,7 @@ export class SelectionSet {
11721204
if (!updatedSelections) {
11731205
return this.withUpdatedFragments(newFragments);
11741206
}
1175-
return updatedSelections.toSelectionSet(this.parentType, newFragments);
1207+
return updatedSelections.toSelectionSet(options?.parentType ?? this.parentType, newFragments);
11761208
}
11771209

11781210
private withUpdatedFragments(newFragments: NamedFragments | undefined): SelectionSet {
@@ -1189,6 +1221,10 @@ export class SelectionSet {
11891221
return this.lazyMap((selection) => selection.withNormalizedDefer(normalizer));
11901222
}
11911223

1224+
hasDefer(): boolean {
1225+
return this.selections().some((s) => s.hasDefer());
1226+
}
1227+
11921228
/**
11931229
* Returns the selection select from filtering out any selection that does not match the provided predicate.
11941230
*
@@ -1355,6 +1391,18 @@ export class SelectionSet {
13551391
}
13561392
}
13571393

1394+
/**
1395+
* Returns true if any of the element in this selection set matches the provided predicate.
1396+
*/
1397+
some(predicate: (elt: OperationElement) => boolean): boolean {
1398+
for (const selection of this.selections()) {
1399+
if (predicate(selection.element) || (selection.selectionSet && selection.selectionSet.some(predicate))) {
1400+
return true;
1401+
}
1402+
}
1403+
return false;
1404+
}
1405+
13581406
toOperationString(
13591407
rootKind: SchemaRootKind,
13601408
variableDefinitions: VariableDefinitions,
@@ -1766,7 +1814,7 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
17661814
return this.withUpdatedComponents(element, this.selectionSet);
17671815
}
17681816

1769-
protected mapToSelectionSet(mapper: (s: SelectionSet) => SelectionSet): TOwnType {
1817+
mapToSelectionSet(mapper: (s: SelectionSet) => SelectionSet): TOwnType {
17701818
if (!this.selectionSet) {
17711819
return this.us();
17721820
}
@@ -1781,9 +1829,13 @@ abstract class AbstractSelection<TElement extends OperationElement, TIsLeaf exte
17811829

17821830
abstract withNormalizedDefer(normalizer: DeferNormalizer): TOwnType | SelectionSet;
17831831

1832+
abstract hasDefer(): boolean;
1833+
17841834
abstract expandAllFragments(): TOwnType | readonly Selection[];
17851835

17861836
abstract expandFragments(names: string[], updatedFragments: NamedFragments | undefined): TOwnType | readonly Selection[];
1837+
1838+
abstract trimUnsatisfiableBranches(parentType: CompositeType): TOwnType | SelectionSet | undefined;
17871839
}
17881840

17891841
export class FieldSelection extends AbstractSelection<Field<any>, undefined, FieldSelection> {
@@ -1953,10 +2005,39 @@ export class FieldSelection extends AbstractSelection<Field<any>, undefined, Fie
19532005
return this.mapToSelectionSet((s) => s.withNormalizedDefer(normalizer));
19542006
}
19552007

2008+
hasDefer(): boolean {
2009+
return !!this.selectionSet?.hasDefer();
2010+
}
2011+
19562012
expandAllFragments(): FieldSelection {
19572013
return this.mapToSelectionSet((s) => s.expandAllFragments());
19582014
}
19592015

2016+
trimUnsatisfiableBranches(_: CompositeType): FieldSelection {
2017+
if (!this.selectionSet) {
2018+
return this;
2019+
}
2020+
2021+
const base = baseType(this.element.definition.type!)
2022+
assert(isCompositeType(base), () => `Field ${this.element} should not have a sub-selection`);
2023+
const trimmed = this.mapToSelectionSet((s) => s.trimUnsatisfiableBranches(base));
2024+
// In rare caes, it's possible that everything in the sub-selection was trimmed away and so the
2025+
// sub-selection is empty. Which suggest something may be wrong with this part of the query
2026+
// intent, but the query was valid while keeping an empty sub-selection isn't. So in that
2027+
// case, we just add some "non-included" __typename field just to keep the query valid.
2028+
if (trimmed.selectionSet?.isEmpty()) {
2029+
return trimmed.withUpdatedSelectionSet(selectionSetOfElement(
2030+
new Field(
2031+
base.typenameField()!,
2032+
undefined,
2033+
[new Directive('include', { 'if': false })],
2034+
)
2035+
));
2036+
} else {
2037+
return trimmed;
2038+
}
2039+
}
2040+
19602041
expandFragments(names: string[], updatedFragments: NamedFragments | undefined): FieldSelection {
19612042
return this.mapToSelectionSet((s) => s.expandFragments(names, updatedFragments));
19622043
}
@@ -2021,6 +2102,10 @@ export abstract class FragmentSelection extends AbstractSelection<FragmentElemen
20212102

20222103
return predicate(thisWithFilteredSelectionSet) ? thisWithFilteredSelectionSet : undefined;
20232104
}
2105+
2106+
hasDefer(): boolean {
2107+
return this.element.hasDefer() || this.selectionSet.hasDefer();
2108+
}
20242109

20252110
equals(that: Selection): boolean {
20262111
if (this === that) {
@@ -2179,6 +2264,87 @@ class InlineFragmentSelection extends FragmentSelection {
21792264
: this.withUpdatedComponents(newElement, newSelection);
21802265
}
21812266

2267+
trimUnsatisfiableBranches(currentType: CompositeType): FragmentSelection | SelectionSet | undefined {
2268+
const thisCondition = this.element.typeCondition;
2269+
// Note that if the condition has directives, we preserve the fragment no matter what.
2270+
if (this.element.appliedDirectives.length === 0) {
2271+
if (!thisCondition || currentType === this.element.typeCondition) {
2272+
const trimmed = this.selectionSet.trimUnsatisfiableBranches(currentType);
2273+
return trimmed.isEmpty() ? undefined : trimmed;
2274+
}
2275+
2276+
// If the current type is an object, then we never need to keep the current fragment because:
2277+
// - either the fragment is also an object, but we've eliminated the case where the 2 types are the same,
2278+
// so this is just an unsatisfiable branch.
2279+
// - or it's not an object, but then the current type is more precise and no poitn in "casting" to a
2280+
// less precise interface/union.
2281+
if (isObjectType(currentType)) {
2282+
if (isObjectType(thisCondition)) {
2283+
return undefined;
2284+
} else {
2285+
const trimmed = this.selectionSet.trimUnsatisfiableBranches(currentType);
2286+
return trimmed.isEmpty() ? undefined : trimmed;
2287+
}
2288+
}
2289+
}
2290+
2291+
// In all other cases, we first recurse on the sub-selection.
2292+
const trimmedSelectionSet = this.selectionSet.trimUnsatisfiableBranches(this.element.typeCondition ?? this.parentType);
2293+
2294+
// First, could be that everything was unsatisfiable.
2295+
if (trimmedSelectionSet.isEmpty()) {
2296+
if (this.element.appliedDirectives.length === 0) {
2297+
return undefined;
2298+
} else {
2299+
return this.withUpdatedSelectionSet(selectionSetOfElement(
2300+
new Field(
2301+
(this.element.typeCondition ?? this.parentType).typenameField()!,
2302+
undefined,
2303+
[new Directive('include', { 'if': false })],
2304+
)
2305+
));
2306+
}
2307+
}
2308+
2309+
// Second, we check if some of the sub-selection fragments can be "lifted" outside of this fragment. This can happen if:
2310+
// 1. the current fragment is an abstract type,
2311+
// 2. the sub-fragment is an object type,
2312+
// 3. the sub-fragment type is a valid runtime of the current type.
2313+
if (this.element.appliedDirectives.length === 0 && isAbstractType(thisCondition!)) {
2314+
assert(!isObjectType(currentType), () => `Should not have got here if ${currentType} is an object type`);
2315+
const currentRuntimes = possibleRuntimeTypes(currentType);
2316+
const liftableSelections: Selection[] = [];
2317+
for (const selection of trimmedSelectionSet.selections()) {
2318+
if (selection.kind === 'FragmentSelection'
2319+
&& selection.element.typeCondition
2320+
&& isObjectType(selection.element.typeCondition)
2321+
&& currentRuntimes.includes(selection.element.typeCondition)
2322+
) {
2323+
liftableSelections.push(selection);
2324+
}
2325+
}
2326+
2327+
// If we can lift all selections, then that just mean we can get rid of the current fragment altogether
2328+
if (liftableSelections.length === trimmedSelectionSet.selections().length) {
2329+
return trimmedSelectionSet;
2330+
}
2331+
2332+
// Otherwise, if there is "liftable" selections, we must return a set comprised of those lifted selection,
2333+
// and the current fragment _without_ those lifted selections.
2334+
if (liftableSelections.length > 0) {
2335+
const newSet = new SelectionSetUpdates();
2336+
newSet.add(liftableSelections);
2337+
newSet.add(this.withUpdatedSelectionSet(
2338+
trimmedSelectionSet.filter((s) => !liftableSelections.includes(s)),
2339+
));
2340+
return newSet.toSelectionSet(this.parentType);
2341+
}
2342+
}
2343+
2344+
return this.selectionSet === trimmedSelectionSet ? this : this.withUpdatedSelectionSet(trimmedSelectionSet);
2345+
}
2346+
2347+
21822348
expandAllFragments(): FragmentSelection {
21832349
return this.mapToSelectionSet((s) => s.expandAllFragments());
21842350
}
@@ -2227,6 +2393,10 @@ class FragmentSpreadSelection extends FragmentSelection {
22272393
assert(false, `Unsupported`);
22282394
}
22292395

2396+
trimUnsatisfiableBranches(_: CompositeType): FragmentSelection {
2397+
return this;
2398+
}
2399+
22302400
namedFragments(): NamedFragments | undefined {
22312401
return this.fragments;
22322402
}

0 commit comments

Comments
 (0)