You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by ty...@apache.org on 2014/09/17 22:55:27 UTC

git commit: Support IN clause for any clustering column

Repository: cassandra
Updated Branches:
  refs/heads/trunk 87ba0dc1b -> 9553dae93


Support IN clause for any clustering column

Patch by Benjamin Lerer; reviewed by Tyler Hobbs for CASSANDRA-4762


Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo
Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/9553dae9
Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/9553dae9
Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/9553dae9

Branch: refs/heads/trunk
Commit: 9553dae93ba328f3538b213051d180c21717c158
Parents: 87ba0dc
Author: blerer <b_...@hotmail.com>
Authored: Wed Sep 17 13:54:45 2014 -0500
Committer: Tyler Hobbs <ty...@datastax.com>
Committed: Wed Sep 17 13:54:45 2014 -0500

----------------------------------------------------------------------
 CHANGES.txt                                     |   1 +
 .../cql3/statements/SelectStatement.java        |  90 +++-----
 .../db/composites/CompositesBuilder.java        | 219 +++++++++++++++++++
 .../cql3/SingleColumnRelationTest.java          | 119 ++++++++++
 4 files changed, 366 insertions(+), 63 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cassandra/blob/9553dae9/CHANGES.txt
----------------------------------------------------------------------
diff --git a/CHANGES.txt b/CHANGES.txt
index ca192a7..a11de41 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 3.0
+ * Support IN clause on any clustering column (CASSANDRA-4762)
  * Improve compaction logging (CASSANDRA-7818)
  * Remove YamlFileNetworkTopologySnitch (CASSANDRA-7917)
  * Support Java source code for user-defined functions (CASSANDRA-7562)

http://git-wip-us.apache.org/repos/asf/cassandra/blob/9553dae9/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
index ad66232..e135c0a 100644
--- a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java
@@ -31,6 +31,7 @@ import org.github.jamm.MemoryMeter;
 import org.apache.cassandra.auth.Permission;
 import org.apache.cassandra.cql3.*;
 import org.apache.cassandra.db.composites.*;
+import org.apache.cassandra.db.composites.Composite.EOC;
 import org.apache.cassandra.transport.messages.ResultMessage;
 import org.apache.cassandra.config.CFMetaData;
 import org.apache.cassandra.config.ColumnDefinition;
@@ -694,7 +695,7 @@ public class SelectStatement implements CQLStatement, MeasurableForPreparedCache
         // we always do a slice for CQL3 tables, so it's ok to ignore them here
         assert !isColumnRange();
 
-        CBuilder builder = cfm.comparator.prefixBuilder();
+        CompositesBuilder builder = new CompositesBuilder(cfm.comparator.prefixBuilder(), cfm.comparator);
         Iterator<ColumnDefinition> idIter = cfm.clusteringColumns().iterator();
         for (Restriction r : columnRestrictions)
         {
@@ -702,36 +703,20 @@ public class SelectStatement implements CQLStatement, MeasurableForPreparedCache
             assert r != null && !r.isSlice();
 
             List<ByteBuffer> values = r.values(options);
-            if (values.size() == 1)
-            {
-                ByteBuffer val = values.get(0);
-                if (val == null)
-                    throw new InvalidRequestException(String.format("Invalid null value for clustering key part %s", def.name));
-                builder.add(val);
-            }
-            else
-            {
-                // We have a IN, which we only support for the last column.
-                // If compact, just add all values and we're done. Otherwise,
-                // for each value of the IN, creates all the columns corresponding to the selection.
-                if (values.isEmpty())
-                    return null;
-                SortedSet<CellName> columns = new TreeSet<CellName>(cfm.comparator);
-                Iterator<ByteBuffer> iter = values.iterator();
-                while (iter.hasNext())
-                {
-                    ByteBuffer val = iter.next();
-                    if (val == null)
-                        throw new InvalidRequestException(String.format("Invalid null value for clustering key part %s", def.name));
 
-                    Composite prefix = builder.buildWith(val);
-                    columns.addAll(addSelectedColumns(prefix));
-                }
-                return columns;
-            }
+            if (values.isEmpty())
+                return null;
+
+            builder.addEachElementToAll(values);
+            if (builder.containsNull())
+                throw new InvalidRequestException(String.format("Invalid null value for clustering key part %s",
+                                                                def.name));
         }
+        SortedSet<CellName> columns = new TreeSet<CellName>(cfm.comparator);
+        for (Composite composite : builder.build())
+            columns.addAll(addSelectedColumns(composite));
 
-        return addSelectedColumns(builder.build());
+        return columns;
     }
 
     private SortedSet<CellName> addSelectedColumns(Composite prefix)
@@ -810,6 +795,7 @@ public class SelectStatement implements CQLStatement, MeasurableForPreparedCache
             }
         }
 
+        CompositesBuilder compositeBuilder = new CompositesBuilder(builder, isReversed ? type.reverseComparator() : type);
         // The end-of-component of composite doesn't depend on whether the
         // component type is reversed or not (i.e. the ReversedType is applied
         // to the component comparator but not to the end-of-component itself),
@@ -829,41 +815,21 @@ public class SelectStatement implements CQLStatement, MeasurableForPreparedCache
                 // There wasn't any non EQ relation on that key, we select all records having the preceding component as prefix.
                 // For composites, if there was preceding component and we're computing the end, we must change the last component
                 // End-Of-Component, otherwise we would be selecting only one record.
-                Composite prefix = builder.build();
-                return Collections.singletonList(!prefix.isEmpty() && eocBound == Bound.END ? prefix.end() : prefix);
+                EOC eoc = !compositeBuilder.isEmpty() && eocBound == Bound.END ? EOC.END : EOC.NONE;
+                return compositeBuilder.buildWithEOC(eoc);
             }
             if (r.isSlice())
             {
-                builder.add(getSliceValue(r, b, options));
-                Relation.Type relType = ((Restriction.Slice)r).getRelation(eocBound, b);
-                return Collections.singletonList(builder.build().withEOC(eocForRelation(relType)));
+                compositeBuilder.addElementToAll(getSliceValue(r, b, options));
+                Relation.Type relType = ((Restriction.Slice) r).getRelation(eocBound, b);
+                return compositeBuilder.buildWithEOC(eocForRelation(relType));
             }
-            else
-            {
-                List<ByteBuffer> values = r.values(options);
-                if (values.size() != 1)
-                {
-                    // IN query, we only support it on the clustering columns
-                    assert def.position() == defs.size() - 1;
-                    // The IN query might not have listed the values in comparator order, so we need to re-sort
-                    // the bounds lists to make sure the slices works correctly (also, to avoid duplicates).
-                    TreeSet<Composite> s = new TreeSet<>(isReversed ? type.reverseComparator() : type);
-                    for (ByteBuffer val : values)
-                    {
-                        if (val == null)
-                            throw new InvalidRequestException(String.format("Invalid null clustering key part %s", def.name));
-                        Composite prefix = builder.buildWith(val);
-                        // See below for why this
-                        s.add((eocBound == Bound.END && builder.remainingCount() > 0) ? prefix.end() : prefix);
-                    }
-                    return new ArrayList<>(s);
-                }
 
-                ByteBuffer val = values.get(0);
-                if (val == null)
-                    throw new InvalidRequestException(String.format("Invalid null clustering key part %s", def.name));
-                builder.add(val);
-            }
+            compositeBuilder.addEachElementToAll(r.values(options));
+
+            if (compositeBuilder.containsNull())
+                throw new InvalidRequestException(
+                        String.format("Invalid null clustering key part %s", def.name));
         }
         // Means no relation at all or everything was an equal
         // Note: if the builder is "full", there is no need to use the end-of-component bit. For columns selection,
@@ -871,8 +837,8 @@ public class SelectStatement implements CQLStatement, MeasurableForPreparedCache
         // with 2ndary index is done, and with the the partition provided with an EQ, we'll end up here, and in that
         // case using the eoc would be bad, since for the random partitioner we have no guarantee that
         // prefix.end() will sort after prefix (see #5240).
-        Composite prefix = builder.build();
-        return Collections.singletonList(eocBound == Bound.END && builder.remainingCount() > 0 ? prefix.end() : prefix);
+        EOC eoc = eocBound == Bound.END && compositeBuilder.hasRemaining() ? EOC.END : EOC.NONE;
+        return compositeBuilder.buildWithEOC(eoc);
     }
 
     private static Composite.EOC eocForRelation(Relation.Type op)
@@ -1873,9 +1839,7 @@ public class SelectStatement implements CQLStatement, MeasurableForPreparedCache
                 }
                 else if (restriction.isIN())
                 {
-                    if (!restriction.isMultiColumn() && i != stmt.columnRestrictions.length - 1)
-                        throw new InvalidRequestException(String.format("Clustering column \"%s\" cannot be restricted by an IN relation", cdef.name));
-                    else if (stmt.selectACollection())
+                    if (stmt.selectACollection())
                         throw new InvalidRequestException(String.format("Cannot restrict column \"%s\" by IN relation as a collection is selected by the query", cdef.name));
                 }
 

http://git-wip-us.apache.org/repos/asf/cassandra/blob/9553dae9/src/java/org/apache/cassandra/db/composites/CompositesBuilder.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/db/composites/CompositesBuilder.java b/src/java/org/apache/cassandra/db/composites/CompositesBuilder.java
new file mode 100644
index 0000000..29fe905
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/composites/CompositesBuilder.java
@@ -0,0 +1,219 @@
+/*
+ * 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.composites;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.cassandra.db.composites.Composite.EOC;
+
+import static java.util.Collections.singletonList;
+
+/**
+ * Builder that allow to build multiple composites at the same time.
+ */
+public final class CompositesBuilder
+{
+    /**
+     * The builder used to build the <code>Composite</code>s.
+     */
+    private final CBuilder builder;
+
+    /**
+     * The comparator used to sort the returned <code>Composite</code>s.
+     */
+    private final Comparator<Composite> comparator;
+
+    /**
+     * The elements of the composites
+     */
+    private final List<List<ByteBuffer>> elementsList = new ArrayList<>();
+
+    /**
+     * The number of elements that still can be added.
+     */
+    private int remaining;
+
+    /**
+     * <code>true</code> if the composites have been build, <code>false</code> otherwise.
+     */
+    private boolean built;
+
+    /**
+     * <code>true</code> if the composites contains some <code>null</code> elements.
+     */
+    private boolean containsNull;
+
+    public CompositesBuilder(CBuilder builder, Comparator<Composite> comparator)
+    {
+        this.builder = builder;
+        this.comparator = comparator;
+        this.remaining = builder.remainingCount();
+    }
+
+    /**
+     * Adds the specified element to all the composites.
+     * <p>
+     * If this builder contains 2 composites: A-B and A-C a call to this method to add D will result in the composites:
+     * A-B-D and A-C-D.
+     * </p>
+     *
+     * @param value the value of the next element
+     * @return this <code>CompositeBuilder</code>
+     */
+    public CompositesBuilder addElementToAll(ByteBuffer value)
+    {
+        checkUpdateable();
+
+        if (isEmpty())
+            elementsList.add(new ArrayList<ByteBuffer>());
+
+        for (int i = 0, m = elementsList.size(); i < m; i++)
+        {
+            if (value == null)
+                containsNull = true;
+
+            elementsList.get(i).add(value);
+        }
+        remaining--;
+        return this;
+    }
+
+    /**
+     * Adds individually each of the specified elements to the end of all of the existing composites.
+     * <p>
+     * If this builder contains 2 composites: A-B and A-C a call to this method to add D and E will result in the 4
+     * composites: A-B-D, A-B-E, A-C-D and A-C-E.
+     * </p>
+     *
+     * @param values the elements to add
+     * @return this <code>CompositeBuilder</code>
+     */
+    public CompositesBuilder addEachElementToAll(List<ByteBuffer> values)
+    {
+        checkUpdateable();
+
+        if (isEmpty())
+            elementsList.add(new ArrayList<ByteBuffer>());
+
+        for (int i = 0, m = elementsList.size(); i < m; i++)
+        {
+            List<ByteBuffer> oldComposite = elementsList.remove(0);
+
+            for (int j = 0, n = values.size(); j < n; j++)
+            {
+                List<ByteBuffer> newComposite = new ArrayList<>(oldComposite);
+                elementsList.add(newComposite);
+
+                ByteBuffer value = values.get(j);
+
+                if (value == null)
+                    containsNull = true;
+
+                newComposite.add(values.get(j));
+            }
+        }
+
+        remaining--;
+        return this;
+    }
+
+    /**
+     * Returns the number of elements that can be added to the composites.
+     *
+     * @return the number of elements that can be added to the composites.
+     */
+    public int remainingCount()
+    {
+        return remaining;
+    }
+
+    /**
+     * Checks if some elements can still be added to the composites.
+     *
+     * @return <code>true</code> if it is possible to add more elements to the composites, <code>false</code> otherwise.
+     */
+    public boolean hasRemaining()
+    {
+        return remaining > 0;
+    }
+
+    /**
+     * Checks if this builder is empty.
+     *
+     * @return <code>true</code> if this builder is empty, <code>false</code> otherwise.
+     */
+    public boolean isEmpty()
+    {
+        return elementsList.isEmpty();
+    }
+
+    /**
+     * Checks if the composites contains null elements.
+     *
+     * @return <code>true</code> if the composites contains <code>null</code> elements, <code>false</code> otherwise.
+     */
+    public boolean containsNull()
+    {
+        return containsNull;
+    }
+
+    /**
+     * Builds the <code>Composites</code>.
+     *
+     * @return the composites
+     */
+    public List<Composite> build()
+    {
+        return buildWithEOC(EOC.NONE);
+    }
+
+    /**
+     * Builds the <code>Composites</code> with the specified EOC.
+     *
+     * @return the composites
+     */
+    public List<Composite> buildWithEOC(EOC eoc)
+    {
+        built = true;
+
+        if (elementsList.isEmpty())
+            return singletonList(builder.build().withEOC(eoc));
+
+        // Use a TreeSet to sort and eliminate duplicates
+        Set<Composite> set = new TreeSet<Composite>(comparator);
+
+        for (int i = 0, m = elementsList.size(); i < m; i++)
+        {
+            List<ByteBuffer> elements = elementsList.get(i);
+            set.add(builder.buildWith(elements).withEOC(eoc));
+        }
+
+        return new ArrayList<>(set);
+    }
+
+    private void checkUpdateable()
+    {
+        if (!hasRemaining() || built)
+            throw new IllegalStateException("this CompositesBuilder cannot be updated anymore");
+    }
+}

http://git-wip-us.apache.org/repos/asf/cassandra/blob/9553dae9/test/unit/org/apache/cassandra/cql3/SingleColumnRelationTest.java
----------------------------------------------------------------------
diff --git a/test/unit/org/apache/cassandra/cql3/SingleColumnRelationTest.java b/test/unit/org/apache/cassandra/cql3/SingleColumnRelationTest.java
index 120c780..c93147b 100644
--- a/test/unit/org/apache/cassandra/cql3/SingleColumnRelationTest.java
+++ b/test/unit/org/apache/cassandra/cql3/SingleColumnRelationTest.java
@@ -17,6 +17,8 @@
  */
 package org.apache.cassandra.cql3;
 
+import java.util.Arrays;
+
 import org.junit.Test;
 
 public class SingleColumnRelationTest extends CQLTester
@@ -48,4 +50,121 @@ public class SingleColumnRelationTest extends CQLTester
         assertInvalid("SELECT * FROM %s WHERE c = 0 AND b <= ?", set(0));
         assertInvalid("SELECT * FROM %s WHERE c = 0 AND b IN (?)", set(0));
     }
+
+    @Test
+    public void testClusteringColumnRelations() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a text, b int, c int, d int, primary key(a, b, c))");
+        execute("insert into %s (a, b, c, d) values (?, ?, ?, ?)", "first", 1, 5, 1);
+        execute("insert into %s (a, b, c, d) values (?, ?, ?, ?)", "first", 2, 6, 2);
+        execute("insert into %s (a, b, c, d) values (?, ?, ?, ?)", "first", 3, 7, 3);
+        execute("insert into %s (a, b, c, d) values (?, ?, ?, ?)", "second", 4, 8, 4);
+
+        testSelectQueriesWithClusteringColumnRelations();
+    }
+
+    @Test
+    public void testClusteringColumnRelationsWithCompactStorage() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a text, b int, c int, d int, primary key(a, b, c)) WITH COMPACT STORAGE;");
+        execute("insert into %s (a, b, c, d) values (?, ?, ?, ?)", "first", 1, 5, 1);
+        execute("insert into %s (a, b, c, d) values (?, ?, ?, ?)", "first", 2, 6, 2);
+        execute("insert into %s (a, b, c, d) values (?, ?, ?, ?)", "first", 3, 7, 3);
+        execute("insert into %s (a, b, c, d) values (?, ?, ?, ?)", "second", 4, 8, 4);
+
+        testSelectQueriesWithClusteringColumnRelations();
+    }
+
+    private void testSelectQueriesWithClusteringColumnRelations() throws Throwable
+    {
+        assertRows(execute("select * from %s where a in (?, ?)", "first", "second"),
+                   row("first", 1, 5, 1),
+                   row("first", 2, 6, 2),
+                   row("first", 3, 7, 3),
+                   row("second", 4, 8, 4));
+
+        assertRows(execute("select * from %s where a = ? and b = ? and c in (?, ?)", "first", 2, 6, 7),
+                   row("first", 2, 6, 2));
+
+        assertRows(execute("select * from %s where a = ? and b in (?, ?) and c in (?, ?)", "first", 2, 3, 6, 7),
+                   row("first", 2, 6, 2),
+                   row("first", 3, 7, 3));
+
+        assertRows(execute("select * from %s where a = ? and b in (?, ?) and c in (?, ?)", "first", 3, 2, 7, 6),
+                   row("first", 2, 6, 2),
+                   row("first", 3, 7, 3));
+
+        assertRows(execute("select * from %s where a = ? and c in (?, ?) and b in (?, ?)", "first", 7, 6, 3, 2),
+                   row("first", 2, 6, 2),
+                   row("first", 3, 7, 3));
+
+        assertRows(execute("select c, d from %s where a = ? and c in (?, ?) and b in (?, ?)", "first", 7, 6, 3, 2),
+                   row(6, 2),
+                   row(7, 3));
+
+        assertRows(execute("select c, d from %s where a = ? and c in (?, ?) and b in (?, ?, ?)", "first", 7, 6, 3, 2, 3),
+                   row(6, 2),
+                   row(7, 3));
+
+        assertRows(execute("select * from %s where a = ? and b in (?, ?) and c = ?", "first", 3, 2, 7),
+                   row("first", 3, 7, 3));
+
+        assertRows(execute("select * from %s where a = ? and b in ? and c in ?",
+                           "first", Arrays.asList(3, 2), Arrays.asList(7, 6)),
+                   row("first", 2, 6, 2),
+                   row("first", 3, 7, 3));
+
+        assertInvalid("select * from %s where a = ? and b in ? and c in ?", "first", null, Arrays.asList(7, 6));
+
+        assertRows(execute("select * from %s where a = ? and c >= ? and b in (?, ?)", "first", 6, 3, 2),
+                   row("first", 2, 6, 2),
+                   row("first", 3, 7, 3));
+
+        assertRows(execute("select * from %s where a = ? and c > ? and b in (?, ?)", "first", 6, 3, 2),
+                   row("first", 3, 7, 3));
+
+        assertRows(execute("select * from %s where a = ? and c <= ? and b in (?, ?)", "first", 6, 3, 2),
+                   row("first", 2, 6, 2));
+
+        assertRows(execute("select * from %s where a = ? and c < ? and b in (?, ?)", "first", 7, 3, 2),
+                   row("first", 2, 6, 2));
+
+        assertRows(execute("select * from %s where a = ? and c in (?, ?) and b in (?, ?) order by b DESC",
+                           "first", 7, 6, 3, 2),
+                   row("first", 3, 7, 3),
+                   row("first", 2, 6, 2));
+    }
+
+    @Test
+    public void testPartitionKeyColumnRelations() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a text, b int, c int, d int, primary key((a, b), c))");
+        execute("insert into %s (a, b, c, d) values (?, ?, ?, ?)", "first", 1, 1, 1);
+        execute("insert into %s (a, b, c, d) values (?, ?, ?, ?)", "first", 2, 2, 2);
+        execute("insert into %s (a, b, c, d) values (?, ?, ?, ?)", "first", 3, 3, 3);
+        execute("insert into %s (a, b, c, d) values (?, ?, ?, ?)", "second", 4, 4, 4);
+
+        assertInvalid("select * from %s where a in (?, ?)", "first", "second");
+        assertInvalid("select * from %s where a in (?, ?) and b in (?, ?)", "first", "second", 2, 3);
+    }
+
+    @Test
+    public void testClusteringColumnRelationsWithClusteringOrder() throws Throwable
+    {
+        createTable("CREATE TABLE %s (a text, b int, c int, d int, primary key(a, b, c)) WITH CLUSTERING ORDER BY (b DESC);");
+        execute("insert into %s (a, b, c, d) values (?, ?, ?, ?)", "first", 1, 5, 1);
+        execute("insert into %s (a, b, c, d) values (?, ?, ?, ?)", "first", 2, 6, 2);
+        execute("insert into %s (a, b, c, d) values (?, ?, ?, ?)", "first", 3, 7, 3);
+        execute("insert into %s (a, b, c, d) values (?, ?, ?, ?)", "second", 4, 8, 4);
+
+        assertRows(execute("select * from %s where a = ? and c in (?, ?) and b in (?, ?) order by b DESC",
+                           "first", 7, 6, 3, 2),
+                   row("first", 3, 7, 3),
+                   row("first", 2, 6, 2));
+
+        assertRows(execute("select * from %s where a = ? and c in (?, ?) and b in (?, ?) order by b ASC",
+                           "first", 7, 6, 3, 2),
+                   row("first", 2, 6, 2),
+                   row("first", 3, 7, 3));
+    }
 }