Skip to content

Commit aeb83e1

Browse files
Null propagation
1 parent 8e1194a commit aeb83e1

12 files changed

+339
-42
lines changed

src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public OperationExecutionNode(
6060
_responseNames = GetResponseNamesFromPath(operation, source);
6161
}
6262

63-
// TODO: Should this be a span
63+
// TODO: Move
6464
private readonly ImmutableArray<string> _responseNames;
6565

6666
public override int Id { get; }
@@ -405,8 +405,8 @@ private static void AddErrors(
405405
buffer[i] = new SourceSchemaError(error, variables[i].Path);
406406
}
407407
}
408-
409-
context.AddErrors(buffer.AsSpan(0, bufferLength), responseNames);
408+
409+
context.AddErrors(buffer.AsSpan(0, bufferLength), responseNames.AsSpan());
410410
}
411411
finally
412412
{

src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ internal void AddPartialResults(SelectionPath sourcePath, ReadOnlySpan<SourceSch
105105
internal void AddPartialResults(ObjectResult result, ReadOnlySpan<Selection> selections)
106106
=> _resultStore.AddPartialResults(result, selections);
107107

108-
internal void AddErrors(ReadOnlySpan<SourceSchemaError> errors, ImmutableArray<string> responseNames)
108+
internal void AddErrors(ReadOnlySpan<SourceSchemaError> errors, ReadOnlySpan<string> responseNames)
109109
=> _resultStore.AddErrors(errors, responseNames);
110110

111111
internal PooledArrayWriter CreateRentedBuffer()

src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ public bool AddPartialResults(
119119
}
120120
}
121121

122-
public void AddErrors(ReadOnlySpan<SourceSchemaError> errors, ImmutableArray<string> responseNames)
122+
public bool AddErrors(ReadOnlySpan<SourceSchemaError> errors, ReadOnlySpan<string> responseNames)
123123
{
124124
_lock.EnterWriteLock();
125125

@@ -130,32 +130,24 @@ public void AddErrors(ReadOnlySpan<SourceSchemaError> errors, ImmutableArray<str
130130

131131
while (Unsafe.IsAddressLessThan(ref error, ref end))
132132
{
133-
// if (error.Path.IsRoot)
134-
// {
135-
// var selectionSet = _operation.RootSelectionSet;
136-
// if (!_valueCompletion.BuildResult(selectionSet, data, errorTrie, _root))
137-
// {
138-
// return false;
139-
// }
140-
// }
141-
// else
142-
// {
143-
// var startResult = GetStartObjectResult(error.Path);
144-
// if (!_valueCompletion.BuildResult(startResult.SelectionSet, data, errorTrie, startResult))
145-
// {
146-
// return false;
147-
// }
148-
// }
149-
150-
foreach (var responseName in responseNames)
133+
if (error.Path.IsRoot)
151134
{
152-
var errorWithPath = ErrorBuilder.FromError(error.Error)
153-
.SetPath(error.Path.Append(responseName))
154-
// TODO: Locations
155-
.Build();
156-
157-
// TODO: Null propagation
158-
_errors.Add(errorWithPath);
135+
if (!_valueCompletion.BuildResult(_root, responseNames, error))
136+
{
137+
// TODO: This is wrong
138+
_root = null!;
139+
return false;
140+
}
141+
}
142+
else
143+
{
144+
var startResult = GetStartObjectResult(error.Path);
145+
if (!_valueCompletion.BuildResult(startResult, responseNames, error))
146+
{
147+
// TODO: This is wrong
148+
_root = null!;
149+
return false;
150+
}
159151
}
160152

161153
error = ref Unsafe.Add(ref error, 1)!;
@@ -165,6 +157,8 @@ public void AddErrors(ReadOnlySpan<SourceSchemaError> errors, ImmutableArray<str
165157
{
166158
_lock.ExitWriteLock();
167159
}
160+
161+
return true;
168162
}
169163

170164
public void AddPartialResults(ObjectResult result, ReadOnlySpan<Selection> selections)

src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ObjectListResult.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Text.Json;
22
using HotChocolate.Execution;
3+
using HotChocolate.Types;
34

45
namespace HotChocolate.Fusion.Execution;
56

@@ -30,6 +31,13 @@ public override void SetNextValueNull()
3031
Items.Add(null);
3132
}
3233

34+
public override bool TrySetValueNull(int index)
35+
{
36+
Items[index] = null;
37+
38+
return !ElementType.IsNonNullType();
39+
}
40+
3341
/// <summary>
3442
/// Adds the given value to the list.
3543
/// </summary>

src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ObjectResult.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ internal void MoveFieldTo(string fieldName, ObjectResult target)
7272
target._fieldMap[fieldName] = field;
7373
}
7474

75+
/// <inheritdoc />
76+
public override bool TrySetValueNull(int index)
77+
{
78+
var field = _fields[index];
79+
field.SetNextValueNull();
80+
81+
return !field.Selection.Type.IsNonNullType();
82+
}
83+
7584
/// <summary>
7685
/// Writes the object result to the specified JSON writer.
7786
/// </summary>

src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/ValueCompletion.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,58 @@ public bool BuildResult(
114114
return true;
115115
}
116116

117+
public bool BuildResult(
118+
ObjectResult objectResult,
119+
ReadOnlySpan<string> responseNames,
120+
SourceSchemaError sourceSchemaError)
121+
{
122+
foreach (var responseName in responseNames)
123+
{
124+
var fieldResult = objectResult[responseName];
125+
126+
if (!fieldResult.Selection.IsIncluded(_includeFlags))
127+
{
128+
continue;
129+
}
130+
131+
var error = ErrorBuilder.FromError(sourceSchemaError.Error)
132+
.SetPath(sourceSchemaError.Path.Append(responseName))
133+
.AddLocation(fieldResult.Selection.SyntaxNodes[0].Node)
134+
.Build();
135+
136+
_errors.Add(error);
137+
138+
if (_errorHandling is ErrorHandling.Propagate)
139+
{
140+
// TODO: We need an invalidated state for the resultdata
141+
if (!PropagateNullValues(fieldResult))
142+
{
143+
return false;
144+
}
145+
}
146+
}
147+
148+
return true;
149+
}
150+
151+
private static bool PropagateNullValues(ResultData result)
152+
{
153+
while (result.Parent is not null)
154+
{
155+
var index = result.ParentIndex;
156+
var parent = result.Parent;
157+
158+
if (parent.TrySetValueNull(index))
159+
{
160+
return true;
161+
}
162+
163+
result = parent;
164+
}
165+
166+
return false;
167+
}
168+
117169
private bool TryCompleteValue(
118170
Selection selection,
119171
IType type,

src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/SubgraphErrorTests.cs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,39 @@ public async Task Subgraph_Request_Fails_For_Root_Field()
171171
// assert
172172
using var client = GraphQLHttpClient.Create(gateway.CreateClient());
173173

174+
using var result = await client.PostAsync(
175+
"""
176+
{
177+
nullableTopProduct {
178+
price
179+
}
180+
}
181+
""",
182+
new Uri("http://localhost:5000/graphql"));
183+
184+
// act
185+
using var response = await result.ReadAsResultAsync();
186+
response.MatchSnapshot();
187+
}
188+
189+
[Fact]
190+
public async Task Subgraph_Request_Fails_For_Root_Field_NonNull()
191+
{
192+
// arrange
193+
using var server1 = CreateSourceSchema(
194+
"A",
195+
b => b.AddQueryType<SourceSchema1.Query>(),
196+
isOffline: true);
197+
198+
// act
199+
using var gateway = await CreateCompositeSchemaAsync(
200+
[
201+
("A", server1),
202+
]);
203+
204+
// assert
205+
using var client = GraphQLHttpClient.Create(gateway.CreateClient());
206+
174207
using var result = await client.PostAsync(
175208
"""
176209
{
@@ -397,6 +430,45 @@ public async Task No_Data_And_Error_Without_Path_For_Lookup()
397430

398431
[Fact]
399432
public async Task Subgraph_Request_Fails_For_Lookup()
433+
{
434+
// arrange
435+
using var server1 = CreateSourceSchema(
436+
"A",
437+
b => b.AddQueryType<SourceSchema1.Query>());
438+
439+
using var server2 = CreateSourceSchema(
440+
"B",
441+
b => b.AddQueryType<SourceSchema2.Query>(),
442+
isOffline: true);
443+
444+
// act
445+
using var gateway = await CreateCompositeSchemaAsync(
446+
[
447+
("A", server1),
448+
("B", server2),
449+
]);
450+
451+
// assert
452+
using var client = GraphQLHttpClient.Create(gateway.CreateClient());
453+
454+
using var result = await client.PostAsync(
455+
"""
456+
{
457+
nullableTopProduct {
458+
price
459+
name
460+
}
461+
}
462+
""",
463+
new Uri("http://localhost:5000/graphql"));
464+
465+
// act
466+
using var response = await result.ReadAsResultAsync();
467+
response.MatchSnapshot();
468+
}
469+
470+
[Fact]
471+
public async Task Subgraph_Request_Fails_For_Lookup_NonNull()
400472
{
401473
// arrange
402474
using var server1 = CreateSourceSchema(
@@ -436,6 +508,45 @@ public async Task Subgraph_Request_Fails_For_Lookup()
436508

437509
[Fact]
438510
public async Task Subgraph_Request_Fails_For_Lookup_On_List()
511+
{
512+
// arrange
513+
using var server1 = CreateSourceSchema(
514+
"A",
515+
b => b.AddQueryType<SourceSchema1.Query>());
516+
517+
using var server2 = CreateSourceSchema(
518+
"B",
519+
b => b.AddQueryType<SourceSchema2.Query>(),
520+
isOffline: true);
521+
522+
// act
523+
using var gateway = await CreateCompositeSchemaAsync(
524+
[
525+
("A", server1),
526+
("B", server2),
527+
]);
528+
529+
// assert
530+
using var client = GraphQLHttpClient.Create(gateway.CreateClient());
531+
532+
using var result = await client.PostAsync(
533+
"""
534+
{
535+
topProducts {
536+
price
537+
name
538+
}
539+
}
540+
""",
541+
new Uri("http://localhost:5000/graphql"));
542+
543+
// act
544+
using var response = await result.ReadAsResultAsync();
545+
response.MatchSnapshot();
546+
}
547+
548+
[Fact]
549+
public async Task Subgraph_Request_Fails_For_Lookup_On_List_NonNull()
439550
{
440551
// arrange
441552
using var server1 = CreateSourceSchema(
@@ -481,6 +592,8 @@ public class Query
481592
{
482593
public Product GetTopProduct() => new(1, 13.99);
483594

595+
public Product? GetNullableTopProduct() => new (1, 13.99);
596+
484597
public List<Product> GetTopProducts()
485598
=> [new(1, 13.99), new(2, 13.99), new(3, 13.99)];
486599

src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SubgraphErrorTests.Subgraph_Request_Fails_For_Lookup.snap

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"data": {
3-
"topProduct": {
3+
"nullableTopProduct": {
44
"price": 13.99,
55
"name": null
66
}
@@ -9,7 +9,7 @@
99
{
1010
"message": "Unexpected Execution Error",
1111
"path": [
12-
"topProduct",
12+
"nullableTopProduct",
1313
"name"
1414
]
1515
}
@@ -18,23 +18,23 @@
1818
"fusion": {
1919
"operationPlan": {
2020
"operation": {
21-
"document": "{\n topProduct {\n price\n name\n id @fusion__requirement\n }\n}",
22-
"hash": "e24d93b6245cedd878fff4452f5f16b9"
21+
"document": "{\n nullableTopProduct {\n price\n name\n id @fusion__requirement\n }\n}",
22+
"hash": "12015865a78f4ab2330778c5486a4026"
2323
},
2424
"nodes": [
2525
{
2626
"id": 1,
2727
"type": "Operation",
2828
"schema": "A",
29-
"operation": "query Op_e24d93b6_1 {\n topProduct {\n price\n id\n }\n}"
29+
"operation": "query Op_12015865_1 {\n nullableTopProduct {\n price\n id\n }\n}"
3030
},
3131
{
3232
"id": 2,
3333
"type": "Operation",
3434
"schema": "B",
35-
"operation": "query Op_e24d93b6_2(\n $__fusion_1_id: Int!\n) {\n productById(id: $__fusion_1_id) {\n name\n }\n}",
35+
"operation": "query Op_12015865_2(\n $__fusion_1_id: Int!\n) {\n productById(id: $__fusion_1_id) {\n name\n }\n}",
3636
"source": "$.productById",
37-
"target": "$.topProduct",
37+
"target": "$.nullableTopProduct",
3838
"requirements": [
3939
{
4040
"name": "__fusion_1_id",

0 commit comments

Comments
 (0)