Skip to content
This repository was archived by the owner on Feb 6, 2025. It is now read-only.

Commit 1a3d7fc

Browse files
Query renaming field renaming (#982)
* Create tests for query renames * Implement query renaming for renamed fields * lint * lint * Remove stray newline * Fix comments * Capitalize AST * Fix docstrings to include field renamings * Replace conditionals with is None checks * Loosen mypy.ini to allow untyped call * run typing_copilot * Remove extra space
1 parent e667d58 commit 1a3d7fc

File tree

5 files changed

+356
-32
lines changed

5 files changed

+356
-32
lines changed

graphql_compiler/schema_transformation/rename_query.py

Lines changed: 96 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
# Copyright 2019-present Kensho Technologies, LLC.
22
from typing import Any, Dict, List, Union
33

4+
from graphql import GraphQLSchema
45
from graphql.language.ast import (
56
DocumentNode,
67
FieldNode,
8+
InterfaceTypeDefinitionNode,
79
NamedTypeNode,
10+
ObjectTypeDefinitionNode,
811
OperationDefinitionNode,
912
OperationType,
1013
SelectionSetNode,
1114
)
1215
from graphql.language.visitor import Visitor, VisitorAction, visit
1316
from graphql.validation import validate
1417

18+
from ..ast_manipulation import get_ast_with_non_null_and_list_stripped
1519
from ..exceptions import GraphQLValidationError
1620
from .rename_schema import RenamedSchemaDescriptor
1721
from .utils import RenameQueryNodeTypesT, get_copy_of_node_with_new_name
@@ -20,21 +24,21 @@
2024
def rename_query(
2125
ast: DocumentNode, renamed_schema_descriptor: RenamedSchemaDescriptor
2226
) -> DocumentNode:
23-
"""Translate names of types using reverse_name_map of the input RenamedSchemaDescriptor.
27+
"""Translate names of types/fields using reverse_name_map of the input RenamedSchemaDescriptor.
2428
25-
The direction in which types and fields are renamed is opposite of the process that
26-
produced the renamed schema descriptor. If a type X was renamed to Y in the schema, then
27-
any occurrences of type Y in the input query ast will be renamed to X.
29+
The direction in which types/fields are renamed is opposite of the process that
30+
produced the renamed schema descriptor. If a type/field X was renamed to Y in the schema, then
31+
any occurrences of Y in the input query AST will be renamed to X.
2832
29-
All type names (including ones in type coercions), as well as root vertex fields (fields
30-
of the query type) will be renamed. No other field names will be renamed.
33+
All type names (including ones in type coercions) and field names will be renamed.
3134
3235
Args:
3336
ast: represents a query
3437
renamed_schema_descriptor: namedtuple including the attribute reverse_name_map, which maps
35-
the new, renamed names of types to their original names. This
36-
function will revert these renamed types in the query ast back to
37-
their original names
38+
the new, renamed names of types to their original names, and
39+
reverse_field_name_map which has a similar role for fields. This
40+
function will revert these renamed types/fields in the query AST
41+
back to their original names
3842
3943
Returns:
4044
New AST representing the renamed query
@@ -70,25 +74,43 @@ def rename_query(
7074
'selection "{}"'.format(type(selection).__name__, selection)
7175
)
7276

73-
visitor = RenameQueryVisitor(renamed_schema_descriptor.reverse_name_map)
77+
visitor = RenameQueryVisitor(
78+
renamed_schema_descriptor.schema,
79+
renamed_schema_descriptor.reverse_name_map,
80+
renamed_schema_descriptor.reverse_field_name_map,
81+
)
7482
renamed_ast = visit(ast, visitor)
7583

7684
return renamed_ast
7785

7886

7987
class RenameQueryVisitor(Visitor):
80-
def __init__(self, type_renamings: Dict[str, str]) -> None:
81-
"""Create a visitor for renaming types and root vertex fields in a query AST.
88+
def __init__(
89+
self,
90+
schema: GraphQLSchema,
91+
type_renamings: Dict[str, str],
92+
field_renamings: Dict[str, Dict[str, str]],
93+
) -> None:
94+
"""Create a visitor for renaming types and fields in a query AST.
8295
8396
Args:
97+
schema: The renamed schema that the original query is written against
8498
type_renamings: Maps type or root field names to the new value in the dict.
8599
Any name not in the dict will be unchanged
100+
field_renamings: Maps type names to a dict mapping the field names to the new value.
101+
Any names not in the dicts will be unchanged
86102
"""
103+
self.schema = schema
87104
self.type_renamings = type_renamings
105+
self.field_renamings = field_renamings
88106
self.selection_set_level = 0
107+
# Acts like a stack that records the types of the current scopes. The last item is the top
108+
# of the stack. Each entry is the name of a type in the new schema, i.e. not the name of
109+
# the type in the original schema if it was renamed.
110+
self.current_type_name: List[str] = []
89111

90112
def _rename_name(self, node: RenameQueryNodeTypesT) -> RenameQueryNodeTypesT:
91-
"""Change the name of the input node if necessary, according to type_renamings.
113+
"""Change the name of the input node if necessary, according to the renamings.
92114
93115
Args:
94116
node: represents a field in an AST, containing a .name attribute. It is not modified
@@ -98,7 +120,21 @@ def _rename_name(self, node: RenameQueryNodeTypesT) -> RenameQueryNodeTypesT:
98120
the name was not changed, the returned object is the exact same object as the input
99121
"""
100122
name_string = node.name.value
101-
new_name_string = self.type_renamings.get(name_string, name_string) # Default use original
123+
if isinstance(node, FieldNode) and self.selection_set_level > 1:
124+
field_name = node.name.value
125+
# The top item in the stack is the type of the field, and the one immediately after that
126+
# is the type that contains this field in the schema
127+
current_type_name = self.current_type_name[-2]
128+
current_type_name_in_original_schema = self.type_renamings.get(
129+
current_type_name, current_type_name
130+
)
131+
new_name_string = self.field_renamings.get(
132+
current_type_name_in_original_schema, {}
133+
).get(field_name, field_name)
134+
else:
135+
new_name_string = self.type_renamings.get(
136+
name_string, name_string
137+
) # Default use original
102138
if new_name_string == name_string:
103139
return node
104140
else:
@@ -110,6 +146,7 @@ def enter_named_type(
110146
) -> Union[NamedTypeNode, VisitorAction]:
111147
"""Rename name of node."""
112148
# NamedType nodes describe types in the schema, appearing in InlineFragments
149+
self.current_type_name.append(node.name.value)
113150
renamed_node = self._rename_name(node)
114151
if renamed_node is node: # Name unchanged, continue traversal
115152
return None
@@ -131,18 +168,56 @@ def leave_selection_set(
131168
def enter_field(
132169
self, node: FieldNode, key: Any, parent: Any, path: List[Any], ancestors: List[Any]
133170
) -> Union[FieldNode, VisitorAction]:
134-
"""Rename root vertex fields."""
171+
"""Rename fields."""
135172
# For a Field to be a root vertex field, it needs to be the first level of
136173
# selections (fields in more nested selections are normal fields that should not be
137174
# modified)
138175
# As FragmentDefinition is not allowed, the parent of the selection must be a query
139176
# As a query may not start with an inline fragment, all first level selections are
140177
# fields
141178
if self.selection_set_level == 1:
142-
renamed_node = self._rename_name(node)
143-
if renamed_node is node: # Name unchanged, continue traversal
144-
return None
145-
else: # Name changed, return new node, `visit` will make shallow copies along path
146-
return renamed_node
179+
self.current_type_name.append(node.name.value)
180+
else:
181+
# Entered a regular field and we want to find its type
182+
current_type_name = self.current_type_name[-1]
183+
current_type = self.schema.get_type(current_type_name)
184+
if current_type is None:
185+
raise AssertionError(
186+
f"Current type is {current_type_name} which doesn't exist in the schema. This "
187+
f"is a bug."
188+
)
189+
if current_type.ast_node is None:
190+
raise AssertionError(
191+
f"Current type {current_type_name} should have non-null field ast_node, which "
192+
f"contains information such as the current type's fields. However, ast_node "
193+
f"was None. This is a bug."
194+
)
195+
if not isinstance(
196+
current_type.ast_node, (ObjectTypeDefinitionNode, InterfaceTypeDefinitionNode)
197+
):
198+
raise AssertionError(
199+
f"Current type {current_type_name}'s ast_node field should be an "
200+
f"ObjectTypeDefinitionNode. However, the actual type was "
201+
f"{type(current_type.ast_node)}. This is a bug."
202+
)
203+
current_type_fields = current_type.ast_node.fields
204+
for field_node in current_type_fields:
205+
# Unfortunately, fields is a list instead of some other datastructure so we actually
206+
# have to loop through them all.
207+
if field_node.name.value == node.name.value:
208+
field_type_name = get_ast_with_non_null_and_list_stripped(
209+
field_node.type
210+
).name.value
211+
self.current_type_name.append(field_type_name)
212+
break
213+
renamed_node = self._rename_name(node)
214+
if renamed_node is node: # Name unchanged, continue traversal
215+
return None
216+
else: # Name changed, return new node, `visit` will make shallow copies along path
217+
return renamed_node
147218

148-
return None
219+
def leave_field(
220+
self, node: FieldNode, key: Any, parent: Any, path: List[Any], ancestors: List[Any]
221+
) -> None:
222+
"""Record that we left a field."""
223+
self.current_type_name.pop()

graphql_compiler/tests/schema_transformation_tests/input_schema_strings.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,11 @@ class InputSchemaStrings(object):
421421
query: SchemaQuery
422422
}
423423
424+
directive @output(
425+
\"\"\"What to designate the output field generated from this property field.\"\"\"
426+
out_name: String!
427+
) on FIELD
428+
424429
type Human {
425430
id: String
426431
name: String

0 commit comments

Comments
 (0)