@@ -133,15 +133,16 @@ def lower_ir(ir_blocks, location_types, type_equivalence_hints=None):
ir_blocks: list of IR blocks to lower into Gremlin-compatible form
location_types: a dict of location objects -> GraphQL type objects at that location
type_equivalence_hints: optional dict of GraphQL interface or type -> GraphQL union.
Used as a workaround for Gremlin's lack of inheritance-awareness
When this parameter is not specified or is empty, type coercion
coerces to the *exact* type being coerced to without regard for
subclasses of that type. This parameter allows the user to
manually specify which GraphQL interfaces and types are
superclasses of which other types, and emits Gremlin code
that performs type coercion with this information in mind.
No recursive expansion of type equivalence hints will be performed,
and only type-level correctness of the hints is enforced.
Used as a workaround for GraphQL's lack of support for
inheritance across "types" (i.e. non-interfaces), as well as a
workaround for Gremlin's total lack of inheritance-awareness.
The key-value pairs in the dict specify that the "key" type
is equivalent to the "value" type, i.e. that the GraphQL type or
interface in the key is the most-derived common supertype
of every GraphQL type in the "value" GraphQL union.
Recursive expansion of type equivalence hints is not performed,
and only type-level correctness of this argument is enforced.
See README.md for more details on everything this parameter does.
*****
Be very careful with this option, as bad input here will
lead to incorrect output queries being generated.
@@ -347,12 +347,27 @@ def visitor_fn(expression):
# Public API #
##############

def lower_ir(ir_blocks, location_types):
def lower_ir(ir_blocks, location_types, type_equivalence_hints=None):
"""Lower the IR into an IR form that can be represented in MATCH queries.
Args:
ir_blocks: list of IR blocks to lower into MATCH-compatible form
location_types: a dict of location objects -> GraphQL type objects at that location
type_equivalence_hints: optional dict of GraphQL interface or type -> GraphQL union.
Used as a workaround for GraphQL's lack of support for
inheritance across "types" (i.e. non-interfaces), as well as a
workaround for Gremlin's total lack of inheritance-awareness.
The key-value pairs in the dict specify that the "key" type
is equivalent to the "value" type, i.e. that the GraphQL type or
interface in the key is the most-derived common supertype
of every GraphQL type in the "value" GraphQL union.
Recursive expansion of type equivalence hints is not performed,
and only type-level correctness of this argument is enforced.
See README.md for more details on everything this parameter does.
*****
Be very careful with this option, as bad input here will
lead to incorrect output queries being generated.
*****
Returns:
MatchQuery object containing the IR blocks organized in a MATCH-like structure
@@ -2,7 +2,7 @@
"""End-to-end tests of the GraphQL compiler."""
import unittest

from graphql import GraphQLID, GraphQLList, GraphQLString, GraphQLUnionType
from graphql import GraphQLID, GraphQLList, GraphQLString
import six

from ..compiler import OutputMetadata, compile_graphql_to_gremlin, compile_graphql_to_match
@@ -14,36 +14,22 @@
def check_test_data(test_case, graphql_input, expected_match, expected_gremlin,
expected_output_metadata, expected_input_metadata, type_equivalence_hints=None):
"""Assert that the GraphQL input generates all expected MATCH and Gremlin data."""
result = compile_graphql_to_match(test_case.schema, graphql_input)
compare_match(test_case, expected_match, result.query)
test_case.assertEqual(expected_output_metadata, result.output_metadata)
compare_input_metadata(test_case, expected_input_metadata, result.input_metadata)

if type_equivalence_hints:
# For test convenience, we accept the type equivalence hints in string form.
# Here, we convert them to the required GraphQL types, generating meaningless
# temporary names for the new interfaces.
schema_based_type_equivalence_hints = {}
name_format = 'temp_union_{}'
name_counter = 0
for key, value in six.iteritems(type_equivalence_hints):
new_key = test_case.schema.get_type(key)

new_value_name = name_format.format(name_counter)
name_counter += 1

# The 'resolve_type' argument below is only to side-step an unnecessary sanity-check
# in the underlying GraphQL library. The library assumes we are going to use
# its execution system, so it complains that we don't provide a means to resolve which
# of the allowed types in the union is actually present. We don't care, because we
# simply compile the GraphQL query directly to a database query.
new_value = GraphQLUnionType(new_value_name,
types=[test_case.schema.get_type(x) for x in value],
resolve_type=lambda: None)
schema_based_type_equivalence_hints[new_key] = new_value
# Here, we convert them to the required GraphQL types.
schema_based_type_equivalence_hints = {
test_case.schema.get_type(key): test_case.schema.get_type(value)
for key, value in six.iteritems(type_equivalence_hints)
}
else:
schema_based_type_equivalence_hints = None

result = compile_graphql_to_match(test_case.schema, graphql_input,
type_equivalence_hints=schema_based_type_equivalence_hints)
compare_match(test_case, expected_match, result.query)
test_case.assertEqual(expected_output_metadata, result.output_metadata)
compare_input_metadata(test_case, expected_input_metadata, result.input_metadata)

result = compile_graphql_to_gremlin(test_case.schema, graphql_input,
type_equivalence_hints=schema_based_type_equivalence_hints)
compare_gremlin(test_case, expected_gremlin, result.query)
@@ -1681,7 +1667,7 @@ def test_gremlin_type_hints(self):
}
}'''
type_equivalence_hints = {
'Event': {'Event', 'BirthEvent'}
'Event': 'EventOrBirthEvent'
}

expected_match = '''
@@ -2098,3 +2084,106 @@ def test_fold_date_and_datetime_fields(self):

check_test_data(self, graphql_input, expected_match, expected_gremlin,
expected_output_metadata, expected_input_metadata)

def test_coercion_to_union_base_type_inside_fold(self):
# Given type_equivalence_hints = { Event: EventOrBirthEvent },
# the coercion should be optimized away as a no-op.
graphql_input = '''{
Animal {
name @output(out_name: "animal_name")
out_Animal_ImportantEvent @fold {
... on Event {
name @output(out_name: "important_events")
}
}
}
}'''
type_equivalence_hints = {
'Event': 'EventOrBirthEvent'
}

expected_match = '''
SELECT
Animal___1.name AS `animal_name`,
Animal___1.out("Animal_ImportantEvent").name AS `important_events`
FROM (
MATCH {{
class: Animal,
as: Animal___1
}}
RETURN $matches
)
'''
expected_gremlin = '''
g.V('@class', 'Animal')
.as('Animal___1')
.transform{it, m -> new com.orientechnologies.orient.core.record.impl.ODocument([
animal_name: m.Animal___1.name,
important_events: (
(m.Animal___1.out_Animal_ImportantEvent == null) ? [] : (
m.Animal___1.out_Animal_ImportantEvent.collect{
entry -> entry.inV.next().name
}
)
)
])}
'''

expected_output_metadata = {
'animal_name': OutputMetadata(type=GraphQLString, optional=False),
'important_events': OutputMetadata(type=GraphQLList(GraphQLString), optional=False),
}
expected_input_metadata = {}

check_test_data(self, graphql_input, expected_match, expected_gremlin,
expected_output_metadata, expected_input_metadata,
type_equivalence_hints=type_equivalence_hints)

def test_no_op_coercion_inside_fold(self):
# The type where the coercion is applied is already Entity, so the coercion is a no-op.
graphql_input = '''{
Animal {
name @output(out_name: "animal_name")
out_Entity_Related @fold {
... on Entity {
name @output(out_name: "related_entities")
}
}
}
}'''

expected_match = '''
SELECT
Animal___1.name AS `animal_name`,
Animal___1.out("Entity_Related").name AS `related_entities`
FROM (
MATCH {{
class: Animal,
as: Animal___1
}}
RETURN $matches
)
'''
expected_gremlin = '''
g.V('@class', 'Animal')
.as('Animal___1')
.transform{it, m -> new com.orientechnologies.orient.core.record.impl.ODocument([
animal_name: m.Animal___1.name,
related_entities: (
(m.Animal___1.out_Entity_Related == null) ? [] : (
m.Animal___1.out_Entity_Related.collect{
entry -> entry.inV.next().name
}
)
)
])}
'''

expected_output_metadata = {
'animal_name': OutputMetadata(type=GraphQLString, optional=False),
'related_entities': OutputMetadata(type=GraphQLList(GraphQLString), optional=False),
}
expected_input_metadata = {}

check_test_data(self, graphql_input, expected_match, expected_gremlin,
expected_output_metadata, expected_input_metadata)
@@ -126,6 +126,7 @@ def get_schema():
out_Animal_OfSpecies: [Species]
out_Animal_FedAt: [Event]
out_Animal_BornAt: [BirthEvent]
out_Animal_ImportantEvent: [EventOrBirthEvent]
in_Entity_Related: [Entity]
out_Entity_Related: [Entity]
}
@@ -161,6 +162,7 @@ def get_schema():
uuid: ID
event_date: DateTime
in_Animal_FedAt: [Animal]
in_Animal_ImportantEvent: [Animal]
in_Entity_Related: [Entity]
out_Entity_Related: [Entity]
}
@@ -173,10 +175,14 @@ def get_schema():
event_date: DateTime
in_Animal_FedAt: [Animal]
in_Animal_BornAt: [Animal]
in_Animal_ImportantEvent: [Animal]
in_Entity_Related: [Entity]
out_Entity_Related: [Entity]
}
# Because of the above, the base type for this union is Event.
union EventOrBirthEvent = Event | BirthEvent
type RootSchemaQuery {
Animal: Animal
BirthEvent: BirthEvent
@@ -887,3 +887,62 @@ def test_invalid_variable_types(self):
for invalid_graphql in invalid_queries:
with self.assertRaises(GraphQLCompilationError):
graphql_to_ir(self.schema, invalid_graphql)

def test_filter_within_fold_scope(self):
# Filtering within a fold scope is currently not allowed.
invalid_queries = [
'''{
Animal {
name @output(out_name: "name")
out_Animal_ParentOf @fold {
name @filter(op_name: "=", value: ["$desired"]) @output(out_name: "child")
}
}
}''',

'''{
Animal {
name @output(out_name: "name")
out_Animal_ParentOf @fold
@filter(op_name: "name_or_alias", value: ["$desired"]) {
name @output(out_name: "child")
}
}
}''',
]

for invalid_graphql in invalid_queries:
with self.assertRaises(GraphQLCompilationError):
graphql_to_ir(self.schema, invalid_graphql)

def test_non_no_op_coercion_within_fold_scope(self):
# Applying a type coercion that is not a no-op (e.g. not coercing to
# the base type of a union type, or to the current type of the scope)
# is currently not allowed within a fold scope.
invalid_queries = [
'''{
Animal {
name @output(out_name: "name")
out_Entity_Related @fold {
... on Animal {
name @output(out_name: "related")
}
}
}
}''',

'''{
Animal {
name @output(out_name: "name")
out_Animal_ImportantEvent @fold {
... on BirthEvent {
name @output(out_name: "related")
}
}
}
}''',
]

for invalid_graphql in invalid_queries:
with self.assertRaises(GraphQLCompilationError):
graphql_to_ir(self.schema, invalid_graphql)