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 b64f1d6d1 CAY-2906 In-memory evaluation of `(not) exists` expressions
b64f1d6d1 is described below

commit b64f1d6d16dab73c34d3e261d76461796623e16d
Author: Nikita Timofeev <[email protected]>
AuthorDate: Wed Dec 10 18:04:24 2025 +0400

    CAY-2906 In-memory evaluation of `(not) exists` expressions
---
 .../org/apache/cayenne/exp/parser/ASTExists.java   |  28 +++-
 .../org/apache/cayenne/exp/parser/ASTSubquery.java |  26 +++-
 .../org/apache/cayenne/exp/parser/ASTExistsIT.java | 146 +++++++++++++++++++++
 3 files changed, 198 insertions(+), 2 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 733387b8c..ac1dafb85 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
@@ -19,9 +19,13 @@
 
 package org.apache.cayenne.exp.parser;
 
+import org.apache.cayenne.Persistent;
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.ExpressionFactory;
 
+import java.util.Collection;
+import java.util.Map;
+
 /**
  * @since 4.2
  */
@@ -43,7 +47,29 @@ public class ASTExists extends ConditionNode {
 
     @Override
     protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) 
throws Exception {
-        return null;
+        if(evaluatedChildren.length == 0) {
+            return Boolean.FALSE;
+        }
+        return notEmpty(evaluatedChildren[0]);
+    }
+
+    private Boolean notEmpty(Object child) {
+        if(child instanceof Boolean) {
+            return (Boolean)child;
+        }
+        if(child instanceof Collection) {
+            return !((Collection<?>)child).isEmpty();
+        }
+        if(child instanceof Map) {
+            return !((Map<?, ?>)child).isEmpty();
+        }
+        if(child instanceof Object[]) {
+            return ((Object[])child).length > 0;
+        }
+        if(child instanceof Persistent) {
+            return Boolean.TRUE;
+        }
+        return Boolean.FALSE;
     }
 
     @Override
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTSubquery.java 
b/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTSubquery.java
index c2aa5fc9d..a736c5625 100644
--- a/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTSubquery.java
+++ b/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTSubquery.java
@@ -26,13 +26,26 @@ import org.apache.cayenne.Persistent;
 import org.apache.cayenne.access.translator.select.FluentSelectWrapper;
 import org.apache.cayenne.access.translator.select.TranslatableQueryWrapper;
 import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.TraversalHandler;
 import org.apache.cayenne.query.FluentSelect;
+import org.apache.cayenne.query.Ordering;
 
 /**
  * @since 4.2
  */
 public class ASTSubquery extends SimpleNode {
 
+    private static final TraversalHandler IN_MEMORY_VALIDATOR = new 
TraversalHandler() {
+        @Override
+        public void startNode(Expression node, Expression parentNode) {
+            if (node.getType() == Expression.ENCLOSING_OBJECT) {
+                throw new UnsupportedOperationException(
+                        "Can't evaluate subquery expression with enclosing 
object expression."
+                );
+            }
+        }
+    };
+
     private final TranslatableQueryWrapper query;
 
     public ASTSubquery(FluentSelect<?, ?> query) {
@@ -57,10 +70,21 @@ public class ASTSubquery extends SimpleNode {
         } else {
             throw new UnsupportedOperationException("Can't evaluate subquery 
expression against non-persistent object");
         }
-
+        validateForInmemory(query);
         return context.select(query.unwrap());
     }
 
+    /**
+     * Check that we can execute this subquery directly
+     */
+    private void validateForInmemory(TranslatableQueryWrapper query) {
+        query.getQualifier().traverse(IN_MEMORY_VALIDATOR);
+        query.getHavingQualifier().traverse(IN_MEMORY_VALIDATOR);
+        for(Ordering ordering : query.getOrderings()) {
+            ordering.getSortSpec().traverse(IN_MEMORY_VALIDATOR);
+        }
+    }
+
     @Override
     public Expression shallowCopy() {
         return new ASTSubquery(query);
diff --git 
a/cayenne/src/test/java/org/apache/cayenne/exp/parser/ASTExistsIT.java 
b/cayenne/src/test/java/org/apache/cayenne/exp/parser/ASTExistsIT.java
new file mode 100644
index 000000000..c49399b96
--- /dev/null
+++ b/cayenne/src/test/java/org/apache/cayenne/exp/parser/ASTExistsIT.java
@@ -0,0 +1,146 @@
+/*****************************************************************
+ *   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.*;
+
+@UseCayenneRuntime(CayenneProjects.TESTMAP_PROJECT)
+public class ASTExistsIT 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 testEvaluateInMemoryExistsSubquery() {
+        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 testEvaluateInMemoryExistsExpression() {
+        doEvaluateNoQuery(Artist.PAINTING_ARRAY.exists());
+
+        
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE).like("p%").exists());
+
+        
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE).like("not_exists%").exists());
+
+        
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.TO_PAINTING_INFO).exists());
+
+        
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).exists());
+
+        
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).dot(Gallery.GALLERY_NAME).like("g%").exists());
+
+        
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).dot(Gallery.GALLERY_NAME).like("not_exists%").exists());
+
+        
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).dot(Gallery.GALLERY_NAME).like("g%")
+                
.andExp(Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE).like("p%"))
+                .exists());
+
+        
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%"))
+                .exists());
+
+        
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%"))
+                .exists());
+
+        
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%"))
+                .exists());
+
+    }
+
+    private void doEvaluateNoQuery(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())
+                .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())
+                .select(context);
+
+        List<Artist> artistsFiltered = exp.filterObjects(artists);
+        assertEquals(exp.toString(), artistSelected, artistsFiltered);
+    }
+}
\ No newline at end of file

Reply via email to