Skip to content
Closed
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 @@ -70,5 +70,5 @@ public static class LogEntryCodes
public const string RootSubscriptionUsed = "ROOT_SUBSCRIPTION_USED";
public const string SpecifiedByUrlMismatch = "SPECIFIED_BY_URL_MISMATCH";
public const string TypeKindMismatch = "TYPE_KIND_MISMATCH";
public const string Unsatisfiable = "UNSATISFIABLE";
public const string UnsatisfiableQueryPath = "UNSATISFIABLE_QUERY_PATH";
}
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ private void VisitOutputField(
_log.Write(
LogEntryBuilder.New()
.SetMessage(error.ToString())
.SetCode(LogEntryCodes.Unsatisfiable)
.SetCode(LogEntryCodes.UnsatisfiableQueryPath)
.SetSeverity(LogSeverity.Error)
.SetExtension("error", error)
.Build());
Expand Down Expand Up @@ -297,7 +297,7 @@ private void VisitNodeField(
_log.Write(
LogEntryBuilder.New()
.SetMessage(error.ToString())
.SetCode(LogEntryCodes.Unsatisfiable)
.SetCode(LogEntryCodes.UnsatisfiableQueryPath)
.SetSeverity(LogSeverity.Error)
.SetExtension("error", error)
.Build());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type User {

// assert
Assert.True(result.IsFailure);
Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code));
string.Join("\n\n", log.Select(e => e.Message)).MatchInlineSnapshot(
"""
Unable to access the field 'User.membershipStatus' on path 'A:Query.profileById<Profile> -> A:Profile.user<User>'.
Expand Down Expand Up @@ -119,6 +120,7 @@ type Product {

// assert
Assert.True(result.IsFailure);
Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code));
string.Join("\n\n", log.Select(e => e.Message)).MatchInlineSnapshot(
"""
Unable to access the field 'Product.sku' on path 'B:Query.productById<Product>'.
Expand Down Expand Up @@ -179,6 +181,7 @@ type Product {

// assert
Assert.True(result.IsFailure);
Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code));
string.Join("\n\n", log.Select(e => e.Message)).MatchInlineSnapshot(
"""
Unable to access the field 'Product.sku' on path 'A:Query.productById<Product>'.
Expand Down Expand Up @@ -319,6 +322,7 @@ type Address @shareable {

// assert
Assert.True(result.IsFailure);
Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code));
string.Join("\n\n", log.Select(e => e.Message)).MatchInlineSnapshot(
"""
Unable to access the field 'Address.country' on path 'A:Query.userById<User> -> A:User.address<Address>'.
Expand Down Expand Up @@ -375,6 +379,7 @@ type Category @key(fields: "id") {

// assert
Assert.True(result.IsFailure);
Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code));
string.Join("\n\n", log.Select(e => e.Message)).MatchInlineSnapshot(
"""
Unable to access the field 'Category.name' on path 'B:Query.categoryById<Category>'.
Expand Down Expand Up @@ -1470,6 +1475,7 @@ type Product {

// assert
Assert.True(result.IsFailure);
Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code));
string.Join("\n\n", log.Select(e => e.Message)).MatchInlineSnapshot(
"""
Unable to access the field 'Product.sku' on path 'A:Query.productById<Product>'.
Expand Down Expand Up @@ -1632,6 +1638,7 @@ interface Animal {

// assert
Assert.False(result.IsSuccess);
Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code));
string.Join("\n\n", log.Select(e => e.Message))
.MatchInlineSnapshot(
"""
Expand Down Expand Up @@ -1980,6 +1987,7 @@ type Dog {

// assert
Assert.False(result.IsSuccess);
Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code));
string.Join("\n\n", log.Select(e => e.Message))
.MatchInlineSnapshot(
"""
Expand Down Expand Up @@ -2032,6 +2040,7 @@ type Product {

// assert
Assert.True(result.IsFailure);
Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code));
string.Join("\n\n", log.Select(e => e.Message)).MatchInlineSnapshot(
"""
Unable to access the field 'Product.title' on path 'A:Query.productById<Product>'.
Expand Down Expand Up @@ -2120,6 +2129,7 @@ type Section {

// assert
Assert.True(result.IsFailure);
Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code));
string.Join("\n\n", log.Select(e => e.Message)).MatchInlineSnapshot(
"""
Unable to access the field 'Product.title' on path 'A:Query.productById<Product>'.
Expand Down Expand Up @@ -2300,6 +2310,7 @@ type Viewer {

// assert
Assert.True(result.IsFailure);
Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code));
string.Join("\n\n", log.Select(e => e.Message)).MatchInlineSnapshot(
"""
Unable to access the field 'Viewer.lastName' on path 'A:Query.viewer<Viewer>'.
Expand Down Expand Up @@ -2360,6 +2371,7 @@ type Viewer {

// assert
Assert.True(result.IsFailure);
Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code));
string.Join("\n\n", log.Select(e => e.Message)).MatchInlineSnapshot(
"""
Unable to access the field 'Viewer.fullName' on path 'A:Query.viewer<Viewer>'.
Expand Down Expand Up @@ -2529,6 +2541,7 @@ type Section {

// assert
Assert.True(result.IsFailure);
Assert.All(log, e => Assert.Equal(LogEntryCodes.UnsatisfiableQueryPath, e.Code));
string.Join("\n\n", log.Select(e => e.Message)).MatchInlineSnapshot(
"""
Unable to access the field 'Category.name' on path 'A:Query.productById<Product> -> B:Product.category<Category>'.
Expand Down
12 changes: 6 additions & 6 deletions website/src/docs/fusion/v16/composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Validates the merged schema as a whole. The rules here treat the composed schema

### 8. Validate Satisfiability

Performs reachability analysis. Starting from the root types, the pipeline walks every reachable field in the merged schema and confirms it can be resolved by at least one subgraph given the available `@lookup` and `@key` paths. If a field is reachable from a query but no subgraph can produce it, satisfiability fails with `UNSATISFIABLE`. This is the last line of defense against shapes that look valid statically but cannot actually be served at runtime.
Performs reachability analysis. Starting from the root types, the pipeline walks every reachable field in the merged schema and confirms it can be resolved by at least one subgraph given the available `@lookup` and `@key` paths. If a field is reachable from a query but no subgraph can produce it, satisfiability fails with `UNSATISFIABLE_QUERY_PATH`. This is the last line of defense against shapes that look valid statically but cannot actually be served at runtime.

## Common Scenarios

Expand Down Expand Up @@ -180,7 +180,7 @@ The placeholders `{0}`, `{1}`, etc. in the message column are replaced with the
| [ROOT_QUERY_USED](https://graphql.github.io/composite-schemas-spec/draft/#sec-Root-Query-Used) | The root query type in schema '{0}' must be named 'Query'. | Fusion requires the root query type to use the canonical name `Query`. Rename the type in the named source schema. A type named `Query` must not exist unless it is the root query type. See [Getting Started](/docs/fusion/v16/getting-started) for the expected schema layout. |
| [ROOT_SUBSCRIPTION_USED](https://graphql.github.io/composite-schemas-spec/draft/#sec-Root-Subscription-Used) | The root subscription type in schema '{0}' must be named 'Subscription'. | Fusion requires the root subscription type to use the canonical name `Subscription`. Rename the type in the named source schema. A type named `Subscription` must not exist unless it is the root subscription type. |
| [TYPE_KIND_MISMATCH](https://graphql.github.io/composite-schemas-spec/draft/#sec-Type-Kind-Mismatch) | The type '{0}' has a different kind in schema '{1}' ({2}) than it does in schema '{3}' ({4}). | The same type name was used for different kinds (for example, an object in one source schema and an interface in another). Decide which kind is correct and update the other source schema, or rename one of the types so they no longer collide. |
| `UNSATISFIABLE` | (Message varies. Includes the unreachable field, the path the validator tried, and the lookups it considered.) | Some reachable field cannot be resolved through the available `@lookup` and `@key` paths. See [Diagnosing UNSATISFIABLE Errors](#diagnosing-unsatisfiable-errors) for how to read the message and fix the underlying gap. |
| [UNSATISFIABLE_QUERY_PATH](https://graphql.github.io/composite-schemas-spec/draft/#sec-Unsatisfiable-Query-Path) | (Message varies. Includes the unreachable field, the path the validator tried, and the lookups it considered.) | Some reachable field cannot be resolved through the available `@lookup` and `@key` paths. See [Diagnosing UNSATISFIABLE_QUERY_PATH Errors](#diagnosing-unsatisfiable_query_path-errors) for how to read the message and fix the underlying gap. |

### Warnings

Expand All @@ -189,9 +189,9 @@ The placeholders `{0}`, `{1}`, etc. in the message column are replaced with the
| [LOOKUP_RETURNS_NON_NULLABLE_TYPE](https://graphql.github.io/composite-schemas-spec/draft/#sec-Lookup-Returns-Non-Nullable-Type) | The lookup field '{0}' in schema '{1}' should return a nullable type. | Lookups should return nullable entities so the gateway can represent missing keys without throwing. Change the return type of the lookup field to nullable. See [Entities and Lookups](/docs/fusion/v16/entities-and-lookups). |
| [SPECIFIED_BY_URL_MISMATCH](https://graphql.github.io/composite-schemas-spec/draft/#sec-SpecifiedBy-URL-Mismatch) | The scalar type '{0}' has a different specified-by URL in schema '{1}' ({2}) than it does in schema '{3}' ({4}). | The same scalar declares conflicting `@specifiedBy(url: ...)` URLs in different source schemas. Align the URL across all source schemas that define the scalar so the composed schema points to a single specification. |

### Diagnosing UNSATISFIABLE Errors
### Diagnosing UNSATISFIABLE_QUERY_PATH Errors

Most composition errors point at a single source schema or definition. `UNSATISFIABLE` is different. It is raised by the satisfiability validator (phase 8) and tells you that the merged schema as a whole has at least one reachable field that no source schema can serve through the available `@lookup` and `@key` paths.
Most composition errors point at a single source schema or definition. `UNSATISFIABLE_QUERY_PATH` is different. It is raised by the satisfiability validator (phase 8) and tells you that the merged schema as a whole has at least one reachable field that no source schema can serve through the available `@lookup` and `@key` paths.

Because the validator weighs every option for reaching a field across every source schema, the diagnostic carries a tree of nested errors that explain why each candidate option failed. Reading that tree from the bottom up is the fastest way to find the gap.

Expand Down Expand Up @@ -231,7 +231,7 @@ Set the option via the CLI flag during composition:
nitro fusion compose --include-satisfiability-paths
```

Paths add noise to short outputs and pay off on any non-trivial graph. Turn them on whenever an `UNSATISFIABLE` error is hard to triage.
Paths add noise to short outputs and pay off on any non-trivial graph. Turn them on whenever an `UNSATISFIABLE_QUERY_PATH` error is hard to triage.

#### Common causes and fixes

Expand All @@ -240,7 +240,7 @@ Paths add noise to short outputs and pay off on any non-trivial graph. Turn them
- **`@require` reaches for a field that is not on the path.** A `@require(field: "...")` references parent fields that the validator cannot resolve given the path it took. Either expose the required field on the parent type along that path, or rewrite the requirement.
- **`Node` interface without a node lookup.** A type implements `Node` but no source schema exposes a `node(id: ID!): Node` lookup that returns it. Add one in any source schema that owns the entity.

When you fix the root cause, both the top-level `UNSATISFIABLE` error and its nested children disappear together.
When you fix the root cause, both the top-level `UNSATISFIABLE_QUERY_PATH` error and its nested children disappear together.

#### Temporarily ignoring a non-accessible field

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Validating Fusion configuration of API 'QXBpCmcwMTlkMmIzMGUzNGY3YzQ2OTBjNTgxOTNk

❌ [ERR] Unable to access the field 'Review.productVariant'.
Unable to transition between schemas 'REVIEWS' and 'PRODUCTS' for access to field 'PRODUCTS:Review.productVariant<Product>'.
No lookups found for type 'Review' in schema 'PRODUCTS'. (UNSATISFIABLE)
No lookups found for type 'Review' in schema 'PRODUCTS'. (UNSATISFIABLE_QUERY_PATH)
Satisfiability validation failed.

-->
Expand Down Expand Up @@ -410,7 +410,7 @@ public static class Query
}
```

Missing `@lookup` annotations will usually manifest as `UNSATISFIABLE` errors in the composition. See [Entities and lookups](/docs/fusion/v16/entities-and-lookups) for details.
Missing `@lookup` annotations will usually manifest as `UNSATISFIABLE_QUERY_PATH` errors in the composition. See [Entities and lookups](/docs/fusion/v16/entities-and-lookups) for details.

If your graph has overlapping fields, i.e. multiple subgraphs providing the same field, you now also have to explicitly mark those fields as shareable from both sides.

Expand Down
Loading