Skip to content

Commit

Permalink
CAY-2816 (NOT) EXIST usability - provide simple expression syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
stariy95 committed Feb 2, 2024
1 parent d43d291 commit 553e0ab
Show file tree
Hide file tree
Showing 19 changed files with 1,108 additions and 12 deletions.
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SimpleNode, Map<DbRelationship, List<DbPathMarker>>> 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<RelationshipToNode> 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<RelationshipToNode> 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<Persistent> 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<RelationshipToNode> uniqueNodes(Map<SimpleNode, Map<DbRelationship, List<DbPathMarker>>> parents) {
List<RelationshipToNode> 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<SimpleNode, Map<DbRelationship, List<DbPathMarker>>> groupPathsByParentAndRelationship(
Expression expressionToTranslate) {
Map<SimpleNode, Map<DbRelationship, List<DbPathMarker>>> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -113,6 +118,35 @@ Node translate(Expression qualifier) {
return rootNode;
}

/**
* Preprocess complex expressions that ExpressionFactory can't handle at the creation time.
* <br>
* 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)) {
Expand Down

0 comments on commit 553e0ab

Please sign in to comment.