IGNITE-13559 Migrates spring-data modules to ignite-extensions. (#25)

diff --git a/.gitignore b/.gitignore
index 3a94647..82018b9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -142,3 +142,7 @@
 .history
 .vscode
 .classpath
+
+# Ignite work folder
+**/ignite/work/
+**/ignite/README.txt
diff --git a/modules/spring-data-2.0-ext/README.txt b/modules/spring-data-2.0-ext/README.txt
new file mode 100644
index 0000000..001ec34
--- /dev/null
+++ b/modules/spring-data-2.0-ext/README.txt
@@ -0,0 +1,43 @@
+Apache Ignite Spring Module
+---------------------------
+
+Apache Ignite Spring Data 2.0 extension provides an integration with Spring Data 2.0 framework.
+
+Main features:
+
+- Supports multiple Ignite instances on same JVM (@RepositoryConfig).
+- Supports query tuning parameters in @Query annotation
+- Supports projections
+- Supports Page and Stream responses
+- Supports Sql Fields Query resultset transformation into the domain entity
+- Supports named parameters (:myParam) into SQL queries, declared using @Param("myParam")
+- Supports advanced parameter binding and SpEL expressions into SQL queries:
+- Template variables:
+    - #entityName - the simple class name of the domain entity
+- Method parameter expressions: Parameters are exposed for indexed access ([0] is the first query method's param) or via the name declared using @Param. The actual SpEL expression binding is triggered by ?#. Example: ?#{[0] or ?#{#myParamName}
+- Advanced SpEL expressions: While advanced parameter binding is a very useful feature, the real power of SpEL stems from the fact, that the expressions can refer to framework abstractions or other application components through SpEL EvaluationContext extension model.
+- Supports SpEL expressions into Text queries (TextQuery).
+
+Importing Spring Data 2.0 extension In Maven Project
+----------------------------------------
+
+If you are using Maven to manage dependencies of your project, you can add Spring Data 2.0 extension
+dependency like this (replace '${ignite-spring-data_2.0-ext.version}' with actual version of Ignite Spring Data 2.0
+extension you are interested in):
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+                        http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    ...
+    <dependencies>
+        ...
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-spring-data_2.0-ext</artifactId>
+            <version>${ignite-spring-data_2.0-ext.version}</version>
+        </dependency>
+        ...
+    </dependencies>
+    ...
+</project>
diff --git a/modules/spring-data-2.0-ext/examples/config/example-default.xml b/modules/spring-data-2.0-ext/examples/config/example-default.xml
new file mode 100644
index 0000000..e6c359d
--- /dev/null
+++ b/modules/spring-data-2.0-ext/examples/config/example-default.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<!--
+    Ignite configuration with all defaults and enabled p2p deployment and enabled events.
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xsi:schemaLocation="
+        http://www.springframework.org/schema/beans
+        http://www.springframework.org/schema/beans/spring-beans.xsd
+        http://www.springframework.org/schema/util
+        http://www.springframework.org/schema/util/spring-util.xsd">
+    <bean abstract="true" id="ignite.cfg" class="org.apache.ignite.configuration.IgniteConfiguration">
+        <!-- Set to true to enable distributed class loading for examples, default is false. -->
+        <property name="peerClassLoadingEnabled" value="true"/>
+
+        <!-- Enable task execution events for examples. -->
+        <property name="includeEventTypes">
+            <list>
+                <!--Task execution events-->
+                <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_STARTED"/>
+                <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_FINISHED"/>
+                <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_FAILED"/>
+                <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_TIMEDOUT"/>
+                <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_SESSION_ATTR_SET"/>
+                <util:constant static-field="org.apache.ignite.events.EventType.EVT_TASK_REDUCED"/>
+
+                <!--Cache events-->
+                <util:constant static-field="org.apache.ignite.events.EventType.EVT_CACHE_OBJECT_PUT"/>
+                <util:constant static-field="org.apache.ignite.events.EventType.EVT_CACHE_OBJECT_READ"/>
+                <util:constant static-field="org.apache.ignite.events.EventType.EVT_CACHE_OBJECT_REMOVED"/>
+            </list>
+        </property>
+
+        <!-- Explicitly configure TCP discovery SPI to provide list of initial nodes. -->
+        <property name="discoverySpi">
+            <bean class="org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi">
+                <property name="ipFinder">
+                    <!--
+                        Ignite provides several options for automatic discovery that can be used
+                        instead os static IP based discovery. For information on all options refer
+                        to our documentation: http://apacheignite.readme.io/docs/cluster-config
+                    -->
+                    <!-- Uncomment static IP finder to enable static-based discovery of initial nodes. -->
+                    <!--<bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder">-->
+                    <bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.multicast.TcpDiscoveryMulticastIpFinder">
+                        <property name="addresses">
+                            <list>
+                                <!-- In distributed environment, replace with actual host IP address. -->
+                                <value>127.0.0.1:47500..47509</value>
+                            </list>
+                        </property>
+                    </bean>
+                </property>
+            </bean>
+        </property>
+    </bean>
+</beans>
diff --git a/modules/spring-data-2.0-ext/examples/config/example-spring-data.xml b/modules/spring-data-2.0-ext/examples/config/example-spring-data.xml
new file mode 100644
index 0000000..4f51c12
--- /dev/null
+++ b/modules/spring-data-2.0-ext/examples/config/example-spring-data.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<!--
+    Ignite Spring configuration file to startup Ignite cache.
+
+    This file demonstrates how to configure cache using Spring. Provided cache
+    will be created on node startup.
+
+    Use this configuration file when running Spring Data examples.
+
+    When starting a standalone node, you need to execute the following command:
+    {IGNITE_HOME}/bin/ignite.{bat|sh} modules/spring-data-2.0/examples/config/example-spring-data.xml
+
+    When starting Ignite from Java IDE, pass path to this file to Ignition:
+    Ignition.start("modules/spring-data-2.0/examples/config/example-spring-data.xml");
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="
+        http://www.springframework.org/schema/beans
+        http://www.springframework.org/schema/beans/spring-beans.xsd">
+    <!-- Imports default Ignite configuration -->
+    <import resource="example-default.xml"/>
+
+    <bean parent="ignite.cfg">
+        <property name="igniteInstanceName" value="springDataNode" />
+
+        <property name="cacheConfiguration">
+            <list>
+                <bean class="org.apache.ignite.configuration.CacheConfiguration">
+                    <!--
+                        Apache Ignite uses an IgniteRepository extension which inherits from Spring Data's CrudRepository.
+                        The SQL grid is also enabled to aceess Spring Data repository. The @RepositoryConfig annotation
+                        maps the PersonRepository to an Ignite's cache named "PersonCache".
+                    -->
+                    <property name="name" value="PersonCache"/>
+                    <property name="indexedTypes">
+                        <list>
+                            <value>java.lang.Long</value>
+                            <value>org.apache.ignite.springdata20.examples.model.Person</value>
+                        </list>
+                    </property>
+                </bean>
+            </list>
+        </property>
+    </bean>
+</beans>
diff --git a/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/PersonRepository.java b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/PersonRepository.java
new file mode 100644
index 0000000..2e0a729
--- /dev/null
+++ b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/PersonRepository.java
@@ -0,0 +1,59 @@
+/*
+ * 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.springdata20.examples;
+
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.springdata20.examples.model.Person;
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.apache.ignite.springdata20.repository.config.Query;
+import org.apache.ignite.springdata20.repository.config.RepositoryConfig;
+import org.springframework.data.domain.Pageable;
+
+/**
+ * Apache Ignite Spring Data repository backed by Ignite Person's cache.
+ * </p>
+ * To link the repository with an Ignite cache use {@link RepositoryConfig#cacheName()} annotation's parameter.
+ */
+@RepositoryConfig(cacheName = "PersonCache")
+public interface PersonRepository extends IgniteRepository<Person, Long> {
+    /**
+     * Gets all the persons with the given name.
+     * @param name Person name.
+     * @return A list of Persons with the given first name.
+     */
+    public List<Person> findByFirstName(String name);
+
+    /**
+     * Returns top Person with the specified surname.
+     * @param name Person surname.
+     * @return Person that satisfy the query.
+     */
+    public Cache.Entry<Long, Person> findTopByLastNameLike(String name);
+
+    /**
+     * Getting ids of all the Person satisfying the custom query from {@link Query} annotation.
+     *
+     * @param orgId Query parameter.
+     * @param pageable Pageable interface.
+     * @return A list of Persons' ids.
+     */
+    @Query("SELECT id FROM Person WHERE orgId > ?")
+    public List<Long> selectId(long orgId, Pageable pageable);
+}
+
diff --git a/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/SpringApplicationConfiguration.java b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/SpringApplicationConfiguration.java
new file mode 100644
index 0000000..d7b4a91
--- /dev/null
+++ b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/SpringApplicationConfiguration.java
@@ -0,0 +1,51 @@
+/*
+ * 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.springdata20.examples;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.apache.ignite.springdata20.repository.config.EnableIgniteRepositories;
+import org.apache.ignite.springdata20.repository.support.IgniteRepositoryFactoryBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Every {@link IgniteRepository} is bound to a specific Apache Ignite that it communicates to in order to mutate and
+ * read data via Spring Data API. To pass an instance of Apache Ignite cache to an {@link IgniteRepository} it's
+ * required to initialize {@link IgniteRepositoryFactoryBean} with on of the following:
+ * <ul>
+ * <li>{@link Ignite} instance bean named "igniteInstance"</li>
+ * <li>{@link IgniteConfiguration} bean named "igniteCfg"</li>
+ * <li>A path to Ignite's Spring XML configuration named "igniteSpringCfgPath"</li>
+ * <ul/>
+ * In this example the first approach is utilized.
+ */
+@Configuration
+@EnableIgniteRepositories
+public class SpringApplicationConfiguration {
+    /**
+     * Creating Apache Ignite instance bean. A bean will be passed to {@link IgniteRepositoryFactoryBean} to initialize
+     * all Ignite based Spring Data repositories and connect to a cluster.
+     */
+    @Bean
+    public Ignite igniteInstance() {
+        return Ignition.start("modules/spring-data-2.0-ext/examples/config/example-spring-data.xml");
+    }
+}
diff --git a/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/SpringDataExample.java b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/SpringDataExample.java
new file mode 100644
index 0000000..a3d5547
--- /dev/null
+++ b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/SpringDataExample.java
@@ -0,0 +1,150 @@
+/*
+ * 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.springdata20.examples;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.TreeMap;
+import javax.cache.Cache;
+import org.apache.ignite.springdata20.examples.model.Person;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.data.domain.PageRequest;
+
+/**
+ * The example demonstrates how to interact with an Apache Ignite cluster by means of Spring Data API.
+ *
+ * Additional cluster nodes can be started with special configuration file which
+ * enables P2P class loading: {@code 'ignite.{sh|bat} modules/spring-data-2.0/examples/config/example-spring-data.xml'}.
+ */
+public class SpringDataExample {
+    /** Spring Application Context. */
+    private static AnnotationConfigApplicationContext ctx;
+
+    /** Ignite Spring Data repository. */
+    private static PersonRepository repo;
+
+    /**
+     * Executes the example.
+     * @param args Command line arguments, none required.
+     */
+    public static void main(String[] args) {
+        // Initializing Spring Data context and Ignite repository.
+        igniteSpringDataInit();
+
+        populateRepository();
+
+        findPersons();
+
+        queryRepository();
+
+        System.out.println("\n>>> Cleaning out the repository...");
+
+        repo.deleteAll();
+
+        System.out.println("\n>>> Repository size: " + repo.count());
+
+        // Destroying the context.
+        ctx.destroy();
+    }
+
+    /**
+     * Initializes Spring Data and Ignite repositories.
+     */
+    private static void igniteSpringDataInit() {
+        ctx = new AnnotationConfigApplicationContext();
+
+        // Explicitly registering Spring configuration.
+        ctx.register(SpringApplicationConfiguration.class);
+
+        ctx.refresh();
+
+        // Getting a reference to PersonRepository.
+        repo = ctx.getBean(PersonRepository.class);
+    }
+
+    /**
+     * Fills the repository in with sample data.
+     */
+    private static void populateRepository() {
+        TreeMap<Long, Person> persons = new TreeMap<>();
+
+        persons.put(1L, new Person(1L, 2000L, "John", "Smith", 15000, "Worked for Apple"));
+        persons.put(2L, new Person(2L, 2000L, "Brad", "Pitt", 16000, "Worked for Oracle"));
+        persons.put(3L, new Person(3L, 1000L, "Mark", "Tomson", 10000, "Worked for Sun"));
+        persons.put(4L, new Person(4L, 2000L, "Erick", "Smith", 13000, "Worked for Apple"));
+        persons.put(5L, new Person(5L, 1000L, "John", "Rozenberg", 25000, "Worked for RedHat"));
+        persons.put(6L, new Person(6L, 2000L, "Denis", "Won", 35000, "Worked for CBS"));
+        persons.put(7L, new Person(7L, 1000L, "Abdula", "Adis", 45000, "Worked for NBC"));
+        persons.put(8L, new Person(8L, 2000L, "Roman", "Ive", 15000, "Worked for Sun"));
+
+        // Adding data into the repository.
+        repo.save(persons);
+
+        System.out.println("\n>>> Added " + repo.count() + " Persons into the repository.");
+    }
+
+    /**
+     * Gets a list of Persons using standard read operations.
+     */
+    private static void findPersons() {
+        // Getting Person with specific ID.
+        Person person = repo.findById(2L).orElse(null);
+
+        System.out.println("\n>>> Found Person [id=" + 2L + ", val=" + person + "]");
+
+        // Getting a list of Persons.
+
+        ArrayList<Long> ids = new ArrayList<>();
+
+        for (long i = 0; i < 5; i++)
+            ids.add(i);
+
+        Iterator<Person> persons = repo.findAllById(ids).iterator();
+
+        System.out.println("\n>>> Persons list for specific ids: ");
+
+        while (persons.hasNext())
+            System.out.println("   >>>   " + persons.next());
+    }
+
+    /**
+     * Execute advanced queries over the repository.
+     */
+    private static void queryRepository() {
+        System.out.println("\n>>> Persons with name 'John':");
+
+        List<Person> persons = repo.findByFirstName("John");
+
+        for (Person person: persons)
+            System.out.println("   >>>   " + person);
+
+
+        Cache.Entry<Long, Person> topPerson = repo.findTopByLastNameLike("Smith");
+
+        System.out.println("\n>>> Top Person with surname 'Smith': " + topPerson.getValue());
+
+
+        List<Long> ids = repo.selectId(1000L, new PageRequest(0, 4));
+
+        System.out.println("\n>>> Persons working for organization with ID > 1000: ");
+
+        for (Long id: ids)
+            System.out.println("   >>>   [id=" + id + "]");
+    }
+}
diff --git a/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/Address.java b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/Address.java
new file mode 100644
index 0000000..e7e6348
--- /dev/null
+++ b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/Address.java
@@ -0,0 +1,72 @@
+/*
+ * 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.springdata20.examples.model;
+
+import org.apache.ignite.binary.BinaryObjectException;
+import org.apache.ignite.binary.BinaryReader;
+import org.apache.ignite.binary.BinaryWriter;
+import org.apache.ignite.binary.Binarylizable;
+
+/**
+ * Employee address.
+ * <p>
+ * This class implements {@link Binarylizable} only for example purposes,
+ * in order to show how to customize serialization and deserialization of
+ * binary objects.
+ */
+public class Address implements Binarylizable {
+    /** Street. */
+    private String street;
+
+    /** ZIP code. */
+    private int zip;
+
+    /**
+     * Required for binary deserialization.
+     */
+    public Address() {
+        // No-op.
+    }
+
+    /**
+     * @param street Street.
+     * @param zip ZIP code.
+     */
+    public Address(String street, int zip) {
+        this.street = street;
+        this.zip = zip;
+    }
+
+    /** {@inheritDoc} */
+    @Override public void writeBinary(BinaryWriter writer) throws BinaryObjectException {
+        writer.writeString("street", street);
+        writer.writeInt("zip", zip);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void readBinary(BinaryReader reader) throws BinaryObjectException {
+        street = reader.readString("street");
+        zip = reader.readInt("zip");
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return "Address [street=" + street +
+            ", zip=" + zip + ']';
+    }
+}
diff --git a/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/Employee.java b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/Employee.java
new file mode 100644
index 0000000..7996793
--- /dev/null
+++ b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/Employee.java
@@ -0,0 +1,93 @@
+/*
+ * 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.springdata20.examples.model;
+
+import java.util.Collection;
+
+/**
+ * This class represents employee object.
+ */
+public class Employee {
+    /** Name. */
+    private String name;
+
+    /** Salary. */
+    private long salary;
+
+    /** Address. */
+    private Address addr;
+
+    /** Departments. */
+    private Collection<String> departments;
+
+    /**
+     * Required for binary deserialization.
+     */
+    public Employee() {
+        // No-op.
+    }
+
+    /**
+     * @param name Name.
+     * @param salary Salary.
+     * @param addr Address.
+     * @param departments Departments.
+     */
+    public Employee(String name, long salary,Address addr, Collection<String> departments) {
+        this.name = name;
+        this.salary = salary;
+        this.addr = addr;
+        this.departments = departments;
+    }
+
+    /**
+     * @return Name.
+     */
+    public String name() {
+        return name;
+    }
+
+    /**
+     * @return Salary.
+     */
+    public long salary() {
+        return salary;
+    }
+
+    /**
+     * @return Address.
+     */
+    public Address address() {
+        return addr;
+    }
+
+    /**
+     * @return Departments.
+     */
+    public Collection<String> departments() {
+        return departments;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return "Employee [name=" + name +
+            ", salary=" + salary +
+            ", address=" + addr +
+            ", departments=" + departments + ']';
+    }
+}
diff --git a/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/EmployeeKey.java b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/EmployeeKey.java
new file mode 100644
index 0000000..5d16ebd
--- /dev/null
+++ b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/EmployeeKey.java
@@ -0,0 +1,93 @@
+/*
+ * 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.springdata20.examples.model;
+
+import org.apache.ignite.cache.affinity.AffinityKeyMapped;
+
+/**
+ * This class represents key for employee object.
+ * <p>
+ * Used in query example to collocate employees
+ * with their organizations.
+ */
+public class EmployeeKey {
+    /** ID. */
+    private int id;
+
+    /** Organization ID. */
+    @AffinityKeyMapped
+    private int organizationId;
+
+    /**
+     * Required for binary deserialization.
+     */
+    public EmployeeKey() {
+        // No-op.
+    }
+
+    /**
+     * @param id ID.
+     * @param organizationId Organization ID.
+     */
+    public EmployeeKey(int id, int organizationId) {
+        this.id = id;
+        this.organizationId = organizationId;
+    }
+
+    /**
+     * @return ID.
+     */
+    public int id() {
+        return id;
+    }
+
+    /**
+     * @return Organization ID.
+     */
+    public int organizationId() {
+        return organizationId;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+
+        if (o == null || getClass() != o.getClass())
+            return false;
+
+        EmployeeKey key = (EmployeeKey)o;
+
+        return id == key.id && organizationId == key.organizationId;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        int res = id;
+
+        res = 31 * res + organizationId;
+
+        return res;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return "EmployeeKey [id=" + id +
+            ", organizationId=" + organizationId + ']';
+    }
+}
diff --git a/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/Organization.java b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/Organization.java
new file mode 100644
index 0000000..d06e05c
--- /dev/null
+++ b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/Organization.java
@@ -0,0 +1,132 @@
+/*
+ * 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.springdata20.examples.model;
+
+import java.sql.Timestamp;
+import java.util.concurrent.atomic.AtomicLong;
+import org.apache.ignite.cache.query.annotations.QuerySqlField;
+
+/**
+ * This class represents organization object.
+ */
+public class Organization {
+    /** */
+    private static final AtomicLong ID_GEN = new AtomicLong();
+
+    /** Organization ID (indexed). */
+    @QuerySqlField(index = true)
+    private Long id;
+
+    /** Organization name (indexed). */
+    @QuerySqlField(index = true)
+    private String name;
+
+    /** Address. */
+    private Address addr;
+
+    /** Type. */
+    private OrganizationType type;
+
+    /** Last update time. */
+    private Timestamp lastUpdated;
+
+    /**
+     * Required for binary deserialization.
+     */
+    public Organization() {
+        // No-op.
+    }
+
+    /**
+     * @param name Organization name.
+     */
+    public Organization(String name) {
+        id = ID_GEN.incrementAndGet();
+
+        this.name = name;
+    }
+
+    /**
+     * @param id Organization ID.
+     * @param name Organization name.
+     */
+    public Organization(long id, String name) {
+        this.id = id;
+        this.name = name;
+    }
+
+    /**
+     * @param name Name.
+     * @param addr Address.
+     * @param type Type.
+     * @param lastUpdated Last update time.
+     */
+    public Organization(String name, Address addr, OrganizationType type, Timestamp lastUpdated) {
+        id = ID_GEN.incrementAndGet();
+
+        this.name = name;
+        this.addr = addr;
+        this.type = type;
+
+        this.lastUpdated = lastUpdated;
+    }
+
+    /**
+     * @return Organization ID.
+     */
+    public Long id() {
+        return id;
+    }
+
+    /**
+     * @return Name.
+     */
+    public String name() {
+        return name;
+    }
+
+    /**
+     * @return Address.
+     */
+    public Address address() {
+        return addr;
+    }
+
+    /**
+     * @return Type.
+     */
+    public OrganizationType type() {
+        return type;
+    }
+
+    /**
+     * @return Last update time.
+     */
+    public Timestamp lastUpdated() {
+        return lastUpdated;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return "Organization [id=" + id +
+            ", name=" + name +
+            ", address=" + addr +
+            ", type=" + type +
+            ", lastUpdated=" + lastUpdated + ']';
+    }
+}
diff --git a/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/OrganizationType.java b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/OrganizationType.java
new file mode 100644
index 0000000..7bcb0c2
--- /dev/null
+++ b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/OrganizationType.java
@@ -0,0 +1,32 @@
+/*
+ * 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.springdata20.examples.model;
+
+/**
+ * Organization type enum.
+ */
+public enum OrganizationType {
+    /** Non-profit organization. */
+    NON_PROFIT,
+
+    /** Private organization. */
+    PRIVATE,
+
+    /** Government organization. */
+    GOVERNMENT
+}
diff --git a/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/Person.java b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/Person.java
new file mode 100644
index 0000000..e9250be
--- /dev/null
+++ b/modules/spring-data-2.0-ext/examples/main/java/org/apache/ignite/springdata20/examples/model/Person.java
@@ -0,0 +1,145 @@
+/*
+ * 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.springdata20.examples.model;
+
+import java.io.Serializable;
+import java.util.concurrent.atomic.AtomicLong;
+import org.apache.ignite.cache.affinity.AffinityKey;
+import org.apache.ignite.cache.query.annotations.QuerySqlField;
+import org.apache.ignite.cache.query.annotations.QueryTextField;
+
+/**
+ * Person class.
+ */
+public class Person implements Serializable {
+    /** */
+    private static final AtomicLong ID_GEN = new AtomicLong();
+
+    /** Person ID (indexed). */
+    @QuerySqlField(index = true)
+    public Long id;
+
+    /** Organization ID (indexed). */
+    @QuerySqlField(index = true)
+    public Long orgId;
+
+    /** First name (not-indexed). */
+    @QuerySqlField
+    public String firstName;
+
+    /** Last name (not indexed). */
+    @QuerySqlField
+    public String lastName;
+
+    /** Resume text (create LUCENE-based TEXT index for this field). */
+    @QueryTextField
+    public String resume;
+
+    /** Salary (indexed). */
+    @QuerySqlField(index = true)
+    public double salary;
+
+    /** Custom cache key to guarantee that person is always collocated with its organization. */
+    private transient AffinityKey<Long> key;
+
+    /**
+     * Default constructor.
+     */
+    public Person() {
+        // No-op.
+    }
+
+    /**
+     * Constructs person record.
+     *
+     * @param org       Organization.
+     * @param firstName First name.
+     * @param lastName  Last name.
+     * @param salary    Salary.
+     * @param resume    Resume text.
+     */
+    public Person(Organization org, String firstName, String lastName, double salary, String resume) {
+        // Generate unique ID for this person.
+        id = ID_GEN.incrementAndGet();
+
+        orgId = org.id();
+
+        this.firstName = firstName;
+        this.lastName = lastName;
+        this.salary = salary;
+        this.resume = resume;
+    }
+
+    /**
+     * Constructs person record.
+     *
+     * @param id Person ID.
+     * @param orgId Organization ID.
+     * @param firstName First name.
+     * @param lastName Last name.
+     * @param salary    Salary.
+     * @param resume    Resume text.
+     */
+    public Person(Long id, Long orgId, String firstName, String lastName, double salary, String resume) {
+        this.id = id;
+        this.orgId = orgId;
+        this.firstName = firstName;
+        this.lastName = lastName;
+        this.salary = salary;
+        this.resume = resume;
+    }
+
+    /**
+     * Constructs person record.
+     *
+     * @param id Person ID.
+     * @param firstName First name.
+     * @param lastName Last name.
+     */
+    public Person(Long id, String firstName, String lastName) {
+        this.id = id;
+
+        this.firstName = firstName;
+        this.lastName = lastName;
+    }
+
+    /**
+     * Gets cache affinity key. Since in some examples person needs to be collocated with organization, we create
+     * custom affinity key to guarantee this collocation.
+     *
+     * @return Custom affinity key to guarantee that person is always collocated with organization.
+     */
+    public AffinityKey<Long> key() {
+        if (key == null)
+            key = new AffinityKey<>(id, orgId);
+
+        return key;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override public String toString() {
+        return "Person [id=" + id +
+                ", orgId=" + orgId +
+                ", lastName=" + lastName +
+                ", firstName=" + firstName +
+                ", salary=" + salary +
+                ", resume=" + resume + ']';
+    }
+}
diff --git a/modules/spring-data-2.0-ext/licenses/apache-2.0.txt b/modules/spring-data-2.0-ext/licenses/apache-2.0.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/modules/spring-data-2.0-ext/licenses/apache-2.0.txt
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       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.
diff --git a/modules/spring-data-2.0-ext/modules/core/src/test/config/log4j-test.xml b/modules/spring-data-2.0-ext/modules/core/src/test/config/log4j-test.xml
new file mode 100755
index 0000000..3061bd4
--- /dev/null
+++ b/modules/spring-data-2.0-ext/modules/core/src/test/config/log4j-test.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<!DOCTYPE log4j:configuration PUBLIC "-//APACHE//DTD LOG4J 1.2//EN"
+    "http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd">
+<!--
+    Log4j configuration.
+-->
+<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="false">
+    <!--
+        Logs System.out messages to console.
+    -->
+    <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender">
+        <!-- Log to STDOUT. -->
+        <param name="Target" value="System.out"/>
+
+        <!-- Log from DEBUG and higher. -->
+        <param name="Threshold" value="DEBUG"/>
+
+        <!-- The default pattern: Date Priority [Category] Message\n -->
+        <layout class="org.apache.log4j.PatternLayout">
+            <param name="ConversionPattern" value="[%d{ISO8601}][%-5p][%t][%c{1}] %m%n"/>
+        </layout>
+
+        <!-- Do not log beyond INFO level. -->
+        <filter class="org.apache.log4j.varia.LevelRangeFilter">
+            <param name="levelMin" value="DEBUG"/>
+            <param name="levelMax" value="INFO"/>
+        </filter>
+    </appender>
+
+    <!--
+        Logs all System.err messages to console.
+    -->
+    <appender name="CONSOLE_ERR" class="org.apache.log4j.ConsoleAppender">
+        <!-- Log to STDERR. -->
+        <param name="Target" value="System.err"/>
+
+        <!-- Log from WARN and higher. -->
+        <param name="Threshold" value="WARN"/>
+
+        <!-- The default pattern: Date Priority [Category] Message\n -->
+        <layout class="org.apache.log4j.PatternLayout">
+            <param name="ConversionPattern" value="[%d{ISO8601}][%-5p][%t][%c{1}] %m%n"/>
+        </layout>
+    </appender>
+
+    <!--
+        Logs all output to specified file.
+    -->
+    <appender name="FILE" class="org.apache.log4j.RollingFileAppender">
+        <param name="Threshold" value="DEBUG"/>
+        <param name="File" value="ignite/work/log/ignite.log"/>
+        <param name="Append" value="true"/>
+        <param name="MaxFileSize" value="10MB"/>
+        <param name="MaxBackupIndex" value="10"/>
+        <layout class="org.apache.log4j.PatternLayout">
+            <param name="ConversionPattern" value="[%d{ISO8601}][%-5p][%t][%c{1}] %m%n"/>
+        </layout>
+    </appender>
+
+    <!-- Disable all open source debugging. -->
+    <category name="org">
+        <level value="INFO"/>
+    </category>
+
+    <category name="org.eclipse.jetty">
+        <level value="INFO"/>
+    </category>
+
+    <!-- Default settings. -->
+    <root>
+        <!-- Print at info by default. -->
+        <level value="INFO"/>
+
+        <!-- Append to file and console. -->
+        <appender-ref ref="FILE"/>
+        <appender-ref ref="CONSOLE"/>
+        <appender-ref ref="CONSOLE_ERR"/>
+    </root>
+</log4j:configuration>
diff --git a/modules/spring-data-2.0-ext/modules/core/src/test/config/tests.properties b/modules/spring-data-2.0-ext/modules/core/src/test/config/tests.properties
new file mode 100644
index 0000000..0faf5b8
--- /dev/null
+++ b/modules/spring-data-2.0-ext/modules/core/src/test/config/tests.properties
@@ -0,0 +1,22 @@
+#
+# 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.
+#
+
+# Local address to bind to.
+local.ip=127.0.0.1
+
+# TCP communication port
+comm.tcp.port=30010
diff --git a/modules/spring-data-2.0-ext/pom.xml b/modules/spring-data-2.0-ext/pom.xml
new file mode 100644
index 0000000..00f7fa5
--- /dev/null
+++ b/modules/spring-data-2.0-ext/pom.xml
@@ -0,0 +1,182 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<!--
+    POM file.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.ignite</groupId>
+        <artifactId>ignite-extensions-parent</artifactId>
+        <version>1</version>
+        <relativePath>../../parent</relativePath>
+    </parent>
+
+    <artifactId>ignite-spring-data_2.0-ext</artifactId>
+    <version>1.0.0-SNAPSHOT</version>
+    <url>http://ignite.apache.org</url>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-core</artifactId>
+            <version>${ignite.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-indexing</artifactId>
+            <version>${ignite.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-log4j</artifactId>
+            <version>${ignite.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.data</groupId>
+            <artifactId>spring-data-commons</artifactId>
+            <version>${spring.data-2.0.version}</version>
+            <!-- Exclude slf4j logging in favor of log4j -->
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>jcl-over-slf4j</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-spring</artifactId>
+            <version>${ignite.version}</version>
+            <!--Remove exclusion while upgrading ignite-spring version to 5.0-->
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-core</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-beans</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-aop</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-context</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-expression</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-tx</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-jdbc</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <!--Remove spring-core and spring-beans dependencies while upgrading ignite-spring version to 5.0-->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-core</artifactId>
+            <version>${spring-5.0.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-beans</artifactId>
+            <version>${spring-5.0.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-context</artifactId>
+            <version>${spring-5.0.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-tx</artifactId>
+            <version>${spring-5.0.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>commons-lang</groupId>
+            <artifactId>commons-lang</artifactId>
+            <version>${commons.lang.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-core</artifactId>
+            <version>${ignite.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-tools</artifactId>
+            <version>${ignite.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <profiles>
+        <profile>
+            <id>examples</id>
+
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.codehaus.mojo</groupId>
+                        <artifactId>build-helper-maven-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <phase>generate-sources</phase>
+                                <goals>
+                                    <goal>add-source</goal>
+                                </goals>
+                                <configuration>
+                                    <sources>
+                                        <source>examples/main/java</source>
+                                    </sources>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+
+</project>
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/IgniteRepository.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/IgniteRepository.java
new file mode 100644
index 0000000..8ba8de6
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/IgniteRepository.java
@@ -0,0 +1,109 @@
+/*
+ * 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.springdata20.repository;
+
+import java.io.Serializable;
+import java.util.Map;
+import javax.cache.expiry.ExpiryPolicy;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.data.repository.CrudRepository;
+
+/**
+ * Apache Ignite repository that extends basic capabilities of {@link CrudRepository}.
+ *
+ * @param <V> the cache value type
+ * @param <K> the cache key type
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+public interface IgniteRepository<V, K extends Serializable> extends CrudRepository<V, K> {
+    /**
+     * Returns the Ignite instance bound to the repository
+     *
+     * @return the Ignite instance bound to the repository
+     */
+    public Ignite ignite();
+
+    /**
+     * Returns the Ignite Cache bound to the repository
+     *
+     * @return the Ignite Cache bound to the repository
+     */
+    public IgniteCache<K, V> cache();
+
+    /**
+     * Saves a given entity using provided key.
+     * </p>
+     * It's suggested to use this method instead of default {@link CrudRepository#save(Object)} that generates IDs
+     * (keys) that are not unique cluster wide.
+     *
+     * @param <S>    Entity type.
+     * @param key    Entity's key.
+     * @param entity Entity to save.
+     * @return Saved entity.
+     */
+    public <S extends V> S save(K key, S entity);
+
+    /**
+     * Saves all given keys and entities combinations.
+     * </p>
+     * It's suggested to use this method instead of default {@link CrudRepository#save(Object)} that generates IDs
+     * (keys) that are not unique cluster wide.
+     *
+     * @param <S>      Type of entities.
+     * @param entities Map of key-entities pairs to save.
+     * @return Saved entities.
+     */
+    public <S extends V> Iterable<S> save(Map<K, S> entities);
+
+    /**
+     * Saves a given entity using provided key with expiry policy
+     * </p>
+     * It's suggested to use this method instead of default {@link CrudRepository#save(Object)} that generates IDs
+     * (keys) that are not unique cluster wide.
+     *
+     * @param <S>       Entity type.
+     * @param key       Entity's key.
+     * @param entity    Entity to save.
+     * @param expiryPlc ExpiryPolicy to apply, if not null.
+     * @return Saved entity.
+     */
+    public <S extends V> S save(K key, S entity, @Nullable ExpiryPolicy expiryPlc);
+
+    /**
+     * Saves all given keys and entities combinations with expiry policy
+     * </p>
+     * It's suggested to use this method instead of default {@link CrudRepository#save(Object)} that generates IDs
+     * (keys) that are not unique cluster wide.
+     *
+     * @param <S>       Type of entities.
+     * @param entities  Map of key-entities pairs to save.
+     * @param expiryPlc ExpiryPolicy to apply, if not null.
+     * @return Saved entities.
+     */
+    public <S extends V> Iterable<S> save(Map<K, S> entities, @Nullable ExpiryPolicy expiryPlc);
+
+    /**
+     * Deletes all the entities for the provided ids.
+     *
+     * @param ids List of ids to delete.
+     */
+    public void deleteAllById(Iterable<K> ids);
+
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/DynamicQueryConfig.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/DynamicQueryConfig.java
new file mode 100644
index 0000000..ed422a9
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/DynamicQueryConfig.java
@@ -0,0 +1,348 @@
+/*
+ * 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.springdata20.repository.config;
+
+/**
+ * Runtime Dynamic query configuration.
+ * <p>
+ * Can be used as special repository method parameter to provide at runtime:
+ * <ol>
+ * <li>Dynamic query string (requires {@link Query#dynamicQuery()} == true)
+ * <li>Ignite query tuning*
+ * </ol>
+ * <p>
+ * * Please, note that {@link Query} annotation parameters will be ignored in favor of those defined in
+ * {@link DynamicQueryConfig} parameter if present.
+ *
+ * @author Manuel Núñez Sánchez (manuel.nunez@hawkore.com)
+ */
+public class DynamicQueryConfig {
+    /** */
+    private String value = "";
+
+    /** */
+    private boolean textQuery;
+
+    /** */
+    private boolean forceFieldsQry;
+
+    /** */
+    private boolean collocated;
+
+    /** */
+    private int timeout;
+
+    /** */
+    private boolean enforceJoinOrder;
+
+    /** */
+    private boolean distributedJoins;
+
+    /** */
+    private boolean lazy;
+
+    /** */
+    private boolean local;
+
+    /** */
+    private int[] parts;
+
+    /** */
+    private int limit;
+
+    /**
+     * From Query annotation.
+     *
+     * @param queryConfiguration the query configuration
+     * @return the dynamic query config
+     */
+    public static DynamicQueryConfig fromQueryAnnotation(Query queryConfiguration) {
+        DynamicQueryConfig config = new DynamicQueryConfig();
+        if (queryConfiguration != null) {
+            config.value = queryConfiguration.value();
+            config.collocated = queryConfiguration.collocated();
+            config.timeout = queryConfiguration.timeout();
+            config.enforceJoinOrder = queryConfiguration.enforceJoinOrder();
+            config.distributedJoins = queryConfiguration.distributedJoins();
+            config.lazy = queryConfiguration.lazy();
+            config.parts = queryConfiguration.parts();
+            config.local = queryConfiguration.local();
+            config.limit = queryConfiguration.limit();
+        }
+        return config;
+    }
+
+    /**
+     * Query text string.
+     *
+     * @return the string
+     */
+    public String value() {
+        return value;
+    }
+
+    /**
+     * Whether must use TextQuery search.
+     *
+     * @return the boolean
+     */
+    public boolean textQuery() {
+        return textQuery;
+    }
+
+    /**
+     * Force SqlFieldsQuery type, deactivating auto-detection based on SELECT statement. Useful for non SELECT
+     * statements or to not return hidden fields on SELECT * statements.
+     *
+     * @return the boolean
+     */
+    public boolean forceFieldsQuery() {
+        return forceFieldsQry;
+    }
+
+    /**
+     * Sets flag defining if this query is collocated.
+     * <p>
+     * Collocation flag is used for optimization purposes of queries with GROUP BY statements. Whenever Ignite executes
+     * a distributed query, it sends sub-queries to individual cluster members. If you know in advance that the elements
+     * of your query selection are collocated together on the same node and you group by collocated key (primary or
+     * affinity key), then Ignite can make significant performance and network optimizations by grouping data on remote
+     * nodes.
+     *
+     * <p>
+     * Only applicable to SqlFieldsQuery
+     *
+     * @return the boolean
+     */
+    public boolean collocated() {
+        return collocated;
+    }
+
+    /**
+     * Query timeout in millis. Sets the query execution timeout. Query will be automatically cancelled if the execution
+     * timeout is exceeded. Zero value disables timeout
+     *
+     * <p>
+     * Only applicable to SqlFieldsQuery and SqlQuery
+     *
+     * @return the int
+     */
+    public int timeout() {
+        return timeout;
+    }
+
+    /**
+     * Sets flag to enforce join order of tables in the query. If set to {@code true} query optimizer will not reorder
+     * tables in join. By default is {@code false}.
+     * <p>
+     * It is not recommended to enable this property until you are sure that your indexes and the query itself are
+     * correct and tuned as much as possible but query optimizer still produces wrong join order.
+     *
+     * <p>
+     * Only applicable to SqlFieldsQuery
+     *
+     * @return the boolean
+     */
+    public boolean enforceJoinOrder() {
+        return enforceJoinOrder;
+    }
+
+    /**
+     * Specify if distributed joins are enabled for this query.
+     * <p>
+     * Only applicable to SqlFieldsQuery and SqlQuery
+     *
+     * @return the boolean
+     */
+    public boolean distributedJoins() {
+        return distributedJoins;
+    }
+
+    /**
+     * Sets lazy query execution flag.
+     * <p>
+     * By default Ignite attempts to fetch the whole query result set to memory and send it to the client. For small and
+     * medium result sets this provides optimal performance and minimize duration of internal database locks, thus
+     * increasing concurrency.
+     * <p>
+     * If result set is too big to fit in available memory this could lead to excessive GC pauses and even
+     * OutOfMemoryError. Use this flag as a hint for Ignite to fetch result set lazily, thus minimizing memory
+     * consumption at the cost of moderate performance hit.
+     * <p>
+     * Defaults to {@code false}, meaning that the whole result set is fetched to memory eagerly.
+     * <p>
+     * Only applicable to SqlFieldsQuery
+     *
+     * @return the boolean
+     */
+    public boolean lazy() {
+        return lazy;
+    }
+
+    /**
+     * Sets whether this query should be executed on local node only.
+     *
+     * @return the boolean
+     */
+    public boolean local() {
+        return local;
+    }
+
+    /**
+     * Sets partitions for a query. The query will be executed only on nodes which are primary for specified
+     * partitions.
+     * <p>
+     * Note what passed array'll be sorted in place for performance reasons, if it wasn't sorted yet.
+     * <p>
+     * Only applicable to SqlFieldsQuery and SqlQuery
+     *
+     * @return the int [ ]
+     */
+    public int[] parts() {
+        return parts;
+    }
+
+    /**
+     * Gets limit to response records count for TextQuery. If 0 or less, considered to be no limit.
+     *
+     * @return Limit value.
+     */
+    public int limit() {
+        return limit;
+    }
+
+    /**
+     * Sets value.
+     *
+     * @param value the value
+     * @return this for chaining
+     */
+    public DynamicQueryConfig setValue(String value) {
+        this.value = value;
+        return this;
+    }
+
+    /**
+     * Sets text query.
+     *
+     * @param textQuery the text query
+     * @return this for chaining
+     */
+    public DynamicQueryConfig setTextQuery(boolean textQuery) {
+        this.textQuery = textQuery;
+        return this;
+    }
+
+    /**
+     * Sets force fields query.
+     *
+     * @param forceFieldsQuery the force fields query
+     * @return this for chaining
+     */
+    public DynamicQueryConfig setForceFieldsQuery(boolean forceFieldsQuery) {
+        forceFieldsQry = forceFieldsQuery;
+        return this;
+    }
+
+    /**
+     * Sets collocated.
+     *
+     * @param collocated the collocated
+     * @return this for chaining
+     */
+    public DynamicQueryConfig setCollocated(boolean collocated) {
+        this.collocated = collocated;
+        return this;
+    }
+
+    /**
+     * Sets timeout.
+     *
+     * @param timeout the timeout
+     * @return this for chaining
+     */
+    public DynamicQueryConfig setTimeout(int timeout) {
+        this.timeout = timeout;
+        return this;
+    }
+
+    /**
+     * Sets enforce join order.
+     *
+     * @param enforceJoinOrder the enforce join order
+     * @return this for chaining
+     */
+    public DynamicQueryConfig setEnforceJoinOrder(boolean enforceJoinOrder) {
+        this.enforceJoinOrder = enforceJoinOrder;
+        return this;
+    }
+
+    /**
+     * Sets distributed joins.
+     *
+     * @param distributedJoins the distributed joins
+     * @return this for chaining
+     */
+    public DynamicQueryConfig setDistributedJoins(boolean distributedJoins) {
+        this.distributedJoins = distributedJoins;
+        return this;
+    }
+
+    /**
+     * Sets lazy.
+     *
+     * @param lazy the lazy
+     * @return this for chaining
+     */
+    public DynamicQueryConfig setLazy(boolean lazy) {
+        this.lazy = lazy;
+        return this;
+    }
+
+    /**
+     * Sets local.
+     *
+     * @param local the local
+     * @return this for chaining
+     */
+    public DynamicQueryConfig setLocal(boolean local) {
+        this.local = local;
+        return this;
+    }
+
+    /**
+     * Sets parts.
+     *
+     * @param parts the parts
+     * @return this for chaining
+     */
+    public DynamicQueryConfig setParts(int[] parts) {
+        this.parts = parts;
+        return this;
+    }
+
+    /**
+     * Sets limit to response records count for TextQuery.
+     *
+     * @param limit If 0 or less, considered to be no limit.
+     * @return {@code this} For chaining.
+     */
+    public DynamicQueryConfig setLimit(int limit) {
+        this.limit = limit;
+        return this;
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/EnableIgniteRepositories.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/EnableIgniteRepositories.java
new file mode 100644
index 0000000..1bab4ca
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/EnableIgniteRepositories.java
@@ -0,0 +1,119 @@
+/*
+ * 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.springdata20.repository.config;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apache.ignite.springdata20.repository.support.IgniteRepositoryFactoryBean;
+import org.apache.ignite.springdata20.repository.support.IgniteRepositoryImpl;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.context.annotation.ComponentScan.Filter;
+import org.springframework.context.annotation.Import;
+import org.springframework.data.repository.query.QueryLookupStrategy;
+import org.springframework.data.repository.query.QueryLookupStrategy.Key;
+
+/**
+ * Annotation to activate Apache Ignite repositories. If no base package is configured through either {@link #value()},
+ * {@link #basePackages()} or {@link #basePackageClasses()} it will trigger scanning of the package of annotated class.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+@Import(IgniteRepositoriesRegistar.class)
+public @interface EnableIgniteRepositories {
+    /**
+     * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.:
+     * {@code @EnableIgniteRepositories("org.my.pkg")} instead of
+     * {@code @EnableIgniteRepositories(basePackages="org.my.pkg")}.
+     */
+    String[] value() default {};
+
+    /**
+     * Base packages to scan for annotated components. {@link #value()} is an alias for (and mutually exclusive with)
+     * this attribute. Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names.
+     */
+    String[] basePackages() default {};
+
+    /**
+     * Type-safe alternative to {@link #basePackages()} for specifying the packages to scan for annotated components.
+     * The package of each class specified will be scanned. Consider creating a special no-op marker class or interface
+     * in each package that serves no purpose other than being referenced by this attribute.
+     */
+    Class<?>[] basePackageClasses() default {};
+
+    /**
+     * Specifies which types are not eligible for component scanning.
+     */
+    Filter[] excludeFilters() default {};
+
+    /**
+     * Specifies which types are eligible for component scanning. Further narrows the set of candidate components from
+     * everything in {@link #basePackages()} to everything in the base packages that matches the given filter or
+     * filters.
+     */
+    Filter[] includeFilters() default {};
+
+    /**
+     * Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So
+     * for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning
+     * for {@code PersonRepositoryImpl}.
+     *
+     * @return Postfix to be used when looking up custom repository implementations.
+     */
+    String repositoryImplementationPostfix() default "Impl";
+
+    /**
+     * Configures the location of where to find the Spring Data named queries properties file.
+     *
+     * @return Location of where to find the Spring Data named queries properties file.
+     */
+    String namedQueriesLocation() default "";
+
+    /**
+     * Returns the key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods. Defaults to
+     * {@link Key#CREATE_IF_NOT_FOUND}.
+     *
+     * @return Key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods.
+     */
+    Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND;
+
+    /**
+     * Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to {@link
+     * IgniteRepositoryFactoryBean}.
+     *
+     * @return {@link FactoryBean} class to be used for each repository instance.
+     */
+    Class<?> repositoryFactoryBeanClass() default IgniteRepositoryFactoryBean.class;
+
+    /**
+     * Configure the repository base class to be used to create repository proxies for this particular configuration.
+     *
+     * @return Repository base class to be used to create repository proxies for this particular configuration.
+     */
+    Class<?> repositoryBaseClass() default IgniteRepositoryImpl.class;
+
+    /**
+     * Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the
+     * repositories infrastructure.
+     */
+    boolean considerNestedRepositories() default false;
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/IgniteRepositoriesRegistar.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/IgniteRepositoriesRegistar.java
new file mode 100644
index 0000000..83ff7ff
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/IgniteRepositoriesRegistar.java
@@ -0,0 +1,36 @@
+/*
+ * 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.springdata20.repository.config;
+
+import java.lang.annotation.Annotation;
+import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport;
+import org.springframework.data.repository.config.RepositoryConfigurationExtension;
+
+/**
+ * Apache Ignite specific implementation of {@link RepositoryBeanDefinitionRegistrarSupport}.
+ */
+public class IgniteRepositoriesRegistar extends RepositoryBeanDefinitionRegistrarSupport {
+    /** {@inheritDoc} */
+    @Override protected Class<? extends Annotation> getAnnotation() {
+        return EnableIgniteRepositories.class;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected RepositoryConfigurationExtension getExtension() {
+        return new IgniteRepositoryConfigurationExtension();
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/IgniteRepositoryConfigurationExtension.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/IgniteRepositoryConfigurationExtension.java
new file mode 100644
index 0000000..354e35b
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/IgniteRepositoryConfigurationExtension.java
@@ -0,0 +1,49 @@
+/*
+ * 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.springdata20.repository.config;
+
+import java.util.Collection;
+import java.util.Collections;
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.apache.ignite.springdata20.repository.support.IgniteRepositoryFactoryBean;
+import org.springframework.data.repository.config.RepositoryConfigurationExtension;
+import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport;
+
+/**
+ * Apache Ignite specific implementation of {@link RepositoryConfigurationExtension}.
+ */
+public class IgniteRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport {
+    /** {@inheritDoc} */
+    @Override public String getModuleName() {
+        return "Apache Ignite";
+    }
+
+    /** {@inheritDoc} */
+    @Override protected String getModulePrefix() {
+        return "ignite";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getRepositoryFactoryBeanClassName() {
+        return IgniteRepositoryFactoryBean.class.getName();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected Collection<Class<?>> getIdentifyingTypes() {
+        return Collections.singleton(IgniteRepository.class);
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/Query.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/Query.java
new file mode 100644
index 0000000..7b44d19
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/Query.java
@@ -0,0 +1,137 @@
+/*
+ * 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.springdata20.repository.config;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to provide a user defined query for a method.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface Query {
+    /**
+     * Query text string. If not provided, Ignite query generator for Spring Data framework will be used to generate one
+     * (only if textQuery = false (default))
+     */
+    String value() default "";
+
+    /**
+     * Whether annotated repository method must use TextQuery search.
+     */
+    boolean textQuery() default false;
+
+    /**
+     * Force SqlFieldsQuery type, deactivating auto-detection based on SELECT statement. Useful for non SELECT
+     * statements or to not return hidden fields on SELECT * statements.
+     */
+    boolean forceFieldsQuery() default false;
+
+    /**
+     * Sets flag defining if this query is collocated.
+     * <p>
+     * Collocation flag is used for optimization purposes of queries with GROUP BY statements. Whenever Ignite executes
+     * a distributed query, it sends sub-queries to individual cluster members. If you know in advance that the elements
+     * of your query selection are collocated together on the same node and you group by collocated key (primary or
+     * affinity key), then Ignite can make significant performance and network optimizations by grouping data on remote
+     * nodes.
+     *
+     * <p>
+     * Only applicable to SqlFieldsQuery
+     */
+    boolean collocated() default false;
+
+    /**
+     * Query timeout in millis. Sets the query execution timeout. Query will be automatically cancelled if the execution
+     * timeout is exceeded. Zero value disables timeout
+     *
+     * <p>
+     * Only applicable to SqlFieldsQuery and SqlQuery
+     */
+    int timeout() default 0;
+
+    /**
+     * Sets flag to enforce join order of tables in the query. If set to {@code true} query optimizer will not reorder
+     * tables in join. By default is {@code false}.
+     * <p>
+     * It is not recommended to enable this property until you are sure that your indexes and the query itself are
+     * correct and tuned as much as possible but query optimizer still produces wrong join order.
+     *
+     * <p>
+     * Only applicable to SqlFieldsQuery
+     */
+    boolean enforceJoinOrder() default false;
+
+    /**
+     * Specify if distributed joins are enabled for this query.
+     * <p>
+     * Only applicable to SqlFieldsQuery and SqlQuery
+     */
+    boolean distributedJoins() default false;
+
+    /**
+     * Sets lazy query execution flag.
+     * <p>
+     * By default Ignite attempts to fetch the whole query result set to memory and send it to the client. For small and
+     * medium result sets this provides optimal performance and minimize duration of internal database locks, thus
+     * increasing concurrency.
+     * <p>
+     * If result set is too big to fit in available memory this could lead to excessive GC pauses and even
+     * OutOfMemoryError. Use this flag as a hint for Ignite to fetch result set lazily, thus minimizing memory
+     * consumption at the cost of moderate performance hit.
+     * <p>
+     * Defaults to {@code false}, meaning that the whole result set is fetched to memory eagerly.
+     * <p>
+     * Only applicable to SqlFieldsQuery
+     */
+    boolean lazy() default false;
+
+    /**
+     * Sets whether this query should be executed on local node only.
+     */
+    boolean local() default false;
+
+    /**
+     * Sets partitions for a query. The query will be executed only on nodes which are primary for specified
+     * partitions.
+     * <p>
+     * Note what passed array'll be sorted in place for performance reasons, if it wasn't sorted yet.
+     * <p>
+     * Only applicable to SqlFieldsQuery and SqlQuery
+     */
+    int[] parts() default {};
+
+    /**
+     * Specify whether the annotated method must provide a non null {@link DynamicQueryConfig} parameter with a non
+     * empty value (query string) or {@link DynamicQueryConfig#textQuery()} == true.
+     * <p>
+     * Please, note that  {@link DynamicQueryConfig#textQuery()} annotation parameters will be ignored in favor of those
+     * defined in {@link DynamicQueryConfig} parameter if present (runtime ignite query tuning).
+     */
+    boolean dynamicQuery() default false;
+
+    /**
+     * Sets limit to response records count for TextQuery. If 0 or less, considered to be no limit.
+     */
+    int limit() default 0;
+
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/RepositoryConfig.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/RepositoryConfig.java
new file mode 100644
index 0000000..359b75b
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/RepositoryConfig.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.springdata20.repository.config;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.configuration.IgniteConfiguration;
+
+/**
+ * The annotation can be used to pass Ignite specific parameters to a bound repository.
+ *
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface RepositoryConfig {
+    /**
+     * Cache name string.
+     *
+     * @return A name of a distributed Apache Ignite cache an annotated repository will be mapped to.
+     */
+    String cacheName() default "";
+
+    /**
+     * Ignite instance string. Default "igniteInstance".
+     *
+     * @return {@link Ignite} instance spring bean name
+     */
+    String igniteInstance() default "igniteInstance";
+
+    /**
+     * Ignite cfg string. Default "igniteCfg".
+     *
+     * @return {@link IgniteConfiguration} spring bean name
+     */
+    String igniteCfg() default "igniteCfg";
+
+    /**
+     * Ignite spring cfg path string. Default "igniteSpringCfgPath".
+     *
+     * @return A path to Ignite's Spring XML configuration spring bean name
+     */
+    String igniteSpringCfgPath() default "igniteSpringCfgPath";
+
+    /**
+     * Auto create cache. Default false to enforce control over cache creation and to avoid cache creation by mistake
+     * <p>
+     * Tells to Ignite Repository factory wether cache should be auto created if not exists.
+     *
+     * @return the boolean
+     */
+    boolean autoCreateCache() default false;
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/package-info.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/package-info.java
new file mode 100644
index 0000000..1c7b2f9
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/config/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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 description. -->
+ * Package includes Spring Data integration related configuration files.
+ */
+package org.apache.ignite.springdata20.repository.config;
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/package-info.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/package-info.java
new file mode 100644
index 0000000..9df5513
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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 description. -->
+ * Package contains Apache Ignite Spring Data integration.
+ */
+package org.apache.ignite.springdata20.repository;
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/DeclaredQuery.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/DeclaredQuery.java
new file mode 100644
index 0000000..297d35c
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/DeclaredQuery.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2018-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      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.springdata20.repository.query;
+
+import java.util.List;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.util.StringUtils;
+
+/**
+ * A wrapper for a String representation of a query offering information about the query.
+ *
+ * @author Jens Schauder
+ */
+interface DeclaredQuery {
+    /**
+     * Creates a {@literal DeclaredQuery} from a query {@literal String}.
+     *
+     * @param qry might be {@literal null} or empty.
+     * @return a {@literal DeclaredQuery} instance even for a {@literal null} or empty argument.
+     */
+    public static DeclaredQuery of(@Nullable String qry) {
+        return StringUtils.isEmpty(qry) ? EmptyDeclaredQuery.EMPTY_QUERY : new StringQuery(qry);
+    }
+
+    /**
+     * @return whether the underlying query has at least one named parameter.
+     */
+    public boolean hasNamedParameter();
+
+    /**
+     * Returns the query string.
+     */
+    public String getQueryString();
+
+    /**
+     * Returns the main alias used in the query.
+     *
+     * @return the alias
+     */
+    @Nullable
+    public String getAlias();
+
+    /**
+     * Returns whether the query is using a constructor expression.
+     */
+    public boolean hasConstructorExpression();
+
+    /**
+     * Returns whether the query uses the default projection, i.e. returns the main alias defined for the query.
+     */
+    public boolean isDefaultProjection();
+
+    /**
+     * Returns the {@link StringQuery.ParameterBinding}s registered.
+     */
+    public List<StringQuery.ParameterBinding> getParameterBindings();
+
+    /**
+     * Creates a new {@literal DeclaredQuery} representing a count query, i.e. a query returning the number of rows to
+     * be expected from the original query, either derived from the query wrapped by this instance or from the
+     * information passed as arguments.
+     *
+     * @param cntQry           an optional query string to be used if present.
+     * @param cntQryProjection an optional return type for the query.
+     * @return a new {@literal DeclaredQuery} instance.
+     */
+    public DeclaredQuery deriveCountQuery(@Nullable String cntQry, @Nullable String cntQryProjection);
+
+    /**
+     * @return whether paging is implemented in the query itself, e.g. using SpEL expressions.
+     */
+    public default boolean usesPaging() {
+        return false;
+    }
+
+    /**
+     * Returns whether the query uses JDBC style parameters, i.e. parameters denoted by a simple ? without any index or
+     * name.
+     *
+     * @return Whether the query uses JDBC style parameters.
+     */
+    public boolean usesJdbcStyleParameters();
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/EmptyDeclaredQuery.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/EmptyDeclaredQuery.java
new file mode 100644
index 0000000..34fdb8c
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/EmptyDeclaredQuery.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2018-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      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.springdata20.repository.query;
+
+import java.util.Collections;
+import java.util.List;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * NULL-Object pattern implementation for {@link DeclaredQuery}.
+ *
+ * @author Jens Schauder
+ */
+class EmptyDeclaredQuery implements DeclaredQuery {
+    /**
+     * An implementation implementing the NULL-Object pattern for situations where there is no query.
+     */
+    static final DeclaredQuery EMPTY_QUERY = new EmptyDeclaredQuery();
+
+    /** {@inheritDoc} */
+    @Override public boolean hasNamedParameter() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getQueryString() {
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getAlias() {
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean hasConstructorExpression() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean isDefaultProjection() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public List<StringQuery.ParameterBinding> getParameterBindings() {
+        return Collections.emptyList();
+    }
+
+    /** {@inheritDoc} */
+    @Override public DeclaredQuery deriveCountQuery(@Nullable String cntQry, @Nullable String cntQryProjection) {
+        Assert.hasText(cntQry, "CountQuery must not be empty!");
+        return DeclaredQuery.of(cntQry);
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean usesJdbcStyleParameters() {
+        return false;
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/ExpressionBasedStringQuery.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/ExpressionBasedStringQuery.java
new file mode 100644
index 0000000..b7559a5
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/ExpressionBasedStringQuery.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      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.springdata20.repository.query;
+
+import java.util.regex.Pattern;
+import org.springframework.data.repository.core.EntityMetadata;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.expression.Expression;
+import org.springframework.expression.ParserContext;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.util.Assert;
+
+/**
+ * Extension of {@link StringQuery} that evaluates the given query string as a SpEL template-expression.
+ * <p>
+ * Currently the following template variables are available:
+ * <ol>
+ * <li>{@code #entityName} - the simple class name of the given entity</li>
+ * <ol>
+ *
+ * @author Thomas Darimont
+ * @author Oliver Gierke
+ * @author Tom Hombergs
+ */
+class ExpressionBasedStringQuery extends StringQuery {
+    /**
+     * Expression parameter.
+     */
+    private static final String EXPRESSION_PARAMETER = "?#{";
+
+    /**
+     * Quoted expression parameter.
+     */
+    private static final String QUOTED_EXPRESSION_PARAMETER = "?__HASH__{";
+
+    /**
+     * Expression parameter quoting.
+     */
+    private static final Pattern EXPRESSION_PARAMETER_QUOTING = Pattern.compile(Pattern.quote(EXPRESSION_PARAMETER));
+
+    /**
+     * Expression parameter unquoting.
+     */
+    private static final Pattern EXPRESSION_PARAMETER_UNQUOTING = Pattern.compile(
+        Pattern.quote(QUOTED_EXPRESSION_PARAMETER));
+
+    /**
+     * Entity name.
+     */
+    private static final String ENTITY_NAME = "entityName";
+
+    /**
+     * Entity name variable.
+     */
+    private static final String ENTITY_NAME_VARIABLE = "#" + ENTITY_NAME;
+
+    /**
+     * Entity name variable expression.
+     */
+    private static final String ENTITY_NAME_VARIABLE_EXPRESSION = "#{" + ENTITY_NAME_VARIABLE + "}";
+
+    /**
+     * Creates a new instance for the given query and {@link EntityMetadata}.
+     *
+     * @param qry      must not be {@literal null} or empty.
+     * @param metadata must not be {@literal null}.
+     * @param parser   must not be {@literal null}.
+     */
+    public ExpressionBasedStringQuery(String qry, RepositoryMetadata metadata, SpelExpressionParser parser) {
+        super(renderQueryIfExpressionOrReturnQuery(qry, metadata, parser));
+    }
+
+    /**
+     * Creates an instance from a given {@link DeclaredQuery}.
+     *
+     * @param qry      the original query. Must not be {@literal null}.
+     * @param metadata the {@link RepositoryMetadata} for the given entity. Must not be {@literal null}.
+     * @param parser   Parser for resolving SpEL expressions. Must not be {@literal null}.
+     * @return A query supporting SpEL expressions.
+     */
+    static ExpressionBasedStringQuery from(DeclaredQuery qry,
+        RepositoryMetadata metadata,
+        SpelExpressionParser parser) {
+        return new ExpressionBasedStringQuery(qry.getQueryString(), metadata, parser);
+    }
+
+    /**
+     * @param qry,     the query expression potentially containing a SpEL expression. Must not be {@literal null}.}
+     * @param metadata the {@link RepositoryMetadata} for the given entity. Must not be {@literal null}.
+     * @param parser   Must not be {@literal null}.
+     * @return rendered query
+     */
+    private static String renderQueryIfExpressionOrReturnQuery(String qry,
+        RepositoryMetadata metadata,
+        SpelExpressionParser parser) {
+
+        Assert.notNull(qry, "query must not be null!");
+        Assert.notNull(metadata, "metadata must not be null!");
+        Assert.notNull(parser, "parser must not be null!");
+
+        if (!containsExpression(qry))
+            return qry;
+
+        StandardEvaluationContext evalCtx = new StandardEvaluationContext();
+        evalCtx.setVariable(ENTITY_NAME, metadata.getDomainType().getSimpleName());
+
+        qry = potentiallyQuoteExpressionsParameter(qry);
+
+        Expression expr = parser.parseExpression(qry, ParserContext.TEMPLATE_EXPRESSION);
+
+        String result = expr.getValue(evalCtx, String.class);
+
+        if (result == null)
+            return qry;
+
+        return potentiallyUnquoteParameterExpressions(result);
+    }
+
+    /**
+     * @param result Result.
+     */
+    private static String potentiallyUnquoteParameterExpressions(String result) {
+        return EXPRESSION_PARAMETER_UNQUOTING.matcher(result).replaceAll(EXPRESSION_PARAMETER);
+    }
+
+    /**
+     * @param qry Query.
+     */
+    private static String potentiallyQuoteExpressionsParameter(String qry) {
+        return EXPRESSION_PARAMETER_QUOTING.matcher(qry).replaceAll(QUOTED_EXPRESSION_PARAMETER);
+    }
+
+    /**
+     * @param qry Query.
+     */
+    private static boolean containsExpression(String qry) {
+        return qry.contains(ENTITY_NAME_VARIABLE_EXPRESSION);
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/IgniteQuery.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/IgniteQuery.java
new file mode 100644
index 0000000..f8e6d48
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/IgniteQuery.java
@@ -0,0 +1,134 @@
+/*
+ * 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.springdata20.repository.query;
+
+import org.apache.ignite.internal.util.typedef.internal.S;
+
+/**
+ * Ignite query helper class. For internal use only.
+ *
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+public class IgniteQuery {
+    /** */
+    enum Option {
+        /** Query will be used with Sort object. */
+        SORTING,
+
+        /** Query will be used with Pageable object. */
+        PAGINATION,
+
+        /** No advanced option. */
+        NONE
+    }
+
+    /**
+     * Query text string.
+     */
+    private final String qrySql;
+
+    /**
+     * Whether this is a SQL fields query
+     */
+    private final boolean isFieldQuery;
+
+    /**
+     * Whether this is Text query
+     */
+    private final boolean isTextQuery;
+
+    /**
+     * Whether was autogenerated (by method name)
+     */
+    private final boolean isAutogenerated;
+
+    /**
+     * Type of option.
+     */
+    private final Option option;
+
+    /**
+     * @param qrySql          the query string.
+     * @param isFieldQuery    Is field query.
+     * @param isTextQuery     Is a TextQuery
+     * @param isAutogenerated query was autogenerated
+     * @param option          Option.
+     */
+    public IgniteQuery(String qrySql,
+        boolean isFieldQuery,
+        boolean isTextQuery,
+        boolean isAutogenerated,
+        Option option) {
+        this.qrySql = qrySql;
+        this.isFieldQuery = isFieldQuery;
+        this.isTextQuery = isTextQuery;
+        this.isAutogenerated = isAutogenerated;
+        this.option = option;
+    }
+
+    /**
+     * Text string of the query.
+     *
+     * @return SQL query text string.
+     */
+    public String qryStr() {
+        return qrySql;
+    }
+
+    /**
+     * Returns {@code true} if it's Ignite SQL fields query, {@code false} otherwise.
+     *
+     * @return {@code true} if it's Ignite SQL fields query, {@code false} otherwise.
+     */
+    public boolean isFieldQuery() {
+        return isFieldQuery;
+    }
+
+    /**
+     * Returns {@code true} if it's Ignite Text query, {@code false} otherwise.
+     *
+     * @return {@code true} if it's Ignite Text query, {@code false} otherwise.
+     */
+    public boolean isTextQuery() {
+        return isTextQuery;
+    }
+
+    /**
+     * Returns {@code true} if it's autogenerated, {@code false} otherwise.
+     *
+     * @return {@code true} if it's autogenerated, {@code false} otherwise.
+     */
+    public boolean isAutogenerated() {
+        return isAutogenerated;
+    }
+
+    /**
+     * Advanced querying option.
+     *
+     * @return querying option.
+     */
+    public Option options() {
+        return option;
+    }
+
+    /** */
+    @Override public String toString() {
+        return S.toString(IgniteQuery.class, this);
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/IgniteQueryGenerator.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/IgniteQueryGenerator.java
new file mode 100644
index 0000000..864903a
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/IgniteQueryGenerator.java
@@ -0,0 +1,276 @@
+/*
+ * 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.springdata20.repository.query;
+
+import java.lang.reflect.Method;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mapping.PropertyPath;
+import org.springframework.data.mapping.PropertyReferenceException;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.query.parser.Part;
+import org.springframework.data.repository.query.parser.PartTree;
+
+/**
+ * Ignite query generator for Spring Data framework.
+ */
+public class IgniteQueryGenerator {
+    /** */
+    private IgniteQueryGenerator() {
+        // No-op.
+    }
+
+    /**
+     * @param mtd      Method.
+     * @param metadata Metadata.
+     * @return Generated ignite query.
+     */
+    public static IgniteQuery generateSql(Method mtd, RepositoryMetadata metadata) {
+        PartTree parts;
+
+        try {
+            parts = new PartTree(mtd.getName(), metadata.getDomainType());
+        }
+        catch (PropertyReferenceException e) {
+            parts = new PartTree(mtd.getName(), metadata.getIdType());
+        }
+
+        boolean isCountOrFieldQuery = parts.isCountProjection();
+
+        StringBuilder sql = new StringBuilder();
+
+        if (parts.isDelete()) {
+            sql.append("DELETE ");
+
+            // For the DML queries aside from SELECT *, they should run over SqlFieldQuery
+            isCountOrFieldQuery = true;
+        }
+        else {
+            sql.append("SELECT ");
+
+            if (parts.isDistinct())
+                throw new UnsupportedOperationException("DISTINCT clause in not supported.");
+
+            if (isCountOrFieldQuery)
+                sql.append("COUNT(1) ");
+            else
+                sql.append("* ");
+        }
+
+        sql.append("FROM ").append(metadata.getDomainType().getSimpleName());
+
+        if (parts.iterator().hasNext()) {
+            sql.append(" WHERE ");
+
+            for (PartTree.OrPart orPart : parts) {
+                sql.append("(");
+
+                for (Part part : orPart) {
+                    handleQueryPart(sql, part, metadata.getDomainType());
+                    sql.append(" AND ");
+                }
+
+                sql.delete(sql.length() - 5, sql.length());
+
+                sql.append(") OR ");
+            }
+
+            sql.delete(sql.length() - 4, sql.length());
+        }
+
+        addSorting(sql, parts.getSort());
+
+        if (parts.isLimiting()) {
+            sql.append(" LIMIT ");
+            sql.append(parts.getMaxResults().intValue());
+        }
+
+        return new IgniteQuery(sql.toString(), isCountOrFieldQuery, false, true, getOptions(mtd));
+    }
+
+    /**
+     * Add a dynamic part of query for the sorting support.
+     *
+     * @param sql  SQL text string.
+     * @param sort Sort method.
+     * @return Sorting criteria in StringBuilder.
+     */
+    public static StringBuilder addSorting(StringBuilder sql, Sort sort) {
+        if (sort != null && sort != Sort.unsorted()) {
+            sql.append(" ORDER BY ");
+
+            for (Sort.Order order : sort) {
+                sql.append(order.getProperty()).append(" ").append(order.getDirection());
+
+                if (order.getNullHandling() != Sort.NullHandling.NATIVE) {
+                    sql.append(" ").append("NULL ");
+
+                    switch (order.getNullHandling()) {
+                        case NULLS_FIRST:
+                            sql.append("FIRST");
+                            break;
+                        case NULLS_LAST:
+                            sql.append("LAST");
+                            break;
+                        default:
+                    }
+                }
+                sql.append(", ");
+            }
+
+            sql.delete(sql.length() - 2, sql.length());
+        }
+
+        return sql;
+    }
+
+    /**
+     * Add a dynamic part of a query for the pagination support.
+     *
+     * @param sql      Builder instance.
+     * @param pageable Pageable instance.
+     * @return Builder instance.
+     */
+    public static StringBuilder addPaging(StringBuilder sql, Pageable pageable) {
+
+        addSorting(sql, pageable.getSort());
+
+        sql.append(" LIMIT ").append(pageable.getPageSize()).append(" OFFSET ").append(pageable.getOffset());
+
+        return sql;
+    }
+
+    /**
+     * Determines whether query is dynamic or not (by list of method parameters)
+     *
+     * @param mtd Method.
+     * @return type of options
+     */
+    public static IgniteQuery.Option getOptions(Method mtd) {
+        IgniteQuery.Option option = IgniteQuery.Option.NONE;
+
+        Class<?>[] types = mtd.getParameterTypes();
+        if (types.length > 0) {
+            Class<?> type = types[types.length - 1];
+
+            if (Sort.class.isAssignableFrom(type))
+                option = IgniteQuery.Option.SORTING;
+            else if (Pageable.class.isAssignableFrom(type))
+                option = IgniteQuery.Option.PAGINATION;
+        }
+
+        for (int i = 0; i < types.length - 1; i++) {
+            Class<?> tp = types[i];
+
+            if (tp == Sort.class || tp == Pageable.class)
+                throw new AssertionError("Sort and Pageable parameters are allowed only in the last position");
+        }
+
+        return option;
+    }
+
+    /**
+     * Check and correct table name if using column name from compound key.
+     */
+    private static String getColumnName(Part part, Class<?> domainType) {
+        PropertyPath prperty = part.getProperty();
+
+        if (prperty.getType() != domainType)
+            return domainType.getSimpleName() + "." + prperty.getSegment();
+        else
+            return part.toString();
+    }
+
+    /**
+     * Transform part to qryStr expression
+     */
+    private static void handleQueryPart(StringBuilder sql, Part part, Class<?> domainType) {
+        sql.append("(");
+
+        sql.append(getColumnName(part, domainType));
+
+        switch (part.getType()) {
+            case SIMPLE_PROPERTY:
+                sql.append("=?");
+                break;
+            case NEGATING_SIMPLE_PROPERTY:
+                sql.append("<>?");
+                break;
+            case GREATER_THAN:
+                sql.append(">?");
+                break;
+            case GREATER_THAN_EQUAL:
+                sql.append(">=?");
+                break;
+            case LESS_THAN:
+                sql.append("<?");
+                break;
+            case LESS_THAN_EQUAL:
+                sql.append("<=?");
+                break;
+            case IS_NOT_NULL:
+                sql.append(" IS NOT NULL");
+                break;
+            case IS_NULL:
+                sql.append(" IS NULL");
+                break;
+            case BETWEEN:
+                sql.append(" BETWEEN ? AND ?");
+                break;
+            case FALSE:
+                sql.append(" = FALSE");
+                break;
+            case TRUE:
+                sql.append(" = TRUE");
+                break;
+            //TODO: review this legacy code, LIKE should be -> LIKE ?
+            case LIKE:
+            case CONTAINING:
+                sql.append(" LIKE '%' || ? || '%'");
+                break;
+            case NOT_CONTAINING:
+                //TODO: review this legacy code, NOT_LIKE should be -> NOT LIKE ?
+            case NOT_LIKE:
+                sql.append(" NOT LIKE '%' || ? || '%'");
+                break;
+            case STARTING_WITH:
+                sql.append(" LIKE  ? || '%'");
+                break;
+            case ENDING_WITH:
+                sql.append(" LIKE '%' || ?");
+                break;
+            case IN:
+                sql.append(" IN ?");
+                break;
+            case NOT_IN:
+                sql.append(" NOT IN ?");
+                break;
+            case REGEX:
+                sql.append(" REGEXP ?");
+                break;
+            case NEAR:
+            case AFTER:
+            case BEFORE:
+            case EXISTS:
+            default:
+                throw new UnsupportedOperationException(part.getType() + " is not supported!");
+        }
+
+        sql.append(")");
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/IgniteRepositoryQuery.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/IgniteRepositoryQuery.java
new file mode 100644
index 0000000..96ff57b
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/IgniteRepositoryQuery.java
@@ -0,0 +1,1043 @@
+/*
+ * 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.springdata20.repository.query;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.AbstractCollection;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.TreeMap;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import javax.cache.Cache;
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.apache.ignite.binary.BinaryType;
+import org.apache.ignite.cache.query.Query;
+import org.apache.ignite.cache.query.QueryCursor;
+import org.apache.ignite.cache.query.SqlFieldsQuery;
+import org.apache.ignite.cache.query.SqlQuery;
+import org.apache.ignite.cache.query.TextQuery;
+import org.apache.ignite.internal.GridKernalContext;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.binary.BinaryUtils;
+import org.apache.ignite.internal.processors.cache.CacheEntryImpl;
+import org.apache.ignite.internal.processors.cache.binary.CacheObjectBinaryProcessorImpl;
+import org.apache.ignite.internal.processors.cache.binary.IgniteBinaryImpl;
+import org.apache.ignite.internal.processors.cache.query.QueryCursorEx;
+import org.apache.ignite.internal.processors.query.GridQueryFieldMetadata;
+import org.apache.ignite.internal.processors.query.QueryUtils;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.springdata20.repository.config.DynamicQueryConfig;
+import org.apache.ignite.springdata20.repository.query.StringQuery.ParameterBinding;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.domain.SliceImpl;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.query.EvaluationContextProvider;
+import org.springframework.data.repository.query.Parameter;
+import org.springframework.data.repository.query.Parameters;
+import org.springframework.data.repository.query.QueryMethod;
+import org.springframework.data.repository.query.RepositoryQuery;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.ParserContext;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.util.StringUtils;
+
+import static org.apache.ignite.springdata20.repository.support.IgniteRepositoryFactory.isFieldQuery;
+
+/**
+ * Ignite query implementation.
+ * <p>
+ * <p>
+ * Features:
+ * <ol>
+ * <li> Supports query tuning parameters</li>
+ * <li> Supports projections</li>
+ * <li> Supports Page and Stream responses</li>
+ * <li> Supports SqlFieldsQuery resultset transformation into the domain entity</li>
+ * <li> Supports named parameters (:myParam) into SQL queries, declared using @Param("myParam") annotation</li>
+ * <li> Supports advanced parameter binding and SpEL expressions into SQL queries
+ * <ol>
+ * <li><b>Template variables</b>:
+ * <ol>
+ * <li>{@code #entityName} - the simple class name of the domain entity</li>
+ * </ol>
+ * </li>
+ * <li><b>Method parameter expressions</b>: Parameters are exposed for indexed access ([0] is the first query method's
+ * param) or via the name declared using @Param. The actual SpEL expression binding is triggered by '?#'. Example:
+ * ?#{[0]} or ?#{#myParamName}</li>
+ * <li><b>Advanced SpEL expressions</b>: While advanced parameter binding is a very useful feature, the real power of
+ * SpEL stems from the fact, that the expressions can refer to framework abstractions or other application components
+ * through SpEL EvaluationContext extension model.</li>
+ * </ol>
+ * Examples:
+ * <pre>
+ * {@code @Query}(value = "SELECT * from #{#entityName} where email = :email")
+ * User searchUserByEmail({@code @Param}("email") String email);
+ *
+ * {@code @Query}(value = "SELECT * from #{#entityName} where country = ?#{[0]} and city = ?#{[1]}")
+ * List<User> searchUsersByCity({@code @Param}("country") String country, {@code @Param}("city") String city,
+ * Pageable pageable);
+ *
+ * {@code @Query}(value = "SELECT * from #{#entityName} where email = ?")
+ * User searchUserByEmail(String email);
+ *
+ * {@code @Query}(value = "SELECT * from #{#entityName} where lucene = ?#{
+ * luceneQueryBuilder.search().refresh(true).filter(luceneQueryBuilder.match('city',#city)).build()}")
+ * List<User> searchUsersByCity({@code @Param}("city") String city, Pageable pageable);
+ * </pre>
+ * </li>
+ * <li> Supports SpEL expressions into Text queries ({@link TextQuery}). Examples:
+ * <pre>
+ * {@code @Query}(textQuery = true, value = "email: #{#email}")
+ * User searchUserByEmail({@code @Param}("email") String email);
+ *
+ * {@code @Query}(textQuery = true, value = "#{#textToSearch}")
+ * List<User> searchUsersByText({@code @Param}("textToSearch") String text, Pageable pageable);
+ *
+ * {@code @Query}(textQuery = true, value = "#{[0]}")
+ * List<User> searchUsersByText(String textToSearch, Pageable pageable);
+ *
+ * {@code @Query}(textQuery = true, value = "#{luceneQueryBuilder.search().refresh(true).filter(luceneQueryBuilder
+ * .match('city', #city)).build()}")
+ * List<User> searchUserByCity({@code @Param}("city") String city, Pageable pageable);
+ * </pre>
+ * </li>
+ * <li> Supports dynamic query and tuning at runtime by using {@link DynamicQueryConfig} method parameter. Examples:
+ * <pre>
+ * {@code @Query}(value = "SELECT * from #{#entityName} where email = :email")
+ * User searchUserByEmailWithQueryTuning({@code @Param}("email") String email, {@code @Param}("ignoredUsedAsQueryTuning") DynamicQueryConfig config);
+ *
+ * {@code @Query}(dynamicQuery = true)
+ * List<User> searchUsersByCityWithDynamicQuery({@code @Param}("country") String country, {@code @Param}("city") String city,
+ * {@code @Param}("ignoredUsedAsDynamicQueryAndTuning") DynamicQueryConfig config, Pageable pageable);
+ *
+ * ...
+ * DynamicQueryConfig onlyTunning = new DynamicQueryConfig().setCollocated(true);
+ * repo.searchUserByEmailWithQueryTuning("user@mail.com", onlyTunning);
+ *
+ * DynamicQueryConfig withDynamicQuery = new DynamicQueryConfig().value("SELECT * from #{#entityName} where country = ?#{[0] and city = ?#{[1]}").setForceFieldsQuery(true).setLazy(true).setCollocated(true);
+ * repo.searchUsersByCityWithDynamicQuery("Spain", "Madrid", withDynamicQuery, new PageRequest(0, 100));
+ *
+ * </pre>
+ * </li>
+ * </ol>
+ *
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+@SuppressWarnings("unchecked")
+public class IgniteRepositoryQuery implements RepositoryQuery {
+    /** */
+    private static final TreeMap<String, Class<?>> binaryFieldClass = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+
+    /**
+     * Defines the way how to process query result
+     */
+    private enum ReturnStrategy {
+        /** Need to return only one value. */
+        ONE_VALUE,
+
+        /** Need to return one cache entry */
+        CACHE_ENTRY,
+
+        /** Need to return list of cache entries */
+        LIST_OF_CACHE_ENTRIES,
+
+        /** Need to return list of values */
+        LIST_OF_VALUES,
+
+        /** Need to return list of lists */
+        LIST_OF_LISTS,
+
+        /** Need to return slice */
+        SLICE_OF_VALUES,
+
+        /** Slice of cache entries */
+        SLICE_OF_CACHE_ENTRIES,
+
+        /** Slice of lists */
+        SLICE_OF_LISTS,
+
+        /** Need to return Page of values */
+        PAGE_OF_VALUES,
+
+        /** Need to return stream of values */
+        STREAM_OF_VALUES,
+    }
+
+    /** */
+    private final Class<?> type;
+
+    /** */
+    private final IgniteQuery staticQuery;
+
+    /** */
+    private final IgniteCache cache;
+
+    /** */
+    private final Ignite ignite;
+
+    /** Required by qryStr field query type for binary manipulation */
+    private IgniteBinaryImpl igniteBinary;
+
+    /** */
+    private BinaryType igniteBinType;
+
+    /** */
+    private final Method mtd;
+
+    /** */
+    private final RepositoryMetadata metadata;
+
+    /** */
+    private final ProjectionFactory factory;
+
+    /** */
+    private final ReturnStrategy staticReturnStgy;
+
+    /** Detect if returned data from method is projected */
+    private final boolean hasProjection;
+
+    /** Whether projection is dynamic (provided as method parameter) */
+    private final boolean hasDynamicProjection;
+
+    /** Dynamic projection parameter index */
+    private final int dynamicProjectionIndex;
+
+    /** Dynamic query configuration */
+    private final int dynamicQueryConfigurationIndex;
+
+    /** The return query method */
+    private final QueryMethod qMethod;
+
+    /** The return domain class of QueryMethod */
+    private final Class<?> returnedDomainClass;
+
+    /** */
+    private final SpelExpressionParser expressionParser;
+
+    /** Could provide ExtensionAwareQueryMethodEvaluationContextProvider */
+    private final EvaluationContextProvider queryMethodEvaluationContextProvider;
+
+    /** Static query configuration. */
+    private final DynamicQueryConfig staticQueryConfiguration;
+
+    /**
+     * Instantiates a new Ignite repository query.
+     *
+     * @param ignite                               the ignite
+     * @param metadata                             Metadata.
+     * @param staticQuery                          Query.
+     * @param mtd                                  Method.
+     * @param factory                              Factory.
+     * @param cache                                Cache.
+     * @param staticQueryConfiguration             the query configuration
+     * @param queryMethodEvaluationContextProvider the query method evaluation context provider
+     */
+    public IgniteRepositoryQuery(Ignite ignite,
+        RepositoryMetadata metadata,
+        @Nullable IgniteQuery staticQuery,
+        Method mtd,
+        ProjectionFactory factory,
+        IgniteCache cache,
+        @Nullable DynamicQueryConfig staticQueryConfiguration,
+        EvaluationContextProvider queryMethodEvaluationContextProvider) {
+        this.metadata = metadata;
+        this.mtd = mtd;
+        this.factory = factory;
+        type = metadata.getDomainType();
+
+        this.cache = cache;
+        this.ignite = ignite;
+
+        this.staticQueryConfiguration = staticQueryConfiguration;
+        this.staticQuery = staticQuery;
+
+        if (this.staticQuery != null)
+            staticReturnStgy = calcReturnType(mtd, this.staticQuery.isFieldQuery());
+        else
+            staticReturnStgy = null;
+
+        expressionParser = new SpelExpressionParser();
+        this.queryMethodEvaluationContextProvider = queryMethodEvaluationContextProvider;
+
+        qMethod = getQueryMethod();
+
+        // control projection
+        hasDynamicProjection = getQueryMethod().getParameters().hasDynamicProjection();
+        hasProjection = hasDynamicProjection || getQueryMethod().getResultProcessor().getReturnedType()
+            .isProjecting();
+
+        dynamicProjectionIndex = qMethod.getParameters().getDynamicProjectionIndex();
+
+        returnedDomainClass = getQueryMethod().getReturnedObjectType();
+
+        dynamicQueryConfigurationIndex = getDynamicQueryConfigurationIndex(qMethod);
+
+        // ensure dynamic query configuration param exists if dynamicQuery = true
+        if (dynamicQueryConfigurationIndex == -1 && this.staticQuery == null) {
+            throw new IllegalStateException(
+                "When passing dynamicQuery = true via org.apache.ignite.springdata.repository.config.Query "
+                    + "annotation, you must provide a non null method parameter of type DynamicQueryConfig");
+        }
+        // ensure domain class is registered on marshaller to transform row to entity
+        registerClassOnMarshaller(((IgniteEx)ignite).context(), type);
+    }
+
+    /**
+     * {@inheritDoc} @param values the values
+     *
+     * @return the object
+     */
+    @Override public Object execute(Object[] values) {
+        Object[] parameters = values;
+
+        // config via Query annotation (dynamicQuery = false)
+        DynamicQueryConfig config = staticQueryConfiguration;
+
+        // or condition to allow query tunning
+        if (config == null || dynamicQueryConfigurationIndex != -1) {
+            DynamicQueryConfig newConfig = (DynamicQueryConfig)values[dynamicQueryConfigurationIndex];
+            parameters = ArrayUtils.removeElement(parameters, dynamicQueryConfigurationIndex);
+            if (newConfig != null) {
+                // upset query configuration
+                config = newConfig;
+            }
+        }
+        // query configuration is required, via Query annotation or per parameter (within provided values param)
+        if (config == null) {
+            throw new IllegalStateException(
+                "Unable to execute query. When passing dynamicQuery = true via org.apache.ignite.springdata"
+                    + ".repository.config.Query annotation, you must provide a non null method parameter of type "
+                    + "DynamicQueryConfig");
+        }
+
+        IgniteQuery qry = getQuery(config);
+
+        ReturnStrategy returnStgy = getReturnStgy(qry);
+
+        Query iQry = prepareQuery(qry, config, returnStgy, parameters);
+
+        QueryCursor qryCursor = cache.query(iQry);
+
+        return transformQueryCursor(qry, returnStgy, parameters, qryCursor);
+    }
+
+    /** {@inheritDoc} */
+    @Override public QueryMethod getQueryMethod() {
+        return new QueryMethod(mtd, metadata, factory);
+    }
+
+    private <T extends Parameter> int getDynamicQueryConfigurationIndex(QueryMethod method) {
+        Iterator<T> it = (Iterator<T>)method.getParameters().iterator();
+        int i = 0;
+        boolean found = false;
+        int index = -1;
+        while (it.hasNext()) {
+            T parameter = it.next();
+
+            if (DynamicQueryConfig.class.isAssignableFrom(parameter.getType())) {
+                if (found) {
+                    throw new IllegalStateException("Invalid '" + method.getName() + "' repository method signature. "
+                        + "Only ONE DynamicQueryConfig parameter is allowed");
+                }
+
+                found = true;
+                index = i;
+            }
+
+            i++;
+        }
+        return index;
+    }
+
+    /** */
+    private synchronized IgniteBinaryImpl binary() {
+        if (igniteBinary == null)
+            igniteBinary = (IgniteBinaryImpl)ignite.binary();
+
+        return igniteBinary;
+    }
+
+    /** */
+    private synchronized BinaryType binType() {
+        if (igniteBinType == null)
+            igniteBinType = binary().type(type);
+
+        return igniteBinType;
+    }
+
+    /**
+     * @param mtd Method.
+     * @param isFieldQry Is field query.
+     * @return Return strategy type.
+     */
+    private ReturnStrategy calcReturnType(Method mtd, boolean isFieldQry) {
+        Class<?> returnType = mtd.getReturnType();
+
+        if (returnType == Slice.class) {
+            if (isFieldQry) {
+                if (hasAssignableGenericReturnTypeFrom(ArrayList.class, mtd))
+                    return ReturnStrategy.SLICE_OF_LISTS;
+            }
+            else if (hasAssignableGenericReturnTypeFrom(Cache.Entry.class, mtd))
+                return ReturnStrategy.SLICE_OF_CACHE_ENTRIES;
+            return ReturnStrategy.SLICE_OF_VALUES;
+        }
+        else if (returnType == Page.class)
+            return ReturnStrategy.PAGE_OF_VALUES;
+        else if (returnType == Stream.class)
+            return ReturnStrategy.STREAM_OF_VALUES;
+        else if (Cache.Entry.class.isAssignableFrom(returnType))
+            return ReturnStrategy.CACHE_ENTRY;
+        else if (Iterable.class.isAssignableFrom(returnType)) {
+            if (isFieldQry) {
+                if (hasAssignableGenericReturnTypeFrom(ArrayList.class, mtd))
+                    return ReturnStrategy.LIST_OF_LISTS;
+            }
+            else if (hasAssignableGenericReturnTypeFrom(Cache.Entry.class, mtd))
+                return ReturnStrategy.LIST_OF_CACHE_ENTRIES;
+            return ReturnStrategy.LIST_OF_VALUES;
+        }
+        else
+            return ReturnStrategy.ONE_VALUE;
+    }
+
+    /**
+     * @param cls Class.
+     * @param mtd Method.
+     * @return if {@code mtd} return type is assignable from {@code cls}
+     */
+    private boolean hasAssignableGenericReturnTypeFrom(Class<?> cls, Method mtd) {
+        Type genericReturnType = mtd.getGenericReturnType();
+
+        if (!(genericReturnType instanceof ParameterizedType))
+            return false;
+
+        Type[] actualTypeArguments = ((ParameterizedType)genericReturnType).getActualTypeArguments();
+
+        if (actualTypeArguments.length == 0)
+            return false;
+
+        if (actualTypeArguments[0] instanceof ParameterizedType) {
+            ParameterizedType type = (ParameterizedType)actualTypeArguments[0];
+
+            Class<?> type1 = (Class)type.getRawType();
+
+            return type1.isAssignableFrom(cls);
+        }
+
+        if (actualTypeArguments[0] instanceof Class) {
+            Class typeArg = (Class)actualTypeArguments[0];
+
+            return typeArg.isAssignableFrom(cls);
+        }
+
+        return false;
+    }
+
+    /**
+     * When select fields by query H2 returns Timestamp for types java.util.Date and java.qryStr.Timestamp
+     *
+     * @see org.apache.ignite.internal.processors.query.h2.H2DatabaseType map.put(Timestamp.class, TIMESTAMP)
+     * map.put(java.util.Date.class, TIMESTAMP) map.put(java.qryStr.Date.class, DATE)
+     */
+    private static <T> T fixExpectedType(final Object object, final Class<T> expected) {
+        if (expected != null && object instanceof java.sql.Timestamp && expected.equals(java.util.Date.class))
+            return (T)new java.util.Date(((java.sql.Timestamp)object).getTime());
+
+        return (T)object;
+    }
+
+    /**
+     * @param cfg Config.
+     */
+    private IgniteQuery getQuery(@Nullable DynamicQueryConfig cfg) {
+        if (staticQuery != null)
+            return staticQuery;
+
+        if (cfg != null && (StringUtils.hasText(cfg.value()) || cfg.textQuery())) {
+            return new IgniteQuery(cfg.value(),
+                !cfg.textQuery() && (isFieldQuery(cfg.value()) || cfg.forceFieldsQuery()), cfg.textQuery(),
+                false, IgniteQueryGenerator.getOptions(mtd));
+        }
+
+        throw new IllegalStateException("Unable to obtain a valid query. When passing dynamicQuery = true via org"
+            + ".apache.ignite.springdata.repository.config.Query annotation, you must"
+            + " provide a non null method parameter of type DynamicQueryConfig with a "
+            + "non empty value (query string) or textQuery = true");
+    }
+
+    /**
+     * @param qry Query.
+     */
+    private ReturnStrategy getReturnStgy(IgniteQuery qry) {
+        if (staticReturnStgy != null)
+            return staticReturnStgy;
+
+        if (qry != null)
+            return calcReturnType(mtd, qry.isFieldQuery());
+
+        throw new IllegalStateException("Unable to obtain a valid return strategy. When passing dynamicQuery = true "
+            + "via org.apache.ignite.springdata.repository.config.Query annotation, "
+            + "you must provide a non null method parameter of type "
+            + "DynamicQueryConfig with a non empty value (query string) or textQuery "
+            + "= true");
+    }
+
+    /**
+     * @param cls Class.
+     */
+    private static boolean isPrimitiveOrWrapper(Class<?> cls) {
+        return cls.isPrimitive() ||
+            Boolean.class.equals(cls) ||
+            Byte.class.equals(cls) ||
+            Character.class.equals(cls) ||
+            Short.class.equals(cls) ||
+            Integer.class.equals(cls) ||
+            Long.class.equals(cls) ||
+            Float.class.equals(cls) ||
+            Double.class.equals(cls) ||
+            Void.class.equals(cls) ||
+            String.class.equals(cls) ||
+            UUID.class.equals(cls);
+    }
+
+    /**
+     * @param prmtrs    Prmtrs.
+     * @param qryCursor Query cursor.
+     * @return Query cursor or slice
+     */
+    @Nullable
+    private Object transformQueryCursor(IgniteQuery qry,
+        ReturnStrategy returnStgy,
+        Object[] prmtrs,
+        QueryCursor qryCursor) {
+        final Class<?> returnClass;
+
+        if (hasProjection) {
+            if (hasDynamicProjection)
+                returnClass = (Class<?>)prmtrs[dynamicProjectionIndex];
+            else
+                returnClass = returnedDomainClass;
+        }
+        else
+            returnClass = returnedDomainClass;
+
+        if (qry.isFieldQuery()) {
+            // take control over single primite result from queries, i.e. DELETE, SELECT COUNT, UPDATE ...
+            boolean singlePrimitiveResult = isPrimitiveOrWrapper(returnClass);
+
+            final List<GridQueryFieldMetadata> meta = ((QueryCursorEx)qryCursor).fieldsMeta();
+
+            Function<List<?>, ?> cWrapperTransformFunction = null;
+
+            if (type.equals(returnClass)) {
+                IgniteBinaryImpl binary = binary();
+                BinaryType binType = binType();
+                cWrapperTransformFunction = row -> rowToEntity(binary, binType, row, meta);
+            }
+            else {
+                if (hasProjection || singlePrimitiveResult) {
+                    if (singlePrimitiveResult)
+                        cWrapperTransformFunction = row -> row.get(0);
+                    else {
+                        // Map row -> projection class
+                        cWrapperTransformFunction = row -> factory
+                            .createProjection(returnClass, rowToMap(row, meta));
+                    }
+                }
+                else
+                    cWrapperTransformFunction = row -> rowToMap(row, meta);
+            }
+
+            QueryCursorWrapper<?, ?> cWrapper = new QueryCursorWrapper<>((QueryCursor<List<?>>)qryCursor,
+                cWrapperTransformFunction);
+
+            switch (returnStgy) {
+                case PAGE_OF_VALUES:
+                    return new PageImpl(cWrapper.getAll(), (Pageable)prmtrs[prmtrs.length - 1], 0);
+                case LIST_OF_VALUES:
+                    return cWrapper.getAll();
+                case STREAM_OF_VALUES:
+                    return cWrapper.stream();
+                case ONE_VALUE:
+                    Iterator<?> iter = cWrapper.iterator();
+                    if (iter.hasNext()) {
+                        Object resp = iter.next();
+                        U.closeQuiet(cWrapper);
+                        return resp;
+                    }
+                    return null;
+                case SLICE_OF_VALUES:
+                    return new SliceImpl(cWrapper.getAll(), (Pageable)prmtrs[prmtrs.length - 1], true);
+                case SLICE_OF_LISTS:
+                    return new SliceImpl(qryCursor.getAll(), (Pageable)prmtrs[prmtrs.length - 1], true);
+                case LIST_OF_LISTS:
+                    return qryCursor.getAll();
+                default:
+                    throw new IllegalStateException();
+            }
+        }
+        else {
+            Iterable<CacheEntryImpl> qryIter = (Iterable<CacheEntryImpl>)qryCursor;
+
+            Function<CacheEntryImpl, ?> cWrapperTransformFunction;
+
+            if (hasProjection && !type.equals(returnClass))
+                cWrapperTransformFunction = row -> factory.createProjection(returnClass, row.getValue());
+            else
+                cWrapperTransformFunction = row -> row.getValue();
+
+            QueryCursorWrapper<?, ?> cWrapper = new QueryCursorWrapper<>((QueryCursor<CacheEntryImpl>)qryCursor,
+                cWrapperTransformFunction);
+
+            switch (returnStgy) {
+                case PAGE_OF_VALUES:
+                    return new PageImpl(cWrapper.getAll(), (Pageable)prmtrs[prmtrs.length - 1], 0);
+                case LIST_OF_VALUES:
+                    return cWrapper.getAll();
+                case STREAM_OF_VALUES:
+                    return cWrapper.stream();
+                case ONE_VALUE:
+                    Iterator<?> iter1 = cWrapper.iterator();
+                    if (iter1.hasNext()) {
+                        Object resp = iter1.next();
+                        U.closeQuiet(cWrapper);
+                        return resp;
+                    }
+                    return null;
+                case CACHE_ENTRY:
+                    Iterator<?> iter2 = qryIter.iterator();
+                    if (iter2.hasNext()) {
+                        Object resp2 = iter2.next();
+                        U.closeQuiet(qryCursor);
+                        return resp2;
+                    }
+                    return null;
+                case SLICE_OF_VALUES:
+                    return new SliceImpl(cWrapper.getAll(), (Pageable)prmtrs[prmtrs.length - 1], true);
+                case SLICE_OF_CACHE_ENTRIES:
+                    return new SliceImpl(qryCursor.getAll(), (Pageable)prmtrs[prmtrs.length - 1], true);
+                case LIST_OF_CACHE_ENTRIES:
+                    return qryCursor.getAll();
+                default:
+                    throw new IllegalStateException();
+            }
+        }
+    }
+
+    /**
+     * Extract bindable values
+     *
+     * @param values            values invoking query method
+     * @param queryMethodParams query method parameter definitions
+     * @param queryBindings     All parameters found on query string that need to be binded
+     * @return new list of parameters
+     */
+    private Object[] extractBindableValues(Object[] values,
+        Parameters<?, ?> queryMethodParams,
+        List<ParameterBinding> queryBindings) {
+        // no binding params then exit
+        if (queryBindings.isEmpty())
+            return values;
+
+        Object[] newValues = new Object[queryBindings.size()];
+
+        // map bindable parameters from query method: (index/name) - index
+        HashMap<String, Integer> methodParams = new HashMap<>();
+
+        // create an evaluation context for custom query
+        EvaluationContext queryEvalContext = queryMethodEvaluationContextProvider
+            .getEvaluationContext(queryMethodParams, values);
+
+        // By default queryEvalContext:
+        // - make accesible query method parameters by index:
+        // @Query("select u from User u where u.age = ?#{[0]}")
+        // List<User> findUsersByAge(int age);
+        // - make accesible query method parameters by name:
+        // @Query("select u from User u where u.firstname = ?#{#customer.firstname}")
+        // List<User> findUsersByCustomersFirstname(@Param("customer") Customer customer);
+
+        // query method param's index by name and position
+        queryMethodParams.getBindableParameters().forEach(p -> {
+            if (p.isNamedParameter()) {
+                // map by name (annotated by @Param)
+                methodParams.put(p.getName().get(), p.getIndex());
+            }
+            // map by position
+            methodParams.put(String.valueOf(p.getIndex()), p.getIndex());
+        });
+
+        // process all parameters on query and extract new values to bind
+        for (int i = 0; i < queryBindings.size(); i++) {
+            ParameterBinding p = queryBindings.get(i);
+
+            if (p.isExpression()) {
+                // Evaluate SpEl expressions (synthetic parameter value) , example ?#{#customer.firstname}
+                newValues[i] = expressionParser.parseExpression(p.getExpression()).getValue(queryEvalContext);
+            }
+            else {
+                // Extract parameter value by name or position respectively from invoking values
+                newValues[i] = values[methodParams.get(
+                    p.getName() != null ? p.getName() : String.valueOf(p.getRequiredPosition() - 1))];
+            }
+        }
+
+        return newValues;
+    }
+
+    /**
+     * @param qry        Query.
+     * @param config     Config.
+     * @param returnStgy Return stgy.
+     * @param values     Values.
+     * @return prepared query for execution
+     */
+    private Query prepareQuery(IgniteQuery qry, DynamicQueryConfig config, ReturnStrategy returnStgy, Object[] values) {
+        Object[] parameters = values;
+
+        String queryString = qry.qryStr();
+
+        Query query;
+
+        checkRequiredPageable(returnStgy, values);
+
+        if (!qry.isTextQuery()) {
+            if (!qry.isAutogenerated()) {
+                StringQuery squery = new ExpressionBasedStringQuery(queryString, metadata, expressionParser);
+                queryString = squery.getQueryString();
+                parameters = extractBindableValues(parameters, getQueryMethod().getParameters(),
+                    squery.getParameterBindings());
+            }
+            else {
+                // remove dynamic projection from parameters
+                if (hasDynamicProjection)
+                    parameters = ArrayUtils.remove(parameters, dynamicProjectionIndex);
+            }
+
+            switch (qry.options()) {
+                case SORTING:
+                    queryString = IgniteQueryGenerator
+                        .addSorting(new StringBuilder(queryString), (Sort)values[values.length - 1])
+                        .toString();
+                    if (qry.isAutogenerated())
+                        parameters = Arrays.copyOfRange(parameters, 0, values.length - 1);
+                    break;
+                case PAGINATION:
+                    queryString = IgniteQueryGenerator
+                        .addPaging(new StringBuilder(queryString), (Pageable)values[values.length - 1])
+                        .toString();
+                    if (qry.isAutogenerated())
+                        parameters = Arrays.copyOfRange(parameters, 0, values.length - 1);
+                    break;
+                default:
+            }
+
+            if (qry.isFieldQuery()) {
+                SqlFieldsQuery sqlFieldsQry = new SqlFieldsQuery(queryString);
+                sqlFieldsQry.setArgs(parameters);
+
+                sqlFieldsQry.setCollocated(config.collocated());
+                sqlFieldsQry.setDistributedJoins(config.distributedJoins());
+                sqlFieldsQry.setEnforceJoinOrder(config.enforceJoinOrder());
+                sqlFieldsQry.setLazy(config.lazy());
+                sqlFieldsQry.setLocal(config.local());
+
+                if (config.parts() != null && config.parts().length > 0)
+                    sqlFieldsQry.setPartitions(config.parts());
+
+                sqlFieldsQry.setTimeout(config.timeout(), TimeUnit.MILLISECONDS);
+
+                query = sqlFieldsQry;
+            }
+            else {
+                SqlQuery sqlQry = new SqlQuery(type, queryString);
+                sqlQry.setArgs(parameters);
+
+                sqlQry.setDistributedJoins(config.distributedJoins());
+                sqlQry.setLocal(config.local());
+
+                if (config.parts() != null && config.parts().length > 0)
+                    sqlQry.setPartitions(config.parts());
+
+                sqlQry.setTimeout(config.timeout(), TimeUnit.MILLISECONDS);
+
+                query = sqlQry;
+            }
+        }
+        else {
+            int pageSize = -1;
+
+            switch (qry.options()) {
+                case PAGINATION:
+                    pageSize = ((Pageable)parameters[parameters.length - 1]).getPageSize();
+                    break;
+            }
+
+            // check if queryString contains SpEL template expressions and evaluate them if any
+            if (queryString.contains("#{")) {
+                EvaluationContext queryEvalContext = queryMethodEvaluationContextProvider
+                    .getEvaluationContext(getQueryMethod().getParameters(),
+                        values);
+
+                Object eval = expressionParser.parseExpression(queryString, ParserContext.TEMPLATE_EXPRESSION)
+                    .getValue(queryEvalContext);
+
+                if (!(eval instanceof String)) {
+                    throw new IllegalStateException(
+                        "TextQuery with SpEL expressions must produce a String response, but found " + eval.getClass()
+                            .getName()
+                            + ". Please, check your expression: " + queryString);
+                }
+                queryString = (String)eval;
+            }
+
+            TextQuery textQuery = new TextQuery(type, queryString, config.limit());
+
+            textQuery.setLocal(config.local());
+
+            if (pageSize > -1)
+                textQuery.setPageSize(pageSize);
+
+            query = textQuery;
+        }
+        return query;
+    }
+
+    /** */
+    private static Map<String, Object> rowToMap(final List<?> row, final List<GridQueryFieldMetadata> meta) {
+        // use treemap with case insensitive property name
+        final TreeMap<String, Object> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        for (int i = 0; i < meta.size(); i++) {
+            // don't want key or val columns
+            final String metaField = meta.get(i).fieldName().toLowerCase();
+            if (!metaField.equalsIgnoreCase(QueryUtils.KEY_FIELD_NAME) && !metaField.equalsIgnoreCase(
+                QueryUtils.VAL_FIELD_NAME))
+                map.put(metaField, row.get(i));
+        }
+        return map;
+    }
+
+    /**
+     * convert row ( with list of field values) into domain entity
+     */
+    private <V> V rowToEntity(final IgniteBinaryImpl binary,
+        final BinaryType binType,
+        final List<?> row,
+        final List<GridQueryFieldMetadata> meta) {
+        // additional data returned by query not present on domain object type
+        final TreeMap<String, Object> metadata = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        final BinaryObjectBuilder bldr = binary.builder(binType.typeName());
+
+        for (int i = 0; i < row.size(); i++) {
+            final GridQueryFieldMetadata fMeta = meta.get(i);
+            final String metaField = fMeta.fieldName();
+            // add existing entity fields to binary object
+            if (binType.field(fMeta.fieldName()) != null && !metaField.equalsIgnoreCase(QueryUtils.KEY_FIELD_NAME)
+                && !metaField.equalsIgnoreCase(QueryUtils.VAL_FIELD_NAME)) {
+                final Object fieldValue = row.get(i);
+                if (fieldValue != null) {
+                    final Class<?> clazz = getClassForBinaryField(binary, binType, fMeta);
+                    // null values must not be set into binary objects
+                    bldr.setField(metaField, fixExpectedType(fieldValue, clazz));
+                }
+            }
+            else {
+                // don't want key or val column... but wants null values
+                if (!metaField.equalsIgnoreCase(QueryUtils.KEY_FIELD_NAME) && !metaField.equalsIgnoreCase(
+                    QueryUtils.VAL_FIELD_NAME))
+                    metadata.put(metaField, row.get(i));
+            }
+        }
+        return bldr.build().deserialize();
+    }
+
+    /**
+     * Obtains real field class from resultset metadata field whether it's available
+     */
+    private Class<?> getClassForBinaryField(final IgniteBinaryImpl binary,
+        final BinaryType binType,
+        final GridQueryFieldMetadata fieldMeta) {
+        try {
+            final String fieldId = fieldMeta.schemaName() + "." + fieldMeta.typeName() + "." + fieldMeta.fieldName();
+
+            if (binaryFieldClass.containsKey(fieldId))
+                return binaryFieldClass.get(fieldId);
+
+            Class<?> clazz = null;
+
+            synchronized (binaryFieldClass) {
+
+                if (binaryFieldClass.containsKey(fieldId))
+                    return binaryFieldClass.get(fieldId);
+
+                String fieldName = null;
+
+                // search field name on binary type (query returns case insensitive
+                // field name) but BinaryType is not case insensitive
+                for (final String fname : binType.fieldNames()) {
+                    if (fname.equalsIgnoreCase(fieldMeta.fieldName())) {
+                        fieldName = fname;
+                        break;
+                    }
+                }
+
+                final CacheObjectBinaryProcessorImpl proc = (CacheObjectBinaryProcessorImpl)binary.processor();
+
+                // search for class by typeId, if not found use
+                // fieldMeta.fieldTypeName() class
+                clazz = BinaryUtils.resolveClass(proc.binaryContext(), binary.typeId(binType.fieldTypeName(fieldName)),
+                    fieldMeta.fieldTypeName(), ignite.configuration().getClassLoader(), true);
+
+                binaryFieldClass.put(fieldId, clazz);
+            }
+
+            return clazz;
+        }
+        catch (final Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * Validates operations that requires Pageable parameter
+     *
+     * @param returnStgy Return stgy.
+     * @param prmtrs     Prmtrs.
+     */
+    private void checkRequiredPageable(ReturnStrategy returnStgy, Object[] prmtrs) {
+        try {
+            if (returnStgy == ReturnStrategy.PAGE_OF_VALUES || returnStgy == ReturnStrategy.SLICE_OF_VALUES
+                || returnStgy == ReturnStrategy.SLICE_OF_CACHE_ENTRIES) {
+                Pageable page = (Pageable)prmtrs[prmtrs.length - 1];
+                page.isPaged();
+            }
+        }
+        catch (NullPointerException | IndexOutOfBoundsException | ClassCastException e) {
+            throw new IllegalStateException(
+                "For " + returnStgy.name() + " you must provide on last method parameter a non null Pageable instance");
+        }
+    }
+
+    /**
+     * @param ctx   Context.
+     * @param clazz Clazz.
+     */
+    private static void registerClassOnMarshaller(final GridKernalContext ctx, final Class<?> clazz) {
+        try {
+            // ensure class registration for marshaller on cluster...
+            if (!U.isJdk(clazz))
+                U.marshal(ctx, clazz.newInstance());
+        }
+        catch (final Exception ignored) {
+            // silent
+        }
+    }
+
+    /**
+     * Ignite QueryCursor wrapper.
+     * <p>
+     * Ensures closing underline cursor when there is no data.
+     *
+     * @param <T> input type
+     * @param <V> transformed output type
+     */
+    public static class QueryCursorWrapper<T, V> extends AbstractCollection<V> implements QueryCursor<V> {
+        /**
+         * Delegate query cursor.
+         */
+        private final QueryCursor<T> delegate;
+
+        /**
+         * Transformer.
+         */
+        private final Function<T, V> transformer;
+
+        /**
+         * Instantiates a new Query cursor wrapper.
+         *
+         * @param delegate    delegate QueryCursor with T input elements
+         * @param transformer Function to transform T to V elements
+         */
+        public QueryCursorWrapper(final QueryCursor<T> delegate, final Function<T, V> transformer) {
+            this.delegate = delegate;
+            this.transformer = transformer;
+        }
+
+        /** {@inheritDoc} */
+        @Override public Iterator<V> iterator() {
+            final Iterator<T> it = delegate.iterator();
+
+            return new Iterator<V>() {
+                /** */
+                @Override public boolean hasNext() {
+                    if (!it.hasNext()) {
+                        U.closeQuiet(delegate);
+                        return false;
+                    }
+                    return true;
+                }
+
+                /** */
+                @Override public V next() {
+                    final V r = transformer.apply(it.next());
+                    if (r != null)
+                        return r;
+                    throw new NoSuchElementException();
+                }
+            };
+        }
+
+        /** {@inheritDoc} */
+        @Override public void close() {
+            U.closeQuiet(delegate);
+        }
+
+        /** {@inheritDoc} */
+        @Override public List<V> getAll() {
+            final List<V> data = new ArrayList<>();
+            delegate.forEach(i -> data.add(transformer.apply(i)));
+            U.closeQuiet(delegate);
+            return data;
+        }
+
+        /** {@inheritDoc} */
+        @Override public int size() {
+            return 0;
+        }
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/QueryUtils.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/QueryUtils.java
new file mode 100644
index 0000000..c586770
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/QueryUtils.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright 2008-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      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.springdata20.repository.query;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.data.util.Streamable;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+import static java.util.regex.Pattern.DOTALL;
+import static java.util.regex.Pattern.compile;
+
+/**
+ * Simple utility class to create queries.
+ *
+ * @author Oliver Gierke
+ * @author Kevin Raymond
+ * @author Thomas Darimont
+ * @author Komi Innocent
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ * @author Sébastien Péralta
+ * @author Jens Schauder
+ * @author Nils Borrmann
+ * @author Reda.Housni -Alaoui
+ */
+public abstract class QueryUtils {
+    /**
+     * The constant COUNT_QUERY_STRING.
+     */
+    public static final String COUNT_QUERY_STRING = "select count(%s) from %s x";
+
+    /**
+     * The constant DELETE_ALL_QUERY_STRING.
+     */
+    public static final String DELETE_ALL_QUERY_STRING = "delete from %s x";
+
+    /**
+     * Used Regex/Unicode categories (see http://www.unicode.org/reports/tr18/#General_Category_Property): Z Separator
+     * Cc Control Cf Format P Punctuation
+     */
+    private static final String IDENTIFIER = "[._[\\P{Z}&&\\P{Cc}&&\\P{Cf}&&\\P{P}]]+";
+
+    /**
+     * The Colon no double colon.
+     */
+    static final String COLON_NO_DOUBLE_COLON = "(?<![:\\\\]):";
+
+    /**
+     * The Identifier group.
+     */
+    static final String IDENTIFIER_GROUP = String.format("(%s)", IDENTIFIER);
+
+    /** */
+    private static final String COUNT_REPLACEMENT_TEMPLATE = "select count(%s) $5$6$7";
+
+    /** */
+    private static final String SIMPLE_COUNT_VALUE = "$2";
+
+    /** */
+    private static final String COMPLEX_COUNT_VALUE = "$3$6";
+
+    /** */
+    private static final String ORDER_BY_PART = "(?iu)\\s+order\\s+by\\s+.*$";
+
+    /** */
+    private static final Pattern ALIAS_MATCH;
+
+    /** */
+    private static final Pattern COUNT_MATCH;
+
+    /** */
+    private static final Pattern PROJECTION_CLAUSE = Pattern
+        .compile("select\\s+(.+)\\s+from", Pattern.CASE_INSENSITIVE);
+
+    /** */
+    private static final String JOIN = "join\\s+(fetch\\s+)?" + IDENTIFIER + "\\s+(as\\s+)?" + IDENTIFIER_GROUP;
+
+    /** */
+    private static final Pattern JOIN_PATTERN = Pattern.compile(JOIN, Pattern.CASE_INSENSITIVE);
+
+    /** */
+    private static final String EQUALS_CONDITION_STRING = "%s.%s = :%s";
+
+    /** */
+    private static final Pattern NAMED_PARAMETER = Pattern.compile(
+        COLON_NO_DOUBLE_COLON + IDENTIFIER + "|\\#" + IDENTIFIER, CASE_INSENSITIVE);
+
+    /** */
+    private static final Pattern CONSTRUCTOR_EXPRESSION;
+
+    /** */
+    private static final int QUERY_JOIN_ALIAS_GROUP_INDEX = 3;
+
+    /** */
+    private static final int VARIABLE_NAME_GROUP_INDEX = 4;
+
+    /** */
+    private static final Pattern FUNCTION_PATTERN;
+
+    static {
+        StringBuilder builder = new StringBuilder();
+        builder.append("(?<=from)"); // from as starting delimiter
+        builder.append("(?:\\s)+"); // at least one space separating
+        builder.append(IDENTIFIER_GROUP); // Entity name, can be qualified (any
+        builder.append("(?:\\sas)*"); // exclude possible "as" keyword
+        builder.append("(?:\\s)+"); // at least one space separating
+        builder.append("(?!(?:where))(\\w+)"); // the actual alias
+
+        ALIAS_MATCH = compile(builder.toString(), CASE_INSENSITIVE);
+
+        builder = new StringBuilder();
+        builder.append("(select\\s+((distinct )?(.+?)?)\\s+)?(from\\s+");
+        builder.append(IDENTIFIER);
+        builder.append("(?:\\s+as)?\\s+)");
+        builder.append(IDENTIFIER_GROUP);
+        builder.append("(.*)");
+
+        COUNT_MATCH = compile(builder.toString(), CASE_INSENSITIVE);
+
+        builder = new StringBuilder();
+        builder.append("select");
+        builder.append("\\s+"); // at least one space separating
+        builder.append("(.*\\s+)?"); // anything in between (e.g. distinct) at least one space separating
+        builder.append("new");
+        builder.append("\\s+"); // at least one space separating
+        builder.append(IDENTIFIER);
+        builder.append("\\s*"); // zero to unlimited space separating
+        builder.append("\\(");
+        builder.append(".*");
+        builder.append("\\)");
+
+        CONSTRUCTOR_EXPRESSION = compile(builder.toString(), CASE_INSENSITIVE + DOTALL);
+
+        builder = new StringBuilder();
+        // any function call including parameters within the brackets
+        builder.append("\\w+\\s*\\([\\w\\.,\\s'=]+\\)");
+        // the potential alias
+        builder.append("\\s+[as|AS]+\\s+(([\\w\\.]+))");
+
+        FUNCTION_PATTERN = compile(builder.toString());
+    }
+
+    /**
+     * Private constructor to prevent instantiation.
+     */
+    private QueryUtils() {
+        // No-op.
+    }
+
+    /**
+     * Returns the query string to execute an exists query for the given id attributes.
+     *
+     * @param entityName        the name of the entity to create the query for, must not be {@literal null}.
+     * @param cntQryPlaceHolder the placeholder for the count clause, must not be {@literal null}.
+     * @param idAttrs           the id attributes for the entity, must not be {@literal null}.
+     * @return the exists query string
+     */
+    public static String getExistsQueryString(String entityName,
+        String cntQryPlaceHolder,
+        Iterable<String> idAttrs) {
+        String whereClause = Streamable.of(idAttrs).stream() //
+            .map(idAttribute -> String.format(EQUALS_CONDITION_STRING, "x", idAttribute,
+                idAttribute)) //
+            .collect(Collectors.joining(" AND ", " WHERE ", ""));
+
+        return String.format(COUNT_QUERY_STRING, cntQryPlaceHolder, entityName) + whereClause;
+    }
+
+    /**
+     * Returns the query string for the given class name.
+     *
+     * @param template   must not be {@literal null}.
+     * @param entityName must not be {@literal null}.
+     * @return the template with placeholders replaced by the {@literal entityName}. Guaranteed to be not {@literal
+     *     null}.
+     */
+    public static String getQueryString(String template, String entityName) {
+        Assert.hasText(entityName, "Entity name must not be null or empty!");
+
+        return String.format(template, entityName);
+    }
+
+    /**
+     * Returns the aliases used for {@code left (outer) join}s.
+     *
+     * @param qry a query string to extract the aliases of joins from. Must not be {@literal null}.
+     * @return a {@literal Set} of aliases used in the query. Guaranteed to be not {@literal null}.
+     */
+    static Set<String> getOuterJoinAliases(String qry) {
+        Set<String> result = new HashSet<>();
+        Matcher matcher = JOIN_PATTERN.matcher(qry);
+
+        while (matcher.find()) {
+            String alias = matcher.group(QUERY_JOIN_ALIAS_GROUP_INDEX);
+            if (StringUtils.hasText(alias))
+                result.add(alias);
+        }
+
+        return result;
+    }
+
+    /**
+     * Returns the aliases used for aggregate functions like {@code SUM, COUNT, ...}.
+     *
+     * @param qry a {@literal String} containing a query. Must not be {@literal null}.
+     * @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}.
+     */
+    static Set<String> getFunctionAliases(String qry) {
+        Set<String> result = new HashSet<>();
+        Matcher matcher = FUNCTION_PATTERN.matcher(qry);
+
+        while (matcher.find()) {
+            String alias = matcher.group(1);
+
+            if (StringUtils.hasText(alias))
+                result.add(alias);
+        }
+
+        return result;
+    }
+
+    /**
+     * Resolves the alias for the entity to be retrieved from the given JPA query.
+     *
+     * @param qry must not be {@literal null}.
+     * @return Might return {@literal null}.
+     */
+    @Nullable
+    static String detectAlias(String qry) {
+        Matcher matcher = ALIAS_MATCH.matcher(qry);
+
+        return matcher.find() ? matcher.group(2) : null;
+    }
+
+    /**
+     * Creates a count projected query from the given original query.
+     *
+     * @param originalQry   must not be {@literal null}.
+     * @param cntProjection may be {@literal null}.
+     * @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}.
+     */
+    static String createCountQueryFor(String originalQry, @Nullable String cntProjection) {
+        Assert.hasText(originalQry, "OriginalQuery must not be null or empty!");
+
+        Matcher matcher = COUNT_MATCH.matcher(originalQry);
+        String countQuery;
+
+        if (cntProjection == null) {
+            String variable = matcher.matches() ? matcher.group(VARIABLE_NAME_GROUP_INDEX) : null;
+            boolean useVariable = variable != null && StringUtils.hasText(variable) && !variable.startsWith("new")
+                && !variable.startsWith("count(") && !variable.contains(",");
+
+            String replacement = useVariable ? SIMPLE_COUNT_VALUE : COMPLEX_COUNT_VALUE;
+            countQuery = matcher.replaceFirst(String.format(COUNT_REPLACEMENT_TEMPLATE, replacement));
+        }
+        else
+            countQuery = matcher.replaceFirst(String.format(COUNT_REPLACEMENT_TEMPLATE, cntProjection));
+
+        return countQuery.replaceFirst(ORDER_BY_PART, "");
+    }
+
+    /**
+     * Returns whether the given JPQL query contains a constructor expression.
+     *
+     * @param qry must not be {@literal null} or empty.
+     * @return boolean
+     */
+    public static boolean hasConstructorExpression(String qry) {
+        Assert.hasText(qry, "Query must not be null or empty!");
+
+        return CONSTRUCTOR_EXPRESSION.matcher(qry).find();
+    }
+
+    /**
+     * Returns the projection part of the query, i.e. everything between {@code select} and {@code from}.
+     *
+     * @param qry must not be {@literal null} or empty.
+     * @return projection
+     */
+    public static String getProjection(String qry) {
+        Assert.hasText(qry, "Query must not be null or empty!");
+
+        Matcher matcher = PROJECTION_CLAUSE.matcher(qry);
+        String projection = matcher.find() ? matcher.group(1) : "";
+        return projection.trim();
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/StringQuery.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/StringQuery.java
new file mode 100644
index 0000000..7280276
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/StringQuery.java
@@ -0,0 +1,879 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      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.springdata20.repository.query;
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.ignite.springdata20.repository.query.spel.SpelQueryContext;
+import org.apache.ignite.springdata20.repository.query.spel.SpelQueryContext.SpelExtractor;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.data.domain.Range;
+import org.springframework.data.domain.Range.Bound;
+import org.springframework.data.repository.query.parser.Part.Type;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+import static org.springframework.util.ObjectUtils.nullSafeEquals;
+import static org.springframework.util.ObjectUtils.nullSafeHashCode;
+
+/**
+ * Encapsulation of a JPA query String. Offers access to parameters as bindings. The internal query String is cleaned
+ * from decorated parameters like {@literal %:lastname%} and the matching bindings take care of applying the decorations
+ * in the {@link ParameterBinding#prepare(Object)} method. Note that this class also handles replacing SpEL expressions
+ * with synthetic bind parameters
+ *
+ * @author Oliver Gierke
+ * @author Thomas Darimont
+ * @author Oliver Wehrens
+ * @author Mark Paluch
+ * @author Jens Schauder
+ */
+class StringQuery implements DeclaredQuery {
+    /** */
+    private final String query;
+
+    /** */
+    private final List<ParameterBinding> bindings;
+
+    /** */
+    @Nullable
+    private final String alias;
+
+    /** */
+    private final boolean hasConstructorExpression;
+
+    /** */
+    private final boolean containsPageableInSpel;
+
+    /** */
+    private final boolean usesJdbcStyleParameters;
+
+    /**
+     * Creates a new {@link StringQuery} from the given JPQL query.
+     *
+     * @param query must not be {@literal null} or empty.
+     */
+    StringQuery(String query) {
+        Assert.hasText(query, "Query must not be null or empty!");
+
+        bindings = new ArrayList<>();
+        containsPageableInSpel = query.contains("#pageable");
+
+        Metadata queryMeta = new Metadata();
+        this.query = ParameterBindingParser.INSTANCE
+            .parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query, bindings,
+                queryMeta);
+
+        usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters;
+        alias = QueryUtils.detectAlias(query);
+        hasConstructorExpression = QueryUtils.hasConstructorExpression(query);
+    }
+
+    /**
+     * Returns whether we have found some like bindings.
+     */
+    boolean hasParameterBindings() {
+        return !bindings.isEmpty();
+    }
+
+    /** */
+    String getProjection() {
+        return QueryUtils.getProjection(query);
+    }
+
+    // See org.springframework.data.jpa.repository.query.DeclaredQuery#getParameterBindings()
+    /** {@inheritDoc} */
+    @Override public List<ParameterBinding> getParameterBindings() {
+        return bindings;
+    }
+
+    // See org.springframework.data.jpa.repository.query.DeclaredQuery#deriveCountQuery(java.lang.String, java.lang
+    /** {@inheritDoc} */
+    @Override public DeclaredQuery deriveCountQuery(@Nullable String countQuery,
+        @Nullable String countQueryProjection) {
+        return DeclaredQuery
+            .of(countQuery != null ? countQuery : QueryUtils.createCountQueryFor(query, countQueryProjection));
+    }
+
+    // See org.springframework.data.jpa.repository.query.DeclaredQuery#usesJdbcStyleParameters()
+    /** */
+    @Override public boolean usesJdbcStyleParameters() {
+        return usesJdbcStyleParameters;
+    }
+
+    // See org.springframework.data.jpa.repository.query.DeclaredQuery#getQueryString()
+    /** {@inheritDoc} */
+    @Override public String getQueryString() {
+        return query;
+    }
+
+    // See org.springframework.data.jpa.repository.query.DeclaredQuery#getAlias()
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public String getAlias() {
+        return alias;
+    }
+
+    // See org.springframework.data.jpa.repository.query.DeclaredQuery#hasConstructorExpression()
+    /** {@inheritDoc} */
+    @Override public boolean hasConstructorExpression() {
+        return hasConstructorExpression;
+    }
+
+    // See org.springframework.data.jpa.repository.query.DeclaredQuery#isDefaultProjection()
+    /** {@inheritDoc} */
+    @Override public boolean isDefaultProjection() {
+        return getProjection().equalsIgnoreCase(alias);
+    }
+
+    // See org.springframework.data.jpa.repository.query.DeclaredQuery#hasNamedParameter()
+    /** {@inheritDoc} */
+    @Override public boolean hasNamedParameter() {
+        return bindings.stream().anyMatch(b -> b.getName() != null);
+    }
+
+    // See org.springframework.data.jpa.repository.query.DeclaredQuery#usesPaging()
+    /** {@inheritDoc} */
+    @Override public boolean usesPaging() {
+        return containsPageableInSpel;
+    }
+
+    /**
+     * A parser that extracts the parameter bindings from a given query string.
+     *
+     * @author Thomas Darimont
+     */
+    enum ParameterBindingParser {
+        /** */
+        INSTANCE;
+
+        /** */
+        private static final String EXPRESSION_PARAMETER_PREFIX = "__$synthetic$__";
+
+        /** */
+        public static final String POSITIONAL_OR_INDEXED_PARAMETER = "\\?(\\d*+(?![#\\w]))";
+        // .....................................................................^ not followed by a hash or a letter.
+        // .................................................................^ zero or more digits.
+        // .............................................................^ start with a question mark.
+
+        /** */
+        private static final Pattern PARAMETER_BINDING_BY_INDEX = Pattern.compile(POSITIONAL_OR_INDEXED_PARAMETER);
+
+        /** */
+        private static final Pattern PARAMETER_BINDING_PATTERN;
+
+        /** */
+        private static final String MESSAGE =
+            "Already found parameter binding with same index / parameter name but differing binding type! "
+                + "Already have: %s, found %s! If you bind a parameter multiple times make sure they use the same "
+                + "binding.";
+
+        /** */
+        private static final int INDEXED_PARAMETER_GROUP = 4;
+
+        /** */
+        private static final int NAMED_PARAMETER_GROUP = 6;
+
+        /** */
+        private static final int COMPARISION_TYPE_GROUP = 1;
+
+        static {
+            List<String> keywords = new ArrayList<>();
+
+            for (ParameterBindingType type : ParameterBindingType.values()) {
+                if (type.getKeyword() != null) {
+                    keywords.add(type.getKeyword());
+                }
+            }
+
+            StringBuilder builder = new StringBuilder();
+            builder.append("(");
+            builder.append(StringUtils.collectionToDelimitedString(keywords, "|")); // keywords
+            builder.append(")?");
+            builder.append("(?: )?"); // some whitespace
+            builder.append("\\(?"); // optional braces around parameters
+            builder.append("(");
+            builder.append("%?(" + POSITIONAL_OR_INDEXED_PARAMETER + ")%?"); // position parameter and parameter index
+            builder.append("|"); // or
+
+            // named parameter and the parameter name
+            builder.append("%?(" + QueryUtils.COLON_NO_DOUBLE_COLON + QueryUtils.IDENTIFIER_GROUP + ")%?");
+
+            builder.append(")");
+            builder.append("\\)?"); // optional braces around parameters
+
+            PARAMETER_BINDING_PATTERN = Pattern.compile(builder.toString(), CASE_INSENSITIVE);
+        }
+
+        /**
+         * Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings.
+         * Returns the cleaned up query.
+         */
+        private String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query,
+            List<ParameterBinding> bindings,
+            Metadata queryMeta) {
+            int greatestParamIdx = tryFindGreatestParameterIndexIn(query);
+            boolean parametersShouldBeAccessedByIdx = greatestParamIdx != -1;
+
+            /*
+             * Prefer indexed access over named parameters if only SpEL Expression parameters are present.
+             */
+            if (!parametersShouldBeAccessedByIdx && query.contains("?#{")) {
+                parametersShouldBeAccessedByIdx = true;
+                greatestParamIdx = 0;
+            }
+
+            SpelExtractor spelExtractor = createSpelExtractor(query, parametersShouldBeAccessedByIdx,
+                greatestParamIdx);
+
+            String resultingQry = spelExtractor.getQueryString();
+            Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(resultingQry);
+            QuotationMap quotedAreas = new QuotationMap(resultingQry);
+
+            int expressionParamIdx = parametersShouldBeAccessedByIdx ? greatestParamIdx : 0;
+
+            boolean usesJpaStyleParameters = false;
+
+            while (matcher.find()) {
+                if (quotedAreas.isQuoted(matcher.start()))
+                    continue;
+
+                String paramIdxStr = matcher.group(INDEXED_PARAMETER_GROUP);
+                String paramName = paramIdxStr != null ? null : matcher.group(NAMED_PARAMETER_GROUP);
+                Integer paramIdx = getParameterIndex(paramIdxStr);
+
+                String typeSrc = matcher.group(COMPARISION_TYPE_GROUP);
+                String expression = spelExtractor
+                    .getParameter(paramName == null ? paramIdxStr : paramName);
+                String replacement = null;
+
+                Assert.isTrue(paramIdxStr != null || paramName != null,
+                    () -> String.format("We need either a name or an index! Offending query string: %s", query));
+
+                expressionParamIdx++;
+                if (paramIdxStr != null && paramIdxStr.isEmpty()) {
+                    queryMeta.usesJdbcStyleParameters = true;
+                    paramIdx = expressionParamIdx;
+                }
+                else
+                    usesJpaStyleParameters = true;
+
+                // named parameters (:param) will be untouched by spelExtractor, so replace them by ? as we don't
+                // know position
+                if (paramName != null)
+                    replacement = "?";
+
+                if (usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) {
+                    throw new IllegalArgumentException(
+                        "Mixing of ? (? or :myNamedParam) parameters and other forms like ?1 (SpEL espressions or "
+                            + "indexed) is not supported!. Please, if you are using expressions or "
+                            + "indexed params, replace all named parameters by expressions. Example :myNamedParam "
+                            + "by ?#{#myNamedParam}.");
+                }
+
+                switch (ParameterBindingType.of(typeSrc)) {
+                    case LIKE:
+                        Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2));
+                        replacement = matcher.group(3);
+
+                        if (paramIdx != null)
+                            checkAndRegister(new LikeParameterBinding(paramIdx, likeType, expression), bindings);
+                        else {
+                            checkAndRegister(new LikeParameterBinding(paramName, likeType, expression), bindings);
+
+                            replacement = expression != null ? ":" + paramName : matcher.group(5);
+                        }
+
+                        break;
+
+                    case IN:
+                        if (paramIdx != null)
+                            checkAndRegister(new InParameterBinding(paramIdx, expression), bindings);
+                        else
+                            checkAndRegister(new InParameterBinding(paramName, expression), bindings);
+
+                        break;
+
+                    case AS_IS: // fall-through we don't need a special parameter binding for the given parameter.
+                    default:
+                        bindings.add(paramIdx != null
+                            ? new ParameterBinding(null, paramIdx, expression)
+                            : new ParameterBinding(paramName, null, expression));
+                }
+
+                if (replacement != null)
+                    resultingQry = replaceFirst(resultingQry, matcher.group(2), replacement);
+            }
+
+            return resultingQry;
+        }
+
+        /** */
+        private static SpelExtractor createSpelExtractor(String queryWithSpel,
+            boolean parametersShouldBeAccessedByIndex,
+            int greatestParameterIndex) {
+
+            /*
+             * If parameters need to be bound by index, we bind the synthetic expression parameters starting from
+             * position of the greatest discovered index parameter in order to
+             * not mix-up with the actual parameter indices.
+             */
+            int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0;
+
+            BiFunction<Integer, String, String> indexToParameterName = parametersShouldBeAccessedByIndex
+                ? (index, expression) -> String.valueOf(
+                index + expressionParameterIndex + 1)
+                : (index, expression) ->
+                EXPRESSION_PARAMETER_PREFIX + (index
+                    + 1);
+
+            String fixedPrefix = parametersShouldBeAccessedByIndex ? "?" : ":";
+
+            BiFunction<String, String, String> parameterNameToReplacement = (prefix, name) -> fixedPrefix + name;
+
+            return SpelQueryContext.of(indexToParameterName, parameterNameToReplacement).parse(queryWithSpel);
+        }
+
+        /** */
+        private static String replaceFirst(String text, String substring, String replacement) {
+            int index = text.indexOf(substring);
+            if (index < 0)
+                return text;
+
+            return text.substring(0, index) + replacement + text.substring(index + substring.length());
+        }
+
+        /** */
+        @Nullable
+        private static Integer getParameterIndex(@Nullable String parameterIndexString) {
+            if (parameterIndexString == null || parameterIndexString.isEmpty())
+                return null;
+            return Integer.valueOf(parameterIndexString);
+        }
+
+        /** */
+        private static int tryFindGreatestParameterIndexIn(String query) {
+            Matcher parameterIndexMatcher = PARAMETER_BINDING_BY_INDEX.matcher(query);
+
+            int greatestParameterIndex = -1;
+            while (parameterIndexMatcher.find()) {
+
+                String parameterIndexString = parameterIndexMatcher.group(1);
+                Integer parameterIndex = getParameterIndex(parameterIndexString);
+                if (parameterIndex != null)
+                    greatestParameterIndex = Math.max(greatestParameterIndex, parameterIndex);
+            }
+
+            return greatestParameterIndex;
+        }
+
+        /** */
+        private static void checkAndRegister(ParameterBinding binding, List<ParameterBinding> bindings) {
+
+            bindings.stream() //
+                .filter(it -> it.hasName(binding.getName()) || it.hasPosition(binding.getPosition())) //
+                .forEach(it -> Assert.isTrue(it.equals(binding), String.format(MESSAGE, it, binding)));
+
+            if (!bindings.contains(binding))
+                bindings.add(binding);
+        }
+
+        /**
+         * An enum for the different types of bindings.
+         *
+         * @author Thomas Darimont
+         * @author Oliver Gierke
+         */
+        private enum ParameterBindingType {
+            // Trailing whitespace is intentional to reflect that the keywords must be used with at least one whitespace
+            // character, while = does not.
+            /** */
+            LIKE("like "),
+
+            /** */
+            IN("in "),
+
+            /** */
+            AS_IS(null);
+
+            /** */
+            @Nullable
+            private final String keyword;
+
+            /** */
+            ParameterBindingType(@Nullable String keyword) {
+                this.keyword = keyword;
+            }
+
+            /**
+             * Returns the keyword that will tirgger the binding type or {@literal null} if the type is not triggered by
+             * a keyword.
+             *
+             * @return the keyword
+             */
+            @Nullable
+            public String getKeyword() {
+                return keyword;
+            }
+
+            /**
+             * Return the appropriate {@link ParameterBindingType} for the given {@link String}. Returns {@literal
+             * #AS_IS} in case no other {@link ParameterBindingType} could be found.
+             */
+            static ParameterBindingType of(String typeSource) {
+                if (!StringUtils.hasText(typeSource))
+                    return AS_IS;
+
+                for (ParameterBindingType type : values()) {
+                    if (type.name().equalsIgnoreCase(typeSource.trim()))
+                        return type;
+                }
+
+                throw new IllegalArgumentException(String.format("Unsupported parameter binding type %s!", typeSource));
+            }
+        }
+    }
+
+    /**
+     * A generic parameter binding with name or position information.
+     *
+     * @author Thomas Darimont
+     */
+    static class ParameterBinding {
+        /** */
+        @Nullable
+        private final String name;
+
+        /** */
+        @Nullable
+        private final String expression;
+
+        /** */
+        @Nullable
+        private final Integer position;
+
+        /**
+         * Creates a new {@link ParameterBinding} for the parameter with the given position.
+         *
+         * @param position must not be {@literal null}.
+         */
+        ParameterBinding(Integer position) {
+            this(null, position, null);
+        }
+
+        /**
+         * Creates a new {@link ParameterBinding} for the parameter with the given name, position and expression
+         * information. Either {@literal name} or {@literal position} must be not {@literal null}.
+         *
+         * @param name       of the parameter may be {@literal null}.
+         * @param position   of the parameter may be {@literal null}.
+         * @param expression the expression to apply to any value for this parameter.
+         */
+        ParameterBinding(@Nullable String name, @Nullable Integer position, @Nullable String expression) {
+
+            if (name == null)
+                Assert.notNull(position, "Position must not be null!");
+
+            if (position == null)
+                Assert.notNull(name, "Name must not be null!");
+
+            this.name = name;
+            this.position = position;
+            this.expression = expression;
+        }
+
+        /**
+         * Returns whether the binding has the given name. Will always be {@literal false} in case the {@link
+         * ParameterBinding} has been set up from a position.
+         */
+        boolean hasName(@Nullable String name) {
+            return position == null && this.name != null && this.name.equals(name);
+        }
+
+        /**
+         * Returns whether the binding has the given position. Will always be {@literal false} in case the {@link
+         * ParameterBinding} has been set up from a name.
+         */
+        boolean hasPosition(@Nullable Integer position) {
+            return position != null && name == null && position.equals(this.position);
+        }
+
+        /**
+         * @return the name
+         */
+        @Nullable
+        public String getName() {
+            return name;
+        }
+
+        /**
+         * @return the name
+         * @throws IllegalStateException if the name is not available.
+         */
+        String getRequiredName() throws IllegalStateException {
+
+            String name = getName();
+
+            if (name != null)
+                return name;
+
+            throw new IllegalStateException(String.format("Required name for %s not available!", this));
+        }
+
+        /**
+         * @return the position
+         */
+        @Nullable
+        Integer getPosition() {
+            return position;
+        }
+
+        /**
+         * @return the position
+         * @throws IllegalStateException if the position is not available.
+         */
+        int getRequiredPosition() throws IllegalStateException {
+
+            Integer position = getPosition();
+
+            if (position != null)
+                return position;
+
+            throw new IllegalStateException(String.format("Required position for %s not available!", this));
+        }
+
+        /**
+         * @return {@literal true} if this parameter binding is a synthetic SpEL expression.
+         */
+        public boolean isExpression() {
+            return expression != null;
+        }
+
+        /** */
+        @Override public int hashCode() {
+
+            int result = 17;
+
+            result += nullSafeHashCode(name);
+            result += nullSafeHashCode(position);
+            result += nullSafeHashCode(expression);
+
+            return result;
+        }
+
+        /** */
+        @Override public boolean equals(Object obj) {
+
+            if (!(obj instanceof ParameterBinding))
+                return false;
+
+            ParameterBinding that = (ParameterBinding)obj;
+
+            return nullSafeEquals(name, that.name) && nullSafeEquals(position, that.position)
+                && nullSafeEquals(expression, that.expression);
+        }
+
+        /** */
+        @Override public String toString() {
+            return String.format("ParameterBinding [name: %s, position: %d, expression: %s]", getName(), getPosition(),
+                getExpression());
+        }
+
+        /**
+         * @param valueToBind value to prepare
+         */
+        @Nullable
+        public Object prepare(@Nullable Object valueToBind) {
+            return valueToBind;
+        }
+
+        /** */
+        @Nullable
+        public String getExpression() {
+            return expression;
+        }
+    }
+
+    /**
+     * Represents a {@link ParameterBinding} in a JPQL query augmented with instructions of how to apply a parameter as
+     * an {@code IN} parameter.
+     *
+     * @author Thomas Darimont
+     */
+    static class InParameterBinding extends ParameterBinding {
+        /**
+         * Creates a new {@link InParameterBinding} for the parameter with the given name.
+         */
+        InParameterBinding(String name, @Nullable String expression) {
+            super(name, null, expression);
+        }
+
+        /**
+         * Creates a new {@link InParameterBinding} for the parameter with the given position.
+         */
+        InParameterBinding(int position, @Nullable String expression) {
+            super(null, position, expression);
+        }
+
+        /*
+         * (non-Javadoc)
+         * @see org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding#prepare(java.lang.Object)
+         */
+        @Override public Object prepare(@Nullable Object value) {
+            if (!ObjectUtils.isArray(value))
+                return value;
+
+            int length = Array.getLength(value);
+            Collection<Object> result = new ArrayList<>(length);
+
+            for (int i = 0; i < length; i++)
+                result.add(Array.get(value, i));
+
+            return result;
+        }
+
+    }
+
+    /**
+     * Represents a parameter binding in a JPQL query augmented with instructions of how to apply a parameter as LIKE
+     * parameter. This allows expressions like {@code …like %?1} in the JPQL query, which is not allowed by plain JPA.
+     *
+     * @author Oliver Gierke
+     * @author Thomas Darimont
+     */
+    static class LikeParameterBinding extends ParameterBinding {
+        /** */
+        private static final List<Type> SUPPORTED_TYPES = Arrays.asList(Type.CONTAINING, Type.STARTING_WITH,
+            Type.ENDING_WITH, Type.LIKE);
+
+        /** */
+        private final Type type;
+
+        /**
+         * Creates a new {@link LikeParameterBinding} for the parameter with the given name and {@link Type}.
+         *
+         * @param name must not be {@literal null} or empty.
+         * @param type must not be {@literal null}.
+         */
+        LikeParameterBinding(String name, Type type) {
+            this(name, type, null);
+        }
+
+        /**
+         * Creates a new {@link LikeParameterBinding} for the parameter with the given name and {@link Type} and
+         * parameter binding input.
+         *
+         * @param name       must not be {@literal null} or empty.
+         * @param type       must not be {@literal null}.
+         * @param expression may be {@literal null}.
+         */
+        LikeParameterBinding(String name, Type type, @Nullable String expression) {
+
+            super(name, null, expression);
+
+            Assert.hasText(name, "Name must not be null or empty!");
+            Assert.notNull(type, "Type must not be null!");
+
+            Assert.isTrue(SUPPORTED_TYPES.contains(type), String.format("Type must be one of %s!",
+                StringUtils.collectionToCommaDelimitedString(SUPPORTED_TYPES)));
+
+            this.type = type;
+        }
+
+        /**
+         * Creates a new {@link LikeParameterBinding} for the parameter with the given position and {@link Type}.
+         *
+         * @param position position of the parameter in the query.
+         * @param type     must not be {@literal null}.
+         */
+        LikeParameterBinding(int position, Type type) {
+            this(position, type, null);
+        }
+
+        /**
+         * Creates a new {@link LikeParameterBinding} for the parameter with the given position and {@link Type}.
+         *
+         * @param position   position of the parameter in the query.
+         * @param type       must not be {@literal null}.
+         * @param expression may be {@literal null}.
+         */
+        LikeParameterBinding(int position, Type type, @Nullable String expression) {
+
+            super(null, position, expression);
+
+            Assert.isTrue(position > 0, "Position must be greater than zero!");
+            Assert.notNull(type, "Type must not be null!");
+
+            Assert.isTrue(SUPPORTED_TYPES.contains(type), String.format("Type must be one of %s!",
+                StringUtils.collectionToCommaDelimitedString(SUPPORTED_TYPES)));
+
+            this.type = type;
+        }
+
+        /**
+         * Returns the {@link Type} of the binding.
+         *
+         * @return the type
+         */
+        public Type getType() {
+            return type;
+        }
+
+        /**
+         * Prepares the given raw keyword according to the like type.
+         */
+        @Nullable
+        @Override public Object prepare(@Nullable Object value) {
+            if (value == null)
+                return null;
+
+            switch (type) {
+                case STARTING_WITH:
+                    return String.format("%s%%", value.toString());
+                case ENDING_WITH:
+                    return String.format("%%%s", value.toString());
+                case CONTAINING:
+                    return String.format("%%%s%%", value.toString());
+                case LIKE:
+                default:
+                    return value;
+            }
+        }
+
+        /** */
+        @Override public boolean equals(Object obj) {
+            if (!(obj instanceof LikeParameterBinding))
+                return false;
+
+            LikeParameterBinding that = (LikeParameterBinding)obj;
+
+            return super.equals(obj) && type.equals(that.type);
+        }
+
+        /** */
+        @Override public int hashCode() {
+
+            int result = super.hashCode();
+
+            result += nullSafeHashCode(type);
+
+            return result;
+        }
+
+        /** */
+        @Override public String toString() {
+            return String.format("LikeBinding [name: %s, position: %d, type: %s]", getName(), getPosition(), type);
+        }
+
+        /**
+         * Extracts the like {@link Type} from the given JPA like expression.
+         *
+         * @param expression must not be {@literal null} or empty.
+         */
+        private static Type getLikeTypeFrom(String expression) {
+
+            Assert.hasText(expression, "Expression must not be null or empty!");
+
+            if (expression.matches("%.*%"))
+                return Type.CONTAINING;
+
+            if (expression.startsWith("%"))
+                return Type.ENDING_WITH;
+
+            if (expression.endsWith("%"))
+                return Type.STARTING_WITH;
+
+            return Type.LIKE;
+        }
+
+    }
+
+    /** */
+    static class Metadata {
+        /**
+         * Uses jdbc style parameters.
+         */
+        private boolean usesJdbcStyleParameters;
+    }
+
+    /**
+     * Value object to analyze a {@link String} to determine the parts of the {@link String} that are quoted and offers
+     * an API to query that information.
+     *
+     * @author Jens Schauder
+     * @author Oliver Gierke
+     */
+    static class QuotationMap {
+        /** */
+        private static final Collection<Character> QUOTING_CHARACTERS = Arrays.asList('"', '\'');
+
+        /** */
+        private final List<Range<Integer>> quotedRanges = new ArrayList<>();
+
+        /**
+         * Creates a new instance for the query.
+         *
+         * @param query can be {@literal null}.
+         */
+        public QuotationMap(@Nullable String query) {
+            if (query == null)
+                return;
+
+            Character inQuotation = null;
+            int start = 0;
+
+            for (int i = 0; i < query.length(); i++) {
+                char currentChar = query.charAt(i);
+
+                if (QUOTING_CHARACTERS.contains(currentChar)) {
+                    if (inQuotation == null) {
+
+                        inQuotation = currentChar;
+                        start = i;
+                    }
+                    else if (currentChar == inQuotation) {
+                        inQuotation = null;
+
+                        quotedRanges.add(Range.from(Bound.inclusive(start)).to(Bound.inclusive(i)));
+                    }
+                }
+            }
+
+            if (inQuotation != null) {
+                throw new IllegalArgumentException(
+                    String.format("The string <%s> starts a quoted range at %d, but never ends it.", query, start));
+            }
+        }
+
+        /**
+         * Checks if a given index is within a quoted range.
+         *
+         * @param idx to check if it is part of a quoted range.
+         * @return whether the query contains a quoted range at {@literal index}.
+         */
+        public boolean isQuoted(int idx) {
+            return quotedRanges.stream().anyMatch(r -> r.contains(idx));
+        }
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/package-info.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/package-info.java
new file mode 100644
index 0000000..c9f90dc
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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 description. -->
+ * Package includes classes that integrates with Apache Ignite SQL engine.
+ */
+package org.apache.ignite.springdata20.repository.query;
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/spel/SpelEvaluator.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/spel/SpelEvaluator.java
new file mode 100644
index 0000000..1c30673
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/spel/SpelEvaluator.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2018-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ignite.springdata20.repository.query.spel;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.ignite.springdata20.repository.query.spel.SpelQueryContext.SpelExtractor;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.data.repository.query.EvaluationContextProvider;
+import org.springframework.data.repository.query.Parameters;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.util.Assert;
+
+/**
+ * Evaluates SpEL expressions as extracted by the {@link SpelExtractor} based on parameter information from a method and
+ * parameter values from a method call.
+ *
+ * @author Jens Schauder
+ * @author Gerrit Meier
+ * @author Oliver Gierke
+ */
+public class SpelEvaluator {
+    /** */
+    private static final SpelExpressionParser PARSER = new SpelExpressionParser();
+
+    /** */
+    private final EvaluationContextProvider evaluationCtxProvider;
+
+    /** */
+    private final Parameters<?, ?> parameters;
+
+    /** */
+    private final SpelExtractor extractor;
+
+    /**
+     * @param evaluationCtxProvider Evaluation context provider.
+     * @param parameters            Parameters.
+     * @param extractor             Extractor.
+     */
+    public SpelEvaluator(EvaluationContextProvider evaluationCtxProvider,
+        Parameters<?, ?> parameters,
+        SpelExtractor extractor) {
+        this.evaluationCtxProvider = evaluationCtxProvider;
+        this.parameters = parameters;
+        this.extractor = extractor;
+    }
+
+    /**
+     * Evaluate all the SpEL expressions in {@link #parameters} based on values provided as an argument.
+     *
+     * @param values Parameter values. Must not be {@literal null}.
+     * @return a map from parameter name to evaluated value. Guaranteed to be not {@literal null}.
+     */
+    public Map<String, Object> evaluate(Object[] values) {
+        Assert.notNull(values, "Values must not be null.");
+
+        EvaluationContext evaluationCtx = evaluationCtxProvider.getEvaluationContext(parameters, values);
+
+        return extractor.getParameters().collect(Collectors.toMap(//
+            Map.Entry::getKey, //
+            it -> getSpElValue(evaluationCtx, it.getValue()) //
+        ));
+    }
+
+    /**
+     * Returns the query string produced by the intermediate SpEL expression collection step.
+     *
+     * @return the query string produced by the intermediate SpEL expression collection step
+     */
+    public String getQueryString() {
+        return extractor.getQueryString();
+    }
+
+    /**
+     * @param evaluationCtx Evaluation context.
+     * @param expression    Expression.
+     */
+    @Nullable
+    private static Object getSpElValue(EvaluationContext evaluationCtx, String expression) {
+        return PARSER.parseExpression(expression).getValue(evaluationCtx);
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/spel/SpelQueryContext.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/spel/SpelQueryContext.java
new file mode 100644
index 0000000..40de67a
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/query/spel/SpelQueryContext.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright 2018-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.ignite.springdata20.repository.query.spel;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.function.BiFunction;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.data.domain.Range;
+import org.springframework.data.domain.Range.Bound;
+import org.springframework.data.repository.query.EvaluationContextProvider;
+import org.springframework.data.repository.query.Parameters;
+import org.springframework.util.Assert;
+
+/**
+ * Source of {@link SpelExtractor} encapsulating configuration often common for all queries.
+ *
+ * @author Jens Schauder
+ * @author Gerrit Meier
+ */
+public class SpelQueryContext {
+    /** */
+    private static final String SPEL_PATTERN_STRING = "([:?])#\\{([^}]+)}";
+
+    /** */
+    private static final Pattern SPEL_PATTERN = Pattern.compile(SPEL_PATTERN_STRING);
+
+    /**
+     * A function from the index of a SpEL expression in a query and the actual SpEL expression to the parameter name to
+     * be used in place of the SpEL expression. A typical implementation is expected to look like
+     * <code>(index, spel) -> "__some_placeholder_" + index</code>
+     */
+    private final BiFunction<Integer, String, String> paramNameSrc;
+
+    /**
+     * A function from a prefix used to demarcate a SpEL expression in a query and a parameter name as returned from
+     * {@link #paramNameSrc} to a {@literal String} to be used as a replacement of the SpEL in the query. The returned
+     * value should normally be interpretable as a bind parameter by the underlying persistence mechanism. A typical
+     * implementation is expected to look like <code>(prefix, name) -> prefix + name</code> or
+     * <code>(prefix, name) -> "{" + name + "}"</code>
+     */
+    private final BiFunction<String, String, String> replacementSrc;
+
+    /** */
+    private SpelQueryContext(BiFunction<Integer, String, String> paramNameSrc,
+        BiFunction<String, String, String> replacementSrc) {
+        this.paramNameSrc = paramNameSrc;
+        this.replacementSrc = replacementSrc;
+    }
+
+    /**
+     * Of spel query context.
+     *
+     * @param parameterNameSource the parameter name source
+     * @param replacementSource   the replacement source
+     * @return the spel query context
+     */
+    public static SpelQueryContext of(BiFunction<Integer, String, String> parameterNameSource,
+        BiFunction<String, String, String> replacementSource) {
+        return new SpelQueryContext(parameterNameSource, replacementSource);
+    }
+
+    /**
+     * Parses the query for SpEL expressions using the pattern:
+     *
+     * <pre>
+     * &lt;prefix&gt;#{&lt;spel&gt;}
+     * </pre>
+     * <p>
+     * with prefix being the character ':' or '?'. Parsing honors quoted {@literal String}s enclosed in single or double
+     * quotation marks.
+     *
+     * @param qry a query containing SpEL expressions in the format described above. Must not be {@literal null}.
+     * @return A {@link SpelExtractor} which makes the query with SpEL expressions replaced by bind parameters and a map
+     * from bind parameter to SpEL expression available. Guaranteed to be not {@literal null}.
+     */
+    public SpelExtractor parse(String qry) {
+        return new SpelExtractor(qry);
+    }
+
+    /**
+     * Createsa {@link EvaluatingSpelQueryContext} from the current one and the given {@link
+     * EvaluationContextProvider}*.
+     *
+     * @param provider must not be {@literal null}.
+     * @return Evaluating Spel QueryContext
+     */
+    public EvaluatingSpelQueryContext withEvaluationContextProvider(EvaluationContextProvider provider) {
+        Assert.notNull(provider, "EvaluationContextProvider must not be null!");
+
+        return new EvaluatingSpelQueryContext(provider, paramNameSrc, replacementSrc);
+    }
+
+    /**
+     * An extension of {@link SpelQueryContext} that can create {@link SpelEvaluator} instances as it also knows about a
+     * {@link EvaluationContextProvider}.
+     *
+     * @author Oliver Gierke
+     */
+    public static class EvaluatingSpelQueryContext extends SpelQueryContext {
+        /** */
+        private final EvaluationContextProvider evaluationContextProvider;
+
+        /**
+         * Creates a new {@link EvaluatingSpelQueryContext} for the given {@link EvaluationContextProvider}, parameter
+         * name source and replacement source.
+         *
+         * @param evaluationCtxProvider must not be {@literal null}.
+         * @param paramNameSrc          must not be {@literal null}.
+         * @param replacementSrc        must not be {@literal null}.
+         */
+        private EvaluatingSpelQueryContext(EvaluationContextProvider evaluationCtxProvider,
+            BiFunction<Integer, String, String> paramNameSrc, BiFunction<String, String, String> replacementSrc) {
+            super(paramNameSrc, replacementSrc);
+
+            evaluationContextProvider = evaluationCtxProvider;
+        }
+
+        /**
+         * Parses the query for SpEL expressions using the pattern:
+         *
+         * <pre>
+         * &lt;prefix&gt;#{&lt;spel&gt;}
+         * </pre>
+         * <p>
+         * with prefix being the character ':' or '?'. Parsing honors quoted {@literal String}s enclosed in single or
+         * double quotation marks.
+         *
+         * @param qry        a query containing SpEL expressions in the format described above. Must not be {@literal
+         *                   null}.
+         * @param parameters a {@link Parameters} instance describing query method parameters
+         * @return A {@link SpelEvaluator} which allows to evaluate the SpEL expressions. Will never be {@literal null}.
+         */
+        public SpelEvaluator parse(String qry, Parameters<?, ?> parameters) {
+            return new SpelEvaluator(evaluationContextProvider, parameters, parse(qry));
+        }
+    }
+
+    /**
+     * Parses a query string, identifies the contained SpEL expressions, replaces them with bind parameters and offers a
+     * {@link Map} from those bind parameters to the spel expression.
+     * <p>
+     * The parser detects quoted parts of the query string and does not detect SpEL expressions inside such quoted parts
+     * of the query.
+     *
+     * @author Jens Schauder
+     * @author Oliver Gierke
+     */
+    public class SpelExtractor {
+        /** */
+        private static final int PREFIX_GROUP_INDEX = 1;
+
+        /** */
+        private static final int EXPRESSION_GROUP_INDEX = 2;
+
+        /** */
+        private final String query;
+
+        /** */
+        private final Map<String, String> expressions;
+
+        /** */
+        private final QuotationMap quotations;
+
+        /**
+         * Creates a SpelExtractor from a query String.
+         *
+         * @param qry must not be {@literal null}.
+         */
+        SpelExtractor(String qry) {
+            Assert.notNull(qry, "Query must not be null");
+
+            Map<String, String> exps = new HashMap<>();
+            Matcher matcher = SPEL_PATTERN.matcher(qry);
+            StringBuilder resultQry = new StringBuilder();
+            QuotationMap quotedAreas = new QuotationMap(qry);
+
+            int expressionCounter = 0;
+            int matchedUntil = 0;
+
+            while (matcher.find()) {
+                if (quotedAreas.isQuoted(matcher.start()))
+                    resultQry.append(qry, matchedUntil, matcher.end());
+
+                else {
+                    String spelExpression = matcher.group(EXPRESSION_GROUP_INDEX);
+                    String prefix = matcher.group(PREFIX_GROUP_INDEX);
+
+                    String paramName = paramNameSrc.apply(expressionCounter, spelExpression);
+                    String replacement = replacementSrc.apply(prefix, paramName);
+
+                    resultQry.append(qry, matchedUntil, matcher.start());
+                    resultQry.append(replacement);
+
+                    exps.put(paramName, spelExpression);
+                    expressionCounter++;
+                }
+
+                matchedUntil = matcher.end();
+            }
+
+            resultQry.append(qry.substring(matchedUntil));
+
+            expressions = Collections.unmodifiableMap(exps);
+            query = resultQry.toString();
+            quotations = quotedAreas;
+        }
+
+        /**
+         * The query with all the SpEL expressions replaced with bind parameters.
+         *
+         * @return Guaranteed to be not {@literal null}.
+         */
+        public String getQueryString() {
+            return query;
+        }
+
+        /**
+         * Is quoted.
+         *
+         * @param idx the idx
+         * @return the boolean
+         */
+        public boolean isQuoted(int idx) {
+            return quotations.isQuoted(idx);
+        }
+
+        /**
+         * Gets parameter.
+         *
+         * @param name the name
+         * @return the parameter
+         */
+        public String getParameter(String name) {
+            return expressions.get(name);
+        }
+
+        /**
+         * A {@literal Map} from parameter name to SpEL expression.
+         *
+         * @return Guaranteed to be not {@literal null}.
+         */
+        Map<String, String> getParameterMap() {
+            return expressions;
+        }
+
+        /**
+         * Gets parameters.
+         *
+         * @return the parameters
+         */
+        Stream<Entry<String, String>> getParameters() {
+            return expressions.entrySet().stream();
+        }
+    }
+
+    /**
+     * Value object to analyze a {@link String} to determine the parts of the {@link String} that are quoted and offers
+     * an API to query that information.
+     *
+     * @author Jens Schauder
+     * @author Oliver Gierke
+     */
+    static class QuotationMap {
+        /** */
+        private static final Collection<Character> QUOTING_CHARACTERS = Arrays.asList('"', '\'');
+
+        /** */
+        private final List<Range<Integer>> quotedRanges = new ArrayList<>();
+
+        /**
+         * Creates a new {@link QuotationMap} for the query.
+         *
+         * @param qry can be {@literal null}.
+         */
+        public QuotationMap(@Nullable String qry) {
+            if (qry == null)
+                return;
+
+            Character inQuotation = null;
+            int start = 0;
+
+            for (int i = 0; i < qry.length(); i++) {
+
+                char curChar = qry.charAt(i);
+
+                if (QUOTING_CHARACTERS.contains(curChar)) {
+
+                    if (inQuotation == null) {
+
+                        inQuotation = curChar;
+                        start = i;
+
+                    }
+                    else if (curChar == inQuotation) {
+
+                        inQuotation = null;
+
+                        quotedRanges.add(Range.from(Bound.inclusive(start)).to(Bound.inclusive(i)));
+                    }
+                }
+            }
+
+            if (inQuotation != null) {
+                throw new IllegalArgumentException(
+                    String.format("The string <%s> starts a quoted range at %d, but never ends it.", qry, start));
+            }
+        }
+
+        /**
+         * Checks if a given index is within a quoted range.
+         *
+         * @param idx to check if it is part of a quoted range.
+         * @return whether the query contains a quoted range at {@literal index}.
+         */
+        public boolean isQuoted(int idx) {
+            return quotedRanges.stream().anyMatch(r -> r.contains(idx));
+        }
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/ConditionFalse.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/ConditionFalse.java
new file mode 100644
index 0000000..1b4b378
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/ConditionFalse.java
@@ -0,0 +1,32 @@
+/*
+ * 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.springdata20.repository.support;
+
+import org.springframework.context.annotation.Condition;
+import org.springframework.context.annotation.ConditionContext;
+import org.springframework.core.type.AnnotatedTypeMetadata;
+
+/**
+ * Always false condition. Tells spring context never load bean with such Condition.
+ */
+public class ConditionFalse implements Condition {
+    /** {@inheritDoc} */
+    @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
+        return false;
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/IgniteRepositoryFactory.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/IgniteRepositoryFactory.java
new file mode 100644
index 0000000..87f637d
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/IgniteRepositoryFactory.java
@@ -0,0 +1,274 @@
+/*
+ * 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.springdata20.repository.support;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.apache.ignite.springdata20.repository.config.DynamicQueryConfig;
+import org.apache.ignite.springdata20.repository.config.Query;
+import org.apache.ignite.springdata20.repository.config.RepositoryConfig;
+import org.apache.ignite.springdata20.repository.query.IgniteQuery;
+import org.apache.ignite.springdata20.repository.query.IgniteQueryGenerator;
+import org.apache.ignite.springdata20.repository.query.IgniteRepositoryQuery;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanExpressionContext;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.expression.StandardBeanExpressionResolver;
+import org.springframework.data.repository.core.EntityInformation;
+import org.springframework.data.repository.core.RepositoryInformation;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.core.support.AbstractEntityInformation;
+import org.springframework.data.repository.core.support.RepositoryFactorySupport;
+import org.springframework.data.repository.query.EvaluationContextProvider;
+import org.springframework.data.repository.query.QueryLookupStrategy;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Crucial for spring-data functionality class. Create proxies for repositories.
+ * <p>
+ * Supports multiple Ignite Instances on same JVM.
+ * <p>
+ * This is pretty useful working with Spring repositories bound to different Ignite intances within same application.
+ *
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+public class IgniteRepositoryFactory extends RepositoryFactorySupport {
+    /** Spring application context */
+    private final ApplicationContext ctx;
+
+    /** Spring application bean factory */
+    private final DefaultListableBeanFactory beanFactory;
+
+    /** Spring application expression resolver */
+    private final StandardBeanExpressionResolver resolver = new StandardBeanExpressionResolver();
+
+    /** Spring application bean expression context */
+    private final BeanExpressionContext beanExpressionContext;
+
+    /** Mapping of a repository to a cache. */
+    private final Map<Class<?>, String> repoToCache = new HashMap<>();
+
+    /** Mapping of a repository to a ignite instance. */
+    private final Map<Class<?>, Ignite> repoToIgnite = new HashMap<>();
+
+    /**
+     * Creates the factory with initialized {@link Ignite} instance.
+     *
+     * @param ctx the ctx
+     */
+    public IgniteRepositoryFactory(ApplicationContext ctx) {
+        this.ctx = ctx;
+
+        beanFactory = new DefaultListableBeanFactory(ctx.getAutowireCapableBeanFactory());
+
+        beanExpressionContext = new BeanExpressionContext(beanFactory, null);
+    }
+
+    /** */
+    private Ignite igniteForRepoConfig(RepositoryConfig config) {
+        try {
+            String igniteInstanceName = evaluateExpression(config.igniteInstance());
+            return (Ignite)ctx.getBean(igniteInstanceName);
+        }
+        catch (BeansException ex) {
+            try {
+                String igniteConfigName = evaluateExpression(config.igniteCfg());
+                IgniteConfiguration cfg = (IgniteConfiguration)ctx.getBean(igniteConfigName);
+                try {
+                    // first try to attach to existing ignite instance
+                    return Ignition.ignite(cfg.getIgniteInstanceName());
+                }
+                catch (Exception ignored) {
+                    // nop
+                }
+                return Ignition.start(cfg);
+            }
+            catch (BeansException ex2) {
+                try {
+                    String igniteSpringCfgPath = evaluateExpression(config.igniteSpringCfgPath());
+                    String path = (String)ctx.getBean(igniteSpringCfgPath);
+                    return Ignition.start(path);
+                }
+                catch (BeansException ex3) {
+                    throw new IgniteException("Failed to initialize Ignite repository factory. Ignite instance or"
+                        + " IgniteConfiguration or a path to Ignite's spring XML "
+                        + "configuration must be defined in the"
+                        + " application configuration");
+                }
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public <T, ID> EntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
+        return new AbstractEntityInformation<T, ID>(domainClass) {
+            /** {@inheritDoc} */
+            @Override public ID getId(T entity) {
+                return null;
+            }
+
+            /** {@inheritDoc} */
+            @Override public Class<ID> getIdType() {
+                return null;
+            }
+        };
+    }
+
+    /** {@inheritDoc} */
+    @Override protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
+        return IgniteRepositoryImpl.class;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected synchronized RepositoryMetadata getRepositoryMetadata(Class<?> repoItf) {
+        Assert.notNull(repoItf, "Repository interface must be set.");
+        Assert.isAssignable(IgniteRepository.class, repoItf, "Repository must implement IgniteRepository interface.");
+
+        RepositoryConfig annotation = repoItf.getAnnotation(RepositoryConfig.class);
+
+        Assert.notNull(annotation, "Set a name of an Apache Ignite cache using @RepositoryConfig annotation to map "
+            + "this repository to the underlying cache.");
+
+        Assert.hasText(annotation.cacheName(), "Set a name of an Apache Ignite cache using @RepositoryConfig "
+            + "annotation to map this repository to the underlying cache.");
+
+        String cacheName = evaluateExpression(annotation.cacheName());
+
+        repoToCache.put(repoItf, cacheName);
+
+        repoToIgnite.put(repoItf, igniteForRepoConfig(annotation));
+
+        return super.getRepositoryMetadata(repoItf);
+    }
+
+    /**
+     * Evaluate the SpEL expression
+     *
+     * @param spelExpression SpEL expression
+     * @return the result of execution of the SpEL expression
+     */
+    private String evaluateExpression(String spelExpression) {
+        return (String)resolver.evaluate(spelExpression, beanExpressionContext);
+    }
+
+    /** Control underlying cache creation to avoid cache creation by mistake */
+    private IgniteCache getRepositoryCache(Class<?> repoIf) {
+        Ignite ignite = repoToIgnite.get(repoIf);
+
+        RepositoryConfig config = repoIf.getAnnotation(RepositoryConfig.class);
+
+        String cacheName = repoToCache.get(repoIf);
+
+        IgniteCache c = config.autoCreateCache() ? ignite.getOrCreateCache(cacheName) : ignite.cache(cacheName);
+
+        if (c == null) {
+            throw new IllegalStateException(
+                "Cache '" + cacheName + "' not found for repository interface " + repoIf.getName()
+                    + ". Please, add a cache configuration to ignite configuration"
+                    + " or pass autoCreateCache=true to org.apache.ignite.springdata20"
+                    + ".repository.config.RepositoryConfig annotation.");
+        }
+
+        return c;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected Object getTargetRepository(RepositoryInformation metadata) {
+        Ignite ignite = repoToIgnite.get(metadata.getRepositoryInterface());
+
+        return getTargetRepositoryViaReflection(metadata, ignite,
+            getRepositoryCache(metadata.getRepositoryInterface()));
+    }
+
+    /** {@inheritDoc} */
+    @Override protected Optional<QueryLookupStrategy> getQueryLookupStrategy(final QueryLookupStrategy.Key key,
+        EvaluationContextProvider evaluationContextProvider) {
+        return Optional.of((mtd, metadata, factory, namedQueries) -> {
+            final Query annotation = mtd.getAnnotation(Query.class);
+            final Ignite ignite = repoToIgnite.get(metadata.getRepositoryInterface());
+
+            if (annotation != null && (StringUtils.hasText(annotation.value()) || annotation.textQuery() || annotation
+                .dynamicQuery())) {
+
+                String qryStr = annotation.value();
+
+                boolean annotatedIgniteQuery = !annotation.dynamicQuery() && (StringUtils.hasText(qryStr) || annotation
+                    .textQuery());
+
+                IgniteQuery query = annotatedIgniteQuery ? new IgniteQuery(qryStr,
+                    !annotation.textQuery() && (isFieldQuery(qryStr) || annotation.forceFieldsQuery()),
+                    annotation.textQuery(), false, IgniteQueryGenerator.getOptions(mtd)) : null;
+
+                if (key != QueryLookupStrategy.Key.CREATE) {
+                    return new IgniteRepositoryQuery(ignite, metadata, query, mtd, factory,
+                        getRepositoryCache(metadata.getRepositoryInterface()),
+                        annotatedIgniteQuery ? DynamicQueryConfig.fromQueryAnnotation(annotation) : null,
+                        evaluationContextProvider);
+                }
+            }
+
+            if (key == QueryLookupStrategy.Key.USE_DECLARED_QUERY) {
+                throw new IllegalStateException("To use QueryLookupStrategy.Key.USE_DECLARED_QUERY, pass "
+                    + "a query string via org.apache.ignite.springdata20.repository"
+                    + ".config.Query annotation.");
+            }
+
+            return new IgniteRepositoryQuery(ignite, metadata, IgniteQueryGenerator.generateSql(mtd, metadata), mtd,
+                factory, getRepositoryCache(metadata.getRepositoryInterface()),
+                DynamicQueryConfig.fromQueryAnnotation(annotation), evaluationContextProvider);
+        });
+    }
+
+    /**
+     * @param qry Query string.
+     * @return {@code true} if query is SqlFieldsQuery.
+     */
+    public static boolean isFieldQuery(String qry) {
+        String qryUpperCase = qry.toUpperCase();
+
+        return isStatement(qryUpperCase) && !qryUpperCase.matches("^SELECT\\s+(?:\\w+\\.)?+\\*.*");
+    }
+
+    /**
+     * Evaluates if the query starts with a clause.<br>
+     * <code>SELECT, INSERT, UPDATE, MERGE, DELETE</code>
+     *
+     * @param qryUpperCase Query string in upper case.
+     * @return {@code true} if query is full SQL statement.
+     */
+    private static boolean isStatement(String qryUpperCase) {
+        return qryUpperCase.matches("^\\s*SELECT\\b.*") ||
+            // update
+            qryUpperCase.matches("^\\s*UPDATE\\b.*") ||
+            // delete
+            qryUpperCase.matches("^\\s*DELETE\\b.*") ||
+            // merge
+            qryUpperCase.matches("^\\s*MERGE\\b.*") ||
+            // insert
+            qryUpperCase.matches("^\\s*INSERT\\b.*");
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/IgniteRepositoryFactoryBean.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/IgniteRepositoryFactoryBean.java
new file mode 100644
index 0000000..5b3d612
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/IgniteRepositoryFactoryBean.java
@@ -0,0 +1,67 @@
+/*
+ * 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.springdata20.repository.support;
+
+import java.io.Serializable;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.data.repository.Repository;
+import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
+import org.springframework.data.repository.core.support.RepositoryFactorySupport;
+
+/**
+ * Apache Ignite repository factory bean.
+ * <p>
+ * The {@link org.apache.ignite.springdata20.repository.config.RepositoryConfig} requires to define one of the
+ * parameters below in your Spring application configuration in order to get an access to Apache Ignite cluster:
+ * <ul>
+ * <li>{@link Ignite} instance bean named "igniteInstance" by default</li>
+ * <li>{@link IgniteConfiguration} bean named "igniteCfg" by default</li>
+ * <li>A path to Ignite's Spring XML configuration named "igniteSpringCfgPath" by default</li>
+ * <ul/>
+ *
+ * @param <T> Repository type, {@link IgniteRepository}
+ * @param <V> Domain object class.
+ * @param <K> Domain object key, super expects {@link Serializable}.
+ */
+public class IgniteRepositoryFactoryBean<T extends Repository<V, K>, V, K extends Serializable>
+    extends RepositoryFactoryBeanSupport<T, V, K> implements ApplicationContextAware {
+    /** */
+    private ApplicationContext ctx;
+
+    /**
+     * @param repoInterface Repository interface.
+     */
+    protected IgniteRepositoryFactoryBean(Class<? extends T> repoInterface) {
+        super(repoInterface);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void setApplicationContext(ApplicationContext ctx) throws BeansException {
+        this.ctx = ctx;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected RepositoryFactorySupport createRepositoryFactory() {
+        return new IgniteRepositoryFactory(ctx);
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/IgniteRepositoryImpl.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/IgniteRepositoryImpl.java
new file mode 100644
index 0000000..50a7d4c
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/IgniteRepositoryImpl.java
@@ -0,0 +1,221 @@
+/*
+ * 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.springdata20.repository.support;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+import javax.cache.Cache;
+import javax.cache.expiry.ExpiryPolicy;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.CachePeekMode;
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.context.annotation.Conditional;
+
+/**
+ * General Apache Ignite repository implementation. This bean should've never been loaded by context directly, only via
+ * {@link IgniteRepositoryFactory}
+ *
+ * @param <V> the cache value type
+ * @param <K> the cache key type
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+@Conditional(ConditionFalse.class)
+public class IgniteRepositoryImpl<V, K extends Serializable> implements IgniteRepository<V, K> {
+    /**
+     * Ignite Cache bound to the repository
+     */
+    private final IgniteCache<K, V> cache;
+
+    /**
+     * Ignite instance bound to the repository
+     */
+    private final Ignite ignite;
+
+    /**
+     * Repository constructor.
+     *
+     * @param ignite the ignite
+     * @param cache  Initialized cache instance.
+     */
+    public IgniteRepositoryImpl(Ignite ignite, IgniteCache<K, V> cache) {
+        this.cache = cache;
+        this.ignite = ignite;
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteCache<K, V> cache() {
+        return cache;
+    }
+
+    /** {@inheritDoc} */
+    @Override public Ignite ignite() {
+        return ignite;
+    }
+
+    /** {@inheritDoc} */
+    @Override public <S extends V> S save(K key, S entity) {
+        cache.put(key, entity);
+
+        return entity;
+    }
+
+    /** {@inheritDoc} */
+    @Override public <S extends V> Iterable<S> save(Map<K, S> entities) {
+        cache.putAll(entities);
+
+        return entities.values();
+    }
+
+    /** {@inheritDoc} */
+    @Override public <S extends V> S save(K key, S entity, @Nullable ExpiryPolicy expiryPlc) {
+        if (expiryPlc != null)
+            cache.withExpiryPolicy(expiryPlc).put(key, entity);
+        else
+            cache.put(key, entity);
+        return entity;
+    }
+
+    /** {@inheritDoc} */
+    @Override public <S extends V> Iterable<S> save(Map<K, S> entities, @Nullable ExpiryPolicy expiryPlc) {
+        if (expiryPlc != null)
+            cache.withExpiryPolicy(expiryPlc).putAll(entities);
+        else
+            cache.putAll(entities);
+        return entities.values();
+    }
+
+    /**
+     * Not implemented.
+     */
+    @Override public <S extends V> S save(S entity) {
+        throw new UnsupportedOperationException("Use IgniteRepository.save(key,value) method instead.");
+    }
+
+    /**
+     * Not implemented.
+     */
+    @Override public <S extends V> Iterable<S> saveAll(Iterable<S> entities) {
+        throw new UnsupportedOperationException("Use IgniteRepository.save(Map<keys,value>) method instead.");
+    }
+
+    /** {@inheritDoc} */
+    @Override public Optional<V> findById(K id) {
+        return Optional.ofNullable(cache.get(id));
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean existsById(K id) {
+        return cache.containsKey(id);
+    }
+
+    /** {@inheritDoc} */
+    @Override public Iterable<V> findAll() {
+        final Iterator<Cache.Entry<K, V>> iter = cache.iterator();
+
+        return new Iterable<V>() {
+            /** */
+            @Override public Iterator<V> iterator() {
+                return new Iterator<V>() {
+                    /** {@inheritDoc} */
+                    @Override public boolean hasNext() {
+                        return iter.hasNext();
+                    }
+
+                    /** {@inheritDoc} */
+                    @Override public V next() {
+                        return iter.next().getValue();
+                    }
+
+                    /** {@inheritDoc} */
+                    @Override public void remove() {
+                        iter.remove();
+                    }
+                };
+            }
+        };
+    }
+
+    /** {@inheritDoc} */
+    @Override public Iterable<V> findAllById(Iterable<K> ids) {
+        if (ids instanceof Set)
+            return cache.getAll((Set<K>)ids).values();
+
+        if (ids instanceof Collection)
+            return cache.getAll(new HashSet<>((Collection<K>)ids)).values();
+
+        TreeSet<K> keys = new TreeSet<>();
+
+        for (K id : ids)
+            keys.add(id);
+
+        return cache.getAll(keys).values();
+    }
+
+    /** {@inheritDoc} */
+    @Override public long count() {
+        return cache.size(CachePeekMode.PRIMARY);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void deleteById(K id) {
+        cache.remove(id);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void delete(V entity) {
+        throw new UnsupportedOperationException("Use IgniteRepository.deleteById(key) method instead.");
+    }
+
+    /** {@inheritDoc} */
+    @Override public void deleteAll(Iterable<? extends V> entities) {
+        throw new UnsupportedOperationException("Use IgniteRepository.deleteAllById(keys) method instead.");
+    }
+
+    /** {@inheritDoc} */
+    @Override public void deleteAllById(Iterable<K> ids) {
+        if (ids instanceof Set) {
+            cache.removeAll((Set<K>)ids);
+            return;
+        }
+
+        if (ids instanceof Collection) {
+            cache.removeAll(new HashSet<>((Collection<K>)ids));
+            return;
+        }
+
+        TreeSet<K> keys = new TreeSet<>();
+
+        for (K id : ids)
+            keys.add(id);
+
+        cache.removeAll(keys);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void deleteAll() {
+        cache.clear();
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/package-info.java b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/package-info.java
new file mode 100644
index 0000000..a9d4fd2
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/main/java/org/apache/ignite/springdata20/repository/support/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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 description. -->
+ * Package contains supporting files required by Spring Data framework.
+ */
+package org.apache.ignite.springdata20.repository.support;
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCompoundKeyTest.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCompoundKeyTest.java
new file mode 100644
index 0000000..6a69f32
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCompoundKeyTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.springdata;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.Statement;
+import java.util.Optional;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.springdata.compoundkey.City;
+import org.apache.ignite.springdata.compoundkey.CityKey;
+import org.apache.ignite.springdata.compoundkey.CityRepository;
+import org.apache.ignite.springdata.compoundkey.CompoundKeyApplicationConfiguration;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+
+import static org.apache.ignite.springdata.compoundkey.CompoundKeyApplicationConfiguration.CLI_CONN_PORT;
+
+/**
+ * Test with using conpoud key in spring-data
+ * */
+public class IgniteSpringDataCompoundKeyTest extends GridCommonAbstractTest {
+    /** Application context */
+    private static AnnotationConfigApplicationContext ctx;
+
+    /** City repository */
+    private static CityRepository repo;
+
+    /** Cache name */
+    private static final String CACHE_NAME = "City";
+
+    /** Cities count */
+    private static final int TOTAL_COUNT = 5;
+
+    /** Count Afganistan cities */
+    private static final int AFG_COUNT = 4;
+
+    /** Kabul identifier */
+    private static final int KABUL_ID = 1;
+
+    /** Quandahar identifier */
+    private static final int QUANDAHAR_ID = 2;
+
+    /** Afganistan county code */
+    private static final String AFG = "AFG";
+
+    /** test city Kabul */
+    private static final City KABUL = new City("Kabul", "Kabol", 1780000);
+
+    /** test city Quandahar */
+    private static final City QUANDAHAR = new City("Qandahar","Qandahar", 237500);
+
+    /**
+     * Performs context initialization before tests.
+     */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        ctx = new AnnotationConfigApplicationContext();
+        ctx.register(CompoundKeyApplicationConfiguration.class);
+        ctx.refresh();
+
+        repo = ctx.getBean(CityRepository.class);
+    }
+
+    /**
+     * Load data
+     * */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        loadData();
+
+        assertEquals(TOTAL_COUNT, repo.count());
+    }
+
+    /**
+     * Performs context destroy after tests.
+     */
+    @Override protected void afterTestsStopped() {
+        ctx.close();
+    }
+
+    /** load data*/
+    public void loadData() throws Exception {
+        Ignite ignite = ctx.getBean(Ignite.class);
+
+        if (ignite.cacheNames().contains(CACHE_NAME))
+            ignite.destroyCache(CACHE_NAME);
+
+        try (Connection conn = DriverManager.getConnection("jdbc:ignite:thin://127.0.0.1:" + CLI_CONN_PORT + '/')) {
+            Statement st = conn.createStatement();
+
+            st.execute("DROP TABLE IF EXISTS City");
+            st.execute("CREATE TABLE City (ID INT, Name VARCHAR, CountryCode CHAR(3), District VARCHAR, Population INT, PRIMARY KEY (ID, CountryCode)) WITH \"template=partitioned, backups=1, affinityKey=CountryCode, CACHE_NAME=City, KEY_TYPE=org.apache.ignite.springdata.compoundkey.CityKey, VALUE_TYPE=org.apache.ignite.springdata.compoundkey.City\"");
+            st.execute("SET STREAMING ON;");
+            st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (1,'Kabul','AFG','Kabol',1780000)");
+            st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (2,'Qandahar','AFG','Qandahar',237500)");
+            st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (3,'Herat','AFG','Herat',186800)");
+            st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (4,'Mazar-e-Sharif','AFG','Balkh',127800)");
+            st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (5,'Amsterdam','NLD','Noord-Holland',731200)");
+        }
+    }
+
+    /** Test */
+    @Test
+    public void test() {
+        assertEquals(Optional.of(KABUL), repo.findById(new CityKey(KABUL_ID, AFG)));
+        assertEquals(AFG_COUNT, repo.findByCountryCode(AFG).size());
+        assertEquals(QUANDAHAR, repo.findById(QUANDAHAR_ID));
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfExpressionTest.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfExpressionTest.java
new file mode 100644
index 0000000..5841f41
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfExpressionTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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.springdata;
+
+import java.util.Collection;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.springdata.misc.ApplicationConfiguration;
+import org.apache.ignite.springdata.misc.Person;
+import org.apache.ignite.springdata.misc.PersonExpressionRepository;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+
+/**
+ * Test with using repository which is configured by Spring EL
+ */
+public class IgniteSpringDataCrudSelfExpressionTest extends GridCommonAbstractTest {
+    /** Number of entries to store */
+    private static final int CACHE_SIZE = 1000;
+
+    /** Repository. */
+    private static PersonExpressionRepository repo;
+
+    /** Context. */
+    private static AnnotationConfigApplicationContext ctx;
+
+    /** */
+    @Rule
+    public final ExpectedException expected = ExpectedException.none();
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        ctx = new AnnotationConfigApplicationContext();
+        ctx.register(ApplicationConfiguration.class);
+        ctx.refresh();
+
+        repo = ctx.getBean(PersonExpressionRepository.class);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        fillInRepository();
+
+        assertEquals(CACHE_SIZE, repo.count());
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        repo.deleteAll();
+
+        assertEquals(0, repo.count());
+
+        super.afterTest();
+    }
+
+    /** */
+    private void fillInRepository() {
+        for (int i = 0; i < CACHE_SIZE - 5; i++) {
+            repo.save(i, new Person("person" + Integer.toHexString(i),
+                "lastName" + Integer.toHexString((i + 16) % 256)));
+        }
+
+        repo.save((int) repo.count(), new Person("uniquePerson", "uniqueLastName"));
+        repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+        repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+        repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+        repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTestsStopped() {
+        ctx.close();
+    }
+
+    /**
+     * Tests put & get operations.
+     */
+    @Test
+    public void testPutGet() {
+        Person person = new Person("some_name", "some_surname");
+
+        int id = CACHE_SIZE + 1;
+
+        assertEquals(person, repo.save(id, person));
+
+        assertTrue(repo.existsById(id));
+
+        assertEquals(person, repo.findById(id).get());
+
+        expected.expect(UnsupportedOperationException.class);
+        expected.expectMessage("Use IgniteRepository.save(key,value) method instead.");
+        repo.save(person);
+    }
+
+    /**
+     * Tests SpEL expression.
+     */
+    @Test
+    public void testCacheCount() {
+        Ignite ignite = ctx.getBean("igniteInstance", Ignite.class);
+
+        Collection<String> cacheNames = ignite.cacheNames();
+
+        assertFalse("The SpEL \"#{cacheNames.personCacheName}\" isn't processed!",
+            cacheNames.contains("#{cacheNames.personCacheName}"));
+
+        assertTrue("Cache \"PersonCache\" isn't found!",
+            cacheNames.contains("PersonCache"));
+    }
+
+    /** */
+    @Test
+    public void testCacheCountTWO() {
+        Ignite ignite = ctx.getBean("igniteInstanceTWO", Ignite.class);
+
+        Collection<String> cacheNames = ignite.cacheNames();
+
+        assertFalse("The SpEL \"#{cacheNames.personCacheName}\" isn't processed!",
+            cacheNames.contains("#{cacheNames.personCacheName}"));
+
+        assertTrue("Cache \"PersonCache\" isn't found!",
+            cacheNames.contains("PersonCache"));
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfTest.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfTest.java
new file mode 100644
index 0000000..38785ef
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfTest.java
@@ -0,0 +1,447 @@
+/*
+ * 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.springdata;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.TreeSet;
+import org.apache.ignite.springdata.misc.ApplicationConfiguration;
+import org.apache.ignite.springdata.misc.FullNameProjection;
+import org.apache.ignite.springdata.misc.Person;
+import org.apache.ignite.springdata.misc.PersonProjection;
+import org.apache.ignite.springdata.misc.PersonRepository;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+
+/**
+ * CRUD tests.
+ */
+public class IgniteSpringDataCrudSelfTest extends GridCommonAbstractTest {
+    /** Repository. */
+    private static PersonRepository repo;
+
+    /** Context. */
+    private static AnnotationConfigApplicationContext ctx;
+
+    /** Number of entries to store */
+    private static int CACHE_SIZE = 1000;
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        ctx = new AnnotationConfigApplicationContext();
+
+        ctx.register(ApplicationConfiguration.class);
+
+        ctx.refresh();
+
+        repo = ctx.getBean(PersonRepository.class);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        fillInRepository();
+
+        assertEquals(CACHE_SIZE, repo.count());
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        repo.deleteAll();
+
+        assertEquals(0, repo.count());
+
+        super.afterTest();
+    }
+
+    /** */
+    private void fillInRepository() {
+        for (int i = 0; i < CACHE_SIZE - 5; i++) {
+            repo.save(i, new Person("person" + Integer.toHexString(i),
+                "lastName" + Integer.toHexString((i + 16) % 256)));
+        }
+
+        repo.save((int) repo.count(), new Person("uniquePerson", "uniqueLastName"));
+        repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+        repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+        repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+        repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTestsStopped() throws Exception {
+        ctx.destroy();
+    }
+
+    /** */
+    @Test
+    public void testPutGet() {
+        Person person = new Person("some_name", "some_surname");
+
+        int id = CACHE_SIZE + 1;
+
+        assertEquals(person, repo.save(id, person));
+
+        assertTrue(repo.existsById(id));
+
+        assertEquals(person, repo.findById(id).get());
+
+        try {
+            repo.save(person);
+
+            fail("Managed to save a Person without ID");
+        }
+        catch (UnsupportedOperationException e) {
+            //excepted
+        }
+    }
+
+    /** */
+    @Test
+    public void testPutAllGetAll() {
+        LinkedHashMap<Integer, Person> map = new LinkedHashMap<>();
+
+        for (int i = CACHE_SIZE; i < CACHE_SIZE + 50; i++)
+            map.put(i, new Person("some_name" + i, "some_surname" + i));
+
+        Iterator<Person> persons = repo.save(map).iterator();
+
+        assertEquals(CACHE_SIZE + 50, repo.count());
+
+        Iterator<Person> origPersons = map.values().iterator();
+
+        while (persons.hasNext())
+            assertEquals(origPersons.next(), persons.next());
+
+        try {
+            repo.saveAll(map.values());
+
+            fail("Managed to save a list of Persons with ids");
+        }
+        catch (UnsupportedOperationException e) {
+            //expected
+        }
+
+        persons = repo.findAllById(map.keySet()).iterator();
+
+        int counter = 0;
+
+        while (persons.hasNext()) {
+            persons.next();
+            counter++;
+        }
+
+        assertEquals(map.size(), counter);
+    }
+
+    /** */
+    @Test
+    public void testGetAll() {
+        assertEquals(CACHE_SIZE, repo.count());
+
+        Iterator<Person> persons = repo.findAll().iterator();
+
+        int counter = 0;
+
+        while (persons.hasNext()) {
+            persons.next();
+            counter++;
+        }
+
+        assertEquals(repo.count(), counter);
+    }
+
+    /** */
+    @Test
+    public void testDelete() {
+        assertEquals(CACHE_SIZE, repo.count());
+
+        repo.deleteById(0);
+
+        assertEquals(CACHE_SIZE - 1, repo.count());
+        assertEquals(Optional.empty(),repo.findById(0));
+
+        try {
+            repo.delete(new Person("", ""));
+
+            fail("Managed to delete a Person without id");
+        }
+        catch (UnsupportedOperationException e) {
+            //expected
+        }
+    }
+
+    /** */
+    @Test
+    public void testDeleteSet() {
+        assertEquals(CACHE_SIZE, repo.count());
+
+        TreeSet<Integer> ids = new TreeSet<>();
+
+        for (int i = 0; i < CACHE_SIZE / 2; i++)
+            ids.add(i);
+
+        repo.deleteAllById(ids);
+
+        assertEquals(CACHE_SIZE / 2, repo.count());
+
+        try {
+            ArrayList<Person> persons = new ArrayList<>();
+
+            for (int i = 0; i < 3; i++)
+                persons.add(new Person(String.valueOf(i), String.valueOf(i)));
+
+            repo.deleteAll(persons);
+
+            fail("Managed to delete Persons without ids");
+        }
+        catch (UnsupportedOperationException e) {
+            //expected
+        }
+    }
+
+    /** */
+    @Test
+    public void testDeleteAll() {
+        assertEquals(CACHE_SIZE, repo.count());
+
+        repo.deleteAll();
+
+        assertEquals(0, repo.count());
+    }
+
+    /**
+     * Delete existing record.
+     */
+    @Test
+    public void testDeleteByFirstName() {
+        assertEquals(repo.countByFirstNameLike("uniquePerson"), 1);
+
+        long cnt = repo.deleteByFirstName("uniquePerson");
+
+        assertEquals(1, cnt);
+    }
+
+    /**
+     * Delete NON existing record.
+     */
+    @Test
+    public void testDeleteExpression() {
+        long cnt = repo.deleteByFirstName("880");
+
+        assertEquals(0, cnt);
+    }
+
+    /**
+     * Delete Multiple records due to where.
+     */
+    @Test
+    public void testDeleteExpressionMultiple() {
+        long count = repo.countByFirstName("nonUniquePerson");
+        long cnt = repo.deleteByFirstName("nonUniquePerson");
+
+        assertEquals(cnt, count);
+    }
+
+    /**
+     * Remove should do the same than Delete.
+     */
+    @Test
+    public void testRemoveExpression() {
+        repo.removeByFirstName("person3f");
+
+        long count = repo.count();
+        assertEquals(CACHE_SIZE - 1, count);
+    }
+
+    /**
+     * Delete unique record using lower case key word.
+     */
+    @Test
+    public void testDeleteQuery() {
+        repo.deleteBySecondNameLowerCase("uniqueLastName");
+
+        long countAfter = repo.count();
+        assertEquals(CACHE_SIZE - 1, countAfter);
+    }
+
+    /**
+     * Try to delete with a wrong @Query.
+     */
+    @Test
+    public void testWrongDeleteQuery() {
+        long countBefore = repo.countByFirstNameLike("person3f");
+
+        try {
+            repo.deleteWrongByFirstNameQuery("person3f");
+        }
+        catch (Exception e) {
+            //expected
+        }
+
+        long countAfter = repo.countByFirstNameLike("person3f");
+        assertEquals(countBefore, countAfter);
+    }
+
+    /**
+     * Update with a @Query a record.
+     */
+    @Test
+    public void testUpdateQueryMixedCase() {
+        final String newSecondName = "updatedUniqueSecondName";
+        int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+        assertEquals(1, cnt);
+
+        List<Person> person = repo.findByFirstName("uniquePerson");
+        assertEquals(person.get(0).getSecondName(), "updatedUniqueSecondName");
+    }
+
+    /**
+     * Update with a @Query a record
+     */
+    @Test
+    public void testUpdateQueryMixedCaseProjection() {
+        final String newSecondName = "updatedUniqueSecondName1";
+        int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+        assertEquals(1, cnt);
+
+        List<PersonProjection> person = repo.queryByFirstNameWithProjection("uniquePerson");
+        assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName1");
+    }
+
+    /** */
+    @Test
+    public void testUpdateQueryMixedCaseProjectionNamedParameter() {
+        final String newSecondName = "updatedUniqueSecondName2";
+        int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+        assertEquals(1, cnt);
+
+        List<PersonProjection> person = repo.queryByFirstNameWithProjectionNamedParameter("uniquePerson");
+        assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName2");
+    }
+
+    /** */
+    @Test
+    public void testUpdateQueryMixedCaseDynamicProjectionNamedParameter() {
+        final String newSecondName = "updatedUniqueSecondName2";
+        int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+        assertEquals(1, cnt);
+
+        List<PersonProjection> person = repo.queryByFirstNameWithProjectionNamedParameter(PersonProjection.class, "uniquePerson");
+        assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName2");
+
+        List<FullNameProjection> personFullName = repo.queryByFirstNameWithProjectionNamedParameter(FullNameProjection.class, "uniquePerson");
+        assertEquals(personFullName.get(0).getFullName(), "uniquePerson updatedUniqueSecondName2");
+    }
+
+    /** */
+    @Test
+    public void testUpdateQueryOneMixedCaseDynamicProjectionNamedParameter() {
+        final String newSecondName = "updatedUniqueSecondName2";
+        int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+        assertEquals(1, cnt);
+
+        PersonProjection person = repo.queryOneByFirstNameWithProjectionNamedParameter(PersonProjection.class, "uniquePerson");
+        assertEquals(person.getFullName(), "uniquePerson updatedUniqueSecondName2");
+
+        FullNameProjection personFullName = repo.queryOneByFirstNameWithProjectionNamedParameter(FullNameProjection.class, "uniquePerson");
+        assertEquals(personFullName.getFullName(), "uniquePerson updatedUniqueSecondName2");
+    }
+
+    /** */
+    @Test
+    public void testUpdateQueryMixedCaseProjectionIndexedParameter() {
+        final String newSecondName = "updatedUniqueSecondName3";
+        int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+        assertEquals(1, cnt);
+
+        List<PersonProjection> person = repo.queryByFirstNameWithProjectionNamedIndexedParameter("notUsed","uniquePerson");
+        assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName3");
+    }
+
+    /** */
+    @Test
+    public void testUpdateQueryMixedCaseProjectionIndexedParameterLuceneTextQuery() {
+        final String newSecondName = "updatedUniqueSecondName4";
+        int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+        assertEquals(1, cnt);
+
+        List<PersonProjection> person = repo.textQueryByFirstNameWithProjectionNamedParameter("uniquePerson");
+        assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName4");
+    }
+
+    /** */
+    @Test
+    public void testUpdateQueryMixedCaseProjectionNamedParameterAndTemplateDomainEntityVariable() {
+        final String newSecondName = "updatedUniqueSecondName5";
+        int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+        assertEquals(1, cnt);
+
+        List<PersonProjection> person = repo.queryByFirstNameWithProjectionNamedParameterAndTemplateDomainEntityVariable("uniquePerson");
+        assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName5");
+    }
+
+    /** */
+    @Test
+    public void testUpdateQueryMixedCaseProjectionNamedParameterWithSpELExtension() {
+        final String newSecondName = "updatedUniqueSecondName6";
+        int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+        assertEquals(1, cnt);
+
+        List<PersonProjection> person = repo.queryByFirstNameWithProjectionNamedParameterWithSpELExtension("uniquePerson");
+        assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName6");
+        assertEquals(person.get(0).getFirstName(), person.get(0).getFirstNameTransformed());
+    }
+
+    /**
+     * Update with a wrong @Query
+     */
+    @Test
+    public void testWrongUpdateQuery() {
+        final String newSecondName = "updatedUniqueSecondName";
+        int rowsUpdated = 0;
+
+        try {
+            rowsUpdated = repo.setWrongFixedSecondName(newSecondName, "uniquePerson");
+        }
+        catch (Exception ignored) {
+            //expected
+        }
+
+        assertEquals(0, rowsUpdated);
+
+        List<Person> person = repo.findByFirstName("uniquePerson");
+        assertEquals(person.get(0).getSecondName(), "uniqueLastName");
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataQueriesSelfTest.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataQueriesSelfTest.java
new file mode 100644
index 0000000..9c16f6d
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataQueriesSelfTest.java
@@ -0,0 +1,409 @@
+/*
+ * 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.springdata;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.springdata.misc.ApplicationConfiguration;
+import org.apache.ignite.springdata.misc.Person;
+import org.apache.ignite.springdata.misc.PersonProjection;
+import org.apache.ignite.springdata.misc.PersonRepository;
+import org.apache.ignite.springdata.misc.PersonRepositoryOtherIgniteInstance;
+import org.apache.ignite.springdata.misc.PersonSecondRepository;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.domain.Sort;
+
+/**
+ *
+ */
+public class IgniteSpringDataQueriesSelfTest extends GridCommonAbstractTest {
+    /** Repository. */
+    private static PersonRepository repo;
+
+    /** Repository 2. */
+    private static PersonSecondRepository repo2;
+
+    /**
+     * Repository Ignite Instance cluster TWO.
+     */
+    private static PersonRepositoryOtherIgniteInstance repoTWO;
+
+    /** Context. */
+    private static AnnotationConfigApplicationContext ctx;
+
+    /**
+     * Number of entries to store
+     */
+    private static int CACHE_SIZE = 1000;
+
+    /**
+     * Performs context initialization before tests.
+     */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        ctx = new AnnotationConfigApplicationContext();
+
+        ctx.register(ApplicationConfiguration.class);
+
+        ctx.refresh();
+
+        repo = ctx.getBean(PersonRepository.class);
+        repo2 = ctx.getBean(PersonSecondRepository.class);
+        // repository on another ignite instance (and another cluster)
+        repoTWO = ctx.getBean(PersonRepositoryOtherIgniteInstance.class);
+
+        for (int i = 0; i < CACHE_SIZE; i++) {
+            repo.save(i, new Person("person" + Integer.toHexString(i),
+                "lastName" + Integer.toHexString((i + 16) % 256)));
+            repoTWO.save(i, new Person("TWOperson" + Integer.toHexString(i),
+                "lastName" + Integer.toHexString((i + 16) % 256)));
+        }
+    }
+
+    /**
+     * Performs context destroy after tests.
+     */
+    @Override protected void afterTestsStopped() throws Exception {
+        ctx.destroy();
+    }
+
+    /** */
+    @Test
+    public void testExplicitQuery() {
+        List<Person> persons = repo.simpleQuery("person4a");
+
+        assertFalse(persons.isEmpty());
+
+        for (Person person : persons)
+            assertEquals("person4a", person.getFirstName());
+    }
+
+    /** */
+    @Test
+    public void testExplicitQueryTWO() {
+        List<Person> persons = repoTWO.simpleQuery("TWOperson4a");
+
+        assertFalse(persons.isEmpty());
+
+        for (Person person : persons)
+            assertEquals("TWOperson4a", person.getFirstName());
+    }
+
+    /** */
+    @Test
+    public void testEqualsPart() {
+        List<Person> persons = repo.findByFirstName("person4e");
+
+        assertFalse(persons.isEmpty());
+
+        for (Person person : persons)
+            assertEquals("person4e", person.getFirstName());
+    }
+
+    /** */
+    @Test
+    public void testEqualsPartTWO() {
+        List<Person> persons = repoTWO.findByFirstName("TWOperson4e");
+
+        assertFalse(persons.isEmpty());
+
+        for (Person person : persons)
+            assertEquals("TWOperson4e", person.getFirstName());
+    }
+
+    /** */
+    @Test
+    public void testContainingPart() {
+        List<Person> persons = repo.findByFirstNameContaining("person4");
+
+        assertFalse(persons.isEmpty());
+
+        for (Person person : persons)
+            assertTrue(person.getFirstName().startsWith("person4"));
+    }
+
+    /** */
+    @Test
+    public void testContainingPartTWO() {
+        List<Person> persons = repoTWO.findByFirstNameContaining("TWOperson4");
+
+        assertFalse(persons.isEmpty());
+
+        for (Person person : persons)
+            assertTrue(person.getFirstName().startsWith("TWOperson4"));
+    }
+
+    /** */
+    @Test
+    public void testTopPart() {
+        Iterable<Person> top = repo.findTopByFirstNameContaining("person4");
+
+        Iterator<Person> iter = top.iterator();
+
+        Person person = iter.next();
+
+        assertFalse(iter.hasNext());
+
+        assertTrue(person.getFirstName().startsWith("person4"));
+    }
+
+    /** */
+    @Test
+    public void testTopPartTWO() {
+        Iterable<Person> top = repoTWO.findTopByFirstNameContaining("TWOperson4");
+
+        Iterator<Person> iter = top.iterator();
+
+        Person person = iter.next();
+
+        assertFalse(iter.hasNext());
+
+        assertTrue(person.getFirstName().startsWith("TWOperson4"));
+    }
+
+    /** */
+    @Test
+    public void testLikeAndLimit() {
+        Iterable<Person> like = repo.findFirst10ByFirstNameLike("person");
+
+        int cnt = 0;
+
+        for (Person next : like) {
+            assertTrue(next.getFirstName().contains("person"));
+
+            cnt++;
+        }
+
+        assertEquals(10, cnt);
+    }
+
+    /** */
+    @Test
+    public void testLikeAndLimitTWO() {
+        Iterable<Person> like = repoTWO.findFirst10ByFirstNameLike("TWOperson");
+
+        int cnt = 0;
+
+        for (Person next : like) {
+            assertTrue(next.getFirstName().contains("TWOperson"));
+
+            cnt++;
+        }
+
+        assertEquals(10, cnt);
+    }
+
+    /** */
+    @Test
+    public void testCount() {
+        int cnt = repo.countByFirstNameLike("person");
+
+        assertEquals(1000, cnt);
+    }
+
+    /** */
+    @Test
+    public void testCountTWO() {
+        int cnt = repoTWO.countByFirstNameLike("TWOperson");
+
+        assertEquals(1000, cnt);
+    }
+
+    /** */
+    @Test
+    public void testCount2() {
+        int cnt = repo.countByFirstNameLike("person4");
+
+        assertTrue(cnt < 1000);
+    }
+
+    /** */
+    @Test
+    public void testCount2TWO() {
+        int cnt = repoTWO.countByFirstNameLike("TWOperson4");
+
+        assertTrue(cnt < 1000);
+    }
+
+    /** */
+    @Test
+    public void testPageable() {
+        PageRequest pageable = new PageRequest(1, 5, Sort.Direction.DESC, "firstName");
+
+        HashSet<String> firstNames = new HashSet<>();
+
+        List<Person> pageable1 = repo.findByFirstNameRegex("^[a-z]+$", pageable);
+
+        assertEquals(5, pageable1.size());
+
+        for (Person person : pageable1) {
+            firstNames.add(person.getFirstName());
+
+            assertTrue(person.getFirstName().matches("^[a-z]+$"));
+        }
+
+        List<Person> pageable2 = repo.findByFirstNameRegex("^[a-z]+$", pageable.next());
+
+        assertEquals(5, pageable2.size());
+
+        for (Person person : pageable2) {
+            firstNames.add(person.getFirstName());
+
+            assertTrue(person.getFirstName().matches("^[a-z]+$"));
+        }
+
+        assertEquals(10, firstNames.size());
+    }
+
+    /** */
+    @Test
+    public void testAndAndOr() {
+        int cntAnd = repo.countByFirstNameLikeAndSecondNameLike("person1", "lastName1");
+
+        int cntOr = repo.countByFirstNameStartingWithOrSecondNameStartingWith("person1", "lastName1");
+
+        assertTrue(cntAnd <= cntOr);
+    }
+
+    /** */
+    @Test
+    public void testQueryWithSort() {
+        List<Person> persons = repo.queryWithSort("^[a-z]+$", new Sort(Sort.Direction.DESC, "secondName"));
+
+        Person previous = persons.get(0);
+
+        for (Person person : persons) {
+            assertTrue(person.getSecondName().compareTo(previous.getSecondName()) <= 0);
+
+            assertTrue(person.getFirstName().matches("^[a-z]+$"));
+
+            previous = person;
+        }
+    }
+
+    /** */
+    @Test
+    public void testQueryWithPaging() {
+        List<Person> persons = repo.queryWithPageable("^[a-z]+$", new PageRequest(1, 7, Sort.Direction.DESC, "secondName"));
+
+        assertEquals(7, persons.size());
+
+        Person previous = persons.get(0);
+
+        for (Person person : persons) {
+            assertTrue(person.getSecondName().compareTo(previous.getSecondName()) <= 0);
+
+            assertTrue(person.getFirstName().matches("^[a-z]+$"));
+
+            previous = person;
+        }
+    }
+
+    /** */
+    @Test
+    public void testQueryFields() {
+        List<String> persons = repo.selectField("^[a-z]+$", new PageRequest(1, 7, Sort.Direction.DESC, "secondName"));
+
+        assertEquals(7, persons.size());
+    }
+
+    /** */
+    @Test
+    public void testFindCacheEntries() {
+        List<Cache.Entry<Integer, Person>> cacheEntries = repo.findBySecondNameLike("stName1");
+
+        assertFalse(cacheEntries.isEmpty());
+
+        for (Cache.Entry<Integer, Person> entry : cacheEntries)
+            assertTrue(entry.getValue().getSecondName().contains("stName1"));
+    }
+
+    /** */
+    @Test
+    public void testFindOneCacheEntry() {
+        Cache.Entry<Integer, Person> cacheEntry = repo.findTopBySecondNameLike("tName18");
+
+        assertNotNull(cacheEntry);
+
+        assertTrue(cacheEntry.getValue().getSecondName().contains("tName18"));
+    }
+
+    /** */
+    @Test
+    public void testFindOneValue() {
+        PersonProjection person = repo.findTopBySecondNameStartingWith("lastName18");
+
+        assertNotNull(person);
+
+        assertTrue(person.getFullName().split("\\s")[1].startsWith("lastName18"));
+    }
+
+    /** */
+    @Test
+    public void testSelectSeveralFields() {
+        List<List> lists = repo.selectSeveralField("^[a-z]+$", new PageRequest(2, 6));
+
+        assertEquals(6, lists.size());
+
+        for (List list : lists) {
+            assertEquals(2, list.size());
+
+            assertTrue(list.get(0) instanceof Integer);
+        }
+    }
+
+    /** */
+    @Test
+    public void testCountQuery() {
+        int cnt = repo.countQuery(".*");
+
+        assertEquals(256, cnt);
+    }
+
+    /** */
+    @Test
+    public void testSliceOfCacheEntries() {
+        Slice<Cache.Entry<Integer, Person>> slice = repo2.findBySecondNameIsNot("lastName18", new PageRequest(3, 4));
+
+        assertEquals(4, slice.getSize());
+
+        for (Cache.Entry<Integer, Person> entry : slice)
+            assertFalse("lastName18".equals(entry.getValue().getSecondName()));
+    }
+
+    /** */
+    @Test
+    public void testSliceOfLists() {
+        Slice<List> lists = repo2.querySliceOfList("^[a-z]+$", new PageRequest(0, 3));
+
+        assertEquals(3, lists.getSize());
+
+        for (List list : lists) {
+            assertEquals(2, list.size());
+
+            assertTrue(list.get(0) instanceof Integer);
+        }
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/City.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/City.java
new file mode 100644
index 0000000..e86a575
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/City.java
@@ -0,0 +1,113 @@
+/*
+ * 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.springdata.compoundkey;
+
+import java.util.Objects;
+
+/**
+ * Value-class
+ * */
+public class City {
+    /** City name */
+    private String name;
+
+    /** City district */
+    private String district;
+
+    /** City population */
+    private int population;
+
+    /**
+     * @param name city name
+     * @param district city district
+     * @param population city population
+     * */
+    public City(String name, String district, int population) {
+        this.name = name;
+        this.district = district;
+        this.population = population;
+    }
+
+    /**
+     * @return city name
+     * */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * @param name city name
+     * */
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    /**
+     * @return city district
+     * */
+    public String getDistrict() {
+        return district;
+    }
+
+    /**
+     * @param district city district
+     * */
+    public void setDistrict(String district) {
+        this.district = district;
+    }
+
+    /**
+     * @return city population
+     * */
+    public int getPopulation() {
+        return population;
+    }
+
+    /**
+     * @param population city population
+     * */
+    public void setPopulation(int population) {
+        this.population = population;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return name + " | " + district + " | " + population;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+
+        if (o == null || getClass() != o.getClass())
+            return false;
+
+        City city = (City)o;
+
+        return
+                Objects.equals(this.name, city.name) &&
+                        Objects.equals(this.district, city.district) &&
+                        this.population == city.population;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        return Objects.hash(name, district, population);
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityKey.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityKey.java
new file mode 100644
index 0000000..8a5dba0
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityKey.java
@@ -0,0 +1,76 @@
+/*
+ * 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.springdata.compoundkey;
+
+import java.io.Serializable;
+import java.util.Objects;
+import org.apache.ignite.cache.affinity.AffinityKeyMapped;
+
+/** Compound key for city class  */
+public class CityKey implements Serializable {
+    /** city identifier */
+    private int ID;
+
+    /** affinity key countrycode  */
+    @AffinityKeyMapped
+    private String COUNTRYCODE;
+
+    /**
+     * @param id city identifier
+     * @param countryCode city countrycode
+     * */
+    public CityKey(int id, String countryCode) {
+        this.ID = id;
+        this.COUNTRYCODE = countryCode;
+    }
+
+    /**
+     * @return city id
+     * */
+    public int getId() {
+        return ID;
+    }
+
+    /**
+     * @return countrycode
+     * */
+    public String getCountryCode() {
+        return COUNTRYCODE;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (o == null || getClass() != o.getClass())
+            return false;
+        CityKey key = (CityKey)o;
+        return ID == key.ID &&
+                COUNTRYCODE.equals(key.COUNTRYCODE);
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        return Objects.hash(ID, COUNTRYCODE);
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return ID + " | " + COUNTRYCODE;
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityRepository.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityRepository.java
new file mode 100644
index 0000000..d9adee3
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityRepository.java
@@ -0,0 +1,43 @@
+/*
+ * 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.springdata.compoundkey;
+
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.apache.ignite.springdata20.repository.config.RepositoryConfig;
+import org.springframework.stereotype.Repository;
+
+/** City repository */
+@Repository
+@RepositoryConfig(cacheName = "City", autoCreateCache = true)
+public interface CityRepository extends IgniteRepository<City, CityKey> {
+    /**
+     * Find city by id
+     * @param id city identifier
+     * @return city
+     * */
+    public City findById(int id);
+
+    /**
+     * Find all cities by coutrycode
+     * @param cc coutrycode
+     * @return list of cache enrties CityKey -> City
+     * */
+    public List<Cache.Entry<CityKey, City>> findByCountryCode(String cc);
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CompoundKeyApplicationConfiguration.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CompoundKeyApplicationConfiguration.java
new file mode 100644
index 0000000..03c05cd
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CompoundKeyApplicationConfiguration.java
@@ -0,0 +1,45 @@
+/*
+ * 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.springdata.compoundkey;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.ClientConnectorConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.springdata20.repository.config.EnableIgniteRepositories;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Spring application configuration
+ * */
+@Configuration
+@EnableIgniteRepositories
+public class CompoundKeyApplicationConfiguration {
+    /** */
+    public static final int CLI_CONN_PORT = 10810;
+
+    /**
+     * Ignite instance bean
+     * */
+    @Bean
+    public Ignite igniteInstance() {
+        return Ignition.start(new IgniteConfiguration()
+            .setClientConnectorConfiguration(new ClientConnectorConfiguration().setPort(CLI_CONN_PORT)));
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/ApplicationConfiguration.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/ApplicationConfiguration.java
new file mode 100644
index 0000000..bce3340
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/ApplicationConfiguration.java
@@ -0,0 +1,113 @@
+/*
+ * 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.springdata.misc;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
+import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder;
+import org.apache.ignite.springdata.misc.SampleEvaluationContextExtension.SamplePassParamExtension;
+import org.apache.ignite.springdata20.repository.config.EnableIgniteRepositories;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.repository.query.spi.EvaluationContextExtension;
+
+/** */
+@Configuration
+@EnableIgniteRepositories
+public class ApplicationConfiguration {
+    /** */
+    public static final String IGNITE_INSTANCE_ONE = "IGNITE_INSTANCE_ONE";
+
+    /** */
+    public static final String IGNITE_INSTANCE_TWO = "IGNITE_INSTANCE_TWO";
+
+    /**
+     * The bean with cache names
+     */
+    @Bean
+    public CacheNamesBean cacheNames() {
+        CacheNamesBean bean = new CacheNamesBean();
+
+        bean.setPersonCacheName("PersonCache");
+
+        return bean;
+    }
+
+    /** */
+    @Bean
+    public EvaluationContextExtension sampleSpELExtension() {
+        return new SampleEvaluationContextExtension();
+    }
+
+    /** */
+    @Bean(value = "sampleExtensionBean")
+    public SamplePassParamExtension sampleExtensionBean() {
+        return new SamplePassParamExtension();
+    }
+
+    /**
+     * Ignite instance bean - no instance name provided on RepositoryConfig
+     */
+    @Bean
+    public Ignite igniteInstance() {
+        IgniteConfiguration cfg = new IgniteConfiguration();
+
+        cfg.setIgniteInstanceName(IGNITE_INSTANCE_ONE);
+
+        CacheConfiguration ccfg = new CacheConfiguration("PersonCache");
+
+        ccfg.setIndexedTypes(Integer.class, Person.class);
+
+        cfg.setCacheConfiguration(ccfg);
+
+        TcpDiscoverySpi spi = new TcpDiscoverySpi();
+
+        spi.setIpFinder(new TcpDiscoveryVmIpFinder(true));
+
+        cfg.setDiscoverySpi(spi);
+
+        return Ignition.start(cfg);
+    }
+
+    /**
+     * Ignite instance bean with not default name
+     */
+    @Bean
+    public Ignite igniteInstanceTWO() {
+        IgniteConfiguration cfg = new IgniteConfiguration();
+
+        cfg.setIgniteInstanceName(IGNITE_INSTANCE_TWO);
+
+        CacheConfiguration ccfg = new CacheConfiguration("PersonCache");
+
+        ccfg.setIndexedTypes(Integer.class, Person.class);
+
+        cfg.setCacheConfiguration(ccfg);
+
+        TcpDiscoverySpi spi = new TcpDiscoverySpi();
+
+        spi.setIpFinder(new TcpDiscoveryVmIpFinder(true));
+
+        cfg.setDiscoverySpi(spi);
+
+        return Ignition.start(cfg);
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/CacheNamesBean.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/CacheNamesBean.java
new file mode 100644
index 0000000..6185744
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/CacheNamesBean.java
@@ -0,0 +1,42 @@
+/*
+ * 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.springdata.misc;
+
+/**
+ * The bean with cache names.
+ */
+public class CacheNamesBean {
+    /** */
+    private String personCacheName;
+
+    /**
+     * Get name of cache for persons.
+     *
+     * @return Name of cache.
+     */
+    public String getPersonCacheName() {
+        return personCacheName;
+    }
+
+    /**
+     * @param personCacheName Name of cache.
+     */
+    public void setPersonCacheName(String personCacheName) {
+        this.personCacheName = personCacheName;
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/FullNameProjection.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/FullNameProjection.java
new file mode 100644
index 0000000..23f8f8e
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/FullNameProjection.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.springdata.misc;
+
+import org.springframework.beans.factory.annotation.Value;
+
+/**
+ * Advanced SpEl Expressions into projection
+ *
+ * @author Manuel Núñez Sánchez (manuel.nunez@hawkore.com)
+ */
+public interface FullNameProjection {
+    /**
+     * Sample of using SpEL expression
+     * @return
+     */
+    @Value("#{target.firstName + ' ' + target.secondName}")
+    String getFullName();
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/Person.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/Person.java
new file mode 100644
index 0000000..531c67f
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/Person.java
@@ -0,0 +1,100 @@
+/*
+ * 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.springdata.misc;
+
+import java.util.Objects;
+import org.apache.ignite.cache.query.annotations.QuerySqlField;
+import org.apache.ignite.cache.query.annotations.QueryTextField;
+
+/**
+ * DTO class.
+ */
+public class Person {
+    /** First name. */
+    @QuerySqlField(index = true)
+    @QueryTextField
+    private String firstName;
+
+    /** Second name. */
+    @QuerySqlField(index = true)
+    private String secondName;
+
+    /**
+     * @param firstName First name.
+     * @param secondName Second name.
+     */
+    public Person(String firstName, String secondName) {
+        this.firstName = firstName;
+        this.secondName = secondName;
+    }
+
+    /**
+     * @return First name.
+     */
+    public String getFirstName() {
+        return firstName;
+    }
+
+    /**
+     * @param firstName First name.
+     */
+    public void setFirstName(String firstName) {
+        this.firstName = firstName;
+    }
+
+    /**
+     * @return Second name.
+     */
+    public String getSecondName() {
+        return secondName;
+    }
+
+    /**
+     * @param secondName Second name.
+     */
+    public void setSecondName(String secondName) {
+        this.secondName = secondName;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return "Person{" +
+            "firstName='" + firstName + '\'' +
+            ", secondName='" + secondName + '\'' +
+            '}';
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+
+        if (o == null || getClass() != o.getClass())
+            return false;
+
+        Person person = (Person)o;
+
+        return Objects.equals(firstName, person.firstName) &&
+            Objects.equals(secondName, person.secondName);
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        return Objects.hash(firstName, secondName);
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonExpressionRepository.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonExpressionRepository.java
new file mode 100644
index 0000000..8222eb3
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonExpressionRepository.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.springdata.misc;
+
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.apache.ignite.springdata20.repository.config.RepositoryConfig;
+
+/**
+ *
+ */
+@RepositoryConfig(cacheName = "#{cacheNames.personCacheName}")
+public interface PersonExpressionRepository extends IgniteRepository<Person, Integer> {
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonKey.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonKey.java
new file mode 100644
index 0000000..2537c57
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonKey.java
@@ -0,0 +1,54 @@
+/*
+ * 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.springdata.misc;
+
+import java.io.Serializable;
+
+/**
+ * Compound key.
+ */
+public class PersonKey implements Serializable {
+    /** */
+    private int id1;
+
+    /** */
+    private int id2;
+
+    /**
+     * @param id1 ID1.
+     * @param id2 ID2.
+     */
+    public PersonKey(int id1, int id2) {
+        this.id1 = id1;
+        this.id2 = id2;
+    }
+
+    /**
+     * @return ID1
+     */
+    public int getId1() {
+        return id1;
+    }
+
+    /**
+     * @return ID1
+     */
+    public int getId2() {
+        return id1;
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonProjection.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonProjection.java
new file mode 100644
index 0000000..a187a08
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonProjection.java
@@ -0,0 +1,43 @@
+/*
+ * 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.springdata.misc;
+
+import org.springframework.beans.factory.annotation.Value;
+
+/**
+ * Advanced SpEl Expressions into projection
+ *
+ * @author Manuel Núñez Sánchez (manuel.nunez@hawkore.com)
+ */
+public interface PersonProjection {
+    /** */
+    String getFirstName();
+
+    /**
+     * Sample of using registered spring bean into SpEL expression
+     * @return
+     */
+    @Value("#{@sampleExtensionBean.transformParam(target.firstName)}")
+    String getFirstNameTransformed();
+
+    /**
+     * Sample of using SpEL expression
+     * @return
+     */
+    @Value("#{target.firstName + ' ' + target.secondName}")
+    String getFullName();
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepository.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepository.java
new file mode 100644
index 0000000..c50a2d0
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepository.java
@@ -0,0 +1,148 @@
+/*
+ * 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.springdata.misc;
+
+import java.util.Collection;
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.apache.ignite.springdata20.repository.config.Query;
+import org.apache.ignite.springdata20.repository.config.RepositoryConfig;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.repository.query.Param;
+
+/**
+ * Test repository.
+ */
+@RepositoryConfig(cacheName = "PersonCache")
+public interface PersonRepository extends IgniteRepository<Person, Integer> {
+    /** */
+    public List<Person> findByFirstName(String val);
+
+    /** */
+    @Query("firstName = ?")
+    public List<PersonProjection> queryByFirstNameWithProjection(String val);
+
+    /** */
+    @Query("firstName = :firstname")
+    public List<PersonProjection> queryByFirstNameWithProjectionNamedParameter(@Param("firstname") String val);
+
+    /** */
+    @Query("firstName = :firstname")
+    public <P> List<P> queryByFirstNameWithProjectionNamedParameter(Class<P> dynamicProjection, @Param("firstname") String val);
+
+    /** */
+    @Query("firstName = :firstname")
+    public <P> P queryOneByFirstNameWithProjectionNamedParameter(Class<P> dynamicProjection, @Param("firstname") String val);
+
+    /** */
+    @Query("firstName = ?#{[1]}")
+    public List<PersonProjection> queryByFirstNameWithProjectionNamedIndexedParameter(@Param("notUsed") String notUsed, @Param("firstname") String val);
+
+    /** */
+    @Query(textQuery = true, value = "#{#firstname}", limit = 2)
+    public List<PersonProjection> textQueryByFirstNameWithProjectionNamedParameter(@Param("firstname") String val);
+
+    @Query(value = "select * from (sElecT * from #{#entityName} where firstName = :firstname)", forceFieldsQuery = true)
+    public List<PersonProjection> queryByFirstNameWithProjectionNamedParameterAndTemplateDomainEntityVariable(@Param("firstname") String val);
+
+    @Query(value = "firstName = ?#{sampleExtension.transformParam(#firstname)}")
+    public List<PersonProjection> queryByFirstNameWithProjectionNamedParameterWithSpELExtension(@Param("firstname") String val);
+
+    /** */
+    public List<Person> findByFirstNameContaining(String val);
+
+    /** */
+    public List<Person> findByFirstNameRegex(String val, Pageable pageable);
+
+    /** */
+    public Collection<Person> findTopByFirstNameContaining(String val);
+
+    /** */
+    public Iterable<Person> findFirst10ByFirstNameLike(String val);
+
+    /** */
+    public int countByFirstName(String val);
+
+    /** */
+    public int countByFirstNameLike(String val);
+
+    /** */
+    public int countByFirstNameLikeAndSecondNameLike(String like1, String like2);
+
+    /** */
+    public int countByFirstNameStartingWithOrSecondNameStartingWith(String like1, String like2);
+
+    /** */
+    public List<Cache.Entry<Integer, Person>> findBySecondNameLike(String val);
+
+    /** */
+    public Cache.Entry<Integer, Person> findTopBySecondNameLike(String val);
+
+    /** */
+    public PersonProjection findTopBySecondNameStartingWith(String val);
+
+    /** */
+    @Query("firstName = ?")
+    public List<Person> simpleQuery(String val);
+
+    /** */
+    @Query("firstName REGEXP ?")
+    public List<Person> queryWithSort(String val, Sort sort);
+
+    /** */
+    @Query("SELECT * FROM Person WHERE firstName REGEXP ?")
+    public List<Person> queryWithPageable(String val, Pageable pageable);
+
+    /** */
+    @Query("SELECT secondName FROM Person WHERE firstName REGEXP ?")
+    public List<String> selectField(String val, Pageable pageable);
+
+    /** */
+    @Query("SELECT _key, secondName FROM Person WHERE firstName REGEXP ?")
+    public List<List> selectSeveralField(String val, Pageable pageable);
+
+    /** */
+    @Query("SELECT count(1) FROM (SELECT DISTINCT secondName FROM Person WHERE firstName REGEXP ?)")
+    public int countQuery(String val);
+
+    /** Top 3 query */
+    public List<Person> findTop3ByFirstName(String val);
+
+    /** Delete query */
+    public long deleteByFirstName(String firstName);
+
+    /** Remove Query */
+    public List<Person> removeByFirstName(String firstName);
+
+    /** Delete using @Query with keyword in lower-case */
+    @Query("delete FROM Person WHERE secondName = ?")
+    public void deleteBySecondNameLowerCase(String secondName);
+
+    /** Delete using @Query but with errors on the query */
+    @Query("DELETE FROM Person WHERE firstName = ? AND ERRORS = 'ERRORS'")
+    public void deleteWrongByFirstNameQuery(String firstName);
+
+    /** Update using @Query with keyword in mixed-case */
+    @Query("upDATE Person SET secondName = ? WHERE firstName = ?")
+    public int setFixedSecondNameMixedCase(String secondName, String firstName);
+
+    /** Update using @Query but with errors on the query */
+    @Query("UPDATE Person SET secondName = ? WHERE firstName = ? AND ERRORS = 'ERRORS'")
+    public int setWrongFixedSecondName(String secondName, String firstName);
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryOtherIgniteInstance.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryOtherIgniteInstance.java
new file mode 100644
index 0000000..ac6231b
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryOtherIgniteInstance.java
@@ -0,0 +1,143 @@
+
+/*
+ * 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.springdata.misc;
+
+import java.util.Collection;
+import java.util.List;
+
+import javax.cache.Cache;
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.apache.ignite.springdata20.repository.config.Query;
+import org.apache.ignite.springdata20.repository.config.RepositoryConfig;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.repository.query.Param;
+
+/**
+ *
+ */
+@RepositoryConfig(igniteInstance = "igniteInstanceTWO", cacheName = "PersonCache")
+public interface PersonRepositoryOtherIgniteInstance extends IgniteRepository<Person, Integer> {
+    /** */
+    public List<Person> findByFirstName(String val);
+
+    /** */
+    @Query("firstName = ?")
+    public List<PersonProjection> queryByFirstNameWithProjection(String val);
+
+    /** */
+    @Query("firstName = :firstname")
+    public List<PersonProjection> queryByFirstNameWithProjectionNamedParameter(@Param("firstname") String val);
+
+    /** */
+    @Query("firstName = ?#{[1]}")
+    public List<PersonProjection> queryByFirstNameWithProjectionNamedIndexedParameter(@Param("notUsed") String notUsed, @Param("firstname") String val);
+
+    /** */
+    @Query(textQuery = true, value = "#{#firstname}", limit = 2)
+    public List<PersonProjection> textQueryByFirstNameWithProjectionNamedParameter(@Param("firstname") String val);
+
+    @Query(value = "select * from (sElecT * from #{#entityName} where firstName = :firstname)", forceFieldsQuery = true)
+    public List<PersonProjection> queryByFirstNameWithProjectionNamedParameterAndTemplateDomainEntityVariable(@Param("firstname") String val);
+
+    @Query(value = "firstName = ?#{sampleExtension.transformParam(#firstname)}")
+    public List<PersonProjection> queryByFirstNameWithProjectionNamedParameterWithSpELExtension(@Param("firstname") String val);
+
+    /** */
+    public List<Person> findByFirstNameContaining(String val);
+
+    /** */
+    public List<Person> findByFirstNameRegex(String val, Pageable pageable);
+
+    /** */
+    public Collection<Person> findTopByFirstNameContaining(String val);
+
+    /** */
+    public Iterable<Person> findFirst10ByFirstNameLike(String val);
+
+    /** */
+    public int countByFirstName(String val);
+
+    /** */
+    public int countByFirstNameLike(String val);
+
+    /** */
+    public int countByFirstNameLikeAndSecondNameLike(String like1, String like2);
+
+    /** */
+    public int countByFirstNameStartingWithOrSecondNameStartingWith(String like1, String like2);
+
+    /** */
+    public List<Cache.Entry<Integer, Person>> findBySecondNameLike(String val);
+
+    /** */
+    public Cache.Entry<Integer, Person> findTopBySecondNameLike(String val);
+
+    /** */
+    public PersonProjection findTopBySecondNameStartingWith(String val);
+
+    /** */
+    @Query("firstName = ?")
+    public List<Person> simpleQuery(String val);
+
+    /** */
+    @Query("firstName REGEXP ?")
+    public List<Person> queryWithSort(String val, Sort sort);
+
+    /** */
+    @Query("SELECT * FROM Person WHERE firstName REGEXP ?")
+    public List<Person> queryWithPageable(String val, Pageable pageable);
+
+    /** */
+    @Query("SELECT secondName FROM Person WHERE firstName REGEXP ?")
+    public List<String> selectField(String val, Pageable pageable);
+
+    /** */
+    @Query("SELECT _key, secondName FROM Person WHERE firstName REGEXP ?")
+    public List<List> selectSeveralField(String val, Pageable pageable);
+
+    /** */
+    @Query("SELECT count(1) FROM (SELECT DISTINCT secondName FROM Person WHERE firstName REGEXP ?)")
+    public int countQuery(String val);
+
+    /** Top 3 query */
+    public List<Person> findTop3ByFirstName(String val);
+
+    /** Delete query */
+    public long deleteByFirstName(String firstName);
+
+    /** Remove Query */
+    public List<Person> removeByFirstName(String firstName);
+
+    /** Delete using @Query with keyword in lower-case */
+    @Query("delete FROM Person WHERE secondName = ?")
+    public void deleteBySecondNameLowerCase(String secondName);
+
+    /** Delete using @Query but with errors on the query */
+    @Query("DELETE FROM Person WHERE firstName = ? AND ERRORS = 'ERRORS'")
+    public void deleteWrongByFirstNameQuery(String firstName);
+
+    /** Update using @Query with keyword in mixed-case */
+    @Query("upDATE Person SET secondName = ? WHERE firstName = ?")
+    public int setFixedSecondNameMixedCase(String secondName, String firstName);
+
+    /** Update using @Query but with errors on the query */
+    @Query("UPDATE Person SET secondName = ? WHERE firstName = ? AND ERRORS = 'ERRORS'")
+    public int setWrongFixedSecondName(String secondName, String firstName);
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryWithCompoundKey.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryWithCompoundKey.java
new file mode 100644
index 0000000..bf77597
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryWithCompoundKey.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.springdata.misc;
+
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.apache.ignite.springdata20.repository.config.RepositoryConfig;
+
+/**
+ * Test repository.
+ */
+@RepositoryConfig(cacheName = "PersonWithKeyCache", autoCreateCache = true)
+public interface PersonRepositoryWithCompoundKey extends IgniteRepository<Person, PersonKey> {
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonSecondRepository.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonSecondRepository.java
new file mode 100644
index 0000000..2bc713c
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/PersonSecondRepository.java
@@ -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.
+ */
+
+package org.apache.ignite.springdata.misc;
+
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.springdata20.repository.IgniteRepository;
+import org.apache.ignite.springdata20.repository.config.Query;
+import org.apache.ignite.springdata20.repository.config.RepositoryConfig;
+import org.springframework.data.domain.AbstractPageRequest;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Slice;
+
+/**
+ * Test repository.
+ */
+@RepositoryConfig(cacheName = "PersonCache")
+public interface PersonSecondRepository extends IgniteRepository<Person, Integer> {
+    /** */
+    public Slice<Cache.Entry<Integer, Person>> findBySecondNameIsNot(String val, PageRequest pageReq);
+
+    /** */
+    @Query("SELECT _key, secondName FROM Person WHERE firstName REGEXP ?")
+    public Slice<List> querySliceOfList(String val, AbstractPageRequest pageReq);
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/SampleEvaluationContextExtension.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/SampleEvaluationContextExtension.java
new file mode 100644
index 0000000..0fa535d
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/springdata/misc/SampleEvaluationContextExtension.java
@@ -0,0 +1,92 @@
+/*
+ * 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.springdata.misc;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.data.repository.query.spi.EvaluationContextExtensionSupport;
+
+/**
+ * Sample EvaluationContext Extension for Spring Data 2.0
+ * <p>
+ * Use SpEl expressions into your {@code @Query} definitions.
+ * <p>
+ * First, you need to register your extension into your spring data configuration. Sample:
+ * <pre>
+ * {@code @Configuration}
+ * {@code @EnableIgniteRepositories}(basePackages = ... )
+ * public class MyIgniteRepoConfig {
+ * ...
+ *      {@code @Bean}
+ *      public EvaluationContextExtension sampleSpELExtension() {
+ *          return new SampleEvaluationContextExtension();
+ *      }
+ * ...
+ * }
+ * </pre>
+ *
+ * <p>
+ * Sample of usage into your {@code @Query} definitions:
+ * <pre>
+ * {@code @RepositoryConfig}(cacheName = "users")
+ * public interface UserRepository
+ * extends IgniteRepository<User, UUID>{
+ *     [...]
+ *
+ *     {@code @Query}(value = "SELECT * from #{#entityName} where email = ?#{sampleExtension.transformParam(#email)}")
+ *     User searchUserByEmail(@Param("email") String email);
+ *
+ *      [...]
+ *     }
+ * </pre>
+ * <p>
+ *
+ * @author Manuel Núñez Sánchez (manuel.nunez@hawkore.com)
+ */
+public class SampleEvaluationContextExtension extends EvaluationContextExtensionSupport {
+    /** */
+    private static final SamplePassParamExtension SAMPLE_PASS_PARAM_EXTENSION_INSTANCE = new SamplePassParamExtension();
+
+    /** */
+    private static final Map<String, Object> properties = new HashMap<>();
+
+    /** */
+    private static final String SAMPLE_EXTENSION_SPEL_VAR = "sampleExtension";
+
+    static {
+        properties.put(SAMPLE_EXTENSION_SPEL_VAR, SAMPLE_PASS_PARAM_EXTENSION_INSTANCE);
+    }
+
+    /** */
+    @Override public String getExtensionId() {
+        return "HK-SAMPLE-PASS-PARAM-EXTENSION";
+    }
+
+    /** */
+    @Override public Map<String, Object> getProperties() {
+        return properties;
+    }
+
+    /** */
+    public static class SamplePassParamExtension {
+        // just return same param
+        public Object transformParam(Object param) {
+            return param;
+        }
+    }
+}
diff --git a/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/testsuites/IgniteSpringData2TestSuite.java b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/testsuites/IgniteSpringData2TestSuite.java
new file mode 100644
index 0000000..4efc48b
--- /dev/null
+++ b/modules/spring-data-2.0-ext/src/test/java/org/apache/ignite/testsuites/IgniteSpringData2TestSuite.java
@@ -0,0 +1,38 @@
+/*
+ * 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.testsuites;
+
+import org.apache.ignite.springdata.IgniteSpringDataCompoundKeyTest;
+import org.apache.ignite.springdata.IgniteSpringDataCrudSelfExpressionTest;
+import org.apache.ignite.springdata.IgniteSpringDataCrudSelfTest;
+import org.apache.ignite.springdata.IgniteSpringDataQueriesSelfTest;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+/**
+ * Ignite Spring Data 2.0 test suite.
+ */
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+    IgniteSpringDataCrudSelfTest.class,
+    IgniteSpringDataQueriesSelfTest.class,
+    IgniteSpringDataCrudSelfExpressionTest.class,
+    IgniteSpringDataCompoundKeyTest.class
+})
+public class IgniteSpringData2TestSuite {
+}
diff --git a/modules/spring-data-2.2-ext/README.txt b/modules/spring-data-2.2-ext/README.txt
new file mode 100644
index 0000000..8489445
--- /dev/null
+++ b/modules/spring-data-2.2-ext/README.txt
@@ -0,0 +1,43 @@
+Apache Ignite Spring Module
+---------------------------
+
+Apache Ignite Spring Data 2.2 extension provides an integration with Spring Data 2.2 framework.
+
+Main features:
+
+- Supports multiple Ignite instances on same JVM (@RepositoryConfig).
+- Supports query tuning parameters in @Query annotation
+- Supports projections
+- Supports Page and Stream responses
+- Supports Sql Fields Query resultset transformation into the domain entity
+- Supports named parameters (:myParam) into SQL queries, declared using @Param("myParam")
+- Supports advanced parameter binding and SpEL expressions into SQL queries:
+- Template variables:
+    - #entityName - the simple class name of the domain entity
+- Method parameter expressions: Parameters are exposed for indexed access ([0] is the first query method's param) or via the name declared using @Param. The actual SpEL expression binding is triggered by ?#. Example: ?#{[0] or ?#{#myParamName}
+- Advanced SpEL expressions: While advanced parameter binding is a very useful feature, the real power of SpEL stems from the fact, that the expressions can refer to framework abstractions or other application components through SpEL EvaluationContext extension model.
+- Supports SpEL expressions into Text queries (TextQuery).
+
+Importing Spring Data 2.2 extension In Maven Project
+----------------------------------------
+
+If you are using Maven to manage dependencies of your project, you can add Spring Data 2.2 extension
+dependency like this (replace '${ignite-spring-data_2.2-ext.version}' with actual version of Ignite Spring Data 2.2
+extension you are interested in):
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+                        http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    ...
+    <dependencies>
+        ...
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-spring-data_2.2-ext</artifactId>
+            <version>${ignite-spring-data_2.2-ext.version}</version>
+        </dependency>
+        ...
+    </dependencies>
+    ...
+</project>
diff --git a/modules/spring-data-2.2-ext/licenses/apache-2.0.txt b/modules/spring-data-2.2-ext/licenses/apache-2.0.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/modules/spring-data-2.2-ext/licenses/apache-2.0.txt
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       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.
diff --git a/modules/spring-data-2.2-ext/modules/core/src/test/config/log4j-test.xml b/modules/spring-data-2.2-ext/modules/core/src/test/config/log4j-test.xml
new file mode 100755
index 0000000..3061bd4
--- /dev/null
+++ b/modules/spring-data-2.2-ext/modules/core/src/test/config/log4j-test.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<!DOCTYPE log4j:configuration PUBLIC "-//APACHE//DTD LOG4J 1.2//EN"
+    "http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd">
+<!--
+    Log4j configuration.
+-->
+<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="false">
+    <!--
+        Logs System.out messages to console.
+    -->
+    <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender">
+        <!-- Log to STDOUT. -->
+        <param name="Target" value="System.out"/>
+
+        <!-- Log from DEBUG and higher. -->
+        <param name="Threshold" value="DEBUG"/>
+
+        <!-- The default pattern: Date Priority [Category] Message\n -->
+        <layout class="org.apache.log4j.PatternLayout">
+            <param name="ConversionPattern" value="[%d{ISO8601}][%-5p][%t][%c{1}] %m%n"/>
+        </layout>
+
+        <!-- Do not log beyond INFO level. -->
+        <filter class="org.apache.log4j.varia.LevelRangeFilter">
+            <param name="levelMin" value="DEBUG"/>
+            <param name="levelMax" value="INFO"/>
+        </filter>
+    </appender>
+
+    <!--
+        Logs all System.err messages to console.
+    -->
+    <appender name="CONSOLE_ERR" class="org.apache.log4j.ConsoleAppender">
+        <!-- Log to STDERR. -->
+        <param name="Target" value="System.err"/>
+
+        <!-- Log from WARN and higher. -->
+        <param name="Threshold" value="WARN"/>
+
+        <!-- The default pattern: Date Priority [Category] Message\n -->
+        <layout class="org.apache.log4j.PatternLayout">
+            <param name="ConversionPattern" value="[%d{ISO8601}][%-5p][%t][%c{1}] %m%n"/>
+        </layout>
+    </appender>
+
+    <!--
+        Logs all output to specified file.
+    -->
+    <appender name="FILE" class="org.apache.log4j.RollingFileAppender">
+        <param name="Threshold" value="DEBUG"/>
+        <param name="File" value="ignite/work/log/ignite.log"/>
+        <param name="Append" value="true"/>
+        <param name="MaxFileSize" value="10MB"/>
+        <param name="MaxBackupIndex" value="10"/>
+        <layout class="org.apache.log4j.PatternLayout">
+            <param name="ConversionPattern" value="[%d{ISO8601}][%-5p][%t][%c{1}] %m%n"/>
+        </layout>
+    </appender>
+
+    <!-- Disable all open source debugging. -->
+    <category name="org">
+        <level value="INFO"/>
+    </category>
+
+    <category name="org.eclipse.jetty">
+        <level value="INFO"/>
+    </category>
+
+    <!-- Default settings. -->
+    <root>
+        <!-- Print at info by default. -->
+        <level value="INFO"/>
+
+        <!-- Append to file and console. -->
+        <appender-ref ref="FILE"/>
+        <appender-ref ref="CONSOLE"/>
+        <appender-ref ref="CONSOLE_ERR"/>
+    </root>
+</log4j:configuration>
diff --git a/modules/spring-data-2.2-ext/modules/core/src/test/config/tests.properties b/modules/spring-data-2.2-ext/modules/core/src/test/config/tests.properties
new file mode 100644
index 0000000..0faf5b8
--- /dev/null
+++ b/modules/spring-data-2.2-ext/modules/core/src/test/config/tests.properties
@@ -0,0 +1,22 @@
+#
+# 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.
+#
+
+# Local address to bind to.
+local.ip=127.0.0.1
+
+# TCP communication port
+comm.tcp.port=30010
diff --git a/modules/spring-data-2.2-ext/pom.xml b/modules/spring-data-2.2-ext/pom.xml
new file mode 100644
index 0000000..b0c00ba
--- /dev/null
+++ b/modules/spring-data-2.2-ext/pom.xml
@@ -0,0 +1,154 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<!--
+    POM file.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.ignite</groupId>
+        <artifactId>ignite-extensions-parent</artifactId>
+        <version>1</version>
+        <relativePath>../../parent</relativePath>
+    </parent>
+
+    <artifactId>ignite-spring-data_2.2-ext</artifactId>
+    <version>1.0.0-SNAPSHOT</version>
+    <url>http://ignite.apache.org</url>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-core</artifactId>
+            <version>${ignite.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-indexing</artifactId>
+            <version>${ignite.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-log4j</artifactId>
+            <version>${ignite.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.data</groupId>
+            <artifactId>spring-data-commons</artifactId>
+            <version>${spring.data-2.2.version}</version>
+            <!-- Exclude slf4j logging in favor of log4j -->
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>jcl-over-slf4j</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-spring</artifactId>
+            <version>${ignite.version}</version>
+            <!--Remove exclusion while upgrading ignite-spring version to 5.2-->
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-core</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-beans</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-aop</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-context</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-expression</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-tx</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-jdbc</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <!--Remove spring-core and spring-beans dependencies while upgrading ignite-spring version to 5.2-->
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-core</artifactId>
+            <version>${spring-5.2.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-beans</artifactId>
+            <version>${spring-5.2.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-context</artifactId>
+            <version>${spring-5.2.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-tx</artifactId>
+            <version>${spring-5.2.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>commons-lang</groupId>
+            <artifactId>commons-lang</artifactId>
+            <version>${commons.lang.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-core</artifactId>
+            <version>${ignite.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-tools</artifactId>
+            <version>${ignite.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/IgniteRepository.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/IgniteRepository.java
new file mode 100644
index 0000000..a8c8c6f
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/IgniteRepository.java
@@ -0,0 +1,109 @@
+/*
+ * 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.springdata22.repository;
+
+import java.io.Serializable;
+import java.util.Map;
+import javax.cache.expiry.ExpiryPolicy;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.data.repository.CrudRepository;
+
+/**
+ * Apache Ignite repository that extends basic capabilities of {@link CrudRepository}.
+ *
+ * @param <V> the cache value type
+ * @param <K> the cache key type
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+public interface IgniteRepository<V, K extends Serializable> extends CrudRepository<V, K> {
+    /**
+     * Returns the Ignite instance bound to the repository
+     *
+     * @return the Ignite instance bound to the repository
+     */
+    public Ignite ignite();
+
+    /**
+     * Returns the Ignite Cache bound to the repository
+     *
+     * @return the Ignite Cache bound to the repository
+     */
+    public IgniteCache<K, V> cache();
+
+    /**
+     * Saves a given entity using provided key.
+     * </p>
+     * It's suggested to use this method instead of default {@link CrudRepository#save(Object)} that generates IDs
+     * (keys) that are not unique cluster wide.
+     *
+     * @param <S>    Entity type.
+     * @param key    Entity's key.
+     * @param entity Entity to save.
+     * @return Saved entity.
+     */
+    public <S extends V> S save(K key, S entity);
+
+    /**
+     * Saves all given keys and entities combinations.
+     * </p>
+     * It's suggested to use this method instead of default {@link CrudRepository#save(Object)} that generates IDs
+     * (keys) that are not unique cluster wide.
+     *
+     * @param <S>      Type of entities.
+     * @param entities Map of key-entities pairs to save.
+     * @return Saved entities.
+     */
+    public <S extends V> Iterable<S> save(Map<K, S> entities);
+
+    /**
+     * Saves a given entity using provided key with expiry policy
+     * </p>
+     * It's suggested to use this method instead of default {@link CrudRepository#save(Object)} that generates IDs
+     * (keys) that are not unique cluster wide.
+     *
+     * @param <S>       Entity type.
+     * @param key       Entity's key.
+     * @param entity    Entity to save.
+     * @param expiryPlc ExpiryPolicy to apply, if not null.
+     * @return Saved entity.
+     */
+    public <S extends V> S save(K key, S entity, @Nullable ExpiryPolicy expiryPlc);
+
+    /**
+     * Saves all given keys and entities combinations with expiry policy
+     * </p>
+     * It's suggested to use this method instead of default {@link CrudRepository#save(Object)} that generates IDs
+     * (keys) that are not unique cluster wide.
+     *
+     * @param <S>       Type of entities.
+     * @param entities  Map of key-entities pairs to save.
+     * @param expiryPlc ExpiryPolicy to apply, if not null.
+     * @return Saved entities.
+     */
+    public <S extends V> Iterable<S> save(Map<K, S> entities, @Nullable ExpiryPolicy expiryPlc);
+
+    /**
+     * Deletes all the entities for the provided ids.
+     *
+     * @param ids List of ids to delete.
+     */
+    public void deleteAllById(Iterable<K> ids);
+
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/DynamicQueryConfig.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/DynamicQueryConfig.java
new file mode 100644
index 0000000..f915267
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/DynamicQueryConfig.java
@@ -0,0 +1,348 @@
+/*
+ * 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.springdata22.repository.config;
+
+/**
+ * Runtime Dynamic query configuration.
+ * <p>
+ * Can be used as special repository method parameter to provide at runtime:
+ * <ol>
+ * <li>Dynamic query string (requires {@link Query#dynamicQuery()} == true)
+ * <li>Ignite query tuning*
+ * </ol>
+ * <p>
+ * * Please, note that {@link Query} annotation parameters will be ignored in favor of those defined in
+ * {@link DynamicQueryConfig} parameter if present.
+ *
+ * @author Manuel Núñez Sánchez (manuel.nunez@hawkore.com)
+ */
+public class DynamicQueryConfig {
+    /** */
+    private String value = "";
+
+    /** */
+    private boolean textQuery;
+
+    /** */
+    private boolean forceFieldsQry;
+
+    /** */
+    private boolean collocated;
+
+    /** */
+    private int timeout;
+
+    /** */
+    private boolean enforceJoinOrder;
+
+    /** */
+    private boolean distributedJoins;
+
+    /** */
+    private boolean lazy;
+
+    /** */
+    private boolean local;
+
+    /** */
+    private int[] parts;
+
+    /** */
+    private int limit;
+
+    /**
+     * From Query annotation.
+     *
+     * @param queryConfiguration the query configuration
+     * @return the dynamic query config
+     */
+    public static DynamicQueryConfig fromQueryAnnotation(Query queryConfiguration) {
+        DynamicQueryConfig config = new DynamicQueryConfig();
+        if (queryConfiguration != null) {
+            config.value = queryConfiguration.value();
+            config.collocated = queryConfiguration.collocated();
+            config.timeout = queryConfiguration.timeout();
+            config.enforceJoinOrder = queryConfiguration.enforceJoinOrder();
+            config.distributedJoins = queryConfiguration.distributedJoins();
+            config.lazy = queryConfiguration.lazy();
+            config.parts = queryConfiguration.parts();
+            config.local = queryConfiguration.local();
+            config.limit = queryConfiguration.limit();
+        }
+        return config;
+    }
+
+    /**
+     * Query text string.
+     *
+     * @return the string
+     */
+    public String value() {
+        return value;
+    }
+
+    /**
+     * Whether must use TextQuery search.
+     *
+     * @return the boolean
+     */
+    public boolean textQuery() {
+        return textQuery;
+    }
+
+    /**
+     * Force SqlFieldsQuery type, deactivating auto-detection based on SELECT statement. Useful for non SELECT
+     * statements or to not return hidden fields on SELECT * statements.
+     *
+     * @return the boolean
+     */
+    public boolean forceFieldsQuery() {
+        return forceFieldsQry;
+    }
+
+    /**
+     * Sets flag defining if this query is collocated.
+     * <p>
+     * Collocation flag is used for optimization purposes of queries with GROUP BY statements. Whenever Ignite executes
+     * a distributed query, it sends sub-queries to individual cluster members. If you know in advance that the elements
+     * of your query selection are collocated together on the same node and you group by collocated key (primary or
+     * affinity key), then Ignite can make significant performance and network optimizations by grouping data on remote
+     * nodes.
+     *
+     * <p>
+     * Only applicable to SqlFieldsQuery
+     *
+     * @return the boolean
+     */
+    public boolean collocated() {
+        return collocated;
+    }
+
+    /**
+     * Query timeout in millis. Sets the query execution timeout. Query will be automatically cancelled if the execution
+     * timeout is exceeded. Zero value disables timeout
+     *
+     * <p>
+     * Only applicable to SqlFieldsQuery and SqlQuery
+     *
+     * @return the int
+     */
+    public int timeout() {
+        return timeout;
+    }
+
+    /**
+     * Sets flag to enforce join order of tables in the query. If set to {@code true} query optimizer will not reorder
+     * tables in join. By default is {@code false}.
+     * <p>
+     * It is not recommended to enable this property until you are sure that your indexes and the query itself are
+     * correct and tuned as much as possible but query optimizer still produces wrong join order.
+     *
+     * <p>
+     * Only applicable to SqlFieldsQuery
+     *
+     * @return the boolean
+     */
+    public boolean enforceJoinOrder() {
+        return enforceJoinOrder;
+    }
+
+    /**
+     * Specify if distributed joins are enabled for this query.
+     * <p>
+     * Only applicable to SqlFieldsQuery and SqlQuery
+     *
+     * @return the boolean
+     */
+    public boolean distributedJoins() {
+        return distributedJoins;
+    }
+
+    /**
+     * Sets lazy query execution flag.
+     * <p>
+     * By default Ignite attempts to fetch the whole query result set to memory and send it to the client. For small and
+     * medium result sets this provides optimal performance and minimize duration of internal database locks, thus
+     * increasing concurrency.
+     * <p>
+     * If result set is too big to fit in available memory this could lead to excessive GC pauses and even
+     * OutOfMemoryError. Use this flag as a hint for Ignite to fetch result set lazily, thus minimizing memory
+     * consumption at the cost of moderate performance hit.
+     * <p>
+     * Defaults to {@code false}, meaning that the whole result set is fetched to memory eagerly.
+     * <p>
+     * Only applicable to SqlFieldsQuery
+     *
+     * @return the boolean
+     */
+    public boolean lazy() {
+        return lazy;
+    }
+
+    /**
+     * Sets whether this query should be executed on local node only.
+     *
+     * @return the boolean
+     */
+    public boolean local() {
+        return local;
+    }
+
+    /**
+     * Sets partitions for a query. The query will be executed only on nodes which are primary for specified
+     * partitions.
+     * <p>
+     * Note what passed array'll be sorted in place for performance reasons, if it wasn't sorted yet.
+     * <p>
+     * Only applicable to SqlFieldsQuery and SqlQuery
+     *
+     * @return the int [ ]
+     */
+    public int[] parts() {
+        return parts;
+    }
+
+    /**
+     * Gets limit to response records count for TextQuery. If 0 or less, considered to be no limit.
+     *
+     * @return Limit value.
+     */
+    public int limit() {
+        return limit;
+    }
+
+    /**
+     * Sets value.
+     *
+     * @param value the value
+     * @return this for chaining
+     */
+    public DynamicQueryConfig setValue(String value) {
+        this.value = value;
+        return this;
+    }
+
+    /**
+     * Sets text query.
+     *
+     * @param textQuery the text query
+     * @return this for chaining
+     */
+    public DynamicQueryConfig setTextQuery(boolean textQuery) {
+        this.textQuery = textQuery;
+        return this;
+    }
+
+    /**
+     * Sets force fields query.
+     *
+     * @param forceFieldsQuery the force fields query
+     * @return this for chaining
+     */
+    public DynamicQueryConfig setForceFieldsQuery(boolean forceFieldsQuery) {
+        forceFieldsQry = forceFieldsQuery;
+        return this;
+    }
+
+    /**
+     * Sets collocated.
+     *
+     * @param collocated the collocated
+     * @return this for chaining
+     */
+    public DynamicQueryConfig setCollocated(boolean collocated) {
+        this.collocated = collocated;
+        return this;
+    }
+
+    /**
+     * Sets timeout.
+     *
+     * @param timeout the timeout
+     * @return this for chaining
+     */
+    public DynamicQueryConfig setTimeout(int timeout) {
+        this.timeout = timeout;
+        return this;
+    }
+
+    /**
+     * Sets enforce join order.
+     *
+     * @param enforceJoinOrder the enforce join order
+     * @return this for chaining
+     */
+    public DynamicQueryConfig setEnforceJoinOrder(boolean enforceJoinOrder) {
+        this.enforceJoinOrder = enforceJoinOrder;
+        return this;
+    }
+
+    /**
+     * Sets distributed joins.
+     *
+     * @param distributedJoins the distributed joins
+     * @return this for chaining
+     */
+    public DynamicQueryConfig setDistributedJoins(boolean distributedJoins) {
+        this.distributedJoins = distributedJoins;
+        return this;
+    }
+
+    /**
+     * Sets lazy.
+     *
+     * @param lazy the lazy
+     * @return this for chaining
+     */
+    public DynamicQueryConfig setLazy(boolean lazy) {
+        this.lazy = lazy;
+        return this;
+    }
+
+    /**
+     * Sets local.
+     *
+     * @param local the local
+     * @return this for chaining
+     */
+    public DynamicQueryConfig setLocal(boolean local) {
+        this.local = local;
+        return this;
+    }
+
+    /**
+     * Sets parts.
+     *
+     * @param parts the parts
+     * @return this for chaining
+     */
+    public DynamicQueryConfig setParts(int[] parts) {
+        this.parts = parts;
+        return this;
+    }
+
+    /**
+     * Sets limit to response records count for TextQuery.
+     *
+     * @param limit If 0 or less, considered to be no limit.
+     * @return {@code this} For chaining.
+     */
+    public DynamicQueryConfig setLimit(int limit) {
+        this.limit = limit;
+        return this;
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/EnableIgniteRepositories.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/EnableIgniteRepositories.java
new file mode 100644
index 0000000..ad465c3
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/EnableIgniteRepositories.java
@@ -0,0 +1,120 @@
+/*
+ * 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.springdata22.repository.config;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.apache.ignite.springdata22.repository.support.IgniteRepositoryFactoryBean;
+import org.apache.ignite.springdata22.repository.support.IgniteRepositoryImpl;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.context.annotation.ComponentScan.Filter;
+import org.springframework.context.annotation.Import;
+import org.springframework.data.repository.query.QueryLookupStrategy;
+import org.springframework.data.repository.query.QueryLookupStrategy.Key;
+
+/**
+ * Annotation to activate Apache Ignite repositories. If no base package is configured through either {@link #value()},
+ * {@link #basePackages()} or {@link #basePackageClasses()} it will trigger scanning of the package of annotated class.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+@Import(IgniteRepositoriesRegistar.class)
+public @interface EnableIgniteRepositories {
+    /**
+     * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.:
+     * {@code @EnableIgniteRepositories("org.my.pkg")} instead of
+     * {@code @EnableIgniteRepositories(basePackages="org.my.pkg")}.
+     */
+    String[] value() default {};
+
+    /**
+     * Base packages to scan for annotated components. {@link #value()} is an alias for (and mutually exclusive with)
+     * this attribute. Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names.
+     */
+    String[] basePackages() default {};
+
+    /**
+     * Type-safe alternative to {@link #basePackages()} for specifying the packages to scan for annotated components.
+     * The package of each class specified will be scanned. Consider creating a special no-op marker class or interface
+     * in each package that serves no purpose other than being referenced by this attribute.
+     */
+    Class<?>[] basePackageClasses() default {};
+
+    /**
+     * Specifies which types are not eligible for component scanning.
+     */
+    Filter[] excludeFilters() default {};
+
+    /**
+     * Specifies which types are eligible for component scanning. Further narrows the set of candidate components from
+     * everything in {@link #basePackages()} to everything in the base packages that matches the given filter or
+     * filters.
+     */
+    Filter[] includeFilters() default {};
+
+    /**
+     * Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So
+     * for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning
+     * for {@code PersonRepositoryImpl}.
+     *
+     * @return Postfix to be used when looking up custom repository implementations.
+     */
+    String repositoryImplementationPostfix() default "Impl";
+
+    /**
+     * Configures the location of where to find the Spring Data named queries properties file.
+     *
+     * @return Location of where to find the Spring Data named queries properties file.
+     */
+    String namedQueriesLocation() default "";
+
+    /**
+     * Returns the key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods. Defaults to
+     * {@link Key#CREATE_IF_NOT_FOUND}.
+     *
+     * @return Key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods.
+     */
+    Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND;
+
+    /**
+     * Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to {@link
+     * IgniteRepositoryFactoryBean}.
+     *
+     * @return {@link FactoryBean} class to be used for each repository instance.
+     */
+    Class<?> repositoryFactoryBeanClass() default IgniteRepositoryFactoryBean.class;
+
+    /**
+     * Configure the repository base class to be used to create repository proxies for this particular configuration.
+     *
+     * @return Repository base class to be used to create repository proxies for this particular configuration.
+     */
+    Class<?> repositoryBaseClass() default IgniteRepositoryImpl.class;
+
+    /**
+     * Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the
+     * repositories infrastructure.
+     */
+    boolean considerNestedRepositories() default false;
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/IgniteRepositoriesRegistar.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/IgniteRepositoriesRegistar.java
new file mode 100644
index 0000000..ddecd77
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/IgniteRepositoriesRegistar.java
@@ -0,0 +1,37 @@
+/*
+ * 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.springdata22.repository.config;
+
+import java.lang.annotation.Annotation;
+
+import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport;
+import org.springframework.data.repository.config.RepositoryConfigurationExtension;
+
+/**
+ * Apache Ignite specific implementation of {@link RepositoryBeanDefinitionRegistrarSupport}.
+ */
+public class IgniteRepositoriesRegistar extends RepositoryBeanDefinitionRegistrarSupport {
+    /** {@inheritDoc} */
+    @Override protected Class<? extends Annotation> getAnnotation() {
+        return EnableIgniteRepositories.class;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected RepositoryConfigurationExtension getExtension() {
+        return new IgniteRepositoryConfigurationExtension();
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/IgniteRepositoryConfigurationExtension.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/IgniteRepositoryConfigurationExtension.java
new file mode 100644
index 0000000..4989196
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/IgniteRepositoryConfigurationExtension.java
@@ -0,0 +1,49 @@
+/*
+ * 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.springdata22.repository.config;
+
+import java.util.Collection;
+import java.util.Collections;
+import org.apache.ignite.springdata22.repository.IgniteRepository;
+import org.apache.ignite.springdata22.repository.support.IgniteRepositoryFactoryBean;
+import org.springframework.data.repository.config.RepositoryConfigurationExtension;
+import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport;
+
+/**
+ * Apache Ignite specific implementation of {@link RepositoryConfigurationExtension}.
+ */
+public class IgniteRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport {
+    /** {@inheritDoc} */
+    @Override public String getModuleName() {
+        return "Apache Ignite";
+    }
+
+    /** {@inheritDoc} */
+    @Override protected String getModulePrefix() {
+        return "ignite";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getRepositoryFactoryBeanClassName() {
+        return IgniteRepositoryFactoryBean.class.getName();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected Collection<Class<?>> getIdentifyingTypes() {
+        return Collections.singleton(IgniteRepository.class);
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/Query.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/Query.java
new file mode 100644
index 0000000..d1af467
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/Query.java
@@ -0,0 +1,136 @@
+/*
+ * 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.springdata22.repository.config;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to provide a user defined query for a method.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface Query {
+    /**
+     * Query text string. If not provided, Ignite query generator for Spring Data framework will be used to generate one
+     * (only if textQuery = false (default))
+     */
+    String value() default "";
+
+    /**
+     * Whether annotated repository method must use TextQuery search.
+     */
+    boolean textQuery() default false;
+
+    /**
+     * Force SqlFieldsQuery type, deactivating auto-detection based on SELECT statement. Useful for non SELECT
+     * statements or to not return hidden fields on SELECT * statements.
+     */
+    boolean forceFieldsQuery() default false;
+
+    /**
+     * Sets flag defining if this query is collocated.
+     * <p>
+     * Collocation flag is used for optimization purposes of queries with GROUP BY statements. Whenever Ignite executes
+     * a distributed query, it sends sub-queries to individual cluster members. If you know in advance that the elements
+     * of your query selection are collocated together on the same node and you group by collocated key (primary or
+     * affinity key), then Ignite can make significant performance and network optimizations by grouping data on remote
+     * nodes.
+     *
+     * <p>
+     * Only applicable to SqlFieldsQuery
+     */
+    boolean collocated() default false;
+
+    /**
+     * Query timeout in millis. Sets the query execution timeout. Query will be automatically cancelled if the execution
+     * timeout is exceeded. Zero value disables timeout
+     *
+     * <p>
+     * Only applicable to SqlFieldsQuery and SqlQuery
+     */
+    int timeout() default 0;
+
+    /**
+     * Sets flag to enforce join order of tables in the query. If set to {@code true} query optimizer will not reorder
+     * tables in join. By default is {@code false}.
+     * <p>
+     * It is not recommended to enable this property until you are sure that your indexes and the query itself are
+     * correct and tuned as much as possible but query optimizer still produces wrong join order.
+     *
+     * <p>
+     * Only applicable to SqlFieldsQuery
+     */
+    boolean enforceJoinOrder() default false;
+
+    /**
+     * Specify if distributed joins are enabled for this query.
+     * <p>
+     * Only applicable to SqlFieldsQuery and SqlQuery
+     */
+    boolean distributedJoins() default false;
+
+    /**
+     * Sets lazy query execution flag.
+     * <p>
+     * By default Ignite attempts to fetch the whole query result set to memory and send it to the client. For small and
+     * medium result sets this provides optimal performance and minimize duration of internal database locks, thus
+     * increasing concurrency.
+     * <p>
+     * If result set is too big to fit in available memory this could lead to excessive GC pauses and even
+     * OutOfMemoryError. Use this flag as a hint for Ignite to fetch result set lazily, thus minimizing memory
+     * consumption at the cost of moderate performance hit.
+     * <p>
+     * Defaults to {@code false}, meaning that the whole result set is fetched to memory eagerly.
+     * <p>
+     * Only applicable to SqlFieldsQuery
+     */
+    boolean lazy() default false;
+
+    /**
+     * Sets whether this query should be executed on local node only.
+     */
+    boolean local() default false;
+
+    /**
+     * Sets partitions for a query. The query will be executed only on nodes which are primary for specified
+     * partitions.
+     * <p>
+     * Note what passed array'll be sorted in place for performance reasons, if it wasn't sorted yet.
+     * <p>
+     * Only applicable to SqlFieldsQuery and SqlQuery
+     */
+    int[] parts() default {};
+
+    /**
+     * Specify whether the annotated method must provide a non null {@link DynamicQueryConfig} parameter with a non
+     * empty value (query string) or {@link DynamicQueryConfig#textQuery()} == true.
+     * <p>
+     * Please, note that  {@link DynamicQueryConfig#textQuery()} annotation parameters will be ignored in favor of those
+     * defined in {@link DynamicQueryConfig} parameter if present (runtime ignite query tuning).
+     */
+    boolean dynamicQuery() default false;
+
+    /**
+     * Sets limit to response records count for TextQuery. If 0 or less, considered to be no limit.
+     */
+    int limit() default 0;
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/RepositoryConfig.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/RepositoryConfig.java
new file mode 100644
index 0000000..6699ed6
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/RepositoryConfig.java
@@ -0,0 +1,77 @@
+/*
+ * 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.springdata22.repository.config;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.configuration.IgniteConfiguration;
+
+/**
+ * The annotation can be used to pass Ignite specific parameters to a bound repository.
+ *
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface RepositoryConfig {
+    /**
+     * Cache name string.
+     *
+     * @return A name of a distributed Apache Ignite cache an annotated repository will be mapped to.
+     */
+    String cacheName() default "";
+
+    /**
+     * Ignite instance string. Default "igniteInstance".
+     *
+     * @return {@link Ignite} instance spring bean name
+     */
+    String igniteInstance() default "igniteInstance";
+
+    /**
+     * Ignite cfg string. Default "igniteCfg".
+     *
+     * @return {@link IgniteConfiguration} spring bean name
+     */
+    String igniteCfg() default "igniteCfg";
+
+    /**
+     * Ignite spring cfg path string. Default "igniteSpringCfgPath".
+     *
+     * @return A path to Ignite's Spring XML configuration spring bean name
+     */
+    String igniteSpringCfgPath() default "igniteSpringCfgPath";
+
+    /**
+     * Auto create cache. Default false to enforce control over cache creation and to avoid cache creation by mistake
+     * <p>
+     * Tells to Ignite Repository factory wether cache should be auto created if not exists.
+     *
+     * @return the boolean
+     */
+    boolean autoCreateCache() default false;
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/package-info.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/package-info.java
new file mode 100644
index 0000000..4e3203b
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/config/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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 description. -->
+ * Package includes Spring Data integration related configuration files.
+ */
+package org.apache.ignite.springdata22.repository.config;
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/package-info.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/package-info.java
new file mode 100644
index 0000000..2a865f5
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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 description. -->
+ * Package contains Apache Ignite Spring Data integration.
+ */
+package org.apache.ignite.springdata22.repository;
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/DeclaredQuery.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/DeclaredQuery.java
new file mode 100644
index 0000000..74ab2cc
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/DeclaredQuery.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2018-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      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.springdata22.repository.query;
+
+import java.util.List;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.util.StringUtils;
+
+/**
+ * A wrapper for a String representation of a query offering information about the query.
+ *
+ * @author Jens Schauder
+ */
+interface DeclaredQuery {
+    /**
+     * Creates a {@literal DeclaredQuery} from a query {@literal String}.
+     *
+     * @param qry might be {@literal null} or empty.
+     * @return a {@literal DeclaredQuery} instance even for a {@literal null} or empty argument.
+     */
+    public static DeclaredQuery of(@Nullable String qry) {
+        return StringUtils.isEmpty(qry) ? EmptyDeclaredQuery.EMPTY_QUERY : new StringQuery(qry);
+    }
+
+    /**
+     * @return whether the underlying query has at least one named parameter.
+     */
+    public boolean hasNamedParameter();
+
+    /**
+     * Returns the query string.
+     */
+    public String getQueryString();
+
+    /**
+     * Returns the main alias used in the query.
+     *
+     * @return the alias
+     */
+    @Nullable
+    public String getAlias();
+
+    /**
+     * Returns whether the query is using a constructor expression.
+     */
+    public boolean hasConstructorExpression();
+
+    /**
+     * Returns whether the query uses the default projection, i.e. returns the main alias defined for the query.
+     */
+    public boolean isDefaultProjection();
+
+    /**
+     * Returns the {@link StringQuery.ParameterBinding}s registered.
+     */
+    public List<StringQuery.ParameterBinding> getParameterBindings();
+
+    /**
+     * Creates a new {@literal DeclaredQuery} representing a count query, i.e. a query returning the number of rows to
+     * be expected from the original query, either derived from the query wrapped by this instance or from the
+     * information passed as arguments.
+     *
+     * @param cntQry           an optional query string to be used if present.
+     * @param cntQryProjection an optional return type for the query.
+     * @return a new {@literal DeclaredQuery} instance.
+     */
+    public DeclaredQuery deriveCountQuery(@Nullable String cntQry, @Nullable String cntQryProjection);
+
+    /**
+     * @return whether paging is implemented in the query itself, e.g. using SpEL expressions.
+     */
+    public default boolean usesPaging() {
+        return false;
+    }
+
+    /**
+     * Returns whether the query uses JDBC style parameters, i.e. parameters denoted by a simple ? without any index or
+     * name.
+     *
+     * @return Whether the query uses JDBC style parameters.
+     */
+    public boolean usesJdbcStyleParameters();
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/EmptyDeclaredQuery.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/EmptyDeclaredQuery.java
new file mode 100644
index 0000000..7803537
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/EmptyDeclaredQuery.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2018-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      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.springdata22.repository.query;
+
+import java.util.Collections;
+import java.util.List;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * NULL-Object pattern implementation for {@link DeclaredQuery}.
+ *
+ * @author Jens Schauder
+ */
+class EmptyDeclaredQuery implements DeclaredQuery {
+    /**
+     * An implementation implementing the NULL-Object pattern for situations where there is no query.
+     */
+    static final DeclaredQuery EMPTY_QUERY = new EmptyDeclaredQuery();
+
+    /** {@inheritDoc} */
+    @Override public boolean hasNamedParameter() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getQueryString() {
+        return "";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getAlias() {
+        return null;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean hasConstructorExpression() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean isDefaultProjection() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override public List<StringQuery.ParameterBinding> getParameterBindings() {
+        return Collections.emptyList();
+    }
+
+    /** {@inheritDoc} */
+    @Override public DeclaredQuery deriveCountQuery(@Nullable String cntQry, @Nullable String cntQryProjection) {
+        Assert.hasText(cntQry, "CountQuery must not be empty!");
+        return DeclaredQuery.of(cntQry);
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean usesJdbcStyleParameters() {
+        return false;
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/ExpressionBasedStringQuery.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/ExpressionBasedStringQuery.java
new file mode 100644
index 0000000..bdbd81d
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/ExpressionBasedStringQuery.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      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.springdata22.repository.query;
+
+import java.util.regex.Pattern;
+import org.springframework.data.repository.core.EntityMetadata;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.expression.Expression;
+import org.springframework.expression.ParserContext;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.util.Assert;
+
+/**
+ * Extension of {@link StringQuery} that evaluates the given query string as a SpEL template-expression.
+ * <p>
+ * Currently the following template variables are available:
+ * <ol>
+ * <li>{@code #entityName} - the simple class name of the given entity</li>
+ * <ol>
+ *
+ * @author Thomas Darimont
+ * @author Oliver Gierke
+ * @author Tom Hombergs
+ */
+class ExpressionBasedStringQuery extends StringQuery {
+    /**
+     * Expression parameter.
+     */
+    private static final String EXPRESSION_PARAMETER = "?#{";
+
+    /**
+     * Quoted expression parameter.
+     */
+    private static final String QUOTED_EXPRESSION_PARAMETER = "?__HASH__{";
+
+    /**
+     * Expression parameter quoting.
+     */
+    private static final Pattern EXPRESSION_PARAMETER_QUOTING = Pattern.compile(Pattern.quote(EXPRESSION_PARAMETER));
+
+    /**
+     * Expression parameter unquoting.
+     */
+    private static final Pattern EXPRESSION_PARAMETER_UNQUOTING = Pattern.compile(
+        Pattern.quote(QUOTED_EXPRESSION_PARAMETER));
+
+    /**
+     * Entity name.
+     */
+    private static final String ENTITY_NAME = "entityName";
+
+    /**
+     * Entity name variable.
+     */
+    private static final String ENTITY_NAME_VARIABLE = "#" + ENTITY_NAME;
+
+    /**
+     * Entity name variable expression.
+     */
+    private static final String ENTITY_NAME_VARIABLE_EXPRESSION = "#{" + ENTITY_NAME_VARIABLE + "}";
+
+    /**
+     * Creates a new instance for the given query and {@link EntityMetadata}.
+     *
+     * @param qry      must not be {@literal null} or empty.
+     * @param metadata must not be {@literal null}.
+     * @param parser   must not be {@literal null}.
+     */
+    public ExpressionBasedStringQuery(String qry, RepositoryMetadata metadata, SpelExpressionParser parser) {
+        super(renderQueryIfExpressionOrReturnQuery(qry, metadata, parser));
+    }
+
+    /**
+     * Creates an instance from a given {@link DeclaredQuery}.
+     *
+     * @param qry      the original query. Must not be {@literal null}.
+     * @param metadata the {@link RepositoryMetadata} for the given entity. Must not be {@literal null}.
+     * @param parser   Parser for resolving SpEL expressions. Must not be {@literal null}.
+     * @return A query supporting SpEL expressions.
+     */
+    static ExpressionBasedStringQuery from(DeclaredQuery qry,
+        RepositoryMetadata metadata,
+        SpelExpressionParser parser) {
+        return new ExpressionBasedStringQuery(qry.getQueryString(), metadata, parser);
+    }
+
+    /**
+     * @param qry,     the query expression potentially containing a SpEL expression. Must not be {@literal null}.}
+     * @param metadata the {@link RepositoryMetadata} for the given entity. Must not be {@literal null}.
+     * @param parser   Must not be {@literal null}.
+     * @return rendered query
+     */
+    private static String renderQueryIfExpressionOrReturnQuery(String qry,
+        RepositoryMetadata metadata,
+        SpelExpressionParser parser) {
+
+        Assert.notNull(qry, "query must not be null!");
+        Assert.notNull(metadata, "metadata must not be null!");
+        Assert.notNull(parser, "parser must not be null!");
+
+        if (!containsExpression(qry))
+            return qry;
+
+        StandardEvaluationContext evalCtx = new StandardEvaluationContext();
+        evalCtx.setVariable(ENTITY_NAME, metadata.getDomainType().getSimpleName());
+
+        qry = potentiallyQuoteExpressionsParameter(qry);
+
+        Expression expr = parser.parseExpression(qry, ParserContext.TEMPLATE_EXPRESSION);
+
+        String result = expr.getValue(evalCtx, String.class);
+
+        if (result == null)
+            return qry;
+
+        return potentiallyUnquoteParameterExpressions(result);
+    }
+
+    /**
+     * @param result Result.
+     */
+    private static String potentiallyUnquoteParameterExpressions(String result) {
+        return EXPRESSION_PARAMETER_UNQUOTING.matcher(result).replaceAll(EXPRESSION_PARAMETER);
+    }
+
+    /**
+     * @param qry Query.
+     */
+    private static String potentiallyQuoteExpressionsParameter(String qry) {
+        return EXPRESSION_PARAMETER_QUOTING.matcher(qry).replaceAll(QUOTED_EXPRESSION_PARAMETER);
+    }
+
+    /**
+     * @param qry Query.
+     */
+    private static boolean containsExpression(String qry) {
+        return qry.contains(ENTITY_NAME_VARIABLE_EXPRESSION);
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/IgniteQuery.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/IgniteQuery.java
new file mode 100644
index 0000000..b045a4f
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/IgniteQuery.java
@@ -0,0 +1,134 @@
+/*
+ * 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.springdata22.repository.query;
+
+import org.apache.ignite.internal.util.typedef.internal.S;
+
+/**
+ * Ignite query helper class. For internal use only.
+ *
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+public class IgniteQuery {
+    /** */
+    enum Option {
+        /** Query will be used with Sort object. */
+        SORTING,
+
+        /** Query will be used with Pageable object. */
+        PAGINATION,
+
+        /** No advanced option. */
+        NONE
+    }
+
+    /**
+     * Query text string.
+     */
+    private final String qrySql;
+
+    /**
+     * Whether this is a SQL fields query
+     */
+    private final boolean isFieldQuery;
+
+    /**
+     * Whether this is Text query
+     */
+    private final boolean isTextQuery;
+
+    /**
+     * Whether was autogenerated (by method name)
+     */
+    private final boolean isAutogenerated;
+
+    /**
+     * Type of option.
+     */
+    private final Option option;
+
+    /**
+     * @param qrySql          the query string.
+     * @param isFieldQuery    Is field query.
+     * @param isTextQuery     Is a TextQuery
+     * @param isAutogenerated query was autogenerated
+     * @param option          Option.
+     */
+    public IgniteQuery(String qrySql,
+        boolean isFieldQuery,
+        boolean isTextQuery,
+        boolean isAutogenerated,
+        Option option) {
+        this.qrySql = qrySql;
+        this.isFieldQuery = isFieldQuery;
+        this.isTextQuery = isTextQuery;
+        this.isAutogenerated = isAutogenerated;
+        this.option = option;
+    }
+
+    /**
+     * Text string of the query.
+     *
+     * @return SQL query text string.
+     */
+    public String qryStr() {
+        return qrySql;
+    }
+
+    /**
+     * Returns {@code true} if it's Ignite SQL fields query, {@code false} otherwise.
+     *
+     * @return {@code true} if it's Ignite SQL fields query, {@code false} otherwise.
+     */
+    public boolean isFieldQuery() {
+        return isFieldQuery;
+    }
+
+    /**
+     * Returns {@code true} if it's Ignite Text query, {@code false} otherwise.
+     *
+     * @return {@code true} if it's Ignite Text query, {@code false} otherwise.
+     */
+    public boolean isTextQuery() {
+        return isTextQuery;
+    }
+
+    /**
+     * Returns {@code true} if it's autogenerated, {@code false} otherwise.
+     *
+     * @return {@code true} if it's autogenerated, {@code false} otherwise.
+     */
+    public boolean isAutogenerated() {
+        return isAutogenerated;
+    }
+
+    /**
+     * Advanced querying option.
+     *
+     * @return querying option.
+     */
+    public Option options() {
+        return option;
+    }
+
+    /** */
+    @Override public String toString() {
+        return S.toString(IgniteQuery.class, this);
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/IgniteQueryGenerator.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/IgniteQueryGenerator.java
new file mode 100644
index 0000000..a0be11d
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/IgniteQueryGenerator.java
@@ -0,0 +1,276 @@
+/*
+ * 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.springdata22.repository.query;
+
+import java.lang.reflect.Method;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mapping.PropertyPath;
+import org.springframework.data.mapping.PropertyReferenceException;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.query.parser.Part;
+import org.springframework.data.repository.query.parser.PartTree;
+
+/**
+ * Ignite query generator for Spring Data framework.
+ */
+public class IgniteQueryGenerator {
+    /** */
+    private IgniteQueryGenerator() {
+        // No-op.
+    }
+
+    /**
+     * @param mtd      Method.
+     * @param metadata Metadata.
+     * @return Generated ignite query.
+     */
+    public static IgniteQuery generateSql(Method mtd, RepositoryMetadata metadata) {
+        PartTree parts;
+
+        try {
+            parts = new PartTree(mtd.getName(), metadata.getDomainType());
+        }
+        catch (PropertyReferenceException e) {
+            parts = new PartTree(mtd.getName(), metadata.getIdType());
+        }
+
+        boolean isCountOrFieldQuery = parts.isCountProjection();
+
+        StringBuilder sql = new StringBuilder();
+
+        if (parts.isDelete()) {
+            sql.append("DELETE ");
+
+            // For the DML queries aside from SELECT *, they should run over SqlFieldQuery
+            isCountOrFieldQuery = true;
+        }
+        else {
+            sql.append("SELECT ");
+
+            if (parts.isDistinct())
+                throw new UnsupportedOperationException("DISTINCT clause in not supported.");
+
+            if (isCountOrFieldQuery)
+                sql.append("COUNT(1) ");
+            else
+                sql.append("* ");
+        }
+
+        sql.append("FROM ").append(metadata.getDomainType().getSimpleName());
+
+        if (parts.iterator().hasNext()) {
+            sql.append(" WHERE ");
+
+            for (PartTree.OrPart orPart : parts) {
+                sql.append("(");
+
+                for (Part part : orPart) {
+                    handleQueryPart(sql, part, metadata.getDomainType());
+                    sql.append(" AND ");
+                }
+
+                sql.delete(sql.length() - 5, sql.length());
+
+                sql.append(") OR ");
+            }
+
+            sql.delete(sql.length() - 4, sql.length());
+        }
+
+        addSorting(sql, parts.getSort());
+
+        if (parts.isLimiting()) {
+            sql.append(" LIMIT ");
+            sql.append(parts.getMaxResults().intValue());
+        }
+
+        return new IgniteQuery(sql.toString(), isCountOrFieldQuery, false, true, getOptions(mtd));
+    }
+
+    /**
+     * Add a dynamic part of query for the sorting support.
+     *
+     * @param sql  SQL text string.
+     * @param sort Sort method.
+     * @return Sorting criteria in StringBuilder.
+     */
+    public static StringBuilder addSorting(StringBuilder sql, Sort sort) {
+        if (sort != null && sort != Sort.unsorted()) {
+            sql.append(" ORDER BY ");
+
+            for (Sort.Order order : sort) {
+                sql.append(order.getProperty()).append(" ").append(order.getDirection());
+
+                if (order.getNullHandling() != Sort.NullHandling.NATIVE) {
+                    sql.append(" ").append("NULL ");
+
+                    switch (order.getNullHandling()) {
+                        case NULLS_FIRST:
+                            sql.append("FIRST");
+                            break;
+                        case NULLS_LAST:
+                            sql.append("LAST");
+                            break;
+                        default:
+                    }
+                }
+                sql.append(", ");
+            }
+
+            sql.delete(sql.length() - 2, sql.length());
+        }
+
+        return sql;
+    }
+
+    /**
+     * Add a dynamic part of a query for the pagination support.
+     *
+     * @param sql      Builder instance.
+     * @param pageable Pageable instance.
+     * @return Builder instance.
+     */
+    public static StringBuilder addPaging(StringBuilder sql, Pageable pageable) {
+
+        addSorting(sql, pageable.getSort());
+
+        sql.append(" LIMIT ").append(pageable.getPageSize()).append(" OFFSET ").append(pageable.getOffset());
+
+        return sql;
+    }
+
+    /**
+     * Determines whether query is dynamic or not (by list of method parameters)
+     *
+     * @param mtd Method.
+     * @return type of options
+     */
+    public static IgniteQuery.Option getOptions(Method mtd) {
+        IgniteQuery.Option option = IgniteQuery.Option.NONE;
+
+        Class<?>[] types = mtd.getParameterTypes();
+        if (types.length > 0) {
+            Class<?> type = types[types.length - 1];
+
+            if (Sort.class.isAssignableFrom(type))
+                option = IgniteQuery.Option.SORTING;
+            else if (Pageable.class.isAssignableFrom(type))
+                option = IgniteQuery.Option.PAGINATION;
+        }
+
+        for (int i = 0; i < types.length - 1; i++) {
+            Class<?> tp = types[i];
+
+            if (tp == Sort.class || tp == Pageable.class)
+                throw new AssertionError("Sort and Pageable parameters are allowed only in the last position");
+        }
+
+        return option;
+    }
+
+    /**
+     * Check and correct table name if using column name from compound key.
+     */
+    private static String getColumnName(Part part, Class<?> domainType) {
+        PropertyPath prperty = part.getProperty();
+
+        if (prperty.getType() != domainType)
+            return domainType.getSimpleName() + "." + prperty.getSegment();
+        else
+            return part.toString();
+    }
+
+    /**
+     * Transform part to qryStr expression
+     */
+    private static void handleQueryPart(StringBuilder sql, Part part, Class<?> domainType) {
+        sql.append("(");
+
+        sql.append(getColumnName(part, domainType));
+
+        switch (part.getType()) {
+            case SIMPLE_PROPERTY:
+                sql.append("=?");
+                break;
+            case NEGATING_SIMPLE_PROPERTY:
+                sql.append("<>?");
+                break;
+            case GREATER_THAN:
+                sql.append(">?");
+                break;
+            case GREATER_THAN_EQUAL:
+                sql.append(">=?");
+                break;
+            case LESS_THAN:
+                sql.append("<?");
+                break;
+            case LESS_THAN_EQUAL:
+                sql.append("<=?");
+                break;
+            case IS_NOT_NULL:
+                sql.append(" IS NOT NULL");
+                break;
+            case IS_NULL:
+                sql.append(" IS NULL");
+                break;
+            case BETWEEN:
+                sql.append(" BETWEEN ? AND ?");
+                break;
+            case FALSE:
+                sql.append(" = FALSE");
+                break;
+            case TRUE:
+                sql.append(" = TRUE");
+                break;
+            //TODO: review this legacy code, LIKE should be -> LIKE ?
+            case LIKE:
+            case CONTAINING:
+                sql.append(" LIKE '%' || ? || '%'");
+                break;
+            case NOT_CONTAINING:
+                //TODO: review this legacy code, NOT_LIKE should be -> NOT LIKE ?
+            case NOT_LIKE:
+                sql.append(" NOT LIKE '%' || ? || '%'");
+                break;
+            case STARTING_WITH:
+                sql.append(" LIKE  ? || '%'");
+                break;
+            case ENDING_WITH:
+                sql.append(" LIKE '%' || ?");
+                break;
+            case IN:
+                sql.append(" IN ?");
+                break;
+            case NOT_IN:
+                sql.append(" NOT IN ?");
+                break;
+            case REGEX:
+                sql.append(" REGEXP ?");
+                break;
+            case NEAR:
+            case AFTER:
+            case BEFORE:
+            case EXISTS:
+            default:
+                throw new UnsupportedOperationException(part.getType() + " is not supported!");
+        }
+
+        sql.append(")");
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/IgniteRepositoryQuery.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/IgniteRepositoryQuery.java
new file mode 100644
index 0000000..9ab9606
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/IgniteRepositoryQuery.java
@@ -0,0 +1,1043 @@
+/*
+ * 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.springdata22.repository.query;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.AbstractCollection;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.TreeMap;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import javax.cache.Cache;
+import org.apache.commons.lang.ArrayUtils;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.binary.BinaryObjectBuilder;
+import org.apache.ignite.binary.BinaryType;
+import org.apache.ignite.cache.query.Query;
+import org.apache.ignite.cache.query.QueryCursor;
+import org.apache.ignite.cache.query.SqlFieldsQuery;
+import org.apache.ignite.cache.query.SqlQuery;
+import org.apache.ignite.cache.query.TextQuery;
+import org.apache.ignite.internal.GridKernalContext;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.binary.BinaryUtils;
+import org.apache.ignite.internal.processors.cache.CacheEntryImpl;
+import org.apache.ignite.internal.processors.cache.binary.CacheObjectBinaryProcessorImpl;
+import org.apache.ignite.internal.processors.cache.binary.IgniteBinaryImpl;
+import org.apache.ignite.internal.processors.cache.query.QueryCursorEx;
+import org.apache.ignite.internal.processors.query.GridQueryFieldMetadata;
+import org.apache.ignite.internal.processors.query.QueryUtils;
+import org.apache.ignite.internal.util.typedef.internal.U;
+import org.apache.ignite.springdata22.repository.config.DynamicQueryConfig;
+import org.apache.ignite.springdata22.repository.query.StringQuery.ParameterBinding;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.domain.SliceImpl;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.query.Parameter;
+import org.springframework.data.repository.query.Parameters;
+import org.springframework.data.repository.query.QueryMethod;
+import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
+import org.springframework.data.repository.query.RepositoryQuery;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.ParserContext;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.util.StringUtils;
+
+import static org.apache.ignite.springdata22.repository.support.IgniteRepositoryFactory.isFieldQuery;
+
+/**
+ * Ignite query implementation.
+ * <p>
+ * <p>
+ * Features:
+ * <ol>
+ * <li> Supports query tuning parameters</li>
+ * <li> Supports projections</li>
+ * <li> Supports Page and Stream responses</li>
+ * <li> Supports SqlFieldsQuery resultset transformation into the domain entity</li>
+ * <li> Supports named parameters (:myParam) into SQL queries, declared using @Param("myParam") annotation</li>
+ * <li> Supports advanced parameter binding and SpEL expressions into SQL queries
+ * <ol>
+ * <li><b>Template variables</b>:
+ * <ol>
+ * <li>{@code #entityName} - the simple class name of the domain entity</li>
+ * </ol>
+ * </li>
+ * <li><b>Method parameter expressions</b>: Parameters are exposed for indexed access ([0] is the first query method's
+ * param) or via the name declared using @Param. The actual SpEL expression binding is triggered by '?#'. Example:
+ * ?#{[0]} or ?#{#myParamName}</li>
+ * <li><b>Advanced SpEL expressions</b>: While advanced parameter binding is a very useful feature, the real power of
+ * SpEL stems from the fact, that the expressions can refer to framework abstractions or other application components
+ * through SpEL EvaluationContext extension model.</li>
+ * </ol>
+ * Examples:
+ * <pre>
+ * {@code @Query}(value = "SELECT * from #{#entityName} where email = :email")
+ * User searchUserByEmail({@code @Param}("email") String email);
+ *
+ * {@code @Query}(value = "SELECT * from #{#entityName} where country = ?#{[0]} and city = ?#{[1]}")
+ * List<User> searchUsersByCity({@code @Param}("country") String country, {@code @Param}("city") String city,
+ * Pageable pageable);
+ *
+ * {@code @Query}(value = "SELECT * from #{#entityName} where email = ?")
+ * User searchUserByEmail(String email);
+ *
+ * {@code @Query}(value = "SELECT * from #{#entityName} where lucene = ?#{
+ * luceneQueryBuilder.search().refresh(true).filter(luceneQueryBuilder.match('city',#city)).build()}")
+ * List<User> searchUsersByCity({@code @Param}("city") String city, Pageable pageable);
+ * </pre>
+ * </li>
+ * <li> Supports SpEL expressions into Text queries ({@link TextQuery}). Examples:
+ * <pre>
+ * {@code @Query}(textQuery = true, value = "email: #{#email}")
+ * User searchUserByEmail({@code @Param}("email") String email);
+ *
+ * {@code @Query}(textQuery = true, value = "#{#textToSearch}")
+ * List<User> searchUsersByText({@code @Param}("textToSearch") String text, Pageable pageable);
+ *
+ * {@code @Query}(textQuery = true, value = "#{[0]}")
+ * List<User> searchUsersByText(String textToSearch, Pageable pageable);
+ *
+ * {@code @Query}(textQuery = true, value = "#{luceneQueryBuilder.search().refresh(true).filter(luceneQueryBuilder
+ * .match('city', #city)).build()}")
+ * List<User> searchUserByCity({@code @Param}("city") String city, Pageable pageable);
+ * </pre>
+ * </li>
+ * <li> Supports dynamic query and tuning at runtime by using {@link DynamicQueryConfig} method parameter. Examples:
+ * <pre>
+ * {@code @Query}(value = "SELECT * from #{#entityName} where email = :email")
+ * User searchUserByEmailWithQueryTuning({@code @Param}("email") String email, {@code @Param}("ignoredUsedAsQueryTuning") DynamicQueryConfig config);
+ *
+ * {@code @Query}(dynamicQuery = true)
+ * List<User> searchUsersByCityWithDynamicQuery({@code @Param}("country") String country, {@code @Param}("city") String city,
+ * {@code @Param}("ignoredUsedAsDynamicQueryAndTuning") DynamicQueryConfig config, Pageable pageable);
+ *
+ * ...
+ * DynamicQueryConfig onlyTunning = new DynamicQueryConfig().setCollocated(true);
+ * repo.searchUserByEmailWithQueryTuning("user@mail.com", onlyTunning);
+ *
+ * DynamicQueryConfig withDynamicQuery = new DynamicQueryConfig().value("SELECT * from #{#entityName} where country = ?#{[0] and city = ?#{[1]}").setForceFieldsQuery(true).setLazy(true).setCollocated(true);
+ * repo.searchUsersByCityWithDynamicQuery("Spain", "Madrid", withDynamicQuery, new PageRequest(0, 100));
+ *
+ * </pre>
+ * </li>
+ * </ol>
+ *
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+@SuppressWarnings("unchecked")
+public class IgniteRepositoryQuery implements RepositoryQuery {
+    /** */
+    private static final TreeMap<String, Class<?>> binaryFieldClass = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+
+    /**
+     * Defines the way how to process query result
+     */
+    private enum ReturnStrategy {
+        /** Need to return only one value. */
+        ONE_VALUE,
+
+        /** Need to return one cache entry */
+        CACHE_ENTRY,
+
+        /** Need to return list of cache entries */
+        LIST_OF_CACHE_ENTRIES,
+
+        /** Need to return list of values */
+        LIST_OF_VALUES,
+
+        /** Need to return list of lists */
+        LIST_OF_LISTS,
+
+        /** Need to return slice */
+        SLICE_OF_VALUES,
+
+        /** Slice of cache entries */
+        SLICE_OF_CACHE_ENTRIES,
+
+        /** Slice of lists */
+        SLICE_OF_LISTS,
+
+        /** Need to return Page of values */
+        PAGE_OF_VALUES,
+
+        /** Need to return stream of values */
+        STREAM_OF_VALUES,
+    }
+
+    /** */
+    private final Class<?> type;
+
+    /** */
+    private final IgniteQuery staticQuery;
+
+    /** */
+    private final IgniteCache cache;
+
+    /** */
+    private final Ignite ignite;
+
+    /** Required by qryStr field query type for binary manipulation */
+    private IgniteBinaryImpl igniteBinary;
+
+    /** */
+    private BinaryType igniteBinType;
+
+    /** */
+    private final Method mtd;
+
+    /** */
+    private final RepositoryMetadata metadata;
+
+    /** */
+    private final ProjectionFactory factory;
+
+    /** */
+    private final ReturnStrategy staticReturnStgy;
+
+    /** Detect if returned data from method is projected */
+    private final boolean hasProjection;
+
+    /** Whether projection is dynamic (provided as method parameter) */
+    private final boolean hasDynamicProjection;
+
+    /** Dynamic projection parameter index */
+    private final int dynamicProjectionIndex;
+
+    /** Dynamic query configuration */
+    private final int dynamicQueryConfigurationIndex;
+
+    /** The return query method */
+    private final QueryMethod qMethod;
+
+    /** The return domain class of QueryMethod */
+    private final Class<?> returnedDomainClass;
+
+    /** */
+    private final SpelExpressionParser expressionParser;
+
+    /** Could provide ExtensionAwareQueryMethodEvaluationContextProvider */
+    private final QueryMethodEvaluationContextProvider queryMethodEvaluationContextProvider;
+
+    /** Static query configuration. */
+    private final DynamicQueryConfig staticQueryConfiguration;
+
+    /**
+     * Instantiates a new Ignite repository query.
+     *
+     * @param ignite                               the ignite
+     * @param metadata                             Metadata.
+     * @param staticQuery                          Query.
+     * @param mtd                                  Method.
+     * @param factory                              Factory.
+     * @param cache                                Cache.
+     * @param staticQueryConfiguration             the query configuration
+     * @param queryMethodEvaluationContextProvider the query method evaluation context provider
+     */
+    public IgniteRepositoryQuery(Ignite ignite,
+        RepositoryMetadata metadata,
+        @Nullable IgniteQuery staticQuery,
+        Method mtd,
+        ProjectionFactory factory,
+        IgniteCache cache,
+        @Nullable DynamicQueryConfig staticQueryConfiguration,
+        QueryMethodEvaluationContextProvider queryMethodEvaluationContextProvider) {
+        this.metadata = metadata;
+        this.mtd = mtd;
+        this.factory = factory;
+        type = metadata.getDomainType();
+
+        this.cache = cache;
+        this.ignite = ignite;
+
+        this.staticQueryConfiguration = staticQueryConfiguration;
+        this.staticQuery = staticQuery;
+
+        if (this.staticQuery != null)
+            staticReturnStgy = calcReturnType(mtd, this.staticQuery.isFieldQuery());
+        else
+            staticReturnStgy = null;
+
+        expressionParser = new SpelExpressionParser();
+        this.queryMethodEvaluationContextProvider = queryMethodEvaluationContextProvider;
+
+        qMethod = getQueryMethod();
+
+        // control projection
+        hasDynamicProjection = getQueryMethod().getParameters().hasDynamicProjection();
+        hasProjection = hasDynamicProjection || getQueryMethod().getResultProcessor().getReturnedType()
+            .isProjecting();
+
+        dynamicProjectionIndex = qMethod.getParameters().getDynamicProjectionIndex();
+
+        returnedDomainClass = getQueryMethod().getReturnedObjectType();
+
+        dynamicQueryConfigurationIndex = getDynamicQueryConfigurationIndex(qMethod);
+
+        // ensure dynamic query configuration param exists if dynamicQuery = true
+        if (dynamicQueryConfigurationIndex == -1 && this.staticQuery == null) {
+            throw new IllegalStateException(
+                "When passing dynamicQuery = true via org.apache.ignite.springdata.repository.config.Query "
+                    + "annotation, you must provide a non null method parameter of type DynamicQueryConfig");
+        }
+        // ensure domain class is registered on marshaller to transform row to entity
+        registerClassOnMarshaller(((IgniteEx)ignite).context(), type);
+    }
+
+    /**
+     * {@inheritDoc} @param values the values
+     *
+     * @return the object
+     */
+    @Override public Object execute(Object[] values) {
+        Object[] parameters = values;
+
+        // config via Query annotation (dynamicQuery = false)
+        DynamicQueryConfig config = staticQueryConfiguration;
+
+        // or condition to allow query tunning
+        if (config == null || dynamicQueryConfigurationIndex != -1) {
+            DynamicQueryConfig newConfig = (DynamicQueryConfig)values[dynamicQueryConfigurationIndex];
+            parameters = ArrayUtils.removeElement(parameters, dynamicQueryConfigurationIndex);
+            if (newConfig != null) {
+                // upset query configuration
+                config = newConfig;
+            }
+        }
+        // query configuration is required, via Query annotation or per parameter (within provided values param)
+        if (config == null) {
+            throw new IllegalStateException(
+                "Unable to execute query. When passing dynamicQuery = true via org.apache.ignite.springdata"
+                    + ".repository.config.Query annotation, you must provide a non null method parameter of type "
+                    + "DynamicQueryConfig");
+        }
+
+        IgniteQuery qry = getQuery(config);
+
+        ReturnStrategy returnStgy = getReturnStgy(qry);
+
+        Query iQry = prepareQuery(qry, config, returnStgy, parameters);
+
+        QueryCursor qryCursor = cache.query(iQry);
+
+        return transformQueryCursor(qry, returnStgy, parameters, qryCursor);
+    }
+
+    /** {@inheritDoc} */
+    @Override public QueryMethod getQueryMethod() {
+        return new QueryMethod(mtd, metadata, factory);
+    }
+
+    private <T extends Parameter> int getDynamicQueryConfigurationIndex(QueryMethod method) {
+        Iterator<T> it = (Iterator<T>)method.getParameters().iterator();
+        int i = 0;
+        boolean found = false;
+        int index = -1;
+        while (it.hasNext()) {
+            T parameter = it.next();
+
+            if (DynamicQueryConfig.class.isAssignableFrom(parameter.getType())) {
+                if (found) {
+                    throw new IllegalStateException("Invalid '" + method.getName() + "' repository method signature. "
+                        + "Only ONE DynamicQueryConfig parameter is allowed");
+                }
+
+                found = true;
+                index = i;
+            }
+
+            i++;
+        }
+        return index;
+    }
+
+    /** */
+    private synchronized IgniteBinaryImpl binary() {
+        if (igniteBinary == null)
+            igniteBinary = (IgniteBinaryImpl)ignite.binary();
+
+        return igniteBinary;
+    }
+
+    /** */
+    private synchronized BinaryType binType() {
+        if (igniteBinType == null)
+            igniteBinType = binary().type(type);
+
+        return igniteBinType;
+    }
+
+    /**
+     * @param mtd Method.
+     * @param isFieldQry Is field query.
+     * @return Return strategy type.
+     */
+    private ReturnStrategy calcReturnType(Method mtd, boolean isFieldQry) {
+        Class<?> returnType = mtd.getReturnType();
+
+        if (returnType == Slice.class) {
+            if (isFieldQry) {
+                if (hasAssignableGenericReturnTypeFrom(ArrayList.class, mtd))
+                    return ReturnStrategy.SLICE_OF_LISTS;
+            }
+            else if (hasAssignableGenericReturnTypeFrom(Cache.Entry.class, mtd))
+                return ReturnStrategy.SLICE_OF_CACHE_ENTRIES;
+            return ReturnStrategy.SLICE_OF_VALUES;
+        }
+        else if (returnType == Page.class)
+            return ReturnStrategy.PAGE_OF_VALUES;
+        else if (returnType == Stream.class)
+            return ReturnStrategy.STREAM_OF_VALUES;
+        else if (Cache.Entry.class.isAssignableFrom(returnType))
+            return ReturnStrategy.CACHE_ENTRY;
+        else if (Iterable.class.isAssignableFrom(returnType)) {
+            if (isFieldQry) {
+                if (hasAssignableGenericReturnTypeFrom(ArrayList.class, mtd))
+                    return ReturnStrategy.LIST_OF_LISTS;
+            }
+            else if (hasAssignableGenericReturnTypeFrom(Cache.Entry.class, mtd))
+                return ReturnStrategy.LIST_OF_CACHE_ENTRIES;
+            return ReturnStrategy.LIST_OF_VALUES;
+        }
+        else
+            return ReturnStrategy.ONE_VALUE;
+    }
+
+    /**
+     * @param cls Class.
+     * @param mtd Method.
+     * @return if {@code mtd} return type is assignable from {@code cls}
+     */
+    private boolean hasAssignableGenericReturnTypeFrom(Class<?> cls, Method mtd) {
+        Type genericReturnType = mtd.getGenericReturnType();
+
+        if (!(genericReturnType instanceof ParameterizedType))
+            return false;
+
+        Type[] actualTypeArguments = ((ParameterizedType)genericReturnType).getActualTypeArguments();
+
+        if (actualTypeArguments.length == 0)
+            return false;
+
+        if (actualTypeArguments[0] instanceof ParameterizedType) {
+            ParameterizedType type = (ParameterizedType)actualTypeArguments[0];
+
+            Class<?> type1 = (Class)type.getRawType();
+
+            return type1.isAssignableFrom(cls);
+        }
+
+        if (actualTypeArguments[0] instanceof Class) {
+            Class typeArg = (Class)actualTypeArguments[0];
+
+            return typeArg.isAssignableFrom(cls);
+        }
+
+        return false;
+    }
+
+    /**
+     * When select fields by query H2 returns Timestamp for types java.util.Date and java.qryStr.Timestamp
+     *
+     * @see org.apache.ignite.internal.processors.query.h2.H2DatabaseType map.put(Timestamp.class, TIMESTAMP)
+     * map.put(java.util.Date.class, TIMESTAMP) map.put(java.qryStr.Date.class, DATE)
+     */
+    private static <T> T fixExpectedType(final Object object, final Class<T> expected) {
+        if (expected != null && object instanceof java.sql.Timestamp && expected.equals(java.util.Date.class))
+            return (T)new java.util.Date(((java.sql.Timestamp)object).getTime());
+
+        return (T)object;
+    }
+
+    /**
+     * @param cfg Config.
+     */
+    private IgniteQuery getQuery(@Nullable DynamicQueryConfig cfg) {
+        if (staticQuery != null)
+            return staticQuery;
+
+        if (cfg != null && (StringUtils.hasText(cfg.value()) || cfg.textQuery())) {
+            return new IgniteQuery(cfg.value(),
+                !cfg.textQuery() && (isFieldQuery(cfg.value()) || cfg.forceFieldsQuery()), cfg.textQuery(),
+                false, IgniteQueryGenerator.getOptions(mtd));
+        }
+
+        throw new IllegalStateException("Unable to obtain a valid query. When passing dynamicQuery = true via org"
+            + ".apache.ignite.springdata.repository.config.Query annotation, you must"
+            + " provide a non null method parameter of type DynamicQueryConfig with a "
+            + "non empty value (query string) or textQuery = true");
+    }
+
+    /**
+     * @param qry Query.
+     */
+    private ReturnStrategy getReturnStgy(IgniteQuery qry) {
+        if (staticReturnStgy != null)
+            return staticReturnStgy;
+
+        if (qry != null)
+            return calcReturnType(mtd, qry.isFieldQuery());
+
+        throw new IllegalStateException("Unable to obtain a valid return strategy. When passing dynamicQuery = true "
+            + "via org.apache.ignite.springdata.repository.config.Query annotation, "
+            + "you must provide a non null method parameter of type "
+            + "DynamicQueryConfig with a non empty value (query string) or textQuery "
+            + "= true");
+    }
+
+    /**
+     * @param cls Class.
+     */
+    private static boolean isPrimitiveOrWrapper(Class<?> cls) {
+        return cls.isPrimitive() ||
+            Boolean.class.equals(cls) ||
+            Byte.class.equals(cls) ||
+            Character.class.equals(cls) ||
+            Short.class.equals(cls) ||
+            Integer.class.equals(cls) ||
+            Long.class.equals(cls) ||
+            Float.class.equals(cls) ||
+            Double.class.equals(cls) ||
+            Void.class.equals(cls) ||
+            String.class.equals(cls) ||
+            UUID.class.equals(cls);
+    }
+
+    /**
+     * @param prmtrs    Prmtrs.
+     * @param qryCursor Query cursor.
+     * @return Query cursor or slice
+     */
+    @Nullable
+    private Object transformQueryCursor(IgniteQuery qry,
+        ReturnStrategy returnStgy,
+        Object[] prmtrs,
+        QueryCursor qryCursor) {
+        final Class<?> returnClass;
+
+        if (hasProjection) {
+            if (hasDynamicProjection)
+                returnClass = (Class<?>)prmtrs[dynamicProjectionIndex];
+            else
+                returnClass = returnedDomainClass;
+        }
+        else
+            returnClass = returnedDomainClass;
+
+        if (qry.isFieldQuery()) {
+            // take control over single primite result from queries, i.e. DELETE, SELECT COUNT, UPDATE ...
+            boolean singlePrimitiveResult = isPrimitiveOrWrapper(returnClass);
+
+            final List<GridQueryFieldMetadata> meta = ((QueryCursorEx)qryCursor).fieldsMeta();
+
+            Function<List<?>, ?> cWrapperTransformFunction = null;
+
+            if (type.equals(returnClass)) {
+                IgniteBinaryImpl binary = binary();
+                BinaryType binType = binType();
+                cWrapperTransformFunction = row -> rowToEntity(binary, binType, row, meta);
+            }
+            else {
+                if (hasProjection || singlePrimitiveResult) {
+                    if (singlePrimitiveResult)
+                        cWrapperTransformFunction = row -> row.get(0);
+                    else {
+                        // Map row -> projection class
+                        cWrapperTransformFunction = row -> factory
+                            .createProjection(returnClass, rowToMap(row, meta));
+                    }
+                }
+                else
+                    cWrapperTransformFunction = row -> rowToMap(row, meta);
+            }
+
+            QueryCursorWrapper<?, ?> cWrapper = new QueryCursorWrapper<>((QueryCursor<List<?>>)qryCursor,
+                cWrapperTransformFunction);
+
+            switch (returnStgy) {
+                case PAGE_OF_VALUES:
+                    return new PageImpl(cWrapper.getAll(), (Pageable)prmtrs[prmtrs.length - 1], 0);
+                case LIST_OF_VALUES:
+                    return cWrapper.getAll();
+                case STREAM_OF_VALUES:
+                    return cWrapper.stream();
+                case ONE_VALUE:
+                    Iterator<?> iter = cWrapper.iterator();
+                    if (iter.hasNext()) {
+                        Object resp = iter.next();
+                        U.closeQuiet(cWrapper);
+                        return resp;
+                    }
+                    return null;
+                case SLICE_OF_VALUES:
+                    return new SliceImpl(cWrapper.getAll(), (Pageable)prmtrs[prmtrs.length - 1], true);
+                case SLICE_OF_LISTS:
+                    return new SliceImpl(qryCursor.getAll(), (Pageable)prmtrs[prmtrs.length - 1], true);
+                case LIST_OF_LISTS:
+                    return qryCursor.getAll();
+                default:
+                    throw new IllegalStateException();
+            }
+        }
+        else {
+            Iterable<CacheEntryImpl> qryIter = (Iterable<CacheEntryImpl>)qryCursor;
+
+            Function<CacheEntryImpl, ?> cWrapperTransformFunction;
+
+            if (hasProjection && !type.equals(returnClass))
+                cWrapperTransformFunction = row -> factory.createProjection(returnClass, row.getValue());
+            else
+                cWrapperTransformFunction = row -> row.getValue();
+
+            QueryCursorWrapper<?, ?> cWrapper = new QueryCursorWrapper<>((QueryCursor<CacheEntryImpl>)qryCursor,
+                cWrapperTransformFunction);
+
+            switch (returnStgy) {
+                case PAGE_OF_VALUES:
+                    return new PageImpl(cWrapper.getAll(), (Pageable)prmtrs[prmtrs.length - 1], 0);
+                case LIST_OF_VALUES:
+                    return cWrapper.getAll();
+                case STREAM_OF_VALUES:
+                    return cWrapper.stream();
+                case ONE_VALUE:
+                    Iterator<?> iter1 = cWrapper.iterator();
+                    if (iter1.hasNext()) {
+                        Object resp = iter1.next();
+                        U.closeQuiet(cWrapper);
+                        return resp;
+                    }
+                    return null;
+                case CACHE_ENTRY:
+                    Iterator<?> iter2 = qryIter.iterator();
+                    if (iter2.hasNext()) {
+                        Object resp2 = iter2.next();
+                        U.closeQuiet(qryCursor);
+                        return resp2;
+                    }
+                    return null;
+                case SLICE_OF_VALUES:
+                    return new SliceImpl(cWrapper.getAll(), (Pageable)prmtrs[prmtrs.length - 1], true);
+                case SLICE_OF_CACHE_ENTRIES:
+                    return new SliceImpl(qryCursor.getAll(), (Pageable)prmtrs[prmtrs.length - 1], true);
+                case LIST_OF_CACHE_ENTRIES:
+                    return qryCursor.getAll();
+                default:
+                    throw new IllegalStateException();
+            }
+        }
+    }
+
+    /**
+     * Extract bindable values
+     *
+     * @param values            values invoking query method
+     * @param queryMethodParams query method parameter definitions
+     * @param queryBindings     All parameters found on query string that need to be binded
+     * @return new list of parameters
+     */
+    private Object[] extractBindableValues(Object[] values,
+        Parameters<?, ?> queryMethodParams,
+        List<ParameterBinding> queryBindings) {
+        // no binding params then exit
+        if (queryBindings.isEmpty())
+            return values;
+
+        Object[] newValues = new Object[queryBindings.size()];
+
+        // map bindable parameters from query method: (index/name) - index
+        HashMap<String, Integer> methodParams = new HashMap<>();
+
+        // create an evaluation context for custom query
+        EvaluationContext queryEvalContext = queryMethodEvaluationContextProvider
+            .getEvaluationContext(queryMethodParams, values);
+
+        // By default queryEvalContext:
+        // - make accesible query method parameters by index:
+        // @Query("select u from User u where u.age = ?#{[0]}")
+        // List<User> findUsersByAge(int age);
+        // - make accesible query method parameters by name:
+        // @Query("select u from User u where u.firstname = ?#{#customer.firstname}")
+        // List<User> findUsersByCustomersFirstname(@Param("customer") Customer customer);
+
+        // query method param's index by name and position
+        queryMethodParams.getBindableParameters().forEach(p -> {
+            if (p.isNamedParameter()) {
+                // map by name (annotated by @Param)
+                methodParams.put(p.getName().get(), p.getIndex());
+            }
+            // map by position
+            methodParams.put(String.valueOf(p.getIndex()), p.getIndex());
+        });
+
+        // process all parameters on query and extract new values to bind
+        for (int i = 0; i < queryBindings.size(); i++) {
+            ParameterBinding p = queryBindings.get(i);
+
+            if (p.isExpression()) {
+                // Evaluate SpEl expressions (synthetic parameter value) , example ?#{#customer.firstname}
+                newValues[i] = expressionParser.parseExpression(p.getExpression()).getValue(queryEvalContext);
+            }
+            else {
+                // Extract parameter value by name or position respectively from invoking values
+                newValues[i] = values[methodParams.get(
+                    p.getName() != null ? p.getName() : String.valueOf(p.getRequiredPosition() - 1))];
+            }
+        }
+
+        return newValues;
+    }
+
+    /**
+     * @param qry        Query.
+     * @param config     Config.
+     * @param returnStgy Return stgy.
+     * @param values     Values.
+     * @return prepared query for execution
+     */
+    private Query prepareQuery(IgniteQuery qry, DynamicQueryConfig config, ReturnStrategy returnStgy, Object[] values) {
+        Object[] parameters = values;
+
+        String queryString = qry.qryStr();
+
+        Query query;
+
+        checkRequiredPageable(returnStgy, values);
+
+        if (!qry.isTextQuery()) {
+            if (!qry.isAutogenerated()) {
+                StringQuery squery = new ExpressionBasedStringQuery(queryString, metadata, expressionParser);
+                queryString = squery.getQueryString();
+                parameters = extractBindableValues(parameters, getQueryMethod().getParameters(),
+                    squery.getParameterBindings());
+            }
+            else {
+                // remove dynamic projection from parameters
+                if (hasDynamicProjection)
+                    parameters = ArrayUtils.remove(parameters, dynamicProjectionIndex);
+            }
+
+            switch (qry.options()) {
+                case SORTING:
+                    queryString = IgniteQueryGenerator
+                        .addSorting(new StringBuilder(queryString), (Sort)values[values.length - 1])
+                        .toString();
+                    if (qry.isAutogenerated())
+                        parameters = Arrays.copyOfRange(parameters, 0, values.length - 1);
+                    break;
+                case PAGINATION:
+                    queryString = IgniteQueryGenerator
+                        .addPaging(new StringBuilder(queryString), (Pageable)values[values.length - 1])
+                        .toString();
+                    if (qry.isAutogenerated())
+                        parameters = Arrays.copyOfRange(parameters, 0, values.length - 1);
+                    break;
+                default:
+            }
+
+            if (qry.isFieldQuery()) {
+                SqlFieldsQuery sqlFieldsQry = new SqlFieldsQuery(queryString);
+                sqlFieldsQry.setArgs(parameters);
+
+                sqlFieldsQry.setCollocated(config.collocated());
+                sqlFieldsQry.setDistributedJoins(config.distributedJoins());
+                sqlFieldsQry.setEnforceJoinOrder(config.enforceJoinOrder());
+                sqlFieldsQry.setLazy(config.lazy());
+                sqlFieldsQry.setLocal(config.local());
+
+                if (config.parts() != null && config.parts().length > 0)
+                    sqlFieldsQry.setPartitions(config.parts());
+
+                sqlFieldsQry.setTimeout(config.timeout(), TimeUnit.MILLISECONDS);
+
+                query = sqlFieldsQry;
+            }
+            else {
+                SqlQuery sqlQry = new SqlQuery(type, queryString);
+                sqlQry.setArgs(parameters);
+
+                sqlQry.setDistributedJoins(config.distributedJoins());
+                sqlQry.setLocal(config.local());
+
+                if (config.parts() != null && config.parts().length > 0)
+                    sqlQry.setPartitions(config.parts());
+
+                sqlQry.setTimeout(config.timeout(), TimeUnit.MILLISECONDS);
+
+                query = sqlQry;
+            }
+        }
+        else {
+            int pageSize = -1;
+
+            switch (qry.options()) {
+                case PAGINATION:
+                    pageSize = ((Pageable)parameters[parameters.length - 1]).getPageSize();
+                    break;
+            }
+
+            // check if queryString contains SpEL template expressions and evaluate them if any
+            if (queryString.contains("#{")) {
+                EvaluationContext queryEvalContext = queryMethodEvaluationContextProvider
+                    .getEvaluationContext(getQueryMethod().getParameters(),
+                        values);
+
+                Object eval = expressionParser.parseExpression(queryString, ParserContext.TEMPLATE_EXPRESSION)
+                    .getValue(queryEvalContext);
+
+                if (!(eval instanceof String)) {
+                    throw new IllegalStateException(
+                        "TextQuery with SpEL expressions must produce a String response, but found " + eval.getClass()
+                            .getName()
+                            + ". Please, check your expression: " + queryString);
+                }
+                queryString = (String)eval;
+            }
+
+            TextQuery textQuery = new TextQuery(type, queryString, config.limit());
+
+            textQuery.setLocal(config.local());
+
+            if (pageSize > -1)
+                textQuery.setPageSize(pageSize);
+
+            query = textQuery;
+        }
+        return query;
+    }
+
+    /** */
+    private static Map<String, Object> rowToMap(final List<?> row, final List<GridQueryFieldMetadata> meta) {
+        // use treemap with case insensitive property name
+        final TreeMap<String, Object> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        for (int i = 0; i < meta.size(); i++) {
+            // don't want key or val columns
+            final String metaField = meta.get(i).fieldName().toLowerCase();
+            if (!metaField.equalsIgnoreCase(QueryUtils.KEY_FIELD_NAME) && !metaField.equalsIgnoreCase(
+                QueryUtils.VAL_FIELD_NAME))
+                map.put(metaField, row.get(i));
+        }
+        return map;
+    }
+
+    /**
+     * convert row ( with list of field values) into domain entity
+     */
+    private <V> V rowToEntity(final IgniteBinaryImpl binary,
+        final BinaryType binType,
+        final List<?> row,
+        final List<GridQueryFieldMetadata> meta) {
+        // additional data returned by query not present on domain object type
+        final TreeMap<String, Object> metadata = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+        final BinaryObjectBuilder bldr = binary.builder(binType.typeName());
+
+        for (int i = 0; i < row.size(); i++) {
+            final GridQueryFieldMetadata fMeta = meta.get(i);
+            final String metaField = fMeta.fieldName();
+            // add existing entity fields to binary object
+            if (binType.field(fMeta.fieldName()) != null && !metaField.equalsIgnoreCase(QueryUtils.KEY_FIELD_NAME)
+                && !metaField.equalsIgnoreCase(QueryUtils.VAL_FIELD_NAME)) {
+                final Object fieldValue = row.get(i);
+                if (fieldValue != null) {
+                    final Class<?> clazz = getClassForBinaryField(binary, binType, fMeta);
+                    // null values must not be set into binary objects
+                    bldr.setField(metaField, fixExpectedType(fieldValue, clazz));
+                }
+            }
+            else {
+                // don't want key or val column... but wants null values
+                if (!metaField.equalsIgnoreCase(QueryUtils.KEY_FIELD_NAME) && !metaField.equalsIgnoreCase(
+                    QueryUtils.VAL_FIELD_NAME))
+                    metadata.put(metaField, row.get(i));
+            }
+        }
+        return bldr.build().deserialize();
+    }
+
+    /**
+     * Obtains real field class from resultset metadata field whether it's available
+     */
+    private Class<?> getClassForBinaryField(final IgniteBinaryImpl binary,
+        final BinaryType binType,
+        final GridQueryFieldMetadata fieldMeta) {
+        try {
+            final String fieldId = fieldMeta.schemaName() + "." + fieldMeta.typeName() + "." + fieldMeta.fieldName();
+
+            if (binaryFieldClass.containsKey(fieldId))
+                return binaryFieldClass.get(fieldId);
+
+            Class<?> clazz = null;
+
+            synchronized (binaryFieldClass) {
+
+                if (binaryFieldClass.containsKey(fieldId))
+                    return binaryFieldClass.get(fieldId);
+
+                String fieldName = null;
+
+                // search field name on binary type (query returns case insensitive
+                // field name) but BinaryType is not case insensitive
+                for (final String fname : binType.fieldNames()) {
+                    if (fname.equalsIgnoreCase(fieldMeta.fieldName())) {
+                        fieldName = fname;
+                        break;
+                    }
+                }
+
+                final CacheObjectBinaryProcessorImpl proc = (CacheObjectBinaryProcessorImpl)binary.processor();
+
+                // search for class by typeId, if not found use
+                // fieldMeta.fieldTypeName() class
+                clazz = BinaryUtils.resolveClass(proc.binaryContext(), binary.typeId(binType.fieldTypeName(fieldName)),
+                    fieldMeta.fieldTypeName(), ignite.configuration().getClassLoader(), true);
+
+                binaryFieldClass.put(fieldId, clazz);
+            }
+
+            return clazz;
+        }
+        catch (final Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * Validates operations that requires Pageable parameter
+     *
+     * @param returnStgy Return stgy.
+     * @param prmtrs     Prmtrs.
+     */
+    private void checkRequiredPageable(ReturnStrategy returnStgy, Object[] prmtrs) {
+        try {
+            if (returnStgy == ReturnStrategy.PAGE_OF_VALUES || returnStgy == ReturnStrategy.SLICE_OF_VALUES
+                || returnStgy == ReturnStrategy.SLICE_OF_CACHE_ENTRIES) {
+                Pageable page = (Pageable)prmtrs[prmtrs.length - 1];
+                page.isPaged();
+            }
+        }
+        catch (NullPointerException | IndexOutOfBoundsException | ClassCastException e) {
+            throw new IllegalStateException(
+                "For " + returnStgy.name() + " you must provide on last method parameter a non null Pageable instance");
+        }
+    }
+
+    /**
+     * @param ctx   Context.
+     * @param clazz Clazz.
+     */
+    private static void registerClassOnMarshaller(final GridKernalContext ctx, final Class<?> clazz) {
+        try {
+            // ensure class registration for marshaller on cluster...
+            if (!U.isJdk(clazz))
+                U.marshal(ctx, clazz.newInstance());
+        }
+        catch (final Exception ignored) {
+            // silent
+        }
+    }
+
+    /**
+     * Ignite QueryCursor wrapper.
+     * <p>
+     * Ensures closing underline cursor when there is no data.
+     *
+     * @param <T> input type
+     * @param <V> transformed output type
+     */
+    public static class QueryCursorWrapper<T, V> extends AbstractCollection<V> implements QueryCursor<V> {
+        /**
+         * Delegate query cursor.
+         */
+        private final QueryCursor<T> delegate;
+
+        /**
+         * Transformer.
+         */
+        private final Function<T, V> transformer;
+
+        /**
+         * Instantiates a new Query cursor wrapper.
+         *
+         * @param delegate    delegate QueryCursor with T input elements
+         * @param transformer Function to transform T to V elements
+         */
+        public QueryCursorWrapper(final QueryCursor<T> delegate, final Function<T, V> transformer) {
+            this.delegate = delegate;
+            this.transformer = transformer;
+        }
+
+        /** {@inheritDoc} */
+        @Override public Iterator<V> iterator() {
+            final Iterator<T> it = delegate.iterator();
+
+            return new Iterator<V>() {
+                /** */
+                @Override public boolean hasNext() {
+                    if (!it.hasNext()) {
+                        U.closeQuiet(delegate);
+                        return false;
+                    }
+                    return true;
+                }
+
+                /** */
+                @Override public V next() {
+                    final V r = transformer.apply(it.next());
+                    if (r != null)
+                        return r;
+                    throw new NoSuchElementException();
+                }
+            };
+        }
+
+        /** {@inheritDoc} */
+        @Override public void close() {
+            U.closeQuiet(delegate);
+        }
+
+        /** {@inheritDoc} */
+        @Override public List<V> getAll() {
+            final List<V> data = new ArrayList<>();
+            delegate.forEach(i -> data.add(transformer.apply(i)));
+            U.closeQuiet(delegate);
+            return data;
+        }
+
+        /** {@inheritDoc} */
+        @Override public int size() {
+            return 0;
+        }
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/QueryUtils.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/QueryUtils.java
new file mode 100644
index 0000000..711d297
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/QueryUtils.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright 2008-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      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.springdata22.repository.query;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.data.util.Streamable;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+import static java.util.regex.Pattern.DOTALL;
+import static java.util.regex.Pattern.compile;
+
+/**
+ * Simple utility class to create queries.
+ *
+ * @author Oliver Gierke
+ * @author Kevin Raymond
+ * @author Thomas Darimont
+ * @author Komi Innocent
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ * @author Sébastien Péralta
+ * @author Jens Schauder
+ * @author Nils Borrmann
+ * @author Reda.Housni -Alaoui
+ */
+public abstract class QueryUtils {
+    /**
+     * The constant COUNT_QUERY_STRING.
+     */
+    public static final String COUNT_QUERY_STRING = "select count(%s) from %s x";
+
+    /**
+     * The constant DELETE_ALL_QUERY_STRING.
+     */
+    public static final String DELETE_ALL_QUERY_STRING = "delete from %s x";
+
+    /**
+     * Used Regex/Unicode categories (see http://www.unicode.org/reports/tr18/#General_Category_Property): Z Separator
+     * Cc Control Cf Format P Punctuation
+     */
+    private static final String IDENTIFIER = "[._[\\P{Z}&&\\P{Cc}&&\\P{Cf}&&\\P{P}]]+";
+
+    /**
+     * The Colon no double colon.
+     */
+    static final String COLON_NO_DOUBLE_COLON = "(?<![:\\\\]):";
+
+    /**
+     * The Identifier group.
+     */
+    static final String IDENTIFIER_GROUP = String.format("(%s)", IDENTIFIER);
+
+    /** */
+    private static final String COUNT_REPLACEMENT_TEMPLATE = "select count(%s) $5$6$7";
+
+    /** */
+    private static final String SIMPLE_COUNT_VALUE = "$2";
+
+    /** */
+    private static final String COMPLEX_COUNT_VALUE = "$3$6";
+
+    /** */
+    private static final String ORDER_BY_PART = "(?iu)\\s+order\\s+by\\s+.*$";
+
+    /** */
+    private static final Pattern ALIAS_MATCH;
+
+    /** */
+    private static final Pattern COUNT_MATCH;
+
+    /** */
+    private static final Pattern PROJECTION_CLAUSE = Pattern
+        .compile("select\\s+(.+)\\s+from", Pattern.CASE_INSENSITIVE);
+
+    /** */
+    private static final String JOIN = "join\\s+(fetch\\s+)?" + IDENTIFIER + "\\s+(as\\s+)?" + IDENTIFIER_GROUP;
+
+    /** */
+    private static final Pattern JOIN_PATTERN = Pattern.compile(JOIN, Pattern.CASE_INSENSITIVE);
+
+    /** */
+    private static final String EQUALS_CONDITION_STRING = "%s.%s = :%s";
+
+    /** */
+    private static final Pattern NAMED_PARAMETER = Pattern.compile(
+        COLON_NO_DOUBLE_COLON + IDENTIFIER + "|\\#" + IDENTIFIER, CASE_INSENSITIVE);
+
+    /** */
+    private static final Pattern CONSTRUCTOR_EXPRESSION;
+
+    /** */
+    private static final int QUERY_JOIN_ALIAS_GROUP_INDEX = 3;
+
+    /** */
+    private static final int VARIABLE_NAME_GROUP_INDEX = 4;
+
+    /** */
+    private static final Pattern FUNCTION_PATTERN;
+
+    static {
+        StringBuilder builder = new StringBuilder();
+        builder.append("(?<=from)"); // from as starting delimiter
+        builder.append("(?:\\s)+"); // at least one space separating
+        builder.append(IDENTIFIER_GROUP); // Entity name, can be qualified (any
+        builder.append("(?:\\sas)*"); // exclude possible "as" keyword
+        builder.append("(?:\\s)+"); // at least one space separating
+        builder.append("(?!(?:where))(\\w+)"); // the actual alias
+
+        ALIAS_MATCH = compile(builder.toString(), CASE_INSENSITIVE);
+
+        builder = new StringBuilder();
+        builder.append("(select\\s+((distinct )?(.+?)?)\\s+)?(from\\s+");
+        builder.append(IDENTIFIER);
+        builder.append("(?:\\s+as)?\\s+)");
+        builder.append(IDENTIFIER_GROUP);
+        builder.append("(.*)");
+
+        COUNT_MATCH = compile(builder.toString(), CASE_INSENSITIVE);
+
+        builder = new StringBuilder();
+        builder.append("select");
+        builder.append("\\s+"); // at least one space separating
+        builder.append("(.*\\s+)?"); // anything in between (e.g. distinct) at least one space separating
+        builder.append("new");
+        builder.append("\\s+"); // at least one space separating
+        builder.append(IDENTIFIER);
+        builder.append("\\s*"); // zero to unlimited space separating
+        builder.append("\\(");
+        builder.append(".*");
+        builder.append("\\)");
+
+        CONSTRUCTOR_EXPRESSION = compile(builder.toString(), CASE_INSENSITIVE + DOTALL);
+
+        builder = new StringBuilder();
+        // any function call including parameters within the brackets
+        builder.append("\\w+\\s*\\([\\w\\.,\\s'=]+\\)");
+        // the potential alias
+        builder.append("\\s+[as|AS]+\\s+(([\\w\\.]+))");
+
+        FUNCTION_PATTERN = compile(builder.toString());
+    }
+
+    /**
+     * Private constructor to prevent instantiation.
+     */
+    private QueryUtils() {
+        // No-op.
+    }
+
+    /**
+     * Returns the query string to execute an exists query for the given id attributes.
+     *
+     * @param entityName        the name of the entity to create the query for, must not be {@literal null}.
+     * @param cntQryPlaceHolder the placeholder for the count clause, must not be {@literal null}.
+     * @param idAttrs           the id attributes for the entity, must not be {@literal null}.
+     * @return the exists query string
+     */
+    public static String getExistsQueryString(String entityName,
+        String cntQryPlaceHolder,
+        Iterable<String> idAttrs) {
+        String whereClause = Streamable.of(idAttrs).stream() //
+            .map(idAttribute -> String.format(EQUALS_CONDITION_STRING, "x", idAttribute,
+                idAttribute)) //
+            .collect(Collectors.joining(" AND ", " WHERE ", ""));
+
+        return String.format(COUNT_QUERY_STRING, cntQryPlaceHolder, entityName) + whereClause;
+    }
+
+    /**
+     * Returns the query string for the given class name.
+     *
+     * @param template   must not be {@literal null}.
+     * @param entityName must not be {@literal null}.
+     * @return the template with placeholders replaced by the {@literal entityName}. Guaranteed to be not {@literal
+     *     null}.
+     */
+    public static String getQueryString(String template, String entityName) {
+        Assert.hasText(entityName, "Entity name must not be null or empty!");
+
+        return String.format(template, entityName);
+    }
+
+    /**
+     * Returns the aliases used for {@code left (outer) join}s.
+     *
+     * @param qry a query string to extract the aliases of joins from. Must not be {@literal null}.
+     * @return a {@literal Set} of aliases used in the query. Guaranteed to be not {@literal null}.
+     */
+    static Set<String> getOuterJoinAliases(String qry) {
+        Set<String> result = new HashSet<>();
+        Matcher matcher = JOIN_PATTERN.matcher(qry);
+
+        while (matcher.find()) {
+            String alias = matcher.group(QUERY_JOIN_ALIAS_GROUP_INDEX);
+            if (StringUtils.hasText(alias))
+                result.add(alias);
+        }
+
+        return result;
+    }
+
+    /**
+     * Returns the aliases used for aggregate functions like {@code SUM, COUNT, ...}.
+     *
+     * @param qry a {@literal String} containing a query. Must not be {@literal null}.
+     * @return a {@literal Set} containing all found aliases. Guaranteed to be not {@literal null}.
+     */
+    static Set<String> getFunctionAliases(String qry) {
+        Set<String> result = new HashSet<>();
+        Matcher matcher = FUNCTION_PATTERN.matcher(qry);
+
+        while (matcher.find()) {
+            String alias = matcher.group(1);
+
+            if (StringUtils.hasText(alias))
+                result.add(alias);
+        }
+
+        return result;
+    }
+
+    /**
+     * Resolves the alias for the entity to be retrieved from the given JPA query.
+     *
+     * @param qry must not be {@literal null}.
+     * @return Might return {@literal null}.
+     */
+    @Nullable
+    static String detectAlias(String qry) {
+        Matcher matcher = ALIAS_MATCH.matcher(qry);
+
+        return matcher.find() ? matcher.group(2) : null;
+    }
+
+    /**
+     * Creates a count projected query from the given original query.
+     *
+     * @param originalQry   must not be {@literal null}.
+     * @param cntProjection may be {@literal null}.
+     * @return a query String to be used a count query for pagination. Guaranteed to be not {@literal null}.
+     */
+    static String createCountQueryFor(String originalQry, @Nullable String cntProjection) {
+        Assert.hasText(originalQry, "OriginalQuery must not be null or empty!");
+
+        Matcher matcher = COUNT_MATCH.matcher(originalQry);
+        String countQuery;
+
+        if (cntProjection == null) {
+            String variable = matcher.matches() ? matcher.group(VARIABLE_NAME_GROUP_INDEX) : null;
+            boolean useVariable = variable != null && StringUtils.hasText(variable) && !variable.startsWith("new")
+                && !variable.startsWith("count(") && !variable.contains(",");
+
+            String replacement = useVariable ? SIMPLE_COUNT_VALUE : COMPLEX_COUNT_VALUE;
+            countQuery = matcher.replaceFirst(String.format(COUNT_REPLACEMENT_TEMPLATE, replacement));
+        }
+        else
+            countQuery = matcher.replaceFirst(String.format(COUNT_REPLACEMENT_TEMPLATE, cntProjection));
+
+        return countQuery.replaceFirst(ORDER_BY_PART, "");
+    }
+
+    /**
+     * Returns whether the given JPQL query contains a constructor expression.
+     *
+     * @param qry must not be {@literal null} or empty.
+     * @return boolean
+     */
+    public static boolean hasConstructorExpression(String qry) {
+        Assert.hasText(qry, "Query must not be null or empty!");
+
+        return CONSTRUCTOR_EXPRESSION.matcher(qry).find();
+    }
+
+    /**
+     * Returns the projection part of the query, i.e. everything between {@code select} and {@code from}.
+     *
+     * @param qry must not be {@literal null} or empty.
+     * @return projection
+     */
+    public static String getProjection(String qry) {
+        Assert.hasText(qry, "Query must not be null or empty!");
+
+        Matcher matcher = PROJECTION_CLAUSE.matcher(qry);
+        String projection = matcher.find() ? matcher.group(1) : "";
+        return projection.trim();
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/StringQuery.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/StringQuery.java
new file mode 100644
index 0000000..8957e69
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/StringQuery.java
@@ -0,0 +1,879 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      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.springdata22.repository.query;
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.data.domain.Range;
+import org.springframework.data.domain.Range.Bound;
+import org.springframework.data.repository.query.SpelQueryContext;
+import org.springframework.data.repository.query.SpelQueryContext.SpelExtractor;
+import org.springframework.data.repository.query.parser.Part.Type;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+import static org.springframework.util.ObjectUtils.nullSafeEquals;
+import static org.springframework.util.ObjectUtils.nullSafeHashCode;
+
+/**
+ * Encapsulation of a JPA query String. Offers access to parameters as bindings. The internal query String is cleaned
+ * from decorated parameters like {@literal %:lastname%} and the matching bindings take care of applying the decorations
+ * in the {@link ParameterBinding#prepare(Object)} method. Note that this class also handles replacing SpEL expressions
+ * with synthetic bind parameters
+ *
+ * @author Oliver Gierke
+ * @author Thomas Darimont
+ * @author Oliver Wehrens
+ * @author Mark Paluch
+ * @author Jens Schauder
+ */
+class StringQuery implements DeclaredQuery {
+    /** */
+    private final String query;
+
+    /** */
+    private final List<ParameterBinding> bindings;
+
+    /** */
+    @Nullable
+    private final String alias;
+
+    /** */
+    private final boolean hasConstructorExpression;
+
+    /** */
+    private final boolean containsPageableInSpel;
+
+    /** */
+    private final boolean usesJdbcStyleParameters;
+
+    /**
+     * Creates a new {@link StringQuery} from the given JPQL query.
+     *
+     * @param query must not be {@literal null} or empty.
+     */
+    StringQuery(String query) {
+        Assert.hasText(query, "Query must not be null or empty!");
+
+        bindings = new ArrayList<>();
+        containsPageableInSpel = query.contains("#pageable");
+
+        Metadata queryMeta = new Metadata();
+        this.query = ParameterBindingParser.INSTANCE
+            .parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(query, bindings,
+                queryMeta);
+
+        usesJdbcStyleParameters = queryMeta.usesJdbcStyleParameters;
+        alias = QueryUtils.detectAlias(query);
+        hasConstructorExpression = QueryUtils.hasConstructorExpression(query);
+    }
+
+    /**
+     * Returns whether we have found some like bindings.
+     */
+    boolean hasParameterBindings() {
+        return !bindings.isEmpty();
+    }
+
+    /** */
+    String getProjection() {
+        return QueryUtils.getProjection(query);
+    }
+
+    // See org.springframework.data.jpa.repository.query.DeclaredQuery#getParameterBindings()
+    /** {@inheritDoc} */
+    @Override public List<ParameterBinding> getParameterBindings() {
+        return bindings;
+    }
+
+    // See org.springframework.data.jpa.repository.query.DeclaredQuery#deriveCountQuery(java.lang.String, java.lang
+    /** {@inheritDoc} */
+    @Override public DeclaredQuery deriveCountQuery(@Nullable String countQuery,
+        @Nullable String countQueryProjection) {
+        return DeclaredQuery
+            .of(countQuery != null ? countQuery : QueryUtils.createCountQueryFor(query, countQueryProjection));
+    }
+
+    // See org.springframework.data.jpa.repository.query.DeclaredQuery#usesJdbcStyleParameters()
+    /** */
+    @Override public boolean usesJdbcStyleParameters() {
+        return usesJdbcStyleParameters;
+    }
+
+    // See org.springframework.data.jpa.repository.query.DeclaredQuery#getQueryString()
+    /** {@inheritDoc} */
+    @Override public String getQueryString() {
+        return query;
+    }
+
+    // See org.springframework.data.jpa.repository.query.DeclaredQuery#getAlias()
+    /** {@inheritDoc} */
+    @Override @Nullable
+    public String getAlias() {
+        return alias;
+    }
+
+    // See org.springframework.data.jpa.repository.query.DeclaredQuery#hasConstructorExpression()
+    /** {@inheritDoc} */
+    @Override public boolean hasConstructorExpression() {
+        return hasConstructorExpression;
+    }
+
+    // See org.springframework.data.jpa.repository.query.DeclaredQuery#isDefaultProjection()
+    /** {@inheritDoc} */
+    @Override public boolean isDefaultProjection() {
+        return getProjection().equalsIgnoreCase(alias);
+    }
+
+    // See org.springframework.data.jpa.repository.query.DeclaredQuery#hasNamedParameter()
+    /** {@inheritDoc} */
+    @Override public boolean hasNamedParameter() {
+        return bindings.stream().anyMatch(b -> b.getName() != null);
+    }
+
+    // See org.springframework.data.jpa.repository.query.DeclaredQuery#usesPaging()
+    /** {@inheritDoc} */
+    @Override public boolean usesPaging() {
+        return containsPageableInSpel;
+    }
+
+    /**
+     * A parser that extracts the parameter bindings from a given query string.
+     *
+     * @author Thomas Darimont
+     */
+    enum ParameterBindingParser {
+        /** */
+        INSTANCE;
+
+        /** */
+        private static final String EXPRESSION_PARAMETER_PREFIX = "__$synthetic$__";
+
+        /** */
+        public static final String POSITIONAL_OR_INDEXED_PARAMETER = "\\?(\\d*+(?![#\\w]))";
+        // .....................................................................^ not followed by a hash or a letter.
+        // .................................................................^ zero or more digits.
+        // .............................................................^ start with a question mark.
+
+        /** */
+        private static final Pattern PARAMETER_BINDING_BY_INDEX = Pattern.compile(POSITIONAL_OR_INDEXED_PARAMETER);
+
+        /** */
+        private static final Pattern PARAMETER_BINDING_PATTERN;
+
+        /** */
+        private static final String MESSAGE =
+            "Already found parameter binding with same index / parameter name but differing binding type! "
+                + "Already have: %s, found %s! If you bind a parameter multiple times make sure they use the same "
+                + "binding.";
+
+        /** */
+        private static final int INDEXED_PARAMETER_GROUP = 4;
+
+        /** */
+        private static final int NAMED_PARAMETER_GROUP = 6;
+
+        /** */
+        private static final int COMPARISION_TYPE_GROUP = 1;
+
+        static {
+            List<String> keywords = new ArrayList<>();
+
+            for (ParameterBindingType type : ParameterBindingType.values()) {
+                if (type.getKeyword() != null) {
+                    keywords.add(type.getKeyword());
+                }
+            }
+
+            StringBuilder builder = new StringBuilder();
+            builder.append("(");
+            builder.append(StringUtils.collectionToDelimitedString(keywords, "|")); // keywords
+            builder.append(")?");
+            builder.append("(?: )?"); // some whitespace
+            builder.append("\\(?"); // optional braces around parameters
+            builder.append("(");
+            builder.append("%?(" + POSITIONAL_OR_INDEXED_PARAMETER + ")%?"); // position parameter and parameter index
+            builder.append("|"); // or
+
+            // named parameter and the parameter name
+            builder.append("%?(" + QueryUtils.COLON_NO_DOUBLE_COLON + QueryUtils.IDENTIFIER_GROUP + ")%?");
+
+            builder.append(")");
+            builder.append("\\)?"); // optional braces around parameters
+
+            PARAMETER_BINDING_PATTERN = Pattern.compile(builder.toString(), CASE_INSENSITIVE);
+        }
+
+        /**
+         * Parses {@link ParameterBinding} instances from the given query and adds them to the registered bindings.
+         * Returns the cleaned up query.
+         */
+        private String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(String query,
+            List<ParameterBinding> bindings,
+            Metadata queryMeta) {
+            int greatestParamIdx = tryFindGreatestParameterIndexIn(query);
+            boolean parametersShouldBeAccessedByIdx = greatestParamIdx != -1;
+
+            /*
+             * Prefer indexed access over named parameters if only SpEL Expression parameters are present.
+             */
+            if (!parametersShouldBeAccessedByIdx && query.contains("?#{")) {
+                parametersShouldBeAccessedByIdx = true;
+                greatestParamIdx = 0;
+            }
+
+            SpelExtractor spelExtractor = createSpelExtractor(query, parametersShouldBeAccessedByIdx,
+                greatestParamIdx);
+
+            String resultingQry = spelExtractor.getQueryString();
+            Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(resultingQry);
+            QuotationMap quotedAreas = new QuotationMap(resultingQry);
+
+            int expressionParamIdx = parametersShouldBeAccessedByIdx ? greatestParamIdx : 0;
+
+            boolean usesJpaStyleParameters = false;
+
+            while (matcher.find()) {
+                if (quotedAreas.isQuoted(matcher.start()))
+                    continue;
+
+                String paramIdxStr = matcher.group(INDEXED_PARAMETER_GROUP);
+                String paramName = paramIdxStr != null ? null : matcher.group(NAMED_PARAMETER_GROUP);
+                Integer paramIdx = getParameterIndex(paramIdxStr);
+
+                String typeSrc = matcher.group(COMPARISION_TYPE_GROUP);
+                String expression = spelExtractor
+                    .getParameter(paramName == null ? paramIdxStr : paramName);
+                String replacement = null;
+
+                Assert.isTrue(paramIdxStr != null || paramName != null,
+                    () -> String.format("We need either a name or an index! Offending query string: %s", query));
+
+                expressionParamIdx++;
+                if (paramIdxStr != null && paramIdxStr.isEmpty()) {
+                    queryMeta.usesJdbcStyleParameters = true;
+                    paramIdx = expressionParamIdx;
+                }
+                else
+                    usesJpaStyleParameters = true;
+
+                // named parameters (:param) will be untouched by spelExtractor, so replace them by ? as we don't
+                // know position
+                if (paramName != null)
+                    replacement = "?";
+
+                if (usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) {
+                    throw new IllegalArgumentException(
+                        "Mixing of ? (? or :myNamedParam) parameters and other forms like ?1 (SpEL espressions or "
+                            + "indexed) is not supported!. Please, if you are using expressions or "
+                            + "indexed params, replace all named parameters by expressions. Example :myNamedParam "
+                            + "by ?#{#myNamedParam}.");
+                }
+
+                switch (ParameterBindingType.of(typeSrc)) {
+                    case LIKE:
+                        Type likeType = LikeParameterBinding.getLikeTypeFrom(matcher.group(2));
+                        replacement = matcher.group(3);
+
+                        if (paramIdx != null)
+                            checkAndRegister(new LikeParameterBinding(paramIdx, likeType, expression), bindings);
+                        else {
+                            checkAndRegister(new LikeParameterBinding(paramName, likeType, expression), bindings);
+
+                            replacement = expression != null ? ":" + paramName : matcher.group(5);
+                        }
+
+                        break;
+
+                    case IN:
+                        if (paramIdx != null)
+                            checkAndRegister(new InParameterBinding(paramIdx, expression), bindings);
+                        else
+                            checkAndRegister(new InParameterBinding(paramName, expression), bindings);
+
+                        break;
+
+                    case AS_IS: // fall-through we don't need a special parameter binding for the given parameter.
+                    default:
+                        bindings.add(paramIdx != null
+                            ? new ParameterBinding(null, paramIdx, expression)
+                            : new ParameterBinding(paramName, null, expression));
+                }
+
+                if (replacement != null)
+                    resultingQry = replaceFirst(resultingQry, matcher.group(2), replacement);
+            }
+
+            return resultingQry;
+        }
+
+        /** */
+        private static SpelExtractor createSpelExtractor(String queryWithSpel,
+            boolean parametersShouldBeAccessedByIndex,
+            int greatestParameterIndex) {
+
+            /*
+             * If parameters need to be bound by index, we bind the synthetic expression parameters starting from
+             * position of the greatest discovered index parameter in order to
+             * not mix-up with the actual parameter indices.
+             */
+            int expressionParameterIndex = parametersShouldBeAccessedByIndex ? greatestParameterIndex : 0;
+
+            BiFunction<Integer, String, String> indexToParameterName = parametersShouldBeAccessedByIndex
+                ? (index, expression) -> String.valueOf(
+                index + expressionParameterIndex + 1)
+                : (index, expression) ->
+                EXPRESSION_PARAMETER_PREFIX + (index
+                    + 1);
+
+            String fixedPrefix = parametersShouldBeAccessedByIndex ? "?" : ":";
+
+            BiFunction<String, String, String> parameterNameToReplacement = (prefix, name) -> fixedPrefix + name;
+
+            return SpelQueryContext.of(indexToParameterName, parameterNameToReplacement).parse(queryWithSpel);
+        }
+
+        /** */
+        private static String replaceFirst(String text, String substring, String replacement) {
+            int index = text.indexOf(substring);
+            if (index < 0)
+                return text;
+
+            return text.substring(0, index) + replacement + text.substring(index + substring.length());
+        }
+
+        /** */
+        @Nullable
+        private static Integer getParameterIndex(@Nullable String parameterIndexString) {
+            if (parameterIndexString == null || parameterIndexString.isEmpty())
+                return null;
+            return Integer.valueOf(parameterIndexString);
+        }
+
+        /** */
+        private static int tryFindGreatestParameterIndexIn(String query) {
+            Matcher parameterIndexMatcher = PARAMETER_BINDING_BY_INDEX.matcher(query);
+
+            int greatestParameterIndex = -1;
+            while (parameterIndexMatcher.find()) {
+
+                String parameterIndexString = parameterIndexMatcher.group(1);
+                Integer parameterIndex = getParameterIndex(parameterIndexString);
+                if (parameterIndex != null)
+                    greatestParameterIndex = Math.max(greatestParameterIndex, parameterIndex);
+            }
+
+            return greatestParameterIndex;
+        }
+
+        /** */
+        private static void checkAndRegister(ParameterBinding binding, List<ParameterBinding> bindings) {
+
+            bindings.stream() //
+                .filter(it -> it.hasName(binding.getName()) || it.hasPosition(binding.getPosition())) //
+                .forEach(it -> Assert.isTrue(it.equals(binding), String.format(MESSAGE, it, binding)));
+
+            if (!bindings.contains(binding))
+                bindings.add(binding);
+        }
+
+        /**
+         * An enum for the different types of bindings.
+         *
+         * @author Thomas Darimont
+         * @author Oliver Gierke
+         */
+        private enum ParameterBindingType {
+            // Trailing whitespace is intentional to reflect that the keywords must be used with at least one whitespace
+            // character, while = does not.
+            /** */
+            LIKE("like "),
+
+            /** */
+            IN("in "),
+
+            /** */
+            AS_IS(null);
+
+            /** */
+            @Nullable
+            private final String keyword;
+
+            /** */
+            ParameterBindingType(@Nullable String keyword) {
+                this.keyword = keyword;
+            }
+
+            /**
+             * Returns the keyword that will tirgger the binding type or {@literal null} if the type is not triggered by
+             * a keyword.
+             *
+             * @return the keyword
+             */
+            @Nullable
+            public String getKeyword() {
+                return keyword;
+            }
+
+            /**
+             * Return the appropriate {@link ParameterBindingType} for the given {@link String}. Returns {@literal
+             * #AS_IS} in case no other {@link ParameterBindingType} could be found.
+             */
+            static ParameterBindingType of(String typeSource) {
+                if (!StringUtils.hasText(typeSource))
+                    return AS_IS;
+
+                for (ParameterBindingType type : values()) {
+                    if (type.name().equalsIgnoreCase(typeSource.trim()))
+                        return type;
+                }
+
+                throw new IllegalArgumentException(String.format("Unsupported parameter binding type %s!", typeSource));
+            }
+        }
+    }
+
+    /**
+     * A generic parameter binding with name or position information.
+     *
+     * @author Thomas Darimont
+     */
+    static class ParameterBinding {
+        /** */
+        @Nullable
+        private final String name;
+
+        /** */
+        @Nullable
+        private final String expression;
+
+        /** */
+        @Nullable
+        private final Integer position;
+
+        /**
+         * Creates a new {@link ParameterBinding} for the parameter with the given position.
+         *
+         * @param position must not be {@literal null}.
+         */
+        ParameterBinding(Integer position) {
+            this(null, position, null);
+        }
+
+        /**
+         * Creates a new {@link ParameterBinding} for the parameter with the given name, position and expression
+         * information. Either {@literal name} or {@literal position} must be not {@literal null}.
+         *
+         * @param name       of the parameter may be {@literal null}.
+         * @param position   of the parameter may be {@literal null}.
+         * @param expression the expression to apply to any value for this parameter.
+         */
+        ParameterBinding(@Nullable String name, @Nullable Integer position, @Nullable String expression) {
+
+            if (name == null)
+                Assert.notNull(position, "Position must not be null!");
+
+            if (position == null)
+                Assert.notNull(name, "Name must not be null!");
+
+            this.name = name;
+            this.position = position;
+            this.expression = expression;
+        }
+
+        /**
+         * Returns whether the binding has the given name. Will always be {@literal false} in case the {@link
+         * ParameterBinding} has been set up from a position.
+         */
+        boolean hasName(@Nullable String name) {
+            return position == null && this.name != null && this.name.equals(name);
+        }
+
+        /**
+         * Returns whether the binding has the given position. Will always be {@literal false} in case the {@link
+         * ParameterBinding} has been set up from a name.
+         */
+        boolean hasPosition(@Nullable Integer position) {
+            return position != null && name == null && position.equals(this.position);
+        }
+
+        /**
+         * @return the name
+         */
+        @Nullable
+        public String getName() {
+            return name;
+        }
+
+        /**
+         * @return the name
+         * @throws IllegalStateException if the name is not available.
+         */
+        String getRequiredName() throws IllegalStateException {
+
+            String name = getName();
+
+            if (name != null)
+                return name;
+
+            throw new IllegalStateException(String.format("Required name for %s not available!", this));
+        }
+
+        /**
+         * @return the position
+         */
+        @Nullable
+        Integer getPosition() {
+            return position;
+        }
+
+        /**
+         * @return the position
+         * @throws IllegalStateException if the position is not available.
+         */
+        int getRequiredPosition() throws IllegalStateException {
+
+            Integer position = getPosition();
+
+            if (position != null)
+                return position;
+
+            throw new IllegalStateException(String.format("Required position for %s not available!", this));
+        }
+
+        /**
+         * @return {@literal true} if this parameter binding is a synthetic SpEL expression.
+         */
+        public boolean isExpression() {
+            return expression != null;
+        }
+
+        /** */
+        @Override public int hashCode() {
+
+            int result = 17;
+
+            result += nullSafeHashCode(name);
+            result += nullSafeHashCode(position);
+            result += nullSafeHashCode(expression);
+
+            return result;
+        }
+
+        /** */
+        @Override public boolean equals(Object obj) {
+
+            if (!(obj instanceof ParameterBinding))
+                return false;
+
+            ParameterBinding that = (ParameterBinding)obj;
+
+            return nullSafeEquals(name, that.name) && nullSafeEquals(position, that.position)
+                && nullSafeEquals(expression, that.expression);
+        }
+
+        /** */
+        @Override public String toString() {
+            return String.format("ParameterBinding [name: %s, position: %d, expression: %s]", getName(), getPosition(),
+                getExpression());
+        }
+
+        /**
+         * @param valueToBind value to prepare
+         */
+        @Nullable
+        public Object prepare(@Nullable Object valueToBind) {
+            return valueToBind;
+        }
+
+        /** */
+        @Nullable
+        public String getExpression() {
+            return expression;
+        }
+    }
+
+    /**
+     * Represents a {@link ParameterBinding} in a JPQL query augmented with instructions of how to apply a parameter as
+     * an {@code IN} parameter.
+     *
+     * @author Thomas Darimont
+     */
+    static class InParameterBinding extends ParameterBinding {
+        /**
+         * Creates a new {@link InParameterBinding} for the parameter with the given name.
+         */
+        InParameterBinding(String name, @Nullable String expression) {
+            super(name, null, expression);
+        }
+
+        /**
+         * Creates a new {@link InParameterBinding} for the parameter with the given position.
+         */
+        InParameterBinding(int position, @Nullable String expression) {
+            super(null, position, expression);
+        }
+
+        /*
+         * (non-Javadoc)
+         * @see org.springframework.data.jpa.repository.query.StringQuery.ParameterBinding#prepare(java.lang.Object)
+         */
+        @Override public Object prepare(@Nullable Object value) {
+            if (!ObjectUtils.isArray(value))
+                return value;
+
+            int length = Array.getLength(value);
+            Collection<Object> result = new ArrayList<>(length);
+
+            for (int i = 0; i < length; i++)
+                result.add(Array.get(value, i));
+
+            return result;
+        }
+
+    }
+
+    /**
+     * Represents a parameter binding in a JPQL query augmented with instructions of how to apply a parameter as LIKE
+     * parameter. This allows expressions like {@code …like %?1} in the JPQL query, which is not allowed by plain JPA.
+     *
+     * @author Oliver Gierke
+     * @author Thomas Darimont
+     */
+    static class LikeParameterBinding extends ParameterBinding {
+        /** */
+        private static final List<Type> SUPPORTED_TYPES = Arrays.asList(Type.CONTAINING, Type.STARTING_WITH,
+            Type.ENDING_WITH, Type.LIKE);
+
+        /** */
+        private final Type type;
+
+        /**
+         * Creates a new {@link LikeParameterBinding} for the parameter with the given name and {@link Type}.
+         *
+         * @param name must not be {@literal null} or empty.
+         * @param type must not be {@literal null}.
+         */
+        LikeParameterBinding(String name, Type type) {
+            this(name, type, null);
+        }
+
+        /**
+         * Creates a new {@link LikeParameterBinding} for the parameter with the given name and {@link Type} and
+         * parameter binding input.
+         *
+         * @param name       must not be {@literal null} or empty.
+         * @param type       must not be {@literal null}.
+         * @param expression may be {@literal null}.
+         */
+        LikeParameterBinding(String name, Type type, @Nullable String expression) {
+
+            super(name, null, expression);
+
+            Assert.hasText(name, "Name must not be null or empty!");
+            Assert.notNull(type, "Type must not be null!");
+
+            Assert.isTrue(SUPPORTED_TYPES.contains(type), String.format("Type must be one of %s!",
+                StringUtils.collectionToCommaDelimitedString(SUPPORTED_TYPES)));
+
+            this.type = type;
+        }
+
+        /**
+         * Creates a new {@link LikeParameterBinding} for the parameter with the given position and {@link Type}.
+         *
+         * @param position position of the parameter in the query.
+         * @param type     must not be {@literal null}.
+         */
+        LikeParameterBinding(int position, Type type) {
+            this(position, type, null);
+        }
+
+        /**
+         * Creates a new {@link LikeParameterBinding} for the parameter with the given position and {@link Type}.
+         *
+         * @param position   position of the parameter in the query.
+         * @param type       must not be {@literal null}.
+         * @param expression may be {@literal null}.
+         */
+        LikeParameterBinding(int position, Type type, @Nullable String expression) {
+
+            super(null, position, expression);
+
+            Assert.isTrue(position > 0, "Position must be greater than zero!");
+            Assert.notNull(type, "Type must not be null!");
+
+            Assert.isTrue(SUPPORTED_TYPES.contains(type), String.format("Type must be one of %s!",
+                StringUtils.collectionToCommaDelimitedString(SUPPORTED_TYPES)));
+
+            this.type = type;
+        }
+
+        /**
+         * Returns the {@link Type} of the binding.
+         *
+         * @return the type
+         */
+        public Type getType() {
+            return type;
+        }
+
+        /**
+         * Prepares the given raw keyword according to the like type.
+         */
+        @Nullable
+        @Override public Object prepare(@Nullable Object value) {
+            if (value == null)
+                return null;
+
+            switch (type) {
+                case STARTING_WITH:
+                    return String.format("%s%%", value.toString());
+                case ENDING_WITH:
+                    return String.format("%%%s", value.toString());
+                case CONTAINING:
+                    return String.format("%%%s%%", value.toString());
+                case LIKE:
+                default:
+                    return value;
+            }
+        }
+
+        /** */
+        @Override public boolean equals(Object obj) {
+            if (!(obj instanceof LikeParameterBinding))
+                return false;
+
+            LikeParameterBinding that = (LikeParameterBinding)obj;
+
+            return super.equals(obj) && type.equals(that.type);
+        }
+
+        /** */
+        @Override public int hashCode() {
+
+            int result = super.hashCode();
+
+            result += nullSafeHashCode(type);
+
+            return result;
+        }
+
+        /** */
+        @Override public String toString() {
+            return String.format("LikeBinding [name: %s, position: %d, type: %s]", getName(), getPosition(), type);
+        }
+
+        /**
+         * Extracts the like {@link Type} from the given JPA like expression.
+         *
+         * @param expression must not be {@literal null} or empty.
+         */
+        private static Type getLikeTypeFrom(String expression) {
+
+            Assert.hasText(expression, "Expression must not be null or empty!");
+
+            if (expression.matches("%.*%"))
+                return Type.CONTAINING;
+
+            if (expression.startsWith("%"))
+                return Type.ENDING_WITH;
+
+            if (expression.endsWith("%"))
+                return Type.STARTING_WITH;
+
+            return Type.LIKE;
+        }
+
+    }
+
+    /** */
+    static class Metadata {
+        /**
+         * Uses jdbc style parameters.
+         */
+        private boolean usesJdbcStyleParameters;
+    }
+
+    /**
+     * Value object to analyze a {@link String} to determine the parts of the {@link String} that are quoted and offers
+     * an API to query that information.
+     *
+     * @author Jens Schauder
+     * @author Oliver Gierke
+     */
+    static class QuotationMap {
+        /** */
+        private static final Collection<Character> QUOTING_CHARACTERS = Arrays.asList('"', '\'');
+
+        /** */
+        private final List<Range<Integer>> quotedRanges = new ArrayList<>();
+
+        /**
+         * Creates a new instance for the query.
+         *
+         * @param query can be {@literal null}.
+         */
+        public QuotationMap(@Nullable String query) {
+            if (query == null)
+                return;
+
+            Character inQuotation = null;
+            int start = 0;
+
+            for (int i = 0; i < query.length(); i++) {
+                char currentChar = query.charAt(i);
+
+                if (QUOTING_CHARACTERS.contains(currentChar)) {
+                    if (inQuotation == null) {
+
+                        inQuotation = currentChar;
+                        start = i;
+                    }
+                    else if (currentChar == inQuotation) {
+                        inQuotation = null;
+
+                        quotedRanges.add(Range.from(Bound.inclusive(start)).to(Bound.inclusive(i)));
+                    }
+                }
+            }
+
+            if (inQuotation != null) {
+                throw new IllegalArgumentException(
+                    String.format("The string <%s> starts a quoted range at %d, but never ends it.", query, start));
+            }
+        }
+
+        /**
+         * Checks if a given index is within a quoted range.
+         *
+         * @param idx to check if it is part of a quoted range.
+         * @return whether the query contains a quoted range at {@literal index}.
+         */
+        public boolean isQuoted(int idx) {
+            return quotedRanges.stream().anyMatch(r -> r.contains(idx));
+        }
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/package-info.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/package-info.java
new file mode 100644
index 0000000..cfa3801
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/query/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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 description. -->
+ * Package includes classes that integrates with Apache Ignite SQL engine.
+ */
+package org.apache.ignite.springdata22.repository.query;
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/ConditionFalse.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/ConditionFalse.java
new file mode 100644
index 0000000..a69dc8e
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/ConditionFalse.java
@@ -0,0 +1,32 @@
+/*
+ * 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.springdata22.repository.support;
+
+import org.springframework.context.annotation.Condition;
+import org.springframework.context.annotation.ConditionContext;
+import org.springframework.core.type.AnnotatedTypeMetadata;
+
+/**
+ * Always false condition. Tells spring context never load bean with such Condition.
+ */
+public class ConditionFalse implements Condition {
+    /** {@inheritDoc} */
+    @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
+        return false;
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/IgniteRepositoryFactory.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/IgniteRepositoryFactory.java
new file mode 100644
index 0000000..97d31e9
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/IgniteRepositoryFactory.java
@@ -0,0 +1,274 @@
+/*
+ * 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.springdata22.repository.support;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.springdata22.repository.IgniteRepository;
+import org.apache.ignite.springdata22.repository.config.DynamicQueryConfig;
+import org.apache.ignite.springdata22.repository.config.Query;
+import org.apache.ignite.springdata22.repository.config.RepositoryConfig;
+import org.apache.ignite.springdata22.repository.query.IgniteQuery;
+import org.apache.ignite.springdata22.repository.query.IgniteQueryGenerator;
+import org.apache.ignite.springdata22.repository.query.IgniteRepositoryQuery;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanExpressionContext;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.expression.StandardBeanExpressionResolver;
+import org.springframework.data.repository.core.EntityInformation;
+import org.springframework.data.repository.core.RepositoryInformation;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.core.support.AbstractEntityInformation;
+import org.springframework.data.repository.core.support.RepositoryFactorySupport;
+import org.springframework.data.repository.query.QueryLookupStrategy;
+import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Crucial for spring-data functionality class. Create proxies for repositories.
+ * <p>
+ * Supports multiple Ignite Instances on same JVM.
+ * <p>
+ * This is pretty useful working with Spring repositories bound to different Ignite intances within same application.
+ *
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+public class IgniteRepositoryFactory extends RepositoryFactorySupport {
+    /** Spring application context */
+    private final ApplicationContext ctx;
+
+    /** Spring application bean factory */
+    private final DefaultListableBeanFactory beanFactory;
+
+    /** Spring application expression resolver */
+    private final StandardBeanExpressionResolver resolver = new StandardBeanExpressionResolver();
+
+    /** Spring application bean expression context */
+    private final BeanExpressionContext beanExpressionContext;
+
+    /** Mapping of a repository to a cache. */
+    private final Map<Class<?>, String> repoToCache = new HashMap<>();
+
+    /** Mapping of a repository to a ignite instance. */
+    private final Map<Class<?>, Ignite> repoToIgnite = new HashMap<>();
+
+    /**
+     * Creates the factory with initialized {@link Ignite} instance.
+     *
+     * @param ctx the ctx
+     */
+    public IgniteRepositoryFactory(ApplicationContext ctx) {
+        this.ctx = ctx;
+
+        beanFactory = new DefaultListableBeanFactory(ctx.getAutowireCapableBeanFactory());
+
+        beanExpressionContext = new BeanExpressionContext(beanFactory, null);
+    }
+
+    /** */
+    private Ignite igniteForRepoConfig(RepositoryConfig config) {
+        try {
+            String igniteInstanceName = evaluateExpression(config.igniteInstance());
+            return (Ignite)ctx.getBean(igniteInstanceName);
+        }
+        catch (BeansException ex) {
+            try {
+                String igniteConfigName = evaluateExpression(config.igniteCfg());
+                IgniteConfiguration cfg = (IgniteConfiguration)ctx.getBean(igniteConfigName);
+                try {
+                    // first try to attach to existing ignite instance
+                    return Ignition.ignite(cfg.getIgniteInstanceName());
+                }
+                catch (Exception ignored) {
+                    // nop
+                }
+                return Ignition.start(cfg);
+            }
+            catch (BeansException ex2) {
+                try {
+                    String igniteSpringCfgPath = evaluateExpression(config.igniteSpringCfgPath());
+                    String path = (String)ctx.getBean(igniteSpringCfgPath);
+                    return Ignition.start(path);
+                }
+                catch (BeansException ex3) {
+                    throw new IgniteException("Failed to initialize Ignite repository factory. Ignite instance or"
+                        + " IgniteConfiguration or a path to Ignite's spring XML "
+                        + "configuration must be defined in the"
+                        + " application configuration");
+                }
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public <T, ID> EntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
+        return new AbstractEntityInformation<T, ID>(domainClass) {
+            /** {@inheritDoc} */
+            @Override public ID getId(T entity) {
+                return null;
+            }
+
+            /** {@inheritDoc} */
+            @Override public Class<ID> getIdType() {
+                return null;
+            }
+        };
+    }
+
+    /** {@inheritDoc} */
+    @Override protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
+        return IgniteRepositoryImpl.class;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected synchronized RepositoryMetadata getRepositoryMetadata(Class<?> repoItf) {
+        Assert.notNull(repoItf, "Repository interface must be set.");
+        Assert.isAssignable(IgniteRepository.class, repoItf, "Repository must implement IgniteRepository interface.");
+
+        RepositoryConfig annotation = repoItf.getAnnotation(RepositoryConfig.class);
+
+        Assert.notNull(annotation, "Set a name of an Apache Ignite cache using @RepositoryConfig annotation to map "
+            + "this repository to the underlying cache.");
+
+        Assert.hasText(annotation.cacheName(), "Set a name of an Apache Ignite cache using @RepositoryConfig "
+            + "annotation to map this repository to the underlying cache.");
+
+        String cacheName = evaluateExpression(annotation.cacheName());
+
+        repoToCache.put(repoItf, cacheName);
+
+        repoToIgnite.put(repoItf, igniteForRepoConfig(annotation));
+
+        return super.getRepositoryMetadata(repoItf);
+    }
+
+    /**
+     * Evaluate the SpEL expression
+     *
+     * @param spelExpression SpEL expression
+     * @return the result of execution of the SpEL expression
+     */
+    private String evaluateExpression(String spelExpression) {
+        return (String)resolver.evaluate(spelExpression, beanExpressionContext);
+    }
+
+    /** Control underlying cache creation to avoid cache creation by mistake */
+    private IgniteCache getRepositoryCache(Class<?> repoIf) {
+        Ignite ignite = repoToIgnite.get(repoIf);
+
+        RepositoryConfig config = repoIf.getAnnotation(RepositoryConfig.class);
+
+        String cacheName = repoToCache.get(repoIf);
+
+        IgniteCache c = config.autoCreateCache() ? ignite.getOrCreateCache(cacheName) : ignite.cache(cacheName);
+
+        if (c == null) {
+            throw new IllegalStateException(
+                "Cache '" + cacheName + "' not found for repository interface " + repoIf.getName()
+                    + ". Please, add a cache configuration to ignite configuration"
+                    + " or pass autoCreateCache=true to org.apache.ignite.springdata22"
+                    + ".repository.config.RepositoryConfig annotation.");
+        }
+
+        return c;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected Object getTargetRepository(RepositoryInformation metadata) {
+        Ignite ignite = repoToIgnite.get(metadata.getRepositoryInterface());
+
+        return getTargetRepositoryViaReflection(metadata, ignite,
+            getRepositoryCache(metadata.getRepositoryInterface()));
+    }
+
+    /** {@inheritDoc} */
+    @Override protected Optional<QueryLookupStrategy> getQueryLookupStrategy(final QueryLookupStrategy.Key key,
+        QueryMethodEvaluationContextProvider evaluationContextProvider) {
+        return Optional.of((mtd, metadata, factory, namedQueries) -> {
+            final Query annotation = mtd.getAnnotation(Query.class);
+            final Ignite ignite = repoToIgnite.get(metadata.getRepositoryInterface());
+
+            if (annotation != null && (StringUtils.hasText(annotation.value()) || annotation.textQuery() || annotation
+                .dynamicQuery())) {
+
+                String qryStr = annotation.value();
+
+                boolean annotatedIgniteQuery = !annotation.dynamicQuery() && (StringUtils.hasText(qryStr) || annotation
+                    .textQuery());
+
+                IgniteQuery query = annotatedIgniteQuery ? new IgniteQuery(qryStr,
+                    !annotation.textQuery() && (isFieldQuery(qryStr) || annotation.forceFieldsQuery()),
+                    annotation.textQuery(), false, IgniteQueryGenerator.getOptions(mtd)) : null;
+
+                if (key != QueryLookupStrategy.Key.CREATE) {
+                    return new IgniteRepositoryQuery(ignite, metadata, query, mtd, factory,
+                        getRepositoryCache(metadata.getRepositoryInterface()),
+                        annotatedIgniteQuery ? DynamicQueryConfig.fromQueryAnnotation(annotation) : null,
+                        evaluationContextProvider);
+                }
+            }
+
+            if (key == QueryLookupStrategy.Key.USE_DECLARED_QUERY) {
+                throw new IllegalStateException("To use QueryLookupStrategy.Key.USE_DECLARED_QUERY, pass "
+                    + "a query string via org.apache.ignite.springdata22.repository"
+                    + ".config.Query annotation.");
+            }
+
+            return new IgniteRepositoryQuery(ignite, metadata, IgniteQueryGenerator.generateSql(mtd, metadata), mtd,
+                factory, getRepositoryCache(metadata.getRepositoryInterface()),
+                DynamicQueryConfig.fromQueryAnnotation(annotation), evaluationContextProvider);
+        });
+    }
+
+    /**
+     * @param qry Query string.
+     * @return {@code true} if query is SqlFieldsQuery.
+     */
+    public static boolean isFieldQuery(String qry) {
+        String qryUpperCase = qry.toUpperCase();
+
+        return isStatement(qryUpperCase) && !qryUpperCase.matches("^SELECT\\s+(?:\\w+\\.)?+\\*.*");
+    }
+
+    /**
+     * Evaluates if the query starts with a clause.<br>
+     * <code>SELECT, INSERT, UPDATE, MERGE, DELETE</code>
+     *
+     * @param qryUpperCase Query string in upper case.
+     * @return {@code true} if query is full SQL statement.
+     */
+    private static boolean isStatement(String qryUpperCase) {
+        return qryUpperCase.matches("^\\s*SELECT\\b.*") ||
+            // update
+            qryUpperCase.matches("^\\s*UPDATE\\b.*") ||
+            // delete
+            qryUpperCase.matches("^\\s*DELETE\\b.*") ||
+            // merge
+            qryUpperCase.matches("^\\s*MERGE\\b.*") ||
+            // insert
+            qryUpperCase.matches("^\\s*INSERT\\b.*");
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/IgniteRepositoryFactoryBean.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/IgniteRepositoryFactoryBean.java
new file mode 100644
index 0000000..ece9f31
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/IgniteRepositoryFactoryBean.java
@@ -0,0 +1,67 @@
+/*
+ * 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.springdata22.repository.support;
+
+import java.io.Serializable;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.springdata22.repository.IgniteRepository;
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.data.repository.Repository;
+import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
+import org.springframework.data.repository.core.support.RepositoryFactorySupport;
+
+/**
+ * Apache Ignite repository factory bean.
+ * <p>
+ * The {@link org.apache.ignite.springdata22.repository.config.RepositoryConfig} requires to define one of the
+ * parameters below in your Spring application configuration in order to get an access to Apache Ignite cluster:
+ * <ul>
+ * <li>{@link Ignite} instance bean named "igniteInstance" by default</li>
+ * <li>{@link IgniteConfiguration} bean named "igniteCfg" by default</li>
+ * <li>A path to Ignite's Spring XML configuration named "igniteSpringCfgPath" by default</li>
+ * <ul/>
+ *
+ * @param <T> Repository type, {@link IgniteRepository}
+ * @param <V> Domain object class.
+ * @param <K> Domain object key, super expects {@link Serializable}.
+ */
+public class IgniteRepositoryFactoryBean<T extends Repository<V, K>, V, K extends Serializable>
+    extends RepositoryFactoryBeanSupport<T, V, K> implements ApplicationContextAware {
+    /** */
+    private ApplicationContext ctx;
+
+    /**
+     * @param repoInterface Repository interface.
+     */
+    protected IgniteRepositoryFactoryBean(Class<? extends T> repoInterface) {
+        super(repoInterface);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void setApplicationContext(ApplicationContext ctx) throws BeansException {
+        this.ctx = ctx;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected RepositoryFactorySupport createRepositoryFactory() {
+        return new IgniteRepositoryFactory(ctx);
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/IgniteRepositoryImpl.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/IgniteRepositoryImpl.java
new file mode 100644
index 0000000..c4d9834
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/IgniteRepositoryImpl.java
@@ -0,0 +1,221 @@
+/*
+ * 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.springdata22.repository.support;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+import javax.cache.Cache;
+import javax.cache.expiry.ExpiryPolicy;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.CachePeekMode;
+import org.apache.ignite.springdata22.repository.IgniteRepository;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.context.annotation.Conditional;
+
+/**
+ * General Apache Ignite repository implementation. This bean should've never been loaded by context directly, only via
+ * {@link IgniteRepositoryFactory}
+ *
+ * @param <V> the cache value type
+ * @param <K> the cache key type
+ * @author Apache Ignite Team
+ * @author Manuel Núñez (manuel.nunez@hawkore.com)
+ */
+@Conditional(ConditionFalse.class)
+public class IgniteRepositoryImpl<V, K extends Serializable> implements IgniteRepository<V, K> {
+    /**
+     * Ignite Cache bound to the repository
+     */
+    private final IgniteCache<K, V> cache;
+
+    /**
+     * Ignite instance bound to the repository
+     */
+    private final Ignite ignite;
+
+    /**
+     * Repository constructor.
+     *
+     * @param ignite the ignite
+     * @param cache  Initialized cache instance.
+     */
+    public IgniteRepositoryImpl(Ignite ignite, IgniteCache<K, V> cache) {
+        this.cache = cache;
+        this.ignite = ignite;
+    }
+
+    /** {@inheritDoc} */
+    @Override public IgniteCache<K, V> cache() {
+        return cache;
+    }
+
+    /** {@inheritDoc} */
+    @Override public Ignite ignite() {
+        return ignite;
+    }
+
+    /** {@inheritDoc} */
+    @Override public <S extends V> S save(K key, S entity) {
+        cache.put(key, entity);
+
+        return entity;
+    }
+
+    /** {@inheritDoc} */
+    @Override public <S extends V> Iterable<S> save(Map<K, S> entities) {
+        cache.putAll(entities);
+
+        return entities.values();
+    }
+
+    /** {@inheritDoc} */
+    @Override public <S extends V> S save(K key, S entity, @Nullable ExpiryPolicy expiryPlc) {
+        if (expiryPlc != null)
+            cache.withExpiryPolicy(expiryPlc).put(key, entity);
+        else
+            cache.put(key, entity);
+        return entity;
+    }
+
+    /** {@inheritDoc} */
+    @Override public <S extends V> Iterable<S> save(Map<K, S> entities, @Nullable ExpiryPolicy expiryPlc) {
+        if (expiryPlc != null)
+            cache.withExpiryPolicy(expiryPlc).putAll(entities);
+        else
+            cache.putAll(entities);
+        return entities.values();
+    }
+
+    /**
+     * Not implemented.
+     */
+    @Override public <S extends V> S save(S entity) {
+        throw new UnsupportedOperationException("Use IgniteRepository.save(key,value) method instead.");
+    }
+
+    /**
+     * Not implemented.
+     */
+    @Override public <S extends V> Iterable<S> saveAll(Iterable<S> entities) {
+        throw new UnsupportedOperationException("Use IgniteRepository.save(Map<keys,value>) method instead.");
+    }
+
+    /** {@inheritDoc} */
+    @Override public Optional<V> findById(K id) {
+        return Optional.ofNullable(cache.get(id));
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean existsById(K id) {
+        return cache.containsKey(id);
+    }
+
+    /** {@inheritDoc} */
+    @Override public Iterable<V> findAll() {
+        final Iterator<Cache.Entry<K, V>> iter = cache.iterator();
+
+        return new Iterable<V>() {
+            /** */
+            @Override public Iterator<V> iterator() {
+                return new Iterator<V>() {
+                    /** {@inheritDoc} */
+                    @Override public boolean hasNext() {
+                        return iter.hasNext();
+                    }
+
+                    /** {@inheritDoc} */
+                    @Override public V next() {
+                        return iter.next().getValue();
+                    }
+
+                    /** {@inheritDoc} */
+                    @Override public void remove() {
+                        iter.remove();
+                    }
+                };
+            }
+        };
+    }
+
+    /** {@inheritDoc} */
+    @Override public Iterable<V> findAllById(Iterable<K> ids) {
+        if (ids instanceof Set)
+            return cache.getAll((Set<K>)ids).values();
+
+        if (ids instanceof Collection)
+            return cache.getAll(new HashSet<>((Collection<K>)ids)).values();
+
+        TreeSet<K> keys = new TreeSet<>();
+
+        for (K id : ids)
+            keys.add(id);
+
+        return cache.getAll(keys).values();
+    }
+
+    /** {@inheritDoc} */
+    @Override public long count() {
+        return cache.size(CachePeekMode.PRIMARY);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void deleteById(K id) {
+        cache.remove(id);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void delete(V entity) {
+        throw new UnsupportedOperationException("Use IgniteRepository.deleteById(key) method instead.");
+    }
+
+    /** {@inheritDoc} */
+    @Override public void deleteAll(Iterable<? extends V> entities) {
+        throw new UnsupportedOperationException("Use IgniteRepository.deleteAllById(keys) method instead.");
+    }
+
+    /** {@inheritDoc} */
+    @Override public void deleteAllById(Iterable<K> ids) {
+        if (ids instanceof Set) {
+            cache.removeAll((Set<K>)ids);
+            return;
+        }
+
+        if (ids instanceof Collection) {
+            cache.removeAll(new HashSet<>((Collection<K>)ids));
+            return;
+        }
+
+        TreeSet<K> keys = new TreeSet<>();
+
+        for (K id : ids)
+            keys.add(id);
+
+        cache.removeAll(keys);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void deleteAll() {
+        cache.clear();
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/package-info.java b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/package-info.java
new file mode 100644
index 0000000..cc15fc7
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/main/java/org/apache/ignite/springdata22/repository/support/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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 description. -->
+ * Package contains supporting files required by Spring Data framework.
+ */
+package org.apache.ignite.springdata22.repository.support;
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCompoundKeyTest.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCompoundKeyTest.java
new file mode 100644
index 0000000..c4f9c0e
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCompoundKeyTest.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.springdata;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.Statement;
+import java.util.Optional;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.springdata.compoundkey.City;
+import org.apache.ignite.springdata.compoundkey.CityKey;
+import org.apache.ignite.springdata.compoundkey.CityRepository;
+import org.apache.ignite.springdata.compoundkey.CompoundKeyApplicationConfiguration;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+
+/**
+ * Test with using conpoud key in spring-data
+ * */
+public class IgniteSpringDataCompoundKeyTest extends GridCommonAbstractTest {
+    /** Application context */
+    private static AnnotationConfigApplicationContext ctx;
+
+    /** City repository */
+    private static CityRepository repo;
+
+    /** Cache name */
+    private static final String CACHE_NAME = "City";
+
+    /** Cities count */
+    private static final int TOTAL_COUNT = 5;
+
+    /** Count Afganistan cities */
+    private static final int AFG_COUNT = 4;
+
+    /** Kabul identifier */
+    private static final int KABUL_ID = 1;
+
+    /** Quandahar identifier */
+    private static final int QUANDAHAR_ID = 2;
+
+    /** Afganistan county code */
+    private static final String AFG = "AFG";
+
+    /** test city Kabul */
+    private static final City KABUL = new City("Kabul", "Kabol", 1780000);
+
+    /** test city Quandahar */
+    private static final City QUANDAHAR = new City("Qandahar","Qandahar", 237500);
+
+    /**
+     * Performs context initialization before tests.
+     */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        ctx = new AnnotationConfigApplicationContext();
+        ctx.register(CompoundKeyApplicationConfiguration.class);
+        ctx.refresh();
+
+        repo = ctx.getBean(CityRepository.class);
+    }
+
+    /**
+     * Load data
+     * */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        loadData();
+
+        assertEquals(TOTAL_COUNT, repo.count());
+    }
+
+    /**
+     * Performs context destroy after tests.
+     */
+    @Override protected void afterTestsStopped() {
+        ctx.close();
+    }
+
+    /** load data*/
+    public void loadData() throws Exception {
+        Ignite ignite = ctx.getBean(Ignite.class);
+
+        if (ignite.cacheNames().contains(CACHE_NAME))
+            ignite.destroyCache(CACHE_NAME);
+
+        try (Connection conn = DriverManager.getConnection("jdbc:ignite:thin://127.0.0.1:" + CompoundKeyApplicationConfiguration.CLI_CONN_PORT + '/')) {
+            Statement st = conn.createStatement();
+
+            st.execute("DROP TABLE IF EXISTS City");
+            st.execute("CREATE TABLE City (ID INT, Name VARCHAR, CountryCode CHAR(3), District VARCHAR, Population INT, PRIMARY KEY (ID, CountryCode)) WITH \"template=partitioned, backups=1, affinityKey=CountryCode, CACHE_NAME=City, KEY_TYPE=org.apache.ignite.springdata.compoundkey.CityKey, VALUE_TYPE=org.apache.ignite.springdata.compoundkey.City\"");
+            st.execute("SET STREAMING ON;");
+            st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (1,'Kabul','AFG','Kabol',1780000)");
+            st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (2,'Qandahar','AFG','Qandahar',237500)");
+            st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (3,'Herat','AFG','Herat',186800)");
+            st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (4,'Mazar-e-Sharif','AFG','Balkh',127800)");
+            st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (5,'Amsterdam','NLD','Noord-Holland',731200)");
+        }
+    }
+
+    /** Test */
+    @Test
+    public void test() {
+        assertEquals(Optional.of(KABUL), repo.findById(new CityKey(KABUL_ID, AFG)));
+        assertEquals(AFG_COUNT, repo.findByCountryCode(AFG).size());
+        assertEquals(QUANDAHAR, repo.findById(QUANDAHAR_ID));
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfExpressionTest.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfExpressionTest.java
new file mode 100644
index 0000000..d9de9b7
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfExpressionTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.springdata;
+
+import java.util.Collection;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.springdata.misc.ApplicationConfiguration;
+import org.apache.ignite.springdata.misc.Person;
+import org.apache.ignite.springdata.misc.PersonExpressionRepository;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+
+/**
+ * Test with using repository which is configured by Spring EL
+ */
+public class IgniteSpringDataCrudSelfExpressionTest extends GridCommonAbstractTest {
+    /** Number of entries to store */
+    private static final int CACHE_SIZE = 1000;
+
+    /** Repository. */
+    private static PersonExpressionRepository repo;
+
+    /** Context. */
+    private static AnnotationConfigApplicationContext ctx;
+
+    /** */
+    @Rule
+    public final ExpectedException expected = ExpectedException.none();
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        ctx = new AnnotationConfigApplicationContext();
+        ctx.register(ApplicationConfiguration.class);
+        ctx.refresh();
+
+        repo = ctx.getBean(PersonExpressionRepository.class);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        fillInRepository();
+
+        assertEquals(CACHE_SIZE, repo.count());
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        repo.deleteAll();
+
+        assertEquals(0, repo.count());
+
+        super.afterTest();
+    }
+
+    /** */
+    private void fillInRepository() {
+        for (int i = 0; i < CACHE_SIZE - 5; i++) {
+            repo.save(i, new Person("person" + Integer.toHexString(i),
+                "lastName" + Integer.toHexString((i + 16) % 256)));
+        }
+
+        repo.save((int) repo.count(), new Person("uniquePerson", "uniqueLastName"));
+        repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+        repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+        repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+        repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTestsStopped() {
+        ctx.close();
+    }
+
+    /**
+     * Tests put & get operations.
+     */
+    @Test
+    public void testPutGet() {
+        Person person = new Person("some_name", "some_surname");
+
+        int id = CACHE_SIZE + 1;
+
+        assertEquals(person, repo.save(id, person));
+
+        assertTrue(repo.existsById(id));
+
+        assertEquals(person, repo.findById(id).get());
+
+        expected.expect(UnsupportedOperationException.class);
+        expected.expectMessage("Use IgniteRepository.save(key,value) method instead.");
+        repo.save(person);
+    }
+
+    /**
+     * Tests SpEL expression.
+     */
+    @Test
+    public void testCacheCount() {
+        Ignite ignite = ctx.getBean("igniteInstance", Ignite.class);
+
+        Collection<String> cacheNames = ignite.cacheNames();
+
+        assertFalse("The SpEL \"#{cacheNames.personCacheName}\" isn't processed!",
+            cacheNames.contains("#{cacheNames.personCacheName}"));
+
+        assertTrue("Cache \"PersonCache\" isn't found!",
+            cacheNames.contains("PersonCache"));
+    }
+
+    /** */
+    @Test
+    public void testCacheCountTWO() {
+        Ignite ignite = ctx.getBean("igniteInstanceTWO", Ignite.class);
+
+        Collection<String> cacheNames = ignite.cacheNames();
+
+        assertFalse("The SpEL \"#{cacheNames.personCacheName}\" isn't processed!",
+            cacheNames.contains("#{cacheNames.personCacheName}"));
+
+        assertTrue("Cache \"PersonCache\" isn't found!",
+            cacheNames.contains("PersonCache"));
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfTest.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfTest.java
new file mode 100644
index 0000000..38785ef
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfTest.java
@@ -0,0 +1,447 @@
+/*
+ * 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.springdata;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.TreeSet;
+import org.apache.ignite.springdata.misc.ApplicationConfiguration;
+import org.apache.ignite.springdata.misc.FullNameProjection;
+import org.apache.ignite.springdata.misc.Person;
+import org.apache.ignite.springdata.misc.PersonProjection;
+import org.apache.ignite.springdata.misc.PersonRepository;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+
+/**
+ * CRUD tests.
+ */
+public class IgniteSpringDataCrudSelfTest extends GridCommonAbstractTest {
+    /** Repository. */
+    private static PersonRepository repo;
+
+    /** Context. */
+    private static AnnotationConfigApplicationContext ctx;
+
+    /** Number of entries to store */
+    private static int CACHE_SIZE = 1000;
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        ctx = new AnnotationConfigApplicationContext();
+
+        ctx.register(ApplicationConfiguration.class);
+
+        ctx.refresh();
+
+        repo = ctx.getBean(PersonRepository.class);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        fillInRepository();
+
+        assertEquals(CACHE_SIZE, repo.count());
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        repo.deleteAll();
+
+        assertEquals(0, repo.count());
+
+        super.afterTest();
+    }
+
+    /** */
+    private void fillInRepository() {
+        for (int i = 0; i < CACHE_SIZE - 5; i++) {
+            repo.save(i, new Person("person" + Integer.toHexString(i),
+                "lastName" + Integer.toHexString((i + 16) % 256)));
+        }
+
+        repo.save((int) repo.count(), new Person("uniquePerson", "uniqueLastName"));
+        repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+        repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+        repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+        repo.save((int) repo.count(), new Person("nonUniquePerson", "nonUniqueLastName"));
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTestsStopped() throws Exception {
+        ctx.destroy();
+    }
+
+    /** */
+    @Test
+    public void testPutGet() {
+        Person person = new Person("some_name", "some_surname");
+
+        int id = CACHE_SIZE + 1;
+
+        assertEquals(person, repo.save(id, person));
+
+        assertTrue(repo.existsById(id));
+
+        assertEquals(person, repo.findById(id).get());
+
+        try {
+            repo.save(person);
+
+            fail("Managed to save a Person without ID");
+        }
+        catch (UnsupportedOperationException e) {
+            //excepted
+        }
+    }
+
+    /** */
+    @Test
+    public void testPutAllGetAll() {
+        LinkedHashMap<Integer, Person> map = new LinkedHashMap<>();
+
+        for (int i = CACHE_SIZE; i < CACHE_SIZE + 50; i++)
+            map.put(i, new Person("some_name" + i, "some_surname" + i));
+
+        Iterator<Person> persons = repo.save(map).iterator();
+
+        assertEquals(CACHE_SIZE + 50, repo.count());
+
+        Iterator<Person> origPersons = map.values().iterator();
+
+        while (persons.hasNext())
+            assertEquals(origPersons.next(), persons.next());
+
+        try {
+            repo.saveAll(map.values());
+
+            fail("Managed to save a list of Persons with ids");
+        }
+        catch (UnsupportedOperationException e) {
+            //expected
+        }
+
+        persons = repo.findAllById(map.keySet()).iterator();
+
+        int counter = 0;
+
+        while (persons.hasNext()) {
+            persons.next();
+            counter++;
+        }
+
+        assertEquals(map.size(), counter);
+    }
+
+    /** */
+    @Test
+    public void testGetAll() {
+        assertEquals(CACHE_SIZE, repo.count());
+
+        Iterator<Person> persons = repo.findAll().iterator();
+
+        int counter = 0;
+
+        while (persons.hasNext()) {
+            persons.next();
+            counter++;
+        }
+
+        assertEquals(repo.count(), counter);
+    }
+
+    /** */
+    @Test
+    public void testDelete() {
+        assertEquals(CACHE_SIZE, repo.count());
+
+        repo.deleteById(0);
+
+        assertEquals(CACHE_SIZE - 1, repo.count());
+        assertEquals(Optional.empty(),repo.findById(0));
+
+        try {
+            repo.delete(new Person("", ""));
+
+            fail("Managed to delete a Person without id");
+        }
+        catch (UnsupportedOperationException e) {
+            //expected
+        }
+    }
+
+    /** */
+    @Test
+    public void testDeleteSet() {
+        assertEquals(CACHE_SIZE, repo.count());
+
+        TreeSet<Integer> ids = new TreeSet<>();
+
+        for (int i = 0; i < CACHE_SIZE / 2; i++)
+            ids.add(i);
+
+        repo.deleteAllById(ids);
+
+        assertEquals(CACHE_SIZE / 2, repo.count());
+
+        try {
+            ArrayList<Person> persons = new ArrayList<>();
+
+            for (int i = 0; i < 3; i++)
+                persons.add(new Person(String.valueOf(i), String.valueOf(i)));
+
+            repo.deleteAll(persons);
+
+            fail("Managed to delete Persons without ids");
+        }
+        catch (UnsupportedOperationException e) {
+            //expected
+        }
+    }
+
+    /** */
+    @Test
+    public void testDeleteAll() {
+        assertEquals(CACHE_SIZE, repo.count());
+
+        repo.deleteAll();
+
+        assertEquals(0, repo.count());
+    }
+
+    /**
+     * Delete existing record.
+     */
+    @Test
+    public void testDeleteByFirstName() {
+        assertEquals(repo.countByFirstNameLike("uniquePerson"), 1);
+
+        long cnt = repo.deleteByFirstName("uniquePerson");
+
+        assertEquals(1, cnt);
+    }
+
+    /**
+     * Delete NON existing record.
+     */
+    @Test
+    public void testDeleteExpression() {
+        long cnt = repo.deleteByFirstName("880");
+
+        assertEquals(0, cnt);
+    }
+
+    /**
+     * Delete Multiple records due to where.
+     */
+    @Test
+    public void testDeleteExpressionMultiple() {
+        long count = repo.countByFirstName("nonUniquePerson");
+        long cnt = repo.deleteByFirstName("nonUniquePerson");
+
+        assertEquals(cnt, count);
+    }
+
+    /**
+     * Remove should do the same than Delete.
+     */
+    @Test
+    public void testRemoveExpression() {
+        repo.removeByFirstName("person3f");
+
+        long count = repo.count();
+        assertEquals(CACHE_SIZE - 1, count);
+    }
+
+    /**
+     * Delete unique record using lower case key word.
+     */
+    @Test
+    public void testDeleteQuery() {
+        repo.deleteBySecondNameLowerCase("uniqueLastName");
+
+        long countAfter = repo.count();
+        assertEquals(CACHE_SIZE - 1, countAfter);
+    }
+
+    /**
+     * Try to delete with a wrong @Query.
+     */
+    @Test
+    public void testWrongDeleteQuery() {
+        long countBefore = repo.countByFirstNameLike("person3f");
+
+        try {
+            repo.deleteWrongByFirstNameQuery("person3f");
+        }
+        catch (Exception e) {
+            //expected
+        }
+
+        long countAfter = repo.countByFirstNameLike("person3f");
+        assertEquals(countBefore, countAfter);
+    }
+
+    /**
+     * Update with a @Query a record.
+     */
+    @Test
+    public void testUpdateQueryMixedCase() {
+        final String newSecondName = "updatedUniqueSecondName";
+        int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+        assertEquals(1, cnt);
+
+        List<Person> person = repo.findByFirstName("uniquePerson");
+        assertEquals(person.get(0).getSecondName(), "updatedUniqueSecondName");
+    }
+
+    /**
+     * Update with a @Query a record
+     */
+    @Test
+    public void testUpdateQueryMixedCaseProjection() {
+        final String newSecondName = "updatedUniqueSecondName1";
+        int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+        assertEquals(1, cnt);
+
+        List<PersonProjection> person = repo.queryByFirstNameWithProjection("uniquePerson");
+        assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName1");
+    }
+
+    /** */
+    @Test
+    public void testUpdateQueryMixedCaseProjectionNamedParameter() {
+        final String newSecondName = "updatedUniqueSecondName2";
+        int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+        assertEquals(1, cnt);
+
+        List<PersonProjection> person = repo.queryByFirstNameWithProjectionNamedParameter("uniquePerson");
+        assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName2");
+    }
+
+    /** */
+    @Test
+    public void testUpdateQueryMixedCaseDynamicProjectionNamedParameter() {
+        final String newSecondName = "updatedUniqueSecondName2";
+        int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+        assertEquals(1, cnt);
+
+        List<PersonProjection> person = repo.queryByFirstNameWithProjectionNamedParameter(PersonProjection.class, "uniquePerson");
+        assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName2");
+
+        List<FullNameProjection> personFullName = repo.queryByFirstNameWithProjectionNamedParameter(FullNameProjection.class, "uniquePerson");
+        assertEquals(personFullName.get(0).getFullName(), "uniquePerson updatedUniqueSecondName2");
+    }
+
+    /** */
+    @Test
+    public void testUpdateQueryOneMixedCaseDynamicProjectionNamedParameter() {
+        final String newSecondName = "updatedUniqueSecondName2";
+        int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+        assertEquals(1, cnt);
+
+        PersonProjection person = repo.queryOneByFirstNameWithProjectionNamedParameter(PersonProjection.class, "uniquePerson");
+        assertEquals(person.getFullName(), "uniquePerson updatedUniqueSecondName2");
+
+        FullNameProjection personFullName = repo.queryOneByFirstNameWithProjectionNamedParameter(FullNameProjection.class, "uniquePerson");
+        assertEquals(personFullName.getFullName(), "uniquePerson updatedUniqueSecondName2");
+    }
+
+    /** */
+    @Test
+    public void testUpdateQueryMixedCaseProjectionIndexedParameter() {
+        final String newSecondName = "updatedUniqueSecondName3";
+        int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+        assertEquals(1, cnt);
+
+        List<PersonProjection> person = repo.queryByFirstNameWithProjectionNamedIndexedParameter("notUsed","uniquePerson");
+        assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName3");
+    }
+
+    /** */
+    @Test
+    public void testUpdateQueryMixedCaseProjectionIndexedParameterLuceneTextQuery() {
+        final String newSecondName = "updatedUniqueSecondName4";
+        int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+        assertEquals(1, cnt);
+
+        List<PersonProjection> person = repo.textQueryByFirstNameWithProjectionNamedParameter("uniquePerson");
+        assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName4");
+    }
+
+    /** */
+    @Test
+    public void testUpdateQueryMixedCaseProjectionNamedParameterAndTemplateDomainEntityVariable() {
+        final String newSecondName = "updatedUniqueSecondName5";
+        int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+        assertEquals(1, cnt);
+
+        List<PersonProjection> person = repo.queryByFirstNameWithProjectionNamedParameterAndTemplateDomainEntityVariable("uniquePerson");
+        assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName5");
+    }
+
+    /** */
+    @Test
+    public void testUpdateQueryMixedCaseProjectionNamedParameterWithSpELExtension() {
+        final String newSecondName = "updatedUniqueSecondName6";
+        int cnt = repo.setFixedSecondNameMixedCase(newSecondName, "uniquePerson");
+
+        assertEquals(1, cnt);
+
+        List<PersonProjection> person = repo.queryByFirstNameWithProjectionNamedParameterWithSpELExtension("uniquePerson");
+        assertEquals(person.get(0).getFullName(), "uniquePerson updatedUniqueSecondName6");
+        assertEquals(person.get(0).getFirstName(), person.get(0).getFirstNameTransformed());
+    }
+
+    /**
+     * Update with a wrong @Query
+     */
+    @Test
+    public void testWrongUpdateQuery() {
+        final String newSecondName = "updatedUniqueSecondName";
+        int rowsUpdated = 0;
+
+        try {
+            rowsUpdated = repo.setWrongFixedSecondName(newSecondName, "uniquePerson");
+        }
+        catch (Exception ignored) {
+            //expected
+        }
+
+        assertEquals(0, rowsUpdated);
+
+        List<Person> person = repo.findByFirstName("uniquePerson");
+        assertEquals(person.get(0).getSecondName(), "uniqueLastName");
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataQueriesSelfTest.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataQueriesSelfTest.java
new file mode 100644
index 0000000..d35c781
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataQueriesSelfTest.java
@@ -0,0 +1,409 @@
+/*
+ * 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.springdata;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.springdata.misc.ApplicationConfiguration;
+import org.apache.ignite.springdata.misc.Person;
+import org.apache.ignite.springdata.misc.PersonProjection;
+import org.apache.ignite.springdata.misc.PersonRepository;
+import org.apache.ignite.springdata.misc.PersonRepositoryOtherIgniteInstance;
+import org.apache.ignite.springdata.misc.PersonSecondRepository;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.domain.Sort;
+
+/**
+ *
+ */
+public class IgniteSpringDataQueriesSelfTest extends GridCommonAbstractTest {
+    /** Repository. */
+    private static PersonRepository repo;
+
+    /** Repository 2. */
+    private static PersonSecondRepository repo2;
+
+    /**
+     * Repository Ignite Instance cluster TWO.
+     */
+    private static PersonRepositoryOtherIgniteInstance repoTWO;
+
+    /** Context. */
+    private static AnnotationConfigApplicationContext ctx;
+
+    /**
+     * Number of entries to store
+     */
+    private static int CACHE_SIZE = 1000;
+
+    /**
+     * Performs context initialization before tests.
+     */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        ctx = new AnnotationConfigApplicationContext();
+
+        ctx.register(ApplicationConfiguration.class);
+
+        ctx.refresh();
+
+        repo = ctx.getBean(PersonRepository.class);
+        repo2 = ctx.getBean(PersonSecondRepository.class);
+        // repository on another ignite instance (and another cluster)
+        repoTWO = ctx.getBean(PersonRepositoryOtherIgniteInstance.class);
+
+        for (int i = 0; i < CACHE_SIZE; i++) {
+            repo.save(i, new Person("person" + Integer.toHexString(i),
+                "lastName" + Integer.toHexString((i + 16) % 256)));
+            repoTWO.save(i, new Person("TWOperson" + Integer.toHexString(i),
+                "lastName" + Integer.toHexString((i + 16) % 256)));
+        }
+    }
+
+    /**
+     * Performs context destroy after tests.
+     */
+    @Override protected void afterTestsStopped() throws Exception {
+        ctx.destroy();
+    }
+
+    /** */
+    @Test
+    public void testExplicitQuery() {
+        List<Person> persons = repo.simpleQuery("person4a");
+
+        assertFalse(persons.isEmpty());
+
+        for (Person person : persons)
+            assertEquals("person4a", person.getFirstName());
+    }
+
+    /** */
+    @Test
+    public void testExplicitQueryTWO() {
+        List<Person> persons = repoTWO.simpleQuery("TWOperson4a");
+
+        assertFalse(persons.isEmpty());
+
+        for (Person person : persons)
+            assertEquals("TWOperson4a", person.getFirstName());
+    }
+
+    /** */
+    @Test
+    public void testEqualsPart() {
+        List<Person> persons = repo.findByFirstName("person4e");
+
+        assertFalse(persons.isEmpty());
+
+        for (Person person : persons)
+            assertEquals("person4e", person.getFirstName());
+    }
+
+    /** */
+    @Test
+    public void testEqualsPartTWO() {
+        List<Person> persons = repoTWO.findByFirstName("TWOperson4e");
+
+        assertFalse(persons.isEmpty());
+
+        for (Person person : persons)
+            assertEquals("TWOperson4e", person.getFirstName());
+    }
+
+    /** */
+    @Test
+    public void testContainingPart() {
+        List<Person> persons = repo.findByFirstNameContaining("person4");
+
+        assertFalse(persons.isEmpty());
+
+        for (Person person : persons)
+            assertTrue(person.getFirstName().startsWith("person4"));
+    }
+
+    /** */
+    @Test
+    public void testContainingPartTWO() {
+        List<Person> persons = repoTWO.findByFirstNameContaining("TWOperson4");
+
+        assertFalse(persons.isEmpty());
+
+        for (Person person : persons)
+            assertTrue(person.getFirstName().startsWith("TWOperson4"));
+    }
+
+    /** */
+    @Test
+    public void testTopPart() {
+        Iterable<Person> top = repo.findTopByFirstNameContaining("person4");
+
+        Iterator<Person> iter = top.iterator();
+
+        Person person = iter.next();
+
+        assertFalse(iter.hasNext());
+
+        assertTrue(person.getFirstName().startsWith("person4"));
+    }
+
+    /** */
+    @Test
+    public void testTopPartTWO() {
+        Iterable<Person> top = repoTWO.findTopByFirstNameContaining("TWOperson4");
+
+        Iterator<Person> iter = top.iterator();
+
+        Person person = iter.next();
+
+        assertFalse(iter.hasNext());
+
+        assertTrue(person.getFirstName().startsWith("TWOperson4"));
+    }
+
+    /** */
+    @Test
+    public void testLikeAndLimit() {
+        Iterable<Person> like = repo.findFirst10ByFirstNameLike("person");
+
+        int cnt = 0;
+
+        for (Person next : like) {
+            assertTrue(next.getFirstName().contains("person"));
+
+            cnt++;
+        }
+
+        assertEquals(10, cnt);
+    }
+
+    /** */
+    @Test
+    public void testLikeAndLimitTWO() {
+        Iterable<Person> like = repoTWO.findFirst10ByFirstNameLike("TWOperson");
+
+        int cnt = 0;
+
+        for (Person next : like) {
+            assertTrue(next.getFirstName().contains("TWOperson"));
+
+            cnt++;
+        }
+
+        assertEquals(10, cnt);
+    }
+
+    /** */
+    @Test
+    public void testCount() {
+        int cnt = repo.countByFirstNameLike("person");
+
+        assertEquals(1000, cnt);
+    }
+
+    /** */
+    @Test
+    public void testCountTWO() {
+        int cnt = repoTWO.countByFirstNameLike("TWOperson");
+
+        assertEquals(1000, cnt);
+    }
+
+    /** */
+    @Test
+    public void testCount2() {
+        int cnt = repo.countByFirstNameLike("person4");
+
+        assertTrue(cnt < 1000);
+    }
+
+    /** */
+    @Test
+    public void testCount2TWO() {
+        int cnt = repoTWO.countByFirstNameLike("TWOperson4");
+
+        assertTrue(cnt < 1000);
+    }
+
+    /** */
+    @Test
+    public void testPageable() {
+        PageRequest pageable = PageRequest.of(1, 5, Sort.Direction.DESC, "firstName");
+
+        HashSet<String> firstNames = new HashSet<>();
+
+        List<Person> pageable1 = repo.findByFirstNameRegex("^[a-z]+$", pageable);
+
+        assertEquals(5, pageable1.size());
+
+        for (Person person : pageable1) {
+            firstNames.add(person.getFirstName());
+
+            assertTrue(person.getFirstName().matches("^[a-z]+$"));
+        }
+
+        List<Person> pageable2 = repo.findByFirstNameRegex("^[a-z]+$", pageable.next());
+
+        assertEquals(5, pageable2.size());
+
+        for (Person person : pageable2) {
+            firstNames.add(person.getFirstName());
+
+            assertTrue(person.getFirstName().matches("^[a-z]+$"));
+        }
+
+        assertEquals(10, firstNames.size());
+    }
+
+    /** */
+    @Test
+    public void testAndAndOr() {
+        int cntAnd = repo.countByFirstNameLikeAndSecondNameLike("person1", "lastName1");
+
+        int cntOr = repo.countByFirstNameStartingWithOrSecondNameStartingWith("person1", "lastName1");
+
+        assertTrue(cntAnd <= cntOr);
+    }
+
+    /** */
+    @Test
+    public void testQueryWithSort() {
+        List<Person> persons = repo.queryWithSort("^[a-z]+$", Sort.by(Sort.Direction.DESC, "secondName"));
+
+        Person previous = persons.get(0);
+
+        for (Person person : persons) {
+            assertTrue(person.getSecondName().compareTo(previous.getSecondName()) <= 0);
+
+            assertTrue(person.getFirstName().matches("^[a-z]+$"));
+
+            previous = person;
+        }
+    }
+
+    /** */
+    @Test
+    public void testQueryWithPaging() {
+        List<Person> persons = repo.queryWithPageable("^[a-z]+$", PageRequest.of(1, 7, Sort.Direction.DESC, "secondName"));
+
+        assertEquals(7, persons.size());
+
+        Person previous = persons.get(0);
+
+        for (Person person : persons) {
+            assertTrue(person.getSecondName().compareTo(previous.getSecondName()) <= 0);
+
+            assertTrue(person.getFirstName().matches("^[a-z]+$"));
+
+            previous = person;
+        }
+    }
+
+    /** */
+    @Test
+    public void testQueryFields() {
+        List<String> persons = repo.selectField("^[a-z]+$", PageRequest.of(1, 7, Sort.Direction.DESC, "secondName"));
+
+        assertEquals(7, persons.size());
+    }
+
+    /** */
+    @Test
+    public void testFindCacheEntries() {
+        List<Cache.Entry<Integer, Person>> cacheEntries = repo.findBySecondNameLike("stName1");
+
+        assertFalse(cacheEntries.isEmpty());
+
+        for (Cache.Entry<Integer, Person> entry : cacheEntries)
+            assertTrue(entry.getValue().getSecondName().contains("stName1"));
+    }
+
+    /** */
+    @Test
+    public void testFindOneCacheEntry() {
+        Cache.Entry<Integer, Person> cacheEntry = repo.findTopBySecondNameLike("tName18");
+
+        assertNotNull(cacheEntry);
+
+        assertTrue(cacheEntry.getValue().getSecondName().contains("tName18"));
+    }
+
+    /** */
+    @Test
+    public void testFindOneValue() {
+        PersonProjection person = repo.findTopBySecondNameStartingWith("lastName18");
+
+        assertNotNull(person);
+
+        assertTrue(person.getFullName().split("\\s")[1].startsWith("lastName18"));
+    }
+
+    /** */
+    @Test
+    public void testSelectSeveralFields() {
+        List<List> lists = repo.selectSeveralField("^[a-z]+$", PageRequest.of(2, 6));
+
+        assertEquals(6, lists.size());
+
+        for (List list : lists) {
+            assertEquals(2, list.size());
+
+            assertTrue(list.get(0) instanceof Integer);
+        }
+    }
+
+    /** */
+    @Test
+    public void testCountQuery() {
+        int cnt = repo.countQuery(".*");
+
+        assertEquals(256, cnt);
+    }
+
+    /** */
+    @Test
+    public void testSliceOfCacheEntries() {
+        Slice<Cache.Entry<Integer, Person>> slice = repo2.findBySecondNameIsNot("lastName18", PageRequest.of(3, 4));
+
+        assertEquals(4, slice.getSize());
+
+        for (Cache.Entry<Integer, Person> entry : slice)
+            assertFalse("lastName18".equals(entry.getValue().getSecondName()));
+    }
+
+    /** */
+    @Test
+    public void testSliceOfLists() {
+        Slice<List> lists = repo2.querySliceOfList("^[a-z]+$", PageRequest.of(0, 3));
+
+        assertEquals(3, lists.getSize());
+
+        for (List list : lists) {
+            assertEquals(2, list.size());
+
+            assertTrue(list.get(0) instanceof Integer);
+        }
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/City.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/City.java
new file mode 100644
index 0000000..e86a575
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/City.java
@@ -0,0 +1,113 @@
+/*
+ * 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.springdata.compoundkey;
+
+import java.util.Objects;
+
+/**
+ * Value-class
+ * */
+public class City {
+    /** City name */
+    private String name;
+
+    /** City district */
+    private String district;
+
+    /** City population */
+    private int population;
+
+    /**
+     * @param name city name
+     * @param district city district
+     * @param population city population
+     * */
+    public City(String name, String district, int population) {
+        this.name = name;
+        this.district = district;
+        this.population = population;
+    }
+
+    /**
+     * @return city name
+     * */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * @param name city name
+     * */
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    /**
+     * @return city district
+     * */
+    public String getDistrict() {
+        return district;
+    }
+
+    /**
+     * @param district city district
+     * */
+    public void setDistrict(String district) {
+        this.district = district;
+    }
+
+    /**
+     * @return city population
+     * */
+    public int getPopulation() {
+        return population;
+    }
+
+    /**
+     * @param population city population
+     * */
+    public void setPopulation(int population) {
+        this.population = population;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return name + " | " + district + " | " + population;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+
+        if (o == null || getClass() != o.getClass())
+            return false;
+
+        City city = (City)o;
+
+        return
+                Objects.equals(this.name, city.name) &&
+                        Objects.equals(this.district, city.district) &&
+                        this.population == city.population;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        return Objects.hash(name, district, population);
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityKey.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityKey.java
new file mode 100644
index 0000000..88226fe
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityKey.java
@@ -0,0 +1,79 @@
+/*
+ * 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.springdata.compoundkey;
+
+import java.io.Serializable;
+import java.util.Objects;
+import org.apache.ignite.cache.affinity.AffinityKeyMapped;
+
+/** Compound key for city class  */
+public class CityKey implements Serializable {
+    /** city identifier */
+    private int ID;
+
+    /** affinity key countrycode  */
+    @AffinityKeyMapped
+    private String COUNTRYCODE;
+
+    /**
+     * @param id city identifier
+     * @param countryCode city countrycode
+     * */
+    public CityKey(int id, String countryCode) {
+        this.ID = id;
+        this.COUNTRYCODE = countryCode;
+    }
+
+    /**
+     * @return city id
+     * */
+    public int getId() {
+        return ID;
+    }
+
+    /**
+     * @return countrycode
+     * */
+    public String getCountryCode() {
+        return COUNTRYCODE;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+
+        if (o == null || getClass() != o.getClass())
+            return false;
+
+        CityKey key = (CityKey)o;
+
+        return ID == key.ID &&
+                COUNTRYCODE.equals(key.COUNTRYCODE);
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        return Objects.hash(ID, COUNTRYCODE);
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return ID + " | " + COUNTRYCODE;
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityRepository.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityRepository.java
new file mode 100644
index 0000000..ab745e7
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityRepository.java
@@ -0,0 +1,43 @@
+/*
+ * 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.springdata.compoundkey;
+
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.springdata22.repository.IgniteRepository;
+import org.apache.ignite.springdata22.repository.config.RepositoryConfig;
+import org.springframework.stereotype.Repository;
+
+/** City repository */
+@Repository
+@RepositoryConfig(cacheName = "City", autoCreateCache = true)
+public interface CityRepository extends IgniteRepository<City, CityKey> {
+    /**
+     * Find city by id
+     * @param id city identifier
+     * @return city
+     * */
+    public City findById(int id);
+
+    /**
+     * Find all cities by coutrycode
+     * @param cc coutrycode
+     * @return list of cache enrties CityKey -> City
+     * */
+    public List<Cache.Entry<CityKey, City>> findByCountryCode(String cc);
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CompoundKeyApplicationConfiguration.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CompoundKeyApplicationConfiguration.java
new file mode 100644
index 0000000..2d472a2
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CompoundKeyApplicationConfiguration.java
@@ -0,0 +1,45 @@
+/*
+ * 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.springdata.compoundkey;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.ClientConnectorConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.springdata22.repository.config.EnableIgniteRepositories;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Spring application configuration
+ * */
+@Configuration
+@EnableIgniteRepositories
+public class CompoundKeyApplicationConfiguration {
+    /** */
+    public static final int CLI_CONN_PORT = 10810;
+
+    /**
+     * Ignite instance bean
+     * */
+    @Bean
+    public Ignite igniteInstance() {
+        return Ignition.start(new IgniteConfiguration()
+            .setClientConnectorConfiguration(new ClientConnectorConfiguration().setPort(CLI_CONN_PORT)));
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/ApplicationConfiguration.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/ApplicationConfiguration.java
new file mode 100644
index 0000000..1443d5d
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/ApplicationConfiguration.java
@@ -0,0 +1,113 @@
+/*
+ * 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.springdata.misc;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
+import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder;
+import org.apache.ignite.springdata.misc.SampleEvaluationContextExtension.SamplePassParamExtension;
+import org.apache.ignite.springdata22.repository.config.EnableIgniteRepositories;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.spel.spi.EvaluationContextExtension;
+
+/** */
+@Configuration
+@EnableIgniteRepositories
+public class ApplicationConfiguration {
+    /** */
+    public static final String IGNITE_INSTANCE_ONE = "IGNITE_INSTANCE_ONE";
+
+    /** */
+    public static final String IGNITE_INSTANCE_TWO = "IGNITE_INSTANCE_TWO";
+
+    /**
+     * The bean with cache names
+     */
+    @Bean
+    public CacheNamesBean cacheNames() {
+        CacheNamesBean bean = new CacheNamesBean();
+
+        bean.setPersonCacheName("PersonCache");
+
+        return bean;
+    }
+
+    /** */
+    @Bean
+    public EvaluationContextExtension sampleSpELExtension() {
+        return new SampleEvaluationContextExtension();
+    }
+
+    /** */
+    @Bean(value = "sampleExtensionBean")
+    public SamplePassParamExtension sampleExtensionBean() {
+        return new SamplePassParamExtension();
+    }
+
+    /**
+     * Ignite instance bean - no instance name provided on RepositoryConfig
+     */
+    @Bean
+    public Ignite igniteInstance() {
+        IgniteConfiguration cfg = new IgniteConfiguration();
+
+        cfg.setIgniteInstanceName(IGNITE_INSTANCE_ONE);
+
+        CacheConfiguration ccfg = new CacheConfiguration("PersonCache");
+
+        ccfg.setIndexedTypes(Integer.class, Person.class);
+
+        cfg.setCacheConfiguration(ccfg);
+
+        TcpDiscoverySpi spi = new TcpDiscoverySpi();
+
+        spi.setIpFinder(new TcpDiscoveryVmIpFinder(true));
+
+        cfg.setDiscoverySpi(spi);
+
+        return Ignition.start(cfg);
+    }
+
+    /**
+     * Ignite instance bean with not default name
+     */
+    @Bean
+    public Ignite igniteInstanceTWO() {
+        IgniteConfiguration cfg = new IgniteConfiguration();
+
+        cfg.setIgniteInstanceName(IGNITE_INSTANCE_TWO);
+
+        CacheConfiguration ccfg = new CacheConfiguration("PersonCache");
+
+        ccfg.setIndexedTypes(Integer.class, Person.class);
+
+        cfg.setCacheConfiguration(ccfg);
+
+        TcpDiscoverySpi spi = new TcpDiscoverySpi();
+
+        spi.setIpFinder(new TcpDiscoveryVmIpFinder(true));
+
+        cfg.setDiscoverySpi(spi);
+
+        return Ignition.start(cfg);
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/CacheNamesBean.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/CacheNamesBean.java
new file mode 100644
index 0000000..6185744
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/CacheNamesBean.java
@@ -0,0 +1,42 @@
+/*
+ * 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.springdata.misc;
+
+/**
+ * The bean with cache names.
+ */
+public class CacheNamesBean {
+    /** */
+    private String personCacheName;
+
+    /**
+     * Get name of cache for persons.
+     *
+     * @return Name of cache.
+     */
+    public String getPersonCacheName() {
+        return personCacheName;
+    }
+
+    /**
+     * @param personCacheName Name of cache.
+     */
+    public void setPersonCacheName(String personCacheName) {
+        this.personCacheName = personCacheName;
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/FullNameProjection.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/FullNameProjection.java
new file mode 100644
index 0000000..23f8f8e
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/FullNameProjection.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.springdata.misc;
+
+import org.springframework.beans.factory.annotation.Value;
+
+/**
+ * Advanced SpEl Expressions into projection
+ *
+ * @author Manuel Núñez Sánchez (manuel.nunez@hawkore.com)
+ */
+public interface FullNameProjection {
+    /**
+     * Sample of using SpEL expression
+     * @return
+     */
+    @Value("#{target.firstName + ' ' + target.secondName}")
+    String getFullName();
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/Person.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/Person.java
new file mode 100644
index 0000000..531c67f
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/Person.java
@@ -0,0 +1,100 @@
+/*
+ * 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.springdata.misc;
+
+import java.util.Objects;
+import org.apache.ignite.cache.query.annotations.QuerySqlField;
+import org.apache.ignite.cache.query.annotations.QueryTextField;
+
+/**
+ * DTO class.
+ */
+public class Person {
+    /** First name. */
+    @QuerySqlField(index = true)
+    @QueryTextField
+    private String firstName;
+
+    /** Second name. */
+    @QuerySqlField(index = true)
+    private String secondName;
+
+    /**
+     * @param firstName First name.
+     * @param secondName Second name.
+     */
+    public Person(String firstName, String secondName) {
+        this.firstName = firstName;
+        this.secondName = secondName;
+    }
+
+    /**
+     * @return First name.
+     */
+    public String getFirstName() {
+        return firstName;
+    }
+
+    /**
+     * @param firstName First name.
+     */
+    public void setFirstName(String firstName) {
+        this.firstName = firstName;
+    }
+
+    /**
+     * @return Second name.
+     */
+    public String getSecondName() {
+        return secondName;
+    }
+
+    /**
+     * @param secondName Second name.
+     */
+    public void setSecondName(String secondName) {
+        this.secondName = secondName;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return "Person{" +
+            "firstName='" + firstName + '\'' +
+            ", secondName='" + secondName + '\'' +
+            '}';
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+
+        if (o == null || getClass() != o.getClass())
+            return false;
+
+        Person person = (Person)o;
+
+        return Objects.equals(firstName, person.firstName) &&
+            Objects.equals(secondName, person.secondName);
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        return Objects.hash(firstName, secondName);
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonExpressionRepository.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonExpressionRepository.java
new file mode 100644
index 0000000..689f856
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonExpressionRepository.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.springdata.misc;
+
+import org.apache.ignite.springdata22.repository.IgniteRepository;
+import org.apache.ignite.springdata22.repository.config.RepositoryConfig;
+
+/**
+ *
+ */
+@RepositoryConfig(cacheName = "#{cacheNames.personCacheName}")
+public interface PersonExpressionRepository extends IgniteRepository<Person, Integer> {
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonKey.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonKey.java
new file mode 100644
index 0000000..2537c57
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonKey.java
@@ -0,0 +1,54 @@
+/*
+ * 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.springdata.misc;
+
+import java.io.Serializable;
+
+/**
+ * Compound key.
+ */
+public class PersonKey implements Serializable {
+    /** */
+    private int id1;
+
+    /** */
+    private int id2;
+
+    /**
+     * @param id1 ID1.
+     * @param id2 ID2.
+     */
+    public PersonKey(int id1, int id2) {
+        this.id1 = id1;
+        this.id2 = id2;
+    }
+
+    /**
+     * @return ID1
+     */
+    public int getId1() {
+        return id1;
+    }
+
+    /**
+     * @return ID1
+     */
+    public int getId2() {
+        return id1;
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonProjection.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonProjection.java
new file mode 100644
index 0000000..a187a08
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonProjection.java
@@ -0,0 +1,43 @@
+/*
+ * 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.springdata.misc;
+
+import org.springframework.beans.factory.annotation.Value;
+
+/**
+ * Advanced SpEl Expressions into projection
+ *
+ * @author Manuel Núñez Sánchez (manuel.nunez@hawkore.com)
+ */
+public interface PersonProjection {
+    /** */
+    String getFirstName();
+
+    /**
+     * Sample of using registered spring bean into SpEL expression
+     * @return
+     */
+    @Value("#{@sampleExtensionBean.transformParam(target.firstName)}")
+    String getFirstNameTransformed();
+
+    /**
+     * Sample of using SpEL expression
+     * @return
+     */
+    @Value("#{target.firstName + ' ' + target.secondName}")
+    String getFullName();
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepository.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepository.java
new file mode 100644
index 0000000..0a5826f
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepository.java
@@ -0,0 +1,151 @@
+
+/*
+ * 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.springdata.misc;
+
+import java.util.Collection;
+import java.util.List;
+
+import javax.cache.Cache;
+import org.apache.ignite.springdata22.repository.IgniteRepository;
+import org.apache.ignite.springdata22.repository.config.Query;
+import org.apache.ignite.springdata22.repository.config.RepositoryConfig;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.repository.query.Param;
+
+/**
+ * Test repository.
+ */
+@RepositoryConfig(cacheName = "PersonCache")
+public interface PersonRepository extends IgniteRepository<Person, Integer> {
+    /** */
+    public List<Person> findByFirstName(String val);
+
+    /** */
+    @Query("firstName = ?")
+    public List<PersonProjection> queryByFirstNameWithProjection(String val);
+
+    /** */
+    @Query("firstName = :firstname")
+    public List<PersonProjection> queryByFirstNameWithProjectionNamedParameter(@Param("firstname") String val);
+
+    /** */
+    @Query("firstName = :firstname")
+    public <P> List<P> queryByFirstNameWithProjectionNamedParameter(Class<P> dynamicProjection, @Param("firstname") String val);
+
+    /** */
+    @Query("firstName = :firstname")
+    public <P> P queryOneByFirstNameWithProjectionNamedParameter(Class<P> dynamicProjection, @Param("firstname") String val);
+
+    /** */
+    @Query("firstName = ?#{[1]}")
+    public List<PersonProjection> queryByFirstNameWithProjectionNamedIndexedParameter(@Param("notUsed") String notUsed, @Param("firstname") String val);
+
+    /** */
+    @Query(textQuery = true, value = "#{#firstname}", limit = 2)
+    public List<PersonProjection> textQueryByFirstNameWithProjectionNamedParameter(@Param("firstname") String val);
+
+    @Query(value = "select * from (sElecT * from #{#entityName} where firstName = :firstname)", forceFieldsQuery = true)
+    public List<PersonProjection> queryByFirstNameWithProjectionNamedParameterAndTemplateDomainEntityVariable(@Param("firstname") String val);
+
+    @Query(value = "firstName = ?#{sampleExtension.transformParam(#firstname)}")
+    public List<PersonProjection> queryByFirstNameWithProjectionNamedParameterWithSpELExtension(@Param("firstname") String val);
+
+    /** */
+    public List<Person> findByFirstNameContaining(String val);
+
+    /** */
+    public List<Person> findByFirstNameRegex(String val, Pageable pageable);
+
+    /** */
+    public Collection<Person> findTopByFirstNameContaining(String val);
+
+    /** */
+    public Iterable<Person> findFirst10ByFirstNameLike(String val);
+
+    /** */
+    public int countByFirstName(String val);
+
+    /** */
+    public int countByFirstNameLike(String val);
+
+    /** */
+    public int countByFirstNameLikeAndSecondNameLike(String like1, String like2);
+
+    /** */
+    public int countByFirstNameStartingWithOrSecondNameStartingWith(String like1, String like2);
+
+    /** */
+    public List<Cache.Entry<Integer, Person>> findBySecondNameLike(String val);
+
+    /** */
+    public Cache.Entry<Integer, Person> findTopBySecondNameLike(String val);
+
+    /** */
+    public PersonProjection findTopBySecondNameStartingWith(String val);
+
+    /** */
+    @Query("firstName = ?")
+    public List<Person> simpleQuery(String val);
+
+    /** */
+    @Query("firstName REGEXP ?")
+    public List<Person> queryWithSort(String val, Sort sort);
+
+    /** */
+    @Query("SELECT * FROM Person WHERE firstName REGEXP ?")
+    public List<Person> queryWithPageable(String val, Pageable pageable);
+
+    /** */
+    @Query("SELECT secondName FROM Person WHERE firstName REGEXP ?")
+    public List<String> selectField(String val, Pageable pageable);
+
+    /** */
+    @Query("SELECT _key, secondName FROM Person WHERE firstName REGEXP ?")
+    public List<List> selectSeveralField(String val, Pageable pageable);
+
+    /** */
+    @Query("SELECT count(1) FROM (SELECT DISTINCT secondName FROM Person WHERE firstName REGEXP ?)")
+    public int countQuery(String val);
+
+    /** Top 3 query */
+    public List<Person> findTop3ByFirstName(String val);
+
+    /** Delete query */
+    public long deleteByFirstName(String firstName);
+
+    /** Remove Query */
+    public List<Person> removeByFirstName(String firstName);
+
+    /** Delete using @Query with keyword in lower-case */
+    @Query("delete FROM Person WHERE secondName = ?")
+    public void deleteBySecondNameLowerCase(String secondName);
+
+    /** Delete using @Query but with errors on the query */
+    @Query("DELETE FROM Person WHERE firstName = ? AND ERRORS = 'ERRORS'")
+    public void deleteWrongByFirstNameQuery(String firstName);
+
+    /** Update using @Query with keyword in mixed-case */
+    @Query("upDATE Person SET secondName = ? WHERE firstName = ?")
+    public int setFixedSecondNameMixedCase(String secondName, String firstName);
+
+    /** Update using @Query but with errors on the query */
+    @Query("UPDATE Person SET secondName = ? WHERE firstName = ? AND ERRORS = 'ERRORS'")
+    public int setWrongFixedSecondName(String secondName, String firstName);
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryOtherIgniteInstance.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryOtherIgniteInstance.java
new file mode 100644
index 0000000..307556b
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryOtherIgniteInstance.java
@@ -0,0 +1,143 @@
+
+/*
+ * 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.springdata.misc;
+
+import java.util.Collection;
+import java.util.List;
+
+import javax.cache.Cache;
+import org.apache.ignite.springdata22.repository.IgniteRepository;
+import org.apache.ignite.springdata22.repository.config.Query;
+import org.apache.ignite.springdata22.repository.config.RepositoryConfig;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.repository.query.Param;
+
+/**
+ *
+ */
+@RepositoryConfig(igniteInstance = "igniteInstanceTWO", cacheName = "PersonCache")
+public interface PersonRepositoryOtherIgniteInstance extends IgniteRepository<Person, Integer> {
+    /** */
+    public List<Person> findByFirstName(String val);
+
+    /** */
+    @Query("firstName = ?")
+    public List<PersonProjection> queryByFirstNameWithProjection(String val);
+
+    /** */
+    @Query("firstName = :firstname")
+    public List<PersonProjection> queryByFirstNameWithProjectionNamedParameter(@Param("firstname") String val);
+
+    /** */
+    @Query("firstName = ?#{[1]}")
+    public List<PersonProjection> queryByFirstNameWithProjectionNamedIndexedParameter(@Param("notUsed") String notUsed, @Param("firstname") String val);
+
+    /** */
+    @Query(textQuery = true, value = "#{#firstname}", limit = 2)
+    public List<PersonProjection> textQueryByFirstNameWithProjectionNamedParameter(@Param("firstname") String val);
+
+    @Query(value = "select * from (sElecT * from #{#entityName} where firstName = :firstname)", forceFieldsQuery = true)
+    public List<PersonProjection> queryByFirstNameWithProjectionNamedParameterAndTemplateDomainEntityVariable(@Param("firstname") String val);
+
+    @Query(value = "firstName = ?#{sampleExtension.transformParam(#firstname)}")
+    public List<PersonProjection> queryByFirstNameWithProjectionNamedParameterWithSpELExtension(@Param("firstname") String val);
+
+    /** */
+    public List<Person> findByFirstNameContaining(String val);
+
+    /** */
+    public List<Person> findByFirstNameRegex(String val, Pageable pageable);
+
+    /** */
+    public Collection<Person> findTopByFirstNameContaining(String val);
+
+    /** */
+    public Iterable<Person> findFirst10ByFirstNameLike(String val);
+
+    /** */
+    public int countByFirstName(String val);
+
+    /** */
+    public int countByFirstNameLike(String val);
+
+    /** */
+    public int countByFirstNameLikeAndSecondNameLike(String like1, String like2);
+
+    /** */
+    public int countByFirstNameStartingWithOrSecondNameStartingWith(String like1, String like2);
+
+    /** */
+    public List<Cache.Entry<Integer, Person>> findBySecondNameLike(String val);
+
+    /** */
+    public Cache.Entry<Integer, Person> findTopBySecondNameLike(String val);
+
+    /** */
+    public PersonProjection findTopBySecondNameStartingWith(String val);
+
+    /** */
+    @Query("firstName = ?")
+    public List<Person> simpleQuery(String val);
+
+    /** */
+    @Query("firstName REGEXP ?")
+    public List<Person> queryWithSort(String val, Sort sort);
+
+    /** */
+    @Query("SELECT * FROM Person WHERE firstName REGEXP ?")
+    public List<Person> queryWithPageable(String val, Pageable pageable);
+
+    /** */
+    @Query("SELECT secondName FROM Person WHERE firstName REGEXP ?")
+    public List<String> selectField(String val, Pageable pageable);
+
+    /** */
+    @Query("SELECT _key, secondName FROM Person WHERE firstName REGEXP ?")
+    public List<List> selectSeveralField(String val, Pageable pageable);
+
+    /** */
+    @Query("SELECT count(1) FROM (SELECT DISTINCT secondName FROM Person WHERE firstName REGEXP ?)")
+    public int countQuery(String val);
+
+    /** Top 3 query */
+    public List<Person> findTop3ByFirstName(String val);
+
+    /** Delete query */
+    public long deleteByFirstName(String firstName);
+
+    /** Remove Query */
+    public List<Person> removeByFirstName(String firstName);
+
+    /** Delete using @Query with keyword in lower-case */
+    @Query("delete FROM Person WHERE secondName = ?")
+    public void deleteBySecondNameLowerCase(String secondName);
+
+    /** Delete using @Query but with errors on the query */
+    @Query("DELETE FROM Person WHERE firstName = ? AND ERRORS = 'ERRORS'")
+    public void deleteWrongByFirstNameQuery(String firstName);
+
+    /** Update using @Query with keyword in mixed-case */
+    @Query("upDATE Person SET secondName = ? WHERE firstName = ?")
+    public int setFixedSecondNameMixedCase(String secondName, String firstName);
+
+    /** Update using @Query but with errors on the query */
+    @Query("UPDATE Person SET secondName = ? WHERE firstName = ? AND ERRORS = 'ERRORS'")
+    public int setWrongFixedSecondName(String secondName, String firstName);
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryWithCompoundKey.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryWithCompoundKey.java
new file mode 100644
index 0000000..97b0b5f
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryWithCompoundKey.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.springdata.misc;
+
+import org.apache.ignite.springdata22.repository.IgniteRepository;
+import org.apache.ignite.springdata22.repository.config.RepositoryConfig;
+
+/**
+ * Test repository.
+ */
+@RepositoryConfig(cacheName = "PersonWithKeyCache", autoCreateCache = true)
+public interface PersonRepositoryWithCompoundKey extends IgniteRepository<Person, PersonKey> {
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonSecondRepository.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonSecondRepository.java
new file mode 100644
index 0000000..24f1c61
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/PersonSecondRepository.java
@@ -0,0 +1,41 @@
+/*
+ * 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.springdata.misc;
+
+import java.util.List;
+
+import javax.cache.Cache;
+import org.apache.ignite.springdata22.repository.IgniteRepository;
+import org.apache.ignite.springdata22.repository.config.Query;
+import org.apache.ignite.springdata22.repository.config.RepositoryConfig;
+import org.springframework.data.domain.AbstractPageRequest;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Slice;
+
+/**
+ * Test repository.
+ */
+@RepositoryConfig(cacheName = "PersonCache")
+public interface PersonSecondRepository extends IgniteRepository<Person, Integer> {
+    /** */
+    public Slice<Cache.Entry<Integer, Person>> findBySecondNameIsNot(String val, PageRequest pageReq);
+
+    /** */
+    @Query("SELECT _key, secondName FROM Person WHERE firstName REGEXP ?")
+    public Slice<List> querySliceOfList(String val, AbstractPageRequest pageReq);
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/SampleEvaluationContextExtension.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/SampleEvaluationContextExtension.java
new file mode 100644
index 0000000..9db4343
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/springdata/misc/SampleEvaluationContextExtension.java
@@ -0,0 +1,93 @@
+/*
+ * 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.springdata.misc;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.data.spel.spi.EvaluationContextExtension;
+
+/**
+ * Sample EvaluationContext Extension for Spring Data 2.2
+ * <p>
+ * Use SpEl expressions into your {@code @Query} definitions.
+ * <p>
+ * First, you need to register your extension into your spring data configuration. Sample:
+ * <pre>
+ * {@code @Configuration}
+ * {@code @EnableIgniteRepositories}(basePackages = ... )
+ * public class MyIgniteRepoConfig {
+ * ...
+ *      {@code @Bean}
+ *      public EvaluationContextExtension sampleSpELExtension() {
+ *          return new SampleEvaluationContextExtension();
+ *      }
+ * ...
+ * }
+ * </pre>
+ *
+ * <p>
+ * Sample of usage into your {@code @Query} definitions:
+ * <pre>
+ * {@code @RepositoryConfig}(cacheName = "users")
+ * public interface UserRepository
+ * extends IgniteRepository<User, UUID>{
+ *     [...]
+ *
+ *     {@code @Query}(value = "SELECT * from #{#entityName} where email = ?#{sampleExtension.transformParam(#email)}")
+ *     User searchUserByEmail(@Param("email") String email);
+
+ *      [...]
+ *     }
+ * </pre>
+ * <p>
+ *
+ * @author Manuel Núñez Sánchez (manuel.nunez@hawkore.com)
+ */
+public class SampleEvaluationContextExtension implements EvaluationContextExtension {
+    /** */
+    private static final SamplePassParamExtension SAMPLE_PASS_PARAM_EXTENSION_INSTANCE = new SamplePassParamExtension();
+
+    /** */
+    private static final Map<String, Object> properties = new HashMap<>();
+
+    /** */
+    private static final String SAMPLE_EXTENSION_SPEL_VAR = "sampleExtension";
+
+    static {
+        properties.put(SAMPLE_EXTENSION_SPEL_VAR, SAMPLE_PASS_PARAM_EXTENSION_INSTANCE);
+    }
+
+    /** */
+    @Override public String getExtensionId() {
+        return "HK-SAMPLE-PASS-PARAM-EXTENSION";
+    }
+
+    /** */
+    @Override public Map<String, Object> getProperties() {
+        return properties;
+    }
+
+    /** */
+    public static class SamplePassParamExtension {
+        // just return same param
+        /** */
+        public Object transformParam(Object param) {
+            return param;
+        }
+    }
+}
diff --git a/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/testsuites/IgniteSpringData22TestSuite.java b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/testsuites/IgniteSpringData22TestSuite.java
new file mode 100644
index 0000000..872fe9f
--- /dev/null
+++ b/modules/spring-data-2.2-ext/src/test/java/org/apache/ignite/testsuites/IgniteSpringData22TestSuite.java
@@ -0,0 +1,38 @@
+/*
+ * 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.testsuites;
+
+import org.apache.ignite.springdata.IgniteSpringDataCompoundKeyTest;
+import org.apache.ignite.springdata.IgniteSpringDataCrudSelfExpressionTest;
+import org.apache.ignite.springdata.IgniteSpringDataCrudSelfTest;
+import org.apache.ignite.springdata.IgniteSpringDataQueriesSelfTest;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+/**
+ * Ignite Spring Data 2.2 test suite.
+ */
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+    IgniteSpringDataCrudSelfTest.class,
+    IgniteSpringDataQueriesSelfTest.class,
+    IgniteSpringDataCrudSelfExpressionTest.class,
+    IgniteSpringDataCompoundKeyTest.class
+})
+public class IgniteSpringData22TestSuite {
+}
diff --git a/modules/spring-data-ext/README.txt b/modules/spring-data-ext/README.txt
new file mode 100644
index 0000000..eb7aad9
--- /dev/null
+++ b/modules/spring-data-ext/README.txt
@@ -0,0 +1,28 @@
+Apache Ignite Spring Module
+---------------------------
+
+Apache Ignite Spring Data extension provides an integration with Spring Data framework.
+
+Importing Spring Data Module In Maven Project
+----------------------------------------
+
+If you are using Maven to manage dependencies of your project, you can add Spring Data extension
+dependency like this (replace '${ignite-spring-data-ext.version}' with actual version of Ignite Spring Data extension
+you are interested in):
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+                        http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    ...
+    <dependencies>
+        ...
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-spring-data-ext</artifactId>
+            <version>${ignite-spring-data-ext.version}</version>
+        </dependency>
+        ...
+    </dependencies>
+    ...
+</project>
diff --git a/modules/spring-data-ext/licenses/apache-2.0.txt b/modules/spring-data-ext/licenses/apache-2.0.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/modules/spring-data-ext/licenses/apache-2.0.txt
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       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.
diff --git a/modules/spring-data-ext/modules/core/src/test/config/log4j-test.xml b/modules/spring-data-ext/modules/core/src/test/config/log4j-test.xml
new file mode 100755
index 0000000..3061bd4
--- /dev/null
+++ b/modules/spring-data-ext/modules/core/src/test/config/log4j-test.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<!DOCTYPE log4j:configuration PUBLIC "-//APACHE//DTD LOG4J 1.2//EN"
+    "http://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/xml/doc-files/log4j.dtd">
+<!--
+    Log4j configuration.
+-->
+<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="false">
+    <!--
+        Logs System.out messages to console.
+    -->
+    <appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender">
+        <!-- Log to STDOUT. -->
+        <param name="Target" value="System.out"/>
+
+        <!-- Log from DEBUG and higher. -->
+        <param name="Threshold" value="DEBUG"/>
+
+        <!-- The default pattern: Date Priority [Category] Message\n -->
+        <layout class="org.apache.log4j.PatternLayout">
+            <param name="ConversionPattern" value="[%d{ISO8601}][%-5p][%t][%c{1}] %m%n"/>
+        </layout>
+
+        <!-- Do not log beyond INFO level. -->
+        <filter class="org.apache.log4j.varia.LevelRangeFilter">
+            <param name="levelMin" value="DEBUG"/>
+            <param name="levelMax" value="INFO"/>
+        </filter>
+    </appender>
+
+    <!--
+        Logs all System.err messages to console.
+    -->
+    <appender name="CONSOLE_ERR" class="org.apache.log4j.ConsoleAppender">
+        <!-- Log to STDERR. -->
+        <param name="Target" value="System.err"/>
+
+        <!-- Log from WARN and higher. -->
+        <param name="Threshold" value="WARN"/>
+
+        <!-- The default pattern: Date Priority [Category] Message\n -->
+        <layout class="org.apache.log4j.PatternLayout">
+            <param name="ConversionPattern" value="[%d{ISO8601}][%-5p][%t][%c{1}] %m%n"/>
+        </layout>
+    </appender>
+
+    <!--
+        Logs all output to specified file.
+    -->
+    <appender name="FILE" class="org.apache.log4j.RollingFileAppender">
+        <param name="Threshold" value="DEBUG"/>
+        <param name="File" value="ignite/work/log/ignite.log"/>
+        <param name="Append" value="true"/>
+        <param name="MaxFileSize" value="10MB"/>
+        <param name="MaxBackupIndex" value="10"/>
+        <layout class="org.apache.log4j.PatternLayout">
+            <param name="ConversionPattern" value="[%d{ISO8601}][%-5p][%t][%c{1}] %m%n"/>
+        </layout>
+    </appender>
+
+    <!-- Disable all open source debugging. -->
+    <category name="org">
+        <level value="INFO"/>
+    </category>
+
+    <category name="org.eclipse.jetty">
+        <level value="INFO"/>
+    </category>
+
+    <!-- Default settings. -->
+    <root>
+        <!-- Print at info by default. -->
+        <level value="INFO"/>
+
+        <!-- Append to file and console. -->
+        <appender-ref ref="FILE"/>
+        <appender-ref ref="CONSOLE"/>
+        <appender-ref ref="CONSOLE_ERR"/>
+    </root>
+</log4j:configuration>
diff --git a/modules/spring-data-ext/modules/core/src/test/config/tests.properties b/modules/spring-data-ext/modules/core/src/test/config/tests.properties
new file mode 100644
index 0000000..0faf5b8
--- /dev/null
+++ b/modules/spring-data-ext/modules/core/src/test/config/tests.properties
@@ -0,0 +1,22 @@
+#
+# 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.
+#
+
+# Local address to bind to.
+local.ip=127.0.0.1
+
+# TCP communication port
+comm.tcp.port=30010
diff --git a/modules/spring-data-ext/pom.xml b/modules/spring-data-ext/pom.xml
new file mode 100644
index 0000000..b7547f1
--- /dev/null
+++ b/modules/spring-data-ext/pom.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  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.
+-->
+
+<!--
+    POM file.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.ignite</groupId>
+        <artifactId>ignite-extensions-parent</artifactId>
+        <version>1</version>
+        <relativePath>../../parent</relativePath>
+    </parent>
+
+    <artifactId>ignite-spring-data-ext</artifactId>
+    <version>1.0.0-SNAPSHOT</version>
+    <url>http://ignite.apache.org</url>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-core</artifactId>
+            <version>${ignite.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-indexing</artifactId>
+            <version>${ignite.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-log4j</artifactId>
+            <version>${ignite.version}</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.data</groupId>
+            <artifactId>spring-data-commons</artifactId>
+            <version>${spring.data.version}</version>
+            <!-- Exclude slf4j logging in favor of log4j -->
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>jcl-over-slf4j</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-core</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-beans</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-spring</artifactId>
+            <version>${ignite.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-core</artifactId>
+            <version>${ignite.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.ignite</groupId>
+            <artifactId>ignite-tools</artifactId>
+            <version>${ignite.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/IgniteRepository.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/IgniteRepository.java
new file mode 100644
index 0000000..472d2e0
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/IgniteRepository.java
@@ -0,0 +1,58 @@
+/*
+ * 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.springdata.repository;
+
+import java.io.Serializable;
+import java.util.Map;
+import org.springframework.data.repository.CrudRepository;
+
+/**
+ * Apache Ignite repository that extends basic capabilities of {@link CrudRepository}.
+ */
+public interface IgniteRepository<T, ID extends Serializable> extends CrudRepository<T, ID> {
+    /**
+     * Saves a given entity using provided key.
+     * </p>
+     * It's suggested to use this method instead of default {@link CrudRepository#save(Object)} that generates
+     * IDs (keys) that are not unique cluster wide.
+     *
+     * @param key Entity's key.
+     * @param entity Entity to save.
+     * @param <S> Entity type.
+     * @return Saved entity.
+     */
+    <S extends T> S save(ID key, S entity);
+
+    /**
+     * Saves all given keys and entities combinations.
+     * </p>
+     * It's suggested to use this method instead of default {@link CrudRepository#save(Iterable)} that generates
+     * IDs (keys) that are not unique cluster wide.
+     *
+     * @param entities Map of key-entities pairs to save.
+     * @param <S> type of entities.
+     * @return Saved entities.
+     */
+    <S extends T> Iterable<S> save(Map<ID, S> entities);
+
+    /**
+     * Deletes all the entities for the provided ids.
+     *
+     * @param ids List of ids to delete.
+     */
+    void deleteAll(Iterable<ID> ids);
+}
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/EnableIgniteRepositories.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/EnableIgniteRepositories.java
new file mode 100644
index 0000000..f73fc3d
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/EnableIgniteRepositories.java
@@ -0,0 +1,119 @@
+/*
+ * 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.springdata.repository.config;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.apache.ignite.springdata.repository.support.IgniteRepositoryFactoryBean;
+import org.apache.ignite.springdata.repository.support.IgniteRepositoryImpl;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.context.annotation.ComponentScan.Filter;
+import org.springframework.context.annotation.Import;
+import org.springframework.data.repository.query.QueryLookupStrategy;
+import org.springframework.data.repository.query.QueryLookupStrategy.Key;
+
+/**
+ * Annotation to activate Apache Ignite repositories. If no base package is configured through either {@link #value()},
+ * {@link #basePackages()} or {@link #basePackageClasses()} it will trigger scanning of the package of annotated class.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+@Import(IgniteRepositoriesRegistar.class)
+public @interface EnableIgniteRepositories {
+    /**
+     * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.:
+     * {@code @EnableIgniteRepositories("org.my.pkg")} instead of
+     * {@code @EnableIgniteRepositories(basePackages="org.my.pkg")}.
+     */
+    String[] value() default {};
+
+    /**
+     * Base packages to scan for annotated components. {@link #value()} is an alias for (and mutually exclusive with)
+     * this attribute. Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names.
+     */
+    String[] basePackages() default {};
+
+    /**
+     * Type-safe alternative to {@link #basePackages()} for specifying the packages to scan for annotated components.
+     * The package of each class specified will be scanned. Consider creating a special no-op marker class or interface
+     * in each package that serves no purpose other than being referenced by this attribute.
+     */
+    Class<?>[] basePackageClasses() default {};
+
+    /**
+     * Specifies which types are not eligible for component scanning.
+     */
+    Filter[] excludeFilters() default {};
+
+    /**
+     * Specifies which types are eligible for component scanning. Further narrows the set of candidate components from
+     * everything in {@link #basePackages()} to everything in the base packages that matches the given filter or
+     * filters.
+     */
+    Filter[] includeFilters() default {};
+
+    /**
+     * Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So
+     * for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning
+     * for {@code PersonRepositoryImpl}.
+     *
+     * @return Postfix to be used when looking up custom repository implementations.
+     */
+    String repositoryImplementationPostfix() default "Impl";
+
+    /**
+     * Configures the location of where to find the Spring Data named queries properties file.
+     *
+     * @return Location of where to find the Spring Data named queries properties file.
+     */
+    String namedQueriesLocation() default "";
+
+    /**
+     * Returns the key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods. Defaults to
+     * {@link Key#CREATE_IF_NOT_FOUND}.
+     *
+     * @return Key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods.
+     */
+    Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND;
+
+    /**
+     * Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to
+     * {@link IgniteRepositoryFactoryBean}.
+     *
+     * @return {@link FactoryBean} class to be used for each repository instance.
+     */
+    Class<?> repositoryFactoryBeanClass() default IgniteRepositoryFactoryBean.class;
+
+    /**
+     * Configure the repository base class to be used to create repository proxies for this particular configuration.
+     *
+     * @return Repository base class to be used to create repository proxies for this particular configuration.
+     */
+    Class<?> repositoryBaseClass() default IgniteRepositoryImpl.class;
+
+    /**
+     * Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the
+     * repositories infrastructure.
+     */
+    boolean considerNestedRepositories() default false;
+}
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/IgniteRepositoriesRegistar.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/IgniteRepositoriesRegistar.java
new file mode 100644
index 0000000..0f65c32
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/IgniteRepositoriesRegistar.java
@@ -0,0 +1,36 @@
+/*
+ * 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.springdata.repository.config;
+
+import java.lang.annotation.Annotation;
+import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport;
+import org.springframework.data.repository.config.RepositoryConfigurationExtension;
+
+/**
+ * Apache Ignite specific implementation of {@link RepositoryBeanDefinitionRegistrarSupport}.
+ */
+public class IgniteRepositoriesRegistar extends RepositoryBeanDefinitionRegistrarSupport {
+    /** {@inheritDoc} */
+    @Override protected Class<? extends Annotation> getAnnotation() {
+        return EnableIgniteRepositories.class;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected RepositoryConfigurationExtension getExtension() {
+        return new IgniteRepositoryConfigurationExtension();
+    }
+}
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/IgniteRepositoryConfigurationExtension.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/IgniteRepositoryConfigurationExtension.java
new file mode 100644
index 0000000..630690a
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/IgniteRepositoryConfigurationExtension.java
@@ -0,0 +1,49 @@
+/*
+ * 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.springdata.repository.config;
+
+import java.util.Collection;
+import java.util.Collections;
+import org.apache.ignite.springdata.repository.IgniteRepository;
+import org.apache.ignite.springdata.repository.support.IgniteRepositoryFactoryBean;
+import org.springframework.data.repository.config.RepositoryConfigurationExtension;
+import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport;
+
+/**
+ * Apache Ignite specific implementation of {@link RepositoryConfigurationExtension}.
+ */
+public class IgniteRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport {
+    /** {@inheritDoc} */
+    @Override public String getModuleName() {
+        return "Apache Ignite";
+    }
+
+    /** {@inheritDoc} */
+    @Override protected String getModulePrefix() {
+        return "ignite";
+    }
+
+    /** {@inheritDoc} */
+    @Override public String getRepositoryFactoryClassName() {
+        return IgniteRepositoryFactoryBean.class.getName();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected Collection<Class<?>> getIdentifyingTypes() {
+        return Collections.<Class<?>>singleton(IgniteRepository.class);
+    }
+}
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/Query.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/Query.java
new file mode 100644
index 0000000..1095942
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/Query.java
@@ -0,0 +1,37 @@
+/*
+ * 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.springdata.repository.config;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to provide a user defined SQL query for a method.
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface Query {
+    /**
+     * SQL query text string.
+     */
+    String value() default "";
+}
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/RepositoryConfig.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/RepositoryConfig.java
new file mode 100644
index 0000000..4a3703e
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/RepositoryConfig.java
@@ -0,0 +1,39 @@
+/*
+ * 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.springdata.repository.config;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * The annotation can be used to pass Ignite specific parameters to a bound repository.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface RepositoryConfig {
+    /**
+     * @return A name of a distributed Apache Ignite cache an annotated repository will be mapped to.
+     */
+    String cacheName() default "";
+}
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/package-info.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/package-info.java
new file mode 100644
index 0000000..c7f473b
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/config/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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 description. -->
+ * Package includes Spring Data integration related configuration files.
+ */
+
+package org.apache.ignite.springdata.repository.config;
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/package-info.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/package-info.java
new file mode 100644
index 0000000..773abea
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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 description. -->
+ * Package contains Apache Ignite Spring Data integration.
+ */
+
+package org.apache.ignite.springdata.repository;
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/IgniteQuery.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/IgniteQuery.java
new file mode 100644
index 0000000..f630ca0
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/IgniteQuery.java
@@ -0,0 +1,83 @@
+/*
+ * 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.springdata.repository.query;
+
+/**
+ * Ignite query helper class. For internal use only.
+ */
+public class IgniteQuery {
+    /** */
+    enum Option {
+        /** Query will be used with Sort object. */
+        SORTING,
+
+        /** Query will be used with Pageable object. */
+        PAGINATION,
+
+        /** No advanced option. */
+        NONE
+    }
+
+    /** Sql query text string. */
+    private final String sql;
+
+    /** */
+    private final boolean isFieldQuery;
+
+    /** Type of option. */
+    private final Option option;
+
+    /**
+     * @param sql Sql.
+     * @param isFieldQuery Is field query.
+     * @param option Option.
+     */
+    public IgniteQuery(String sql, boolean isFieldQuery, Option option) {
+        this.sql = sql;
+        this.isFieldQuery = isFieldQuery;
+        this.option = option;
+    }
+
+    /**
+     * Text string of the query.
+     *
+     * @return SQL query text string.
+     */
+    public String sql() {
+        return sql;
+    }
+
+    /**
+     * Returns {@code true} if it's Ignite SQL fields query, {@code false} otherwise.
+     *
+     * @return {@code true} if it's Ignite SQL fields query, {@code false} otherwise.
+     */
+    public boolean isFieldQuery() {
+        return isFieldQuery;
+    }
+
+    /**
+     * Advanced querying option.
+     *
+     * @return querying option.
+     */
+    public Option options() {
+        return option;
+    }
+}
+
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/IgniteQueryGenerator.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/IgniteQueryGenerator.java
new file mode 100644
index 0000000..e1f3f2f
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/IgniteQueryGenerator.java
@@ -0,0 +1,265 @@
+/*
+ * 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.springdata.repository.query;
+
+import java.lang.reflect.Method;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mapping.PropertyPath;
+import org.springframework.data.mapping.PropertyReferenceException;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.query.parser.Part;
+import org.springframework.data.repository.query.parser.PartTree;
+
+/**
+ * Ignite query generator for Spring Data framework.
+ */
+public class IgniteQueryGenerator {
+
+    /**
+     * @param mtd Method.
+     * @param metadata Metadata.
+     */
+    @NotNull public static IgniteQuery generateSql(Method mtd, RepositoryMetadata metadata) {
+        PartTree parts;
+
+        try {
+            parts = new PartTree(mtd.getName(), metadata.getDomainType());
+        }
+        catch (PropertyReferenceException e) {
+            parts = new PartTree(mtd.getName(), metadata.getIdType());
+        }
+
+        StringBuilder sql = new StringBuilder();
+
+        if (parts.isDelete())
+            throw new UnsupportedOperationException("DELETE clause is not supported now.");
+        else {
+            sql.append("SELECT ");
+
+            if (parts.isDistinct())
+                throw new UnsupportedOperationException("DISTINCT clause in not supported.");
+
+            if (parts.isCountProjection())
+                sql.append("COUNT(1) ");
+            else
+                sql.append(" * ");
+        }
+
+        sql.append("FROM ").append(metadata.getDomainType().getSimpleName());
+
+        if (parts.iterator().hasNext()) {
+            sql.append(" WHERE ");
+
+            for (PartTree.OrPart orPart : parts) {
+                sql.append("(");
+                for (Part part : orPart) {
+                    handleQueryPart(sql, part, metadata.getDomainType());
+                    sql.append(" AND ");
+                }
+
+                sql.delete(sql.length() - 5, sql.length());
+
+                sql.append(") OR ");
+            }
+
+            sql.delete(sql.length() - 4, sql.length());
+        }
+
+        addSorting(sql, parts.getSort());
+
+        if (parts.isLimiting()) {
+            sql.append(" LIMIT ");
+            sql.append(parts.getMaxResults().intValue());
+        }
+
+        return new IgniteQuery(sql.toString(), parts.isCountProjection(), getOptions(mtd));
+    }
+
+    /**
+     * Add a dynamic part of query for the sorting support.
+     *
+     * @param sql SQL text string.
+     * @param sort Sort method.
+     */
+    public static StringBuilder addSorting(StringBuilder sql, Sort sort) {
+        if (sort != null) {
+            sql.append(" ORDER BY ");
+
+            for (Sort.Order order : sort) {
+                sql.append(order.getProperty()).append(" ").append(order.getDirection());
+
+                if (order.getNullHandling() != Sort.NullHandling.NATIVE) {
+                    sql.append(" ").append("NULL ");
+                    switch (order.getNullHandling()) {
+                        case NULLS_FIRST:
+                            sql.append("FIRST");
+                            break;
+                        case NULLS_LAST:
+                            sql.append("LAST");
+                            break;
+                    }
+                }
+                sql.append(", ");
+            }
+
+            sql.delete(sql.length() - 2, sql.length());
+        }
+
+        return sql;
+    }
+
+    /**
+     * Add a dynamic part of a query for the pagination support.
+     *
+     * @param sql Builder instance.
+     * @param pageable Pageable instance.
+     * @return Builder instance.
+     */
+    public static StringBuilder addPaging(StringBuilder sql, Pageable pageable) {
+        if (pageable.getSort() != null)
+            addSorting(sql, pageable.getSort());
+
+        sql.append(" LIMIT ").append(pageable.getPageSize()).append(" OFFSET ").append(pageable.getOffset());
+
+        return sql;
+    }
+
+    /**
+     * Determines whether query is dynamic or not (by list of method parameters)
+     *
+     * @param mtd Method.
+     * @return type of options
+     */
+    public static IgniteQuery.Option getOptions(Method mtd) {
+        IgniteQuery.Option option = IgniteQuery.Option.NONE;
+
+        Class<?>[] types = mtd.getParameterTypes();
+
+        if (types.length > 0) {
+            Class<?> type = types[types.length - 1];
+
+            if (Sort.class.isAssignableFrom(type))
+                option = IgniteQuery.Option.SORTING;
+            else if (Pageable.class.isAssignableFrom(type))
+                option = IgniteQuery.Option.PAGINATION;
+        }
+
+        for (int i = 0; i < types.length - 1; i++) {
+            Class<?> tp = types[i];
+            if (tp == Sort.class || tp == Pageable.class)
+                throw new AssertionError("Sort and Pageable parameters are allowed only in the last position");
+        }
+
+        return option;
+    }
+
+    /**
+     * Check and correct table name if using column name from compound key.
+     */
+    private static String getColumnName(Part part, Class<?> domainType) {
+        PropertyPath prperty = part.getProperty();
+
+        if (prperty.getType() != domainType)
+            return domainType.getSimpleName() + "." + prperty.getSegment();
+        else
+            return part.toString();
+    }
+
+    /**
+     * Transform part to sql expression
+     */
+    private static void handleQueryPart(StringBuilder sql, Part part, Class<?> domainType) {
+        sql.append("(");
+
+        sql.append(getColumnName(part, domainType));
+
+        switch (part.getType()) {
+            case SIMPLE_PROPERTY:
+                sql.append("=?");
+                break;
+            case NEGATING_SIMPLE_PROPERTY:
+                sql.append("<>?");
+                break;
+            case GREATER_THAN:
+                sql.append(">?");
+                break;
+            case GREATER_THAN_EQUAL:
+                sql.append(">=?");
+                break;
+            case LESS_THAN:
+                sql.append("<?");
+                break;
+            case LESS_THAN_EQUAL:
+                sql.append("<=?");
+                break;
+            case IS_NOT_NULL:
+                sql.append(" IS NOT NULL");
+                break;
+            case IS_NULL:
+                sql.append(" IS NULL");
+                break;
+            case BETWEEN:
+                sql.append(" BETWEEN ? AND ?");
+                break;
+            case FALSE:
+                sql.append(" = FALSE");
+                break;
+            case TRUE:
+                sql.append(" = TRUE");
+                break;
+            case CONTAINING:
+                sql.append(" LIKE '%' || ? || '%'");
+                break;
+            case NOT_CONTAINING:
+                sql.append(" NOT LIKE '%' || ? || '%'");
+                break;
+            case LIKE:
+                sql.append(" LIKE '%' || ? || '%'");
+                break;
+            case NOT_LIKE:
+                sql.append(" NOT LIKE '%' || ? || '%'");
+                break;
+            case STARTING_WITH:
+                sql.append(" LIKE  ? || '%'");
+                break;
+            case ENDING_WITH:
+                sql.append(" LIKE '%' || ?");
+                break;
+            case IN:
+                sql.append(" IN ?");
+                break;
+            case NOT_IN:
+                sql.append(" NOT IN ?");
+                break;
+            case REGEX:
+                sql.append(" REGEXP ?");
+                break;
+            case NEAR:
+            case AFTER:
+            case BEFORE:
+            case EXISTS:
+            default:
+                throw new UnsupportedOperationException(part.getType() + " is not supported!");
+        }
+
+        sql.append(")");
+    }
+}
+
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/IgniteRepositoryQuery.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/IgniteRepositoryQuery.java
new file mode 100644
index 0000000..e351c61
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/IgniteRepositoryQuery.java
@@ -0,0 +1,326 @@
+/*
+ * 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.springdata.repository.query;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.query.Query;
+import org.apache.ignite.cache.query.QueryCursor;
+import org.apache.ignite.cache.query.SqlFieldsQuery;
+import org.apache.ignite.cache.query.SqlQuery;
+import org.apache.ignite.internal.processors.cache.CacheEntryImpl;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.domain.SliceImpl;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.query.QueryMethod;
+import org.springframework.data.repository.query.RepositoryQuery;
+
+import static org.apache.ignite.springdata.repository.query.IgniteQueryGenerator.addPaging;
+import static org.apache.ignite.springdata.repository.query.IgniteQueryGenerator.addSorting;
+
+/**
+ * Ignite SQL query implementation.
+ */
+@SuppressWarnings({"unchecked", "rawtypes"})
+public class IgniteRepositoryQuery implements RepositoryQuery {
+    /** Defines the way how to process query result */
+    private enum ReturnStrategy {
+        /** Need to return only one value. */
+        ONE_VALUE,
+
+        /** Need to return one cache entry */
+        CACHE_ENTRY,
+
+        /** Need to return list of cache entries */
+        LIST_OF_CACHE_ENTRIES,
+
+        /** Need to return list of values */
+        LIST_OF_VALUES,
+
+        /** Need to return list of lists */
+        LIST_OF_LISTS,
+
+        /** Need to return slice */
+        SLICE_OF_VALUES,
+
+        /** Slice of cache entries. */
+        SLICE_OF_CACHE_ENTRIES,
+
+        /** Slice of lists. */
+        SLICE_OF_LISTS
+    }
+
+    /** Type. */
+    private final Class<?> type;
+
+    /** Sql. */
+    private final IgniteQuery qry;
+
+    /** Cache. */
+    private final IgniteCache cache;
+
+    /** Method. */
+    private final Method mtd;
+
+    /** Metadata. */
+    private final RepositoryMetadata metadata;
+
+    /** Factory. */
+    private final ProjectionFactory factory;
+
+    /** Return strategy. */
+    private final ReturnStrategy returnStgy;
+
+    /**
+     * @param metadata Metadata.
+     * @param qry Query.
+     * @param mtd Method.
+     * @param factory Factory.
+     * @param cache Cache.
+     */
+    public IgniteRepositoryQuery(RepositoryMetadata metadata, IgniteQuery qry,
+        Method mtd, ProjectionFactory factory, IgniteCache cache) {
+        type = metadata.getDomainType();
+        this.qry = qry;
+        this.cache = cache;
+        this.metadata = metadata;
+        this.mtd = mtd;
+        this.factory = factory;
+
+        returnStgy = calcReturnType(mtd, qry.isFieldQuery());
+    }
+
+    /** {@inheritDoc} */
+    @Override public Object execute(Object[] prmtrs) {
+        Query qry = prepareQuery(prmtrs);
+
+        try (QueryCursor qryCursor = cache.query(qry)) {
+            return transformQueryCursor(prmtrs, qryCursor);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override public QueryMethod getQueryMethod() {
+        return new QueryMethod(mtd, metadata, factory);
+    }
+
+    /**
+     * @param mtd Method.
+     * @param isFieldQry Is field query.
+     * @return Return strategy type.
+     */
+    private ReturnStrategy calcReturnType(Method mtd, boolean isFieldQry) {
+        Class<?> returnType = mtd.getReturnType();
+
+        if (returnType.isAssignableFrom(ArrayList.class)) {
+            if (isFieldQry) {
+                if (hasAssignableGenericReturnTypeFrom(ArrayList.class, mtd))
+                    return ReturnStrategy.LIST_OF_LISTS;
+            }
+            else if (hasAssignableGenericReturnTypeFrom(Cache.Entry.class, mtd))
+                return ReturnStrategy.LIST_OF_CACHE_ENTRIES;
+
+            return ReturnStrategy.LIST_OF_VALUES;
+        }
+        else if (returnType == Slice.class) {
+            if (isFieldQry) {
+                if (hasAssignableGenericReturnTypeFrom(ArrayList.class, mtd))
+                    return ReturnStrategy.SLICE_OF_LISTS;
+            }
+            else if (hasAssignableGenericReturnTypeFrom(Cache.Entry.class, mtd))
+                return ReturnStrategy.SLICE_OF_CACHE_ENTRIES;
+
+            return ReturnStrategy.SLICE_OF_VALUES;
+        }
+        else if (Cache.Entry.class.isAssignableFrom(returnType))
+            return ReturnStrategy.CACHE_ENTRY;
+        else
+            return ReturnStrategy.ONE_VALUE;
+    }
+
+    /**
+     * @param cls Class 1.
+     * @param mtd Method.
+     * @return if {@code mtd} return type is assignable from {@code cls}
+     */
+    private boolean hasAssignableGenericReturnTypeFrom(Class<?> cls, Method mtd) {
+        Type[] actualTypeArguments = ((ParameterizedType)mtd.getGenericReturnType()).getActualTypeArguments();
+
+        if (actualTypeArguments.length == 0)
+            return false;
+
+        if (actualTypeArguments[0] instanceof ParameterizedType) {
+            ParameterizedType type = (ParameterizedType)actualTypeArguments[0];
+
+            Class<?> type1 = (Class)type.getRawType();
+
+            return type1.isAssignableFrom(cls);
+        }
+
+        if (actualTypeArguments[0] instanceof Class) {
+            Class typeArg = (Class)actualTypeArguments[0];
+
+            return typeArg.isAssignableFrom(cls);
+        }
+
+        return false;
+    }
+
+    /**
+     * @param prmtrs Prmtrs.
+     * @param qryCursor Query cursor.
+     * @return Query cursor or slice
+     */
+    @Nullable private Object transformQueryCursor(Object[] prmtrs, QueryCursor qryCursor) {
+        if (qry.isFieldQuery()) {
+            Iterable<List> qryIter = (Iterable<List>)qryCursor;
+
+            switch (returnStgy) {
+                case LIST_OF_VALUES:
+                    List list = new ArrayList<>();
+
+                    for (List entry : qryIter)
+                        list.add(entry.get(0));
+
+                    return list;
+
+                case ONE_VALUE:
+                    Iterator<List> iter = qryIter.iterator();
+
+                    if (iter.hasNext())
+                        return iter.next().get(0);
+
+                    return null;
+
+                case SLICE_OF_VALUES:
+                    List content = new ArrayList<>();
+
+                    for (List entry : qryIter)
+                        content.add(entry.get(0));
+
+                    return new SliceImpl(content, (Pageable)prmtrs[prmtrs.length - 1], true);
+
+                case SLICE_OF_LISTS:
+                    return new SliceImpl(qryCursor.getAll(), (Pageable)prmtrs[prmtrs.length - 1], true);
+
+                case LIST_OF_LISTS:
+                    return qryCursor.getAll();
+
+                default:
+                    throw new IllegalStateException();
+            }
+        }
+        else {
+            Iterable<CacheEntryImpl> qryIter = (Iterable<CacheEntryImpl>)qryCursor;
+
+            switch (returnStgy) {
+                case LIST_OF_VALUES:
+                    List list = new ArrayList<>();
+
+                    for (CacheEntryImpl entry : qryIter)
+                        list.add(entry.getValue());
+
+                    return list;
+
+                case ONE_VALUE:
+                    Iterator<CacheEntryImpl> iter1 = qryIter.iterator();
+
+                    if (iter1.hasNext())
+                        return iter1.next().getValue();
+
+                    return null;
+
+                case CACHE_ENTRY:
+                    Iterator<CacheEntryImpl> iter2 = qryIter.iterator();
+
+                    if (iter2.hasNext())
+                        return iter2.next();
+
+                    return null;
+
+                case SLICE_OF_VALUES:
+                    List content = new ArrayList<>();
+
+                    for (CacheEntryImpl entry : qryIter)
+                        content.add(entry.getValue());
+
+                    return new SliceImpl(content, (Pageable)prmtrs[prmtrs.length - 1], true);
+
+                case SLICE_OF_CACHE_ENTRIES:
+                    return new SliceImpl(qryCursor.getAll(), (Pageable)prmtrs[prmtrs.length - 1], true);
+
+                case LIST_OF_CACHE_ENTRIES:
+                    return qryCursor.getAll();
+
+                default:
+                    throw new IllegalStateException();
+            }
+        }
+    }
+
+    /**
+     * @param prmtrs Prmtrs.
+     * @return prepared query for execution
+     */
+    @SuppressWarnings("deprecation")
+    @NotNull private Query prepareQuery(Object[] prmtrs) {
+        Object[] parameters = prmtrs;
+        String sql = qry.sql();
+
+        switch (qry.options()) {
+            case SORTING:
+                sql = addSorting(new StringBuilder(sql), (Sort)parameters[parameters.length - 1]).toString();
+                parameters = Arrays.copyOfRange(parameters, 0, parameters.length - 1);
+
+                break;
+
+            case PAGINATION:
+                sql = addPaging(new StringBuilder(sql), (Pageable)parameters[parameters.length - 1]).toString();
+                parameters = Arrays.copyOfRange(parameters, 0, parameters.length - 1);
+
+                break;
+
+            case NONE:
+                // No-op.
+        }
+
+        if (qry.isFieldQuery()) {
+            SqlFieldsQuery sqlFieldsQry = new SqlFieldsQuery(sql);
+            sqlFieldsQry.setArgs(parameters);
+
+            return sqlFieldsQry;
+        }
+
+        SqlQuery sqlQry = new SqlQuery(type, sql);
+        sqlQry.setArgs(parameters);
+
+        return sqlQry;
+    }
+}
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/package-info.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/package-info.java
new file mode 100644
index 0000000..9b6fdb1
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/query/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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 description. -->
+ * Package includes classes that integrates with Apache Ignite SQL engine.
+ */
+
+package org.apache.ignite.springdata.repository.query;
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/IgniteRepositoryFactory.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/IgniteRepositoryFactory.java
new file mode 100644
index 0000000..de2549f
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/IgniteRepositoryFactory.java
@@ -0,0 +1,168 @@
+/*
+ * 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.springdata.repository.support;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.springdata.repository.IgniteRepository;
+import org.apache.ignite.springdata.repository.config.Query;
+import org.apache.ignite.springdata.repository.config.RepositoryConfig;
+import org.apache.ignite.springdata.repository.query.IgniteQuery;
+import org.apache.ignite.springdata.repository.query.IgniteQueryGenerator;
+import org.apache.ignite.springdata.repository.query.IgniteRepositoryQuery;
+import org.springframework.data.projection.ProjectionFactory;
+import org.springframework.data.repository.core.EntityInformation;
+import org.springframework.data.repository.core.NamedQueries;
+import org.springframework.data.repository.core.RepositoryInformation;
+import org.springframework.data.repository.core.RepositoryMetadata;
+import org.springframework.data.repository.core.support.AbstractEntityInformation;
+import org.springframework.data.repository.core.support.RepositoryFactorySupport;
+import org.springframework.data.repository.query.EvaluationContextProvider;
+import org.springframework.data.repository.query.QueryLookupStrategy;
+import org.springframework.data.repository.query.RepositoryQuery;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Crucial for spring-data functionality class. Create proxies for repositories.
+ */
+public class IgniteRepositoryFactory extends RepositoryFactorySupport {
+    /** Ignite instance */
+    private Ignite ignite;
+
+    /** Mapping of a repository to a cache. */
+    private final Map<Class<?>, String> repoToCache = new HashMap<>();
+
+    /**
+     * Creates the factory with initialized {@link Ignite} instance.
+     *
+     * @param ignite
+     */
+    public IgniteRepositoryFactory(Ignite ignite) {
+        this.ignite = ignite;
+    }
+
+    /**
+     * Initializes the factory with provided {@link IgniteConfiguration} that is used to start up an underlying
+     * {@link Ignite} instance.
+     *
+     * @param cfg Ignite configuration.
+     */
+    public IgniteRepositoryFactory(IgniteConfiguration cfg) {
+        this.ignite = Ignition.start(cfg);
+    }
+
+    /**
+     * Initializes the factory with provided a configuration under {@code springCfgPath} that is used to start up
+     * an underlying {@link Ignite} instance.
+     *
+     * @param springCfgPath A path to Ignite configuration.
+     */
+    public IgniteRepositoryFactory(String springCfgPath) {
+        this.ignite = Ignition.start(springCfgPath);
+    }
+
+    /** {@inheritDoc} */
+    @Override public <T, ID extends Serializable> EntityInformation<T, ID> getEntityInformation(Class<T> domainClass) {
+        return new AbstractEntityInformation<T, ID>(domainClass) {
+            @Override public ID getId(T entity) {
+                return null;
+            }
+
+            @Override public Class<ID> getIdType() {
+                return null;
+            }
+        };
+    }
+
+    /** {@inheritDoc} */
+    @Override protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
+        return IgniteRepositoryImpl.class;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected RepositoryMetadata getRepositoryMetadata(Class<?> repoItf) {
+        Assert.notNull(repoItf, "Repository interface must be set.");
+        Assert.isAssignable(IgniteRepository.class, repoItf, "Repository must implement IgniteRepository interface.");
+
+        RepositoryConfig annotation = repoItf.getAnnotation(RepositoryConfig.class);
+
+        Assert.notNull(annotation, "Set a name of an Apache Ignite cache using @RepositoryConfig annotation to map " +
+            "this repository to the underlying cache.");
+
+        Assert.hasText(annotation.cacheName(), "Set a name of an Apache Ignite cache using @RepositoryConfig " +
+            "annotation to map this repository to the underlying cache.");
+
+        repoToCache.put(repoItf, annotation.cacheName());
+
+        return super.getRepositoryMetadata(repoItf);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected Object getTargetRepository(RepositoryInformation metadata) {
+        return getTargetRepositoryViaReflection(metadata,
+            ignite.getOrCreateCache(repoToCache.get(metadata.getRepositoryInterface())));
+    }
+
+    /** {@inheritDoc} */
+    @Override protected QueryLookupStrategy getQueryLookupStrategy(final QueryLookupStrategy.Key key,
+        EvaluationContextProvider evaluationCtxProvider) {
+
+        return new QueryLookupStrategy() {
+            @Override public RepositoryQuery resolveQuery(final Method mtd, final RepositoryMetadata metadata,
+                final ProjectionFactory factory, NamedQueries namedQueries) {
+
+                final Query annotation = mtd.getAnnotation(Query.class);
+
+                if (annotation != null) {
+                    String qryStr = annotation.value();
+
+                    if (key != Key.CREATE && StringUtils.hasText(qryStr))
+                        return new IgniteRepositoryQuery(metadata,
+                            new IgniteQuery(qryStr, isFieldQuery(qryStr), IgniteQueryGenerator.getOptions(mtd)),
+                            mtd, factory, ignite.getOrCreateCache(repoToCache.get(metadata.getRepositoryInterface())));
+                }
+
+                if (key == QueryLookupStrategy.Key.USE_DECLARED_QUERY)
+                    throw new IllegalStateException("To use QueryLookupStrategy.Key.USE_DECLARED_QUERY, pass " +
+                        "a query string via org.apache.ignite.springdata.repository.config.Query annotation.");
+
+                return new IgniteRepositoryQuery(metadata, IgniteQueryGenerator.generateSql(mtd, metadata), mtd,
+                    factory, ignite.getOrCreateCache(repoToCache.get(metadata.getRepositoryInterface())));
+            }
+        };
+    }
+
+    /**
+     * @param qry Query string.
+     * @return {@code true} if query is SQLFieldsQuery.
+     */
+    private boolean isFieldQuery(String qry) {
+        return qry.matches("^SELECT.*") && !qry.matches("^SELECT\\s+(?:\\w+\\.)?+\\*.*");
+    }
+}
+
+
+
+
+
+
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/IgniteRepositoryFactoryBean.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/IgniteRepositoryFactoryBean.java
new file mode 100644
index 0000000..f87dce5
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/IgniteRepositoryFactoryBean.java
@@ -0,0 +1,92 @@
+/*
+ * 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.springdata.repository.support;
+
+import java.io.Serializable;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteException;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.springdata.repository.IgniteRepository;
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.data.repository.Repository;
+import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport;
+import org.springframework.data.repository.core.support.RepositoryFactorySupport;
+
+/**
+ * Apache Ignite repository factory bean.
+ *
+ * The repository requires to define one of the parameters below in your Spring application configuration in order
+ * to get an access to Apache Ignite cluster:
+ * <ul>
+ * <li>{@link Ignite} instance bean named "igniteInstance"</li>
+ * <li>{@link IgniteConfiguration} bean named "igniteCfg"</li>
+ * <li>A path to Ignite's Spring XML configuration named "igniteSpringCfgPath"</li>
+ * <ul/>
+ *
+ * @param <T> Repository type, {@link IgniteRepository}
+ * @param <S> Domain object class.
+ * @param <ID> Domain object key, super expects {@link Serializable}.
+ */
+public class IgniteRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable>
+    extends RepositoryFactoryBeanSupport<T, S, ID> implements ApplicationContextAware {
+    /** Application context. */
+    private ApplicationContext ctx;
+
+    /**
+     * @param repositoryInterface Repository interface.
+     */
+    protected IgniteRepositoryFactoryBean(Class<? extends T> repositoryInterface) {
+        super(repositoryInterface);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void setApplicationContext(ApplicationContext context) throws BeansException {
+        this.ctx = context;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected RepositoryFactorySupport createRepositoryFactory() {
+        try {
+            Ignite ignite = (Ignite)ctx.getBean("igniteInstance");
+
+            return new IgniteRepositoryFactory(ignite);
+        }
+        catch (BeansException ex) {
+            try {
+                IgniteConfiguration cfg = (IgniteConfiguration)ctx.getBean("igniteCfg");
+
+                return new IgniteRepositoryFactory(cfg);
+            }
+            catch (BeansException ex2) {
+                try {
+                    String path = (String)ctx.getBean("igniteSpringCfgPath");
+
+                    return new IgniteRepositoryFactory(path);
+                }
+                catch (BeansException ex3) {
+                    throw new IgniteException("Failed to initialize Ignite repository factory. Ignite instance or" +
+                        " IgniteConfiguration or a path to Ignite's spring XML configuration must be defined in the" +
+                        " application configuration");
+                }
+            }
+        }
+    }
+}
+
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/IgniteRepositoryImpl.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/IgniteRepositoryImpl.java
new file mode 100644
index 0000000..95d933b
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/IgniteRepositoryImpl.java
@@ -0,0 +1,167 @@
+/*
+ * 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.springdata.repository.support;
+
+import java.io.Serializable;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import javax.cache.Cache;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.CachePeekMode;
+import org.apache.ignite.springdata.repository.IgniteRepository;
+
+import static java.util.Collections.emptySet;
+
+/**
+ * General Apache Ignite repository implementation.
+ */
+public class IgniteRepositoryImpl<T, ID extends Serializable> implements IgniteRepository<T, ID> {
+    /** Ignite Cache bound to the repository */
+    private final IgniteCache<ID, T> cache;
+
+    /**
+     * Repository constructor.
+     *
+     * @param cache Initialized cache instance.
+     */
+    public IgniteRepositoryImpl(IgniteCache<ID, T> cache) {
+        this.cache = cache;
+    }
+
+    /** {@inheritDoc} */
+    @Override public <S extends T> S save(ID key, S entity) {
+        cache.put(key, entity);
+
+        return entity;
+    }
+
+    /** {@inheritDoc} */
+    @Override public <S extends T> Iterable<S> save(Map<ID, S> entities) {
+        cache.putAll(entities);
+
+        return entities.values();
+    }
+
+    /** {@inheritDoc} */
+    @Override public <S extends T> S save(S entity) {
+        throw new UnsupportedOperationException("Use IgniteRepository.save(key,value) method instead.");
+    }
+
+    /** {@inheritDoc} */
+    @Override public <S extends T> Iterable<S> save(Iterable<S> entities) {
+        throw new UnsupportedOperationException("Use IgniteRepository.save(Map<keys,value>) method instead.");
+    }
+
+    /** {@inheritDoc} */
+    @Override public T findOne(ID id) {
+        return cache.get(id);
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean exists(ID id) {
+        return cache.containsKey(id);
+    }
+
+    /** {@inheritDoc} */
+    @Override public Iterable<T> findAll() {
+        final Iterator<Cache.Entry<ID, T>> iter = cache.iterator();
+
+        return new Iterable<T>() {
+            @Override public Iterator<T> iterator() {
+                return new Iterator<T>() {
+                    @Override public boolean hasNext() {
+                        return iter.hasNext();
+                    }
+
+                    @Override public T next() {
+                        return iter.next().getValue();
+                    }
+
+                    @Override public void remove() {
+                        iter.remove();
+                    }
+                };
+            }
+        };
+    }
+
+    /**
+     * @param ids Collection of IDs.
+     * @return Collection transformed to set.
+     */
+    private Set<ID> toSet(Iterable<ID> ids) {
+        if (ids instanceof Set)
+            return (Set<ID>)ids;
+
+        Iterator<ID> itr = ids.iterator();
+
+        if (!itr.hasNext())
+            return emptySet();
+
+        ID key = itr.next();
+
+        Set<ID> keys = key instanceof Comparable ? new TreeSet<>() : new HashSet<>();
+
+        keys.add(key);
+
+        while (itr.hasNext()) {
+            key = itr.next();
+
+            keys.add(key);
+        }
+
+        return keys;
+    }
+
+    /** {@inheritDoc} */
+    @Override public Iterable<T> findAll(Iterable<ID> ids) {
+        return cache.getAll(toSet(ids)).values();
+    }
+
+    /** {@inheritDoc} */
+    @Override public long count() {
+        return cache.size(CachePeekMode.PRIMARY);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void delete(ID id) {
+        cache.remove(id);
+    }
+
+    /** {@inheritDoc} */
+    @Override public void delete(T entity) {
+        throw new UnsupportedOperationException("Use IgniteRepository.delete(key) method instead.");
+    }
+
+    /** {@inheritDoc} */
+    @Override public void delete(Iterable<? extends T> entities) {
+        throw new UnsupportedOperationException("Use IgniteRepository.deleteAll(keys) method instead.");
+    }
+
+    /** {@inheritDoc} */
+    @Override public void deleteAll(Iterable<ID> ids) {
+        cache.removeAll(toSet(ids));
+    }
+
+    /** {@inheritDoc} */
+    @Override public void deleteAll() {
+        cache.clear();
+    }
+}
diff --git a/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/package-info.java b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/package-info.java
new file mode 100644
index 0000000..749c9e7
--- /dev/null
+++ b/modules/spring-data-ext/src/main/java/org/apache/ignite/springdata/repository/support/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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 description. -->
+ * Package contains supporting files required by Spring Data framework.
+ */
+
+package org.apache.ignite.springdata.repository.support;
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCompoundKeyTest.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCompoundKeyTest.java
new file mode 100644
index 0000000..62ff8c5
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCompoundKeyTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.springdata;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.Statement;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.springdata.compoundkey.City;
+import org.apache.ignite.springdata.compoundkey.CityRepository;
+import org.apache.ignite.springdata.compoundkey.CompoundKeyApplicationConfiguration;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+
+import static org.apache.ignite.springdata.compoundkey.CompoundKeyApplicationConfiguration.CLI_CONN_PORT;
+
+/**
+ * Test with using conpoud key in spring-data
+ * */
+public class IgniteSpringDataCompoundKeyTest extends GridCommonAbstractTest {
+    /** Application context */
+    private static AnnotationConfigApplicationContext ctx;
+
+    /** City repository */
+    private static CityRepository repo;
+
+    /** Cache name */
+    private static final String CACHE_NAME = "City";
+
+    /** Cities count */
+    private static final int TOTAL_COUNT = 5;
+
+    /** Count Afganistan cities */
+    private static final int AFG_COUNT = 4;
+
+    /** Quandahar identifier */
+    private static final int QUANDAHAR_ID = 2;
+
+    /** Afganistan county code */
+    private static final String AFG = "AFG";
+
+    /** test city Quandahar */
+    private static final City QUANDAHAR = new City("Qandahar","Qandahar", 237500);
+
+    /**
+     * Performs context initialization before tests.
+     */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        ctx = new AnnotationConfigApplicationContext();
+        ctx.register(CompoundKeyApplicationConfiguration.class);
+        ctx.refresh();
+
+        repo = ctx.getBean(CityRepository.class);
+    }
+
+    /**
+     * Load data
+     * */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        loadData();
+
+        assertEquals(TOTAL_COUNT, repo.count());
+    }
+
+    /**
+     * Performs context destroy after tests.
+     */
+    @Override protected void afterTestsStopped() {
+        ctx.close();
+    }
+
+    /** load data*/
+    public void loadData() throws Exception {
+        Ignite ignite = ctx.getBean(Ignite.class);
+
+        if (ignite.cacheNames().contains(CACHE_NAME))
+            ignite.destroyCache(CACHE_NAME);
+
+        try (Connection conn = DriverManager.getConnection("jdbc:ignite:thin://127.0.0.1:" + CLI_CONN_PORT + '/')) {
+            Statement st = conn.createStatement();
+
+            st.execute("DROP TABLE IF EXISTS City");
+            st.execute("CREATE TABLE City (ID INT, Name VARCHAR, CountryCode CHAR(3), District VARCHAR, Population INT, PRIMARY KEY (ID, CountryCode)) WITH \"template=partitioned, backups=1, affinityKey=CountryCode, CACHE_NAME=City, KEY_TYPE=org.apache.ignite.springdata.compoundkey.CityKey, VALUE_TYPE=org.apache.ignite.springdata.compoundkey.City\"");
+            st.execute("SET STREAMING ON;");
+            st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (1,'Kabul','AFG','Kabol',1780000)");
+            st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (2,'Qandahar','AFG','Qandahar',237500)");
+            st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (3,'Herat','AFG','Herat',186800)");
+            st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (4,'Mazar-e-Sharif','AFG','Balkh',127800)");
+            st.execute("INSERT INTO City(ID, Name, CountryCode, District, Population) VALUES (5,'Amsterdam','NLD','Noord-Holland',731200)");
+        }
+    }
+
+    /** Test */
+    @Test
+    public void test() {
+        assertEquals(AFG_COUNT, repo.findByCountryCode(AFG).size());
+        assertEquals(QUANDAHAR, repo.findById(QUANDAHAR_ID));
+    }
+}
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfTest.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfTest.java
new file mode 100644
index 0000000..65f9239
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfTest.java
@@ -0,0 +1,290 @@
+/*
+ * 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.springdata;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.TreeSet;
+import org.apache.ignite.internal.IgniteEx;
+import org.apache.ignite.internal.processors.query.RunningQueryManager;
+import org.apache.ignite.internal.processors.query.h2.IgniteH2Indexing;
+import org.apache.ignite.springdata.misc.ApplicationConfiguration;
+import org.apache.ignite.springdata.misc.Person;
+import org.apache.ignite.springdata.misc.PersonKey;
+import org.apache.ignite.springdata.misc.PersonRepository;
+import org.apache.ignite.springdata.misc.PersonRepositoryWithCompoundKey;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+
+/**
+ * CRUD tests.
+ */
+public class IgniteSpringDataCrudSelfTest extends GridCommonAbstractTest {
+    /** Number of entries to store. */
+    private static final int CACHE_SIZE = 1000;
+
+    /** Context. */
+    private static AnnotationConfigApplicationContext ctx;
+
+    /** Repository. */
+    private static PersonRepository repo;
+
+    /** Repository. */
+    private static PersonRepositoryWithCompoundKey repoWithCompoundKey;
+
+    /** */
+    @Rule
+    public final ExpectedException expected = ExpectedException.none();
+
+    /** */
+    private static IgniteEx ignite;
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        ctx = new AnnotationConfigApplicationContext();
+        ctx.register(ApplicationConfiguration.class);
+        ctx.refresh();
+
+        repo = ctx.getBean(PersonRepository.class);
+        repoWithCompoundKey = ctx.getBean(PersonRepositoryWithCompoundKey.class);
+        ignite = ctx.getBean(IgniteEx.class);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        fillInRepository();
+
+        assertEquals(CACHE_SIZE, repo.count());
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        repo.deleteAll();
+
+        assertEquals(0, repo.count());
+
+        super.afterTest();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTestsStopped() {
+        ctx.destroy();
+    }
+
+    /** */
+    @Test
+    public void testPutGet() {
+        Person person = new Person("some_name", "some_surname");
+
+        int id = CACHE_SIZE + 1;
+
+        assertEquals(person, repo.save(id, person));
+
+        assertTrue(repo.exists(id));
+
+        assertEquals(person, repo.findOne(id));
+
+        expected.expect(UnsupportedOperationException.class);
+        expected.expectMessage("Use IgniteRepository.save(key,value) method instead.");
+        repo.save(person);
+    }
+
+    /** */
+    @Test
+    public void testPutAllGetAll() {
+        LinkedHashMap<Integer, Person> map = new LinkedHashMap<>();
+
+        for (int i = CACHE_SIZE; i < CACHE_SIZE + 50; i++)
+            map.put(i, new Person("some_name" + i, "some_surname" + i));
+
+        Iterator<Person> persons = repo.save(map).iterator();
+
+        assertEquals(CACHE_SIZE + 50, repo.count());
+
+        Iterator<Person> origPersons = map.values().iterator();
+
+        while (persons.hasNext())
+            assertEquals(origPersons.next(), persons.next());
+
+        expected.expect(UnsupportedOperationException.class);
+        expected.expectMessage("Use IgniteRepository.save(Map<keys,value>) method instead.");
+        repo.save(map.values());
+
+        persons = repo.findAll(map.keySet()).iterator();
+
+        int counter = 0;
+
+        while (persons.hasNext()) {
+            persons.next();
+            counter++;
+        }
+
+        assertEquals(map.size(), counter);
+    }
+
+    /** */
+    @Test
+    public void testGetAll() {
+        assertEquals(CACHE_SIZE, repo.count());
+
+        Iterator<Person> persons = repo.findAll().iterator();
+
+        int counter = 0;
+
+        while (persons.hasNext()) {
+            persons.next();
+            counter++;
+        }
+
+        assertEquals(repo.count(), counter);
+    }
+
+    /** */
+    @Test
+    public void testDelete() {
+        assertEquals(CACHE_SIZE, repo.count());
+
+        repo.delete(0);
+
+        assertEquals(CACHE_SIZE - 1, repo.count());
+        assertNull(repo.findOne(0));
+
+        expected.expect(UnsupportedOperationException.class);
+        expected.expectMessage("Use IgniteRepository.delete(key) method instead.");
+        repo.delete(new Person("", ""));
+    }
+
+    /**
+     *
+     */
+    @Test
+    public void testDeleteSet() {
+        assertEquals(CACHE_SIZE, repo.count());
+
+        TreeSet<Integer> ids = new TreeSet<>();
+
+        for (int i = 0; i < CACHE_SIZE / 2; i++)
+            ids.add(i);
+
+        expected.expect(UnsupportedOperationException.class);
+        expected.expectMessage("Use IgniteRepository.deleteAll(keys) method instead.");
+        repo.deleteAll(ids);
+
+        assertEquals(CACHE_SIZE / 2, repo.count());
+
+        ArrayList<Person> persons = new ArrayList<>();
+
+        for (int i = 0; i < 3; i++)
+            persons.add(new Person(String.valueOf(i), String.valueOf(i)));
+
+        repo.delete(persons);
+    }
+
+    /**
+     *
+     */
+    @Test
+    public void testDeleteAll() {
+        assertEquals(CACHE_SIZE, repo.count());
+
+        repo.deleteAll();
+
+        assertEquals(0, repo.count());
+    }
+
+    /** */
+    private void fillInRepository() {
+        for (int i = 0; i < CACHE_SIZE; i++)
+            repo.save(i, new Person("person" + Integer.toHexString(i),
+                "lastName" + Integer.toHexString((i + 16) % 256)));
+    }
+
+    /** */
+    @Test
+    public void shouldDeleteAll() {
+        List<PersonKey> ids = prepareDataWithNonComparableKeys();
+
+        repoWithCompoundKey.deleteAll(ids);
+
+        assertEquals(0, repoWithCompoundKey.count());
+    }
+
+    /** */
+    @Test
+    public void shouldFindAll() {
+        List<PersonKey> ids = prepareDataWithNonComparableKeys();
+
+        Iterable<Person> res = repoWithCompoundKey.findAll(ids);
+
+        assertEquals(2, res.spliterator().estimateSize());
+    }
+
+    /** */
+    private List<PersonKey> prepareDataWithNonComparableKeys() {
+        List<PersonKey> ids = new ArrayList<>();
+
+        PersonKey key = new PersonKey(1, 1);
+        ids.add(key);
+
+        repoWithCompoundKey.save(key, new Person("test1", "test1"));
+
+        key = new PersonKey(2, 2);
+        ids.add(key);
+
+        repoWithCompoundKey.save(key, new Person("test2", "test2"));
+
+        assertEquals(2, repoWithCompoundKey.count());
+
+        return ids;
+    }
+
+    /** */
+    @Test
+    public void shouldNotLeakCursorsInRunningQueryManager() {
+        RunningQueryManager runningQryMgr = ((IgniteH2Indexing)ignite.context().query().getIndexing()).runningQueryManager();
+
+        assertEquals(0, runningQryMgr.longRunningQueries(0).size());
+
+        List<Person> res = repo.simpleQuery("person0");
+
+        assertEquals(1, res.size());
+
+        assertEquals(0, runningQryMgr.longRunningQueries(0).size());
+
+        Person person = repo.findTopBySecondNameStartingWith("lastName");
+
+        assertNotNull(person);
+
+        assertEquals(0, runningQryMgr.longRunningQueries(0).size());
+
+        long cnt = repo.countByFirstName("person0");
+
+        assertEquals(1, cnt);
+
+        assertEquals(0, runningQryMgr.longRunningQueries(0).size());
+    }
+}
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataQueriesSelfTest.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataQueriesSelfTest.java
new file mode 100644
index 0000000..51492c1
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/IgniteSpringDataQueriesSelfTest.java
@@ -0,0 +1,320 @@
+/*
+ * 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.springdata;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.springdata.misc.ApplicationConfiguration;
+import org.apache.ignite.springdata.misc.Person;
+import org.apache.ignite.springdata.misc.PersonRepository;
+import org.apache.ignite.springdata.misc.PersonSecondRepository;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Slice;
+import org.springframework.data.domain.Sort;
+
+/**
+ *
+ */
+public class IgniteSpringDataQueriesSelfTest extends GridCommonAbstractTest {
+    /** Repository. */
+    private static PersonRepository repo;
+
+    /** Repository 2. */
+    private static PersonSecondRepository repo2;
+
+    /** Context. */
+    private static AnnotationConfigApplicationContext ctx;
+
+    /** Number of entries to store */
+    private static int CACHE_SIZE = 1000;
+
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        ctx = new AnnotationConfigApplicationContext();
+
+        ctx.register(ApplicationConfiguration.class);
+
+        ctx.refresh();
+
+        repo = ctx.getBean(PersonRepository.class);
+        repo2 = ctx.getBean(PersonSecondRepository.class);
+
+        for (int i = 0; i < CACHE_SIZE; i++)
+            repo.save(i, new Person("person" + Integer.toHexString(i),
+                "lastName" + Integer.toHexString((i + 16) % 256)));
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTestsStopped() throws Exception {
+        ctx.destroy();
+    }
+
+    /** */
+    @Test
+    public void testExplicitQuery() {
+        List<Person> persons = repo.simpleQuery("person4a");
+
+        assertFalse(persons.isEmpty());
+
+        for (Person person : persons)
+            assertEquals("person4a", person.getFirstName());
+    }
+
+    /** */
+    @Test
+    public void testEqualsPart() {
+        List<Person> persons = repo.findByFirstName("person4e");
+
+        assertFalse(persons.isEmpty());
+
+        for (Person person : persons)
+            assertEquals("person4e", person.getFirstName());
+    }
+
+    /** */
+    @Test
+    public void testContainingPart() {
+        List<Person> persons = repo.findByFirstNameContaining("person4");
+
+        assertFalse(persons.isEmpty());
+
+        for (Person person : persons)
+            assertTrue(person.getFirstName().startsWith("person4"));
+    }
+
+    /** */
+    @Test
+    public void testTopPart() {
+        Iterable<Person> top = repo.findTopByFirstNameContaining("person4");
+
+        Iterator<Person> iter = top.iterator();
+
+        Person person = iter.next();
+
+        assertFalse(iter.hasNext());
+
+        assertTrue(person.getFirstName().startsWith("person4"));
+    }
+
+    /** */
+    @Test
+    public void testLikeAndLimit() {
+        Iterable<Person> like = repo.findFirst10ByFirstNameLike("person");
+
+        int cnt = 0;
+
+        for (Person next : like) {
+            assertTrue(next.getFirstName().contains("person"));
+
+            cnt++;
+        }
+
+        assertEquals(10, cnt);
+    }
+
+    /** */
+    @Test
+    public void testCount() {
+        int cnt = repo.countByFirstNameLike("person");
+
+        assertEquals(1000, cnt);
+    }
+
+    /** */
+    @Test
+    public void testCount2() {
+        int cnt = repo.countByFirstNameLike("person4");
+
+        assertTrue(cnt < 1000);
+    }
+
+    /** */
+    @Test
+    public void testPageable() {
+        PageRequest pageable = new PageRequest(1, 5, Sort.Direction.DESC, "firstName");
+
+        HashSet<String> firstNames = new HashSet<>();
+
+        List<Person> pageable1 = repo.findByFirstNameRegex("^[a-z]+$", pageable);
+
+        assertEquals(5, pageable1.size());
+
+        for (Person person : pageable1) {
+            firstNames.add(person.getFirstName());
+            assertTrue(person.getFirstName().matches("^[a-z]+$"));
+        }
+
+        List<Person> pageable2 = repo.findByFirstNameRegex("^[a-z]+$", pageable.next());
+
+        assertEquals(5, pageable2.size());
+
+        for (Person person : pageable2) {
+            firstNames.add(person.getFirstName());
+            assertTrue(person.getFirstName().matches("^[a-z]+$"));
+        }
+
+        assertEquals(10, firstNames.size());
+    }
+
+    /** */
+    @Test
+    public void testAndAndOr() {
+        int cntAnd = repo.countByFirstNameLikeAndSecondNameLike("person1", "lastName1");
+
+        int cntOr = repo.countByFirstNameStartingWithOrSecondNameStartingWith("person1", "lastName1");
+
+        assertTrue(cntAnd <= cntOr);
+    }
+
+    /** */
+    @Test
+    public void testQueryWithSort() {
+        List<Person> persons = repo.queryWithSort("^[a-z]+$", new Sort(Sort.Direction.DESC, "secondName"));
+
+        Person previous = persons.get(0);
+
+        for (Person person : persons) {
+            assertTrue(person.getSecondName().compareTo(previous.getSecondName()) <= 0);
+
+            assertTrue(person.getFirstName().matches("^[a-z]+$"));
+
+            previous = person;
+        }
+    }
+
+    /** */
+    @Test
+    public void testQueryWithPaging() {
+        List<Person> persons = repo.queryWithPageable("^[a-z]+$", new PageRequest(1, 7, Sort.Direction.DESC, "secondName"));
+
+        assertEquals(7, persons.size());
+
+        Person previous = persons.get(0);
+
+        for (Person person : persons) {
+            assertTrue(person.getSecondName().compareTo(previous.getSecondName()) <= 0);
+
+            assertTrue(person.getFirstName().matches("^[a-z]+$"));
+
+            previous = person;
+        }
+    }
+
+    /** */
+    @Test
+    public void testQueryFields() {
+        List<String> persons = repo.selectField("^[a-z]+$", new PageRequest(1, 7, Sort.Direction.DESC, "secondName"));
+
+        assertEquals(7, persons.size());
+    }
+
+    /** */
+    @Test
+    public void testFindCacheEntries() {
+        List<Cache.Entry<Integer, Person>> cacheEntries = repo.findBySecondNameLike("stName1");
+
+        assertFalse(cacheEntries.isEmpty());
+
+        for (Cache.Entry<Integer, Person> entry : cacheEntries)
+            assertTrue(entry.getValue().getSecondName().contains("stName1"));
+    }
+
+    /** */
+    @Test
+    public void testFindOneCacheEntry() {
+        Cache.Entry<Integer, Person> cacheEntry = repo.findTopBySecondNameLike("tName18");
+
+        assertNotNull(cacheEntry);
+
+        assertTrue(cacheEntry.getValue().getSecondName().contains("tName18"));
+    }
+
+    /** */
+    @Test
+    public void testFindOneValue() {
+        Person person = repo.findTopBySecondNameStartingWith("lastName18");
+
+        assertNotNull(person);
+
+        assertTrue(person.getSecondName().startsWith("lastName18"));
+    }
+
+    /** */
+    @Test
+    public void testSelectSeveralFields() {
+        List<List> lists = repo.selectSeveralField("^[a-z]+$", new PageRequest(2, 6));
+
+        assertEquals(6, lists.size());
+
+        for (List list : lists) {
+            assertEquals(2, list.size());
+
+            assertTrue(list.get(0) instanceof Integer);
+        }
+    }
+
+    /** */
+    @Test
+    public void testCountQuery() {
+        int cnt = repo.countQuery(".*");
+
+        assertEquals(256, cnt);
+    }
+
+    /** */
+    @Test
+    public void testSliceOfCacheEntries() {
+        Slice<Cache.Entry<Integer, Person>> slice = repo2.findBySecondNameIsNot("lastName18", new PageRequest(3, 4));
+
+        assertEquals(4, slice.getSize());
+
+        for (Cache.Entry<Integer, Person> entry : slice)
+            assertFalse("lastName18".equals(entry.getValue().getSecondName()));
+    }
+
+    /** */
+    @Test
+    public void testSliceOfLists() {
+        Slice<List> lists = repo2.querySliceOfList("^[a-z]+$", new PageRequest(0, 3));
+
+        assertEquals(3, lists.getSize());
+
+        for (List list : lists) {
+            assertEquals(2, list.size());
+
+            assertTrue(list.get(0) instanceof Integer);
+        }
+    }
+
+    /**
+     * Tests the repository method with a custom query which takes no parameters.
+     */
+    @Test
+    public void testCountAllPersons() {
+        int cnt = repo.countAllPersons();
+
+        assertEquals(CACHE_SIZE, cnt);
+    }
+}
+
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/City.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/City.java
new file mode 100644
index 0000000..e86a575
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/City.java
@@ -0,0 +1,113 @@
+/*
+ * 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.springdata.compoundkey;
+
+import java.util.Objects;
+
+/**
+ * Value-class
+ * */
+public class City {
+    /** City name */
+    private String name;
+
+    /** City district */
+    private String district;
+
+    /** City population */
+    private int population;
+
+    /**
+     * @param name city name
+     * @param district city district
+     * @param population city population
+     * */
+    public City(String name, String district, int population) {
+        this.name = name;
+        this.district = district;
+        this.population = population;
+    }
+
+    /**
+     * @return city name
+     * */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * @param name city name
+     * */
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    /**
+     * @return city district
+     * */
+    public String getDistrict() {
+        return district;
+    }
+
+    /**
+     * @param district city district
+     * */
+    public void setDistrict(String district) {
+        this.district = district;
+    }
+
+    /**
+     * @return city population
+     * */
+    public int getPopulation() {
+        return population;
+    }
+
+    /**
+     * @param population city population
+     * */
+    public void setPopulation(int population) {
+        this.population = population;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return name + " | " + district + " | " + population;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+
+        if (o == null || getClass() != o.getClass())
+            return false;
+
+        City city = (City)o;
+
+        return
+                Objects.equals(this.name, city.name) &&
+                        Objects.equals(this.district, city.district) &&
+                        this.population == city.population;
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        return Objects.hash(name, district, population);
+    }
+}
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityKey.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityKey.java
new file mode 100644
index 0000000..88226fe
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityKey.java
@@ -0,0 +1,79 @@
+/*
+ * 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.springdata.compoundkey;
+
+import java.io.Serializable;
+import java.util.Objects;
+import org.apache.ignite.cache.affinity.AffinityKeyMapped;
+
+/** Compound key for city class  */
+public class CityKey implements Serializable {
+    /** city identifier */
+    private int ID;
+
+    /** affinity key countrycode  */
+    @AffinityKeyMapped
+    private String COUNTRYCODE;
+
+    /**
+     * @param id city identifier
+     * @param countryCode city countrycode
+     * */
+    public CityKey(int id, String countryCode) {
+        this.ID = id;
+        this.COUNTRYCODE = countryCode;
+    }
+
+    /**
+     * @return city id
+     * */
+    public int getId() {
+        return ID;
+    }
+
+    /**
+     * @return countrycode
+     * */
+    public String getCountryCode() {
+        return COUNTRYCODE;
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+
+        if (o == null || getClass() != o.getClass())
+            return false;
+
+        CityKey key = (CityKey)o;
+
+        return ID == key.ID &&
+                COUNTRYCODE.equals(key.COUNTRYCODE);
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        return Objects.hash(ID, COUNTRYCODE);
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return ID + " | " + COUNTRYCODE;
+    }
+}
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityRepository.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityRepository.java
new file mode 100644
index 0000000..691fe8e
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CityRepository.java
@@ -0,0 +1,43 @@
+/*
+ * 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.springdata.compoundkey;
+
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.springdata.repository.IgniteRepository;
+import org.apache.ignite.springdata.repository.config.RepositoryConfig;
+import org.springframework.stereotype.Repository;
+
+/** City repository */
+@Repository
+@RepositoryConfig(cacheName = "City")
+public interface CityRepository extends IgniteRepository<City, CityKey> {
+    /**
+     * Find city by id
+     * @param id city identifier
+     * @return city
+     * */
+    public City findById(int id);
+
+    /**
+     * Find all cities by coutrycode
+     * @param cc coutrycode
+     * @return list of cache enrties CityKey -> City
+     * */
+    public List<Cache.Entry<CityKey, City>> findByCountryCode(String cc);
+}
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CompoundKeyApplicationConfiguration.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CompoundKeyApplicationConfiguration.java
new file mode 100644
index 0000000..7701f3d
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/compoundkey/CompoundKeyApplicationConfiguration.java
@@ -0,0 +1,45 @@
+/*
+ * 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.springdata.compoundkey;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.ClientConnectorConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.springdata.repository.config.EnableIgniteRepositories;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Spring application configuration
+ * */
+@Configuration
+@EnableIgniteRepositories
+public class CompoundKeyApplicationConfiguration {
+    /** */
+    public static final int CLI_CONN_PORT = 10810;
+
+    /**
+     * Ignite instance bean
+     * */
+    @Bean
+    public Ignite igniteInstance() {
+        return Ignition.start(new IgniteConfiguration()
+            .setClientConnectorConfiguration(new ClientConnectorConfiguration().setPort(CLI_CONN_PORT)));
+    }
+}
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/ApplicationConfiguration.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/ApplicationConfiguration.java
new file mode 100644
index 0000000..ff1045e
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/ApplicationConfiguration.java
@@ -0,0 +1,51 @@
+/*
+ * 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.springdata.misc;
+
+import org.apache.ignite.Ignite;
+import org.apache.ignite.Ignition;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.configuration.IgniteConfiguration;
+import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
+import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder;
+import org.apache.ignite.springdata.repository.config.EnableIgniteRepositories;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ *
+ */
+@Configuration
+@EnableIgniteRepositories
+public class ApplicationConfiguration {
+    /**
+     * @return Ignite instance.
+     */
+    @Bean
+    public Ignite igniteInstance() {
+        IgniteConfiguration cfg = new IgniteConfiguration()
+            .setCacheConfiguration(
+                new CacheConfiguration<Integer, Person>("PersonCache")
+                    .setIndexedTypes(Integer.class, Person.class),
+                new CacheConfiguration<PersonKey, Person>("PersonWithKeyCache")
+            )
+            .setDiscoverySpi(new TcpDiscoverySpi().setIpFinder(new TcpDiscoveryVmIpFinder(true)));
+
+        return Ignition.start(cfg);
+    }
+}
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/Person.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/Person.java
new file mode 100644
index 0000000..154937f
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/Person.java
@@ -0,0 +1,98 @@
+/*
+ * 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.springdata.misc;
+
+import java.util.Objects;
+import org.apache.ignite.cache.query.annotations.QuerySqlField;
+
+/**
+ * DTO class.
+ */
+public class Person {
+    /** First name. */
+    @QuerySqlField(index = true)
+    private String firstName;
+
+    /** Second name. */
+    @QuerySqlField(index = true)
+    private String secondName;
+
+    /**
+     * @param firstName First name.
+     * @param secondName Second name.
+     */
+    public Person(String firstName, String secondName) {
+        this.firstName = firstName;
+        this.secondName = secondName;
+    }
+
+    /**
+     * @return First name.
+     */
+    public String getFirstName() {
+        return firstName;
+    }
+
+    /**
+     * @param firstName First name.
+     */
+    public void setFirstName(String firstName) {
+        this.firstName = firstName;
+    }
+
+    /**
+     * @return Second name.
+     */
+    public String getSecondName() {
+        return secondName;
+    }
+
+    /**
+     * @param secondName Second name.
+     */
+    public void setSecondName(String secondName) {
+        this.secondName = secondName;
+    }
+
+    /** {@inheritDoc} */
+    @Override public String toString() {
+        return "Person{" +
+            "firstName='" + firstName + '\'' +
+            ", secondName='" + secondName + '\'' +
+            '}';
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+
+        if (o == null || getClass() != o.getClass())
+            return false;
+
+        Person person = (Person)o;
+
+        return Objects.equals(firstName, person.firstName) &&
+            Objects.equals(secondName, person.secondName);
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        return Objects.hash(firstName, secondName);
+    }
+}
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonKey.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonKey.java
new file mode 100644
index 0000000..2537c57
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonKey.java
@@ -0,0 +1,54 @@
+/*
+ * 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.springdata.misc;
+
+import java.io.Serializable;
+
+/**
+ * Compound key.
+ */
+public class PersonKey implements Serializable {
+    /** */
+    private int id1;
+
+    /** */
+    private int id2;
+
+    /**
+     * @param id1 ID1.
+     * @param id2 ID2.
+     */
+    public PersonKey(int id1, int id2) {
+        this.id1 = id1;
+        this.id2 = id2;
+    }
+
+    /**
+     * @return ID1
+     */
+    public int getId1() {
+        return id1;
+    }
+
+    /**
+     * @return ID1
+     */
+    public int getId2() {
+        return id1;
+    }
+}
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepository.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepository.java
new file mode 100644
index 0000000..d8444d1
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepository.java
@@ -0,0 +1,99 @@
+
+/*
+ * 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.springdata.misc;
+
+import java.util.Collection;
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.springdata.repository.IgniteRepository;
+import org.apache.ignite.springdata.repository.config.Query;
+import org.apache.ignite.springdata.repository.config.RepositoryConfig;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+
+/**
+ * Test repository.
+ */
+@RepositoryConfig(cacheName = "PersonCache")
+public interface PersonRepository extends IgniteRepository<Person, Integer> {
+    /** */
+    public List<Person> findByFirstName(String val);
+
+    /** */
+    public List<Person> findByFirstNameContaining(String val);
+
+    /** */
+    public List<Person> findByFirstNameRegex(String val, Pageable pageable);
+
+    /** */
+    public Collection<Person> findTopByFirstNameContaining(String val);
+
+    /** */
+    public Iterable<Person> findFirst10ByFirstNameLike(String val);
+
+    /** */
+    public int countByFirstName(String val);
+
+    /** */
+    public int countByFirstNameLike(String val);
+
+    /** */
+    public int countByFirstNameLikeAndSecondNameLike(String like1, String like2);
+
+    /** */
+    public int countByFirstNameStartingWithOrSecondNameStartingWith(String like1, String like2);
+
+    /** */
+    public List<Cache.Entry<Integer, Person>> findBySecondNameLike(String val);
+
+    /** */
+    public Cache.Entry<Integer, Person> findTopBySecondNameLike(String val);
+
+    /** */
+    public Person findTopBySecondNameStartingWith(String val);
+
+    /** */
+    @Query("firstName = ?")
+    public List<Person> simpleQuery(String val);
+
+    /** */
+    @Query("firstName REGEXP ?")
+    public List<Person> queryWithSort(String val, Sort sort);
+
+    /** */
+    @Query("SELECT * FROM Person WHERE firstName REGEXP ?")
+    public List<Person> queryWithPageable(String val, Pageable pageable);
+
+    /** */
+    @Query("SELECT secondName FROM Person WHERE firstName REGEXP ?")
+    public List<String> selectField(String val, Pageable pageable);
+
+    /** */
+    @Query("SELECT _key, secondName FROM Person WHERE firstName REGEXP ?")
+    public List<List> selectSeveralField(String val, Pageable pageable);
+
+    /** */
+    @Query("SELECT count(1) FROM (SELECT DISTINCT secondName FROM Person WHERE firstName REGEXP ?)")
+    public int countQuery(String val);
+
+    /** */
+    @Query("SELECT count(*) FROM Person")
+    public int countAllPersons();
+}
+
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryWithCompoundKey.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryWithCompoundKey.java
new file mode 100644
index 0000000..e868911
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonRepositoryWithCompoundKey.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.springdata.misc;
+
+import org.apache.ignite.springdata.repository.IgniteRepository;
+import org.apache.ignite.springdata.repository.config.RepositoryConfig;
+
+/**
+ * Test repository.
+ */
+@RepositoryConfig(cacheName = "PersonWithKeyCache")
+public interface PersonRepositoryWithCompoundKey extends IgniteRepository<Person, PersonKey> {
+}
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonSecondRepository.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonSecondRepository.java
new file mode 100644
index 0000000..a82e822
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/springdata/misc/PersonSecondRepository.java
@@ -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.
+ */
+
+package org.apache.ignite.springdata.misc;
+
+import java.util.List;
+import javax.cache.Cache;
+import org.apache.ignite.springdata.repository.IgniteRepository;
+import org.apache.ignite.springdata.repository.config.Query;
+import org.apache.ignite.springdata.repository.config.RepositoryConfig;
+import org.springframework.data.domain.AbstractPageRequest;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Slice;
+
+/**
+ *
+ */
+@RepositoryConfig(cacheName = "PersonCache")
+public interface PersonSecondRepository extends IgniteRepository<Person, Integer> {
+    /** */
+    public Slice<Cache.Entry<Integer, Person>> findBySecondNameIsNot(String val, PageRequest pageReq);
+
+    /** */
+    @Query("SELECT _key, secondName FROM Person WHERE firstName REGEXP ?")
+    public Slice<List> querySliceOfList(String val, AbstractPageRequest pageReq);
+}
diff --git a/modules/spring-data-ext/src/test/java/org/apache/ignite/testsuites/IgniteSpringDataTestSuite.java b/modules/spring-data-ext/src/test/java/org/apache/ignite/testsuites/IgniteSpringDataTestSuite.java
new file mode 100644
index 0000000..cc063a6
--- /dev/null
+++ b/modules/spring-data-ext/src/test/java/org/apache/ignite/testsuites/IgniteSpringDataTestSuite.java
@@ -0,0 +1,37 @@
+/*
+ * 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.testsuites;
+
+import org.apache.ignite.springdata.IgniteSpringDataCompoundKeyTest;
+import org.apache.ignite.springdata.IgniteSpringDataCrudSelfTest;
+import org.apache.ignite.springdata.IgniteSpringDataQueriesSelfTest;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+/**
+ * Ignite Spring Data test suite.
+ */
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+    IgniteSpringDataCrudSelfTest.class,
+    IgniteSpringDataQueriesSelfTest.class,
+    IgniteSpringDataCompoundKeyTest.class
+})
+public class IgniteSpringDataTestSuite {
+}
+
diff --git a/parent/pom.xml b/parent/pom.xml
index 6444b27..d4c69fa 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -126,10 +126,12 @@
         <snappy.version>1.1.7.2</snappy.version>
         <spark.hadoop.version>2.6.5</spark.hadoop.version>
         <spark.version>2.3.0</spark.version>
-        <spring.data.version>1.13.14.RELEASE</spring.data.version> <!-- don't forget to update spring version -->
-        <spring.version>4.3.18.RELEASE</spring.version><!-- don't forget to update spring-data version -->
-        <spring.data-2.0.version>2.0.9.RELEASE</spring.data-2.0.version> <!-- don't forget to update spring-5.0 version -->
-        <spring-5.0.version>5.0.8.RELEASE</spring-5.0.version><!-- don't forget to update spring-data-2.0 version -->
+        <spring.data.version>1.13.23.RELEASE</spring.data.version> <!-- don't forget to update spring version -->
+        <spring.version>4.3.26.RELEASE</spring.version><!-- don't forget to update spring-data version -->
+        <spring.data-2.0.version>2.0.13.RELEASE</spring.data-2.0.version> <!-- don't forget to update spring-5.0 version -->
+        <spring-5.0.version>5.0.16.RELEASE</spring-5.0.version><!-- don't forget to update spring-data-2.0 version -->
+        <spring.data-2.2.version>2.2.3.RELEASE</spring.data-2.2.version> <!-- don't forget to update spring-5.2 version -->
+        <spring-5.2.version>5.2.3.RELEASE</spring-5.2.version><!-- don't forget to update spring-data-2.2 version -->
         <spring41.osgi.feature.version>4.1.7.RELEASE_1</spring41.osgi.feature.version>
         <spring-boot.version>2.2.2.RELEASE</spring-boot.version>
         <storm.version>1.1.1</storm.version>
@@ -234,7 +236,7 @@
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
-            <version>4.11</version>
+            <version>4.12</version>
             <scope>test</scope>
         </dependency>
 
@@ -346,6 +348,10 @@
                                 <packages>org.apache.ignite.springdata20.repository*</packages>
                             </group>
                             <group>
+                                <title>SpringData 2.2 integration</title>
+                                <packages>org.apache.ignite.springdata22.repository*</packages>
+                            </group>
+                            <group>
                             <title>RocketMQ integration</title>
                                 <packages>org.apache.ignite.stream.rocketmq*</packages>
                             </group>
@@ -722,6 +728,14 @@
                                         <exclude>**/keystore/ca/*.txt.attr</exclude><!--auto generated files-->
                                         <exclude>**/keystore/ca/*serial</exclude><!--auto generated files-->
                                         <exclude>**/META-INF/services/**</exclude> <!-- Interface mappings: cannot be changed -->
+                                        <!-- spring data 2.X modules, borrowed code from spring-jpa 5.2.0, Apache 2.0 licenses -->
+                                        <exclude>src/main/java/org/apache/ignite/springdata2*/repository/query/QueryUtils.java</exclude>
+                                        <exclude>src/main/java/org/apache/ignite/springdata2*/repository/query/EmptyDeclaredQuery.java</exclude>
+                                        <exclude>src/main/java/org/apache/ignite/springdata2*/repository/query/ExpressionBasedStringQuery.java</exclude>
+                                        <exclude>src/main/java/org/apache/ignite/springdata2*/repository/query/spel/SpelEvaluator.java</exclude>
+                                        <exclude>src/main/java/org/apache/ignite/springdata2*/repository/query/spel/SpelQueryContext.java</exclude>
+                                        <exclude>src/main/java/org/apache/ignite/springdata2*/repository/query/DeclaredQuery.java</exclude>
+                                        <exclude>src/main/java/org/apache/ignite/springdata2*/repository/query/StringQuery.java</exclude>
                                         <!--special excludes-->
                                         <exclude>idea/ignite_codeStyle.xml</exclude>
                                         <exclude>**/DEVNOTES*.txt</exclude>
diff --git a/pom.xml b/pom.xml
index 55d3af5..a3232d1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -55,6 +55,9 @@
         <module>modules/camel-ext</module>
         <module>modules/jms11-ext</module>
         <module>modules/kafka-ext</module>
+        <module>modules/spring-data-ext</module>
+        <module>modules/spring-data-2.0-ext</module>
+        <module>modules/spring-data-2.2-ext</module>
     </modules>
 
     <profiles>
@@ -124,6 +127,18 @@
                                 </goals>
                                 <phase>validate</phase>
                                 <configuration>
+                                    <additionalDependencies>
+                                        <!--
+                                        Only the last version of spring data is included to class path, some classes
+                                        required by old spring data modules are absent in this version.
+                                        Add spring-data-2.0 explicitly to be able to find all required classes.
+                                        -->
+                                        <dependency>
+                                            <groupId>org.springframework.data</groupId>
+                                            <artifactId>spring-data-commons</artifactId>
+                                            <version>${spring.data-2.0.version}</version>
+                                        </dependency>
+                                    </additionalDependencies>
                                     <reportOutputDirectory>${basedir}/target/javadoc</reportOutputDirectory>
                                     <destDir>core</destDir>
                                     <subpackages>org.apache.ignite -exclude org.apache.ignite.codegen:org.apache.ignite.examples:org.apache.ignite.internal:org.apache.ignite.schema:org.apache.ignite.tests:org.apache.ignite.tools:org.apache.ignite.util:org.apache.ignite.spi.discovery.tcp.messages:org.apache.ignite.spi.discovery.tcp.internal:org.apache.ignite.spi.communication.tcp.internal:org.apache.ignite.spi.discovery.zk.internal:org.apache.ignite.spi.deployment.uri.scanners:org.apache.ignite.spi.deployment.uri.tasks:org.apache.ignite.yardstick:org.apache.ignite.webtest</subpackages>