diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 779a587..0c8fe7d 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -22,10 +22,10 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Set up JDK
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}
distribution: 'temurin'
diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml
index b9dc0b4..e6c8d64 100644
--- a/.github/workflows/validate.yaml
+++ b/.github/workflows/validate.yaml
@@ -19,10 +19,10 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Set up JDK
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}
distribution: 'temurin'
diff --git a/jdbc/pom.xml b/jdbc/pom.xml
index ed83a57..9984f39 100644
--- a/jdbc/pom.xml
+++ b/jdbc/pom.xml
@@ -15,12 +15,13 @@
pom
- 2.0.2
- 1.7.25
+ 2.0.6
+ 1.7.36basic-example
+ spring-data-jpa-v5
@@ -38,4 +39,16 @@
+
+
+
+ jdk17-examples
+
+ [17
+
+
+ spring-data-jpa
+
+
+
\ No newline at end of file
diff --git a/jdbc/spring-data-jpa-v5/pom.xml b/jdbc/spring-data-jpa-v5/pom.xml
new file mode 100644
index 0000000..86c1e36
--- /dev/null
+++ b/jdbc/spring-data-jpa-v5/pom.xml
@@ -0,0 +1,147 @@
+
+ 4.0.0
+
+ tech.ydb.jdbc.examples
+ ydb-jdbc-examples
+ 1.1.0-SNAPSHOT
+
+
+ spring-data-jpa-v5
+ Spring Data JPA Example Hibernate 5
+ Basic example for SpringBoot3 and Hibernate 6
+
+ 1.9.22
+ 0.9.1
+ 2.5.7
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+ ${spring.boot.version}
+
+
+ org.springframework.data
+ spring-data-commons
+ ${spring.boot.version}
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+ ${kotlin.version}
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ ${kotlin.version}
+
+
+ tech.ydb.dialects
+ hibernate-ydb-dialect-v5
+ ${hibernate.ydb.dialect.version}
+
+
+ tech.ydb.jdbc
+ ydb-jdbc-driver-shaded
+
+
+ com.github.javafaker
+ javafaker
+ 1.0.2
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-core
+ 1.7.3
+
+
+ org.postgresql
+ postgresql
+ 42.7.1
+
+
+ org.testcontainers
+ postgresql
+ 1.19.1
+ test
+
+
+ tech.ydb.test
+ ydb-junit5-support
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ ${spring.boot.version}
+ test
+
+
+
+ ${project.basedir}/src/main/kotlin
+ ${project.basedir}/src/test/kotlin
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring.boot.version}
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ test-compile
+
+ test-compile
+
+
+
+
+
+ -Xjsr305=strict
+
+
+ spring
+ jpa
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-allopen
+ ${kotlin.version}
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-noarg
+ ${kotlin.version}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/jdbc/spring-data-jpa-v5/src/main/kotlin/tech/ydb/jpa/pagination/Author.kt b/jdbc/spring-data-jpa-v5/src/main/kotlin/tech/ydb/jpa/pagination/Author.kt
new file mode 100644
index 0000000..8690d96
--- /dev/null
+++ b/jdbc/spring-data-jpa-v5/src/main/kotlin/tech/ydb/jpa/pagination/Author.kt
@@ -0,0 +1,17 @@
+package tech.ydb.jpa.pagination
+
+import javax.persistence.Entity
+import javax.persistence.Id
+import javax.persistence.Table
+
+@Entity
+@Table(name = "authors")
+class Author {
+
+ @Id
+ lateinit var id: String
+
+ lateinit var firstName: String
+
+ lateinit var lastName: String
+}
diff --git a/jdbc/spring-data-jpa-v5/src/main/kotlin/tech/ydb/jpa/pagination/Book.kt b/jdbc/spring-data-jpa-v5/src/main/kotlin/tech/ydb/jpa/pagination/Book.kt
new file mode 100644
index 0000000..54af00f
--- /dev/null
+++ b/jdbc/spring-data-jpa-v5/src/main/kotlin/tech/ydb/jpa/pagination/Book.kt
@@ -0,0 +1,19 @@
+package tech.ydb.jpa.pagination
+
+import java.util.*
+import javax.persistence.*
+
+@Entity
+@Table(name = "books")
+class Book {
+
+ @Id
+ lateinit var id: String
+
+ lateinit var title: String
+ lateinit var isbn10: String
+ lateinit var publicationDate: Date
+
+ @ManyToOne
+ lateinit var author: Author
+}
diff --git a/jdbc/spring-data-jpa-v5/src/main/kotlin/tech/ydb/jpa/pagination/BookRepository.kt b/jdbc/spring-data-jpa-v5/src/main/kotlin/tech/ydb/jpa/pagination/BookRepository.kt
new file mode 100644
index 0000000..67be22d
--- /dev/null
+++ b/jdbc/spring-data-jpa-v5/src/main/kotlin/tech/ydb/jpa/pagination/BookRepository.kt
@@ -0,0 +1,39 @@
+package tech.ydb.jpa.pagination
+
+import org.springframework.data.domain.*
+import org.springframework.data.jpa.repository.Query
+import org.springframework.data.repository.CrudRepository
+import org.springframework.data.repository.query.Param
+
+interface BookRepository : CrudRepository {
+
+ /**
+ * Uses an offset based pagination that first sorts the entries by their [ publication_date][Book.getPublicationDate]
+ * and then limits the result by dropping the number of rows specified in the
+ * [offset][Pageable.getOffset] clause. To retrieve [Page.getTotalElements] an additional count query
+ * is executed.
+ *
+ * @param title
+ * @param pageable
+ */
+ @Query(
+ "SELECT * FROM books WHERE books.title LIKE %:title% ORDER BY books.publication_date",
+ countQuery = "SELECT count(*) FROM books WHERE books.title LIKE %:title%",
+ nativeQuery = true
+ )
+ fun findByTitleContainsOrderByPublicationDate(@Param("title") title: String, pageable: Pageable): Page
+
+ /**
+ * Uses an offset based slicing that first sorts the entries by their [ publication_date][Book.getPublicationDate]
+ * and then limits the result by dropping the number of rows specified in the
+ * [offset][Pageable.getOffset] clause.
+ *
+ * @param title
+ * @param pageable
+ */
+ @Query(
+ "SELECT * FROM books WHERE books.title LIKE %:title% ORDER BY books.publication_date",
+ nativeQuery = true
+ )
+ fun findBooksByTitleContainsOrderByPublicationDate(title: String, pageable: Pageable): Slice
+}
diff --git a/jdbc/spring-data-jpa-v5/src/main/kotlin/tech/ydb/jpa/pagination/PagingApplication.kt b/jdbc/spring-data-jpa-v5/src/main/kotlin/tech/ydb/jpa/pagination/PagingApplication.kt
new file mode 100644
index 0000000..89f961d
--- /dev/null
+++ b/jdbc/spring-data-jpa-v5/src/main/kotlin/tech/ydb/jpa/pagination/PagingApplication.kt
@@ -0,0 +1,6 @@
+package tech.ydb.jpa.pagination
+
+import org.springframework.boot.autoconfigure.SpringBootApplication
+
+@SpringBootApplication
+class PagingApplication
diff --git a/jdbc/spring-data-jpa-v5/src/main/kotlin/tech/ydb/jpa/simple/SimpleUserRepository.kt b/jdbc/spring-data-jpa-v5/src/main/kotlin/tech/ydb/jpa/simple/SimpleUserRepository.kt
new file mode 100644
index 0000000..a2e9ff4
--- /dev/null
+++ b/jdbc/spring-data-jpa-v5/src/main/kotlin/tech/ydb/jpa/simple/SimpleUserRepository.kt
@@ -0,0 +1,124 @@
+package tech.ydb.jpa.simple;
+
+import org.springframework.data.domain.Pageable
+import org.springframework.data.domain.Slice
+import org.springframework.data.jpa.repository.Query
+import org.springframework.data.repository.CrudRepository
+import org.springframework.scheduling.annotation.Async
+import java.util.concurrent.CompletableFuture
+import java.util.stream.Stream
+
+/**
+ * Simple repository interface for {@link User} instances. The interface is used to declare the so-called query methods,
+ * i.e. methods to retrieve single entities or collections of them.
+ */
+interface SimpleUserRepository : CrudRepository {
+
+ /**
+ * Find the user with the given username. This method will be translated into a query using the
+ * {@link jakarta.persistence.NamedQuery} annotation at the {@link User} class.
+ *
+ * @param username
+ */
+ fun findByTheUsersName(username: String): User
+
+ /**
+ * Uses {@link Optional} as return and parameter type.
+ *
+ * @param username
+ */
+ fun findByUsername(username: String?): User?
+
+ /**
+ * Find all users with the given lastname. This method will be translated into a query by constructing it directly
+ * from the method name as there is no other query declared.
+ *
+ * @param lastname
+ */
+ fun findByLastname(lastname: String): List
+
+
+ /**
+ * Returns all users with the given firstname. This method will be translated into a query using the one declared in
+ * the {@link Query} annotation declared one.
+ *
+ * @param firstname
+ */
+ @Query("select u from User u where u.firstname = :firstname")
+ fun findByFirstname(firstname: String): List
+
+
+ /**
+ * Returns all users with the given name as first- or lastname. This makes the query to method relation much more
+ * refactoring-safe as the order of the method parameters is completely irrelevant.
+ *
+ * @param name
+ */
+ @Query("select u from User u where u.firstname = :name or u.lastname = :name")
+ fun findByFirstnameOrLastname(name: String): List
+
+ /**
+ * Returns the total number of entries deleted as their lastnames match the given one.
+ *
+ * @param lastname
+ * @return
+ */
+ fun removeByLastname(lastname: String): Long
+
+ /**
+ * Returns a {@link Slice} counting a maximum number of {@link Pageable#getPageSize()} users matching given criteria
+ * starting at {@link Pageable#getOffset()} without prior count of the total number of elements available.
+ *
+ * @param lastname
+ * @param page
+ */
+ @Query(
+ "SELECT * FROM users WHERE users.lastname = :lastname ORDER BY users.username",
+ nativeQuery = true
+ )
+ fun findByLastnameOrderByUsernameAsc(lastname: String, page: Pageable): Slice
+
+ /**
+ * Return the first 2 users ordered by their lastname asc.
+ *
+ *
+ * Example for findFirstK / findTopK functionality.
+ *
+ */
+ @Query("SELECT * FROM users ORDER BY lastname LIMIT 2", nativeQuery = true)
+ fun findFirst2ByOrderByLastnameAsc(): List
+
+
+ /**
+ * Return all the users with the given firstname or lastname. Makes use of SpEL (Spring Expression Language).
+ *
+ * @param user
+ */
+ @Query("select u from User u where u.firstname = :#{#user.firstname} or u.lastname = :#{#user.lastname}")
+ fun findByFirstnameOrLastname(user: User): Iterable
+
+ /**
+ * Sample default method.
+ *
+ * @param user
+ */
+ fun findByLastname(user: User): List {
+ return findByLastname(user.lastname);
+ }
+
+ /**
+ * Sample method to demonstrate support for {@link Stream} as a return type with a custom query. The query is executed
+ * in a streaming fashion which means that the method returns as soon as the first results are ready.
+ */
+ @Query("select u from User u")
+ fun streamAllCustomers(): Stream
+
+ /**
+ * Sample method to demonstrate support for {@link Stream} as a return type with a derived query. The query is
+ * executed in a streaming fashion which means that the method returns as soon as the first results are ready.
+ */
+ fun findAllByLastnameIsNotNull(): Stream
+
+ @Async
+ fun readAllBy(): CompletableFuture>
+}
diff --git a/jdbc/spring-data-jpa-v5/src/main/kotlin/tech/ydb/jpa/simple/User.kt b/jdbc/spring-data-jpa-v5/src/main/kotlin/tech/ydb/jpa/simple/User.kt
new file mode 100644
index 0000000..676b7b5
--- /dev/null
+++ b/jdbc/spring-data-jpa-v5/src/main/kotlin/tech/ydb/jpa/simple/User.kt
@@ -0,0 +1,45 @@
+package tech.ydb.jpa.simple
+
+import javax.persistence.*
+import org.springframework.data.util.ProxyUtils
+
+@Entity
+@Table(name = "Users", indexes = [Index(name = "username_index", columnList = "username")])
+@NamedQuery(name = "User.findByTheUsersName", query = "from User u where u.username = ?1")
+class User {
+
+ @Id
+ var id: Long = 0
+
+ lateinit var username: String
+
+ lateinit var firstname: String
+
+ lateinit var lastname: String
+
+ override fun equals(other: Any?): Boolean {
+ if (null == other) {
+ return false
+ }
+
+ if (this === other) {
+ return true
+ }
+
+ if (javaClass != ProxyUtils.getUserClass(other)) {
+ return false
+ }
+
+ val that: User = other as User
+
+ return this.id == that.id
+ }
+
+ override fun hashCode(): Int {
+ var hashCode = 17
+
+ hashCode += id.hashCode() * 31
+
+ return hashCode
+ }
+}
diff --git a/jdbc/spring-data-jpa-v5/src/main/kotlin/tech/ydb/jpa/simple/UserApplication.kt b/jdbc/spring-data-jpa-v5/src/main/kotlin/tech/ydb/jpa/simple/UserApplication.kt
new file mode 100644
index 0000000..5b745c4
--- /dev/null
+++ b/jdbc/spring-data-jpa-v5/src/main/kotlin/tech/ydb/jpa/simple/UserApplication.kt
@@ -0,0 +1,12 @@
+package tech.ydb.jpa.simple
+
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.autoconfigure.domain.EntityScan
+import org.springframework.scheduling.annotation.EnableAsync
+
+/**
+ * @author Kirill Kurdyukov
+ */
+@EnableAsync
+@SpringBootApplication
+class UserApplication
\ No newline at end of file
diff --git a/jdbc/spring-data-jpa-v5/src/main/resources/application-postgres.properties b/jdbc/spring-data-jpa-v5/src/main/resources/application-postgres.properties
new file mode 100644
index 0000000..fbba779
--- /dev/null
+++ b/jdbc/spring-data-jpa-v5/src/main/resources/application-postgres.properties
@@ -0,0 +1 @@
+spring.datasource.driver-class-name=org.postgresql.Driver
\ No newline at end of file
diff --git a/jdbc/spring-data-jpa-v5/src/main/resources/application-ydb.properties b/jdbc/spring-data-jpa-v5/src/main/resources/application-ydb.properties
new file mode 100644
index 0000000..73bd993
--- /dev/null
+++ b/jdbc/spring-data-jpa-v5/src/main/resources/application-ydb.properties
@@ -0,0 +1,4 @@
+spring.jpa.properties.hibernate.dialect=tech.ydb.hibernate.dialect.YdbDialect
+
+spring.datasource.driver-class-name=tech.ydb.jdbc.YdbDriver
+spring.datasource.url=jdbc:ydb:grpc://localhost:2136/local
diff --git a/jdbc/spring-data-jpa-v5/src/test/kotlin/tech/ydb/jpa/PostgresDockerTest.kt b/jdbc/spring-data-jpa-v5/src/test/kotlin/tech/ydb/jpa/PostgresDockerTest.kt
new file mode 100644
index 0000000..fa7bb09
--- /dev/null
+++ b/jdbc/spring-data-jpa-v5/src/test/kotlin/tech/ydb/jpa/PostgresDockerTest.kt
@@ -0,0 +1,33 @@
+package tech.ydb.jpa
+
+import org.springframework.test.context.ActiveProfiles
+import org.springframework.test.context.DynamicPropertyRegistry
+import org.springframework.test.context.DynamicPropertySource
+import org.testcontainers.containers.PostgreSQLContainer
+import org.testcontainers.containers.wait.strategy.Wait
+
+/**
+ * Debug mode
+ *
+ * @author Kirill Kurdyukov
+ */
+@ActiveProfiles("test", "postgres")
+abstract class PostgresDockerTest {
+
+ companion object {
+ private val postgresContainer: PostgreSQLContainer<*> = PostgreSQLContainer("postgres:latest")
+ .withDatabaseName("testdb")
+ .withUsername("test")
+ .withPassword("test")
+ .waitingFor(Wait.forListeningPort())
+
+ @JvmStatic
+ @DynamicPropertySource
+ fun prepareProperties(registry: DynamicPropertyRegistry) {
+ postgresContainer.start()
+ registry.add("spring.datasource.url", postgresContainer::getJdbcUrl)
+ registry.add("spring.datasource.password", postgresContainer::getPassword)
+ registry.add("spring.datasource.username", postgresContainer::getUsername)
+ }
+ }
+}
\ No newline at end of file
diff --git a/jdbc/spring-data-jpa-v5/src/test/kotlin/tech/ydb/jpa/YdbDockerTest.kt b/jdbc/spring-data-jpa-v5/src/test/kotlin/tech/ydb/jpa/YdbDockerTest.kt
new file mode 100644
index 0000000..4b743b4
--- /dev/null
+++ b/jdbc/spring-data-jpa-v5/src/test/kotlin/tech/ydb/jpa/YdbDockerTest.kt
@@ -0,0 +1,29 @@
+package tech.ydb.jpa
+
+import org.junit.jupiter.api.extension.RegisterExtension
+import org.springframework.test.context.ActiveProfiles
+import org.springframework.test.context.DynamicPropertyRegistry
+import org.springframework.test.context.DynamicPropertySource
+import tech.ydb.test.junit5.YdbHelperExtension
+
+/**
+ * @author Kirill Kurdyukov
+ */
+@ActiveProfiles("test", "ydb")
+abstract class YdbDockerTest {
+
+ companion object {
+ @JvmField
+ @RegisterExtension
+ val ydb = YdbHelperExtension()
+
+ @JvmStatic
+ @DynamicPropertySource
+ fun propertySource(registry: DynamicPropertyRegistry) {
+ registry.add("spring.datasource.url") {
+ "jdbc:ydb:${if (ydb.useTls()) "grpcs://" else "grpc://"}" +
+ "${ydb.endpoint()}${ydb.database()}${ydb.authToken()?.let { "?token=$it" } ?: ""}"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/jdbc/spring-data-jpa-v5/src/test/kotlin/tech/ydb/jpa/pagination/PaginationTests.kt b/jdbc/spring-data-jpa-v5/src/test/kotlin/tech/ydb/jpa/pagination/PaginationTests.kt
new file mode 100644
index 0000000..ffb803f
--- /dev/null
+++ b/jdbc/spring-data-jpa-v5/src/test/kotlin/tech/ydb/jpa/pagination/PaginationTests.kt
@@ -0,0 +1,110 @@
+package tech.ydb.jpa.pagination
+
+import com.github.javafaker.Faker
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+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.transaction.annotation.Transactional
+import tech.ydb.jpa.PostgresDockerTest
+import tech.ydb.jpa.YdbDockerTest
+import java.util.*
+import java.util.concurrent.TimeUnit
+import javax.persistence.EntityManager
+
+/**
+ * Show different types of paging styles using [Page], [Slice] and [Window].
+ *
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ */
+@SpringBootTest
+@Transactional
+internal class PaginationTests : YdbDockerTest() {
+
+ @Autowired
+ lateinit var books: BookRepository
+
+ @BeforeEach
+ fun setUp() {
+ val faker = Faker()
+
+ val authorList = createAuthors(faker)
+ createBooks(faker, authorList)
+ }
+
+ /**
+ * Page through the results using an offset/limit approach where the server skips over the number of results specified
+ * via [Pageable.getOffset]. The [Page] return type will run an additional count query to
+ * read the total number of matching rows on each request.
+ */
+ @Test
+ fun pageThroughResultsWithSkipAndLimit() {
+ var page: Page
+ var pageRequest: Pageable = PageRequest.of(0, 2)
+
+ do {
+ page = books.findByTitleContainsOrderByPublicationDate("the", pageRequest)
+ assertThat(page.content.size).isGreaterThanOrEqualTo(1).isLessThanOrEqualTo(2)
+
+ pageRequest = page.nextPageable()
+ } while (page.hasNext())
+ }
+
+ /**
+ * Run through the results using an offset/limit approach where the server skips over the number of results specified
+ * via [Pageable.getOffset]. No additional count query to read the total number of matching rows is
+ * issued. Still [Slice] requests, but does not emit, one row more than specified via [Page.getSize] to
+ * feed [Slice.hasNext]
+ */
+ @Test
+ fun sliceThroughResultsWithSkipAndLimit() {
+ var slice: Slice
+ var pageRequest: Pageable = PageRequest.of(0, 2)
+
+ do {
+ slice = books.findBooksByTitleContainsOrderByPublicationDate("the", pageRequest)
+ assertThat(slice.content.size).isGreaterThanOrEqualTo(1).isLessThanOrEqualTo(2)
+
+ pageRequest = slice.nextPageable()
+ } while (slice.hasNext())
+ }
+
+ // --> Test Data
+ @Autowired
+ lateinit var em: EntityManager
+
+ private fun createAuthors(faker: Faker): List {
+ val authors = List(11) { id: Int ->
+ val author = Author()
+ author.id = "author-%s".format(id)
+ author.firstName = faker.name().firstName()
+ author.lastName = faker.name().lastName()
+
+ em.persist(author)
+ author
+ }
+
+ return authors
+ }
+
+ private fun createBooks(faker: Faker, authors: List): List {
+ val rand = Random()
+ return List(100) { id: Int ->
+ val book = Book()
+ book.id = "book-%03d".format(id)
+ book.title = (if (id % 2 == 0) "the-crazy-book-" else "") + faker.book().title()
+ book.isbn10 = UUID.randomUUID().toString().substring(0, 10)
+ book.publicationDate = faker.date().past(5000, TimeUnit.DAYS)
+ book.author = authors[rand.nextInt(authors.size)]
+
+ em.persist(book)
+ book
+ }
+ }
+}
diff --git a/jdbc/spring-data-jpa-v5/src/test/kotlin/tech/ydb/jpa/simple/SimpleRepositoryTest.kt b/jdbc/spring-data-jpa-v5/src/test/kotlin/tech/ydb/jpa/simple/SimpleRepositoryTest.kt
new file mode 100644
index 0000000..ccd0eee
--- /dev/null
+++ b/jdbc/spring-data-jpa-v5/src/test/kotlin/tech/ydb/jpa/simple/SimpleRepositoryTest.kt
@@ -0,0 +1,200 @@
+package tech.ydb.jpa.simple
+
+import kotlinx.coroutines.future.await
+import kotlinx.coroutines.runBlocking
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.dao.InvalidDataAccessApiUsageException
+import org.springframework.data.domain.PageRequest
+import org.springframework.transaction.annotation.Propagation
+import org.springframework.transaction.annotation.Transactional
+import tech.ydb.jpa.YdbDockerTest
+import java.util.stream.Collectors
+
+/**
+ * @author Kirill Kurdyukov
+ */
+@SpringBootTest
+@Transactional
+class SimpleRepositoryTest : YdbDockerTest() {
+
+ @Autowired
+ lateinit var repository: SimpleUserRepository
+
+ lateinit var user: User
+
+ @BeforeEach
+ fun setUp() {
+ user = User().apply {
+ username = "foobar"
+ firstname = "firstname"
+ lastname = "lastname"
+ }
+ }
+
+ @Test
+ fun findSavedUserById() {
+ user = repository.save(user)
+
+ assertThat(repository.findById(user.id)).hasValue(user)
+ }
+
+ @Test
+ fun findSavedUserByLastname() {
+ user = repository.save(user)
+
+ assertThat(repository.findByLastname("lastname")).contains(user)
+ }
+
+ @Test
+ fun findByFirstnameOrLastname() {
+ user = repository.save(user)
+
+ assertThat(repository.findByFirstnameOrLastname("lastname")).contains(user)
+ }
+
+ @Test
+ fun useOptionalAsReturnAndParameterType() {
+ assertNull(repository.findByUsername("foobar"))
+
+ repository.save(user)
+
+ assertNotNull(repository.findByUsername("foobar"))
+ }
+
+ @Test
+ fun removeByLastname() {
+ // create a 2nd user with the same lastname as user
+
+ val user2 = User().apply { id = 1; lastname = user.lastname }
+
+ // create a 3rd user as control group
+ val user3 = User().apply { id = 2; lastname = "no-positive-match" }
+
+ repository.saveAll(listOf(user, user2, user3))
+
+ assertThat(repository.removeByLastname(user.lastname)).isEqualTo(2L)
+ assertThat(repository.existsById(user3.id)).isTrue()
+ }
+
+ @Test
+ fun useSliceToLoadContent() {
+ val totalNumberUsers = 11
+ val source: MutableList = ArrayList(totalNumberUsers)
+
+ for (i in 1..totalNumberUsers) {
+ val user = User().apply {
+ id = i.toLong()
+ lastname = user.lastname
+ username = "${user.lastname}-${String.format("%03d", i)}"
+ }
+
+ source.add(user)
+ }
+
+ repository.saveAll(source)
+
+ val users = repository.findByLastnameOrderByUsernameAsc(user.lastname, PageRequest.of(1, 5))
+
+ assertThat(users).containsAll(source.subList(5, 10))
+ }
+
+ @Test
+ fun findFirst2ByOrderByLastnameAsc() {
+ val user0 = User().apply { id = 1; lastname = "lastname-0" }
+
+ val user1 = User().apply { id = 2; lastname = "lastname-1" }
+
+ val user2 = User().apply { id = 3; lastname = "lastname-2" }
+
+ // we deliberately save the items in reverse
+ repository.saveAll(listOf(user2, user1, user0))
+
+ val result = repository.findFirst2ByOrderByLastnameAsc()
+
+ assertThat(result).containsExactly(user0, user1)
+ }
+
+ @Test
+ fun findByFirstnameOrLastnameUsingSpEL() {
+ val first = User().apply { id = 1; lastname = "lastname" }
+
+ val second = User().apply { id = 2; firstname = "firstname" }
+
+ val third = User()
+
+ repository.saveAll(listOf(first, second, third))
+
+ val reference = User().apply { id = 3; firstname = "firstname"; lastname = "lastname" }
+
+ val users = repository.findByFirstnameOrLastname(reference)
+
+ assertThat(users).contains(first)
+ assertThat(users).contains(second)
+ assertThat(users).hasSize(2)
+ }
+
+ /**
+ * Streaming data from the store by using a repository method that returns a [Stream]. Note, that since the
+ * resulting [Stream] contains state it needs to be closed explicitly after use!
+ */
+ @Test
+ fun useJava8StreamsWithCustomQuery() {
+ val user1 = repository.save(User().apply { id = 1; firstname = "Customer1"; lastname = "Foo" })
+ val user2 = repository.save(User().apply { id = 2; firstname = "Customer2"; lastname = "Bar" })
+
+ repository.streamAllCustomers().use { stream ->
+ assertThat(stream.collect(Collectors.toList())).contains(user1, user2)
+ }
+ }
+
+ /**
+ * Streaming data from the store by using a repository method that returns a [Stream] with a derived query.
+ * Note, that since the resulting [Stream] contains state it needs to be closed explicitly after use!
+ */
+ @Test
+ fun useJava8StreamsWithDerivedQuery() {
+ val user1 = repository.save(User().apply { id = 1; firstname = "Customer1"; lastname = "Foo" })
+ val user2 = repository.save(User().apply { id = 2; firstname = "Customer2"; lastname = "Bar" })
+
+ repository.findAllByLastnameIsNotNull().use { stream ->
+ assertThat(stream.collect(Collectors.toList())).contains(user1, user2)
+ }
+ }
+
+ /**
+ * Query methods using streaming need to be used inside a surrounding transaction to keep the connection open while
+ * the stream is consumed. We simulate that not being the case by actively disabling the transaction here.
+ */
+ @Test
+ @Transactional(propagation = Propagation.NOT_SUPPORTED)
+ fun rejectsStreamExecutionIfNoSurroundingTransactionActive() {
+ Assertions.assertThrows(InvalidDataAccessApiUsageException::class.java) {
+ repository.findAllByLastnameIsNotNull()
+ }
+ }
+
+ /**
+ * Here we demonstrate the usage of [CompletableFuture] as a result wrapper for asynchronous repository query
+ * methods. Note, that we need to disable the surrounding transaction to be able to asynchronously read the written
+ * data from another thread within the same test method.
+ */
+ @Test
+ @Transactional(propagation = Propagation.NOT_SUPPORTED)
+ fun supportsCompletableFuturesAsReturnTypeWrapper() {
+ repository.save(User().apply { id = 1; firstname = "Customer1"; lastname = "Foo" })
+ repository.save(User().apply { id = 2; firstname = "Customer2"; lastname = "Bar" })
+
+ runBlocking {
+ val users = repository.readAllBy().await()
+ assertThat(users).hasSize(2)
+ }
+
+ repository.deleteAll()
+ }
+}
\ No newline at end of file
diff --git a/jdbc/spring-data-jpa-v5/src/test/resources/application-test.properties b/jdbc/spring-data-jpa-v5/src/test/resources/application-test.properties
new file mode 100644
index 0000000..4abc7c5
--- /dev/null
+++ b/jdbc/spring-data-jpa-v5/src/test/resources/application-test.properties
@@ -0,0 +1,5 @@
+spring.jpa.show-sql=true
+spring.jpa.properties.hibernate.format_sql=false
+spring.jpa.properties.hibernate.highlight_sql=true
+
+spring.jpa.properties.hibernate.hbm2ddl.auto=create
\ No newline at end of file
diff --git a/jdbc/spring-data-jpa-v5/src/test/resources/logback.xml b/jdbc/spring-data-jpa-v5/src/test/resources/logback.xml
new file mode 100644
index 0000000..77232d4
--- /dev/null
+++ b/jdbc/spring-data-jpa-v5/src/test/resources/logback.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ %d %5p %40.40c:%4L - %m%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/jdbc/spring-data-jpa/pom.xml b/jdbc/spring-data-jpa/pom.xml
new file mode 100644
index 0000000..30294b5
--- /dev/null
+++ b/jdbc/spring-data-jpa/pom.xml
@@ -0,0 +1,149 @@
+
+ 4.0.0
+
+ tech.ydb.jdbc.examples
+ ydb-jdbc-examples
+ 1.1.0-SNAPSHOT
+
+
+ spring-data-jpa
+ Spring Data JPA Example
+ Basic example for SpringBoot3 and Hibernate 6
+
+ 17
+ 1.9.22
+ 0.9.1
+ 3.2.1
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+ ${spring.boot.version}
+
+
+ org.springframework.data
+ spring-data-commons
+ ${spring.boot.version}
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+ ${kotlin.version}
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ ${kotlin.version}
+
+
+ tech.ydb.dialects
+ hibernate-ydb-dialect
+ ${hibernate.ydb.dialect.version}
+
+
+ tech.ydb.jdbc
+ ydb-jdbc-driver-shaded
+
+
+ com.github.javafaker
+ javafaker
+ 1.0.2
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-core
+ 1.7.3
+
+
+ org.postgresql
+ postgresql
+ 42.7.1
+
+
+ org.testcontainers
+ postgresql
+ 1.19.1
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ ${spring.boot.version}
+ test
+
+
+ tech.ydb.test
+ ydb-junit5-support
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+
+
+
+
+
+ ${project.basedir}/src/main/kotlin
+ ${project.basedir}/src/test/kotlin
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring.boot.version}
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ test-compile
+
+ test-compile
+
+
+
+
+
+ -Xjsr305=strict
+
+
+ spring
+ jpa
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-allopen
+ ${kotlin.version}
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-noarg
+ ${kotlin.version}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/jdbc/spring-data-jpa/src/main/kotlin/tech/ydb/jpa/pagination/Author.kt b/jdbc/spring-data-jpa/src/main/kotlin/tech/ydb/jpa/pagination/Author.kt
new file mode 100644
index 0000000..a0f516b
--- /dev/null
+++ b/jdbc/spring-data-jpa/src/main/kotlin/tech/ydb/jpa/pagination/Author.kt
@@ -0,0 +1,17 @@
+package tech.ydb.jpa.pagination
+
+import jakarta.persistence.Entity
+import jakarta.persistence.Id
+import jakarta.persistence.Table
+
+@Entity
+@Table(name = "authors")
+class Author {
+
+ @Id
+ lateinit var id: String
+
+ lateinit var firstName: String
+
+ lateinit var lastName: String
+}
diff --git a/jdbc/spring-data-jpa/src/main/kotlin/tech/ydb/jpa/pagination/Book.kt b/jdbc/spring-data-jpa/src/main/kotlin/tech/ydb/jpa/pagination/Book.kt
new file mode 100644
index 0000000..3141a48
--- /dev/null
+++ b/jdbc/spring-data-jpa/src/main/kotlin/tech/ydb/jpa/pagination/Book.kt
@@ -0,0 +1,22 @@
+package tech.ydb.jpa.pagination
+
+import jakarta.persistence.Entity
+import jakarta.persistence.Id
+import jakarta.persistence.ManyToOne
+import jakarta.persistence.Table
+import java.util.*
+
+@Entity
+@Table(name = "books")
+class Book {
+
+ @Id
+ lateinit var id: String
+
+ lateinit var title: String
+ lateinit var isbn10: String
+ lateinit var publicationDate: Date
+
+ @ManyToOne
+ lateinit var author: Author
+}
diff --git a/jdbc/spring-data-jpa/src/main/kotlin/tech/ydb/jpa/pagination/BookRepository.kt b/jdbc/spring-data-jpa/src/main/kotlin/tech/ydb/jpa/pagination/BookRepository.kt
new file mode 100644
index 0000000..1505d66
--- /dev/null
+++ b/jdbc/spring-data-jpa/src/main/kotlin/tech/ydb/jpa/pagination/BookRepository.kt
@@ -0,0 +1,49 @@
+package tech.ydb.jpa.pagination
+
+import org.springframework.data.domain.*
+import org.springframework.data.jpa.repository.Query
+import org.springframework.data.repository.ListCrudRepository
+import org.springframework.data.repository.query.Param
+
+interface BookRepository : ListCrudRepository {
+
+ /**
+ * Uses an offset based pagination that first sorts the entries by their [ publication_date][Book.getPublicationDate]
+ * and then limits the result by dropping the number of rows specified in the
+ * [offset][Pageable.getOffset] clause. To retrieve [Page.getTotalElements] an additional count query
+ * is executed.
+ *
+ * @param title
+ * @param pageable
+ */
+ @Query(
+ value = "SELECT * FROM books WHERE title LIKE %:title% ORDER BY books.publication_date",
+ countQuery = "SELECT count(*) FROM books WHERE title LIKE %:title% ",
+ nativeQuery = true
+ )
+ fun findByTitleContainsOrderByPublicationDate(@Param("title") title: String, pageable: Pageable): Page
+
+ /**
+ * Uses an offset based slicing that first sorts the entries by their [ publication_date][Book.getPublicationDate] and then limits the result by dropping the number of rows specified in the
+ * [offset][Pageable.getOffset] clause.
+ *
+ * @param title
+ * @param pageable
+ */
+ @Query(
+ value = "SELECT * FROM books WHERE title LIKE %:title% ORDER BY books.publication_date",
+ nativeQuery = true
+ )
+ fun findBooksByTitleContainsOrderByPublicationDate(title: String, pageable: Pageable): Slice
+
+ /**
+ * Depending on the provided [ScrollPosition] either [ offset][org.springframework.data.domain.OffsetScrollPosition]
+ * or [keyset][org.springframework.data.domain.KeysetScrollPosition] scrolling is possible. Scrolling
+ * through results requires a stable [org.springframework.data.domain.Sort] which is different from what
+ * [Pageable.getSort] offers. The limit is defined via the Top keyword.
+ *
+ * @param title
+ * @param scrollPosition
+ */
+ fun findTop2ByTitleContainsOrderByPublicationDate(title: String, scrollPosition: ScrollPosition): Window
+}
diff --git a/jdbc/spring-data-jpa/src/main/kotlin/tech/ydb/jpa/pagination/PagingApplication.kt b/jdbc/spring-data-jpa/src/main/kotlin/tech/ydb/jpa/pagination/PagingApplication.kt
new file mode 100644
index 0000000..89f961d
--- /dev/null
+++ b/jdbc/spring-data-jpa/src/main/kotlin/tech/ydb/jpa/pagination/PagingApplication.kt
@@ -0,0 +1,6 @@
+package tech.ydb.jpa.pagination
+
+import org.springframework.boot.autoconfigure.SpringBootApplication
+
+@SpringBootApplication
+class PagingApplication
diff --git a/jdbc/spring-data-jpa/src/main/kotlin/tech/ydb/jpa/simple/SimpleUserRepository.kt b/jdbc/spring-data-jpa/src/main/kotlin/tech/ydb/jpa/simple/SimpleUserRepository.kt
new file mode 100644
index 0000000..a877d9c
--- /dev/null
+++ b/jdbc/spring-data-jpa/src/main/kotlin/tech/ydb/jpa/simple/SimpleUserRepository.kt
@@ -0,0 +1,150 @@
+package tech.ydb.jpa.simple;
+
+import org.springframework.data.domain.Limit;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.ListCrudRepository;
+import org.springframework.scheduling.annotation.Async;
+import java.util.concurrent.CompletableFuture
+import java.util.stream.Stream
+
+/**
+ * Simple repository interface for {@link User} instances. The interface is used to declare the so-called query methods,
+ * i.e. methods to retrieve single entities or collections of them.
+ */
+interface SimpleUserRepository : ListCrudRepository {
+
+ /**
+ * Find the user with the given username. This method will be translated into a query using the
+ * {@link jakarta.persistence.NamedQuery} annotation at the {@link User} class.
+ *
+ * @param username
+ */
+ fun findByTheUsersName(username: String): User
+
+ /**
+ * Uses {@link Optional} as return and parameter type.
+ *
+ * @param username
+ */
+ fun findByUsername(username: String?): User?
+
+ /**
+ * Find all users with the given lastname. This method will be translated into a query by constructing it directly
+ * from the method name as there is no other query declared.
+ *
+ * @param lastname
+ */
+ fun findByLastname(lastname: String): List
+
+ /**
+ * Find at most the number of users defined via maxResults with the given lastname.
+ * This method will be translated into a query by constructing it directly from the method name as there is no other
+ * query declared.
+ *
+ * @param lastname
+ * @param maxResults the maximum number of results returned.
+ */
+ fun findByLastname(lastname: String, maxResults: Limit): List
+
+ /**
+ * Returns all users with the given firstname. This method will be translated into a query using the one declared in
+ * the {@link Query} annotation declared one.
+ *
+ * @param firstname
+ */
+ @Query("select u from User u where u.firstname = :firstname")
+ fun findByFirstname(firstname: String): List
+
+ /**
+ * Returns at most the number of users defined via {@link Limit} with the given firstname. This method will be
+ * translated into a query using the one declared in the {@link Query} annotation declared one.
+ *
+ * @param firstname
+ * @param maxResults the maximum number of results returned.
+ */
+ @Query("select u from User u where u.firstname = :firstname")
+ fun findByFirstname(firstname: String, maxResults: Limit): List
+
+ /**
+ * Returns all users with the given name as first- or lastname. This makes the query to method relation much more
+ * refactoring-safe as the order of the method parameters is completely irrelevant.
+ *
+ * @param name
+ */
+ @Query("select u from User u where u.firstname = :name or u.lastname = :name")
+ fun findByFirstnameOrLastname(name: String): List
+
+ /**
+ * Returns the total number of entries deleted as their lastnames match the given one.
+ *
+ * @param lastname
+ * @return
+ */
+ fun removeByLastname(lastname: String): Long
+
+ /**
+ * Returns a {@link Slice} counting a maximum number of {@link Pageable#getPageSize()} users matching given criteria
+ * starting at {@link Pageable#getOffset()} without prior count of the total number of elements available.
+ *
+ * @param lastname
+ * @param page
+ */
+ fun findByLastnameOrderByUsernameAsc(lastname: String, page: Pageable): Slice
+
+ /**
+ * Return the first 2 users ordered by their lastname asc.
+ *
+ *
+ * Example for findFirstK / findTopK functionality.
+ *
+ */
+ fun findFirst2ByOrderByLastnameAsc(): List
+
+ /**
+ * Return the first 2 users ordered by the given {@code sort} definition.
+ *
+ *
+ * This variant is very flexible because one can ask for the first K results when a ASC ordering
+ * is used as well as for the last K results when a DESC ordering is used.
+ *
+ *
+ * @param sort
+ */
+ fun findTop2By(sort: Sort): List
+
+ /**
+ * Return all the users with the given firstname or lastname. Makes use of SpEL (Spring Expression Language).
+ *
+ * @param user
+ */
+ @Query("select u from User u where u.firstname = :#{#user.firstname} or u.lastname = :#{#user.lastname}")
+ fun findByFirstnameOrLastname(user: User): Iterable
+
+ /**
+ * Sample default method.
+ *
+ * @param user
+ */
+ fun findByLastname(user: User): List {
+ return findByLastname(user.lastname);
+ }
+
+ /**
+ * Sample method to demonstrate support for {@link Stream} as a return type with a custom query. The query is executed
+ * in a streaming fashion which means that the method returns as soon as the first results are ready.
+ */
+ @Query("select u from User u")
+ fun streamAllCustomers(): Stream
+
+ /**
+ * Sample method to demonstrate support for {@link Stream} as a return type with a derived query. The query is
+ * executed in a streaming fashion which means that the method returns as soon as the first results are ready.
+ */
+ fun findAllByLastnameIsNotNull(): Stream
+
+ @Async
+ fun readAllBy(): CompletableFuture>
+}
diff --git a/jdbc/spring-data-jpa/src/main/kotlin/tech/ydb/jpa/simple/User.kt b/jdbc/spring-data-jpa/src/main/kotlin/tech/ydb/jpa/simple/User.kt
new file mode 100644
index 0000000..69f44ab
--- /dev/null
+++ b/jdbc/spring-data-jpa/src/main/kotlin/tech/ydb/jpa/simple/User.kt
@@ -0,0 +1,57 @@
+package tech.ydb.jpa.simple
+
+import jakarta.persistence.*
+import org.hibernate.annotations.GenericGenerator
+import org.hibernate.engine.spi.SharedSessionContractImplementor
+import org.hibernate.id.IdentifierGenerator
+import org.springframework.data.util.ProxyUtils
+import java.util.concurrent.ThreadLocalRandom
+
+@Entity
+@Table(name = "Users", indexes = [Index(name = "username_index", columnList = "username")])
+@NamedQuery(name = "User.findByTheUsersName", query = "from User u where u.username = ?1")
+class User {
+
+ @Id
+ @GeneratedValue(generator = "random-int-id")
+ @GenericGenerator(name = "random-int-id", type = RandomLongGenerator::class)
+ var id: Long = 0
+
+ lateinit var username: String
+
+ lateinit var firstname: String
+
+ lateinit var lastname: String
+
+ class RandomLongGenerator : IdentifierGenerator {
+ override fun generate(session: SharedSessionContractImplementor, `object`: Any): Any {
+ return ThreadLocalRandom.current().nextLong()
+ }
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (null == other) {
+ return false
+ }
+
+ if (this === other) {
+ return true
+ }
+
+ if (javaClass != ProxyUtils.getUserClass(other)) {
+ return false
+ }
+
+ val that: User = other as User
+
+ return this.id == that.id
+ }
+
+ override fun hashCode(): Int {
+ var hashCode = 17
+
+ hashCode += id.hashCode() * 31
+
+ return hashCode
+ }
+}
diff --git a/jdbc/spring-data-jpa/src/main/kotlin/tech/ydb/jpa/simple/UserApplication.kt b/jdbc/spring-data-jpa/src/main/kotlin/tech/ydb/jpa/simple/UserApplication.kt
new file mode 100644
index 0000000..35f39d0
--- /dev/null
+++ b/jdbc/spring-data-jpa/src/main/kotlin/tech/ydb/jpa/simple/UserApplication.kt
@@ -0,0 +1,11 @@
+package tech.ydb.jpa.simple
+
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.scheduling.annotation.EnableAsync
+
+/**
+ * @author Kirill Kurdyukov
+ */
+@EnableAsync
+@SpringBootApplication
+class UserApplication
\ No newline at end of file
diff --git a/jdbc/spring-data-jpa/src/main/resources/application-postgres.properties b/jdbc/spring-data-jpa/src/main/resources/application-postgres.properties
new file mode 100644
index 0000000..fbba779
--- /dev/null
+++ b/jdbc/spring-data-jpa/src/main/resources/application-postgres.properties
@@ -0,0 +1 @@
+spring.datasource.driver-class-name=org.postgresql.Driver
\ No newline at end of file
diff --git a/jdbc/spring-data-jpa/src/main/resources/application-ydb.properties b/jdbc/spring-data-jpa/src/main/resources/application-ydb.properties
new file mode 100644
index 0000000..73bd993
--- /dev/null
+++ b/jdbc/spring-data-jpa/src/main/resources/application-ydb.properties
@@ -0,0 +1,4 @@
+spring.jpa.properties.hibernate.dialect=tech.ydb.hibernate.dialect.YdbDialect
+
+spring.datasource.driver-class-name=tech.ydb.jdbc.YdbDriver
+spring.datasource.url=jdbc:ydb:grpc://localhost:2136/local
diff --git a/jdbc/spring-data-jpa/src/test/kotlin/tech/ydb/jpa/PostgresDockerTest.kt b/jdbc/spring-data-jpa/src/test/kotlin/tech/ydb/jpa/PostgresDockerTest.kt
new file mode 100644
index 0000000..fa7bb09
--- /dev/null
+++ b/jdbc/spring-data-jpa/src/test/kotlin/tech/ydb/jpa/PostgresDockerTest.kt
@@ -0,0 +1,33 @@
+package tech.ydb.jpa
+
+import org.springframework.test.context.ActiveProfiles
+import org.springframework.test.context.DynamicPropertyRegistry
+import org.springframework.test.context.DynamicPropertySource
+import org.testcontainers.containers.PostgreSQLContainer
+import org.testcontainers.containers.wait.strategy.Wait
+
+/**
+ * Debug mode
+ *
+ * @author Kirill Kurdyukov
+ */
+@ActiveProfiles("test", "postgres")
+abstract class PostgresDockerTest {
+
+ companion object {
+ private val postgresContainer: PostgreSQLContainer<*> = PostgreSQLContainer("postgres:latest")
+ .withDatabaseName("testdb")
+ .withUsername("test")
+ .withPassword("test")
+ .waitingFor(Wait.forListeningPort())
+
+ @JvmStatic
+ @DynamicPropertySource
+ fun prepareProperties(registry: DynamicPropertyRegistry) {
+ postgresContainer.start()
+ registry.add("spring.datasource.url", postgresContainer::getJdbcUrl)
+ registry.add("spring.datasource.password", postgresContainer::getPassword)
+ registry.add("spring.datasource.username", postgresContainer::getUsername)
+ }
+ }
+}
\ No newline at end of file
diff --git a/jdbc/spring-data-jpa/src/test/kotlin/tech/ydb/jpa/YdbDockerTest.kt b/jdbc/spring-data-jpa/src/test/kotlin/tech/ydb/jpa/YdbDockerTest.kt
new file mode 100644
index 0000000..4b743b4
--- /dev/null
+++ b/jdbc/spring-data-jpa/src/test/kotlin/tech/ydb/jpa/YdbDockerTest.kt
@@ -0,0 +1,29 @@
+package tech.ydb.jpa
+
+import org.junit.jupiter.api.extension.RegisterExtension
+import org.springframework.test.context.ActiveProfiles
+import org.springframework.test.context.DynamicPropertyRegistry
+import org.springframework.test.context.DynamicPropertySource
+import tech.ydb.test.junit5.YdbHelperExtension
+
+/**
+ * @author Kirill Kurdyukov
+ */
+@ActiveProfiles("test", "ydb")
+abstract class YdbDockerTest {
+
+ companion object {
+ @JvmField
+ @RegisterExtension
+ val ydb = YdbHelperExtension()
+
+ @JvmStatic
+ @DynamicPropertySource
+ fun propertySource(registry: DynamicPropertyRegistry) {
+ registry.add("spring.datasource.url") {
+ "jdbc:ydb:${if (ydb.useTls()) "grpcs://" else "grpc://"}" +
+ "${ydb.endpoint()}${ydb.database()}${ydb.authToken()?.let { "?token=$it" } ?: ""}"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/jdbc/spring-data-jpa/src/test/kotlin/tech/ydb/jpa/pagination/PaginationTests.kt b/jdbc/spring-data-jpa/src/test/kotlin/tech/ydb/jpa/pagination/PaginationTests.kt
new file mode 100644
index 0000000..c8ba42d
--- /dev/null
+++ b/jdbc/spring-data-jpa/src/test/kotlin/tech/ydb/jpa/pagination/PaginationTests.kt
@@ -0,0 +1,168 @@
+package tech.ydb.jpa.pagination
+
+import com.github.javafaker.Faker
+import jakarta.persistence.EntityManager
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.data.domain.Page
+import org.springframework.data.domain.PageRequest
+import org.springframework.data.domain.Pageable
+import org.springframework.data.domain.Window
+import org.springframework.data.domain.ScrollPosition
+import org.springframework.data.domain.Slice
+import org.springframework.data.support.WindowIterator
+import org.springframework.transaction.annotation.Transactional
+import tech.ydb.jpa.PostgresDockerTest
+import java.util.*
+import java.util.concurrent.TimeUnit
+
+/**
+ * Show different types of paging styles using [Page], [Slice] and [Window].
+ *
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ */
+@SpringBootTest
+@Transactional
+internal class PaginationTests : PostgresDockerTest() /* TODO ESCAPE '\' don't support */{
+
+ @Autowired
+ lateinit var books: BookRepository
+
+ @BeforeEach
+ fun setUp() {
+ val faker = Faker()
+
+ val authorList = createAuthors(faker)
+ createBooks(faker, authorList)
+ }
+
+ /**
+ * Page through the results using an offset/limit approach where the server skips over the number of results specified
+ * via [Pageable.getOffset]. The [Page] return type will run an additional count query to
+ * read the total number of matching rows on each request.
+ */
+ @Test
+ fun pageThroughResultsWithSkipAndLimit() {
+ var page: Page
+ var pageRequest: Pageable = PageRequest.of(0, 2)
+
+ do {
+ page = books.findByTitleContainsOrderByPublicationDate("the", pageRequest)
+ assertThat(page.content.size).isGreaterThanOrEqualTo(1).isLessThanOrEqualTo(2)
+
+ pageRequest = page.nextPageable()
+ } while (page.hasNext())
+ }
+
+ /**
+ * Run through the results using an offset/limit approach where the server skips over the number of results specified
+ * via [Pageable.getOffset]. No additional count query to read the total number of matching rows is
+ * issued. Still [Slice] requests, but does not emit, one row more than specified via [Page.getSize] to
+ * feed [Slice.hasNext]
+ */
+ @Test
+ fun sliceThroughResultsWithSkipAndLimit() {
+ var slice: Slice
+ var pageRequest: Pageable = PageRequest.of(0, 2)
+
+ do {
+ slice = books.findBooksByTitleContainsOrderByPublicationDate("the", pageRequest)
+ assertThat(slice.content.size).isGreaterThanOrEqualTo(1).isLessThanOrEqualTo(2)
+
+ pageRequest = slice.nextPageable()
+ } while (slice.hasNext())
+ }
+
+ /**
+ * Scroll through the results using an offset/limit approach where the server skips over the number of results
+ * specified via [OffsetScrollPosition.getOffset].
+ *
+ *
+ * This approach is similar to the [slicing one][.sliceThroughResultsWithSkipAndLimit].
+ */
+ @Test
+ fun scrollThroughResultsWithSkipAndLimit() {
+ var window: Window
+ var scrollPosition: ScrollPosition = ScrollPosition.offset()
+
+ do {
+ window = books.findTop2ByTitleContainsOrderByPublicationDate("the", scrollPosition)
+ assertThat(window.content.size).isGreaterThanOrEqualTo(1).isLessThanOrEqualTo(2)
+
+ scrollPosition = window.positionAt(window.content.size - 1)
+ } while (window.hasNext())
+ }
+
+ /**
+ * Scroll through the results using an offset/limit approach where the server skips over the number of results
+ * specified via [OffsetScrollPosition.getOffset] using [WindowIterator].
+ *
+ *
+ * This approach is similar to the [slicing one][.sliceThroughResultsWithSkipAndLimit].
+ */
+ @Test
+ fun scrollThroughResultsUsingWindowIteratorWithSkipAndLimit() {
+ val iterator: WindowIterator = WindowIterator
+ .of { scrollPosition: ScrollPosition -> books.findTop2ByTitleContainsOrderByPublicationDate("the-crazy-book-", scrollPosition) }
+ .startingAt(ScrollPosition.offset())
+
+ val allBooks: List = iterator.asSequence().toList()
+
+ assertThat(allBooks).hasSize(50)
+ }
+
+ /**
+ * Scroll through the results using an index based approach where the [keyset][KeysetScrollPosition.getKeys]
+ * keeps track of already seen values to resume scrolling by altering the where clause to only return rows after the
+ * values contained in the keyset. Set logging.level.org.hibernate.SQL=debug to show the modified query in
+ * the log.
+ */
+ @Test
+ fun scrollThroughResultsWithKeyset() {
+ var window: Window
+ var scrollPosition: ScrollPosition = ScrollPosition.keyset()
+ do {
+ window = books.findTop2ByTitleContainsOrderByPublicationDate("the", scrollPosition)
+ assertThat(window.content.size).isGreaterThanOrEqualTo(1).isLessThanOrEqualTo(2)
+
+ scrollPosition = window.positionAt(window.content.size - 1)
+ } while (window.hasNext())
+ }
+
+ // --> Test Data
+ @Autowired
+ lateinit var em: EntityManager
+
+ private fun createAuthors(faker: Faker): List {
+ val authors = List(11) { id: Int ->
+ val author = Author()
+ author.id = "author-%s".format(id)
+ author.firstName = faker.name().firstName()
+ author.lastName = faker.name().lastName()
+
+ em.persist(author)
+ author
+ }
+
+ return authors
+ }
+
+ private fun createBooks(faker: Faker, authors: List): List {
+ val rand = Random()
+ return List(100) { id: Int ->
+ val book = Book()
+ book.id = "book-%03d".format(id)
+ book.title = (if (id % 2 == 0) "the-crazy-book-" else "") + faker.book().title()
+ book.isbn10 = UUID.randomUUID().toString().substring(0, 10)
+ book.publicationDate = faker.date().past(5000, TimeUnit.DAYS)
+ book.author = authors[rand.nextInt(authors.size)]
+
+ em.persist(book)
+ book
+ }
+ }
+}
diff --git a/jdbc/spring-data-jpa/src/test/kotlin/tech/ydb/jpa/simple/SimpleRepositoryTest.kt b/jdbc/spring-data-jpa/src/test/kotlin/tech/ydb/jpa/simple/SimpleRepositoryTest.kt
new file mode 100644
index 0000000..0821c50
--- /dev/null
+++ b/jdbc/spring-data-jpa/src/test/kotlin/tech/ydb/jpa/simple/SimpleRepositoryTest.kt
@@ -0,0 +1,236 @@
+package tech.ydb.jpa.simple
+
+import kotlinx.coroutines.future.await
+import kotlinx.coroutines.runBlocking
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Assertions.assertNotNull
+import org.junit.jupiter.api.Assertions.assertNull
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.dao.InvalidDataAccessApiUsageException
+import org.springframework.data.domain.Limit
+import org.springframework.data.domain.PageRequest
+import org.springframework.data.domain.Sort
+import org.springframework.transaction.annotation.Propagation
+import org.springframework.transaction.annotation.Transactional
+import tech.ydb.jpa.YdbDockerTest
+import java.util.stream.Collectors
+
+/**
+ * @author Kirill Kurdyukov
+ */
+@SpringBootTest
+@Transactional
+class SimpleRepositoryTest : YdbDockerTest() {
+
+ @Autowired
+ lateinit var repository: SimpleUserRepository
+
+ lateinit var user: User
+
+ @BeforeEach
+ fun setUp() {
+ user = User().apply {
+ username = "foobar"
+ firstname = "firstname"
+ lastname = "lastname"
+ }
+ }
+
+ @Test
+ fun findSavedUserById() {
+ user = repository.save(user)
+
+ assertThat(repository.findById(user.id)).hasValue(user)
+ }
+
+ @Test
+ fun findSavedUserByLastname() {
+ user = repository.save(user)
+
+ assertThat(repository.findByLastname("lastname")).contains(user)
+ }
+
+ @Test
+ fun findLimitedNumberOfUsersViaDerivedQuery() {
+ (0..10).forEach { _ -> repository.save(User().apply { lastname = "lastname" }) }
+
+ assertThat(repository.findByLastname("lastname", Limit.of(5))).hasSize(5)
+ }
+
+ @Test
+ fun findLimitedNumberOfUsersViaAnnotatedQuery() {
+ (0..10).forEach { _ -> repository.save(User().apply { firstname = "firstname" }) }
+
+ assertThat(repository.findByFirstname("firstname", Limit.of(5))).hasSize(5)
+ }
+
+ @Test
+ fun findByFirstnameOrLastname() {
+ user = repository.save(user)
+
+ assertThat(repository.findByFirstnameOrLastname("lastname")).contains(user)
+ }
+
+ @Test
+ fun useOptionalAsReturnAndParameterType() {
+ assertNull(repository.findByUsername("foobar"))
+
+ repository.save(user)
+
+ assertNotNull(repository.findByUsername("foobar"))
+ }
+
+ @Test
+ fun removeByLastname() {
+ // create a 2nd user with the same lastname as user
+
+ val user2 = User().apply { lastname = user.lastname }
+
+ // create a 3rd user as control group
+ val user3 = User().apply { lastname = "no-positive-match" }
+
+ repository.saveAll(listOf(user, user2, user3))
+
+ assertThat(repository.removeByLastname(user.lastname)).isEqualTo(2L)
+ assertThat(repository.existsById(user3.id)).isTrue()
+ }
+
+ @Test
+ fun useSliceToLoadContent() {
+ val totalNumberUsers = 11
+ val source: MutableList = ArrayList(totalNumberUsers)
+
+ for (i in 1..totalNumberUsers) {
+ val user = User().apply {
+ lastname = user.lastname
+ username = "${user.lastname}-${String.format("%03d", i)}"
+ }
+
+ source.add(user)
+ }
+
+ repository.saveAll(source)
+
+ val users = repository.findByLastnameOrderByUsernameAsc(user.lastname, PageRequest.of(1, 5))
+
+ assertThat(users).containsAll(source.subList(5, 10))
+ }
+
+ @Test
+ fun findFirst2ByOrderByLastnameAsc() {
+ val user0 = User().apply { lastname = "lastname-0" }
+
+ val user1 = User().apply { lastname = "lastname-1" }
+
+ val user2 = User().apply { lastname = "lastname-2" }
+
+ // we deliberately save the items in reverse
+ repository.saveAll(listOf(user2, user1, user0))
+
+ val result = repository.findFirst2ByOrderByLastnameAsc()
+
+ assertThat(result).containsExactly(user0, user1)
+ }
+
+ @Test
+ fun findTop2ByWithSort() {
+ val user0 = User().apply { lastname = "lastname-0" }
+
+ val user1 = User().apply { lastname = "lastname-1" }
+
+ val user2 = User().apply { lastname = "lastname-2" }
+
+ // we deliberately save the items in reverse
+ repository.saveAll(listOf(user2, user1, user0))
+
+ val resultAsc = repository.findTop2By(Sort.by(Sort.Direction.ASC, "lastname"))
+
+ assertThat(resultAsc).containsExactly(user0, user1)
+
+ val resultDesc = repository.findTop2By(Sort.by(Sort.Direction.DESC, "lastname"))
+
+ assertThat(resultDesc).containsExactly(user2, user1)
+ }
+
+ @Test
+ fun findByFirstnameOrLastnameUsingSpEL() {
+ val first = User().apply { lastname = "lastname" }
+
+ val second = User().apply { firstname = "firstname" }
+
+ val third = User()
+
+ repository.saveAll(listOf(first, second, third))
+
+ val reference = User().apply { firstname = "firstname"; lastname = "lastname" }
+
+ val users = repository.findByFirstnameOrLastname(reference)
+
+ assertThat(users).contains(first)
+ assertThat(users).contains(second)
+ assertThat(users).hasSize(2)
+ }
+
+ /**
+ * Streaming data from the store by using a repository method that returns a [Stream]. Note, that since the
+ * resulting [Stream] contains state it needs to be closed explicitly after use!
+ */
+ @Test
+ fun useJava8StreamsWithCustomQuery() {
+ val user1 = repository.save(User().apply { firstname = "Customer1"; lastname = "Foo" })
+ val user2 = repository.save(User().apply { firstname = "Customer2"; lastname = "Bar" })
+
+ repository.streamAllCustomers().use { stream ->
+ assertThat(stream.collect(Collectors.toList())).contains(user1, user2)
+ }
+ }
+
+ /**
+ * Streaming data from the store by using a repository method that returns a [Stream] with a derived query.
+ * Note, that since the resulting [Stream] contains state it needs to be closed explicitly after use!
+ */
+ @Test
+ fun useJava8StreamsWithDerivedQuery() {
+ val user1 = repository.save(User().apply { firstname = "Customer1"; lastname = "Foo" })
+ val user2 = repository.save(User().apply { firstname = "Customer2"; lastname = "Bar" })
+
+ repository.findAllByLastnameIsNotNull().use { stream ->
+ assertThat(stream.collect(Collectors.toList())).contains(user1, user2)
+ }
+ }
+
+ /**
+ * Query methods using streaming need to be used inside a surrounding transaction to keep the connection open while
+ * the stream is consumed. We simulate that not being the case by actively disabling the transaction here.
+ */
+ @Test
+ @Transactional(propagation = Propagation.NOT_SUPPORTED)
+ fun rejectsStreamExecutionIfNoSurroundingTransactionActive() {
+ Assertions.assertThrows(InvalidDataAccessApiUsageException::class.java) {
+ repository.findAllByLastnameIsNotNull()
+ }
+ }
+
+ /**
+ * Here we demonstrate the usage of [CompletableFuture] as a result wrapper for asynchronous repository query
+ * methods. Note, that we need to disable the surrounding transaction to be able to asynchronously read the written
+ * data from another thread within the same test method.
+ */
+ @Test
+ @Transactional(propagation = Propagation.NOT_SUPPORTED)
+ fun supportsCompletableFuturesAsReturnTypeWrapper() {
+ repository.save(User().apply { firstname = "Customer1"; lastname = "Foo" })
+ repository.save(User().apply { firstname = "Customer2"; lastname = "Bar" })
+
+ runBlocking {
+ val users = repository.readAllBy().await()
+ assertThat(users).hasSize(2)
+ }
+
+ repository.deleteAll()
+ }
+}
\ No newline at end of file
diff --git a/jdbc/spring-data-jpa/src/test/resources/application-test.properties b/jdbc/spring-data-jpa/src/test/resources/application-test.properties
new file mode 100644
index 0000000..4abc7c5
--- /dev/null
+++ b/jdbc/spring-data-jpa/src/test/resources/application-test.properties
@@ -0,0 +1,5 @@
+spring.jpa.show-sql=true
+spring.jpa.properties.hibernate.format_sql=false
+spring.jpa.properties.hibernate.highlight_sql=true
+
+spring.jpa.properties.hibernate.hbm2ddl.auto=create
\ No newline at end of file
diff --git a/jdbc/spring-data-jpa/src/test/resources/logback.xml b/jdbc/spring-data-jpa/src/test/resources/logback.xml
new file mode 100644
index 0000000..5a56f30
--- /dev/null
+++ b/jdbc/spring-data-jpa/src/test/resources/logback.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ %d %5p %40.40c:%4L - %m%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 1dfcae2..1f1188a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,10 +15,10 @@
UTF-8
- 2.20.0
+ 2.22.11.82
- 2.1.7
+ 2.1.11
@@ -66,7 +66,7 @@
org.apache.maven.pluginsmaven-compiler-plugin
- 3.11.0
+ 3.12.11.8
@@ -81,7 +81,7 @@
org.apache.maven.pluginsmaven-surefire-plugin
- 3.1.0
+ 3.2.5true
@@ -91,7 +91,7 @@
org.apache.maven.pluginsmaven-dependency-plugin
- 3.5.0
+ 3.6.1copy-dependencies
diff --git a/ydb-cookbook/src/main/java/tech/ydb/examples/simple/AlterTable.java b/ydb-cookbook/src/main/java/tech/ydb/examples/simple/AlterTable.java
index 6242f43..3b9dada 100644
--- a/ydb-cookbook/src/main/java/tech/ydb/examples/simple/AlterTable.java
+++ b/ydb-cookbook/src/main/java/tech/ydb/examples/simple/AlterTable.java
@@ -1,6 +1,7 @@
package tech.ydb.examples.simple;
import java.time.Duration;
+
import tech.ydb.core.grpc.GrpcTransport;
import tech.ydb.examples.SimpleExample;
import tech.ydb.table.Session;
@@ -8,7 +9,6 @@
import tech.ydb.table.description.TableColumn;
import tech.ydb.table.description.TableDescription;
import tech.ydb.table.settings.AlterTableSettings;
-import tech.ydb.table.values.OptionalType;
import tech.ydb.table.values.PrimitiveType;
@@ -42,8 +42,8 @@ protected void run(GrpcTransport transport, String pathPrefix) {
session.alterTable(tablePath, new AlterTableSettings()
.setTraceId("some-trace-id")
- .addColumn("name", OptionalType.of(PrimitiveType.Text))
- .addColumn("age", OptionalType.of(PrimitiveType.Uint32))
+ .addNullableColumn("name", PrimitiveType.Text)
+ .addNullableColumn("age", PrimitiveType.Uint32)
.dropColumn("value")
).join().expectSuccess("cannot alter table");