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")

Reply via email to