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

There is a newer version: 4.10.5
Show 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.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.TypeRole;
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.query.JoinPath;
import io.micronaut.data.model.query.QueryModel;
import io.micronaut.data.model.query.QueryParameter;
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.SqlQueryBuilder;
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.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 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.Set;
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)) {
                SqlQueryBuilder queryBuilder = new SqlQueryBuilder(beanDefinition.getAnnotationMetadata());
                queryBuilders.put(beanType, queryBuilder);
            } else {
                repositoriesWithHardcodedDataSource.put(beanType, targetDs);
            }
        }
    }

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

    @Override
    public  StoredQuery decorate(MethodInvocationContext context, StoredQuery storedQuery) {
        Class repositoryType = context.getTarget().getClass();
        SqlQueryBuilder 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 SqlQueryBuilder queryBuilder = findQueryBuilder(repositoryType);
            final QueryResult queryResult = queryBuilder.buildInsert(annotationMetadata, 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 SqlQueryBuilder 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 SqlQueryBuilder queryBuilder = findQueryBuilder(repositoryType);

            final String idName;
            final PersistentProperty identity = persistentEntity.getIdentity();
            if (identity != null) {
                idName = identity.getName();
            } else {
                idName = TypeRole.ID;
            }

            final QueryModel queryModel = QueryModel.from(persistentEntity)
                    .idEq(new QueryParameter(idName));
            List updateProperties = persistentEntity.getPersistentProperties()
                    .stream().filter(p ->
                            !(p instanceof Association association && association.isForeignKey()) &&
                                    p.getAnnotationMetadata().booleanValue(AutoPopulated.class, "updateable").orElse(true)
                    )
                    .map(PersistentProperty::getName)
                    .toList();
            final QueryResult queryResult = queryBuilder.buildUpdate(annotationMetadata, queryModel, updateProperties);
            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 SqlQueryBuilder 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 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();
                }
            });
        }

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

    private SqlQueryBuilder findQueryBuilder(Class repositoryType) {
        SqlQueryBuilder 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
     * @return the {@link SqlTypeMapper} able to decode from column value into given type
     * @param  the entity type
     * @param  the result 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
     * @return the {@link JsonQueryResultMapper}
     * @param  the entity type
     */
    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();

    /**
     * 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) {
        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 || preparedQuery.isDtoProjection()) {
            Class resultType = preparedQuery.getResultType();
            final Set joinFetchPaths = preparedQuery.getJoinFetchPaths();
            if (isEntityResult) {
                return new SqlResultEntityTypeMapper<>(
                    getEntity(resultType),
                    columnNameResultSetReader,
                    joinFetchPaths,
                    sqlJsonColumnMapperProvider.getJsonColumnReader(preparedQuery, rsType),
                    loadListener,
                    conversionService);
            } else {
                RuntimePersistentEntity resultPersistentEntity = getEntity(resultType);
                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,
                    joinFetchPaths,
                    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;
    }
}