Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1090,13 +1090,28 @@ private void PlanInlineFieldWithRequirements(
new VariableNode(new NameNode($"{requirementKey}_{argument.Name}"))));
}

// we partition the requiring field's children against the consumer schema and
// only inline the resolvable subset. Unresolvable descendants and nested
// fields-with-requirements are pushed onto the backlog so that they are planned
// as re-entrant lookups instead of being inlined into a step that cannot resolve
// them.
var childSelections =
ExtractResolvableChildSelections(
stepConsumer.StepId,
workItem.EstimatedDepth,
workItem.Selection,
Comment on lines +1098 to +1102
current,
index,
ref backlog);

var operation =
InlineSelections(
currentStep.Definition,
index,
currentStep.Type,
workItem.Selection.SelectionSetId,
new SelectionSetNode([workItem.Selection.Node.WithArguments(arguments)]));
new SelectionSetNode(
[workItem.Selection.Node.WithArguments(arguments).WithSelectionSet(childSelections)]));

var requirements = currentStep.Requirements;

Expand Down Expand Up @@ -1699,11 +1714,18 @@ private static List<ArgumentNode> GetLookupArguments(Lookup lookup, string requi

var selectionSetId = index.GetId(selection.Node.SelectionSet);
var selectionSetType = selection.Field.Type.AsTypeDefinition();

// the field's path points at the selection set the field lives in. Its child
// selections live one level deeper, so we append the field's response name to
// root the partitioned children (and any re-entrant lookups derived from them)
// at the correct path.
var childPath = selection.Path.AppendField(
selection.Node.Alias?.Value ?? selection.Node.Name.Value);
var selectionSet = new SelectionSet(
selectionSetId,
selection.Node.SelectionSet,
selectionSetType,
selection.Path);
childPath);

var input = new SelectionSetPartitionerInput
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
using HotChocolate.Fusion.Types;

namespace HotChocolate.Fusion.Planning;

public class RequirementReentrancyTests : FusionTestBase
{
[Fact]
public void Plan_Should_Reenter_Catalog_When_InnerProductCategory_Crosses_RequireBoundary()
{
// arrange
var schema = CreateRecommendationSchema();

// act
// products (catalog) -> recommendations (recommendation service, @require "category")
// -> product.category (must re-enter the catalog service)
var plan = PlanOperation(
schema,
"""
query getProducts {
products {
category
recommendations {
product {
category
}
}
}
}
""");

// assert
// the recommendation service does not own Product.category, so its operation must
// select only the inner product key (id) and a separate re-entrant catalog lookup
// resolves product.category (target $.products.recommendations.product).
MatchInline(
plan,
"""
operation:
- document: |
query getProducts {
products {
category
category @fusion__requirement
recommendations {
product {
category
id @fusion__requirement
}
}
id @fusion__requirement
}
}
name: getProducts
hash: 123456789101112
searchSpace: 2
expandedNodes: 8
nodes:
- id: 1
type: Operation
schema: CATALOG
operation: |
query getProducts_123456789101112_1 {
products {
category
id
}
}
- id: 3
type: Operation
schema: RECOMMENDATION
operation: |
query getProducts_123456789101112_3(
$__fusion_2_category: ProductCategory!
$__fusion_3_id: ID!
) {
productById(id: $__fusion_3_id) {
recommendations(category: $__fusion_2_category) {
product {
id
}
}
}
}
source: $.productById
target: $.products
requirements:
- name: __fusion_2_category
selectionMap: >-
category
- name: __fusion_3_id
selectionMap: >-
id
dependencies:
- id: 1
- id: 4
type: Operation
schema: CATALOG
operation: |
query getProducts_123456789101112_4($__fusion_4_id: ID!) {
productById(id: $__fusion_4_id) {
category
}
}
source: $.productById
target: $.products.recommendations.product
requirements:
- name: __fusion_4_id
selectionMap: >-
id
dependencies:
- id: 3
""");
}

[Fact]
public void Plan_Should_StayInRecommendation_When_InnerProduct_Selects_OnlyId()
{
// arrange
var schema = CreateRecommendationSchema();

// act
// control: inner selection is `id`, already owned by the recommendation service,
// so no re-entrant lookup back into the catalog is required.
var plan = PlanOperation(
schema,
"""
query getProducts {
products {
category
recommendations {
product {
id
}
}
}
}
""");

// assert
MatchSnapshot(plan);
}

[Fact]
public void Plan_Should_Reenter_Catalog_When_EnteringFromRecommendation_Standalone()
{
// arrange
var schema = CreateRecommendationSchema();

// act
// control: enter from the recommendation service directly (no outer @require
// boundary), product.category still needs a re-entrant lookup into the catalog.
var plan = PlanOperation(
schema,
"""
query getProducts {
recommendations {
product {
category
}
}
}
""");

// assert
MatchSnapshot(plan);
}

private static FusionSchemaDefinition CreateRecommendationSchema()
{
return ComposeSchema(
"""
# name: CATALOG
schema {
query: Query
}

type Query {
products: [Product!]
productById(id: ID! @is(field: "id")): Product @lookup
}

type Product @key(fields: "id") {
id: ID!
category: ProductCategory!
}

enum ProductCategory {
ELECTRONICS
BOOKS
}
""",
"""
# name: RECOMMENDATION
schema {
query: Query
}

type Query {
recommendations: [Recommendation!]
productById(id: ID! @is(field: "id")): Product @lookup @internal
}

type Product @key(fields: "id") {
id: ID!
recommendations(
category: ProductCategory! @require(field: "category")): [Recommendation!]
}

type Recommendation @key(fields: "id") {
id: ID!
product: Product
}

enum ProductCategory {
ELECTRONICS
BOOKS
}
""");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
operation:
- document: |
query getProducts {
recommendations {
product {
category
id @fusion__requirement
}
}
}
name: getProducts
hash: 123456789101112
searchSpace: 1
expandedNodes: 2
nodes:
- id: 1
type: Operation
schema: RECOMMENDATION
operation: |
query getProducts_123456789101112_1 {
recommendations {
product {
id
}
}
}
- id: 2
type: Operation
schema: CATALOG
operation: |
query getProducts_123456789101112_2($__fusion_1_id: ID!) {
productById(id: $__fusion_1_id) {
category
}
}
source: $.productById
target: $.recommendations.product
requirements:
- name: __fusion_1_id
selectionMap: >-
id
dependencies:
- id: 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
operation:
- document: |
query getProducts {
products {
category
category @fusion__requirement
recommendations {
product {
id
}
}
id @fusion__requirement
}
}
name: getProducts
hash: 123456789101112
searchSpace: 1
expandedNodes: 2
nodes:
- id: 1
type: Operation
schema: CATALOG
operation: |
query getProducts_123456789101112_1 {
products {
category
id
}
}
- id: 2
type: Operation
schema: RECOMMENDATION
operation: |
query getProducts_123456789101112_2(
$__fusion_1_id: ID!
$__fusion_2_category: ProductCategory!
) {
productById(id: $__fusion_1_id) {
recommendations(category: $__fusion_2_category) {
product {
id
}
}
}
}
source: $.productById
target: $.products
requirements:
- name: __fusion_1_id
selectionMap: >-
id
- name: __fusion_2_category
selectionMap: >-
category
dependencies:
- id: 1
Loading