Skip to content

Conversation

@yokazawa
Copy link

@yokazawa yokazawa commented Nov 24, 2025

Summary by CodeRabbit

  • Tests

    • Added federation integration test for merging concrete types in root fields with interface fragments.
  • New Features

    • Extended GraphQL schema with new Base interface and related types (Category, Owner) for improved data organization.
    • Added ProductA and ProductB implementations with category relationships.
    • Introduced new ProductA query to retrieve product information with nested category and owner details.

✏️ Tip: You can customize this high-level summary in your review settings.

Checklist

  • I have discussed my proposed changes in an issue and have received approval to proceed.
  • I have followed the coding standards of the project.
  • Tests or benchmarks have been added or updated.

This adds a federation integration test that reproduces a missing field in the response when:
querying a concrete type in the root field, and
using an interface fragment with concrete type fragments inside.

The new test merge_concrete_type_in_root_field_and_interface_fragment expects displayOwner to be present under productA.category, but currently the field is missing in the response.

This is related to cosmo issue wundergraph/cosmo#2346.

Additional findings

I further investigated this on the router side and confirmed:

  • The federated subgraph request generated by the router does include the displayOwner field.
  • The federated subgraph response sent back to the router also includes the displayOwner field with the expected value.
  • However, the final response from the router to the client does not contain displayOwner.

This strongly suggests that:

  • Planning and subgraph operation generation are correct (the field is requested),
  • Subgraphs behave correctly (the field is returned),
  • The bug is in the router's response assembly layer (v2 resolve execution), where the subgraph JSON is mapped into the final GraphQL response.

Response plan structure (resolve tree)

I instrumented ExecutionEngine.Execute to dump p.Response.Data (the resolve.Object tree) right before calling ResolveGraphQLResponse.

For the failing test (merge_concrete_type_in_root_field_and_interface_fragment), the tree for productA.category looks like this (base64-decoded Name fields shown for clarity):

{
  "Data": {
    "productA": {
      "category": {
        "id": { ... },
        "displayOwner": {
          "name": { ... }
        }
      }
    }
  }
}

More precisely, the Field structure under category is:

  • Name = "id", Value.Path = ["id"]
  • Name = "displayOwner", Value.Path = ["displayOwner"]
    • child field: Name = "name", Value.Path = ["name"]

So at the plan/postprocess stage, the resolve tree correctly contains a displayOwner field with a nested name field.

Type condition metadata on displayOwner

Looking at the raw JSON dump of p.Response.Data, the relevant part of the Field metadata is:
For category.displayOwner:

{
  "Name": "displayOwner",
  "Value": {
    "Path": ["displayOwner"],
    "Fields": [
      {
        "Name": "name",
        "Value": { "Path": ["name"], "Nullable": false },
        "ParentOnTypeNames": [
          {
            "Depth": 2,
            "Names": ["ProductB", "ProductA"]
          }
        ],
        "Info": {
          "Name": "name",
          "ExactParentTypeName": "Owner",
          "ParentTypeNames": ["Owner"],
          "NamedType": "String"
        }
      }
    ]
  },
  "ParentOnTypeNames": [
    {
      "Depth": 1,
      "Names": ["ProductB"]
    }
  ],
  "Info": {
    "Name": "owner",
    "ExactParentTypeName": "Category",
    "ParentTypeNames": ["Category"],
    "NamedType": "Owner"
  }
}
  • Note the missmatch
    • The inner name field has ParentOnTypeNames.Names = ["ProductB", "ProductA"] at Depth = 2, which matches the fragment:
      • ... on ProductB { category { displayOwner: owner { name } } }
      • ... on ProductA { category { displayOwner: owner { name } } }
    • But the parent displayOwner field has ParentOnTypeNames.Names = ["ProductB"] at Depth = 1.
  • In other words, the resolve tree currently treats:
    • displayOwner as if it were only valid on ProductB,
    • while its child name is marked as valid on both ProductA and ProductB.

Since the concrete runtime type for productA is ProductA, this likely causes the router to skip resolving displayOwner for ProductA altogether due to the type condition metadata, even though the subgraph response contains the expected owner value.

Suspected area

Given the above:

  • The field is present in the plan/postprocess resolve tree (displayOwner is there).
  • The subgraph request/response both contain displayOwner as expected.
  • The final router response is missing displayOwner.

It seems likely that the bug is in the type-condition handling for abstract types in the v2 engine, rather than in planning or subgraph I/O:

Either when building OnTypeNames / ParentOnTypeNames for fields under fragments like:

  • ... on Base { ... on ProductA { ... } ... on ProductB { ... } }

Or when evaluating those type conditions at resolve time to decide whether a field should be resolved for a particular concrete type.

In this specific case, it looks like the type-condition metadata for displayOwner was narrowed down to ProductB only, even though the query selects it for both ProductA and ProductB. This causes displayOwner to be dropped for ProductA at execution time.

@yokazawa yokazawa requested a review from a team as a code owner November 24, 2025 02:04
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 24, 2025

Walkthrough

This change adds test infrastructure for federation scenarios involving concrete type merging within root fields and interface fragments. New schema types (Base interface, Category, Owner, ProductA, ProductB), a corresponding resolver, generated models, a GraphQL query, and an integration test case are introduced to exercise this federation behavior.

Changes

Cohort / File(s) Summary
Test Infrastructure
execution/engine/federation_integration_test.go, execution/federationtesting/testdata/queries/merge_concrete_type_in_root_field_and_interface_fragment.graphql
Adds new subtest "merge concrete type in root field and interface fragment" with assertion against expected JSON response structure; introduces corresponding GraphQL query with inline fragments on Base interface type.
Schema Definitions
execution/federationtesting/accounts/graph/schema.graphqls
Adds productA field to Query; introduces Base interface with category field; adds Category and Owner types; implements ProductA and ProductB types with Base interface.
Generated Models
execution/federationtesting/accounts/graph/model/models_gen.go
Generates Base interface with IsBase() and GetCategory() methods; creates Category, Owner, ProductA, ProductB structs with appropriate fields and interface implementations.
Resolver
execution/federationtesting/accounts/graph/schema.resolvers.go
Implements ProductA resolver returning a ProductA instance with nested Category and Owner objects.

Sequence Diagram

sequenceDiagram
    participant Test as Integration Test
    participant Gateway
    participant QueryResolver
    participant Categories as Category/Owner Data

    Test->>Gateway: Execute GraphQL query<br/>productA { category { id owner { name } } }
    Gateway->>QueryResolver: Resolve productA (root field)
    QueryResolver->>Categories: Return ProductA with Category
    Gateway->>Categories: Resolve Base interface fragment<br/>on concrete ProductA type
    Gateway->>Categories: Extract category.id and owner.name<br/>from concrete type
    Gateway-->>Test: Return merged concrete type<br/>within interface context
    Note over Gateway: Merges concrete type selection<br/>with interface fragment selection
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Verify that the test case correctly exercises the federated query execution path with interface fragment merging
  • Confirm resolver implementation correctly constructs the nested object hierarchy
  • Check that schema type relationships (Base → ProductA/ProductB → Category → Owner) align with query expectations and federation semantics

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding a regression test for a specific bug involving merging concrete types in root fields with interface fragments.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
execution/engine/federation_integration_test.go (1)

573-583: New regression subtest is consistent and correctly wired

The subtest setup, query path, and expected JSON all align with the new schema, resolver, and query (ProductA → Category c111 → Owner name "owner" under alias displayOwner). It matches the structure of existing federation integration tests and should reliably capture the regression once the engine is fixed.

If you want to make the intent clearer for future readers, consider adding a short comment above this subtest referencing cosmo issue 2346 and noting that it currently fails as a regression guard.

execution/federationtesting/accounts/graph/schema.graphqls (1)

14-15: Schema additions line up with resolvers, models, and query usage

The new productA field, Base interface, and Category/Owner/ProductA/ProductB types are coherent:

  • ProductA implements Base with category: Category supports the ... on Base and nested concrete fragments used in the new query.
  • Nullability and field names match the Go models and the ProductA resolver.
  • Including ProductB as another Base implementation is a good way to mirror the multi‑concrete‑type scenario behind the regression.

No schema-level issues from a federation/testing perspective.

If you expect Category.owner to always be present in this test context, you could tighten it to Owner! in a follow‑up, but it’s not required for this regression.

Also applies to: 185-205

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d95839a and 445ae41.

⛔ Files ignored due to path filters (1)
  • execution/federationtesting/accounts/graph/generated/generated.go is excluded by !**/generated/**
📒 Files selected for processing (5)
  • execution/engine/federation_integration_test.go (1 hunks)
  • execution/federationtesting/accounts/graph/model/models_gen.go (3 hunks)
  • execution/federationtesting/accounts/graph/schema.graphqls (2 hunks)
  • execution/federationtesting/accounts/graph/schema.resolvers.go (1 hunks)
  • execution/federationtesting/testdata/queries/merge_concrete_type_in_root_field_and_interface_fragment.graphql (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: Noroth
Repo: wundergraph/graphql-go-tools PR: 1341
File: v2/pkg/engine/datasource/grpc_datasource/execution_plan.go:1039-1097
Timestamp: 2025-11-19T10:53:06.342Z
Learning: In v2/pkg/engine/datasource/grpc_datasource field resolver response handling, the `resolveRequiredFields` function intentionally uses two distinct approaches: for simple GraphQL object types it populates `message.Fields`, while for composite types (interface/union) it exclusively uses `message.FieldSelectionSet` with fragment-based selections. This differs from `buildFieldMessage` (regular queries) because field resolver responses returning composite types must align with protobuf oneOf structure, where all selections—including common interface fields—are handled through fragment selections built by `buildCompositeField`. The two approaches cannot be mixed in field resolver responses.
📚 Learning: 2025-11-19T09:42:17.644Z
Learnt from: Noroth
Repo: wundergraph/graphql-go-tools PR: 1341
File: v2/pkg/engine/datasource/grpc_datasource/execution_plan_visitor_federation.go:406-429
Timestamp: 2025-11-19T09:42:17.644Z
Learning: In the wundergraph/graphql-go-tools gRPC datasource implementation (v2/pkg/engine/datasource/grpc_datasource), field resolvers must have arguments. The system does not currently support defining field resolvers without arguments. This invariant ensures that the `parentCallID` increment in `enterFieldResolver` is always matched by a decrement in `LeaveField` (which checks `r.operation.FieldHasArguments(ref)`).

Applied to files:

  • execution/federationtesting/accounts/graph/schema.resolvers.go
📚 Learning: 2025-07-02T15:28:02.122Z
Learnt from: SkArchon
Repo: wundergraph/graphql-go-tools PR: 1203
File: v2/pkg/engine/resolve/loader.go:63-67
Timestamp: 2025-07-02T15:28:02.122Z
Learning: In the graphql-go-tools codebase, result structs are consistently initialized with non-nil bytes.Buffer instances, making additional nil checks for res.out unnecessary defensive programming.

Applied to files:

  • execution/engine/federation_integration_test.go
📚 Learning: 2025-11-19T10:53:06.342Z
Learnt from: Noroth
Repo: wundergraph/graphql-go-tools PR: 1341
File: v2/pkg/engine/datasource/grpc_datasource/execution_plan.go:1039-1097
Timestamp: 2025-11-19T10:53:06.342Z
Learning: In v2/pkg/engine/datasource/grpc_datasource field resolver response handling, the `resolveRequiredFields` function intentionally uses two distinct approaches: for simple GraphQL object types it populates `message.Fields`, while for composite types (interface/union) it exclusively uses `message.FieldSelectionSet` with fragment-based selections. This differs from `buildFieldMessage` (regular queries) because field resolver responses returning composite types must align with protobuf oneOf structure, where all selections—including common interface fields—are handled through fragment selections built by `buildCompositeField`. The two approaches cannot be mixed in field resolver responses.

Applied to files:

  • execution/engine/federation_integration_test.go
🧬 Code graph analysis (1)
execution/federationtesting/accounts/graph/schema.resolvers.go (1)
execution/federationtesting/accounts/graph/model/models_gen.go (5)
  • ProductA (166-169)
  • ProductA (171-171)
  • Category (128-131)
  • Owner (156-158)
  • Name (49-52)
🔇 Additional comments (3)
execution/federationtesting/testdata/queries/merge_concrete_type_in_root_field_and_interface_fragment.graphql (1)

1-27: Query shape correctly matches schema and expected JSON

The query and ProductDetail fragment correctly model the scenario: productA as a concrete root field, refined via an interface fragment (Base) and nested concrete type fragments (ProductA/ProductB). The alias displayOwner: owner lines up with the expected response category.displayOwner.name in the integration test, so this is well‑aligned with the regression you want to capture.

execution/federationtesting/accounts/graph/schema.resolvers.go (1)

214-225: ProductA resolver is consistent with schema and tests

The ProductA resolver correctly constructs the nested ProductA → Category → Owner graph and matches the IDs/names asserted in the federation integration test. It follows the existing pattern of static data resolvers in this file.

execution/federationtesting/accounts/graph/model/models_gen.go (1)

21-24: Generated model changes correctly reflect the new schema

The added Base interface, Category/Owner structs, and ProductA/ProductB types (with their IsBase and GetCategory implementations) are consistent with the GraphQL schema and the new resolver. This is standard gqlgen output and looks correct; no manual changes are needed here.

Also applies to: 128-132, 156-158, 166-181

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant