From 553e0ab34d0e325fea9a55d41134075b81ceb312 Mon Sep 17 00:00:00 2001 From: Nikita Timofeev Date: Fri, 2 Feb 2024 13:38:38 +0400 Subject: [PATCH] CAY-2816 (NOT) EXIST usability - provide simple expression syntax --- RELEASE-NOTES.txt | 1 + .../select/ExistsExpressionTranslator.java | 316 ++++++++++++++++++ .../select/QualifierTranslator.java | 34 ++ .../org/apache/cayenne/exp/Expression.java | 22 ++ .../apache/cayenne/exp/ExpressionFactory.java | 20 ++ .../org/apache/cayenne/exp/parser/ASTAll.java | 23 ++ .../org/apache/cayenne/exp/parser/ASTAny.java | 23 ++ .../apache/cayenne/exp/parser/ASTExists.java | 39 ++- .../org/apache/cayenne/exp/parser/ASTIn.java | 11 +- .../cayenne/exp/parser/ASTNotExists.java | 39 ++- .../apache/cayenne/exp/parser/ASTNotIn.java | 11 +- .../apache/cayenne/exp/parser/ASTPath.java | 20 ++ .../exp/parser/AggregateConditionNode.java | 20 ++ .../cayenne/exp/parser/ConditionNode.java | 28 ++ .../apache/cayenne/exp/parser/SimpleNode.java | 18 + .../exp/property/RelationshipProperty.java | 10 + .../select/ExistsExpressionTranslatorIT.java | 169 ++++++++++ .../QualifierTranslatorExistExpressionIT.java | 292 ++++++++++++++++ .../query/ObjectSelect_SubqueryIT.java | 24 ++ 19 files changed, 1108 insertions(+), 12 deletions(-) create mode 100644 cayenne/src/main/java/org/apache/cayenne/access/translator/select/ExistsExpressionTranslator.java create mode 100644 cayenne/src/test/java/org/apache/cayenne/access/translator/select/ExistsExpressionTranslatorIT.java create mode 100644 cayenne/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorExistExpressionIT.java diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 70d85f9d0a..cc6d9e51f4 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -47,6 +47,7 @@ CAY-2802 Upgrade Gradle to 7.6.1 CAY-2803 Test infrastructure: declarative custom DI modules in ServerCase CAY-2805 Stop calling exp parser internally CAY-2814 Select query iterator() and batchIterator() methods return incorrect results +CAY-2816 (NOT) EXIST usability - provide simple expression syntax CAY-2817 Pagination flow refactoring CAY-2818 JDK 21 support CAY-2819 DataContext.performIteratedQuery() method should be unified with iterator() method diff --git a/cayenne/src/main/java/org/apache/cayenne/access/translator/select/ExistsExpressionTranslator.java b/cayenne/src/main/java/org/apache/cayenne/access/translator/select/ExistsExpressionTranslator.java new file mode 100644 index 0000000000..cfb1f89510 --- /dev/null +++ b/cayenne/src/main/java/org/apache/cayenne/access/translator/select/ExistsExpressionTranslator.java @@ -0,0 +1,316 @@ +/***************************************************************** + * 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.access.translator.select; + +import org.apache.cayenne.Persistent; +import org.apache.cayenne.exp.Expression; +import org.apache.cayenne.exp.ExpressionFactory; +import org.apache.cayenne.exp.TraversalHandler; +import org.apache.cayenne.exp.parser.ASTDbPath; +import org.apache.cayenne.exp.parser.AggregateConditionNode; +import org.apache.cayenne.exp.parser.ConditionNode; +import org.apache.cayenne.exp.parser.Node; +import org.apache.cayenne.exp.parser.SimpleNode; +import org.apache.cayenne.map.DbEntity; +import org.apache.cayenne.map.DbJoin; +import org.apache.cayenne.map.DbRelationship; +import org.apache.cayenne.map.ObjEntity; +import org.apache.cayenne.query.ObjectSelect; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @since 5.0 + */ +class ExistsExpressionTranslator { + + private static final String EMPTY_PATH = ""; + + Expression translate(TranslatorContext context, Expression expressionToTranslate, boolean not) { + DbEntity entity = context.getRootDbEntity(); + ObjEntity objEntity = context.getMetadata().getObjEntity(); + if (objEntity != null) { + // unwrap all paths to DB + expressionToTranslate = objEntity.translateToDbPath(expressionToTranslate); + } + + // 0. quick path for a simple case - exists query for a single path expression + // TODO: maybe we should support path as a condition in a general translator too, not only here + if (expressionToTranslate instanceof ASTDbPath) { + DbPathMarker marker = createPathMarker(entity, (ASTDbPath) expressionToTranslate); + Expression pathExistExp = markerToExpression(marker); + if(marker.relationship == null) { + return pathExistExp; + } + return subqueryExpression(not, marker.relationship, pathExistExp); + } + + // 1. transform all paths + expressionToTranslate = expressionToTranslate.transform( + o -> o instanceof ASTDbPath ? createPathMarker(entity, (ASTDbPath) o) : o + ); + + // 2. group paths with db relationship by their parent conditions and relationships + Map>> parents + = groupPathsByParentAndRelationship(expressionToTranslate); + if (parents.isEmpty()) { + // no relationships in the original expression, so use it as is + return expressionToTranslate; + } + + // 3. make pairs relationship <-> node that should spawn a subquery + List relationshipToNodes = uniqueNodes(parents); + + // 4. generate subqueries and paste them to the original expression + return generateSubqueriesAndReplace(expressionToTranslate, not, relationshipToNodes); + } + + private Expression generateSubqueriesAndReplace(Expression expressionToTranslate, boolean not, List relationshipToNodes) { + Expression finalExpression = null; + for (RelationshipToNode pair : relationshipToNodes) { + Expression exp = nodeToExpression(pair.node); + SimpleNode replacement = subqueryExpression(not, pair.relationship, exp); + + Node parent = pair.node.jjtGetParent(); + if (parent == null) { + if (finalExpression != null) { + throw new IllegalStateException("Expected single root expression"); + } + finalExpression = replacement; + } else { + finalExpression = expressionToTranslate; + for (int i = 0; i < parent.jjtGetNumChildren(); i++) { + if (parent.jjtGetChild(i) == pair.node) { + parent.jjtAddChild(replacement, i); + replacement.jjtSetParent(parent); + } + } + } + } + return finalExpression; + } + + private SimpleNode subqueryExpression(boolean not, DbRelationship relationship, Expression exp) { + for (DbJoin join : relationship.getJoins()) { + Expression joinMatchExp = ExpressionFactory.matchDbExp(join.getTargetName(), + ExpressionFactory.enclosingObjectExp(ExpressionFactory.dbPathExp(join.getSourceName()))); + if (exp == null) { + exp = joinMatchExp; + } else { + exp = exp.andExp(joinMatchExp); + } + } + ObjectSelect select = ObjectSelect.query(Persistent.class) + .dbEntityName(relationship.getTargetEntityName()) + .where(exp); + return (SimpleNode) (not + ? ExpressionFactory.notExists(select) + : ExpressionFactory.exists(select)); + } + + private Expression nodeToExpression(SimpleNode node) { + if (node instanceof ParentMarker) { + return null; + } + if (node instanceof DbPathMarker) { + return markerToExpression((DbPathMarker) node); + } + return node.deepCopy(); + } + + private Expression markerToExpression(DbPathMarker marker) { + // special case for an empty path + // we don't need additional qualifier, just plain exists subquery + if (marker.getPath().equals(EMPTY_PATH)) { + return null; + } + return ExpressionFactory.noMatchExp(marker, null); + } + + private List uniqueNodes(Map>> parents) { + List relationshipToNodes = new ArrayList<>(parents.size()); + parents.forEach((parent, relToPath) -> + relToPath.forEach((rel, paths) -> { + if (paths.size() != parent.jjtGetNumChildren()) { + paths.forEach(p -> { + SimpleNode nearestCondition = getParentCondition(p); + relationshipToNodes.add(new RelationshipToNode(rel, nearestCondition)); + }); + } else { + relationshipToNodes.add(new RelationshipToNode(rel, parent)); + } + }) + ); + return relationshipToNodes; + } + + private Map>> groupPathsByParentAndRelationship( + Expression expressionToTranslate) { + Map>> parents = new HashMap<>(4); + expressionToTranslate.traverse((SimpleTraversalHandler) (node, parentNode) -> { + if (node instanceof DbPathMarker) { + DbPathMarker marker = (DbPathMarker) node; + if (marker.root()) { + return; + } + SimpleNode parent = getParentAggregateCondition(parentNode); + parents.computeIfAbsent(parent, p -> new HashMap<>(4)) + .computeIfAbsent(marker.relationship, r -> new ArrayList<>(4)) + .add(marker); + } + }); + return parents; + } + + private SimpleNode getParentAggregateCondition(Expression parentNode) { + Node parent = (Node) parentNode; + while (parent != null && !(parent instanceof AggregateConditionNode)) { + parent = parent.jjtGetParent(); + } + if (parent == null) { + parent = new ParentMarker(); + } + return (SimpleNode) parent; + } + + private SimpleNode getParentCondition(Expression parentNode) { + Node parent = (Node) parentNode; + while (parent != null && !(parent instanceof ConditionNode)) { + parent = parent.jjtGetParent(); + } + if (parent == null) { + parent = new ParentMarker(); + } + return (SimpleNode) parent; + } + + private Expression relationshipPathsMarker(Expression exp, DbEntity entity) { + return exp.transform(o -> { + if (o instanceof ASTDbPath) { + return createPathMarker(entity, (ASTDbPath) o); + } + return o; + }); + } + + private DbPathMarker createPathMarker(DbEntity entity, ASTDbPath o) { + String path = o.getPath(); + String newPath; + String firstSegment; + int dotIndex = path.indexOf("."); + if (dotIndex == -1) { + firstSegment = path; + newPath = EMPTY_PATH; + } else { + firstSegment = path.substring(0, dotIndex); + newPath = path.substring(dotIndex + 1); + } + // mark relationship that this path relates to and transform path + DbRelationship relationship = entity.getRelationship(firstSegment); + if (relationship == null) { + newPath = path; + } + return new DbPathMarker(newPath, relationship); + } + + static class RelationshipToNode { + final DbRelationship relationship; + final SimpleNode node; + + RelationshipToNode(DbRelationship relationship, SimpleNode node) { + this.relationship = relationship; + this.node = node; + } + } + + static class DbPathMarker extends ASTDbPath { + + final DbRelationship relationship; + + DbPathMarker(String path, DbRelationship relationship) { + super(path); + this.relationship = relationship; + } + + @Override + public Expression shallowCopy() { + return new DbPathMarker(getPath(), relationship); + } + + @Override + public boolean equals(Object object) { + return this == object; + } + + @Override + public int hashCode() { + return System.identityHashCode(this); + } + + boolean root() { + return relationship == null; + } + } + + static class ParentMarker extends ConditionNode { + + public ParentMarker() { + super(0); + } + + @Override + public Expression shallowCopy() { + return this; + } + + @Override + protected int getRequiredChildrenCount() { + return 0; + } + + @Override + protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception { + return null; + } + + @Override + protected String getExpressionOperator(int index) { + return null; + } + + @Override + public boolean equals(Object object) { + return this == object; + } + + @Override + public int hashCode() { + return System.identityHashCode(this); + } + } + + interface SimpleTraversalHandler extends TraversalHandler { + @Override + void endNode(Expression node, Expression parentNode); + } +} diff --git a/cayenne/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java b/cayenne/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java index ddab8332fb..a3d5d99ae1 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java @@ -32,8 +32,10 @@ import org.apache.cayenne.exp.parser.ASTCustomOperator; import org.apache.cayenne.exp.parser.ASTDbIdPath; import org.apache.cayenne.exp.parser.ASTDbPath; +import org.apache.cayenne.exp.parser.ASTExists; import org.apache.cayenne.exp.parser.ASTFullObject; import org.apache.cayenne.exp.parser.ASTFunctionCall; +import org.apache.cayenne.exp.parser.ASTNotExists; import org.apache.cayenne.exp.parser.ASTObjPath; import org.apache.cayenne.exp.parser.ASTScalar; import org.apache.cayenne.exp.parser.ASTSubquery; @@ -88,6 +90,9 @@ Node translate(Expression qualifier) { return null; } + // expand complex expressions that could be only interpreted at the execution time + qualifier = expandExpression(qualifier); + Node rootNode = new EmptyNode(); expressionsToSkip.clear(); boolean hasCurrentNode = currentNode != null; @@ -113,6 +118,35 @@ Node translate(Expression qualifier) { return rootNode; } + /** + * Preprocess complex expressions that ExpressionFactory can't handle at the creation time. + *
+ * Right we only expand {@code EXIST} expressions that could spawn several subqueries. + * + * @param qualifier to process + * @return qualifier with preprocessed complex expressions + */ + Expression expandExpression(Expression qualifier) { + return qualifier.transform(o -> { + if(o instanceof ASTExists || o instanceof ASTNotExists) { + return expandExistsExpression((SimpleNode) o); + } + return o; + }); + } + + Expression expandExistsExpression(SimpleNode exists) { + Object child = exists.getOperand(0); + if(child instanceof ASTSubquery) { + return exists; + } + if(child instanceof Expression) { + return new ExistsExpressionTranslator().translate(context, (Expression) child, exists instanceof ASTNotExists); + } else { + throw new IllegalArgumentException("Expected expression as a child, got " + child); + } + } + @Override public void startNode(Expression node, Expression parentNode) { if(expressionsToSkip.contains(node) || expressionsToSkip.contains(parentNode)) { diff --git a/cayenne/src/main/java/org/apache/cayenne/exp/Expression.java b/cayenne/src/main/java/org/apache/cayenne/exp/Expression.java index fa0a55f5d3..4694703ca4 100644 --- a/cayenne/src/main/java/org/apache/cayenne/exp/Expression.java +++ b/cayenne/src/main/java/org/apache/cayenne/exp/Expression.java @@ -445,6 +445,28 @@ public Expression orExp(Expression exp, Expression... expressions) { */ public abstract Expression notExp(); + /** + * Returns expression that will be dynamically resolved to proper subqueries based on a relationships used + * (if no relationships are present in the original expression no subqueries will be used). + * + * @return exists expression + * + * @see ExpressionFactory#exists(Expression) + * @since 5.0 + */ + public abstract Expression exists(); + + /** + * Returns expression that will be dynamically resolved to proper subqueries based on a relationships used + * (if no relationships are present in the original expression no subqueries will be used). + * + * @return not exists expression + * + * @see ExpressionFactory#notExists(Expression) + * @since 5.0 + */ + public abstract Expression notExists(); + /** * Returns a count of operands of this expression. In real life there are * unary (count == 1), binary (count == 2) and ternary (count == 3) diff --git a/cayenne/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java b/cayenne/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java index b17775411b..a17df38b16 100644 --- a/cayenne/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java +++ b/cayenne/src/main/java/org/apache/cayenne/exp/ExpressionFactory.java @@ -1452,6 +1452,16 @@ public static Expression exists(FluentSelect subQuery) { return new ASTExists(new ASTSubquery(subQuery)); } + /** + * Builds expression representing EXIST subquery over a given path + * @param exp expression to use for an EXISTS + * @return expression representing exists subquery + * @since 5.0 + */ + public static Expression exists(Expression exp) { + return new ASTExists(exp); + } + /** * @param subQuery {@link org.apache.cayenne.query.ObjectSelect} or {@link ColumnSelect} * @since 4.2 @@ -1460,6 +1470,16 @@ public static Expression notExists(FluentSelect subQuery) { return new ASTNotExists(new ASTSubquery(subQuery)); } + /** + * Builds expression representing NOT EXIST subquery over a given path + * @param exp expression to use for an NOT EXISTS + * @return expression representing exists subquery + * @since 5.0 + */ + public static Expression notExists(Expression exp) { + return new ASTNotExists(exp); + } + /** * @since 4.2 */ diff --git a/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTAll.java b/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTAll.java index 631c146a61..e58160784f 100644 --- a/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTAll.java +++ b/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTAll.java @@ -51,8 +51,31 @@ protected String getExpressionOperator(int index) { return "ALL"; } + @Override + public void jjtSetParent(Node n) { + parent = n; + } + @Override public int getType() { return Expression.ALL; } + + /** + * @inheritDoc + * @since 5.0 + */ + @Override + public Expression exists() { + throw new UnsupportedOperationException("Can't use exists() with ALL"); + } + + /** + * @inheritDoc + * @since 5.0 + */ + @Override + public Expression notExists() { + throw new UnsupportedOperationException("Can't use not exists() with ALL"); + } } diff --git a/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTAny.java b/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTAny.java index 0a1fabfd6a..9eec5af3ab 100644 --- a/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTAny.java +++ b/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTAny.java @@ -46,6 +46,11 @@ protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws E return null; } + @Override + public void jjtSetParent(Node n) { + parent = n; + } + @Override protected String getExpressionOperator(int index) { return "ANY"; @@ -55,4 +60,22 @@ protected String getExpressionOperator(int index) { public int getType() { return Expression.ANY; } + + /** + * @inheritDoc + * @since 5.0 + */ + @Override + public Expression exists() { + throw new UnsupportedOperationException("Can't use exists() operator with ANY"); + } + + /** + * @inheritDoc + * @since 5.0 + */ + @Override + public Expression notExists() { + throw new UnsupportedOperationException("Can't use not exists() operator with ANY"); + } } 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 e3045200c2..295e97dc13 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 @@ -20,15 +20,16 @@ package org.apache.cayenne.exp.parser; import org.apache.cayenne.exp.Expression; +import org.apache.cayenne.exp.ExpressionFactory; /** * @since 4.2 */ public class ASTExists extends ConditionNode { - public ASTExists(ASTSubquery subquery) { + public ASTExists(Expression expression) { super(0); - jjtAddChild(subquery, 0); + jjtAddChild((SimpleNode)expression, 0); } ASTExists(int id) { @@ -50,6 +51,22 @@ protected String getExpressionOperator(int index) { return null; } + @Override + public void jjtSetParent(Node n) { + parent = n; + } + + @Override + public void setOperand(int index, Object value) { + Node node = (value == null || value instanceof Node) ? (Node) value : new ASTScalar(value); + jjtAddChild(node, index); + + // set the parent, as jjtAddChild doesn't do it... + if (node != null) { + ((SimpleNode)node).parent = this; + } + } + @Override public Expression shallowCopy() { return new ASTExists(id); @@ -59,4 +76,22 @@ public Expression shallowCopy() { public int getType() { return Expression.EXISTS; } + + /** + * @inheritDoc + * @since 5.0 + */ + @Override + public Expression exists() { + return this; + } + + /** + * @inheritDoc + * @since 5.0 + */ + @Override + public Expression notExists() { + return ExpressionFactory.notExists((Expression) getOperand(0)); + } } diff --git a/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTIn.java b/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTIn.java index bb59ae3e84..c6d05dcf86 100644 --- a/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTIn.java +++ b/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTIn.java @@ -99,10 +99,13 @@ protected Object transformExpression(Function transformer) { if (transformed instanceof ASTIn) { ASTIn exp = (ASTIn) transformed; if (exp.jjtGetNumChildren() == 2) { - ASTList list = (ASTList) exp.jjtGetChild(1); - Object[] objects = (Object[]) list.evaluate(null); - if (objects.length == 0) { - transformed = new ASTFalse(); + Node child = exp.jjtGetChild(1); + if(child instanceof ASTList) { + ASTList list = (ASTList) child; + Object[] objects = (Object[]) list.evaluate(null); + if (objects.length == 0) { + transformed = new ASTFalse(); + } } } } 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 61e78ef22f..f9bc52d1fc 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 @@ -20,15 +20,16 @@ package org.apache.cayenne.exp.parser; import org.apache.cayenne.exp.Expression; +import org.apache.cayenne.exp.ExpressionFactory; /** * @since 4.2 */ public class ASTNotExists extends ConditionNode { - public ASTNotExists(ASTSubquery subquery) { + public ASTNotExists(Expression expression) { super(0); - jjtAddChild(subquery, 0); + jjtAddChild((SimpleNode)expression, 0); } ASTNotExists(int id) { @@ -50,6 +51,22 @@ protected String getExpressionOperator(int index) { return null; } + @Override + public void jjtSetParent(Node n) { + parent = n; + } + + @Override + public void setOperand(int index, Object value) { + Node node = (value == null || value instanceof Node) ? (Node) value : new ASTScalar(value); + jjtAddChild(node, index); + + // set the parent, as jjtAddChild doesn't do it... + if (node != null) { + ((SimpleNode)node).parent = this; + } + } + @Override public Expression shallowCopy() { return new ASTNotExists(id); @@ -59,4 +76,22 @@ public Expression shallowCopy() { public int getType() { return Expression.NOT_EXISTS; } + + /** + * @inheritDoc + * @since 5.0 + */ + @Override + public Expression exists() { + return ExpressionFactory.exists((Expression) getOperand(0)); + } + + /** + * @inheritDoc + * @since 5.0 + */ + @Override + public Expression notExists() { + return this; + } } diff --git a/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTNotIn.java b/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTNotIn.java index 9bab164b95..f49ff00d25 100644 --- a/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTNotIn.java +++ b/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTNotIn.java @@ -91,10 +91,13 @@ protected Object transformExpression(Function transformer) { if (transformed instanceof ASTNotIn) { ASTNotIn exp = (ASTNotIn) transformed; if (exp.jjtGetNumChildren() == 2) { - ASTList list = (ASTList) exp.jjtGetChild(1); - Object[] objects = (Object[]) list.evaluate(null); - if (objects.length == 0) { - transformed = new ASTTrue(); + Node child = exp.jjtGetChild(1); + if(child instanceof ASTList) { + ASTList list = (ASTList) child; + Object[] objects = (Object[]) list.evaluate(null); + if (objects.length == 0) { + transformed = new ASTTrue(); + } } } } diff --git a/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTPath.java b/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTPath.java index 7d5561db76..0977278185 100644 --- a/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTPath.java +++ b/cayenne/src/main/java/org/apache/cayenne/exp/parser/ASTPath.java @@ -22,6 +22,8 @@ import java.util.Iterator; import java.util.Map; +import org.apache.cayenne.exp.Expression; +import org.apache.cayenne.exp.ExpressionFactory; import org.apache.cayenne.map.Entity; import org.apache.cayenne.map.PathComponent; import org.apache.cayenne.util.CayenneMapEntry; @@ -114,6 +116,24 @@ protected String getExpressionOperator(int index) { + "'"); } + /** + * @inheritDoc + * @since 5.0 + */ + @Override + public Expression exists() { + return ExpressionFactory.exists(this); + } + + /** + * @inheritDoc + * @since 5.0 + */ + @Override + public Expression notExists() { + return ExpressionFactory.notExists(this); + } + @Override public int hashCode() { return path.hashCode(); diff --git a/cayenne/src/main/java/org/apache/cayenne/exp/parser/AggregateConditionNode.java b/cayenne/src/main/java/org/apache/cayenne/exp/parser/AggregateConditionNode.java index 26bdeaf87b..a26fea89f0 100644 --- a/cayenne/src/main/java/org/apache/cayenne/exp/parser/AggregateConditionNode.java +++ b/cayenne/src/main/java/org/apache/cayenne/exp/parser/AggregateConditionNode.java @@ -21,7 +21,9 @@ import java.util.function.Function; +import org.apache.cayenne.exp.Expression; import org.apache.cayenne.exp.ExpressionException; +import org.apache.cayenne.exp.ExpressionFactory; /** * Superclass of aggregated conditional nodes such as NOT, AND, OR. Performs @@ -96,4 +98,22 @@ public void jjtAddChild(Node n, int i) { super.jjtAddChild(n, i); } + + /** + * @inheritDoc + * @since 5.0 + */ + @Override + public Expression exists() { + return ExpressionFactory.exists(this); + } + + /** + * @inheritDoc + * @since 5.0 + */ + @Override + public Expression notExists() { + return ExpressionFactory.notExists(this); + } } diff --git a/cayenne/src/main/java/org/apache/cayenne/exp/parser/ConditionNode.java b/cayenne/src/main/java/org/apache/cayenne/exp/parser/ConditionNode.java index 63a0f459d6..b06cda676f 100644 --- a/cayenne/src/main/java/org/apache/cayenne/exp/parser/ConditionNode.java +++ b/cayenne/src/main/java/org/apache/cayenne/exp/parser/ConditionNode.java @@ -23,7 +23,9 @@ import java.util.Collection; import java.util.Map; +import org.apache.cayenne.exp.Expression; import org.apache.cayenne.exp.ExpressionException; +import org.apache.cayenne.exp.ExpressionFactory; /** * Superclass of conditional expressions. @@ -87,4 +89,30 @@ protected Object evaluateNode(Object o) throws Exception { abstract protected int getRequiredChildrenCount(); abstract protected Boolean evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception; + + /** + * Returns expression that will be dynamically resolved to proper subqueries based on a relationships used + * (if no relationships are present in the original expression no subqueries will be used). + * + * @return exists expression + * + * @see ExpressionFactory#exists(Expression) + * @since 5.0 + */ + public Expression exists() { + return ExpressionFactory.exists(this); + } + + /** + * Returns expression that will be dynamically resolved to proper subqueries based on a relationships used + * (if no relationships are present in the original expression no subqueries will be used). + * + * @return not exists expression + * + * @see ExpressionFactory#notExists(Expression) + * @since 5.0 + */ + public Expression notExists() { + return ExpressionFactory.notExists(this); + } } diff --git a/cayenne/src/main/java/org/apache/cayenne/exp/parser/SimpleNode.java b/cayenne/src/main/java/org/apache/cayenne/exp/parser/SimpleNode.java index 95646d3703..c306051c90 100644 --- a/cayenne/src/main/java/org/apache/cayenne/exp/parser/SimpleNode.java +++ b/cayenne/src/main/java/org/apache/cayenne/exp/parser/SimpleNode.java @@ -397,6 +397,24 @@ public Expression notExp() { return new ASTNot(this); } + /** + * @inheritDoc + * @since 5.0 + */ + @Override + public Expression exists() { + throw new UnsupportedOperationException("Can't use exists() operator with this expression"); + } + + /** + * @inheritDoc + * @since 5.0 + */ + @Override + public Expression notExists() { + throw new UnsupportedOperationException("Can't use not exists() operator with this expression"); + } + @Override public Object evaluate(Object o) { // wrap in try/catch to provide unified exception processing diff --git a/cayenne/src/main/java/org/apache/cayenne/exp/property/RelationshipProperty.java b/cayenne/src/main/java/org/apache/cayenne/exp/property/RelationshipProperty.java index 8c665d7544..e58348ae3c 100644 --- a/cayenne/src/main/java/org/apache/cayenne/exp/property/RelationshipProperty.java +++ b/cayenne/src/main/java/org/apache/cayenne/exp/property/RelationshipProperty.java @@ -22,6 +22,8 @@ import org.apache.cayenne.CayenneRuntimeException; import org.apache.cayenne.EmbeddableObject; import org.apache.cayenne.Persistent; +import org.apache.cayenne.exp.Expression; +import org.apache.cayenne.exp.ExpressionFactory; import org.apache.cayenne.query.PrefetchTreeNode; /** @@ -160,4 +162,12 @@ default EmbeddableProperty dot(EmbeddablePropert PropertyUtils.buildExp(path, getExpression().getPathAliases()), property.getType()); } + + default Expression exists() { + return ExpressionFactory.exists(getExpression()); + } + + default Expression notExists() { + return ExpressionFactory.notExists(getExpression()); + } } diff --git a/cayenne/src/test/java/org/apache/cayenne/access/translator/select/ExistsExpressionTranslatorIT.java b/cayenne/src/test/java/org/apache/cayenne/access/translator/select/ExistsExpressionTranslatorIT.java new file mode 100644 index 0000000000..a6a7fcf294 --- /dev/null +++ b/cayenne/src/test/java/org/apache/cayenne/access/translator/select/ExistsExpressionTranslatorIT.java @@ -0,0 +1,169 @@ +/***************************************************************** + * 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.access.translator.select; + +import org.apache.cayenne.ObjectContext; +import org.apache.cayenne.di.Inject; +import org.apache.cayenne.exp.Expression; +import org.apache.cayenne.exp.ExpressionFactory; +import org.apache.cayenne.map.DbEntity; +import org.apache.cayenne.map.ObjEntity; +import org.apache.cayenne.query.MockQueryMetadata; +import org.apache.cayenne.query.QueryMetadata; +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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@UseCayenneRuntime(CayenneProjects.TESTMAP_PROJECT) +public class ExistsExpressionTranslatorIT extends RuntimeCase { + + @Inject + private ObjectContext context; + + private TranslatorContext translatorContext; + + private ExistsExpressionTranslator translator; + + @Before + public void setUp() { + translatorContext = mock(TranslatorContext.class); + DbEntity dbArtist = context.getEntityResolver().getDbEntity("ARTIST"); + ObjEntity objArtist = context.getEntityResolver().getObjEntity("Artist"); + when(translatorContext.getRootDbEntity()).thenReturn(dbArtist); + QueryMetadata metadata = new MockQueryMetadata() { + @Override + public ObjEntity getObjEntity() { + return objArtist; + } + }; + when(translatorContext.getMetadata()).thenReturn(metadata); + + translator = new ExistsExpressionTranslator(); + } + + @Test + public void testSimplePath() { + Expression exp = ExpressionFactory.exp("paintingArray"); + Expression translated = translator.translate(translatorContext, exp, false); + + assertNotNull(translated); + } + + @Test + public void testSimplePathNoRelationship() { + Expression exp = ExpressionFactory.exp("artistName"); + Expression translated = translator.translate(translatorContext, exp, false); + + assertNotNull(translated); + } + + @Test + public void testSimpleLongPath() { + Expression exp = ExpressionFactory.exp("paintingArray.toGallery"); + Expression translated = translator.translate(translatorContext, exp, false); + + assertNotNull(translated); + } + + @Test + public void testSimpleCondition() { + Expression exp = ExpressionFactory.exp("paintingArray.paintingTitle = 'test'"); + Expression translated = translator.translate(translatorContext, exp, false); + + assertNotNull(translated); + } + + @Test + public void testSimpleConditionsSameRoot() { + Expression exp = ExpressionFactory.exp("paintingArray.paintingTitle = 'test' " + + "or paintingArray.paintingTitle = 'test2'"); + Expression translated = translator.translate(translatorContext, exp, false); + + assertNotNull(translated); + } + + @Test + public void testSimpleConditionsDifferentRoots() { + Expression exp = ExpressionFactory.exp("paintingArray.paintingTitle = 'test' " + + "or groupArray.name = 'test'"); + Expression translated = translator.translate(translatorContext, exp, false); + + assertNotNull(translated); + } + + @Test + public void testComplexCondition() { + Expression exp = ExpressionFactory.exp("length(paintingArray.paintingTitle) in (1, 2, 3)"); + Expression translated = translator.translate(translatorContext, exp, false); + + assertNotNull(translated); + } + + @Test + public void testComplexConditionsSameRoot() { + Expression exp = ExpressionFactory.exp("(length(paintingArray.paintingTitle) in (1, 2, 3)) " + + "or (paintingArray.estimatedPrice > 10000)"); + Expression translated = translator.translate(translatorContext, exp, false); + + assertNotNull(translated); + } + + @Test + public void testComplexConditionsDifferentRoots() { + Expression exp = ExpressionFactory.exp("(length(paintingArray.paintingTitle) in (1, 2, 3)) " + + "or (length(groupArray.name) < 10)"); + Expression translated = translator.translate(translatorContext, exp, false); + + assertNotNull(translated); + } + + @Test + public void testNoRelationships() { + Expression exp = ExpressionFactory.exp("artistName like 'test%'"); + Expression translated = translator.translate(translatorContext, exp, false); + + assertNotNull(translated); + assertEquals("db:ARTIST_NAME like \"test%\"", translated.toString()); + } + + @Test + public void testDbPath() { + Expression exp = ExpressionFactory.exp("db:PAINTING_ARRAY"); + + translatorContext = mock(TranslatorContext.class); + DbEntity dbArtist = context.getEntityResolver().getDbEntity("ARTIST"); + when(translatorContext.getRootDbEntity()).thenReturn(dbArtist); + when(translatorContext.getMetadata()).thenReturn(new MockQueryMetadata()); + + translator = new ExistsExpressionTranslator(); + + Expression translated = translator.translate(translatorContext, exp, false); + + assertNotNull(translated); + assertEquals("db:PAINTING_ARRAY != null", translated.toString()); + } +} \ No newline at end of file diff --git a/cayenne/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorExistExpressionIT.java b/cayenne/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorExistExpressionIT.java new file mode 100644 index 0000000000..53debdbe87 --- /dev/null +++ b/cayenne/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorExistExpressionIT.java @@ -0,0 +1,292 @@ +/***************************************************************** + * 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.access.translator.select; + +import org.apache.cayenne.ObjectContext; +import org.apache.cayenne.access.sqlbuilder.SQLGenerationVisitor; +import org.apache.cayenne.access.sqlbuilder.StringBuilderAppendable; +import org.apache.cayenne.access.sqlbuilder.sqltree.Node; +import org.apache.cayenne.di.Inject; +import org.apache.cayenne.exp.Expression; +import org.apache.cayenne.exp.ExpressionFactory; +import org.apache.cayenne.query.ObjectSelect; +import org.apache.cayenne.runtime.CayenneRuntime; +import org.apache.cayenne.testdo.testmap.Artist; +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.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@UseCayenneRuntime(CayenneProjects.TESTMAP_PROJECT) +public class QualifierTranslatorExistExpressionIT extends RuntimeCase { + + @Inject + private CayenneRuntime runtime; + + @Inject + private ObjectContext context; + + @Test + public void testExistsSimplePath() { + Expression exp = ExpressionFactory + .exp("paintingArray") + .exists(); + + ObjectSelect query = ObjectSelect.query(Artist.class, exp); + + DefaultSelectTranslator translator + = new DefaultSelectTranslator(query, runtime.getDataDomain().getDefaultNode().getAdapter(), context.getEntityResolver()); + + QualifierTranslator qualifierTranslator = translator.getContext().getQualifierTranslator(); + + Node node = qualifierTranslator.translate(query.getWhere()); + + assertSQL(" EXISTS (" + + "SELECT t1.PAINTING_ID FROM PAINTING t1 " + + "WHERE t1.ARTIST_ID = t0.ARTIST_ID" + + ")", node); + } + + @Test + public void testExistsSimplePathNoRelationship() { + Expression exp = ExpressionFactory + .exp("artistName") + .exists(); + + ObjectSelect query = ObjectSelect.query(Artist.class, exp); + + DefaultSelectTranslator translator + = new DefaultSelectTranslator(query, runtime.getDataDomain().getDefaultNode().getAdapter(), context.getEntityResolver()); + + QualifierTranslator qualifierTranslator = translator.getContext().getQualifierTranslator(); + + Node node = qualifierTranslator.translate(query.getWhere()); + + assertSQL(" t0.ARTIST_NAME IS NOT NULL", node); + } + + @Test + public void testExistsLongPathSimpleAttribute() { + Expression exp = ExpressionFactory + .exp("paintingArray.paintingTitle") + .exists(); + + ObjectSelect query = ObjectSelect.query(Artist.class, exp); + + DefaultSelectTranslator translator + = new DefaultSelectTranslator(query, runtime.getDataDomain().getDefaultNode().getAdapter(), context.getEntityResolver()); + + QualifierTranslator qualifierTranslator = translator.getContext().getQualifierTranslator(); + + Node node = qualifierTranslator.translate(query.getWhere()); + + assertSQL(" EXISTS (" + + "SELECT t1.PAINTING_ID FROM PAINTING t1 " + + "WHERE ( t1.PAINTING_TITLE IS NOT NULL ) AND ( t1.ARTIST_ID = t0.ARTIST_ID )" + + ")", node); + } + + @Test + public void testExistsLongToOnePath() { + Expression exp = ExpressionFactory + .exp("paintingArray.toGallery") + .exists(); + + ObjectSelect query = ObjectSelect.query(Artist.class, exp); + + DefaultSelectTranslator translator + = new DefaultSelectTranslator(query, runtime.getDataDomain().getDefaultNode().getAdapter(), context.getEntityResolver()); + + QualifierTranslator qualifierTranslator = translator.getContext().getQualifierTranslator(); + + Node node = qualifierTranslator.translate(query.getWhere()); + + assertSQL(" EXISTS (" + + "SELECT t1.PAINTING_ID FROM PAINTING t1 " + + "WHERE ( t1.GALLERY_ID IS NOT NULL ) AND ( t1.ARTIST_ID = t0.ARTIST_ID )" + + ")", node); + } + + @Test + public void testExistsLongToManyPath() { + Expression exp = ExpressionFactory + .exp("groupArray.childGroupsArray") + .exists(); + + ObjectSelect query = ObjectSelect.query(Artist.class, exp); + + DefaultSelectTranslator translator + = new DefaultSelectTranslator(query, runtime.getDataDomain().getDefaultNode().getAdapter(), context.getEntityResolver()); + + QualifierTranslator qualifierTranslator = translator.getContext().getQualifierTranslator(); + + Node node = qualifierTranslator.translate(query.getWhere()); + + assertSQL(" EXISTS (" + + "SELECT DISTINCT t1.ARTIST_ID, t1.GROUP_ID FROM ARTIST_GROUP t1 " + + "JOIN ARTGROUP t2 ON t1.GROUP_ID = t2.GROUP_ID " + + "JOIN ARTGROUP t3 ON t2.GROUP_ID = t3.PARENT_GROUP_ID " + + "WHERE ( t3.GROUP_ID IS NOT NULL ) AND ( t1.ARTIST_ID = t0.ARTIST_ID )" + + ")", node); + } + + @Test + public void testExistsSimpleCondition() { + Expression exp = ExpressionFactory + .exp("paintingArray.paintingTitle = 'test'") + .exists(); + + ObjectSelect query = ObjectSelect.query(Artist.class, exp); + + DefaultSelectTranslator translator + = new DefaultSelectTranslator(query, runtime.getDataDomain().getDefaultNode().getAdapter(), context.getEntityResolver()); + + QualifierTranslator qualifierTranslator = translator.getContext().getQualifierTranslator(); + + Node node = qualifierTranslator.translate(query.getWhere()); + + assertSQL(" EXISTS (" + + "SELECT t1.PAINTING_ID FROM PAINTING t1 " + + "WHERE ( t1.PAINTING_TITLE = 'test' ) AND ( t1.ARTIST_ID = t0.ARTIST_ID )" + + ")", node); + } + + @Test + public void testExistsAggConditionSameRoot() { + Expression exp = ExpressionFactory + .exp("paintingArray.paintingTitle = 'test' " + + "or paintingArray.paintingTitle = 'test2'") + .exists(); + + ObjectSelect query = ObjectSelect.query(Artist.class, exp); + + DefaultSelectTranslator translator + = new DefaultSelectTranslator(query, runtime.getDataDomain().getDefaultNode().getAdapter(), context.getEntityResolver()); + + QualifierTranslator qualifierTranslator = translator.getContext().getQualifierTranslator(); + + Node node = qualifierTranslator.translate(query.getWhere()); + + assertSQL(" EXISTS (" + + "SELECT t1.PAINTING_ID FROM PAINTING t1 " + + "WHERE ( ( t1.PAINTING_TITLE = 'test' ) OR ( t1.PAINTING_TITLE = 'test2' ) ) " + + "AND ( t1.ARTIST_ID = t0.ARTIST_ID )" + + ")", node); + } + + @Test + public void testExistsAggConditionMultipleRoots() { + Expression exp = ExpressionFactory + .exp("(paintingArray.paintingTitle = 'test' " + + "or paintingArray.paintingTitle = 'test2') and groupArray.name = 'test'") + .exists(); + + ObjectSelect query = ObjectSelect.query(Artist.class, exp); + + DefaultSelectTranslator translator + = new DefaultSelectTranslator(query, runtime.getDataDomain().getDefaultNode().getAdapter(), context.getEntityResolver()); + + QualifierTranslator qualifierTranslator = translator.getContext().getQualifierTranslator(); + + Node node = qualifierTranslator.translate(query.getWhere()); + + assertSQL(" EXISTS (" + + "SELECT t1.PAINTING_ID FROM PAINTING t1 " + + "WHERE ( ( t1.PAINTING_TITLE = 'test' ) OR ( t1.PAINTING_TITLE = 'test2' ) ) " + + "AND ( t1.ARTIST_ID = t0.ARTIST_ID )" + + ") " + + "AND EXISTS (" + + "SELECT t2.ARTIST_ID, t2.GROUP_ID FROM ARTIST_GROUP t2 " + + "JOIN ARTGROUP t3 ON t2.GROUP_ID = t3.GROUP_ID " + + "WHERE ( t3.NAME = 'test' ) AND ( t2.ARTIST_ID = t0.ARTIST_ID ))", node); + } + + @Test + public void testExistsAggDifferentRoots() { + Expression exp = ExpressionFactory + .exp("paintingArray.paintingTitle = 'test' " + + "and (groupArray.name = 'test' or paintingArray.paintingTitle = 'test2')") + .exists(); + + ObjectSelect query = ObjectSelect.query(Artist.class, exp); + + DefaultSelectTranslator translator + = new DefaultSelectTranslator(query, runtime.getDataDomain().getDefaultNode().getAdapter(), context.getEntityResolver()); + + QualifierTranslator qualifierTranslator = translator.getContext().getQualifierTranslator(); + + Node node = qualifierTranslator.translate(query.getWhere()); + + assertSQL(" EXISTS (" + + "SELECT t1.PAINTING_ID FROM PAINTING t1 " + + "WHERE ( t1.PAINTING_TITLE = 'test' ) AND ( t1.ARTIST_ID = t0.ARTIST_ID )" + + ") " + + "AND ( " + + "EXISTS (" + + "SELECT t2.ARTIST_ID, t2.GROUP_ID FROM ARTIST_GROUP t2 " + + "JOIN ARTGROUP t3 ON t2.GROUP_ID = t3.GROUP_ID " + + "WHERE ( t3.NAME = 'test' ) AND ( t2.ARTIST_ID = t0.ARTIST_ID )" + + ") " + + "OR " + + "EXISTS (" + + "SELECT t4.PAINTING_ID FROM PAINTING t4 " + + "WHERE ( t4.PAINTING_TITLE = 'test2' ) AND ( t4.ARTIST_ID = t0.ARTIST_ID )" + + ") " + + ")", node); + } + + @Test + public void testExistsComplexConditionsDifferentRoots() { + Expression exp = ExpressionFactory + .exp("(length(paintingArray.paintingTitle) in (1, 2, 3)) " + + "or (length(groupArray.name) < 10)") + .exists(); + ObjectSelect query = ObjectSelect.query(Artist.class, exp); + + DefaultSelectTranslator translator + = new DefaultSelectTranslator(query, runtime.getDataDomain().getDefaultNode().getAdapter(), context.getEntityResolver()); + + QualifierTranslator qualifierTranslator = translator.getContext().getQualifierTranslator(); + + Node node = qualifierTranslator.translate(query.getWhere()); + + assertSQL(" EXISTS (" + + "SELECT t1.PAINTING_ID FROM PAINTING t1 " + + "WHERE LENGTH( t1.PAINTING_TITLE ) IN ( 1, 2, 3) AND ( t1.ARTIST_ID = t0.ARTIST_ID )" + + ") OR " + + "EXISTS (" + + "SELECT t2.ARTIST_ID, t2.GROUP_ID FROM ARTIST_GROUP t2 " + + "JOIN ARTGROUP t3 ON t2.GROUP_ID = t3.GROUP_ID " + + "WHERE ( LENGTH( t3.NAME ) < 10 ) AND ( t2.ARTIST_ID = t0.ARTIST_ID )" + + ")", node); + } + + protected void assertSQL(String expected, Node node) { + assertNotNull(node); + + SQLGenerationVisitor visitor = new SQLGenerationVisitor(new StringBuilderAppendable()); + node.visit(visitor); + assertEquals(expected, visitor.getSQLString()); + } + +} diff --git a/cayenne/src/test/java/org/apache/cayenne/query/ObjectSelect_SubqueryIT.java b/cayenne/src/test/java/org/apache/cayenne/query/ObjectSelect_SubqueryIT.java index ade42de1b9..9e968c0689 100644 --- a/cayenne/src/test/java/org/apache/cayenne/query/ObjectSelect_SubqueryIT.java +++ b/cayenne/src/test/java/org/apache/cayenne/query/ObjectSelect_SubqueryIT.java @@ -79,6 +79,30 @@ public void selectQuery_simpleExists() { assertEquals(20L, count); } + @Test + public void selectQuery_existsExpressionSimple() { + long count = ObjectSelect.query(Artist.class) + .where(Artist.PAINTING_ARRAY.exists()) + .selectCount(context); + assertEquals(5L, count); + } + + @Test + public void selectQuery_existsExpression() { + long count = ObjectSelect.query(Artist.class) + .where(Artist.PAINTING_ARRAY.dot(Painting.PAINTING_TITLE).like("painting%").exists()) + .selectCount(context); + assertEquals(5L, count); + } + + @Test + public void selectQuery_simpleNotExistsExpression() { + long count = ObjectSelect.query(Artist.class) + .where(Artist.PAINTING_ARRAY.notExists()) + .selectCount(context); + assertEquals(15L, count); + } + @Test public void selectQuery_existsWithExpressionFromParentQuery() { Expression exp = Painting.TO_ARTIST.eq(Artist.ARTIST_ID_PK_PROPERTY.enclosing())