tech.ydb.test
ydb-junit5-support
diff --git a/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/YdbQueryMethod.java b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/YdbQueryMethod.java
new file mode 100644
index 0000000..a27fca5
--- /dev/null
+++ b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/YdbQueryMethod.java
@@ -0,0 +1,36 @@
+package tech.ydb.data.core;
+
+import java.lang.reflect.Method;
+
+import org.springframework.data.jdbc.repository.query.JdbcQueryMethod;
+import org.springframework.data.mapping.context.MappingContext;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
+import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
+import org.springframework.data.repository.core.NamedQueries;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.query.Parameters;
+import org.springframework.data.repository.query.ParametersSource;
+
+/**
+ * Custom {@link JdbcQueryMethod} implementation specific to YDB.
+ *
+ * @author Mikhail Polivakha
+ */
+public class YdbQueryMethod extends JdbcQueryMethod {
+
+ public YdbQueryMethod(
+ Method method,
+ RepositoryMetadata metadata,
+ ProjectionFactory factory,
+ NamedQueries namedQueries,
+ MappingContext extends RelationalPersistentEntity>, ? extends RelationalPersistentProperty> mappingContext
+ ) {
+ super(method, metadata, factory, namedQueries, mappingContext);
+ }
+
+ @Override
+ protected Parameters, ?> createParameters(ParametersSource parametersSource) {
+ return new YdbQueryParameters(parametersSource);
+ }
+}
diff --git a/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/YdbQueryParameters.java b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/YdbQueryParameters.java
new file mode 100644
index 0000000..557364b
--- /dev/null
+++ b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/YdbQueryParameters.java
@@ -0,0 +1,60 @@
+package tech.ydb.data.core;
+
+import java.lang.annotation.Annotation;
+import java.sql.SQLType;
+
+import org.springframework.core.MethodParameter;
+import org.springframework.data.jdbc.repository.query.JdbcParameter;
+import org.springframework.data.jdbc.repository.query.JdbcParameters;
+import org.springframework.data.relational.repository.query.RelationalParameters;
+import org.springframework.data.repository.query.ParametersSource;
+import org.springframework.data.util.Lazy;
+import org.springframework.data.util.TypeInformation;
+
+import tech.ydb.data.core.convert.YQLType;
+import tech.ydb.data.core.convert.YdbType;
+import tech.ydb.table.values.PrimitiveType;
+
+/**
+ * Parameters of the YDB query. Custom implementation of {@link JdbcParameters}, emerged from requirement
+ * to support {@link YdbType} on method parameters:
+ *
+ *
+ * @Query("SELECT * FROM my_table WHERE type = :type")
+ * Optional findByType(@Param("type") @YdbType("Text") String type);
+ *
+ *
+ * @author Mikhail Polivakha
+ */
+public class YdbQueryParameters extends JdbcParameters {
+
+ public YdbQueryParameters(ParametersSource parametersSource) {
+ super(parametersSource, methodParameter -> {
+
+ YdbType ydbType = methodParameter.getParameterAnnotation(YdbType.class);
+
+ if (ydbType != null) {
+ YQLType sqlType = new YQLType(PrimitiveType.valueOf(ydbType.value()));
+ return new YdbMethodParameter(
+ methodParameter,
+ parametersSource.getDomainTypeInformation(),
+ sqlType,
+ Lazy.of(() -> sqlType) // TODO: Do we support the array/collection-like types?
+ );
+ } else {
+ return new YdbMethodParameter(methodParameter, parametersSource.getDomainTypeInformation());
+ }
+ });
+ }
+
+ static class YdbMethodParameter extends JdbcParameter {
+
+ public YdbMethodParameter(MethodParameter parameter, TypeInformation> domainType) {
+ super(parameter, domainType);
+ }
+
+ public YdbMethodParameter(MethodParameter parameter, TypeInformation> domainType, SQLType sqlType, Lazy actualSqlType) {
+ super(parameter, domainType, sqlType, actualSqlType);
+ }
+ }
+}
diff --git a/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YdbType.java b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YdbType.java
index 1bdeafc..8e4ada6 100644
--- a/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YdbType.java
+++ b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YdbType.java
@@ -5,11 +5,14 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import tech.ydb.table.values.PrimitiveType;
+
/**
* @author Madiyar Nurgazin
+ * @author Mikhail Polivakha
*/
@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.FIELD)
+@Target({ElementType.FIELD, ElementType.PARAMETER})
public @interface YdbType {
String value();
}
diff --git a/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/repository/support/YdbRepositoryFactory.java b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/repository/support/YdbRepositoryFactory.java
new file mode 100644
index 0000000..b2d4352
--- /dev/null
+++ b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/repository/support/YdbRepositoryFactory.java
@@ -0,0 +1,131 @@
+package tech.ydb.data.repository.support;
+
+import java.lang.reflect.Method;
+import java.util.Optional;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.data.jdbc.core.convert.DataAccessStrategy;
+import org.springframework.data.jdbc.core.convert.JdbcConverter;
+import org.springframework.data.jdbc.repository.QueryMappingConfiguration;
+import org.springframework.data.jdbc.repository.query.JdbcQueryMethod;
+import org.springframework.data.jdbc.repository.query.PartTreeJdbcQuery;
+import org.springframework.data.jdbc.repository.query.RowMapperFactory;
+import org.springframework.data.jdbc.repository.query.StringBasedJdbcQuery;
+import org.springframework.data.jdbc.repository.support.BeanFactoryAwareRowMapperFactory;
+import org.springframework.data.jdbc.repository.support.JdbcQueryLookupStrategy;
+import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
+import org.springframework.data.mapping.callback.EntityCallbacks;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.relational.core.dialect.Dialect;
+import org.springframework.data.relational.core.mapping.RelationalMappingContext;
+import org.springframework.data.repository.core.NamedQueries;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.query.CachingValueExpressionDelegate;
+import org.springframework.data.repository.query.QueryLookupStrategy;
+import org.springframework.data.repository.query.RepositoryQuery;
+import org.springframework.data.repository.query.ValueExpressionDelegate;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
+import org.springframework.lang.Nullable;
+
+import tech.ydb.data.core.YdbQueryMethod;
+
+/**
+ * Custom {@link JdbcRepositoryFactory repository factory} to allow for query tuning.
+ *
+ * @author Mikhail Polivakha
+ */
+public class YdbRepositoryFactory extends JdbcRepositoryFactory {
+
+ private final EntityCallbacks entityCallbacks;
+ private final QueryMappingConfiguration queryMappingConfiguration;
+
+ private static final Log LOG = LogFactory.getLog(JdbcQueryLookupStrategy.class);
+
+ /**
+ * Creates a new {@link JdbcRepositoryFactory} for the given {@link DataAccessStrategy},
+ * {@link RelationalMappingContext} and {@link ApplicationEventPublisher}.
+ *
+ * @param dataAccessStrategy must not be {@literal null}.
+ * @param context must not be {@literal null}.
+ * @param converter must not be {@literal null}.
+ * @param dialect must not be {@literal null}.
+ * @param publisher must not be {@literal null}.
+ * @param operations must not be {@literal null}.
+ */
+ public YdbRepositoryFactory(
+ DataAccessStrategy dataAccessStrategy,
+ RelationalMappingContext context,
+ JdbcConverter converter,
+ Dialect dialect,
+ ApplicationEventPublisher publisher,
+ NamedParameterJdbcOperations operations,
+ EntityCallbacks entityCallbacks,
+ QueryMappingConfiguration queryMappingConfiguration
+ ) {
+ super(dataAccessStrategy, context, converter, dialect, publisher, operations);
+
+ this.entityCallbacks = entityCallbacks;
+ this.queryMappingConfiguration = queryMappingConfiguration;
+ }
+
+ @Override
+ protected Optional getQueryLookupStrategy(QueryLookupStrategy.Key key, ValueExpressionDelegate valueExpressionDelegate) {
+ return Optional.of(
+ new YdbQueryLookupStrategy(
+ publisher,
+ entityCallbacks,
+ context,
+ converter,
+ dialect,
+ beanFactory,
+ queryMappingConfiguration,
+ operations,
+ new CachingValueExpressionDelegate(valueExpressionDelegate)
+ )
+ );
+ }
+
+ static class YdbQueryLookupStrategy extends JdbcQueryLookupStrategy {
+
+ private final RowMapperFactory rowMapperFactory;
+
+ public YdbQueryLookupStrategy(
+ ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks,
+ RelationalMappingContext context, JdbcConverter converter, Dialect dialect, BeanFactory beanFactory,
+ QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations,
+ ValueExpressionDelegate delegate
+ ) {
+ super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations, delegate);
+
+ this.rowMapperFactory = new BeanFactoryAwareRowMapperFactory(context, converter, queryMappingConfiguration, callbacks, publisher, beanFactory);
+ }
+
+ @Override
+ public RepositoryQuery resolveQuery(
+ Method method,
+ RepositoryMetadata repositoryMetadata,
+ ProjectionFactory projectionFactory,
+ NamedQueries namedQueries
+ ) {
+ JdbcQueryMethod queryMethod = new YdbQueryMethod(method, repositoryMetadata, projectionFactory, namedQueries, getMappingContext());
+
+ if (namedQueries.hasQuery(queryMethod.getNamedQueryName()) || queryMethod.hasAnnotatedQuery()) {
+
+ if (queryMethod.hasAnnotatedQuery() && queryMethod.hasAnnotatedQueryName()) {
+ LOG.warn(String.format(
+ "Query method %s is annotated with both, a query and a query name; Using the declared query", method));
+ }
+
+ String queryString = evaluateTableExpressions(repositoryMetadata, queryMethod.getRequiredQuery());
+
+ return new StringBasedJdbcQuery(queryString, queryMethod, getOperations(), rowMapperFactory, getConverter(),
+ delegate);
+ } else {
+ return new PartTreeJdbcQuery(getMappingContext(), queryMethod, getDialect(), getConverter(), getOperations(), rowMapperFactory);
+ }
+ }
+ }
+}
diff --git a/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/repository/support/YdbRepositoryFactoryBean.java b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/repository/support/YdbRepositoryFactoryBean.java
new file mode 100644
index 0000000..8f7d405
--- /dev/null
+++ b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/repository/support/YdbRepositoryFactoryBean.java
@@ -0,0 +1,38 @@
+package tech.ydb.data.repository.support;
+
+import java.io.Serializable;
+
+import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactoryBean;
+import org.springframework.data.repository.Repository;
+import org.springframework.data.repository.core.support.RepositoryFactorySupport;
+
+/**
+ * Custom implementation, specific for YDB Spring Data repositories.
+ *
+ * @author Mikhail Polivakha
+ */
+public class YdbRepositoryFactoryBean, S, ID extends Serializable> extends JdbcRepositoryFactoryBean {
+
+ /**
+ * Creates a new {@link JdbcRepositoryFactoryBean} for the given repository interface.
+ *
+ * @param repositoryInterface must not be {@literal null}.
+ */
+ public YdbRepositoryFactoryBean(Class extends T> repositoryInterface) {
+ super(repositoryInterface);
+ }
+
+ @Override
+ protected RepositoryFactorySupport doCreateRepositoryFactory() {
+ return new YdbRepositoryFactory(
+ dataAccessStrategy,
+ mappingContext,
+ converter,
+ dialect,
+ publisher,
+ operations,
+ entityCallbacks,
+ queryMappingConfiguration
+ );
+ }
+}
diff --git a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/YdbBaseTest.java b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/YdbBaseTest.java
index 93f0a8e..031db0c 100644
--- a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/YdbBaseTest.java
+++ b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/YdbBaseTest.java
@@ -1,5 +1,6 @@
package tech.ydb.data;
+import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.boot.test.autoconfigure.data.jdbc.AutoConfigureDataJdbc;
import org.springframework.boot.test.context.SpringBootTest;
diff --git a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/YdbJdbcConfiguration.java b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/YdbJdbcConfiguration.java
index 6f0bca8..d4be6ff 100644
--- a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/YdbJdbcConfiguration.java
+++ b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/YdbJdbcConfiguration.java
@@ -4,7 +4,9 @@
import org.springframework.context.annotation.Import;
import org.springframework.data.jdbc.repository.config.EnableJdbcAuditing;
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
+
import tech.ydb.data.repository.config.AbstractYdbJdbcConfiguration;
+import tech.ydb.data.repository.support.YdbRepositoryFactoryBean;
/**
* @author Madiyar Nurgazin
@@ -13,8 +15,11 @@
@Configuration
@EnableJdbcRepositories(
considerNestedRepositories = true,
- basePackages = "tech.ydb.data"
+ basePackages = "tech.ydb.data",
+ repositoryFactoryBeanClass = YdbRepositoryFactoryBean.class
)
@EnableJdbcAuditing
@Import(AbstractYdbJdbcConfiguration.class)
-public class YdbJdbcConfiguration {}
\ No newline at end of file
+public class YdbJdbcConfiguration {
+
+}
diff --git a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/repository/AuthorRepository.java b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/all_types_table/repository/AuthorRepository.java
similarity index 93%
rename from spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/repository/AuthorRepository.java
rename to spring-data-jdbc-ydb/src/test/java/tech/ydb/data/all_types_table/repository/AuthorRepository.java
index 86582a4..cb31c60 100644
--- a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/repository/AuthorRepository.java
+++ b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/all_types_table/repository/AuthorRepository.java
@@ -1,4 +1,4 @@
-package tech.ydb.data.books.repository;
+package tech.ydb.data.all_types_table.repository;
import java.util.List;
import org.springframework.data.jdbc.repository.query.Query;
diff --git a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/repository/BookRepository.java b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/all_types_table/repository/BookRepository.java
similarity index 94%
rename from spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/repository/BookRepository.java
rename to spring-data-jdbc-ydb/src/test/java/tech/ydb/data/all_types_table/repository/BookRepository.java
index e35c163..5e44292 100644
--- a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/repository/BookRepository.java
+++ b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/all_types_table/repository/BookRepository.java
@@ -1,4 +1,4 @@
-package tech.ydb.data.books.repository;
+package tech.ydb.data.all_types_table.repository;
import java.util.List;
import java.util.Optional;
diff --git a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/repository/ReviewRepository.java b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/all_types_table/repository/ReviewRepository.java
similarity index 91%
rename from spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/repository/ReviewRepository.java
rename to spring-data-jdbc-ydb/src/test/java/tech/ydb/data/all_types_table/repository/ReviewRepository.java
index dae7191..cc835f8 100644
--- a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/repository/ReviewRepository.java
+++ b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/all_types_table/repository/ReviewRepository.java
@@ -1,4 +1,4 @@
-package tech.ydb.data.books.repository;
+package tech.ydb.data.all_types_table.repository;
import java.util.List;
import org.springframework.data.domain.Pageable;
diff --git a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/RepositoriesIntegrationTest.java b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/RepositoriesIntegrationTest.java
index 1608a7f..9e57e3f 100644
--- a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/RepositoriesIntegrationTest.java
+++ b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/RepositoriesIntegrationTest.java
@@ -14,9 +14,9 @@
import tech.ydb.data.books.entity.Book;
import tech.ydb.data.books.entity.BookAuthor;
import tech.ydb.data.books.entity.Review;
-import tech.ydb.data.books.repository.AuthorRepository;
-import tech.ydb.data.books.repository.BookRepository;
-import tech.ydb.data.books.repository.ReviewRepository;
+import tech.ydb.data.all_types_table.repository.AuthorRepository;
+import tech.ydb.data.all_types_table.repository.BookRepository;
+import tech.ydb.data.all_types_table.repository.ReviewRepository;
/**
* @author Madiyar Nurgazin
diff --git a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/TypesCustomizationIntegrationTest.java b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/TypesCustomizationIntegrationTest.java
new file mode 100644
index 0000000..c352a79
--- /dev/null
+++ b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/TypesCustomizationIntegrationTest.java
@@ -0,0 +1,56 @@
+package tech.ydb.data.books;
+
+import java.util.Optional;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.jdbc.repository.query.Query;
+import org.springframework.data.relational.core.mapping.Table;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.data.repository.query.Param;
+import org.springframework.jdbc.UncategorizedSQLException;
+
+import tech.ydb.data.YdbBaseTest;
+import tech.ydb.data.core.convert.YdbType;
+
+public class TypesCustomizationIntegrationTest extends YdbBaseTest {
+
+ @Autowired
+ private SimpleEntityRepository simpleEntityRepository;
+
+ @Test
+ void testCustomTypes() {
+ Assertions
+ .assertThatThrownBy(() -> simpleEntityRepository.findByWrongType(12))
+ .isInstanceOf(UncategorizedSQLException.class)
+ .hasMessageContaining("Cannot cast [class java.lang.Integer: 12] to [Uuid]");
+
+ Assertions
+ .assertThatCode(() -> simpleEntityRepository.findBySpecificType(12))
+ .doesNotThrowAnyException();
+ }
+
+ interface SimpleEntityRepository extends CrudRepository {
+
+ //language=sql
+ @Query("SELECT * FROM simple_entity WHERE with_specific_type = :withSpecificType")
+ Optional findBySpecificType(@Param("withSpecificType") @YdbType("Uint32") int withSpecificType);
+
+ //language=sql
+ @Query("SELECT * FROM simple_entity WHERE with_specific_type = :withSpecificType")
+ Optional findByWrongType(@Param("withSpecificType") @YdbType("Uuid") int withSpecificType);
+ }
+
+ @Table
+ static class SimpleEntity {
+
+ @Id
+ private Long id;
+
+ private String name;
+
+ private Integer withSpecificType;
+ }
+}
diff --git a/spring-data-jdbc-ydb/src/test/resources/changelogs/changelog.yaml b/spring-data-jdbc-ydb/src/test/resources/changelogs/changelog.yaml
index 49cc7cd..a103a20 100644
--- a/spring-data-jdbc-ydb/src/test/resources/changelogs/changelog.yaml
+++ b/spring-data-jdbc-ydb/src/test/resources/changelogs/changelog.yaml
@@ -5,6 +5,9 @@ databaseChangeLog:
- include:
file: "books.yaml"
relativeToChangelogFile: true
+ - include:
+ file: "types_customization.yaml"
+ relativeToChangelogFile: true
- changeSet:
id: "insert-all-types-table"
author: "Madiyar Nurgazin"
diff --git a/spring-data-jdbc-ydb/src/test/resources/changelogs/types_customization.yaml b/spring-data-jdbc-ydb/src/test/resources/changelogs/types_customization.yaml
new file mode 100644
index 0000000..a941258
--- /dev/null
+++ b/spring-data-jdbc-ydb/src/test/resources/changelogs/types_customization.yaml
@@ -0,0 +1,20 @@
+databaseChangeLog:
+ - changeSet:
+ id: "authors"
+ author: "Mikhail Polivakha"
+ changes:
+ - createTable:
+ tableName: simple_entity
+ columns:
+ - column:
+ name: id
+ type: bigint
+ constraints:
+ primaryKey: true
+ nullable: false
+ - column:
+ name: name
+ type: text
+ - column:
+ name: with_specific_type
+ type: uint64