io.micronaut.data.model.query.builder.sql.AbstractSqlLikeQueryBuilder2 Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of micronaut-data-model Show documentation
Show all versions of micronaut-data-model Show documentation
Data Repository Support for Micronaut
The newest version!
/*
* Copyright 2017-2020 original authors
*
* 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
*
* 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 io.micronaut.data.model.query.builder.sql;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.util.ArgumentUtils;
import io.micronaut.core.util.ArrayUtils;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.data.annotation.DataAnnotationUtils;
import io.micronaut.data.annotation.DataTransformer;
import io.micronaut.data.annotation.EntityRepresentation;
import io.micronaut.data.annotation.IgnoreWhere;
import io.micronaut.data.annotation.Join;
import io.micronaut.data.annotation.MappedEntity;
import io.micronaut.data.annotation.TypeRole;
import io.micronaut.data.annotation.Where;
import io.micronaut.data.annotation.repeatable.WhereSpecifications;
import io.micronaut.data.model.Association;
import io.micronaut.data.model.DataType;
import io.micronaut.data.model.Embedded;
import io.micronaut.data.model.JsonDataType;
import io.micronaut.data.model.PersistentAssociationPath;
import io.micronaut.data.model.PersistentEntity;
import io.micronaut.data.model.PersistentEntityUtils;
import io.micronaut.data.model.PersistentProperty;
import io.micronaut.data.model.PersistentPropertyPath;
import io.micronaut.data.model.Sort;
import io.micronaut.data.model.jpa.criteria.IExpression;
import io.micronaut.data.model.jpa.criteria.IPredicate;
import io.micronaut.data.model.jpa.criteria.ISelection;
import io.micronaut.data.model.jpa.criteria.PersistentEntityRoot;
import io.micronaut.data.model.jpa.criteria.PersistentEntitySubquery;
import io.micronaut.data.model.jpa.criteria.impl.AbstractPersistentEntityQuery;
import io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils;
import io.micronaut.data.model.jpa.criteria.impl.DefaultPersistentPropertyPath;
import io.micronaut.data.model.jpa.criteria.impl.ExpressionVisitor;
import io.micronaut.data.model.jpa.criteria.impl.IParameterExpression;
import io.micronaut.data.model.jpa.criteria.impl.SelectionVisitor;
import io.micronaut.data.model.jpa.criteria.impl.expression.BinaryExpression;
import io.micronaut.data.model.jpa.criteria.impl.expression.FunctionExpression;
import io.micronaut.data.model.jpa.criteria.impl.expression.IdExpression;
import io.micronaut.data.model.jpa.criteria.impl.expression.LiteralExpression;
import io.micronaut.data.model.jpa.criteria.impl.expression.SubqueryExpression;
import io.micronaut.data.model.jpa.criteria.impl.expression.UnaryExpression;
import io.micronaut.data.model.jpa.criteria.impl.predicate.BinaryPredicate;
import io.micronaut.data.model.jpa.criteria.impl.predicate.ConjunctionPredicate;
import io.micronaut.data.model.jpa.criteria.impl.predicate.DisjunctionPredicate;
import io.micronaut.data.model.jpa.criteria.impl.predicate.ExistsSubqueryPredicate;
import io.micronaut.data.model.jpa.criteria.impl.predicate.InPredicate;
import io.micronaut.data.model.jpa.criteria.impl.predicate.LikePredicate;
import io.micronaut.data.model.jpa.criteria.impl.predicate.NegatedPredicate;
import io.micronaut.data.model.jpa.criteria.impl.predicate.PredicateBinaryOp;
import io.micronaut.data.model.jpa.criteria.impl.selection.AliasedSelection;
import io.micronaut.data.model.jpa.criteria.impl.selection.CompoundSelection;
import io.micronaut.data.model.naming.NamingStrategy;
import io.micronaut.data.model.query.BindingParameter;
import io.micronaut.data.model.query.JoinPath;
import io.micronaut.data.model.query.QueryParameter;
import io.micronaut.data.model.query.builder.QueryBuilder2;
import io.micronaut.data.model.query.builder.QueryParameterBinding;
import io.micronaut.data.model.query.builder.QueryResult;
import io.micronaut.data.model.query.impl.AdvancedPredicateVisitor;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.Order;
import jakarta.persistence.criteria.ParameterExpression;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Selection;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.StringJoiner;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
import static io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils.requireProperty;
import static io.micronaut.data.model.query.builder.QueryBuilder.VARIABLE_PATTERN;
/**
* An abstract class for builders that build SQL-like queries.
*
* @author graemerocher
* @author Denis Stepanov
* @since 1.0.0
*/
@Internal
@SuppressWarnings("FileLength")
public abstract class AbstractSqlLikeQueryBuilder2 implements QueryBuilder2 {
public static final String ORDER_BY_CLAUSE = " ORDER BY ";
protected static final String SELECT_CLAUSE = "SELECT ";
protected static final String AS_CLAUSE = " AS ";
protected static final String FROM_CLAUSE = " FROM ";
protected static final String WHERE_CLAUSE = " WHERE ";
protected static final char COMMA = ',';
protected static final char CLOSE_BRACKET = ')';
protected static final char OPEN_BRACKET = '(';
protected static final char SPACE = ' ';
protected static final char DOT = '.';
protected static final String NOT = "NOT";
protected static final String AND = "AND";
protected static final String LOGICAL_AND = " " + AND + " ";
protected static final String RETURNING = " RETURNING ";
protected static final String OR = "OR";
protected static final String LOGICAL_OR = " " + OR + " ";
protected static final String DISTINCT = "DISTINCT ";
protected static final String ALIAS_REPLACE_QUOTED = "@\\.";
protected static final String CANNOT_QUERY_ON_ID_WITH_ENTITY_THAT_HAS_NO_ID = "Cannot query on ID with entity that has no ID";
private static final String UNSUPPORTED_EXPRESSION = "Unsupported expression: ";
/**
* Get dialect.
*
* @return dialect
*/
protected Dialect getDialect() {
return Dialect.ANSI;
}
/**
* @return True if embedded properties should be traversed
*/
protected boolean traverseEmbedded() {
return true;
}
/**
* Convert the literal value to it's SQL representation.
*
* @param value The literal value
* @return converter value
*/
@NonNull
protected String asLiteral(@Nullable Object value) {
if (value instanceof LiteralExpression> literalExpression) {
value = literalExpression.getValue();
}
if (value instanceof Expression> expression) {
throw new IllegalArgumentException(UNSUPPORTED_EXPRESSION + expression);
}
if (value == null) {
return "NULL";
}
if (value instanceof Number number) {
return Long.toString(number.longValue());
}
if (value instanceof Boolean) {
return value.toString().toUpperCase(Locale.ROOT);
}
return "'" + value + "'";
}
protected final QueryPropertyPath asQueryPropertyPath(String tableAlias, PersistentProperty persistentProperty) {
return new QueryPropertyPath(asPersistentPropertyPath(persistentProperty), tableAlias);
}
private PersistentPropertyPath asPersistentPropertyPath(PersistentProperty persistentProperty) {
return PersistentPropertyPath.of(Collections.emptyList(), persistentProperty, persistentProperty.getName());
}
@Override
public QueryResult buildSelect(AnnotationMetadata annotationMetadata, SelectQueryDefinition definition) {
QueryBuilder queryBuilder = new QueryBuilder();
QueryState queryState = buildQuery(annotationMetadata, definition, queryBuilder, false, null);
return QueryResult.of(
queryState.getFinalQuery(),
queryState.getQueryParts(),
queryState.getParameterBindings(),
definition.limit(),
definition.offset(),
queryState.getJoinPaths()
);
}
@NonNull
protected final QueryState buildQuery(AnnotationMetadata annotationMetadata,
SelectQueryDefinition definition,
QueryBuilder queryBuilder,
boolean supportsQueryPagination,
@Nullable String tableAliasPrefix) {
QueryState queryState = new QueryState(queryBuilder, definition, true, true, tableAliasPrefix);
Predicate predicate = definition.predicate();
Selection> selection = definition.selection();
Objects.requireNonNull(selection, "Select query selection must not be null");
List joinPaths = new ArrayList<>(definition.getJoinPaths());
joinPaths.sort((o1, o2) -> Comparator.comparingInt(String::length).thenComparing(String::compareTo).compare(o1.getPath(), o2.getPath()));
for (JoinPath joinPath : joinPaths) {
queryState.applyJoin(joinPath);
}
StringBuilder query = queryState.getQuery();
query.append(SELECT_CLAUSE);
buildSelectClause(annotationMetadata, definition, queryState);
appendForUpdate(QueryPosition.AFTER_TABLE_NAME, definition, query);
queryState.generateJoinQuery();
if (predicate != null || annotationMetadata.hasStereotype(WhereSpecifications.class) || queryState.getEntity().getAnnotationMetadata().hasStereotype(WhereSpecifications.class)) {
buildWhereClause(annotationMetadata, predicate, queryState);
}
appendPaginationAndOrder(annotationMetadata, definition, supportsQueryPagination, queryState);
appendForUpdate(QueryPosition.END_OF_QUERY, definition, queryState.getQuery());
return queryState;
}
protected void appendPaginationAndOrder(AnnotationMetadata annotationMetadata,
SelectQueryDefinition definition,
boolean pagination,
QueryState queryState) {
}
/**
* Get the table name for the given entity.
*
* @param entity The entity
* @return The table name
*/
protected abstract String getTableName(PersistentEntity entity);
/**
* Get the table name for the given entity.
*
* @param entity The entity
* @return The table name
*/
protected String getUnescapedTableName(PersistentEntity entity) {
return entity.getPersistedName();
}
/**
* Get an alias name for the given entity.
*
* @param entity The entity
* @return The alias name
*/
protected String getAliasName(PersistentEntity entity) {
return entity.getAnnotationMetadata().stringValue(MappedEntity.class, "alias")
.orElseGet(() -> getTableName(entity) + "_");
}
/**
* Get the alias name.
*
* @param joinPath The join path
* @return The alias
*/
public String getAliasName(JoinPath joinPath) {
return joinPath.getAlias().orElseGet(() -> {
String joinPathAlias = getPathOnlyAliasName(joinPath);
// if "root association" has a declared alias, don't add entity alias as a prefix to match behavior of @Join(alias= "...")
if (joinPath.getAssociationPath()[0].hasDeclaredAliasName()) {
return joinPathAlias;
}
PersistentEntity owner = joinPath.getAssociationPath()[0].getOwner();
String ownerAlias = getAliasName(owner);
if (ownerAlias.endsWith("_") && joinPathAlias.startsWith("_")) {
return ownerAlias + joinPathAlias.substring(1);
} else {
return ownerAlias + joinPathAlias;
}
});
}
/**
* Get the alias name for just the join path.
*
* @param joinPath The join path
* @return The alias
*/
@NonNull
protected String getPathOnlyAliasName(JoinPath joinPath) {
return joinPath.getAlias().orElseGet(() -> {
var p = new StringBuilder();
for (Association ass : joinPath.getAssociationPath()) {
p.append(ass.getAliasName());
if (ass.hasDeclaredAliasName() && ass != joinPath.getAssociation()) {
p.append('_');
}
}
return p.toString();
});
}
/**
* Build a join expression for the given alias, association, join type and builder.
*
* @param joinType The join type
* @param query The query builder
* @param queryState The state
* @param joinAssociation The association
* @param associationOwner The associated owner
* @param currentJoinAlias The current join alias
* @param lastJoinAlias The last join alias
*/
protected void buildJoin(String joinType,
StringBuilder query,
QueryState queryState,
PersistentAssociationPath joinAssociation,
PersistentEntity associationOwner,
String currentJoinAlias,
String lastJoinAlias) {
}
/**
* Get the column name for the given property.
*
* @param persistentProperty The property
* @return The column name
*/
protected abstract String getColumnName(PersistentProperty persistentProperty);
/**
* Returns escaped (quoted) column if escape needed.
*
* @param column the column
* @param escape an indicator telling whether column needs to be escaped (quoted)
* @return escaped (quoted) column if instructed to do so, otherwise original column value
*/
private String escapeColumnIfNeeded(String column, boolean escape) {
if (escape) {
return quote(column);
}
return column;
}
private void buildSelectClause(AnnotationMetadata annotationMetadata,
SelectQueryDefinition definition,
QueryState queryState) {
buildSelect(
annotationMetadata,
queryState,
definition.selection(),
definition.isDistinct()
);
queryState.getQuery().append(FROM_CLAUSE)
.append(getTableName(queryState.getEntity()))
.append(getTableAsKeyword())
.append(queryState.getRootAlias());
}
/**
* Whether queries should be escaped for the given entity.
*
* @param entity The entity
* @return True if they should be escaped
*/
protected boolean shouldEscape(@NonNull PersistentEntity entity) {
return entity.getAnnotationMetadata().booleanValue(MappedEntity.class, "escape").orElse(true);
}
/**
* Get the AS keyword to use for table aliases.
*
* @return The AS keyword if any
*/
protected String getTableAsKeyword() {
return AS_CLAUSE;
}
/**
* Quote a column name for the dialect.
*
* @param persistedName The persisted name.
* @return The quoted name
*/
protected String quote(String persistedName) {
return "\"" + persistedName + "\"";
}
/**
* Build select statement.
*
* @param annotationMetadata the annotation metadata
* @param queryState the query state
* @param selection projection list (can be empty, then selects all columns)
* @param distinct is distinct selection
*/
protected void buildSelect(AnnotationMetadata annotationMetadata,
QueryState queryState,
Selection> selection,
boolean distinct) {
if (selection instanceof ISelection> selectionVisitable) {
selectionVisitable.visitSelection(createSelectionVisitor(annotationMetadata, queryState, distinct));
} else {
throw new IllegalStateException("Unknown selection type: " + selection.getClass().getName());
}
}
/**
* Create a selection visitor.
*
* @param annotationMetadata The annotation metadata
* @param queryState The query state
* @param distinct The distinct
* @return The visitor
*/
protected SqlSelectionVisitor createSelectionVisitor(AnnotationMetadata annotationMetadata,
QueryState queryState,
boolean distinct) {
return new SqlSelectionVisitor(queryState, annotationMetadata, distinct);
}
/**
* Gets {@link NamingStrategy} for the property path. Subclasses might override and potentially
* provide different strategy in some cases.
*
* @param propertyPath the property path representation
* @return naming strategy for the property path
*/
protected NamingStrategy getNamingStrategy(PersistentPropertyPath propertyPath) {
return propertyPath.getNamingStrategy();
}
/**
* Gets {@link NamingStrategy} for the entity. Subclasses might override and potentially
* provide different strategy in some cases.
*
* @param entity the persistent entity
* @return naming strategy for the entity
*/
protected NamingStrategy getNamingStrategy(PersistentEntity entity) {
return entity.getNamingStrategy();
}
/**
* Gets the mapped name from the association using {@link NamingStrategy}.
*
* @param namingStrategy the naming strategy being used
* @param association the association
* @return the mapped name for the association
*/
@NonNull
protected String getMappedName(@NonNull NamingStrategy namingStrategy, @NonNull Association association) {
return namingStrategy.mappedName(association);
}
/**
* Gets the mapped name from for the list of associations and property using {@link NamingStrategy}.
*
* @param namingStrategy the naming strategy
* @param associations the association list
* @param property the property
* @return the mappen name for the list of associations and property using given naming strategy
*/
@NonNull
protected String getMappedName(@NonNull NamingStrategy namingStrategy, @NonNull List associations, @NonNull PersistentProperty property) {
return namingStrategy.mappedName(associations, property);
}
/**
* Gets the mapped name from for the list of associations and property using {@link NamingStrategy}.
*
* @param namingStrategy the naming strategy
* @param propertyPath the property path
* @return the mappen name for the list of associations and property using given naming strategy
*/
@NonNull
protected String getMappedName(@NonNull NamingStrategy namingStrategy, @NonNull PersistentPropertyPath propertyPath) {
return namingStrategy.mappedName(propertyPath.getAssociations(), propertyPath.getProperty());
}
/**
* Builds where clause.
*
* @param annotationMetadata the annotation metadata for the method
* @param predicate the predicate
* @param queryState the query state
*/
protected void buildWhereClause(AnnotationMetadata annotationMetadata, Predicate predicate, QueryState queryState) {
String additionalWhere = buildAdditionalWhereClause(queryState, annotationMetadata);
RenderablePredicate additionalWherePredicate = findAdditionalPredicate(additionalWhere);
if (additionalWherePredicate != null) {
if (predicate == null) {
predicate = new ConjunctionPredicate(List.of(additionalWherePredicate));
} else {
predicate = new ConjunctionPredicate((Collection) List.of(predicate, additionalWherePredicate));
}
}
if (predicate != null) {
queryState.getQuery().append(WHERE_CLAUSE);
if (predicate instanceof IPredicate predicateVisitable) {
predicateVisitable.visitPredicate(createPredicateVisitor(annotationMetadata, queryState));
} else {
throw new IllegalStateException("Unsupported predicate type: " + predicate.getClass().getName());
}
}
}
/**
* Create a predicate visitor.
*
* @param annotationMetadata The annotation metadata
* @param queryState The query state
* @return The visitor
*/
protected SqlPredicateVisitor createPredicateVisitor(AnnotationMetadata annotationMetadata, QueryState queryState) {
return new SqlPredicateVisitor(queryState, annotationMetadata);
}
/**
* Builds additional where clause if there is {@link Where} annotation on the entity.
*
* @param queryState the query state
* @param annotationMetadata the annotation metadata
* @return where clause if there was {@link Where} annotation on the entity (or joins for JPA implementation)
*/
protected String buildAdditionalWhereClause(QueryState queryState, AnnotationMetadata annotationMetadata) {
return buildAdditionalWhereString(queryState.getRootAlias(), queryState.getEntity(), annotationMetadata);
}
@Nullable
private RenderablePredicate findAdditionalPredicate(String additionalWhere) {
if (StringUtils.isEmpty(additionalWhere)) {
return null;
}
return new RenderablePredicate() {
@Override
void render(StringBuilder query, PropertyParameterCreator propertyParameterCreator) {
Matcher matcher = VARIABLE_PATTERN.matcher(additionalWhere);
int index = 0;
while (matcher.find()) {
query.append(additionalWhere, index, matcher.start(2));
index = matcher.end(2);
String name = matcher.group(3);
propertyParameterCreator.pushAdditionalParameter(name);
}
if (index < additionalWhere.length()) {
query.append(additionalWhere, index, additionalWhere.length());
}
}
};
}
/**
* Builds WHERE clause for the entity and given alias if {@link IgnoreWhere} is not present.
*
* @param alias the entity alias
* @param entity the entity
* @param annotationMetadata the entity metadata
* @return the WHERE clause
*/
protected String buildAdditionalWhereString(String alias, PersistentEntity entity, AnnotationMetadata annotationMetadata) {
if (annotationMetadata.hasAnnotation(IgnoreWhere.class)) {
return "";
}
final String whereStr = resolveWhereForAnnotationMetadata(alias, annotationMetadata);
if (StringUtils.isNotEmpty(whereStr)) {
return whereStr;
} else {
return resolveWhereForAnnotationMetadata(alias, entity.getAnnotationMetadata());
}
}
/**
* Builds WHERE clause based on {@link Where} annotation on the metadata.
*
* @param joinPath the join path
* @param annotationMetadata the annotation metadata
* @return WHERE clause if {@link Where} annotation is declared and {@link IgnoreWhere} is not present, otherwise empty string
*/
protected final String buildAdditionalWhereString(JoinPath joinPath, AnnotationMetadata annotationMetadata) {
if (annotationMetadata.hasAnnotation(IgnoreWhere.class)) {
return "";
}
Association association = joinPath.getAssociation();
if (association == null) {
return "";
}
String alias = getAliasName(joinPath);
return resolveWhereForAnnotationMetadata(alias, association.getAssociatedEntity().getAnnotationMetadata());
}
/**
* Resolves where clause if there is {@link Where} annotation on the entity.
*
* @param alias the entity alias
* @param annotationMetadata the entity annotation metadata
* @return where clause with entity alias if entity has declared where annotation
*/
protected final String resolveWhereForAnnotationMetadata(String alias, AnnotationMetadata annotationMetadata) {
return annotationMetadata.getAnnotationValuesByType(Where.class)
.stream()
.flatMap(av -> av.stringValue().stream())
.map(val -> replaceAlias(alias, val))
.filter(StringUtils::isNotEmpty)
.collect(Collectors.joining(LOGICAL_AND));
}
/**
* Appends order to the query.
*
* @param annotationMetadata the annotation metadata
* @param orders the orders
* @param queryState the query state
*/
protected void appendOrder(AnnotationMetadata annotationMetadata, List orders, QueryState queryState) {
if (!orders.isEmpty()) {
StringBuilder buff = queryState.getQuery();
buff.append(ORDER_BY_CLAUSE);
String jsonEntityColumn = getJsonEntityColumn(annotationMetadata);
Iterator i = orders.iterator();
while (i.hasNext()) {
Order order = i.next();
QueryPropertyPath propertyPath = queryState.findProperty(requireProperty(order.getExpression()).getPropertyPath());
String currentAlias = propertyPath.getTableAlias();
if (currentAlias != null) {
buff.append(currentAlias).append(DOT);
}
if (jsonEntityColumn != null) {
buff.append(jsonEntityColumn).append(DOT);
}
String direction;
if (order.isAscending()) {
direction = "ASC";
} else {
direction = "DESC";
}
if (computePropertyPaths() && jsonEntityColumn == null) {
buff.append(propertyPath.getColumnName()).append(SPACE).append(direction);
} else {
buff.append(propertyPath.getPath());
if (jsonEntityColumn != null) {
appendJsonProjection(buff, propertyPath.getProperty().getDataType());
}
buff.append(SPACE).append(direction);
}
if (i.hasNext()) {
buff.append(",");
}
}
}
}
/**
* Adds "forUpdate" pessimistic locking.
*
* @param queryPosition The query position
* @param definition The definition
* @param queryBuilder The builder
*/
protected void appendForUpdate(QueryPosition queryPosition, SelectQueryDefinition definition, StringBuilder queryBuilder) {
if (definition.isForUpdate()) {
throw new IllegalStateException("For update not supported for current query builder: " + getClass().getSimpleName());
}
}
private String getJsonEntityColumn(AnnotationMetadata annotationMetadata) {
AnnotationValue entityRepresentationAnnotationValue = annotationMetadata.getAnnotation(EntityRepresentation.class);
if (entityRepresentationAnnotationValue != null) {
return entityRepresentationAnnotationValue.getRequiredValue("column", String.class);
}
return null;
}
private void buildUpdateStatement(AnnotationMetadata annotationMetadata, QueryState queryState, Map propertiesToUpdate) {
StringBuilder queryString = queryState.getQuery();
queryString.append(SPACE).append("SET").append(SPACE);
PersistentEntity entity = queryState.getEntity();
boolean jsonEntity = isJsonEntity(annotationMetadata, queryState.getEntity());
if (jsonEntity && propertiesToUpdate.size() == 1) {
checkDialectSupportsJsonEntity(entity);
// Update JsonView DATA column
String name = propertiesToUpdate.keySet().iterator().next();
String jsonEntityColumn = getJsonEntityColumn(annotationMetadata);
if (name.equals(jsonEntityColumn)) {
Object value = propertiesToUpdate.get(name);
queryString.append(queryState.getRootAlias()).append(DOT).append(jsonEntityColumn).append("=");
if (value instanceof BindingParameter) {
int key = 1;
queryState.pushParameter(new QueryParameterBinding() {
@Override
public String getName() {
return String.valueOf(key);
}
@Override
public String getKey() {
return String.valueOf(key);
}
@Override
public DataType getDataType() {
return DataType.JSON;
}
@Override
public JsonDataType getJsonDataType() {
return JsonDataType.DEFAULT;
}
});
} else {
queryString.append(asLiteral(value));
}
return;
}
}
// keys need to be sorted before query is built
List> update = propertiesToUpdate.entrySet().stream()
.map(e -> {
QueryPropertyPath propertyPath = queryState.findProperty(e.getKey());
if (propertyPath.getProperty() instanceof Association association && association.isForeignKey()) {
throw new IllegalArgumentException("Foreign key associations cannot be updated as part of a batch update statement");
}
return new AbstractMap.SimpleEntry<>(propertyPath, e.getValue());
})
.filter(e -> !(e.getValue() instanceof QueryParameter) || !e.getKey().getProperty().isGenerated())
.collect(Collectors.toList());
boolean[] needsTrimming = {false};
if (!computePropertyPaths() || jsonEntity) {
String jsonViewColumnName = getJsonEntityColumn(annotationMetadata);
if (jsonViewColumnName != null) {
queryString.append(queryState.getRootAlias()).append(DOT).append(jsonViewColumnName).append("= json_transform(").append(jsonViewColumnName);
}
for (Map.Entry entry : update) {
QueryPropertyPath propertyPath = entry.getKey();
PersistentProperty prop = propertyPath.getProperty();
String tableAlias = propertyPath.getTableAlias();
if (jsonViewColumnName != null) {
queryString.append(", SET '$.").append(propertyPath.getPath()).append("' = ");
} else {
if (tableAlias != null) {
queryString.append(tableAlias).append(DOT);
}
queryString.append(propertyPath.getPath()).append('=');
}
if (entry.getValue() instanceof BindingParameter bindingParameter) {
appendUpdateSetParameter(queryString, tableAlias, prop, () -> {
queryState.pushParameter(bindingParameter, newBindingContext(propertyPath.propertyPath));
});
} else {
queryString.append(asLiteral(entry.getValue()));
}
if (jsonViewColumnName == null) {
queryString.append(COMMA);
needsTrimming[0] = true;
}
}
if (jsonViewColumnName != null) {
queryString.append(CLOSE_BRACKET);
}
} else {
NamingStrategy namingStrategy = getNamingStrategy(queryState.getEntity());
for (Map.Entry entry : update) {
QueryPropertyPath propertyPath = entry.getKey();
if (entry.getValue() instanceof BindingParameter bindingParameter) {
PersistentEntityUtils.traversePersistentProperties(propertyPath.getPropertyPath(), traverseEmbedded(), (associations, property) -> {
String tableAlias = propertyPath.getTableAlias();
if (tableAlias != null) {
queryString.append(tableAlias).append(DOT);
}
String columnName = getMappedName(namingStrategy, associations, property);
if (queryState.escape) {
columnName = quote(columnName);
}
queryString.append(columnName).append('=');
appendUpdateSetParameter(queryString, tableAlias, property, () -> {
queryState.pushParameter(
bindingParameter,
newBindingContext(
propertyPath.propertyPath,
PersistentPropertyPath.of(associations, property)
)
);
});
queryString.append(COMMA);
needsTrimming[0] = true;
});
} else {
String tableAlias = propertyPath.getTableAlias();
if (tableAlias != null) {
queryString.append(tableAlias).append(DOT);
}
queryString.append(propertyPath.getColumnName()).append('=');
queryString.append(asLiteral(entry.getValue()));
queryString.append(COMMA);
needsTrimming[0] = true;
}
}
}
if (needsTrimming[0]) {
queryString.setLength(queryString.length() - 1);
}
}
/**
* Appends the SET=? call to the query string.
*
* @param sb The string builder
* @param alias The alias
* @param prop The property
* @param appendParameter The append parameter action
*/
protected void appendUpdateSetParameter(StringBuilder sb, String alias, PersistentProperty prop, Runnable appendParameter) {
Optional dataTransformerWriteValue = getDataTransformerWriteValue(alias, prop);
if (dataTransformerWriteValue.isPresent()) {
appendTransformed(sb, dataTransformerWriteValue.get(), appendParameter);
} else {
appendParameter.run();
}
}
/**
* Appends custom query part.
*
* @param sb The string builder
* @param transformed The transformed query part
* @param appendParameter The append parameter action
*/
protected void appendTransformed(StringBuilder sb, String transformed, Runnable appendParameter) {
int parameterPosition = transformed.indexOf('?');
if (parameterPosition > -1) {
if (transformed.lastIndexOf('?') != parameterPosition) {
throw new IllegalStateException("Only one parameter placeholder is allowed!");
}
sb.append(transformed, 0, parameterPosition);
appendParameter.run();
sb.append(transformed.substring(parameterPosition + 1));
} else {
sb.append(transformed);
}
}
/**
* Whether property path expressions require computation by the implementation. In a certain query dialects
* property paths are supported (such as JPA-QL where you can do select foo.bar) whilst for explicit SQL queries paths like
* this have to be computed into aliases / column name references.
*
* @return True if property path computation is required.
*/
protected abstract boolean computePropertyPaths();
@Override
public QueryResult buildUpdate(@NonNull AnnotationMetadata annotationMetadata, @NonNull UpdateQueryDefinition definition) {
Map propertiesToUpdate = definition.propertiesToUpdate();
if (propertiesToUpdate.isEmpty()) {
throw new IllegalArgumentException("No properties specified to update");
}
boolean useAlias = isAliasForBatch(definition.persistentEntity(), annotationMetadata);
QueryState queryState = new QueryState(definition, false, useAlias);
StringBuilder queryString = queryState.getQuery();
String tableAlias = queryState.getRootAlias();
String tableName = getTableName(definition.persistentEntity());
queryString.append("UPDATE ").append(tableName);
if (tableAlias != null) {
queryString.append(SPACE).append(tableAlias);
}
buildUpdateStatement(annotationMetadata, queryState, propertiesToUpdate);
buildWhereClause(annotationMetadata, definition.predicate(), queryState);
Selection> returningSelection = definition.returningSelection();
if (returningSelection != null) {
if (!getDialect().supportsUpdateReturning()) {
throw new IllegalStateException("Dialect: " + getDialect() + " doesn't support UPDATE ... RETURNING clause");
}
queryString.append(RETURNING);
buildSelect(
annotationMetadata,
queryState,
returningSelection,
false
);
}
return QueryResult.of(
queryState.getFinalQuery(),
queryState.getQueryParts(),
queryState.getParameterBindings()
);
}
@Override
public QueryResult buildDelete(@NonNull AnnotationMetadata annotationMetadata, @NonNull DeleteQueryDefinition definition) {
boolean useAlias = isAliasForBatch(definition.persistentEntity(), annotationMetadata);
QueryState queryState = new QueryState(definition, false, useAlias);
StringBuilder queryString = queryState.getQuery();
String tableAlias = queryState.getRootAlias();
StringBuilder query = appendDeleteClause(queryString);
String tableName = getTableName(definition.persistentEntity());
query.append(tableName).append(SPACE);
if (tableAlias != null) {
query.append(getTableAsKeyword()).append(tableAlias);
}
buildWhereClause(annotationMetadata, definition.predicate(), queryState);
Selection> returningSelection = definition.returningSelection();
if (returningSelection != null) {
if (!getDialect().supportsDeleteReturning()) {
throw new IllegalStateException("Dialect: " + getDialect() + " doesn't support DELETE ... RETURNING clause");
}
queryString.append(RETURNING);
buildSelect(
annotationMetadata,
queryState,
returningSelection,
false
);
}
return QueryResult.of(
queryState.getFinalQuery(),
queryState.getQueryParts(),
queryState.getParameterBindings()
);
}
/**
* Should aliases be used in batch statements.
*
* @param persistentEntity the persistent entity
* @param annotationMetadata the method annotation metadata
* @return True if they should
*/
protected abstract boolean isAliasForBatch(PersistentEntity persistentEntity, AnnotationMetadata annotationMetadata);
/**
* Append the delete clause.
*
* @param queryString The query string
* @return The delete clause
*/
@NonNull
protected StringBuilder appendDeleteClause(StringBuilder queryString) {
return queryString.append("DELETE ").append(FROM_CLAUSE);
}
/**
* Encode the given query into the encoded query instance.
*
* @param query The query
* @param entity The root entity
* @param annotationMetadata The annotation metadata
* @param sort The sort
* @param nativeQuery Whether the query is native query, in which case sort field names will be supplied by the user and not verified
* @param tableAlias The table alias
* @return The encoded query
*/
@NonNull
public String buildOrderBy(String query,
@NonNull PersistentEntity entity,
@NonNull AnnotationMetadata annotationMetadata,
@NonNull Sort sort,
boolean nativeQuery,
@Nullable
String tableAlias) {
ArgumentUtils.requireNonNull("entity", entity);
ArgumentUtils.requireNonNull("sort", sort);
List orders = sort.getOrderBy();
if (CollectionUtils.isEmpty(orders)) {
throw new IllegalArgumentException("Sort is empty");
}
StringBuilder buff = new StringBuilder(ORDER_BY_CLAUSE);
Iterator i = orders.iterator();
while (i.hasNext()) {
Sort.Order order = i.next();
String property = order.getProperty();
boolean ignoreCase = order.isIgnoreCase();
if (ignoreCase) {
buff.append("LOWER(");
}
buff.append(buildPropertyByName(property, query, entity, annotationMetadata, nativeQuery, tableAlias));
if (ignoreCase) {
buff.append(")");
}
buff.append(SPACE).append(order.getDirection());
if (i.hasNext()) {
buff.append(",");
}
}
return buff.toString();
}
/**
* Encode the given property retrieval into a query instance.
* For example, property name might be encoded as {@code `person_.name`} using
* its path and table's alias.
*
* @param propertyName The name of the property
* @param query The query
* @param entity The root entity
* @param annotationMetadata The annotation metadata
* @param nativeQuery Whether the query is native query, in which case the property name will be supplied by the user and not verified
* @param tableAlias The table alias
* @return The encoded query
*/
public String buildPropertyByName(@NonNull String propertyName,
@NonNull String query,
@NonNull PersistentEntity entity,
@NonNull AnnotationMetadata annotationMetadata,
boolean nativeQuery,
@Nullable
String tableAlias) {
if (nativeQuery) {
return propertyName;
}
PersistentPropertyPath path = entity.getPropertyPath(propertyName);
if (path == null) {
throw new IllegalArgumentException("Cannot sort on non-existent property path: " + propertyName);
}
List associations = new ArrayList<>(path.getAssociations());
int assocCount = associations.size();
// If last association is embedded, it does not need to be joined to the alias since it will be in the destination table
// JPA/Hibernate is special case and in that case we leave association for specific handling below
if (assocCount > 0 && computePropertyPaths() && associations.get(assocCount - 1).isEmbedded()) {
associations.remove(assocCount - 1);
}
StringBuilder buff = new StringBuilder();
String aliasName = tableAlias == null ? getAliasName(entity) : tableAlias;
if (associations.isEmpty()) {
buff.append(aliasName);
} else {
StringJoiner joiner = new StringJoiner(".");
for (Association association : associations) {
joiner.add(association.getName());
}
String joinAlias = getAliasName(new JoinPath(joiner.toString(), associations.toArray(new Association[0]), Join.Type.DEFAULT, null));
if (!computePropertyPaths()) {
if (!query.contains(" " + joinAlias + " ") && !query.endsWith(" " + joinAlias)) {
// Special hack case for JPA, Hibernate can join the relation with cross join automatically when referenced by the property path
// This probably should be removed in the future major version
buff.append(aliasName).append(DOT);
StringJoiner pathJoiner = new StringJoiner(".");
for (Association association : associations) {
pathJoiner.add(association.getName());
}
buff.append(pathJoiner);
} else {
buff.append(joinAlias);
}
} else {
buff.append(joinAlias);
}
}
buff.append(DOT);
String jsonEntityColumn = getJsonEntityColumn(annotationMetadata);
if (jsonEntityColumn != null) {
buff.append(jsonEntityColumn).append(DOT);
}
if (!computePropertyPaths() || jsonEntityColumn != null) {
buff.append(path.getProperty().getName());
if (jsonEntityColumn != null) {
appendJsonProjection(buff, path.getProperty().getDataType());
}
} else {
buff.append(getColumnName(path.getProperty()));
}
return buff.toString();
}
/**
* Join associations and property as path.
*
* @param associations The associations
* @param property The property
* @return joined path
*/
protected static String asPath(List associations, PersistentProperty property) {
if (associations.isEmpty()) {
return property.getName();
}
StringJoiner joiner = new StringJoiner(".");
for (Association association : associations) {
joiner.add(association.getName());
}
joiner.add(property.getName());
return joiner.toString();
}
/**
* Join associations as path.
*
* @param associations The associations
* @return joined path
*/
private static String asPath(List associations) {
StringJoiner joiner = new StringJoiner(".");
for (Association association : associations) {
joiner.add(association.getName());
}
return joiner.toString();
}
private Optional getDataTransformerValue(String alias, PersistentProperty prop, String val) {
return prop.getAnnotationMetadata()
.stringValue(DataTransformer.class, val)
.map(v -> replaceAlias(alias, v));
}
private String replaceAlias(String alias, String v) {
return v.replaceAll(ALIAS_REPLACE_QUOTED, alias == null ? "" : alias + ".");
}
private BindingParameter.BindingContext newBindingContext(@Nullable PersistentPropertyPath ref,
@Nullable PersistentPropertyPath persistentPropertyPath) {
return BindingParameter.BindingContext.create()
.incomingMethodParameterProperty(ref)
.outgoingQueryParameterProperty(persistentPropertyPath);
}
/**
* Creates new binding parameter context.
*
* @param ref the persistent property reference
* @return new binding parameter context
*/
protected BindingParameter.BindingContext newBindingContext(@Nullable PersistentPropertyPath ref) {
return BindingParameter.BindingContext.create()
.incomingMethodParameterProperty(ref)
.outgoingQueryParameterProperty(ref);
}
/**
* Returns transformed value if the data transformer id defined.
*
* @param alias query table alias
* @param prop a property
* @return optional transformed value
*/
protected Optional getDataTransformerReadValue(String alias, PersistentProperty prop) {
return getDataTransformerValue(alias, prop, "read");
}
/**
* Returns transformed value if the data transformer id defined.
*
* @param alias query table alias
* @param prop a property
* @return optional transformed value
*/
protected Optional getDataTransformerWriteValue(String alias, PersistentProperty prop) {
return getDataTransformerValue(alias, prop, "write");
}
/**
* Format the parameter at the given index.
*
* @param index The parameter
* @return The index
*/
protected abstract Placeholder formatParameter(int index);
/**
* Resolves the join type.
*
* @param jt The join type
* @return The join type.
*/
public abstract String resolveJoinType(Join.Type jt);
/**
* Gets column alias if defined as alias field on MappedProperty annotation on the mapping field.
*
* @param property the persistent property
* @return column alias if defined, otherwise an empty string
*/
protected final String getColumnAlias(PersistentProperty property) {
return property.getAlias();
}
/**
* If and when {@link EntityRepresentation} annotation with JSON type is used for the repository method but dialect does not support
* JSON entity representations this will throw {@link IllegalArgumentException}.
*
* @param entity the persistent entity
*/
protected void checkDialectSupportsJsonEntity(PersistentEntity entity) {
if (!getDialect().supportsJsonEntity()) {
throw new IllegalArgumentException("Json representation for entity " + entity.getSimpleName() + " is not supported by the dialect " + getDialect());
}
}
/**
* Checks whether {@link EntityRepresentation} annotation with JSON type is used for the repository method.
* If current dialect does not support handling JSON entity representations, {@link IllegalArgumentException} is thrown.
*
* @param annotationMetadata the annotation metadata
* @param entity the persistent entity
* @return true if {@link EntityRepresentation} annotation with JSON type is used for the repository method
*/
protected boolean isJsonEntity(AnnotationMetadata annotationMetadata, PersistentEntity entity) {
boolean jsonEntity = DataAnnotationUtils.hasJsonEntityRepresentationAnnotation(annotationMetadata);
if (jsonEntity) {
checkDialectSupportsJsonEntity(entity);
}
return jsonEntity;
}
protected final void appendExpression(AnnotationMetadata annotationMetadata,
StringBuilder query,
QueryState queryState,
Expression> expression,
boolean isProjection) {
if (expression instanceof io.micronaut.data.model.jpa.criteria.PersistentPropertyPath> persistentPropertyPath) {
appendPropertyRef(annotationMetadata, query, queryState, persistentPropertyPath.getPropertyPath(), isProjection);
} else if (expression instanceof ParameterExpression> parameterExpression) {
if (expression instanceof BindingParameter bindingParameter) {
queryState.pushParameter(bindingParameter, newBindingContext(null));
} else {
throw new IllegalArgumentException("Unknown parameter: " + parameterExpression);
}
} else if (expression instanceof LiteralExpression> literalExpression) {
query.append(asLiteral(literalExpression.getValue()));
} else {
throw new IllegalArgumentException("Unsupported expression type: " + expression.getClass());
}
}
protected final void appendPropertyRef(AnnotationMetadata annotationMetadata,
StringBuilder query,
QueryState queryState,
PersistentPropertyPath pp,
boolean isProjection) {
if (computePropertyPaths() && pp.getProperty() instanceof Embedded) {
throw new IllegalArgumentException("Embedded are not allowed as an expression!");
}
QueryPropertyPath propertyPath = queryState.findProperty(pp);
String tableAlias = propertyPath.getTableAlias();
String readTransformer = isProjection ? getDataTransformerReadValue(tableAlias, propertyPath.getProperty()).orElse(null) : null;
if (readTransformer != null) {
query.append(readTransformer);
return;
}
if (tableAlias != null) {
query.append(tableAlias).append(DOT);
}
boolean computePropertyPaths = computePropertyPaths();
boolean jsonEntity = isJsonEntity(annotationMetadata, queryState.entity);
if (computePropertyPaths && !jsonEntity) {
query.append(propertyPath.getColumnName());
} else {
if (jsonEntity) {
String jsonEntityColumn = getJsonEntityColumn(annotationMetadata);
if (jsonEntityColumn != null) {
query.append(jsonEntityColumn).append(DOT);
PersistentProperty property = propertyPath.getProperty();
if (property == queryState.entity.getIdentity()) {
query.append('"').append(property.getPersistedName()).append('"');
} else {
query.append(propertyPath.getPath());
}
DataType dataType = propertyPath.getProperty().getDataType();
appendJsonProjection(query, dataType);
}
} else {
query.append(propertyPath.getPath());
}
}
}
/**
* Appends value projection for JSON View field.
*
* @param sb the string builder
* @param dataType the property data type
*/
private void appendJsonProjection(StringBuilder sb, DataType dataType) {
if (dataType.isNumeric() || dataType == DataType.BOOLEAN) {
// Boolean is represented as number in Oracle (which only supports json view)
sb.append(".numberOnly()");
} else if (dataType == DataType.STRING) {
sb.append(".stringOnly()");
} else if (dataType == DataType.TIMESTAMP) {
sb.append(".timestamp()");
} else if (dataType == DataType.DATE) {
sb.append(".date()");
}
}
private void appendConcat(StringBuilder writer, Collection partsWriters) {
if (getDialect() == Dialect.ORACLE) {
for (Iterator iterator = partsWriters.iterator(); iterator.hasNext(); ) {
iterator.next().run();
if (iterator.hasNext()) {
writer.append(" || ");
}
}
} else {
writer.append("CONCAT(");
for (Iterator iterator = partsWriters.iterator(); iterator.hasNext(); ) {
iterator.next().run();
if (iterator.hasNext()) {
writer.append(COMMA);
}
}
writer.append(")");
}
}
@Override
public String buildLimitAndOffset(long limit, long offset) {
StringBuilder builder = new StringBuilder();
appendLimitAndOffset(getDialect(), limit, offset, builder);
return builder.toString();
}
/**
* Append limit and offset.
*
* @param dialect The dialect
* @param limit The limit
* @param offset The offset
* @param builder The builder
*/
protected void appendLimitAndOffset(Dialect dialect, long limit, long offset, StringBuilder builder) {
boolean hasLimit = limit > 0;
boolean hasOffset = offset > 0;
if (!hasLimit && !hasOffset) {
return;
}
builder.append(' ');
switch (dialect) {
case SQL_SERVER -> {
// SQL server requires OFFSET always
if (hasOffset) {
builder.append("OFFSET ").append(offset).append(" ROWS ");
} else {
builder.append("OFFSET 0 ROWS ");
}
if (hasLimit) {
builder.append("FETCH NEXT ").append(limit).append(" ROWS ONLY");
}
}
case ORACLE -> {
if (hasOffset) {
builder.append("OFFSET ").append(offset).append(" ROWS");
}
if (hasLimit) {
if (hasOffset) {
builder.append(" ");
}
builder.append("FETCH NEXT ").append(limit).append(" ROWS ONLY");
}
}
default -> {
if (hasLimit) {
builder.append("LIMIT ").append(limit);
}
if (hasOffset) {
if (hasLimit) {
builder.append(" ");
}
builder.append("OFFSET ").append(offset);
}
}
}
}
protected record QueryBuilder(AtomicInteger position,
List parameterBindings,
StringBuilder query,
List queryParts) {
public QueryBuilder() {
this(new AtomicInteger(0), new ArrayList<>(), new StringBuilder(), new ArrayList<>());
}
}
/**
* The state of the query.
*/
@Internal
protected final class QueryState implements PropertyParameterCreator {
private final AbstractSqlLikeQueryBuilder2.QueryBuilder queryBuilder;
private final String rootAlias;
private final Map appliedJoinPaths = new LinkedHashMap<>();
private final boolean allowJoins;
private final BaseQueryDefinition baseQueryDefinition;
private final boolean escape;
private final PersistentEntity entity;
private List joinPaths = new ArrayList<>();
private QueryState(AbstractSqlLikeQueryBuilder2.QueryBuilder queryBuilder, BaseQueryDefinition query, boolean allowJoins, boolean useAlias) {
this(queryBuilder, query, allowJoins, useAlias, null);
}
private QueryState(AbstractSqlLikeQueryBuilder2.QueryBuilder queryBuilder, BaseQueryDefinition query, boolean allowJoins, boolean useAlias, String tableAliasPrefix) {
this.queryBuilder = queryBuilder;
this.allowJoins = allowJoins;
this.baseQueryDefinition = query;
this.entity = query.persistentEntity();
this.escape = AbstractSqlLikeQueryBuilder2.this.shouldEscape(entity);
this.rootAlias = useAlias || tableAliasPrefix != null ? (tableAliasPrefix == null ? "" : tableAliasPrefix) + AbstractSqlLikeQueryBuilder2.this.getAliasName(entity) : null;
}
public QueryState(BaseQueryDefinition query, boolean allowJoins, boolean useAlias) {
this(new QueryBuilder(), query, allowJoins, useAlias);
}
/**
* @return The root alias
*/
@Nullable
public String getRootAlias() {
return rootAlias;
}
/**
* @return The entity
*/
public PersistentEntity getEntity() {
return entity;
}
public String getFinalQuery() {
if (!queryBuilder.query.isEmpty() || !queryBuilder.queryParts.isEmpty()) {
queryBuilder.queryParts.add(queryBuilder.query.toString());
queryBuilder.query.setLength(0);
}
StringBuilder sb = new StringBuilder(queryBuilder.queryParts.get(0));
int i = 1;
for (int k = 1; k < queryBuilder.queryParts.size(); k++) {
QueryParameterBinding queryParameterBinding = queryBuilder.parameterBindings.get(i - 1);
if (queryParameterBinding.getRole() == null) {
Placeholder placeholder = formatParameter(i++);
sb.append(placeholder.name);
sb.append(queryBuilder.queryParts.get(k));
} // Avoid create a placeholder for a role parameter
}
return sb.toString();
}
public List getQueryParts() {
return queryBuilder.queryParts;
}
/**
* @return The query string
*/
public StringBuilder getQuery() {
return queryBuilder.query;
}
/**
* @return Does the query allow joins
*/
public boolean isAllowJoins() {
return allowJoins;
}
/**
* @return The query model object
*/
public BaseQueryDefinition baseQueryDefinition() {
return baseQueryDefinition;
}
/**
* Constructs a new parameter placeholder.
*
* @return The parameter
*/
private Placeholder newParameter() {
return formatParameter(queryBuilder.position.incrementAndGet());
}
@Nullable
public String findJoinAlias(String path) {
JoinPath joinPath = appliedJoinPaths.get(path);
return joinPath == null ? null : joinPath.getAlias().orElseThrow();
}
@NonNull
public String getJoinAlias(String path) {
String joinAlias = findJoinAlias(path);
if (joinAlias == null) {
throw new IllegalArgumentException("Property is not joined at path: " + path);
}
return joinAlias;
}
/**
* Applies a join for the given association.
*
* @param joinPath The join path
*/
public void applyJoin(@NonNull JoinPath joinPath) {
joinPaths.add(joinPath);
if (appliedJoinPaths.containsKey(joinPath.getPath())) {
return;
}
Optional ojp = baseQueryDefinition().getJoinPath(joinPath.getPath());
if (ojp.isPresent()) {
joinPath = ojp.get();
}
Join.Type jt = joinPath.getJoinType();
String jpAlias = joinPath.getAlias().orElse(null);
Association[] associationPath = joinPath.getAssociationPath();
if (ArrayUtils.isEmpty(associationPath)) {
throw new IllegalArgumentException("Invalid association path [" + joinPath.getPath() + "]");
}
StringJoiner pathSoFar = new StringJoiner(".");
for (int i = 0; i < associationPath.length; i++) {
Association association = associationPath[i];
pathSoFar.add(association.getName());
if (association.isEmbedded()) {
continue;
}
String currentPath = pathSoFar.toString();
JoinPath existingJoinPath = appliedJoinPaths.get(currentPath);
if (existingJoinPath == null) {
JoinPath joinPathToUse = baseQueryDefinition.getJoinPath(currentPath).orElse(null);
if (joinPathToUse == null) {
joinPathToUse = new JoinPath(
currentPath,
Arrays.copyOfRange(associationPath, 0, i + 1),
jt,
jpAlias);
}
String currentAlias = getAliasName(joinPathToUse);
joinPathToUse = joinPathToUse.withAlias(currentAlias);
appliedJoinPaths.put(currentPath, joinPathToUse);
}
}
}
private String getAliasName(JoinPath joinPath) {
return joinPath.getAlias().orElseGet(() -> {
String joinPathAlias = getPathOnlyAliasName(joinPath);
// if "root association" has a declared alias, don't add entity alias as a prefix to match behavior of @Join(alias= "...")
if (joinPath.getAssociationPath()[0].hasDeclaredAliasName()) {
return joinPathAlias;
}
PersistentEntity owner = joinPath.getAssociationPath()[0].getOwner();
String ownerAlias;
if (owner.equals(entity)) {
if (rootAlias == null) {
ownerAlias = AbstractSqlLikeQueryBuilder2.this.getAliasName(owner);
} else {
ownerAlias = rootAlias;
}
} else {
ownerAlias = AbstractSqlLikeQueryBuilder2.this.getAliasName(owner);
}
if (ownerAlias.endsWith("_") && joinPathAlias.startsWith("_")) {
return ownerAlias + joinPathAlias.substring(1);
} else {
return ownerAlias + joinPathAlias;
}
});
}
/**
* Generates the JOIN query.
*/
public void generateJoinQuery() {
for (JoinPath joinPath : appliedJoinPaths.values()) {
List joinedTablePath = new ArrayList<>(5);
List joinAssociationsPath = new ArrayList<>(5);
List previousAssociations = joinPath.getLeadingAssociations();
for (int i = previousAssociations.size(); i-- > 0; ) {
Association association = previousAssociations.get(i);
if (association.isEmbedded()) {
joinAssociationsPath.add(0, association);
continue;
}
joinedTablePath = previousAssociations.subList(0, i + 1);
break;
}
String lastJoinAlias;
if (joinedTablePath.isEmpty()) {
lastJoinAlias = rootAlias;
} else {
String associatedJoinedTablePath = asPath(joinedTablePath);
JoinPath joinPath1 = appliedJoinPaths.get(associatedJoinedTablePath);
if (joinPath1 == null) {
throw new IllegalStateException("Path " + associatedJoinedTablePath + " not found. All: " + appliedJoinPaths.keySet());
}
lastJoinAlias = joinPath1.getAlias().orElseThrow();
}
generateJoin(
joinPath,
new PersistentAssociationPath(
joinAssociationsPath,
joinPath.getAssociation()
),
lastJoinAlias
);
}
}
private void generateJoin(JoinPath joinPath, PersistentAssociationPath joinAssociation, String lastJoinAlias) {
buildJoin(
resolveJoinType(joinPath.getJoinType()),
queryBuilder.query,
this,
joinAssociation,
findOwner(entity, joinAssociation),
joinPath.getAlias().orElseThrow(),
lastJoinAlias
);
}
private PersistentEntity findOwner(PersistentEntity mainEntity, PersistentAssociationPath joinAssociation) {
PersistentEntity owner = joinAssociation.getAssociation().getOwner();
if (!owner.isEmbeddable()) {
return owner;
}
List associations = joinAssociation.getAssociations();
ListIterator listIterator = associations.listIterator(associations.size());
while (listIterator.hasPrevious()) {
Association association = listIterator.previous();
if (!association.getOwner().isEmbeddable()) {
return association.getOwner();
}
}
return mainEntity;
}
/**
* Checks if the path is joined already.
*
* @param associationPath The association path.
* @return true if joined
*/
public boolean isJoined(String associationPath) {
return appliedJoinPaths.containsKey(associationPath);
}
/**
* @return Should escape the query
*/
public boolean shouldEscape() {
return escape;
}
/**
* The parameter binding.
*
* @return The parameter binding
*/
public List getParameterBindings() {
return queryBuilder.parameterBindings;
}
@Override
public void pushParameter(@NonNull BindingParameter bindingParameter, @NonNull BindingParameter.BindingContext bindingContext) {
Placeholder placeholder = newParameter();
bindingContext = bindingContext
.index(queryBuilder.position.get() + 1);
if (bindingContext.getName() == null) {
bindingContext = bindingContext.name(placeholder.key());
}
queryBuilder.parameterBindings.add(
bindingParameter.bind(bindingContext)
);
queryBuilder.queryParts.add(queryBuilder.query.toString());
queryBuilder.query.setLength(0);
}
/**
* Adds query parameter binding.
*
* @param parameterBinding the query parameter binding
*/
public void pushParameter(@NonNull QueryParameterBinding parameterBinding) {
queryBuilder.parameterBindings.add(parameterBinding);
queryBuilder.queryParts.add(queryBuilder.query.toString());
queryBuilder.query.setLength(0);
}
public List getJoinPaths() {
return joinPaths;
}
public void setJoinPaths(List joinPaths) {
this.joinPaths = joinPaths;
}
@NonNull
private QueryPropertyPath findProperty(String propertyPath) {
PersistentPropertyPath pp = entity.getPropertyPath(propertyPath);
if (pp != null) {
return findPropertyInternal(pp);
} else if (TypeRole.ID.equals(propertyPath) && entity.getIdentity() != null) {
// special case handling for ID
return new QueryPropertyPath(
new PersistentPropertyPath(Collections.emptyList(), entity.getIdentity(), entity.getIdentity().getName()),
rootAlias
);
}
throw new IllegalArgumentException("Cannot order on non-existent property path: " + pp);
}
@NonNull
private QueryPropertyPath findProperty(PersistentPropertyPath propertyPath) {
return findPropertyInternal(propertyPath);
}
@NonNull
private QueryPropertyPath findPropertyInternal(PersistentPropertyPath propertyPath) {
if (propertyPath.getAssociations().isEmpty()) {
return new QueryPropertyPath(propertyPath, rootAlias);
}
PersistentProperty property = propertyPath.getProperty();
List joinPath = new ArrayList<>(propertyPath.getAssociations());
ListIterator listIterator = joinPath.listIterator(joinPath.size());
while (listIterator.hasPrevious()) {
Association association = listIterator.previous();
if (association.isEmbedded()) {
listIterator.remove();
} else if (PersistentEntityUtils.isAccessibleWithoutJoin(association, property)) {
property = association;
// We don't need to join to access the id of the relation if it is not a foreign key association
listIterator.remove();
} else {
break;
}
}
if (!joinPath.isEmpty()) {
// 'joinPath.prop' should be represented as a path of 'prop' with a join alias
return new QueryPropertyPath(
new PersistentPropertyPath(Collections.emptyList(), property),
getRequiredJoinPathAlias(asPath(joinPath))
);
}
return new QueryPropertyPath(propertyPath, rootAlias);
}
@NonNull
private String getRequiredJoinPathAlias(String path) {
if (!isAllowJoins()) {
throw new IllegalArgumentException("Joins cannot be used in a DELETE or UPDATE operation and path: " + path);
}
return getJoinAlias(path);
}
}
/**
* Represents a placeholder in query.
*
* @param name The name of the placeholder
* @param key The key to set the value of the placeholder
*/
public record Placeholder(String name, String key) {
@Override
public String toString() {
return name;
}
}
/**
* Represents a path to a property.
*/
protected class QueryPropertyPath {
private final PersistentPropertyPath propertyPath;
private final String tableAlias;
/**
* Default constructor.
*
* @param propertyPath The propertyPath
* @param tableAlias The tableAlias
*/
public QueryPropertyPath(@NonNull PersistentPropertyPath propertyPath, @Nullable String tableAlias) {
this.propertyPath = propertyPath;
this.tableAlias = tableAlias;
}
/**
* @return The associations
*/
@NonNull
public List getAssociations() {
return propertyPath.getAssociations();
}
/**
* @return The property
*/
@NonNull
public PersistentProperty getProperty() {
return propertyPath.getProperty();
}
/**
* @return The path
*/
@NonNull
public String getPath() {
return propertyPath.getPath();
}
/**
* @return The path
*/
@Nullable
public String getTableAlias() {
return tableAlias;
}
/**
* @return already escaped column name
*/
public String getColumnName() {
String columnName = getMappedName(getNamingStrategy(), propertyPath.getAssociations(), propertyPath.getProperty());
if (shouldEscape()) {
return quote(columnName);
}
return columnName;
}
/**
* @return the naming strategy
*/
public NamingStrategy getNamingStrategy() {
return AbstractSqlLikeQueryBuilder2.this.getNamingStrategy(propertyPath);
}
/**
* @return should escape
*/
public boolean shouldEscape() {
return AbstractSqlLikeQueryBuilder2.this.shouldEscape(propertyPath.findPropertyOwner().orElse(propertyPath.getProperty().getOwner()));
}
/**
* @return the persistent property path
*/
public PersistentPropertyPath getPropertyPath() {
return propertyPath;
}
}
protected enum QueryPosition {
AFTER_TABLE_NAME, END_OF_QUERY
}
/**
* The predicate visitor to construct the query.
*/
protected class SqlPredicateVisitor implements AdvancedPredicateVisitor {
protected final PersistentEntity persistentEntity;
protected final String tableAlias;
protected final StringBuilder query;
protected final QueryState queryState;
protected final AnnotationMetadata annotationMetadata;
protected SqlPredicateVisitor(QueryState queryState, AnnotationMetadata annotationMetadata) {
this.queryState = queryState;
this.annotationMetadata = annotationMetadata;
persistentEntity = queryState.getEntity();
tableAlias = queryState.getRootAlias();
query = queryState.getQuery();
}
@Override
public PersistentPropertyPath getRequiredProperty(io.micronaut.data.model.jpa.criteria.PersistentPropertyPath> persistentPropertyPath) {
return persistentPropertyPath.getPropertyPath();
}
private void visitPredicate(IExpression expression) {
if (expression instanceof RenderablePredicate renderablePredicate) {
renderablePredicate.render(query, queryState);
} else if (expression instanceof IPredicate predicateVisitable) {
predicateVisitable.visitPredicate(this);
} else if (expression instanceof io.micronaut.data.model.jpa.criteria.PersistentPropertyPath>) {
visitIsTrue(expression);
} else {
throw new IllegalStateException("Unknown boolean expression: " + expression);
}
}
@Override
public void visit(ConjunctionPredicate conjunction) {
if (conjunction.getPredicates().isEmpty()) {
return;
}
boolean requiresBracket = query.charAt(query.length() - 1) != '(';
if (requiresBracket) {
query.append(OPEN_BRACKET);
}
visitConjunctionPredicates(conjunction.getPredicates());
if (requiresBracket) {
query.append(CLOSE_BRACKET);
}
}
private void visitConjunctionPredicates(Collection extends IExpression> predicates) {
Iterator extends IExpression> iterator = predicates.iterator();
boolean appendLogicalAnd = true;
while (iterator.hasNext()) {
IExpression expression = iterator.next();
if (expression instanceof ConjunctionPredicate conjunctionPredicate) {
Collection extends IExpression> conjunctionPredicates = conjunctionPredicate.getPredicates();
if (CollectionUtils.isEmpty(conjunctionPredicates)) {
// Nothing was added to the query so skip adding AND
appendLogicalAnd = false;
} else {
visitConjunctionPredicates(conjunctionPredicates);
}
} else {
visitPredicate(expression);
}
if (appendLogicalAnd && iterator.hasNext()) {
query.append(LOGICAL_AND);
}
}
}
@Override
public void visit(DisjunctionPredicate disjunction) {
if (disjunction.getPredicates().isEmpty()) {
return;
}
query.append(OPEN_BRACKET);
visitDisjunctionPredicates(disjunction.getPredicates());
query.append(CLOSE_BRACKET);
}
private void visitDisjunctionPredicates(Collection extends IExpression> predicates) {
Iterator extends IExpression> iterator = predicates.iterator();
while (iterator.hasNext()) {
IExpression expression = iterator.next();
if (expression instanceof DisjunctionPredicate disjunctionPredicate) {
visitDisjunctionPredicates(disjunctionPredicate.getPredicates());
} else {
visitPredicate(expression);
}
if (iterator.hasNext()) {
query.append(LOGICAL_OR);
}
}
}
@Override
public void visit(NegatedPredicate negate) {
IExpression negated = negate.getNegated();
if (negated instanceof InPredicate> p) {
visitIn(p.getExpression(), p.getValues(), true);
} else {
query.append(NOT).append(OPEN_BRACKET);
if (negated instanceof ConjunctionPredicate conjunctionPredicate) {
visitConjunctionPredicates(conjunctionPredicate.getPredicates());
} else if (negated instanceof DisjunctionPredicate disjunctionPredicate) {
visitDisjunctionPredicates(disjunctionPredicate.getPredicates());
} else {
visitPredicate(negated);
}
query.append(CLOSE_BRACKET);
}
}
@Override
public void visit(LikePredicate likePredicate) {
boolean supportsILike = getDialect() == Dialect.POSTGRES;
boolean isCaseInsensitive = !supportsILike && likePredicate.isCaseInsensitive();
if (isCaseInsensitive) {
query.append("LOWER(");
}
appendExpression(likePredicate.getExpression());
if (isCaseInsensitive) {
query.append(")");
}
if (likePredicate.isNegated()) {
query.append(" NOT");
}
if (likePredicate.isCaseInsensitive() && supportsILike) {
query.append(" ILIKE ");
} else {
query.append(" LIKE ");
}
Expression pattern = likePredicate.getPattern();
if (isCaseInsensitive) {
if (pattern instanceof LiteralExpression literalExpression) {
query.append(literalExpression.getValue().toUpperCase());
} else {
query.append("LOWER(");
appendExpression(pattern);
query.append(")");
}
} else {
appendExpression(pattern);
}
Expression escapeChar = likePredicate.getEscapeChar();
if (escapeChar != null) {
query.append(" ESCAPE ");
appendExpression(escapeChar);
}
}
@Override
public void visit(ExistsSubqueryPredicate existsSubqueryPredicate) {
query.append("EXISTS");
appendExpression(existsSubqueryPredicate.getSubquery());
}
@Override
public void visitEquals(Expression> leftExpression, Expression> rightExpression, boolean ignoreCase) {
if (leftExpression instanceof io.micronaut.data.model.jpa.criteria.PersistentPropertyPath> persistentPropertyPath) {
PersistentPropertyPath propertyPath = persistentPropertyPath.getPropertyPath();
PersistentProperty property = propertyPath.getProperty();
if (computePropertyPaths() && property instanceof Association) {
List predicates = new ArrayList<>();
PersistentEntityUtils.traverse(propertyPath, pp ->
predicates.add(new BinaryPredicate(
new DefaultPersistentPropertyPath<>(pp, null),
rightExpression,
ignoreCase ? PredicateBinaryOp.EQUALS_IGNORE_CASE : PredicateBinaryOp.EQUALS
))
);
if (predicates.size() == 1) {
predicates.iterator().next().visitPredicate(this);
} else {
visit(new ConjunctionPredicate(predicates));
}
return;
}
}
if (ignoreCase) {
appendCaseInsensitiveOp(leftExpression, rightExpression, " = ");
} else {
appendBinaryOperation(" = ", leftExpression, rightExpression);
}
}
@Override
public void visitNotEquals(Expression> leftExpression, Expression> rightExpression, boolean ignoreCase) {
if (leftExpression instanceof io.micronaut.data.model.jpa.criteria.PersistentPropertyPath> persistentPropertyPath) {
PersistentPropertyPath propertyPath = persistentPropertyPath.getPropertyPath();
PersistentProperty property = propertyPath.getProperty();
if (computePropertyPaths() && property instanceof Association) {
List predicates = new ArrayList<>();
PersistentEntityUtils.traverse(propertyPath, pp ->
predicates.add(new BinaryPredicate(
new DefaultPersistentPropertyPath<>(pp, null),
rightExpression,
ignoreCase ? PredicateBinaryOp.NOT_EQUALS_IGNORE_CASE : PredicateBinaryOp.NOT_EQUALS
))
);
if (predicates.size() == 1) {
predicates.iterator().next().visitPredicate(this);
} else {
visit(new ConjunctionPredicate(predicates));
}
return;
}
}
if (ignoreCase) {
appendCaseInsensitiveOp(leftExpression, rightExpression, " != ");
} else {
appendBinaryOperation(" != ", leftExpression, rightExpression);
}
}
@Override
public void visitGreaterThan(Expression> leftExpression, Expression> rightExpression) {
appendBinaryOperation(" > ", leftExpression, rightExpression);
}
@Override
public void visitGreaterThanOrEquals(Expression> leftExpression, Expression> rightExpression) {
appendBinaryOperation(" >= ", leftExpression, rightExpression);
}
@Override
public void visitLessThan(Expression> leftExpression, Expression> rightExpression) {
appendBinaryOperation(" < ", leftExpression, rightExpression);
}
@Override
public void visitLessThanOrEquals(Expression> leftExpression, Expression> rightExpression) {
appendBinaryOperation(" <= ", leftExpression, rightExpression);
}
@Override
public void visitStartsWith(Expression> leftExpression, Expression> rightExpression, boolean ignoreCase) {
appendLikeConcatComparison(leftExpression, rightExpression, ignoreCase, "?", "'%'");
}
@Override
public void visitContains(Expression> leftExpression, Expression> expression, boolean ignoreCase) {
appendLikeConcatComparison(leftExpression, expression, ignoreCase, "'%'", "?", "'%'");
}
@Override
public void visitEndsWith(Expression> leftExpression, Expression> expression, boolean ignoreCase) {
appendLikeConcatComparison(leftExpression, expression, ignoreCase, "'%'", "?");
}
private void appendLikeConcatComparison(Expression> leftExpression, Expression> expression, boolean ignoreCase, String... parts) {
boolean isPostgres = getDialect() == Dialect.POSTGRES;
if (ignoreCase && !isPostgres) {
query.append("LOWER(");
appendExpression(leftExpression);
query.append(")");
} else {
appendExpression(leftExpression);
}
if (isPostgres) {
query.append(" ILIKE ");
} else {
query.append(" LIKE ");
}
appendConcat(query, Arrays.stream(parts).map(p -> {
if ("?".equals(p)) {
if (ignoreCase && !isPostgres) {
return (Runnable) () -> {
query.append("LOWER(");
appendExpression(expression, leftExpression);
query.append(")");
};
} else {
return (Runnable) () -> appendExpression(expression, leftExpression);
}
}
return (Runnable) () -> query.append(p);
}).toList());
}
@Override
public void visitIdEquals(Expression> expression) {
if (persistentEntity.hasCompositeIdentity()) {
new ConjunctionPredicate(
Arrays.stream(persistentEntity.getCompositeIdentity())
.map(prop -> {
PersistentPropertyPath propertyPath = asPersistentPropertyPath(prop);
return new BinaryPredicate(
new DefaultPersistentPropertyPath<>(propertyPath, null),
expression,
PredicateBinaryOp.EQUALS
);
}
)
.toList()
).visitPredicate(this);
} else if (persistentEntity.hasIdentity()) {
new BinaryPredicate(
new DefaultPersistentPropertyPath<>(new PersistentPropertyPath(persistentEntity.getIdentity()), null),
expression,
PredicateBinaryOp.EQUALS
).visitPredicate(this);
} else {
throw new IllegalStateException("No ID found for entity: " + persistentEntity.getName());
}
}
protected final void appendPropertyRef(PersistentPropertyPath propertyPath) {
AbstractSqlLikeQueryBuilder2.this.appendPropertyRef(annotationMetadata, query, queryState, propertyPath, false);
}
private void appendBinaryOperation(@NonNull String operator, @NonNull Expression> leftExpression, @NonNull Expression> rightExpression) {
appendExpression(leftExpression, null);
query.append(operator);
appendExpression(rightExpression, leftExpression);
}
private void appendExpression(Expression> expression) {
appendExpression(expression, null);
}
protected final void appendExpression(Expression> expression, @Nullable Expression> boundedExpression) {
CriteriaUtils.requireIExpression(expression).visitExpression(new ExpressionVisitor() {
@Override
public void visit(io.micronaut.data.model.jpa.criteria.PersistentPropertyPath> persistentPropertyPath) {
appendPropertyRef(persistentPropertyPath.getPropertyPath());
}
@Override
public void visit(PersistentEntityRoot> entityRoot) {
visit(new IdExpression<>(entityRoot));
}
@Override
public void visit(LiteralExpression> literalExpression) {
query.append(asLiteral(literalExpression));
}
@Override
public void visit(UnaryExpression> unaryExpression) {
Expression> expression = unaryExpression.getExpression();
switch (unaryExpression.getType()) {
case SUM, AVG, MAX, MIN, UPPER, LOWER ->
appendFunction(unaryExpression.getType().name(), expression);
default ->
throw new IllegalStateException(UNSUPPORTED_EXPRESSION + unaryExpression.getType());
}
}
@Override
public void visit(BinaryExpression> binaryExpression) {
Expression> left = binaryExpression.getLeft();
Expression> right = binaryExpression.getRight();
switch (binaryExpression.getType()) {
case SUM -> {
appendExpression(left);
query.append(" + ");
appendExpression(right);
}
case CONCAT -> appendFunction("CONCAT", List.of(left, right));
default ->
throw new IllegalStateException(UNSUPPORTED_EXPRESSION + binaryExpression.getType());
}
}
@Override
public void visit(IdExpression, ?> idExpression) {
PersistentEntity persistentEntity = idExpression.getRoot().getPersistentEntity();
if (persistentEntity.hasCompositeIdentity()) {
throw new IllegalStateException("ID expression with composite IDs not allowed");
}
if (persistentEntity.getIdentityProperties().size() > 1) {
throw new IllegalStateException("ID expression with multiple IDs not allowed");
}
PersistentProperty identity = persistentEntity.getIdentity();
appendPropertyRef(new PersistentPropertyPath(identity));
}
@Override
public void visit(FunctionExpression> functionExpression) {
appendFunction(functionExpression.getName(), functionExpression.getExpressions());
}
@Override
public void visit(IParameterExpression> parameterExpression) {
appendBindingParameter(parameterExpression, findParameterBoundProperty(boundedExpression));
}
@Override
public void visit(SubqueryExpression> subqueryExpression) {
query.append(subqueryExpression.getType().name());
visit(subqueryExpression.getSubquery());
}
@Override
public void visit(PersistentEntitySubquery> subquery) {
AbstractPersistentEntityQuery, ?> abstractPersistentEntityQuery = (AbstractPersistentEntityQuery, ?>) subquery;
SelectQueryDefinition selectQueryDefinition = abstractPersistentEntityQuery.toSelectQueryDefinition();
String outerAlias = queryState.getRootAlias();
if (outerAlias == null) {
outerAlias = getAliasName(queryState.getEntity());
}
boolean requiresBrackets = query.charAt(query.length() - 1) != '(';
if (requiresBrackets) {
query.append("(");
}
buildQuery(AnnotationMetadata.EMPTY_METADATA, selectQueryDefinition, queryState.queryBuilder, false, outerAlias);
if (requiresBrackets) {
query.append(")");
}
}
});
}
private PersistentPropertyPath findParameterBoundProperty(Expression> binaryOpExpression) {
// We want to find the property bound to the parameter
if (binaryOpExpression == null) {
return null;
}
if (binaryOpExpression instanceof UnaryExpression> unaryExpression) {
return findParameterBoundProperty(unaryExpression.getExpression());
}
if (binaryOpExpression instanceof io.micronaut.data.model.jpa.criteria.PersistentPropertyPath> persistentPropertyPath) {
return persistentPropertyPath.getPropertyPath();
}
return null;
}
private void appendFunction(String functionName, Expression> expression) {
appendFunction(functionName, List.of(expression));
}
private void appendFunction(String functionName, List> expressions) {
query.append(functionName)
.append(OPEN_BRACKET);
for (Iterator> iterator = expressions.iterator(); iterator.hasNext(); ) {
Expression> expression = iterator.next();
appendExpression(expression);
if (iterator.hasNext()) {
query.append(COMMA);
}
}
query.append(CLOSE_BRACKET);
}
private void appendBindingParameter(BindingParameter bindingParameter,
@Nullable PersistentPropertyPath entityPropertyPath) {
Runnable pushParameter = () -> {
queryState.pushParameter(
bindingParameter,
newBindingContext(null, entityPropertyPath)
);
};
if (entityPropertyPath == null) {
pushParameter.run();
} else {
QueryPropertyPath qpp = queryState.findProperty(entityPropertyPath);
String writeTransformer = getDataTransformerWriteValue(qpp.tableAlias, entityPropertyPath.getProperty()).orElse(null);
if (writeTransformer != null) {
appendTransformed(query, writeTransformer, pushParameter);
} else {
pushParameter.run();
}
}
}
private void appendCaseInsensitiveOp(Expression> leftExpression, Expression> expression, String operator) {
query.append("LOWER(");
appendExpression(leftExpression);
query.append(")")
.append(operator)
.append("LOWER(");
appendExpression(expression, leftExpression);
query.append(")");
}
@Override
public void visitIsFalse(Expression> expression) {
appendUnaryCondition(" = FALSE", expression);
}
@Override
public void visitIsNotNull(Expression> expression) {
appendUnaryCondition(" IS NOT NULL", expression);
}
@Override
public void visitIsNull(Expression> expression) {
appendUnaryCondition(" IS NULL", expression);
}
@Override
public void visitIsTrue(Expression> expression) {
appendUnaryCondition(" = TRUE", expression);
}
@Override
public void visitIsEmpty(Expression> expression) {
appendEmptyExpression(" IS NULL" + " " + OR + StringUtils.SPACE, " = ''", " IS EMPTY", expression);
}
@Override
public void visitIsNotEmpty(Expression> expression) {
if (getDialect() == Dialect.ORACLE) {
PersistentPropertyPath propertyPath = requireProperty(expression).getPropertyPath();
// Oracle treats blank and null the same
if (propertyPath.getProperty().isAssignable(CharSequence.class)) {
appendPropertyRef(propertyPath);
query.append(" IS NOT NULL");
} else {
appendPropertyRef(propertyPath);
query.append(" IS NOT EMPTY");
}
} else {
appendEmptyExpression(" IS NOT NULL" + " " + AND + StringUtils.SPACE, " <> ''", " IS NOT EMPTY", expression);
}
}
private void appendEmptyExpression(String charSequencePrefix,
String charSequenceSuffix,
String listSuffix,
Expression> expression) {
if (((IExpression>) expression).getExpressionType().isTextual()) {
appendExpression(expression);
query.append(charSequencePrefix);
appendExpression(expression);
query.append(charSequenceSuffix);
} else {
appendExpression(expression);
query.append(listSuffix);
}
}
private void appendUnaryCondition(String sqlOp, Expression> expression) {
appendExpression(expression);
query.append(sqlOp);
}
@Override
public void visitInBetween(Expression> value, Expression> from, Expression> to) {
query.append(OPEN_BRACKET);
appendExpression(value);
query.append(" >= ");
appendExpression(from, value);
query.append(LOGICAL_AND);
appendExpression(value);
query.append(" <= ");
appendExpression(to, value);
query.append(CLOSE_BRACKET);
}
@Override
public void visitIn(Expression> expression, Collection> values, boolean negated) {
if (values.isEmpty()) {
return;
}
PersistentPropertyPath propertyPath = requireProperty(expression).getPropertyPath();
appendExpression(expression);
query.append(negated ? " NOT IN (" : " IN (");
boolean hasOneParameter = values.stream().filter(v -> v instanceof ParameterExpression).count() == 1;
Iterator> iterator = values.iterator();
while (iterator.hasNext()) {
Object value = iterator.next();
if (value instanceof ParameterExpression) {
BindingParameter.BindingContext bindingContext = newBindingContext(propertyPath);
if (hasOneParameter) {
bindingContext = bindingContext.expandable();
}
queryState.pushParameter((BindingParameter) value, bindingContext);
} else {
appendExpression((Expression>) value);
}
if (iterator.hasNext()) {
query.append(COMMA);
}
}
query.append(CLOSE_BRACKET);
}
}
/**
* The selection visitor to construct the query.
*/
protected class SqlSelectionVisitor implements SelectionVisitor {
protected final QueryState queryState;
protected final StringBuilder query;
protected final AnnotationMetadata annotationMetadata;
protected final boolean distinct;
protected final String tableAlias;
protected final PersistentEntity entity;
protected String columnAlias;
private boolean isCompound;
public SqlSelectionVisitor(QueryState queryState, AnnotationMetadata annotationMetadata, boolean distinct) {
this.queryState = queryState;
this.query = queryState.getQuery();
this.annotationMetadata = annotationMetadata;
this.distinct = distinct;
this.tableAlias = queryState.getRootAlias();
this.entity = queryState.getEntity();
}
@Override
public void visit(io.micronaut.data.model.jpa.criteria.PersistentPropertyPath> persistentPropertyPath) {
PersistentPropertyPath propertyPath = persistentPropertyPath.getPropertyPath();
PersistentProperty property = propertyPath.getProperty();
if (isCompound) {
// Compound property which is part of a DTO
if (property instanceof Association association && !property.isEmbedded()) {
if (queryState.isJoined(propertyPath.getPath())) {
appendCompoundAssociationProjection(new PersistentAssociationPath(propertyPath.getAssociations(), association));
} else {
query.setLength(query.length() - 1);
}
} else {
appendCompoundPropertyProjection(propertyPath);
}
} else {
if (distinct) {
query.append(DISTINCT);
}
if (property instanceof Association association && !property.isEmbedded()) {
appendAssociationProjection(association, propertyPath);
} else {
appendPropertyProjection(findProperty(propertyPath.getPath()));
}
}
}
@Override
public void visit(AliasedSelection> aliasedSelection) {
columnAlias = aliasedSelection.getAlias();
aliasedSelection.getSelection().visitSelection(this);
columnAlias = null;
}
@Override
public void visit(PersistentEntityRoot> entityRoot) {
if (distinct) {
query.append(DISTINCT);
}
selectAllColumnsAndJoined();
}
@Override
public void visit(PersistentEntitySubquery> subquery) {
throw new IllegalStateException("Subquery not supported in selection");
}
@Override
public void visit(CompoundSelection> compoundSelection) {
if (distinct) {
query.append(DISTINCT);
}
isCompound = true;
Iterator> iterator = compoundSelection.getCompoundSelectionItems().iterator();
while (iterator.hasNext()) {
Selection> selection = iterator.next();
if (selection instanceof ISelection> selectionVisitable) {
selectionVisitable.visitSelection(this);
} else {
throw new IllegalStateException("Unknown selection object: " + selection);
}
if (iterator.hasNext()) {
query.append(COMMA);
}
}
isCompound = false;
}
@Override
public void visit(LiteralExpression> literalExpression) {
// Support alias?
query.append(asLiteral(literalExpression.getValue()));
}
@Override
public void visit(UnaryExpression> unaryExpression) {
Expression> expression = unaryExpression.getExpression();
switch (unaryExpression.getType()) {
case SUM, AVG, MAX, MIN, UPPER, LOWER ->
appendFunction(unaryExpression.getType().name(), expression);
case COUNT -> {
if (expression instanceof PersistentEntityRoot) {
appendRowCount(tableAlias);
} else if (expression instanceof io.micronaut.data.model.jpa.criteria.PersistentPropertyPath> persistentPropertyPath) {
appendFunction("COUNT", persistentPropertyPath);
} else {
throw new IllegalStateException("Illegal expression: " + expression + " for count selection!");
}
}
case COUNT_DISTINCT -> {
if (expression instanceof PersistentEntityRoot) {
appendRowCountDistinct(tableAlias);
} else if (expression instanceof io.micronaut.data.model.jpa.criteria.PersistentPropertyPath> persistentPropertyPath) {
appendFunction("COUNT(DISTINCT", persistentPropertyPath);
query.append(CLOSE_BRACKET);
} else {
throw new IllegalStateException("Illegal expression: " + expression + " for count distinct selection!");
}
}
default ->
throw new IllegalStateException(UNSUPPORTED_EXPRESSION + unaryExpression.getType());
}
}
@Override
public void visit(BinaryExpression> binaryExpression) {
Expression> left = binaryExpression.getLeft();
Expression> right = binaryExpression.getRight();
switch (binaryExpression.getType()) {
case SUM -> {
appendExpression(left);
query.append(" + ");
appendExpression(right);
}
case CONCAT -> appendFunction("CONCAT", List.of(left, right));
default ->
throw new IllegalStateException(UNSUPPORTED_EXPRESSION + binaryExpression.getType());
}
}
@Override
public void visit(IdExpression, ?> idExpression) {
// Support distinct?
if (entity.hasCompositeIdentity()) {
for (PersistentProperty identity : entity.getCompositeIdentity()) {
appendPropertyProjection(asQueryPropertyPath(queryState.getRootAlias(), identity));
query.append(COMMA);
}
query.setLength(query.length() - 1);
} else if (entity.hasIdentity()) {
List identityProperties = entity.getIdentityProperties();
if (identityProperties.isEmpty()) {
throw new IllegalArgumentException(CANNOT_QUERY_ON_ID_WITH_ENTITY_THAT_HAS_NO_ID);
}
for (PersistentProperty identity : identityProperties) {
appendPropertyProjection(asQueryPropertyPath(queryState.getRootAlias(), identity));
}
} else {
throw new IllegalArgumentException(CANNOT_QUERY_ON_ID_WITH_ENTITY_THAT_HAS_NO_ID);
}
}
@Override
public void visit(FunctionExpression> functionExpression) {
appendFunction(functionExpression.getName(), functionExpression.getExpressions());
}
/**
* Appends the compound (part of entity or DTO) property projection.
*
* @param propertyPath The property path
*/
@Internal
protected void appendCompoundPropertyProjection(PersistentPropertyPath propertyPath) {
PersistentEntity entity = propertyPath.getProperty().getOwner();
boolean escape = shouldEscape(entity);
NamingStrategy namingStrategy = getNamingStrategy(entity);
int[] propertiesCount = new int[1];
PersistentEntityUtils.traversePersistentProperties(propertyPath, traverseEmbedded(), (associations, p) -> {
appendProperty(query, associations, p, namingStrategy, queryState.rootAlias, escape);
propertiesCount[0]++;
});
query.setLength(query.length() - 1);
if (StringUtils.isNotEmpty(columnAlias)) {
if (propertiesCount[0] > 1) {
throw new IllegalStateException("Cannot apply a column alias: " + columnAlias + " with expanded property: " + propertyPath);
}
if (propertiesCount[0] == 1) {
query.append(AS_CLAUSE).append(columnAlias);
}
}
}
/**
* Appends the compound (part of entity or DTO) association projection.
*
* @param propertyPath The property path
*/
@Internal
protected void appendCompoundAssociationProjection(PersistentAssociationPath propertyPath) {
if (!query.isEmpty() && query.charAt(query.length() - 1) == ',') {
// Strip last .
query.setLength(query.length() - 1);
}
selectAllColumnsFromJoinPaths(queryState.baseQueryDefinition().getJoinPaths(), null);
}
/**
* Append the property projection.
*
* @param propertyPath The property
*/
protected void appendPropertyProjection(QueryPropertyPath propertyPath) {
boolean jsonEntity = isJsonEntity(annotationMetadata, entity);
if (!computePropertyPaths() || jsonEntity) {
query.append(propertyPath.getTableAlias()).append(DOT);
String jsonEntityColumn = null;
if (jsonEntity) {
jsonEntityColumn = getJsonEntityColumn(annotationMetadata);
if (jsonEntityColumn != null) {
checkDialectSupportsJsonEntity(entity);
}
query.append(jsonEntityColumn).append(DOT);
}
query.append(propertyPath.getPath());
if (jsonEntityColumn != null) {
appendJsonProjection(query, propertyPath.getProperty().getDataType());
}
return;
}
String tableAlias = propertyPath.getTableAlias();
boolean escape = propertyPath.shouldEscape();
NamingStrategy namingStrategy = propertyPath.getNamingStrategy();
boolean[] needsTrimming = {false};
int[] propertiesCount = new int[1];
PersistentEntityUtils.traversePersistentProperties(propertyPath.getAssociations(), propertyPath.getProperty(), traverseEmbedded(), (associations, property) -> {
appendProperty(query, associations, property, namingStrategy, tableAlias, escape);
needsTrimming[0] = true;
propertiesCount[0]++;
});
if (needsTrimming[0]) {
query.setLength(query.length() - 1);
}
if (StringUtils.isNotEmpty(columnAlias)) {
if (propertiesCount[0] > 1) {
throw new IllegalStateException("Cannot apply a column alias: " + columnAlias + " with expanded property: " + propertyPath);
}
if (propertiesCount[0] == 1) {
query.append(AS_CLAUSE).append(columnAlias);
}
}
}
/**
* Appends selection projection for the property which is association.
*
* @param association the persistent property
* @param propertyPath the persistent property path
*/
protected void appendAssociationProjection(Association association,
PersistentPropertyPath propertyPath) {
String joinedPath = propertyPath.getPath();
if (!queryState.isJoined(joinedPath)) {
query.setLength(query.length() - 1);
return;
}
String joinAlias = queryState.findJoinAlias(propertyPath.getPath());
selectAllColumns(AnnotationMetadata.EMPTY_METADATA, association.getAssociatedEntity(), joinAlias);
Collection joinPaths = queryState.baseQueryDefinition().getJoinPaths();
List newJoinPaths = new ArrayList<>(joinPaths.size());
Map joinAliasOverride = new HashMap<>();
for (JoinPath joinPath : joinPaths) {
if (joinPath.getPath().startsWith(joinedPath) && !joinPath.getPath().equals(joinedPath)) {
int removedItems = 1;
for (int k = 0; k < joinedPath.length(); k++) {
if (joinedPath.charAt(k) == '.') {
removedItems++;
}
}
JoinPath newJoinPath = new JoinPath(
joinPath.getPath().substring(joinedPath.length() + 1),
Arrays.copyOfRange(joinPath.getAssociationPath(), removedItems, joinPath.getAssociationPath().length),
joinPath.getJoinType(),
joinPath.getAlias().orElse(null)
);
newJoinPaths.add(newJoinPath);
joinAliasOverride.put(newJoinPath, getAliasName(joinPath));
}
}
queryState.setJoinPaths(newJoinPaths);
selectAllColumnsFromJoinPaths(newJoinPaths, joinAliasOverride);
}
/**
* Appends a row count projection to the query string.
*
* @param logicalName The alias to the table name
*/
protected void appendRowCount(String logicalName) {
throw new IllegalStateException("Not supported!");
}
/**
* Appends a row count distinct projection to the query string.
*
* @param logicalName The alias to the table name
*/
protected void appendRowCountDistinct(String logicalName) {
throw new IllegalStateException("Not supported!");
}
/**
* Select all the columns from the entity.
*
* @param annotationMetadata The annotation metadata
* @param persistentEntity The persistent entity
* @param tableAlias The table alias
*/
protected void selectAllColumns(AnnotationMetadata annotationMetadata, PersistentEntity persistentEntity, String tableAlias) {
throw new IllegalStateException("Not supported!");
}
/**
* Select all the columns from the entity and the joined entities.
*/
protected void selectAllColumnsAndJoined() {
throw new IllegalStateException("Not supported!");
}
/**
* Does nothing but subclasses might override and implement new behavior.
*
* @param allPaths The join paths
* @param joinAliasOverride The join alias override
*/
protected void selectAllColumnsFromJoinPaths(Collection allPaths,
@Nullable
Map joinAliasOverride) {
}
protected final void appendProperty(StringBuilder sb,
List associations,
PersistentProperty property,
NamingStrategy namingStrategy,
String tableAlias,
boolean escape) {
String transformed = getDataTransformerReadValue(tableAlias, property).orElse(null);
String columnAlias = getColumnAlias(property);
boolean useAlias = StringUtils.isNotEmpty(columnAlias);
if (transformed != null) {
sb.append(transformed).append(AS_CLAUSE).append(useAlias ? columnAlias : property.getPersistedName());
} else {
String column = getMappedName(namingStrategy, associations, property);
column = escapeColumnIfNeeded(column, escape);
if (tableAlias == null) {
sb.append(column);
} else {
sb.append(tableAlias).append(DOT).append(column);
}
if (useAlias) {
sb.append(AS_CLAUSE).append(columnAlias);
}
}
sb.append(COMMA);
}
private void appendFunction(String functionName, Expression> expression) {
appendFunction(functionName, List.of(expression));
}
private void appendFunction(String functionName, List> expressions) {
query.append(functionName)
.append(OPEN_BRACKET);
for (Iterator> iterator = expressions.iterator(); iterator.hasNext(); ) {
Expression> expression = iterator.next();
appendExpression(expression);
if (iterator.hasNext()) {
query.append(COMMA);
}
}
query.append(CLOSE_BRACKET);
if (columnAlias != null) {
query.append(AS_CLAUSE).append(columnAlias);
}
}
private void appendExpression(Expression> expression) {
AbstractSqlLikeQueryBuilder2.this.appendExpression(annotationMetadata, query, queryState, expression, true);
}
private QueryPropertyPath findProperty(String propertyPath) {
return queryState.findProperty(propertyPath);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy