Skip to content

Commit 29a2576

Browse files
Progressive override (query planning / validation) (#2902)
Implement progressive override functionality in QP and validation logic Co-authored-by: Sachin D. Shinde <[email protected]>
1 parent 6f7266b commit 29a2576

File tree

13 files changed

+1089
-111
lines changed

13 files changed

+1089
-111
lines changed

.changeset/empty-dodos-turn.md

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@apollo/query-planner": minor
3+
"@apollo/query-graphs": minor
4+
"@apollo/composition": minor
5+
"@apollo/federation-internals": minor
6+
"@apollo/subgraph": minor
7+
"@apollo/gateway": minor
8+
---
9+
10+
Implement progressive `@override` functionality
11+
12+
The progressive `@override` feature brings a new argument to the `@override` directive: `label: String`. When a label is added to an `@override` application, the override becomes conditional, depending on parameters provided to the query planner (a set of which labels should be overridden). Note that this feature will be supported in router for enterprise users only.
13+
14+
Out-of-the-box, the router will support a percentage-based use case for progressive `@override`. For example:
15+
```graphql
16+
type Query {
17+
hello: String @override(from: "original", label: "percent(5)")
18+
}
19+
```
20+
The above example will override the root `hello` field from the "original" subgraph 5% of the time.
21+
22+
More complex use cases will be supported by the router via the use of coprocessors/rhai to resolve arbitrary labels to true/false values (i.e. via a feature flag service).

.cspell/cspell-dict.txt

+1
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ Reviwed
190190
Rhai
191191
righ
192192
rulesof
193+
runtimes
193194
samee
194195
Sant
195196
SATISFIABILITY

composition-js/src/__tests__/override.compose.test.ts

+163-1
Original file line numberDiff line numberDiff line change
@@ -954,7 +954,7 @@ describe("composition involving @override directive", () => {
954954
@join__type(graph: SUBGRAPH2, key: \\"k\\")
955955
{
956956
k: ID
957-
a: Int @join__field(graph: SUBGRAPH1, override: \\"Subgraph2\\", overrideLabel: \\"foo\\")
957+
a: Int @join__field(graph: SUBGRAPH1, override: \\"Subgraph2\\", overrideLabel: \\"foo\\") @join__field(graph: SUBGRAPH2, overrideLabel: \\"foo\\")
958958
b: Int @join__field(graph: SUBGRAPH2)
959959
}"
960960
`);
@@ -971,6 +971,70 @@ describe("composition involving @override directive", () => {
971971
b: Int
972972
}
973973
`);
974+
975+
expect(result.supergraphSdl).toMatchInlineSnapshot(`
976+
"schema
977+
@link(url: \\"https://specs.apollo.dev/link/v1.0\\")
978+
@link(url: \\"https://specs.apollo.dev/join/v0.4\\", for: EXECUTION)
979+
{
980+
query: Query
981+
}
982+
983+
directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION
984+
985+
directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
986+
987+
directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
988+
989+
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
990+
991+
directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
992+
993+
directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
994+
995+
directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
996+
997+
directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
998+
999+
scalar join__DirectiveArguments
1000+
1001+
scalar join__FieldSet
1002+
1003+
enum join__Graph {
1004+
SUBGRAPH1 @join__graph(name: \\"Subgraph1\\", url: \\"https://Subgraph1\\")
1005+
SUBGRAPH2 @join__graph(name: \\"Subgraph2\\", url: \\"https://Subgraph2\\")
1006+
}
1007+
1008+
scalar link__Import
1009+
1010+
enum link__Purpose {
1011+
\\"\\"\\"
1012+
\`SECURITY\` features provide metadata necessary to securely resolve fields.
1013+
\\"\\"\\"
1014+
SECURITY
1015+
1016+
\\"\\"\\"
1017+
\`EXECUTION\` features provide metadata necessary for operation execution.
1018+
\\"\\"\\"
1019+
EXECUTION
1020+
}
1021+
1022+
type Query
1023+
@join__type(graph: SUBGRAPH1)
1024+
@join__type(graph: SUBGRAPH2)
1025+
{
1026+
t: T @join__field(graph: SUBGRAPH1)
1027+
}
1028+
1029+
type T
1030+
@join__type(graph: SUBGRAPH1, key: \\"k\\")
1031+
@join__type(graph: SUBGRAPH2, key: \\"k\\")
1032+
{
1033+
k: ID
1034+
a: Int @join__field(graph: SUBGRAPH1, override: \\"Subgraph2\\", overrideLabel: \\"foo\\") @join__field(graph: SUBGRAPH2, overrideLabel: \\"foo\\")
1035+
b: Int @join__field(graph: SUBGRAPH2)
1036+
}"
1037+
`);
9741038
});
9751039

9761040
describe("label validation", () => {
@@ -1088,5 +1152,103 @@ describe("composition involving @override directive", () => {
10881152
}
10891153
);
10901154
});
1155+
1156+
describe("composition validation", () => {
1157+
it("forced jump from S1 -> S2 -> S1 due to @override usages", () => {
1158+
const subgraph1 = {
1159+
name: "Subgraph1",
1160+
url: "https://Subgraph1",
1161+
typeDefs: gql`
1162+
type Query {
1163+
t: T
1164+
}
1165+
1166+
type T @key(fields: "id") {
1167+
id: ID
1168+
a: A @override(from: "Subgraph2", label: "foo")
1169+
}
1170+
1171+
type A @key(fields: "id") {
1172+
id: ID
1173+
b: Int
1174+
}
1175+
`,
1176+
};
1177+
1178+
const subgraph2 = {
1179+
name: "Subgraph2",
1180+
url: "https://Subgraph2",
1181+
typeDefs: gql`
1182+
type T @key(fields: "id") {
1183+
id: ID
1184+
a: A
1185+
}
1186+
1187+
type A @key(fields: "id") {
1188+
id: ID
1189+
b: Int @override(from: "Subgraph1", label: "foo")
1190+
}
1191+
`,
1192+
};
1193+
1194+
const result = composeAsFed2Subgraphs([subgraph1, subgraph2]);
1195+
assertCompositionSuccess(result);
1196+
});
1197+
1198+
it("errors on overridden fields in @requires FieldSet", () => {
1199+
const subgraph1 = {
1200+
name: "Subgraph1",
1201+
url: "https://Subgraph1",
1202+
typeDefs: gql`
1203+
type Query {
1204+
t: T
1205+
}
1206+
1207+
type T @key(fields: "id") {
1208+
id: ID
1209+
a: A @override(from: "Subgraph2", label: "foo")
1210+
}
1211+
1212+
type A @key(fields: "id") {
1213+
id: ID
1214+
b: Int
1215+
c: Int
1216+
}
1217+
`,
1218+
};
1219+
1220+
const subgraph2 = {
1221+
name: "Subgraph2",
1222+
url: "https://Subgraph2",
1223+
typeDefs: gql`
1224+
type T @key(fields: "id") {
1225+
id: ID
1226+
a: A
1227+
b: Int @requires(fields: "a { c }")
1228+
}
1229+
1230+
type A @key(fields: "id") {
1231+
id: ID
1232+
b: Int @override(from: "Subgraph1", label: "foo")
1233+
c: Int @external
1234+
}
1235+
`,
1236+
};
1237+
1238+
const result = composeAsFed2Subgraphs([subgraph1, subgraph2]);
1239+
expect(result.errors).toBeDefined();
1240+
expect(result.errors![0]).toMatchInlineSnapshot(`
1241+
[GraphQLError: The following supergraph API query:
1242+
{
1243+
t {
1244+
b
1245+
}
1246+
}
1247+
cannot be satisfied by the subgraphs because:
1248+
- from subgraph "Subgraph1": cannot find field "T.b".
1249+
- from subgraph "Subgraph2": cannot satisfy @require conditions on field "T.b".]
1250+
`);
1251+
});
1252+
});
10911253
});
10921254
});

composition-js/src/merging/merge.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1301,12 +1301,14 @@ class Merger {
13011301
const percentRegex = /^percent\((\d{1,2}(\.\d{1,8})?|100)\)$/;
13021302
if (labelRegex.test(overrideLabel)) {
13031303
result.setOverrideLabel(idx, overrideLabel);
1304+
result.setOverrideLabel(fromIdx, overrideLabel);
13041305
} else if (percentRegex.test(overrideLabel)) {
13051306
const parts = percentRegex.exec(overrideLabel);
13061307
if (parts) {
13071308
const percent = parseFloat(parts[1]);
13081309
if (percent >= 0 && percent <= 100) {
13091310
result.setOverrideLabel(idx, overrideLabel);
1311+
result.setOverrideLabel(fromIdx, overrideLabel);
13101312
}
13111313
}
13121314
}
@@ -1589,7 +1591,7 @@ class Merger {
15891591
if (!allTypesEqual) {
15901592
return true;
15911593
}
1592-
if (mergeContext.some(({ usedOverridden }) => usedOverridden)) {
1594+
if (mergeContext.some(({ usedOverridden, overrideLabel }) => usedOverridden || !!overrideLabel)) {
15931595
return true;
15941596
}
15951597

@@ -1642,7 +1644,8 @@ class Merger {
16421644
for (const [idx, source] of sources.entries()) {
16431645
const usedOverridden = mergeContext.isUsedOverridden(idx);
16441646
const unusedOverridden = mergeContext.isUnusedOverridden(idx);
1645-
if (!source || unusedOverridden) {
1647+
const overrideLabel = mergeContext.overrideLabel(idx);
1648+
if (!source || (unusedOverridden && !overrideLabel)) {
16461649
continue;
16471650
}
16481651

0 commit comments

Comments
 (0)