-
Notifications
You must be signed in to change notification settings - Fork 51
Schema renaming: Interface implementation suppression #1002
base: main
Are you sure you want to change the base?
Conversation
Codecov Report
@@ Coverage Diff @@
## main #1002 +/- ##
==========================================
+ Coverage 94.38% 94.40% +0.02%
==========================================
Files 113 113
Lines 9080 9133 +53
==========================================
+ Hits 8570 8622 +52
- Misses 510 511 +1
Continue to review full report at Codecov.
|
renamed_schema_ast = visit(schema_ast, visitor) | ||
return renamed_schema_ast | ||
|
||
|
||
def _recursively_get_ancestor_interface_names( |
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.
Necessary to define this function outside the RenameSchemaTypesVisitor
object because we need access to the schema's definitions
field in order to get the interface definition nodes.
This depends on some other code being merged into main, such as the code for interface implementation suppression in PR #1002, so this is just a rough draft at the moment and just contains the rough outline for what this code would look like.
- If you suppress a type Foo implementing an interface Bar, then Bar will remain in the schema but | ||
not in the root query type, making it unqueryable. This is to prevent situations where a scope in | ||
a query is of type Bar without also being of some more specific type, which may yield vertices of | ||
type Foo even though it was suppressed. For this reason, all interfaces that Bar implements will | ||
also be made unqueryable in the same way, and this removal from the root query type will happen | ||
recursively until all interfaces that Foo implements (either directly or indirectly) are | ||
unqueryable. |
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.
Looks like this comment could be updated to mention that field interface suppression is not yet implemented i.e. what this test is addressing.
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.
Hm, so this is a bit tricky-- the plan for field renamings in PR 1005 is more like allowing something that should already exist, specifically if we have a schema like
# Exists interface named Foo with a single field named id
type Bar implements Foo {
name: String
id: String
}
then it should be legal to rename the name
field, just like if Bar
implemented no interfaces. In other words, it seems like it's less of a large functionality change that's coming soon and more like allowing something that should be allowed without adding more special cases to the field renaming API, had the NotImplementedError
checks not been so strict.
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.
Is the case in the test case I linked above going to be implemented eventually or will it always be a CascadingSuppressionError
? If it is going to be implemented eventually, maybe in the Operations that are not yet supported but will be implemented:
section there should be something like automatic suppression of fields when the field is of a suppressed interface type
?
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.
The current implementation will always be a CascadingSuppressionError
-- the problem in that test case is that the Giraffe
type's friend
field is of type Character
, and Character
is to be made unqueryable. However, there's no way to suppress any fields in Giraffe
right now because Giraffe
implements an interface, so until the changes in #1005 relax this restriction, there's no way to implement the part of the test that shows how we'd want to do such a thing.
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.
Right, I'm not proposing you implement in this PR, but I think it should be documented somewhere that you can't suppress an object that implements an interface if that interfaces is a field type on another object. Is that already documented somewhere that I missed?
Co-authored-by: chewselene <52711428+chewselene@users.noreply.github.com>
Co-authored-by: chewselene <52711428+chewselene@users.noreply.github.com>
Co-authored-by: chewselene <52711428+chewselene@users.noreply.github.com>
type_not_suppressed = type_renamings[type_name] is not None | ||
if type_not_suppressed: | ||
continue | ||
if type_name in interface_and_object_type_name_to_definition_node_map: |
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.
type_not_suppressed = type_renamings[type_name] is not None | |
if type_not_suppressed: | |
continue | |
if type_name in interface_and_object_type_name_to_definition_node_map: | |
# <comment> | |
if type_renamings[type_name] is None and type_name in interface_and_object_type_name_to_definition_node_map: |
nit: this seems like it could all be done in 1 conditional. What does it mean if type_name in interface_and_object_type_name_to_definition_node_map
is False
?
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.
This is a bit unfortunate-- so, as it is, we need to compute which interfaces should be made unqueryable before doing any sort of renaming. However, all the error handling for unused type renamings happens after all the renaming is done. If type_name
, an entry in type_renamings
, is not in the map, then type_name
doesn't actually correspond to a type in the schema, and it should have no effect. Now that I look at this again, there might be a better way to organize this but I'm not quite sure how might work best-- open to suggestions!
(I suppose this also affects the first part, where we might do this in a single conditional.)
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.
When type_name not in interface_and_object_type_name_to_definition_node_map
, the user is attempting to rename something that is not in the schema, right? It seems like that should be an error to me because renaming something that doesn't exist does not make sense. Am I understanding the meaning correctly, and, if so, any reasons this shouldn't be an error?
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 does end up as an error, but the error handling happens later on. The structure of the code, before this PR, is along the lines of:
- Go through the schema, applying renamings via the visitor pattern
- If there are any unused renamings, then raise an error saying which renamings were unused.
Now, we're in a bit of a catch-22 situation: the point of the visitor is that we don't have access to the entire schema-- only individual nodes at any one time. However, we can't find out which interface nodes to be made unqueryable without schema_ast.definitions
, which needs to be done outside a visitor object (and therefore before any detection of unused renamings).
The workaround that this PR currently uses is basically ignoring unused type renamings because they'll be handled later on in the helper functions that use visitor objects. Hacky, but I'm not sure if we have other options here.
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'm assuming you want to hold off on raising an error message until later because it will be a more complete error message with all the issues in the schema renaming, right? This could definitely benefit from a comment explaining that and I still think it can just be 1 conditional.
- If you suppress a type Foo implementing an interface Bar, then Bar will remain in the schema but | ||
not in the root query type, making it unqueryable. This is to prevent situations where a scope in | ||
a query is of type Bar without also being of some more specific type, which may yield vertices of | ||
type Foo even though it was suppressed. For this reason, all interfaces that Bar implements will | ||
also be made unqueryable in the same way, and this removal from the root query type will happen | ||
recursively until all interfaces that Foo implements (either directly or indirectly) are | ||
unqueryable. |
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.
Is the case in the test case I linked above going to be implemented eventually or will it always be a CascadingSuppressionError
? If it is going to be implemented eventually, maybe in the Operations that are not yet supported but will be implemented:
section there should be something like automatic suppression of fields when the field is of a suppressed interface type
?
Interfaces present a challenge for schema renaming that don't show up with regular object types uninvolved with interfaces.
This PR implements suppressing object types that implement interfaces. The main issue is that suppress an object type
Foo
that implements an interfaceBar
can cause problems if, later on, someone queries for all objects that implementBar
and ends up getting objects of suppressed typeFoo
. If this happens, we remove the interface (as well as all its ancestor interfaces*) fromRootSchemaQuery
to make it unqueryable.*For the directed graph consisting of all types in the schema as nodes and the edges are from an interface to a type that implements it (for every such interface and type pair), an interface is an ancestor of another type iff there's a path from the interface's node to that type's node.