-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Semantic nullability rfc implementation #4337
base: 16.x.x
Are you sure you want to change the base?
Conversation
Co-Authored-By: Benjie <[email protected]> Co-Authored-By: twof <[email protected]>
Hi @JoviDeCroock, I'm @github-actions bot happy to help you with this PR 👋 Supported commandsPlease post this commands in separate comments and only one per comment:
|
3ed6ea8
to
7c0b4bb
Compare
7c0b4bb
to
0f13010
Compare
Don't have the bandwidth to progress there further, just posting in case that helps. |
@yaacovCR That does seem to be the inverse of what I'm proposing, I would only have a wrapping type for semantic-nullability which in your PR seems to make the newly introduced type the nullable one, I think that for clarity of the community we would do good in making the SEMANTIC_NON_NULL the only newly introduced AST node. I have yet to explore the feasibility though, I got caught up in a few things |
@JoviDeCroock I find this feature very difficult in terms of the upgrade story => definitely part of the challenge of making this work. I see the advantage in having the new wrapper type be the new My pre-existing PR, now rebased in #4338, like this PR and like #4271 (as opposed to #4192) is adopting Either way, I think the upgrade story is a bit confusing, however. For example, we have a function called My feedback to the spec folks if they are looking for it is that I find it extremely difficult to reason constantly in terms of the two different types of nullability and that we should separate these concepts more broadly into different terms, for example, perhaps |
I'd advise against using "optional" on output types, it implies that the field itself can be omitted, rather than that it can be null (i.e. the difference between |
This reduces the new AST-nodes to only be for the newly introduced type, this does make it so that when we invoke `print` we have to rely on the user to either specify that we're in semantic nullability mode _or_ we could do a pre-traverse and when we enter a node with semantic-non-null we toggle it on ourselves. The main reasoning behind removing the new name for our existing null type is that I would prefer to be backwards compatible in terms of schema structure. This because it might become complex for people to reason about composed schemas, i.e. a lot of individually parsed schemas that later on compose into a larger one. I know that _technically_ this is covered because in the classic ones we'll have the non wrapped null type and in the modern ones we'll have the semantic nullable wrapped type. For schema-builders like pothos and others I think this is rather complex to reason about _and_ to supply us with. I would instead choose to absorb this complexity in the feature and stay backwards compatible. This also sets us up for the SDL not being a breaking change, we only add one AST-type, what's left now is to settle on a semantic non-null syntax and making everything backwards compatible.
I think we corrected this in Discord, and it looks like SemanticNonNullable has the correct behavior in code. |
@@ -152,6 +154,13 @@ export interface ExecutionArgs { | |||
fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>; | |||
typeResolver?: Maybe<GraphQLTypeResolver<any, any>>; | |||
subscribeFieldResolver?: Maybe<GraphQLFieldResolver<any, any>>; | |||
/** | |||
* Set to `false` to disable error propagation. Experimental. | |||
* TODO: describe what this does |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Outstanding TODO. We might want to say something here about how this is being used and include a link to the RFC or spec edits.
There were a few tests that broke due to changes to print params
it('prints SemanticNullableType', () => { | ||
expect( | ||
print(parseType('MyType?', { allowSemanticNullability: true }), { | ||
useSemanticNullability: true, | ||
}), | ||
).to.equal(dedent`MyType?`); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think anything printed with allowSemanticNullability
, will need to be prefixed with the document directive (@SemanticNullability
) because otherwise the contents are liable to misinterpretation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's less allow and more just on or off, right? What it's set to changes how a type is interpreted:
Traditional | @semanticNullability | |
---|---|---|
Int? | ERROR! | Int |
Int | Int | SemanticNonNull |
Int! | NonNull | NonNull |
Document directives must come at the top of the document, and in my mental model they effectively change the "mode" of the rest of the parsing. You might think of other document directives that might change the mode, e.g. @commentsAreDescriptions
to re-enable the ancient parsing feature where comments (rather than strings) before fields would become descriptions; or @prefixedDirectives
which might put directives before things rather than after. (I'm not proposing either of these be made real, I'm just trying to show that the space for document directives is not just a set of size 1.)
Once you hit your first non-ignored non-document-directive token the mode is then frozen, and you can pass that mode into parseType
and other parsing related functions.
Thus I think it would be more like:
it('prints SemanticNullableType', () => { | |
expect( | |
print(parseType('MyType?', { allowSemanticNullability: true }), { | |
useSemanticNullability: true, | |
}), | |
).to.equal(dedent`MyType?`); | |
}); | |
it('prints SemanticNullableType', () => { | |
expect( | |
print(parseType('MyType?', { mode: { semanticNullability: true }}), { | |
useSemanticNullability: true, | |
}), | |
).to.equal(dedent`MyType?`); | |
}); |
I'm not sure "mode" is really the right name to use for the result of applying document level directives, suggestions welcome. It could be that we just take the document-level directives and store their literal values into the object; for example:
@semanticNullability
@behavior(onError: PROPAGATE, onDeprecated: ERROR)
@compositeScalars(serialize: true)
{ __typename }
might have a "mode" of:
const parseOptions = {
mode: {
semanticNullability: {},
behavior: { onError: "PROPAGATE", onDeprecated: "ERROR" },
compositeScalars: { serialize: true },
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Note that this mode should also be an output of parsing, e.g. we might use a document directive in an executable document to enable @noErrorPropagation
- and we'll need to see that. Originally I thought that @noErrorPropagation
would go on the operation itself rather than the document, but the issue with that is that fragments will have two modes if they're used in two operations with different @noErrorPropagation
settings, so codegen becomes complicated. Instead, making it a document directive means it applies to all operations and fragments in a single document in unison.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work! Thanks for putting on the finishing touches. I've left some comments, and it looks like we're missing some code coverage, but after that I think we'll be good to go.
Oh one thing I missed. I think we need to define the document directive in |
5867f34
to
05f16f2
Compare
src/utilities/printSchema.ts
Outdated
@@ -36,6 +36,8 @@ import type { GraphQLSchema } from '../type/schema'; | |||
|
|||
import { astFromValue } from './astFromValue'; | |||
|
|||
// TODO: we might need to add a flag to print the schema with semantic-non-null types |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CC @twof @benjie how do we expect to tackle this, document directives aren't allowed as it stands and we'd need to store this on the schema itself. We do have the possibility of a schema directive but that might span different documents.
Similar thoughts arise for String(GraphQLOutputType)
and inspect(type)
when do we print ?
it needs to have some brand on every type that it belongs to a semantic non-null document.
05f16f2
to
4cf86ed
Compare
beb54c4
to
702da5d
Compare
d7278da
to
ebe2ee5
Compare
930c7d2
to
cfe1dcc
Compare
This type is reused for variables, inputs and list-types which can't be smantically non-null. list-types can be but only if they are used in an SDL context. This is kind of a short-coming of our types, we conflate SDL and execution language.
cfe1dcc
to
4c8a02b
Compare
Introduction
This is the working implementation for semantic nullability, this originates from work done by @twof and @benjie in #4271 and #4192. This PR intends to supersede this work and drive it home.
When Semantic nullability is on we change the semantics of the schema-type system, currently there are two types of nullability, it's either nullable or not. When semantic nullability is introduced we expand this, any field can be null, non-null or semantically null.
Semantically null fields can only return a
null
value when there's an error associated with the field, when we return null and there's no error associated we'll bubble up the error to the nearest concretely nullable field.Semantic nullability will be introduced either by means of a directive in the SDL
@SemanticNullability
which converts the SDL document to use semantic nullability or by switching on theparse
configuration.Documentation
One of the most important parts for adoption would be to adequately document this feature and rope in folks maintaining schema builder libraries.
Open questions
Currently we employ quite a bit of new semantics to achieve the scenario, we introduce both a semantic null and non-null type. This might confuse some people in terms of explicitness but helps a lot with the
print
implementation as it clearly delineates the new behaviour types.My proposal would be to only introduce 1 new AST type which would be SEMANTIC_NON_NULLABLE, the complication would arise for the printer but it would be the least changes for folks creating schema builders and other tools in our ecosystem.
Should we be more concrete about input-types not being able to express semantic nullability?
There is a lot of work still to be done on test coverage but I wanted to raise the PR already to let y'all know that I'm working on it. This is a place for discussion as well, any alternatives can be raised, I am still familiarising myself more on the matter and will probably be doing so for a few days.
Update (5th of February 2025)
We merged 869ca46 which reduces the increase in AST-nodes to 1, this helps us a bit in terms of introducing new concepts and backwards compatability of AST traversal. In an ideal world we would only introduce a new syntax so this becomes easier for printing as well and is backwards compatible when it comes to SDL.