Skip to content

Commit 0f81665

Browse files
authored
Make use of sub-queries, to get rid of most apoc calls. (#222)
* Make use of sub-queries, to get rid of most apoc calls. resolves #214 * remove unnecessary outer sub-query * cleanup + comment * fix test after merge in master * add optional match for 1..1 relations
1 parent cbdacfe commit 0f81665

31 files changed

+1201
-547
lines changed

core/src/main/kotlin/org/neo4j/graphql/ExtensionFunctions.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.neo4j.graphql
22

33
import graphql.language.Description
44
import graphql.language.VariableReference
5+
import graphql.schema.GraphQLOutputType
56
import org.neo4j.cypherdsl.core.*
67
import java.util.*
78

@@ -17,6 +18,9 @@ fun queryParameter(value: Any?, vararg parts: String?): Parameter<*> {
1718
return org.neo4j.cypherdsl.core.Cypher.parameter(name).withValue(value?.toJavaValue())
1819
}
1920

21+
fun Expression.collect(type: GraphQLOutputType) = if (type.isList()) Functions.collect(this) else this
22+
fun StatementBuilder.OngoingReading.withSubQueries(subQueries: List<Statement>) = subQueries.fold(this, { it, sub -> it.call(sub) })
23+
2024
fun normalizeName(vararg parts: String?) = parts.mapNotNull { it?.capitalize() }.filter { it.isNotBlank() }.joinToString("").decapitalize()
2125
//fun normalizeName(vararg parts: String?) = parts.filterNot { it.isNullOrBlank() }.joinToString("_")
2226

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import org.neo4j.graphql.DirectiveConstants.Companion.PROPERTY_NAME
1616
import org.neo4j.graphql.DirectiveConstants.Companion.RELATION_NAME
1717
import org.neo4j.graphql.DirectiveConstants.Companion.RELATION_TO
1818
import org.neo4j.graphql.handler.projection.ProjectionBase
19+
import org.slf4j.LoggerFactory
1920
import java.math.BigDecimal
2021
import java.math.BigInteger
2122

@@ -157,11 +158,17 @@ fun <T> GraphQLDirective.getArgument(argumentName: String, defaultValue: T? = nu
157158
}
158159

159160
fun GraphQLFieldDefinition.cypherDirective(): CypherDirective? = getDirective(CYPHER)?.let {
160-
CypherDirective(
161-
it.getMandatoryArgument(CYPHER_STATEMENT),
162-
it.getMandatoryArgument(CYPHER_PASS_THROUGH, false)
163-
)
164-
}
161+
val originalStatement = it.getMandatoryArgument<String>(CYPHER_STATEMENT)
162+
// Arguments on the field are passed to the Cypher statement and can be used by name.
163+
// They must not be prefixed by $ since they are no longer parameters. Just use the same name as the fields' argument.
164+
val rewrittenStatement = originalStatement.replace(Regex("\\\$([_a-zA-Z]\\w*)"), "$1")
165+
if (originalStatement != rewrittenStatement) {
166+
LoggerFactory.getLogger(CypherDirective::class.java)
167+
.warn("The field arguments used in the directives statement must not contain parameters. The statement was replaced. Please adjust your GraphQl Schema.\n\tGot : {}\n\tReplaced by: {}\n\tField : {} ({})",
168+
originalStatement, rewrittenStatement, this.name, this.definition?.sourceLocation)
169+
}
170+
CypherDirective(rewrittenStatement, it.getMandatoryArgument(CYPHER_PASS_THROUGH, false))
171+
}
165172

166173
data class CypherDirective(val statement: String, val passThrough: Boolean)
167174

core/src/main/kotlin/org/neo4j/graphql/handler/CreateTypeHandler.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import graphql.schema.DataFetchingEnvironment
99
import graphql.schema.GraphQLObjectType
1010
import graphql.schema.idl.TypeDefinitionRegistry
1111
import org.neo4j.cypherdsl.core.Statement
12-
import org.neo4j.cypherdsl.core.StatementBuilder
1312
import org.neo4j.graphql.*
1413

1514
/**
@@ -86,11 +85,11 @@ class CreateTypeHandler private constructor(schemaConfig: SchemaConfig) : BaseDa
8685
val node = org.neo4j.cypherdsl.core.Cypher.node(type.name, *additionalTypes.toTypedArray()).named(variable)
8786

8887
val properties = properties(variable, field.arguments)
89-
val mapProjection = projectFields(node, field, type, env)
88+
val (mapProjection, subQueries) = projectFields(node, field, type, env)
9089

91-
val update: StatementBuilder.OngoingUpdate = org.neo4j.cypherdsl.core.Cypher.create(node.withProperties(*properties))
92-
return update
90+
return org.neo4j.cypherdsl.core.Cypher.create(node.withProperties(*properties))
9391
.with(node)
92+
.withSubQueries(subQueries)
9493
.returning(node.project(mapProjection).`as`(field.aliasOrName()))
9594
.build()
9695
}

core/src/main/kotlin/org/neo4j/graphql/handler/CypherDirectiveHandler.kt

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@ import graphql.schema.DataFetcher
66
import graphql.schema.DataFetchingEnvironment
77
import graphql.schema.GraphQLFieldsContainer
88
import graphql.schema.idl.TypeDefinitionRegistry
9-
import org.neo4j.cypherdsl.core.Functions
109
import org.neo4j.cypherdsl.core.Statement
1110
import org.neo4j.graphql.*
1211

1312
/**
1413
* This class handles all logic related to custom Cypher queries declared by fields with a @cypher directive
1514
*/
16-
class CypherDirectiveHandler(private val isQuery: Boolean, schemaConfig: SchemaConfig) : BaseDataFetcher(schemaConfig) {
15+
class CypherDirectiveHandler(schemaConfig: SchemaConfig) : BaseDataFetcher(schemaConfig) {
1716

1817
class Factory(schemaConfig: SchemaConfig,
1918
typeDefinitionRegistry: TypeDefinitionRegistry,
@@ -22,8 +21,7 @@ class CypherDirectiveHandler(private val isQuery: Boolean, schemaConfig: SchemaC
2221

2322
override fun createDataFetcher(operationType: OperationType, fieldDefinition: FieldDefinition): DataFetcher<Cypher>? {
2423
fieldDefinition.cypherDirective() ?: return null
25-
val isQuery = operationType == OperationType.QUERY
26-
return CypherDirectiveHandler(isQuery, schemaConfig)
24+
return CypherDirectiveHandler(schemaConfig)
2725
}
2826
}
2927

@@ -33,31 +31,21 @@ class CypherDirectiveHandler(private val isQuery: Boolean, schemaConfig: SchemaC
3331
val cypherDirective = fieldDefinition.cypherDirective()
3432
?: throw IllegalStateException("Expect field ${env.logField()} to have @cypher directive present")
3533

36-
val query = if (isQuery) {
37-
val nestedQuery = cypherDirective(variable, fieldDefinition, field, cypherDirective)
38-
org.neo4j.cypherdsl.core.Cypher.unwind(nestedQuery).`as`(variable)
39-
} else {
40-
val args = cypherDirectiveQuery(variable, fieldDefinition, field, cypherDirective)
41-
42-
val value = org.neo4j.cypherdsl.core.Cypher.name("value")
43-
org.neo4j.cypherdsl.core.Cypher.call("apoc.cypher.doIt")
44-
.withArgs(*args)
45-
.yield(value)
46-
.with(org.neo4j.cypherdsl.core.Cypher.property(value, Functions.head(org.neo4j.cypherdsl.core.Cypher.call("keys").withArgs(value).asFunction())).`as`(variable))
47-
}
4834
val node = org.neo4j.cypherdsl.core.Cypher.anyNode(variable)
49-
val readingWithWhere = if (type != null && !cypherDirective.passThrough) {
50-
val projectionEntries = projectFields(node, field, type, env)
51-
query.returning(node.project(projectionEntries).`as`(field.aliasOrName()))
52-
} else {
53-
query.returning(node.`as`(field.aliasOrName()))
54-
}
55-
val ordering = orderBy(node, field.arguments, fieldDefinition, env.variables)
56-
val skipLimit = SkipLimit(variable, field.arguments, fieldDefinition)
57-
58-
val resultWithSkipLimit = readingWithWhere
59-
.let { if (ordering != null) skipLimit.format(it.orderBy(*ordering.toTypedArray())) else skipLimit.format(it) }
60-
61-
return resultWithSkipLimit.build()
35+
val ctxVariable = node.requiredSymbolicName
36+
val nestedQuery = cypherDirective(ctxVariable, fieldDefinition, field, cypherDirective, null, env)
37+
38+
return org.neo4j.cypherdsl.core.Cypher.call(nestedQuery)
39+
.let { reading ->
40+
if (type == null || cypherDirective.passThrough) {
41+
reading.returning(ctxVariable.`as`(field.aliasOrName()))
42+
} else {
43+
val (fieldProjection, nestedSubQueries) = projectFields(node, field, type, env)
44+
reading
45+
.withSubQueries(nestedSubQueries)
46+
.returning(ctxVariable.project(fieldProjection).`as`(field.aliasOrName()))
47+
}
48+
}
49+
.build()
6250
}
6351
}

core/src/main/kotlin/org/neo4j/graphql/handler/DeleteHandler.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import graphql.schema.idl.TypeDefinitionRegistry
1212
import org.neo4j.cypherdsl.core.Node
1313
import org.neo4j.cypherdsl.core.Relationship
1414
import org.neo4j.cypherdsl.core.Statement
15-
import org.neo4j.cypherdsl.core.StatementBuilder.OngoingUpdate
1615
import org.neo4j.graphql.*
1716

1817
/**
@@ -91,12 +90,13 @@ class DeleteHandler private constructor(schemaConfig: SchemaConfig) : BaseDataFe
9190
.where(where)
9291
}
9392
val deletedElement = propertyContainer.requiredSymbolicName.`as`("toDelete")
94-
val mapProjection = projectFields(propertyContainer, field, type, env)
93+
val (mapProjection, subQueries) = projectFields(propertyContainer, field, type, env)
9594

9695
val projection = propertyContainer.project(mapProjection).`as`(variable)
97-
val update: OngoingUpdate = select.with(deletedElement, projection)
96+
return select
97+
.withSubQueries(subQueries)
98+
.with(deletedElement, projection)
9899
.detachDelete(deletedElement)
99-
return update
100100
.returning(projection.asName().`as`(field.aliasOrName()))
101101
.build()
102102
}

core/src/main/kotlin/org/neo4j/graphql/handler/MergeOrUpdateHandler.kt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import graphql.schema.idl.TypeDefinitionRegistry
1111
import org.neo4j.cypherdsl.core.Node
1212
import org.neo4j.cypherdsl.core.Relationship
1313
import org.neo4j.cypherdsl.core.Statement
14-
import org.neo4j.cypherdsl.core.StatementBuilder.OngoingMatchAndUpdate
1514
import org.neo4j.graphql.*
1615

1716
/**
@@ -114,12 +113,12 @@ class MergeOrUpdateHandler private constructor(private val merge: Boolean, schem
114113
}
115114
}
116115
val properties = properties(variable, field.arguments)
117-
val mapProjection = projectFields(propertyContainer, field, type, env)
118-
val update: OngoingMatchAndUpdate = select
119-
.mutate(propertyContainer, org.neo4j.cypherdsl.core.Cypher.mapOf(*properties))
116+
val (mapProjection, subQueries) = projectFields(propertyContainer, field, type, env)
120117

121-
return update
118+
return select
119+
.mutate(propertyContainer, org.neo4j.cypherdsl.core.Cypher.mapOf(*properties))
122120
.with(propertyContainer)
121+
.withSubQueries(subQueries)
123122
.returning(propertyContainer.project(mapProjection).`as`(field.aliasOrName()))
124123
.build()
125124
}

core/src/main/kotlin/org/neo4j/graphql/handler/QueryHandler.kt

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -121,17 +121,12 @@ class QueryHandler private constructor(schemaConfig: SchemaConfig) : BaseDataFet
121121
match.where(where)
122122
}
123123

124-
val ordering = orderBy(propertyContainer, field.arguments, fieldDefinition, env.variables)
125-
val skipLimit = SkipLimit(variable, field.arguments, fieldDefinition)
126-
127-
val projectionEntries = projectFields(propertyContainer, field, type, env)
124+
val (projectionEntries, subQueries) = projectFields(propertyContainer, field, type, env)
128125
val mapProjection = propertyContainer.project(projectionEntries).`as`(field.aliasOrName())
129-
val resultWithSkipLimit = ongoingReading.returning(mapProjection)
130-
.let {
131-
val orderedResult = ordering?.let { o -> it.orderBy(*o.toTypedArray()) } ?: it
132-
skipLimit.format(orderedResult)
133-
}
134-
135-
return resultWithSkipLimit.build()
126+
return ongoingReading
127+
.withSubQueries(subQueries)
128+
.returning(mapProjection)
129+
.skipLimitOrder(propertyContainer.requiredSymbolicName, fieldDefinition, field, env)
130+
.build()
136131
}
137132
}

0 commit comments

Comments
 (0)