From ae8c88e0cb5b2390cb270026122412082b2dfba0 Mon Sep 17 00:00:00 2001 From: mipo256 Date: Sat, 5 Jul 2025 22:18:49 +0300 Subject: [PATCH] GH-2007 Propagated the SqlType to the parameter source during Spel expression evaluation Signed-off-by: mipo256 --- .../query/StringBasedJdbcQuery.java | 36 +++++++- .../JdbcRepositoryIntegrationTests.java | 61 ++++++++++++ .../query/StringBasedJdbcQueryUnitTests.java | 92 ++++++++++++++++++- .../JdbcRepositoryIntegrationTests-db2.sql | 6 ++ .../JdbcRepositoryIntegrationTests-h2.sql | 5 + .../JdbcRepositoryIntegrationTests-hsql.sql | 5 + ...JdbcRepositoryIntegrationTests-mariadb.sql | 5 + .../JdbcRepositoryIntegrationTests-mssql.sql | 6 ++ .../JdbcRepositoryIntegrationTests-mysql.sql | 5 + .../JdbcRepositoryIntegrationTests-oracle.sql | 6 ++ ...dbcRepositoryIntegrationTests-postgres.sql | 6 ++ 11 files changed, 227 insertions(+), 6 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java index b9f92fc790..454ad7bbd1 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java @@ -31,6 +31,7 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactory; import org.springframework.data.expression.ValueEvaluationContext; +import org.springframework.data.expression.ValueExpression; import org.springframework.data.jdbc.core.convert.JdbcColumnTypes; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.mapping.JdbcValue; @@ -176,7 +177,7 @@ private String evaluateExpressions(Object[] objects, Parameters bindablePa .getEvaluationContext(objects); parsedQuery.getParameterMap().forEach((paramName, valueExpression) -> { - parameterMap.addValue(paramName, valueExpression.evaluate(evaluationContext)); + addEvaluatedParameterToParameterSource(parameterMap, paramName, valueExpression, evaluationContext); }); return parsedQuery.getQueryString(); @@ -185,6 +186,39 @@ private String evaluateExpressions(Object[] objects, Parameters bindablePa return this.query; } + private static void addEvaluatedParameterToParameterSource( + MapSqlParameterSource parameterMap, + String paramName, + ValueExpression valueExpression, + ValueEvaluationContext evaluationContext) { + + Object evaluatedValue = valueExpression.evaluate(evaluationContext); + Class valueType = valueExpression.getValueType(evaluationContext); + + SQLType sqlType; + + if (valueType == null) { + if (evaluatedValue != null) { + sqlType = getSqlType(evaluatedValue.getClass()); + } else { + sqlType = null; + } + } else { + sqlType = getSqlType(valueType); + } + + if (sqlType != null) { + parameterMap.addValue(paramName, evaluatedValue, sqlType.getVendorTypeNumber()); + } else { + parameterMap.addValue(paramName, evaluatedValue); + } + } + + private static SQLType getSqlType(Class valueType) { + Class resolvedPrimitiveType = JdbcColumnTypes.INSTANCE.resolvePrimitiveType(valueType); + return JdbcUtil.targetSqlTypeFor(resolvedPrimitiveType); + } + private JdbcQueryExecution createJdbcQueryExecution(RelationalParameterAccessor accessor, ResultProcessor processor) { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index 50fe9d03a9..49eac7e565 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -41,7 +41,10 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.PropertiesFactoryBean; import org.springframework.context.ApplicationListener; @@ -112,6 +115,7 @@ public class JdbcRepositoryIntegrationTests { @Autowired RootRepository rootRepository; @Autowired WithDelimitedColumnRepository withDelimitedColumnRepository; @Autowired EntityWithSequenceRepository entityWithSequenceRepository; + @Autowired ExpressionSqlTypePropagationRepository expressionSqlTypePropagationRepository; public static Stream findAllByExamplePageableSource() { @@ -346,6 +350,20 @@ public void update() { }); } + @ParameterizedTest + @NullSource + @EnumSource(value = EnumClass.class) + void shouldSaveWithCustomSpellExpressions(EnumClass value) { + expressionSqlTypePropagationRepository.saveWithSpel(new ExpressionSqlTypePropagation(1L, value)); + + var found = expressionSqlTypePropagationRepository.findById(1L); + + assertThat(found).isPresent().hasValueSatisfying(entity -> { + assertThat(entity.getIdentifier()).isEqualTo(1L); + assertThat(entity.getEnumClass()).isEqualTo(value); + }); + } + @Test // DATAJDBC-98 public void updateMany() { @@ -1573,6 +1591,18 @@ interface WithDelimitedColumnRepository extends CrudRepository {} + interface ExpressionSqlTypePropagationRepository extends CrudRepository { + + // language=sql + @Modifying + @Query(value = """ + INSERT INTO EXPRESSION_SQL_TYPE_PROPAGATION(identifier, enum_class) + VALUES(:#{#expressionSqlTypePropagation.identifier}, :#{#expressionSqlTypePropagation.enumClass}) + """) + void saveWithSpel(@Param("expressionSqlTypePropagation") ExpressionSqlTypePropagation expressionSqlTypePropagation); + } + + interface DummyProjection { String getName(); } @@ -1608,6 +1638,11 @@ EntityWithSequenceRepository entityWithSequenceRepository() { return factory.getRepository(EntityWithSequenceRepository.class); } + @Bean + ExpressionSqlTypePropagationRepository simpleEnumClassRepository() { + return factory.getRepository(ExpressionSqlTypePropagationRepository.class); + } + @Bean NamedQueries namedQueries() throws IOException { @@ -1893,6 +1928,32 @@ public Long getId() { } } + static class ExpressionSqlTypePropagation { + + @Id + Long identifier; + + EnumClass enumClass; + + public ExpressionSqlTypePropagation(Long identifier, EnumClass enumClass) { + this.identifier = identifier; + this.enumClass = enumClass; + } + + public EnumClass getEnumClass() { + return enumClass; + } + + public Long getIdentifier() { + return identifier; + } + } + + enum EnumClass { + ACTIVE, + DELETE + } + static class EntityWithSequence { @Id diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java index 7cbd004c71..d8e26ccbfa 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java @@ -15,25 +15,36 @@ */ package org.springframework.data.jdbc.repository.query; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.LIST; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import java.lang.reflect.Method; import java.sql.JDBCType; import java.sql.ResultSet; +import java.sql.Types; +import java.time.DayOfWeek; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.stream.Stream; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; - import org.springframework.core.convert.converter.Converter; import org.springframework.core.env.StandardEnvironment; import org.springframework.dao.DataAccessException; @@ -105,7 +116,7 @@ void emptyQueryThrowsException() { JdbcQueryMethod queryMethod = createMethod("noAnnotation"); - Assertions.assertThatExceptionOfType(IllegalStateException.class) // + assertThatExceptionOfType(IllegalStateException.class) // .isThrownBy(() -> createQuery(queryMethod).execute(new Object[] {})); } @@ -299,6 +310,37 @@ void convertsEnumCollectionParameterIntoStringCollectionParameter() { assertThat(sqlParameterSource.getValue("directions")).asList().containsExactlyInAnyOrder("LEFT", "RIGHT"); } + @Test // GH-1212 + void spelParametersSqlTypesArePropagatedCorrectly() { + + String type = "TYPE"; + int score = 12; + Instant creationDate = Instant.now(); + DayOfWeek dayOfWeek = DayOfWeek.SUNDAY; + ComplexEntity expressionRootObject = new ComplexEntity(type, score, creationDate, dayOfWeek); + + SqlParameterSource sqlParameterSource = forMethod("spelContainingQuery", ComplexEntity.class) + .withArguments(expressionRootObject).extractParameterSource(); + + var expectedSqlTypes = Map.of( + type, Types.VARCHAR, + score, Types.INTEGER, + creationDate, Types.TIMESTAMP, + dayOfWeek, Types.VARCHAR + ); + + assertThat(sqlParameterSource.getParameterNames()).hasSize(5); // 1 root + 4 expressions + assertThat(sqlParameterSource.getParameterNames()).satisfies(parameterNames -> { + for (var paramName : parameterNames) { + if (paramName.equalsIgnoreCase("complexEntity")) { + continue; // do not check root for sqlType + } + Object value = sqlParameterSource.getValue(paramName); + assertThat(sqlParameterSource.getSqlType(paramName)).isEqualTo(expectedSqlTypes.get(value)); + } + }); + } + @Test // GH-1212 void convertsEnumCollectionParameterUsingCustomConverterWhenRegisteredForType() { @@ -506,6 +548,15 @@ interface MyRepository extends Repository { @Query(value = "some sql statement") List findByEnumTypeIn(Set directions); + @Query(value = """ + SELECT * FROM my_table + WHERE t = :#{#complexEntity.type} + AND s = :#{#complexEntity.score} + AND cd = :#{#complexEntity.creationDate} + AND dow = :#{#complexEntity.dayOfWeek} + """) + List spelContainingQuery(ComplexEntity complexEntity); + @Query(value = "some sql statement") List findBySimpleValue(Integer value); @@ -652,6 +703,37 @@ public Object getRootObject() { } } + static class ComplexEntity { + + String type; + Integer score; + Instant creationDate; + DayOfWeek dayOfWeek; + + public ComplexEntity(String type, Integer score, Instant creationDate, DayOfWeek dayOfWeek) { + this.type = type; + this.score = score; + this.creationDate = creationDate; + this.dayOfWeek = dayOfWeek; + } + + public String getType() { + return type; + } + + public Integer getScore() { + return score; + } + + public Instant getCreationDate() { + return creationDate; + } + + public DayOfWeek getDayOfWeek() { + return dayOfWeek; + } + } + private class StubRowMapperFactory implements RowMapperFactory { private final String preparedReference; diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql index 38269a87db..f274e8bdd9 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql @@ -6,6 +6,7 @@ DROP TABLE WITH_DELIMITED_COLUMN; DROP TABLE ENTITY_WITH_SEQUENCE; DROP SEQUENCE ENTITY_SEQUENCE; DROP TABLE PROVIDED_ID_ENTITY; +DROP TABLE EXPRESSION_SQL_TYPE_PROPAGATION; CREATE TABLE dummy_entity ( @@ -63,3 +64,8 @@ CREATE TABLE PROVIDED_ID_ENTITY ID BIGINT NOT NULL PRIMARY KEY, NAME VARCHAR(30) ); + +CREATE TABLE EXPRESSION_SQL_TYPE_PROPAGATION( + IDENTIFIER BIGINT NOT NULL PRIMARY KEY, + ENUM_CLASS VARCHAR(30) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql index c24060be24..4704b4210d 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql @@ -54,3 +54,8 @@ CREATE TABLE PROVIDED_ID_ENTITY ID BIGINT PRIMARY KEY, NAME VARCHAR(30) ); + +CREATE TABLE EXPRESSION_SQL_TYPE_PROPAGATION( + IDENTIFIER BIGINT PRIMARY KEY, + ENUM_CLASS VARCHAR(30) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql index c24060be24..4704b4210d 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql @@ -54,3 +54,8 @@ CREATE TABLE PROVIDED_ID_ENTITY ID BIGINT PRIMARY KEY, NAME VARCHAR(30) ); + +CREATE TABLE EXPRESSION_SQL_TYPE_PROPAGATION( + IDENTIFIER BIGINT PRIMARY KEY, + ENUM_CLASS VARCHAR(30) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql index 6291f2b934..2479ce21ed 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql @@ -54,3 +54,8 @@ CREATE TABLE PROVIDED_ID_ENTITY ID BIGINT PRIMARY KEY, NAME VARCHAR(30) ); + +CREATE TABLE EXPRESSION_SQL_TYPE_PROPAGATION( + IDENTIFIER BIGINT PRIMARY KEY, + ENUM_CLASS VARCHAR(30) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql index 6c70d68a58..6de31ac54c 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql @@ -6,6 +6,7 @@ DROP TABLE IF EXISTS WITH_DELIMITED_COLUMN; DROP TABLE IF EXISTS ENTITY_WITH_SEQUENCE; DROP SEQUENCE IF EXISTS ENTITY_SEQUENCE; DROP TABLE IF EXISTS PROVIDED_ID_ENTITY; +DROP TABLE IF EXISTS EXPRESSION_SQL_TYPE_PROPAGATION; CREATE TABLE dummy_entity ( @@ -63,3 +64,8 @@ CREATE TABLE PROVIDED_ID_ENTITY ID BIGINT PRIMARY KEY, NAME VARCHAR(30) ); + +CREATE TABLE EXPRESSION_SQL_TYPE_PROPAGATION( + IDENTIFIER BIGINT PRIMARY KEY, + ENUM_CLASS VARCHAR(30) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql index 035e52d4b5..717afd961f 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql @@ -49,3 +49,8 @@ CREATE TABLE PROVIDED_ID_ENTITY ID BIGINT PRIMARY KEY, NAME VARCHAR(30) ); + +CREATE TABLE EXPRESSION_SQL_TYPE_PROPAGATION( + IDENTIFIER BIGINT PRIMARY KEY, + ENUM_CLASS VARCHAR(30) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql index 13091960de..a81c96d347 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql @@ -6,6 +6,7 @@ DROP TABLE WITH_DELIMITED_COLUMN CASCADE CONSTRAINTS PURGE; DROP TABLE ENTITY_WITH_SEQUENCE CASCADE CONSTRAINTS PURGE; DROP SEQUENCE ENTITY_SEQUENCE; DROP TABLE PROVIDED_ID_ENTITY CASCADE CONSTRAINTS PURGE; +DROP TABLE EXPRESSION_SQL_TYPE_PROPAGATION CASCADE CONSTRAINTS PURGE; CREATE TABLE DUMMY_ENTITY ( @@ -63,3 +64,8 @@ CREATE TABLE PROVIDED_ID_ENTITY ID NUMBER PRIMARY KEY, NAME VARCHAR2(30) ); + +CREATE TABLE EXPRESSION_SQL_TYPE_PROPAGATION( + IDENTIFIER BIGINT PRIMARY KEY, + ENUM_CLASS VARCHAR2(30) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql index 05aea26e12..fcf877a9b1 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql @@ -6,6 +6,7 @@ DROP TABLE WITH_DELIMITED_COLUMN; DROP TABLE ENTITY_WITH_SEQUENCE; DROP SEQUENCE ENTITY_SEQUENCE; DROP TABLE PROVIDED_ID_ENTITY; +DROP TABLE EXPRESSION_SQL_TYPE_PROPAGATION; CREATE TABLE dummy_entity ( @@ -63,3 +64,8 @@ CREATE TABLE PROVIDED_ID_ENTITY ID BIGINT PRIMARY KEY, NAME VARCHAR(30) ); + +CREATE TABLE EXPRESSION_SQL_TYPE_PROPAGATION( + IDENTIFIER BIGINT PRIMARY KEY, + ENUM_CLASS VARCHAR(30) +);