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