io.micronaut.data.document.model.query.builder.CosmosSqlQueryBuilder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of micronaut-data-document-model Show documentation
Show all versions of micronaut-data-document-model Show documentation
Data Repository Support for Micronaut
/*
* Copyright 2017-2022 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.document.model.query.builder;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.Creator;
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.CollectionUtils;
import io.micronaut.data.annotation.repeatable.WhereSpecifications;
import io.micronaut.data.model.Association;
import io.micronaut.data.model.Embedded;
import io.micronaut.data.model.Pageable;
import io.micronaut.data.model.Pageable.Mode;
import io.micronaut.data.model.PersistentEntity;
import io.micronaut.data.model.PersistentProperty;
import io.micronaut.data.model.PersistentPropertyPath;
import io.micronaut.data.model.naming.NamingStrategies;
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.QueryModel;
import io.micronaut.data.model.query.builder.QueryParameterBinding;
import io.micronaut.data.model.query.builder.QueryResult;
import io.micronaut.data.model.query.builder.sql.SqlQueryBuilder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.StringJoiner;
import java.util.function.BiConsumer;
/**
* The Azure Cosmos DB sql query builder.
*
* @author radovanradic
* @since 3.9.0
*/
public final class CosmosSqlQueryBuilder extends SqlQueryBuilder {
private static final String VALUE = "VALUE ";
private static final String SELECT_COUNT = "COUNT(1)";
private static final String JOIN = " JOIN ";
private static final String IN = " IN ";
private static final String IS_NULL = "IS_NULL";
private static final String IS_DEFINED = "IS_DEFINED";
private static final String ARRAY_CONTAINS = "ARRAY_CONTAINS";
private static final NamingStrategy RAW_NAMING_STRATEGY = new NamingStrategies.Raw();
@Creator
public CosmosSqlQueryBuilder(AnnotationMetadata annotationMetadata) {
super(annotationMetadata);
initializeCriteriaHandlers();
}
/**
* Default constructor.
*/
public CosmosSqlQueryBuilder() {
super();
initializeCriteriaHandlers();
}
@Override
protected String asLiteral(Object value) {
if (value instanceof Boolean) {
return value.toString();
}
return super.asLiteral(value);
}
@Override
protected void appendProjectionRowCount(StringBuilder queryString, String logicalName) {
queryString.append(SELECT_COUNT);
}
@Override
protected void appendProjectionRowCountDistinct(StringBuilder queryString, QueryState queryState, PersistentEntity entity, AnnotationMetadata annotationMetadata, String logicalName) {
throw new UnsupportedOperationException("Count distinct is not supported by Micronaut Data Azure Cosmos.");
}
@Override
protected NamingStrategy getNamingStrategy(PersistentEntity entity) {
return entity.findNamingStrategy().orElse(RAW_NAMING_STRATEGY);
}
@Override
protected NamingStrategy getNamingStrategy(PersistentPropertyPath propertyPath) {
return propertyPath.findNamingStrategy().orElse(RAW_NAMING_STRATEGY);
}
@Override
protected void traversePersistentProperties(List associations,
PersistentProperty property,
BiConsumer, PersistentProperty> consumerProperty) {
if (property instanceof Embedded) {
consumerProperty.accept(associations, property);
return;
}
super.traversePersistentProperties(associations, property, consumerProperty);
}
@Override
public QueryResult buildQuery(@NonNull AnnotationMetadata annotationMetadata, @NonNull QueryModel query) {
ArgumentUtils.requireNonNull("annotationMetadata", annotationMetadata);
ArgumentUtils.requireNonNull("query", query);
QueryState queryState = new QueryState(query, true, true);
List joinPaths = new ArrayList<>(query.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 select = new StringBuilder(SELECT_CLAUSE);
String logicalName = queryState.getRootAlias();
PersistentEntity entity = queryState.getEntity();
List projections = query.getProjections();
buildSelect(
annotationMetadata,
queryState,
select,
projections,
logicalName,
entity
);
// For projections, we need to have VALUE in order to be able to read value
// but for DTO when there can be more fields retrieved (meaning there is comma in the query) then VALUE cannot work
// also literal projection does not need VALUE
if (projections.size() == 1 && !(projections.get(0) instanceof QueryModel.LiteralProjection) && !(projections.get(0) instanceof QueryModel.RootEntityProjection) && select.indexOf(",") == -1) {
select.insert(SELECT_CLAUSE.length(), VALUE);
}
select.append(FROM_CLAUSE).append(getTableName(entity)).append(SPACE).append(logicalName);
QueryModel queryModel = queryState.getQueryModel();
Collection allPaths = queryModel.getJoinPaths();
appendJoins(queryState, select, allPaths, null);
queryState.getQuery().insert(0, select);
QueryModel.Junction criteria = query.getCriteria();
if (!criteria.isEmpty() || annotationMetadata.hasStereotype(WhereSpecifications.class) || queryState.getEntity().getAnnotationMetadata().hasStereotype(WhereSpecifications.class)) {
buildWhereClause(annotationMetadata, criteria, queryState);
}
appendOrder(annotationMetadata, query, queryState);
appendForUpdate(QueryPosition.END_OF_QUERY, query, queryState.getQuery());
return QueryResult.of(
queryState.getFinalQuery(),
queryState.getQueryParts(),
queryState.getParameterBindings(),
queryState.getAdditionalRequiredParameters(),
query.getMax(),
query.getOffset(),
queryState.getJoinPaths()
);
}
@Internal
@Override
protected void selectAllColumnsFromJoinPaths(QueryState queryState,
StringBuilder queryBuffer,
Collection allPaths,
@Nullable Map joinAliasOverride) {
// Does nothing since we don't select columns in joins
}
/**
* We use this method instead of {@link #selectAllColumnsFromJoinPaths(QueryState, StringBuilder, Collection, Map)}
* and said method is empty because Cosmos Db has different join logic.
* @param queryState
* @param queryBuffer
* @param allPaths
* @param joinAliasOverride
*/
private void appendJoins(QueryState queryState,
StringBuilder queryBuffer,
Collection allPaths,
@Nullable Map joinAliasOverride) {
if (CollectionUtils.isEmpty(allPaths)) {
return;
}
String logicalName = queryState.getRootAlias();
Map joinedPaths = new HashMap<>();
for (JoinPath joinPath : allPaths) {
Association association = joinPath.getAssociation();
if (association.isEmbedded()) {
// joins on embedded don't make sense
continue;
}
String joinAlias = joinAliasOverride == null ? getAliasName(joinPath) : joinAliasOverride.get(joinPath);
// cannot join family_.children c join family_children.pets p but instead must do
// join family_.children c join c.pets p (must go via children table)
String path = logicalName + DOT + joinPath.getPath();
for (Map.Entry entry : joinedPaths.entrySet()) {
String joinedPath = entry.getKey();
String prefix = joinedPath + DOT;
if (path.startsWith(prefix) && !joinedPath.equals(path)) {
path = entry.getValue() + DOT + path.replace(prefix, "");
break;
}
}
queryBuffer.append(JOIN).append(joinAlias).append(IN).append(path);
joinedPaths.put(path, joinAlias);
}
}
@Override
protected boolean appendAssociationProjection(QueryState queryState, StringBuilder queryString, PersistentProperty property, PersistentPropertyPath propertyPath, String columnAlias) {
String joinedPath = propertyPath.getPath();
if (!queryState.isJoined(joinedPath)) {
queryString.setLength(queryString.length() - 1);
return false;
}
String joinAlias = queryState.computeAlias(propertyPath.getPath());
selectAllColumns(((Association) property).getAssociatedEntity(), joinAlias, queryString);
return true;
}
@Override
protected void selectAllColumns(AnnotationMetadata annotationMetadata, QueryState queryState, StringBuilder queryBuffer) {
queryBuffer.append(DISTINCT).append(SPACE).append(VALUE).append(queryState.getRootAlias());
}
@Override
protected void buildJoin(String joinType,
StringBuilder sb,
QueryState queryState,
List joinAssociationsPath,
String joinAlias,
Association association,
PersistentEntity associatedEntity,
PersistentEntity associationOwner,
String currentJoinAlias) {
// Does nothing since joins in Cosmos Db work different way
}
@Override
protected StringBuilder appendDeleteClause(StringBuilder queryString) {
// For delete we return SELECT * FROM ... WHERE to get documents and use API to delete them
return queryString.append("SELECT * ").append(FROM_CLAUSE);
}
@Override
protected boolean isAliasForBatch(PersistentEntity persistentEntity, AnnotationMetadata annotationMetadata) {
return true;
}
@Override
protected boolean computePropertyPaths() {
return false;
}
@Override
public QueryResult buildInsert(AnnotationMetadata repositoryMetadata, PersistentEntity entity) {
return null;
}
@Override
public QueryResult buildUpdate(AnnotationMetadata annotationMetadata, QueryModel query, Map propertiesToUpdate) {
QueryResult queryResult = super.buildUpdate(annotationMetadata, query, propertiesToUpdate);
String resultQuery = queryResult.getQuery();
PersistentEntity entity = query.getPersistentEntity();
String tableAlias = getAliasName(entity);
String tableName = getTableName(entity);
final String finalQuery = "SELECT * FROM " + tableName + SPACE + tableAlias + SPACE +
resultQuery.substring(resultQuery.toLowerCase(Locale.ROOT).indexOf("where"));
StringJoiner stringJoiner = new StringJoiner(",");
propertiesToUpdate.keySet().forEach(stringJoiner::add);
final String update = stringJoiner.toString();
return new QueryResult() {
@NonNull
@Override
public String getQuery() {
return finalQuery;
}
@Override
public String getUpdate() {
return update;
}
@Override
public List getQueryParts() {
return queryResult.getQueryParts();
}
@Override
public List getParameterBindings() {
return queryResult.getParameterBindings();
}
@Override
public Map getAdditionalRequiredParameters() {
return queryResult.getAdditionalRequiredParameters();
}
};
}
@NonNull
@Override
public QueryResult buildPagination(@NonNull Pageable pageable) {
if (pageable.getMode() != Mode.OFFSET) {
throw new UnsupportedOperationException("Pageable mode " + pageable.getMode() + " is not supported by cosmos operations");
}
int size = pageable.getSize();
if (size > 0) {
StringBuilder builder = new StringBuilder(" ");
long from = pageable.getOffset();
builder.append("OFFSET ").append(from).append(" LIMIT ").append(size).append(" ");
return QueryResult.of(
builder.toString(),
Collections.emptyList(),
Collections.emptyList(),
Collections.emptyMap()
);
}
return QueryResult.of(
"",
Collections.emptyList(),
Collections.emptyList(),
Collections.emptyMap()
);
}
/**
* Initializes criteria handlers specific for Cosmos Db.
*/
private void initializeCriteriaHandlers() {
addCriterionHandler(QueryModel.IsNull.class, (ctx, criterion) -> {
ctx.query().append(NOT).append(SPACE).append(IS_DEFINED).append(OPEN_BRACKET);
appendPropertyRef(ctx.query(), ctx.getAnnotationMetadata(), ctx.getPersistentEntity(), ctx.getRequiredProperty(criterion));
ctx.query().append(CLOSE_BRACKET).append(SPACE).append(OR).append(SPACE);
ctx.query().append(IS_NULL).append(OPEN_BRACKET);
appendPropertyRef(ctx.query(), ctx.getAnnotationMetadata(), ctx.getPersistentEntity(), ctx.getRequiredProperty(criterion));
ctx.query().append(CLOSE_BRACKET);
});
addCriterionHandler(QueryModel.IsNotNull.class, (ctx, criterion) -> {
ctx.query().append(IS_DEFINED).append(OPEN_BRACKET);
appendPropertyRef(ctx.query(), ctx.getAnnotationMetadata(), ctx.getPersistentEntity(), ctx.getRequiredProperty(criterion));
ctx.query().append(CLOSE_BRACKET).append(SPACE).append(AND).append(SPACE);
ctx.query().append(NOT).append(SPACE).append(IS_NULL).append(OPEN_BRACKET);
appendPropertyRef(ctx.query(), ctx.getAnnotationMetadata(), ctx.getPersistentEntity(), ctx.getRequiredProperty(criterion));
ctx.query().append(CLOSE_BRACKET);
});
addCriterionHandler(QueryModel.IsEmpty.class, (ctx, criterion) -> {
ctx.query().append(NOT).append(SPACE).append(IS_DEFINED).append(OPEN_BRACKET);
appendPropertyRef(ctx.query(), ctx.getAnnotationMetadata(), ctx.getPersistentEntity(), ctx.getRequiredProperty(criterion));
ctx.query().append(CLOSE_BRACKET).append(SPACE).append(OR).append(SPACE);
ctx.query().append(IS_NULL).append(OPEN_BRACKET);
appendPropertyRef(ctx.query(), ctx.getAnnotationMetadata(), ctx.getPersistentEntity(), ctx.getRequiredProperty(criterion));
ctx.query().append(CLOSE_BRACKET).append(SPACE).append(OR).append(SPACE);
appendPropertyRef(ctx.query(), ctx.getAnnotationMetadata(), ctx.getPersistentEntity(), ctx.getRequiredProperty(criterion));
ctx.query().append(EQUALS).append("''");
});
addCriterionHandler(QueryModel.IsNotEmpty.class, (ctx, criterion) -> {
ctx.query().append(IS_DEFINED).append(OPEN_BRACKET);
appendPropertyRef(ctx.query(), ctx.getAnnotationMetadata(), ctx.getPersistentEntity(), ctx.getRequiredProperty(criterion));
ctx.query().append(CLOSE_BRACKET).append(SPACE).append(AND).append(SPACE);
ctx.query().append(NOT).append(SPACE).append(IS_NULL).append(OPEN_BRACKET);
appendPropertyRef(ctx.query(), ctx.getAnnotationMetadata(), ctx.getPersistentEntity(), ctx.getRequiredProperty(criterion));
ctx.query().append(CLOSE_BRACKET).append(SPACE).append(AND).append(SPACE);
appendPropertyRef(ctx.query(), ctx.getAnnotationMetadata(), ctx.getPersistentEntity(), ctx.getRequiredProperty(criterion));
ctx.query().append(NOT_EQUALS).append("''");
});
addCriterionHandler(QueryModel.ArrayContains.class, (ctx, criterion) -> {
QueryPropertyPath propertyPath = ctx.getRequiredProperty(criterion.getProperty(), QueryModel.ArrayContains.class);
StringBuilder whereClause = ctx.query();
whereClause.append(ARRAY_CONTAINS).append(OPEN_BRACKET);
appendPropertyRef(whereClause, ctx.getAnnotationMetadata(), ctx.getPersistentEntity(), propertyPath);
whereClause.append(COMMA);
Object value = criterion.getValue();
if (value instanceof BindingParameter bindingParameter) {
ctx.pushParameter(bindingParameter, newBindingContext(propertyPath.getPropertyPath()));
} else {
asLiterals(ctx.query(), value);
}
whereClause.append(COMMA).append("true").append(CLOSE_BRACKET);
});
}
}