From bddacebe878ec39fb9f0e92dddfd0faea26d66c5 Mon Sep 17 00:00:00 2001
From: Mark Paluch <mark.paluch@broadcom.com>
Date: Fri, 16 May 2025 11:31:35 +0200
Subject: [PATCH 01/28] Prepare next development iteration.

See #2042
---
 pom.xml                               | 2 +-
 spring-data-jdbc-distribution/pom.xml | 2 +-
 spring-data-jdbc/pom.xml              | 4 ++--
 spring-data-r2dbc/pom.xml             | 4 ++--
 spring-data-relational/pom.xml        | 4 ++--
 5 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/pom.xml b/pom.xml
index 26dfeef3b2..cb2e2ed5a6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
 
 	<groupId>org.springframework.data</groupId>
 	<artifactId>spring-data-relational-parent</artifactId>
-	<version>3.5.0</version>
+	<version>3.5.1-SNAPSHOT</version>
 	<packaging>pom</packaging>
 
 	<name>Spring Data Relational Parent</name>
diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml
index 6f50d55550..67f0fc9eff 100644
--- a/spring-data-jdbc-distribution/pom.xml
+++ b/spring-data-jdbc-distribution/pom.xml
@@ -14,7 +14,7 @@
 	<parent>
 		<groupId>org.springframework.data</groupId>
 		<artifactId>spring-data-relational-parent</artifactId>
-		<version>3.5.0</version>
+		<version>3.5.1-SNAPSHOT</version>
 		<relativePath>../pom.xml</relativePath>
 	</parent>
 
diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml
index 373ed5a9d0..f8185d06d0 100644
--- a/spring-data-jdbc/pom.xml
+++ b/spring-data-jdbc/pom.xml
@@ -6,7 +6,7 @@
 	<modelVersion>4.0.0</modelVersion>
 
 	<artifactId>spring-data-jdbc</artifactId>
-	<version>3.5.0</version>
+	<version>3.5.1-SNAPSHOT</version>
 
 	<name>Spring Data JDBC</name>
 	<description>Spring Data module for JDBC repositories.</description>
@@ -15,7 +15,7 @@
 	<parent>
 		<groupId>org.springframework.data</groupId>
 		<artifactId>spring-data-relational-parent</artifactId>
-		<version>3.5.0</version>
+		<version>3.5.1-SNAPSHOT</version>
 	</parent>
 
 	<properties>
diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml
index 7c1bafe14a..556a23247c 100644
--- a/spring-data-r2dbc/pom.xml
+++ b/spring-data-r2dbc/pom.xml
@@ -6,7 +6,7 @@
 	<modelVersion>4.0.0</modelVersion>
 
 	<artifactId>spring-data-r2dbc</artifactId>
-	<version>3.5.0</version>
+	<version>3.5.1-SNAPSHOT</version>
 
 	<name>Spring Data R2DBC</name>
 	<description>Spring Data module for R2DBC</description>
@@ -15,7 +15,7 @@
 	<parent>
 		<groupId>org.springframework.data</groupId>
 		<artifactId>spring-data-relational-parent</artifactId>
-		<version>3.5.0</version>
+		<version>3.5.1-SNAPSHOT</version>
 	</parent>
 
 	<properties>
diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml
index 2bb86054a6..41c9d7db2d 100644
--- a/spring-data-relational/pom.xml
+++ b/spring-data-relational/pom.xml
@@ -6,7 +6,7 @@
 	<modelVersion>4.0.0</modelVersion>
 
 	<artifactId>spring-data-relational</artifactId>
-	<version>3.5.0</version>
+	<version>3.5.1-SNAPSHOT</version>
 
 	<name>Spring Data Relational</name>
 	<description>Spring Data Relational support</description>
@@ -14,7 +14,7 @@
 	<parent>
 		<groupId>org.springframework.data</groupId>
 		<artifactId>spring-data-relational-parent</artifactId>
-		<version>3.5.0</version>
+		<version>3.5.1-SNAPSHOT</version>
 	</parent>
 
 	<properties>

From 4041729fb47d477469d176d6c38f46ccec510029 Mon Sep 17 00:00:00 2001
From: Mark Paluch <mark.paluch@broadcom.com>
Date: Fri, 16 May 2025 11:31:36 +0200
Subject: [PATCH 02/28] After release cleanups.

See #2042
---
 Jenkinsfile |  2 +-
 pom.xml     | 20 ++++++++++++++++----
 2 files changed, 17 insertions(+), 5 deletions(-)

diff --git a/Jenkinsfile b/Jenkinsfile
index 8919ba10f4..dde814060b 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -9,7 +9,7 @@ pipeline {
 
 	triggers {
 		pollSCM 'H/10 * * * *'
-		upstream(upstreamProjects: "spring-data-commons/main", threshold: hudson.model.Result.SUCCESS)
+		upstream(upstreamProjects: "spring-data-commons/3.5.x", threshold: hudson.model.Result.SUCCESS)
 	}
 
 	options {
diff --git a/pom.xml b/pom.xml
index cb2e2ed5a6..06a3b31137 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,12 +15,12 @@
 	<parent>
 		<groupId>org.springframework.data.build</groupId>
 		<artifactId>spring-data-parent</artifactId>
-		<version>3.5.0</version>
+		<version>3.5.1-SNAPSHOT</version>
 	</parent>
 
 	<properties>
 		<dist.id>spring-data-jdbc</dist.id>
-		<springdata.commons>3.5.0</springdata.commons>
+		<springdata.commons>3.5.1-SNAPSHOT</springdata.commons>
 		<liquibase.version>4.21.1</liquibase.version>
 		<sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis>
 
@@ -310,8 +310,20 @@
 	</build>
 
 	<repositories>
-		
-		
+		<repository>
+			<id>spring-snapshot</id>
+			<url>https://repo.spring.io/snapshot</url>
+			<snapshots>
+				<enabled>true</enabled>
+			</snapshots>
+			<releases>
+				<enabled>false</enabled>
+			</releases>
+		</repository>
+		<repository>
+			<id>spring-milestone</id>
+			<url>https://repo.spring.io/milestone</url>
+		</repository>
 	</repositories>
 
 </project>

From 651a4f5a1959c51857bedf32ce72d408d1d5a1a6 Mon Sep 17 00:00:00 2001
From: mipo256 <mikhailpolivakha@gmail.com>
Date: Sun, 1 Jun 2025 16:20:15 +0300
Subject: [PATCH 03/28] Applies proper event handling before saving in batch.

Signed-off-by: mipo256 <mikhailpolivakha@gmail.com>

Commit message edited by Jens Schauder.

Original pull request #2065
Closes #2064
---
 .../data/jdbc/core/JdbcAggregateTemplate.java | 59 +++++++++++++++----
 ...JdbcAggregateTemplateIntegrationTests.java | 58 +++++++++++++++++-
 ...cAggregateTemplateIntegrationTests-db2.sql | 10 +++-
 ...bcAggregateTemplateIntegrationTests-h2.sql |  8 ++-
 ...AggregateTemplateIntegrationTests-hsql.sql |  8 ++-
 ...regateTemplateIntegrationTests-mariadb.sql |  8 ++-
 ...ggregateTemplateIntegrationTests-mssql.sql | 10 +++-
 ...ggregateTemplateIntegrationTests-mysql.sql |  8 ++-
 ...gregateTemplateIntegrationTests-oracle.sql | 10 +++-
 ...egateTemplateIntegrationTests-postgres.sql | 10 +++-
 10 files changed, 168 insertions(+), 21 deletions(-)

diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java
index 928a18fcd6..4ff357d317 100644
--- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java
+++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java
@@ -28,6 +28,7 @@
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.data.domain.Page;
@@ -50,11 +51,22 @@
 import org.springframework.data.relational.core.mapping.RelationalMappingContext;
 import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
 import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
-import org.springframework.data.relational.core.mapping.event.*;
+import org.springframework.data.relational.core.mapping.event.AbstractRelationalEvent;
+import org.springframework.data.relational.core.mapping.event.AfterConvertCallback;
+import org.springframework.data.relational.core.mapping.event.AfterConvertEvent;
+import org.springframework.data.relational.core.mapping.event.AfterDeleteCallback;
+import org.springframework.data.relational.core.mapping.event.AfterDeleteEvent;
+import org.springframework.data.relational.core.mapping.event.AfterSaveCallback;
+import org.springframework.data.relational.core.mapping.event.AfterSaveEvent;
+import org.springframework.data.relational.core.mapping.event.BeforeConvertCallback;
+import org.springframework.data.relational.core.mapping.event.BeforeConvertEvent;
+import org.springframework.data.relational.core.mapping.event.BeforeDeleteCallback;
+import org.springframework.data.relational.core.mapping.event.BeforeDeleteEvent;
+import org.springframework.data.relational.core.mapping.event.BeforeSaveCallback;
+import org.springframework.data.relational.core.mapping.event.BeforeSaveEvent;
+import org.springframework.data.relational.core.mapping.event.Identifier;
 import org.springframework.data.relational.core.query.Query;
 import org.springframework.data.support.PageableExecutionUtils;
-import org.springframework.data.util.Streamable;
-import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 
@@ -175,7 +187,7 @@ public <T> T save(T instance) {
 
 	@Override
 	public <T> List<T> saveAll(Iterable<T> instances) {
-		return doInBatch(instances, (first) -> (second -> changeCreatorSelectorForSave(first).apply(second)));
+		return saveInBatch(instances, instance -> changeCreatorSelectorForSave(instance));
 	}
 
 	/**
@@ -196,7 +208,7 @@ public <T> T insert(T instance) {
 
 	@Override
 	public <T> List<T> insertAll(Iterable<T> instances) {
-		return doInBatch(instances, (__) -> (entity -> createInsertChange(prepareVersionForInsert(entity))));
+		return doInBatch(instances, entity -> createInsertChange(prepareVersionForInsert(entity)));
 	}
 
 	/**
@@ -217,10 +229,28 @@ public <T> T update(T instance) {
 
 	@Override
 	public <T> List<T> updateAll(Iterable<T> instances) {
-		return doInBatch(instances, (__) -> (entity -> createUpdateChange(prepareVersionForUpdate(entity))));
+		return doInBatch(instances, entity -> createUpdateChange(prepareVersionForUpdate(entity)));
 	}
 
-	private <T> List<T> doInBatch(Iterable<T> instances,Function<T, Function<T, RootAggregateChange<T>>> changeCreatorFunction) {
+	private <T> List<T> saveInBatch(Iterable<T> instances, Function<T, AggregateChangeCreator<T>> changes) {
+
+		Assert.notNull(instances, "Aggregate instances must not be null");
+
+		if (!instances.iterator().hasNext()) {
+			return Collections.emptyList();
+		}
+
+		List<EntityAndChangeCreator<T>> entityAndChangeCreators = new ArrayList<>();
+
+		for (T instance : instances) {
+			verifyIdProperty(instance);
+			entityAndChangeCreators.add(new EntityAndChangeCreator<>(instance, changes.apply(instance)));
+		}
+
+		return performSaveAll(entityAndChangeCreators);
+	}
+
+	private <T> List<T> doInBatch(Iterable<T> instances, AggregateChangeCreator<T> changeCreatorFunction) {
 
 		Assert.notNull(instances, "Aggregate instances must not be null");
 
@@ -231,7 +261,7 @@ private <T> List<T> doInBatch(Iterable<T> instances,Function<T, Function<T, Root
 		List<EntityAndChangeCreator<T>> entityAndChangeCreators = new ArrayList<>();
 		for (T instance : instances) {
 			verifyIdProperty(instance);
-			entityAndChangeCreators.add(new EntityAndChangeCreator<T>(instance, changeCreatorFunction.apply(instance)));
+			entityAndChangeCreators.add(new EntityAndChangeCreator<T>(instance, changeCreatorFunction));
 		}
 		return performSaveAll(entityAndChangeCreators);
 	}
@@ -484,7 +514,7 @@ private <T> RootAggregateChange<T> beforeExecute(EntityAndChangeCreator<T> insta
 
 		T aggregateRoot = triggerBeforeConvert(instance.entity);
 
-		RootAggregateChange<T> change = instance.changeCreator.apply(aggregateRoot);
+		RootAggregateChange<T> change = instance.changeCreator.createAggregateChange(aggregateRoot);
 
 		aggregateRoot = triggerBeforeSave(change.getRoot(), change);
 
@@ -542,7 +572,7 @@ private <T> List<T> performSaveAll(Iterable<EntityAndChangeCreator<T>> instances
 		return results;
 	}
 
-	private <T> Function<T, RootAggregateChange<T>> changeCreatorSelectorForSave(T instance) {
+	private <T> AggregateChangeCreator<T> changeCreatorSelectorForSave(T instance) {
 
 		return context.getRequiredPersistentEntity(instance.getClass()).isNew(instance)
 				? entity -> createInsertChange(prepareVersionForInsert(entity))
@@ -681,6 +711,13 @@ private <T> T triggerBeforeDelete(@Nullable T aggregateRoot, Object id, MutableA
 	private record EntityAndPreviousVersion<T> (T entity, @Nullable Number version) {
 	}
 
-	private record EntityAndChangeCreator<T> (T entity, Function<T, RootAggregateChange<T>> changeCreator) {
+	private record EntityAndChangeCreator<T> (T entity, AggregateChangeCreator<T> changeCreator) {
+	}
+
+	private interface AggregateChangeCreator<T> extends Function<T, RootAggregateChange<T>> {
+
+		default RootAggregateChange<T> createAggregateChange(T instance) {
+			return this.apply(instance);
+		}
 	}
 }
diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java
index 4f047f8406..ccf8c04026 100644
--- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java
+++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java
@@ -29,8 +29,10 @@
 import java.util.stream.IntStream;
 import java.util.stream.Stream;
 
+import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -52,6 +54,7 @@
 import org.springframework.data.jdbc.testing.TestClass;
 import org.springframework.data.jdbc.testing.TestConfiguration;
 import org.springframework.data.jdbc.testing.TestDatabaseFeatures;
+import org.springframework.data.mapping.callback.EntityCallbacks;
 import org.springframework.data.mapping.context.InvalidPersistentPropertyPath;
 import org.springframework.data.relational.core.conversion.DbActionExecutionException;
 import org.springframework.data.relational.core.mapping.Column;
@@ -60,6 +63,7 @@
 import org.springframework.data.relational.core.mapping.MappedCollection;
 import org.springframework.data.relational.core.mapping.RelationalMappingContext;
 import org.springframework.data.relational.core.mapping.Table;
+import org.springframework.data.relational.core.mapping.event.BeforeConvertCallback;
 import org.springframework.data.relational.core.query.Criteria;
 import org.springframework.data.relational.core.query.CriteriaDefinition;
 import org.springframework.data.relational.core.query.Query;
@@ -1373,6 +1377,22 @@ void mapWithEnumKey() {
 		assertThat(enumMapOwners).containsExactly(enumMapOwner);
 	}
 
+	@Test //GH-2064
+	void saveAllBeforeConvertCallback() {
+		var first = new BeforeConvertCallbackForSaveBatch("first");
+		var second = new BeforeConvertCallbackForSaveBatch("second");
+		var third = new BeforeConvertCallbackForSaveBatch("third");
+
+		template.saveAll(List.of(first, second, third));
+
+		var allEntriesInTable = template.findAll(BeforeConvertCallbackForSaveBatch.class);
+
+		Assertions.assertThat(allEntriesInTable)
+				.hasSize(3)
+				.extracting(BeforeConvertCallbackForSaveBatch::getName)
+				.containsOnly("first", "second", "third");
+	}
+
 	@Test // GH-1684
 	void oneToOneWithIdenticalIdColumnName() {
 
@@ -2184,6 +2204,32 @@ public Short getVersion() {
 		}
 	}
 
+	@Table("BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH")
+	static class BeforeConvertCallbackForSaveBatch {
+
+		@Id
+		private String id;
+
+		private String name;
+
+		public BeforeConvertCallbackForSaveBatch(String name) {
+			this.name = name;
+		}
+
+		public String getId() {
+			return id;
+		}
+
+		public BeforeConvertCallbackForSaveBatch setId(String id) {
+			this.id = id;
+			return this;
+		}
+
+		public String getName() {
+			return name;
+		}
+	}
+
 	@Table("VERSIONED_AGGREGATE")
 	static class AggregateWithPrimitiveShortVersion extends VersionedAggregate {
 
@@ -2271,9 +2317,17 @@ TestClass testClass() {
 		}
 
 		@Bean
-		JdbcAggregateOperations operations(ApplicationEventPublisher publisher, RelationalMappingContext context,
+		BeforeConvertCallback<BeforeConvertCallbackForSaveBatch> callback() {
+			return aggregate -> {
+				aggregate.setId(UUID.randomUUID().toString());
+				return aggregate;
+			};
+		}
+
+		@Bean
+		JdbcAggregateOperations operations(ApplicationContext applicationContext, RelationalMappingContext context,
 				DataAccessStrategy dataAccessStrategy, JdbcConverter converter) {
-			return new JdbcAggregateTemplate(publisher, context, converter, dataAccessStrategy);
+			return new JdbcAggregateTemplate(applicationContext, context, converter, dataAccessStrategy);
 		}
 	}
 
diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql
index e93990e31b..a22367cd1a 100644
--- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql
+++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql
@@ -59,6 +59,8 @@ DROP TABLE THIRD;
 DROP TABLE SEC;
 DROP TABLE FIRST;
 
+DROP TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH;
+
 CREATE TABLE LEGO_SET
 (
   "id1"  BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY,
@@ -467,4 +469,10 @@ CREATE TABLE THIRD
     SEC BIGINT      NOT NULL,
     NAME   VARCHAR(20) NOT NULL,
     FOREIGN KEY (SEC) REFERENCES SEC (ID)
-);
\ No newline at end of file
+);
+
+CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH
+(
+    ID VARCHAR(36) PRIMARY KEY NOT NULL,
+    NAME VARCHAR(20)
+);
diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql
index 24ef5bdeab..2d06a21416 100644
--- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql
+++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql
@@ -417,4 +417,10 @@ CREATE TABLE THIRD
     SEC BIGINT      NOT NULL,
     NAME   VARCHAR(20) NOT NULL,
     FOREIGN KEY (SEC) REFERENCES SEC (ID)
-);
\ No newline at end of file
+);
+
+CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH
+(
+    ID VARCHAR PRIMARY KEY,
+    NAME VARCHAR
+);
diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql
index 21e80a6c98..824959d5d7 100644
--- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql
+++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql
@@ -419,4 +419,10 @@ CREATE TABLE THIRD
     SEC BIGINT      NOT NULL,
     NAME   VARCHAR(20) NOT NULL,
     FOREIGN KEY (SEC) REFERENCES SEC (ID)
-);
\ No newline at end of file
+);
+
+CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH
+(
+    ID VARCHAR PRIMARY KEY,
+    NAME VARCHAR
+);
diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql
index 14636eff40..43c43e8697 100644
--- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql
+++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql
@@ -391,4 +391,10 @@ CREATE TABLE THIRD
     SEC BIGINT      NOT NULL,
     NAME   VARCHAR(20) NOT NULL,
     FOREIGN KEY (SEC) REFERENCES SEC (ID)
-);
\ No newline at end of file
+);
+
+CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH
+(
+    ID VARCHAR(36) PRIMARY KEY,
+    NAME VARCHAR(20)
+);
diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql
index d922614f26..e72cd53dc3 100644
--- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql
+++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql
@@ -441,4 +441,12 @@ CREATE TABLE THIRD
     SEC BIGINT      NOT NULL,
     NAME   VARCHAR(20) NOT NULL,
     FOREIGN KEY (SEC) REFERENCES SEC (ID)
-);
\ No newline at end of file
+);
+
+DROP TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH;
+
+CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH
+(
+    ID VARCHAR PRIMARY KEY,
+    NAME VARCHAR
+);
diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql
index 3672630b26..87cb8734a3 100644
--- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql
+++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql
@@ -397,4 +397,10 @@ CREATE TABLE THIRD
     SEC BIGINT      NOT NULL,
     NAME   VARCHAR(20) NOT NULL,
     FOREIGN KEY (SEC) REFERENCES SEC (ID)
-);
\ No newline at end of file
+);
+
+CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH
+(
+    ID VARCHAR(36) PRIMARY KEY,
+    NAME VARCHAR(20)
+);
diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql
index 706e5e46d9..636e68112a 100644
--- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql
+++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql
@@ -49,6 +49,8 @@ DROP TABLE THIRD CASCADE CONSTRAINTS PURGE;
 DROP TABLE SEC CASCADE CONSTRAINTS PURGE;
 DROP TABLE FIRST CASCADE CONSTRAINTS PURGE;
 
+DROP TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH CASCADE CONSTRAINTS PURGE;
+
 CREATE TABLE LEGO_SET
 (
     "id1" NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY,
@@ -447,4 +449,10 @@ CREATE TABLE THIRD
     SEC NUMBER      NOT NULL,
     NAME   VARCHAR(20) NOT NULL,
     FOREIGN KEY (SEC) REFERENCES SEC (ID)
-);
\ No newline at end of file
+);
+
+CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH
+(
+    ID VARCHAR PRIMARY KEY,
+    NAME VARCHAR
+);
diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql
index 36f20896b7..5c4a8c0225 100644
--- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql
+++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql
@@ -52,6 +52,8 @@ DROP TABLE THIRD;
 DROP TABLE SEC;
 DROP TABLE FIRST;
 
+DROP TABLE "BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH";
+
 CREATE TABLE LEGO_SET
 (
     "id1" SERIAL PRIMARY KEY,
@@ -470,4 +472,10 @@ CREATE TABLE THIRD
     SEC BIGINT      NOT NULL,
     NAME   VARCHAR(20) NOT NULL,
     FOREIGN KEY (SEC) REFERENCES SEC (ID)
-);
\ No newline at end of file
+);
+
+CREATE TABLE "BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH"
+(
+    ID VARCHAR PRIMARY KEY,
+    NAME VARCHAR
+);

From c00158afd4cbd29f79391779ca6aefcf194e18ec Mon Sep 17 00:00:00 2001
From: Jens Schauder <jens.schauder@broadcom.com>
Date: Wed, 11 Jun 2025 11:10:29 +0200
Subject: [PATCH 04/28] Fixes integration tests.

Adding sizes to VARCHAR fields.

Original pull request #2065
See #2064
---
 .../JdbcAggregateTemplateIntegrationTests-mssql.sql           | 4 ++--
 .../JdbcAggregateTemplateIntegrationTests-oracle.sql          | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql
index e72cd53dc3..4d94a9156c 100644
--- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql
+++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql
@@ -447,6 +447,6 @@ DROP TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH;
 
 CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH
 (
-    ID VARCHAR PRIMARY KEY,
-    NAME VARCHAR
+    ID VARCHAR(50) PRIMARY KEY,
+    NAME VARCHAR(50)
 );
diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql
index 636e68112a..95134d8c13 100644
--- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql
+++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql
@@ -453,6 +453,6 @@ CREATE TABLE THIRD
 
 CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH
 (
-    ID VARCHAR PRIMARY KEY,
-    NAME VARCHAR
+    ID VARCHAR(50) PRIMARY KEY,
+    NAME VARCHAR(50)
 );

From e7a7bece1cfbc5effcf05a9faaf894b5755e2459 Mon Sep 17 00:00:00 2001
From: Jens Schauder <jens.schauder@broadcom.com>
Date: Wed, 11 Jun 2025 11:20:25 +0200
Subject: [PATCH 05/28] Switch to VARCHAR2 for Oracle tests.

VARCHAR should not be used with Oracle.

Original pull request #2065
See #2064
---
 ...gregateTemplateIntegrationTests-oracle.sql | 110 +++++++++---------
 .../JdbcRepositoryIntegrationTests-oracle.sql |   4 +-
 ...WithCollectionsIntegrationTests-oracle.sql |   2 +-
 ...sitoryWithListsIntegrationTests-oracle.sql |   4 +-
 ...ngConfigurationIntegrationTests-oracle.sql |   2 +-
 5 files changed, 61 insertions(+), 61 deletions(-)

diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql
index 95134d8c13..015c266d05 100644
--- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql
+++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql
@@ -54,14 +54,14 @@ DROP TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH CASCADE CONSTRAINTS PURGE;
 CREATE TABLE LEGO_SET
 (
     "id1" NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY,
-    NAME VARCHAR(30)
+    NAME VARCHAR2(30)
 );
 CREATE TABLE MANUAL
 (
     "id2" NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY,
     LEGO_SET    NUMBER,
     "alternative" NUMBER,
-    CONTENT     VARCHAR(2000)
+    CONTENT     VARCHAR2(2000)
 );
 
 ALTER TABLE MANUAL
@@ -71,27 +71,27 @@ ALTER TABLE MANUAL
 CREATE TABLE ONE_TO_ONE_PARENT
 (
     "id3" NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY,
-    content VARCHAR(30)
+    content VARCHAR2(30)
 );
 CREATE TABLE Child_No_Id
 (
     ONE_TO_ONE_PARENT INTEGER PRIMARY KEY,
-    CONTENT           VARCHAR(30)
+    CONTENT           VARCHAR2(30)
 );
 
 CREATE TABLE LIST_PARENT
 (
     "id4"   NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY,
-    NAME VARCHAR(100)
+    NAME VARCHAR2(100)
 );
 CREATE TABLE SIMPLE_LIST_PARENT
 (
     ID   NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY,
-    NAME VARCHAR(100)
+    NAME VARCHAR2(100)
 );
 CREATE TABLE element_no_id
 (
-    CONTENT         VARCHAR(100),
+    CONTENT         VARCHAR2(100),
     SIMPLE_LIST_PARENT_key NUMBER,
     SIMPLE_LIST_PARENT     NUMBER,
     LIST_PARENT_key NUMBER,
@@ -107,14 +107,14 @@ CREATE TABLE BYTE_ARRAY_OWNER
 CREATE TABLE CHAIN4
 (
     FOUR        NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY,
-    FOUR_VALUE VARCHAR(20)
+    FOUR_VALUE VARCHAR2(20)
 );
 
 
 CREATE TABLE CHAIN3
 (
     THREE       NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY,
-    THREE_VALUE VARCHAR(20),
+    THREE_VALUE VARCHAR2(20),
     CHAIN4      NUMBER,
     FOREIGN KEY (CHAIN4) REFERENCES CHAIN4 (FOUR)
 );
@@ -122,7 +122,7 @@ CREATE TABLE CHAIN3
 CREATE TABLE CHAIN2
 (
     TWO       NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY,
-    TWO_VALUE VARCHAR(20),
+    TWO_VALUE VARCHAR2(20),
     CHAIN3    NUMBER,
     FOREIGN KEY (CHAIN3) REFERENCES CHAIN3 (THREE)
 );
@@ -130,7 +130,7 @@ CREATE TABLE CHAIN2
 CREATE TABLE CHAIN1
 (
     ONE       NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY,
-    ONE_VALUE VARCHAR(20),
+    ONE_VALUE VARCHAR2(20),
     CHAIN2    NUMBER,
     FOREIGN KEY (CHAIN2) REFERENCES CHAIN2 (TWO)
 );
@@ -138,7 +138,7 @@ CREATE TABLE CHAIN1
 CREATE TABLE CHAIN0
 (
     ZERO       NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY,
-    ZERO_VALUE VARCHAR(20),
+    ZERO_VALUE VARCHAR2(20),
     CHAIN1     NUMBER,
     FOREIGN KEY (CHAIN1) REFERENCES CHAIN1 (ONE)
 );
@@ -146,33 +146,33 @@ CREATE TABLE CHAIN0
 CREATE TABLE NO_ID_CHAIN4
 (
     FOUR       NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY,
-    FOUR_VALUE VARCHAR(20)
+    FOUR_VALUE VARCHAR2(20)
 );
 
 CREATE TABLE NO_ID_CHAIN3
 (
-    THREE_VALUE  VARCHAR(20),
+    THREE_VALUE  VARCHAR2(20),
     NO_ID_CHAIN4 NUMBER,
     FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR)
 );
 
 CREATE TABLE NO_ID_CHAIN2
 (
-    TWO_VALUE    VARCHAR(20),
+    TWO_VALUE    VARCHAR2(20),
     NO_ID_CHAIN4 NUMBER,
     FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR)
 );
 
 CREATE TABLE NO_ID_CHAIN1
 (
-    ONE_VALUE    VARCHAR(20),
+    ONE_VALUE    VARCHAR2(20),
     NO_ID_CHAIN4 NUMBER,
     FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR)
 );
 
 CREATE TABLE NO_ID_CHAIN0
 (
-    ZERO_VALUE   VARCHAR(20),
+    ZERO_VALUE   VARCHAR2(20),
     NO_ID_CHAIN4 NUMBER,
     FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR)
 );
@@ -180,12 +180,12 @@ CREATE TABLE NO_ID_CHAIN0
 CREATE TABLE NO_ID_LIST_CHAIN4
 (
     FOUR       NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY,
-    FOUR_VALUE VARCHAR(20)
+    FOUR_VALUE VARCHAR2(20)
 );
 
 CREATE TABLE NO_ID_LIST_CHAIN3
 (
-    THREE_VALUE           VARCHAR(20),
+    THREE_VALUE           VARCHAR2(20),
     NO_ID_LIST_CHAIN4     NUMBER,
     NO_ID_LIST_CHAIN4_KEY NUMBER,
     PRIMARY KEY (NO_ID_LIST_CHAIN4,
@@ -195,7 +195,7 @@ CREATE TABLE NO_ID_LIST_CHAIN3
 
 CREATE TABLE NO_ID_LIST_CHAIN2
 (
-    TWO_VALUE             VARCHAR(20),
+    TWO_VALUE             VARCHAR2(20),
     NO_ID_LIST_CHAIN4     NUMBER,
     NO_ID_LIST_CHAIN4_KEY NUMBER,
     NO_ID_LIST_CHAIN3_KEY NUMBER,
@@ -213,7 +213,7 @@ CREATE TABLE NO_ID_LIST_CHAIN2
 
 CREATE TABLE NO_ID_LIST_CHAIN1
 (
-    ONE_VALUE             VARCHAR(20),
+    ONE_VALUE             VARCHAR2(20),
     NO_ID_LIST_CHAIN4     NUMBER,
     NO_ID_LIST_CHAIN4_KEY NUMBER,
     NO_ID_LIST_CHAIN3_KEY NUMBER,
@@ -235,7 +235,7 @@ CREATE TABLE NO_ID_LIST_CHAIN1
 
 CREATE TABLE NO_ID_LIST_CHAIN0
 (
-    ZERO_VALUE            VARCHAR(20),
+    ZERO_VALUE            VARCHAR2(20),
     NO_ID_LIST_CHAIN4     NUMBER,
     NO_ID_LIST_CHAIN4_KEY NUMBER,
     NO_ID_LIST_CHAIN3_KEY NUMBER,
@@ -264,14 +264,14 @@ CREATE TABLE NO_ID_LIST_CHAIN0
 CREATE TABLE NO_ID_MAP_CHAIN4
 (
     FOUR       NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY,
-    FOUR_VALUE VARCHAR(20)
+    FOUR_VALUE VARCHAR2(20)
 );
 
 CREATE TABLE NO_ID_MAP_CHAIN3
 (
-    THREE_VALUE          VARCHAR(20),
+    THREE_VALUE          VARCHAR2(20),
     NO_ID_MAP_CHAIN4     NUMBER,
-    NO_ID_MAP_CHAIN4_KEY VARCHAR(20),
+    NO_ID_MAP_CHAIN4_KEY VARCHAR2(20),
     PRIMARY KEY (NO_ID_MAP_CHAIN4,
                  NO_ID_MAP_CHAIN4_KEY),
     FOREIGN KEY (NO_ID_MAP_CHAIN4) REFERENCES NO_ID_MAP_CHAIN4 (FOUR)
@@ -279,10 +279,10 @@ CREATE TABLE NO_ID_MAP_CHAIN3
 
 CREATE TABLE NO_ID_MAP_CHAIN2
 (
-    TWO_VALUE            VARCHAR(20),
+    TWO_VALUE            VARCHAR2(20),
     NO_ID_MAP_CHAIN4     NUMBER,
-    NO_ID_MAP_CHAIN4_KEY VARCHAR(20),
-    NO_ID_MAP_CHAIN3_KEY VARCHAR(20),
+    NO_ID_MAP_CHAIN4_KEY VARCHAR2(20),
+    NO_ID_MAP_CHAIN3_KEY VARCHAR2(20),
     PRIMARY KEY (NO_ID_MAP_CHAIN4,
                  NO_ID_MAP_CHAIN4_KEY,
                  NO_ID_MAP_CHAIN3_KEY),
@@ -297,11 +297,11 @@ CREATE TABLE NO_ID_MAP_CHAIN2
 
 CREATE TABLE NO_ID_MAP_CHAIN1
 (
-    ONE_VALUE            VARCHAR(20),
+    ONE_VALUE            VARCHAR2(20),
     NO_ID_MAP_CHAIN4     NUMBER,
-    NO_ID_MAP_CHAIN4_KEY VARCHAR(20),
-    NO_ID_MAP_CHAIN3_KEY VARCHAR(20),
-    NO_ID_MAP_CHAIN2_KEY VARCHAR(20),
+    NO_ID_MAP_CHAIN4_KEY VARCHAR2(20),
+    NO_ID_MAP_CHAIN3_KEY VARCHAR2(20),
+    NO_ID_MAP_CHAIN2_KEY VARCHAR2(20),
     PRIMARY KEY (NO_ID_MAP_CHAIN4,
                  NO_ID_MAP_CHAIN4_KEY,
                  NO_ID_MAP_CHAIN3_KEY,
@@ -319,12 +319,12 @@ CREATE TABLE NO_ID_MAP_CHAIN1
 
 CREATE TABLE NO_ID_MAP_CHAIN0
 (
-    ZERO_VALUE           VARCHAR(20),
+    ZERO_VALUE           VARCHAR2(20),
     NO_ID_MAP_CHAIN4     NUMBER,
-    NO_ID_MAP_CHAIN4_KEY VARCHAR(20),
-    NO_ID_MAP_CHAIN3_KEY VARCHAR(20),
-    NO_ID_MAP_CHAIN2_KEY VARCHAR(20),
-    NO_ID_MAP_CHAIN1_KEY VARCHAR(20),
+    NO_ID_MAP_CHAIN4_KEY VARCHAR2(20),
+    NO_ID_MAP_CHAIN3_KEY VARCHAR2(20),
+    NO_ID_MAP_CHAIN2_KEY VARCHAR2(20),
+    NO_ID_MAP_CHAIN1_KEY VARCHAR2(20),
     PRIMARY KEY (NO_ID_MAP_CHAIN4,
                  NO_ID_MAP_CHAIN4_KEY,
                  NO_ID_MAP_CHAIN3_KEY,
@@ -352,8 +352,8 @@ CREATE TABLE VERSIONED_AGGREGATE
 CREATE TABLE WITH_READ_ONLY
 (
     ID        NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY,
-    NAME      VARCHAR(200),
-    READ_ONLY VARCHAR(200) DEFAULT 'from-db'
+    NAME      VARCHAR2(200),
+    READ_ONLY VARCHAR2(200) DEFAULT 'from-db'
 );
 
 
@@ -372,35 +372,35 @@ CREATE TABLE WITH_ID_ONLY
 CREATE TABLE WITH_INSERT_ONLY
 (
   ID NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY,
-  INSERT_ONLY VARCHAR(100)
+  INSERT_ONLY VARCHAR2(100)
 );
 
 CREATE TABLE MULTIPLE_COLLECTIONS
 (
     ID NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY,
-    NAME VARCHAR(100)
+    NAME VARCHAR2(100)
 );
 
 CREATE TABLE SET_ELEMENT
 (
     MULTIPLE_COLLECTIONS NUMBER,
-    NAME VARCHAR(100)
+    NAME VARCHAR2(100)
 );
 
 CREATE TABLE LIST_ELEMENT
 (
     MULTIPLE_COLLECTIONS NUMBER,
     MULTIPLE_COLLECTIONS_KEY INT,
-    NAME VARCHAR(100)
+    NAME VARCHAR2(100)
 );
 
 CREATE TABLE MAP_ELEMENT
 (
     MULTIPLE_COLLECTIONS NUMBER,
-    MULTIPLE_COLLECTIONS_KEY VARCHAR(10),
+    MULTIPLE_COLLECTIONS_KEY VARCHAR2(10),
     ENUM_MAP_OWNER NUMBER,
-    ENUM_MAP_OWNER_KEY VARCHAR(10),
-    NAME VARCHAR(100)
+    ENUM_MAP_OWNER_KEY VARCHAR2(10),
+    NAME VARCHAR2(100)
 );
 
 CREATE TABLE AUTHOR
@@ -411,48 +411,48 @@ CREATE TABLE AUTHOR
 CREATE TABLE BOOK
 (
     AUTHOR NUMBER,
-    NAME VARCHAR(100)
+    NAME VARCHAR2(100)
 );
 
 CREATE TABLE ENUM_MAP_OWNER
 (
     ID NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
-    NAME VARCHAR(100)
+    NAME VARCHAR2(100)
 );
 
 CREATE TABLE WITH_ONE_TO_ONE
 (
-    ID VARCHAR(100)
+    ID VARCHAR2(100)
 );
 
 CREATE TABLE REFERENCED
 (
-    "renamed" VARCHAR(100),
+    "renamed" VARCHAR2(100),
     ID NUMBER
 );
 CREATE TABLE FIRST
 (
     ID   NUMBER      NOT NULL PRIMARY KEY,
-    NAME VARCHAR(20) NOT NULL
+    NAME VARCHAR2(20) NOT NULL
 );
 
 CREATE TABLE SEC
 (
     ID    NUMBER      NOT NULL PRIMARY KEY,
     FIRST NUMBER      NOT NULL,
-    NAME  VARCHAR(20) NOT NULL,
+    NAME  VARCHAR2(20) NOT NULL,
     FOREIGN KEY (FIRST) REFERENCES FIRST (ID)
 );
 
 CREATE TABLE THIRD
 (
     SEC NUMBER      NOT NULL,
-    NAME   VARCHAR(20) NOT NULL,
+    NAME   VARCHAR2(20) NOT NULL,
     FOREIGN KEY (SEC) REFERENCES SEC (ID)
 );
 
 CREATE TABLE BEFORE_CONVERT_CALLBACK_FOR_SAVE_BATCH
 (
-    ID VARCHAR(50) PRIMARY KEY,
-    NAME VARCHAR(50)
+    ID VARCHAR2(50) PRIMARY KEY,
+    NAME VARCHAR2(50)
 );
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 428ff48f3f..9bdcc9cbfa 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
@@ -45,8 +45,8 @@ CREATE TABLE LEAF
 CREATE TABLE WITH_DELIMITED_COLUMN
 (
     ID                      NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY,
-    "ORG.XTUNIT.IDENTIFIER" VARCHAR(100),
-    STYPE                   VARCHAR(100)
+    "ORG.XTUNIT.IDENTIFIER" VARCHAR2(100),
+    STYPE                   VARCHAR2(100)
 );
 
 CREATE TABLE ENTITY_WITH_SEQUENCE
diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsIntegrationTests-oracle.sql
index 52dbc96a4d..94dde79962 100644
--- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsIntegrationTests-oracle.sql
+++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithCollectionsIntegrationTests-oracle.sql
@@ -8,6 +8,6 @@ CREATE TABLE DUMMY_ENTITY (
 
 CREATE TABLE ELEMENT (
     ID NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY,
-    CONTENT VARCHAR(100),
+    CONTENT VARCHAR2(100),
     DUMMY_ENTITY NUMBER
 );
diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-oracle.sql
index ba93df3173..6a71b9f212 100644
--- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-oracle.sql
+++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryWithListsIntegrationTests-oracle.sql
@@ -14,7 +14,7 @@ CREATE TABLE DUMMY_ENTITY
 CREATE TABLE ELEMENT
 (
     ID               NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY,
-    CONTENT          VARCHAR(100),
+    CONTENT          VARCHAR2(100),
     DUMMY_ENTITY_KEY NUMBER,
     DUMMY_ENTITY     NUMBER
 );
@@ -31,7 +31,7 @@ CREATE TABLE intermediate
 );
 CREATE TABLE leaf
 (
-    name             VARCHAR(100),
+    name             VARCHAR2(100),
     intermediate     NUMBER NOT NULL,
     intermediate_key NUMBER NOT NULL
 );
diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests-oracle.sql
index 18c251e189..a6e74e044c 100644
--- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests-oracle.sql
+++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests-oracle.sql
@@ -1,2 +1,2 @@
 DROP TABLE CAR;
-CREATE TABLE CAR ( id NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY, model VARCHAR(100));
+CREATE TABLE CAR ( id NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY, model VARCHAR2(100));

From 1511e153139187db85b4ce6e0d8396ffc0706d92 Mon Sep 17 00:00:00 2001
From: Jens Schauder <jens.schauder@broadcom.com>
Date: Wed, 11 Jun 2025 11:42:57 +0200
Subject: [PATCH 06/28] Polishing.

Formatting.
Simplification of code.

Original pull request #2065
See #2064
---
 .../data/jdbc/core/JdbcAggregateTemplate.java | 29 +++++--------------
 ...JdbcAggregateTemplateIntegrationTests.java | 26 ++++++++---------
 2 files changed, 19 insertions(+), 36 deletions(-)

diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java
index 4ff357d317..2db2a3f18e 100644
--- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java
+++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateTemplate.java
@@ -28,7 +28,6 @@
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 
-import org.jspecify.annotations.Nullable;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.data.domain.Page;
@@ -51,22 +50,10 @@
 import org.springframework.data.relational.core.mapping.RelationalMappingContext;
 import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
 import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
-import org.springframework.data.relational.core.mapping.event.AbstractRelationalEvent;
-import org.springframework.data.relational.core.mapping.event.AfterConvertCallback;
-import org.springframework.data.relational.core.mapping.event.AfterConvertEvent;
-import org.springframework.data.relational.core.mapping.event.AfterDeleteCallback;
-import org.springframework.data.relational.core.mapping.event.AfterDeleteEvent;
-import org.springframework.data.relational.core.mapping.event.AfterSaveCallback;
-import org.springframework.data.relational.core.mapping.event.AfterSaveEvent;
-import org.springframework.data.relational.core.mapping.event.BeforeConvertCallback;
-import org.springframework.data.relational.core.mapping.event.BeforeConvertEvent;
-import org.springframework.data.relational.core.mapping.event.BeforeDeleteCallback;
-import org.springframework.data.relational.core.mapping.event.BeforeDeleteEvent;
-import org.springframework.data.relational.core.mapping.event.BeforeSaveCallback;
-import org.springframework.data.relational.core.mapping.event.BeforeSaveEvent;
-import org.springframework.data.relational.core.mapping.event.Identifier;
+import org.springframework.data.relational.core.mapping.event.*;
 import org.springframework.data.relational.core.query.Query;
 import org.springframework.data.support.PageableExecutionUtils;
+import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 
@@ -82,6 +69,7 @@
  * @author Chirag Tailor
  * @author Diego Krupitza
  * @author Sergey Korotaev
+ * @author Mikhail Polivakha
  */
 public class JdbcAggregateTemplate implements JdbcAggregateOperations {
 
@@ -708,16 +696,13 @@ private <T> T triggerBeforeDelete(@Nullable T aggregateRoot, Object id, MutableA
 		return null;
 	}
 
-	private record EntityAndPreviousVersion<T> (T entity, @Nullable Number version) {
+	private record EntityAndPreviousVersion<T>(T entity, @Nullable Number version) {
 	}
 
-	private record EntityAndChangeCreator<T> (T entity, AggregateChangeCreator<T> changeCreator) {
+	private record EntityAndChangeCreator<T>(T entity, AggregateChangeCreator<T> changeCreator) {
 	}
 
-	private interface AggregateChangeCreator<T> extends Function<T, RootAggregateChange<T>> {
-
-		default RootAggregateChange<T> createAggregateChange(T instance) {
-			return this.apply(instance);
-		}
+	private interface AggregateChangeCreator<T> {
+		RootAggregateChange<T> createAggregateChange(T instance);
 	}
 }
diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java
index ccf8c04026..65e50ada3c 100644
--- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java
+++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java
@@ -29,11 +29,9 @@
 import java.util.stream.IntStream;
 import java.util.stream.Stream;
 
-import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.ApplicationContext;
-import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
@@ -54,7 +52,6 @@
 import org.springframework.data.jdbc.testing.TestClass;
 import org.springframework.data.jdbc.testing.TestConfiguration;
 import org.springframework.data.jdbc.testing.TestDatabaseFeatures;
-import org.springframework.data.mapping.callback.EntityCallbacks;
 import org.springframework.data.mapping.context.InvalidPersistentPropertyPath;
 import org.springframework.data.relational.core.conversion.DbActionExecutionException;
 import org.springframework.data.relational.core.mapping.Column;
@@ -1377,20 +1374,20 @@ void mapWithEnumKey() {
 		assertThat(enumMapOwners).containsExactly(enumMapOwner);
 	}
 
-	@Test //GH-2064
+	@Test // GH-2064
 	void saveAllBeforeConvertCallback() {
-		var first = new BeforeConvertCallbackForSaveBatch("first");
-		var second = new BeforeConvertCallbackForSaveBatch("second");
-		var third = new BeforeConvertCallbackForSaveBatch("third");
+
+		BeforeConvertCallbackForSaveBatch first = new BeforeConvertCallbackForSaveBatch("first");
+		BeforeConvertCallbackForSaveBatch second = new BeforeConvertCallbackForSaveBatch("second");
+		BeforeConvertCallbackForSaveBatch third = new BeforeConvertCallbackForSaveBatch("third");
 
 		template.saveAll(List.of(first, second, third));
 
-		var allEntriesInTable = template.findAll(BeforeConvertCallbackForSaveBatch.class);
+		List<BeforeConvertCallbackForSaveBatch> allEntriesInTable = template
+				.findAll(BeforeConvertCallbackForSaveBatch.class);
 
-		Assertions.assertThat(allEntriesInTable)
-				.hasSize(3)
-				.extracting(BeforeConvertCallbackForSaveBatch::getName)
-				.containsOnly("first", "second", "third");
+		assertThat(allEntriesInTable).hasSize(3).extracting(BeforeConvertCallbackForSaveBatch::getName)
+				.containsExactlyInAnyOrder("first", "second", "third");
 	}
 
 	@Test // GH-1684
@@ -2220,9 +2217,8 @@ public String getId() {
 			return id;
 		}
 
-		public BeforeConvertCallbackForSaveBatch setId(String id) {
+		public void setId(String id) {
 			this.id = id;
-			return this;
 		}
 
 		public String getName() {
@@ -2260,12 +2256,14 @@ static class WithIdOnly {
 
 	@Table
 	static class WithInsertOnly {
+
 		@Id Long id;
 		@InsertOnlyProperty String insertOnly;
 	}
 
 	@Table
 	static class MultipleCollections {
+
 		@Id Long id;
 		String name;
 		List<ListElement> listElements = new ArrayList<>();

From 2ca8f4fb6f5827b73ce2adc8aeb29b9b2cb9e4f4 Mon Sep 17 00:00:00 2001
From: Jens Schauder <jens.schauder@broadcom.com>
Date: Mon, 23 Dec 2024 14:09:53 +0100
Subject: [PATCH 07/28] `JdbcAggregateTemplate` honors columns specified in
 query.

If no columns are given, all columns are selected by default.

If columns are specified, only these are selected.
Joins normally triggered by columns from 1:1 relationships are not implemented, and the corresponding columns don't get loaded and can't be specified in a query.

Limiting columns is not supported for single query loading.

Closes #1803
Original pull request: #1967
---
 .../data/jdbc/core/convert/SqlGenerator.java  | 96 +++++++++++++++----
 ...JdbcAggregateTemplateIntegrationTests.java | 26 ++++-
 .../core/convert/SqlGeneratorUnitTests.java   | 45 ++++++++-
 .../data/relational/core/query/Query.java     |  3 +-
 4 files changed, 146 insertions(+), 24 deletions(-)

diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java
index 7ac637e8c3..064ab5487f 100644
--- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java
+++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java
@@ -39,6 +39,7 @@
 import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
 import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
 
 /**
  * Generates SQL statements to be used by {@link SimpleJdbcRepository}
@@ -507,45 +508,85 @@ private String createFindAllSql() {
 	}
 
 	private SelectBuilder.SelectWhere selectBuilder() {
-		return selectBuilder(Collections.emptyList());
+		return selectBuilder(Collections.emptyList(), Query.empty());
+	}
+
+	private SelectBuilder.SelectWhere selectBuilder(Query query) {
+		return selectBuilder(Collections.emptyList(), query);
 	}
 
 	private SelectBuilder.SelectWhere selectBuilder(Collection<SqlIdentifier> keyColumns) {
+		return selectBuilder(keyColumns, Query.empty());
+	}
+
+	private SelectBuilder.SelectWhere selectBuilder(Collection<SqlIdentifier> keyColumns, Query query) {
 
 		Table table = getTable();
 
-		Set<Expression> columnExpressions = new LinkedHashSet<>();
+		Projection projection = getProjection(keyColumns, query, table);
+		SelectBuilder.SelectAndFrom selectBuilder = StatementBuilder.select(projection.columns());
+		SelectBuilder.SelectJoin baseSelect = selectBuilder.from(table);
 
-		List<Join> joinTables = new ArrayList<>();
-		for (PersistentPropertyPath<RelationalPersistentProperty> path : mappingContext
-				.findPersistentPropertyPaths(entity.getType(), p -> true)) {
+		for (Join join : projection.joins()) {
+			baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId);
+		}
 
-			AggregatePath extPath = mappingContext.getAggregatePath(path);
+		return (SelectBuilder.SelectWhere) baseSelect;
+	}
 
-			// add a join if necessary
-			Join join = getJoin(extPath);
-			if (join != null) {
-				joinTables.add(join);
+	private Projection getProjection(Collection<SqlIdentifier> keyColumns, Query query, Table table) {
+
+		Set<Expression> columns = new LinkedHashSet<>();
+		Set<Join> joins = new LinkedHashSet<>();
+
+		if (!CollectionUtils.isEmpty(query.getColumns())) {
+			for (SqlIdentifier columnName : query.getColumns()) {
+
+				String columnNameString = columnName.getReference();
+				RelationalPersistentProperty property = entity.getPersistentProperty(columnNameString);
+				if (property != null) {
+
+					AggregatePath aggregatePath = mappingContext.getAggregatePath(
+							mappingContext.getPersistentPropertyPath(columnNameString, entity.getTypeInformation()));
+					gatherColumn(aggregatePath, joins, columns);
+				} else {
+					columns.add(Column.create(columnName, table));
+				}
 			}
+		} else {
+			for (PersistentPropertyPath<RelationalPersistentProperty> path : mappingContext
+					.findPersistentPropertyPaths(entity.getType(), p -> true)) {
+
+				AggregatePath aggregatePath = mappingContext.getAggregatePath(path);
 
-			Column column = getColumn(extPath);
-			if (column != null) {
-				columnExpressions.add(column);
+				gatherColumn(aggregatePath, joins, columns);
 			}
 		}
 
 		for (SqlIdentifier keyColumn : keyColumns) {
-			columnExpressions.add(table.column(keyColumn).as(keyColumn));
+			columns.add(table.column(keyColumn).as(keyColumn));
 		}
 
-		SelectBuilder.SelectAndFrom selectBuilder = StatementBuilder.select(columnExpressions);
-		SelectBuilder.SelectJoin baseSelect = selectBuilder.from(table);
+		return new Projection(columns, joins);
+	}
 
-		for (Join join : joinTables) {
-			baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId);
+	private void gatherColumn(AggregatePath aggregatePath, Set<Join> joins, Set<Expression> columns) {
+
+		joins.addAll(getJoins(aggregatePath));
+
+		Column column = getColumn(aggregatePath);
+		if (column != null) {
+			columns.add(column);
 		}
+	}
 
-		return (SelectBuilder.SelectWhere) baseSelect;
+	/**
+	 * Projection including its source joins.
+	 *
+	 * @param columns
+	 * @param joins
+	 */
+	record Projection(Set<Expression> columns, Set<Join> joins) {
 	}
 
 	private SelectBuilder.SelectOrdered selectBuilder(Collection<SqlIdentifier> keyColumns, Sort sort,
@@ -611,9 +652,24 @@ Column getColumn(AggregatePath path) {
 		return sqlContext.getColumn(path);
 	}
 
+	List<Join> getJoins(AggregatePath path) {
+
+		List<Join> joins = new ArrayList<>();
+		while (!path.isRoot()) {
+			Join join = getJoin(path);
+			if (join != null) {
+				joins.add(join);
+			}
+
+			path = path.getParentPath();
+		}
+		return joins;
+	}
+
 	@Nullable
 	Join getJoin(AggregatePath path) {
 
+		// TODO: This doesn't handle paths with length > 1 correctly
 		if (!path.isEntity() || path.isEmbedded() || path.isMultiValued()) {
 			return null;
 		}
@@ -876,7 +932,7 @@ public String selectByQuery(Query query, MapSqlParameterSource parameterSource)
 
 		Assert.notNull(parameterSource, "parameterSource must not be null");
 
-		SelectBuilder.SelectWhere selectBuilder = selectBuilder();
+		SelectBuilder.SelectWhere selectBuilder = selectBuilder(query);
 
 		Select select = applyQueryOnSelect(query, parameterSource, selectBuilder) //
 				.build();
diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java
index 65e50ada3c..e27c386d7e 100644
--- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java
+++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java
@@ -29,6 +29,7 @@
 import java.util.stream.IntStream;
 import java.util.stream.Stream;
 
+import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.ApplicationContext;
@@ -237,7 +238,25 @@ void findAllByQuery() {
 		Query query = Query.query(criteria);
 		Iterable<SimpleListParent> reloadedById = template.findAll(query, SimpleListParent.class);
 
-		assertThat(reloadedById).extracting(e -> e.id, e -> e.content.size()).containsExactly(tuple(two.id, 2));
+		assertThat(reloadedById) //
+				.extracting(e -> e.id, e-> e.name, e -> e.content.size()) //
+				.containsExactly(tuple(two.id, two.name, 2));
+	}
+
+	@Test // GH-1803
+	void findAllByQueryWithColumns() {
+
+		template.save(SimpleListParent.of("one", "one_1"));
+		SimpleListParent two = template.save(SimpleListParent.of("two", "two_1", "two_2"));
+		template.save(SimpleListParent.of("three", "three_1", "three_2", "three_3"));
+
+		CriteriaDefinition criteria = CriteriaDefinition.from(Criteria.where("id").is(two.id));
+		Query query = Query.query(criteria).columns("id");
+		Iterable<SimpleListParent> reloadedById = template.findAll(query, SimpleListParent.class);
+
+		assertThat(reloadedById) //
+				.extracting(e -> e.id, e-> e.name, e -> e.content.size()) //
+				.containsExactly(tuple(two.id, null, 2));
 	}
 
 	@Test // GH-1601
@@ -2337,5 +2356,10 @@ static class JdbcAggregateTemplateIntegrationTests extends AbstractJdbcAggregate
 	static class JdbcAggregateTemplateSingleQueryLoadingIntegrationTests
 			extends AbstractJdbcAggregateTemplateIntegrationTests {
 
+		@Disabled
+		@Override
+		void findAllByQueryWithColumns() {
+			super.findAllByQueryWithColumns();
+		}
 	}
 }
diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java
index cc264cbe62..51637b72a8 100644
--- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java
+++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java
@@ -351,12 +351,13 @@ void findAllPagedAndSorted() {
 	@Test // GH-1919
 	void selectByQuery() {
 
-		Query query = Query.query(Criteria.where("id").is(23L));
+		Query query = Query.query(Criteria.where("id").is(23L)).columns(new String[0]);
 
 		String sql = sqlGenerator.selectByQuery(query, new MapSqlParameterSource());
 
 		assertThat(sql).contains( //
 				"SELECT", //
+				"dummy_entity.id1 AS id1, dummy_entity.x_name AS x_name", //
 				"FROM dummy_entity", //
 				"LEFT OUTER JOIN referenced_entity ref ON ref.dummy_entity = dummy_entity.id1", //
 				"LEFT OUTER JOIN second_level_referenced_entity ref_further ON ref_further.referenced_entity = ref.x_l1id", //
@@ -364,6 +365,45 @@ void selectByQuery() {
 		);
 	}
 
+	@Test // GH-1803
+	void selectByQueryWithColumnLimit() {
+
+		Query query = Query.empty().columns("id", "alpha", "beta", "gamma");
+
+		String sql = sqlGenerator.selectByQuery(query, new MapSqlParameterSource());
+
+		assertThat(sql).contains( //
+				"SELECT dummy_entity.id1 AS id1, dummy_entity.alpha, dummy_entity.beta, dummy_entity.gamma", //
+				"FROM dummy_entity" //
+		);
+	}
+
+	@Test // GH-1803
+	void selectingSetContentSelectsAllColumns() {
+
+		Query query = Query.empty().columns("elements.content");
+
+		String sql = sqlGenerator.selectByQuery(query, new MapSqlParameterSource());
+
+		assertThat(sql).contains( //
+				"SELECT dummy_entity.id1 AS id1, dummy_entity.x_name AS x_name"//
+		);
+	}
+
+	@Test // GH-1803
+	void selectByQueryWithMappedColumnPathsRendersCorrectSelection() {
+
+		Query query = Query.empty().columns("ref.content");
+
+		String sql = sqlGenerator.selectByQuery(query, new MapSqlParameterSource());
+
+		assertThat(sql).contains( //
+				"SELECT", //
+				"ref.id1 AS id1, ref.content AS x_content", //
+				"FROM dummy_entity", //
+				"LEFT OUTER JOIN referenced_entity ref ON ref.dummy_entity = dummy_entity.id1");
+	}
+
 	@Test // GH-1919
 	void selectBySortedQuery() {
 
@@ -381,7 +421,8 @@ void selectBySortedQuery() {
 				"ORDER BY dummy_entity.id1 ASC" //
 		);
 		assertThat(sql).containsOnlyOnce("LEFT OUTER JOIN referenced_entity ref ON ref.dummy_entity = dummy_entity.id1");
-		assertThat(sql).containsOnlyOnce("LEFT OUTER JOIN second_level_referenced_entity ref_further ON ref_further.referenced_entity = ref.x_l1id");
+		assertThat(sql).containsOnlyOnce(
+				"LEFT OUTER JOIN second_level_referenced_entity ref_further ON ref_further.referenced_entity = ref.x_l1id");
 	}
 
 	@Test // DATAJDBC-131, DATAJDBC-111
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Query.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Query.java
index 3a8e9d72c6..6d1ed69d4f 100644
--- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Query.java
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Query.java
@@ -41,6 +41,7 @@
  */
 public class Query {
 
+	private static final Query EMPTY = new Query(null);
 	private static final int NO_LIMIT = -1;
 
 	private final @Nullable CriteriaDefinition criteria;
@@ -84,7 +85,7 @@ private Query(@Nullable CriteriaDefinition criteria, List<SqlIdentifier> columns
 	 * @return
 	 */
 	public static Query empty() {
-		return new Query(null);
+		return EMPTY;
 	}
 
 	/**

From ddc2c6d45349824ba430774d49ab61a3bb6b1803 Mon Sep 17 00:00:00 2001
From: Mark Paluch <mark.paluch@broadcom.com>
Date: Wed, 11 Jun 2025 15:12:41 +0200
Subject: [PATCH 08/28] Polishing.

Resolve mapped property paths to aggregate paths. Add tests.
Clean up unused profile declarations.

See #1803
Original pull request: #1967
---
 pom.xml                                       |  9 -----
 .../data/jdbc/core/convert/SqlGenerator.java  | 38 +++++++++----------
 .../core/convert/SqlGeneratorUnitTests.java   |  3 +-
 3 files changed, 20 insertions(+), 30 deletions(-)

diff --git a/pom.xml b/pom.xml
index 06a3b31137..63522780a6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -52,7 +52,6 @@
 		<!-- test dependencies -->
 		<archunit.version>1.3.0</archunit.version>
 
-		<jmh.version>1.37</jmh.version>
 		<mbr.version>0.4.0.BUILD-SNAPSHOT</mbr.version>
 	</properties>
 
@@ -165,22 +164,14 @@
 		<profile>
 			<id>jmh</id>
 			<dependencies>
-				<dependency>
-					<groupId>com.github.mp911de.microbenchmark-runner</groupId>
-					<artifactId>microbenchmark-runner-junit5</artifactId>
-					<version>${mbr.version}</version>
-					<scope>test</scope>
-				</dependency>
 				<dependency>
 					<groupId>org.openjdk.jmh</groupId>
 					<artifactId>jmh-core</artifactId>
-					<version>${jmh.version}</version>
 					<scope>test</scope>
 				</dependency>
 				<dependency>
 					<groupId>org.openjdk.jmh</groupId>
 					<artifactId>jmh-generator-annprocess</artifactId>
-					<version>${jmh.version}</version>
 					<scope>test</scope>
 				</dependency>
 			</dependencies>
diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java
index 064ab5487f..783f12f157 100644
--- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java
+++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java
@@ -23,6 +23,7 @@
 import org.springframework.data.domain.Sort;
 import org.springframework.data.jdbc.repository.support.SimpleJdbcRepository;
 import org.springframework.data.mapping.PersistentPropertyPath;
+import org.springframework.data.mapping.context.InvalidPersistentPropertyPath;
 import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.relational.core.dialect.Dialect;
 import org.springframework.data.relational.core.dialect.RenderContextFactory;
@@ -36,10 +37,10 @@
 import org.springframework.data.relational.core.sql.render.RenderContext;
 import org.springframework.data.relational.core.sql.render.SqlRenderer;
 import org.springframework.data.util.Lazy;
+import org.springframework.data.util.Predicates;
 import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
 import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
-import org.springframework.util.CollectionUtils;
 
 /**
  * Generates SQL statements to be used by {@link SimpleJdbcRepository}
@@ -539,27 +540,26 @@ private Projection getProjection(Collection<SqlIdentifier> keyColumns, Query que
 		Set<Expression> columns = new LinkedHashSet<>();
 		Set<Join> joins = new LinkedHashSet<>();
 
-		if (!CollectionUtils.isEmpty(query.getColumns())) {
-			for (SqlIdentifier columnName : query.getColumns()) {
+		for (SqlIdentifier columnName : query.getColumns()) {
 
-				String columnNameString = columnName.getReference();
-				RelationalPersistentProperty property = entity.getPersistentProperty(columnNameString);
-				if (property != null) {
+			try {
+				AggregatePath aggregatePath = mappingContext.getAggregatePath(
+						mappingContext.getPersistentPropertyPath(columnName.getReference(), entity.getTypeInformation()));
 
-					AggregatePath aggregatePath = mappingContext.getAggregatePath(
-							mappingContext.getPersistentPropertyPath(columnNameString, entity.getTypeInformation()));
-					gatherColumn(aggregatePath, joins, columns);
-				} else {
-					columns.add(Column.create(columnName, table));
-				}
+				includeColumnAndJoin(aggregatePath, joins, columns);
+			} catch (InvalidPersistentPropertyPath e) {
+				columns.add(Column.create(columnName, table));
 			}
-		} else {
+		}
+
+		if (columns.isEmpty()) {
+
 			for (PersistentPropertyPath<RelationalPersistentProperty> path : mappingContext
-					.findPersistentPropertyPaths(entity.getType(), p -> true)) {
+					.findPersistentPropertyPaths(entity.getType(), Predicates.isTrue())) {
 
 				AggregatePath aggregatePath = mappingContext.getAggregatePath(path);
 
-				gatherColumn(aggregatePath, joins, columns);
+				includeColumnAndJoin(aggregatePath, joins, columns);
 			}
 		}
 
@@ -570,7 +570,8 @@ private Projection getProjection(Collection<SqlIdentifier> keyColumns, Query que
 		return new Projection(columns, joins);
 	}
 
-	private void gatherColumn(AggregatePath aggregatePath, Set<Join> joins, Set<Expression> columns) {
+	private void includeColumnAndJoin(AggregatePath aggregatePath, Collection<Join> joins,
+			Collection<Expression> columns) {
 
 		joins.addAll(getJoins(aggregatePath));
 
@@ -639,10 +640,7 @@ Column getColumn(AggregatePath path) {
 			// Simple entities without id include there backreference as a synthetic id in order to distinguish null entities
 			// from entities with only null values.
 
-			if (path.isQualified() //
-					|| path.isCollectionLike() //
-					|| path.hasIdProperty() //
-			) {
+			if (path.isQualified() || path.isCollectionLike() || path.hasIdProperty()) {
 				return null;
 			}
 
diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java
index 51637b72a8..62e95245d7 100644
--- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java
+++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java
@@ -32,6 +32,7 @@
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+
 import org.springframework.data.annotation.Id;
 import org.springframework.data.annotation.ReadOnlyProperty;
 import org.springframework.data.annotation.Version;
@@ -399,7 +400,7 @@ void selectByQueryWithMappedColumnPathsRendersCorrectSelection() {
 
 		assertThat(sql).contains( //
 				"SELECT", //
-				"ref.id1 AS id1, ref.content AS x_content", //
+				"ref.x_content AS ref_x_content", //
 				"FROM dummy_entity", //
 				"LEFT OUTER JOIN referenced_entity ref ON ref.dummy_entity = dummy_entity.id1");
 	}

From ee3af26030954680987eb9398f7e2b84c955699f Mon Sep 17 00:00:00 2001
From: Mark Paluch <mark.paluch@broadcom.com>
Date: Fri, 13 Jun 2025 13:39:15 +0200
Subject: [PATCH 09/28] Prepare 3.5.1 (2025.0.1).

See #2057
---
 pom.xml                       | 20 ++++----------------
 src/main/resources/notice.txt |  3 ++-
 2 files changed, 6 insertions(+), 17 deletions(-)

diff --git a/pom.xml b/pom.xml
index 63522780a6..31083253b8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,12 +15,12 @@
 	<parent>
 		<groupId>org.springframework.data.build</groupId>
 		<artifactId>spring-data-parent</artifactId>
-		<version>3.5.1-SNAPSHOT</version>
+		<version>3.5.1</version>
 	</parent>
 
 	<properties>
 		<dist.id>spring-data-jdbc</dist.id>
-		<springdata.commons>3.5.1-SNAPSHOT</springdata.commons>
+		<springdata.commons>3.5.1</springdata.commons>
 		<liquibase.version>4.21.1</liquibase.version>
 		<sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis>
 
@@ -301,20 +301,8 @@
 	</build>
 
 	<repositories>
-		<repository>
-			<id>spring-snapshot</id>
-			<url>https://repo.spring.io/snapshot</url>
-			<snapshots>
-				<enabled>true</enabled>
-			</snapshots>
-			<releases>
-				<enabled>false</enabled>
-			</releases>
-		</repository>
-		<repository>
-			<id>spring-milestone</id>
-			<url>https://repo.spring.io/milestone</url>
-		</repository>
+		
+		
 	</repositories>
 
 </project>
diff --git a/src/main/resources/notice.txt b/src/main/resources/notice.txt
index ef332e5895..bce712e7c9 100644
--- a/src/main/resources/notice.txt
+++ b/src/main/resources/notice.txt
@@ -1,4 +1,4 @@
-Spring Data Relational 3.5 GA (2025.0.0)
+Spring Data Relational 3.5.1 (2025.0.1)
 Copyright (c) [2017-2019] Pivotal Software, Inc.
 
 This product is licensed to you under the Apache License, Version 2.0 (the "License").
@@ -60,5 +60,6 @@ conditions of the subcomponent's license, as noted in the LICENSE file.
 
 
 
+
 
 

From 3117465d35617f35a61e93a0ff79e727c6965255 Mon Sep 17 00:00:00 2001
From: Mark Paluch <mark.paluch@broadcom.com>
Date: Fri, 13 Jun 2025 13:39:35 +0200
Subject: [PATCH 10/28] Release version 3.5.1 (2025.0.1).

See #2057
---
 pom.xml                               | 2 +-
 spring-data-jdbc-distribution/pom.xml | 2 +-
 spring-data-jdbc/pom.xml              | 4 ++--
 spring-data-r2dbc/pom.xml             | 4 ++--
 spring-data-relational/pom.xml        | 4 ++--
 5 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/pom.xml b/pom.xml
index 31083253b8..c644703166 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
 
 	<groupId>org.springframework.data</groupId>
 	<artifactId>spring-data-relational-parent</artifactId>
-	<version>3.5.1-SNAPSHOT</version>
+	<version>3.5.1</version>
 	<packaging>pom</packaging>
 
 	<name>Spring Data Relational Parent</name>
diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml
index 67f0fc9eff..fce8dc0c83 100644
--- a/spring-data-jdbc-distribution/pom.xml
+++ b/spring-data-jdbc-distribution/pom.xml
@@ -14,7 +14,7 @@
 	<parent>
 		<groupId>org.springframework.data</groupId>
 		<artifactId>spring-data-relational-parent</artifactId>
-		<version>3.5.1-SNAPSHOT</version>
+		<version>3.5.1</version>
 		<relativePath>../pom.xml</relativePath>
 	</parent>
 
diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml
index f8185d06d0..9c240b9dac 100644
--- a/spring-data-jdbc/pom.xml
+++ b/spring-data-jdbc/pom.xml
@@ -6,7 +6,7 @@
 	<modelVersion>4.0.0</modelVersion>
 
 	<artifactId>spring-data-jdbc</artifactId>
-	<version>3.5.1-SNAPSHOT</version>
+	<version>3.5.1</version>
 
 	<name>Spring Data JDBC</name>
 	<description>Spring Data module for JDBC repositories.</description>
@@ -15,7 +15,7 @@
 	<parent>
 		<groupId>org.springframework.data</groupId>
 		<artifactId>spring-data-relational-parent</artifactId>
-		<version>3.5.1-SNAPSHOT</version>
+		<version>3.5.1</version>
 	</parent>
 
 	<properties>
diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml
index 556a23247c..607292127d 100644
--- a/spring-data-r2dbc/pom.xml
+++ b/spring-data-r2dbc/pom.xml
@@ -6,7 +6,7 @@
 	<modelVersion>4.0.0</modelVersion>
 
 	<artifactId>spring-data-r2dbc</artifactId>
-	<version>3.5.1-SNAPSHOT</version>
+	<version>3.5.1</version>
 
 	<name>Spring Data R2DBC</name>
 	<description>Spring Data module for R2DBC</description>
@@ -15,7 +15,7 @@
 	<parent>
 		<groupId>org.springframework.data</groupId>
 		<artifactId>spring-data-relational-parent</artifactId>
-		<version>3.5.1-SNAPSHOT</version>
+		<version>3.5.1</version>
 	</parent>
 
 	<properties>
diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml
index 41c9d7db2d..005568f815 100644
--- a/spring-data-relational/pom.xml
+++ b/spring-data-relational/pom.xml
@@ -6,7 +6,7 @@
 	<modelVersion>4.0.0</modelVersion>
 
 	<artifactId>spring-data-relational</artifactId>
-	<version>3.5.1-SNAPSHOT</version>
+	<version>3.5.1</version>
 
 	<name>Spring Data Relational</name>
 	<description>Spring Data Relational support</description>
@@ -14,7 +14,7 @@
 	<parent>
 		<groupId>org.springframework.data</groupId>
 		<artifactId>spring-data-relational-parent</artifactId>
-		<version>3.5.1-SNAPSHOT</version>
+		<version>3.5.1</version>
 	</parent>
 
 	<properties>

From 82d4a6619d5d0ea1f402bc126920f518a94fb68c Mon Sep 17 00:00:00 2001
From: Mark Paluch <mark.paluch@broadcom.com>
Date: Fri, 13 Jun 2025 13:42:18 +0200
Subject: [PATCH 11/28] Prepare next development iteration.

See #2057
---
 pom.xml                               | 2 +-
 spring-data-jdbc-distribution/pom.xml | 2 +-
 spring-data-jdbc/pom.xml              | 4 ++--
 spring-data-r2dbc/pom.xml             | 4 ++--
 spring-data-relational/pom.xml        | 4 ++--
 5 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/pom.xml b/pom.xml
index c644703166..1d1b28ffef 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
 
 	<groupId>org.springframework.data</groupId>
 	<artifactId>spring-data-relational-parent</artifactId>
-	<version>3.5.1</version>
+	<version>3.5.2-SNAPSHOT</version>
 	<packaging>pom</packaging>
 
 	<name>Spring Data Relational Parent</name>
diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml
index fce8dc0c83..3e9bea453e 100644
--- a/spring-data-jdbc-distribution/pom.xml
+++ b/spring-data-jdbc-distribution/pom.xml
@@ -14,7 +14,7 @@
 	<parent>
 		<groupId>org.springframework.data</groupId>
 		<artifactId>spring-data-relational-parent</artifactId>
-		<version>3.5.1</version>
+		<version>3.5.2-SNAPSHOT</version>
 		<relativePath>../pom.xml</relativePath>
 	</parent>
 
diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml
index 9c240b9dac..a7d0ffe0aa 100644
--- a/spring-data-jdbc/pom.xml
+++ b/spring-data-jdbc/pom.xml
@@ -6,7 +6,7 @@
 	<modelVersion>4.0.0</modelVersion>
 
 	<artifactId>spring-data-jdbc</artifactId>
-	<version>3.5.1</version>
+	<version>3.5.2-SNAPSHOT</version>
 
 	<name>Spring Data JDBC</name>
 	<description>Spring Data module for JDBC repositories.</description>
@@ -15,7 +15,7 @@
 	<parent>
 		<groupId>org.springframework.data</groupId>
 		<artifactId>spring-data-relational-parent</artifactId>
-		<version>3.5.1</version>
+		<version>3.5.2-SNAPSHOT</version>
 	</parent>
 
 	<properties>
diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml
index 607292127d..e16c7e5735 100644
--- a/spring-data-r2dbc/pom.xml
+++ b/spring-data-r2dbc/pom.xml
@@ -6,7 +6,7 @@
 	<modelVersion>4.0.0</modelVersion>
 
 	<artifactId>spring-data-r2dbc</artifactId>
-	<version>3.5.1</version>
+	<version>3.5.2-SNAPSHOT</version>
 
 	<name>Spring Data R2DBC</name>
 	<description>Spring Data module for R2DBC</description>
@@ -15,7 +15,7 @@
 	<parent>
 		<groupId>org.springframework.data</groupId>
 		<artifactId>spring-data-relational-parent</artifactId>
-		<version>3.5.1</version>
+		<version>3.5.2-SNAPSHOT</version>
 	</parent>
 
 	<properties>
diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml
index 005568f815..e44441fb9b 100644
--- a/spring-data-relational/pom.xml
+++ b/spring-data-relational/pom.xml
@@ -6,7 +6,7 @@
 	<modelVersion>4.0.0</modelVersion>
 
 	<artifactId>spring-data-relational</artifactId>
-	<version>3.5.1</version>
+	<version>3.5.2-SNAPSHOT</version>
 
 	<name>Spring Data Relational</name>
 	<description>Spring Data Relational support</description>
@@ -14,7 +14,7 @@
 	<parent>
 		<groupId>org.springframework.data</groupId>
 		<artifactId>spring-data-relational-parent</artifactId>
-		<version>3.5.1</version>
+		<version>3.5.2-SNAPSHOT</version>
 	</parent>
 
 	<properties>

From d8547c287220a33a5dc66d093d26ce59d595b25a Mon Sep 17 00:00:00 2001
From: Mark Paluch <mark.paluch@broadcom.com>
Date: Fri, 13 Jun 2025 13:42:19 +0200
Subject: [PATCH 12/28] After release cleanups.

See #2057
---
 pom.xml | 20 ++++++++++++++++----
 1 file changed, 16 insertions(+), 4 deletions(-)

diff --git a/pom.xml b/pom.xml
index 1d1b28ffef..c461de2f16 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,12 +15,12 @@
 	<parent>
 		<groupId>org.springframework.data.build</groupId>
 		<artifactId>spring-data-parent</artifactId>
-		<version>3.5.1</version>
+		<version>3.5.2-SNAPSHOT</version>
 	</parent>
 
 	<properties>
 		<dist.id>spring-data-jdbc</dist.id>
-		<springdata.commons>3.5.1</springdata.commons>
+		<springdata.commons>3.5.2-SNAPSHOT</springdata.commons>
 		<liquibase.version>4.21.1</liquibase.version>
 		<sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis>
 
@@ -301,8 +301,20 @@
 	</build>
 
 	<repositories>
-		
-		
+		<repository>
+			<id>spring-snapshot</id>
+			<url>https://repo.spring.io/snapshot</url>
+			<snapshots>
+				<enabled>true</enabled>
+			</snapshots>
+			<releases>
+				<enabled>false</enabled>
+			</releases>
+		</repository>
+		<repository>
+			<id>spring-milestone</id>
+			<url>https://repo.spring.io/milestone</url>
+		</repository>
 	</repositories>
 
 </project>

From 5ab251d65d3c098a4e1f23bbb0b87f8c0b349b42 Mon Sep 17 00:00:00 2001
From: Jens Schauder <jens.schauder@broadcom.com>
Date: Mon, 16 Jun 2025 08:46:36 +0200
Subject: [PATCH 13/28] Fix Typo in error message.

Closes #2073
---
 .../org/springframework/data/relational/domain/SqlSort.java     | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/domain/SqlSort.java b/spring-data-relational/src/main/java/org/springframework/data/relational/domain/SqlSort.java
index 2ee60cfcd7..ccc9509231 100644
--- a/spring-data-relational/src/main/java/org/springframework/data/relational/domain/SqlSort.java
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/domain/SqlSort.java
@@ -84,7 +84,7 @@ public static void validate(Sort.Order order) {
 
 		if (!predicate.test(property)) {
 			throw new IllegalArgumentException(
-					"order fields that are not marked as unsafe must only consist of digits, letter, '.', '_', and '\'. If you want to sort by arbitrary expressions please use RelationalSort.unsafe. Note that such expressions become part of SQL statements and therefore need to be sanatized to prevent SQL injection attacks.");
+					"order fields that are not marked as unsafe must only consist of digits, letter, '.', '_', and '\'. If you want to sort by arbitrary expressions please use RelationalSort.unsafe. Note that such expressions become part of SQL statements and therefore need to be sanitized to prevent SQL injection attacks.");
 		}
 	}
 

From dcae93a2d2c433632f9c50db72556dfea1a8815a Mon Sep 17 00:00:00 2001
From: JaeYeon Kim <ghgh415263@naver.com>
Date: Mon, 16 Jun 2025 21:31:24 +0900
Subject: [PATCH 14/28] Fixing Typo in JdbcAggregateChangeExecutionContext
 Exception Message.

Signed-off-by: JaeYeon Kim <ghgh415263@naver.com>

Original pull request #2074
---
 .../data/jdbc/core/JdbcAggregateChangeExecutionContext.java     | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java
index 2ec070ab76..87644844df 100644
--- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java
+++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java
@@ -55,7 +55,7 @@
 class JdbcAggregateChangeExecutionContext {
 
 	private static final String UPDATE_FAILED = "Failed to update entity [%s]; Id [%s] not found in database";
-	private static final String UPDATE_FAILED_OPTIMISTIC_LOCKING = "Failed to update entity [%s]; The entity was updated since it was rea or it isn't in the database at all";
+	private static final String UPDATE_FAILED_OPTIMISTIC_LOCKING = "Failed to update entity [%s]; The entity was updated since it was read or it isn't in the database at all";
 
 	private final RelationalMappingContext context;
 	private final JdbcConverter converter;

From 366f6832701bbec8e00ca4b48a9f1b70410bafd5 Mon Sep 17 00:00:00 2001
From: JaeYeon Kim <JaeYeon.Kim@ibm.com>
Date: Sun, 6 Jul 2025 19:54:53 +0900
Subject: [PATCH 15/28] Fix typos in DbAction class documentation

Signed-off-by: JaeYeon Kim <JaeYeon.Kim@ibm.com>

Squashed by Jens Schauder

Original pull request #2080
---
 .../data/relational/core/conversion/DbAction.java             | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java
index ea3000ce99..ec5ab5106a 100644
--- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/DbAction.java
@@ -178,7 +178,7 @@ public String toString() {
 	}
 
 	/**
-	 * Represents a delete statement for all entities that that a reachable via a give path from the aggregate root.
+	 * Represents a delete statement for all entities that are reachable via a given path from the aggregate root.
 	 *
 	 * @param <T> type of the entity for which this represents a database interaction.
 	 */
@@ -249,7 +249,7 @@ public String toString() {
 	}
 
 	/**
-	 * Represents an delete statement for all entities that that a reachable via a give path from any aggregate root of a
+	 * Represents a delete statement for all entities that are reachable via a given path from any aggregate root of a
 	 * given type.
 	 *
 	 * @param <T> type of the entity for which this represents a database interaction.

From 67be9f07095ec8b30b7ac55ff55c8450d36a071e Mon Sep 17 00:00:00 2001
From: Mark Paluch <mark.paluch@broadcom.com>
Date: Mon, 14 Jul 2025 10:36:20 +0200
Subject: [PATCH 16/28] Upgrade JDBC and R2DBC drivers.

DB2 12.1.2.0, MariaDB 3.5.4/1.3.0, MSSQL 12.10.1, MySQL 9.3.0, Postgres 42.7.7

Closes #2085
---
 pom.xml | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/pom.xml b/pom.xml
index c461de2f16..346d0dba8e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,20 +31,20 @@
 
 		<!-- databases and jdbc drivers -->
 		<!-- note that these currently do not control the versions of databases used via Testcontainers for testing -->
-		<db2.version>12.1.0.0</db2.version>
+		<db2.version>12.1.2.0</db2.version>
 		<h2.version>2.3.232</h2.version>
 		<hikari.version>5.1.0</hikari.version>
 		<hsqldb.version>2.7.3</hsqldb.version>
-		<mariadb-java-client.version>3.5.3</mariadb-java-client.version>
-		<mssql.version>12.10.0.jre11</mssql.version>
-		<mysql-connector-java.version>9.2.0</mysql-connector-java.version>
-		<postgresql.version>42.7.5</postgresql.version>
+		<mariadb-java-client.version>3.5.4</mariadb-java-client.version>
+		<mssql.version>12.10.1.jre11</mssql.version>
+		<mysql-connector-java.version>9.3.0</mysql-connector-java.version>
+		<postgresql.version>42.7.7</postgresql.version>
 		<oracle.version>23.8.0.25.04</oracle.version>
 
 		<!-- R2DBC driver dependencies-->
 		<r2dbc-postgresql.version>1.0.7.RELEASE</r2dbc-postgresql.version>
 		<r2dbc-h2.version>1.0.0.RELEASE</r2dbc-h2.version>
-		<r2dbc-mariadb.version>1.1.4</r2dbc-mariadb.version>
+		<r2dbc-mariadb.version>1.3.0</r2dbc-mariadb.version>
 		<r2dbc-mssql.version>1.0.2.RELEASE</r2dbc-mssql.version>
 		<r2dbc-mysql.version>1.4.1</r2dbc-mysql.version>
 		<oracle-r2dbc.version>1.3.0</oracle-r2dbc.version>

From ddb3e3d814212fdde4776d0736ddf2eada9df5f6 Mon Sep 17 00:00:00 2001
From: Jens Schauder <jens.schauder@broadcom.com>
Date: Mon, 14 Jul 2025 11:41:17 +0200
Subject: [PATCH 17/28] Fixes comparison to use equality instead of reference.

TypeInformation.OBJECT must not be compared by reference since TypeInformation instances come form a LRU cache and if TypeInformation.OBJECT gets evicted future instances might be equal, but won't have the same reference.

Closes #2083
---
 .../relational/core/conversion/MappingRelationalConverter.java  | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java
index 395c64e677..a80b6f1aa6 100644
--- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java
@@ -693,7 +693,7 @@ public Object writeValue(@Nullable Object value, TypeInformation<?> type) {
 				return getConversionService().convert(value, customWriteTarget.get());
 			}
 
-			if (TypeInformation.OBJECT != type) {
+			if (!TypeInformation.OBJECT.equals(type)) {
 
 				if (type.getType().isAssignableFrom(value.getClass())) {
 

From cfb39c172ad15dc4cf8d0ed530d807ae5cd35466 Mon Sep 17 00:00:00 2001
From: Jens Schauder <jens.schauder@broadcom.com>
Date: Tue, 8 Jul 2025 13:53:35 +0200
Subject: [PATCH 18/28] Apply custom conversions for collections.

The target type for the conversion of the complete collection gets adapted when a custom conversion was applied to the elements.

Also `JdbcValue` elements as a result of a custom conversion get unwrapped.

Closes #2078
Original pull request: #2081
---
 .../core/convert/MappingJdbcConverter.java    | 21 ++++++++++++++++
 ...itoryCustomConversionIntegrationTests.java | 25 +++++++++++++++++++
 ...oryCustomConversionIntegrationTests-h2.sql |  1 +
 ...yCustomConversionIntegrationTests-hsql.sql |  1 +
 ...tomConversionIntegrationTests-postgres.sql |  1 +
 .../MappingRelationalConverter.java           | 25 +++++++++++++++++--
 6 files changed, 72 insertions(+), 2 deletions(-)

diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java
index 7460931dab..45245bae1b 100644
--- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java
+++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java
@@ -255,6 +255,9 @@ public JdbcValue writeJdbcValue(@Nullable Object value, TypeInformation<?> colum
 		}
 
 		Class<?> componentType = convertedValue.getClass().getComponentType();
+
+		if (convertedValue.getClass().isArray()) {
+
 		if (componentType != byte.class && componentType != Byte.class) {
 
 			Object[] objectArray = requireObjectArray(convertedValue);
@@ -268,6 +271,24 @@ public JdbcValue writeJdbcValue(@Nullable Object value, TypeInformation<?> colum
 		return JdbcValue.of(convertedValue, JDBCType.BINARY);
 	}
 
+		return JdbcValue.of(convertedValue, sqlType);
+	}
+
+	/**
+	 * Unwraps values of type {@link JdbcValue}.
+	 *
+	 * @param convertedValue a value that might need unwrapping.
+	 */
+	@Override
+	@Nullable
+	protected Object unwrap(@Nullable Object convertedValue) {
+
+		if (convertedValue instanceof JdbcValue jdbcValue) {
+			return jdbcValue.getValue();
+		}
+		return convertedValue;
+	}
+
 	@SuppressWarnings("unchecked")
 	@Override
 	public <R> R readAndResolve(TypeInformation<R> type, RowDocument source, Identifier identifier) {
diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCustomConversionIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCustomConversionIntegrationTests.java
index d8f823f12d..39eff02265 100644
--- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCustomConversionIntegrationTests.java
+++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCustomConversionIntegrationTests.java
@@ -38,8 +38,10 @@
 import org.springframework.data.jdbc.core.mapping.JdbcValue;
 import org.springframework.data.jdbc.repository.query.Query;
 import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
+import org.springframework.data.jdbc.testing.EnabledOnFeature;
 import org.springframework.data.jdbc.testing.IntegrationTest;
 import org.springframework.data.jdbc.testing.TestConfiguration;
+import org.springframework.data.jdbc.testing.TestDatabaseFeatures;
 import org.springframework.data.repository.CrudRepository;
 
 /**
@@ -61,6 +63,11 @@ EntityWithStringyBigDecimalRepository repository(JdbcRepositoryFactory factory)
 			return factory.getRepository(EntityWithStringyBigDecimalRepository.class);
 		}
 
+		@Bean
+		EntityWithDirectionsRepository repositoryWithDirections(JdbcRepositoryFactory factory) {
+			return factory.getRepository(EntityWithDirectionsRepository.class);
+		}
+
 		@Bean
 		JdbcCustomConversions jdbcCustomConversions() {
 			return new JdbcCustomConversions(asList(StringToBigDecimalConverter.INSTANCE, BigDecimalToString.INSTANCE,
@@ -70,6 +77,7 @@ JdbcCustomConversions jdbcCustomConversions() {
 	}
 
 	@Autowired EntityWithStringyBigDecimalRepository repository;
+	@Autowired EntityWithDirectionsRepository repositoryWithDirections;
 
 	/**
 	 * In PostrgreSQL this fails if a simple converter like the following is used.
@@ -162,6 +170,18 @@ void queryByEnumTypeEqual() {
 				.containsExactly(Direction.CENTER);
 	}
 
+	@Test // GH-2078
+	@EnabledOnFeature(TestDatabaseFeatures.Feature.SUPPORTS_ARRAYS)
+	void saveAndLoadListOfDirectionsAsArray() {
+
+		EntityWithDirections saved = repositoryWithDirections
+				.save(new EntityWithDirections(null, List.of(Direction.CENTER, Direction.RIGHT)));
+
+		EntityWithDirections reloaded = repositoryWithDirections.findById(saved.id).orElseThrow();
+
+		assertThat(reloaded).isEqualTo(saved);
+	}
+
 	interface EntityWithStringyBigDecimalRepository extends CrudRepository<EntityWithStringyBigDecimal, CustomId> {
 
 		@Query("SELECT * FROM ENTITY_WITH_STRINGY_BIG_DECIMAL WHERE DIRECTION IN (:types)")
@@ -171,6 +191,8 @@ interface EntityWithStringyBigDecimalRepository extends CrudRepository<EntityWit
 		List<EntityWithStringyBigDecimal> findByEnumType(Direction type);
 	}
 
+	interface EntityWithDirectionsRepository extends CrudRepository<EntityWithDirections, Long> {}
+
 	private static class EntityWithStringyBigDecimal {
 
 		@Id CustomId id;
@@ -194,6 +216,9 @@ private static class OtherEntity {
 		Date created;
 	}
 
+	record EntityWithDirections(@Id Long id, List<Direction> directions) {
+	}
+
 	enum Direction {
 		LEFT, CENTER, RIGHT
 	}
diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-h2.sql
index 426153b9e3..b6aebdd4df 100644
--- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-h2.sql
+++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-h2.sql
@@ -1,2 +1,3 @@
 CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id IDENTITY PRIMARY KEY, Stringy_number DECIMAL(20,10), DIRECTION INTEGER);
 CREATE TABLE OTHER_ENTITY ( ID IDENTITY PRIMARY KEY, CREATED DATE, ENTITY_WITH_STRINGY_BIG_DECIMAL INTEGER);
+CREATE TABLE ENTITY_WITH_DIRECTIONS ( ID IDENTITY PRIMARY KEY, DIRECTIONS INTEGER ARRAY);
diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-hsql.sql
index 9508fbb0e2..a56b15ecf2 100644
--- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-hsql.sql
+++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-hsql.sql
@@ -1,3 +1,4 @@
 CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id IDENTITY PRIMARY KEY, Stringy_number DECIMAL(20,10), DIRECTION INTEGER);
 CREATE TABLE OTHER_ENTITY ( ID IDENTITY PRIMARY KEY, CREATED DATE, ENTITY_WITH_STRINGY_BIG_DECIMAL INTEGER);
+CREATE TABLE ENTITY_WITH_DIRECTIONS ( ID IDENTITY PRIMARY KEY, DIRECTIONS INTEGER ARRAY);
 
diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-postgres.sql
index 882d8df894..1692e6bcdc 100644
--- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-postgres.sql
+++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-postgres.sql
@@ -1,2 +1,3 @@
 CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id SERIAL PRIMARY KEY, Stringy_number DECIMAL(20,10), DIRECTION INTEGER);
 CREATE TABLE OTHER_ENTITY ( ID SERIAL PRIMARY KEY, CREATED DATE, ENTITY_WITH_STRINGY_BIG_DECIMAL INTEGER);
+CREATE TABLE ENTITY_WITH_DIRECTIONS ( ID SERIAL PRIMARY KEY, DIRECTIONS INTEGER ARRAY);
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java
index a80b6f1aa6..d605dc7307 100644
--- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java
@@ -777,14 +777,35 @@ private Object writeCollection(Iterable<?> value, TypeInformation<?> type) {
 		}
 
 		for (Object o : value) {
-			mapped.add(writeValue(o, component));
+			mapped.add(unwrap(writeValue(o, component)));
 		}
 
 		if (type.getType().isInstance(mapped) || !type.isCollectionLike()) {
 			return mapped;
 		}
 
-		return getConversionService().convert(mapped, type.getType());
+		// if we succeeded converting the members of the collection, we actually ignore the fallback targetType since that
+		// was derived without considering custom conversions.
+		Class<?> targetType = type.getType();
+		if (!mapped.isEmpty()) {
+
+			Class<?> targetComponentType = mapped.get(0).getClass();
+			targetType = Array.newInstance(targetComponentType, 0).getClass();
+		}
+		return getConversionService().convert(mapped, targetType);
+	}
+
+	/**
+	 * Unwraps technology specific wrappers. Custom conversions may choose to return a wrapper class that contains additional information for the technology driver.
+	 * These wrappers can't be used as members of a collection, therefore we may have to unwrap the values.
+	 *
+	 * This method allows technology specific implemenations to provide such an unwrapping mechanism.
+	 *
+	 * @param convertedValue a value that might need unwrapping.
+	 */
+	@Nullable
+	protected Object unwrap(@Nullable Object convertedValue) {
+		return convertedValue;
 	}
 
 	static Predicate<RelationalPersistentProperty> isConstructorArgument(PersistentEntity<?, ?> entity) {

From f7c5eb9ed9179450562036d1758a29551816903a Mon Sep 17 00:00:00 2001
From: Mark Paluch <mark.paluch@broadcom.com>
Date: Wed, 16 Jul 2025 15:24:48 +0200
Subject: [PATCH 19/28] Polishing.

Fix typos. Add since tag.

See #2078
Original pull request: #2081
---
 .../core/convert/MappingJdbcConverter.java    | 38 +++++++++----------
 .../MappingRelationalConverter.java           | 21 +++++++---
 2 files changed, 35 insertions(+), 24 deletions(-)

diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java
index 45245bae1b..3abfaa53c4 100644
--- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java
+++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java
@@ -80,7 +80,7 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements
 	 * {@link #MappingJdbcConverter(RelationalMappingContext, RelationResolver, CustomConversions, JdbcTypeFactory)}
 	 * (MappingContext, RelationResolver, JdbcTypeFactory)} to convert arrays and large objects into JDBC-specific types.
 	 *
-	 * @param context          must not be {@literal null}.
+	 * @param context must not be {@literal null}.
 	 * @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}.
 	 */
 	public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver) {
@@ -98,12 +98,12 @@ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver r
 	/**
 	 * Creates a new {@link MappingJdbcConverter} given {@link MappingContext}.
 	 *
-	 * @param context          must not be {@literal null}.
+	 * @param context must not be {@literal null}.
 	 * @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}.
-	 * @param typeFactory      must not be {@literal null}
+	 * @param typeFactory must not be {@literal null}
 	 */
 	public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver,
-								CustomConversions conversions, JdbcTypeFactory typeFactory) {
+			CustomConversions conversions, JdbcTypeFactory typeFactory) {
 
 		super(context, conversions);
 
@@ -258,18 +258,18 @@ public JdbcValue writeJdbcValue(@Nullable Object value, TypeInformation<?> colum
 
 		if (convertedValue.getClass().isArray()) {
 
-		if (componentType != byte.class && componentType != Byte.class) {
+			if (componentType != byte.class && componentType != Byte.class) {
 
-			Object[] objectArray = requireObjectArray(convertedValue);
-			return JdbcValue.of(typeFactory.createArray(objectArray), JDBCType.ARRAY);
-		}
+				Object[] objectArray = requireObjectArray(convertedValue);
+				return JdbcValue.of(typeFactory.createArray(objectArray), JDBCType.ARRAY);
+			}
 
-		if (componentType == Byte.class) {
-			convertedValue = ArrayUtils.toPrimitive((Byte[]) convertedValue);
-		}
+			if (componentType == Byte.class) {
+				convertedValue = ArrayUtils.toPrimitive((Byte[]) convertedValue);
+			}
 
-		return JdbcValue.of(convertedValue, JDBCType.BINARY);
-	}
+			return JdbcValue.of(convertedValue, JDBCType.BINARY);
+		}
 
 		return JdbcValue.of(convertedValue, sqlType);
 	}
@@ -306,7 +306,7 @@ public <R> R readAndResolve(TypeInformation<R> type, RowDocument source, Identif
 
 	@Override
 	protected RelationalPropertyValueProvider newValueProvider(RowDocumentAccessor documentAccessor,
-															   ValueExpressionEvaluator evaluator, ConversionContext context) {
+			ValueExpressionEvaluator evaluator, ConversionContext context) {
 
 		if (context instanceof ResolvingConversionContext rcc) {
 
@@ -335,7 +335,7 @@ class ResolvingRelationalPropertyValueProvider implements RelationalPropertyValu
 		private final Identifier identifier;
 
 		private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider delegate, RowDocumentAccessor accessor,
-														 ResolvingConversionContext context, Identifier identifier) {
+				ResolvingConversionContext context, Identifier identifier) {
 
 			AggregatePath path = context.aggregatePath();
 
@@ -344,7 +344,7 @@ private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider dele
 			this.context = context;
 			this.identifier = path.isEntity()
 					? potentiallyAppendIdentifier(identifier, path.getRequiredLeafEntity(),
-					property -> delegate.getValue(path.append(property)))
+							property -> delegate.getValue(path.append(property)))
 					: identifier;
 		}
 
@@ -352,7 +352,7 @@ private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider dele
 		 * Conditionally append the identifier if the entity has an identifier property.
 		 */
 		static Identifier potentiallyAppendIdentifier(Identifier base, RelationalPersistentEntity<?> entity,
-													  Function<RelationalPersistentProperty, Object> getter) {
+				Function<RelationalPersistentProperty, Object> getter) {
 
 			if (entity.hasIdProperty()) {
 
@@ -481,7 +481,7 @@ public RelationalPropertyValueProvider withContext(ConversionContext context) {
 
 			return context == this.context ? this
 					: new ResolvingRelationalPropertyValueProvider(delegate.withContext(context), accessor,
-					(ResolvingConversionContext) context, identifier);
+							(ResolvingConversionContext) context, identifier);
 		}
 	}
 
@@ -493,7 +493,7 @@ public RelationalPropertyValueProvider withContext(ConversionContext context) {
 	 * @param identifier
 	 */
 	private record ResolvingConversionContext(ConversionContext delegate, AggregatePath aggregatePath,
-											  Identifier identifier) implements ConversionContext {
+			Identifier identifier) implements ConversionContext {
 
 		@Override
 		public <S> S convert(Object source, TypeInformation<? extends S> typeHint) {
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java
index d605dc7307..463fc33f13 100644
--- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java
@@ -44,7 +44,16 @@
 import org.springframework.data.mapping.PersistentPropertyAccessor;
 import org.springframework.data.mapping.PersistentPropertyPathAccessor;
 import org.springframework.data.mapping.context.MappingContext;
-import org.springframework.data.mapping.model.*;
+import org.springframework.data.mapping.model.CachingValueExpressionEvaluatorFactory;
+import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
+import org.springframework.data.mapping.model.EntityInstantiator;
+import org.springframework.data.mapping.model.ParameterValueProvider;
+import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider;
+import org.springframework.data.mapping.model.PropertyValueProvider;
+import org.springframework.data.mapping.model.SimpleTypeHolder;
+import org.springframework.data.mapping.model.SpELContext;
+import org.springframework.data.mapping.model.ValueExpressionEvaluator;
+import org.springframework.data.mapping.model.ValueExpressionParameterValueProvider;
 import org.springframework.data.projection.EntityProjection;
 import org.springframework.data.projection.EntityProjectionIntrospector;
 import org.springframework.data.projection.EntityProjectionIntrospector.ProjectionPredicate;
@@ -792,16 +801,18 @@ private Object writeCollection(Iterable<?> value, TypeInformation<?> type) {
 			Class<?> targetComponentType = mapped.get(0).getClass();
 			targetType = Array.newInstance(targetComponentType, 0).getClass();
 		}
+
 		return getConversionService().convert(mapped, targetType);
 	}
 
 	/**
-	 * Unwraps technology specific wrappers. Custom conversions may choose to return a wrapper class that contains additional information for the technology driver.
-	 * These wrappers can't be used as members of a collection, therefore we may have to unwrap the values.
-	 *
-	 * This method allows technology specific implemenations to provide such an unwrapping mechanism.
+	 * Unwraps technology-specific wrappers. Custom conversions may choose to return a wrapper class that contains
+	 * additional information for the driver. These wrappers can't be used as members of a collection, therefore we may
+	 * have to unwrap the values. This method allows technology-specific implementations to provide such an unwrapping
+	 * mechanism.
 	 *
 	 * @param convertedValue a value that might need unwrapping.
+	 * @since 3.5.2
 	 */
 	@Nullable
 	protected Object unwrap(@Nullable Object convertedValue) {

From da4033d298a4f18e296496430bd41784b0402fc8 Mon Sep 17 00:00:00 2001
From: Mark Paluch <mark.paluch@broadcom.com>
Date: Thu, 17 Jul 2025 14:00:56 +0200
Subject: [PATCH 20/28] Upgrade to Maven Wrapper 3.9.11.

See #2090
---
 .mvn/wrapper/maven-wrapper.properties | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
index 5af18f05b4..b300b57322 100755
--- a/.mvn/wrapper/maven-wrapper.properties
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -1,2 +1,2 @@
-#Thu Nov 07 09:47:30 CET 2024
-distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
+#Thu Jul 17 14:00:56 CEST 2025
+distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip

From 310303cc86950f325ddb5ab68935a9ce31b91262 Mon Sep 17 00:00:00 2001
From: Mark Paluch <mark.paluch@broadcom.com>
Date: Fri, 18 Jul 2025 10:27:39 +0200
Subject: [PATCH 21/28] Prepare 3.5.2 (2025.0.2).

See #2072
---
 pom.xml                       | 20 ++++----------------
 src/main/resources/notice.txt |  3 ++-
 2 files changed, 6 insertions(+), 17 deletions(-)

diff --git a/pom.xml b/pom.xml
index 346d0dba8e..a058f60b2d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,12 +15,12 @@
 	<parent>
 		<groupId>org.springframework.data.build</groupId>
 		<artifactId>spring-data-parent</artifactId>
-		<version>3.5.2-SNAPSHOT</version>
+		<version>3.5.2</version>
 	</parent>
 
 	<properties>
 		<dist.id>spring-data-jdbc</dist.id>
-		<springdata.commons>3.5.2-SNAPSHOT</springdata.commons>
+		<springdata.commons>3.5.2</springdata.commons>
 		<liquibase.version>4.21.1</liquibase.version>
 		<sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis>
 
@@ -301,20 +301,8 @@
 	</build>
 
 	<repositories>
-		<repository>
-			<id>spring-snapshot</id>
-			<url>https://repo.spring.io/snapshot</url>
-			<snapshots>
-				<enabled>true</enabled>
-			</snapshots>
-			<releases>
-				<enabled>false</enabled>
-			</releases>
-		</repository>
-		<repository>
-			<id>spring-milestone</id>
-			<url>https://repo.spring.io/milestone</url>
-		</repository>
+		
+		
 	</repositories>
 
 </project>
diff --git a/src/main/resources/notice.txt b/src/main/resources/notice.txt
index bce712e7c9..12bccf973a 100644
--- a/src/main/resources/notice.txt
+++ b/src/main/resources/notice.txt
@@ -1,4 +1,4 @@
-Spring Data Relational 3.5.1 (2025.0.1)
+Spring Data Relational 3.5.2 (2025.0.2)
 Copyright (c) [2017-2019] Pivotal Software, Inc.
 
 This product is licensed to you under the Apache License, Version 2.0 (the "License").
@@ -61,5 +61,6 @@ conditions of the subcomponent's license, as noted in the LICENSE file.
 
 
 
+
 
 

From 054aa8498a14ce1b80bf3dbac2706bddf73bea89 Mon Sep 17 00:00:00 2001
From: Mark Paluch <mark.paluch@broadcom.com>
Date: Fri, 18 Jul 2025 10:27:59 +0200
Subject: [PATCH 22/28] Release version 3.5.2 (2025.0.2).

See #2072
---
 pom.xml                               | 2 +-
 spring-data-jdbc-distribution/pom.xml | 2 +-
 spring-data-jdbc/pom.xml              | 4 ++--
 spring-data-r2dbc/pom.xml             | 4 ++--
 spring-data-relational/pom.xml        | 4 ++--
 5 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/pom.xml b/pom.xml
index a058f60b2d..55abb28476 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
 
 	<groupId>org.springframework.data</groupId>
 	<artifactId>spring-data-relational-parent</artifactId>
-	<version>3.5.2-SNAPSHOT</version>
+	<version>3.5.2</version>
 	<packaging>pom</packaging>
 
 	<name>Spring Data Relational Parent</name>
diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml
index 3e9bea453e..9875d679ba 100644
--- a/spring-data-jdbc-distribution/pom.xml
+++ b/spring-data-jdbc-distribution/pom.xml
@@ -14,7 +14,7 @@
 	<parent>
 		<groupId>org.springframework.data</groupId>
 		<artifactId>spring-data-relational-parent</artifactId>
-		<version>3.5.2-SNAPSHOT</version>
+		<version>3.5.2</version>
 		<relativePath>../pom.xml</relativePath>
 	</parent>
 
diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml
index a7d0ffe0aa..29703003ee 100644
--- a/spring-data-jdbc/pom.xml
+++ b/spring-data-jdbc/pom.xml
@@ -6,7 +6,7 @@
 	<modelVersion>4.0.0</modelVersion>
 
 	<artifactId>spring-data-jdbc</artifactId>
-	<version>3.5.2-SNAPSHOT</version>
+	<version>3.5.2</version>
 
 	<name>Spring Data JDBC</name>
 	<description>Spring Data module for JDBC repositories.</description>
@@ -15,7 +15,7 @@
 	<parent>
 		<groupId>org.springframework.data</groupId>
 		<artifactId>spring-data-relational-parent</artifactId>
-		<version>3.5.2-SNAPSHOT</version>
+		<version>3.5.2</version>
 	</parent>
 
 	<properties>
diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml
index e16c7e5735..1c34802a6f 100644
--- a/spring-data-r2dbc/pom.xml
+++ b/spring-data-r2dbc/pom.xml
@@ -6,7 +6,7 @@
 	<modelVersion>4.0.0</modelVersion>
 
 	<artifactId>spring-data-r2dbc</artifactId>
-	<version>3.5.2-SNAPSHOT</version>
+	<version>3.5.2</version>
 
 	<name>Spring Data R2DBC</name>
 	<description>Spring Data module for R2DBC</description>
@@ -15,7 +15,7 @@
 	<parent>
 		<groupId>org.springframework.data</groupId>
 		<artifactId>spring-data-relational-parent</artifactId>
-		<version>3.5.2-SNAPSHOT</version>
+		<version>3.5.2</version>
 	</parent>
 
 	<properties>
diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml
index e44441fb9b..edcb3ba3d5 100644
--- a/spring-data-relational/pom.xml
+++ b/spring-data-relational/pom.xml
@@ -6,7 +6,7 @@
 	<modelVersion>4.0.0</modelVersion>
 
 	<artifactId>spring-data-relational</artifactId>
-	<version>3.5.2-SNAPSHOT</version>
+	<version>3.5.2</version>
 
 	<name>Spring Data Relational</name>
 	<description>Spring Data Relational support</description>
@@ -14,7 +14,7 @@
 	<parent>
 		<groupId>org.springframework.data</groupId>
 		<artifactId>spring-data-relational-parent</artifactId>
-		<version>3.5.2-SNAPSHOT</version>
+		<version>3.5.2</version>
 	</parent>
 
 	<properties>

From 53a20d06596aa04b93e2357bf2d96d1b258e686c Mon Sep 17 00:00:00 2001
From: Mark Paluch <mark.paluch@broadcom.com>
Date: Fri, 18 Jul 2025 10:30:33 +0200
Subject: [PATCH 23/28] Prepare next development iteration.

See #2072
---
 pom.xml                               | 2 +-
 spring-data-jdbc-distribution/pom.xml | 2 +-
 spring-data-jdbc/pom.xml              | 4 ++--
 spring-data-r2dbc/pom.xml             | 4 ++--
 spring-data-relational/pom.xml        | 4 ++--
 5 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/pom.xml b/pom.xml
index 55abb28476..d9685780d2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
 
 	<groupId>org.springframework.data</groupId>
 	<artifactId>spring-data-relational-parent</artifactId>
-	<version>3.5.2</version>
+	<version>3.5.3-SNAPSHOT</version>
 	<packaging>pom</packaging>
 
 	<name>Spring Data Relational Parent</name>
diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml
index 9875d679ba..f051a59589 100644
--- a/spring-data-jdbc-distribution/pom.xml
+++ b/spring-data-jdbc-distribution/pom.xml
@@ -14,7 +14,7 @@
 	<parent>
 		<groupId>org.springframework.data</groupId>
 		<artifactId>spring-data-relational-parent</artifactId>
-		<version>3.5.2</version>
+		<version>3.5.3-SNAPSHOT</version>
 		<relativePath>../pom.xml</relativePath>
 	</parent>
 
diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml
index 29703003ee..7b42c4ed4a 100644
--- a/spring-data-jdbc/pom.xml
+++ b/spring-data-jdbc/pom.xml
@@ -6,7 +6,7 @@
 	<modelVersion>4.0.0</modelVersion>
 
 	<artifactId>spring-data-jdbc</artifactId>
-	<version>3.5.2</version>
+	<version>3.5.3-SNAPSHOT</version>
 
 	<name>Spring Data JDBC</name>
 	<description>Spring Data module for JDBC repositories.</description>
@@ -15,7 +15,7 @@
 	<parent>
 		<groupId>org.springframework.data</groupId>
 		<artifactId>spring-data-relational-parent</artifactId>
-		<version>3.5.2</version>
+		<version>3.5.3-SNAPSHOT</version>
 	</parent>
 
 	<properties>
diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml
index 1c34802a6f..e838426c90 100644
--- a/spring-data-r2dbc/pom.xml
+++ b/spring-data-r2dbc/pom.xml
@@ -6,7 +6,7 @@
 	<modelVersion>4.0.0</modelVersion>
 
 	<artifactId>spring-data-r2dbc</artifactId>
-	<version>3.5.2</version>
+	<version>3.5.3-SNAPSHOT</version>
 
 	<name>Spring Data R2DBC</name>
 	<description>Spring Data module for R2DBC</description>
@@ -15,7 +15,7 @@
 	<parent>
 		<groupId>org.springframework.data</groupId>
 		<artifactId>spring-data-relational-parent</artifactId>
-		<version>3.5.2</version>
+		<version>3.5.3-SNAPSHOT</version>
 	</parent>
 
 	<properties>
diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml
index edcb3ba3d5..58d71f6533 100644
--- a/spring-data-relational/pom.xml
+++ b/spring-data-relational/pom.xml
@@ -6,7 +6,7 @@
 	<modelVersion>4.0.0</modelVersion>
 
 	<artifactId>spring-data-relational</artifactId>
-	<version>3.5.2</version>
+	<version>3.5.3-SNAPSHOT</version>
 
 	<name>Spring Data Relational</name>
 	<description>Spring Data Relational support</description>
@@ -14,7 +14,7 @@
 	<parent>
 		<groupId>org.springframework.data</groupId>
 		<artifactId>spring-data-relational-parent</artifactId>
-		<version>3.5.2</version>
+		<version>3.5.3-SNAPSHOT</version>
 	</parent>
 
 	<properties>

From 9c9161d8834a20ea47b4b704ff2b12f486a187f2 Mon Sep 17 00:00:00 2001
From: Mark Paluch <mark.paluch@broadcom.com>
Date: Fri, 18 Jul 2025 10:30:34 +0200
Subject: [PATCH 24/28] After release cleanups.

See #2072
---
 pom.xml | 20 ++++++++++++++++----
 1 file changed, 16 insertions(+), 4 deletions(-)

diff --git a/pom.xml b/pom.xml
index d9685780d2..d500717228 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,12 +15,12 @@
 	<parent>
 		<groupId>org.springframework.data.build</groupId>
 		<artifactId>spring-data-parent</artifactId>
-		<version>3.5.2</version>
+		<version>3.5.3-SNAPSHOT</version>
 	</parent>
 
 	<properties>
 		<dist.id>spring-data-jdbc</dist.id>
-		<springdata.commons>3.5.2</springdata.commons>
+		<springdata.commons>3.5.3-SNAPSHOT</springdata.commons>
 		<liquibase.version>4.21.1</liquibase.version>
 		<sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis>
 
@@ -301,8 +301,20 @@
 	</build>
 
 	<repositories>
-		
-		
+		<repository>
+			<id>spring-snapshot</id>
+			<url>https://repo.spring.io/snapshot</url>
+			<snapshots>
+				<enabled>true</enabled>
+			</snapshots>
+			<releases>
+				<enabled>false</enabled>
+			</releases>
+		</repository>
+		<repository>
+			<id>spring-milestone</id>
+			<url>https://repo.spring.io/milestone</url>
+		</repository>
 	</repositories>
 
 </project>

From 2ac5ce63972b1ad14d8aac4ad61d8ba2817e933e Mon Sep 17 00:00:00 2001
From: Mark Paluch <mark.paluch@broadcom.com>
Date: Fri, 8 Aug 2025 15:23:16 +0200
Subject: [PATCH 25/28] Upgrade JDBC drivers.

Closes #2105
---
 pom.xml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/pom.xml b/pom.xml
index d500717228..fb30b6799d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -36,10 +36,10 @@
 		<hikari.version>5.1.0</hikari.version>
 		<hsqldb.version>2.7.3</hsqldb.version>
 		<mariadb-java-client.version>3.5.4</mariadb-java-client.version>
-		<mssql.version>12.10.1.jre11</mssql.version>
-		<mysql-connector-java.version>9.3.0</mysql-connector-java.version>
+		<mssql.version>13.1.1.jre11-preview</mssql.version>
+		<mysql-connector-java.version>9.4.0</mysql-connector-java.version>
 		<postgresql.version>42.7.7</postgresql.version>
-		<oracle.version>23.8.0.25.04</oracle.version>
+		<oracle.version>23.9.0.25.07</oracle.version>
 
 		<!-- R2DBC driver dependencies-->
 		<r2dbc-postgresql.version>1.0.7.RELEASE</r2dbc-postgresql.version>

From f860f70545a5c5675cfaa9676c60080cdb19fc5a Mon Sep 17 00:00:00 2001
From: Mark Paluch <mark.paluch@broadcom.com>
Date: Mon, 11 Aug 2025 15:59:27 +0200
Subject: [PATCH 26/28] Remove misleading `@Nullable` annotation from
 `AggregateReference.getId()`.

Aggregate references must be valid on their own and a null reference would represent a broken reference as we consider null identifiers as transient.

Closes #2104
---
 .../data/jdbc/core/mapping/AggregateReference.java  | 13 +++++++++----
 1 file changed, 9 insertions(+), 4 deletions(-)

diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/AggregateReference.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/AggregateReference.java
index a75054588b..08ab0b324b 100644
--- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/AggregateReference.java
+++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/AggregateReference.java
@@ -31,14 +31,21 @@
  */
 public interface AggregateReference<T, ID> {
 
+	/**
+	 * Creates an {@link AggregateReference} that refers to the target aggregate root with the given id.
+	 *
+	 * @param id aggregate identifier. Must not be {@literal null}, can be a simple value.
+	 * @return
+	 * @param <T> target aggregate type.
+	 * @param <ID> target aggregate identifier type.
+	 */
 	static <T, ID> AggregateReference<T, ID> to(ID id) {
 		return new IdOnlyAggregateReference<>(id);
 	}
 
 	/**
-	 * @return the id of the referenced aggregate. May be {@code null}.
+	 * @return the id of the referenced aggregate.
 	 */
-	@Nullable
 	ID getId();
 
 	/**
@@ -55,7 +62,6 @@ class IdOnlyAggregateReference<T, ID> implements AggregateReference<T, ID> {
 		public IdOnlyAggregateReference(ID id) {
 
 			Assert.notNull(id, "Id must not be null");
-
 			this.id = id;
 		}
 
@@ -82,7 +88,6 @@ public int hashCode() {
 
 		@Override
 		public String toString() {
-
 			return "IdOnlyAggregateReference{" + "id=" + id + '}';
 		}
 	}

From e69e3e5b24c1bafa5a1eb4e9ad0500b6e043bd5a Mon Sep 17 00:00:00 2001
From: Mark Paluch <mark.paluch@broadcom.com>
Date: Mon, 11 Aug 2025 16:05:54 +0200
Subject: [PATCH 27/28] Deprecate `IdOnlyAggregateReference`.

Closes #2107
---
 .../data/jdbc/core/mapping/AggregateReference.java             | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/AggregateReference.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/AggregateReference.java
index 08ab0b324b..63c0a79448 100644
--- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/AggregateReference.java
+++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/AggregateReference.java
@@ -54,7 +54,10 @@ static <T, ID> AggregateReference<T, ID> to(ID id) {
 	 *
 	 * @param <T>
 	 * @param <ID>
+	 * @deprecated since 3.5.3, will be converted to a package-private class in 4.0 as this class is an implementation
+	 *             detail and should not be used by application code.
 	 */
+	@Deprecated(since = "3.5.3", forRemoval = true)
 	class IdOnlyAggregateReference<T, ID> implements AggregateReference<T, ID> {
 
 		private final ID id;

From d75c58a9f10a8f9f78e18ad9634c2d97070e3fa0 Mon Sep 17 00:00:00 2001
From: Mark Paluch <mark.paluch@broadcom.com>
Date: Tue, 12 Aug 2025 14:08:32 +0200
Subject: [PATCH 28/28] Apply DTO projection through JDBC's Query by Example.

Spring Data JDBC doesn't allow projections through JdbcAggregateOperations yet and so we need to apply DTO conversion.

Closes #2098
---
 .../FetchableFluentQueryByExample.java        | 19 ++++++---
 .../support/FluentQuerySupport.java           | 28 ++++++++++---
 .../support/SimpleJdbcRepository.java         |  6 ++-
 .../JdbcRepositoryIntegrationTests.java       | 42 +++++++++++++++++--
 4 files changed, 80 insertions(+), 15 deletions(-)

diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FetchableFluentQueryByExample.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FetchableFluentQueryByExample.java
index fe07f07588..54db9be99a 100644
--- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FetchableFluentQueryByExample.java
+++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FetchableFluentQueryByExample.java
@@ -32,6 +32,8 @@
 import org.springframework.data.domain.Sort;
 import org.springframework.data.domain.Window;
 import org.springframework.data.jdbc.core.JdbcAggregateOperations;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.relational.core.conversion.RelationalConverter;
 import org.springframework.data.relational.core.query.Query;
 import org.springframework.data.relational.repository.query.RelationalExampleMapper;
 import org.springframework.util.Assert;
@@ -47,19 +49,26 @@ class FetchableFluentQueryByExample<S, R> extends FluentQuerySupport<S, R> {
 
 	private final RelationalExampleMapper exampleMapper;
 	private final JdbcAggregateOperations entityOperations;
+	private final ProjectionFactory projectionFactory;
+	private final RelationalConverter converter;
 
 	FetchableFluentQueryByExample(Example<S> example, Class<R> resultType, RelationalExampleMapper exampleMapper,
-			JdbcAggregateOperations entityOperations) {
-		this(example, Sort.unsorted(), 0, resultType, Collections.emptyList(), exampleMapper, entityOperations);
+			JdbcAggregateOperations entityOperations, RelationalConverter converter, ProjectionFactory projectionFactory) {
+		this(example, Sort.unsorted(), 0, resultType, Collections.emptyList(), exampleMapper, entityOperations, converter,
+				projectionFactory);
 	}
 
 	FetchableFluentQueryByExample(Example<S> example, Sort sort, int limit, Class<R> resultType,
-			List<String> fieldsToInclude, RelationalExampleMapper exampleMapper, JdbcAggregateOperations entityOperations) {
+			List<String> fieldsToInclude, RelationalExampleMapper exampleMapper, JdbcAggregateOperations entityOperations,
+			RelationalConverter converter,
+			ProjectionFactory projectionFactory) {
 
-		super(example, sort, limit, resultType, fieldsToInclude);
+		super(example, sort, limit, resultType, fieldsToInclude, projectionFactory, converter);
 
 		this.exampleMapper = exampleMapper;
 		this.entityOperations = entityOperations;
+		this.converter = converter;
+		this.projectionFactory = projectionFactory;
 	}
 
 	@Override
@@ -167,6 +176,6 @@ protected <R> FluentQuerySupport<S, R> create(Example<S> example, Sort sort, int
 			List<String> fieldsToInclude) {
 
 		return new FetchableFluentQueryByExample<>(example, sort, limit, resultType, fieldsToInclude, this.exampleMapper,
-				this.entityOperations);
+				this.entityOperations, this.converter, this.projectionFactory);
 	}
 }
diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FluentQuerySupport.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FluentQuerySupport.java
index 1b96b3c668..1c3ea5de08 100644
--- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FluentQuerySupport.java
+++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/FluentQuerySupport.java
@@ -21,9 +21,12 @@
 import java.util.function.Function;
 
 import org.springframework.core.convert.support.DefaultConversionService;
+import org.springframework.data.convert.DtoInstantiatingConverter;
 import org.springframework.data.domain.Example;
 import org.springframework.data.domain.Sort;
-import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
+import org.springframework.data.projection.EntityProjection;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.relational.core.conversion.RelationalConverter;
 import org.springframework.data.repository.query.FluentQuery;
 import org.springframework.util.Assert;
 
@@ -41,16 +44,19 @@ abstract class FluentQuerySupport<S, R> implements FluentQuery.FetchableFluentQu
 	private final int limit;
 	private final Class<R> resultType;
 	private final List<String> fieldsToInclude;
+	private final ProjectionFactory projectionFactory;
+	private final RelationalConverter converter;
 
-	private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
-
-	FluentQuerySupport(Example<S> example, Sort sort, int limit, Class<R> resultType, List<String> fieldsToInclude) {
+	FluentQuerySupport(Example<S> example, Sort sort, int limit, Class<R> resultType, List<String> fieldsToInclude,
+			ProjectionFactory projectionFactory, RelationalConverter converter) {
 
 		this.example = example;
 		this.sort = sort;
 		this.limit = limit;
 		this.resultType = resultType;
 		this.fieldsToInclude = fieldsToInclude;
+		this.projectionFactory = projectionFactory;
+		this.converter = converter;
 	}
 
 	@Override
@@ -118,8 +124,18 @@ private Function<Object, R> getConversionFunction(Class<S> inputType, Class<R> t
 			return (Function<Object, R>) Function.identity();
 		}
 
-		if (targetType.isInterface()) {
-			return o -> projectionFactory.createProjection(targetType, o);
+		EntityProjection<?, ?> entityProjection = converter.introspectProjection(targetType, inputType);
+
+		if (entityProjection.isProjection()) {
+
+			if (targetType.isInterface()) {
+				return o -> projectionFactory.createProjection(targetType, o);
+			}
+
+			DtoInstantiatingConverter dtoConverter = new DtoInstantiatingConverter(targetType, converter.getMappingContext(),
+					converter.getEntityInstantiators());
+
+			return o -> (R) dtoConverter.convert(o);
 		}
 
 		return o -> DefaultConversionService.getSharedInstance().convert(o, targetType);
diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java
index 65eba4b02c..9e2eefaaac 100644
--- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java
+++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/SimpleJdbcRepository.java
@@ -26,6 +26,7 @@
 import org.springframework.data.jdbc.core.JdbcAggregateOperations;
 import org.springframework.data.jdbc.core.convert.JdbcConverter;
 import org.springframework.data.mapping.PersistentEntity;
+import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
 import org.springframework.data.relational.repository.query.RelationalExampleMapper;
 import org.springframework.data.repository.CrudRepository;
 import org.springframework.data.repository.PagingAndSortingRepository;
@@ -48,9 +49,11 @@
 public class SimpleJdbcRepository<T, ID>
 		implements CrudRepository<T, ID>, PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
 
+	private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
 	private final JdbcAggregateOperations entityOperations;
 	private final PersistentEntity<T, ?> entity;
 	private final RelationalExampleMapper exampleMapper;
+	private final JdbcConverter converter;
 
 	public SimpleJdbcRepository(JdbcAggregateOperations entityOperations, PersistentEntity<T, ?> entity,
 			JdbcConverter converter) {
@@ -60,6 +63,7 @@ public SimpleJdbcRepository(JdbcAggregateOperations entityOperations, Persistent
 
 		this.entityOperations = entityOperations;
 		this.entity = entity;
+		this.converter = converter;
 		this.exampleMapper = new RelationalExampleMapper(converter.getMappingContext());
 	}
 
@@ -197,7 +201,7 @@ public <S extends T, R> R findBy(Example<S> example, Function<FluentQuery.Fetcha
 		Assert.notNull(queryFunction, "Query function must not be null");
 
 		FluentQuery.FetchableFluentQuery<S> fluentQuery = new FetchableFluentQueryByExample<>(example,
-				example.getProbeType(), this.exampleMapper, this.entityOperations);
+				example.getProbeType(), this.exampleMapper, this.entityOperations, this.converter, this.projectionFactory);
 
 		return queryFunction.apply(fluentQuery);
 	}
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 7c854b823f..e0cc272d77 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
@@ -42,6 +42,7 @@
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
+
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.config.PropertiesFactoryBean;
 import org.springframework.context.ApplicationListener;
@@ -51,7 +52,16 @@
 import org.springframework.core.io.ClassPathResource;
 import org.springframework.dao.IncorrectResultSizeDataAccessException;
 import org.springframework.data.annotation.Id;
-import org.springframework.data.domain.*;
+import org.springframework.data.domain.Example;
+import org.springframework.data.domain.ExampleMatcher;
+import org.springframework.data.domain.Limit;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.ScrollPosition;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Window;
 import org.springframework.data.jdbc.core.mapping.AggregateReference;
 import org.springframework.data.jdbc.repository.query.Modifying;
 import org.springframework.data.jdbc.repository.query.Query;
@@ -64,8 +74,8 @@
 import org.springframework.data.jdbc.testing.TestDatabaseFeatures;
 import org.springframework.data.relational.core.mapping.Column;
 import org.springframework.data.relational.core.mapping.MappedCollection;
-import org.springframework.data.relational.core.mapping.Table;
 import org.springframework.data.relational.core.mapping.Sequence;
+import org.springframework.data.relational.core.mapping.Table;
 import org.springframework.data.relational.core.mapping.event.AbstractRelationalEvent;
 import org.springframework.data.relational.core.mapping.event.AfterConvertEvent;
 import org.springframework.data.relational.core.sql.LockMode;
@@ -1187,6 +1197,32 @@ void fetchByExampleFluentCountSimple() {
 		assertThat(matches).isEqualTo(2);
 	}
 
+	@Test // GH-2098
+	void projectByExample() {
+
+		String searchName = "Diego";
+		Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS);
+
+		DummyEntity entity = createEntity();
+
+		entity.setName(searchName);
+		entity.setPointInTime(now.minusSeconds(10000));
+		entity = repository.save(entity);
+
+		record DummyProjection(String name) {
+
+		}
+
+		Example<DummyEntity> example = Example.of(createEntity(searchName, it -> it.setBytes(null)));
+
+		DummyProjection projection = repository.findBy(example,
+				p -> p.project("name").as(DummyProjection.class).firstValue());
+		assertThat(projection.name()).isEqualTo(entity.name);
+
+		projection = repository.findBy(example, p -> p.project("flag").as(DummyProjection.class).firstValue());
+		assertThat(projection.name()).isNull();
+	}
+
 	@Test // GH-1192
 	void fetchByExampleFluentOnlyInstantFirstSimple() {
 
@@ -1888,10 +1924,10 @@ public String getName() {
 
 	static class DummyEntity {
 
+		@Id Long idProp;
 		String name;
 		Instant pointInTime;
 		OffsetDateTime offsetDateTime;
-		@Id private Long idProp;
 		boolean flag;
 		AggregateReference<DummyEntity, Long> ref;
 		Direction direction;
