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