All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.blazebit.persistence.impl.ResolvingQueryGenerator Maven / Gradle / Ivy

There is a newer version: 1.6.12
Show newest version
/*
 * Copyright 2014 - 2020 Blazebit.
 *
 * Licensed 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
 *
 *      http://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 com.blazebit.persistence.impl;

import com.blazebit.persistence.BaseFinalSetOperationBuilder;
import com.blazebit.persistence.impl.function.exist.ExistFunction;
import com.blazebit.persistence.parser.EntityMetamodel;
import com.blazebit.persistence.parser.SimpleQueryGenerator;
import com.blazebit.persistence.parser.expression.AggregateExpression;
import com.blazebit.persistence.parser.expression.ArithmeticExpression;
import com.blazebit.persistence.parser.expression.ArrayExpression;
import com.blazebit.persistence.parser.expression.Expression;
import com.blazebit.persistence.parser.expression.FunctionExpression;
import com.blazebit.persistence.parser.expression.ListIndexExpression;
import com.blazebit.persistence.parser.expression.MapKeyExpression;
import com.blazebit.persistence.parser.expression.MapValueExpression;
import com.blazebit.persistence.parser.expression.NullExpression;
import com.blazebit.persistence.parser.expression.OrderByItem;
import com.blazebit.persistence.parser.expression.ParameterExpression;
import com.blazebit.persistence.parser.expression.PathExpression;
import com.blazebit.persistence.parser.expression.PathReference;
import com.blazebit.persistence.parser.expression.QualifiedExpression;
import com.blazebit.persistence.parser.expression.SubqueryExpression;
import com.blazebit.persistence.parser.expression.TreatExpression;
import com.blazebit.persistence.parser.expression.WindowDefinition;
import com.blazebit.persistence.parser.predicate.BetweenPredicate;
import com.blazebit.persistence.parser.predicate.CompoundPredicate;
import com.blazebit.persistence.parser.predicate.EqPredicate;
import com.blazebit.persistence.parser.predicate.ExistsPredicate;
import com.blazebit.persistence.parser.predicate.GePredicate;
import com.blazebit.persistence.parser.predicate.GtPredicate;
import com.blazebit.persistence.parser.predicate.InPredicate;
import com.blazebit.persistence.parser.predicate.IsEmptyPredicate;
import com.blazebit.persistence.parser.predicate.IsNullPredicate;
import com.blazebit.persistence.parser.predicate.LePredicate;
import com.blazebit.persistence.parser.predicate.LikePredicate;
import com.blazebit.persistence.parser.predicate.LtPredicate;
import com.blazebit.persistence.parser.predicate.MemberOfPredicate;
import com.blazebit.persistence.parser.predicate.Predicate;
import com.blazebit.persistence.parser.predicate.PredicateQuantifier;
import com.blazebit.persistence.parser.util.JpaMetamodelUtils;
import com.blazebit.persistence.parser.util.TypeConverter;
import com.blazebit.persistence.parser.util.TypeUtils;
import com.blazebit.persistence.spi.DbmsDialect;
import com.blazebit.persistence.spi.JpaProvider;
import com.blazebit.persistence.spi.JpqlFunction;

import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.EmbeddableType;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.IdentifiableType;
import javax.persistence.metamodel.ManagedType;
import javax.persistence.metamodel.MapAttribute;
import javax.persistence.metamodel.Type;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 *
 * @author Moritz Becker
 * @author Christian Beikov
 * @since 1.0.0
 */
public class ResolvingQueryGenerator extends SimpleQueryGenerator {

    private static final Set BUILT_IN_FUNCTIONS;

    protected String aliasPrefix;
    private boolean resolveSelectAliases = true;
    private boolean externalRepresentation;
    private boolean quantifiedPredicate;
    private Set renderedJoinNodes;
    private ClauseType clauseType;
    private Map treatedJoinNodesForConstraints;
    private final EntityMetamodel entityMetamodel;
    private final Set currentlyResolvingAliases;
    private final AliasManager aliasManager;
    private final ParameterManager parameterManager;
    private final AssociationParameterTransformerFactory parameterTransformerFactory;
    private final JpaProvider jpaProvider;
    private final DbmsDialect dbmsDialect;
    private final Map registeredFunctions;
    private final Map registeredFunctionsNames;

    static {
        Set functions = new HashSet<>();
        functions.add("concat");
        functions.add("substring");
        functions.add("lower");
        functions.add("upper");
        functions.add("length");
        functions.add("locate");
        functions.add("abs");
        functions.add("sqrt");
        functions.add("mod");
        functions.add("coalesce");
        functions.add("nullif");
        functions.add("size");
        functions.add("type");
        functions.add("avg");
        functions.add("max");
        functions.add("min");
        functions.add("sum");
        functions.add("count");
        functions.add("current_date");
        functions.add("current_time");
        functions.add("current_timestamp");
        BUILT_IN_FUNCTIONS = functions;
    }

    public ResolvingQueryGenerator(EntityMetamodel entityMetamodel, AliasManager aliasManager, ParameterManager parameterManager, AssociationParameterTransformerFactory parameterTransformerFactory, JpaProvider jpaProvider, DbmsDialect dbmsDialect, Map registeredFunctions) {
        this.entityMetamodel = entityMetamodel;
        this.aliasManager = aliasManager;
        this.parameterManager = parameterManager;
        this.parameterTransformerFactory = parameterTransformerFactory;
        this.jpaProvider = jpaProvider;
        this.dbmsDialect = dbmsDialect;
        this.registeredFunctions = registeredFunctions;
        this.currentlyResolvingAliases = new HashSet<>();
        this.registeredFunctionsNames = new HashMap<>(registeredFunctions.size());
        for (Map.Entry registeredFunctionEntry : registeredFunctions.entrySet()) {
            registeredFunctionsNames.put(registeredFunctionEntry.getKey().toLowerCase(), registeredFunctionEntry.getKey());
        }
    }

    @Override
    public void generate(Expression expression) {
        // Top level null expressions might need to be rendered as nullif because of lacking support in most JPA provider query languages
        if (expression instanceof NullExpression) {
            // The SET clause always needs the NULL literal
            if (clauseType != ClauseType.SET) {
                if (externalRepresentation) {
                    sb.append("NULL");
                } else {
                    sb.append(jpaProvider.getNullExpression());
                }
                return;
            }
        }
        expression.accept(this);
    }

    @Override
    public void visit(ListIndexExpression expression) {
        PathExpression path = expression.getPath();
        String deReferenceFunction = null;
        if (!externalRepresentation && path.getPathReference() != null && (deReferenceFunction = ((JoinNode) path.getPathReference().getBaseNode()).getDeReferenceFunction()) != null)  {
            sb.append(deReferenceFunction);
        }
        sb.append("INDEX(");
        path.accept(this);
        sb.append(')');
        if (deReferenceFunction != null) {
            sb.append(')');
        }
    }

    @Override
    public void visit(MapKeyExpression expression) {
        PathExpression path = expression.getPath();
        String deReferenceFunction = null;
        if (!externalRepresentation && path.getPathReference() != null && (deReferenceFunction = ((JoinNode) path.getPathReference().getBaseNode()).getDeReferenceFunction()) != null)  {
            sb.append(deReferenceFunction);
        }
        sb.append("KEY(");
        path.accept(this);
        sb.append(')');
        if (deReferenceFunction != null) {
            sb.append(')');
        }
    }

    @Override
    public void visit(MapValueExpression expression) {
        // NOTE: We decide if we need a VALUE wrapper during the rendering of the path expression anyway
        expression.getPath().accept(this);
    }

    @Override
    public void visit(FunctionExpression expression) {
        if (externalRepresentation && expression.getRealArgument() != null) {
            expression.getRealArgument().accept(this);
            return;
        }
        // A type constraint of a treat expression from within an aggregate may not "escape" the aggregate
        Map oldTreatedJoinNodesForConstraints = treatedJoinNodesForConstraints;
        if (expression instanceof AggregateExpression) {
            treatedJoinNodesForConstraints = null;
        }

        if (com.blazebit.persistence.parser.util.ExpressionUtils.isOuterFunction(expression)) {
            // Outer can only have paths, no need to set expression context for parameters
            expression.getExpressions().get(0).accept(this);
        } else if (ExpressionUtils.isFunctionFunctionExpression(expression)) {
            final List arguments = expression.getExpressions();
            final String literalFunctionName = ExpressionUtils.unwrapStringLiteral(arguments.get(0).toString());
            String resolvedFunctionName = resolveRenderedFunctionName(literalFunctionName);

            final List argumentsWithoutFunctionName;
            if (arguments.size() > 1) {
                argumentsWithoutFunctionName = arguments.subList(1, arguments.size());
            } else {
                argumentsWithoutFunctionName = Collections.emptyList();
            }
            renderFunctionFunction(resolvedFunctionName, argumentsWithoutFunctionName, expression.getResolvedWindowDefinition());
        } else if (isCountStarFunction(expression)) {
            renderCountStar(expression.getResolvedWindowDefinition());
        } else if (BUILT_IN_FUNCTIONS.contains(expression.getFunctionName().toLowerCase()) && expression.getResolvedWindowDefinition() == null) {
            super.visit(expression);
        } else {
            renderFunctionFunction(resolveRenderedFunctionName(expression.getFunctionName()), expression.getExpressions(), expression.getResolvedWindowDefinition());
        }

        treatedJoinNodesForConstraints = oldTreatedJoinNodesForConstraints;
    }

    private String resolveRenderedFunctionName(String literalFunctionName) {
        String registeredFunctionName = registeredFunctionsNames.get(literalFunctionName.toLowerCase());
        return registeredFunctionName == null ? literalFunctionName : registeredFunctionName;
    }

    @SuppressWarnings("unchecked")
    protected void renderCountStar(WindowDefinition windowDefinition) {
        if (jpaProvider.supportsCustomFunctions()) {
            if (jpaProvider.supportsCountStar() && windowDefinition == null) {
                sb.append("COUNT(*)");
            } else if (windowDefinition != null) {
                renderFunctionFunction(resolveRenderedFunctionName("WINDOW_COUNT"), (List) (List) Collections.emptyList(), windowDefinition);
            } else {
                renderFunctionFunction(resolveRenderedFunctionName("COUNT_STAR"), (List) (List) Collections.emptyList(), null);
            }
        } else {
            if (windowDefinition != null) {
                throw new IllegalArgumentException("JPA provider does not support custom function invocation!");
            }
            sb.append("COUNT(1)");
        }
    }

    @Override
    public void visit(SubqueryExpression expression) {
        if (!externalRepresentation && expression.getSubquery() instanceof SubqueryInternalBuilder) {
            final AbstractCommonQueryBuilder subquery = (AbstractCommonQueryBuilder) expression.getSubquery();
            subquery.prepareAndCheck();
            final boolean hasFirstResult = subquery.getFirstResult() != 0;
            final boolean hasMaxResults = subquery.getMaxResults() != Integer.MAX_VALUE;
            final boolean hasLimit = hasFirstResult || hasMaxResults;
            final boolean hasSetOperations = subquery instanceof BaseFinalSetOperationBuilder;
            final boolean hasEntityFunctions = subquery.joinManager.hasEntityFunctions();
            final boolean isSimple = !hasLimit && !hasSetOperations && !hasEntityFunctions;

            if (isSimple) {
                sb.append('(');
                subquery.buildBaseQueryString(sb, externalRepresentation, null);
                sb.append(')');
            } else {
                if (!externalRepresentation) {
                    sb.append('(');
                }
                Expression subqueryExpression = subquery.asExpression(externalRepresentation, quantifiedPredicate);
                if (!externalRepresentation && subqueryExpression instanceof SubqueryExpression) {
                    sb.append(((SubqueryExpression) subqueryExpression).getSubquery().getQueryString());
                } else {
                    subqueryExpression.accept(this);
                }
                if (!externalRepresentation) {
                    sb.append(')');
                }
            }
        } else {
            sb.append('(');
            sb.append(expression.getSubquery().getQueryString());
            sb.append(')');
        }
    }

    @Override
    protected boolean isSimpleSubquery(SubqueryExpression expression) {
        if (!externalRepresentation && expression.getSubquery() instanceof SubqueryInternalBuilder) {
            final AbstractCommonQueryBuilder subquery = (AbstractCommonQueryBuilder) expression.getSubquery();
            final boolean hasFirstResult = subquery.getFirstResult() != 0;
            final boolean hasMaxResults = subquery.getMaxResults() != Integer.MAX_VALUE;
            final boolean hasLimit = hasFirstResult || hasMaxResults;
            final boolean hasSetOperations = subquery instanceof BaseFinalSetOperationBuilder;
            final boolean hasEntityFunctions = subquery.joinManager.hasEntityFunctions();
            return !hasLimit && !hasSetOperations && !hasEntityFunctions && !subquery.joinManager.hasLateInlineNodes();
        }
        return super.isSimpleSubquery(expression);
    }

    protected void renderFunctionFunction(String functionName, List arguments, WindowDefinition windowDefinition) {
        ParameterRenderingMode oldParameterRenderingMode = setParameterRenderingMode(ParameterRenderingMode.PLACEHOLDER);
        int size = arguments.size();
        if (registeredFunctions.containsKey(functionName)) {
            sb.append(jpaProvider.getCustomFunctionInvocation(functionName, windowDefinition == null || windowDefinition.isEmpty() ? size : size + 1));
            if (size == 0) {
                visitWindowDefinition(windowDefinition);
            } else {
                arguments.get(0).accept(this);
                for (int i = 1; i < size; i++) {
                    sb.append(",");
                    arguments.get(i).accept(this);
                }
                if (windowDefinition != null && !windowDefinition.isEmpty()) {
                    sb.append(",");
                    visitWindowDefinition(windowDefinition);
                }
            }
            sb.append(')');
        } else if (jpaProvider.supportsJpa21()) {
            // Add the JPA 2.1 Function style function
            sb.append("FUNCTION('");
            sb.append(functionName);
            sb.append('\'');

            for (int i = 0; i < size; i++) {
                sb.append(',');
                arguments.get(i).accept(this);
            }
            if (windowDefinition != null && !windowDefinition.isEmpty()) {
                sb.append(",");
                visitWindowDefinition(windowDefinition);
            }

            sb.append(')');
        } else {
            throw new IllegalArgumentException("Unknown function [" + functionName + "] is used!");
        }
        setParameterRenderingMode(oldParameterRenderingMode);
    }

    @Override
    protected void visitWindowDefinition(WindowDefinition windowDefinition) {
        if (windowDefinition != null) {
            Predicate filterPredicate = windowDefinition.getFilterPredicate();
            if (filterPredicate != null) {
                sb.append("'FILTER',CASE WHEN ");
                filterPredicate.accept(this);
                sb.append(" THEN 1 ELSE 0 END");
            }

            List partitionExpressions = windowDefinition.getPartitionExpressions();

            int size = partitionExpressions.size();
            if (size != 0) {
                if (filterPredicate != null) {
                    sb.append(",");
                }
                sb.append("'PARTITION BY',");
                partitionExpressions.get(0).accept(this);
                for (int i = 1; i < size; i++) {
                    sb.append(",");
                    partitionExpressions.get(i).accept(this);
                }
            }

            List orderByExpressions = windowDefinition.getOrderByExpressions();
            size = orderByExpressions.size();
            if (size != 0) {
                if (filterPredicate != null || partitionExpressions.size() != 0) {
                    sb.append(",");
                }
                sb.append("'ORDER BY',");
                visit(orderByExpressions.get(0));
                for (int i = 1; i < size; i++) {
                    sb.append(",");
                    visit(orderByExpressions.get(i));
                }
            }

            if (windowDefinition.getFrameMode() != null) {
                if (filterPredicate != null || partitionExpressions.size() != 0 || orderByExpressions.size() != 0) {
                    sb.append(",");
                }
                sb.append('\'');
                sb.append(windowDefinition.getFrameMode().name());
                sb.append("'");
                if (windowDefinition.getFrameEndType() != null) {
                    sb.append(",'BETWEEN'");
                }

                if (windowDefinition.getFrameStartExpression() != null) {
                    sb.append(",");
                    windowDefinition.getFrameStartExpression().accept(this);
                }

                sb.append(",'");
                sb.append(getFrameType(windowDefinition.getFrameStartType()));
                sb.append("'");

                if (windowDefinition.getFrameEndType() != null) {
                    sb.append(",'AND'");
                    if (windowDefinition.getFrameEndExpression() != null) {
                        sb.append(",");
                        windowDefinition.getFrameEndExpression().accept(this);
                    }

                    sb.append(",'");
                    sb.append(getFrameType(windowDefinition.getFrameEndType()));
                    sb.append("'");
                }

                if (windowDefinition.getFrameExclusionType() != null) {
                    sb.append(",'");
                    sb.append(getFrameExclusionType(windowDefinition.getFrameExclusionType()));
                    sb.append("'");
                }
            }
        }
    }

    private void visit(OrderByItem orderByItem) {
        orderByItem.getExpression().accept(this);
        sb.append(",'");
        sb.append(orderByItem.isAscending() ? "ASC" : "DESC");
        sb.append(orderByItem.isNullFirst() ? " NULLS FIRST" : " NULLS LAST");
        sb.append("'");
    }

    private boolean isCountStarFunction(FunctionExpression expression) {
        return expression instanceof AggregateExpression && expression.getExpressions().isEmpty()
            && "COUNT".equalsIgnoreCase(expression.getFunctionName());
    }

    @Override
    public void visit(TreatExpression expression) {
        if (jpaProvider.supportsRootTreat()) {
            super.visit(expression);
        } else if (jpaProvider.supportsSubtypePropertyResolving()) {
            // NOTE: this might be wrong when having multiple same named properties
            expression.getExpression().accept(this);
        } else {
            throw new IllegalArgumentException("Can not render treat expression[" + expression.toString() + "] as the JPA provider does not support it!");
        }
    }

    @Override
    public void visit(PathExpression expression) {
        if (resolveSelectAliases && expression.getExpressions().size() == 1) {
            AliasInfo aliasInfo;
            String potentialAlias = expression.getExpressions().get(0).toString();
            try {
                if (currentlyResolvingAliases.add(potentialAlias) && (aliasInfo = aliasManager.getAliasInfo(potentialAlias)) != null) {
                    if (aliasInfo instanceof SelectInfo) {
                        SelectInfo selectAliasInfo = (SelectInfo) aliasInfo;
                        if (selectAliasInfo.getExpression() instanceof PathExpression) {
                            PathExpression aliasedExpression = (PathExpression) selectAliasInfo.getExpression();
                            boolean collectionKeyPath = aliasedExpression.isCollectionQualifiedPath();
                            boolean usedInCollectionFunction = aliasedExpression.isUsedInCollectionFunction();
                            aliasedExpression.setCollectionQualifiedPath(expression.isCollectionQualifiedPath());
                            aliasedExpression.setUsedInCollectionFunction(expression.isUsedInCollectionFunction());
                            try {
                                selectAliasInfo.getExpression().accept(this);
                            } finally {
                                aliasedExpression.setCollectionQualifiedPath(collectionKeyPath);
                                aliasedExpression.setUsedInCollectionFunction(usedInCollectionFunction);
                            }
                        } else {
                            selectAliasInfo.getExpression().accept(this);
                        }
                        return;
                    }
                }
            } finally {
                currentlyResolvingAliases.remove(potentialAlias);
            }
        }
        JoinNode baseNode;
        String field;
        if ((baseNode = (JoinNode) expression.getBaseNode()) == null) {
            super.visit(expression);
        } else {
            String collectionValueFunction = jpaProvider.getCollectionValueFunction();
            if ((field = expression.getField()) == null) {
                if (expression.isUsedInCollectionFunction() || renderAbsolutePath(expression)) {
                    super.visit(expression);
                } else {
                    // NOTE: Hibernate uses the column from a join table if VALUE is used which is wrong, so drop the VALUE here
                    boolean valueFunction = collectionValueFunction != null && needsValueFunction(expression, baseNode, field);

                    if (valueFunction) {
                        sb.append(collectionValueFunction);
                        sb.append('(');
                    }

                    if (aliasPrefix != null) {
                        sb.append(aliasPrefix);
                    }

                    baseNode.appendAlias(sb, externalRepresentation);

                    if (valueFunction) {
                        sb.append(')');
                    }
                }
            } else {
                List treatedJoinNodes = baseNode.getJoinNodesForTreatConstraint();
                if (treatedJoinNodesForConstraints != null) {
                    for (JoinNode node : treatedJoinNodes) {
                        treatedJoinNodesForConstraints.put(node, Boolean.FALSE);
                    }
                }

                ManagedType baseNodeType = baseNode.getManagedType();
                boolean addTypeCaseWhen = !treatedJoinNodes.isEmpty()
                        && baseNodeType instanceof EntityType
                        && jpaProvider.needsTypeConstraintForColumnSharing()
                        && jpaProvider.isColumnShared((EntityType) baseNodeType, field);
                if (addTypeCaseWhen) {
                    sb.append("CASE WHEN ");
                    boolean first = true;

                    for (int i = 0; i < treatedJoinNodes.size(); i++) {
                        JoinNode treatedJoinNode = treatedJoinNodes.get(i);

                        // When the JPA provider supports rendering treat joins and we have a treat join node
                        // we skip the type constraint as that is already applied through the join
                        if (jpaProvider.supportsTreatJoin() && treatedJoinNode.isTreatJoinNode()) {
                            continue;
                        }

                        if (first) {
                            first = false;
                        } else {
                            sb.append(" AND ");
                        }

                        sb.append("TYPE(");
                        sb.append(treatedJoinNode.getAlias());
                        sb.append(") IN (");
                        for (EntityType entitySubtype : entityMetamodel.getEntitySubtypes(treatedJoinNode.getTreatType())) {
                            sb.append(entitySubtype.getName());
                            sb.append(", ");
                        }

                        sb.setLength(sb.length() - 2);
                        sb.append(')');
                    }

                    sb.append(" THEN ");
                }

                boolean valueFunction = collectionValueFunction != null && needsValueFunction(expression, baseNode, field);
                // NOTE: There is no need to check for whether the JPA provider support implicit downcasting here
                // If it didn't, the query building would have already failed before. Here we just decide whether to render the treat or not
                boolean renderTreat = jpaProvider.supportsRootTreat();

                if (valueFunction) {
                    sb.append(collectionValueFunction);
                    sb.append('(');

                    if (aliasPrefix != null) {
                        sb.append(aliasPrefix);
                    }

                    baseNode.appendAlias(sb, renderTreat, externalRepresentation);
                    sb.append(')');
                    sb.append(".").append(field);
                } else {
                    if (aliasPrefix != null) {
                        sb.append(aliasPrefix);
                    }

                    baseNode.appendDeReference(sb, field, renderTreat, externalRepresentation, jpaProvider.needsElementCollectionIdCutoff());
                }

                if (addTypeCaseWhen) {
                    sb.append(" END");
                }
            }
        }
    }

    private boolean needsValueFunction(PathExpression expression, JoinNode baseNode, String field) {
        return !expression.isCollectionQualifiedPath() && baseNode.getParentTreeNode() != null && baseNode.getParentTreeNode().isMap() && (field == null || jpaProvider.supportsCollectionValueDereference());
    }

    private boolean renderAbsolutePath(PathExpression expression) {
        JoinNode baseNode = (JoinNode) expression.getBaseNode();
        return renderedJoinNodes != null && !renderedJoinNodes.contains(baseNode);
    }

    @Override
    protected boolean needsParenthesisForCaseResult(Expression expression) {
        // Hibernate parser complains about arithmetic expression in the then clause, only works with parenthesis
        // Since other JPA providers don't have a problem with parenthesis, we don't introduce another property in the JpaProvider interface for this
        return expression instanceof ArithmeticExpression;
    }

    @Override
    protected String getBooleanConditionalExpression(boolean value) {
        return jpaProvider.getBooleanConditionalExpression(value);
    }

    @Override
    protected String getBooleanExpression(boolean value) {
        return jpaProvider.getBooleanExpression(value);
    }

    @Override
    protected String escapeCharacter(char character) {
        return jpaProvider.escapeCharacter(character);
    }

    @Override
    public void visit(ParameterExpression expression) {
        // Workaround for hibernate
        // TODO: Remove when HHH-7407 is fixed
        boolean needsBrackets = jpaProvider.needsBracketsForListParameter() && expression.isCollectionValued();

        if (needsBrackets) {
            sb.append('(');
        }

        super.visit(expression);

        if (needsBrackets) {
            sb.append(')');
        }
    }

    @Override
    protected Set getSupportedEnumTypes() {
        return entityMetamodel.getEnumTypes().keySet();
    }

    @Override
    protected String getLiteralParameterValue(ParameterExpression expression) {
        Object value = expression.getValue();
        if (value == null) {
            value = parameterManager.getParameterValue(expression.getName());
        }

        if (value != null) {
            final TypeConverter converter = (TypeConverter) TypeUtils.getConverter(value.getClass(), getSupportedEnumTypes());
            if (converter != null) {
                // We can't render enum values as literals directly, only in the context of a predicate, so we need the BooleanLiteralRenderingContext.PLAIN
                if (value instanceof Enum) {
                    if (getBooleanLiteralRenderingContext() == BooleanLiteralRenderingContext.PLAIN) {
                        return converter.toString(value);
                    }
                } else if (jpaProvider.supportsTemporalLiteral() || !TypeUtils.isTemporalConverter(converter)) {
                    return converter.toString(value);
                }
            }
        }

        return null;
    }

    public void setResolveSelectAliases(boolean replaceSelectAliases) {
        this.resolveSelectAliases = replaceSelectAliases;
    }

    public boolean isResolveSelectAliases() {
        return resolveSelectAliases;
    }

    public void setAliasPrefix(String aliasPrefix) {
        this.aliasPrefix = aliasPrefix;
    }

    public void addAlias(String alias) {
        currentlyResolvingAliases.add(alias);
    }

    public void removeAlias(String alias) {
        currentlyResolvingAliases.remove(alias);
    }

    public void setRenderedJoinNodes(Set renderedJoinNodes) {
        this.renderedJoinNodes = renderedJoinNodes;
    }

    public ClauseType getClauseType() {
        return clauseType;
    }

    public void setClauseType(ClauseType clauseType) {
        this.clauseType = clauseType;
    }

    public boolean isExternalRepresentation() {
        return externalRepresentation;
    }

    public void setExternalRepresentation(boolean externalRepresentation) {
        this.externalRepresentation = externalRepresentation;
    }

    @Override
    public void visit(ArrayExpression expression) {
    }

    @Override
    public void visit(InPredicate predicate) {
        boolean quantifiedPredicate = this.quantifiedPredicate;
        this.quantifiedPredicate = true;
        if (predicate.getRight().size() == 1 && jpaProvider.needsAssociationToIdRewriteInOnClause() && clauseType == ClauseType.JOIN) {
            Expression right = predicate.getRight().get(0);
            if (right instanceof ParameterExpression) {
                ParameterExpression parameterExpression = (ParameterExpression) right;
                @SuppressWarnings("unchecked")
                Type associationType = getAssociationType(predicate.getLeft(), right);
                // If the association type is a entity type, we transform it
                if (associationType instanceof EntityType) {
                    renderEquality(predicate.getLeft(), right, predicate.isNegated(), PredicateQuantifier.ONE);
                } else {
                    super.visit(predicate);
                }
            } else if (right instanceof PathExpression) {
                renderEquality(predicate.getLeft(), right, predicate.isNegated(), PredicateQuantifier.ONE);
            } else {
                super.visit(predicate);
            }
        } else {
            super.visit(predicate);
        }
        this.quantifiedPredicate = quantifiedPredicate;
    }

    private Type getAssociationType(Expression expression1, Expression expression2) {
        if (expression1 instanceof PathExpression) {
            return ((PathExpression) expression1).getPathReference().getType();
        }

        return ((PathExpression) expression2).getPathReference().getType();
    }

    @Override
    public void visit(final EqPredicate predicate) {
        boolean quantifiedPredicate = this.quantifiedPredicate;
        this.quantifiedPredicate = predicate.getQuantifier() != PredicateQuantifier.ONE;
        renderEquality(predicate.getLeft(), predicate.getRight(), predicate.isNegated(), predicate.getQuantifier());
        if (predicate.isNegated()) {
            flipTreatedJoinNodeConstraints();
        }
        this.quantifiedPredicate = quantifiedPredicate;
    }

    private void renderEquality(Expression left, Expression right, boolean negated, PredicateQuantifier quantifier) {
        final String operator;
        if (negated) {
            operator = " <> ";
        } else {
            operator = " = ";
        }

        BooleanLiteralRenderingContext oldBooleanLiteralRenderingContext = setBooleanLiteralRenderingContext(BooleanLiteralRenderingContext.PLAIN);
        // TODO: Currently we assume that types can be inferred, and render parameters through but e.g. ":param1 = :param2" will fail
        ParameterRenderingMode oldParameterRenderingMode = setParameterRenderingMode(ParameterRenderingMode.PLACEHOLDER);

        Expression expressionToSplit = needsEmbeddableSplitting(left, right);

        if (jpaProvider.needsAssociationToIdRewriteInOnClause() && clauseType == ClauseType.JOIN) {
            boolean rewritten = renderAssociationIdIfPossible(left);
            sb.append(operator);
            if (quantifier != PredicateQuantifier.ONE) {
                sb.append(quantifier.toString());
            }
            rewritten |= renderAssociationIdIfPossible(right);
            if (rewritten) {
                rewriteToIdParam(left);
                rewriteToIdParam(right);
            }
        } else {
            if (expressionToSplit == null || dbmsDialect.supportsAnsiRowValueConstructor() || !(left instanceof ParameterExpression) && !(right instanceof ParameterExpression)) {
                left.accept(this);
                sb.append(operator);
                if (quantifier != PredicateQuantifier.ONE) {
                    sb.append(quantifier.toString());
                }
                right.accept(this);
            } else {
                // We split the path and the parameter expression accordingly
                // TODO: Try to handle map key expressions, although no JPA provider supports de-referencing map keys
                PathExpression pathExpression = (PathExpression) expressionToSplit;
                ParameterExpression parameterExpression;
                if (left instanceof ParameterExpression) {
                    parameterExpression = (ParameterExpression) left;
                } else {
                    parameterExpression = (ParameterExpression) right;
                }
                PathReference pathReference = pathExpression.getPathReference();
                EmbeddableType embeddableType = (EmbeddableType) pathReference.getType();
                String parameterName = parameterExpression.getName();
                Map> parameterAccessPaths = new HashMap<>();
                ParameterManager.ParameterImpl parameter = parameterManager.getParameter(parameterName);
                sb.append('(');
                for (Attribute attribute : embeddableType.getAttributes()) {
                    ((JoinNode) pathReference.getBaseNode()).appendDeReference(sb, pathReference.getField() + "." + attribute.getName(), externalRepresentation);
                    String embeddedPropertyName = attribute.getName();
                    String subParamName = "_" + parameterName + "_" + embeddedPropertyName.replace('.', '_');
                    sb.append(operator);
                    sb.append(":").append(subParamName);
                    if (parameter.getTranformer() == null) {
                        parameterManager.registerParameterName(subParamName, false, null, null);
                    }
                    parameterAccessPaths.put(subParamName, Arrays.asList(embeddedPropertyName.split("\\.")));

                    sb.append(" AND ");
                }
                sb.setLength(sb.length() - " AND ".length());
                sb.append(')');

                if (parameter.getTranformer() == null) {
                    parameter.setTranformer(new SplittingParameterTransformer(parameterManager, entityMetamodel, embeddableType.getJavaType(), parameterAccessPaths));
                }
            }
        }
        setBooleanLiteralRenderingContext(oldBooleanLiteralRenderingContext);
        setParameterRenderingMode(oldParameterRenderingMode);
    }

    private Expression needsEmbeddableSplitting(Expression left, Expression right) {
        Expression l = needsEmbeddableSplitting(left);
        if (l != null) {
            return l;
        }
        return needsEmbeddableSplitting(right);
    }

    private Expression needsEmbeddableSplitting(Expression expr) {
        if (expr instanceof MapKeyExpression) {
            PathReference pathReference = ((QualifiedExpression) expr).getPath().getPathReference();
            if (pathReference != null) {
                JoinNode joinNode = (JoinNode) pathReference.getBaseNode();
                if (joinNode.getDeReferenceFunction() != null && ((MapAttribute) joinNode.getParentTreeNode().getAttribute()).getKeyType() instanceof EmbeddableType) {
                    return expr;
                }
            }
        } else if (expr instanceof PathExpression) {
            PathReference pathReference = ((PathExpression) expr).getPathReference();
            if (pathReference != null) {
                JoinNode joinNode = (JoinNode) pathReference.getBaseNode();
                if (joinNode.getDeReferenceFunction() != null && pathReference.getType() instanceof EmbeddableType) {
                    // We need to split this embeddable
                    return expr;
                }
            }
        }
        return null;
    }

    private boolean renderAssociationIdIfPossible(Expression expression) {
        expression.accept(this);

        if (expression instanceof PathExpression) {
            PathExpression pathExpression = (PathExpression) expression;

            // Before Hibernate 5.1 there was a "broken" possibility to use multiple join nodes in the WITH clause
            // That involves only suffixing association paths so that predicates look like "p = other.relation.id"
            if (!jpaProvider.needsBrokenAssociationToIdRewriteInOnClause() || pathExpression.getBaseNode() != null && pathExpression.getField() != null) {
                Type pathType = pathExpression.getPathReference().getType();
                if (pathType instanceof ManagedType && JpaMetamodelUtils.isIdentifiable((ManagedType) pathType)) {
                    String idName = JpaMetamodelUtils.getSingleIdAttribute((IdentifiableType) pathType).getName();
                    sb.append('.');
                    sb.append(idName);
                    return true;
                }
            }
        }

        return false;
    }

    private void rewriteToIdParam(Expression expression) {
        if (!(expression instanceof ParameterExpression)) {
            return;
        }
        ParameterExpression parameterExpression = (ParameterExpression) expression;
        ParameterManager.ParameterImpl param = (ParameterManager.ParameterImpl) parameterManager.getParameter(parameterExpression.getName());
        param.setTranformer(parameterTransformerFactory.getToIdTransformer());
    }

    @Override
    public void visit(IsNullPredicate predicate) {
        // Null check does not require a type to be known
        ParameterRenderingMode oldParameterRenderingMode = setParameterRenderingMode(ParameterRenderingMode.PLACEHOLDER);
        predicate.getExpression().accept(this);
        if (predicate.isNegated()) {
            sb.append(" IS NOT NULL");
            flipTreatedJoinNodeConstraints();
        } else {
            sb.append(" IS NULL");
        }
        setParameterRenderingMode(oldParameterRenderingMode);
    }

    @Override
    public void visit(IsEmptyPredicate predicate) {
        super.visit(predicate);
        if (predicate.isNegated()) {
            flipTreatedJoinNodeConstraints();
        }
    }

    @Override
    public void visit(MemberOfPredicate predicate) {
        super.visit(predicate);
        if (predicate.isNegated()) {
            flipTreatedJoinNodeConstraints();
        }
    }

    @Override
    public void visit(LikePredicate predicate) {
        super.visit(predicate);
        if (predicate.isNegated()) {
            flipTreatedJoinNodeConstraints();
        }
    }

    @Override
    public void visit(BetweenPredicate predicate) {
        super.visit(predicate);
        if (predicate.isNegated()) {
            flipTreatedJoinNodeConstraints();
        }
    }

    @Override
    public void visit(ExistsPredicate predicate) {
        SubqueryExpression subqueryExpression;
        if (predicate.getExpression() instanceof SubqueryExpression && !isSimpleSubquery(subqueryExpression = (SubqueryExpression) predicate.getExpression())) {
            if (externalRepresentation) {
                if (predicate.isNegated()) {
                    sb.append("NOT EXISTS ");
                } else {
                    sb.append("EXISTS ");
                }
                sb.append('(');
                predicate.getExpression().accept(this);
                sb.append(')');
            } else {
                sb.append("1 = ");
                sb.append(jpaProvider.getCustomFunctionInvocation(ExistFunction.FUNCTION_NAME, 1));
                subqueryExpression.accept(this);
                if (predicate.isNegated()) {
                    sb.append(",1");
                }
                sb.append(")");
            }
        } else {
            super.visit(predicate);
        }
        if (predicate.isNegated()) {
            flipTreatedJoinNodeConstraints();
        }
    }

    @Override
    public void visit(GtPredicate predicate) {
        boolean quantifiedPredicate = this.quantifiedPredicate;
        this.quantifiedPredicate = predicate.getQuantifier() != PredicateQuantifier.ONE;
        super.visit(predicate);
        if (predicate.isNegated()) {
            flipTreatedJoinNodeConstraints();
        }
        this.quantifiedPredicate = quantifiedPredicate;
    }

    @Override
    public void visit(GePredicate predicate) {
        boolean quantifiedPredicate = this.quantifiedPredicate;
        this.quantifiedPredicate = predicate.getQuantifier() != PredicateQuantifier.ONE;
        super.visit(predicate);
        if (predicate.isNegated()) {
            flipTreatedJoinNodeConstraints();
        }
        this.quantifiedPredicate = quantifiedPredicate;
    }

    @Override
    public void visit(LtPredicate predicate) {
        boolean quantifiedPredicate = this.quantifiedPredicate;
        this.quantifiedPredicate = predicate.getQuantifier() != PredicateQuantifier.ONE;
        super.visit(predicate);
        if (predicate.isNegated()) {
            flipTreatedJoinNodeConstraints();
        }
        this.quantifiedPredicate = quantifiedPredicate;
    }

    @Override
    public void visit(LePredicate predicate) {
        boolean quantifiedPredicate = this.quantifiedPredicate;
        this.quantifiedPredicate = predicate.getQuantifier() != PredicateQuantifier.ONE;
        super.visit(predicate);
        if (predicate.isNegated()) {
            flipTreatedJoinNodeConstraints();
        }
        this.quantifiedPredicate = quantifiedPredicate;
    }

    protected void visitWhenClauseCondition(Expression condition) {
        if (!(condition instanceof Predicate) || condition instanceof CompoundPredicate) {
            condition.accept(this);
            return;
        }

        Predicate p = (Predicate) condition;

        int startPosition = sb.length();
        Map oldTreatedJoinNodesForConstraints = treatedJoinNodesForConstraints;
        treatedJoinNodesForConstraints = new LinkedHashMap<>();

        p.accept(this);

        insertTreatJoinConstraint(startPosition, sb.length());
        treatedJoinNodesForConstraints = oldTreatedJoinNodesForConstraints;
    }

    @Override
    public void visit(CompoundPredicate predicate) {
        BooleanLiteralRenderingContext oldConditionalContext = setBooleanLiteralRenderingContext(BooleanLiteralRenderingContext.PREDICATE);
        ParameterRenderingMode oldParameterRenderingMode = setParameterRenderingMode(ParameterRenderingMode.PLACEHOLDER);
        boolean parenthesisRequired = predicate.getChildren().size() > 1;
        if (predicate.isNegated()) {
            sb.append("NOT ");
            if (parenthesisRequired) {
                sb.append('(');
            }
        }

        if (predicate.getChildren().size() == 1) {
            int startPosition = sb.length();
            Map oldTreatedJoinNodesForConstraints = treatedJoinNodesForConstraints;
            treatedJoinNodesForConstraints = new LinkedHashMap<>();

            predicate.getChildren().get(0).accept(this);

            insertTreatJoinConstraint(startPosition, sb.length());
            treatedJoinNodesForConstraints = oldTreatedJoinNodesForConstraints;
            return;
        }
        final int startLen = sb.length();
        final String operator = " " + predicate.getOperator().toString() + " ";
        final List children = predicate.getChildren();
        int size = children.size();
        Map oldTreatedJoinNodesForConstraints = treatedJoinNodesForConstraints;
        treatedJoinNodesForConstraints = new LinkedHashMap<>();
        for (int i = 0; i < size; i++) {
            int startPosition = sb.length();
            int endPosition;

            Predicate child = children.get(i);
            if (child instanceof CompoundPredicate && ((CompoundPredicate) child).getOperator() != predicate.getOperator() && !child.isNegated()) {
                sb.append("(");
                int len = sb.length();
                child.accept(this);
                // If the child was empty, we remove the opening parenthesis again
                if (len == sb.length()) {
                    sb.deleteCharAt(len - 1);
                    endPosition = sb.length();
                } else {
                    sb.append(")");
                    endPosition = sb.length();
                    sb.append(operator);
                }
            } else {
                child.accept(this);
                endPosition = sb.length();
                sb.append(operator);
            }

            insertTreatJoinConstraint(startPosition, endPosition);
            treatedJoinNodesForConstraints.clear();
        }

        treatedJoinNodesForConstraints = oldTreatedJoinNodesForConstraints;

        // Delete the last operator only if the children actually generated something
        if (startLen < sb.length()) {
            sb.delete(sb.length() - operator.length(), sb.length());
        }
        if (predicate.isNegated() && parenthesisRequired) {
            sb.append(')');
        }
        setBooleanLiteralRenderingContext(oldConditionalContext);
        setParameterRenderingMode(oldParameterRenderingMode);
    }

    private void flipTreatedJoinNodeConstraints() {
        if (treatedJoinNodesForConstraints != null) {
            for (Map.Entry entry : treatedJoinNodesForConstraints.entrySet()) {
                if (entry.getValue() == Boolean.TRUE) {
                    entry.setValue(Boolean.FALSE);
                } else {
                    entry.setValue(Boolean.TRUE);
                }
            }
        }
    }

    private boolean insertTreatJoinConstraint(int startPosition, int endPosition) {
        if (!treatedJoinNodesForConstraints.isEmpty()) {
            // Compute the set of entity type positive conjunct and negative disjunct restrictions per treated join node
            Map>[]> typeRestrictions = new LinkedHashMap<>(treatedJoinNodesForConstraints.size());
            for (Map.Entry entry : treatedJoinNodesForConstraints.entrySet()) {
                JoinNode node = entry.getKey();
                // When the JPA provider supports rendering treat joins and we have a treat join node
                // we skip the type constraint as that is already applied through the join
                if (jpaProvider.supportsTreatJoin() && node.isTreatJoinNode()) {
                    continue;
                }
                Boolean negatedContext = entry.getValue();
                JoinAliasInfo joinAliasInfo = entry.getKey().getAliasInfo();
                JoinNode treatedJoinNode = entry.getKey();
                if (joinAliasInfo instanceof TreatedJoinAliasInfo) {
                    treatedJoinNode = ((TreatedJoinAliasInfo) joinAliasInfo).getTreatedJoinNode();
                }
                Set>[] entityTypes = typeRestrictions.get(treatedJoinNode);
                if (entityTypes == null) {
                    entityTypes = new Set[2];
                    typeRestrictions.put(treatedJoinNode, entityTypes);
                }

                if (negatedContext == Boolean.TRUE) {
                    // Negated context
                    if (entityTypes[1] == null) {
                        entityTypes[1] = new HashSet<>(entityMetamodel.getEntitySubtypes((EntityType) treatedJoinNode.getBaseType()));
                    }
                    entityTypes[1].removeAll(entityMetamodel.getEntitySubtypes(node.getTreatType()));
                } else {
                    if (entityTypes[0] == null) {
                        entityTypes[0] = new HashSet<>();
                    }
                    entityTypes[0].addAll(entityMetamodel.getEntitySubtypes(node.getTreatType()));
                }
            }
            if (!typeRestrictions.isEmpty()) {
                StringBuilder treatConditionBuilder = new StringBuilder(typeRestrictions.size() * 40);
                treatConditionBuilder.append('(');
                for (Map.Entry>[]> entry : typeRestrictions.entrySet()) {
                    JoinNode node = entry.getKey();

                    if (entry.getValue()[1] != null && !entry.getValue()[1].isEmpty()) {
                        treatConditionBuilder.append("TYPE(");
                        treatConditionBuilder.append(node.getAlias());
                        treatConditionBuilder.append(") IN (");
                        for (EntityType entityType : entry.getValue()[1]) {
                            treatConditionBuilder.append(entityType.getName());
                            treatConditionBuilder.append(", ");
                        }
                        treatConditionBuilder.setLength(treatConditionBuilder.length() - 2);
                        treatConditionBuilder.append(")");
                        treatConditionBuilder.append(" OR ");
                    }

                    if (entry.getValue()[0] != null) {
                        treatConditionBuilder.append("TYPE(");
                        treatConditionBuilder.append(node.getAlias());
                        treatConditionBuilder.append(") IN (");
                        for (EntityType entityType : entry.getValue()[0]) {
                            treatConditionBuilder.append(entityType.getName());
                            treatConditionBuilder.append(", ");
                        }
                        treatConditionBuilder.setLength(treatConditionBuilder.length() - 2);
                        treatConditionBuilder.append(")");
                        treatConditionBuilder.append(" AND ");
                    }
                }

                // Because we always have the open parenthesis as first char
                if (treatConditionBuilder.length() > 1) {
                    sb.insert(endPosition, ')');
                    sb.insert(startPosition, treatConditionBuilder);
                    return true;
                }
            }
        }

        return false;
    }
}