{
// language=sql
@Modifying
@@ -1626,7 +1627,7 @@ INSERT INTO EXPRESSION_SQL_TYPE_PROPAGATION(identifier, enum_class)
void saveWithSpel(@Param("expressionSqlTypePropagation") ExpressionSqlTypePropagation expressionSqlTypePropagation);
}
- interface DummyProjection {
+ public interface DummyProjection {
String getName();
}
@@ -2029,7 +2030,7 @@ public boolean isNew() {
}
}
- static class DummyEntity {
+ public static class DummyEntity {
@Id Long idProp;
String name;
diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/aot/AotFragmentTestConfigurationSupport.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/aot/AotFragmentTestConfigurationSupport.java
new file mode 100644
index 0000000000..1ac0667468
--- /dev/null
+++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/aot/AotFragmentTestConfigurationSupport.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.jdbc.repository.aot;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import org.mockito.Mockito;
+
+import org.springframework.aot.test.generate.TestGenerationContext;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.ListableBeanFactory;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.beans.factory.config.RuntimeBeanReference;
+import org.springframework.beans.factory.support.AbstractBeanDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.DefaultBeanNameGenerator;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.env.Environment;
+import org.springframework.core.env.StandardEnvironment;
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.core.test.tools.TestCompiler;
+import org.springframework.core.type.AnnotationMetadata;
+import org.springframework.data.expression.ValueExpressionParser;
+import org.springframework.data.jdbc.core.JdbcAggregateOperations;
+import org.springframework.data.jdbc.core.convert.MappingJdbcConverter;
+import org.springframework.data.jdbc.core.convert.QueryMappingConfiguration;
+import org.springframework.data.jdbc.core.dialect.JdbcDialect;
+import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
+import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
+import org.springframework.data.jdbc.repository.query.RowMapperFactory;
+import org.springframework.data.jdbc.repository.support.BeanFactoryAwareRowMapperFactory;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
+import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.core.support.RepositoryComposition;
+import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
+import org.springframework.data.repository.core.support.RepositoryFragment;
+import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor;
+import org.springframework.data.repository.query.ValueExpressionDelegate;
+import org.springframework.util.ReflectionUtils;
+
+/**
+ * Test Configuration Support Class for generated AOT Repository Fragments based on a Repository Interface.
+ *
+ * This configuration generates the AOT repository, compiles sources and configures a BeanFactory to contain the AOT
+ * fragment. Additionally, the fragment is exposed through a {@code repositoryInterface} JDK proxy forwarding method
+ * invocations to the backing AOT fragment. Note that {@code repositoryInterface} is not a repository proxy.
+ *
+ * @author Mark Paluch
+ */
+public class AotFragmentTestConfigurationSupport implements BeanFactoryPostProcessor, ApplicationContextAware {
+
+ private final Class> repositoryInterface;
+ private final JdbcDialect dialect;
+ private final boolean registerFragmentFacade;
+ private final TestJdbcAotRepositoryContext> repositoryContext;
+ private ApplicationContext applicationContext;
+
+ public AotFragmentTestConfigurationSupport(Class> repositoryInterface, JdbcDialect dialect, Class> configClass) {
+ this(repositoryInterface, dialect, configClass, true);
+ }
+
+ public AotFragmentTestConfigurationSupport(Class> repositoryInterface, JdbcDialect dialect, Class> configClass,
+ boolean registerFragmentFacade, Class>... additionalFragments) {
+
+ this.repositoryInterface = repositoryInterface;
+ this.dialect = dialect;
+
+ RepositoryComposition composition = RepositoryComposition
+ .of((List) Arrays.stream(additionalFragments).map(RepositoryFragment::structural).toList());
+ this.repositoryContext = new TestJdbcAotRepositoryContext<>(repositoryInterface, composition,
+ new AnnotationRepositoryConfigurationSource(AnnotationMetadata.introspect(configClass),
+ EnableJdbcRepositories.class, new DefaultResourceLoader(), new StandardEnvironment(),
+ Mockito.mock(BeanDefinitionRegistry.class), DefaultBeanNameGenerator.INSTANCE));
+ this.registerFragmentFacade = registerFragmentFacade;
+ }
+
+ @Bean
+ BeanFactoryAwareRowMapperFactory rowMapperFactory(ApplicationContext context,
+ JdbcAggregateOperations aggregateOperations, Optional queryMappingConfiguration) {
+ return new BeanFactoryAwareRowMapperFactory(context, aggregateOperations,
+ queryMappingConfiguration.orElse(QueryMappingConfiguration.EMPTY));
+ }
+
+ @Override
+ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
+
+ TestGenerationContext generationContext = new TestGenerationContext(repositoryInterface);
+
+ repositoryContext.setBeanFactory(beanFactory);
+
+ JdbcRepositoryContributor jdbcRepositoryContributor = new JdbcRepositoryContributor(repositoryContext, dialect,
+ new MappingJdbcConverter(new JdbcMappingContext(), (identifier, path) -> null));
+ jdbcRepositoryContributor.contribute(generationContext);
+
+ AbstractBeanDefinition aotGeneratedRepository = BeanDefinitionBuilder
+ .genericBeanDefinition(repositoryInterface.getName() + "Impl__AotRepository")
+ .addConstructorArgValue(new RuntimeBeanReference(JdbcAggregateOperations.class))
+ .addConstructorArgValue(new RuntimeBeanReference(RowMapperFactory.class))
+ .addConstructorArgValue(
+ getCreationContext(repositoryContext, beanFactory.getBean(Environment.class), beanFactory))
+ .getBeanDefinition();
+
+ generationContext.writeGeneratedContent();
+
+ TestCompiler.forSystem().withCompilerOptions("-parameters").with(generationContext).compile(compiled -> {
+ beanFactory.setBeanClassLoader(compiled.getClassLoader());
+ ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragment", aotGeneratedRepository);
+ });
+
+ if (registerFragmentFacade) {
+
+ BeanDefinition fragmentFacade = BeanDefinitionBuilder.rootBeanDefinition((Class) repositoryInterface, () -> {
+
+ Object fragment = beanFactory.getBean("fragment");
+ Object proxy = getFragmentFacadeProxy(fragment);
+
+ return repositoryInterface.cast(proxy);
+ }).getBeanDefinition();
+ ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragmentFacade", fragmentFacade);
+ }
+ }
+
+ private Object getFragmentFacadeProxy(Object fragment) {
+
+ return Proxy.newProxyInstance(repositoryInterface.getClassLoader(), new Class>[] { repositoryInterface },
+ (p, method, args) -> {
+
+ Method target = ReflectionUtils.findMethod(fragment.getClass(), method.getName(), method.getParameterTypes());
+
+ if (target == null) {
+ throw new NoSuchMethodException("Method [%s] is not implemented by [%s]".formatted(method, target));
+ }
+
+ try {
+ return target.invoke(fragment, args);
+ } catch (ReflectiveOperationException e) {
+ ReflectionUtils.handleReflectionException(e);
+ }
+
+ return null;
+ });
+ }
+
+ private RepositoryFactoryBeanSupport.FragmentCreationContext getCreationContext(
+ TestJdbcAotRepositoryContext> repositoryContext, Environment environment, ListableBeanFactory beanFactory) {
+
+ RepositoryFactoryBeanSupport.FragmentCreationContext creationContext = new RepositoryFactoryBeanSupport.FragmentCreationContext() {
+ @Override
+ public RepositoryMetadata getRepositoryMetadata() {
+ return repositoryContext.getRepositoryInformation();
+ }
+
+ @Override
+ public ValueExpressionDelegate getValueExpressionDelegate() {
+
+ QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor(environment,
+ beanFactory);
+ return new ValueExpressionDelegate(accessor, ValueExpressionParser.create());
+ }
+
+ @Override
+ public ProjectionFactory getProjectionFactory() {
+ return new SpelAwareProxyProjectionFactory();
+ }
+ };
+
+ return creationContext;
+ }
+
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+ this.applicationContext = applicationContext;
+ }
+}
diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/aot/JdbcRepositoryContributorIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/aot/JdbcRepositoryContributorIntegrationTests.java
new file mode 100644
index 0000000000..a894545705
--- /dev/null
+++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/aot/JdbcRepositoryContributorIntegrationTests.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.jdbc.repository.aot;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.FilterType;
+import org.springframework.context.annotation.Import;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.jdbc.core.JdbcAggregateOperations;
+import org.springframework.data.jdbc.core.dialect.JdbcH2Dialect;
+import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
+import org.springframework.data.jdbc.testing.DatabaseType;
+import org.springframework.data.jdbc.testing.EnabledOnDatabase;
+import org.springframework.data.jdbc.testing.IntegrationTest;
+import org.springframework.data.jdbc.testing.TestClass;
+import org.springframework.data.jdbc.testing.TestConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+/**
+ * Integration tests for AOT processing via {@link JdbcRepositoryContributor}.
+ *
+ * @author Mark Paluch
+ */
+@SpringJUnitConfig(classes = JdbcRepositoryContributorIntegrationTests.JdbcRepositoryContributorConfiguration.class)
+@IntegrationTest
+@EnabledOnDatabase(DatabaseType.H2)
+class JdbcRepositoryContributorIntegrationTests {
+
+ @Autowired UserRepository fragment;
+ @Autowired JdbcAggregateOperations operations;
+
+ @Configuration
+ @EnableJdbcRepositories(jdbcAggregateOperationsRef = "jdbcAggregateOperations", considerNestedRepositories = true,
+ includeFilters = { @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = String.class) })
+ @Import(TestConfiguration.class)
+ static class JdbcRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport {
+
+ public JdbcRepositoryContributorConfiguration() {
+ super(UserRepository.class, JdbcH2Dialect.INSTANCE, JdbcRepositoryContributorConfiguration.class);
+ }
+
+ @Bean
+ TestClass testClass() {
+ return TestClass.of(JdbcRepositoryContributorIntegrationTests.class);
+ }
+
+ @Bean
+ MyRowMapper myRowMapper() {
+ return new MyRowMapper();
+ }
+
+ @Bean
+ SimpleResultSetExtractor simpleResultSetExtractor() {
+ return new SimpleResultSetExtractor();
+ }
+
+ }
+
+ @BeforeEach
+ void beforeEach() {
+
+ operations.deleteAll(User.class);
+
+ operations.insert(new User("Walter", 52));
+ operations.insert(new User("Skyler", 40));
+ operations.insert(new User("Flynn", 16));
+ operations.insert(new User("Mike", 62));
+ operations.insert(new User("Gustavo", 51));
+ operations.insert(new User("Hector", 83));
+ }
+
+ @Test // GH-2121
+ void shouldFindByFirstname() {
+
+ User walter = fragment.findByFirstname("Walter");
+
+ assertThat(walter).isNotNull();
+ assertThat(walter.getFirstname()).isEqualTo("Walter");
+ }
+
+ @Test // GH-2121
+ void shouldFindOptionalByFirstname() {
+
+ assertThat(fragment.findOptionalByFirstname("Walter")).isPresent();
+ assertThat(fragment.findOptionalByFirstname("Hank")).isEmpty();
+ }
+
+ @Test // GH-2121
+ void shouldFindByFirstnameLike() {
+
+ User walter = fragment.findByFirstnameLike("%alter");
+
+ assertThat(walter).isNotNull();
+ assertThat(walter.getFirstname()).isEqualTo("Walter");
+ }
+
+ @Test // GH-2121
+ void shouldFindByFirstnameStartingWith() {
+
+ User walter = fragment.findByFirstnameStartingWith("Wa");
+
+ assertThat(walter).isNotNull();
+ assertThat(walter.getFirstname()).isEqualTo("Walter");
+
+ walter = fragment.findByFirstnameStartingWith("Wa%");
+
+ assertThat(walter).isNull(); // % is escaped
+ }
+
+ @Test // GH-2121
+ void shouldFindByFirstnameEndingWith() {
+
+ User walter = fragment.findByFirstnameEndingWith("lter");
+
+ assertThat(walter).isNotNull();
+ assertThat(walter.getFirstname()).isEqualTo("Walter");
+
+ walter = fragment.findByFirstnameEndingWith("$lter");
+
+ assertThat(walter).isNull(); // % is escaped
+ }
+
+ @Test // GH-2121
+ void shouldFindBetween() {
+
+ List users = fragment.findAllByAgeBetween(40, 51);
+
+ assertThat(users).hasSize(2);
+ }
+
+ @Test // GH-2121
+ void streamByAgeGreaterThan() {
+ assertThat(fragment.streamByAgeGreaterThan(20)).hasSize(5);
+ }
+
+ @Test // GH-2121
+ void shouldReturnSlice() {
+
+ Slice slice = fragment.findSliceByAgeGreaterThan(Pageable.ofSize(4), 10);
+
+ assertThat(slice).hasSize(4);
+
+ assertThat(slice.hasNext()).isTrue();
+ slice = fragment.findSliceByAgeGreaterThan(Pageable.ofSize(6), 10);
+
+ assertThat(slice).hasSize(6);
+ assertThat(slice.hasNext()).isFalse();
+ }
+
+ @Test // GH-2121
+ void shouldReturnPage() {
+
+ Page page = fragment.findPageByAgeGreaterThan(PageRequest.of(0, 4, Sort.by("age")), 10);
+
+ assertThat(page).hasSize(4);
+
+ assertThat(page.hasNext()).isTrue();
+ page = fragment.findPageByAgeGreaterThan(page.nextPageable(), 10);
+
+ assertThat(page).hasSize(2);
+ assertThat(page.hasNext()).isFalse();
+ }
+
+ @Test // GH-2121
+ void countByAgeLessThan() {
+
+ long count = fragment.countByAgeLessThan(20);
+
+ assertThat(count).isOne();
+ }
+
+ @Test // GH-2121
+ void countShortByAgeLessThan() {
+
+ short count = fragment.countShortByAgeLessThan(20);
+
+ assertThat(count).isOne();
+ }
+
+ @Test // GH-2121
+ void existsByAgeLessThan() {
+
+ assertThat(fragment.existsByAgeLessThan(20)).isTrue();
+ assertThat(fragment.existsByAgeLessThan(5)).isFalse();
+ }
+
+ @Test // GH-2121
+ void listWithLimit() {
+
+ List users = fragment.findTop5ByOrderByAge();
+
+ assertThat(users).hasSize(5).extracting(User::getFirstname).containsSequence("Flynn", "Skyler", "Gustavo", "Walter",
+ "Mike");
+ }
+
+ @Test // GH-2121
+ void shouldFindAnnotatedByFirstname() {
+
+ User walter = fragment.findByFirstnameAnnotated("Walter");
+
+ assertThat(walter).isNotNull();
+ assertThat(walter.getFirstname()).isEqualTo("Walter");
+ }
+
+ @Test // GH-2121
+ void shouldFindAnnotatedByFirstnameExpression() {
+
+ User walter = fragment.findByFirstnameExpression("Walter");
+
+ assertThat(walter).isNotNull();
+ assertThat(walter.getFirstname()).isEqualTo("Walter");
+ }
+
+ @Test // GH-2121
+ void shouldFindUsingRowMapper() {
+
+ User walter = fragment.findUsingRowMapper("Walter");
+
+ assertThat(walter).isNotNull();
+ assertThat(walter.getFirstname()).isEqualTo("Row: 0");
+ }
+
+ @Test // GH-2121
+ void shouldFindUsingRowMapperRef() {
+
+ User walter = fragment.findUsingRowMapperRef("Walter");
+
+ assertThat(walter).isNotNull();
+ assertThat(walter.getFirstname()).isEqualTo("Row: 0");
+ }
+
+ @Test // GH-2121
+ void shouldFindUsingResultSetExtractor() {
+
+ int result = fragment.findUsingAndResultSetExtractor("Walter");
+
+ assertThat(result).isOne();
+ }
+
+ @Test // GH-2121
+ void shouldFindUsingResultSetExtractorRef() {
+
+ int result = fragment.findUsingAndResultSetExtractorRef("Walter");
+
+ assertThat(result).isOne();
+ }
+
+ @Test // GH-2121
+ void shouldProjectOneToDto() {
+
+ UserDto dto = fragment.findOneDtoByFirstname("Walter");
+
+ assertThat(dto).isNotNull();
+ assertThat(dto.firstname()).isEqualTo("Walter");
+ }
+
+ @Test // GH-2121
+ void shouldProjectListToDto() {
+
+ List dtos = fragment.findDtoByFirstname("Walter");
+
+ assertThat(dtos).hasSize(1).extracting(UserDto::firstname).containsOnly("Walter");
+ }
+
+ @Test // GH-2121
+ void shouldProjectOneToInterface() {
+
+ UserProjection projection = fragment.findOneInterfaceByFirstname("Walter");
+
+ assertThat(projection).isNotNull();
+ assertThat(projection.getFirstname()).isEqualTo("Walter");
+ }
+
+ @Test // GH-2121
+ void shouldProjectListToInterface() {
+
+ List projections = fragment.findInterfaceByFirstname("Walter");
+
+ assertThat(projections).hasSize(1).extracting(UserProjection::getFirstname).containsOnly("Walter");
+ }
+
+ @Test // GH-2121
+ void shouldProjectDynamically() {
+
+ List dtos = fragment.findDynamicProjectionByFirstname("Walter", UserDto.class);
+ assertThat(dtos).hasSize(1).extracting(UserDto::firstname).containsOnly("Walter");
+
+ List projections = fragment.findDynamicProjectionByFirstname("Walter", UserProjection.class);
+ assertThat(projections).hasSize(1).extracting(UserProjection::getFirstname).containsOnly("Walter");
+ }
+
+ @Test // GH-2121
+ void shouldDeleteByName() {
+
+ assertThat(fragment.deleteByFirstname("Walter")).isTrue();
+ assertThat(fragment.deleteByFirstname("Walter")).isFalse();
+ }
+
+ @Test // GH-2121
+ void shouldDeleteCountByName() {
+
+ assertThat(fragment.deleteCountByFirstname("Walter")).isOne();
+ assertThat(fragment.deleteCountByFirstname("Walter")).isZero();
+ }
+
+ @Test // GH-2121
+ void shouldDeleteAnnotated() {
+
+ assertThat(fragment.deleteAnnotatedQuery("Walter")).isOne();
+ assertThat(fragment.deleteAnnotatedQuery("Walter")).isZero();
+ }
+
+ @Test // GH-2121
+ void shouldDeleteWithoutResult() {
+
+ fragment.deleteWithoutResult("Walter");
+
+ assertThat(fragment.findByFirstname("Walter")).isNull();
+ }
+
+ @Test // GH-2121
+ void shouldDeleteAndReturnByName() {
+
+ assertThat(fragment.deleteOneByFirstname("Walter")).isNotNull();
+ assertThat(fragment.deleteOneByFirstname("Walter")).isNull();
+ }
+
+}
diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/aot/JdbcRepositoryMetadataIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/aot/JdbcRepositoryMetadataIntegrationTests.java
new file mode 100644
index 0000000000..f1b0c66003
--- /dev/null
+++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/aot/JdbcRepositoryMetadataIntegrationTests.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.jdbc.repository.aot;
+
+import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*;
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.FilterType;
+import org.springframework.context.support.AbstractApplicationContext;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
+import org.springframework.data.jdbc.core.JdbcAggregateOperations;
+import org.springframework.data.jdbc.core.JdbcAggregateTemplate;
+import org.springframework.data.jdbc.core.convert.DataAccessStrategy;
+import org.springframework.data.jdbc.core.convert.Identifier;
+import org.springframework.data.jdbc.core.convert.JdbcConverter;
+import org.springframework.data.jdbc.core.convert.MappingJdbcConverter;
+import org.springframework.data.jdbc.core.convert.RelationResolver;
+import org.springframework.data.jdbc.core.dialect.JdbcH2Dialect;
+import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
+import org.springframework.data.mapping.PersistentPropertyPath;
+import org.springframework.data.relational.core.mapping.RelationalMappingContext;
+import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+/**
+ * Integration tests for the {@link UserRepository} JSON metadata via {@link JdbcRepositoryContributor}.
+ *
+ * @author Mark Paluch
+ */
+@SpringJUnitConfig(classes = JdbcRepositoryMetadataIntegrationTests.JdbcRepositoryContributorConfiguration.class)
+class JdbcRepositoryMetadataIntegrationTests {
+
+ @Autowired AbstractApplicationContext context;
+
+ @Configuration
+ @EnableJdbcRepositories(considerNestedRepositories = true,
+ includeFilters = { @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = String.class) })
+ static class JdbcRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport {
+ public JdbcRepositoryContributorConfiguration() {
+ super(UserRepository.class, JdbcH2Dialect.INSTANCE, JdbcRepositoryContributorConfiguration.class);
+ }
+
+ @Bean
+ RelationalMappingContext mappingContext() {
+ return new RelationalMappingContext();
+ }
+
+ @Bean
+ JdbcConverter converter(RelationalMappingContext mappingContext) {
+ return new MappingJdbcConverter(mappingContext, new RelationResolver() {
+ @Override
+ public Iterable