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

io.micronaut.data.runtime.operations.internal.sql.AbstractSqlRepositoryOperations Maven / Gradle / Ivy

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.runtime.operations.internal.sql;

import io.micronaut.aop.MethodInvocationContext;
import io.micronaut.context.ApplicationContextProvider;
import io.micronaut.context.BeanContext;
import io.micronaut.core.annotation.AnnotationClassValue;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.beans.BeanProperty;
import io.micronaut.core.reflect.ReflectionUtils;
import io.micronaut.data.annotation.AutoPopulated;
import io.micronaut.data.annotation.MappedProperty;
import io.micronaut.data.annotation.Repository;
import io.micronaut.data.annotation.TypeDef;
import io.micronaut.data.exceptions.DataAccessException;
import io.micronaut.data.model.Association;
import io.micronaut.data.model.DataType;
import io.micronaut.data.model.JsonDataType;
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.impl.QueryResultPersistentEntityCriteriaQuery;
import io.micronaut.data.model.query.builder.QueryResult;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.model.query.builder.sql.SqlQueryBuilder2;
import io.micronaut.data.model.runtime.AttributeConverterRegistry;
import io.micronaut.data.model.runtime.BeanPropertyWithAnnotationMetadata;
import io.micronaut.data.model.runtime.PreparedQuery;
import io.micronaut.data.model.runtime.QueryParameterBinding;
import io.micronaut.data.model.runtime.QueryResultInfo;
import io.micronaut.data.model.runtime.RuntimeAssociation;
import io.micronaut.data.model.runtime.RuntimeEntityRegistry;
import io.micronaut.data.model.runtime.RuntimePersistentEntity;
import io.micronaut.data.model.runtime.RuntimePersistentProperty;
import io.micronaut.data.model.runtime.StoredQuery;
import io.micronaut.data.operations.HintsCapableRepository;
import io.micronaut.data.runtime.config.DataSettings;
import io.micronaut.data.runtime.convert.DataConversionService;
import io.micronaut.data.runtime.criteria.RuntimeCriteriaBuilder;
import io.micronaut.data.runtime.date.DateTimeProvider;
import io.micronaut.data.runtime.mapper.QueryStatement;
import io.micronaut.data.runtime.mapper.ResultReader;
import io.micronaut.data.runtime.mapper.sql.JsonQueryResultMapper;
import io.micronaut.data.runtime.mapper.sql.SqlJsonValueMapper;
import io.micronaut.data.runtime.mapper.sql.SqlResultEntityTypeMapper;
import io.micronaut.data.runtime.mapper.sql.SqlTypeMapper;
import io.micronaut.data.runtime.operations.internal.AbstractRepositoryOperations;
import io.micronaut.data.runtime.query.MethodContextAwareStoredQueryDecorator;
import io.micronaut.data.runtime.query.PreparedQueryDecorator;
import io.micronaut.data.runtime.query.internal.BasicStoredQuery;
import io.micronaut.data.runtime.query.internal.QueryResultStoredQuery;
import io.micronaut.inject.BeanDefinition;
import io.micronaut.inject.annotation.AnnotationMetadataHierarchy;
import io.micronaut.inject.qualifiers.Qualifiers;
import io.micronaut.json.JsonMapper;
import jakarta.persistence.Tuple;
import org.slf4j.Logger;

import java.io.IOException;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;
import java.util.stream.Stream;

import static io.micronaut.data.model.runtime.StoredQuery.OperationType;

/**
 * Abstract SQL repository implementation not specifically bound to JDBC.
 *
 * @param   The result set type
 * @param   The prepared statement type
 * @param  The exception type
 * @author graemerocher
 * @author Denis Stepanov
 * @since 1.0.0
 */
@Internal
public abstract class AbstractSqlRepositoryOperations
    extends AbstractRepositoryOperations implements ApplicationContextProvider,
    PreparedQueryDecorator,
    MethodContextAwareStoredQueryDecorator,
    HintsCapableRepository {

    protected static final Logger QUERY_LOG = DataSettings.QUERY_LOG;

    protected final String dataSourceName;
    @SuppressWarnings("WeakerAccess")
    protected final ResultReader columnNameResultSetReader;
    @SuppressWarnings("WeakerAccess")
    protected final ResultReader columnIndexResultSetReader;
    @SuppressWarnings("WeakerAccess")
    protected final QueryStatement preparedStatementWriter;
    protected final JsonMapper jsonMapper;
    protected final SqlJsonColumnMapperProvider sqlJsonColumnMapperProvider;
    protected final Map queryBuilders = new HashMap<>(10);
    protected final Map repositoriesWithHardcodedDataSource = new HashMap<>(10);
    private final Map entityInserts = new ConcurrentHashMap<>(10);
    private final Map entityUpdates = new ConcurrentHashMap<>(10);
    private final Map associationInserts = new ConcurrentHashMap<>(10);

    /**
     * Default constructor.
     *
     * @param dataSourceName              The datasource name
     * @param columnNameResultSetReader   The column name result reader
     * @param columnIndexResultSetReader  The column index result reader
     * @param preparedStatementWriter     The prepared statement writer
     * @param dateTimeProvider            The date time provider
     * @param runtimeEntityRegistry       The entity registry
     * @param beanContext                 The bean context
     * @param conversionService           The conversion service
     * @param attributeConverterRegistry  The attribute converter registry
     * @param jsonMapper                  The JSON mapper
     * @param sqlJsonColumnMapperProvider The SQL JSON column mapper provider
     */
    protected AbstractSqlRepositoryOperations(
        String dataSourceName,
        ResultReader columnNameResultSetReader,
        ResultReader columnIndexResultSetReader,
        QueryStatement preparedStatementWriter,
        DateTimeProvider dateTimeProvider,
        RuntimeEntityRegistry runtimeEntityRegistry,
        BeanContext beanContext,
        DataConversionService conversionService,
        AttributeConverterRegistry attributeConverterRegistry,
        JsonMapper jsonMapper,
        SqlJsonColumnMapperProvider sqlJsonColumnMapperProvider) {
        super(dateTimeProvider, runtimeEntityRegistry, conversionService, attributeConverterRegistry);
        this.dataSourceName = dataSourceName;
        this.columnNameResultSetReader = columnNameResultSetReader;
        this.columnIndexResultSetReader = columnIndexResultSetReader;
        this.preparedStatementWriter = preparedStatementWriter;
        this.jsonMapper = jsonMapper;
        this.sqlJsonColumnMapperProvider = sqlJsonColumnMapperProvider;
        Collection> beanDefinitions = beanContext
            .getBeanDefinitions(Object.class, Qualifiers.byStereotype(Repository.class));
        for (BeanDefinition beanDefinition : beanDefinitions) {
            String targetDs = beanDefinition.stringValue(Repository.class).orElse(null);
            Class beanType = beanDefinition.getBeanType();
            if (targetDs == null || targetDs.equalsIgnoreCase(dataSourceName)) {
                SqlQueryBuilder2 queryBuilder = new SqlQueryBuilder2(beanDefinition.getAnnotationMetadata());
                queryBuilders.put(beanType, queryBuilder);
            } else {
                repositoriesWithHardcodedDataSource.put(beanType, targetDs);
            }
        }
    }

    /**
     * @return The result reader that will check for the column existence and return null for {@link ResultReader#readDynamic(Object, Object, DataType)}
     */
    protected ResultReader createColumnNameResultSetReaderWithColumnExistenceAware() {
        return columnNameResultSetReader;
    }

    @Override
    public  PreparedQuery decorate(PreparedQuery preparedQuery) {
        return new DefaultSqlPreparedQuery<>(preparedQuery);
    }

    @Override
    public  StoredQuery decorate(MethodInvocationContext context, StoredQuery storedQuery) {
        Class repositoryType = context.getTarget().getClass();
        SqlQueryBuilder2 queryBuilder = findQueryBuilder(repositoryType);
        RuntimePersistentEntity runtimePersistentEntity = runtimeEntityRegistry.getEntity(storedQuery.getRootEntity());
        return new DefaultSqlStoredQuery<>(storedQuery, runtimePersistentEntity, queryBuilder);
    }

    /**
     * Prepare a statement for execution.
     *
     * @param statementFunction The statement function
     * @param preparedQuery     The prepared query
     * @param isUpdate          Is this an update
     * @param isSingleResult    Is it a single result
     * @param                The query declaring type
     * @param                The query result type
     * @return The prepared statement
     */
    protected  PS prepareStatement(StatementSupplier statementFunction,
                                         @NonNull PreparedQuery preparedQuery,
                                         boolean isUpdate,
                                         boolean isSingleResult) throws Exc {
        SqlPreparedQuery sqlPreparedQuery = getSqlPreparedQuery(preparedQuery);
        sqlPreparedQuery.prepare(null);
        if (!isUpdate) {
            sqlPreparedQuery.attachPageable(preparedQuery.getPageable(), isSingleResult);
        }

        String query = sqlPreparedQuery.getQuery();
        if (QUERY_LOG.isDebugEnabled()) {
            QUERY_LOG.debug("Executing Query: {}", query);
        }
        final PS ps;
        try {
            ps = statementFunction.create(query);
        } catch (Exception e) {
            throw new DataAccessException("Unable to prepare query [" + query + "]: " + e.getMessage(), e);
        }
        return ps;
    }

    /**
     * Set the parameter value on the given statement.
     *
     * @param preparedStatement The prepared statement
     * @param index             The index
     * @param dataType          The data type
     * @param jsonDataType      The JSON representation type if data type is JSON
     * @param value             The value
     * @param storedQuery       The SQL stored query
     */
    protected void setStatementParameter(PS preparedStatement, int index, DataType dataType, JsonDataType jsonDataType, Object value, SqlStoredQuery storedQuery) {
        Dialect dialect = storedQuery.getDialect();
        switch (dataType) {
            case UUID:
                if (value != null && dialect.requiresStringUUID(dataType)) {
                    value = value.toString();
                }
                break;
            case JSON:
                value = getJsonValue(storedQuery, jsonDataType, index, value);
                break;
            case ENTITY:
                if (value != null) {
                    RuntimePersistentProperty idReader = getIdReader(value);
                    Object id = idReader.getProperty().get(value);
                    if (id == null) {
                        throw new DataAccessException("Supplied entity is a transient instance: " + value);
                    }
                    setStatementParameter(preparedStatement, index, idReader.getDataType(), jsonDataType, id, storedQuery);
                    return;
                }
                break;
            default:
                break;
        }

        dataType = dialect.getDataType(dataType);

        if (QUERY_LOG.isTraceEnabled()) {
            QUERY_LOG.trace("Binding parameter at position {} to value {} with data type: {}", index, value, dataType);
        }

        // We want to avoid potential conversion for JSON because mapper already returned value ready to be set as statement parameter
        if (dataType == DataType.JSON && value != null) {
            preparedStatementWriter.setValue(preparedStatement, index, value);
            return;
        }

        preparedStatementWriter.setDynamic(preparedStatement, index, dataType, value);
    }

    private Object getJsonValue(SqlStoredQuery storedQuery, JsonDataType jsonDataType, int index, Object value) {
        if (value == null || value.getClass().equals(String.class)) {
            return value;
        }
        SqlJsonValueMapper sqlJsonValueMapper = sqlJsonColumnMapperProvider.getJsonValueMapper(storedQuery, jsonDataType, value);
        if (sqlJsonValueMapper == null) {
            // if json mapper is not on the classpath and object needs to use JSON value mapper
            throw new IllegalStateException("For JSON data types support Micronaut JsonMapper needs to be available on the classpath.");
        }
        try {
            return sqlJsonValueMapper.mapValue(value, jsonDataType);
        } catch (IOException e) {
            throw new DataAccessException("Failed setting JSON field parameter at index " + index, e);
        }
    }

    /**
     * Resolves a stored insert for the given entity.
     *
     * @param annotationMetadata The repository annotation metadata
     * @param repositoryType     The repository type
     * @param rootEntity         The root entity
     * @param persistentEntity   The persistent entity
     * @param                 The entity type
     * @return The insert
     */
    @NonNull
    protected  SqlStoredQuery resolveEntityInsert(AnnotationMetadata annotationMetadata,
                                                           Class repositoryType,
                                                           @NonNull Class rootEntity,
                                                           @NonNull RuntimePersistentEntity persistentEntity) {

        //noinspection unchecked
        return entityInserts.computeIfAbsent(new QueryKey(repositoryType, rootEntity), (queryKey) -> {
            final SqlQueryBuilder2 queryBuilder = findQueryBuilder(repositoryType);
            final QueryResult queryResult = queryBuilder.buildInsert(annotationMetadata, new SqlQueryBuilder2.InsertQueryDefinitionImpl(persistentEntity));

            return new DefaultSqlStoredQuery<>(QueryResultStoredQuery.single(OperationType.INSERT, "Custom insert", AnnotationMetadata.EMPTY_METADATA, queryResult, rootEntity), persistentEntity, queryBuilder);
        });
    }

    /**
     * Builds a join table insert.
     *
     * @param repositoryType   The repository type
     * @param persistentEntity The entity
     * @param association      The association
     * @param               The entity generic type
     * @return The insert statement
     */
    protected  String resolveAssociationInsert(Class repositoryType,
                                                  RuntimePersistentEntity persistentEntity,
                                                  RuntimeAssociation association) {
        return associationInserts.computeIfAbsent(association, association1 -> {
            final SqlQueryBuilder2 queryBuilder = findQueryBuilder(repositoryType);
            return queryBuilder.buildJoinTableInsert(persistentEntity, association1);
        });
    }

    /**
     * Resolves a stored update for the given entity.
     *
     * @param annotationMetadata The repository annotation metadata
     * @param repositoryType     The repository type
     * @param rootEntity         The root entity
     * @param persistentEntity   The persistent entity
     * @param                 The entity type
     * @return The insert
     */
    @NonNull
    protected  SqlStoredQuery resolveEntityUpdate(AnnotationMetadata annotationMetadata,
                                                           Class repositoryType,
                                                           @NonNull Class rootEntity,
                                                           @NonNull RuntimePersistentEntity persistentEntity) {

        final QueryKey key = new QueryKey(repositoryType, rootEntity);
        //noinspection unchecked
        return entityUpdates.computeIfAbsent(key, (queryKey) -> {
            final SqlQueryBuilder2 queryBuilder = findQueryBuilder(repositoryType);

            var criteriaBuilder = new RuntimeCriteriaBuilder(runtimeEntityRegistry);
            var criteriaUpdate = criteriaBuilder.createCriteriaUpdate(rootEntity);
            var root = criteriaUpdate.getRoot();

            criteriaUpdate.where(
                criteriaBuilder.equal(root.id(), criteriaBuilder.parameter(Object.class))
            );

            persistentEntity.getPersistentProperties()
                .stream().filter(p ->
                    !(p instanceof Association association && association.isForeignKey()) &&
                        p.getAnnotationMetadata().booleanValue(AutoPopulated.class, "updateable").orElse(true)
                )
                .forEach(prop -> criteriaUpdate.set(prop.getName(), criteriaBuilder.parameter(prop.getType())));

            final QueryResult queryResult = ((QueryResultPersistentEntityCriteriaQuery) criteriaUpdate).buildQuery(annotationMetadata, queryBuilder);
            return new DefaultSqlStoredQuery<>(
                QueryResultStoredQuery.single(OperationType.UPDATE, "Custom update", AnnotationMetadata.EMPTY_METADATA, queryResult, rootEntity),
                persistentEntity,
                queryBuilder);
        });
    }

    /**
     * Resolve SQL insert association operation.
     *
     * @param repositoryType   The repository type
     * @param association      The association
     * @param persistentEntity The persistent entity
     * @param entity           The entity
     * @param               The entity type
     * @return The operation
     */
    protected  SqlStoredQuery resolveSqlInsertAssociation(Class repositoryType, RuntimeAssociation association, RuntimePersistentEntity persistentEntity, T entity) {
        String sqlInsert = resolveAssociationInsert(repositoryType, persistentEntity, association);
        final SqlQueryBuilder2 queryBuilder = findQueryBuilder(repositoryType);
        List parameters = new ArrayList<>();
        for (Map.Entry property : idPropertiesWithValues(persistentEntity.getIdentity(), entity).toList()) {
            parameters.add(new QueryParameterBinding() {

                @Override
                public String getName() {
                    return property.getKey().getName();
                }

                @Override
                public DataType getDataType() {
                    return property.getKey().getDataType();
                }

                @Override
                public JsonDataType getJsonDataType() {
                    return property.getKey().getJsonDataType();
                }

                @Override
                public Class getParameterConverterClass() {
                    return property.getKey()
                        .getAnnotationMetadata()
                        .getAnnotation(TypeDef.class)
                        .annotationClassValue("converter")
                        .flatMap(AnnotationClassValue::getType)
                        .orElse(null);
                }

                @Override
                public Object getValue() {
                    return property.getValue();
                }
            });
        }
        for (PersistentPropertyPath pp : idProperties(association.getAssociatedEntity().getIdentity()).toList()) {
            parameters.add(new QueryParameterBinding() {

                @Override
                public String getName() {
                    return pp.getProperty().getName();
                }

                @Override
                public DataType getDataType() {
                    return pp.getProperty().getDataType();
                }

                @Override
                public JsonDataType getJsonDataType() {
                    return pp.getProperty().getJsonDataType();
                }

                @Override
                public String[] getPropertyPath() {
                    return pp.getArrayPath();
                }

                @Override
                public Class getParameterConverterClass() {
                    return pp.getProperty()
                        .getAnnotationMetadata()
                        .getAnnotation(TypeDef.class)
                        .annotationClassValue("converter")
                        .flatMap(AnnotationClassValue::getType)
                        .orElse(null);
                }
            });
        }

        RuntimePersistentEntity associatedEntity = association.getAssociatedEntity();
        return new DefaultSqlStoredQuery<>(new BasicStoredQuery<>(sqlInsert, new String[0], parameters, persistentEntity.getIntrospection().getBeanType(), Object.class, OperationType.INSERT), associatedEntity, queryBuilder);
    }

    private SqlQueryBuilder2 findQueryBuilder(Class repositoryType) {
        SqlQueryBuilder2 queryBuilder = queryBuilders.get(repositoryType);
        if (queryBuilder != null) {
            return queryBuilder;
        }
        String hardcodedDatasource = repositoriesWithHardcodedDataSource.get(repositoryType);
        if (hardcodedDatasource != null) {
            throw new IllegalStateException("Repository [" + repositoryType + "] requires datasource: [" + hardcodedDatasource + "] but this repository operations uses: [" + dataSourceName + "]");
        }
        throw new IllegalStateException("Cannot find a query builder for repository: [" + repositoryType + "]");
    }

    private Stream idProperties(PersistentProperty property) {
        List paths = new ArrayList<>();
        PersistentEntityUtils.traversePersistentProperties(property, (associations, persistentProperty) -> {
            paths.add(new PersistentPropertyPath(associations, property));
        });
        return paths.stream();
    }

    private Stream> idPropertiesWithValues(PersistentProperty property, Object value) {
        List> values = new ArrayList<>();
        PersistentEntityUtils.traversePersistentProperties(property, (associations, persistentProperty) -> {
            values.add(new AbstractMap.SimpleEntry<>(persistentProperty, new PersistentPropertyPath(associations, property).getPropertyValue(value)));
        });
        return values.stream();
    }

    protected final  SqlPreparedQuery getSqlPreparedQuery(PreparedQuery preparedQuery) {
        if (preparedQuery instanceof SqlPreparedQuery sqlPreparedQuery) {
            return sqlPreparedQuery;
        }
        throw new IllegalStateException("Expected for prepared query to be of type: SqlPreparedQuery got: " + preparedQuery.getClass().getName());
    }

    protected final  SqlStoredQuery getSqlStoredQuery(StoredQuery storedQuery) {
        if (storedQuery instanceof SqlStoredQuery sqlStoredQuery) {
            if (sqlStoredQuery.isExpandableQuery() && !(sqlStoredQuery instanceof SqlPreparedQuery)) {
                return new DefaultSqlPreparedQuery<>(sqlStoredQuery);
            }
            return sqlStoredQuery;
        }
        throw new IllegalStateException("Expected for prepared query to be of type: SqlStoredQuery got: " + storedQuery.getClass().getName());
    }

    /**
     * Does supports batch for update queries.
     *
     * @param persistentEntity The persistent entity
     * @param sqlStoredQuery   The sqlStoredQuery
     * @return true if supported
     */
    protected boolean isSupportsBatchInsert(PersistentEntity persistentEntity, SqlStoredQuery sqlStoredQuery) {
        // Oracle and MySql doesn't support a batch with returning generated ID: "DML Returning cannot be batched"
        if (sqlStoredQuery.getOperationType() == OperationType.INSERT_RETURNING) {
            return false;
        }
        return isSupportsBatchInsert(persistentEntity, sqlStoredQuery.getDialect());
    }

    /**
     * Does supports batch for update queries.
     *
     * @param persistentEntity The persistent entity
     * @param dialect          The dialect
     * @return true if supported
     */
    protected boolean isSupportsBatchInsert(PersistentEntity persistentEntity, Dialect dialect) {
        // Oracle and MySql doesn't support a batch with returning generated ID: "DML Returning cannot be batched"
        return switch (dialect) {
            case SQL_SERVER -> false;
            case MYSQL, ORACLE -> {
                if (persistentEntity.getIdentity() != null) {
                    // Oracle and MySql doesn't support a batch with returning generated ID: "DML Returning cannot be batched"
                    yield !persistentEntity.getIdentity().isGenerated();
                }
                yield false;
            }
            default -> true;
        };
    }

    /**
     * Does supports batch for update queries.
     *
     * @param persistentEntity The persistent entity
     * @param sqlStoredQuery   The sqlStoredQuery
     * @return true if supported
     */
    protected boolean isSupportsBatchUpdate(PersistentEntity persistentEntity, SqlStoredQuery sqlStoredQuery) {
        return sqlStoredQuery.getOperationType() != OperationType.UPDATE_RETURNING;
    }

    /**
     * Does supports batch for delete queries.
     *
     * @param persistentEntity The persistent entity
     * @param dialect          The dialect
     * @return true if supported
     */
    protected boolean isSupportsBatchDelete(PersistentEntity persistentEntity, Dialect dialect) {
        return true;
    }

    /**
     * Creates {@link SqlTypeMapper} for reading results from single column into an entity. For now, we support reading from JSON column,
     * however in support we might add XML support etc.
     *
     * @param sqlStoredQuery   the SQL prepared query
     * @param columnName       the column name where we are reading from
     * @param jsonDataType     the JSON representation type
     * @param resultSetType    resultSetType the result set type (different for R2DBC and JDBC)
     * @param persistentEntity the persistent entity
     * @param loadListener     the load listener if needed after entity loaded
     * @param               the entity type
     * @param               the result type
     * @return the {@link SqlTypeMapper} able to decode from column value into given type
     */
    protected final  SqlTypeMapper createQueryResultMapper(SqlStoredQuery sqlStoredQuery, String columnName, JsonDataType jsonDataType, Class resultSetType,
                                                                        RuntimePersistentEntity persistentEntity, BiFunction, Object, Object> loadListener) {
        QueryResultInfo queryResultInfo = sqlStoredQuery.getQueryResultInfo();
        if (queryResultInfo != null && queryResultInfo.getType() != io.micronaut.data.annotation.QueryResult.Type.JSON) {
            throw new IllegalStateException("Unexpected query result type: " + queryResultInfo.getType());
        }
        return createJsonQueryResultMapper(sqlStoredQuery, columnName, jsonDataType, resultSetType, persistentEntity, loadListener);
    }

    /**
     * Return an indicator telling whether prepared query result produces JSON result.
     *
     * @param preparedQuery   the prepared query
     * @param queryResultInfo the query result info, if not null will hold info about result type
     * @return true if result is JSON
     */
    protected final boolean isJsonResult(StoredQuery preparedQuery, QueryResultInfo queryResultInfo) {
        if (preparedQuery.isCount()) {
            return false;
        }
        return queryResultInfo != null && queryResultInfo.getType() == io.micronaut.data.annotation.QueryResult.Type.JSON;
    }

    /**
     * Inserting JSON entity representation (like Oracle Json View) can generate new id, and we support retrieval only numeric auto generated ids.
     *
     * @param storedQuery      the stored query
     * @param persistentEntity the persistent entity
     * @return true if entity being inserted is JSON entity representation with auto generated numeric id
     */
    protected final boolean isJsonEntityGeneratedId(StoredQuery storedQuery, PersistentEntity persistentEntity) {
        if (!storedQuery.isJsonEntity()) {
            return false;
        }
        PersistentProperty identity = persistentEntity.getIdentity();
        if (identity == null) {
            return false;
        }
        return identity.getDataType().isNumeric();
    }

    /**
     * Gets column name for JSON result. If {@link io.micronaut.data.annotation.QueryResult} annotation is present,
     * takes column value from there, otherwise defaults to 'DATA' column name.
     *
     * @param queryResultInfo the query result info from the {@link io.micronaut.data.annotation.QueryResult} annotation, null if annotation not present
     * @return the JSON column name
     */
    protected final String getJsonColumn(QueryResultInfo queryResultInfo) {
        if (queryResultInfo != null) {
            return queryResultInfo.getColumnName();
        }
        return io.micronaut.data.annotation.QueryResult.DEFAULT_COLUMN;
    }

    /**
     * Gets JSON data type for JSON result. If {@link io.micronaut.data.annotation.QueryResult} annotation is present,
     * takes data type value from there, otherwise defaults to {@link JsonDataType#DEFAULT}.
     *
     * @param queryResultInfo the query result info from the {@link io.micronaut.data.annotation.QueryResult} annotation, null if annotation not present
     * @return the JSON data type
     */
    protected final JsonDataType getJsonDataType(QueryResultInfo queryResultInfo) {
        if (queryResultInfo != null) {
            return queryResultInfo.getJsonDataType();
        }
        return JsonDataType.DEFAULT;
    }

    /**
     * Creates {@link JsonQueryResultMapper} for JSON deserialization.
     *
     * @param sqlStoredQuery   the SQL prepared query
     * @param columnName       the column name where query result is stored
     * @param jsonDataType     the json representation type
     * @param resultSetType    the result set type
     * @param persistentEntity the persistent entity
     * @param loadListener     the load listener if needed after entity loaded
     * @param               the entity type
     * @return the {@link JsonQueryResultMapper}
     */
    private  JsonQueryResultMapper createJsonQueryResultMapper(SqlStoredQuery sqlStoredQuery, String columnName, JsonDataType jsonDataType, Class resultSetType,
                                                                               RuntimePersistentEntity persistentEntity, BiFunction, Object, Object> loadListener) {
        return new JsonQueryResultMapper<>(columnName, jsonDataType, persistentEntity, columnNameResultSetReader,
            sqlJsonColumnMapperProvider.getJsonColumnReader(sqlStoredQuery, resultSetType), loadListener);
    }

    /**
     * @return The first result set index.
     * @since 4.2.0
     */
    protected abstract Integer getFirstResultSetIndex();

    protected abstract SqlTypeMapper createTupleMapper();

    /**
     * Creates a result mapper.
     *
     * @param preparedQuery The prepared query
     * @param rsType        The result set type
     * @param            The entity type
     * @param            The result type
     * @return The new mapper
     * @since 4.2.0
     */
    protected  SqlTypeMapper createMapper(SqlStoredQuery preparedQuery, Class rsType) {
        if (preparedQuery.getResultType().equals(Tuple.class)) {
            return (SqlTypeMapper) createTupleMapper();
        }
        BiFunction, Object, Object> loadListener;
        RuntimePersistentEntity persistentEntity = preparedQuery.getPersistentEntity();
        boolean isEntityResult = preparedQuery.getResultDataType() == DataType.ENTITY;
        if (isEntityResult) {
            loadListener = (loadedEntity, o) -> {
                if (loadedEntity.hasPostLoadEventListeners()) {
                    return triggerPostLoad(o, loadedEntity, preparedQuery.getAnnotationMetadata());
                } else {
                    return o;
                }
            };
        } else {
            loadListener = null;
        }
        QueryResultInfo queryResultInfo = preparedQuery.getQueryResultInfo();
        if (isJsonResult(preparedQuery, queryResultInfo)) {
            String column = getJsonColumn(queryResultInfo);
            JsonDataType jsonDataType = getJsonDataType(queryResultInfo);
            return createQueryResultMapper(preparedQuery, column, jsonDataType, rsType, persistentEntity, loadListener);
        }
        if (isEntityResult) {
            ResultReader resultReader =
                preparedQuery.isDtoProjection() ? createColumnNameResultSetReaderWithColumnExistenceAware() : columnNameResultSetReader;
            return new SqlResultEntityTypeMapper<>(
                getEntity(preparedQuery.getResultType()),
                resultReader,
                preparedQuery.getJoinPaths(),
                sqlJsonColumnMapperProvider.getJsonColumnReader(preparedQuery, rsType),
                loadListener,
                conversionService);
        }
        if (preparedQuery.isDtoProjection()) {
            RuntimePersistentEntity resultPersistentEntity = getEntity(preparedQuery.getResultType());
            Collection> beanProperties = resultPersistentEntity.getIntrospection().getBeanProperties();
            RuntimePersistentEntity dtoPersistentEntity = new RuntimePersistentEntity<>(
                resultPersistentEntity.getIntrospection(),
                beanProperties.stream().map(p -> {
                    if (p.hasAnnotation(MappedProperty.class)) {
                        return p;
                    }
                    RuntimePersistentProperty entityProperty = persistentEntity.getPropertyByName(p.getName());
                    if (entityProperty == null || !ReflectionUtils.getWrapperType(entityProperty.getType()).equals(ReflectionUtils.getWrapperType(p.getType()))) {
                        return p;
                    }
                    return new BeanPropertyWithAnnotationMetadata<>(
                        p,
                        new AnnotationMetadataHierarchy(p.getAnnotationMetadata(), entityProperty.getAnnotationMetadata())
                    );
                }).toList()
            );
            return new SqlResultEntityTypeMapper<>(
                dtoPersistentEntity,
                columnNameResultSetReader,
                preparedQuery.getJoinPaths(),
                sqlJsonColumnMapperProvider.getJsonColumnReader(preparedQuery, rsType),
                null,
                conversionService);
        }
        return new SqlTypeMapper<>() {
            @Override
            public boolean hasNext(RS resultSet) {
                return columnIndexResultSetReader.next(resultSet);
            }

            @Override
            public R map(RS rs, Class type) throws DataAccessException {
                Object v = columnIndexResultSetReader.readDynamic(rs, getFirstResultSetIndex(), preparedQuery.getResultDataType());
                if (v == null) {
                    return null;
                } else if (type.isInstance(v)) {
                    return (R) v;
                } else {
                    return columnIndexResultSetReader.convertRequired(v, type);
                }
            }

            @Override
            public Object read(RS object, String name) {
                throw new IllegalStateException("Not supported!");
            }
        };
    }

    /**
     * Used to cache queries for entities.
     */
    private class QueryKey {
        final Class repositoryType;
        final Class entityType;

        QueryKey(Class repositoryType, Class entityType) {
            this.repositoryType = repositoryType;
            this.entityType = entityType;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            QueryKey queryKey = (QueryKey) o;
            return repositoryType.equals(queryKey.repositoryType) &&
                entityType.equals(queryKey.entityType);
        }

        @Override
        public int hashCode() {
            return Objects.hash(repositoryType, entityType);
        }
    }


    /**
     * Functional interface used to supply a statement.
     *
     * @param  The prepared statement type
     */
    @FunctionalInterface
    protected interface StatementSupplier {
        PS create(String ps) throws Exception;
    }
}