You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by pp...@apache.org on 2022/08/15 17:47:27 UTC

[ignite-extensions] branch master updated: IGNITE-17051 Fixed execution of queries containing IN and NOT IN clauses (#170)

This is an automated email from the ASF dual-hosted git repository.

ppa pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite-extensions.git


The following commit(s) were added to refs/heads/master by this push:
     new b2f0cda  IGNITE-17051 Fixed execution of queries containing IN and NOT IN clauses (#170)
b2f0cda is described below

commit b2f0cda14695115f153b2a9fd9258ad6713f653b
Author: Mikhail Petrov <32...@users.noreply.github.com>
AuthorDate: Mon Aug 15 20:47:22 2022 +0300

    IGNITE-17051 Fixed execution of queries containing IN and NOT IN clauses (#170)
---
 .../springdata/repository/query/IgniteQuery.java   |  31 +++-
 .../repository/query/IgniteQueryGenerator.java     |  21 ++-
 .../repository/query/IgniteRepositoryQuery.java    |  42 +++++-
 .../springdata/repository/query/QueryUtils.java    |  51 +++++++
 .../springdata/repository/query/StringQuery.java   |  59 +++++++-
 .../support/IgniteRepositoryFactory.java           |  12 +-
 .../springdata/IgniteSpringDataCrudSelfTest.java   | 166 +++++++++++++++++++++
 .../ignite/springdata/misc/PersonRepository.java   |  93 +++++++++++-
 8 files changed, 458 insertions(+), 17 deletions(-)

diff --git a/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/query/IgniteQuery.java b/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/query/IgniteQuery.java
index 37bd3ce..978e1d7 100644
--- a/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/query/IgniteQuery.java
+++ b/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/query/IgniteQuery.java
@@ -58,27 +58,37 @@ public class IgniteQuery {
      */
     private final boolean isAutogenerated;
 
+    /**
+     * Whether the query requires post-processing based on execution arguments.
+     */
+    private final boolean isParamDependent;
+
     /**
      * 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.
+     * @param qrySql           the query string.
+     * @param isFieldQuery     Is field query.
+     * @param isTextQuery      Is a TextQuery
+     * @param isAutogenerated  query was autogenerated
+     * @param isParamDependent Whether the query requires post-processing based on execution arguments.
+     * @param option           Option.
      */
-    public IgniteQuery(String qrySql,
+    public IgniteQuery(
+        String qrySql,
         boolean isFieldQuery,
         boolean isTextQuery,
         boolean isAutogenerated,
-        Option option) {
+        boolean isParamDependent,
+        Option option
+    ) {
         this.qrySql = qrySql;
         this.isFieldQuery = isFieldQuery;
         this.isTextQuery = isTextQuery;
         this.isAutogenerated = isAutogenerated;
+        this.isParamDependent = isParamDependent;
         this.option = option;
     }
 
@@ -127,6 +137,13 @@ public class IgniteQuery {
         return option;
     }
 
+    /**
+     * @return Whether the query requires post-processing based on execution arguments.
+     */
+    public boolean isParameterDependent() {
+        return isParamDependent;
+    }
+
     /** */
     @Override public String toString() {
         return S.toString(IgniteQuery.class, this);
diff --git a/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/query/IgniteQueryGenerator.java b/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/query/IgniteQueryGenerator.java
index 6bf02bc..3bcfbb7 100644
--- a/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/query/IgniteQueryGenerator.java
+++ b/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/query/IgniteQueryGenerator.java
@@ -26,6 +26,9 @@ import org.springframework.data.repository.core.RepositoryMetadata;
 import org.springframework.data.repository.query.parser.Part;
 import org.springframework.data.repository.query.parser.PartTree;
 
+import static org.springframework.data.repository.query.parser.Part.Type.IN;
+import static org.springframework.data.repository.query.parser.Part.Type.NOT_IN;
+
 /**
  * Ignite query generator for Spring Data framework.
  */
@@ -100,7 +103,14 @@ public class IgniteQueryGenerator {
             sql.append(parts.getMaxResults().intValue());
         }
 
-        return new IgniteQuery(sql.toString(), isCountOrFieldQuery, false, true, getOptions(mtd));
+        return new IgniteQuery(
+            sql.toString(),
+            isCountOrFieldQuery,
+            false,
+            true,
+            isParametersDependent(parts),
+            getOptions(mtd)
+        );
     }
 
     /**
@@ -273,4 +283,13 @@ public class IgniteQueryGenerator {
 
         sql.append(")");
     }
+
+    /**
+     * @param qryPartTree {@link PartTree} query representation.
+     * @return Whether the specified {@link PartTree} contains query part which string representation depends on
+     * query arguments.
+     */
+    private static boolean isParametersDependent(PartTree qryPartTree) {
+        return !qryPartTree.getParts(IN).isEmpty() || !qryPartTree.getParts(NOT_IN).isEmpty();
+    }
 }
diff --git a/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/query/IgniteRepositoryQuery.java b/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/query/IgniteRepositoryQuery.java
index 069e1ec..02faf36 100644
--- a/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/query/IgniteRepositoryQuery.java
+++ b/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/query/IgniteRepositoryQuery.java
@@ -48,11 +48,13 @@ 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.util.GridUnsafe;
+import org.apache.ignite.internal.util.typedef.T2;
 import org.apache.ignite.internal.util.typedef.internal.U;
 import org.apache.ignite.springdata.proxy.IgniteCacheProxy;
 import org.apache.ignite.springdata.proxy.IgniteClientCacheProxy;
 import org.apache.ignite.springdata.repository.config.DynamicQueryConfig;
 import org.apache.ignite.springdata.repository.query.StringQuery.ParameterBinding;
+import org.apache.ignite.springdata.repository.query.StringQuery.ParameterBindingParser;
 import org.jetbrains.annotations.Nullable;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.PageImpl;
@@ -479,9 +481,14 @@ public class IgniteRepositoryQuery implements RepositoryQuery {
             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));
+            return new IgniteQuery(
+                cfg.value(),
+                !cfg.textQuery() && (isFieldQuery(cfg.value()) || cfg.forceFieldsQuery()),
+                cfg.textQuery(),
+                false,
+                true,
+                IgniteQueryGenerator.getOptions(mtd)
+            );
         }
 
         throw new IllegalStateException("Unable to obtain a valid query. When passing dynamicQuery = true via org"
@@ -721,16 +728,22 @@ public class IgniteRepositoryQuery implements RepositoryQuery {
         checkRequiredPageable(returnStgy, values);
 
         if (!qry.isTextQuery()) {
+            boolean isParamDependent;
+
             if (!qry.isAutogenerated()) {
                 StringQuery squery = new ExpressionBasedStringQuery(queryString, metadata, expressionParser);
                 queryString = squery.getQueryString();
                 parameters = extractBindableValues(parameters, getQueryMethod().getParameters(),
                     squery.getParameterBindings());
+
+                isParamDependent = isParameterDependent(squery);
             }
             else {
                 // remove dynamic projection from parameters
                 if (hasDynamicProjection)
                     parameters = ArrayUtils.remove(parameters, dynamicProjectionIndex);
+
+                isParamDependent = qry.isParameterDependent();
             }
 
             switch (qry.options()) {
@@ -751,6 +764,17 @@ public class IgniteRepositoryQuery implements RepositoryQuery {
                 default:
             }
 
+            if (isParamDependent) {
+                T2<String, Object[]> parseRes = ParameterBindingParser.INSTANCE.processParameterDependentClauses(
+                    queryString,
+                    parameters
+                );
+
+                queryString = parseRes.get1();
+
+                parameters = parseRes.get2();
+            }
+
             if (qry.isFieldQuery()) {
                 SqlFieldsQuery sqlFieldsQry = new SqlFieldsQuery(queryString);
                 sqlFieldsQry.setArgs(parameters);
@@ -970,4 +994,16 @@ public class IgniteRepositoryQuery implements RepositoryQuery {
             return 0;
         }
     }
+
+    /**
+     * @return Whether specified query contains clauses which string representation depends on the query arguments.
+     */
+    private boolean isParameterDependent(StringQuery qry) {
+        for (ParameterBinding binding : qry.getParameterBindings()) {
+            if (binding instanceof StringQuery.InParameterBinding)
+                return true;
+        }
+
+        return false;
+    }
 }
diff --git a/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/query/QueryUtils.java b/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/query/QueryUtils.java
index ba8c4aa..1457b5a 100644
--- a/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/query/QueryUtils.java
+++ b/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/query/QueryUtils.java
@@ -17,7 +17,13 @@
 
 package org.apache.ignite.springdata.repository.query;
 
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -306,4 +312,49 @@ public abstract class QueryUtils {
         String projection = matcher.find() ? matcher.group(1) : "";
         return projection.trim();
     }
+
+    /**
+     * @param args Array of query arguments.
+     * @param argIdxs Indexes of the elements that should be expanded.
+     * @return Copy of the specified array with expanded elements that matches the specified indexes.
+     */
+    public static Object[] expandQueryArguments(Object[] args, List<Integer> argIdxs) {
+        Collections.sort(argIdxs);
+
+        List<Object> res = new ArrayList<>();
+
+        int prevIdx = 0;
+
+        for (Integer idx : argIdxs) {
+            copyTo(res, args, prevIdx, idx);
+
+            Object arg = args[idx];
+
+            if (arg.getClass().isArray()) {
+                for (int i = 0; i < Array.getLength(arg); i++)
+                    res.add(Array.get(arg, i));
+            }
+            else if (arg instanceof Collection)
+                res.addAll((Collection<?>)arg);
+            else
+                res.add(arg);
+
+            prevIdx = idx + 1;
+        }
+
+        copyTo(res, args, prevIdx, args.length);
+
+        return res.toArray();
+    }
+
+    /**
+     * Copies elements of the specified array that lays in the specified bounds to the destination {@link List}.
+     * @param dest Destination.
+     * @param src Source.
+     * @param from Starting index (inclusively).
+     * @param to Finishing index (exclusively).
+     */
+    private static void copyTo(List<Object> dest, Object[] src, int from, int to) {
+        dest.addAll(Arrays.asList(src).subList(from, to));
+    }
 }
diff --git a/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/query/StringQuery.java b/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/query/StringQuery.java
index b9f3aec..b83ad81 100644
--- a/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/query/StringQuery.java
+++ b/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/query/StringQuery.java
@@ -21,10 +21,13 @@ import java.lang.reflect.Array;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.function.BiFunction;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import org.apache.ignite.internal.util.typedef.T2;
 import org.jetbrains.annotations.Nullable;
 import org.springframework.data.domain.Range;
 import org.springframework.data.domain.Range.Bound;
@@ -282,8 +285,9 @@ class StringQuery implements DeclaredQuery {
                     usesJpaStyleParameters = true;
 
                 // named parameters (:param) will be untouched by spelExtractor, so replace them by ? as we don't
-                // know position
-                if (paramName != null)
+                // know position. We also replace the indexed parameters because query arguments will be rearranged to
+                // suite the proper order considering parameter indexes.
+                if (paramName != null || paramIdx != null)
                     replacement = "?";
 
                 if (usesJpaStyleParameters && queryMeta.usesJdbcStyleParameters) {
@@ -331,6 +335,57 @@ class StringQuery implements DeclaredQuery {
             return resultingQry;
         }
 
+        /**
+         * Post-process specified query clauses that depend on query arguments (e.g. '?' after IN and NOT IN clauses
+         * will be replaced with '(?, ? ...)' depending on the size of the collection corresponding to the initial query
+         * parameter.
+         *
+         * @param qry Query to parse.
+         * @param args Query arguments.
+         * @return Pair of values which represents parsed query and copy of query arguments updated to suite query
+         * parameters structure.
+         */
+        T2<String, Object[]> processParameterDependentClauses(String qry, Object[] args) {
+            Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(qry);
+
+            StringBuffer parsedQry = new StringBuffer();
+
+            int argIdx = 0;
+
+            List<Integer> argIdxs = new ArrayList<>();
+
+            while (matcher.find()) {
+                String typeSrc = matcher.group(COMPARISION_TYPE_GROUP);
+
+                if (ParameterBindingType.of(typeSrc) == ParameterBindingType.IN) {
+                    Object arg = args[argIdx];
+
+                    int length;
+
+                    if (arg.getClass().isArray())
+                        length = Array.getLength(arg);
+                    else if (arg instanceof Collection)
+                        length = ((Collection<?>)arg).size();
+                    else
+                        length = 1;
+
+                    String replacement = Collections.nCopies(length, "?")
+                        .stream()
+                        .collect(Collectors.joining(", ", "(", ")"));
+
+                    matcher.appendReplacement(parsedQry, matcher.group(0).replaceFirst(Pattern.quote(matcher.group(2)), replacement));
+
+                    argIdxs.add(argIdx);
+                }
+
+                ++argIdx;
+            }
+
+            matcher.appendTail(parsedQry);
+
+            return new T2<>(parsedQry.toString(), QueryUtils.expandQueryArguments(args, argIdxs));
+        }
+
         /** */
         private static SpelExtractor createSpelExtractor(String queryWithSpel,
             boolean parametersShouldBeAccessedByIndex,
diff --git a/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/support/IgniteRepositoryFactory.java b/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/support/IgniteRepositoryFactory.java
index 6673560..7af32b9 100644
--- a/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/support/IgniteRepositoryFactory.java
+++ b/modules/spring-data-ext/spring-data/src/main/java/org/apache/ignite/springdata/repository/support/IgniteRepositoryFactory.java
@@ -139,9 +139,15 @@ public class IgniteRepositoryFactory extends RepositoryFactorySupport {
                 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;
+                IgniteQuery query = annotatedIgniteQuery
+                    ? new IgniteQuery(
+                        qryStr,
+                        !annotation.textQuery() && (isFieldQuery(qryStr) || annotation.forceFieldsQuery()),
+                        annotation.textQuery(),
+                        false,
+                        true,
+                        IgniteQueryGenerator.getOptions(mtd))
+                    : null;
 
                 if (key != QueryLookupStrategy.Key.CREATE) {
                     return new IgniteRepositoryQuery(metadata, query, mtd, factory, cache,
diff --git a/modules/spring-data-ext/spring-data/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfTest.java b/modules/spring-data-ext/spring-data/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfTest.java
index e317b7a..d21f878 100644
--- a/modules/spring-data-ext/spring-data/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfTest.java
+++ b/modules/spring-data-ext/spring-data/src/test/java/org/apache/ignite/springdata/IgniteSpringDataCrudSelfTest.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.springdata;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -31,6 +32,8 @@ 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;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Sort;
 
 /**
  * CRUD tests.
@@ -445,4 +448,167 @@ public class IgniteSpringDataCrudSelfTest extends GridCommonAbstractTest {
         List<Person> person = repo.findByFirstName("uniquePerson");
         assertEquals(person.get(0).getSecondName(), "uniqueLastName");
     }
+
+    /** */
+    @Test
+    public void testMethodBasedQueryWithInClauseAndListArgument() {
+        assertEquals(5, repo.findBySecondNameIn(Arrays.asList("uniqueLastName", "nonUniqueLastName")).size());
+    }
+
+    /** */
+    @Test
+    public void testMethodBasedQueryWithNotInClauseAndListArgument() {
+        assertEquals(CACHE_SIZE - 5, repo.findBySecondNameNotIn(Arrays.asList("uniqueLastName", "nonUniqueLastName")).size());
+    }
+
+    /** */
+    @Test
+    public void testMethodBasedQueryWithInClauseAndListArgumentWithSorting() {
+        assertEquals(
+            5,
+            repo.findBySecondNameIn(
+                Arrays.asList("uniqueLastName", "nonUniqueLastName"),
+                Sort.by(Sort.Direction.DESC, "secondName")
+            ).size());
+    }
+
+    /** */
+    @Test
+    public void testMethodBasedQueryWithInClauseAndListArgumentWithPaging() {
+        assertEquals(
+            5,
+            repo.findBySecondNameIn(
+                Arrays.asList("uniqueLastName", "nonUniqueLastName"),
+                PageRequest.of(0, 10)
+            ).getTotalElements()
+        );
+    }
+
+    /** */
+    @Test
+    public void testMethodBasedQueryWithInClauseAndListArgumentWithProjection() {
+        assertEquals(5, repo.findBySecondNameIn(PersonProjection.class, Arrays.asList("uniqueLastName", "nonUniqueLastName")).size());
+    }
+
+    /** */
+    @Test
+    public void testMethodBasedQueryWithInClauseAndArrayArgument() {
+        assertEquals(5, repo.findBySecondNameIn(new String[] {"uniqueLastName", "nonUniqueLastName"}).size());
+    }
+
+    /** */
+    @Test
+    public void testMethodBasedQueryWithInClauseAndSingleValueArgument() {
+        assertEquals(4, repo.findBySecondNameIn("nonUniqueLastName").size());
+    }
+
+    /** */
+    @Test
+    public void testMethodBasedQueryWithInClauseAlongsideEqualClause() {
+        assertEquals(
+            4,
+            repo.findByFirstNameIsAndSecondNameIn(
+                "nonUniquePerson",
+                Arrays.asList("uniqueLastName", "nonUniqueLastName")
+            ).size()
+        );
+    }
+
+    /** */
+    @Test
+    public void testMethodBasedQueryWithMultipleInClauses() {
+        assertEquals(
+            5,
+            repo.findBySecondNameInAndFirstNameIn(
+                Arrays.asList("uniqueLastName", "nonUniqueLastName"),
+                Arrays.asList("uniquePerson", "nonUniquePerson")
+            ).size()
+        );
+    }
+
+    /** */
+    @Test
+    public void testExplicitQueryWithInClauseAndListArgument() {
+        assertEquals(5, repo.selectInList(Arrays.asList("uniqueLastName", "nonUniqueLastName")).size());
+    }
+
+    /** */
+    @Test
+    public void testExplicitQueryWithNotInClauseAndListArgument() {
+        assertEquals(CACHE_SIZE - 5, repo.selectNotInList(Arrays.asList("uniqueLastName", "nonUniqueLastName")).size());
+    }
+
+    /** */
+    @Test
+    public void testExplicitQueryWithInClauseAndNamedListArgument() {
+        assertEquals(5, repo.selectInListWithNamedParameter(Arrays.asList("uniqueLastName", "nonUniqueLastName")).size());
+    }
+
+    /** */
+    @Test
+    public void testExplicitQueryWithInClauseAndNamedArrayArgument() {
+        assertEquals(5, repo.selectInArrayWithNamedParameter(new String[] {"uniqueLastName", "nonUniqueLastName"}).size());
+    }
+
+    /** */
+    @Test
+    public void testExplicitQueryWithInClauseAndNamedSingleValueArgument() {
+        assertEquals(1, repo.selectInSingleValueWithNamedParameter("uniqueLastName").size());
+    }
+
+    /** */
+    @Test
+    public void testExplicitQueryWithMultipleInClauses() {
+        assertEquals(
+            5,
+            repo.selectWithMultipleInClauses(
+                Arrays.asList("uniqueLastName", "nonUniqueLastName"),
+                Arrays.asList("uniquePerson", "nonUniquePerson")
+            ).size()
+        );
+    }
+
+    /** */
+    @Test
+    public void testExplicitQueryWithInClauseAlongsideEqualClause() {
+        assertEquals(4, repo.selectWithInAndEqualClauses("nonUniquePerson", Arrays.asList("uniqueLastName", "nonUniqueLastName")).size());
+    }
+
+    /** */
+    @Test
+    public void testExplicitQueryWithInClauseAlongsideEqualClauseAndNamedArguments() {
+        assertEquals(
+            4,
+            repo.selectWithInAndEqualClausesAndNamedArguments(
+                Arrays.asList("uniqueLastName", "nonUniqueLastName"), "nonUniquePerson").size());
+    }
+
+    /** */
+    @Test
+    public void testExplicitQueryWithInClauseAndSpellArgument() {
+        assertEquals(4, repo.selectInWithSpellClause(new Person("", "nonUniqueLastName"), "nonUniquePerson").size());
+    }
+
+    /** */
+    @Test
+    public void testExplicitQueryWithInClauseAndIndexedListArguments() {
+        assertEquals(4,
+            repo.selectInListWithMultipleIndexedParameter(
+                0,
+                "nonUniquePerson",
+                Arrays.asList("uniqueLastName", "nonUniqueLastName")
+            ).size());
+    }
+
+    /** */
+    @Test
+    public void testExplicitQueryWithInClauseAndIndexedArrayArguments() {
+        assertEquals(5, repo.selectInArrayWithIndexedParameter(0, new String[] {"uniqueLastName", "nonUniqueLastName"}).size());
+    }
+
+    /** */
+    @Test
+    public void testExplicitQueryWithInClauseAndIndexedSingleValueArguments() {
+        assertEquals(1, repo.selectInSingleValueWithIndexedParameter(0, "uniqueLastName").size());
+    }
 }
diff --git a/modules/spring-data-ext/spring-data/src/test/java/org/apache/ignite/springdata/misc/PersonRepository.java b/modules/spring-data-ext/spring-data/src/test/java/org/apache/ignite/springdata/misc/PersonRepository.java
index d68b9c9..cb2cece 100644
--- a/modules/spring-data-ext/spring-data/src/test/java/org/apache/ignite/springdata/misc/PersonRepository.java
+++ b/modules/spring-data-ext/spring-data/src/test/java/org/apache/ignite/springdata/misc/PersonRepository.java
@@ -20,11 +20,11 @@ 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.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.repository.query.Param;
@@ -160,4 +160,95 @@ public interface PersonRepository extends IgniteRepository<Person, Integer> {
     /** Produces a list of domain entity classes whose fields are obtained from the query result row. */
     @Query(value = "SELECT firstName, birthday FROM Person", forceFieldsQuery = true)
     public List<Person> queryWithIncompleteRowToEntityConversion();
+
+    /** */
+    public List<Cache.Entry<Integer, Person>> findBySecondNameIn(List<String> secondNames);
+
+    /** */
+    public List<Cache.Entry<Integer, Person>> findBySecondNameNotIn(List<String> secondNames);
+
+    /** */
+    public List<Cache.Entry<Integer, Person>> findBySecondNameIn(List<String> secondNames, Sort sort);
+
+    /** */
+    public <P> List<P> findBySecondNameIn(Class<P> dynamicProjection, List<String> secondNames);
+
+    /** */
+    public Page<Person> findBySecondNameIn(List<String> secondNames, Pageable pageable);
+
+    /** */
+    public List<Cache.Entry<Integer, Person>> findBySecondNameIn(String[] secondNames);
+
+    /** */
+    public List<Cache.Entry<Integer, Person>> findBySecondNameIn(String secondNames);
+
+    /** */
+    public List<Cache.Entry<Integer, Person>> findBySecondNameInAndFirstNameIn(List<String> secondNames, List<String> firstNames);
+
+    /** */
+    public List<Cache.Entry<Integer, Person>> findByFirstNameIsAndSecondNameIn(String firstNames, List<String> secondNames);
+
+    /** */
+    @Query(value = "SELECT * FROM Person WHERE secondName IN ?")
+    public List<Cache.Entry<Integer, Person>> selectInList(List<String> secondNames);
+
+    /** */
+    @Query(value = "SELECT * FROM Person WHERE secondName NOT IN ?")
+    public List<Cache.Entry<Integer, Person>> selectNotInList(List<String> secondNames);
+
+    /** */
+    @Query(value = "SELECT * FROM Person WHERE secondName IN :secondNames")
+    public List<Cache.Entry<Integer, Person>> selectInListWithNamedParameter(@Param("secondNames") List<String> secondNames);
+
+    /** */
+    @Query(value = "SELECT * FROM Person WHERE secondName IN :secondNames")
+    public List<Cache.Entry<Integer, Person>> selectInArrayWithNamedParameter(@Param("secondNames") String[] secondNames);
+
+    /** */
+    @Query(value = "SELECT * FROM Person WHERE secondName IN :secondNames")
+    public List<Cache.Entry<Integer, Person>> selectInSingleValueWithNamedParameter(@Param("secondNames") String secondNames);
+
+    /** */
+    @Query(value = "SELECT * FROM Person WHERE firstName IN :firstNames AND secondName IN :secondNames")
+    public List<Cache.Entry<Integer, Person>> selectWithMultipleInClauses(
+        @Param("secondNames") List<String> secondNames,
+        @Param("firstNames") List<String> firstNames
+    );
+
+    /** */
+    @Query(value = "SELECT * FROM Person WHERE firstName = ? AND secondName IN ?")
+    public List<Cache.Entry<Integer, Person>> selectWithInAndEqualClauses(
+        String firstName,
+        List<String> secondNames
+    );
+
+    /** */
+    @Query(value = "SELECT * FROM Person WHERE firstName = :name AND secondName IN :secondNames")
+    public List<Cache.Entry<Integer, Person>> selectWithInAndEqualClausesAndNamedArguments(
+        @Param("secondNames") List<String> secondNames,
+        @Param("name") String firstName
+    );
+
+    /** */
+    @Query(value = "SELECT * FROM Person WHERE firstName = :name AND secondName IN :#{#person.secondName}")
+    public List<Cache.Entry<Integer, Person>> selectInWithSpellClause(
+        @Param("person") Person person,
+        @Param("name") String firstName
+    );
+
+    /** */
+    @Query(value = "SELECT * FROM Person WHERE secondName IN ?3 AND firstName=?2")
+    public List<Cache.Entry<Integer, Person>> selectInListWithMultipleIndexedParameter(
+        int dummyArg,
+        String firstName,
+        List<String> secondNames
+    );
+
+    /** */
+    @Query(value = "SELECT * FROM Person WHERE secondName IN ?2")
+    public List<Cache.Entry<Integer, Person>> selectInArrayWithIndexedParameter(int dummyArg, String[] secondNames);
+
+    /** */
+    @Query(value = "SELECT * FROM Person WHERE secondName IN ?2")
+    public List<Cache.Entry<Integer, Person>> selectInSingleValueWithIndexedParameter(int dummyArg, String secondNames);
 }