jdaugherty commented on code in PR #16836:
URL: https://github.com/apache/grails-core/pull/16836#discussion_r4476702248


##########
grails-data-neo4j/core/src/main/groovy/org/grails/datastore/gorm/neo4j/engine/Neo4jQuery.groovy:
##########
@@ -654,6 +662,22 @@ class Neo4jQuery extends Query {
         RelationshipUtils.matchForAssociation(association, var, attributes)
     }
 
+    /**
+     * The pattern that holds when the node has a relationship for the 
association, or {@code null} when the
+     * property is not an association stored as a relationship. An association 
is not a property of the node, so
+     * a null test on it has to look for the relationship and not for a node 
property.
+     */
+    private static String associationRelationshipPattern(GraphPersistentEntity 
entity, String propertyName, String prefix) {
+        if (entity.isRelationshipEntity()) {
+            return null
+        }
+        PersistentProperty property = entity.getPropertyByName(propertyName)

Review Comment:
   `entity` is not always the entity the criterion belongs to. 
`AssociationQueryHandler` dispatches the criteria nested in an association with 
the entity it was given and only switches the prefix, so inside `owner { ... }` 
this lookup runs against the class the query started from. With `Pet { Owner 
owner; Kind kind }` and `Owner { String kind; Address address }`:
   
   - `Pet.createCriteria().list { owner { isNull('kind') } }` returned the pets 
whose owner has no `kind` on 8.0.x (`m_0.kind IS NULL`). Here `kind` resolves 
to the association `Pet.kind`, the condition becomes `NOT 
(m_0)-[:KIND]->(:Kind)`, and every pet with an owner comes back; 
`isNotNull('kind')` returns nothing. A null test on a plain property that 
worked is now wrong whenever the root class has an association of the same name.
   - `owner { isNull('address') }` and `Pet.where { owner.address == null }` 
still render `m_0.address IS NULL` and return every pet with an owner, because 
`Pet` has no `address`. That is #16584 one level down, and this change does not 
reach it.
   
   One way to fix both: in `AssociationQueryHandler`, dispatch the nested 
criteria with `association.associatedEntity` instead of `entity`, keeping 
`entity` when the association targets a relationship entity. With that, both 
queries return the right rows and the Neo4j core suite still passes. Please add 
a feature for each of the two cases to `IsNullAssociationSpec`.



##########
grails-data-neo4j/core/src/main/groovy/org/grails/datastore/gorm/neo4j/engine/Neo4jQuery.groovy:
##########
@@ -359,6 +363,10 @@ class Neo4jQuery extends Query {
         @Override
         @CompileStatic
         CypherExpression handle(GraphPersistentEntity entity, Query.IsNotNull 
criterion, CypherBuilder builder, String prefix) {
+            String relationship = associationRelationshipPattern(entity, 
criterion.property, prefix)
+            if (relationship != null) {
+                return new CypherExpression("${relationship}")

Review Comment:
   Nit: `new CypherExpression(relationship)`. The GString adds nothing.



##########
grails-data-neo4j/core/src/test/groovy/grails/gorm/tests/IsNullAssociationSpec.groovy:
##########
@@ -0,0 +1,103 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+package grails.gorm.tests
+
+import grails.gorm.annotation.Entity
+import org.apache.grails.data.neo4j.core.Neo4jGormDatastoreSpec
+import spock.lang.Issue
+
+/**
+ * An association is stored as a relationship, so isNull and isNotNull on it 
test for the absence and presence of
+ * the relationship and not for a property of the node.
+ */
+@Issue('16584')
+class IsNullAssociationSpec extends Neo4jGormDatastoreSpec {

Review Comment:
   The spec exercises one shape, a unidirectional to-one to the same class. The 
null test now goes through `formatAssociationPatternFromExisting`, and these 
branches have no feature:
   
   - the reversed pattern of a bidirectional many-to-one 
(`(n)<-[:ITEMS]-(:Shelf)`),
   - a to-many,
   - an association to a relationship entity (`(n)<-[]-(:Celeb)` for 
`Movie.cast`),
   - the `Basic` exclusion in `associationRelationshipPattern`.
   
   The first three return the right rows when I run them, so this is missing 
coverage and not a defect. Please add features for them.



##########
grails-data-neo4j/core/src/main/groovy/org/grails/datastore/gorm/neo4j/engine/Neo4jQuery.groovy:
##########
@@ -654,6 +662,22 @@ class Neo4jQuery extends Query {
         RelationshipUtils.matchForAssociation(association, var, attributes)
     }
 
+    /**
+     * The pattern that holds when the node has a relationship for the 
association, or {@code null} when the
+     * property is not an association stored as a relationship. An association 
is not a property of the node, so
+     * a null test on it has to look for the relationship and not for a node 
property.
+     */
+    private static String associationRelationshipPattern(GraphPersistentEntity 
entity, String propertyName, String prefix) {
+        if (entity.isRelationshipEntity()) {
+            return null
+        }

Review Comment:
   No test reaches this branch: the Neo4j core suite passes with these three 
lines removed. Without them `CastMember.createCriteria().list { isNull('from') 
}` fails with `Type mismatch: expected Node but was Relationship`, because the 
pattern starts at the relationship variable (`NOT (r)-[]->(:Movie)`). Please 
add a feature that runs `isNull` and `isNotNull` on `from` and `to` of a 
relationship entity, for example in `RelationshipMappingSpec`.



##########
grails-data-neo4j/core/src/main/groovy/org/grails/datastore/gorm/neo4j/engine/Neo4jQuery.groovy:
##########
@@ -335,6 +335,10 @@ class Neo4jQuery extends Query {
         @Override
         @CompileStatic
         CypherExpression handle(GraphPersistentEntity entity, Query.IsNull 
criterion, CypherBuilder builder, String prefix) {
+            String relationship = associationRelationshipPattern(entity, 
criterion.property, prefix)
+            if (relationship != null) {
+                return new CypherExpression("NOT ${relationship}")

Review Comment:
   This changes the rows existing queries return, so it needs a section in the 
Grails 8 upgrade guide (`grails-doc/src/en/guide/upgrading/upgrading80x.adoc`), 
also when the earlier result was a bug. For GORM for Neo4j the note has to 
cover:
   
   - `isNull` on an association returned every instance and now returns only 
those without the relationship; `isNotNull` returned none and now returns those 
with it.
   - The same holds for `findAllBy<Association>IsNull` / `IsNotNull`, 
`findAllBy<Association>(null)`, the `countBy` forms, and `where { association 
== null }` / `!= null`.
   - On a to-many association, `isNull` now means the collection has no element.
   - `deleteAll()` and `updateAll()` follow the query: `Pet.where { owner == 
null }.deleteAll()` deleted every pet on 8.0.x and now deletes only the pets 
without an owner.
   
   It should also say how to get the earlier rows back, which is to remove the 
criterion.



##########
grails-data-neo4j/core/src/main/groovy/org/grails/datastore/gorm/neo4j/engine/Neo4jQuery.groovy:
##########
@@ -654,6 +662,22 @@ class Neo4jQuery extends Query {
         RelationshipUtils.matchForAssociation(association, var, attributes)
     }
 
+    /**
+     * The pattern that holds when the node has a relationship for the 
association, or {@code null} when the
+     * property is not an association stored as a relationship. An association 
is not a property of the node, so
+     * a null test on it has to look for the relationship and not for a node 
property.
+     */
+    private static String associationRelationshipPattern(GraphPersistentEntity 
entity, String propertyName, String prefix) {
+        if (entity.isRelationshipEntity()) {
+            return null
+        }
+        PersistentProperty property = entity.getPropertyByName(propertyName)
+        if (property instanceof Association && !(property instanceof Basic)) {

Review Comment:
   Not blocking: `Embedded` is an `Association` and not a `Basic`, so a null 
test on an embedded property now renders `NOT (n)-[:HOME]->(:Address)`. An 
embedded component is flattened onto the owner's node and never has a 
relationship, so by the javadoc above this method should return `null` for it. 
The rows are the same as before (all for `isNull`, none for `isNotNull`), so 
nothing regresses, but please add `Embedded` to the exclusion.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to