Skip to content

Commit 2021d1a

Browse files
authored
Support filtering of null values also via variables (#178)
resolves #47
1 parent af9b2fa commit 2021d1a

File tree

6 files changed

+594
-29
lines changed

6 files changed

+594
-29
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ class QueryHandler private constructor(
9292

9393
val ongoingReading = if ((env.getContext() as? QueryContext)?.optimizedQuery?.contains(QueryContext.OptimizationStrategy.FILTER_AS_MATCH) == true) {
9494

95-
OptimizedFilterHandler(type).generateFilterQuery(variable, fieldDefinition, field, match, propertyContainer)
95+
OptimizedFilterHandler(type).generateFilterQuery(variable, fieldDefinition, field, match, propertyContainer, env.variables)
9696

9797
} else {
9898

core/src/main/kotlin/org/neo4j/graphql/handler/filter/OptimizedFilterHandler.kt

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ typealias ConditionBuilder = (ExposesWith) -> OrderableOngoingReadingAndWithWith
3434
*/
3535
class OptimizedFilterHandler(val type: GraphQLFieldsContainer) : ProjectionBase() {
3636

37-
fun generateFilterQuery(variable: String, fieldDefinition: GraphQLFieldDefinition, field: Field, readingWithoutWhere: OngoingReadingWithoutWhere, rootNode: PropertyContainer): OngoingReading {
37+
fun generateFilterQuery(variable: String, fieldDefinition: GraphQLFieldDefinition, field: Field, readingWithoutWhere: OngoingReadingWithoutWhere, rootNode: PropertyContainer, variables: Map<String, Any>): OngoingReading {
3838
if (type.isRelationType()) {
3939
throw OptimizedQueryException("Optimization for relationship entity type is not implemented. Please provide a test case to help adding further cases.")
4040
}
@@ -43,16 +43,16 @@ class OptimizedFilterHandler(val type: GraphQLFieldsContainer) : ProjectionBase(
4343

4444
val filteredArguments = field.arguments.filterNot { setOf(FIRST, OFFSET, ORDER_BY, FILTER).contains(it.name) }
4545
if (filteredArguments.isNotEmpty()) {
46-
val parsedQuery = QueryParser.parseArguments(filteredArguments, fieldDefinition, type)
47-
val condition = handleQuery(variable, "", rootNode, parsedQuery, type)
46+
val parsedQuery = QueryParser.parseArguments(filteredArguments, fieldDefinition, type, variables)
47+
val condition = handleQuery(variable, "", rootNode, parsedQuery, type, variables)
4848
ongoingReading = readingWithoutWhere.where(condition)
4949
}
5050
for (argument in field.arguments) {
5151
if (argument.name == FILTER) {
52-
val parsedQuery = parseFilter(argument.value as ObjectValue, type)
52+
val parsedQuery = parseFilter(argument.value as ObjectValue, type, variables)
5353
ongoingReading = NestingLevelHandler(parsedQuery, false, rootNode, variable, ongoingReading
5454
?: readingWithoutWhere,
55-
type, argument.value, linkedSetOf(rootNode.requiredSymbolicName))
55+
type, argument.value, linkedSetOf(rootNode.requiredSymbolicName), variables)
5656
.parseFilter()
5757
}
5858
}
@@ -77,7 +77,8 @@ class OptimizedFilterHandler(val type: GraphQLFieldsContainer) : ProjectionBase(
7777
private val matchQueryWithoutWhere: OngoingReading,
7878
private val type: GraphQLFieldsContainer,
7979
private val value: Value<*>?,
80-
private val parentPassThroughWiths: Collection<Expression>
80+
private val parentPassThroughWiths: Collection<Expression>,
81+
private val variables: Map<String, Any>
8182
) {
8283

8384
private fun currentNode() = current as? Node
@@ -164,7 +165,7 @@ class OptimizedFilterHandler(val type: GraphQLFieldsContainer) : ProjectionBase(
164165
levelPassThroughWiths: LinkedHashSet<Expression>
165166
): OrderableOngoingReadingAndWithWithoutWhere {
166167
val objectField = relFilter.queryField
167-
val nestedParsedQuery = parseFilter(objectField.value as ObjectValue, relFilter.fieldDefinition.type.getInnerFieldsContainer())
168+
val nestedParsedQuery = parseFilter(objectField.value as ObjectValue, relFilter.fieldDefinition.type.getInnerFieldsContainer(), variables)
168169
val hasPredicates = nestedParsedQuery.fieldPredicates.isNotEmpty() || nestedParsedQuery.relationPredicates.isNotEmpty()
169170

170171
var queryWithoutWhere = query
@@ -185,7 +186,7 @@ class OptimizedFilterHandler(val type: GraphQLFieldsContainer) : ProjectionBase(
185186
}
186187

187188
val nestingLevelHandler = NestingLevelHandler(nestedParsedQuery, true, relVariable, relVariableName,
188-
readingWithoutWhere, relFilter.relationshipInfo.type, objectField.value, levelPassThroughWiths)
189+
readingWithoutWhere, relFilter.relationshipInfo.type, objectField.value, levelPassThroughWiths, variables)
189190

190191
when (relFilter.op) {
191192
RelationOperator.SOME -> queryWithoutWhere = nestingLevelHandler.parseFilter()

core/src/main/kotlin/org/neo4j/graphql/handler/projection/ProjectionBase.kt

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ open class ProjectionBase {
7272

7373
val filteredArguments = field.arguments.filterNot { setOf(FIRST, OFFSET, ORDER_BY, FILTER).contains(it.name) }
7474

75-
val parsedQuery = parseArguments(filteredArguments, fieldDefinition, type)
76-
val result = handleQuery(variable, "", propertyContainer, parsedQuery, type)
75+
val parsedQuery = parseArguments(filteredArguments, fieldDefinition, type, variables)
76+
val result = handleQuery(variable, "", propertyContainer, parsedQuery, type, variables)
7777

7878
return field.arguments.find { FILTER == it.name }
7979
?.let { arg ->
@@ -83,9 +83,9 @@ open class ProjectionBase {
8383
else -> throw IllegalArgumentException("")
8484
}
8585
}
86-
?.let { parseFilter(it as ObjectValue, type) }
86+
?.let { parseFilter(it as ObjectValue, type, variables) }
8787
?.let {
88-
val filterCondition = handleQuery(normalizeName(FILTER, variable), "", propertyContainer, it, type)
88+
val filterCondition = handleQuery(normalizeName(FILTER, variable), "", propertyContainer, it, type, variables)
8989
result.and(filterCondition)
9090
}
9191
?: result
@@ -96,7 +96,8 @@ open class ProjectionBase {
9696
variableSuffix: String,
9797
propertyContainer: PropertyContainer,
9898
parsedQuery: ParsedQuery,
99-
type: GraphQLFieldsContainer
99+
type: GraphQLFieldsContainer,
100+
variables: Map<String, Any>
100101
): Condition {
101102
var result = parsedQuery.getFieldConditions(propertyContainer, variablePrefix, variableSuffix)
102103

@@ -122,8 +123,8 @@ open class ProjectionBase {
122123
else -> null
123124
}?.let {
124125
val targetNode = predicate.relNode.named(normalizeName(variablePrefix, predicate.relationshipInfo.typeName))
125-
val parsedQuery2 = parseFilter(objectField.value as ObjectValue, type)
126-
val condition = handleQuery(targetNode.requiredSymbolicName.value, "", targetNode, parsedQuery2, type)
126+
val parsedQuery2 = parseFilter(objectField.value as ObjectValue, type, variables)
127+
val condition = handleQuery(targetNode.requiredSymbolicName.value, "", targetNode, parsedQuery2, type, variables)
127128
var where = it
128129
.`in`(listBasedOn(predicate.relationshipInfo.createRelation(propertyContainer as Node, targetNode)).returning(condition))
129130
.where(cond.asCondition())
@@ -135,19 +136,19 @@ open class ProjectionBase {
135136
}
136137
}
137138

138-
fun handleLogicalOperator(value: Value<*>, classifier: String): Condition {
139+
fun handleLogicalOperator(value: Value<*>, classifier: String, variables: Map<String, Any>): Condition {
139140
val objectValue = value as? ObjectValue
140141
?: throw IllegalArgumentException("Only object values are supported for logical operations, but got ${value.javaClass.name}")
141142

142-
val parsedNestedQuery = parseFilter(objectValue, type)
143-
return handleQuery(variablePrefix + classifier, variableSuffix, propertyContainer, parsedNestedQuery, type)
143+
val parsedNestedQuery = parseFilter(objectValue, type, variables)
144+
return handleQuery(variablePrefix + classifier, variableSuffix, propertyContainer, parsedNestedQuery, type, variables)
144145
}
145146

146147
fun handleLogicalOperators(values: List<Value<*>>?, classifier: String): List<Condition> {
147148
return when {
148149
values?.isNotEmpty() == true -> when {
149-
values.size > 1 -> values.mapIndexed { index, value -> handleLogicalOperator(value, "${classifier}${index + 1}") }
150-
else -> values.map { value -> handleLogicalOperator(value, "") }
150+
values.size > 1 -> values.mapIndexed { index, value -> handleLogicalOperator(value, "${classifier}${index + 1}", variables) }
151+
else -> values.map { value -> handleLogicalOperator(value, "", variables) }
151152
}
152153
else -> emptyList()
153154
}

core/src/main/kotlin/org/neo4j/graphql/parser/QueryParser.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ object QueryParser {
9191
/**
9292
* This parser takes an filter object an transform it to the internal [ParsedQuery]-representation
9393
*/
94-
fun parseFilter(objectValue: ObjectValue, type: GraphQLFieldsContainer): ParsedQuery {
94+
fun parseFilter(objectValue: ObjectValue, type: GraphQLFieldsContainer, variables: Map<String, Any>): ParsedQuery {
9595
// Map of all queried fields
9696
// we remove all matching fields from this map, so we can ensure that only known fields got queried
9797
val queriedFields = objectValue.objectFields
@@ -108,13 +108,13 @@ object QueryParser {
108108
?: throw IllegalArgumentException("AND on type `${type.name}` is expected to be a list")
109109
}
110110

111-
return createParsedQuery(queriedFields, type, null, or, and)
111+
return createParsedQuery(queriedFields, type, variables, null, or, and)
112112
}
113113

114114
/**
115115
* This parser takes all non-filter arguments of a graphql-field an transform it to the internal [ParsedQuery]-representation
116116
*/
117-
fun parseArguments(arguments: List<Argument>, fieldDefinition: GraphQLFieldDefinition, type: GraphQLFieldsContainer): ParsedQuery {
117+
fun parseArguments(arguments: List<Argument>, fieldDefinition: GraphQLFieldDefinition, type: GraphQLFieldsContainer, variables: Map<String, Any>): ParsedQuery {
118118
// TODO we should check if the argument is defined on the field definition and throw an error otherwise
119119
// Map of all queried fields
120120
// we remove all matching fields from this map, so we can ensure that only known fields got queried
@@ -130,13 +130,14 @@ object QueryParser {
130130
queriedFields[argument.name] = index++ to ObjectField(argument.name, argument.defaultValue.asGraphQLValue())
131131
}
132132

133-
return createParsedQuery(queriedFields, type, fieldDefinition)
133+
return createParsedQuery(queriedFields, type, variables, fieldDefinition)
134134
}
135135

136136

137137
private fun createParsedQuery(
138138
queriedFields: MutableMap<String, Pair<Int, ObjectField>>,
139139
type: GraphQLFieldsContainer,
140+
variables: Map<String, Any>,
140141
fieldDefinition: GraphQLFieldDefinition? = null,
141142
or: List<Value<Value<*>>>? = null,
142143
and: List<Value<Value<*>>>? = null
@@ -160,7 +161,7 @@ object QueryParser {
160161
.map { it to definedField.name + it.suffix }
161162
.mapNotNull { (predicate, queryFieldName) ->
162163
queriedFields[queryFieldName]?.let { (index, objectField) ->
163-
if (predicate.requireParam xor (objectField.value !is NullValue)) {
164+
if (predicate.requireParam xor (!objectField.value.isNullValue(variables))) {
164165
// if we got a value but the predicate requires none
165166
// or we got a no value but the predicate requires one
166167
// we skip this operator
@@ -187,5 +188,8 @@ object QueryParser {
187188
}
188189
}
189190

191+
private fun Value<*>.isNullValue(variables: Map<String, Any>): Boolean =
192+
this is NullValue || (this is VariableReference && variables[this.name] == null)
193+
190194

191195

0 commit comments

Comments
 (0)