You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by ma...@apache.org on 2023/07/26 16:27:28 UTC

[cassandra] 04/22: Allow CQL queries on multiple indexes without ALLOW FILTERING

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

maedhroz pushed a commit to branch cep-7-sai
in repository https://gitbox.apache.org/repos/asf/cassandra.git

commit aff7786510bb3b58d732d394cfc725b61a39b8bd
Author: Andrés de la Peña <a....@gmail.com>
AuthorDate: Mon Apr 24 16:39:26 2023 +0100

    Allow CQL queries on multiple indexes without ALLOW FILTERING
    
    patch by Andrés de la Peña; reviewed by Berenguer Blasi for CASSANDRA-18217
---
 .../restrictions/ClusteringColumnRestrictions.java |  25 +-
 .../cql3/restrictions/CustomIndexExpression.java   |  18 ++
 .../cql3/restrictions/IndexRestrictions.java       |  45 +++
 .../cql3/restrictions/MultiColumnRestriction.java  |  70 +++--
 .../PartitionKeySingleRestrictionSet.java          |   6 +-
 .../cassandra/cql3/restrictions/Restriction.java   |  13 +-
 .../cql3/restrictions/RestrictionSet.java          |  41 ++-
 .../cql3/restrictions/RestrictionSetWrapper.java   |  11 +-
 .../cql3/restrictions/SingleColumnRestriction.java |  45 ++-
 .../cql3/restrictions/StatementRestrictions.java   |  23 +-
 .../cassandra/cql3/restrictions/TokenFilter.java   |  11 +-
 .../cql3/restrictions/TokenRestriction.java        |   9 +-
 .../cassandra/cql3/statements/SelectStatement.java |  11 +-
 src/java/org/apache/cassandra/index/Index.java     |  21 +-
 .../cassandra/index/sai/StorageAttachedIndex.java  |   6 +
 .../ClusteringColumnRestrictionsTest.java          | 301 +++++++-------------
 .../apache/cassandra/index/CustomIndexTest.java    |  26 +-
 .../index/sai/cql/AllowFilteringTest.java          | 314 +++++++++++++++++++++
 .../cassandra/index/sai/cql/BaseDataModel.java     |  13 +-
 .../index/sai/cql/CollectionIndexingTest.java      |  39 ++-
 .../sai/cql/CompositePartitionKeyIndexTest.java    |   4 +-
 .../cassandra/index/sai/cql/IndexQuerySupport.java |  79 +++---
 .../sai/cql/MixedIndexImplementationsTest.java     |  32 +--
 .../index/sai/cql/MultipleColumnIndexTest.java     |  10 +-
 .../cassandra/index/sai/cql/types/QuerySet.java    |  16 +-
 .../index/sai/disk/SelectiveIntersectionTest.java  |  12 +-
 .../index/sai/disk/SingleNodeQueryFailureTest.java |   6 +-
 27 files changed, 802 insertions(+), 405 deletions(-)

diff --git a/src/java/org/apache/cassandra/cql3/restrictions/ClusteringColumnRestrictions.java b/src/java/org/apache/cassandra/cql3/restrictions/ClusteringColumnRestrictions.java
index 146ec20557..ddd9601c83 100644
--- a/src/java/org/apache/cassandra/cql3/restrictions/ClusteringColumnRestrictions.java
+++ b/src/java/org/apache/cassandra/cql3/restrictions/ClusteringColumnRestrictions.java
@@ -19,6 +19,8 @@ package org.apache.cassandra.cql3.restrictions;
 
 import java.util.*;
 
+import javax.annotation.Nullable;
+
 import org.apache.cassandra.db.guardrails.Guardrails;
 import org.apache.cassandra.schema.ColumnMetadata;
 import org.apache.cassandra.schema.TableMetadata;
@@ -42,18 +44,13 @@ final class ClusteringColumnRestrictions extends RestrictionSetWrapper
     /**
      * The composite type.
      */
-    protected final ClusteringComparator comparator;
+    private final ClusteringComparator comparator;
 
     /**
      * <code>true</code> if filtering is allowed for this restriction, <code>false</code> otherwise
      */
     private final boolean allowFiltering;
 
-    public ClusteringColumnRestrictions(TableMetadata table)
-    {
-        this(table, false);
-    }
-
     public ClusteringColumnRestrictions(TableMetadata table, boolean allowFiltering)
     {
         this(table.comparator, new RestrictionSet(), allowFiltering);
@@ -68,14 +65,16 @@ final class ClusteringColumnRestrictions extends RestrictionSetWrapper
         this.allowFiltering = allowFiltering;
     }
 
-    public ClusteringColumnRestrictions mergeWith(Restriction restriction) throws InvalidRequestException
+    public ClusteringColumnRestrictions mergeWith(Restriction restriction, @Nullable IndexRegistry indexRegistry) throws InvalidRequestException
     {
         SingleRestriction newRestriction = (SingleRestriction) restriction;
         RestrictionSet newRestrictionSet = restrictions.addRestriction(newRestriction);
 
-        if (!isEmpty() && !allowFiltering)
+        if (!isEmpty() && !allowFiltering && (indexRegistry == null || !newRestriction.hasSupportingIndex(indexRegistry)))
         {
             SingleRestriction lastRestriction = restrictions.lastRestriction();
+            assert lastRestriction != null;
+
             ColumnMetadata lastRestrictionStart = lastRestriction.getFirstColumn();
             ColumnMetadata newRestrictionStart = restriction.getFirstColumn();
 
@@ -156,7 +155,7 @@ final class ClusteringColumnRestrictions extends RestrictionSetWrapper
      * @return <code>true</code> if any of the underlying restriction is a CONTAINS or CONTAINS KEY,
      * <code>false</code> otherwise
      */
-    public final boolean hasContains()
+    public boolean hasContains()
     {
         for (SingleRestriction restriction : restrictions)
         {
@@ -172,7 +171,7 @@ final class ClusteringColumnRestrictions extends RestrictionSetWrapper
      * @return <code>true</code> if any of the underlying restriction is a slice restrictions,
      * <code>false</code> otherwise
      */
-    public final boolean hasSlice()
+    public boolean hasSlice()
     {
         for (SingleRestriction restriction : restrictions)
         {
@@ -188,7 +187,7 @@ final class ClusteringColumnRestrictions extends RestrictionSetWrapper
      * @return <code>true</code> if any underlying restrictions require filtering, <code>false</code>
      * otherwise
      */
-    public final boolean needFiltering()
+    public boolean needFiltering()
     {
         int position = 0;
 
@@ -204,7 +203,7 @@ final class ClusteringColumnRestrictions extends RestrictionSetWrapper
     }
 
     @Override
-    public void addRowFilterTo(RowFilter filter,
+    public void addToRowFilter(RowFilter filter,
                                IndexRegistry indexRegistry,
                                QueryOptions options) throws InvalidRequestException
     {
@@ -215,7 +214,7 @@ final class ClusteringColumnRestrictions extends RestrictionSetWrapper
             // We ignore all the clustering columns that can be handled by slices.
             if (handleInFilter(restriction, position) || restriction.hasSupportingIndex(indexRegistry))
             {
-                restriction.addRowFilterTo(filter, indexRegistry, options);
+                restriction.addToRowFilter(filter, indexRegistry, options);
                 continue;
             }
 
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/CustomIndexExpression.java b/src/java/org/apache/cassandra/cql3/restrictions/CustomIndexExpression.java
index 7a5fff62b4..569418b3f4 100644
--- a/src/java/org/apache/cassandra/cql3/restrictions/CustomIndexExpression.java
+++ b/src/java/org/apache/cassandra/cql3/restrictions/CustomIndexExpression.java
@@ -22,6 +22,7 @@ import java.util.Objects;
 import org.apache.cassandra.cql3.*;
 import org.apache.cassandra.db.filter.RowFilter;
 import org.apache.cassandra.db.marshal.AbstractType;
+import org.apache.cassandra.index.Index;
 import org.apache.cassandra.schema.TableMetadata;
 
 public class CustomIndexExpression
@@ -55,6 +56,23 @@ public class CustomIndexExpression
                                         value.bindAndGet(options));
     }
 
+    /**
+     * Returns whether this expression would need filtering if the specified index group were used.
+     *
+     * @param indexGroup an index group
+     * @return {@code true} if this would need filtering if {@code indexGroup} were used, {@code false} otherwise
+     */
+    public boolean needsFiltering(Index.Group indexGroup)
+    {
+        String indexName = targetIndex.getName();
+        for (Index index : indexGroup.getIndexes())
+        {
+            if (index.getIndexMetadata().name.equals(indexName))
+                return false;
+        }
+        return true;
+    }
+
     public String toCQLString()
     {
         return String.format("expr(%s,%s)", targetIndex.toCQLString(), valueRaw.getText());
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/IndexRestrictions.java b/src/java/org/apache/cassandra/cql3/restrictions/IndexRestrictions.java
index fd89d1b470..fe01e41887 100644
--- a/src/java/org/apache/cassandra/cql3/restrictions/IndexRestrictions.java
+++ b/src/java/org/apache/cassandra/cql3/restrictions/IndexRestrictions.java
@@ -22,6 +22,8 @@ import java.util.ArrayList;
 import java.util.List;
 
 import org.apache.cassandra.cql3.QualifiedName;
+import org.apache.cassandra.index.Index;
+import org.apache.cassandra.index.IndexRegistry;
 import org.apache.cassandra.schema.TableMetadata;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.commons.lang3.builder.ToStringBuilder;
@@ -63,6 +65,49 @@ public class IndexRestrictions
         return customExpressions;
     }
 
+    /**
+     * Returns whether these restrictions would need filtering if the specified index registry were used.
+     *
+     * @param indexRegistry an index registry
+     * @return {@code true} if this would need filtering if {@code indexRegistry} were used, {@code false} otherwise
+     */
+    public boolean needsFiltering(IndexRegistry indexRegistry)
+    {
+        if (isEmpty())
+            return false;
+
+        for (Index.Group group : indexRegistry.listIndexGroups())
+        {
+            if (!needsFiltering(group))
+                return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns whether these restrictions would need filtering if the specified index group were used.
+     *
+     * @param indexGroup an index group
+     * @return {@code true} if this would need filtering if {@code indexGroup} were used, {@code false} otherwise
+     */
+    private boolean needsFiltering(Index.Group indexGroup)
+    {
+        for (Restrictions restrictions : regularRestrictions)
+        {
+            if (restrictions.needsFiltering(indexGroup))
+                return true;
+        }
+
+        for (CustomIndexExpression restriction : customExpressions)
+        {
+            if (restriction.needsFiltering(indexGroup))
+                return true;
+        }
+
+        return false;
+    }
+
     static InvalidRequestException invalidIndex(QualifiedName indexName, TableMetadata table)
     {
         return new InvalidRequestException(String.format(INVALID_INDEX, indexName.getName(), table));
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/MultiColumnRestriction.java b/src/java/org/apache/cassandra/cql3/restrictions/MultiColumnRestriction.java
index 22c000e3b5..37c64c614d 100644
--- a/src/java/org/apache/cassandra/cql3/restrictions/MultiColumnRestriction.java
+++ b/src/java/org/apache/cassandra/cql3/restrictions/MultiColumnRestriction.java
@@ -128,6 +128,28 @@ public abstract class MultiColumnRestriction implements SingleRestriction
         for (Index index : indexRegistry.listIndexes())
            if (isSupportedBy(index))
                return true;
+
+        return false;
+    }
+
+    @Override
+    public boolean needsFiltering(Index.Group indexGroup)
+    {
+        for (ColumnMetadata column : columnDefs)
+        {
+            if (!isSupportedBy(indexGroup, column))
+                return true;
+        }
+        return false;
+    }
+
+    private boolean isSupportedBy(Index.Group indexGroup, ColumnMetadata column)
+    {
+        for (Index index : indexGroup.getIndexes())
+        {
+            if (isSupportedBy(index, column))
+                return true;
+        }
         return false;
     }
 
@@ -138,7 +160,17 @@ public abstract class MultiColumnRestriction implements SingleRestriction
      * @return <code>true</code> this type of restriction is supported by the specified index,
      * <code>false</code> otherwise.
      */
-    protected abstract boolean isSupportedBy(Index index);
+    private boolean isSupportedBy(Index index)
+    {
+        for (ColumnMetadata column : columnDefs)
+        {
+            if (isSupportedBy(index, column))
+                return true;
+        }
+        return false;
+    }
+
+    protected abstract boolean isSupportedBy(Index index, ColumnMetadata def);
 
     public static class EQRestriction extends MultiColumnRestriction
     {
@@ -176,12 +208,9 @@ public abstract class MultiColumnRestriction implements SingleRestriction
         }
 
         @Override
-        protected boolean isSupportedBy(Index index)
+        protected boolean isSupportedBy(Index index, ColumnMetadata column)
         {
-            for(ColumnMetadata column : columnDefs)
-                if (index.supportsExpression(column, Operator.EQ))
-                    return true;
-            return false;
+            return index.supportsExpression(column, Operator.EQ);
         }
 
         @Override
@@ -198,7 +227,7 @@ public abstract class MultiColumnRestriction implements SingleRestriction
         }
 
         @Override
-        public final void addRowFilterTo(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options)
+        public final void addToRowFilter(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options)
         {
             Tuples.Value t = ((Tuples.Value) value.bind(options));
             List<ByteBuffer> values = t.getElements();
@@ -246,16 +275,13 @@ public abstract class MultiColumnRestriction implements SingleRestriction
         }
 
         @Override
-        protected boolean isSupportedBy(Index index)
+        protected boolean isSupportedBy(Index index, ColumnMetadata column)
         {
-            for (ColumnMetadata column: columnDefs)
-                if (index.supportsExpression(column, Operator.IN))
-                    return true;
-            return false;
+            return index.supportsExpression(column, Operator.IN);
         }
 
         @Override
-        public final void addRowFilterTo(RowFilter filter,
+        public final void addToRowFilter(RowFilter filter,
                                          IndexRegistry indexRegistry,
                                          QueryOptions options)
         {
@@ -443,12 +469,9 @@ public abstract class MultiColumnRestriction implements SingleRestriction
         }
 
         @Override
-        protected boolean isSupportedBy(Index index)
+        protected boolean isSupportedBy(Index index, ColumnMetadata column)
         {
-            for(ColumnMetadata def : columnDefs)
-                if (slice.isSupportedBy(def, index))
-                    return true;
-            return false;
+            return slice.isSupportedBy(column, index);
         }
 
         @Override
@@ -499,7 +522,7 @@ public abstract class MultiColumnRestriction implements SingleRestriction
         }
 
         @Override
-        public final void addRowFilterTo(RowFilter filter,
+        public final void addToRowFilter(RowFilter filter,
                                          IndexRegistry indexRegistry,
                                          QueryOptions options)
         {
@@ -573,12 +596,9 @@ public abstract class MultiColumnRestriction implements SingleRestriction
         }
 
         @Override
-        protected boolean isSupportedBy(Index index)
+        protected boolean isSupportedBy(Index index, ColumnMetadata column)
         {
-            for(ColumnMetadata column : columnDefs)
-                if (index.supportsExpression(column, Operator.IS_NOT))
-                    return true;
-            return false;
+            return index.supportsExpression(column, Operator.IS_NOT);
         }
 
         @Override
@@ -588,7 +608,7 @@ public abstract class MultiColumnRestriction implements SingleRestriction
         }
 
         @Override
-        public final void addRowFilterTo(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options)
+        public final void addToRowFilter(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options)
         {
             throw new UnsupportedOperationException("Secondary indexes do not support IS NOT NULL restrictions");
         }
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/PartitionKeySingleRestrictionSet.java b/src/java/org/apache/cassandra/cql3/restrictions/PartitionKeySingleRestrictionSet.java
index 0d3d7e12dd..914681d691 100644
--- a/src/java/org/apache/cassandra/cql3/restrictions/PartitionKeySingleRestrictionSet.java
+++ b/src/java/org/apache/cassandra/cql3/restrictions/PartitionKeySingleRestrictionSet.java
@@ -42,7 +42,7 @@ final class PartitionKeySingleRestrictionSet extends RestrictionSetWrapper imple
     /**
      * The composite type.
      */
-    protected final ClusteringComparator comparator;
+    private final ClusteringComparator comparator;
 
     public PartitionKeySingleRestrictionSet(ClusteringComparator comparator)
     {
@@ -131,13 +131,13 @@ final class PartitionKeySingleRestrictionSet extends RestrictionSetWrapper imple
     }
 
     @Override
-    public void addRowFilterTo(RowFilter filter,
+    public void addToRowFilter(RowFilter filter,
                                IndexRegistry indexRegistry,
                                QueryOptions options)
     {
         for (SingleRestriction restriction : restrictions)
         {
-             restriction.addRowFilterTo(filter, indexRegistry, options);
+             restriction.addToRowFilter(filter, indexRegistry, options);
         }
     }
 
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/Restriction.java b/src/java/org/apache/cassandra/cql3/restrictions/Restriction.java
index 91dedad953..e2ab1aba95 100644
--- a/src/java/org/apache/cassandra/cql3/restrictions/Restriction.java
+++ b/src/java/org/apache/cassandra/cql3/restrictions/Restriction.java
@@ -19,6 +19,7 @@ package org.apache.cassandra.cql3.restrictions;
 
 import java.util.List;
 
+import org.apache.cassandra.index.Index;
 import org.apache.cassandra.schema.ColumnMetadata;
 import org.apache.cassandra.cql3.QueryOptions;
 import org.apache.cassandra.cql3.functions.Function;
@@ -66,7 +67,15 @@ public interface Restriction
      * @param indexRegistry the index registry
      * @return <code>true</code> if the restriction is on indexed columns, <code>false</code>
      */
-    public boolean hasSupportingIndex(IndexRegistry indexRegistry);
+    boolean hasSupportingIndex(IndexRegistry indexRegistry);
+
+    /**
+     * Returns whether this restriction would need filtering if the specified index group were used.
+     *
+     * @param indexGroup an index group
+     * @return {@code true} if this would need filtering if {@code indexGroup} were used, {@code false} otherwise
+     */
+    boolean needsFiltering(Index.Group indexGroup);
 
     /**
      * Adds to the specified row filter the expressions corresponding to this <code>Restriction</code>.
@@ -75,7 +84,7 @@ public interface Restriction
      * @param indexRegistry the index registry
      * @param options the query options
      */
-    public void addRowFilterTo(RowFilter filter,
+    public void addToRowFilter(RowFilter filter,
                                IndexRegistry indexRegistry,
                                QueryOptions options);
 }
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSet.java b/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSet.java
index 344498ce0e..86aa269dcd 100644
--- a/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSet.java
+++ b/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSet.java
@@ -21,10 +21,10 @@ import java.util.*;
 
 import com.google.common.collect.AbstractIterator;
 
+import org.apache.cassandra.index.Index;
 import org.apache.cassandra.schema.ColumnMetadata;
 import org.apache.cassandra.cql3.QueryOptions;
 import org.apache.cassandra.cql3.functions.Function;
-import org.apache.cassandra.cql3.restrictions.SingleColumnRestriction.ContainsRestriction;
 import org.apache.cassandra.db.filter.RowFilter;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.index.IndexRegistry;
@@ -56,7 +56,7 @@ final class RestrictionSet implements Restrictions, Iterable<SingleRestriction>
     /**
      * The restrictions per column.
      */
-    protected final TreeMap<ColumnMetadata, SingleRestriction> restrictions;
+    private final TreeMap<ColumnMetadata, SingleRestriction> restrictions;
 
     /**
      * {@code true} if it contains multi-column restrictions, {@code false} otherwise.
@@ -93,10 +93,21 @@ final class RestrictionSet implements Restrictions, Iterable<SingleRestriction>
     }
 
     @Override
-    public void addRowFilterTo(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options) throws InvalidRequestException
+    public void addToRowFilter(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options) throws InvalidRequestException
     {
         for (Restriction restriction : restrictions.values())
-            restriction.addRowFilterTo(filter, indexRegistry, options);
+            restriction.addToRowFilter(filter, indexRegistry, options);
+    }
+
+    @Override
+    public boolean needsFiltering(Index.Group indexGroup)
+    {
+        for (SingleRestriction restriction : restrictions.values())
+        {
+            if (restriction.needsFiltering(indexGroup))
+                return true;
+        }
+        return false;
     }
 
     @Override
@@ -223,7 +234,7 @@ final class RestrictionSet implements Restrictions, Iterable<SingleRestriction>
     }
 
     @Override
-    public final boolean hasSupportingIndex(IndexRegistry indexRegistry)
+    public boolean hasSupportingIndex(IndexRegistry indexRegistry)
     {
         for (Restriction restriction : restrictions.values())
         {
@@ -281,26 +292,6 @@ final class RestrictionSet implements Restrictions, Iterable<SingleRestriction>
                                    : restriction.mergeWith(otherRestriction);
     }
 
-    /**
-     * Checks if the restrictions contains multiple contains, contains key, or map[key] = value.
-     *
-     * @return <code>true</code> if the restrictions contains multiple contains, contains key, or ,
-     * map[key] = value; <code>false</code> otherwise
-     */
-    public final boolean hasMultipleContains()
-    {
-        int numberOfContains = 0;
-        for (SingleRestriction restriction : restrictions.values())
-        {
-            if (restriction.isContains())
-            {
-                ContainsRestriction contains = (ContainsRestriction) restriction;
-                numberOfContains += (contains.numberOfValues() + contains.numberOfKeys() + contains.numberOfEntries());
-            }
-        }
-        return numberOfContains > 1;
-    }
-
     @Override
     public Iterator<SingleRestriction> iterator()
     {
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSetWrapper.java b/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSetWrapper.java
index 8c89bca314..ac092740c0 100644
--- a/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSetWrapper.java
+++ b/src/java/org/apache/cassandra/cql3/restrictions/RestrictionSetWrapper.java
@@ -24,6 +24,7 @@ import java.util.Set;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.apache.commons.lang3.builder.ToStringStyle;
 
+import org.apache.cassandra.index.Index;
 import org.apache.cassandra.schema.ColumnMetadata;
 import org.apache.cassandra.cql3.QueryOptions;
 import org.apache.cassandra.cql3.functions.Function;
@@ -46,11 +47,11 @@ class RestrictionSetWrapper implements Restrictions
         this.restrictions = restrictions;
     }
 
-    public void addRowFilterTo(RowFilter filter,
+    public void addToRowFilter(RowFilter filter,
                                IndexRegistry indexRegistry,
                                QueryOptions options)
     {
-        restrictions.addRowFilterTo(filter, indexRegistry, options);
+        restrictions.addToRowFilter(filter, indexRegistry, options);
     }
 
     public List<ColumnMetadata> getColumnDefs()
@@ -89,6 +90,12 @@ class RestrictionSetWrapper implements Restrictions
         return restrictions.hasSupportingIndex(indexRegistry);
     }
 
+    @Override
+    public boolean needsFiltering(Index.Group indexGroup)
+    {
+        return restrictions.needsFiltering(indexGroup);
+    }
+
     public ColumnMetadata getFirstColumn()
     {
         return restrictions.getFirstColumn();
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/SingleColumnRestriction.java b/src/java/org/apache/cassandra/cql3/restrictions/SingleColumnRestriction.java
index 5ce78ae577..a398762d2f 100644
--- a/src/java/org/apache/cassandra/cql3/restrictions/SingleColumnRestriction.java
+++ b/src/java/org/apache/cassandra/cql3/restrictions/SingleColumnRestriction.java
@@ -81,6 +81,16 @@ public abstract class SingleColumnRestriction implements SingleRestriction
         return false;
     }
 
+    @Override
+    public boolean needsFiltering(Index.Group indexGroup)
+    {
+        for (Index index : indexGroup.getIndexes())
+            if (isSupportedBy(index))
+                return false;
+
+        return true;
+    }
+
     @Override
     public final SingleRestriction mergeWith(SingleRestriction otherRestriction)
     {
@@ -151,7 +161,7 @@ public abstract class SingleColumnRestriction implements SingleRestriction
         }
 
         @Override
-        public void addRowFilterTo(RowFilter filter,
+        public void addToRowFilter(RowFilter filter,
                                    IndexRegistry indexRegistry,
                                    QueryOptions options)
         {
@@ -215,7 +225,7 @@ public abstract class SingleColumnRestriction implements SingleRestriction
         }
 
         @Override
-        public void addRowFilterTo(RowFilter filter,
+        public void addToRowFilter(RowFilter filter,
                                    IndexRegistry indexRegistry,
                                    QueryOptions options)
         {
@@ -388,7 +398,7 @@ public abstract class SingleColumnRestriction implements SingleRestriction
         }
 
         @Override
-        public void addRowFilterTo(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options)
+        public void addToRowFilter(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options)
         {
             for (Bound b : Bound.values())
                 if (hasBound(b))
@@ -417,10 +427,10 @@ public abstract class SingleColumnRestriction implements SingleRestriction
     // This holds CONTAINS, CONTAINS_KEY, and map[key] = value restrictions because we might want to have any combination of them.
     public static final class ContainsRestriction extends SingleColumnRestriction
     {
-        private List<Term> values = new ArrayList<>(); // for CONTAINS
-        private List<Term> keys = new ArrayList<>(); // for CONTAINS_KEY
-        private List<Term> entryKeys = new ArrayList<>(); // for map[key] = value
-        private List<Term> entryValues = new ArrayList<>(); // for map[key] = value
+        private final List<Term> values = new ArrayList<>(); // for CONTAINS
+        private final List<Term> keys = new ArrayList<>(); // for CONTAINS_KEY
+        private final List<Term> entryKeys = new ArrayList<>(); // for map[key] = value
+        private final List<Term> entryValues = new ArrayList<>(); // for map[key] = value
 
         public ContainsRestriction(ColumnMetadata columnDef, Term t, boolean isKey)
         {
@@ -478,7 +488,7 @@ public abstract class SingleColumnRestriction implements SingleRestriction
         }
 
         @Override
-        public void addRowFilterTo(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options)
+        public void addToRowFilter(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options)
         {
             for (ByteBuffer value : bindAndGet(values, options))
                 filter.add(columnDef, Operator.CONTAINS, value);
@@ -509,6 +519,21 @@ public abstract class SingleColumnRestriction implements SingleRestriction
             return supported;
         }
 
+        @Override
+        public boolean needsFiltering(Index.Group indexGroup)
+        {
+            // multiple contains might require filtering on some indexes, since that is equivalent to a disjunction (or)
+            boolean hasMultipleContains = (numberOfValues() + numberOfKeys() + numberOfEntries()) > 1;
+
+            for (Index index : indexGroup.getIndexes())
+            {
+                if (isSupportedBy(index) && !(hasMultipleContains && index.filtersMultipleContains()))
+                    return false;
+            }
+
+            return true;
+        }
+
         public int numberOfValues()
         {
             return values.size();
@@ -617,7 +642,7 @@ public abstract class SingleColumnRestriction implements SingleRestriction
         }
 
         @Override
-        public void addRowFilterTo(RowFilter filter,
+        public void addToRowFilter(RowFilter filter,
                                    IndexRegistry indexRegistry,
                                    QueryOptions options)
         {
@@ -693,7 +718,7 @@ public abstract class SingleColumnRestriction implements SingleRestriction
         }
 
         @Override
-        public void addRowFilterTo(RowFilter filter,
+        public void addToRowFilter(RowFilter filter,
                                    IndexRegistry indexRegistry,
                                    QueryOptions options)
         {
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java b/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java
index 52deaabbfa..bcf1642aac 100644
--- a/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java
+++ b/src/java/org/apache/cassandra/cql3/restrictions/StatementRestrictions.java
@@ -196,13 +196,13 @@ public final class StatementRestrictions
                 if (!type.allowUseOfSecondaryIndices() || !restriction.hasSupportingIndex(indexRegistry))
                     throw new InvalidRequestException(String.format("LIKE restriction is only supported on properly " +
                                                                     "indexed columns. %s is not valid.",
-                                                                    relation.toString()));
+                                                                    relation));
 
-                addRestriction(restriction);
+                addRestriction(restriction, indexRegistry);
             }
             else
             {
-                addRestriction(relation.toRestriction(table, boundNames));
+                addRestriction(relation.toRestriction(table, boundNames), indexRegistry);
             }
         }
 
@@ -296,13 +296,13 @@ public final class StatementRestrictions
         return !tableNullable.allowFilteringImplicitly();
     }
 
-    private void addRestriction(Restriction restriction)
+    private void addRestriction(Restriction restriction, IndexRegistry indexRegistry)
     {
         ColumnMetadata def = restriction.getFirstColumn();
         if (def.isPartitionKey())
             partitionKeyRestrictions = partitionKeyRestrictions.mergeWith(restriction);
         else if (def.isClusteringColumn())
-            clusteringColumnsRestrictions = clusteringColumnsRestrictions.mergeWith(restriction);
+            clusteringColumnsRestrictions = clusteringColumnsRestrictions.mergeWith(restriction, indexRegistry);
         else
             nonPrimaryKeyRestrictions = nonPrimaryKeyRestrictions.addRestriction((SingleRestriction) restriction);
     }
@@ -682,7 +682,7 @@ public final class StatementRestrictions
 
         RowFilter filter = RowFilter.create();
         for (Restrictions restrictions : filterRestrictions.getRestrictions())
-            restrictions.addRowFilterTo(filter, indexRegistry, options);
+            restrictions.addToRowFilter(filter, indexRegistry, options);
 
         for (CustomIndexExpression expression : filterRestrictions.getCustomIndexExpressions())
             expression.addToRowFilter(filter, table, options);
@@ -868,16 +868,17 @@ public final class StatementRestrictions
      * Checks if the query need to use filtering.
      * @return <code>true</code> if the query need to use filtering, <code>false</code> otherwise.
      */
-    public boolean needFiltering()
+    public boolean needFiltering(TableMetadata table)
     {
+        IndexRegistry indexRegistry = IndexRegistry.obtain(table);
+        if (filterRestrictions.needsFiltering(indexRegistry))
+            return true;
+
         int numberOfRestrictions = filterRestrictions.getCustomIndexExpressions().size();
         for (Restrictions restrictions : filterRestrictions.getRestrictions())
             numberOfRestrictions += restrictions.size();
 
-        return numberOfRestrictions > 1
-                || (numberOfRestrictions == 0 && !clusteringColumnsRestrictions.isEmpty())
-                || (numberOfRestrictions != 0
-                        && nonPrimaryKeyRestrictions.hasMultipleContains());
+        return numberOfRestrictions == 0 && !clusteringColumnsRestrictions.isEmpty();
     }
 
     private void validateSecondaryIndexSelections()
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/TokenFilter.java b/src/java/org/apache/cassandra/cql3/restrictions/TokenFilter.java
index 9f67cc0547..42e3acf716 100644
--- a/src/java/org/apache/cassandra/cql3/restrictions/TokenFilter.java
+++ b/src/java/org/apache/cassandra/cql3/restrictions/TokenFilter.java
@@ -25,6 +25,7 @@ import com.google.common.collect.ImmutableRangeSet;
 import com.google.common.collect.Range;
 import com.google.common.collect.RangeSet;
 
+import org.apache.cassandra.index.Index;
 import org.apache.cassandra.schema.ColumnMetadata;
 import org.apache.cassandra.schema.TableMetadata;
 import org.apache.cassandra.cql3.QueryOptions;
@@ -280,9 +281,15 @@ final class TokenFilter implements PartitionKeyRestrictions
     }
 
     @Override
-    public void addRowFilterTo(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options)
+    public void addToRowFilter(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options)
     {
-        restrictions.addRowFilterTo(filter, indexRegistry, options);
+        restrictions.addToRowFilter(filter, indexRegistry, options);
+    }
+
+    @Override
+    public boolean needsFiltering(Index.Group indexGroup)
+    {
+        return restrictions.needsFiltering(indexGroup);
     }
 
     @Override
diff --git a/src/java/org/apache/cassandra/cql3/restrictions/TokenRestriction.java b/src/java/org/apache/cassandra/cql3/restrictions/TokenRestriction.java
index d7477fbff9..215f3237e8 100644
--- a/src/java/org/apache/cassandra/cql3/restrictions/TokenRestriction.java
+++ b/src/java/org/apache/cassandra/cql3/restrictions/TokenRestriction.java
@@ -22,6 +22,7 @@ import java.util.*;
 
 import com.google.common.base.Joiner;
 
+import org.apache.cassandra.index.Index;
 import org.apache.cassandra.schema.ColumnMetadata;
 import org.apache.cassandra.schema.TableMetadata;
 import org.apache.cassandra.cql3.QueryOptions;
@@ -123,11 +124,17 @@ public abstract class TokenRestriction implements PartitionKeyRestrictions
     }
 
     @Override
-    public void addRowFilterTo(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options)
+    public void addToRowFilter(RowFilter filter, IndexRegistry indexRegistry, QueryOptions options)
     {
         throw new UnsupportedOperationException("Index expression cannot be created for token restriction");
     }
 
+    @Override
+    public boolean needsFiltering(Index.Group indexGroup)
+    {
+        return false;
+    }
+
     @Override
     public final boolean isEmpty()
     {
diff --git a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
index 1487aa30ed..bbfaed1bf1 100644
--- a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
@@ -1176,7 +1176,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
                     orderingComparator = Collections.reverseOrder(orderingComparator);
             }
 
-            checkNeedsFiltering(restrictions);
+            checkNeedsFiltering(table, restrictions);
 
             return new SelectStatement(table,
                                        bindVariables,
@@ -1485,16 +1485,15 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
         }
 
         /** If ALLOW FILTERING was not specified, this verifies that it is not needed */
-        private void checkNeedsFiltering(StatementRestrictions restrictions) throws InvalidRequestException
+        private void checkNeedsFiltering(TableMetadata table, StatementRestrictions restrictions) throws InvalidRequestException
         {
             // non-key-range non-indexed queries cannot involve filtering underneath
             if (!parameters.allowFiltering && (restrictions.isKeyRange() || restrictions.usesSecondaryIndexing()))
             {
-                // We will potentially filter data if either:
-                //  - Have more than one IndexExpression
-                //  - Have no index expression and the row filter is not the identity
+                // We will potentially filter data if the row filter is not the identity and there isn't any index group
+                // supporting all the expressions in the filter.
                 if (restrictions.requiresAllowFilteringIfNotSpecified())
-                    checkFalse(restrictions.needFiltering(), StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE);
+                    checkFalse(restrictions.needFiltering(table), StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE);
             }
         }
 
diff --git a/src/java/org/apache/cassandra/index/Index.java b/src/java/org/apache/cassandra/index/Index.java
index e1c486faf4..5ca9e719a2 100644
--- a/src/java/org/apache/cassandra/index/Index.java
+++ b/src/java/org/apache/cassandra/index/Index.java
@@ -220,7 +220,7 @@ public interface Index
     {
         return INDEX_BUILDER_SUPPORT;
     }
-    
+
     /**
      * Same as {@code getBuildTaskSupport} but can be overloaded with a specific 'recover' logic different than the index building one
      */
@@ -228,7 +228,7 @@ public interface Index
     {
         return getBuildTaskSupport();
     }
-    
+
     /**
      * Returns the type of operations supported by the index in case its building has failed and it's needing recovery.
      *
@@ -277,9 +277,9 @@ public interface Index
 
     /**
      * If the index implementation uses a local table to store its index data, this method should return a
-     * handle to it. If not, an empty {@link Optional} should be returned. This exists to support legacy 
+     * handle to it. If not, an empty {@link Optional} should be returned. This exists to support legacy
      * implementations, and should always be empty for indexes not belonging to a {@link SingletonIndexGroup}.
-     * 
+     *
      * @return an Optional referencing the Index's backing storage table if it has one, or Optional.empty() if not.
      */
     public Optional<ColumnFamilyStore> getBackingTable();
@@ -389,6 +389,17 @@ public interface Index
      */
     public boolean supportsExpression(ColumnMetadata column, Operator operator);
 
+    /**
+     * Returns whether this index does any kind of filtering when the query has multiple contains expressions, assuming
+     * that each of those expressions are supported as defined by {@link #supportsExpression(ColumnMetadata, Operator)}.
+     *
+     * @return {@code true} if this index uses filtering on multiple contains expressions, {@code false} otherwise
+     */
+    default boolean filtersMultipleContains()
+    {
+        return true;
+    }
+
     /**
      * If the index supports custom search expressions using the
      * {@code}SELECT * FROM table WHERE expr(index_name, expression){@code} syntax, this
@@ -636,7 +647,7 @@ public interface Index
          * Replica filtering protection may fetch data that doesn't match query conditions.
          *
          * On coordinator, we need to filter the replicas' responses again.
-         * 
+         *
          * This will not be called if {@link QueryPlan#supportsReplicaFilteringProtection(RowFilter)} returns false.
          *
          * @return filtered response that satisfied query conditions
diff --git a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndex.java b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndex.java
index fdc1759cc5..44095ebf7e 100644
--- a/src/java/org/apache/cassandra/index/sai/StorageAttachedIndex.java
+++ b/src/java/org/apache/cassandra/index/sai/StorageAttachedIndex.java
@@ -480,6 +480,12 @@ public class StorageAttachedIndex implements Index
         return dependsOn(column) && indexContext.supports(operator);
     }
 
+    @Override
+    public boolean filtersMultipleContains()
+    {
+        return false;
+    }
+
     @Override
     public AbstractType<?> customExpressionValueType()
     {
diff --git a/test/unit/org/apache/cassandra/cql3/restrictions/ClusteringColumnRestrictionsTest.java b/test/unit/org/apache/cassandra/cql3/restrictions/ClusteringColumnRestrictionsTest.java
index 029729fe78..bf8e1e4955 100644
--- a/test/unit/org/apache/cassandra/cql3/restrictions/ClusteringColumnRestrictionsTest.java
+++ b/test/unit/org/apache/cassandra/cql3/restrictions/ClusteringColumnRestrictionsTest.java
@@ -54,7 +54,7 @@ public class ClusteringColumnRestrictionsTest
     {
         TableMetadata tableMetadata = newTableMetadata(Sort.ASC);
 
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
+        ClusteringColumnRestrictions restrictions = restrictions(tableMetadata);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -76,8 +76,7 @@ public class ClusteringColumnRestrictionsTest
         ByteBuffer clustering_0 = ByteBufferUtil.bytes(1);
         Restriction eq = newSingleEq(tableMetadata, 0, clustering_0);
 
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(eq);
+        ClusteringColumnRestrictions restrictions =  restrictions(tableMetadata, eq);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -99,8 +98,7 @@ public class ClusteringColumnRestrictionsTest
         ByteBuffer clustering_0 = ByteBufferUtil.bytes(1);
         Restriction eq = newSingleEq(tableMetadata, 0, clustering_0);
 
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(eq);
+        ClusteringColumnRestrictions restrictions =  restrictions(tableMetadata, eq);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -124,9 +122,7 @@ public class ClusteringColumnRestrictionsTest
         TableMetadata tableMetadata = newTableMetadata(Sort.ASC, Sort.ASC);
 
         Restriction in = newSingleIN(tableMetadata, 0, value1, value2, value3);
-
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(in);
+        ClusteringColumnRestrictions restrictions = restrictions(tableMetadata, in);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(3, bounds.size());
@@ -153,8 +149,7 @@ public class ClusteringColumnRestrictionsTest
         ByteBuffer value2 = ByteBufferUtil.bytes(2);
 
         Restriction slice = newSingleSlice(tableMetadata, 0, Bound.START, false, value1);
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        ClusteringColumnRestrictions restrictions = restrictions(tableMetadata, slice);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -165,8 +160,7 @@ public class ClusteringColumnRestrictionsTest
         assertEmptyEnd(get(bounds, 0));
 
         slice = newSingleSlice(tableMetadata, 0, Bound.START, true, value1);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -177,8 +171,7 @@ public class ClusteringColumnRestrictionsTest
         assertEmptyEnd(get(bounds, 0));
 
         slice = newSingleSlice(tableMetadata, 0, Bound.END, true, value1);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -189,8 +182,7 @@ public class ClusteringColumnRestrictionsTest
         assertEndBound(get(bounds, 0), true, value1);
 
         slice = newSingleSlice(tableMetadata, 0, Bound.END, false, value1);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -202,8 +194,7 @@ public class ClusteringColumnRestrictionsTest
 
         slice = newSingleSlice(tableMetadata, 0, Bound.START, false, value1);
         Restriction slice2 = newSingleSlice(tableMetadata, 0, Bound.END, false, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+        restrictions = restrictions(tableMetadata, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -215,8 +206,7 @@ public class ClusteringColumnRestrictionsTest
 
         slice = newSingleSlice(tableMetadata, 0, Bound.START, true, value1);
         slice2 = newSingleSlice(tableMetadata, 0, Bound.END, true, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+        restrictions = restrictions(tableMetadata, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -239,8 +229,7 @@ public class ClusteringColumnRestrictionsTest
         ByteBuffer value2 = ByteBufferUtil.bytes(2);
 
         Restriction slice = newSingleSlice(tableMetadata, 0, Bound.START, false, value1);
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        ClusteringColumnRestrictions restrictions = restrictions(tableMetadata, slice);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -251,8 +240,7 @@ public class ClusteringColumnRestrictionsTest
         assertEndBound(get(bounds, 0), false, value1);
 
         slice = newSingleSlice(tableMetadata, 0, Bound.START, true, value1);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -263,8 +251,7 @@ public class ClusteringColumnRestrictionsTest
         assertEndBound(get(bounds, 0), true, value1);
 
         slice = newSingleSlice(tableMetadata, 0, Bound.END, true, value1);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -275,8 +262,7 @@ public class ClusteringColumnRestrictionsTest
         assertEmptyEnd(get(bounds, 0));
 
         slice = newSingleSlice(tableMetadata, 0, Bound.END, false, value1);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -288,8 +274,7 @@ public class ClusteringColumnRestrictionsTest
 
         slice = newSingleSlice(tableMetadata, 0, Bound.START, false, value1);
         Restriction slice2 = newSingleSlice(tableMetadata, 0, Bound.END, false, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+        restrictions = restrictions(tableMetadata, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -301,8 +286,7 @@ public class ClusteringColumnRestrictionsTest
 
         slice = newSingleSlice(tableMetadata, 0, Bound.START, true, value1);
         slice2 = newSingleSlice(tableMetadata, 0, Bound.END, true, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+        restrictions = restrictions(tableMetadata, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -326,8 +310,7 @@ public class ClusteringColumnRestrictionsTest
         ByteBuffer value3 = ByteBufferUtil.bytes(3);
         Restriction eq = newSingleEq(tableMetadata, 0, value1);
         Restriction in = newSingleIN(tableMetadata, 1, value1, value2, value3);
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(eq).mergeWith(in);
+        ClusteringColumnRestrictions restrictions = restrictions(tableMetadata, eq, in);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(3, bounds.size());
@@ -357,8 +340,7 @@ public class ClusteringColumnRestrictionsTest
         Restriction eq = newSingleEq(tableMetadata, 0, value3);
 
         Restriction slice = newSingleSlice(tableMetadata, 1, Bound.START, false, value1);
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(eq).mergeWith(slice);
+        ClusteringColumnRestrictions restrictions = restrictions(tableMetadata, eq, slice);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -369,8 +351,7 @@ public class ClusteringColumnRestrictionsTest
         assertEndBound(get(bounds, 0), true, value3);
 
         slice = newSingleSlice(tableMetadata, 1, Bound.START, true, value1);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(eq).mergeWith(slice);
+        restrictions = restrictions(tableMetadata, eq, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -381,8 +362,7 @@ public class ClusteringColumnRestrictionsTest
         assertEndBound(get(bounds, 0), true, value3);
 
         slice = newSingleSlice(tableMetadata, 1, Bound.END, true, value1);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(eq).mergeWith(slice);
+        restrictions =  restrictions(tableMetadata, eq, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -393,8 +373,7 @@ public class ClusteringColumnRestrictionsTest
         assertEndBound(get(bounds, 0), true, value3, value1);
 
         slice = newSingleSlice(tableMetadata, 1, Bound.END, false, value1);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(eq).mergeWith(slice);
+        restrictions =  restrictions(tableMetadata, eq, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -406,8 +385,7 @@ public class ClusteringColumnRestrictionsTest
 
         slice = newSingleSlice(tableMetadata, 1, Bound.START, false, value1);
         Restriction slice2 = newSingleSlice(tableMetadata, 1, Bound.END, false, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(eq).mergeWith(slice).mergeWith(slice2);
+        restrictions =  restrictions(tableMetadata, eq, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -419,8 +397,7 @@ public class ClusteringColumnRestrictionsTest
 
         slice = newSingleSlice(tableMetadata, 1, Bound.START, true, value1);
         slice2 = newSingleSlice(tableMetadata, 1, Bound.END, true, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(eq).mergeWith(slice).mergeWith(slice2);
+        restrictions =  restrictions(tableMetadata, eq, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -442,8 +419,7 @@ public class ClusteringColumnRestrictionsTest
         ByteBuffer value1 = ByteBufferUtil.bytes(1);
         ByteBuffer value2 = ByteBufferUtil.bytes(2);
         Restriction eq = newMultiEq(tableMetadata, 0, value1, value2);
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(eq);
+        ClusteringColumnRestrictions restrictions = restrictions(tableMetadata, eq);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -466,8 +442,7 @@ public class ClusteringColumnRestrictionsTest
         ByteBuffer value2 = ByteBufferUtil.bytes(2);
         ByteBuffer value3 = ByteBufferUtil.bytes(3);
         Restriction in = newMultiIN(tableMetadata, 0, asList(value1, value2), asList(value2, value3));
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(in);
+        ClusteringColumnRestrictions restrictions = restrictions(tableMetadata, in);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(2, bounds.size());
@@ -493,8 +468,7 @@ public class ClusteringColumnRestrictionsTest
         ByteBuffer value2 = ByteBufferUtil.bytes(2);
 
         Restriction slice = newMultiSlice(tableMetadata, 0, Bound.START, false, value1);
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        ClusteringColumnRestrictions restrictions = restrictions(tableMetadata, slice);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -505,8 +479,7 @@ public class ClusteringColumnRestrictionsTest
         assertEmptyEnd(get(bounds, 0));
 
         slice = newMultiSlice(tableMetadata, 0, Bound.START, true, value1);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -517,8 +490,7 @@ public class ClusteringColumnRestrictionsTest
         assertEmptyEnd(get(bounds, 0));
 
         slice = newMultiSlice(tableMetadata, 0, Bound.END, true, value1);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -529,8 +501,7 @@ public class ClusteringColumnRestrictionsTest
         assertEndBound(get(bounds, 0), true, value1);
 
         slice = newMultiSlice(tableMetadata, 0, Bound.END, false, value1);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -542,8 +513,7 @@ public class ClusteringColumnRestrictionsTest
 
         slice = newMultiSlice(tableMetadata, 0, Bound.START, false, value1);
         Restriction slice2 = newMultiSlice(tableMetadata, 0, Bound.END, false, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+        restrictions = restrictions(tableMetadata, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -555,8 +525,7 @@ public class ClusteringColumnRestrictionsTest
 
         slice = newMultiSlice(tableMetadata, 0, Bound.START, true, value1);
         slice2 = newMultiSlice(tableMetadata, 0, Bound.END, true, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+        restrictions = restrictions(tableMetadata, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -580,8 +549,7 @@ public class ClusteringColumnRestrictionsTest
         ByteBuffer value2 = ByteBufferUtil.bytes(2);
 
         Restriction slice = newMultiSlice(tableMetadata, 0, Bound.START, false, value1);
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        ClusteringColumnRestrictions restrictions =  restrictions(tableMetadata, slice);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -592,8 +560,7 @@ public class ClusteringColumnRestrictionsTest
         assertEndBound(get(bounds, 0), false, value1);
 
         slice = newMultiSlice(tableMetadata, 0, Bound.START, true, value1);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -604,8 +571,7 @@ public class ClusteringColumnRestrictionsTest
         assertEndBound(get(bounds, 0), true, value1);
 
         slice = newMultiSlice(tableMetadata, 0, Bound.END, true, value1);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -616,8 +582,7 @@ public class ClusteringColumnRestrictionsTest
         assertEmptyEnd(get(bounds, 0));
 
         slice = newMultiSlice(tableMetadata, 0, Bound.END, false, value1);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -629,8 +594,7 @@ public class ClusteringColumnRestrictionsTest
 
         slice = newMultiSlice(tableMetadata, 0, Bound.START, false, value1);
         Restriction slice2 = newMultiSlice(tableMetadata, 0, Bound.END, false, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+        restrictions = restrictions(tableMetadata, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -642,8 +606,7 @@ public class ClusteringColumnRestrictionsTest
 
         slice = newMultiSlice(tableMetadata, 0, Bound.START, true, value1);
         slice2 = newMultiSlice(tableMetadata, 0, Bound.END, true, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+        restrictions = restrictions(tableMetadata, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -667,8 +630,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1) > (1, 2)
         Restriction slice = newMultiSlice(tableMetadata, 0, Bound.START, false, value1, value2);
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        ClusteringColumnRestrictions restrictions =  restrictions(tableMetadata, slice);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -680,8 +642,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1) >= (1, 2)
         slice = newMultiSlice(tableMetadata, 0, Bound.START, true, value1, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -693,8 +654,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1) <= (1, 2)
         slice = newMultiSlice(tableMetadata, 0, Bound.END, true, value1, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -706,8 +666,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1) < (1, 2)
         slice = newMultiSlice(tableMetadata, 0, Bound.END, false, value1, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -720,8 +679,7 @@ public class ClusteringColumnRestrictionsTest
         // (clustering_0, clustering1) > (1, 2) AND (clustering_0) < (2)
         slice = newMultiSlice(tableMetadata, 0, Bound.START, false, value1, value2);
         Restriction slice2 = newMultiSlice(tableMetadata, 0, Bound.END, false, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+        restrictions = restrictions(tableMetadata, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -734,8 +692,7 @@ public class ClusteringColumnRestrictionsTest
         // (clustering_0, clustering1) >= (1, 2) AND (clustering_0, clustering1) <= (2, 1)
         slice = newMultiSlice(tableMetadata, 0, Bound.START, true, value1, value2);
         slice2 = newMultiSlice(tableMetadata, 0, Bound.END, true, value2, value1);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+        restrictions = restrictions(tableMetadata, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -759,8 +716,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1) > (1, 2)
         Restriction slice = newMultiSlice(tableMetadata, 0, Bound.START, false, value1, value2);
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        ClusteringColumnRestrictions restrictions =  restrictions(tableMetadata, slice);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -772,8 +728,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1) >= (1, 2)
         slice = newMultiSlice(tableMetadata, 0, Bound.START, true, value1, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -785,8 +740,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1) <= (1, 2)
         slice = newMultiSlice(tableMetadata, 0, Bound.END, true, value1, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -798,8 +752,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1) < (1, 2)
         slice = newMultiSlice(tableMetadata, 0, Bound.END, false, value1, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -813,8 +766,7 @@ public class ClusteringColumnRestrictionsTest
         // (clustering_0, clustering1) > (1, 2) AND (clustering_0) < (2)
         slice = newMultiSlice(tableMetadata, 0, Bound.START, false, value1, value2);
         Restriction slice2 = newMultiSlice(tableMetadata, 0, Bound.END, false, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+        restrictions = restrictions(tableMetadata, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -827,8 +779,7 @@ public class ClusteringColumnRestrictionsTest
         // (clustering_0, clustering1) >= (1, 2) AND (clustering_0, clustering1) <= (2, 1)
         slice = newMultiSlice(tableMetadata, 0, Bound.START, true, value1, value2);
         slice2 = newMultiSlice(tableMetadata, 0, Bound.END, true, value2, value1);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+        restrictions = restrictions(tableMetadata, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -853,8 +804,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1) > (1, 2)
         Restriction slice = newMultiSlice(tableMetadata, 0, Bound.START, false, value1, value2);
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        ClusteringColumnRestrictions restrictions =  restrictions(tableMetadata, slice);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(2, bounds.size());
@@ -868,8 +818,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1) >= (1, 2)
         slice = newMultiSlice(tableMetadata, 0, Bound.START, true, value1, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(2, bounds.size());
@@ -883,8 +832,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1) <= (1, 2)
         slice = newMultiSlice(tableMetadata, 0, Bound.END, true, value1, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(2, bounds.size());
@@ -898,8 +846,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1) < (1, 2)
         slice = newMultiSlice(tableMetadata, 0, Bound.END, false, value1, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(2, bounds.size());
@@ -914,8 +861,7 @@ public class ClusteringColumnRestrictionsTest
         // (clustering_0, clustering1) > (1, 2) AND (clustering_0) < (2)
         slice = newMultiSlice(tableMetadata, 0, Bound.START, false, value1, value2);
         Restriction slice2 = newMultiSlice(tableMetadata, 0, Bound.END, false, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+        restrictions = restrictions(tableMetadata, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(2, bounds.size());
@@ -930,8 +876,7 @@ public class ClusteringColumnRestrictionsTest
         // (clustering_0) > (1) AND (clustering_0, clustering1) < (2, 1)
         slice = newMultiSlice(tableMetadata, 0, Bound.START, false, value1);
         slice2 = newMultiSlice(tableMetadata, 0, Bound.END, false, value2, value1);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+        restrictions = restrictions(tableMetadata, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(2, bounds.size());
@@ -946,8 +891,7 @@ public class ClusteringColumnRestrictionsTest
         // (clustering_0, clustering1) >= (1, 2) AND (clustering_0, clustering1) <= (2, 1)
         slice = newMultiSlice(tableMetadata, 0, Bound.START, true, value1, value2);
         slice2 = newMultiSlice(tableMetadata, 0, Bound.END, true, value2, value1);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+        restrictions = restrictions(tableMetadata, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(3, bounds.size());
@@ -976,8 +920,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1) > (1, 2)
         Restriction slice = newMultiSlice(tableMetadata, 0, Bound.START, false, value1, value2);
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        ClusteringColumnRestrictions restrictions =  restrictions(tableMetadata, slice);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(2, bounds.size());
@@ -991,8 +934,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1) >= (1, 2)
         slice = newMultiSlice(tableMetadata, 0, Bound.START, true, value1, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(2, bounds.size());
@@ -1006,8 +948,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1) <= (1, 2)
         slice = newMultiSlice(tableMetadata, 0, Bound.END, true, value1, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(2, bounds.size());
@@ -1021,8 +962,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1) < (1, 2)
         slice = newMultiSlice(tableMetadata, 0, Bound.END, false, value1, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(2, bounds.size());
@@ -1037,8 +977,7 @@ public class ClusteringColumnRestrictionsTest
         // (clustering_0, clustering1) > (1, 2) AND (clustering_0) < (2)
         slice = newMultiSlice(tableMetadata, 0, Bound.START, false, value1, value2);
         Restriction slice2 = newMultiSlice(tableMetadata, 0, Bound.END, false, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+        restrictions = restrictions(tableMetadata, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(2, bounds.size());
@@ -1053,8 +992,7 @@ public class ClusteringColumnRestrictionsTest
         // (clustering_0, clustering1) >= (1, 2) AND (clustering_0, clustering1) <= (2, 1)
         slice = newMultiSlice(tableMetadata, 0, Bound.START, true, value1, value2);
         slice2 = newMultiSlice(tableMetadata, 0, Bound.END, true, value2, value1);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+        restrictions = restrictions(tableMetadata, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(3, bounds.size());
@@ -1085,8 +1023,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1, clustering_2, clustering_3) > (1, 2, 3, 4)
         Restriction slice = newMultiSlice(tableMetadata, 0, Bound.START, false, value1, value2, value3, value4);
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        ClusteringColumnRestrictions restrictions =  restrictions(tableMetadata, slice);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(2, bounds.size());
@@ -1101,9 +1038,7 @@ public class ClusteringColumnRestrictionsTest
         // clustering_0 = 1 AND (clustering_1, clustering_2, clustering_3) > (2, 3, 4)
         Restriction eq = newSingleEq(tableMetadata, 0, value1);
         slice = newMultiSlice(tableMetadata, 1, Bound.START, false, value2, value3, value4);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
-        restrictions = restrictions.mergeWith(eq);
+        restrictions = restrictions(tableMetadata, slice, eq);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(2, bounds.size());
@@ -1118,9 +1053,7 @@ public class ClusteringColumnRestrictionsTest
         // clustering_0 IN (1, 2) AND (clustering_1, clustering_2, clustering_3) > (2, 3, 4)
         Restriction in = newSingleIN(tableMetadata, 0, value1, value2);
         slice = newMultiSlice(tableMetadata, 1, Bound.START, false, value2, value3, value4);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
-        restrictions = restrictions.mergeWith(in);
+        restrictions = restrictions(tableMetadata, slice, in);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(4, bounds.size());
@@ -1138,8 +1071,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1) >= (1, 2)
         slice = newMultiSlice(tableMetadata, 0, Bound.START, true, value1, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -1151,8 +1083,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1, clustering_2, clustering_3) >= (1, 2, 3, 4)
         slice = newMultiSlice(tableMetadata, 0, Bound.START, true, value1, value2, value3, value4);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(2, bounds.size());
@@ -1166,8 +1097,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1, clustering_2, clustering_3) <= (1, 2, 3, 4)
         slice = newMultiSlice(tableMetadata, 0, Bound.END, true, value1, value2, value3, value4);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(2, bounds.size());
@@ -1181,8 +1111,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1, clustering_2, clustering_3) < (1, 2, 3, 4)
         slice = newMultiSlice(tableMetadata, 0, Bound.END, false, value1, value2, value3, value4);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(2, bounds.size());
@@ -1197,8 +1126,7 @@ public class ClusteringColumnRestrictionsTest
         // (clustering_0, clustering1, clustering_2, clustering_3) > (1, 2, 3, 4) AND (clustering_0, clustering_1) < (2, 3)
         slice = newMultiSlice(tableMetadata, 0, Bound.START, false, value1, value2, value3, value4);
         Restriction slice2 = newMultiSlice(tableMetadata, 0, Bound.END, false, value2, value3);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+        restrictions = restrictions(tableMetadata, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(2, bounds.size());
@@ -1213,8 +1141,7 @@ public class ClusteringColumnRestrictionsTest
         // (clustering_0, clustering1, clustering_2, clustering_3) >= (1, 2, 3, 4) AND (clustering_0, clustering1, clustering_2, clustering_3) <= (4, 3, 2, 1)
         slice = newMultiSlice(tableMetadata, 0, Bound.START, true, value1, value2, value3, value4);
         slice2 = newMultiSlice(tableMetadata, 0, Bound.END, true, value4, value3, value2, value1);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+        restrictions = restrictions(tableMetadata, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(3, bounds.size());
@@ -1245,8 +1172,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1, clustering_2, clustering_3) > (1, 2, 3, 4)
         Restriction slice = newMultiSlice(tableMetadata, 0, Bound.START, false, value1, value2, value3, value4);
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        ClusteringColumnRestrictions restrictions = restrictions(tableMetadata, slice);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(4, bounds.size());
@@ -1266,9 +1192,7 @@ public class ClusteringColumnRestrictionsTest
         // clustering_0 = 1 AND (clustering_1, clustering_2, clustering_3) > (2, 3, 4)
         Restriction eq = newSingleEq(tableMetadata, 0, value1);
         slice = newMultiSlice(tableMetadata, 1, Bound.START, false, value2, value3, value4);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
-        restrictions = restrictions.mergeWith(eq);
+        restrictions = restrictions(tableMetadata, slice, eq);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(3, bounds.size());
@@ -1284,8 +1208,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1) >= (1, 2)
         slice = newMultiSlice(tableMetadata, 0, Bound.START, true, value1, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(2, bounds.size());
@@ -1299,8 +1222,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1, clustering_2, clustering_3) >= (1, 2, 3, 4)
         slice = newMultiSlice(tableMetadata, 0, Bound.START, true, value1, value2, value3, value4);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(4, bounds.size());
@@ -1318,8 +1240,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1, clustering_2, clustering_3) <= (1, 2, 3, 4)
         slice = newMultiSlice(tableMetadata, 0, Bound.END, true, value1, value2, value3, value4);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(4, bounds.size());
@@ -1337,8 +1258,7 @@ public class ClusteringColumnRestrictionsTest
 
         // (clustering_0, clustering1, clustering_2, clustering_3) < (1, 2, 3, 4)
         slice = newMultiSlice(tableMetadata, 0, Bound.END, false, value1, value2, value3, value4);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice);
+        restrictions = restrictions(tableMetadata, slice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(4, bounds.size());
@@ -1357,8 +1277,7 @@ public class ClusteringColumnRestrictionsTest
         // (clustering_0, clustering1, clustering_2, clustering_3) > (1, 2, 3, 4) AND (clustering_0, clustering_1) < (2, 3)
         slice = newMultiSlice(tableMetadata, 0, Bound.START, false, value1, value2, value3, value4);
         Restriction slice2 = newMultiSlice(tableMetadata, 0, Bound.END, false, value2, value3);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+        restrictions = restrictions(tableMetadata, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(5, bounds.size());
@@ -1379,8 +1298,7 @@ public class ClusteringColumnRestrictionsTest
         // (clustering_0, clustering1, clustering_2, clustering_3) >= (1, 2, 3, 4) AND (clustering_0, clustering1, clustering_2, clustering_3) <= (4, 3, 2, 1)
         slice = newMultiSlice(tableMetadata, 0, Bound.START, true, value1, value2, value3, value4);
         slice2 = newMultiSlice(tableMetadata, 0, Bound.END, true, value4, value3, value2, value1);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(slice).mergeWith(slice2);
+        restrictions = restrictions(tableMetadata, slice, slice2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(7, bounds.size());
@@ -1419,8 +1337,7 @@ public class ClusteringColumnRestrictionsTest
         // clustering_0 = 1 AND (clustering_1, clustering_2) = (2, 3)
         Restriction singleEq = newSingleEq(tableMetadata, 0, value1);
         Restriction multiEq = newMultiEq(tableMetadata, 1, value2, value3);
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(singleEq).mergeWith(multiEq);
+        ClusteringColumnRestrictions restrictions = restrictions(tableMetadata, singleEq, multiEq);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -1434,8 +1351,7 @@ public class ClusteringColumnRestrictionsTest
         singleEq = newSingleEq(tableMetadata, 0, value1);
         Restriction singleEq2 = newSingleEq(tableMetadata, 1, value2);
         multiEq = newMultiEq(tableMetadata, 2, value3, value4);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(singleEq).mergeWith(singleEq2).mergeWith(multiEq);
+        restrictions = restrictions(tableMetadata, singleEq, singleEq2, multiEq);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -1448,8 +1364,7 @@ public class ClusteringColumnRestrictionsTest
         // (clustering_0, clustering_1) = (1, 2) AND clustering_2 = 3
         singleEq = newSingleEq(tableMetadata, 2, value3);
         multiEq = newMultiEq(tableMetadata, 0, value1, value2);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(singleEq).mergeWith(multiEq);
+        restrictions = restrictions(tableMetadata, singleEq, multiEq);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -1463,8 +1378,7 @@ public class ClusteringColumnRestrictionsTest
         singleEq = newSingleEq(tableMetadata, 0, value1);
         singleEq2 = newSingleEq(tableMetadata, 3, value4);
         multiEq = newMultiEq(tableMetadata, 1, value2, value3);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(singleEq).mergeWith(multiEq).mergeWith(singleEq2);
+        restrictions = restrictions(tableMetadata, singleEq, multiEq, singleEq2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -1492,8 +1406,7 @@ public class ClusteringColumnRestrictionsTest
         // clustering_0 = 1 AND (clustering_1, clustering_2) IN ((2, 3), (4, 5))
         Restriction singleEq = newSingleEq(tableMetadata, 0, value1);
         Restriction multiIN = newMultiIN(tableMetadata, 1, asList(value2, value3), asList(value4, value5));
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(singleEq).mergeWith(multiIN);
+        ClusteringColumnRestrictions restrictions = restrictions(tableMetadata, singleEq, multiIN);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(2, bounds.size());
@@ -1508,8 +1421,7 @@ public class ClusteringColumnRestrictionsTest
         // clustering_0 = 1 AND (clustering_1, clustering_2) IN ((2, 3))
         singleEq = newSingleEq(tableMetadata, 0, value1);
         multiIN = newMultiIN(tableMetadata, 1, asList(value2, value3));
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(multiIN).mergeWith(singleEq);
+        restrictions = restrictions(tableMetadata, multiIN, singleEq);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -1523,8 +1435,7 @@ public class ClusteringColumnRestrictionsTest
         singleEq = newSingleEq(tableMetadata, 0, value1);
         Restriction singleEq2 = newSingleEq(tableMetadata, 1, value5);
         multiIN = newMultiIN(tableMetadata, 2, asList(value2, value3), asList(value4, value5));
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(singleEq).mergeWith(multiIN).mergeWith(singleEq2);
+        restrictions = restrictions(tableMetadata, singleEq, multiIN, singleEq2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(2, bounds.size());
@@ -1555,8 +1466,7 @@ public class ClusteringColumnRestrictionsTest
         // clustering_0 = 1 AND (clustering_1, clustering_2) > (2, 3)
         Restriction singleEq = newSingleEq(tableMetadata, 0, value1);
         Restriction multiSlice = newMultiSlice(tableMetadata, 1, Bound.START, false, value2, value3);
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(singleEq).mergeWith(multiSlice);
+        ClusteringColumnRestrictions restrictions = restrictions(tableMetadata, singleEq, multiSlice);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -1570,8 +1480,7 @@ public class ClusteringColumnRestrictionsTest
         singleEq = newSingleEq(tableMetadata, 0, value1);
         multiSlice = newMultiSlice(tableMetadata, 1, Bound.START, false, value2, value3);
         Restriction multiSlice2 = newMultiSlice(tableMetadata, 1, Bound.END, false, value4);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(multiSlice2).mergeWith(singleEq).mergeWith(multiSlice);
+        restrictions = restrictions(tableMetadata, multiSlice2, singleEq, multiSlice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -1585,8 +1494,7 @@ public class ClusteringColumnRestrictionsTest
         singleEq = newSingleEq(tableMetadata, 0, value1);
         multiSlice = newMultiSlice(tableMetadata, 1, Bound.START, true, value2, value3);
         multiSlice2 = newMultiSlice(tableMetadata, 1, Bound.END, true, value4, value5);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(multiSlice2).mergeWith(singleEq).mergeWith(multiSlice);
+        restrictions = restrictions(tableMetadata, multiSlice2, singleEq, multiSlice);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -1613,8 +1521,7 @@ public class ClusteringColumnRestrictionsTest
         // (clustering_0, clustering_1) = (1, 2) AND clustering_2 > 3
         Restriction multiEq = newMultiEq(tableMetadata, 0, value1, value2);
         Restriction singleSlice = newSingleSlice(tableMetadata, 2, Bound.START, false, value3);
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(multiEq).mergeWith(singleSlice);
+        ClusteringColumnRestrictions restrictions = restrictions(tableMetadata, multiEq, singleSlice);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -1639,8 +1546,7 @@ public class ClusteringColumnRestrictionsTest
         // (clustering_0, clustering_1) = (1, 2) AND (clustering_2, clustering_3) > (3, 4)
         Restriction multiEq = newMultiEq(tableMetadata, 0, value1, value2);
         Restriction multiSlice = newMultiSlice(tableMetadata, 2, Bound.START, false, value3, value4);
-        ClusteringColumnRestrictions restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(multiEq).mergeWith(multiSlice);
+        ClusteringColumnRestrictions restrictions = restrictions(tableMetadata, multiEq, multiSlice);
 
         SortedSet<ClusteringBound<?>> bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -1653,8 +1559,7 @@ public class ClusteringColumnRestrictionsTest
         // (clustering_0, clustering_1) = (1, 2) AND (clustering_2, clustering_3) IN ((3, 4), (4, 5))
         multiEq = newMultiEq(tableMetadata, 0, value1, value2);
         Restriction multiIN = newMultiIN(tableMetadata, 2, asList(value3, value4), asList(value4, value5));
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(multiEq).mergeWith(multiIN);
+        restrictions = restrictions(tableMetadata, multiEq, multiIN);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(2, bounds.size());
@@ -1669,8 +1574,7 @@ public class ClusteringColumnRestrictionsTest
         // (clustering_0, clustering_1) = (1, 2) AND (clustering_2, clustering_3) = (3, 4)
         multiEq = newMultiEq(tableMetadata, 0, value1, value2);
         Restriction multiEq2 = newMultiEq(tableMetadata, 2, value3, value4);
-        restrictions = new ClusteringColumnRestrictions(tableMetadata);
-        restrictions = restrictions.mergeWith(multiEq).mergeWith(multiEq2);
+        restrictions = restrictions(tableMetadata, multiEq, multiEq2);
 
         bounds = restrictions.boundsAsClustering(Bound.START, QueryOptions.DEFAULT);
         assertEquals(1, bounds.size());
@@ -1733,11 +1637,10 @@ public class ClusteringColumnRestrictionsTest
         for (int i = 0, m = elements.length; i < m; i++)
         {
             ByteBuffer element = elements[i];
-            assertTrue(String.format("the element %s of the bound is not the expected one: expected %s but was %s",
-                                     i,
-                                     ByteBufferUtil.toInt(element),
-                                     ByteBufferUtil.toInt(bound.bufferAt(i))),
-                       bound.bufferAt(i).equals(element));
+            assertEquals(String.format("the element %s of the bound is not the expected one: expected %s but was %s",
+                                       i,
+                                       ByteBufferUtil.toInt(element),
+                                       ByteBufferUtil.toInt(bound.bufferAt(i))), bound.bufferAt(i), element);
         }
     }
 
@@ -1913,8 +1816,16 @@ public class ClusteringColumnRestrictionsTest
     {
         return Iterables.get(set, i);
     }
+    
+    private static ClusteringColumnRestrictions restrictions(TableMetadata table, Restriction... restrictions)
+    {
+        ClusteringColumnRestrictions clusteringColumnRestrictions = new ClusteringColumnRestrictions(table, false);
+        for (Restriction restriction : restrictions)
+            clusteringColumnRestrictions = clusteringColumnRestrictions.mergeWith(restriction, null);
+        return clusteringColumnRestrictions;
+    }
 
-    private static enum Sort
+    private enum Sort
     {
         ASC,
         DESC;
diff --git a/test/unit/org/apache/cassandra/index/CustomIndexTest.java b/test/unit/org/apache/cassandra/index/CustomIndexTest.java
index a296ea779f..5c5f89f644 100644
--- a/test/unit/org/apache/cassandra/index/CustomIndexTest.java
+++ b/test/unit/org/apache/cassandra/index/CustomIndexTest.java
@@ -37,6 +37,7 @@ import com.datastax.driver.core.exceptions.QueryValidationException;
 import org.apache.cassandra.Util;
 import org.apache.cassandra.cql3.CQLTester;
 import org.apache.cassandra.cql3.ColumnIdentifier;
+import org.apache.cassandra.cql3.Operator;
 import org.apache.cassandra.cql3.restrictions.IndexRestrictions;
 import org.apache.cassandra.cql3.restrictions.StatementRestrictions;
 import org.apache.cassandra.cql3.statements.schema.IndexTarget;
@@ -57,6 +58,7 @@ import org.apache.cassandra.io.sstable.Component;
 import org.apache.cassandra.io.sstable.Descriptor;
 import org.apache.cassandra.io.sstable.SSTableFlushObserver;
 import org.apache.cassandra.index.transactions.IndexTransaction;
+import org.apache.cassandra.schema.ColumnMetadata;
 import org.apache.cassandra.schema.IndexMetadata;
 import org.apache.cassandra.schema.Indexes;
 import org.apache.cassandra.schema.TableMetadata;
@@ -370,7 +372,7 @@ public class CustomIndexTest extends CQLTester
         assertInvalidMessage(String.format(IndexRestrictions.INDEX_NOT_FOUND, indexName, currentTableMetadata().toString()),
                              String.format("SELECT * FROM %%s WHERE expr(%s, 'foo bar baz')", indexName));
 
-        createIndex(String.format("CREATE CUSTOM INDEX %s ON %%s(c) USING '%s'", indexName, StubIndex.class.getName()));
+        createIndex(String.format("CREATE CUSTOM INDEX %s ON %%s(c) USING '%s'", indexName, ColumnTargetedIndex.class.getName()));
 
         assertInvalidThrowMessage(Optional.of(ProtocolVersion.CURRENT),
                                   String.format(IndexRestrictions.INDEX_NOT_FOUND, "no_such_index", currentTableMetadata().toString()),
@@ -390,7 +392,7 @@ public class CustomIndexTest extends CQLTester
                                                 indexName));
 
         // multiple expressions on different indexes
-        createIndex(String.format("CREATE CUSTOM INDEX other_custom_index ON %%s(d) USING '%s'", StubIndex.class.getName()));
+        createIndex(String.format("CREATE CUSTOM INDEX other_custom_index ON %%s(d) USING '%s'", ColumnTargetedIndex.class.getName()));
         assertInvalidThrowMessage(Optional.of(ProtocolVersion.CURRENT),
                                   IndexRestrictions.MULTIPLE_EXPRESSIONS,
                                   QueryValidationException.class,
@@ -404,6 +406,26 @@ public class CustomIndexTest extends CQLTester
         assertRows(execute(String.format("SELECT * FROM %%s WHERE expr(%s, 'foo') AND d=0 ALLOW FILTERING", indexName)), row);
     }
 
+    /**
+     * A {@link StubIndex} that only supports expressions on its target column.
+     */
+    public static final class ColumnTargetedIndex extends StubIndex
+    {
+        private final ColumnMetadata indexedColumn;
+
+        public ColumnTargetedIndex(ColumnFamilyStore baseCfs, IndexMetadata metadata)
+        {
+            super(baseCfs, metadata);
+            indexedColumn = TargetParser.parse(baseCfs.metadata(), metadata).left;
+        }
+
+        @Override
+        public boolean supportsExpression(ColumnMetadata column, Operator operator)
+        {
+            return column.equals(indexedColumn) && super.supportsExpression(column, operator);
+        }
+    }
+
     @Test
     public void customIndexDoesntSupportCustomExpressions() throws Throwable
     {
diff --git a/test/unit/org/apache/cassandra/index/sai/cql/AllowFilteringTest.java b/test/unit/org/apache/cassandra/index/sai/cql/AllowFilteringTest.java
new file mode 100644
index 0000000000..13b0f6148b
--- /dev/null
+++ b/test/unit/org/apache/cassandra/index/sai/cql/AllowFilteringTest.java
@@ -0,0 +1,314 @@
+/*
+ * 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.cassandra.index.sai.cql;
+
+import org.junit.Test;
+
+import org.apache.cassandra.cql3.restrictions.StatementRestrictions;
+import org.apache.cassandra.index.sai.SAITester;
+import org.apache.cassandra.index.sai.StorageAttachedIndex;
+
+import static java.lang.String.format;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * Tests that {@code ALLOW FILTERING} is required only if needed.
+ */
+public class AllowFilteringTest extends SAITester
+{
+    @Test
+    public void testAllowFilteringOnFirstClusteringKeyColumn() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k1 int, k2 int, c1 int, c2 int, c3 int, v1 int, " +
+                    "PRIMARY KEY ((k1, k2), c1, c2, c3))");
+        createIndex(format("CREATE CUSTOM INDEX ON %%s(c1) USING '%s'", StorageAttachedIndex.class.getName()));
+        waitForIndexQueryable();
+
+        // with only index restrictions
+        test("SELECT * FROM %s WHERE c1=0", false);
+        test("SELECT * FROM %s WHERE c1>0", false);
+        test("SELECT * FROM %s WHERE c1>0 AND c1<1", false);
+
+        // with additional simple filtering restrictions
+        test("SELECT * FROM %s WHERE c1=0 AND k1=0", true);
+        test("SELECT * FROM %s WHERE c1=0 AND k2=0", true);
+        test("SELECT * FROM %s WHERE c1=0 AND c2=0", true);
+        test("SELECT * FROM %s WHERE c1=0 AND c3=0", true);
+        test("SELECT * FROM %s WHERE c1=0 AND v1=0", true);
+
+        // with token restrictions
+        test("SELECT * FROM %s WHERE c1=0 AND token(k1, k2) = token(0, 0)", false);
+        test("SELECT * FROM %s WHERE c1=0 AND token(k1, k2) > token(0, 0)", false);
+        test("SELECT * FROM %s WHERE c1=0 AND token(k1, k2) > token(0, 0) AND token(k1, k2) <= token(1, 1)", false);
+
+        // with restriction on partition key
+        test("SELECT * FROM %s WHERE c1=0 AND k1=0 AND k2=0", false);
+        test("SELECT * FROM %s WHERE c1=0 AND k1=0 AND k2=0 AND v1=0", true);
+        test("SELECT * FROM %s WHERE c1=0 AND k1=0 AND k2=0 AND c3=0", true);
+
+        // with restriction on partition key and clustering key prefix
+        test("SELECT * FROM %s WHERE c1=0 AND k1=0 AND k2=0 AND c2=0", false);
+        test("SELECT * FROM %s WHERE c1=0 AND k1=0 AND k2=0 AND c2=0 AND v1=0", true);
+        test("SELECT * FROM %s WHERE c1=0 AND k1=0 AND k2=0 AND c2=0 AND c3>0", false);
+        test("SELECT * FROM %s WHERE c1=0 AND k1=0 AND k2=0 AND c2=0 AND c3>0 AND v1=0", true);
+
+        // with restriction on partition key and full clustering key
+        test("SELECT * FROM %s WHERE c1=0 AND k1=0 AND k2=0 AND c2=0 AND c3=0", false);
+        test("SELECT * FROM %s WHERE c1=0 AND k1=0 AND k2=0 AND c2=0 AND c3=0 AND v1=0", true);
+
+        // with restriction on partition key and full clustering key, multicolumn format
+        test("SELECT * FROM %s WHERE k1=0 AND k2=0 AND (c1, c2, c3) = (0, 0, 0)", false);
+        test("SELECT * FROM %s WHERE k1=0 AND k2=0 AND (c1, c2, c3) = (0, 0, 0) AND v1=0", true);
+    }
+
+    @Test
+    public void testAllowFilteringOnNotFirstClusteringKeyColumn() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k1 int, k2 int, c1 int, c2 int, c3 int, c4 int, v1 int, " +
+                    "PRIMARY KEY ((k1, k2), c1, c2, c3, c4))");
+        createIndex(format("CREATE CUSTOM INDEX ON %%s(c3) USING '%s'", StorageAttachedIndex.class.getName()));
+        waitForIndexQueryable();
+
+        // with only index restrictions
+        test("SELECT * FROM %s WHERE c3=0", false);
+        test("SELECT * FROM %s WHERE c3>0", false);
+        test("SELECT * FROM %s WHERE c3>0 AND c3<1", false);
+
+        // with additional simple filtering restrictions
+        test("SELECT * FROM %s WHERE c3=0 AND k1=0", true);
+        test("SELECT * FROM %s WHERE c3=0 AND k2=0", true);
+        test("SELECT * FROM %s WHERE c3=0 AND c1=0", true);
+        test("SELECT * FROM %s WHERE c3=0 AND c2=0", true);
+        test("SELECT * FROM %s WHERE c3=0 AND c4=0", true);
+        test("SELECT * FROM %s WHERE c3=0 AND v1=0", true);
+
+        // with token restrictions
+        test("SELECT * FROM %s WHERE c3=0 AND token(k1, k2) = token(0, 0)", false);
+        test("SELECT * FROM %s WHERE c3=0 AND token(k1, k2) > token(0, 0)", false);
+        test("SELECT * FROM %s WHERE c3=0 AND token(k1, k2) > token(0, 0) AND token(k1, k2) <= token(1, 1)", false);
+
+        // with restriction on partition key
+        test("SELECT * FROM %s WHERE c3=0 AND k1=0 AND k2=0", false);
+        test("SELECT * FROM %s WHERE c3=0 AND k1=0 AND k2=0 AND v1=0", true);
+        test("SELECT * FROM %s WHERE c3=0 AND k1=0 AND k2=0 AND c2=0", true);
+        test("SELECT * FROM %s WHERE c3=0 AND k1=0 AND k2=0 AND c4=0", true);
+
+        // with restriction on partition key and clustering key prefix
+        test("SELECT * FROM %s WHERE c3=0 AND k1=0 AND k2=0 AND c1=0", true);
+        test("SELECT * FROM %s WHERE c3=0 AND k1=0 AND k2=0 AND c1=0 AND v1=0", true);
+
+        // with restriction on partition key and full clustering key
+        test("SELECT * FROM %s WHERE c3=0 AND k1=0 AND k2=0 AND c1=0 AND c2=0 AND c4=0", false);
+        test("SELECT * FROM %s WHERE c3=0 AND k1=0 AND k2=0 AND c1=0 AND c2=0 AND c4=0 AND v1=0", true);
+
+        // with restriction on partition key and full clustering key, multicolumn format
+        test("SELECT * FROM %s WHERE k1=0 AND k2=0 AND (c1, c2, c3, c4) = (0, 0, 0, 0)", false);
+        test("SELECT * FROM %s WHERE k1=0 AND k2=0 AND (c1, c2, c3, c4) = (0, 0, 0, 0) AND v1=0", true);
+    }
+
+    @Test
+    public void testAllowFilteringOnMultipleClusteringKeyColumns() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k1 int, k2 int, c1 int, c2 int, c3 int, c4 int, v1 int, " +
+                    "PRIMARY KEY ((k1, k2), c1, c2, c3, c4))");
+        createIndex(format("CREATE CUSTOM INDEX ON %%s(c2) USING '%s'", StorageAttachedIndex.class.getName()));
+        createIndex(format("CREATE CUSTOM INDEX ON %%s(c4) USING '%s'", StorageAttachedIndex.class.getName()));
+        waitForIndexQueryable();
+
+        // with only index restrictions
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0", false);
+        test("SELECT * FROM %s WHERE c2=0 AND c4>0", false);
+        test("SELECT * FROM %s WHERE c2=0 AND c4>0 AND c4<1", false);
+        test("SELECT * FROM %s WHERE c2>0 AND c4=0", false);
+        test("SELECT * FROM %s WHERE c2>0 AND c2<1 AND c4=0", false);
+        test("SELECT * FROM %s WHERE c2>0 AND c4>0", false);
+        test("SELECT * FROM %s WHERE c2>0 AND c2<1 AND c4>0 AND c4<1", false);
+
+        // with additional simple filtering restrictions
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND k1=0", true);
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND k2=0", true);
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND c1=0", true);
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND c3=0", true);
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND v1=0", true);
+
+        // with token restrictions
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND token(k1, k2) = token(0, 0)", false);
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND token(k1, k2) > token(0, 0)", false);
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND token(k1, k2) > token(0, 0) AND token(k1, k2) <= token(1, 1)", false);
+
+        // with restriction on partition key
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND k1=0 AND k2=0", false);
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND k1=0 AND k2=0 AND c3=0", true);
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND k1=0 AND k2=0 AND v1=0", true);
+
+        // with restriction on partition key and clustering key prefix
+        test("SELECT * FROM %s WHERE k1=2 AND k2=3 AND c1=4 AND c2=0 AND c4=1", true);
+        test("SELECT * FROM %s WHERE k1=0 AND k2=0 AND c1=0 AND c2=0 AND c4=0 AND v1=0", true);
+
+        // with restriction on partition key and full clustering key
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND k1=0 AND k2=0 AND c1=0 AND c3=0", false);
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND k1=0 AND k2=0 AND c1=0 AND c3=0 AND v1=0", true);
+
+        // with restriction on partition key and full clustering key, multicolumn format
+        test("SELECT * FROM %s WHERE k1=0 AND k2=0 AND (c1, c2, c3, c4) = (0, 0, 0, 0)", false);
+        test("SELECT * FROM %s WHERE k1=0 AND k2=0 AND (c1, c2, c3, c4) = (0, 0, 0, 0) AND v1=0", true);
+    }
+
+    @Test
+    public void testAllowFilteringOnSingleRegularColumn() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k1 int, k2 int, c1 int, c2 int, v1 int, v2 int, PRIMARY KEY ((k1, k2), c1, c2))");
+        createIndex(format("CREATE CUSTOM INDEX ON %%s(v1) USING '%s'", StorageAttachedIndex.class.getName()));
+        waitForIndexQueryable();
+
+        // with only index restrictions
+        test("SELECT * FROM %s WHERE v1=0", false);
+        test("SELECT * FROM %s WHERE v1>0", false);
+        test("SELECT * FROM %s WHERE v1>0 AND v1<1", false);
+
+        // with additional simple filtering restrictions
+        test("SELECT * FROM %s WHERE v1=0 AND k1=0", true);
+        test("SELECT * FROM %s WHERE v1=0 AND k2=0", true);
+        test("SELECT * FROM %s WHERE v1=0 AND c1=0", true);
+        test("SELECT * FROM %s WHERE v1=0 AND c2=0", true);
+        test("SELECT * FROM %s WHERE v1=0 AND v2=0", true);
+
+        // with token restrictions
+        test("SELECT * FROM %s WHERE v1=0 AND token(k1, k2) = token(0, 0)", false);
+        test("SELECT * FROM %s WHERE v1=0 AND token(k1, k2) > token(0, 0)", false);
+        test("SELECT * FROM %s WHERE v1=0 AND token(k1, k2) > token(0, 0) AND token(k1, k2) <= token(1, 1)", false);
+
+        // with restriction on partition key
+        test("SELECT * FROM %s WHERE v1=0 AND k1=0 AND k2=0", false);
+        test("SELECT * FROM %s WHERE v1=0 AND k1=0 AND k2=0 AND c2=0", true);
+        test("SELECT * FROM %s WHERE v1=0 AND k1=0 AND k2=0 AND v2=0", true);
+
+        // with restriction on partition key and clustering key prefix
+        test("SELECT * FROM %s WHERE v1=0 AND k1=0 AND k2=0 AND c1=0", false);
+        test("SELECT * FROM %s WHERE v1=0 AND k1=0 AND k2=0 AND c1=0 AND v2=0", true);
+
+        // with restriction on partition key and full clustering key
+        test("SELECT * FROM %s WHERE v1=0 AND k1=0 AND k2=0 AND c1=0 AND c2=0", false);
+        test("SELECT * FROM %s WHERE v1=0 AND k1=0 AND k2=0 AND c1=0 AND c2=0 AND v2=0", true);
+
+        // with restriction on partition key and full clustering key, multicolumn format
+        test("SELECT * FROM %s WHERE v1=0 AND k1=0 AND k2=0 AND (c1, c2) = (0, 0)", false);
+        test("SELECT * FROM %s WHERE v1=0 AND k1=0 AND k2=0 AND (c1, c2) = (0, 0) AND v2=0", true);
+    }
+
+    @Test
+    public void testAllowFilteringOnMultipleRegularColumns() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k1 int, k2 int, c1 int, c2 int, v1 int, v2 int, v3 int, " +
+                    "PRIMARY KEY ((k1, k2), c1, c2))");
+        createIndex(format("CREATE CUSTOM INDEX ON %%s(v1) USING '%s'", StorageAttachedIndex.class.getName()));
+        createIndex(format("CREATE CUSTOM INDEX ON %%s(v2) USING '%s'", StorageAttachedIndex.class.getName()));
+        waitForIndexQueryable();
+
+        // with only index restrictions
+        test("SELECT * FROM %s WHERE v1=0 AND v2=0", false);
+        test("SELECT * FROM %s WHERE v1>0 AND v2=0", false);
+        test("SELECT * FROM %s WHERE v1>0 AND v1<1 AND v2=0", false);
+        test("SELECT * FROM %s WHERE v1=0 AND v2>0", false);
+        test("SELECT * FROM %s WHERE v1=0 AND v2>0 AND v2<1", false);
+        test("SELECT * FROM %s WHERE v1>0 AND v1<1 AND v2>0 AND v2<1", false);
+
+        // with additional simple filtering restrictions
+        test("SELECT * FROM %s WHERE v1=0 AND v2=0 AND k1=0", true);
+        test("SELECT * FROM %s WHERE v1=0 AND v2=0 AND k2=0", true);
+        test("SELECT * FROM %s WHERE v1=0 AND v2=0 AND c1=0", true);
+        test("SELECT * FROM %s WHERE v1=0 AND v2=0 AND c2=0", true);
+        test("SELECT * FROM %s WHERE v1=0 AND v2=0 AND v3=0", true);
+
+        // with token restrictions
+        test("SELECT * FROM %s WHERE v1=0 AND v2=0 AND token(k1, k2) = token(0, 0)", false);
+        test("SELECT * FROM %s WHERE v1=0 AND v2=0 AND token(k1, k2) > token(0, 0)", false);
+        test("SELECT * FROM %s WHERE v1=0 AND v2=0 AND token(k1, k2) > token(0, 0) AND token(k1, k2) <= token(1, 1)", false);
+
+        // with restriction on partition key
+        test("SELECT * FROM %s WHERE v1=0 AND v2=0 AND k1=0 AND k2=0", false);
+        test("SELECT * FROM %s WHERE v1=0 AND v2=0 AND k1=0 AND k2=0 AND c2=0", true);
+        test("SELECT * FROM %s WHERE v1=0 AND v2=0 AND k1=0 AND k2=0 AND v3=0", true);
+
+        // with restriction on partition key and clustering key prefix
+        test("SELECT * FROM %s WHERE v1=0 AND v2=0 AND k1=0 AND k2=0 AND c1=0", false);
+        test("SELECT * FROM %s WHERE v1=0 AND v2=0 AND k1=0 AND k2=0 AND c1=0 AND v3=0", true);
+
+        // with restriction on partition key and full clustering key
+        test("SELECT * FROM %s WHERE v1=0 AND v2=0 AND k1=0 AND k2=0 AND c1=0 AND c2=0", false);
+        test("SELECT * FROM %s WHERE v1=0 AND v2=0 AND k1=0 AND k2=0 AND c1=0 AND c2=0 AND v3=0", true);
+
+        // with restriction on partition key and full clustering key, multicolumn format
+        test("SELECT * FROM %s WHERE v1=0 AND v2=0 AND k1=0 AND k2=0 AND (c1, c2) = (0, 0)", false);
+        test("SELECT * FROM %s WHERE v1=0 AND v2=0 AND k1=0 AND k2=0 AND (c1, c2) = (0, 0) AND v3=0", true);
+    }
+
+    @Test
+    public void testAllowFilteringOnClusteringAndRegularColumns() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k1 int, k2 int, c1 int, c2 int, c3 int, c4 int, v1 int, v2 int, v3 int, " +
+                    "PRIMARY KEY ((k1, k2), c1, c2, c3, c4))");
+        createIndex(format("CREATE CUSTOM INDEX ON %%s(c2) USING '%s'", StorageAttachedIndex.class.getName()));
+        createIndex(format("CREATE CUSTOM INDEX ON %%s(c4) USING '%s'", StorageAttachedIndex.class.getName()));
+        createIndex(format("CREATE CUSTOM INDEX ON %%s(v1) USING '%s'", StorageAttachedIndex.class.getName()));
+        createIndex(format("CREATE CUSTOM INDEX ON %%s(v2) USING '%s'", StorageAttachedIndex.class.getName()));
+        waitForIndexQueryable();
+
+        // with only index restrictions
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND v1=0 AND v2=0", false);
+        test("SELECT * FROM %s WHERE c2>0 AND c4>0 AND v1>0 AND v2>0", false);
+        test("SELECT * FROM %s WHERE c2>0 AND c2<1 AND c4>0 AND c4<1 AND v1>0 AND v1<0 AND v2>0 AND v2<1", false);
+
+        // with additional simple filtering restrictions
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND v1=0 AND v2=0 AND k1=0", true);
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND v1=0 AND v2=0 AND k2=0", true);
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND v1=0 AND v2=0 AND c3=0", true);
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND v1=0 AND v2=0 AND v3=0", true);
+
+        // with token restrictions
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND v1=0 AND v2=0 AND token(k1, k2) = token(0, 0)", false);
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND v1=0 AND v2=0 AND token(k1, k2) > token(0, 0)", false);
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND v1=0 AND v2=0 AND token(k1, k2) > token(0, 0) AND token(k1, k2) <= token(1, 1)", false);
+
+        // with restriction on partition key
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND v1=0 AND v2=0 AND k1=0 AND k2=0", false);
+
+        // with restriction on partition key and clustering key prefix
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND v1=0 AND v2=0 AND k1=0 AND k2=0 AND c1=0", true);
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND v1=0 AND v2=0 AND k1=0 AND k2=0 AND c1=0 AND v3=0", true);
+
+        // with restriction on partition key and full clustering key
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND v1=0 AND v2=0 AND k1=0 AND k2=0 AND c1=0 AND c3=0", false);
+        test("SELECT * FROM %s WHERE c2=0 AND c4=0 AND v1=0 AND v2=0 AND k1=0 AND k2=0 AND c1=0 AND c3=0 AND v3=0", true);
+
+        // with restriction on partition key and full clustering key, multicolumn format
+        test("SELECT * FROM %s WHERE v1=0 AND v2=0 AND k1=0 AND k2=0 AND (c1, c2, c3, c4) = (0, 0, 0, 0)", false);
+        test("SELECT * FROM %s WHERE v1=0 AND v2=0 AND k1=0 AND k2=0 AND (c1, c2, c3, c4) = (0, 0, 0, 0) AND v3=0", true);
+    }
+
+    private void test(String query, boolean requiresAllowFiltering) throws Throwable
+    {
+        if (requiresAllowFiltering)
+            assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE, query);
+        else
+            assertNotNull(execute(query));
+
+        assertNotNull(execute(query + " ALLOW FILTERING"));
+    }
+}
diff --git a/test/unit/org/apache/cassandra/index/sai/cql/BaseDataModel.java b/test/unit/org/apache/cassandra/index/sai/cql/BaseDataModel.java
index 2772dbc0d0..67621c3b9a 100644
--- a/test/unit/org/apache/cassandra/index/sai/cql/BaseDataModel.java
+++ b/test/unit/org/apache/cassandra/index/sai/cql/BaseDataModel.java
@@ -40,10 +40,9 @@ class BaseDataModel
     public static final String KEYSPACE = "sai_query_keyspace";
 
     public static final String SIMPLE_SELECT_TEMPLATE = "SELECT %s FROM %%s WHERE %s %s ? LIMIT ?";
-    public static final String SIMPLE_SELECT_WITH_FILTERING_TEMPLATE = "SELECT %s FROM %%s WHERE %s %s ? LIMIT ? ALLOW FILTERING";
-    public static final String TWO_CLAUSE_AND_QUERY_TEMPLATE = "SELECT %s FROM %%s WHERE %s %s ? AND %s %s ? LIMIT ? ALLOW FILTERING";
-    public static final String TWO_CLAUSE_AND_QUERY_FILTERING_TEMPLATE = "SELECT %s FROM %%s WHERE %s %s ? AND %s %s ? LIMIT ? ALLOW FILTERING";
-    public static final String THREE_CLAUSE_AND_QUERY_FILTERING_TEMPLATE = "SELECT %s FROM %%s WHERE %s %s ? AND %s %s ? AND %s %s ? LIMIT ? ALLOW FILTERING";
+    public static final String RANGE_QUERY_TEMPLATE = "SELECT %s FROM %%s WHERE %s > ? AND %<s < ? LIMIT ?";
+    public static final String TWO_CLAUSE_AND_QUERY_TEMPLATE = "SELECT %s FROM %%s WHERE %s %s ? AND %s %s ? LIMIT ?";
+    public static final String THREE_CLAUSE_AND_QUERY_TEMPLATE = "SELECT %s FROM %%s WHERE %s %s ? AND %s %s ? AND %s %s ? LIMIT ?";
 
     public static final String ASCII_COLUMN = "abbreviation";
     public static final String BIGINT_COLUMN = "gdp";
@@ -163,9 +162,9 @@ class BaseDataModel
         String keyColumnDefs = keyColumns.stream().map(column -> column.left + ' ' + column.right).collect(Collectors.joining(", "));
         String normalColumnDefs = columns.stream().map(column -> column.left + ' ' + column.right).collect(Collectors.joining(", "));
 
-        String template = "CREATE TABLE %s (%s, %s, PRIMARY KEY (%s))" + tableOptions;
-        tester.createTable(String.format(template, KEYSPACE + '.' + indexedTable, keyColumnDefs, normalColumnDefs, primaryKey));
-        tester.createTable(String.format(template, KEYSPACE + '.' + nonIndexedTable, keyColumnDefs, normalColumnDefs, primaryKey));
+        String template = "CREATE TABLE %s.%s (%s, %s, PRIMARY KEY (%s))" + tableOptions;
+        tester.createTable(String.format(template, KEYSPACE, indexedTable, keyColumnDefs, normalColumnDefs, primaryKey));
+        tester.createTable(String.format(template, KEYSPACE, nonIndexedTable, keyColumnDefs, normalColumnDefs, primaryKey));
     }
 
     public void createIndexes(Executor tester) throws Throwable
diff --git a/test/unit/org/apache/cassandra/index/sai/cql/CollectionIndexingTest.java b/test/unit/org/apache/cassandra/index/sai/cql/CollectionIndexingTest.java
index be09c193e3..a3906ab6dc 100644
--- a/test/unit/org/apache/cassandra/index/sai/cql/CollectionIndexingTest.java
+++ b/test/unit/org/apache/cassandra/index/sai/cql/CollectionIndexingTest.java
@@ -23,6 +23,7 @@ import java.util.HashMap;
 import org.junit.Before;
 import org.junit.Test;
 
+import org.apache.cassandra.cql3.restrictions.StatementRestrictions;
 import org.apache.cassandra.index.sai.SAITester;
 
 import static org.junit.Assert.assertEquals;
@@ -32,10 +33,6 @@ import static org.junit.Assert.assertEquals;
 // is in the cql/types/collections package
 public class CollectionIndexingTest extends SAITester
 {
-    private static final String ALLOW_FILTERING_ERROR = "Cannot execute this query as it might involve data filtering " +
-                                                        "and thus may have unpredictable performance. If you want to " +
-                                                        "execute this query despite the performance unpredictability, " +
-                                                        "use ALLOW FILTERING";
     @Before
     public void setup()
     {
@@ -68,7 +65,7 @@ public class CollectionIndexingTest extends SAITester
     {
         createPopulatedMap("CREATE CUSTOM INDEX ON %s(ENTRIES(value)) USING 'StorageAttachedIndex'");
         assertEquals(2, execute("SELECT * FROM %s WHERE value[1] = 'v1'").size());
-        assertEquals(1, execute("SELECT * FROM %s WHERE value[1] = 'v1' AND value[2] = 'v2' ALLOW FILTERING").size());
+        assertEquals(1, execute("SELECT * FROM %s WHERE value[1] = 'v1' AND value[2] = 'v2'").size());
     }
 
     @Test
@@ -93,16 +90,14 @@ public class CollectionIndexingTest extends SAITester
     public void indexFrozenMapQueryKeys() throws Throwable
     {
         createPopulatedFrozenMap("CREATE CUSTOM INDEX ON %s(FULL(value)) USING 'StorageAttachedIndex'");
-        assertUnsupportedIndexOperator("SELECT * FROM %s WHERE value contains key 1");
-        assertEquals(2, execute("SELECT * FROM %s WHERE value contains key 1 ALLOW FILTERING").size());
+        assertUnsupportedIndexOperator(2, "SELECT * FROM %s WHERE value contains key 1");
     }
 
     @Test
     public void indexFrozenMapQueryValues() throws Throwable
     {
         createPopulatedFrozenMap("CREATE CUSTOM INDEX ON %s(FULL(value)) USING 'StorageAttachedIndex'");
-        assertUnsupportedIndexOperator("SELECT * FROM %s WHERE value contains 'v1'");
-        assertEquals(2, execute("SELECT * FROM %s WHERE value contains 'v1' ALLOW FILTERING").size());
+        assertUnsupportedIndexOperator(2, "SELECT * FROM %s WHERE value contains 'v1'");
     }
 
     @Test
@@ -125,16 +120,14 @@ public class CollectionIndexingTest extends SAITester
     public void indexMapEntriesQueryKeys() throws Throwable
     {
         createPopulatedMap("CREATE CUSTOM INDEX ON %s(ENTRIES(value)) USING 'StorageAttachedIndex'");
-        assertUnsupportedIndexOperator("SELECT * FROM %s WHERE value contains key 1");
-        assertEquals(2, execute("SELECT * FROM %s WHERE value contains key 1 ALLOW FILTERING").size());
+        assertUnsupportedIndexOperator(2, "SELECT * FROM %s WHERE value contains key 1");
     }
 
     @Test
     public void indexMapEntriesQueryValues() throws Throwable
     {
         createPopulatedMap("CREATE CUSTOM INDEX ON %s(ENTRIES(value)) USING 'StorageAttachedIndex'");
-        assertUnsupportedIndexOperator("SELECT * FROM %s WHERE value contains 'v1'");
-        assertEquals(2, execute("SELECT * FROM %s WHERE value contains 'v1' ALLOW FILTERING").size());
+        assertUnsupportedIndexOperator(2, "SELECT * FROM %s WHERE value contains 'v1'");
     }
 
     @Test
@@ -149,16 +142,14 @@ public class CollectionIndexingTest extends SAITester
     public void indexMapKeysQueryValues() throws Throwable
     {
         createPopulatedMap("CREATE CUSTOM INDEX ON %s(KEYS(value)) USING 'StorageAttachedIndex'");
-        assertUnsupportedIndexOperator("SELECT * FROM %s WHERE value contains 'v1'");
-        assertEquals(2, execute("SELECT * FROM %s WHERE value contains 'v1' ALLOW FILTERING").size());
+        assertUnsupportedIndexOperator(2, "SELECT * FROM %s WHERE value contains 'v1'");
     }
 
     @Test
     public void indexMapKeysQueryEntries() throws Throwable
     {
         createPopulatedMap("CREATE CUSTOM INDEX ON %s(KEYS(value)) USING 'StorageAttachedIndex'");
-        assertUnsupportedIndexOperator("SELECT * FROM %s WHERE value[1] = 'v1'");
-        assertEquals(2, execute("SELECT * FROM %s WHERE value[1] = 'v1' ALLOW FILTERING").size());
+        assertUnsupportedIndexOperator(2, "SELECT * FROM %s WHERE value[1] = 'v1'");
     }
 
     @Test
@@ -173,16 +164,14 @@ public class CollectionIndexingTest extends SAITester
     public void indexMapValuesQueryKeys() throws Throwable
     {
         createPopulatedMap("CREATE CUSTOM INDEX ON %s(VALUES(value)) USING 'StorageAttachedIndex'");
-        assertUnsupportedIndexOperator("SELECT * FROM %s WHERE value contains key 1");
-        assertEquals(2, execute("SELECT * FROM %s WHERE value contains key 1 ALLOW FILTERING").size());
+        assertUnsupportedIndexOperator(2, "SELECT * FROM %s WHERE value contains key 1");
     }
 
     @Test
     public void indexMapValuesQueryEntries() throws Throwable
     {
         createPopulatedMap("CREATE CUSTOM INDEX ON %s(VALUES(value)) USING 'StorageAttachedIndex'");
-        assertUnsupportedIndexOperator("SELECT * FROM %s WHERE value[1] = 'v1'");
-        assertEquals(2, execute("SELECT * FROM %s WHERE value[1] = 'v1' ALLOW FILTERING").size());
+        assertUnsupportedIndexOperator(2, "SELECT * FROM %s WHERE value[1] = 'v1'");
     }
 
     private void createPopulatedMap(String createIndex) throws Throwable
@@ -200,6 +189,7 @@ public class CollectionIndexingTest extends SAITester
         }});
     }
 
+    @SuppressWarnings("SameParameterValue")
     private void createPopulatedFrozenMap(String createIndex) throws Throwable
     {
         createTable("CREATE TABLE %s (pk int primary key, value frozen<map<int, text>>)");
@@ -215,6 +205,7 @@ public class CollectionIndexingTest extends SAITester
         }});
     }
 
+    @SuppressWarnings("SameParameterValue")
     private void createPopulatedFrozenList(String createIndex) throws Throwable
     {
         createTable("CREATE TABLE %s (pk int primary key, value frozen<list<int>>)");
@@ -226,8 +217,10 @@ public class CollectionIndexingTest extends SAITester
         execute("INSERT INTO %s (pk, value) VALUES (?, ?)", 4, Arrays.asList(1, 2, 7));
     }
 
-    private void assertUnsupportedIndexOperator(String query, Object... values) throws Throwable
+    @SuppressWarnings("SameParameterValue")
+    private void assertUnsupportedIndexOperator(int expectedSize, String query, Object... values) throws Throwable
     {
-        assertInvalidMessage(ALLOW_FILTERING_ERROR, query, values);
+        assertInvalidMessage(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE, query, values);
+        assertEquals(expectedSize, execute(query + " ALLOW FILTERING").size());
     }
 }
diff --git a/test/unit/org/apache/cassandra/index/sai/cql/CompositePartitionKeyIndexTest.java b/test/unit/org/apache/cassandra/index/sai/cql/CompositePartitionKeyIndexTest.java
index 6bf9891659..bfaabf5b9a 100644
--- a/test/unit/org/apache/cassandra/index/sai/cql/CompositePartitionKeyIndexTest.java
+++ b/test/unit/org/apache/cassandra/index/sai/cql/CompositePartitionKeyIndexTest.java
@@ -92,10 +92,10 @@ public class CompositePartitionKeyIndexTest extends SAITester
         assertRowsIgnoringOrder(execute("SELECT * FROM %s WHERE pk2 = '2'"),
                 expectedRow(2));
 
-        assertRowsIgnoringOrder(execute("SELECT * FROM %s WHERE pk1 > 1 AND pk2 = '2' ALLOW FILTERING"),
+        assertRowsIgnoringOrder(execute("SELECT * FROM %s WHERE pk1 > 1 AND pk2 = '2'"),
                 expectedRow(2));
 
-        assertRowsIgnoringOrder(execute("SELECT * FROM %s WHERE pk1 = -1 AND pk2 = '2' ALLOW FILTERING"));
+        assertRowsIgnoringOrder(execute("SELECT * FROM %s WHERE pk1 = -1 AND pk2 = '2'"));
 
         assertThatThrownBy(()->execute("SELECT * FROM %s WHERE pk1 = -1 AND val = 2"))
                 .hasMessageContaining("use ALLOW FILTERING");
diff --git a/test/unit/org/apache/cassandra/index/sai/cql/IndexQuerySupport.java b/test/unit/org/apache/cassandra/index/sai/cql/IndexQuerySupport.java
index 8305ac0e50..7573fc96d9 100644
--- a/test/unit/org/apache/cassandra/index/sai/cql/IndexQuerySupport.java
+++ b/test/unit/org/apache/cassandra/index/sai/cql/IndexQuerySupport.java
@@ -29,7 +29,11 @@ import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 
+import org.junit.Assert;
+
+import com.datastax.driver.core.exceptions.InvalidQueryException;
 import org.apache.cassandra.cql3.Operator;
+import org.apache.cassandra.cql3.restrictions.StatementRestrictions;
 import org.apache.cassandra.db.marshal.InetAddressType;
 import org.apache.cassandra.db.marshal.SimpleDateType;
 import org.apache.cassandra.db.marshal.TimeType;
@@ -37,6 +41,7 @@ import org.apache.cassandra.db.marshal.TimestampType;
 import org.apache.cassandra.db.marshal.UUIDType;
 import org.apache.cassandra.index.sai.plan.StorageAttachedIndexSearcher;
 import org.apache.cassandra.utils.Pair;
+import org.assertj.core.api.Assertions;
 import org.hamcrest.Matchers;
 
 import static org.junit.Assert.assertEquals;
@@ -356,17 +361,19 @@ public class IndexQuerySupport
             andQuery(tester, model,
                      BaseDataModel.TEXT_COLUMN, Operator.EQ, "Alaska",
                      firstPartitionKey, Operator.EQ, 0,
-                     true);
+                     false);
+
+            boolean hasSimplePartitionKey = !(model instanceof BaseDataModel.CompositePartitionKeyDataModel);
 
             andQuery(tester, model,
                      BaseDataModel.TEXT_COLUMN, Operator.EQ, "Kentucky",
                      firstPartitionKey, Operator.GT, 4,
-                     true);
+                     hasSimplePartitionKey);
 
             andQuery(tester, model,
                      BaseDataModel.TEXT_COLUMN, Operator.EQ, "Wyoming",
                      firstPartitionKey, Operator.LT, 200,
-                     true);
+                     hasSimplePartitionKey);
 
             if (model.keyColumns().size() > 1)
             {
@@ -375,40 +382,42 @@ public class IndexQuerySupport
                 andQuery(tester, model,
                          BaseDataModel.BIGINT_COLUMN, Operator.EQ, 4800000000L,
                          secondPrimaryKey, Operator.EQ, 0,
-                         true);
+                         hasSimplePartitionKey);
 
                 andQuery(tester, model,
                          BaseDataModel.DOUBLE_COLUMN, Operator.EQ, 82169.60,
                          secondPrimaryKey, Operator.GT, 0,
-                         true);
+                         hasSimplePartitionKey);
 
                 andQuery(tester, model,
                          BaseDataModel.DOUBLE_COLUMN, Operator.LT, 1948.54,
                          secondPrimaryKey, Operator.LTE, 2,
-                         true);
+                         hasSimplePartitionKey);
 
                 andQuery(tester, model,
                          BaseDataModel.TEXT_COLUMN, Operator.EQ, "Alaska",
                          firstPartitionKey, Operator.EQ, 0,
-                         secondPrimaryKey, Operator.GTE, -1);
+                         secondPrimaryKey, Operator.GTE, -1,
+                         false);
 
                 andQuery(tester, model,
                          BaseDataModel.TEXT_COLUMN, Operator.EQ, "Kentucky",
                          firstPartitionKey, Operator.GT, 4,
-                         secondPrimaryKey, Operator.LT, 50);
+                         secondPrimaryKey, Operator.LT, 50,
+                         hasSimplePartitionKey);
 
                 andQuery(tester, model,
                          BaseDataModel.TEXT_COLUMN, Operator.EQ, "Wyoming",
                          firstPartitionKey, Operator.LT, 200,
-                         secondPrimaryKey, Operator.GT, 0);
+                         secondPrimaryKey, Operator.GT, 0,
+                         hasSimplePartitionKey);
             }
         }
 
         void query(BaseDataModel.Executor tester, BaseDataModel model, String column, Operator operator, Object value)
         {
             String query = String.format(BaseDataModel.SIMPLE_SELECT_TEMPLATE, BaseDataModel.ASCII_COLUMN, column, operator);
-            String queryValidator = String.format(BaseDataModel.SIMPLE_SELECT_WITH_FILTERING_TEMPLATE, BaseDataModel.ASCII_COLUMN, column, operator);
-            validate(tester, model, query, queryValidator, value, limit);
+            validate(tester, model, query, false, value, limit);
         }
 
         void andQuery(BaseDataModel.Executor tester, BaseDataModel model,
@@ -416,55 +425,59 @@ public class IndexQuerySupport
                       String column2, Operator operator2, Object value2,
                       boolean filtering)
         {
-            String query = String.format(filtering ? BaseDataModel.TWO_CLAUSE_AND_QUERY_FILTERING_TEMPLATE : BaseDataModel.TWO_CLAUSE_AND_QUERY_TEMPLATE,
+            String query = String.format(BaseDataModel.TWO_CLAUSE_AND_QUERY_TEMPLATE,
                                          BaseDataModel.ASCII_COLUMN, column1, operator1, column2, operator2);
 
-            String queryValidator = String.format(BaseDataModel.TWO_CLAUSE_AND_QUERY_FILTERING_TEMPLATE,
-                                                  BaseDataModel.ASCII_COLUMN, column1, operator1, column2, operator2);
-
-            validate(tester, model,query, queryValidator, value1, value2, limit);
+            validate(tester, model, query, filtering, value1, value2, limit);
         }
 
         void andQuery(BaseDataModel.Executor tester, BaseDataModel model,
                       String column1, Operator operator1, Object value1,
                       String column2, Operator operator2, Object value2,
-                      String column3, Operator operator3, Object value3)
+                      String column3, Operator operator3, Object value3,
+                      boolean filtering)
         {
-            // TODO: If we support indexes in all columns, ALLOW FILTERING might go away here...
-            String query = String.format(BaseDataModel.THREE_CLAUSE_AND_QUERY_FILTERING_TEMPLATE,
+            String query = String.format(BaseDataModel.THREE_CLAUSE_AND_QUERY_TEMPLATE,
                                          BaseDataModel.ASCII_COLUMN, column1, operator1, column2, operator2, column3, operator3);
 
-            String queryValidator = String.format(BaseDataModel.THREE_CLAUSE_AND_QUERY_FILTERING_TEMPLATE,
-                                                  BaseDataModel.ASCII_COLUMN, column1, operator1, column2, operator2, column3, operator3);
-
-            validate(tester, model, query, queryValidator, value1, value2, value3, limit);
+            validate(tester, model, query, filtering, value1, value2, value3, limit);
         }
 
         void rangeQuery(BaseDataModel.Executor tester, BaseDataModel model, String column, Object value1, Object value2)
         {
-            String template = "SELECT %s FROM %%s WHERE %s > ? AND %s < ? LIMIT ? ALLOW FILTERING";
-            String templateWithFiltering = "SELECT %s FROM %%s WHERE %s > ? AND %s < ? LIMIT ? ALLOW FILTERING";
-
-            String query = String.format(template, BaseDataModel.ASCII_COLUMN, column, column);
-            String queryValidator = String.format(templateWithFiltering, BaseDataModel.ASCII_COLUMN, column, column);
-            validate(tester, model, query, queryValidator, value1, value2, limit);
+            String query = String.format(BaseDataModel.RANGE_QUERY_TEMPLATE, BaseDataModel.ASCII_COLUMN, column);
+            validate(tester, model, query, false, value1, value2, limit);
         }
 
-        private void validate(BaseDataModel.Executor tester, BaseDataModel model, String query, String validator, Object... values)
+        private void validate(BaseDataModel.Executor tester, BaseDataModel model, String query, boolean needsAllowFiltering, Object... values)
         {
             try
             {
                 tester.counterReset();
 
-                List<Object> actual = model.executeIndexed(tester, query, fetchSize, values);
+                // The non indexed query we use to validate the indexed query results is just the very same query but
+                // with ALLOW FILTERING appended. It might happen that the non indexed query also requires ALLOW
+                // FILTERING because it combines indexed and unindexed columns.
+                Assert.assertFalse(query.contains("ALLOW FILTERING"));
+                String validationQuery = query + " ALLOW FILTERING";
+                String indexedQuery = needsAllowFiltering ? validationQuery : query;
+
+                List<Object> actual = model.executeIndexed(tester, indexedQuery, fetchSize, values);
 
                 // This could be more strict, but it serves as a reasonable paging-aware lower bound:
                 int pageCount = (int) Math.ceil(actual.size() / (double) Math.min(actual.size(), fetchSize));
                 assertThat("Expected more calls to " + StorageAttachedIndexSearcher.class, tester.getCounter(), Matchers.greaterThanOrEqualTo((long) Math.max(1, pageCount)));
 
-                List<Object> expected = model.executeNonIndexed(tester, validator, fetchSize, values);
-
+                List<Object> expected = model.executeNonIndexed(tester, validationQuery, fetchSize, values);
                 assertEquals(expected, actual);
+
+                // verify that the query actually requires ALLOW FILTERING
+                if (needsAllowFiltering)
+                {
+                    Assertions.assertThatThrownBy(() -> model.executeIndexed(tester, query, fetchSize, values))
+                              .isInstanceOf(InvalidQueryException.class)
+                              .hasMessageContaining(StatementRestrictions.REQUIRES_ALLOW_FILTERING_MESSAGE);
+                }
             }
             catch (Throwable ex)
             {
diff --git a/test/unit/org/apache/cassandra/index/sai/cql/MixedIndexImplementationsTest.java b/test/unit/org/apache/cassandra/index/sai/cql/MixedIndexImplementationsTest.java
index 79bb7ac658..f705240d68 100644
--- a/test/unit/org/apache/cassandra/index/sai/cql/MixedIndexImplementationsTest.java
+++ b/test/unit/org/apache/cassandra/index/sai/cql/MixedIndexImplementationsTest.java
@@ -74,11 +74,11 @@ public class MixedIndexImplementationsTest extends SAITester
         assertRowsIgnoringOrder(execute(ossSelect, 0), new Object[][]{{0, 0, 0}, {1, 0, 1}});
         assertRowsIgnoringOrder(execute(ossSelect, 1), new Object[][]{{2, 1, 0}, {3, 1, 1}});
 
-        String ndiSelect = "SELECT * FROM %s WHERE v1 = ? AND v2 = ? ALLOW FILTERING";
-        assertRowsIgnoringOrder(execute(ndiSelect, 0, 0), new Object[]{0, 0, 0});
-        assertRowsIgnoringOrder(execute(ndiSelect, 0, 1), new Object[]{1, 0, 1});
-        assertRowsIgnoringOrder(execute(ndiSelect, 1, 0), new Object[]{2, 1, 0});
-        assertRowsIgnoringOrder(execute(ndiSelect, 1, 1), new Object[]{3, 1, 1});
+        String saiSelect = "SELECT * FROM %s WHERE v1 = ? AND v2 = ? ALLOW FILTERING";
+        assertRowsIgnoringOrder(execute(saiSelect, 0, 0), new Object[]{0, 0, 0});
+        assertRowsIgnoringOrder(execute(saiSelect, 0, 1), new Object[]{1, 0, 1});
+        assertRowsIgnoringOrder(execute(saiSelect, 1, 0), new Object[]{2, 1, 0});
+        assertRowsIgnoringOrder(execute(saiSelect, 1, 1), new Object[]{3, 1, 1});
     }
 
     @Test
@@ -107,17 +107,17 @@ public class MixedIndexImplementationsTest extends SAITester
         testAllowFiltering("SELECT * FROM %s WHERE c4=0", false);
         testAllowFiltering("SELECT * FROM %s WHERE r1=0", false);
         testAllowFiltering("SELECT * FROM %s WHERE r2=0", false);
-        testAllowFiltering("SELECT * FROM %s WHERE c2=0 AND c3=0", true);
-        testAllowFiltering("SELECT * FROM %s WHERE c2=0 AND r1=0", true);
-        testAllowFiltering("SELECT * FROM %s WHERE c2=0 AND r2=0", true);
-        testAllowFiltering("SELECT * FROM %s WHERE c3=0 AND r1=0", true);
-        testAllowFiltering("SELECT * FROM %s WHERE c3=0 AND r2=0", true);
-        testAllowFiltering("SELECT * FROM %s WHERE r1=0 AND r2=0", true);
-        testAllowFiltering("SELECT * FROM %s WHERE c2=0 AND c3=0 AND r1=0", true);
-        testAllowFiltering("SELECT * FROM %s WHERE c2=0 AND c3=0 AND r2=0", true);
-        testAllowFiltering("SELECT * FROM %s WHERE c2=0 AND r1=0 AND r2=0", true);
-        testAllowFiltering("SELECT * FROM %s WHERE c3=0 AND r1=0 AND r2=0", true);
-        testAllowFiltering("SELECT * FROM %s WHERE c2=0 AND c3=0 AND r1=0 AND r2=0", true);
+        testAllowFiltering("SELECT * FROM %s WHERE c2=0 AND c3=0", false);
+        testAllowFiltering("SELECT * FROM %s WHERE c2=0 AND r1=0", false);
+        testAllowFiltering("SELECT * FROM %s WHERE c2=0 AND r2=0", false);
+        testAllowFiltering("SELECT * FROM %s WHERE c3=0 AND r1=0", false);
+        testAllowFiltering("SELECT * FROM %s WHERE c3=0 AND r2=0", false);
+        testAllowFiltering("SELECT * FROM %s WHERE r1=0 AND r2=0", false);
+        testAllowFiltering("SELECT * FROM %s WHERE c2=0 AND c3=0 AND r1=0", false);
+        testAllowFiltering("SELECT * FROM %s WHERE c2=0 AND c3=0 AND r2=0", false);
+        testAllowFiltering("SELECT * FROM %s WHERE c2=0 AND r1=0 AND r2=0", false);
+        testAllowFiltering("SELECT * FROM %s WHERE c3=0 AND r1=0 AND r2=0", false);
+        testAllowFiltering("SELECT * FROM %s WHERE c2=0 AND c3=0 AND r1=0 AND r2=0", false);
 
         // using the not-SAI index on partition key
         testAllowFiltering("SELECT * FROM %s WHERE k1=0", false);
diff --git a/test/unit/org/apache/cassandra/index/sai/cql/MultipleColumnIndexTest.java b/test/unit/org/apache/cassandra/index/sai/cql/MultipleColumnIndexTest.java
index 9190029588..c29b909c8c 100644
--- a/test/unit/org/apache/cassandra/index/sai/cql/MultipleColumnIndexTest.java
+++ b/test/unit/org/apache/cassandra/index/sai/cql/MultipleColumnIndexTest.java
@@ -52,12 +52,12 @@ public class MultipleColumnIndexTest extends SAITester
         execute("INSERT INTO %s(id, text_map) values (2, {'k1':'v1', 'k3':'v3'})");
         execute("INSERT INTO %s(id, text_map) values (3, {'k4':'v4', 'k5':'v5'})");
 
-        assertEquals(1, execute("SELECT * FROM %s WHERE text_map['k1'] = 'v1' AND text_map['k2'] = 'v2' ALLOW FILTERING").size());
+        assertEquals(1, execute("SELECT * FROM %s WHERE text_map['k1'] = 'v1' AND text_map['k2'] = 'v2'").size());
         assertEquals(2, execute("SELECT * FROM %s WHERE text_map CONTAINS 'v1'").size());
         assertEquals(2, execute("SELECT * FROM %s WHERE text_map CONTAINS KEY 'k1'").size());
-        assertEquals(1, execute("SELECT * FROM %s WHERE text_map CONTAINS KEY 'k1' AND text_map CONTAINS KEY 'k2' ALLOW FILTERING").size());
-        assertEquals(2, execute("SELECT * FROM %s WHERE text_map['k1'] = 'v1' AND text_map CONTAINS KEY 'k1' AND text_map CONTAINS 'v1' ALLOW FILTERING").size());
-        assertEquals(1, execute("SELECT * FROM %s WHERE text_map['k1'] = 'v1' AND text_map CONTAINS KEY 'k1' AND text_map CONTAINS KEY 'k2' AND text_map CONTAINS 'v1' ALLOW FILTERING").size());
-        assertEquals(0, execute("SELECT * FROM %s WHERE text_map['k1'] = 'v1' AND text_map CONTAINS KEY 'k1' AND text_map CONTAINS KEY 'k4' ALLOW FILTERING").size());
+        assertEquals(1, execute("SELECT * FROM %s WHERE text_map CONTAINS KEY 'k1' AND text_map CONTAINS KEY 'k2'").size());
+        assertEquals(2, execute("SELECT * FROM %s WHERE text_map['k1'] = 'v1' AND text_map CONTAINS KEY 'k1' AND text_map CONTAINS 'v1'").size());
+        assertEquals(1, execute("SELECT * FROM %s WHERE text_map['k1'] = 'v1' AND text_map CONTAINS KEY 'k1' AND text_map CONTAINS KEY 'k2' AND text_map CONTAINS 'v1'").size());
+        assertEquals(0, execute("SELECT * FROM %s WHERE text_map['k1'] = 'v1' AND text_map CONTAINS KEY 'k1' AND text_map CONTAINS KEY 'k4'").size());
     }
 }
diff --git a/test/unit/org/apache/cassandra/index/sai/cql/types/QuerySet.java b/test/unit/org/apache/cassandra/index/sai/cql/types/QuerySet.java
index f491fa073a..d542e42c2b 100644
--- a/test/unit/org/apache/cassandra/index/sai/cql/types/QuerySet.java
+++ b/test/unit/org/apache/cassandra/index/sai/cql/types/QuerySet.java
@@ -147,7 +147,7 @@ public abstract class QuerySet extends SAITester
                 Iterator<?> valueIterator = ((Collection<?>) allRows[index][VALUE_INDEX]).iterator();
                 Object value1 = valueIterator.next();
                 Object value2 = valueIterator.hasNext() ? valueIterator.next() : value1;
-                assertRowsIgnoringOrder(tester.execute("SELECT * FROM %s WHERE value CONTAINS ? AND value CONTAINS ? ALLOW FILTERING",
+                assertRowsIgnoringOrder(tester.execute("SELECT * FROM %s WHERE value CONTAINS ? AND value CONTAINS ?",
                     value1, value2), getExpectedRows(value1, value2, allRows));
             }
         }
@@ -225,7 +225,7 @@ public abstract class QuerySet extends SAITester
                 Map<?, ?> map = (Map<?, ?>)allRows[index][VALUE_INDEX];
                 Object value1 = map.values().toArray()[getRandom().nextIntBetween(0, map.values().size() - 1)];
                 Object value2 = map.keySet().toArray()[getRandom().nextIntBetween(0, map.values().size() - 1)];
-                assertRowsIgnoringOrder(tester.execute("SELECT * FROM %s WHERE value CONTAINS ? AND value CONTAINS ? ALLOW FILTERING",
+                assertRowsIgnoringOrder(tester.execute("SELECT * FROM %s WHERE value CONTAINS ? AND value CONTAINS ?",
                         value1, value2), getExpectedRows(value1, value2, allRows));
             }
         }
@@ -275,7 +275,7 @@ public abstract class QuerySet extends SAITester
                 Map<?, ?> map = (Map<?, ?>)allRows[index][VALUE_INDEX];
                 Object key1 = map.keySet().toArray()[getRandom().nextIntBetween(0, map.keySet().size() - 1)];
                 Object key2 = map.keySet().toArray()[getRandom().nextIntBetween(0, map.keySet().size() - 1)];
-                assertRowsIgnoringOrder(tester.execute("SELECT * FROM %s WHERE value CONTAINS KEY ? AND value CONTAINS KEY ? ALLOW FILTERING",
+                assertRowsIgnoringOrder(tester.execute("SELECT * FROM %s WHERE value CONTAINS KEY ? AND value CONTAINS KEY ?",
                         key1, key2), getExpectedRows(key1, key2, allRows));
             }
         }
@@ -329,7 +329,7 @@ public abstract class QuerySet extends SAITester
                 Object value1 = map.get(key1);
                 Object key2 = map.keySet().toArray()[getRandom().nextIntBetween(0, map.keySet().size() - 1)];
                 Object value2 = map.get(key2);
-                assertRowsIgnoringOrder(tester.execute("SELECT * FROM %s WHERE value[?] = ? AND value[?] = ? ALLOW FILTERING",
+                assertRowsIgnoringOrder(tester.execute("SELECT * FROM %s WHERE value[?] = ? AND value[?] = ?",
                         key1, value1, key2, value2), getExpectedRows(key1, value1, key2, value2, allRows));
             }
         }
@@ -399,16 +399,16 @@ public abstract class QuerySet extends SAITester
                 Object key2 = map.keySet().toArray()[getRandom().nextIntBetween(0, map.keySet().size() - 1)];
                 Object value2 = map.get(key2);
 
-                assertRowsIgnoringOrder(tester.execute("SELECT * FROM %s WHERE value CONTAINS KEY ? AND value CONTAINS KEY ? ALLOW FILTERING", key1, key2),
+                assertRowsIgnoringOrder(tester.execute("SELECT * FROM %s WHERE value CONTAINS KEY ? AND value CONTAINS KEY ?", key1, key2),
                                         getExpectedKeyRows(key1, key2, allRows));
 
-                assertRowsIgnoringOrder(tester.execute("SELECT * FROM %s WHERE value CONTAINS ? AND value CONTAINS ? ALLOW FILTERING", value1, value2),
+                assertRowsIgnoringOrder(tester.execute("SELECT * FROM %s WHERE value CONTAINS ? AND value CONTAINS ?", value1, value2),
                                         getExpectedValueRows(value1, value2, allRows));
 
-                assertRowsIgnoringOrder(tester.execute("SELECT * FROM %s WHERE value[?] = ? AND value[?] = ? ALLOW FILTERING", key1, value1, key2, value2),
+                assertRowsIgnoringOrder(tester.execute("SELECT * FROM %s WHERE value[?] = ? AND value[?] = ?", key1, value1, key2, value2),
                                         getExpectedEntryRows(key1, value1, key2, value2, allRows));
 
-                assertRowsIgnoringOrder(tester.execute("SELECT * FROM %s WHERE value[?] = ? AND value CONTAINS KEY ? AND value CONTAINS ? ALLOW FILTERING", key1, value1, key2, value2),
+                assertRowsIgnoringOrder(tester.execute("SELECT * FROM %s WHERE value[?] = ? AND value CONTAINS KEY ? AND value CONTAINS ?", key1, value1, key2, value2),
                                         getExpectedMixedRows(key1, value1, key2, value2, allRows));
             }
         }
diff --git a/test/unit/org/apache/cassandra/index/sai/disk/SelectiveIntersectionTest.java b/test/unit/org/apache/cassandra/index/sai/disk/SelectiveIntersectionTest.java
index e656e90340..349a24b98c 100644
--- a/test/unit/org/apache/cassandra/index/sai/disk/SelectiveIntersectionTest.java
+++ b/test/unit/org/apache/cassandra/index/sai/disk/SelectiveIntersectionTest.java
@@ -92,7 +92,7 @@ public class SelectiveIntersectionTest extends SAITester
     @Test
     public void queryAtSelectiveLimitUsesDeferredFlows() throws Throwable
     {
-        assertEquals(1, execute("SELECT * FROM %s WHERE v1 = '20' AND v2 = '1' ALLOW FILTERING").size());
+        assertEquals(1, execute("SELECT * FROM %s WHERE v1 = '20' AND v2 = '1'").size());
         Assert.assertEquals(1, intersectionFlowCounter.get());
 
         Assert.assertEquals(postingsReaderOpenCounter.get(), postingsReaderCloseCounter.get());
@@ -101,7 +101,7 @@ public class SelectiveIntersectionTest extends SAITester
     @Test
     public void queryAboveSelectiveLimitUsesDirectFlows() throws Throwable
     {
-        assertEquals(1, execute("SELECT * FROM %s WHERE v1 = '1' AND v2 = '0' AND v3 = '1' ALLOW FILTERING").size());
+        assertEquals(1, execute("SELECT * FROM %s WHERE v1 = '1' AND v2 = '0' AND v3 = '1'").size());
         Assert.assertEquals(1, intersectionFlowCounter.get());
 
         Assert.assertEquals(postingsReaderOpenCounter.get(), postingsReaderCloseCounter.get());
@@ -112,7 +112,7 @@ public class SelectiveIntersectionTest extends SAITester
     {
         setLimits(1);
 
-        assertEquals(1, execute("SELECT * FROM %s WHERE v1 = '1' AND v2 = '0' AND v3 = '1' ALLOW FILTERING").size());
+        assertEquals(1, execute("SELECT * FROM %s WHERE v1 = '1' AND v2 = '0' AND v3 = '1'").size());
         Assert.assertEquals(0, intersectionFlowCounter.get());
 
         Assert.assertEquals(postingsReaderOpenCounter.get(), postingsReaderCloseCounter.get());
@@ -123,7 +123,7 @@ public class SelectiveIntersectionTest extends SAITester
     {
         setLimits(0);
 
-        assertEquals(1, execute("SELECT * FROM %s WHERE v1 = '1' AND v2 = '0' AND v3 = '1' ALLOW FILTERING").size());
+        assertEquals(1, execute("SELECT * FROM %s WHERE v1 = '1' AND v2 = '0' AND v3 = '1'").size());
         Assert.assertEquals(1, intersectionFlowCounter.get());
 
         Assert.assertEquals(postingsReaderOpenCounter.get(), postingsReaderCloseCounter.get());
@@ -134,13 +134,13 @@ public class SelectiveIntersectionTest extends SAITester
     {
         Session session = sessionNet();
 
-        String trace = getSingleTraceStatement(session, "SELECT * FROM %s WHERE v1 = '1' AND v2 = '0' AND v3 = '1' ALLOW FILTERING", "Selecting");
+        String trace = getSingleTraceStatement(session, "SELECT * FROM %s WHERE v1 = '1' AND v2 = '0' AND v3 = '1'", "Selecting");
 
         assertEquals("Selecting 2 indexes with cardinalities of 1, 10 out of 3 indexes", trace);
 
         setLimits(1);
 
-        trace = getSingleTraceStatement(session, "SELECT * FROM %s WHERE v1 = '1' AND v2 = '0' AND v3 = '1' ALLOW FILTERING", "Selecting");
+        trace = getSingleTraceStatement(session, "SELECT * FROM %s WHERE v1 = '1' AND v2 = '0' AND v3 = '1'", "Selecting");
 
         assertEquals("Selecting 1 index with cardinality of 1 out of 3 indexes", trace);
 
diff --git a/test/unit/org/apache/cassandra/index/sai/disk/SingleNodeQueryFailureTest.java b/test/unit/org/apache/cassandra/index/sai/disk/SingleNodeQueryFailureTest.java
index e417f7ac59..a208aa9770 100644
--- a/test/unit/org/apache/cassandra/index/sai/disk/SingleNodeQueryFailureTest.java
+++ b/test/unit/org/apache/cassandra/index/sai/disk/SingleNodeQueryFailureTest.java
@@ -86,13 +86,13 @@ public class SingleNodeQueryFailureTest extends SAITester
         {
             Injections.inject(injection);
 
-            assertThatThrownBy(() -> executeNet("SELECT id FROM %s WHERE v1 = '0' ALLOW FILTERING"))
+            assertThatThrownBy(() -> executeNet("SELECT id FROM %s WHERE v1 = '0'"))
                     .isInstanceOf(ReadFailureException.class);
 
-            assertThatThrownBy(() -> executeNet("SELECT id FROM %s WHERE v1 = '1' ALLOW FILTERING"))
+            assertThatThrownBy(() -> executeNet("SELECT id FROM %s WHERE v1 = '1'"))
                     .isInstanceOf(ReadFailureException.class);
 
-            assertThatThrownBy(() -> executeNet("SELECT id FROM %s WHERE v1 = '2' ALLOW FILTERING"))
+            assertThatThrownBy(() -> executeNet("SELECT id FROM %s WHERE v1 = '2'"))
                     .isInstanceOf(ReadFailureException.class);
         }
         catch (Exception e)


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org