Skip to content

Commit b64f1d6

Browse files
committed
CAY-2906 In-memory evaluation of (not) exists expressions
1 parent 184ab0a commit b64f1d6

File tree

3 files changed

+198
-2
lines changed

3 files changed

+198
-2
lines changed

cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTExists.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@
1919

2020
package org.apache.cayenne.exp.parser;
2121

22+
import org.apache.cayenne.Persistent;
2223
import org.apache.cayenne.exp.Expression;
2324
import org.apache.cayenne.exp.ExpressionFactory;
2425

26+
import java.util.Collection;
27+
import java.util.Map;
28+
2529
/**
2630
* @since 4.2
2731
*/
@@ -43,7 +47,29 @@ protected int getRequiredChildrenCount() {
4347

4448
@Override
4549
protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
46-
return null;
50+
if(evaluatedChildren.length == 0) {
51+
return Boolean.FALSE;
52+
}
53+
return notEmpty(evaluatedChildren[0]);
54+
}
55+
56+
private Boolean notEmpty(Object child) {
57+
if(child instanceof Boolean) {
58+
return (Boolean)child;
59+
}
60+
if(child instanceof Collection) {
61+
return !((Collection<?>)child).isEmpty();
62+
}
63+
if(child instanceof Map) {
64+
return !((Map<?, ?>)child).isEmpty();
65+
}
66+
if(child instanceof Object[]) {
67+
return ((Object[])child).length > 0;
68+
}
69+
if(child instanceof Persistent) {
70+
return Boolean.TRUE;
71+
}
72+
return Boolean.FALSE;
4773
}
4874

4975
@Override

cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTSubquery.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,26 @@
2626
import org.apache.cayenne.access.translator.select.FluentSelectWrapper;
2727
import org.apache.cayenne.access.translator.select.TranslatableQueryWrapper;
2828
import org.apache.cayenne.exp.Expression;
29+
import org.apache.cayenne.exp.TraversalHandler;
2930
import org.apache.cayenne.query.FluentSelect;
31+
import org.apache.cayenne.query.Ordering;
3032

3133
/**
3234
* @since 4.2
3335
*/
3436
public class ASTSubquery extends SimpleNode {
3537

38+
private static final TraversalHandler IN_MEMORY_VALIDATOR = new TraversalHandler() {
39+
@Override
40+
public void startNode(Expression node, Expression parentNode) {
41+
if (node.getType() == Expression.ENCLOSING_OBJECT) {
42+
throw new UnsupportedOperationException(
43+
"Can't evaluate subquery expression with enclosing object expression."
44+
);
45+
}
46+
}
47+
};
48+
3649
private final TranslatableQueryWrapper query;
3750

3851
public ASTSubquery(FluentSelect<?, ?> query) {
@@ -57,10 +70,21 @@ protected Object evaluateNode(Object o) {
5770
} else {
5871
throw new UnsupportedOperationException("Can't evaluate subquery expression against non-persistent object");
5972
}
60-
73+
validateForInmemory(query);
6174
return context.select(query.unwrap());
6275
}
6376

77+
/**
78+
* Check that we can execute this subquery directly
79+
*/
80+
private void validateForInmemory(TranslatableQueryWrapper query) {
81+
query.getQualifier().traverse(IN_MEMORY_VALIDATOR);
82+
query.getHavingQualifier().traverse(IN_MEMORY_VALIDATOR);
83+
for(Ordering ordering : query.getOrderings()) {
84+
ordering.getSortSpec().traverse(IN_MEMORY_VALIDATOR);
85+
}
86+
}
87+
6488
@Override
6589
public Expression shallowCopy() {
6690
return new ASTSubquery(query);
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*****************************************************************
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
****************************************************************/
19+
20+
package org.apache.cayenne.exp.parser;
21+
22+
import org.apache.cayenne.ObjectContext;
23+
import org.apache.cayenne.di.Inject;
24+
import org.apache.cayenne.exp.Expression;
25+
import org.apache.cayenne.exp.ExpressionException;
26+
import org.apache.cayenne.exp.ExpressionFactory;
27+
import org.apache.cayenne.query.ObjectSelect;
28+
import org.apache.cayenne.test.jdbc.DBHelper;
29+
import org.apache.cayenne.test.jdbc.TableHelper;
30+
import org.apache.cayenne.testdo.testmap.Artist;
31+
import org.apache.cayenne.testdo.testmap.Gallery;
32+
import org.apache.cayenne.testdo.testmap.Painting;
33+
import org.apache.cayenne.unit.di.DataChannelInterceptor;
34+
import org.apache.cayenne.unit.di.runtime.CayenneProjects;
35+
import org.apache.cayenne.unit.di.runtime.RuntimeCase;
36+
import org.apache.cayenne.unit.di.runtime.UseCayenneRuntime;
37+
import org.junit.Before;
38+
import org.junit.Test;
39+
40+
import java.util.List;
41+
42+
import static org.junit.Assert.*;
43+
44+
@UseCayenneRuntime(CayenneProjects.TESTMAP_PROJECT)
45+
public class ASTExistsIT extends RuntimeCase {
46+
47+
@Inject
48+
private ObjectContext context;
49+
50+
@Inject
51+
private DBHelper dbHelper;
52+
53+
@Inject
54+
private DataChannelInterceptor queryInterceptor;
55+
56+
@Before
57+
public void createArtistsDataSet() throws Exception {
58+
TableHelper tArtist = new TableHelper(dbHelper, "ARTIST");
59+
tArtist.setColumns("ARTIST_ID", "ARTIST_NAME", "DATE_OF_BIRTH");
60+
61+
long dateBase = System.currentTimeMillis();
62+
for (int i = 1; i <= 20; i++) {
63+
tArtist.insert(i, "artist" + i, new java.sql.Date(dateBase + 10000 * i));
64+
}
65+
66+
TableHelper tGallery = new TableHelper(dbHelper, "GALLERY");
67+
tGallery.setColumns("GALLERY_ID", "GALLERY_NAME");
68+
tGallery.insert(1, "tate modern");
69+
70+
TableHelper tPaintings = new TableHelper(dbHelper, "PAINTING");
71+
tPaintings.setColumns("PAINTING_ID", "PAINTING_TITLE", "ARTIST_ID", "GALLERY_ID");
72+
for (int i = 1; i <= 20; i++) {
73+
tPaintings.insert(i, "painting" + i, i % 5 + 1, 1);
74+
}
75+
}
76+
77+
@Test(expected = ExpressionException.class)
78+
public void testEvaluateInMemoryExistsSubquery() {
79+
ObjectSelect<Painting> subQuery = ObjectSelect.query(Painting.class)
80+
.where(Painting.TO_ARTIST.eq(Artist.ARTIST_ID_PK_PROPERTY.enclosing()));
81+
82+
doEvaluateWithQuery(ExpressionFactory.notExists(subQuery));
83+
}
84+
85+
@Test
86+
public void testEvaluateInMemoryExistsExpression() {
87+
doEvaluateNoQuery(Artist.PAINTING_ARRAY.exists());
88+
89+
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE).like("p%").exists());
90+
91+
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE).like("not_exists%").exists());
92+
93+
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.TO_PAINTING_INFO).exists());
94+
95+
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).exists());
96+
97+
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).dot(Gallery.GALLERY_NAME).like("g%").exists());
98+
99+
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).dot(Gallery.GALLERY_NAME).like("not_exists%").exists());
100+
101+
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).dot(Gallery.GALLERY_NAME).like("g%")
102+
.andExp(Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE).like("p%"))
103+
.exists());
104+
105+
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).dot(Gallery.GALLERY_NAME).like("not_exists%")
106+
.andExp(Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE).like("p%"))
107+
.exists());
108+
109+
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).dot(Gallery.GALLERY_NAME).like("g%")
110+
.andExp(Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE).like("not_exists%"))
111+
.exists());
112+
113+
doEvaluateNoQuery(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).dot(Gallery.GALLERY_NAME).like("not_exists%")
114+
.andExp(Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE).like("not_exists%"))
115+
.exists());
116+
117+
}
118+
119+
private void doEvaluateNoQuery(Expression exp) {
120+
List<Artist> artistSelected = ObjectSelect.query(Artist.class, exp).select(context);
121+
122+
List<Artist> artists = ObjectSelect.query(Artist.class)
123+
.prefetch(Artist.PAINTING_ARRAY.disjoint())
124+
.prefetch(Artist.PAINTING_ARRAY.dot(Painting.TO_PAINTING_INFO).disjoint())
125+
.prefetch(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).disjoint())
126+
.select(context);
127+
128+
queryInterceptor.runWithQueriesBlocked(() -> {
129+
List<Artist> artistsFiltered = exp.filterObjects(artists);
130+
assertEquals(exp.toString(), artistSelected, artistsFiltered);
131+
});
132+
}
133+
134+
private void doEvaluateWithQuery(Expression exp) {
135+
List<Artist> artistSelected = ObjectSelect.query(Artist.class, exp).select(context);
136+
137+
List<Artist> artists = ObjectSelect.query(Artist.class)
138+
.prefetch(Artist.PAINTING_ARRAY.disjoint())
139+
.prefetch(Artist.PAINTING_ARRAY.dot(Painting.TO_PAINTING_INFO).disjoint())
140+
.prefetch(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).disjoint())
141+
.select(context);
142+
143+
List<Artist> artistsFiltered = exp.filterObjects(artists);
144+
assertEquals(exp.toString(), artistSelected, artistsFiltered);
145+
}
146+
}

0 commit comments

Comments
 (0)