This is an automated email from the ASF dual-hosted git repository.
mpochatkin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git
The following commit(s) were added to refs/heads/main by this push:
new 606e368828 IGNITE-23951 Spring Data JDBC support for Ignite (#4968)
606e368828 is described below
commit 606e3688281a6e38677c1a27d5b4285ae9955936
Author: Maksim Myskov <[email protected]>
AuthorDate: Tue Jan 7 13:58:42 2025 +0300
IGNITE-23951 Spring Data JDBC support for Ignite (#4968)
---
gradle/libs.versions.toml | 2 +
modules/spring/spring-data-ignite/build.gradle | 40 +
.../java/org/apache/ignite/data/IgniteDialect.java | 125 ++++
.../apache/ignite/data/IgniteDialectProvider.java | 33 +
.../src/main/resources/META-INF/spring.factories | 1 +
.../org/apache/ignite/data/SpringDataJdbcTest.java | 815 +++++++++++++++++++++
.../org/apache/ignite/data/TestApplication.java | 30 +
.../ignite/data/repository/Intermediate.java | 103 +++
.../org/apache/ignite/data/repository/Leaf.java | 75 ++
.../org/apache/ignite/data/repository/Person.java | 154 ++++
.../ignite/data/repository/PersonRepository.java | 65 ++
.../org/apache/ignite/data/repository/Root.java | 97 +++
.../ignite/data/repository/RootRepository.java | 28 +
.../src/test/resources/META-INF/spring.factories | 1 +
.../src/test/resources/application.properties | 4 +
settings.gradle | 2 +
16 files changed, 1575 insertions(+)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index bc8a48fa35..ddee42b0d4 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -85,6 +85,7 @@ tree-sitter-sql = "gh-pages-a"
tree-sitter-hocon = "master-a"
otel = "1.45.0"
spring-boot = "3.4.1"
+spring-data = "3.4.0"
#Tools
pmdTool = "6.55.0"
@@ -275,3 +276,4 @@ opentelemetry-exporter-otlp = { module =
"io.opentelemetry:opentelemetry-exporte
spring-boot = { module = "org.springframework.boot:spring-boot", version.ref =
"spring-boot" }
spring-boot-autoconfigure = { module =
"org.springframework.boot:spring-boot-autoconfigure", version.ref =
"spring-boot" }
spring-boot-test = { module = "org.springframework.boot:spring-boot-test",
version.ref = "spring-boot" }
+spring-data-jdbc = { module = "org.springframework.data:spring-data-jdbc",
version.ref = "spring-data"}
diff --git a/modules/spring/spring-data-ignite/build.gradle
b/modules/spring/spring-data-ignite/build.gradle
new file mode 100644
index 0000000000..bcb6248800
--- /dev/null
+++ b/modules/spring/spring-data-ignite/build.gradle
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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.
+ */
+
+apply from: "$rootDir/buildscripts/java-core.gradle"
+apply from: "$rootDir/buildscripts/publishing.gradle"
+apply from: "$rootDir/buildscripts/java-junit5.gradle"
+
+description = "spring-data-ignite"
+
+java {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+}
+
+dependencies {
+ implementation project(':ignite-client')
+ implementation project(":ignite-jdbc")
+ implementation libs.spring.boot
+ implementation libs.spring.boot.autoconfigure
+ implementation libs.spring.data.jdbc
+
+ testImplementation libs.spring.boot.test
+ testImplementation libs.assertj.core
+ testImplementation testFixtures(project(':ignite-runner'))
+ testImplementation testFixtures(project(':ignite-core'))
+}
diff --git
a/modules/spring/spring-data-ignite/src/main/java/org/apache/ignite/data/IgniteDialect.java
b/modules/spring/spring-data-ignite/src/main/java/org/apache/ignite/data/IgniteDialect.java
new file mode 100644
index 0000000000..d5b9f27a2f
--- /dev/null
+++
b/modules/spring/spring-data-ignite/src/main/java/org/apache/ignite/data/IgniteDialect.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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.apache.ignite.data;
+
+import java.util.Collections;
+import java.util.Set;
+import org.springframework.data.relational.core.dialect.AbstractDialect;
+import org.springframework.data.relational.core.dialect.ArrayColumns;
+import org.springframework.data.relational.core.dialect.LimitClause;
+import org.springframework.data.relational.core.dialect.LockClause;
+import org.springframework.data.relational.core.sql.IdentifierProcessing;
+import
org.springframework.data.relational.core.sql.IdentifierProcessing.LetterCasing;
+import
org.springframework.data.relational.core.sql.IdentifierProcessing.Quoting;
+import org.springframework.data.relational.core.sql.LockOptions;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+
+/**
+ * Implementation of Ignite-specific dialect.
+ */
+public class IgniteDialect extends AbstractDialect {
+
+ /**
+ * Singleton instance.
+ */
+ public static final IgniteDialect INSTANCE = new IgniteDialect();
+
+ private IgniteDialect() {}
+
+ private static final LimitClause LIMIT_CLAUSE = new LimitClause() {
+ @Override
+ public String getLimit(long limit) {
+ return "LIMIT " + limit;
+ }
+
+ @Override
+ public String getOffset(long offset) {
+ return "OFFSET " + offset;
+ }
+
+ @Override
+ public String getLimitOffset(long limit, long offset) {
+ return String.format("OFFSET %d ROWS FETCH FIRST %d ROWS ONLY",
offset, limit);
+ }
+
+ @Override
+ public Position getClausePosition() {
+ return Position.AFTER_ORDER_BY;
+ }
+ };
+
+ static class IgniteArrayColumns implements ArrayColumns {
+ @Override
+ public boolean isSupported() {
+ return true;
+ }
+
+ @Override
+ public Class<?> getArrayType(Class<?> userType) {
+ Assert.notNull(userType, "Array component type must not be null");
+
+ return ClassUtils.resolvePrimitiveIfNecessary(userType);
+ }
+ }
+
+ @Override
+ public LimitClause limit() {
+ return LIMIT_CLAUSE;
+ }
+
+ static final LockClause LOCK_CLAUSE = new LockClause() {
+
+ @Override
+ public String getLock(LockOptions lockOptions) {
+ return "";
+ }
+
+ @Override
+ public Position getClausePosition() {
+ return Position.AFTER_ORDER_BY;
+ }
+ };
+
+ @Override
+ public LockClause lock() {
+ return LOCK_CLAUSE;
+ }
+
+ private final IgniteArrayColumns arrayColumns = new IgniteArrayColumns();
+
+ @Override
+ public ArrayColumns getArraySupport() {
+ return arrayColumns;
+ }
+
+ @Override
+ public IdentifierProcessing getIdentifierProcessing() {
+ return IdentifierProcessing.create(Quoting.ANSI,
LetterCasing.UPPER_CASE);
+ }
+
+ @Override
+ public Set<Class<?>> simpleTypes() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public boolean supportsSingleQueryLoading() {
+ return false;
+ }
+}
diff --git
a/modules/spring/spring-data-ignite/src/main/java/org/apache/ignite/data/IgniteDialectProvider.java
b/modules/spring/spring-data-ignite/src/main/java/org/apache/ignite/data/IgniteDialectProvider.java
new file mode 100644
index 0000000000..900aed50c9
--- /dev/null
+++
b/modules/spring/spring-data-ignite/src/main/java/org/apache/ignite/data/IgniteDialectProvider.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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.apache.ignite.data;
+
+import java.util.Optional;
+import org.springframework.data.jdbc.repository.config.DialectResolver;
+import org.springframework.data.relational.core.dialect.Dialect;
+import org.springframework.jdbc.core.JdbcOperations;
+
+/**
+ * Provider for Ignite-specific dialect.
+ */
+public class IgniteDialectProvider implements
DialectResolver.JdbcDialectProvider {
+
+ @Override public Optional<Dialect> getDialect(JdbcOperations operations) {
+ return Optional.of(IgniteDialect.INSTANCE);
+ }
+}
diff --git
a/modules/spring/spring-data-ignite/src/main/resources/META-INF/spring.factories
b/modules/spring/spring-data-ignite/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..ee5bcab7a4
--- /dev/null
+++
b/modules/spring/spring-data-ignite/src/main/resources/META-INF/spring.factories
@@ -0,0 +1 @@
+org.springframework.data.jdbc.repository.config.DialectResolver$JdbcDialectProvider=org.apache.ignite.data.IgniteDialectProvider
diff --git
a/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/SpringDataJdbcTest.java
b/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/SpringDataJdbcTest.java
new file mode 100644
index 0000000000..669233e5e0
--- /dev/null
+++
b/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/SpringDataJdbcTest.java
@@ -0,0 +1,815 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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.apache.ignite.data;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.SoftAssertions.assertSoftly;
+
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Stream;
+import org.apache.ignite.data.repository.Person;
+import org.apache.ignite.data.repository.Person.Direction;
+import org.apache.ignite.data.repository.PersonRepository;
+import org.apache.ignite.data.repository.Root;
+import org.apache.ignite.data.repository.RootRepository;
+import org.apache.ignite.internal.Cluster;
+import org.apache.ignite.internal.ClusterConfiguration;
+import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
+import org.apache.ignite.internal.testframework.WorkDirectory;
+import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.dao.IncorrectResultSizeDataAccessException;
+import org.springframework.data.domain.Example;
+import org.springframework.data.domain.ExampleMatcher;
+import org.springframework.data.domain.Limit;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.ScrollPosition;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Window;
+import org.springframework.data.jdbc.core.mapping.AggregateReference;
+import org.springframework.data.support.WindowIterator;
+import org.springframework.data.util.Streamable;
+
+/**
+ * This is a subset of Spring Data JDBC tests adapted from
+ * <a
href="https://github.com/spring-projects/spring-data-relational/tree/main/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository">
+ * Spring Data JDBC repo</a>.
+ */
+@SpringBootTest(classes = TestApplication.class)
+@ExtendWith(WorkDirectoryExtension.class)
+public class SpringDataJdbcTest extends BaseIgniteAbstractTest {
+
+ @WorkDirectory
+ private static Path workDir;
+ private static Cluster cluster;
+
+ @Autowired
+ PersonRepository repository;
+
+ @Autowired
+ RootRepository rootRepository;
+
+ @BeforeAll
+ static void setUp(TestInfo testInfo) {
+ ClusterConfiguration clusterConfiguration =
ClusterConfiguration.builder(testInfo, workDir).build();
+
+ cluster = new Cluster(clusterConfiguration);
+ cluster.startAndInit(1);
+
+ cluster.aliveNode().sql().execute(null, "CREATE TABLE IF NOT EXISTS
Person ("
+ + " id INT,"
+ + " name VARCHAR,"
+ + " flag BOOLEAN,"
+ + " ref BIGINT,"
+ + " direction VARCHAR,"
+ + " Primary key(id)"
+ + ");");
+
+ cluster.aliveNode().sql().execute(null, "CREATE TABLE IF NOT EXISTS
ROOT ("
+ + " ID BIGINT PRIMARY KEY,"
+ + " NAME VARCHAR(100)"
+ + ");");
+ cluster.aliveNode().sql().execute(null, "CREATE TABLE IF NOT EXISTS
INTERMEDIATE ("
+ + " ID BIGINT PRIMARY KEY,"
+ + " NAME VARCHAR(100),"
+ + " ROOT BIGINT,"
+ + " ROOT_ID BIGINT,"
+ + " ROOT_KEY INTEGER"
+ + ");");
+ cluster.aliveNode().sql().execute(null, "CREATE TABLE IF NOT EXISTS
LEAF ("
+ + " ID BIGINT PRIMARY KEY,"
+ + " NAME VARCHAR(100),"
+ + " INTERMEDIATE BIGINT,"
+ + " INTERMEDIATE_ID BIGINT,"
+ + " INTERMEDIATE_KEY INTEGER"
+ + ");");
+ }
+
+ @BeforeEach
+ void setupEach() {
+ repository.deleteAll();
+ rootRepository.deleteAll();
+ }
+
+ @Test
+ public void savesAnEntity() {
+ repository.save(Person.create());
+
+ assertThat(repository.count()).isEqualTo(1);
+ }
+
+ @Test
+ public void saveAndLoadAnEntity() {
+ Person p1 = repository.save(Person.create());
+
+ assertThat(repository.findById(p1.getId())).hasValueSatisfying(it -> {
+
+ assertThat(it.getId()).isEqualTo(p1.getId());
+ assertThat(it.getName()).isEqualTo(p1.getName());
+ });
+ }
+
+ @Test
+ public void insertsManyEntities() {
+ Person p1 = Person.create();
+ Person p2 = Person.create();
+
+ repository.saveAll(asList(p1, p2));
+
+ assertThat(repository.findAll())
+ .extracting(Person::getId)
+ .containsExactlyInAnyOrder(p1.getId(), p2.getId());
+ }
+
+ @Test
+ public void existsReturnsTrueIffEntityExists() {
+ Person p1 = repository.save(Person.create());
+
+ assertThat(repository.existsById(p1.getId())).isTrue();
+ assertThat(repository.existsById(p1.getId() + 1)).isFalse();
+ }
+
+ @Test
+ public void findAllFindsAllEntities() {
+ Person p1 = repository.save(Person.create());
+ Person p2 = repository.save(Person.create());
+
+ Iterable<Person> all = repository.findAll();
+
+ assertThat(all)
+ .extracting(Person::getId)
+ .containsExactlyInAnyOrder(p1.getId(), p2.getId());
+ }
+
+ @Test
+ public void findAllFindsAllSpecifiedEntities() {
+ Person p1 = repository.save(Person.create());
+ Person p2 = repository.save(Person.create());
+
+ assertThat(repository.findAllById(asList(p1.getId(), p2.getId())))
+ .extracting(Person::getId)
+ .containsExactlyInAnyOrder(p1.getId(), p2.getId());
+ }
+
+ @Test
+ public void countsEntities() {
+ repository.save(Person.create());
+ repository.save(Person.create());
+ repository.save(Person.create());
+
+ assertThat(repository.count()).isEqualTo(3L);
+ }
+
+ @Test
+ public void deleteById() {
+ Person p1 = repository.save(Person.create());
+ Person p2 = repository.save(Person.create());
+ Person p3 = repository.save(Person.create());
+
+ repository.deleteById(p2.getId());
+
+ assertThat(repository.findAll())
+ .extracting(Person::getId)
+ .containsExactlyInAnyOrder(p1.getId(), p3.getId());
+ }
+
+ @Test
+ public void deleteByEntity() {
+ Person p1 = repository.save(Person.create());
+ Person p2 = repository.save(Person.create());
+ Person p3 = repository.save(Person.create());
+
+ repository.delete(p1);
+
+ assertThat(repository.findAll())
+ .extracting(Person::getId)
+ .containsExactlyInAnyOrder(p2.getId(), p3.getId());
+ }
+
+ @Test
+ public void deleteByList() {
+ Person p1 = repository.save(Person.create());
+ Person p2 = repository.save(Person.create());
+ Person p3 = repository.save(Person.create());
+
+ repository.deleteAll(asList(p1, p3));
+
+ assertThat(repository.findAll())
+ .extracting(Person::getId)
+ .containsExactlyInAnyOrder(p2.getId());
+ }
+
+ @Test
+ public void deleteByIdList() {
+ Person p1 = repository.save(Person.create());
+ Person p2 = repository.save(Person.create());
+ Person p3 = repository.save(Person.create());
+
+ repository.deleteAllById(asList(p1.getId(), p3.getId()));
+
+ assertThat(repository.findAll())
+ .extracting(Person::getId)
+ .containsExactlyInAnyOrder(p2.getId());
+ }
+
+ @Test
+ public void deleteAll() {
+ repository.save(Person.create());
+ repository.save(Person.create());
+ repository.save(Person.create());
+
+ assertThat(repository.findAll()).isNotEmpty();
+
+ repository.deleteAll();
+
+ assertThat(repository.findAll()).isEmpty();
+ }
+
+ @Test
+ public void update() {
+ Person p1 = repository.save(Person.create());
+
+ p1.setName("something else");
+ p1.setNew(false);
+ Person saved = repository.save(p1);
+
+ assertThat(repository.findById(p1.getId())).hasValueSatisfying(it -> {
+ assertThat(it.getName()).isEqualTo(saved.getName());
+ });
+ }
+
+ @Test
+ public void updateMany() {
+ Person p1 = repository.save(Person.create());
+ Person p2 = repository.save(Person.create());
+
+ p1.setName("something else");
+ p1.setNew(false);
+ p2.setName("others Name");
+ p2.setNew(false);
+
+ repository.saveAll(asList(p1, p2));
+
+ assertThat(repository.findAll())
+ .extracting(Person::getName)
+ .containsExactlyInAnyOrder(p1.getName(), p2.getName());
+ }
+
+ @Test
+ void insertsOrUpdatesManyEntities() {
+ Person p1 = repository.save(Person.create());
+ p1.setName("something else");
+ p1.setNew(false);
+ Person p2 = Person.create();
+ p2.setName("others name");
+ repository.saveAll(asList(p2, p1));
+
+ assertThat(repository.findAll())
+ .extracting(Person::getName)
+ .containsExactlyInAnyOrder(p1.getName(), p2.getName());
+ }
+
+ @Test
+ public void findByIdReturnsEmptyWhenNoneFound() {
+ // NOT saving anything, so DB is empty
+
+ assertThat(repository.findById(-1L)).isEmpty();
+ }
+
+ @Test
+ public void existsWorksAsExpected() {
+ Person p1 = repository.save(Person.create());
+
+ assertSoftly(softly -> {
+
+ softly.assertThat(repository.existsByName(p1.getName()))
+ .describedAs("Positive")
+ .isTrue();
+ softly.assertThat(repository.existsByName("not an existing name"))
+ .describedAs("Positive")
+ .isFalse();
+ });
+ }
+
+ @Test
+ public void existsInWorksAsExpected() {
+ Person p1 = repository.save(Person.create());
+
+ assertSoftly(softly -> {
+
+ softly.assertThat(repository.existsByNameIn(p1.getName()))
+ .describedAs("Positive")
+ .isTrue();
+ softly.assertThat(repository.existsByNameIn())
+ .describedAs("Negative")
+ .isFalse();
+ });
+ }
+
+ @Test
+ public void existsNotInWorksAsExpected() {
+ Person dummy = repository.save(Person.create());
+
+ assertSoftly(softly -> {
+
+ softly.assertThat(repository.existsByNameNotIn(dummy.getName()))
+ .describedAs("Positive")
+ .isFalse();
+ softly.assertThat(repository.existsByNameNotIn())
+ .describedAs("Negative")
+ .isTrue();
+ });
+ }
+
+ @Test
+ public void countByQueryDerivation() {
+ Person p1 = Person.create();
+ Person p2 = Person.create();
+ p2.setName("other");
+ Person three = Person.create();
+
+ repository.saveAll(asList(p1, p2, three));
+
+ assertThat(repository.countByName(p1.getName())).isEqualTo(2);
+ }
+
+ @Test
+ public void pageByNameShouldReturnCorrectResult() {
+ repository.saveAll(asList(Person.create("a1"), Person.create("a2"),
Person.create("a3")));
+
+ Page<Person> page = repository.findPageByNameContains("a",
PageRequest.of(0, 5));
+
+ assertThat(page.getContent()).hasSize(3);
+ assertThat(page.getTotalElements()).isEqualTo(3);
+ assertThat(page.getTotalPages()).isEqualTo(1);
+
+ assertThat(repository.findPageByNameContains("a", PageRequest.of(0,
2)).getContent()).hasSize(2);
+ assertThat(repository.findPageByNameContains("a", PageRequest.of(1,
2)).getContent()).hasSize(1);
+ }
+
+ @Test
+ public void selectWithLimitShouldReturnCorrectResult() {
+ repository.saveAll(asList(Person.create("a1"), Person.create("a2"),
Person.create("a3")));
+
+ List<Person> page = repository.findByNameContains("a", Limit.of(3));
+ assertThat(page).hasSize(3);
+
+ assertThat(repository.findByNameContains("a", Limit.of(2))).hasSize(2);
+ assertThat(repository.findByNameContains("a",
Limit.unlimited())).hasSize(3);
+ }
+
+ @Test
+ public void sliceByNameShouldReturnCorrectResult() {
+ repository.saveAll(asList(Person.create("a1"), Person.create("a2"),
Person.create("a3")));
+
+ Slice<Person> slice = repository.findSliceByNameContains("a",
PageRequest.of(0, 5));
+
+ assertThat(slice.getContent()).hasSize(3);
+ assertThat(slice.hasNext()).isFalse();
+
+ slice = repository.findSliceByNameContains("a", PageRequest.of(0, 2));
+
+ assertThat(slice.getContent()).hasSize(2);
+ assertThat(slice.hasNext()).isTrue();
+ }
+
+ @Test
+ void derivedQueryWithBooleanLiteralFindsCorrectValues() {
+ repository.save(Person.create());
+ Person p1 = Person.create();
+ p1.setFlag(true);
+ p1 = repository.save(p1);
+
+ List<Person> result = repository.findByFlagTrue();
+
+
assertThat(result).extracting(Person::getId).containsExactly(p1.getId());
+ }
+
+ @Test
+ void queryBySimpleReference() {
+ Person p1 = repository.save(Person.create());
+ Person p2 = Person.create();
+ p2.setRef(AggregateReference.to(p1.getId()));
+ p2 = repository.save(p2);
+
+ List<Person> result = repository.findByRef(p1.getId().intValue());
+
+
assertThat(result).extracting(Person::getId).containsExactly(p2.getId());
+ }
+
+ @Test
+ void queryByAggregateReference() {
+ Person p1 = repository.save(Person.create());
+ Person p2 = Person.create();
+ p2.setRef(AggregateReference.to(p1.getId()));
+ p2 = repository.save(p2);
+
+ List<Person> result = repository.findByRef(p2.getRef());
+
+
assertThat(result).extracting(Person::getId).containsExactly(p2.getId());
+ }
+
+
+ @Test
+ void queryByEnumTypeIn() {
+ Person p1 = Person.create("p1");
+ p1.setDirection(Direction.LEFT);
+ Person p2 = Person.create("p2");
+ p2.setDirection(Direction.CENTER);
+ Person p3 = Person.create("p3");
+ p3.setDirection(Direction.RIGHT);
+ repository.saveAll(asList(p1, p2, p3));
+
+ assertThat(repository.findByEnumTypeIn(Set.of(Direction.LEFT,
Direction.RIGHT)))
+
.extracting(Person::getDirection).containsExactlyInAnyOrder(Direction.LEFT,
Direction.RIGHT);
+ }
+
+ @Test
+ void queryByEnumTypeEqual() {
+ Person p1 = Person.create("p1");
+ p1.setDirection(Direction.LEFT);
+ Person p2 = Person.create("p2");
+ p2.setDirection(Direction.CENTER);
+ Person p3 = Person.create("p3");
+ p3.setDirection(Direction.RIGHT);
+ repository.saveAll(asList(p1, p2, p3));
+
+
assertThat(repository.findByEnumType(Direction.CENTER)).extracting(Person::getDirection)
+ .containsExactlyInAnyOrder(Direction.CENTER);
+ }
+
+ @Test
+ void manyInsertsWithNestedEntities() {
+ Root root1 = Root.create("root1");
+ Root root2 = Root.create("root2");
+
+ List<Root> savedRoots = rootRepository.saveAll(asList(root1, root2));
+
+ List<Root> reloadedRoots = rootRepository.findAllByOrderByIdAsc();
+ assertThat(reloadedRoots).isEqualTo(savedRoots);
+ assertThat(reloadedRoots).hasSize(2);
+ }
+
+ @Test
+ void findOneByExampleShouldGetOne() {
+ Person p1 = Person.create();
+ p1.setFlag(true);
+ repository.save(p1);
+
+ Person p2 = Person.create();
+ p2.setName("Diego");
+ repository.save(p2);
+
+ Example<Person> diegoExample = Example.of(p2);
+ Optional<Person> foundExampleDiego = repository.findOne(diegoExample);
+
+ assertThat(foundExampleDiego.get().getName()).isEqualTo("Diego");
+ }
+
+ @Test
+ void findOneByExampleMultipleMatchShouldGetOne() {
+ repository.save(Person.create());
+ repository.save(Person.create());
+
+ Example<Person> example = Example.of(new Person());
+
+ assertThatThrownBy(() ->
repository.findOne(example)).isInstanceOf(IncorrectResultSizeDataAccessException.class)
+ .hasMessageContaining("expected 1, actual 2");
+ }
+
+ @Test
+ void findOneByExampleShouldGetNone() {
+ Person p1 = Person.create();
+ p1.setFlag(true);
+ repository.save(p1);
+
+ Example<Person> diegoExample =
Example.of(Person.create("NotExisting"));
+
+ Optional<Person> foundExampleDiego = repository.findOne(diegoExample);
+
+ assertThat(foundExampleDiego).isNotPresent();
+ }
+
+ @Test
+ void findAllByExampleShouldGetOne() {
+ Person p1 = Person.create();
+ p1.setFlag(true);
+ repository.save(p1);
+
+ Person p2 = Person.create();
+ p2.setName("Diego");
+ repository.save(p2);
+
+ Example<Person> example = Example.of(Person.create(null, "Diego"));
+
+ Iterable<Person> allFound = repository.findAll(example);
+
+ assertThat(allFound).extracting(Person::getName)
+ .containsExactly(example.getProbe().getName());
+ }
+
+ @Test
+ void findAllByExampleMultipleMatchShouldGetOne() {
+ repository.save(Person.create());
+ repository.save(Person.create());
+
+ Example<Person> example = Example.of(new Person(null, "Name"));
+
+ Iterable<Person> allFound = repository.findAll(example);
+
+ assertThat(allFound)
+ .hasSize(2)
+ .extracting(Person::getName)
+ .containsOnly(example.getProbe().getName());
+ }
+
+ @Test
+ void findAllByExampleShouldGetNone() {
+ Person p1 = Person.create();
+ p1.setFlag(true);
+
+ repository.save(p1);
+
+ Example<Person> example = Example.of(Person.create("NotExisting"));
+
+ Iterable<Person> allFound = repository.findAll(example);
+
+ assertThat(allFound).isEmpty();
+ }
+
+ @Test
+ void findAllByExamplePageableShouldGetOne() {
+ Person p1 = Person.create();
+ p1.setFlag(true);
+
+ repository.save(p1);
+
+ Person p2 = Person.create();
+ p2.setName("Diego");
+
+ repository.save(p2);
+
+ Example<Person> example = Example.of(p2);
+ Pageable pageRequest = PageRequest.of(0, 10);
+
+ Iterable<Person> allFound = repository.findAll(example, pageRequest);
+
+ assertThat(allFound).extracting(Person::getName)
+ .containsExactly(example.getProbe().getName());
+ }
+
+ @Test
+ void findAllByExamplePageableMultipleMatchShouldGetOne() {
+ repository.save(Person.create());
+ repository.save(Person.create());
+
+ Example<Person> example = Example.of(new Person(null, "Name"));
+ Pageable pageRequest = PageRequest.of(0, 10);
+
+ Iterable<Person> allFound = repository.findAll(example, pageRequest);
+
+ assertThat(allFound)
+ .hasSize(2)
+ .extracting(Person::getName)
+ .containsOnly(example.getProbe().getName());
+ }
+
+ @Test
+ void findAllByExamplePageableShouldGetNone() {
+ Person p1 = Person.create();
+ p1.setFlag(true);
+
+ repository.save(p1);
+
+ Example<Person> example = Example.of(Person.create("NotExisting"));
+ Pageable pageRequest = PageRequest.of(0, 10);
+
+ Iterable<Person> allFound = repository.findAll(example, pageRequest);
+
+ assertThat(allFound).isEmpty();
+ }
+
+ @Test
+ void findAllByExamplePageableOutsidePageShouldGetNone() {
+ repository.save(Person.create());
+ repository.save(Person.create());
+
+ Example<Person> example = Example.of(Person.create());
+ Pageable pageRequest = PageRequest.of(10, 10);
+
+ Iterable<Person> allFound = repository.findAll(example, pageRequest);
+
+ assertThat(allFound)
+ .isNotNull()
+ .isEmpty();
+ }
+
+ @ParameterizedTest
+ @MethodSource("findAllByExamplePageableSource")
+ void findAllByExamplePageable(Pageable pageRequest, int size, int
totalPages, List<String> notContains) {
+ for (int i = 0; i < 100; i++) {
+ Person p = Person.create();
+ p.setFlag(true);
+ p.setName("" + i);
+
+ repository.save(p);
+ }
+
+ Person p = Person.create();
+ p.setId(null);
+ p.setName(null);
+ p.setFlag(true);
+
+ Example<Person> example = Example.of(p);
+
+ Page<Person> allFound = repository.findAll(example, pageRequest);
+
+ // page has correct size
+ assertThat(allFound)
+ .isNotNull()
+ .hasSize(size);
+
+ // correct number of total
+ assertThat(allFound.getTotalElements()).isEqualTo(100);
+
+ assertThat(allFound.getTotalPages()).isEqualTo(totalPages);
+
+ if (!notContains.isEmpty()) {
+ assertThat(allFound)
+ .extracting(Person::getName)
+ .doesNotContain(notContains.toArray(new String[0]));
+ }
+ }
+
+ static Stream<Arguments> findAllByExamplePageableSource() {
+ return Stream.of(
+ Arguments.of(PageRequest.of(0, 3), 3, 34, asList("3", "4",
"100")),
+ Arguments.of(PageRequest.of(1, 10), 10, 10, asList("9", "20",
"30")),
+ Arguments.of(PageRequest.of(2, 10), 10, 10, asList("1", "2",
"3")),
+ Arguments.of(PageRequest.of(33, 3), 1, 34,
Collections.emptyList()),
+ Arguments.of(PageRequest.of(36, 3), 0, 34,
Collections.emptyList()),
+ Arguments.of(PageRequest.of(0, 10000), 100, 1,
Collections.emptyList()),
+ Arguments.of(PageRequest.of(100, 10000), 0, 1,
Collections.emptyList())
+ );
+ }
+
+ @Test
+ void existsByExampleShouldGetOne() {
+ Person p1 = Person.create();
+ p1.setFlag(true);
+ repository.save(p1);
+
+ Person p2 = Person.create();
+ p2.setName("Diego");
+ repository.save(p2);
+
+ Example<Person> example = Example.of(Person.create(null, "Diego"));
+
+ boolean exists = repository.exists(example);
+
+ assertThat(exists).isTrue();
+ }
+
+ @Test
+ void existsByExampleMultipleMatchShouldGetOne() {
+ Person p1 = Person.create();
+ repository.save(p1);
+
+ Person p2 = Person.create();
+ repository.save(p2);
+
+ Example<Person> example = Example.of(new Person());
+
+ boolean exists = repository.exists(example);
+ assertThat(exists).isTrue();
+ }
+
+ @Test
+ void existsByExampleShouldGetNone() {
+ Person p1 = Person.create();
+ p1.setFlag(true);
+
+ repository.save(p1);
+
+ Example<Person> example = Example.of(Person.create("NotExisting"));
+
+ boolean exists = repository.exists(example);
+
+ assertThat(exists).isFalse();
+ }
+
+ @Test
+ void countByExampleShouldGetOne() {
+ Person p1 = Person.create();
+ p1.setFlag(true);
+
+ repository.save(p1);
+
+ Person p2 = Person.create();
+ p2.setName("Diego");
+
+ repository.save(p2);
+
+ Example<Person> example = Example.of(p2);
+
+ long count = repository.count(example);
+
+ assertThat(count).isOne();
+ }
+
+ @Test
+ void countByExampleMultipleMatchShouldGetOne() {
+ Person p1 = Person.create();
+ repository.save(p1);
+
+ Person p2 = Person.create();
+ repository.save(p2);
+
+ Example<Person> example = Example.of(new Person());
+
+ long count = repository.count(example);
+ assertThat(count).isEqualTo(2);
+ }
+
+ @Test
+ void countByExampleShouldGetNone() {
+ Person p1 = Person.create();
+ p1.setFlag(true);
+
+ repository.save(p1);
+
+ Example<Person> example = Example.of(Person.create("NotExisting"));
+
+ long count = repository.count(example);
+
+ assertThat(count).isNotNull().isZero();
+ }
+
+ @Test
+ void findByScrollPosition() {
+ Person p1 = Person.create("p1");
+ p1.setFlag(true);
+
+ Person p2 = Person.create("p2");
+ p2.setFlag(true);
+
+ Person p3 = Person.create("p3");
+ p3.setFlag(true);
+
+ Person p4 = Person.create("p4");
+ p4.setFlag(false);
+
+ repository.saveAll(asList(p1, p2, p3, p4));
+
+ Example<Person> example = Example.of(p1,
ExampleMatcher.matching().withIgnorePaths("name", "id"));
+
+ Window<Person> first = repository.findBy(example, q ->
q.limit(2).sortBy(Sort.by("name")))
+ .scroll(ScrollPosition.offset());
+ assertThat(first.map(Person::getName)).containsExactly("p1", "p2");
+
+ Window<Person> second = repository.findBy(example, q ->
q.limit(2).sortBy(Sort.by("name")))
+ .scroll(ScrollPosition.offset(1));
+ assertThat(second.map(Person::getName)).containsExactly("p3");
+
+ WindowIterator<Person> iterator = WindowIterator.of(
+ scrollPosition -> repository.findBy(example, q ->
q.limit(2).sortBy(Sort.by("name")).scroll(scrollPosition)))
+ .startingAt(ScrollPosition.offset());
+
+ List<String> result = Streamable.of(() ->
iterator).stream().map(Person::getName).toList();
+
+ assertThat(result).hasSize(3).containsExactly("p1", "p2", "p3");
+ }
+}
diff --git
a/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/TestApplication.java
b/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/TestApplication.java
new file mode 100644
index 0000000000..becfa6e545
--- /dev/null
+++
b/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/TestApplication.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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.apache.ignite.data;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
+
+/**
+ * Test Application.
+ */
+@EnableJdbcRepositories
+@SpringBootApplication
+public class TestApplication {
+
+}
diff --git
a/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/repository/Intermediate.java
b/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/repository/Intermediate.java
new file mode 100644
index 0000000000..151e2bc787
--- /dev/null
+++
b/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/repository/Intermediate.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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.apache.ignite.data.repository;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+
+import java.util.List;
+import java.util.Objects;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.domain.Persistable;
+import org.springframework.data.relational.core.mapping.MappedCollection;
+
+/**
+ * A class to be a part of nested entities. Root -> Intermediate -> Leaf.
+ */
+public class Intermediate implements Persistable<Long> {
+
+ @Id
+ private Long id;
+ private String name;
+ private Leaf leaf;
+ @MappedCollection(idColumn = "INTERMEDIATE_ID", keyColumn =
"INTERMEDIATE_KEY") private List<Leaf> leaves;
+
+ public Intermediate() {}
+
+ /**
+ * Constructor.
+ *
+ * @param id Id.
+ * @param name Name.
+ * @param leaf Leaf.
+ * @param leaves list of leaves.
+ */
+ public Intermediate(Long id, String name, Leaf leaf, List<Leaf> leaves) {
+ this.id = id;
+ this.name = name;
+ this.leaf = leaf;
+ this.leaves = leaves;
+ }
+
+ @Override
+ public Long getId() {
+ return this.id;
+ }
+
+ @Override
+ public boolean isNew() {
+ return true;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public Leaf getLeaf() {
+ return this.leaf;
+ }
+
+ public List<Leaf> getLeaves() {
+ return this.leaves;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Intermediate that = (Intermediate) o;
+ return Objects.equals(id, that.id) && Objects.equals(name, that.name)
&& Objects.equals(leaf, that.leaf)
+ && Objects.equals(leaves, that.leaves);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, name, leaf, leaves);
+ }
+
+ private static long idCounter = 1;
+
+ public static Intermediate createWithLeaf(String namePrefix) {
+ return new Intermediate(idCounter++, namePrefix + "Intermediate",
Leaf.create(namePrefix), emptyList());
+ }
+
+ public static Intermediate createWithLeaves(String namePrefix) {
+ return new Intermediate(idCounter++, namePrefix + "Intermediate",
null, singletonList(Leaf.create(namePrefix)));
+ }
+}
diff --git
a/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/repository/Leaf.java
b/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/repository/Leaf.java
new file mode 100644
index 0000000000..07e233d3c1
--- /dev/null
+++
b/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/repository/Leaf.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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.apache.ignite.data.repository;
+
+import java.util.Objects;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.domain.Persistable;
+
+/**
+ * A class to be a part of nested entities. Root -> Intermediate -> Leaf.
+ */
+public class Leaf implements Persistable<Long> {
+
+ @Id
+ private Long id;
+ private String name;
+
+ public Leaf(Long id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ public Leaf() {
+
+ }
+
+ @Override
+ public Long getId() {
+ return this.id;
+ }
+
+ @Override
+ public boolean isNew() {
+ return true;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Leaf leaf = (Leaf) o;
+ return Objects.equals(id, leaf.id) && Objects.equals(name, leaf.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, name);
+ }
+
+ private static long idCounter = 1;
+
+ public static Leaf create(String namePrefix) {
+ return new Leaf(idCounter++, namePrefix + "QualifiedLeaf");
+ }
+}
diff --git
a/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/repository/Person.java
b/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/repository/Person.java
new file mode 100644
index 0000000000..3630bbeefc
--- /dev/null
+++
b/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/repository/Person.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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.apache.ignite.data.repository;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.annotation.Transient;
+import org.springframework.data.domain.Persistable;
+import org.springframework.data.jdbc.core.mapping.AggregateReference;
+
+/**
+ * Test class.
+ */
+public class Person implements Persistable<Long> {
+ @Id
+ private Long id;
+
+ private String name;
+
+ private Boolean flag;
+
+ private Direction direction;
+
+ @Transient
+ private boolean isNew = true;
+
+ AggregateReference<Person, Long> ref;
+
+ public Person() {}
+
+ /**
+ * Constructor.
+ *
+ * @param id Id.
+ * @param name Name.
+ */
+ public Person(Long id, String name) {
+ this.id = id;
+ this.name = name;
+ this.direction = Direction.LEFT;
+ this.flag = false;
+ this.ref = new AggregateReference<Person, Long>() {
+ @Override
+ public Long getId() {
+ return 0L;
+ }
+ };
+ }
+
+ @Override
+ public Long getId() {
+ return id;
+ }
+
+ @Override
+ public boolean isNew() {
+ return isNew;
+ }
+
+ public void setNew(boolean newValue) {
+ isNew = newValue;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Boolean getFlag() {
+ return flag;
+ }
+
+ public void setFlag(Boolean flag) {
+ this.flag = flag;
+ }
+
+ public AggregateReference<Person, Long> getRef() {
+ return ref;
+ }
+
+ public void setRef(AggregateReference<Person, Long> ref) {
+ this.ref = ref;
+ }
+
+ public Direction getDirection() {
+ return direction;
+ }
+
+ public void setDirection(Direction direction) {
+ this.direction = direction;
+ }
+
+ /**
+ * Enum for testing purposes. Doesn't have semantic meaning.
+ */
+ public enum Direction {
+ LEFT, CENTER, RIGHT
+ }
+
+ private static long idCounter = 1;
+
+ /**
+ * Constructor.
+ * Id will be generated.
+ */
+ public static Person create() {
+ return new Person(idCounter++, "Name");
+ }
+
+ /**
+ * Constructor.
+ * Id will be generated.
+ *
+ * @param name Name.
+ */
+ public static Person create(String name) {
+ Person person = new Person(idCounter++, name);
+ person.setName(name);
+ return person;
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param id Id.
+ * @param name Name.
+ */
+ public static Person create(Long id, String name) {
+ Person person = new Person(id, name);
+ person.setName(name);
+ return person;
+ }
+}
diff --git
a/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/repository/PersonRepository.java
b/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/repository/PersonRepository.java
new file mode 100644
index 0000000000..a7805a0112
--- /dev/null
+++
b/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/repository/PersonRepository.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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.apache.ignite.data.repository;
+
+import java.util.List;
+import java.util.Set;
+import org.apache.ignite.data.repository.Person.Direction;
+import org.springframework.data.domain.Limit;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.jdbc.core.mapping.AggregateReference;
+import org.springframework.data.jdbc.repository.query.Query;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.data.repository.query.Param;
+import org.springframework.data.repository.query.QueryByExampleExecutor;
+import org.springframework.stereotype.Repository;
+
+/**
+ * Repository for {@link Person}.
+ */
+@Repository
+public interface PersonRepository extends CrudRepository<Person, Long>,
QueryByExampleExecutor<Person> {
+
+ boolean existsByName(String name);
+
+ boolean existsByNameIn(String... names);
+
+ boolean existsByNameNotIn(String... names);
+
+ int countByName(String name);
+
+ Page<Person> findPageByNameContains(String name, Pageable pageable);
+
+ List<Person> findByNameContains(String name, Limit limit);
+
+ Slice<Person> findSliceByNameContains(String name, Pageable pageable);
+
+ List<Person> findByFlagTrue();
+
+ List<Person> findByRef(int ref);
+
+ List<Person> findByRef(AggregateReference<Person, Long> ref);
+
+ @Query("SELECT * FROM PERSON WHERE DIRECTION IN (:directions)")
+ List<Person> findByEnumTypeIn(@Param("directions") Set<Direction>
directions);
+
+ @Query("SELECT * FROM PERSON WHERE DIRECTION = :direction")
+ List<Person> findByEnumType(@Param("direction")Direction direction);
+}
diff --git
a/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/repository/Root.java
b/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/repository/Root.java
new file mode 100644
index 0000000000..471a7a6f1e
--- /dev/null
+++
b/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/repository/Root.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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.apache.ignite.data.repository;
+
+import static java.util.Collections.singletonList;
+
+import java.util.List;
+import java.util.Objects;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.domain.Persistable;
+import org.springframework.data.relational.core.mapping.MappedCollection;
+
+/**
+ * A class to be a part of nested entities. Root -> Intermediate -> Leaf.
+ */
+public class Root implements Persistable<Long> {
+
+ @Id
+ private Long id;
+ private String name;
+ private Intermediate intermediate;
+ @MappedCollection(idColumn = "ROOT_ID", keyColumn = "ROOT_KEY") private
List<Intermediate> intermediates;
+
+ public Root() {}
+
+ private Root(Long id, String name, Intermediate intermediate,
List<Intermediate> intermediates) {
+ this.id = id;
+ this.name = name;
+ this.intermediate = intermediate;
+ this.intermediates = intermediates;
+ }
+
+ @Override
+ public Long getId() {
+ return this.id;
+ }
+
+ @Override
+ public boolean isNew() {
+ return true;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public Intermediate getIntermediate() {
+ return this.intermediate;
+ }
+
+ public List<Intermediate> getIntermediates() {
+ return this.intermediates;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Root root = (Root) o;
+ return Objects.equals(id, root.id) && Objects.equals(name, root.name)
&& Objects.equals(intermediate,
+ root.intermediate) && Objects.equals(intermediates,
root.intermediates);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, name, intermediate, intermediates);
+ }
+
+ private static long idCounter = 1;
+
+ /**
+ * Creates {@link Root}.
+ *
+ * @param namePrefix Name prefix.
+ */
+ public static Root create(String namePrefix) {
+ return new Root(idCounter++, namePrefix,
+ Intermediate.createWithLeaf(namePrefix),
+ singletonList(Intermediate.createWithLeaves(namePrefix)));
+ }
+}
diff --git
a/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/repository/RootRepository.java
b/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/repository/RootRepository.java
new file mode 100644
index 0000000000..b031877d9a
--- /dev/null
+++
b/modules/spring/spring-data-ignite/src/test/java/org/apache/ignite/data/repository/RootRepository.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ * http://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.apache.ignite.data.repository;
+
+import java.util.List;
+import org.springframework.data.repository.ListCrudRepository;
+
+/**
+ * Repository for {@link Root}.
+ */
+public interface RootRepository extends ListCrudRepository<Root, Long> {
+ List<Root> findAllByOrderByIdAsc();
+}
diff --git
a/modules/spring/spring-data-ignite/src/test/resources/META-INF/spring.factories
b/modules/spring/spring-data-ignite/src/test/resources/META-INF/spring.factories
new file mode 100644
index 0000000000..ee5bcab7a4
--- /dev/null
+++
b/modules/spring/spring-data-ignite/src/test/resources/META-INF/spring.factories
@@ -0,0 +1 @@
+org.springframework.data.jdbc.repository.config.DialectResolver$JdbcDialectProvider=org.apache.ignite.data.IgniteDialectProvider
diff --git
a/modules/spring/spring-data-ignite/src/test/resources/application.properties
b/modules/spring/spring-data-ignite/src/test/resources/application.properties
new file mode 100644
index 0000000000..0339ab8d9a
--- /dev/null
+++
b/modules/spring/spring-data-ignite/src/test/resources/application.properties
@@ -0,0 +1,4 @@
+ignite.client.addresses=127.0.0.1:10800
+
+spring.datasource.url=jdbc:ignite:thin://localhost
+spring.datasource.driver-class-name=org.apache.ignite.jdbc.IgniteJdbcDriver
diff --git a/settings.gradle b/settings.gradle
index a286ef494f..31b079d83c 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -180,10 +180,12 @@ if (JavaVersion.current() >= JavaVersion.VERSION_17) {
include(':spring-boot-ignite-client-autoconfigure')
include(':spring-boot-starter-ignite-client')
include(':spring-boot-starter-ignite-client-example')
+ include(':spring-data-ignite')
project(":spring-boot-starter-ignite-client-example").projectDir =
file('modules/spring/spring-boot-starter-ignite-client-example')
project(":spring-boot-ignite-client-autoconfigure").projectDir =
file('modules/spring/spring-boot-ignite-client-autoconfigure')
project(":spring-boot-starter-ignite-client").projectDir =
file('modules/spring/spring-boot-starter-ignite-client')
+ project(":spring-data-ignite").projectDir =
file('modules/spring/spring-data-ignite')
}
ext.isCiServer = System.getenv().containsKey("IGNITE_CI")