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));
+ }
}