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

io.micronaut.data.document.model.query.builder.MongoQueryBuilder2 Maven / Gradle / Ivy

There is a newer version: 4.9.3
Show newest version
/*
 * 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.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.annotation.TypeHint;
import io.micronaut.core.util.ArgumentUtils;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.data.annotation.Relation;
import io.micronaut.data.annotation.TypeRole;
import io.micronaut.data.document.mongo.MongoAnnotations;
import io.micronaut.data.exceptions.MappingException;
import io.micronaut.data.model.Association;
import io.micronaut.data.model.Pageable;
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.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.impl.CriteriaUtils;
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.UnaryExpression;
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.LikePredicate;
import io.micronaut.data.model.jpa.criteria.impl.predicate.NegatedPredicate;
import io.micronaut.data.model.jpa.criteria.impl.predicate.PersistentPropertyInPredicate;
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.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 io.micronaut.serde.config.annotation.SerdeConfig;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.Order;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Selection;

import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
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.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.StringJoiner;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.regex.Pattern;

import static io.micronaut.data.model.jpa.criteria.impl.CriteriaUtils.requireProperty;
import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
import static java.util.Arrays.asList;

/**
 * The Mongo query builder.
 *
 * @author Denis Stepanov
 * @since 3.3
 */
@TypeHint(MongoQueryBuilder2.class)
@Internal
public final class MongoQueryBuilder2 implements QueryBuilder2 {

    /**
     * An object with this property is replaced with an actual query parameter at the runtime.
     */
    public static final String QUERY_PARAMETER_PLACEHOLDER = "$mn_qp";
    public static final String MONGO_DATE_IDENTIFIER = "$date";
    public static final String MONGO_ID_FIELD = "_id";
    private static final String REGEX = "$regex";
    private static final String NOT = "$not";
    private static final String OPTIONS = "$options";

    @Override
    public QueryResult buildInsert(AnnotationMetadata repositoryMetadata, InsertQueryDefinition insertQueryDefinition) {
        return null;
    }

    @Override
    public QueryResult buildSelect(AnnotationMetadata annotationMetadata, SelectQueryDefinition selectQueryDefinition) {
        ArgumentUtils.requireNonNull("annotationMetadata", annotationMetadata);
        ArgumentUtils.requireNonNull("selectQueryDefinition", selectQueryDefinition);

        QueryState queryState = new QueryState(selectQueryDefinition, true);

        Map predicateObj = new LinkedHashMap<>();
        Map group = new LinkedHashMap<>();
        Map projectionObj = new LinkedHashMap<>();
        Map countObj = new LinkedHashMap<>();

        addLookups(selectQueryDefinition.getJoinPaths(), queryState);
        List> pipeline = queryState.rootLookups.pipeline;
        buildProjection(selectQueryDefinition.selection(), group, projectionObj, countObj);
        Predicate predicate = selectQueryDefinition.predicate();
        if (predicate != null) {
            predicateObj = buildWhereClause(predicate, queryState);
        }

        if (!predicateObj.isEmpty()) {
            pipeline.add(Map.of("$match", predicateObj));
        }
        if (!group.isEmpty()) {
            group.put(MONGO_ID_FIELD, null);
            pipeline.add(Map.of("$group", group));
        }
        if (!countObj.isEmpty()) {
            pipeline.add(countObj);
        }
        if (!projectionObj.isEmpty()) {
            pipeline.add(Map.of("$project", projectionObj));
        } else {
            String customProjection = annotationMetadata.stringValue(MongoAnnotations.PROJECTION).orElse(null);
            if (customProjection != null) {
                pipeline.add(Map.of("$project", new RawJsonValue(customProjection)));
            }
        }
        List orders = selectQueryDefinition.order();
        if (!orders.isEmpty()) {
            Map sortObj = new LinkedHashMap<>();
            orders.forEach(order -> {
                io.micronaut.data.model.jpa.criteria.PersistentPropertyPath persistentPropertyPath = requireProperty(order.getExpression());
                sortObj.put(persistentPropertyPath.getPathAsString(), order.isAscending() ? 1 : -1);
            });
            pipeline.add(Map.of("$sort", sortObj));
        } else {
            String customSort = annotationMetadata.stringValue(MongoAnnotations.SORT).orElse(null);
            if (customSort != null) {
                pipeline.add(Map.of("$sort", new RawJsonValue(customSort)));
            }
        }
        if (selectQueryDefinition.offset() > 0) {
            pipeline.add(Map.of("$skip", selectQueryDefinition.offset()));
        }
        if (selectQueryDefinition.limit() != -1) {
            pipeline.add(Map.of("$limit", selectQueryDefinition.limit()));
        }

        String q;
        if (pipeline.isEmpty()) {
            q = "{}";
        } else if (isMatchOnlyStage(pipeline)) {
            q = toJsonString(predicateObj);
        } else {
            q = toJsonString(pipeline);
        }
        return new QueryResult() {

            @NonNull
            @Override
            public String getQuery() {
                return q;
            }

            @Override
            public int getMax() {
                return selectQueryDefinition.limit();
            }

            @Override
            public long getOffset() {
                return selectQueryDefinition.offset();
            }

            @Override
            public List getQueryParts() {
                return Collections.emptyList();
            }

            @Override
            public List getParameterBindings() {
                return queryState.getParameterBindings();
            }

        };
    }

    private void addLookups(Collection joins, QueryState queryState) {
        if (joins.isEmpty()) {
            return;
        }
        List joined = joins.stream().map(JoinPath::getPath)
            .sorted((o1, o2) -> Comparator.comparingInt(String::length).thenComparing(String::compareTo).compare(o1, o2))
            .toList();
        for (String join : joined) {
            StringJoiner rootPath = new StringJoiner(".");
            StringJoiner currentEntityPath = new StringJoiner(".");
            LookupsStage currentLookup = queryState.rootLookups;
            for (String path : StringUtils.splitOmitEmptyStrings(join, '.')) {
                rootPath.add(path);
                currentEntityPath.add(path);
                String thisPath = currentEntityPath.toString();
                if (currentLookup.subLookups.containsKey(thisPath)) {
                    currentLookup = currentLookup.subLookups.get(path);
                    currentEntityPath = new StringJoiner(".");
                    continue;
                }

                PersistentPropertyPath propertyPath = currentLookup.persistentEntity.getPropertyPath(thisPath);
                PersistentProperty property = propertyPath.getProperty();
                if (!(property instanceof Association association)) {
                    continue;
                }
                if (association.getKind() == Relation.Kind.EMBEDDED) {
                    continue;
                }
                LookupsStage lookupStage = new LookupsStage(association.getAssociatedEntity());
                List> pipeline = currentLookup.pipeline;
                Optional inverseSide = association.getInverseSide().map(Function.identity());
                PersistentEntity persistentEntity = association.getOwner();

                String joinedCollectionName = association.getAssociatedEntity().getPersistedName();
                String ownerCollectionName = persistentEntity.getPersistedName();
                if (association.getKind() == Relation.Kind.MANY_TO_MANY || association.isForeignKey() && !inverseSide.isPresent()) {
                    PersistentEntity associatedEntity = association.getAssociatedEntity();
                    PersistentEntity associationOwner = association.getOwner();
                    // JOIN TABLE
                    PersistentProperty identity = associatedEntity.getIdentity();
                    if (identity == null) {
                        throw new IllegalArgumentException("Associated entity [" + associatedEntity.getName() + "] defines no ID. Cannot join.");
                    }
                    final PersistentProperty associatedId = associationOwner.getIdentity();
                    if (associatedId == null) {
                        throw new MappingException("Cannot join on entity [" + associationOwner.getName() + "] that has no declared ID");
                    }
                    Association owningAssociation = inverseSide.orElse(association);
                    boolean isAssociationOwner = !association.getInverseSide().isPresent();
                    NamingStrategy namingStrategy = associationOwner.getNamingStrategy();
                    AnnotationMetadata annotationMetadata = owningAssociation.getAnnotationMetadata();

                    List ownerJoinFields = resolveJoinTableAssociatedFields(annotationMetadata, isAssociationOwner, associationOwner, namingStrategy);
                    List ownerJoinCollectionFields = resolveJoinTableJoinFields(annotationMetadata, isAssociationOwner, associationOwner, namingStrategy);
                    List associationJoinFields = resolveJoinTableAssociatedFields(annotationMetadata, !isAssociationOwner, associatedEntity, namingStrategy);
                    List associationJoinCollectionFields = resolveJoinTableJoinFields(annotationMetadata, !isAssociationOwner, associatedEntity, namingStrategy);

                    String joinCollectionName = namingStrategy.mappedName(owningAssociation);

//                        String joinTableName = annotationMetadata
//                                .stringValue(ANN_JOIN_TABLE, "name")
//                                .orElseGet(() -> namingStrategy.mappedName(association));

                    List> joinCollectionLookupPipeline = new ArrayList<>();
                    pipeline.add(lookup(joinCollectionName, MONGO_ID_FIELD, ownerCollectionName, joinCollectionLookupPipeline, thisPath));
                    joinCollectionLookupPipeline.add(
                        lookup(
                            joinedCollectionName,
                            joinedCollectionName,
                            MONGO_ID_FIELD,
                            lookupStage.pipeline,
                            joinedCollectionName)
                    );
                    joinCollectionLookupPipeline.add(unwind("$" + joinedCollectionName, true));
                    joinCollectionLookupPipeline.add(
                        Map.of("$replaceRoot", Map.of("newRoot", "$" + joinedCollectionName))
                    );
                } else {
                    String currentPath = asPath(propertyPath.getAssociations(), propertyPath.getProperty());
                    if (association.isForeignKey()) {
                        String mappedBy = association.getAnnotationMetadata().stringValue(Relation.class, "mappedBy")
                            .orElseThrow(IllegalStateException::new);
                        PersistentPropertyPath mappedByPath = association.getAssociatedEntity().getPropertyPath(mappedBy);
                        if (mappedByPath == null) {
                            throw new IllegalStateException("Cannot find mapped path: " + mappedBy);
                        }
                        if (!(mappedByPath.getProperty() instanceof Association associationProperty)) {
                            throw new IllegalStateException("Expected association as a mapped path: " + mappedBy);
                        }

                        var localMatchFields = new ArrayList();
                        var foreignMatchFields = new ArrayList();
                        PersistentEntityUtils.traversePersistentProperties(currentLookup.persistentEntity.getIdentity(), (associations, p) -> {
                            localMatchFields.add(asPath(associations, p));
                        });

                        var mappedAssociations = new ArrayList<>(mappedByPath.getAssociations());
                        mappedAssociations.add(associationProperty);

                        PersistentEntityUtils.traversePersistentProperties(mappedAssociations, currentLookup.persistentEntity.getIdentity(), (associations, p) -> {
                            String fieldPath = asPath(associations, p);
                            foreignMatchFields.add(fieldPath);
                        });

                        pipeline.add(lookup(
                            joinedCollectionName,
                            localMatchFields,
                            foreignMatchFields,
                            lookupStage.pipeline,
                            currentPath)
                        );
                    } else {
                        var mappedAssociations = new ArrayList<>(propertyPath.getAssociations());
                        mappedAssociations.add((Association) propertyPath.getProperty());

                        var localMatchFields = new ArrayList();
                        var foreignMatchFields = new ArrayList();
                        PersistentProperty identity = lookupStage.persistentEntity.getIdentity();
                        if (identity == null) {
                            throw new IllegalStateException("Null identity of persistent entity: " + lookupStage.persistentEntity);
                        }
                        PersistentEntityUtils.traversePersistentProperties(mappedAssociations, identity, (associations, p) -> {
                            localMatchFields.add(asPath(associations, p));
                        });
                        PersistentEntityUtils.traversePersistentProperties(identity, (associations, p) -> {
                            foreignMatchFields.add(asPath(associations, p));
                        });

                        pipeline.add(lookup(
                            joinedCollectionName,
                            localMatchFields,
                            foreignMatchFields,
                            lookupStage.pipeline,
                            currentPath)
                        );
                    }
                    if (association.getKind().isSingleEnded()) {
                        pipeline.add(unwind("$" + currentPath, true));
                    }
                }
                currentLookup.subLookups.put(currentEntityPath.toString(), lookupStage);
            }
            queryState.joinPaths.add(join);
        }
    }

    @NonNull
    private List resolveJoinTableJoinFields(AnnotationMetadata annotationMetadata, boolean associationOwner, PersistentEntity entity, NamingStrategy namingStrategy) {
        List joinColumns = getJoinedFields(annotationMetadata, associationOwner, "name");
        if (!joinColumns.isEmpty()) {
            return joinColumns;
        }
        var fields = new ArrayList();
        PersistentEntityUtils.traversePersistentProperties(entity.getIdentity(), (associations, property) -> fields.add(asPath(associations, property)));
        return fields;
    }

    @NonNull
    private List resolveJoinTableAssociatedFields(AnnotationMetadata annotationMetadata, boolean associationOwner, PersistentEntity entity, NamingStrategy namingStrategy) {
        List joinColumns = getJoinedFields(annotationMetadata, associationOwner, "referencedColumnName");
        if (!joinColumns.isEmpty()) {
            return joinColumns;
        }
        PersistentProperty identity = entity.getIdentity();
        if (identity == null) {
            throw new MappingException("Cannot have a foreign key association without an ID on entity: " + entity.getName());
        }
        var fields = new ArrayList();
        PersistentEntityUtils.traversePersistentProperties(identity, (associations, property) -> {
            fields.add(asPath(associations, property));
        });
        return fields;
    }

    @NonNull
    private List getJoinedFields(AnnotationMetadata annotationMetadata, boolean associationOwner, String columnType) {
        // TODO: support @JoinTable style annotation
        return Collections.emptyList();
    }

    private String asPath(List associations, PersistentProperty property) {
        if (associations.isEmpty()) {
            return getPropertyPersistName(property);
        }
        var joiner = new StringJoiner(".");
        for (Association association : associations) {
            joiner.add(getPropertyPersistName(association));
        }
        joiner.add(getPropertyPersistName(property));
        return joiner.toString();
    }

    private Map lookup(String from, String localField, String foreignField, List> pipeline, String as) {
        Map lookup = new LinkedHashMap<>();
        lookup.put("from", from);
        lookup.put("localField", localField);
        lookup.put("foreignField", foreignField);
        lookup.put("pipeline", pipeline);
        lookup.put("as", as);
        return Map.of("$lookup", lookup);
    }

    private Map lookup(String from,
                                       List localFields,
                                       List foreignFields,
                                       List> pipeline,
                                       String as) {
        if (localFields.size() != foreignFields.size()) {
            throw new IllegalStateException("Un-matching join columns size: " + localFields.size() + " != " + foreignFields.size() + " " + localFields + ", " + foreignFields);
        }
        if (localFields.size() == 1) {
            return lookup(from, localFields.iterator().next(), foreignFields.iterator().next(), pipeline, as);
        }
        List> matches = new ArrayList<>(localFields.size());
        Map let = new LinkedHashMap<>();
        int i = 1;
        Iterator foreignIt = foreignFields.iterator();
        for (String localField : localFields) {
            String var = "v" + i++;
            let.put(var, "$" + localField);
            matches.add(Map.of("$eq", Arrays.asList("$$" + var, "$" + foreignIt.next())));
        }

        Map match;
        if (matches.size() > 1) {
            match = Map.of("$match", Map.of("$expr", Map.of("$and", matches)));
        } else {
            match = Map.of("$match", Map.of("$expr", matches.iterator().next()));
        }

        return lookup(from, let, match, pipeline, as);
    }

    private Map lookup(String from,
                                       Map let,
                                       Map match,
                                       List> pipeline,
                                       String as) {

        pipeline.add(match);
        Map lookup = new LinkedHashMap<>();
        lookup.put("from", from);
        lookup.put("let", let);
        lookup.put("pipeline", pipeline);
        lookup.put("as", as);
        return Map.of("$lookup", lookup);
    }

    private Map unwind(String path, boolean preserveNullAndEmptyArrays) {
        Map unwind = new LinkedHashMap<>();
        unwind.put("path", path);
        unwind.put("preserveNullAndEmptyArrays", preserveNullAndEmptyArrays);
        return Map.of("$unwind", unwind);
    }

    private boolean isMatchOnlyStage(List> pipeline) {
        return pipeline.size() == 1 && pipeline.iterator().next().containsKey("$match");
    }

    private Map buildWhereClause(Predicate predicate, QueryState queryState) {
        if (predicate == null) {
            return Map.of();
        }
        Map query = new LinkedHashMap<>();
        if (predicate instanceof IPredicate predicateVisitable) {
            predicateVisitable.visitPredicate(new MongoPredicateVisitor(queryState, query));
        } else {
            throw new IllegalStateException("Unsupported predicate type: " + predicate.getClass().getName());
        }
        return query;
    }

    private void buildProjection(Selection selection,
                                 Map groupObj,
                                 Map projectionObj,
                                 Map countObj) {
        if (selection == null) {
            return;
        }
        if (selection instanceof ISelection selectionVisitable) {
            selectionVisitable.visitSelection(new MongoSelectionVisitor(projectionObj, groupObj, countObj));
        } else {
            throw new IllegalStateException("Unsupported selection type: " + selection.getClass().getName());
        }
    }

    @NonNull
    private PersistentPropertyPath findProperty(QueryState queryState, String name) {
        return findPropertyInternal(queryState, queryState.getEntity(), name);
    }

    private PersistentPropertyPath findPropertyInternal(QueryState queryState, PersistentEntity entity, String name) {
        PersistentPropertyPath propertyPath = entity.getPropertyPath(name);
        if (propertyPath != null) {
            if (propertyPath.getAssociations().isEmpty()) {
                return propertyPath;
            }
            Association joinAssociation = null;
            StringJoiner joinPathJoiner = new StringJoiner(".");
            for (Association association : propertyPath.getAssociations()) {
                joinPathJoiner.add(association.getName());
                if (association.isEmbedded()) {
                    continue;
                }
                if (joinAssociation == null) {
                    joinAssociation = association;
                    continue;
                }
                if (association != joinAssociation.getAssociatedEntity().getIdentity()) {
                    if (!queryState.isAllowJoins()) {
                        throw new IllegalArgumentException("Joins cannot be used in a DELETE or UPDATE operation");
                    }
                    String joinStringPath = joinPathJoiner.toString();
                    if (!queryState.isJoined(joinStringPath)) {
                        throw new IllegalArgumentException("Property is not joined at path: " + joinStringPath);
                    }
                    // Continue to look for a joined property
                    joinAssociation = association;
                } else {
                    // We don't need to join to access the id of the relation
                    joinAssociation = null;
                }
            }
            PersistentProperty property = propertyPath.getProperty();
            if (joinAssociation != null) {
                if (property != joinAssociation.getAssociatedEntity().getIdentity()) {
                    String joinStringPath = joinPathJoiner.toString();
                    if (!queryState.isJoined(joinStringPath)) {
                        throw new IllegalArgumentException("Property is not joined at path: " + joinStringPath);
                    }
                }
                // We don't need to join to access the id of the relation
            }
        } else if (TypeRole.ID.equals(name) && entity.getIdentity() != null) {
            // special case handling for ID
            return PersistentPropertyPath.of(Collections.emptyList(), entity.getIdentity(), entity.getIdentity().getName());
        }
        if (propertyPath == null) {
            throw new IllegalArgumentException("Cannot order on non-existent property path: " + name);
        }
        return propertyPath;
    }

    @Override
    public QueryResult buildUpdate(AnnotationMetadata annotationMetadata, UpdateQueryDefinition updateQueryDefinition) {

        QueryState queryState = new QueryState(updateQueryDefinition, true);

        Predicate predicate = updateQueryDefinition.predicate();

        String predicateQuery;
        if (predicate != null) {
            predicateQuery = toJsonString(
                buildWhereClause(predicate, queryState)
            );
        } else {
            predicateQuery = "";
        }

        Map propertiesToUpdate = updateQueryDefinition.propertiesToUpdate();
        Map sets = CollectionUtils.newLinkedHashMap(propertiesToUpdate.size());
        for (Map.Entry e : propertiesToUpdate.entrySet()) {
            PersistentPropertyPath propertyPath = findProperty(queryState, e.getKey());
            String propertyPersistName = getPropertyPersistName(propertyPath);
            if (e.getValue() instanceof BindingParameter bindingParameter) {
                int index = queryState.pushParameter(
                    bindingParameter,
                    newBindingContext(propertyPath)
                );
                sets.put(propertyPersistName, Map.of(QUERY_PARAMETER_PLACEHOLDER, index));
            } else {
                sets.put(propertyPersistName, e.getValue());
            }
        }

        String update = toJsonString(Map.of("$set", sets));

        return new QueryResult() {

            @NonNull
            @Override
            public String getQuery() {
                return predicateQuery;
            }

            @Override
            public String getUpdate() {
                return update;
            }

            @Override
            public List getQueryParts() {
                return Collections.emptyList();
            }

            @Override
            public List getParameterBindings() {
                return queryState.getParameterBindings();
            }

            @Override
            public Map getAdditionalRequiredParameters() {
                return Collections.emptyMap();
            }

        };
    }

    @Override
    public QueryResult buildDelete(AnnotationMetadata annotationMetadata, DeleteQueryDefinition queryDefinition) {
        ArgumentUtils.requireNonNull("annotationMetadata", annotationMetadata);
        ArgumentUtils.requireNonNull("query", queryDefinition);

        QueryState queryState = new QueryState(queryDefinition, true);

        Predicate predicate = queryDefinition.predicate();

        String predicateQuery = "";
        if (predicate != null) {
            predicateQuery = toJsonString(
                buildWhereClause(predicate, queryState)
            );
        }

        return QueryResult.of(
            predicateQuery,
            Collections.emptyList(),
            queryState.getParameterBindings(),
            queryState.getAdditionalRequiredParameters(),
            queryDefinition.limit(),
            queryDefinition.offset()
        );
    }

    @Override
    public QueryResult buildPagination(Pageable pageable) {
        throw new UnsupportedOperationException();
    }

    private String toJsonString(Object obj) {
        StringBuilder sb = new StringBuilder();
        append(sb, obj);
        return sb.toString();
    }

    private void appendMap(StringBuilder sb, Map map) {
        sb.append("{");
        for (Iterator> iterator = map.entrySet().iterator(); iterator.hasNext(); ) {
            Map.Entry e = iterator.next();
            String key = e.getKey();
            Object value = e.getValue();
            if (!skipValue(value)) {
                if (shouldEscapeKey(key)) {
                    sb.append("'").append(key).append("'");
                } else {
                    sb.append(key);
                }
                sb.append(":");
                append(sb, value);
                if (iterator.hasNext()) {
                    sb.append(",");
                }
            }
        }
        sb.append("}");
    }

    private boolean skipValue(Object obj) {
        if (obj instanceof Map map) {
            return map.isEmpty();
        }
        if (obj instanceof Collection collection) {
            return collection.isEmpty();
        }
        return false;
    }

    private void appendArray(StringBuilder sb, Collection collection) {
        sb.append("[");
        for (Iterator iterator = collection.iterator(); iterator.hasNext(); ) {
            Object value = iterator.next();
            append(sb, value);
            if (iterator.hasNext()) {
                sb.append(",");
            }
        }
        sb.append("]");
    }

    private void append(StringBuilder sb, Object obj) {
        if (obj instanceof Map map) {
            appendMap(sb, map);
        } else if (obj instanceof Collection collection) {
            appendArray(sb, collection);
        } else if (obj instanceof RawJsonValue rawJsonValue) {
            sb.append(rawJsonValue.value);
        } else if (obj == null) {
            sb.append("null");
        } else if (obj instanceof Boolean) {
            sb.append(obj.toString().toLowerCase(Locale.ROOT));
        } else if (obj instanceof Number) {
            sb.append(obj);
        } else {
            sb.append('\'').append(obj).append('\'');
        }
    }

    private boolean shouldEscapeKey(String s) {
        for (char c : s.toCharArray()) {
            if (!Character.isAlphabetic(c) && !Character.isDigit(c) && c != '$' && c != '_') {
                return true;
            }
        }
        return false;
    }

    private BindingParameter.BindingContext newBindingContext(@Nullable PersistentPropertyPath ref) {
        return newBindingContext(ref, ref);
    }

    private BindingParameter.BindingContext newBindingContext(@Nullable PersistentPropertyPath in, @Nullable PersistentPropertyPath out) {
        return BindingParameter.BindingContext.create()
            .incomingMethodParameterProperty(in)
            .outgoingQueryParameterProperty(out);
    }

    /**
     * Gets criterion property name. Used as sort of adapter if property in criteria should have different name that the persistent property.
     * Used currently for id property name to be generated as _id when used in criteria.
     *
     * @param propertyPath the propertyPath
     * @return resulting name for the criteria, if identity field is used in criteria then returns _id else original criteria property name
     */
    private String getPropertyPersistName(PersistentPropertyPath propertyPath) {
        PersistentProperty property = propertyPath.getProperty();
        if (property.getOwner().getIdentity() == property) {
            return MONGO_ID_FIELD;
        }
        return property.getAnnotationMetadata()
            .stringValue(SerdeConfig.class, SerdeConfig.PROPERTY)
            .orElseGet(propertyPath::getPath);
    }

    private String getPropertyPersistName(PersistentProperty property) {
        if (property.getOwner().getIdentity() == property) {
            return MONGO_ID_FIELD;
        }
        return property.getAnnotationMetadata()
            .stringValue(SerdeConfig.class, SerdeConfig.PROPERTY)
            .orElseGet(property::getName);
    }

    private Object asLiteral(@Nullable Object value) {
        if (value instanceof RegexPattern regexPattern) {
            return "'" + Pattern.quote(regexPattern.value) + "'";
        }
        return value;
    }

    /**
     * The lookups stage data holder.
     */
    private static final class LookupsStage {

        private final PersistentEntity persistentEntity;
        private final List> pipeline = new ArrayList<>();
        private final Map subLookups = new HashMap<>();

        private LookupsStage(PersistentEntity persistentEntity) {
            this.persistentEntity = persistentEntity;
        }
    }

    /**
     * The state of the query.
     */
    @Internal
    private static final class QueryState implements PropertyParameterCreator {
        private final Set joinPaths = new TreeSet<>();
        private final AtomicInteger position = new AtomicInteger(0);
        private final Map additionalRequiredParameters = new LinkedHashMap<>();
        private final List parameterBindings;
        private final boolean allowJoins;
        private final PersistentEntity entity;

        private final LookupsStage rootLookups;

        private QueryState(BaseQueryDefinition baseQueryDefinition, boolean allowJoins) {
            this.allowJoins = allowJoins;
            this.entity = baseQueryDefinition.persistentEntity();
            this.parameterBindings = new ArrayList<>(entity.getPersistentPropertyNames().size());
            this.rootLookups = new LookupsStage(entity);
        }

        /**
         * @return The entity
         */
        public PersistentEntity getEntity() {
            return entity;
        }

        /**
         * @return Does the query allow joins
         */
        public boolean isAllowJoins() {
            return allowJoins;
        }

        /**
         * Checks if the path is joined already.
         *
         * @param associationPath The association path.
         * @return true if joined
         */
        public boolean isJoined(String associationPath) {
            for (String joinPath : joinPaths) {
                if (joinPath.startsWith(associationPath)) {
                    return true;
                }
            }
            return joinPaths.contains(associationPath);
        }

        /**
         * The additional required parameters.
         *
         * @return The parameters
         */
        public @NonNull Map getAdditionalRequiredParameters() {
            return this.additionalRequiredParameters;
        }

        /**
         * The parameter binding.
         *
         * @return The parameter binding
         */
        public List getParameterBindings() {
            return parameterBindings;
        }

        @Override
        public int pushParameter(@NonNull BindingParameter bindingParameter, @NonNull BindingParameter.BindingContext bindingContext) {
            int index = position.getAndIncrement();
            bindingContext = bindingContext.index(index);
            parameterBindings.add(
                bindingParameter.bind(bindingContext)
            );
            return index;
        }
    }

    private interface PropertyParameterCreator {

        int pushParameter(@NonNull BindingParameter bindingParameter,
                          @NonNull BindingParameter.BindingContext bindingContext);

    }

    private record RegexPattern(String value) {
    }

    private record RawJsonValue(String value) {
    }

    private class MongoPredicateVisitor implements AdvancedPredicateVisitor {

        private final PersistentEntity persistentEntity;
        private final QueryState queryState;
        private Map query;

        public MongoPredicateVisitor(QueryState queryState, Map query) {
            this.queryState = queryState;
            this.query = query;
            persistentEntity = queryState.getEntity();
        }

        private void appendOperatorExpression(PersistentPropertyPath propertyPath, String op, Object value) {
            if (value instanceof io.micronaut.data.model.jpa.criteria.PersistentPropertyPath persistentPropertyPath) {
                PersistentPropertyPath p2 = getRequiredProperty(persistentPropertyPath);
                query.put("$expr", Map.of(
                    op,
                    asList(
                        "$" + propertyPath.getPath(), "$" + p2.getPath()
                    )
                ));
                return;
            }
            PersistentEntityUtils.traversePersistentProperties(propertyPath, (associations, property) -> {
                String path = asPath(associations, property);
                query.put(path, Collections.singletonMap(op, valueRepresentation(queryState, propertyPath, PersistentPropertyPath.of(associations, property), value)));
            });
        }

        private void visitPredicate(IExpression expression) {
            if (expression instanceof IPredicate predicateVisitable) {
                predicateVisitable.visitPredicate(this);
            } else if (expression instanceof io.micronaut.data.model.jpa.criteria.PersistentPropertyPath propertyPath) {
                visitIsTrue(getRequiredProperty(propertyPath));
            } else {
                throw new IllegalStateException("Unknown boolean expression: " + expression);
            }
        }

        @Override
        public void visit(ConjunctionPredicate conjunction) {
            Collection> predicates = conjunction.getPredicates();
            if (predicates.isEmpty()) {
                return;
            }
            if (predicates.size() == 1) {
                visitPredicate(predicates.iterator().next());
                return;
            }
            List ops = new ArrayList<>(predicates.size());
            query.put("$and", ops);
            visitConjunctionPredicate(predicates, ops);
        }

        private void visitConjunctionPredicate(Collection> predicates, List ops) {
            for (IExpression expression : predicates) {
                if (expression instanceof ConjunctionPredicate conjunctionPredicate) {
                    visitConjunctionPredicate(conjunctionPredicate.getPredicates(), ops);
                } else {
                    Map preQuery = query;
                    query = new LinkedHashMap<>();
                    ops.add(query);
                    visitPredicate(expression);
                    query = preQuery;
                }
            }
        }

        @Override
        public void visit(DisjunctionPredicate disjunction) {
            Collection> predicates = disjunction.getPredicates();
            if (predicates.isEmpty()) {
                return;
            }
            if (predicates.size() == 1) {
                visitPredicate(predicates.iterator().next());
                return;
            }
            List ops = new ArrayList<>(predicates.size());
            query.put("$or", ops);
            visitDisjunctionPredicate(predicates, ops);
        }

        private void visitDisjunctionPredicate(Collection> predicates, List ops) {
            for (IExpression expression : predicates) {
                Map preQuery = query;
                query = new LinkedHashMap<>();
                ops.add(query);
                if (expression instanceof DisjunctionPredicate disjunctionPredicate) {
                    visitDisjunctionPredicate(disjunctionPredicate.getPredicates(), ops);
                } else {
                    visitPredicate(expression);
                }
                query = preQuery;
            }
        }

        @Override
        public void visit(NegatedPredicate negate) {
            IExpression negated = negate.getNegated();
            if (negated instanceof PersistentPropertyInPredicate p) {
                visitIn(getRequiredProperty(p.getPropertyPath()), p.getValues(), true);
                return;
            }
            Map preQuery = query;
            query = new LinkedHashMap<>();
            visitPredicate(negate.getNegated());
            if (query.size() != 1) {
                throw new IllegalStateException("Expected size of 1: Got: " + query + " " + negate.getNegated());
            }
            Map.Entry propertyPredicate = query.entrySet().iterator().next();
            Map negatedPropertyPredicate = Map.of("$not", propertyPredicate.getValue());
            query = preQuery;
            query.put(propertyPredicate.getKey(), negatedPropertyPredicate);
        }

        @Override
        public PersistentPropertyPath getRequiredProperty(io.micronaut.data.model.jpa.criteria.PersistentPropertyPath persistentPropertyPath) {
            return persistentPropertyPath.getPropertyPath();
        }

        @Override
        public void visitIn(PersistentPropertyPath propertyPath, Collection values, boolean negated) {
            query.put(
                getPropertyPersistName(propertyPath),
                Map.of(negated ? "$nin" : "$in", values.stream().map(val -> valueRepresentation(queryState, propertyPath, val)).toList())
            );
        }

        @Override
        public void visitRegexp(PersistentPropertyPath leftProperty, Expression expression) {
            Object value = expression;
            if (expression instanceof LiteralExpression literalExpression) {
                value = new RegexPattern((String) literalExpression.getValue());
            }
            appendOperatorExpression(leftProperty, REGEX, value);
        }

        @Override
        public void visitContains(PersistentPropertyPath leftProperty, Expression expression, boolean ignoreCase) {
            handleRegexPropertyExpression(leftProperty, ignoreCase, false, false, false, expression);
        }

        @Override
        public void visitEndsWith(PersistentPropertyPath leftProperty, Expression expression, boolean ignoreCase) {
            handleRegexPropertyExpression(leftProperty, ignoreCase, false, false, true, expression);
        }

        @Override
        public void visitStartsWith(PersistentPropertyPath leftProperty, Expression expression, boolean ignoreCase) {
            handleRegexPropertyExpression(leftProperty, ignoreCase, false, true, false, expression);
        }

        @Override
        public void visit(LikePredicate likePredicate) {
            if (likePredicate.isCaseInsensitive()) {
                throw new UnsupportedOperationException("ILike is not supported by this implementation.");
            }
            handleRegexPropertyExpression(
                CriteriaUtils.requireProperty(likePredicate.getExpression()).getPropertyPath(),
                false, false, false, false,
                likePredicate.getPattern());
        }

        @Override
        public void visitEquals(PersistentPropertyPath leftProperty, Expression expression, boolean ignoreCase) {
            if (ignoreCase) {
                handleRegexPropertyExpression(leftProperty, true, false, true, true, expression);
                return;
            }
            appendPropertyEquals(leftProperty, expression);
        }

        private void appendPropertyEquals(PersistentPropertyPath leftProperty, Object value) {
            appendOperatorExpression(leftProperty, "$eq", value);
        }

        @Override
        public void visitNotEquals(PersistentPropertyPath leftProperty, Expression expression, boolean ignoreCase) {
            if (ignoreCase) {
                handleRegexPropertyExpression(leftProperty, true, true, true, true, expression);
                return;
            }
            appendPropertyNotEquals(leftProperty, expression);
        }

        private void appendPropertyNotEquals(PersistentPropertyPath leftProperty, Object value) {
            appendOperatorExpression(leftProperty, "$ne", value);
        }

        @Override
        public void visitGreaterThan(PersistentPropertyPath leftProperty, Expression expression) {
            appendOperatorExpression(leftProperty, "$gt", expression);
        }

        @Override
        public void visitGreaterThanOrEquals(PersistentPropertyPath leftProperty, Expression expression) {
            appendOperatorExpression(leftProperty, "$gte", expression);
        }

        @Override
        public void visitLessThan(PersistentPropertyPath leftProperty, Expression expression) {
            appendOperatorExpression(leftProperty, "$lt", expression);
        }

        @Override
        public void visitLessThanOrEquals(PersistentPropertyPath leftProperty, Expression expression) {
            appendOperatorExpression(leftProperty, "$lte", expression);
        }

        @Override
        public void visitInBetween(PersistentPropertyPath property, Expression from, Expression to) {
            String propertyName = getPropertyPersistName(property);
            query.put("$and", asList(
                Map.of(propertyName, Map.of("$gte", valueRepresentation(queryState, property, from))),
                Map.of(propertyName, Map.of("$lte", valueRepresentation(queryState, property, to)))
            ));
        }

        @Override
        public void visitIsFalse(PersistentPropertyPath property) {
            appendPropertyEquals(property, false);
        }

        @Override
        public void visitIsNotNull(PersistentPropertyPath property) {
            appendPropertyNotEquals(property, null);
        }

        @Override
        public void visitIsNull(PersistentPropertyPath property) {
            appendPropertyEquals(property, null);
        }

        @Override
        public void visitIsTrue(PersistentPropertyPath property) {
            appendPropertyEquals(property, true);
        }

        @Override
        public void visitIsEmpty(PersistentPropertyPath property) {
            String propertyName = getPropertyPersistName(property);
            query.put("$or", asList(
                Map.of(propertyName, Map.of("$eq", "")),
                Map.of(propertyName, Map.of("$exists", false))
            ));
        }

        @Override
        public void visitIsNotEmpty(PersistentPropertyPath property) {
            String propertyName = getPropertyPersistName(property);
            query.put("$and", asList(
                Map.of(propertyName, Map.of("$ne", "")),
                Map.of(propertyName, Map.of("$exists", true))
            ));
        }

        @Override
        public void visitArrayContains(PersistentPropertyPath leftProperty, Expression expression) {
            Object value = expression;
            if (expression instanceof LiteralExpression literalExpression) {
                value = literalExpression.getValue();
            }
            Object criteriaValue;
            if (value instanceof Iterable iterable) {
                List values = CollectionUtils.iterableToList(iterable);
                criteriaValue = values.stream().map(val -> valueRepresentation(queryState, leftProperty, val)).toList();
            } else {
                criteriaValue = List.of(valueRepresentation(queryState, leftProperty, value));
            }
            query.put(getPropertyPersistName(leftProperty), Map.of("$all", criteriaValue));
        }

        @Override
        public void visitIdEquals(Expression expression) {
            if (persistentEntity.hasCompositeIdentity()) {
                throw new IllegalStateException("Composite ID not supported!");
            } else if (persistentEntity.hasIdentity()) {
                query.put(
                    MONGO_ID_FIELD,
                    valueRepresentation(queryState, new PersistentPropertyPath(List.of(), persistentEntity.getIdentity()), expression)
                );
            } else {
                throw new IllegalStateException("No ID found for entity: " + persistentEntity.getName());
            }
        }

        private void handleRegexPropertyExpression(PersistentPropertyPath propertyPath,
                                                   boolean ignoreCase,
                                                   boolean negate,
                                                   boolean startsWith,
                                                   boolean endsWith,
                                                   Object value) {
            Object filterValue;
            Map regexCriteria = new LinkedHashMap<>(2);
            regexCriteria.put(OPTIONS, ignoreCase ? "i" : "");
            String regexValue;
            if (value instanceof BindingParameter bindingParameter) {
                int index = queryState.pushParameter(
                    bindingParameter,
                    newBindingContext(propertyPath, propertyPath)
                );
                regexValue = QUERY_PARAMETER_PLACEHOLDER + ":" + index;
            } else {
                regexValue = value.toString();
            }
            StringBuilder regexValueBuff = new StringBuilder();
            if (startsWith) {
                regexValueBuff.append("^");
            }
            regexValueBuff.append(regexValue);
            if (endsWith) {
                regexValueBuff.append("$");
            }
            regexCriteria.put(REGEX, regexValueBuff.toString());
            if (negate) {
                filterValue = Map.of(NOT, regexCriteria);
            } else {
                filterValue = regexCriteria;
            }
            query.put(getPropertyPersistName(propertyPath), filterValue);
        }

        private Object valueRepresentation(PropertyParameterCreator parameterCreator, PersistentPropertyPath propertyPath, Object value) {
            return valueRepresentation(parameterCreator, propertyPath, propertyPath, value);
        }

        private Object valueRepresentation(PropertyParameterCreator parameterCreator,
                                           PersistentPropertyPath inPropertyPath,
                                           PersistentPropertyPath outPropertyPath,
                                           Object value) {
            if (value instanceof LocalDate localDate) {
                return Map.of(MONGO_DATE_IDENTIFIER, formatDate(localDate));
            }
            if (value instanceof LocalDateTime localDateTime) {
                return Map.of(MONGO_DATE_IDENTIFIER, formatDate(localDateTime));
            }
            if (value instanceof BindingParameter bindingParameter) {
                int index = parameterCreator.pushParameter(
                    bindingParameter,
                    newBindingContext(inPropertyPath, outPropertyPath)
                );
                return Map.of(QUERY_PARAMETER_PLACEHOLDER, index);
            } else {
                return asLiteral(value);
            }
        }

        private String formatDate(LocalDate localDate) {
            return formatDate(localDate.atStartOfDay());
        }

        private String formatDate(LocalDateTime localDateTime) {
            return formatDate(localDateTime.atZone(ZoneId.of("Z")).toInstant().toEpochMilli());
        }

        private String formatDate(final long dateTime) {
            return ZonedDateTime.ofInstant(Instant.ofEpochMilli(dateTime), ZoneId.of("Z")).format(ISO_OFFSET_DATE_TIME);
        }

    }

    private final class MongoSelectionVisitor implements SelectionVisitor {

        private final Map projectionObj;
        private final Map groupObj;
        private final Map countObj;
        private String alias;

        public MongoSelectionVisitor(Map projectionObj, Map groupObj, Map countObj) {
            this.projectionObj = projectionObj;
            this.groupObj = groupObj;
            this.countObj = countObj;
        }

        @Override
        public void visit(io.micronaut.data.model.jpa.criteria.PersistentPropertyPath persistentPropertyPath) {
            PersistentProperty property = persistentPropertyPath.getProperty();
            String propertyPersistName = getPropertyPersistName(property);
            projectionObj.put(propertyPersistName, 1);
        }

        @Override
        public void visit(AliasedSelection aliasedSelection) {
            alias = aliasedSelection.getAlias();
            aliasedSelection.getSelection().visitSelection(this);
            alias = null;
        }

        @Override
        public void visit(PersistentEntityRoot entityRoot) {
            // The default is the entity projection
        }

        @Override
        public void visit(CompoundSelection compoundSelection) {
            for (Selection selection : compoundSelection.getCompoundSelectionItems()) {
                if (selection instanceof ISelection selectionVisitable) {
                    selectionVisitable.visitSelection(this);
                } else {
                    throw new IllegalStateException("Unknown selection object: " + selection);
                }
            }
        }

        @Override
        public void visit(LiteralExpression literalExpression) {
            projectionObj.put("val", Map.of("$literal", asLiteral(literalExpression.getValue())));
        }

        @Override
        public void visit(UnaryExpression unaryExpression) {
            Expression expression = unaryExpression.getExpression();
            switch (unaryExpression.getType()) {
                case SUM, AVG, MAX, MIN -> {
                    PersistentPropertyPath propertyPath = requireProperty(expression).getPropertyPath();
                    switch (unaryExpression.getType()) {
                        case SUM -> addProjection(groupObj, "$sum", propertyPath);
                        case AVG -> addProjection(groupObj, "$avg", propertyPath);
                        case MAX -> addProjection(groupObj, "$max", propertyPath);
                        case MIN -> addProjection(groupObj, "$min", propertyPath);
                        default ->
                            throw new IllegalStateException("Unsupported expression type: " + unaryExpression.getExpression());
                    }
                }
                case COUNT -> {
                    // before adding support for count distinct in https://github.com/micronaut-projects/micronaut-data/issues/2695
                    // it was producing the same query as this, same as count basically
                    countObj.put("$count", "result");
                }
                case COUNT_DISTINCT -> {
                    if (expression instanceof PersistentEntityRoot) {
                        // before adding support for count distinct in https://github.com/micronaut-projects/micronaut-data/issues/2695
                        // it was producing the same query as this, same as count basically
                        countObj.put("$count", "result");
                    } else if (expression instanceof io.micronaut.data.model.jpa.criteria.PersistentPropertyPath) {
                        throw new UnsupportedOperationException("Count distinct against property is not supported by Micronaut Data MongoDB.");
                    } else {
                        throw new IllegalStateException("Illegal expression: " + expression + " for count distinct selection!");
                    }
                }
                default ->
                    throw new IllegalStateException("Unsupported expression type: " + unaryExpression.getExpression());
            }
        }

        @Override
        public void visit(IdExpression idExpression) {
            projectionObj.put(MONGO_ID_FIELD, 1);
        }

        private void addProjection(Map groupBy, String op, PersistentPropertyPath propertyPath) {
            groupBy.put(alias == null ? propertyPath.getProperty().getName() : alias, Map.of(op, "$" + propertyPath.getPath()));
        }

        @Override
        public void visit(FunctionExpression functionExpression) {
            throw new UnsupportedOperationException("Function expression is not supported by Micronaut Data MongoDB.");
        }

        @Override
        public void visit(BinaryExpression binaryExpression) {
            throw new UnsupportedOperationException("Binary expression: " + binaryExpression + " is not supported by Micronaut Data MongoDB.");
        }
    }
}