This is an automated email from the ASF dual-hosted git repository.

ntimofeev pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git


The following commit(s) were added to refs/heads/master by this push:
     new 470f96079 CAY-2906 In-memory evaluation of `(not) exists` expressions
470f96079 is described below

commit 470f960797d3f2c72e1504a012234a43a00b3855
Author: Nikita Timofeev <[email protected]>
AuthorDate: Mon Feb 2 13:03:53 2026 +0400

    CAY-2906 In-memory evaluation of `(not) exists` expressions
---
 .../org/apache/cayenne/exp/parser/ASTExists.java   |  14 +-
 .../apache/cayenne/exp/parser/ASTNotExists.java    |  11 +-
 .../apache/cayenne/exp/parser/ASTNotExistsIT.java  | 152 +++++++++++++++++++++
 3 files changed, 172 insertions(+), 5 deletions(-)

diff --git a/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTExists.java 
b/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTExists.java
index ac1dafb85..ffacd9f7a 100644
--- a/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTExists.java
+++ b/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTExists.java
@@ -46,14 +46,20 @@ public class ASTExists extends ConditionNode {
     }
 
     @Override
-    protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) 
throws Exception {
-        if(evaluatedChildren.length == 0) {
+    protected Object evaluateNode(Object o) throws Exception {
+        if (jjtGetNumChildren() != 1) {
             return Boolean.FALSE;
         }
-        return notEmpty(evaluatedChildren[0]);
+        Object firstChild = evaluateChild(0, o);
+        return evaluateSubNode(firstChild, null);
+    }
+
+    @Override
+    protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) 
throws Exception {
+        return notEmpty(o);
     }
 
-    private Boolean notEmpty(Object child) {
+    static Boolean notEmpty(Object child) {
         if(child instanceof Boolean) {
             return (Boolean)child;
         }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTNotExists.java 
b/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTNotExists.java
index 490561182..7348d0c14 100644
--- a/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTNotExists.java
+++ b/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTNotExists.java
@@ -41,9 +41,18 @@ public class ASTNotExists extends ConditionNode {
         return 1;
     }
 
+    @Override
+    protected Object evaluateNode(Object o) throws Exception {
+        if (jjtGetNumChildren() != 1) {
+            return Boolean.FALSE;
+        }
+        Object firstChild = evaluateChild(0, o);
+        return evaluateSubNode(firstChild, null);
+    }
+
     @Override
     protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) 
throws Exception {
-        return null;
+        return !ASTExists.notEmpty(o);
     }
 
     @Override
diff --git 
a/cayenne/src/test/java/org/apache/cayenne/exp/parser/ASTNotExistsIT.java 
b/cayenne/src/test/java/org/apache/cayenne/exp/parser/ASTNotExistsIT.java
new file mode 100644
index 000000000..9b1c1f20b
--- /dev/null
+++ b/cayenne/src/test/java/org/apache/cayenne/exp/parser/ASTNotExistsIT.java
@@ -0,0 +1,152 @@
+/*****************************************************************
+ *   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 org.apache.cayenne.exp.parser;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionException;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.test.jdbc.DBHelper;
+import org.apache.cayenne.test.jdbc.TableHelper;
+import org.apache.cayenne.testdo.testmap.Artist;
+import org.apache.cayenne.testdo.testmap.Gallery;
+import org.apache.cayenne.testdo.testmap.Painting;
+import org.apache.cayenne.unit.di.DataChannelInterceptor;
+import org.apache.cayenne.unit.di.runtime.CayenneProjects;
+import org.apache.cayenne.unit.di.runtime.RuntimeCase;
+import org.apache.cayenne.unit.di.runtime.UseCayenneRuntime;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+@UseCayenneRuntime(CayenneProjects.TESTMAP_PROJECT)
+public class ASTNotExistsIT extends RuntimeCase {
+
+    @Inject
+    private ObjectContext context;
+
+    @Inject
+    private DBHelper dbHelper;
+
+    @Inject
+    private DataChannelInterceptor queryInterceptor;
+
+    @Before
+    public void createArtistsDataSet() throws Exception {
+        TableHelper tArtist = new TableHelper(dbHelper, "ARTIST");
+        tArtist.setColumns("ARTIST_ID", "ARTIST_NAME", "DATE_OF_BIRTH");
+
+        long dateBase = System.currentTimeMillis();
+        for (int i = 1; i <= 20; i++) {
+            tArtist.insert(i, "artist" + i, new java.sql.Date(dateBase + 10000 
* i));
+        }
+
+        TableHelper tGallery = new TableHelper(dbHelper, "GALLERY");
+        tGallery.setColumns("GALLERY_ID", "GALLERY_NAME");
+        tGallery.insert(1, "tate modern");
+
+        TableHelper tPaintings = new TableHelper(dbHelper, "PAINTING");
+        tPaintings.setColumns("PAINTING_ID", "PAINTING_TITLE", "ARTIST_ID", 
"GALLERY_ID");
+        for (int i = 1; i <= 20; i++) {
+            tPaintings.insert(i, "painting" + i, i % 5 + 1, 1);
+        }
+    }
+
+    @Test(expected = ExpressionException.class)
+    public void testEvaluateInMemoryNotExistsSubquery() {
+        ObjectSelect<Painting> subQuery = ObjectSelect.query(Painting.class)
+                
.where(Painting.TO_ARTIST.eq(Artist.ARTIST_ID_PK_PROPERTY.enclosing()));
+
+        doEvaluateWithQuery(ExpressionFactory.notExists(subQuery));
+    }
+
+    @Test
+    public void testEvaluateInMemoryNotExistsExpression() {
+//        doEvaluateNoQuery(Artist.PAINTING_ARRAY.notExists());
+
+        
doEvaluateNoQuery(Artist.ARTIST_ID_PK_PROPERTY.eq(6L).andExp(Artist.PAINTING_ARRAY.notExists()));
+
+        
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE).like("p%").notExists());
+
+        
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE).like("not_exists%").notExists());
+
+        
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.TO_PAINTING_INFO).notExists());
+
+        
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).notExists());
+
+        
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).dot(Gallery.GALLERY_NAME).like("g%").notExists());
+
+        
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).dot(Gallery.GALLERY_NAME).like("not_exists%").notExists());
+
+        
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).dot(Gallery.GALLERY_NAME).like("g%")
+                
.andExp(Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE).like("p%"))
+                .notExists());
+
+        
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).dot(Gallery.GALLERY_NAME).like("not_exists%")
+                
.andExp(Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE).like("p%"))
+                .notExists());
+
+        
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).dot(Gallery.GALLERY_NAME).like("g%")
+                
.andExp(Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE).like("not_exists%"))
+                .notExists());
+
+        
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).dot(Gallery.GALLERY_NAME).like("not_exists%")
+                
.andExp(Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE).like("not_exists%"))
+                .notExists());
+
+    }
+
+    private void doEvaluateNoQuery(Expression exp) {
+        List<Artist> artistSelected = ObjectSelect.query(Artist.class, exp)
+                .orderBy(Artist.ARTIST_ID_PK_PROPERTY.asc())
+                .select(context);
+
+        List<Artist> artists = ObjectSelect.query(Artist.class)
+                .prefetch(Artist.PAINTING_ARRAY.outer().disjoint())
+                
.prefetch(Artist.PAINTING_ARRAY.outer().dot(Painting.TO_PAINTING_INFO).disjoint())
+                
.prefetch(Artist.PAINTING_ARRAY.outer().dot(Painting.TO_GALLERY).disjoint())
+                .orderBy(Artist.ARTIST_ID_PK_PROPERTY.asc())
+                .select(context);
+
+        queryInterceptor.runWithQueriesBlocked(() -> {
+            List<Artist> artistsFiltered = exp.filterObjects(artists);
+            assertEquals(exp.toString(), artistSelected, artistsFiltered);
+        });
+    }
+
+    private void doEvaluateWithQuery(Expression exp) {
+        List<Artist> artistSelected = ObjectSelect.query(Artist.class, 
exp).select(context);
+
+        List<Artist> artists = ObjectSelect.query(Artist.class)
+                .prefetch(Artist.PAINTING_ARRAY.disjoint())
+                
.prefetch(Artist.PAINTING_ARRAY.dot(Painting.TO_PAINTING_INFO).disjoint())
+                
.prefetch(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).disjoint())
+                .orderBy(Artist.ARTIST_ID_PK_PROPERTY.asc())
+                .select(context);
+
+        List<Artist> artistsFiltered = exp.filterObjects(artists);
+        assertEquals(exp.toString(), artistSelected, artistsFiltered);
+    }
+}
\ No newline at end of file

Reply via email to