You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by ad...@apache.org on 2021/07/06 12:21:25 UTC

[cassandra] branch trunk updated: Fix AbstractReadQuery::toCQLString not returning valid CQL

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

adelapena pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra.git


The following commit(s) were added to refs/heads/trunk by this push:
     new e6a311f  Fix AbstractReadQuery::toCQLString not returning valid CQL
e6a311f is described below

commit e6a311f6898b1184d7d58826021a82cbda2f9bc0
Author: Andrés de la Peña <a....@gmail.com>
AuthorDate: Tue Jul 6 13:20:34 2021 +0100

    Fix AbstractReadQuery::toCQLString not returning valid CQL
    
    patch by Andrés de la Peña; reviewed by Benjamin Lerer for CASSANDRA-16510
---
 CHANGES.txt                                        |   1 +
 .../org/apache/cassandra/db/AbstractReadQuery.java |   9 +-
 src/java/org/apache/cassandra/db/Clustering.java   |   2 +-
 src/java/org/apache/cassandra/db/DataRange.java    |  24 +-
 src/java/org/apache/cassandra/db/DecoratedKey.java |  32 +
 .../cassandra/db/PartitionRangeReadCommand.java    |  18 +-
 src/java/org/apache/cassandra/db/ReadCommand.java  |  15 +-
 .../cassandra/db/SinglePartitionReadCommand.java   |  18 +-
 src/java/org/apache/cassandra/db/Slices.java       |  54 +-
 .../db/VirtualTablePartitionRangeReadQuery.java    |  16 +-
 .../db/VirtualTableSinglePartitionReadQuery.java   |  18 +-
 .../db/filter/AbstractClusteringIndexFilter.java   |   9 +-
 .../cassandra/db/filter/ClusteringIndexFilter.java |   3 +-
 .../db/filter/ClusteringIndexNamesFilter.java      |  31 +-
 .../db/filter/ClusteringIndexSliceFilter.java      |   8 +-
 .../apache/cassandra/db/filter/ColumnFilter.java   |   2 +-
 .../cassandra/db/filter/ColumnSubselection.java    |  18 +-
 .../org/apache/cassandra/db/filter/RowFilter.java  |  74 +-
 .../apache/cassandra/db/marshal/AbstractType.java  |   5 +
 .../apache/cassandra/schema/ColumnMetadata.java    |   5 +-
 .../db/AbstractReadQueryToCQLStringTest.java       | 818 +++++++++++++++++++++
 .../cassandra/index/sasi/plan/OperationTest.java   |  10 +
 22 files changed, 1076 insertions(+), 114 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 0744d6a..dda7b90 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 4.1
+ * Fix AbstractReadQuery::toCQLString not returning valid CQL (CASSANDRA-16510)
  * Log when compacting many tombstones (CASSANDRA-16780)
  * Display bytes per level in tablestats for LCS tables (CASSANDRA-16799)
  * Add isolated flush timer to CommitLogMetrics and ensure writes correspond to single WaitingOnCommit data points (CASSANDRA-16701)
diff --git a/src/java/org/apache/cassandra/db/AbstractReadQuery.java b/src/java/org/apache/cassandra/db/AbstractReadQuery.java
index ec1a6b1..374d2b2 100644
--- a/src/java/org/apache/cassandra/db/AbstractReadQuery.java
+++ b/src/java/org/apache/cassandra/db/AbstractReadQuery.java
@@ -17,6 +17,7 @@
  */
 package org.apache.cassandra.db;
 
+import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.db.filter.ColumnFilter;
 import org.apache.cassandra.db.filter.DataLimits;
 import org.apache.cassandra.db.filter.RowFilter;
@@ -102,13 +103,17 @@ abstract class AbstractReadQuery extends MonitorableImpl implements ReadQuery
         StringBuilder sb = new StringBuilder().append("SELECT ")
                                               .append(columnFilter().toCQLString())
                                               .append(" FROM ")
-                                              .append(metadata().keyspace)
+                                              .append(ColumnIdentifier.maybeQuote(metadata().keyspace))
                                               .append('.')
-                                              .append(metadata().name);
+                                              .append(ColumnIdentifier.maybeQuote(metadata().name));
         appendCQLWhereClause(sb);
 
         if (limits() != DataLimits.NONE)
             sb.append(' ').append(limits());
+
+        // ALLOW FILTERING might not be strictly necessary
+        sb.append(" ALLOW FILTERING");
+
         return sb.toString();
     }
 
diff --git a/src/java/org/apache/cassandra/db/Clustering.java b/src/java/org/apache/cassandra/db/Clustering.java
index c685638..f5184e9 100644
--- a/src/java/org/apache/cassandra/db/Clustering.java
+++ b/src/java/org/apache/cassandra/db/Clustering.java
@@ -72,7 +72,7 @@ public interface Clustering<V> extends ClusteringPrefix<V>
         for (int i = 0; i < size(); i++)
         {
             ColumnMetadata c = metadata.clusteringColumns().get(i);
-            sb.append(i == 0 ? "" : ", ").append(c.type.getString(get(i), accessor()));
+            sb.append(i == 0 ? "" : ", ").append(c.type.toCQLString(bufferAt(i)));
         }
         return sb.toString();
     }
diff --git a/src/java/org/apache/cassandra/db/DataRange.java b/src/java/org/apache/cassandra/db/DataRange.java
index 91a62b3..b322912 100644
--- a/src/java/org/apache/cassandra/db/DataRange.java
+++ b/src/java/org/apache/cassandra/db/DataRange.java
@@ -19,7 +19,6 @@ package org.apache.cassandra.db;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 
-import org.apache.cassandra.db.marshal.ByteArrayAccessor;
 import org.apache.cassandra.schema.ColumnMetadata;
 import org.apache.cassandra.schema.TableMetadata;
 import org.apache.cassandra.db.filter.*;
@@ -186,9 +185,10 @@ public class DataRange
      *
      * @return Whether this {@code DataRange} queries everything.
      */
-    public boolean isUnrestricted()
+    public boolean isUnrestricted(TableMetadata metadata)
     {
-        return startKey().isMinimum() && stopKey().isMinimum() && clusteringIndexFilter.selectsAllPartition();
+        return startKey().isMinimum() && stopKey().isMinimum() &&
+               (clusteringIndexFilter.selectsAllPartition() || metadata.clusteringColumns().isEmpty());
     }
 
     public boolean selectsAllPartition()
@@ -257,10 +257,10 @@ public class DataRange
         return String.format("range=%s pfilter=%s", keyRange.getString(metadata.partitionKeyType), clusteringIndexFilter.toString(metadata));
     }
 
-    public String toCQLString(TableMetadata metadata)
+    public String toCQLString(TableMetadata metadata, RowFilter rowFilter)
     {
-        if (isUnrestricted())
-            return "UNRESTRICTED";
+        if (isUnrestricted(metadata))
+            return rowFilter.toCQLString();
 
         StringBuilder sb = new StringBuilder();
 
@@ -278,7 +278,7 @@ public class DataRange
             needAnd = true;
         }
 
-        String filterString = clusteringIndexFilter.toCQLString(metadata);
+        String filterString = clusteringIndexFilter.toCQLString(metadata, rowFilter);
         if (!filterString.isEmpty())
             sb.append(needAnd ? " AND " : "").append(filterString);
 
@@ -312,20 +312,18 @@ public class DataRange
              : (isInclusive ? "<=" : "<");
     }
 
-    // TODO: this is reused in SinglePartitionReadCommand but this should not really be here. Ideally
-    // we need a more "native" handling of composite partition keys.
-    public static void appendKeyString(StringBuilder sb, AbstractType<?> type, ByteBuffer key)
+    private static void appendKeyString(StringBuilder sb, AbstractType<?> type, ByteBuffer key)
     {
         if (type instanceof CompositeType)
         {
             CompositeType ct = (CompositeType)type;
             ByteBuffer[] values = ct.split(key);
             for (int i = 0; i < ct.types.size(); i++)
-                sb.append(i == 0 ? "" : ", ").append(ct.types.get(i).getString(values[i]));
+                sb.append(i == 0 ? "" : ", ").append(ct.types.get(i).toCQLString(values[i]));
         }
         else
         {
-            sb.append(type.getString(key));
+            sb.append(type.toCQLString(key));
         }
     }
 
@@ -393,7 +391,7 @@ public class DataRange
         }
 
         @Override
-        public boolean isUnrestricted()
+        public boolean isUnrestricted(TableMetadata metadata)
         {
             return false;
         }
diff --git a/src/java/org/apache/cassandra/db/DecoratedKey.java b/src/java/org/apache/cassandra/db/DecoratedKey.java
index 92d6414..4dd87d0 100644
--- a/src/java/org/apache/cassandra/db/DecoratedKey.java
+++ b/src/java/org/apache/cassandra/db/DecoratedKey.java
@@ -19,10 +19,15 @@ package org.apache.cassandra.db;
 
 import java.nio.ByteBuffer;
 import java.util.Comparator;
+import java.util.List;
+import java.util.StringJoiner;
 
+import org.apache.cassandra.db.marshal.CompositeType;
 import org.apache.cassandra.dht.IPartitioner;
 import org.apache.cassandra.dht.Token;
 import org.apache.cassandra.dht.Token.KeyBound;
+import org.apache.cassandra.schema.ColumnMetadata;
+import org.apache.cassandra.schema.TableMetadata;
 import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.MurmurHash;
 import org.apache.cassandra.utils.IFilter.FilterKey;
@@ -125,6 +130,33 @@ public abstract class DecoratedKey implements PartitionPosition, FilterKey
         return "DecoratedKey(" + getToken() + ", " + keystring + ")";
     }
 
+    /**
+     * Returns a CQL representation of this key.
+     *
+     * @param metadata the metadata of the table that this key belogs to
+     * @return a CQL representation of this key
+     */
+    public String toCQLString(TableMetadata metadata)
+    {
+        List<ColumnMetadata> columns = metadata.partitionKeyColumns();
+
+        if (columns.size() == 1)
+            return toCQLString(columns.get(0), getKey());
+
+        ByteBuffer[] values = ((CompositeType) metadata.partitionKeyType).split(getKey());
+        StringJoiner joiner = new StringJoiner(" AND ");
+
+        for (int i = 0; i < columns.size(); i++)
+            joiner.add(toCQLString(columns.get(i), values[i]));
+
+        return joiner.toString();
+    }
+
+    private static String toCQLString(ColumnMetadata metadata, ByteBuffer key)
+    {
+        return String.format("%s = %s", metadata.name.toCQLString(), metadata.type.toCQLString(key));
+    }
+
     public Token getToken()
     {
         return token;
diff --git a/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java b/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java
index 82b6e8a..45cd308 100644
--- a/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java
+++ b/src/java/org/apache/cassandra/db/PartitionRangeReadCommand.java
@@ -22,7 +22,6 @@ import java.util.concurrent.TimeUnit;
 
 import com.google.common.annotations.VisibleForTesting;
 
-import org.apache.cassandra.net.MessageFlag;
 import org.apache.cassandra.net.Verb;
 import org.apache.cassandra.schema.TableMetadata;
 import org.apache.cassandra.config.DatabaseDescriptor;
@@ -41,7 +40,6 @@ import org.apache.cassandra.io.sstable.format.SSTableReadsListener;
 import org.apache.cassandra.io.util.DataInputPlus;
 import org.apache.cassandra.io.util.DataOutputPlus;
 import org.apache.cassandra.metrics.TableMetrics;
-import org.apache.cassandra.net.Message;
 import org.apache.cassandra.schema.IndexMetadata;
 import org.apache.cassandra.service.ClientState;
 import org.apache.cassandra.service.StorageProxy;
@@ -360,19 +358,9 @@ public class PartitionRangeReadCommand extends ReadCommand implements PartitionR
 
     protected void appendCQLWhereClause(StringBuilder sb)
     {
-        if (dataRange.isUnrestricted() && rowFilter().isEmpty())
-            return;
-
-        sb.append(" WHERE ");
-        // We put the row filter first because the data range can end by "ORDER BY"
-        if (!rowFilter().isEmpty())
-        {
-            sb.append(rowFilter());
-            if (!dataRange.isUnrestricted())
-                sb.append(" AND ");
-        }
-        if (!dataRange.isUnrestricted())
-            sb.append(dataRange.toCQLString(metadata()));
+        String filterString = dataRange().toCQLString(metadata(), rowFilter());
+        if (!filterString.isEmpty())
+            sb.append(" WHERE ").append(filterString);
     }
 
     /**
diff --git a/src/java/org/apache/cassandra/db/ReadCommand.java b/src/java/org/apache/cassandra/db/ReadCommand.java
index 7b889d1..71bce0b 100644
--- a/src/java/org/apache/cassandra/db/ReadCommand.java
+++ b/src/java/org/apache/cassandra/db/ReadCommand.java
@@ -35,6 +35,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import org.apache.cassandra.config.*;
+import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.db.filter.*;
 import org.apache.cassandra.net.MessageFlag;
 import org.apache.cassandra.net.Verb;
@@ -727,13 +728,21 @@ public abstract class ReadCommand extends AbstractReadQuery
      */
     public String toCQLString()
     {
-        StringBuilder sb = new StringBuilder();
-        sb.append("SELECT ").append(columnFilter().toCQLString());
-        sb.append(" FROM ").append(metadata().keyspace).append('.').append(metadata().name);
+        StringBuilder sb = new StringBuilder().append("SELECT ")
+                                              .append(columnFilter().toCQLString())
+                                              .append(" FROM ")
+                                              .append(ColumnIdentifier.maybeQuote(metadata().keyspace))
+                                              .append('.')
+                                              .append(ColumnIdentifier.maybeQuote(metadata().name));
+
         appendCQLWhereClause(sb);
 
         if (limits() != DataLimits.NONE)
             sb.append(' ').append(limits());
+
+        // ALLOW FILTERING might not be strictly necessary
+        sb.append(" ALLOW FILTERING");
+
         return sb.toString();
     }
 
diff --git a/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java b/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java
index b17506f..026a795 100644
--- a/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java
+++ b/src/java/org/apache/cassandra/db/SinglePartitionReadCommand.java
@@ -1054,20 +1054,18 @@ public class SinglePartitionReadCommand extends ReadCommand implements SinglePar
         return Verb.READ_REQ;
     }
 
+    @Override
     protected void appendCQLWhereClause(StringBuilder sb)
     {
-        sb.append(" WHERE ");
-
-        sb.append(ColumnMetadata.toCQLString(metadata().partitionKeyColumns())).append(" = ");
-        DataRange.appendKeyString(sb, metadata().partitionKeyType, partitionKey().getKey());
-
-        // We put the row filter first because the clustering index filter can end by "ORDER BY"
-        if (!rowFilter().isEmpty())
-            sb.append(" AND ").append(rowFilter());
+        sb.append(" WHERE ").append(partitionKey().toCQLString(metadata()));
 
-        String filterString = clusteringIndexFilter().toCQLString(metadata());
+        String filterString = clusteringIndexFilter().toCQLString(metadata(), rowFilter());
         if (!filterString.isEmpty())
-            sb.append(" AND ").append(filterString);
+        {
+            if (!clusteringIndexFilter().selectsAllPartition() || !rowFilter().isEmpty())
+                sb.append(" AND ");
+            sb.append(filterString);
+        }
     }
 
     protected void serializeSelection(DataOutputPlus out, int version) throws IOException
diff --git a/src/java/org/apache/cassandra/db/Slices.java b/src/java/org/apache/cassandra/db/Slices.java
index 441a5d3..b3f5681 100644
--- a/src/java/org/apache/cassandra/db/Slices.java
+++ b/src/java/org/apache/cassandra/db/Slices.java
@@ -24,6 +24,8 @@ import java.util.*;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Iterators;
 
+import org.apache.cassandra.cql3.Operator;
+import org.apache.cassandra.db.filter.RowFilter;
 import org.apache.cassandra.schema.ColumnMetadata;
 import org.apache.cassandra.schema.TableMetadata;
 import org.apache.cassandra.db.marshal.AbstractType;
@@ -141,7 +143,7 @@ public abstract class Slices implements Iterable<Slice>
      */
     public abstract boolean intersects(List<ByteBuffer> minClusteringValues, List<ByteBuffer> maxClusteringValues);
 
-    public abstract String toCQLString(TableMetadata metadata);
+    public abstract String toCQLString(TableMetadata metadata, RowFilter rowFilter);
 
     /**
      * Checks if this <code>Slices</code> is empty.
@@ -549,7 +551,8 @@ public abstract class Slices implements Iterable<Slice>
             return sb.append("}").toString();
         }
 
-        public String toCQLString(TableMetadata metadata)
+        @Override
+        public String toCQLString(TableMetadata metadata, RowFilter rowFilter)
         {
             StringBuilder sb = new StringBuilder();
 
@@ -593,7 +596,7 @@ public abstract class Slices implements Iterable<Slice>
                         sb.append(" AND ");
                     needAnd = true;
 
-                    sb.append(column.name);
+                    sb.append(column.name.toCQLString());
 
                     Set<ByteBuffer> values = new LinkedHashSet<>();
                     for (int j = 0; j < componentInfo.size(); j++)
@@ -601,20 +604,25 @@ public abstract class Slices implements Iterable<Slice>
 
                     if (values.size() == 1)
                     {
-                        sb.append(" = ").append(column.type.getString(first.startValue));
+                        sb.append(" = ").append(column.type.toCQLString(first.startValue));
+                        rowFilter = rowFilter.without(column, Operator.EQ, first.startValue);
                     }
                     else
                     {
                         sb.append(" IN (");
                         int j = 0;
                         for (ByteBuffer value : values)
-                            sb.append(j++ == 0 ? "" : ", ").append(column.type.getString(value));
+                        {
+                            sb.append(j++ == 0 ? "" : ", ").append(column.type.toCQLString(value));
+                            rowFilter = rowFilter.without(column, Operator.EQ, value);
+                        }
                         sb.append(")");
                     }
                 }
                 else
                 {
                     boolean isReversed = column.isReversedType();
+                    Operator operator;
 
                     // As said above, we assume (without checking) that this means all ComponentOfSlice for this column
                     // are the same, so we only bother about the first.
@@ -623,27 +631,39 @@ public abstract class Slices implements Iterable<Slice>
                         if (needAnd)
                             sb.append(" AND ");
                         needAnd = true;
-                        sb.append(column.name);
+                        sb.append(column.name.toCQLString());
                         if (isReversed)
-                            sb.append(first.startInclusive ? " <= " : " < ");
+                            operator = first.startInclusive ? Operator.LTE : Operator.LT;
                         else
-                            sb.append(first.startInclusive ? " >= " : " > ");
-                        sb.append(column.type.getString(first.startValue));
+                            operator = first.startInclusive ? Operator.GTE : Operator.GT;
+                        sb.append(' ').append(operator.toString()).append(' ')
+                          .append(column.type.toCQLString(first.startValue));
+                        rowFilter = rowFilter.without(column, operator, first.startValue);
                     }
                     if (first.endValue != null)
                     {
                         if (needAnd)
                             sb.append(" AND ");
                         needAnd = true;
-                        sb.append(column.name);
+                        sb.append(column.name.toCQLString());
                         if (isReversed)
-                            sb.append(first.endInclusive ? " >= " : " > ");
+                            operator = first.endInclusive ? Operator.GTE : Operator.GT;
                         else
-                            sb.append(first.endInclusive ? " <= " : " < ");
-                        sb.append(column.type.getString(first.endValue));
+                            operator = first.endInclusive ? Operator.LTE : Operator.LT;
+                        sb.append(' ').append(operator.toString()).append(' ')
+                          .append(column.type.toCQLString(first.endValue));
+                        rowFilter = rowFilter.without(column, operator, first.endValue);
                     }
                 }
             }
+
+            if (!rowFilter.isEmpty())
+            {
+                if (needAnd)
+                    sb.append(" AND ");
+                sb.append(rowFilter.toCQLString());
+            }
+
             return sb.toString();
         }
 
@@ -764,9 +784,10 @@ public abstract class Slices implements Iterable<Slice>
             return "ALL";
         }
 
-        public String toCQLString(TableMetadata metadata)
+        @Override
+        public String toCQLString(TableMetadata metadata, RowFilter rowFilter)
         {
-            return "";
+            return rowFilter.toCQLString();
         }
     }
 
@@ -839,7 +860,8 @@ public abstract class Slices implements Iterable<Slice>
             return "NONE";
         }
 
-        public String toCQLString(TableMetadata metadata)
+        @Override
+        public String toCQLString(TableMetadata metadata, RowFilter rowFilter)
         {
             return "";
         }
diff --git a/src/java/org/apache/cassandra/db/VirtualTablePartitionRangeReadQuery.java b/src/java/org/apache/cassandra/db/VirtualTablePartitionRangeReadQuery.java
index 48cafa1..b24a670 100644
--- a/src/java/org/apache/cassandra/db/VirtualTablePartitionRangeReadQuery.java
+++ b/src/java/org/apache/cassandra/db/VirtualTablePartitionRangeReadQuery.java
@@ -96,18 +96,8 @@ public class VirtualTablePartitionRangeReadQuery extends VirtualTableReadQuery i
     @Override
     protected void appendCQLWhereClause(StringBuilder sb)
     {
-        if (dataRange.isUnrestricted() && rowFilter().isEmpty())
-            return;
-
-        sb.append(" WHERE ");
-        // We put the row filter first because the data range can end by "ORDER BY"
-        if (!rowFilter().isEmpty())
-        {
-            sb.append(rowFilter());
-            if (!dataRange.isUnrestricted())
-                sb.append(" AND ");
-        }
-        if (!dataRange.isUnrestricted())
-            sb.append(dataRange.toCQLString(metadata()));
+        String filterString = dataRange.toCQLString(metadata(), rowFilter());
+        if (!filterString.isEmpty())
+            sb.append(" WHERE ").append(filterString);
     }
 }
diff --git a/src/java/org/apache/cassandra/db/VirtualTableSinglePartitionReadQuery.java b/src/java/org/apache/cassandra/db/VirtualTableSinglePartitionReadQuery.java
index ba9441a..f96f652 100644
--- a/src/java/org/apache/cassandra/db/VirtualTableSinglePartitionReadQuery.java
+++ b/src/java/org/apache/cassandra/db/VirtualTableSinglePartitionReadQuery.java
@@ -32,7 +32,6 @@ import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
 import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry;
 import org.apache.cassandra.db.virtual.VirtualTable;
 import org.apache.cassandra.exceptions.RequestExecutionException;
-import org.apache.cassandra.schema.ColumnMetadata;
 import org.apache.cassandra.schema.TableMetadata;
 import org.apache.cassandra.service.ClientState;
 
@@ -77,18 +76,15 @@ public class VirtualTableSinglePartitionReadQuery extends VirtualTableReadQuery
     @Override
     protected void appendCQLWhereClause(StringBuilder sb)
     {
-        sb.append(" WHERE ");
+        sb.append(" WHERE ").append(partitionKey().toCQLString(metadata()));
 
-        sb.append(ColumnMetadata.toCQLString(metadata().partitionKeyColumns())).append(" = ");
-        DataRange.appendKeyString(sb, metadata().partitionKeyType, partitionKey().getKey());
-
-        // We put the row filter first because the clustering index filter can end by "ORDER BY"
-        if (!rowFilter().isEmpty())
-            sb.append(" AND ").append(rowFilter());
-
-        String filterString = clusteringIndexFilter().toCQLString(metadata());
+        String filterString = clusteringIndexFilter().toCQLString(metadata(), rowFilter());
         if (!filterString.isEmpty())
-            sb.append(" AND ").append(filterString);
+        {
+            if (!clusteringIndexFilter().selectsAllPartition() || !rowFilter().isEmpty())
+                sb.append(" AND ");
+            sb.append(filterString);
+        }
     }
 
     @Override
diff --git a/src/java/org/apache/cassandra/db/filter/AbstractClusteringIndexFilter.java b/src/java/org/apache/cassandra/db/filter/AbstractClusteringIndexFilter.java
index 63c2783..ddcaaed 100644
--- a/src/java/org/apache/cassandra/db/filter/AbstractClusteringIndexFilter.java
+++ b/src/java/org/apache/cassandra/db/filter/AbstractClusteringIndexFilter.java
@@ -54,11 +54,14 @@ public abstract class AbstractClusteringIndexFilter implements ClusteringIndexFi
     {
         if (reversed)
         {
-            sb.append(" ORDER BY (");
+            sb.append(" ORDER BY ");
             int i = 0;
             for (ColumnMetadata column : metadata.clusteringColumns())
-                sb.append(i++ == 0 ? "" : ", ").append(column.name).append(column.type instanceof ReversedType ? " ASC" : " DESC");
-            sb.append(')');
+            {
+                sb.append(i++ == 0 ? "" : ", ")
+                  .append(column.name.toCQLString())
+                  .append(column.type instanceof ReversedType ? " ASC" : " DESC");
+            }
         }
     }
 
diff --git a/src/java/org/apache/cassandra/db/filter/ClusteringIndexFilter.java b/src/java/org/apache/cassandra/db/filter/ClusteringIndexFilter.java
index 6ea0435..08e9eb5 100644
--- a/src/java/org/apache/cassandra/db/filter/ClusteringIndexFilter.java
+++ b/src/java/org/apache/cassandra/db/filter/ClusteringIndexFilter.java
@@ -23,6 +23,7 @@ import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.partitions.CachedPartition;
 import org.apache.cassandra.db.partitions.Partition;
 import org.apache.cassandra.db.rows.*;
+import org.apache.cassandra.index.Index;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.io.util.DataInputPlus;
 import org.apache.cassandra.io.util.DataOutputPlus;
@@ -153,7 +154,7 @@ public interface ClusteringIndexFilter
     public Kind kind();
 
     public String toString(TableMetadata metadata);
-    public String toCQLString(TableMetadata metadata);
+    public String toCQLString(TableMetadata metadata, RowFilter rowFilter);
 
     public interface Serializer
     {
diff --git a/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java b/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java
index ef9ceff..f937c12 100644
--- a/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java
+++ b/src/java/org/apache/cassandra/db/filter/ClusteringIndexNamesFilter.java
@@ -21,6 +21,7 @@ import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.*;
 
+import org.apache.cassandra.cql3.Operator;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.partitions.*;
 import org.apache.cassandra.db.rows.*;
@@ -167,18 +168,36 @@ public class ClusteringIndexNamesFilter extends AbstractClusteringIndexFilter
         return sb.append(')').toString();
     }
 
-    public String toCQLString(TableMetadata metadata)
+    @Override
+    public String toCQLString(TableMetadata metadata, RowFilter rowFilter)
     {
         if (metadata.clusteringColumns().isEmpty() || clusterings.isEmpty())
-            return "";
+            return rowFilter.toCQLString();
+
+        boolean isSingleColumn = metadata.clusteringColumns().size() == 1;
+        boolean isSingleClustering = clusterings.size() == 1;
 
         StringBuilder sb = new StringBuilder();
-        sb.append('(').append(ColumnMetadata.toCQLString(metadata.clusteringColumns())).append(')');
-        sb.append(clusterings.size() == 1 ? " = " : " IN (");
+        sb.append(isSingleColumn ? "" : '(')
+          .append(ColumnMetadata.toCQLString(metadata.clusteringColumns()))
+          .append(isSingleColumn ? "" : ')');
+
+        sb.append(isSingleClustering ? " = " : " IN (");
         int i = 0;
         for (Clustering<?> clustering : clusterings)
-            sb.append(i++ == 0 ? "" : ", ").append('(').append(clustering.toCQLString(metadata)).append(')');
-        sb.append(clusterings.size() == 1 ? "" : ")");
+        {
+            sb.append(i++ == 0 ? "" : ", ")
+              .append(isSingleColumn ? "" : '(')
+              .append(clustering.toCQLString(metadata))
+              .append(isSingleColumn ? "" : ')');
+
+            for (int j = 0; j < clustering.size(); j++)
+                rowFilter = rowFilter.without(metadata.clusteringColumns().get(j), Operator.EQ, clustering.bufferAt(j));
+        }
+        sb.append(isSingleClustering ? "" : ")");
+
+        if (!rowFilter.isEmpty())
+            sb.append(" AND ").append(rowFilter.toCQLString());
 
         appendOrderByToCQLString(metadata, sb);
         return sb.toString();
diff --git a/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java b/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java
index 5df98c3..b96888d 100644
--- a/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java
+++ b/src/java/org/apache/cassandra/db/filter/ClusteringIndexSliceFilter.java
@@ -30,6 +30,7 @@ import org.apache.cassandra.db.transform.Transformation;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
 import org.apache.cassandra.io.util.DataInputPlus;
 import org.apache.cassandra.io.util.DataOutputPlus;
+import org.stringtemplate.v4.ST;
 
 /**
  * A filter over a single partition.
@@ -142,13 +143,12 @@ public class ClusteringIndexSliceFilter extends AbstractClusteringIndexFilter
         return String.format("slice(slices=%s, reversed=%b)", slices, reversed);
     }
 
-    public String toCQLString(TableMetadata metadata)
+    @Override
+    public String toCQLString(TableMetadata metadata, RowFilter rowFilter)
     {
         StringBuilder sb = new StringBuilder();
 
-        if (!selectsAllPartition())
-            sb.append(slices.toCQLString(metadata));
-
+        sb.append(slices.toCQLString(metadata, rowFilter));
         appendOrderByToCQLString(metadata, sb);
 
         return sb.toString();
diff --git a/src/java/org/apache/cassandra/db/filter/ColumnFilter.java b/src/java/org/apache/cassandra/db/filter/ColumnFilter.java
index d9a1b9d..0ed6237 100644
--- a/src/java/org/apache/cassandra/db/filter/ColumnFilter.java
+++ b/src/java/org/apache/cassandra/db/filter/ColumnFilter.java
@@ -940,7 +940,7 @@ public abstract class ColumnFilter
                 if (s.isEmpty())
                     joiner.add(columnName);
                 else
-                    s.forEach(subSel -> joiner.add(String.format("%s%s", columnName, subSel)));
+                    s.forEach(subSel -> joiner.add(String.format("%s%s", columnName, subSel.toString(cql))));
             }
             return joiner.toString();
         }
diff --git a/src/java/org/apache/cassandra/db/filter/ColumnSubselection.java b/src/java/org/apache/cassandra/db/filter/ColumnSubselection.java
index dbb415a..c53c43a 100644
--- a/src/java/org/apache/cassandra/db/filter/ColumnSubselection.java
+++ b/src/java/org/apache/cassandra/db/filter/ColumnSubselection.java
@@ -88,6 +88,14 @@ public abstract class ColumnSubselection implements Comparable<ColumnSubselectio
      */
     public abstract int compareInclusionOf(CellPath path);
 
+    @Override
+    public String toString()
+    {
+        return toString(false);
+    }
+
+    protected abstract String toString(boolean cql);
+
     private static class Slice extends ColumnSubselection
     {
         private final CellPath from;
@@ -122,11 +130,13 @@ public abstract class ColumnSubselection implements Comparable<ColumnSubselectio
         }
 
         @Override
-        public String toString()
+        protected String toString(boolean cql)
         {
             // This assert we're dealing with a collection since that's the only thing it's used for so far.
             AbstractType<?> type = ((CollectionType<?>)column().type).nameComparator();
-            return String.format("[%s:%s]", from == CellPath.BOTTOM ? "" : type.getString(from.get(0)), to == CellPath.TOP ? "" : type.getString(to.get(0)));
+            return String.format("[%s:%s]",
+                                 from == CellPath.BOTTOM ? "" : (cql ? type.toCQLString(from.get(0)) : type.getString(from.get(0))),
+                                 to == CellPath.TOP ? "" : (cql ? type.toCQLString(to.get(0)) : type.getString(to.get(0))));
         }
     }
 
@@ -156,11 +166,11 @@ public abstract class ColumnSubselection implements Comparable<ColumnSubselectio
         }
 
         @Override
-        public String toString()
+        protected String toString(boolean cql)
         {
             // This assert we're dealing with a collection since that's the only thing it's used for so far.
             AbstractType<?> type = ((CollectionType<?>)column().type).nameComparator();
-            return String.format("[%s]", type.getString(element.get(0)));
+            return String.format("[%s]", cql ? type.toCQLString(element.get(0)) : type.getString(element.get(0)));
         }
     }
 
diff --git a/src/java/org/apache/cassandra/db/filter/RowFilter.java b/src/java/org/apache/cassandra/db/filter/RowFilter.java
index 68a1d57..68cc194 100644
--- a/src/java/org/apache/cassandra/db/filter/RowFilter.java
+++ b/src/java/org/apache/cassandra/db/filter/RowFilter.java
@@ -28,6 +28,7 @@ import com.google.common.base.Objects;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import org.apache.cassandra.cql3.ColumnIdentifier;
 import org.apache.cassandra.cql3.Operator;
 import org.apache.cassandra.db.*;
 import org.apache.cassandra.db.context.*;
@@ -238,6 +239,23 @@ public abstract class RowFilter implements Iterable<RowFilter.Expression>
         return withNewExpressions(newExpressions);
     }
 
+    /**
+     * Returns a copy of this filter but without the provided expression. If this filter doesn't contain the specified
+     * expression this method will just return an identical copy of this filter.
+     */
+    public RowFilter without(ColumnMetadata column, Operator op, ByteBuffer value)
+    {
+        if (isEmpty())
+            return this;
+
+        List<Expression> newExpressions = new ArrayList<>(expressions.size() - 1);
+        for (Expression e : expressions)
+            if (!e.column().equals(column) || e.operator() != op || !e.value.equals(value))
+                newExpressions.add(e);
+
+        return withNewExpressions(newExpressions);
+    }
+
     public RowFilter withoutExpressions()
     {
         return withNewExpressions(Collections.emptyList());
@@ -258,12 +276,27 @@ public abstract class RowFilter implements Iterable<RowFilter.Expression>
     @Override
     public String toString()
     {
+        return toString(false);
+    }
+
+    /**
+     * Returns a CQL representation of this row filter.
+     *
+     * @return a CQL representation of this row filter
+     */
+    public String toCQLString()
+    {
+        return toString(true);
+    }
+
+    private String toString(boolean cql)
+    {
         StringBuilder sb = new StringBuilder();
         for (int i = 0; i < expressions.size(); i++)
         {
             if (i > 0)
                 sb.append(" AND ");
-            sb.append(expressions.get(i));
+            sb.append(expressions.get(i).toString(cql));
         }
         return sb.toString();
     }
@@ -478,6 +511,24 @@ public abstract class RowFilter implements Iterable<RowFilter.Expression>
             return Objects.hashCode(column.name, operator, value);
         }
 
+        @Override
+        public String toString()
+        {
+            return toString(false);
+        }
+
+        /**
+         * Returns a CQL representation of this expression.
+         *
+         * @return a CQL representation of this expression
+         */
+        public String toCQLString()
+        {
+            return toString(true);
+        }
+
+        protected abstract String toString(boolean cql);
+
         private static class Serializer
         {
             public void serialize(Expression expression, DataOutputPlus out, int version) throws IOException
@@ -705,7 +756,7 @@ public abstract class RowFilter implements Iterable<RowFilter.Expression>
         }
 
         @Override
-        public String toString()
+        protected String toString(boolean cql)
         {
             AbstractType<?> type = column.type;
             switch (operator)
@@ -725,7 +776,9 @@ public abstract class RowFilter implements Iterable<RowFilter.Expression>
                 default:
                     break;
             }
-            return String.format("%s %s %s", column.name, operator, type.getString(value));
+            return cql
+                 ? String.format("%s %s %s", column.name.toCQLString(), operator, type.toCQLString(value) )
+                 : String.format("%s %s %s", column.name.toString(), operator, type.getString(value));
         }
 
         @Override
@@ -793,10 +846,14 @@ public abstract class RowFilter implements Iterable<RowFilter.Expression>
         }
 
         @Override
-        public String toString()
+        protected String toString(boolean cql)
         {
-            MapType<?, ?> mt = (MapType<?, ?>)column.type;
-            return String.format("%s[%s] = %s", column.name, mt.nameComparator().getString(key), mt.valueComparator().getString(value));
+            MapType<?, ?> mt = (MapType<?, ?>) column.type;
+            AbstractType<?> nt = mt.nameComparator();
+            AbstractType<?> vt = mt.valueComparator();
+            return cql
+                 ? String.format("%s[%s] = %s", column.name.toCQLString(), nt.toCQLString(key), vt.toCQLString(value))
+                 : String.format("%s[%s] = %s", column.name.toString(), nt.getString(key), vt.getString(value));
         }
 
         @Override
@@ -863,10 +920,11 @@ public abstract class RowFilter implements Iterable<RowFilter.Expression>
             return value;
         }
 
-        public String toString()
+        @Override
+        protected String toString(boolean cql)
         {
             return String.format("expr(%s, %s)",
-                                 targetIndex.name,
+                                 cql ? ColumnIdentifier.maybeQuote(targetIndex.name) : targetIndex.name,
                                  Keyspace.openAndGetStore(table)
                                          .indexManager
                                          .getIndex(targetIndex)
diff --git a/src/java/org/apache/cassandra/db/marshal/AbstractType.java b/src/java/org/apache/cassandra/db/marshal/AbstractType.java
index 19cf849..f171e7f 100644
--- a/src/java/org/apache/cassandra/db/marshal/AbstractType.java
+++ b/src/java/org/apache/cassandra/db/marshal/AbstractType.java
@@ -148,6 +148,11 @@ public abstract class AbstractType<T> implements Comparator<ByteBuffer>, Assignm
         return getString(bytes, ByteBufferAccessor.instance);
     }
 
+    public String toCQLString(ByteBuffer bytes)
+    {
+        return asCQL3Type().toCQLLiteral(bytes, ProtocolVersion.CURRENT);
+    }
+
     /** get a byte representation of the given string. */
     public abstract ByteBuffer fromString(String source) throws MarshalException;
 
diff --git a/src/java/org/apache/cassandra/schema/ColumnMetadata.java b/src/java/org/apache/cassandra/schema/ColumnMetadata.java
index d48ca06..fdbd166 100644
--- a/src/java/org/apache/cassandra/schema/ColumnMetadata.java
+++ b/src/java/org/apache/cassandra/schema/ColumnMetadata.java
@@ -33,7 +33,6 @@ import org.apache.cassandra.db.rows.*;
 import org.apache.cassandra.db.marshal.*;
 import org.apache.cassandra.exceptions.InvalidRequestException;
 import org.apache.cassandra.serializers.MarshalException;
-import org.apache.cassandra.utils.ByteBufferUtil;
 import org.github.jamm.Unmetered;
 
 @Unmetered
@@ -447,9 +446,9 @@ public final class ColumnMetadata extends ColumnSpecification implements Selecta
             return "";
 
         StringBuilder sb = new StringBuilder();
-        sb.append(defs.next().name);
+        sb.append(defs.next().name.toCQLString());
         while (defs.hasNext())
-            sb.append(", ").append(defs.next().name);
+            sb.append(", ").append(defs.next().name.toCQLString());
         return sb.toString();
     }
 
diff --git a/test/unit/org/apache/cassandra/db/AbstractReadQueryToCQLStringTest.java b/test/unit/org/apache/cassandra/db/AbstractReadQueryToCQLStringTest.java
new file mode 100644
index 0000000..4d0adea
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/AbstractReadQueryToCQLStringTest.java
@@ -0,0 +1,818 @@
+/*
+ * 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.db;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+
+import org.apache.cassandra.cql3.CQLStatement;
+import org.apache.cassandra.cql3.CQLTester;
+import org.apache.cassandra.cql3.QueryOptions;
+import org.apache.cassandra.cql3.QueryProcessor;
+import org.apache.cassandra.cql3.statements.SelectStatement;
+import org.apache.cassandra.db.marshal.Int32Type;
+import org.apache.cassandra.db.virtual.AbstractVirtualTable;
+import org.apache.cassandra.db.virtual.SimpleDataSet;
+import org.apache.cassandra.db.virtual.VirtualKeyspace;
+import org.apache.cassandra.db.virtual.VirtualKeyspaceRegistry;
+import org.apache.cassandra.db.virtual.VirtualTable;
+import org.apache.cassandra.exceptions.RequestValidationException;
+import org.apache.cassandra.schema.TableMetadata;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.utils.FBUtilities;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for {@link AbstractReadQuery#toCQLString()}.
+ */
+public class AbstractReadQueryToCQLStringTest extends CQLTester
+{
+    @Test
+    public void testSkinnyTable() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int)");
+
+        // column selection on unrestricted partition range query
+        test("SELECT * FROM %s");
+        test("SELECT k FROM %s",
+             "SELECT * FROM %s");
+        test("SELECT v1 FROM %s");
+        test("SELECT v2 FROM %s");
+        test("SELECT k, v1, v2 FROM %s",
+             "SELECT v1, v2 FROM %s");
+
+        // column selection on partition directed query
+        test("SELECT * FROM %s WHERE k = 0");
+        test("SELECT k FROM %s WHERE k = 0",
+             "SELECT * FROM %s WHERE k = 0");
+        test("SELECT v1 FROM %s WHERE k = 0");
+        test("SELECT v2 FROM %s WHERE k = 0");
+        test("SELECT k, v1, v2 FROM %s WHERE k = 0",
+             "SELECT v1, v2 FROM %s WHERE k = 0");
+
+        // token restrictions
+        test("SELECT * FROM %s WHERE token(k) > 0");
+        test("SELECT * FROM %s WHERE token(k) < 0");
+        test("SELECT * FROM %s WHERE token(k) >= 0");
+        test("SELECT * FROM %s WHERE token(k) <= 0");
+        test("SELECT * FROM %s WHERE token(k) = 0",
+             "SELECT * FROM %s WHERE token(k) >= 0 AND token(k) <= 0");
+
+        // row filter without indexed column
+        test("SELECT * FROM %s WHERE v1 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v1 < 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v1 > 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v1 <= 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v1 >= 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v1 = 1 AND v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k = 0 AND v1 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE token(k) > 0 AND v1 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k = 0 AND v1 = 1 AND v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE token(k) > 0 AND v1 = 1 AND v2 = 2 ALLOW FILTERING");
+
+        // row filter with indexed column
+        createIndex("CREATE INDEX ON %s (v1)");
+        test("SELECT * FROM %s WHERE v1 = 1");
+        test("SELECT * FROM %s WHERE v1 < 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v1 > 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v1 <= 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v1 >= 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v1 = 1 AND v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE token(k) > 0 AND v1 = 1");
+        test("SELECT * FROM %s WHERE k = 0 AND v1 = 1",
+             "SELECT * FROM %s WHERE token(k) >= token(0) AND token(k) <= token(0) AND v1 = 1");
+
+        // grouped partition-directed queries, maybe producing multiple queries
+        test("SELECT * FROM %s WHERE k IN (0)",
+             "SELECT * FROM %s WHERE k = 0");
+        test("SELECT * FROM %s WHERE k IN (0, 1)",
+             "SELECT * FROM %s WHERE k = 0",
+             "SELECT * FROM %s WHERE k = 1");
+    }
+
+    @Test
+    public void testSkinnyTableWithMulticolumnKey() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k1 int, k2 int, v1 int, v2 int, PRIMARY KEY((k1, k2)))");
+
+        // column selection on unrestricted partition range query
+        test("SELECT * FROM %s");
+        test("SELECT k1 FROM %s",
+             "SELECT * FROM %s");
+        test("SELECT k2 FROM %s",
+             "SELECT * FROM %s");
+        test("SELECT v1 FROM %s");
+        test("SELECT v2 FROM %s");
+        test("SELECT k1, k2, v1, v2 FROM %s",
+             "SELECT v1, v2 FROM %s");
+
+        // column selection on partition directed query
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2");
+        test("SELECT k1 FROM %s WHERE k1 = 1 AND k2 = 2",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 2");
+        test("SELECT k2 FROM %s WHERE k1 = 1 AND k2 = 2",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 2");
+        test("SELECT v1 FROM %s WHERE k1 = 1 AND k2 = 2");
+        test("SELECT v2 FROM %s WHERE k1 = 1 AND k2 = 2");
+        test("SELECT k1, k2, v1, v2 FROM %s WHERE k1 = 1 AND k2 = 2",
+             "SELECT v1, v2 FROM %s WHERE k1 = 1 AND k2 = 2");
+
+        // token restrictions
+        test("SELECT * FROM %s WHERE token(k1, k2) > 0");
+        test("SELECT * FROM %s WHERE token(k1, k2) < 0");
+        test("SELECT * FROM %s WHERE token(k1, k2) >= 0");
+        test("SELECT * FROM %s WHERE token(k1, k2) <= 0");
+        test("SELECT * FROM %s WHERE token(k1, k2) = 0",
+             "SELECT * FROM %s WHERE token(k1, k2) >= 0 AND token(k1, k2) <= 0");
+
+        // row filter without indexed column
+        test("SELECT * FROM %s WHERE k1 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v1 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k1 = 1 AND v1 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k1 = 1 AND v2 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k2 = 2 AND v1 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k2 = 2 AND v2 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v1 = 1 AND v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 0 AND v1 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 0 AND v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE token(k1, k2) > 0 AND v1 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE token(k1, k2) > 0 AND v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k1 = 0 AND k2 = 0 AND v1 = 1 AND v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE token(k1, k2) > 0 AND v1 = 1 AND v2 = 2 ALLOW FILTERING");
+
+        // row filter with indexed column
+        createIndex("CREATE INDEX ON %s (k1)");
+        createIndex("CREATE INDEX ON %s (k2)");
+        createIndex("CREATE INDEX ON %s (v1)");
+        createIndex("CREATE INDEX ON %s (v2)");
+        test("SELECT * FROM %s WHERE k1 = 1");
+        test("SELECT * FROM %s WHERE k2 = 2");
+        test("SELECT * FROM %s WHERE v1 = 1");
+        test("SELECT * FROM %s WHERE v2 = 2");
+        test("SELECT * FROM %s WHERE k1 > 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k2 > 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v1 > 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v2 > 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k1 = 1 AND v1 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k1 = 1 AND v2 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k2 = 2 AND v1 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k2 = 2 AND v2 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v1 = 1 AND v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE token(k1, k2) > 0 AND k1 = 1");
+        test("SELECT * FROM %s WHERE token(k1, k2) > 0 AND k2 = 2");
+        test("SELECT * FROM %s WHERE token(k1, k2) > 0 AND v1 = 1");
+        test("SELECT * FROM %s WHERE token(k1, k2) > 0 AND v2 = 2");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND v1 = 1",
+             "SELECT * FROM %s WHERE token(k1, k2) >= token(1, 2) AND token(k1, k2) <= token(1, 2) AND v1 = 1");
+
+        // grouped partition-directed queries, maybe producing multiple queries
+        test("SELECT * FROM %s WHERE k1 IN (1) AND k2 = 2",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 2");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 IN (2)",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 2");
+        test("SELECT * FROM %s WHERE k1 IN (1) AND k2 IN (2)",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 2");
+        test("SELECT * FROM %s WHERE k1 IN (0, 1) AND k2 = 2",
+             "SELECT * FROM %s WHERE k1 = 0 AND k2 = 2",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 2");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 IN (2, 3)",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 2",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 3");
+        test("SELECT * FROM %s WHERE k1 IN (0, 1) AND k2 IN (2, 3)",
+             "SELECT * FROM %s WHERE k1 = 0 AND k2 = 2",
+             "SELECT * FROM %s WHERE k1 = 0 AND k2 = 3",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 2",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 3");
+    }
+
+    @Test
+    public void testWideTable() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k int, c int, v1 int, v2 int, s int static, PRIMARY KEY(k, c))");
+
+        // column selection on unrestricted partition range query
+        test("SELECT * FROM %s");
+        test("SELECT k FROM %s",
+             "SELECT * FROM %s");
+        test("SELECT c FROM %s",
+             "SELECT * FROM %s");
+        test("SELECT s FROM %s");
+        test("SELECT v1 FROM %s");
+        test("SELECT v2 FROM %s");
+        test("SELECT k, c, s, v1, v2 FROM %s",
+             "SELECT s, v1, v2 FROM %s");
+
+        // column selection on partition directed query
+        test("SELECT * FROM %s WHERE k = 0");
+        test("SELECT k FROM %s WHERE k = 0",
+             "SELECT * FROM %s WHERE k = 0");
+        test("SELECT s FROM %s WHERE k = 0");
+        test("SELECT v1 FROM %s WHERE k = 0");
+        test("SELECT v2 FROM %s WHERE k = 0");
+        test("SELECT k, c, s, v1, v2 FROM %s WHERE k = 0",
+             "SELECT s, v1, v2 FROM %s WHERE k = 0");
+
+        // clustering filters
+        test("SELECT * FROM %s WHERE k = 0 AND c = 1");
+        test("SELECT * FROM %s WHERE k = 0 AND c < 1");
+        test("SELECT * FROM %s WHERE k = 0 AND c > 1");
+        test("SELECT * FROM %s WHERE k = 0 AND c <= 1");
+        test("SELECT * FROM %s WHERE k = 0 AND c >= 1");
+        test("SELECT * FROM %s WHERE k = 0 AND c > 1 AND c <= 2");
+        test("SELECT * FROM %s WHERE k = 0 AND c >= 1 AND c < 2");
+
+        // token restrictions
+        test("SELECT * FROM %s WHERE token(k) > 0");
+        test("SELECT * FROM %s WHERE token(k) < 0");
+        test("SELECT * FROM %s WHERE token(k) >= 0");
+        test("SELECT * FROM %s WHERE token(k) <= 0");
+        test("SELECT * FROM %s WHERE token(k) = 0",
+             "SELECT * FROM %s WHERE token(k) >= 0 AND token(k) <= 0");
+
+        // row filter without indexed column
+        test("SELECT * FROM %s WHERE c = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE s = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v1 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v1 = 1 AND v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k = 0 AND v1 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k = 0 AND c = 1 AND v1 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE token(k) > 0 AND v1 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k = 0 AND v1 = 1 AND v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k = 0 AND c = 1 AND v1 = 1 AND v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE token(k) > 0 AND v1 = 1 AND v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE token(k) > 0 AND c = 1 AND v1 = 1 AND v2 = 2 ALLOW FILTERING");
+
+        // expression filter with indexed column
+        createIndex("CREATE INDEX ON %s (c)");
+        createIndex("CREATE INDEX ON %s (s)");
+        createIndex("CREATE INDEX ON %s (v1)");
+        test("SELECT * FROM %s WHERE c = 1");
+        test("SELECT * FROM %s WHERE v1 = 1");
+        test("SELECT * FROM %s WHERE s = 1");
+        test("SELECT * FROM %s WHERE v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v1 = 1 AND v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE token(k) > 0 AND v1 = 1");
+        test("SELECT * FROM %s WHERE k = 0 AND v1 = 1",
+             "SELECT * FROM %s WHERE token(k) >= token(0) AND token(k) <= token(0) AND v1 = 1");
+        test("SELECT * FROM %s WHERE k = 0 AND v1 = 1 AND c = 1",
+             "SELECT * FROM %s WHERE token(k) >= token(0) AND token(k) <= token(0) AND c = 1 AND v1 = 1 ALLOW FILTERING");
+
+        // grouped partition-directed queries, maybe producing multiple queries
+        test("SELECT * FROM %s WHERE k IN (0)",
+             "SELECT * FROM %s WHERE k = 0");
+        test("SELECT * FROM %s WHERE k IN (0, 1)",
+             "SELECT * FROM %s WHERE k = 0",
+             "SELECT * FROM %s WHERE k = 1");
+        test("SELECT * FROM %s WHERE k IN (0, 1) AND c = 0",
+             "SELECT * FROM %s WHERE k = 0 AND c = 0",
+             "SELECT * FROM %s WHERE k = 1 AND c = 0");
+        test("SELECT * FROM %s WHERE k IN (0, 1) AND c > 0",
+             "SELECT * FROM %s WHERE k = 0 AND c > 0",
+             "SELECT * FROM %s WHERE k = 1 AND c > 0");
+
+        // order by
+        test("SELECT * FROM %s WHERE k = 0 ORDER BY c",
+             "SELECT * FROM %s WHERE k = 0");
+        test("SELECT * FROM %s WHERE k = 0 ORDER BY c ASC",
+             "SELECT * FROM %s WHERE k = 0");
+        test("SELECT * FROM %s WHERE k = 0 ORDER BY c DESC");
+
+        // order by clustering filter
+        test("SELECT * FROM %s WHERE k = 0 AND c = 1 ORDER BY c",
+             "SELECT * FROM %s WHERE k = 0 AND c = 1");
+        test("SELECT * FROM %s WHERE k = 0 AND c = 1 ORDER BY c ASC",
+             "SELECT * FROM %s WHERE k = 0 AND c = 1");
+        test("SELECT * FROM %s WHERE k = 0 AND c = 1 ORDER BY c DESC");
+    }
+
+    @Test
+    public void testWideTableWithMulticolumnKey() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k1 int, k2 int, c1 int, c2 int, c3 int, v1 int, v2 int, PRIMARY KEY((k1, k2), c1, c2, c3))");
+
+        // column selection on unrestricted partition range query
+        test("SELECT * FROM %s");
+        test("SELECT k1 FROM %s",
+             "SELECT * FROM %s");
+        test("SELECT k2 FROM %s",
+             "SELECT * FROM %s");
+        test("SELECT c1 FROM %s",
+             "SELECT * FROM %s");
+        test("SELECT c2 FROM %s",
+             "SELECT * FROM %s");
+        test("SELECT c3 FROM %s",
+             "SELECT * FROM %s");
+        test("SELECT v1 FROM %s");
+        test("SELECT v2 FROM %s");
+        test("SELECT k1, k2, c1, c2, c3, v1, v2 FROM %s",
+             "SELECT v1, v2 FROM %s");
+
+        // column selection on partition directed query
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2");
+        test("SELECT k1 FROM %s WHERE k1 = 1 AND k2 = 2",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 2");
+        test("SELECT k2 FROM %s WHERE k1 = 1 AND k2 = 2",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 2");
+        test("SELECT c1 FROM %s WHERE k1 = 1 AND k2 = 2",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 2");
+        test("SELECT c2 FROM %s WHERE k1 = 1 AND k2 = 2",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 2");
+        test("SELECT v1 FROM %s WHERE k1 = 1 AND k2 = 2");
+        test("SELECT v2 FROM %s WHERE k1 = 1 AND k2 = 2");
+        test("SELECT k1, k2, c1, c2, v1, v2 FROM %s WHERE k1 = 1 AND k2 = 2",
+             "SELECT v1, v2 FROM %s WHERE k1 = 1 AND k2 = 2");
+
+        // clustering filters
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 = 1");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 < 1");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 > 1");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 <= 1");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 >= 1");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 > 1 AND c1 < 2");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 > 1 AND c1 <= 2");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 >= 1 AND c1 < 2");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 > 1 AND c1 < 2");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 = 1 AND c2 = 2");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 = 1 AND c2 < 2");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 = 1 AND c2 > 2");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 = 1 AND c2 <= 2");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 = 1 AND c2 >= 2");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 = 1 AND c2 > 2 AND c2 < 3");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 = 1 AND c2 > 2 AND c2 <= 3");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 = 1 AND c2 >= 2 AND c2 < 3");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 = 1 AND c2 > 2 AND c2 < 3");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 = 1 AND c2 = 2 AND c3 = 3",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND (c1, c2, c3) = (1, 2, 3)");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 = 1 AND c2 = 2 AND c3 > 3");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 = 1 AND c2 = 2 AND c3 < 3");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 = 1 AND c2 = 2 AND c3 >= 3");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 = 1 AND c2 = 2 AND c3 <= 3");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 = 1 AND c2 = 2 AND c3 > 3 AND c3 < 4");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 = 1 AND c2 = 2 AND c3 > 3 AND c3 <= 4");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 = 1 AND c2 = 2 AND c3 >= 3 AND c3 < 4");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 = 1 AND c2 = 2 AND c3 >= 3 AND c3 <= 4");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND (c1, c2, c3) = (1, 2, 3)");
+
+        // token restrictions
+        test("SELECT * FROM %s WHERE token(k1, k2) > 0");
+        test("SELECT * FROM %s WHERE token(k1, k2) < 0");
+        test("SELECT * FROM %s WHERE token(k1, k2) >= 0");
+        test("SELECT * FROM %s WHERE token(k1, k2) <= 0");
+        test("SELECT * FROM %s WHERE token(k1, k2) = 0",
+             "SELECT * FROM %s WHERE token(k1, k2) >= 0 AND token(k1, k2) <= 0");
+
+        // row filter without indexed column
+        test("SELECT * FROM %s WHERE k1 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k2 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE c1 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE c2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE c3 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v1 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v1 = 1 AND v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND v1 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 = 1 AND v1 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE token(k1, k2) > 0 AND v1 = 1 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND v1 = 1 AND v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 = 1 AND v1 = 1 AND v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE token(k1, k2) > 0 AND v1 = 1 AND v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE token(k1, k2) > 0 AND c1 = 1 AND v1 = 1 AND v2 = 2 ALLOW FILTERING");
+
+        // expression filter with indexed column
+        createIndex("CREATE INDEX ON %s (k1)");
+        createIndex("CREATE INDEX ON %s (k2)");
+        createIndex("CREATE INDEX ON %s (c1)");
+        createIndex("CREATE INDEX ON %s (c2)");
+        createIndex("CREATE INDEX ON %s (c3)");
+        createIndex("CREATE INDEX ON %s (v1)");
+        createIndex("CREATE INDEX ON %s (v2)");
+        test("SELECT * FROM %s WHERE k1 = 1");
+        test("SELECT * FROM %s WHERE k2 = 2");
+        test("SELECT * FROM %s WHERE c1 = 1");
+        test("SELECT * FROM %s WHERE c2 = 2");
+        test("SELECT * FROM %s WHERE c3 = 3");
+        test("SELECT * FROM %s WHERE v1 = 1");
+        test("SELECT * FROM %s WHERE v2 = 2");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2");
+        test("SELECT * FROM %s WHERE c1 = 1 AND c2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE c1 = 1 AND c2 = 2 AND c3 = 3 ALLOW FILTERING",
+             "SELECT * FROM %s WHERE (c1, c2, c3) = (1, 2, 3) ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v1 = 1 AND v2 = 2 ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE token(k1, k2) > 0 AND v1 = 1");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND v1 = 1",
+             "SELECT * FROM %s WHERE token(k1, k2) >= token(1, 2) AND token(k1, k2) <= token(1, 2) AND v1 = 1");
+        test("SELECT * FROM %s WHERE k1 = 1 AND k2 = 2 AND c1 = 1 AND v1 = 1",
+             "SELECT * FROM %s WHERE token(k1, k2) >= token(1, 2) AND token(k1, k2) <= token(1, 2) AND c1 = 1 AND v1 = 1 ALLOW FILTERING");
+
+        // grouped partition-directed queries, maybe producing multiple queries
+        test("SELECT * FROM %s WHERE k1 IN (1) AND k2 IN (2)",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 2");
+        test("SELECT * FROM %s WHERE k1 IN (1, 2) AND k2 IN (3, 4)",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 3",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 4",
+             "SELECT * FROM %s WHERE k1 = 2 AND k2 = 3",
+             "SELECT * FROM %s WHERE k1 = 2 AND k2 = 4");
+        test("SELECT * FROM %s WHERE k1 IN (1, 2) AND k2 IN (3, 4) AND c1 = 0",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 3 AND c1 = 0",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 4 AND c1 = 0",
+             "SELECT * FROM %s WHERE k1 = 2 AND k2 = 3 AND c1 = 0",
+             "SELECT * FROM %s WHERE k1 = 2 AND k2 = 4 AND c1 = 0");
+        test("SELECT * FROM %s WHERE k1 IN (1, 2) AND k2 IN (3, 4) AND c1 > 0",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 3 AND c1 > 0",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 4 AND c1 > 0",
+             "SELECT * FROM %s WHERE k1 = 2 AND k2 = 3 AND c1 > 0",
+             "SELECT * FROM %s WHERE k1 = 2 AND k2 = 4 AND c1 > 0");
+        test("SELECT * FROM %s WHERE k1 IN (1, 2) AND k2 IN (3, 4) AND (c1, c2, c3) IN ((5, 6, 7), (8, 9, 10))",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 3 AND (c1, c2, c3) IN ((5, 6, 7), (8, 9, 10))",
+             "SELECT * FROM %s WHERE k1 = 1 AND k2 = 4 AND (c1, c2, c3) IN ((5, 6, 7), (8, 9, 10))",
+             "SELECT * FROM %s WHERE k1 = 2 AND k2 = 3 AND (c1, c2, c3) IN ((5, 6, 7), (8, 9, 10))",
+             "SELECT * FROM %s WHERE k1 = 2 AND k2 = 4 AND (c1, c2, c3) IN ((5, 6, 7), (8, 9, 10))");
+
+        // order by
+        test("SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 ORDER BY c1",
+             "SELECT * FROM %s WHERE k1 = 0 AND k2 = 2");
+        test("SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 ORDER BY c1 ASC",
+             "SELECT * FROM %s WHERE k1 = 0 AND k2 = 2");
+        test("SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 ORDER BY c1 DESC",
+             "SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 ORDER BY c1 DESC, c2 DESC, c3 DESC");
+        test("SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 ORDER BY c1, c2",
+             "SELECT * FROM %s WHERE k1 = 0 AND k2 = 2");
+        test("SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 ORDER BY c1, c2 ASC",
+             "SELECT * FROM %s WHERE k1 = 0 AND k2 = 2");
+        test("SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 ORDER BY c1 ASC, c2",
+             "SELECT * FROM %s WHERE k1 = 0 AND k2 = 2");
+        test("SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 ORDER BY c1 ASC, c2 ASC, c3 ASC",
+             "SELECT * FROM %s WHERE k1 = 0 AND k2 = 2");
+        test("SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 ORDER BY c1 DESC, c2 DESC, c3 DESC");
+        test("SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 AND c1 = 1 ORDER BY c1",
+             "SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 AND c1 = 1");
+        test("SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 AND c1 = 1 ORDER BY c1 ASC",
+             "SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 AND c1 = 1");
+
+        // order by clustering filter
+        test("SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 AND c1 = 1 ORDER BY c1 DESC",
+             "SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 AND c1 = 1 ORDER BY c1 DESC, c2 DESC, c3 DESC");
+        test("SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 AND c1 = 1 ORDER BY c1, c2",
+             "SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 AND c1 = 1");
+        test("SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 AND c1 = 1 ORDER BY c1, c2 ASC",
+             "SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 AND c1 = 1");
+        test("SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 AND c1 = 1 ORDER BY c1 ASC, c2",
+             "SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 AND c1 = 1");
+        test("SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 AND c1 = 1 ORDER BY c1 ASC, c2 ASC, c3 ASC",
+             "SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 AND c1 = 1");
+        test("SELECT * FROM %s WHERE k1 = 0 AND k2 = 2 AND c1 = 1 ORDER BY c1 DESC, c2 DESC, c3 DESC");
+    }
+
+    @Test
+    public void testQuotedNames() throws Throwable
+    {
+        createKeyspace("CREATE KEYSPACE \"K\" WITH replication={ 'class' : 'SimpleStrategy', 'replication_factor' : 1 }");
+        createTable("CREATE TABLE \"K\".\"T\" (\"K\" int, \"C\" int, \"S\" int static, \"V\" int, PRIMARY KEY(\"K\", \"C\"))");
+
+        // column selection on unrestricted partition range query
+        test("SELECT * FROM \"K\".\"T\"");
+        test("SELECT \"K\" FROM \"K\".\"T\"",
+             "SELECT * FROM \"K\".\"T\"");
+        test("SELECT \"S\" FROM \"K\".\"T\"");
+        test("SELECT \"V\" FROM \"K\".\"T\"");
+        test("SELECT \"K\", \"C\", \"S\", \"V\" FROM \"K\".\"T\"",
+             "SELECT \"S\", \"V\" FROM \"K\".\"T\"");
+
+        // column selection on partition directed query
+        test("SELECT * FROM \"K\".\"T\" WHERE \"K\" = 0");
+        test("SELECT \"K\" FROM \"K\".\"T\" WHERE \"K\" = 0",
+             "SELECT * FROM \"K\".\"T\" WHERE \"K\" = 0");
+        test("SELECT \"S\" FROM \"K\".\"T\" WHERE \"K\" = 0");
+        test("SELECT \"V\" FROM \"K\".\"T\" WHERE \"K\" = 0");
+        test("SELECT \"K\", \"C\", \"S\", \"V\" FROM \"K\".\"T\" WHERE \"K\" = 0",
+             "SELECT \"S\", \"V\" FROM \"K\".\"T\" WHERE \"K\" = 0");
+
+        // filters
+        test("SELECT * FROM \"K\".\"T\" WHERE \"K\" = 0 AND \"C\" = 1");
+        test("SELECT * FROM \"K\".\"T\" WHERE \"K\" = 0 AND \"C\" > 1 AND \"C\" <= 2");
+        test("SELECT * FROM \"K\".\"T\" WHERE \"V\" = 0 ALLOW FILTERING");
+        test("SELECT * FROM \"K\".\"T\" WHERE \"S\" = 0 ALLOW FILTERING");
+        test("SELECT * FROM \"K\".\"T\" WHERE \"C\" = 0 ALLOW FILTERING");
+
+        // order by
+        test("SELECT * FROM \"K\".\"T\" WHERE \"K\" = 0 ORDER BY \"C\" DESC");
+        test("SELECT * FROM \"K\".\"T\" WHERE \"K\" = 0 AND \"C\" = 1 ORDER BY \"C\" DESC");
+    }
+
+    @Test
+    public void testLiterals() throws Throwable
+    {
+        // skinny table
+        createTable("CREATE TABLE %s (k text, c text, v text, PRIMARY KEY(k, c))");
+        test("SELECT * FROM %s WHERE k = 'A'");
+        test("SELECT * FROM %s WHERE c = 'A' ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE v = 'A' ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k = 'A' AND c = 'B'");
+        test("SELECT * FROM %s WHERE k = 'A' AND v = 'B' ALLOW FILTERING");
+
+        // wide table
+        createTable("CREATE TABLE %s (k1 text, k2 text, c1 text, c2 text, v text, PRIMARY KEY((k1, k2), c1, c2))");
+        test("SELECT * FROM %s WHERE k1 = 'A' ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k2 = 'A' ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE c1 = 'A' ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE c2 = 'A' ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE k1 = 'A' AND k2 = 'B'");
+        test("SELECT * FROM %s WHERE k1 = 'A' AND k2 = 'B' AND c1 = 'C'");
+        test("SELECT * FROM %s WHERE k1 = 'A' AND k2 = 'B' AND c1 > 'C'");
+        test("SELECT * FROM %s WHERE k1 = 'A' AND k2 = 'B' AND c1 > 'C' AND c1 <= 'D'");
+        test("SELECT * FROM %s WHERE k1 = 'A' AND k2 = 'B' AND c1 = 'C' AND c2 = 'D'",
+             "SELECT * FROM %s WHERE k1 = 'A' AND k2 = 'B' AND (c1, c2) = ('C', 'D')");
+        test("SELECT * FROM %s WHERE k1 = 'A' AND k2 = 'B' AND c1 = 'C' AND c2 > 'D'");
+        test("SELECT * FROM %s WHERE k1 = 'A' AND k2 = 'B' AND c1 = 'C' AND c2 > 'D' AND c2 <= 'E'");
+    }
+
+    @Test
+    public void testWideTableWithClusteringOrder() throws Throwable
+    {
+        createTable("CREATE TABLE %s (k int, c1 int, c2 int, c3 int, PRIMARY KEY(k, c1, c2, c3)) WITH CLUSTERING ORDER BY (c1 DESC, c2 ASC, c3 DESC)");
+
+        // one column
+        test("SELECT * FROM %s WHERE k = 0 ORDER BY c1",
+             "SELECT * FROM %s WHERE k = 0 ORDER BY c1 ASC, c2 DESC, c3 ASC");
+        test("SELECT * FROM %s WHERE k = 0 ORDER BY c1 ASC",
+             "SELECT * FROM %s WHERE k = 0 ORDER BY c1 ASC, c2 DESC, c3 ASC");
+        test("SELECT * FROM %s WHERE k = 0 ORDER BY c1 DESC",
+             "SELECT * FROM %s WHERE k = 0");
+
+        // two columns
+        test("SELECT * FROM %s WHERE k = 0 ORDER BY c1, c2 DESC",
+             "SELECT * FROM %s WHERE k = 0 ORDER BY c1 ASC, c2 DESC, c3 ASC");
+        test("SELECT * FROM %s WHERE k = 0 ORDER BY c1 ASC, c2 DESC",
+             "SELECT * FROM %s WHERE k = 0 ORDER BY c1 ASC, c2 DESC, c3 ASC");
+        test("SELECT * FROM %s WHERE k = 0 ORDER BY c1 DESC, c2 ASC",
+             "SELECT * FROM %s WHERE k = 0");
+
+        // three columns
+        test("SELECT * FROM %s WHERE k = 0 ORDER BY c1, c2 DESC, c3 ASC",
+             "SELECT * FROM %s WHERE k = 0 ORDER BY c1 ASC, c2 DESC, c3 ASC");
+        test("SELECT * FROM %s WHERE k = 0 ORDER BY c1, c2 DESC, c3 ASC",
+             "SELECT * FROM %s WHERE k = 0 ORDER BY c1 ASC, c2 DESC, c3 ASC");
+        test("SELECT * FROM %s WHERE k = 0 ORDER BY c1 ASC, c2 DESC, c3 ASC",
+             "SELECT * FROM %s WHERE k = 0 ORDER BY c1 ASC, c2 DESC, c3 ASC");
+        test("SELECT * FROM %s WHERE k = 0 ORDER BY c1 DESC, c2 ASC, c3 DESC",
+             "SELECT * FROM %s WHERE k = 0");
+    }
+
+    @Test
+    public void testCollections() throws Throwable
+    {
+        String udt = createType("CREATE TYPE %s (a text, b int)");
+        createTable("CREATE TABLE %s (" +
+                    "k int PRIMARY KEY, " +
+                    "l list<text>, " +
+                    "s set<text>, " +
+                    "m map<text, text>, " +
+                    "t tuple<text, int>, " +
+                    "u " + udt + ")");
+
+        // column selections
+        test("SELECT l FROM %s");
+        test("SELECT s FROM %s");
+        test("SELECT m FROM %s");
+        test("SELECT t FROM %s");
+        test("SELECT u FROM %s");
+        testInvalid("SELECT l['a'] FROM %s");
+        test("SELECT s['a'] FROM %s");
+        test("SELECT m['a'] FROM %s");
+        test("SELECT u.a FROM %s",
+             "SELECT u FROM %s");
+        test("SELECT m['a'], m['b'], s['c'], s['d'], t, u.a, u.b FROM %s",
+             "SELECT m['a'], m['b'], s['c'], s['d'], t, u FROM %s");
+
+        // filtering
+        testInvalid("SELECT * FROM %s WHERE l = ['a', 'b'] ALLOW FILTERING");
+        testInvalid("SELECT * FROM %s WHERE s = {'a', 'b'} ALLOW FILTERING");
+        testInvalid("SELECT * FROM %s WHERE m = {'a': 'b', 'c': 'd'} ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE t = ('a', 1) ALLOW FILTERING");
+        testInvalid("SELECT * FROM %s WHERE u = {a: 'a', b: 1} ALLOW FILTERING");
+        testInvalid("SELECT * FROM %s WHERE l['a'] = 'b' ALLOW FILTERING");
+        testInvalid("SELECT * FROM %s WHERE s['a'] = 'b' ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE m['a'] = 'b' ALLOW FILTERING");
+        testInvalid("SELECT * FROM %s WHERE u.a = 'a' ALLOW FILTERING");
+        testInvalid("SELECT * FROM %s WHERE u.b = 0 ALLOW FILTERING");
+        testInvalid("SELECT * FROM %s WHERE u.a = 'a' ANd u.b = 0 ALLOW FILTERING");
+    }
+
+    @Test
+    public void testFrozenCollections() throws Throwable
+    {
+        String udt = createType("CREATE TYPE %s (a text, b int)");
+        createTable("CREATE TABLE %s (" +
+                    "k int PRIMARY KEY, " +
+                    "l frozen<list<text>>, " +
+                    "s frozen<set<text>>, " +
+                    "m frozen<map<text, text>>, " +
+                    "t frozen<tuple<text, int>>, " +
+                    "u frozen<" + udt + ">)");
+
+        // column selections
+        test("SELECT l FROM %s");
+        test("SELECT s FROM %s");
+        test("SELECT m FROM %s");
+        test("SELECT t FROM %s");
+        test("SELECT u FROM %s");
+        testInvalid("SELECT l['a'] FROM %s");
+        test("SELECT s['a'] FROM %s",
+             "SELECT s FROM %s");
+        test("SELECT m['a'] FROM %s",
+             "SELECT m FROM %s");
+        test("SELECT u.a FROM %s",
+             "SELECT u FROM %s");
+        test("SELECT m['a'], m['b'], s['c'], s['d'], t, u.a, u.b FROM %s",
+             "SELECT m, s, t, u FROM %s");
+
+        // filtering
+        test("SELECT * FROM %s WHERE l = ['a', 'b'] ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE s = {'a', 'b'} ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE m = {'a': 'b', 'c': 'd'} ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE t = ('a', 1) ALLOW FILTERING");
+        test("SELECT * FROM %s WHERE u = {a: 'a', b: 1} ALLOW FILTERING");
+        testInvalid("SELECT * FROM %s WHERE l['a'] = 'a' ALLOW FILTERING");
+        testInvalid("SELECT * FROM %s WHERE s['a'] = 'a' ALLOW FILTERING");
+        testInvalid("SELECT * FROM %s WHERE m['a'] = 'a' ALLOW FILTERING");
+        testInvalid("SELECT * FROM %s WHERE u.a = 'a' ALLOW FILTERING");
+        testInvalid("SELECT * FROM %s WHERE u.b = 0 ALLOW FILTERING");
+        testInvalid("SELECT * FROM %s WHERE u.a = 'a' ANd u.b = 0 ALLOW FILTERING");
+    }
+
+    @Test
+    public void testVirtualTable() throws Throwable
+    {
+        TableMetadata metadata =
+        TableMetadata.builder("vk", "vt")
+                     .kind(TableMetadata.Kind.VIRTUAL)
+                     .addPartitionKeyColumn("k", Int32Type.instance)
+                     .addClusteringColumn("c", Int32Type.instance)
+                     .addRegularColumn("v", Int32Type.instance)
+                     .addStaticColumn("s", Int32Type.instance)
+                     .build();
+        SimpleDataSet data = new SimpleDataSet(metadata);
+        VirtualTable table = new AbstractVirtualTable(metadata)
+        {
+            public DataSet data()
+            {
+                return data;
+            }
+        };
+        VirtualKeyspaceRegistry.instance.register(new VirtualKeyspace("vk", ImmutableList.of(table)));
+
+        // column selection on unrestricted partition range query
+        test("SELECT * FROM vk.vt");
+        test("SELECT k FROM vk.vt",
+             "SELECT * FROM vk.vt");
+        test("SELECT c FROM vk.vt",
+             "SELECT * FROM vk.vt");
+        test("SELECT s FROM vk.vt");
+        test("SELECT v FROM vk.vt");
+        test("SELECT k, c, s, v FROM vk.vt",
+             "SELECT s, v FROM vk.vt");
+
+        // column selection on partition directed query
+        test("SELECT * FROM vk.vt WHERE k = 1");
+        test("SELECT k FROM vk.vt WHERE k = 1",
+             "SELECT * FROM vk.vt WHERE k = 1");
+        test("SELECT c FROM vk.vt WHERE k = 1",
+             "SELECT * FROM vk.vt WHERE k = 1");
+        test("SELECT v FROM vk.vt WHERE k = 1");
+        test("SELECT s FROM vk.vt WHERE k = 1");
+        test("SELECT k, c, s, v FROM vk.vt WHERE k = 1",
+             "SELECT s, v FROM vk.vt WHERE k = 1");
+
+        // clustering filters
+        test("SELECT * FROM vk.vt WHERE k = 0 AND c = 1");
+        test("SELECT * FROM vk.vt WHERE k = 0 AND c < 1");
+        test("SELECT * FROM vk.vt WHERE k = 0 AND c > 1");
+        test("SELECT * FROM vk.vt WHERE k = 0 AND c <= 1");
+        test("SELECT * FROM vk.vt WHERE k = 0 AND c >= 1");
+        test("SELECT * FROM vk.vt WHERE k = 0 AND c > 1 AND c <= 2");
+        test("SELECT * FROM vk.vt WHERE k = 0 AND c >= 1 AND c < 2");
+
+        // token restrictions
+        test("SELECT * FROM vk.vt WHERE token(k) > 0");
+        test("SELECT * FROM vk.vt WHERE token(k) < 0");
+        test("SELECT * FROM vk.vt WHERE token(k) >= 0");
+        test("SELECT * FROM vk.vt WHERE token(k) <= 0");
+        test("SELECT * FROM vk.vt WHERE token(k) = 0",
+             "SELECT * FROM vk.vt WHERE token(k) >= 0 AND token(k) <= 0");
+
+        // row filters
+        test("SELECT * FROM vk.vt WHERE c = 1 ALLOW FILTERING");
+        test("SELECT * FROM vk.vt WHERE s = 1 ALLOW FILTERING");
+        test("SELECT * FROM vk.vt WHERE v = 1 ALLOW FILTERING");
+        test("SELECT * FROM vk.vt WHERE k = 0 AND v = 1 ALLOW FILTERING");
+        test("SELECT * FROM vk.vt WHERE k = 0 AND c = 1 AND v = 1 ALLOW FILTERING");
+        test("SELECT * FROM vk.vt WHERE token(k) > 0 AND v = 1 ALLOW FILTERING");
+        test("SELECT * FROM vk.vt WHERE token(k) > 0 AND c = 1 AND v = 1 ALLOW FILTERING");
+
+        // grouped partition-directed queries, maybe producing multiple queries
+        test("SELECT * FROM vk.vt WHERE k IN (0)",
+             "SELECT * FROM vk.vt WHERE k = 0");
+        test("SELECT * FROM vk.vt WHERE k IN (0, 1)",
+             "SELECT * FROM vk.vt WHERE k = 0",
+             "SELECT * FROM vk.vt WHERE k = 1");
+        test("SELECT * FROM vk.vt WHERE k IN (0, 1) AND c = 0",
+             "SELECT * FROM vk.vt WHERE k = 0 AND c = 0",
+             "SELECT * FROM vk.vt WHERE k = 1 AND c = 0");
+        test("SELECT * FROM vk.vt WHERE k IN (0, 1) AND c > 0",
+             "SELECT * FROM vk.vt WHERE k = 0 AND c > 0",
+             "SELECT * FROM vk.vt WHERE k = 1 AND c > 0");
+
+        // order by
+        test("SELECT * FROM vk.vt WHERE k = 0 ORDER BY c",
+             "SELECT * FROM vk.vt WHERE k = 0");
+        test("SELECT * FROM vk.vt WHERE k = 0 ORDER BY c ASC",
+             "SELECT * FROM vk.vt WHERE k = 0");
+        test("SELECT * FROM vk.vt WHERE k = 0 ORDER BY c DESC");
+
+        // order by clustering filter
+        test("SELECT * FROM vk.vt WHERE k = 0 AND c = 1 ORDER BY c",
+             "SELECT * FROM vk.vt WHERE k = 0 AND c = 1");
+        test("SELECT * FROM vk.vt WHERE k = 0 AND c = 1 ORDER BY c ASC",
+             "SELECT * FROM vk.vt WHERE k = 0 AND c = 1");
+        test("SELECT * FROM vk.vt WHERE k = 0 AND c = 1 ORDER BY c DESC");
+    }
+
+    private List<String> toCQLString(String query)
+    {
+        String fullQuery = formatQuery(query);
+        ClientState state = ClientState.forInternalCalls();
+        CQLStatement statement = QueryProcessor.getStatement(fullQuery, state);
+
+        assertTrue(statement instanceof SelectStatement);
+        SelectStatement select = (SelectStatement) statement;
+
+        QueryOptions options = QueryOptions.forInternalCalls(Collections.emptyList());
+        ReadQuery readQuery = select.getQuery(options, FBUtilities.nowInSeconds());
+
+        if (readQuery instanceof SinglePartitionReadCommand.Group)
+        {
+            SinglePartitionReadCommand.Group group = (SinglePartitionReadCommand.Group) readQuery;
+            return group.queries.stream().map(AbstractReadQuery::toCQLString).collect(Collectors.toList());
+        }
+        else if (readQuery instanceof VirtualTableSinglePartitionReadQuery.Group)
+        {
+            VirtualTableSinglePartitionReadQuery.Group group = (VirtualTableSinglePartitionReadQuery.Group) readQuery;
+            return group.queries.stream().map(AbstractReadQuery::toCQLString).collect(Collectors.toList());
+        }
+        else
+        {
+            assertTrue(readQuery instanceof AbstractReadQuery);
+            return Collections.singletonList(((AbstractReadQuery) readQuery).toCQLString());
+        }
+    }
+
+    private void test(String query) throws Throwable
+    {
+        test(query, query);
+    }
+
+    private void test(String query, String... expected) throws Throwable
+    {
+        List<String> actual = toCQLString(query);
+        List<String> fullExpected = Stream.of(expected)
+                                          .map(this::formatQuery)
+                                          .map(s -> s.endsWith(" ALLOW FILTERING") ? s : s + " ALLOW FILTERING")
+                                          .collect(Collectors.toList());
+        assertEquals(fullExpected, actual);
+
+        // execute both the expected output commands to verify that they are valid CQL
+        for (String q : expected)
+            execute(q);
+    }
+
+    private void testInvalid(String query) throws Throwable
+    {
+        assertInvalidThrow(RequestValidationException.class, query);
+    }
+}
diff --git a/test/unit/org/apache/cassandra/index/sasi/plan/OperationTest.java b/test/unit/org/apache/cassandra/index/sasi/plan/OperationTest.java
index 4468f2c..8620f5c 100644
--- a/test/unit/org/apache/cassandra/index/sasi/plan/OperationTest.java
+++ b/test/unit/org/apache/cassandra/index/sasi/plan/OperationTest.java
@@ -38,6 +38,7 @@ import org.apache.cassandra.db.marshal.LongType;
 import org.apache.cassandra.db.marshal.UTF8Type;
 import org.apache.cassandra.exceptions.ConfigurationException;
 import org.apache.cassandra.schema.KeyspaceParams;
+import org.apache.cassandra.utils.ByteBufferUtil;
 import org.apache.cassandra.utils.FBUtilities;
 
 import org.junit.*;
@@ -651,6 +652,15 @@ public class OperationTest extends SchemaLoader
         {
             throw new UnsupportedOperationException();
         }
+
+        @Override
+        protected String toString(boolean cql)
+        {
+            return String.format("%s %s %s",
+                                 cql ? column.name.toCQLString() : column.name.toString(),
+                                 operator,
+                                 ByteBufferUtil.bytesToHex(value));
+        }
     }
 
     private static Unfiltered buildRow(Cell<?>... cells)

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