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