Skip to content

Commit eea6163

Browse files
authored
Feature/#63 deep filter optimization (#78)
Implement a specific optimzations of deep filter (esp. across relationships) that will generate regular match statements from the root node so that the Cypher planner can pick that up and optimize it. It's not on by default, needs to be enabled in the translator config with `optimizedQuery: [FILTER_AS_MATCH]` Uses neo4j-opencypher-dsl dependency 1.1.0 to generate partial cypher statements. Fixes #63
1 parent 249877d commit eea6163

File tree

16 files changed

+1620
-106
lines changed

16 files changed

+1620
-106
lines changed

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@
113113
<version>${kotlin.version}</version>
114114
<scope>test</scope>
115115
</dependency>
116+
<dependency>
117+
<groupId>org.neo4j</groupId>
118+
<artifactId>neo4j-opencypher-dsl</artifactId>
119+
<version>1.1.0</version>
120+
</dependency>
116121
</dependencies>
117122

118123
<build>

src/main/kotlin/org/neo4j/graphql/BuildingEnv.kt

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,13 @@ class BuildingEnv(val types: MutableMap<String, GraphQLType>) {
9292
else -> addFilterType(getInnerFieldsContainer(typeDefinition), createdTypes)
9393
}
9494

95-
Operators.forType(types[filterType] ?: typeDefinition).forEach { op ->
96-
val wrappedType: GraphQLInputType = when {
97-
op.list -> GraphQLList(GraphQLTypeReference(filterType))
98-
else -> GraphQLTypeReference(filterType)
99-
}
100-
builder.field(GraphQLInputObjectField.newInputObjectField()
101-
.name(op.fieldName(field.name))
102-
.type(wrappedType))
95+
if (field.isRelationship()) {
96+
RelationOperator.createRelationFilterFields(type, field, filterType, builder)
97+
} else {
98+
FieldOperator.forType(types[filterType] ?: typeDefinition)
99+
.forEach { op -> builder.addFilterField(op.fieldName(field.name), op.list, filterType) }
103100
}
101+
104102
}
105103
types[filterName] = builder.build()
106104
return filterName

src/main/kotlin/org/neo4j/graphql/GraphQLExtensions.kt

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import org.neo4j.graphql.DirectiveConstants.Companion.RELATION_FROM
1717
import org.neo4j.graphql.DirectiveConstants.Companion.RELATION_NAME
1818
import org.neo4j.graphql.DirectiveConstants.Companion.RELATION_TO
1919
import org.neo4j.graphql.handler.projection.ProjectionBase
20+
import org.neo4j.opencypherdsl.Node
21+
import org.neo4j.opencypherdsl.Relationship
2022

2123
fun Type<Type<*>>.name(): String? = if (this.inner() is TypeName) (this.inner() as TypeName).name else null
2224
fun Type<Type<*>>.inner(): Type<Type<*>> = when (this) {
@@ -36,7 +38,7 @@ fun GraphQLType.isScalar() = this.inner().let { it is GraphQLScalarType || it.na
3638
fun GraphQLType.isNeo4jType() = this.inner().name?.startsWith("_Neo4j") == true
3739
fun GraphQLFieldDefinition.isNeo4jType(): Boolean = this.type.isNeo4jType()
3840

39-
fun GraphQLFieldDefinition.isRelationship() = this.type.inner().let { it is GraphQLFieldsContainer }
41+
fun GraphQLFieldDefinition.isRelationship() = !type.isNeo4jType() && this.type.inner().let { it is GraphQLFieldsContainer }
4042

4143
fun GraphQLFieldsContainer.hasRelationship(name: String) = this.getFieldDefinition(name)?.isRelationship() ?: false
4244
fun GraphQLDirectiveContainer.isRelationType() = getDirective(DirectiveConstants.RELATION) != null
@@ -46,13 +48,18 @@ fun GraphQLFieldsContainer.relationshipFor(name: String): RelationshipInfo? {
4648
?: throw IllegalArgumentException("$name is not defined on ${this.name}")
4749
val fieldObjectType = field.type.inner() as? GraphQLFieldsContainer ?: return null
4850

49-
// TODO direction is depending on source/target type
50-
51-
val (relDirective, isRelFromType) = (fieldObjectType as? GraphQLDirectiveContainer)
52-
?.getDirective(DirectiveConstants.RELATION)?.let { it to true }
53-
?: field.getDirective(DirectiveConstants.RELATION)?.let { it to false }
54-
?: throw IllegalStateException("Field $field needs an @relation directive")
51+
val (relDirective, isRelFromType) = if (isRelationType()) {
52+
(this as? GraphQLDirectiveContainer)
53+
?.getDirective(DirectiveConstants.RELATION)?.let { it to false }
54+
?: throw IllegalStateException("Type ${this.name} needs an @relation directive")
55+
} else {
56+
(fieldObjectType as? GraphQLDirectiveContainer)
57+
?.getDirective(DirectiveConstants.RELATION)?.let { it to true }
58+
?: field.getDirective(DirectiveConstants.RELATION)?.let { it to false }
59+
?: throw IllegalStateException("Field $field needs an @relation directive")
60+
}
5561

62+
// TODO direction is depending on source/target type
5663

5764
val relInfo = relDetails(fieldObjectType) { argName, defaultValue -> relDirective.getArgument(argName, defaultValue) }
5865

@@ -62,28 +69,31 @@ fun GraphQLFieldsContainer.relationshipFor(name: String): RelationshipInfo? {
6269

6370
fun GraphQLFieldsContainer.getValidTypeLabels(schema: GraphQLSchema): List<String> {
6471
if (this is GraphQLObjectType) {
65-
return listOf(this.label())
72+
return listOf(this.quotedLabel())
6673
}
6774
if (this is GraphQLInterfaceType) {
6875
return schema.getImplementations(this)
69-
.mapNotNull { it.label() }
76+
.mapNotNull { it.quotedLabel() }
7077
}
7178
return emptyList()
7279
}
7380

74-
@Suppress("SimplifiableCallChain")
75-
fun GraphQLFieldsContainer.label(includeAll: Boolean = false) = when {
81+
fun GraphQLFieldsContainer.label(): String = when {
7682
this.isRelationType() ->
7783
(this as? GraphQLDirectiveContainer)
7884
?.getDirective(DirectiveConstants.RELATION)
79-
?.getArgument(RELATION_NAME)?.value?.toJavaValue()?.toString()?.quote()
80-
?: this.name.quote()
81-
else -> when {
82-
includeAll -> (listOf(name) + ((this as? GraphQLObjectType)?.interfaces?.map { it.name } ?: emptyList()))
83-
.map { it.quote() }
84-
.joinToString(":")
85-
else -> name.quote()
86-
}
85+
?.getArgument(RELATION_NAME)?.value?.toJavaValue()?.toString()
86+
?: this.name
87+
else -> name
88+
}
89+
90+
fun GraphQLFieldsContainer.quotedLabel() = this.label().quote()
91+
92+
fun GraphQLFieldsContainer.allLabels() = when {
93+
this.isRelationType() -> this.quotedLabel()
94+
else -> (listOf(name) + ((this as? GraphQLObjectType)?.interfaces?.map { it.name } ?: emptyList()))
95+
.map { it.quote() }
96+
.joinToString(":")
8797
}
8898

8999
fun GraphQLFieldsContainer.relevantFields() = fieldDefinitions
@@ -163,6 +173,13 @@ data class RelationshipInfo(
163173
.map { RelatedField("${relFieldName}_${it.name}", it, relType) }
164174
.firstOrNull()
165175
}
176+
177+
fun createRelation(start: Node, end: Node): Relationship =
178+
when (this.out) {
179+
false -> start.relationshipFrom(end, this.relType)
180+
true -> start.relationshipTo(end, this.relType)
181+
null -> start.relationshipBetween(end, this.relType)
182+
}
166183
}
167184

168185
fun Field.aliasOrName() = (this.alias ?: this.name).quote()
@@ -233,4 +250,16 @@ fun paramName(variable: String, argName: String, value: Any?): String = when (va
233250

234251
fun GraphQLFieldDefinition.isID() = this.type.inner() == Scalars.GraphQLID
235252
fun GraphQLFieldDefinition.isNativeId() = this.name == ProjectionBase.NATIVE_ID
236-
fun GraphQLFieldsContainer.getIdField() = this.fieldDefinitions.find { it.isID() }
253+
fun GraphQLFieldsContainer.getIdField() = this.fieldDefinitions.find { it.isID() }
254+
255+
fun GraphQLInputObjectType.Builder.addFilterField(fieldName: String, isList: Boolean, filterType: String, description: String? = null) {
256+
val wrappedType: GraphQLInputType = when {
257+
isList -> GraphQLList(GraphQLTypeReference(filterType))
258+
else -> GraphQLTypeReference(filterType)
259+
}
260+
val inputField = GraphQLInputObjectField.newInputObjectField()
261+
.name(fieldName)
262+
.description(description)
263+
.type(wrappedType)
264+
this.field(inputField)
265+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package org.neo4j.graphql
2+
3+
class OptimizedQueryException(message: String) : Exception(message)

0 commit comments

Comments
 (0)